127 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|