126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
import {
|
||
api,
|
||
type Connection,
|
||
type Pipeline,
|
||
type RunWithPipeline,
|
||
} from "@/components/api";
|
||
import { StatusBadge } from "@/components/status-badge";
|
||
import { fmtTime, fmtDuration } from "@/components/format";
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from "@/components/ui/card";
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from "@/components/ui/table";
|
||
import { Button } from "@/components/ui/button";
|
||
|
||
export default function DashboardPage() {
|
||
const [connections, setConnections] = useState<Connection[]>([]);
|
||
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
|
||
const [runs, setRuns] = useState<RunWithPipeline[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
Promise.all([
|
||
api<Connection[]>("/api/connections"),
|
||
api<Pipeline[]>("/api/pipelines"),
|
||
api<RunWithPipeline[]>("/api/runs"),
|
||
])
|
||
.then(([c, p, r]) => {
|
||
setConnections(c);
|
||
setPipelines(p);
|
||
setRuns(r);
|
||
})
|
||
.catch((e) => setError(e instanceof Error ? e.message : "加载失败"))
|
||
.finally(() => setLoading(false));
|
||
}, []);
|
||
|
||
if (loading) return <div className="text-sm text-muted-foreground">加载中…</div>;
|
||
if (error) return <div className="text-sm text-red-500">加载失败:{error}</div>;
|
||
|
||
const recent = runs.slice(0, 5);
|
||
const stats = [
|
||
{ label: "连接数", value: connections.length, href: "/connections" },
|
||
{ label: "流水线数", value: pipelines.length, href: "/pipelines" },
|
||
{ label: "运行总数", value: runs.length, href: "/runs" },
|
||
];
|
||
|
||
return (
|
||
<div className="flex flex-col gap-6">
|
||
<h1 className="text-xl font-semibold">Dashboard</h1>
|
||
|
||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||
{stats.map((s) => (
|
||
<Card key={s.label}>
|
||
<CardHeader>
|
||
<CardTitle className="text-sm text-muted-foreground">
|
||
{s.label}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Link href={s.href} className="text-3xl font-semibold hover:underline">
|
||
{s.value}
|
||
</Link>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>最近运行</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{recent.length === 0 ? (
|
||
<div className="text-sm text-muted-foreground">还没有运行记录。</div>
|
||
) : (
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className="w-16">ID</TableHead>
|
||
<TableHead>流水线</TableHead>
|
||
<TableHead>状态</TableHead>
|
||
<TableHead>开始时间</TableHead>
|
||
<TableHead>耗时</TableHead>
|
||
<TableHead className="w-20"></TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{recent.map((r) => (
|
||
<TableRow key={r.id}>
|
||
<TableCell className="font-mono">#{r.id}</TableCell>
|
||
<TableCell>{r.pipeline_name ?? `(已删除 #${r.pipeline_id})`}</TableCell>
|
||
<TableCell>
|
||
<StatusBadge status={r.status} />
|
||
</TableCell>
|
||
<TableCell>{fmtTime(r.started_at)}</TableCell>
|
||
<TableCell>{fmtDuration(r.started_at, r.finished_at)}</TableCell>
|
||
<TableCell>
|
||
<Button variant="link" size="sm">
|
||
<Link href={`/runs/${r.id}`}>详情</Link>
|
||
</Button>
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|