sing-ui/app/pipelines/page.tsx

156 lines
5.5 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 { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { toast } from "sonner";
import { Plus, Trash2 } from "lucide-react";
import { api, type Connection, type Pipeline } from "@/components/api";
import { fmtTime } from "@/components/format";
import { ConfirmDialog } from "@/components/confirm-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export default function PipelinesPage() {
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
const [connMap, setConnMap] = useState<Record<number, string>>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [deleting, setDeleting] = useState<Pipeline | null>(null);
const [deleteLoading, setDeleteLoading] = useState(false);
const load = useCallback(() => {
Promise.all([
api<Pipeline[]>("/api/pipelines"),
api<Connection[]>("/api/connections"),
])
.then(([p, conns]) => {
setPipelines(p);
setConnMap(Object.fromEntries(conns.map((c) => [c.id, c.name])));
})
.catch((e) => setError(e instanceof Error ? e.message : "加载失败"))
.finally(() => setLoading(false));
}, []);
useEffect(load, [load]);
async function doDelete() {
if (!deleting) return;
setDeleteLoading(true);
try {
await api(`/api/pipelines/${deleting.id}`, { method: "DELETE" });
toast.success(`已删除流水线「${deleting.name}`);
setDeleting(null);
load();
} catch (e) {
toast.error(e instanceof Error ? e.message : "删除失败");
} finally {
setDeleteLoading(false);
}
}
const connName = (id: number) => connMap[id] ?? `#${id}`;
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold">Pipelines</h1>
<Button>
<Link href="/pipelines/new" className="flex items-center gap-1">
<Plus className="size-4" />
线
</Link>
</Button>
</div>
{loading ? (
<div className="text-sm text-muted-foreground"></div>
) : error ? (
<div className="text-sm text-red-500">{error}</div>
) : pipelines.length === 0 ? (
<div className="rounded-md border border-dashed p-8 text-center text-sm text-muted-foreground">
线线
</div>
) : (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead> </TableHead>
<TableHead>Mode</TableHead>
<TableHead>Schema </TableHead>
<TableHead></TableHead>
<TableHead className="w-40"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pipelines.map((p) => (
<TableRow key={p.id}>
<TableCell className="font-medium">
<Link href={`/pipelines/${p.id}`} className="hover:underline">
{p.name}
</Link>
</TableCell>
<TableCell>
{connName(p.source_conn_id)} {connName(p.target_conn_id)}
</TableCell>
<TableCell className="font-mono text-xs">{p.mode}</TableCell>
<TableCell>
{p.schema_sync ? (
<Badge variant="outline" className="border-green-500/40 bg-green-500/10 text-green-600 dark:text-green-400">
</Badge>
) : (
<Badge variant="outline" className="text-muted-foreground">
</Badge>
)}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{fmtTime(p.updated_at)}
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button variant="outline" size="sm">
<Link href={`/pipelines/${p.id}`}></Link>
</Button>
<Button variant="outline" size="sm">
<Link href={`/pipelines/${p.id}/edit`}></Link>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setDeleting(p)}
>
<Trash2 className="size-3.5" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
<ConfirmDialog
open={!!deleting}
onOpenChange={(o) => !o && setDeleting(null)}
title={`删除流水线「${deleting?.name ?? ""}」?`}
description="删除后其运行历史仍会保留。"
loading={deleteLoading}
onConfirm={doDelete}
/>
</div>
);
}