import { useParams, Link } from "react-router-dom"; /** Format a duration in ms to a human-readable string. */ function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; const secs = ms / 1000; if (secs < 60) return `${secs.toFixed(1)}s`; const mins = Math.floor(secs / 60); const remainSecs = Math.round(secs % 60); if (mins < 60) return `${mins}m ${remainSecs}s`; const hrs = Math.floor(mins / 60); const remainMins = mins % 60; return `${hrs}h ${remainMins}m`; } import { useExecution } from "@/hooks/useExecutions"; import { useAction } from "@/hooks/useActions"; import { useExecutionStream } from "@/hooks/useExecutionStream"; import { useExecutionHistory } from "@/hooks/useHistory"; import { formatDistanceToNow } from "date-fns"; import { ExecutionStatus } from "@/api"; import { useState, useMemo } from "react"; import { RotateCcw, Loader2 } from "lucide-react"; import ExecuteActionModal from "@/components/common/ExecuteActionModal"; import EntityHistoryPanel from "@/components/common/EntityHistoryPanel"; import WorkflowTasksPanel from "@/components/common/WorkflowTasksPanel"; import ExecutionArtifactsPanel from "@/components/executions/ExecutionArtifactsPanel"; const getStatusColor = (status: string) => { switch (status) { case "succeeded": case "completed": return "bg-green-100 text-green-800"; case "failed": return "bg-red-100 text-red-800"; case "running": return "bg-blue-100 text-blue-800"; case "pending": case "requested": case "scheduling": case "scheduled": return "bg-yellow-100 text-yellow-800"; case "timeout": return "bg-orange-100 text-orange-800"; case "canceling": case "cancelled": return "bg-gray-100 text-gray-800"; case "abandoned": return "bg-red-100 text-red-600"; default: return "bg-gray-100 text-gray-800"; } }; /** Map status to a dot color for the timeline. */ const getTimelineDotColor = (status: string) => { switch (status) { case "completed": return "bg-green-500"; case "failed": return "bg-red-500"; case "running": return "bg-blue-500"; case "requested": case "scheduling": case "scheduled": return "bg-yellow-500"; case "timeout": return "bg-orange-500"; case "canceling": case "cancelled": return "bg-gray-400"; case "abandoned": return "bg-red-400"; default: return "bg-gray-400"; } }; /** Human-readable label for a status value. */ const getStatusLabel = (status: string) => { switch (status) { case "requested": return "Requested"; case "scheduling": return "Scheduling"; case "scheduled": return "Scheduled"; case "running": return "Running"; case "completed": return "Completed"; case "failed": return "Failed"; case "canceling": return "Canceling"; case "cancelled": return "Cancelled"; case "timeout": return "Timed Out"; case "abandoned": return "Abandoned"; default: return status.charAt(0).toUpperCase() + status.slice(1); } }; interface TimelineEntry { status: string; time: string; isInitial: boolean; } export default function ExecutionDetailPage() { const { id } = useParams<{ id: string }>(); const { data: executionData, isLoading, error } = useExecution(Number(id)); const execution = executionData?.data; // Fetch the action so we can get param_schema for the re-run modal const { data: actionData } = useAction(execution?.action_ref || ""); // Determine if this execution is a workflow (action has workflow_def) const isWorkflow = !!actionData?.data?.workflow_def; const [showRerunModal, setShowRerunModal] = useState(false); // Fetch status history for the timeline const { data: historyData, isLoading: historyLoading } = useExecutionHistory( Number(id), { page_size: 100 }, ); // Build timeline entries from history records const timelineEntries = useMemo(() => { const records = historyData?.data ?? []; const entries: TimelineEntry[] = []; for (const record of records) { if (record.operation === "INSERT" && record.new_values?.status) { entries.push({ status: String(record.new_values.status), time: record.time, isInitial: true, }); } else if ( record.operation === "UPDATE" && record.changed_fields.includes("status") && record.new_values?.status ) { entries.push({ status: String(record.new_values.status), time: record.time, isInitial: false, }); } } // History comes newest-first; reverse to chronological order entries.reverse(); return entries; }, [historyData]); // Subscribe to real-time updates for this execution const { isConnected } = useExecutionStream({ executionId: Number(id), enabled: !!id, }); if (isLoading) { return (
); } if (error || !execution) { return (

Error: {error ? (error as Error).message : "Execution not found"}

← Back to Executions
); } const isRunning = execution.status === ExecutionStatus.RUNNING || execution.status === ExecutionStatus.SCHEDULING || execution.status === ExecutionStatus.SCHEDULED || execution.status === ExecutionStatus.REQUESTED; return (
{/* Header */}
← Back to Executions

Execution #{execution.id}

{isWorkflow && ( Workflow )} {execution.status} {isRunning && (
In Progress
)} {isConnected && (
Live
)}

{execution.action_ref}

{execution.workflow_task && (

Task{" "} {execution.workflow_task.task_name} {execution.parent && ( <> in workflow Execution #{execution.parent} )}

)}
{/* Re-Run Modal */} {showRerunModal && actionData?.data && ( setShowRerunModal(false)} initialParameters={execution.config} /> )}
{/* Main Content */}
{/* Status & Timing */}

Execution Details

Status
{execution.status}
Created
{new Date(execution.created).toLocaleString()} ( {formatDistanceToNow(new Date(execution.created), { addSuffix: true, })} )
Updated
{new Date(execution.updated).toLocaleString()}
{execution.enforcement && (
Enforcement ID
{execution.enforcement}
)} {execution.parent && (
Parent Execution
#{execution.parent}
)} {execution.executor && (
Executor ID
{execution.executor}
)}
{/* Config/Parameters */} {execution.config && Object.keys(execution.config).length > 0 && (

Configuration

                {JSON.stringify(execution.config, null, 2)}
              
)} {/* Result */} {execution.result && Object.keys(execution.result).length > 0 && (

Result

                {JSON.stringify(execution.result, null, 2)}
              
)} {/* Timeline */}

Timeline

{historyLoading && (
Loading timeline…
)} {!historyLoading && timelineEntries.length === 0 && ( /* Fallback: no history data yet — show basic created/current status */

{getStatusLabel(execution.status)}

{new Date(execution.created).toLocaleString()}

)} {!historyLoading && timelineEntries.length > 0 && (
{timelineEntries.map((entry, idx) => { const isLast = idx === timelineEntries.length - 1; const time = new Date(entry.time); const prevTime = idx > 0 ? new Date(timelineEntries[idx - 1].time) : null; const durationMs = prevTime ? time.getTime() - prevTime.getTime() : null; return (
{!isLast && (
)}

{getStatusLabel(entry.status)}

{entry.status}

{time.toLocaleString()} ({formatDistanceToNow(time, { addSuffix: true })})

{durationMs !== null && durationMs > 0 && (

+{formatDuration(durationMs)} since previous

)}
); })} {isRunning && (

In Progress…

)}
)}
{/* Sidebar */}
{/* Quick Info */}

Quick Info

Action

{execution.action_ref}
{execution.enforcement && (

Enforcement ID

{execution.enforcement}

)}
{/* Quick Actions */}

Quick Actions

View Action View All Executions
{/* Workflow Tasks (shown only for workflow executions) */} {isWorkflow && (
)} {/* Artifacts */}
{/* Change History */}
); }