mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-16 18:25:17 +02:00
Add Run managemement options and pagination to simulations
This commit is contained in:
parent
893f215f4c
commit
4261c70a2b
4 changed files with 331 additions and 127 deletions
|
|
@ -246,4 +246,25 @@ export async function getAggregateResult(
|
||||||
if (!run || !run.aggregateResults) return null;
|
if (!run || !run.aggregateResults) return null;
|
||||||
|
|
||||||
return run.aggregateResults;
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,13 +47,17 @@ export const SimulationAggregateResult = z.object({
|
||||||
|
|
||||||
export const SimulationRun = z.object({
|
export const SimulationRun = z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
status: z.enum(['pending', 'running', 'completed', 'cancelled', 'failed']),
|
|
||||||
scenarioIds: z.array(z.string()),
|
scenarioIds: z.array(z.string()),
|
||||||
workflowId: z.string(),
|
workflowId: z.string(),
|
||||||
|
status: z.enum(['pending', 'running', 'completed', 'cancelled', 'failed', 'error']),
|
||||||
startedAt: z.string(),
|
startedAt: z.string(),
|
||||||
completedAt: z.string().optional(),
|
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({
|
export const SimulationResult = z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
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 { useParams, useRouter } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
getScenarios,
|
getScenarios,
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
createRunResult,
|
createRunResult,
|
||||||
updateRunStatus,
|
updateRunStatus,
|
||||||
createAggregateResult,
|
createAggregateResult,
|
||||||
|
deleteRun,
|
||||||
} from '../../../actions/simulation_actions';
|
} from '../../../actions/simulation_actions';
|
||||||
import { type WithStringId } from '../../../lib/types/types';
|
import { type WithStringId } from '../../../lib/types/types';
|
||||||
import { Scenario, SimulationRun, SimulationResult } from "../../../lib/types/testing_types";
|
import { Scenario, SimulationRun, SimulationResult } from "../../../lib/types/testing_types";
|
||||||
|
|
@ -77,6 +78,13 @@ export default function SimulationApp() {
|
||||||
const [isLoadingRuns, setIsLoadingRuns] = useState(true);
|
const [isLoadingRuns, setIsLoadingRuns] = useState(true);
|
||||||
const [allRunResults, setAllRunResults] = useState<Record<string, SimulationResultType[]>>({});
|
const [allRunResults, setAllRunResults] = useState<Record<string, SimulationResultType[]>>({});
|
||||||
const [workflowVersions, setWorkflowVersions] = useState<Record<string, WithStringId<z.infer<typeof Workflow>>>>({});
|
const [workflowVersions, setWorkflowVersions] = useState<Record<string, WithStringId<z.infer<typeof Workflow>>>>({});
|
||||||
|
const [menuOpenId, setMenuOpenIdState] = useState<string | null>(null);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const runsPerPage = 10;
|
||||||
|
|
||||||
|
const setMenuOpenId = useCallback((id: string | null) => {
|
||||||
|
setMenuOpenIdState(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Load scenarios on mount
|
// Load scenarios on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -216,8 +224,9 @@ export default function SimulationApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// First verify the workflow exists before creating the run
|
// First verify the workflow exists before creating the run
|
||||||
|
let workflow;
|
||||||
try {
|
try {
|
||||||
await fetchWorkflow(projectId as string, workflowId);
|
workflow = await fetchWorkflow(projectId as string, workflowId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If workflow doesn't exist, clear localStorage and throw error
|
// If workflow doesn't exist, clear localStorage and throw error
|
||||||
localStorage.removeItem(`lastWorkflowId_${projectId}`);
|
localStorage.removeItem(`lastWorkflowId_${projectId}`);
|
||||||
|
|
@ -231,8 +240,7 @@ export default function SimulationApp() {
|
||||||
);
|
);
|
||||||
setActiveRun(newRun);
|
setActiveRun(newRun);
|
||||||
|
|
||||||
// Fetch and store workflow version
|
// Store workflow version
|
||||||
const workflow = await fetchWorkflow(projectId as string, workflowId);
|
|
||||||
setWorkflowVersions(prev => ({
|
setWorkflowVersions(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[workflowId]: workflow
|
[workflowId]: workflow
|
||||||
|
|
@ -282,7 +290,7 @@ export default function SimulationApp() {
|
||||||
await fetchRuns();
|
await fetchRuns();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error starting scenarios:', 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 {
|
} finally {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +304,7 @@ export default function SimulationApp() {
|
||||||
setMenuOpenScenarioId(null);
|
setMenuOpenScenarioId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add useEffect to fetch workflow versions for existing runs
|
// Update the workflow versions fetching effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projectId || !runs.length) return;
|
if (!projectId || !runs.length) return;
|
||||||
|
|
||||||
|
|
@ -310,17 +318,17 @@ export default function SimulationApp() {
|
||||||
versions[workflowId] = workflow;
|
versions[workflowId] = workflow;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching workflow ${workflowId}:`, error);
|
console.error(`Error fetching workflow ${workflowId}:`, error);
|
||||||
// Add a placeholder for deleted workflows
|
// Add a placeholder for deleted/invalid workflows
|
||||||
versions[workflowId] = {
|
versions[workflowId] = {
|
||||||
_id: workflowId,
|
_id: workflowId,
|
||||||
name: "Deleted Workflow",
|
name: "Deleted/Invalid Workflow",
|
||||||
projectId: projectId as string,
|
projectId: projectId as string,
|
||||||
agents: [],
|
agents: [],
|
||||||
prompts: [],
|
prompts: [],
|
||||||
tools: [],
|
tools: [],
|
||||||
startAgent: "",
|
startAgent: "",
|
||||||
createdAt: "",
|
createdAt: new Date().toISOString(),
|
||||||
lastUpdatedAt: "",
|
lastUpdatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -331,6 +339,31 @@ export default function SimulationApp() {
|
||||||
fetchWorkflowVersions();
|
fetchWorkflowVersions();
|
||||||
}, [projectId, runs]);
|
}, [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 (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
{/* Left sidebar */}
|
{/* Left sidebar */}
|
||||||
|
|
@ -436,17 +469,56 @@ export default function SimulationApp() {
|
||||||
) : runs.length === 0 ? (
|
) : runs.length === 0 ? (
|
||||||
<div className="text-center py-4 text-gray-500">No simulation runs yet</div>
|
<div className="text-center py-4 text-gray-500">No simulation runs yet</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<>
|
||||||
{runs.map((run) => (
|
<div className="space-y-4">
|
||||||
<SimulationResultCard
|
{currentRuns.map((run) => (
|
||||||
key={run._id}
|
<SimulationResultCard
|
||||||
run={run}
|
key={run._id}
|
||||||
results={allRunResults[run._id] || []}
|
run={run}
|
||||||
scenarios={scenarios}
|
results={allRunResults[run._id] || []}
|
||||||
workflow={workflowVersions[run.workflowId]}
|
scenarios={scenarios}
|
||||||
/>
|
workflow={workflowVersions[run.workflowId]}
|
||||||
))}
|
onCancelRun={handleCancelRun}
|
||||||
</div>
|
onDeleteRun={handleDeleteRun}
|
||||||
|
menuOpenId={menuOpenId}
|
||||||
|
setMenuOpenId={setMenuOpenId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex justify-center items-center space-x-4 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className={`p-2 rounded-full ${
|
||||||
|
currentPage === 1
|
||||||
|
? 'text-gray-400 cursor-not-allowed'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
Page {currentPage} of {totalPages}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className={`p-2 rounded-full ${
|
||||||
|
currentPage === totalPages
|
||||||
|
? 'text-gray-400 cursor-not-allowed'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
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 { WithStringId } from '../../../../lib/types/types';
|
||||||
import { Scenario, SimulationRun, SimulationResult, SimulationAggregateResult } from "../../../../lib/types/testing_types";
|
import { Scenario, SimulationRun, SimulationResult, SimulationAggregateResult } from "../../../../lib/types/testing_types";
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
@ -16,29 +16,38 @@ interface SimulationResultCardProps {
|
||||||
results: SimulationResultType[];
|
results: SimulationResultType[];
|
||||||
scenarios: ScenarioType[];
|
scenarios: ScenarioType[];
|
||||||
workflow?: WithStringId<z.infer<typeof Workflow>>;
|
workflow?: WithStringId<z.infer<typeof Workflow>>;
|
||||||
|
onCancelRun?: (runId: string) => void;
|
||||||
|
onDeleteRun?: (runId: string) => Promise<void>;
|
||||||
|
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 [isExpanded, setIsExpanded] = useState(false);
|
||||||
const [expandedScenarios, setExpandedScenarios] = useState<Set<string>>(new Set());
|
const [expandedScenarios, setExpandedScenarios] = useState<Set<string>>(new Set());
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
|
||||||
const totalScenarios = run.aggregateResults?.total ?? run.scenarioIds.length;
|
const totalScenarios = run.aggregateResults?.total ?? run.scenarioIds.length;
|
||||||
const passedScenarios = run.aggregateResults?.pass ?? 0;
|
const passedScenarios = run.aggregateResults?.pass ?? 0;
|
||||||
const failedScenarios = run.aggregateResults?.fail ?? 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) => {
|
const getStatusClass = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'completed':
|
case 'completed':
|
||||||
case 'pass':
|
case 'pass':
|
||||||
return `${statusLabelClass} bg-green-100 text-green-800`;
|
return `${statusLabelClass} bg-green-50 text-green-800`;
|
||||||
case 'failed':
|
case 'failed':
|
||||||
case 'fail':
|
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 'running':
|
||||||
case 'pending':
|
case 'pending':
|
||||||
default:
|
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 (
|
return (
|
||||||
<div className="border rounded-lg mb-4 shadow-sm">
|
<div className="border rounded-lg mb-4 shadow-sm">
|
||||||
<div
|
<div
|
||||||
|
|
@ -103,116 +120,206 @@ export const SimulationResultCard = ({ run, results, scenarios, workflow }: Simu
|
||||||
{formatMainTitle(run.startedAt)}
|
{formatMainTitle(run.startedAt)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={getStatusClass(run.status)}>
|
<div className="flex items-center gap-2">
|
||||||
{run.status}
|
<span className={getStatusClass(run.status)}>
|
||||||
</span>
|
{run.status}
|
||||||
|
</span>
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setMenuOpenId(menuOpenId === run._id ? null : run._id);
|
||||||
|
}}
|
||||||
|
className="p-1 rounded-full hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<EllipsisVerticalIcon className="h-5 w-5 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{menuOpenId === run._id && (
|
||||||
|
<div className="absolute right-0 mt-1 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10">
|
||||||
|
<div className="py-1">
|
||||||
|
{(run.status === 'running' || run.status === 'pending') && onCancelRun && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onCancelRun(run._id);
|
||||||
|
setMenuOpenId(null);
|
||||||
|
}}
|
||||||
|
className="flex items-center px-4 py-2 text-sm text-red-600 hover:bg-gray-100 w-full"
|
||||||
|
>
|
||||||
|
<NoSymbolIcon className="h-4 w-4 mr-2" />
|
||||||
|
Cancel Run
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
className="flex items-center px-4 py-2 text-sm text-gray-400 w-full cursor-not-allowed whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<ArrowDownTrayIcon className="h-4 w-4 mr-2" />
|
||||||
|
Download transcripts
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowDeleteConfirm(true);
|
||||||
|
setMenuOpenId(null);
|
||||||
|
}}
|
||||||
|
className="flex items-center px-4 py-2 text-sm text-red-600 hover:bg-gray-100 w-full"
|
||||||
|
>
|
||||||
|
<TrashIcon className="h-4 w-4 mr-2" />
|
||||||
|
Delete run
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="p-4 border-t">
|
<div className="p-4 border-t">
|
||||||
{/* Workflow and timing information in a grid */}
|
{run.status === 'error' ? (
|
||||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
<div className="text-orange-800 bg-orange-50 p-4 rounded-lg">
|
||||||
{workflow && (
|
Your simulation could not be completed. Please run a new simulation again.
|
||||||
<div className="bg-gray-50 p-4 rounded-lg">
|
|
||||||
<div className="text-sm font-medium text-gray-600 mb-1">Workflow Version</div>
|
|
||||||
<div className="font-medium">{workflow.name}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="bg-gray-50 p-4 rounded-lg">
|
|
||||||
<div className="text-sm font-medium text-gray-600 mb-1">Completed</div>
|
|
||||||
<div className="text-sm">
|
|
||||||
{run.completedAt ? formatDateTime(run.completedAt) : 'Not completed'}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-50 p-4 rounded-lg">
|
) : (
|
||||||
<div className="text-sm font-medium text-gray-600 mb-1">Duration</div>
|
<>
|
||||||
<div className="text-sm">{getDuration()}</div>
|
{/* Workflow and timing information in a grid */}
|
||||||
</div>
|
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||||
</div>
|
{workflow && (
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
{/* Results statistics */}
|
<div className="text-sm font-medium text-gray-600 mb-1">Workflow Version</div>
|
||||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
<div className="font-medium">{workflow.name}</div>
|
||||||
<div className="p-4 rounded-lg bg-gray-50">
|
|
||||||
<div className="text-sm text-gray-600">Total Scenarios</div>
|
|
||||||
<div className="text-2xl font-semibold">{totalScenarios}</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 rounded-lg bg-green-50">
|
|
||||||
<div className="text-sm text-green-600">Passed</div>
|
|
||||||
<div className="text-2xl font-semibold text-green-700">{passedScenarios}</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 rounded-lg bg-red-50">
|
|
||||||
<div className="text-sm text-red-600">Failed</div>
|
|
||||||
<div className="text-2xl font-semibold text-red-700">{failedScenarios}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
{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 && (
|
|
||||||
<div
|
|
||||||
key={scenarioId}
|
|
||||||
className={`border rounded-lg overflow-hidden ${
|
|
||||||
result?.result === 'pass' ? 'bg-green-50 border-green-200' :
|
|
||||||
result?.result === 'fail' ? 'bg-red-50 border-red-200' :
|
|
||||||
'bg-gray-50 border-gray-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="p-3 flex items-center justify-between cursor-pointer hover:bg-opacity-80"
|
|
||||||
onClick={(e) => toggleScenario(scenarioId, e)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{isScenarioExpanded ? (
|
|
||||||
<ChevronDownIcon className="h-4 w-4 text-gray-600" />
|
|
||||||
) : (
|
|
||||||
<ChevronRightIcon className="h-4 w-4 text-gray-600" />
|
|
||||||
)}
|
|
||||||
<span className="font-medium text-gray-900">{scenario.name}</span>
|
|
||||||
</div>
|
|
||||||
{result && (
|
|
||||||
<span className={getStatusClass(result.result)}>
|
|
||||||
{result.result}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<div className="text-sm font-medium text-gray-600 mb-1">Completed</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
{run.completedAt ? formatDateTime(run.completedAt) : 'Not completed'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<div className="text-sm font-medium text-gray-600 mb-1">Duration</div>
|
||||||
|
<div className="text-sm">{getDuration()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isScenarioExpanded && (
|
{/* Results statistics */}
|
||||||
<div className="p-3 border-t border-opacity-50 space-y-4">
|
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||||
<div>
|
<div className="p-4 rounded-lg bg-gray-50">
|
||||||
<div className="text-sm font-medium mb-1">Description</div>
|
<div className="text-sm text-gray-600">Total Scenarios</div>
|
||||||
<div className="text-sm text-gray-700">
|
<div className="text-2xl font-semibold">{totalScenarios}</div>
|
||||||
{scenario.description}
|
</div>
|
||||||
|
<div className="p-4 rounded-lg bg-green-50">
|
||||||
|
<div className="text-sm text-green-600">Passed</div>
|
||||||
|
<div className="text-2xl font-semibold text-green-700">{passedScenarios}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 rounded-lg bg-red-50">
|
||||||
|
<div className="text-sm text-red-600">Failed</div>
|
||||||
|
<div className="text-2xl font-semibold text-red-700">{failedScenarios}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{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 && (
|
||||||
|
<div
|
||||||
|
key={scenarioId}
|
||||||
|
className={`border rounded-lg overflow-hidden ${
|
||||||
|
result?.result === 'pass' ? 'bg-green-50 border-green-200' :
|
||||||
|
result?.result === 'fail' ? 'bg-red-50 border-red-200' :
|
||||||
|
'bg-gray-50 border-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="p-3 flex items-center justify-between cursor-pointer hover:bg-opacity-80"
|
||||||
|
onClick={(e) => toggleScenario(scenarioId, e)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{isScenarioExpanded ? (
|
||||||
|
<ChevronDownIcon className="h-4 w-4 text-gray-600" />
|
||||||
|
) : (
|
||||||
|
<ChevronRightIcon className="h-4 w-4 text-gray-600" />
|
||||||
|
)}
|
||||||
|
<span className="font-medium text-gray-900">{scenario.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{result && (
|
||||||
|
<span className={getStatusClass(result.result)}>
|
||||||
|
{result.result}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div className="text-sm font-medium mb-1">Criteria</div>
|
{isScenarioExpanded && (
|
||||||
<div className="text-sm text-gray-700">
|
<div className="p-3 border-t border-opacity-50 space-y-4">
|
||||||
{scenario.criteria || 'No criteria specified'}
|
<div>
|
||||||
</div>
|
<div className="text-sm font-medium mb-1">Description</div>
|
||||||
</div>
|
<div className="text-sm text-gray-700">
|
||||||
<div>
|
{scenario.description}
|
||||||
<div className="text-sm font-medium mb-1">Context</div>
|
</div>
|
||||||
<div className="text-sm text-gray-700">
|
|
||||||
{scenario.context || 'No context provided'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{result && (
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-medium mb-1">Result Details</div>
|
|
||||||
<div className="text-sm text-gray-700">
|
|
||||||
{result.details}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium mb-1">Criteria</div>
|
||||||
|
<div className="text-sm text-gray-700">
|
||||||
|
{scenario.criteria || 'No criteria specified'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium mb-1">Context</div>
|
||||||
|
<div className="text-sm text-gray-700">
|
||||||
|
{scenario.context || 'No context provided'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{result && (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium mb-1">Result Details</div>
|
||||||
|
<div className="text-sm text-gray-700">
|
||||||
|
{result.details}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
</div>
|
})}
|
||||||
);
|
</div>
|
||||||
})}
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDeleteConfirm && (
|
||||||
|
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||||
|
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||||
|
<div className="mt-3 text-center">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900 whitespace-nowrap">
|
||||||
|
Are you sure you want to delete this run?
|
||||||
|
</h3>
|
||||||
|
<div className="mt-6 flex justify-center space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDeleteConfirm(false)}
|
||||||
|
className="px-4 py-2 bg-white text-gray-600 text-sm font-medium border rounded-md hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Retain
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
if (onDeleteRun) {
|
||||||
|
await onDeleteRun(run._id);
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-md hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue