sing-ui/app/runs/[id]/page.tsx

127 lines
3.9 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import {
api,
type Pipeline,
type RunDetail,
} from "@/components/api";
import { LogViewer } from "@/components/log-viewer";
import { StatusBadge } from "@/components/status-badge";
import { fmtTime, fmtDuration } from "@/components/format";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export default function RunDetailPage() {
const params = useParams<{ id: string }>();
const id = Number(params.id);
const [run, setRun] = useState<RunDetail | null>(null);
const [pipelineName, setPipelineName] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const load = useCallback(() => {
api<RunDetail>(`/api/runs/${id}`)
.then((r) => {
setRun(r);
api<Pipeline>(`/api/pipelines/${r.pipeline_id}`)
.then((p) => setPipelineName(p.name))
.catch(() => setPipelineName(null));
})
.catch((e) => setError(e instanceof Error ? e.message : "加载失败"))
.finally(() => setLoading(false));
}, [id]);
useEffect(load, [load]);
if (loading) return <div className="text-sm text-muted-foreground"></div>;
if (error || !run)
return (
<div className="text-sm text-red-500">
{error ? `加载失败:${error}` : "运行记录不存在"}
</div>
);
const isActive = run.status === "running" || run.status === "queued";
return (
<div className="flex flex-col gap-6">
<h1 className="flex items-center gap-3 text-xl font-semibold">
#{run.id}
<StatusBadge status={run.status} />
</h1>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<dl className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm sm:grid-cols-3">
<div>
<dt className="text-muted-foreground">线</dt>
<dd>
<Link
href={`/pipelines/${run.pipeline_id}`}
className="font-medium hover:underline"
>
{pipelineName ?? `#${run.pipeline_id}`}
</Link>
</dd>
</div>
<div>
<dt className="text-muted-foreground"></dt>
<dd>{fmtTime(run.started_at)}</dd>
</div>
<div>
<dt className="text-muted-foreground"></dt>
<dd>{fmtTime(run.finished_at)}</dd>
</div>
<div>
<dt className="text-muted-foreground"></dt>
<dd>{fmtDuration(run.started_at, run.finished_at)}</dd>
</div>
<div>
<dt className="text-muted-foreground"> PID</dt>
<dd className="font-mono">{run.pid ?? "-"}</dd>
</div>
</dl>
{run.error ? (
<div className="mt-4">
<div className="mb-1 text-sm text-muted-foreground"></div>
<pre className="whitespace-pre-wrap break-all rounded-md border border-red-500/40 bg-red-500/10 p-3 font-mono text-xs text-red-600 dark:text-red-400">
{run.error}
</pre>
</div>
) : null}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{isActive ? "(实时)" : ""}</CardTitle>
</CardHeader>
<CardContent>
{isActive ? (
<LogViewer
runId={run.id}
live
className="h-[32rem]"
emptyText="等待日志输出…"
onDone={load}
/>
) : (
<LogViewer log={run.log ?? ""} className="h-[32rem]" />
)}
</CardContent>
</Card>
</div>
);
}