import type { LogEvent, RunDetail, RunWsStateMessage } from "@/contracts"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Clock, GitBranch, GitCommitHorizontal, Terminal, Timer, XCircle, Zap } from "lucide-react"; import { useMemo, useState } from "react"; import { useParams } from "react-router-dom"; import { LoadingPanel, LogViewer, StatusPill, StepRow } from "@/client/components"; import { Breadcrumbs, Button, Card, ConfirmDialog, ErrorBanner } from "@/client/components/ui"; import { useLogStream } from "@/client/hooks"; import { TRIGGER_TYPE_LABELS, formatApiError, formatDuration, formatRunFailureMessage, formatTimestamp, getApiClient, mergeLogEventBySeq, queryKeys, } from "@/client/lib"; import { useToast } from "@/client/toast"; const TERMINAL_STATUSES = new Set(["passed", "failed", "canceled"]); interface RunDetailContentProps { detail: RunDetail; projectName: string | null; runId: string; } const isTerminalStatus = (status: string): boolean => TERMINAL_STATUSES.has(status); const mergeLogs = (currentLogs: LogEvent[], incomingLogs: LogEvent[]): LogEvent[] => incomingLogs.reduce((merged, event) => mergeLogEventBySeq(merged, event), currentLogs); const RunDetailContent = ({ detail, projectName, runId }: RunDetailContentProps) => { const queryClient = useQueryClient(); const { pushToast } = useToast(); const [confirmCancelOpen, setConfirmCancelOpen] = useState(false); const [streamLogs, setStreamLogs] = useState(() => detail.recentLogs); const { run, steps, currentStep, errorMessage, detailAvailable } = detail; const isTerminal = isTerminalStatus(run.status); const cancelRunMutation = useMutation({ mutationFn: () => getApiClient().cancelRun(runId), onSuccess: (result) => { queryClient.setQueryData(queryKeys.runDetail(runId), result); void queryClient.invalidateQueries({ queryKey: queryKeys.projectDetail(result.run.projectId) }); void queryClient.invalidateQueries({ queryKey: queryKeys.projectRuns(result.run.projectId) }); pushToast({ tone: "success", title: "Cancel requested", message: "Run cancellation has been requested." }); }, onError: (reason) => { pushToast({ tone: "error", title: "Cancel failed", message: formatApiError(reason) }); }, }); const handleLogEvent = (event: LogEvent) => { setStreamLogs((current) => mergeLogEventBySeq(current, event)); }; const handleStateUpdate = (message: RunWsStateMessage) => { queryClient.setQueryData(queryKeys.runDetail(runId), (current) => { const source = current ?? detail; return { ...source, run: { ...source.run, status: message.run.status, startedAt: message.run.startedAt, finishedAt: message.run.finishedAt, exitCode: message.run.exitCode, }, currentStep: message.run.currentStep, errorMessage: message.run.errorMessage, steps: message.steps, detailAvailable: true, }; }); if (isTerminalStatus(message.run.status)) { void queryClient.invalidateQueries({ queryKey: queryKeys.runDetail(runId) }); } }; const logStreamStatus = useLogStream({ runId, enabled: !isTerminal, onEvent: handleLogEvent, onStateUpdate: handleStateUpdate, }); const displayedLogs = useMemo( () => (isTerminal ? mergeLogs(streamLogs, detail.recentLogs) : streamLogs), [detail.recentLogs, isTerminal, streamLogs], ); const pageTitle = `Run ${runId.slice(0, 12)}`; const commitDisplay = run.commitSha ? run.commitSha.slice(0, 7) : "\u2014"; const canCancel = !isTerminalStatus(run.status); const isCancelPending = run.status === "cancel_requested" || run.status === "canceling"; const canceling = cancelRunMutation.isPending; return (

{pageTitle}

{run.branch} {commitDisplay} {TRIGGER_TYPE_LABELS[run.triggerType] ?? run.triggerType} {run.startedAt && run.finishedAt ? ( {formatDuration(run.startedAt, run.finishedAt)} ) : null} Queued {formatTimestamp(run.queuedAt)}
{!detailAvailable ? (
Detailed run data is no longer available.
) : null} {errorMessage ? : null}
{steps.length > 0 ? (

Steps

{steps.map((step) => ( ))}
) : null} {canCancel ? ( <> { setConfirmCancelOpen(false); cancelRunMutation.mutate(); }} onCancel={() => setConfirmCancelOpen(false)} title="Cancel this run?" description="This run will be cancelled. This action cannot be undone." confirmLabel="Cancel Run" variant="danger" /> ) : null}
); }; export const RunDetailPage = () => { const { runId } = useParams<{ runId: string }>(); const resolvedRunId = runId ?? ""; const runQuery = useQuery({ queryKey: queryKeys.runDetail(resolvedRunId), queryFn: () => getApiClient().getRunDetail(resolvedRunId), enabled: resolvedRunId.length > 0, refetchInterval: (query) => { const detail = query.state.data; return detail !== undefined && !isTerminalStatus(detail.run.status) ? 30_000 : false; }, }); const projectId = runQuery.data?.run.projectId ?? ""; const projectNameQuery = useQuery({ queryKey: queryKeys.projectDetail(projectId), queryFn: () => getApiClient().getProjectDetail(projectId), enabled: projectId.length > 0, staleTime: 60_000, }); const pageTitle = `Run ${runId?.slice(0, 12) ?? ""}`; if (!runId) { return (
); } if (runQuery.isPending) { return ( <>

{pageTitle}

); } if (runQuery.isError && !runQuery.data) { return (
); } if (!runQuery.data) return null; return ( <> {runQuery.isError ? : null} ); };