"use client"; import React, { useCallback, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import ReactFlow, { Background, Controls, MiniMap, MarkerType, Handle, Position, type Node, type Edge, type NodeProps, } from "reactflow"; import "reactflow/dist/style.css"; import { GitBranch, ZoomIn, ZoomOut, Maximize2, RefreshCw, Layers, ChevronRight, } from "lucide-react"; import { Card, CardBody, CardHeader, CardTitle } from "@/components/ui/Card"; import { Button } from "@/components/ui/Button"; import { Badge, ArtifactTypeBadge, JobStatusBadge } from "@/components/ui/Badge"; import { EmptyState } from "@/components/ui/EmptyState"; import { traceability, artifacts, projects } from "@/lib/api"; import { cn, shortId } from "@/lib/utils"; import type { TraceGraph, ArtifactType } from "@/lib/types"; const DEMO_GRAPH: TraceGraph = { nodes: [ { id: "REQ-441", type: "requirement", label: "Multi-tenant RBAC", project_id: "p1", status: "in_progress", version: 3 }, { id: "TASK-921", type: "task", label: "Tenant middleware", project_id: "p1", status: "done" }, { id: "TASK-922", type: "task", label: "Policy evaluator", project_id: "p1", status: "done" }, { id: "PR-214", type: "pull_request", label: "feat(rbac) #214", project_id: "p1", status: "done" }, { id: "TEST-88", type: "test", label: "tenant_isolation.py", project_id: "p1", status: "done" }, { id: "BUILD-4412", type: "build", label: "build #4412", project_id: "p1", status: "done" }, { id: "DEP-44", type: "deployment", label: "asf-core v0.4.0 → prod", project_id: "p1", status: "done" }, ], edges: [ { id: "e1", source: "TASK-921", target: "REQ-441", label: "implements" }, { id: "e2", source: "TASK-922", target: "REQ-441", label: "implements" }, { id: "e3", source: "PR-214", target: "TASK-921", label: "generated_from" }, { id: "e4", source: "PR-214", target: "TASK-922", label: "generated_from" }, { id: "e5", source: "TEST-88", target: "PR-214", label: "verifies" }, { id: "e6", source: "BUILD-4412", target: "PR-214", label: "generated_from" }, { id: "e7", source: "DEP-44", target: "BUILD-4412", label: "deployed_as" }, ], mock_fields: ["*"], }; const TYPE_COLORS: Record = { requirement: { node: "#e0efff", ring: "#006fe0", text: "#084a8f" }, epic: { node: "#ede9fe", ring: "#7c3aed", text: "#4c1d95" }, story: { node: "#e0efff", ring: "#006fe0", text: "#084a8f" }, task: { node: "#e0f2fe", ring: "#0284c7", text: "#075985" }, architecture_decision: { node: "#ede9fe", ring: "#7c3aed", text: "#4c1d95" }, document: { node: "#f1f5f9", ring: "#64748b", text: "#334155" }, source_file: { node: "#f1f5f9", ring: "#64748b", text: "#334155" }, commit: { node: "#dcfce7", ring: "#16a34a", text: "#166534" }, pull_request: { node: "#e0f2fe", ring: "#0284c7", text: "#075985" }, build: { node: "#fef3c7", ring: "#d97706", text: "#92400e" }, test: { node: "#dcfce7", ring: "#16a34a", text: "#166534" }, security_finding: { node: "#fee2e2", ring: "#dc2626", text: "#991b1b" }, deployment: { node: "#dcfce7", ring: "#16a34a", text: "#166534" }, release: { node: "#dcfce7", ring: "#16a34a", text: "#166534" }, runtime_event: { node: "#fef3c7", ring: "#d97706", text: "#92400e" }, incident: { node: "#fee2e2", ring: "#dc2626", text: "#991b1b" }, ai_analysis: { node: "#ede9fe", ring: "#7c3aed", text: "#4c1d95" }, }; function ArtifactNode({ data, selected }: NodeProps<{ label: string; type: ArtifactType | string; status?: string; version?: number }>) { const c = TYPE_COLORS[data.type] ?? TYPE_COLORS.document; return (
{data.version !== undefined && v{data.version}}
{data.label}
{data.status && (
)}
); } const nodeTypes = { artifact: ArtifactNode as unknown as React.ComponentType }; function layoutLayered(graph: TraceGraph): { nodes: Node[]; edges: Edge[] } { const nodeMap = new Map(graph.nodes.map((n) => [n.id, n])); const outgoing = new Map(); const incoming = new Map(); graph.nodes.forEach((n) => { outgoing.set(n.id, []); incoming.set(n.id, []); }); graph.edges.forEach((e) => { outgoing.get(e.source)?.push(e.target); incoming.get(e.target)?.push(e.source); }); const depth = new Map(); const visit = (id: string, d: number, seen: Set) => { if (seen.has(id)) return; seen.add(id); depth.set(id, Math.max(depth.get(id) ?? 0, d)); outgoing.get(id)?.forEach((next) => visit(next, d + 1, seen)); }; graph.nodes.forEach((n) => { if ((incoming.get(n.id)?.length ?? 0) === 0) visit(n.id, 0, new Set()); }); graph.nodes.forEach((n) => { if (!depth.has(n.id)) depth.set(n.id, 0); }); const byDepth = new Map(); depth.forEach((d, id) => { if (!byDepth.has(d)) byDepth.set(d, []); byDepth.get(d)!.push(id); }); const nodes: Node[] = []; const colW = 240; const rowH = 110; byDepth.forEach((ids, d) => { ids.forEach((id, i) => { const raw = nodeMap.get(id)!; nodes.push({ id, type: "artifact", position: { x: d * colW, y: i * rowH }, data: { label: raw.label, type: raw.type, status: raw.status, version: raw.version }, }); }); }); const edges: Edge[] = graph.edges.map((e) => ({ id: e.id, source: e.source, target: e.target, label: e.label, type: "smoothstep", animated: false, style: { stroke: "#94a3b8", strokeWidth: 1.5 }, labelStyle: { fontSize: 10, fill: "#64748b" }, labelBgStyle: { fill: "#ffffffcc" }, labelBgPadding: [4, 2], markerEnd: { type: MarkerType.ArrowClosed, color: "#94a3b8" }, })); return { nodes, edges }; } export default function TraceabilityPage() { const [rootId, setRootId] = useState(""); const graphQ = useQuery({ queryKey: ["traceability", rootId], queryFn: () => traceability.graph(rootId ? { artifact_id: rootId, max_depth: 4 } : { limit: 200 }), retry: false, refetchOnWindowFocus: false, refetchInterval: 5000, }); const artQ = useQuery({ queryKey: ["traceability-artifacts"], queryFn: () => artifacts.list({ limit: 30 }), retry: false, refetchOnWindowFocus: false, }); const projectsQ = useQuery({ queryKey: ["traceability-projects"], queryFn: () => projects.list({ limit: 50 }), retry: false, refetchOnWindowFocus: false, }); const apiResolved = graphQ.isFetched && graphQ.data !== undefined; const rawGraph: TraceGraph = apiResolved ? graphQ.data! : DEMO_GRAPH; const isMock = !apiResolved; const { nodes, edges } = useMemo(() => layoutLayered(rawGraph), [rawGraph]); const artifactsList = (artQ.data ?? []).length ? artQ.data! : []; // Map project_id → pretty project name for the Inspector panel const projectNameById = useMemo(() => { const m = new Map(); for (const p of projectsQ.data ?? []) { if (p?.id) m.set(String(p.id), p.name ?? shortId(String(p.id))); } return m; }, [projectsQ.data]); const [selId, setSelId] = useState(null); const selectedNode = rawGraph.nodes.find((n) => n.id === selId); const onPaneReady = useCallback(() => {}, []); return (

Traceability

End-to-end artifact graph: Requirement → Task → Pull Request → Build → Test → Deployment. Click a node to inspect.

{isMock && Demo graph}
Graph view {rawGraph.nodes.length} nodes · {rawGraph.edges.length} edges {nodes.length === 0 ? (
) : ( setSelId(n.id)} onPaneClick={() => setSelId(null)} fitView fitViewOptions={{ padding: 0.3 }} proOptions={{ hideAttribution: true }} > (TYPE_COLORS[(n.data as { type?: string })?.type ?? "document"] ?? TYPE_COLORS.document).ring} maskColor="rgba(15, 23, 42, 0.06)" /> )}
Inspector {rawGraph.nodes.length} types {selectedNode ? (
{selectedNode.id}

{selectedNode.label}

{selectedNode.status && (
Status
)} {selectedNode.version !== undefined && (
Version
v{selectedNode.version}
)} {selectedNode.project_id && (
Project
{projectNameById.get(String(selectedNode.project_id)) ?? ( {shortId(selectedNode.project_id)} )}
)} {selectedNode.source && (
Source
{selectedNode.source}
)}
Connected edges
    {rawGraph.edges.filter((e) => e.source === selectedNode.id || e.target === selectedNode.id).map((e) => (
  • {e.source === selectedNode.id ? "→" : "←"} {e.source === selectedNode.id ? e.target : e.source} {e.label}
  • ))}
) : ( )}
Legend
    {Object.entries(TYPE_COLORS).slice(0, 10).map(([k, v]) => (
  • {k.replace("_", " ")}
  • ))}
); }