153 lines
5.3 KiB
TypeScript
153 lines
5.3 KiB
TypeScript
"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"
|
||
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>
|
||
);
|
||
}
|