import React, { useEffect, useState } from "react"; import { renderMermaid } from "./mermaid"; const CAP = 45; export interface Candidate { kind: "table" | "column"; name: string; decision: "promoted" | "excluded" | "pending"; signals?: Record; evidence?: string[]; } export interface Join { from: string; to: string; source?: string; } export interface SchemaLinking { question?: string; candidates: Candidate[]; joins: Join[]; excluded: { kind: string; name: string }[]; open_questions: string[]; } /** The inline "perché" comment for a candidate: evidence first, then signal keys. */ function reasonFor(c: Candidate): string { if (c.evidence?.length) return c.evidence[0]; if (c.signals && Object.keys(c.signals).length) return Object.keys(c.signals).join(", "); return ""; } function buildFlowchart(promoted: Candidate[], joins: Join[]): string { const nodeId = (name: string) => name.replace(/[^a-zA-Z0-9]/g, "_"); const lines: string[] = ["flowchart TB"]; for (const c of promoted) { const id = nodeId(c.name); const reason = reasonFor(c); const tooltip = reason ? ` %% ${reason}` : ""; const shape = c.kind === "table" ? `[${c.name}]` : `(${c.name})`; lines.push(` ${id}${shape}${tooltip}`); } const promotedNames = new Set(promoted.map((c) => c.name)); for (const j of joins) { if (promotedNames.has(j.from) && promotedNames.has(j.to)) { lines.push(` ${nodeId(j.from)} --> ${nodeId(j.to)}`); } } return lines.join("\n"); } function buildHierarchicalRows(promoted: Candidate[]) { const tables = promoted.filter((c) => c.kind === "table"); const columns = promoted.filter((c) => c.kind === "column"); // Group columns under their table (column name format: "table.column") const grouped: { table: Candidate; columns: Candidate[] }[] = tables.map( (t) => ({ table: t, columns: columns.filter((col) => col.name.startsWith(t.name + ".")), }) ); // Orphan columns not attached to any promoted table const attachedCols = new Set( grouped.flatMap((g) => g.columns.map((c) => c.name)) ); const orphans = columns.filter((c) => !attachedCols.has(c.name)); return { grouped, orphans }; } export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) { const promoted = linking.candidates.filter((c) => c.decision === "promoted"); // The graph only contains promoted candidates, so cap on that count. const oversized = promoted.length > CAP; const [view, setView] = useState<"chart" | "table">( oversized ? "table" : "chart" ); const [svg, setSvg] = useState(""); useEffect(() => { if (view !== "chart" || oversized) return; let cancelled = false; const def = buildFlowchart(promoted, linking.joins); renderMermaid(def).then((s) => { if (!cancelled) setSvg(s); }); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [view, linking, oversized]); const { grouped, orphans } = buildHierarchicalRows(promoted); return (
{oversized && (

Too many elements for the graph, use the table

)} {view === "chart" && !oversized && (
)} {view === "table" && ( {grouped.map(({ table, columns: cols }) => ( {cols.map((col) => ( ))} ))} {orphans.map((col) => ( ))}
Name Type Why
{table.name} table {reasonFor(table)}
{col.name} column {reasonFor(col)}
{col.name} column {reasonFor(col)}
)}
); }