From 4261c70a2bcefdab517d6cea66c08eea6b3f8dc1 Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Tue, 18 Feb 2025 10:50:46 +0530 Subject: [PATCH] Add Run managemement options and pagination to simulations --- .../rowboat/app/actions/simulation_actions.ts | 21 ++ apps/rowboat/app/lib/types/testing_types.ts | 10 +- .../projects/[projectId]/simulation/app.tsx | 114 +++++-- .../simulation/components/RunComponents.tsx | 313 ++++++++++++------ 4 files changed, 331 insertions(+), 127 deletions(-) diff --git a/apps/rowboat/app/actions/simulation_actions.ts b/apps/rowboat/app/actions/simulation_actions.ts index 63e8cea1..661315b2 100644 --- a/apps/rowboat/app/actions/simulation_actions.ts +++ b/apps/rowboat/app/actions/simulation_actions.ts @@ -246,4 +246,25 @@ export async function getAggregateResult( if (!run || !run.aggregateResults) return null; return run.aggregateResults; +} + +export async function deleteRun(projectId: string, runId: string) { + try { + // Delete the run using the collection directly + await simulationRunsCollection.deleteOne({ + _id: new ObjectId(runId), + projectId: projectId + }); + + // Delete associated results using the collection directly + await simulationResultsCollection.deleteMany({ + runId: runId, + projectId: projectId + }); + + return { success: true }; + } catch (error) { + console.error('Error deleting run:', error); + throw new Error('Failed to delete run'); + } } \ No newline at end of file diff --git a/apps/rowboat/app/lib/types/testing_types.ts b/apps/rowboat/app/lib/types/testing_types.ts index 7b44b78f..79c6f6e5 100644 --- a/apps/rowboat/app/lib/types/testing_types.ts +++ b/apps/rowboat/app/lib/types/testing_types.ts @@ -47,13 +47,17 @@ export const SimulationAggregateResult = z.object({ export const SimulationRun = z.object({ projectId: z.string(), - status: z.enum(['pending', 'running', 'completed', 'cancelled', 'failed']), scenarioIds: z.array(z.string()), workflowId: z.string(), + status: z.enum(['pending', 'running', 'completed', 'cancelled', 'failed', 'error']), startedAt: z.string(), completedAt: z.string().optional(), - aggregateResults: SimulationAggregateResult.optional(), -}); + aggregateResults: z.object({ + total: z.number(), + pass: z.number(), + fail: z.number(), + }).optional(), + }); export const SimulationResult = z.object({ projectId: z.string(), diff --git a/apps/rowboat/app/projects/[projectId]/simulation/app.tsx b/apps/rowboat/app/projects/[projectId]/simulation/app.tsx index 865a1fe7..7d07e84b 100644 --- a/apps/rowboat/app/projects/[projectId]/simulation/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/simulation/app.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect, useCallback } from 'react'; -import { PlusIcon, PencilIcon, XMarkIcon, EllipsisVerticalIcon, TrashIcon, ChevronRightIcon, PlayIcon, ChevronDownIcon } from '@heroicons/react/24/outline'; +import { PlusIcon, PencilIcon, XMarkIcon, EllipsisVerticalIcon, TrashIcon, ChevronRightIcon, PlayIcon, ChevronDownIcon, ChevronLeftIcon } from '@heroicons/react/24/outline'; import { useParams, useRouter } from 'next/navigation'; import { getScenarios, @@ -15,6 +15,7 @@ import { createRunResult, updateRunStatus, createAggregateResult, + deleteRun, } from '../../../actions/simulation_actions'; import { type WithStringId } from '../../../lib/types/types'; import { Scenario, SimulationRun, SimulationResult } from "../../../lib/types/testing_types"; @@ -77,6 +78,13 @@ export default function SimulationApp() { const [isLoadingRuns, setIsLoadingRuns] = useState(true); const [allRunResults, setAllRunResults] = useState>({}); const [workflowVersions, setWorkflowVersions] = useState>>>({}); + const [menuOpenId, setMenuOpenIdState] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const runsPerPage = 10; + + const setMenuOpenId = useCallback((id: string | null) => { + setMenuOpenIdState(id); + }, []); // Load scenarios on mount useEffect(() => { @@ -216,8 +224,9 @@ export default function SimulationApp() { } // First verify the workflow exists before creating the run + let workflow; try { - await fetchWorkflow(projectId as string, workflowId); + workflow = await fetchWorkflow(projectId as string, workflowId); } catch (error) { // If workflow doesn't exist, clear localStorage and throw error localStorage.removeItem(`lastWorkflowId_${projectId}`); @@ -231,8 +240,7 @@ export default function SimulationApp() { ); setActiveRun(newRun); - // Fetch and store workflow version - const workflow = await fetchWorkflow(projectId as string, workflowId); + // Store workflow version setWorkflowVersions(prev => ({ ...prev, [workflowId]: workflow @@ -282,7 +290,7 @@ export default function SimulationApp() { await fetchRuns(); } catch (error) { console.error('Error starting scenarios:', error); - // Maybe show an error toast here + alert(error instanceof Error ? error.message : 'An error occurred while starting scenarios'); } finally { setIsRunning(false); } @@ -296,7 +304,7 @@ export default function SimulationApp() { setMenuOpenScenarioId(null); }; - // Add useEffect to fetch workflow versions for existing runs + // Update the workflow versions fetching effect useEffect(() => { if (!projectId || !runs.length) return; @@ -310,17 +318,17 @@ export default function SimulationApp() { versions[workflowId] = workflow; } catch (error) { console.error(`Error fetching workflow ${workflowId}:`, error); - // Add a placeholder for deleted workflows + // Add a placeholder for deleted/invalid workflows versions[workflowId] = { _id: workflowId, - name: "Deleted Workflow", + name: "Deleted/Invalid Workflow", projectId: projectId as string, agents: [], prompts: [], tools: [], startAgent: "", - createdAt: "", - lastUpdatedAt: "", + createdAt: new Date().toISOString(), + lastUpdatedAt: new Date().toISOString(), }; } } @@ -331,6 +339,31 @@ export default function SimulationApp() { fetchWorkflowVersions(); }, [projectId, runs]); + const handleCancelRun = async (runId: string) => { + if (!projectId) return; + try { + await updateRunStatus(projectId as string, runId, 'cancelled'); + await fetchRuns(); // Refresh the runs list + } catch (error) { + console.error('Error cancelling run:', error); + } + }; + + const handleDeleteRun = async (runId: string) => { + if (!projectId) return; + try { + await deleteRun(projectId as string, runId); + await fetchRuns(); // Refresh the runs list + } catch (error) { + console.error('Error deleting run:', error); + } + }; + + const indexOfLastRun = currentPage * runsPerPage; + const indexOfFirstRun = indexOfLastRun - runsPerPage; + const currentRuns = runs.slice(indexOfFirstRun, indexOfLastRun); + const totalPages = Math.ceil(runs.length / runsPerPage); + return (
{/* Left sidebar */} @@ -436,17 +469,56 @@ export default function SimulationApp() { ) : runs.length === 0 ? (
No simulation runs yet
) : ( -
- {runs.map((run) => ( - - ))} -
+ <> +
+ {currentRuns.map((run) => ( + + ))} +
+ + {/* Pagination Controls */} + {totalPages > 1 && ( +
+ + + + Page {currentPage} of {totalPages} + + + +
+ )} + )}
)} diff --git a/apps/rowboat/app/projects/[projectId]/simulation/components/RunComponents.tsx b/apps/rowboat/app/projects/[projectId]/simulation/components/RunComponents.tsx index e2376fa9..11ff853d 100644 --- a/apps/rowboat/app/projects/[projectId]/simulation/components/RunComponents.tsx +++ b/apps/rowboat/app/projects/[projectId]/simulation/components/RunComponents.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; +import { ChevronDownIcon, ChevronRightIcon, NoSymbolIcon, EllipsisVerticalIcon, ArrowDownTrayIcon, TrashIcon } from '@heroicons/react/24/outline'; import { WithStringId } from '../../../../lib/types/types'; import { Scenario, SimulationRun, SimulationResult, SimulationAggregateResult } from "../../../../lib/types/testing_types"; import { z } from 'zod'; @@ -16,29 +16,38 @@ interface SimulationResultCardProps { results: SimulationResultType[]; scenarios: ScenarioType[]; workflow?: WithStringId>; + onCancelRun?: (runId: string) => void; + onDeleteRun?: (runId: string) => Promise; + menuOpenId: string | null; + setMenuOpenId: (id: string | null) => void; } -export const SimulationResultCard = ({ run, results, scenarios, workflow }: SimulationResultCardProps) => { +export const SimulationResultCard = ({ run, results, scenarios, workflow, onCancelRun, onDeleteRun, menuOpenId, setMenuOpenId }: SimulationResultCardProps) => { const [isExpanded, setIsExpanded] = useState(false); const [expandedScenarios, setExpandedScenarios] = useState>(new Set()); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const totalScenarios = run.aggregateResults?.total ?? run.scenarioIds.length; const passedScenarios = run.aggregateResults?.pass ?? 0; const failedScenarios = run.aggregateResults?.fail ?? 0; - const statusLabelClass = "px-3 py-1 rounded text-xs min-w-[60px] text-center uppercase font-semibold"; + const statusLabelClass = "w-[110px] px-3 py-1 rounded text-xs text-center uppercase font-semibold inline-block"; const getStatusClass = (status: string) => { switch (status) { case 'completed': case 'pass': - return `${statusLabelClass} bg-green-100 text-green-800`; + return `${statusLabelClass} bg-green-50 text-green-800`; case 'failed': case 'fail': - return `${statusLabelClass} bg-red-100 text-red-800`; + return `${statusLabelClass} bg-red-50 text-red-800`; + case 'error': + return `${statusLabelClass} bg-orange-50 text-orange-800`; + case 'cancelled': + return `${statusLabelClass} bg-gray-50 text-gray-800`; case 'running': case 'pending': default: - return `${statusLabelClass} bg-yellow-100 text-yellow-800`; + return `${statusLabelClass} bg-yellow-50 text-yellow-800`; } }; @@ -87,6 +96,14 @@ export const SimulationResultCard = ({ run, results, scenarios, workflow }: Simu }); }; + useEffect(() => { + if (menuOpenId) { + const closeMenu = () => setMenuOpenId(null); + window.addEventListener('click', closeMenu); + return () => window.removeEventListener('click', closeMenu); + } + }, [menuOpenId, setMenuOpenId]); + return (
- - {run.status} - +
+ + {run.status} + +
+ + + {menuOpenId === run._id && ( +
+
+ {(run.status === 'running' || run.status === 'pending') && onCancelRun && ( + + )} + + +
+
+ )} +
+
{isExpanded && (
- {/* Workflow and timing information in a grid */} -
- {workflow && ( -
-
Workflow Version
-
{workflow.name}
-
- )} -
-
Completed
-
- {run.completedAt ? formatDateTime(run.completedAt) : 'Not completed'} -
+ {run.status === 'error' ? ( +
+ Your simulation could not be completed. Please run a new simulation again.
-
-
Duration
-
{getDuration()}
-
-
- - {/* Results statistics */} -
-
-
Total Scenarios
-
{totalScenarios}
-
-
-
Passed
-
{passedScenarios}
-
-
-
Failed
-
{failedScenarios}
-
-
- -
- {run.scenarioIds.map(scenarioId => { - const scenario = scenarios.find(s => s._id === scenarioId); - const result = results.find(r => r.scenarioId === scenarioId); - const isScenarioExpanded = expandedScenarios.has(scenarioId); - - return scenario && ( -
-
toggleScenario(scenarioId, e)} - > -
- {isScenarioExpanded ? ( - - ) : ( - - )} - {scenario.name} -
- {result && ( - - {result.result} - - )} + ) : ( + <> + {/* Workflow and timing information in a grid */} +
+ {workflow && ( +
+
Workflow Version
+
{workflow.name}
+ )} +
+
Completed
+
+ {run.completedAt ? formatDateTime(run.completedAt) : 'Not completed'} +
+
+
+
Duration
+
{getDuration()}
+
+
- {isScenarioExpanded && ( -
-
-
Description
-
- {scenario.description} + {/* Results statistics */} +
+
+
Total Scenarios
+
{totalScenarios}
+
+
+
Passed
+
{passedScenarios}
+
+
+
Failed
+
{failedScenarios}
+
+
+ +
+ {run.scenarioIds.map(scenarioId => { + const scenario = scenarios.find(s => s._id === scenarioId); + const result = results.find(r => r.scenarioId === scenarioId); + const isScenarioExpanded = expandedScenarios.has(scenarioId); + + return scenario && ( +
+
toggleScenario(scenarioId, e)} + > +
+ {isScenarioExpanded ? ( + + ) : ( + + )} + {scenario.name}
+ {result && ( + + {result.result} + + )}
-
-
Criteria
-
- {scenario.criteria || 'No criteria specified'} -
-
-
-
Context
-
- {scenario.context || 'No context provided'} -
-
- {result && ( -
-
Result Details
-
- {result.details} + + {isScenarioExpanded && ( +
+
+
Description
+
+ {scenario.description} +
+
+
Criteria
+
+ {scenario.criteria || 'No criteria specified'} +
+
+
+
Context
+
+ {scenario.context || 'No context provided'} +
+
+ {result && ( +
+
Result Details
+
+ {result.details} +
+
+ )}
)}
- )} -
- ); - })} + ); + })} +
+ + )} +
+ )} + + {showDeleteConfirm && ( +
+
+
+

+ Are you sure you want to delete this run? +

+
+ + +
+
)}