| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475 |
- "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>;
- }
|