|
|
@@ -1,75 +0,0 @@
|
|
|
-"use client";
|
|
|
-
|
|
|
-import { useState } from "react";
|
|
|
-import { Braces, ChevronRight, CircleOff, Copy, GitFork, X } from "lucide-react";
|
|
|
-import { FieldTree } from "@/components/FieldTree";
|
|
|
-import type { FlowEdgeRecord, FlowNodeRecord, RunGraph } from "@/lib/types";
|
|
|
-
|
|
|
-type Tab = "fields" | "lineage" | "contract";
|
|
|
-
|
|
|
-function EdgeList({ title, edges, graph, direction }: {
|
|
|
- title: string;
|
|
|
- edges: FlowEdgeRecord[];
|
|
|
- graph: RunGraph;
|
|
|
- direction: "incoming" | "outgoing";
|
|
|
-}) {
|
|
|
- return <section className="inspectorSection">
|
|
|
- <h3>{title}<span>{edges.length}</span></h3>
|
|
|
- {edges.length ? <div className="edgeList">{edges.map((edge) => {
|
|
|
- const otherId = direction === "incoming" ? edge.source : edge.target;
|
|
|
- const other = graph.nodes.find((item) => item.id === otherId);
|
|
|
- return <div key={edge.id} className={`edgeRow edge-${edge.kind}`}>
|
|
|
- <GitFork size={14} />
|
|
|
- <div><strong>{other?.title ?? otherId}</strong><small>{edge.label ?? edge.kind}</small></div>
|
|
|
- <ChevronRight size={14} />
|
|
|
- </div>;
|
|
|
- })}</div> : <p className="emptyState"><CircleOff size={15} /> 无连接</p>}
|
|
|
- </section>;
|
|
|
-}
|
|
|
-
|
|
|
-export function Inspector({ item, graph, onClose }: {
|
|
|
- item: FlowNodeRecord;
|
|
|
- graph: RunGraph;
|
|
|
- onClose: () => void;
|
|
|
-}) {
|
|
|
- const [tab, setTab] = useState<Tab>("fields");
|
|
|
- const incoming = graph.edges.filter((edge) => edge.target === item.id);
|
|
|
- const outgoing = graph.edges.filter((edge) => edge.source === item.id);
|
|
|
- const expectedFields = graph.schema_catalog[item.record.model_name] ?? [];
|
|
|
- const copyJson = () => navigator.clipboard?.writeText(JSON.stringify(item.record.payload, null, 2));
|
|
|
- return <aside className="inspector">
|
|
|
- <header className="inspectorHeader">
|
|
|
- <div className={`inspectorIcon type-${item.node_type}`}><Braces size={19} /></div>
|
|
|
- <div><span>{item.phase} / {item.lane}</span><h2>{item.title}</h2></div>
|
|
|
- <button type="button" className="iconButton" onClick={onClose} aria-label="关闭详情"><X size={18} /></button>
|
|
|
- </header>
|
|
|
- <div className="inspectorMeta">
|
|
|
- <span className={`pill status-${item.status}`}>{item.status}</span>
|
|
|
- <code>{item.id}</code>
|
|
|
- </div>
|
|
|
- <nav className="inspectorTabs" aria-label="节点详情页签">
|
|
|
- <button className={tab === "fields" ? "active" : ""} onClick={() => setTab("fields")}>全部字段</button>
|
|
|
- <button className={tab === "lineage" ? "active" : ""} onClick={() => setTab("lineage")}>上下游</button>
|
|
|
- <button className={tab === "contract" ? "active" : ""} onClick={() => setTab("contract")}>字段合同</button>
|
|
|
- </nav>
|
|
|
- <div className="inspectorScroll">
|
|
|
- {tab === "fields" ? <>
|
|
|
- <section className="modelBanner">
|
|
|
- <div><small>PROJECT MODEL</small><strong>{item.record.model_name}</strong></div>
|
|
|
- <button type="button" onClick={copyJson}><Copy size={14} /> 复制 JSON</button>
|
|
|
- </section>
|
|
|
- <FieldTree value={item.record.payload} />
|
|
|
- </> : null}
|
|
|
- {tab === "lineage" ? <>
|
|
|
- <EdgeList title="输入" edges={incoming} graph={graph} direction="incoming" />
|
|
|
- <EdgeList title="输出" edges={outgoing} graph={graph} direction="outgoing" />
|
|
|
- </> : null}
|
|
|
- {tab === "contract" ? <section className="inspectorSection contractFields">
|
|
|
- <h3>当前源码字段<span>{expectedFields.length}</span></h3>
|
|
|
- <p>fake payload 必须与此清单完全同键;后端测试会拒绝遗漏和额外字段。</p>
|
|
|
- <ol>{expectedFields.map((field) => <li key={field}><code>{field}</code><span>{field in item.record.payload ? "present" : "missing"}</span></li>)}</ol>
|
|
|
- </section> : null}
|
|
|
- </div>
|
|
|
- </aside>;
|
|
|
-}
|
|
|
-
|