"use client"; import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { api, type Connection, type Pipeline, type StreamConfig, type StreamInfo, } from "@/components/api"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; const MODES = [ { value: "full-refresh", label: "full-refresh(全量覆盖,默认)" }, { value: "truncate", label: "truncate(清空后写入)" }, { value: "incremental", label: "incremental(增量)" }, { value: "snapshot", label: "snapshot(快照)" }, ]; export default function NewPipelinePage() { const router = useRouter(); const [connections, setConnections] = useState([]); const [connError, setConnError] = useState(null); const [name, setName] = useState(""); const [sourceId, setSourceId] = useState(""); const [targetId, setTargetId] = useState(""); const [mode, setMode] = useState("full-refresh"); const [schemaSync, setSchemaSync] = useState(true); const [streamList, setStreamList] = useState(null); const [streamsLoading, setStreamsLoading] = useState(false); const [selected, setSelected] = useState>({}); const [saving, setSaving] = useState(false); useEffect(() => { api("/api/connections") .then(setConnections) .catch((e) => setConnError(e instanceof Error ? e.message : "加载失败")); }, []); // 切换源连接时同时清空已加载的表(在 onValueChange 里处理) const selectedList = useMemo(() => Object.values(selected), [selected]); async function loadStreams() { if (!sourceId) return; setStreamsLoading(true); try { const list = await api( `/api/connections/${sourceId}/streams` ); setStreamList(list); if (list.length === 0) toast.info("该连接没有可同步的表"); } catch (e) { toast.error(e instanceof Error ? e.message : "加载表失败"); } finally { setStreamsLoading(false); } } function toggleStream(s: StreamInfo, checked: boolean) { setSelected((m) => { const next = { ...m }; if (checked) next[s.name] = { name: s.name }; else delete next[s.name]; return next; }); } function selectAll(on: boolean) { if (!streamList) return; setSelected( on ? Object.fromEntries( streamList.map((s) => [s.name, selected[s.name] ?? { name: s.name }]) ) : {} ); } function setStreamField( name: string, field: "primary_key" | "update_key", value: string ) { setSelected((m) => ({ ...m, [name]: { ...m[name], name, [field]: value || undefined }, })); } async function submit() { if (!name.trim()) return toast.error("请填写流水线名称"); if (!sourceId || !targetId) return toast.error("请选择源连接和目标连接"); if (sourceId === targetId) return toast.error("源连接和目标连接不能相同"); if (selectedList.length === 0) return toast.error("请至少选择一张表"); setSaving(true); try { const p = await api("/api/pipelines", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name.trim(), source_conn_id: Number(sourceId), target_conn_id: Number(targetId), schema_sync: schemaSync ? 1 : 0, mode, streams: JSON.stringify(selectedList), }), }); toast.success("流水线已创建"); router.push(`/pipelines/${p.id}`); } catch (e) { toast.error(e instanceof Error ? e.message : "创建失败"); setSaving(false); } } return (

新建流水线

{connError ? (
连接列表加载失败:{connError}
) : null} 基本信息
setName(e.target.value)} placeholder="例如:测试库 → 本地 dev" />
先用 Atlas 同步 schema(表/索引/约束),再迁移数据
setSchemaSync(!!c)} />
选择要同步的表 选择源连接后点击「加载表」,勾选需要同步的表。
{streamList && streamList.length > 0 ? ( <> 已选 {selectedList.length} / {streamList.length} ) : null}
{streamList === null ? null : streamList.length === 0 ? (
没有可同步的表。
) : (
{streamList.map((s) => { const checked = !!selected[s.name]; return (
toggleStream(s, !!c)} /> {s.name} {mode === "incremental" && checked ? (
setStreamField(s.name, "primary_key", e.target.value) } /> setStreamField(s.name, "update_key", e.target.value) } />
) : null}
); })}
)}
); }