feat(web): workflow run detail page with live logs

This commit is contained in:
2026-07-20 11:53:30 +01:00
parent 2cd9bc1c89
commit e35e8fc839
@@ -0,0 +1,102 @@
"use client";
import { useParams } from "next/navigation";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { api, ServerRun, StepRun } from "@/lib/api";
import { Button, Badge, Card } from "@/components/ui";
type BadgeVariant = "success" | "warning" | "danger" | "neutral" | "accent";
const statusVariant: Record<string, BadgeVariant> = {
success: "success",
failed: "danger",
running: "accent",
queued: "neutral",
skipped: "neutral",
cancelled: "warning",
};
function StatusBadge({ status }: { status: string }) {
return <Badge variant={statusVariant[status] ?? "neutral"}>{status}</Badge>;
}
export default function RunDetail() {
const { runId } = useParams<{ runId: string }>();
const queryClient = useQueryClient();
const { data: run, isLoading } = useQuery({
queryKey: ["run", runId],
queryFn: () => api.getRun(runId),
refetchInterval: (query) => (query.state.data?.status === "running" ? 2000 : false),
});
const cancel = async () => {
await api.cancelRun(runId);
queryClient.invalidateQueries({ queryKey: ["run", runId] });
};
if (isLoading || !run) {
return <div className="p-8 text-text-secondary">Loading</div>;
}
return (
<div className="p-8">
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-text-primary">{run.name}</h1>
<p className="mt-1 flex items-center gap-2 text-sm text-text-secondary">
<span>Run {run.run_id.slice(0, 8)}</span>
<StatusBadge status={run.status} />
</p>
</div>
{run.status === "running" && (
<Button variant="danger" onClick={cancel}>
Cancel
</Button>
)}
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{run.server_runs.map((sr: ServerRun) => (
<Card key={sr.server_id}>
<div className="mb-3 flex items-center justify-between">
<span className="font-medium text-text-primary">{sr.hostname}</span>
<StatusBadge status={sr.status} />
</div>
<div className="space-y-2">
{sr.steps.map((st: StepRun) => (
<details
key={st.order}
className="rounded-lg border border-border bg-surface-2 p-2"
>
<summary className="flex cursor-pointer items-center justify-between gap-2">
<span className="text-sm text-text-primary">{st.name}</span>
<span className="flex items-center gap-2">
<span className="text-xs text-text-secondary">
attempts: {st.attempts}
{st.status === "failed" ? ` · exit ${st.exit_code}` : ""}
</span>
<StatusBadge status={st.status} />
</span>
</summary>
{(st.stdout || st.stderr) && (
<pre className="mt-2 max-h-64 overflow-auto rounded bg-black/40 p-2 font-mono text-xs text-text-secondary">
{st.stdout}
{st.stderr ? `\n${st.stderr}` : ""}
</pre>
)}
</details>
))}
{sr.steps.length === 0 && (
<p className="text-xs text-text-secondary">No steps yet.</p>
)}
</div>
</Card>
))}
{run.server_runs.length === 0 && (
<p className="text-text-secondary">No servers targeted by this run.</p>
)}
</div>
</div>
);
}