sing-ui/app/page.tsx

126 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}