import { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { Title, Text, Group, Button, Stack, Paper, Alert, Loader, CopyButton, Tooltip, ActionIcon, Code, } from "@mantine/core"; import { IconArrowLeft, IconCopy, IconCheck } from "@tabler/icons-react"; import { getRun } from "../api"; import { StatusBadge } from "../components/StatusBadge"; import { StepTimeline } from "../components/StepTimeline"; import type { RunDetail as RunDetailType, DagStepResult, DagRun } from "../types"; function buildLogText(run: DagRun, steps: DagStepResult[]): string { const lines: string[] = []; const started = run.StartedAt ? new Date(run.StartedAt) : null; const finished = run.FinishedAt ? new Date(run.FinishedAt) : null; const durationMs = started && finished ? finished.getTime() - started.getTime() : null; lines.push(`=== DAG run ${run.ID} ===`); lines.push(`dag: ${run.DagName}`); lines.push(`path: ${run.DagPath}`); lines.push(`status: ${run.Status}`); lines.push(`trigger: ${run.Trigger}`); lines.push(`started: ${started ? started.toISOString() : "-"}`); lines.push(`finished: ${finished ? finished.toISOString() : "-"}`); lines.push( `duration: ${durationMs !== null ? `${durationMs} ms` : "running..."}` ); if (run.Error) { lines.push(""); lines.push("run error:"); lines.push(run.Error); } lines.push(""); lines.push(`--- steps (${steps.length}) ---`); for (const s of steps) { lines.push(""); lines.push( `[${s.Status}] ${s.StepName} exit=${s.ExitCode} ${s.DurationMs}ms` ); if (s.StartedAt) lines.push(` started: ${s.StartedAt}`); if (s.FinishedAt) lines.push(` finished: ${s.FinishedAt}`); if (s.Error) { lines.push(" error:"); lines.push(s.Error.split("\n").map((l) => " " + l).join("\n")); } if (s.Stdout) { lines.push(" stdout:"); lines.push(s.Stdout.split("\n").map((l) => " " + l).join("\n")); } if (s.Stderr) { lines.push(" stderr:"); lines.push(s.Stderr.split("\n").map((l) => " " + l).join("\n")); } } return lines.join("\n"); } export function RunDetail() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const load = async () => { if (!id) return; try { setData(await getRun(id)); setError(null); } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }; useEffect(() => { load(); // Auto-refresh while running. const interval = setInterval(() => { if (data?.run.Status === "running") { load(); } }, 2000); return () => clearInterval(interval); }, [id, data?.run.Status]); if (loading) return ; if (error) return {error}; if (!data) return Not found; const { run, steps } = data; const duration = run.FinishedAt ? `${Math.round((new Date(run.FinishedAt).getTime() - new Date(run.StartedAt).getTime()) / 1000)}s` : "running..."; return (
Run {run.ID.substring(0, 16)}... {run.DagName} · {run.Trigger} ·{" "} {new Date(run.StartedAt).toLocaleString()}
{duration}
{run.Error && ( {run.Error} )} Steps ({steps?.length || 0}) {steps?.length ? ( ) : ( No steps recorded )} Logs {({ copied, copy }) => ( {copied ? : } )} {buildLogText(run, steps || [])}
); }