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;
|
||||
|
||||
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({
|
||||
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(),
|
||||
|
|
|
|||
|
|
@ -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<Record<string, SimulationResultType[]>>({});
|
||||
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
|
||||
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 (
|
||||
<div className="flex h-screen">
|
||||
{/* Left sidebar */}
|
||||
|
|
@ -436,17 +469,56 @@ export default function SimulationApp() {
|
|||
) : runs.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-500">No simulation runs yet</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{runs.map((run) => (
|
||||
<SimulationResultCard
|
||||
key={run._id}
|
||||
run={run}
|
||||
results={allRunResults[run._id] || []}
|
||||
scenarios={scenarios}
|
||||
workflow={workflowVersions[run.workflowId]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
{currentRuns.map((run) => (
|
||||
<SimulationResultCard
|
||||
key={run._id}
|
||||
run={run}
|
||||
results={allRunResults[run._id] || []}
|
||||
scenarios={scenarios}
|
||||
workflow={workflowVersions[run.workflowId]}
|
||||
onCancelRun={handleCancelRun}
|
||||
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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<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 [expandedScenarios, setExpandedScenarios] = useState<Set<string>>(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 (
|
||||
<div className="border rounded-lg mb-4 shadow-sm">
|
||||
<div
|
||||
|
|
@ -103,116 +120,206 @@ export const SimulationResultCard = ({ run, results, scenarios, workflow }: Simu
|
|||
{formatMainTitle(run.startedAt)}
|
||||
</div>
|
||||
</div>
|
||||
<span className={getStatusClass(run.status)}>
|
||||
{run.status}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={getStatusClass(run.status)}>
|
||||
{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>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="p-4 border-t">
|
||||
{/* Workflow and timing information in a grid */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
{workflow && (
|
||||
<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>
|
||||
{run.status === 'error' ? (
|
||||
<div className="text-orange-800 bg-orange-50 p-4 rounded-lg">
|
||||
Your simulation could not be completed. Please run a new simulation again.
|
||||
</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>
|
||||
|
||||
{/* Results statistics */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<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>
|
||||
)}
|
||||
) : (
|
||||
<>
|
||||
{/* Workflow and timing information in a grid */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
{workflow && (
|
||||
<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 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 && (
|
||||
<div className="p-3 border-t border-opacity-50 space-y-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1">Description</div>
|
||||
<div className="text-sm text-gray-700">
|
||||
{scenario.description}
|
||||
{/* Results statistics */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<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="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}
|
||||
|
||||
{isScenarioExpanded && (
|
||||
<div className="p-3 border-t border-opacity-50 space-y-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1">Description</div>
|
||||
<div className="text-sm text-gray-700">
|
||||
{scenario.description}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue