"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { toast } from "sonner"; import { Play, GitCompareArrows, Pencil } from "lucide-react"; import { api, ApiError, type Connection, type Pipeline, type RunWithPipeline, type StreamConfig, } from "@/components/api"; import { LogViewer } from "@/components/log-viewer"; import { StatusBadge } from "@/components/status-badge"; import { fmtTime, fmtDuration } from "@/components/format"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Textarea } from "@/components/ui/textarea"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; export default function PipelineDetailPage() { const params = useParams<{ id: string }>(); const id = Number(params.id); const [pipeline, setPipeline] = useState(null); const [connMap, setConnMap] = useState>({}); const [runs, setRuns] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [runLoading, setRunLoading] = useState(false); const [diffLoading, setDiffLoading] = useState(false); const [diffSql, setDiffSql] = useState(null); const loadRuns = useCallback(() => { api("/api/runs") .then((all) => setRuns(all.filter((r) => r.pipeline_id === id)) ) .catch(() => {}); }, [id]); useEffect(() => { Promise.all([ api(`/api/pipelines/${id}`), api("/api/connections"), ]) .then(([p, conns]) => { setPipeline(p); setConnMap(Object.fromEntries(conns.map((c) => [c.id, c.name]))); }) .catch((e) => setError(e instanceof Error ? e.message : "加载失败")) .finally(() => setLoading(false)); loadRuns(); }, [id, loadRuns]); // 有运行中的任务时轮询刷新历史列表 const hasActive = runs.some( (r) => r.status === "running" || r.status === "queued" ); useEffect(() => { if (!hasActive) return; const t = setInterval(loadRuns, 4000); return () => clearInterval(t); }, [hasActive, loadRuns]); const streams = useMemo(() => { if (!pipeline) return []; try { const v = JSON.parse(pipeline.streams); return Array.isArray(v) ? v : []; } catch { return []; } }, [pipeline]); const latestRun = runs[0] ?? null; async function runNow() { setRunLoading(true); try { await api(`/api/pipelines/${id}/run`, { method: "POST" }); toast.success("已开始运行"); loadRuns(); } catch (e) { if (e instanceof ApiError && e.status === 409) { toast.info("该流水线正在运行中,请等待当前运行结束"); } else { toast.error(e instanceof Error ? e.message : "启动失败"); } } finally { setRunLoading(false); } } async function previewDiff() { setDiffLoading(true); setDiffSql(null); try { const res = await api<{ ok: boolean; sql: string }>( `/api/pipelines/${id}/diff` ); setDiffSql(res.sql || "-- 无差异,schema 已一致"); if (!res.ok) toast.info("Atlas 返回了非零状态,请检查 SQL 内容"); } catch (e) { toast.error(e instanceof Error ? e.message : "获取 diff 失败"); } finally { setDiffLoading(false); } } if (loading) return
加载中…
; if (error || !pipeline) return (
{error ? `加载失败:${error}` : "流水线不存在"}
); const connName = (cid: number) => connMap[cid] ?? `#${cid}`; return (

{pipeline.name}

配置
源 → 目标
{connName(pipeline.source_conn_id)} →{" "} {connName(pipeline.target_conn_id)}
同步模式
{pipeline.mode}
表数量
{streams.length}
Schema 同步
{pipeline.schema_sync ? ( ) : ( )}
同步范围
{pipeline.schema_scope === "all" ? "整个库" : "仅选中的表"}
包含外键
{pipeline.include_fk ? "是" : "否"}
创建时间
{fmtTime(pipeline.created_at)}
更新时间
{fmtTime(pipeline.updated_at)}
{streams.length > 0 ? (
{streams.map((s) => ( {s.name} ))}
) : null}
{diffLoading || diffSql !== null ? ( Schema Diff(Atlas dry-run) 目标库需要执行的 DDL。计算可能需要数十秒。 {diffLoading ? (
正在计算 diff,请稍候…
) : (