Claude
Add Test Bench: grid-inspect pipeline stages against real reference implementations
e1e33d1 unverified | "use client"; | |
| import { useEffect, useMemo, useState } from "react"; | |
| import { | |
| ReactFlow, | |
| Background, | |
| BackgroundVariant, | |
| Controls, | |
| type Edge, | |
| type Node, | |
| } from "@xyflow/react"; | |
| import "@xyflow/react/dist/style.css"; | |
| import type { ImplementationMode, Pipeline } from "@/lib/types"; | |
| import { | |
| computeLayout, | |
| computeBodyGroupLayout, | |
| BODY_GROUP_LABEL, | |
| BODY_GROUP_COLOR, | |
| type LayoutMode, | |
| } from "@/lib/graph-layout"; | |
| import StageNode from "./nodes/StageNode"; | |
| import GroupLabelNode from "./nodes/GroupLabelNode"; | |
| const nodeTypes = { stage: StageNode, "group-label": GroupLabelNode }; | |
| const LANDING_STEP_MS = 70; | |
| const GROUP_TRANSITION_MS = 950; | |
| export default function PipelineGraph({ | |
| pipeline, | |
| expandedIds, | |
| selectedId, | |
| activeModes, | |
| showMetrics, | |
| layoutMode, | |
| groupedByLayer, | |
| compact = false, | |
| onSelect, | |
| onToggleExpand, | |
| onSetMode, | |
| onDeepDive, | |
| }: { | |
| pipeline: Pipeline; | |
| expandedIds: Set<string>; | |
| selectedId: string | null; | |
| activeModes: Record<string, ImplementationMode>; | |
| showMetrics: boolean; | |
| layoutMode: LayoutMode; | |
| groupedByLayer: boolean; | |
| compact?: boolean; | |
| onSelect: (id: string) => void; | |
| onToggleExpand: (id: string) => void; | |
| onSetMode: (id: string, mode: ImplementationMode) => void; | |
| onDeepDive: (id: string) => void; | |
| }) { | |
| const [transitioning, setTransitioning] = useState(false); | |
| // Smooth glide, not a remount: when grouping toggles, briefly enable a CSS | |
| // transition on node transforms, then drop it so normal dragging stays crisp. | |
| useEffect(() => { | |
| setTransitioning(true); | |
| const t = setTimeout(() => setTransitioning(false), GROUP_TRANSITION_MS); | |
| return () => clearTimeout(t); | |
| }, [groupedByLayer]); | |
| const { nodes, edges } = useMemo(() => { | |
| const nodes: Node[] = []; | |
| let landingIndex = 0; | |
| if (groupedByLayer) { | |
| const { positions, groupHeaders } = computeBodyGroupLayout(pipeline); | |
| groupHeaders.forEach((h) => { | |
| nodes.push({ | |
| id: `group-header-${h.group}`, | |
| type: "group-label", | |
| position: { x: h.x - 160, y: h.y }, | |
| data: { label: BODY_GROUP_LABEL[h.group], color: BODY_GROUP_COLOR[h.group] }, | |
| draggable: false, | |
| selectable: false, | |
| }); | |
| }); | |
| pipeline.nodes.forEach((n) => { | |
| nodes.push({ | |
| id: n.id, | |
| type: "stage", | |
| position: positions[n.id] ?? { x: 0, y: 0 }, | |
| data: { | |
| node: n, | |
| isChild: false, | |
| isHub: false, | |
| expanded: false, | |
| hasChildren: false, | |
| showMetrics, | |
| activeMode: activeModes[n.id] ?? n.implementations[0]?.mode ?? "local", | |
| landingDelayMs: landingIndex++ * LANDING_STEP_MS, | |
| onSelect, | |
| onSetMode, | |
| onDeepDive, | |
| selected: selectedId === n.id, | |
| }, | |
| draggable: true, | |
| }); | |
| }); | |
| const edges: Edge[] = pipeline.edges | |
| .filter((e) => e.kind !== "hosts") | |
| .map((e) => ({ | |
| id: `e-${e.source}-${e.target}`, | |
| source: e.source, | |
| target: e.target, | |
| label: e.label, | |
| animated: e.kind === "data", | |
| type: "smoothstep", | |
| style: | |
| e.kind === "loop" | |
| ? { stroke: "#2dd4bf", strokeDasharray: "2 4", strokeWidth: 1.5 } | |
| : { stroke: "#f0b429", strokeWidth: 2, filter: "drop-shadow(0 0 3px rgba(240,180,41,0.55))" }, | |
| labelStyle: { fill: "#e6c878", fontSize: 10, fontWeight: 600 }, | |
| labelBgStyle: { fill: "#0e1826", fillOpacity: 0.85 }, | |
| })); | |
| return { nodes, edges }; | |
| } | |
| const { positions, flatChildren } = computeLayout(pipeline, expandedIds, layoutMode, compact); | |
| pipeline.nodes.forEach((n) => { | |
| nodes.push({ | |
| id: n.id, | |
| type: "stage", | |
| position: positions[n.id] ?? { x: 0, y: 0 }, | |
| data: { | |
| node: n, | |
| isChild: false, | |
| isHub: n.category === "orchestration", | |
| expanded: expandedIds.has(n.id), | |
| hasChildren: !!n.children?.length, | |
| showMetrics, | |
| compact, | |
| activeMode: activeModes[n.id] ?? n.implementations[0]?.mode ?? "local", | |
| landingDelayMs: landingIndex++ * LANDING_STEP_MS, | |
| onToggleExpand, | |
| onSelect, | |
| onSetMode, | |
| onDeepDive, | |
| selected: selectedId === n.id, | |
| }, | |
| draggable: true, | |
| }); | |
| }); | |
| Object.entries(flatChildren).forEach(([id, child]) => { | |
| nodes.push({ | |
| id, | |
| type: "stage", | |
| position: positions[id] ?? { x: 0, y: 0 }, | |
| data: { | |
| node: child, | |
| isChild: true, | |
| isHub: false, | |
| expanded: false, | |
| hasChildren: false, | |
| showMetrics, | |
| activeMode: activeModes[id] ?? child.implementations[0]?.mode ?? "local", | |
| landingDelayMs: landingIndex++ * LANDING_STEP_MS, | |
| onSelect, | |
| onSetMode, | |
| onDeepDive, | |
| selected: selectedId === id, | |
| }, | |
| draggable: true, | |
| }); | |
| }); | |
| // Grid mode is for inspecting nodes individually, not reading the flow — | |
| // skip edges entirely rather than let handoff labels clutter a grid. | |
| const edges: Edge[] = layoutMode === "grid" | |
| ? [] | |
| : pipeline.edges.map((e) => ({ | |
| id: `e-${e.source}-${e.target}`, | |
| source: e.source, | |
| target: e.target, | |
| label: e.label, | |
| animated: e.kind === "data", | |
| type: "smoothstep", | |
| style: | |
| e.kind === "hosts" | |
| ? { stroke: "#475569", strokeDasharray: "3 4", opacity: 0.45 } | |
| : e.kind === "loop" | |
| ? { stroke: "#2dd4bf", strokeDasharray: "2 4", strokeWidth: 1.5 } | |
| : { stroke: "#f0b429", strokeWidth: 2, filter: "drop-shadow(0 0 3px rgba(240,180,41,0.55))" }, | |
| labelStyle: { fill: "#e6c878", fontSize: 10, fontWeight: 600 }, | |
| labelBgStyle: { fill: "#0e1826", fillOpacity: 0.85 }, | |
| })); | |
| Object.keys(flatChildren).forEach((childId) => { | |
| const parent = pipeline.nodes.find((n) => | |
| n.children?.some((c) => c.id === childId), | |
| ); | |
| if (parent) { | |
| edges.push({ | |
| id: `e-${parent.id}-${childId}`, | |
| source: parent.id, | |
| target: childId, | |
| type: "straight", | |
| style: { stroke: "#64748b", strokeDasharray: "1 3", opacity: 0.6 }, | |
| }); | |
| } | |
| }); | |
| return { nodes, edges }; | |
| }, [pipeline, expandedIds, selectedId, activeModes, showMetrics, layoutMode, groupedByLayer, compact, onSelect, onToggleExpand, onSetMode, onDeepDive]); | |
| return ( | |
| <ReactFlow | |
| key={layoutMode} | |
| nodes={nodes} | |
| edges={edges} | |
| nodeTypes={nodeTypes} | |
| fitView | |
| fitViewOptions={{ padding: 0.35 }} | |
| minZoom={0.3} | |
| maxZoom={1.5} | |
| proOptions={{ hideAttribution: true }} | |
| className={transitioning ? "grouping-transition" : undefined} | |
| > | |
| <Background variant={BackgroundVariant.Dots} gap={26} size={1} color="#1c2b3f" /> | |
| <Controls | |
| showInteractive={false} | |
| className="!bg-[var(--panel)] !border !border-[var(--panel-border)] !fill-white [&_button]:!border-[var(--panel-border)] [&_button]:!text-white" | |
| /> | |
| </ReactFlow> | |
| ); | |
| } | |