import { useState } from 'react' import { useNavigate } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { executionsApi } from '../api/workflows' import { useExecutionListUpdates } from '../hooks/useExecutionListUpdates' import { formatDistanceToNow } from 'date-fns' import { CheckCircle, XCircle, Clock, Ban, RefreshCw, ChevronLeft, ChevronRight, AlertCircle, Octagon } from 'lucide-react' import clsx from 'clsx' // Helper to safely format timestamps that might be invalid or too large function safeFormatDistanceToNow(timestamp: number): string { if (!timestamp || timestamp < 0 || timestamp > 8640000000000000) { return 'unknown' } try { return formatDistanceToNow(timestamp, { addSuffix: true }) } catch { return 'unknown' } } const PAGE_SIZE = 20 export default function ExecutionsPage() { const navigate = useNavigate() const [page, setPage] = useState(1) // The workflow editor already renders an execution across the canvas with each // node's input and output, so open it there rather than building a second // viewer that would show less. const openExecution = (execution: { id: string; workflowId: string }) => { if (!execution.workflowId) { return } navigate(`/workflows/${execution.workflowId}?execution=${encodeURIComponent(execution.id)}`) } const { data, isLoading, refetch } = useQuery({ queryKey: ['executions', page], queryFn: () => executionsApi.list(undefined, undefined, page, PAGE_SIZE), }) // Subscribe to WebSocket for real-time updates instead of polling useExecutionListUpdates() const executions = data?.executions || [] const total = data?.total || 0 const totalPages = Math.ceil(total / PAGE_SIZE) const hasMore = data?.hasMore || false const getStatusIcon = (status: string) => { switch (status) { case 'completed': return case 'failed': return case 'running': return case 'cancelled': return default: return } } const getStatusBadgeClass = (status: string) => { switch (status) { case 'completed': return 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' case 'failed': return 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400' case 'running': return 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400' case 'cancelled': return 'bg-gray-100 dark:bg-slate-700 text-gray-700 dark:text-gray-400' default: return 'bg-gray-100 dark:bg-slate-700 text-gray-600 dark:text-gray-400' } } return (

Executions

{isLoading ? (
) : executions.length === 0 ? (

No executions yet

) : ( <>
{executions.map((execution: any) => ( openExecution(execution)} title="Open this execution in the workflow editor" className="hover:bg-gray-50 dark:hover:bg-slate-700/50 cursor-pointer" > ))}
Status Workflow Trigger Runner Started Duration
{getStatusIcon(execution.status)} {execution.status}
{execution.workflowName || execution.workflowId} {execution.error && (
{execution.error}
)} {/* A run ended by a stop node carries a message the same way a failed one carries an error, and it is the only record of why a completed run did not do the rest of its work. Shown in a neutral colour: this outcome was chosen, not suffered. */} {!execution.error && execution.stopReason && (
{execution.stopReason}
)}
{execution.triggerType} {execution.runnerId || '-'} {execution.startedAt ? safeFormatDistanceToNow(execution.startedAt) : '-'} {execution.finishedAt && execution.startedAt ? `${((execution.finishedAt - execution.startedAt) / 1000).toFixed(2)}s` : execution.status === 'running' ? 'Running...' : '-'}
{/* Pagination */}
Showing {((page - 1) * PAGE_SIZE) + 1} - {Math.min(page * PAGE_SIZE, total)} of {total} executions
Page {page} of {totalPages || 1}
)}
) }