import React, { useCallback, useMemo, useEffect } from "react"; import { Box, VStack, HStack, Heading, Text, Button, Separator, } from "@chakra-ui/react"; import { ArrowLeft } from "lucide-react"; import ReactFlow, { Background, Controls, MiniMap, Node, Edge, useNodesState, useEdgesState, addEdge, Connection, ConnectionMode, Handle, Position, } from "reactflow"; import dagre from "dagre"; import "reactflow/dist/style.css"; import { useFlowClasses } from "@trustgraph/react-state"; import serviceMap from "../../data/service-map.json"; interface FlowClassEditorViewProps { flowClassId: string; onBack: () => void; } interface ProcessorInfo { [key: string]: unknown; } // Custom node component - use role for connections, direction for positioning const CustomNode = ({ data, }: { data: { label: string; type?: string; provides?: string[]; consumes?: string[]; processorInfo?: ProcessorInfo; }; }) => { const borderColor = data.type === "class" ? "#2563eb" : "#16a34a"; // blue for class, green for flow const backgroundColor = data.type === "class" ? "#eff6ff" : "#f0fdf4"; const provides = data.provides || []; const consumes = data.consumes || []; const processorInfo = data.processorInfo || { connections: [] }; // Group connections by direction for positioning const leftConnections: string[] = []; const rightConnections: string[] = []; interface Connection { name: string; role: string; direction?: string; } // Add provides connections to left or right based on direction provides.forEach((connectionName) => { const conn = (processorInfo.connections as Connection[] | undefined)?.find( (c) => c.name === connectionName && c.role === "provides", ); if (conn?.direction === "in") { leftConnections.push(connectionName); } else { rightConnections.push(connectionName); } }); // Add consumes connections to left or right based on direction consumes.forEach((connectionName) => { const conn = (processorInfo.connections as Connection[] | undefined)?.find( (c) => c.name === connectionName && c.role === "consumes", ); if (conn?.direction === "in") { leftConnections.push(connectionName); } else { rightConnections.push(connectionName); } }); const nodeHeight = Math.max( 50, Math.max(leftConnections.length, rightConnections.length) * 25 + 30, ); return (
{/* LEFT side connections */} {leftConnections.map((connection, index) => { const conn = ( processorInfo.connections as Connection[] | undefined )?.find((c) => c.name === connection); const isProvides = conn?.role === "provides"; return (
{connection}
); })} {/* RIGHT side connections */} {rightConnections.map((connection, index) => { const conn = ( processorInfo.connections as Connection[] | undefined )?.find((c) => c.name === connection); const isProvides = conn?.role === "provides"; return (
{connection}
); })}
{data.label}
{data.type && (
{data.type}
)}
); }; // Interface node component - visually distinct from processors const InterfaceNode = ({ data, }: { data: { label: string; interfaceKind?: string; description?: string; visible?: boolean; queues?: Record; }; }) => { const borderColor = data.interfaceKind === "service" ? "#8b5cf6" : "#ec4899"; // purple for service, pink for flow const backgroundColor = data.interfaceKind === "service" ? "#f3e8ff" : "#fce7f3"; const icon = data.interfaceKind === "service" ? "⚡" : "📦"; return (
{/* Connection handle on the right side */}
{icon} {data.label}
{data.description && (
{data.description}
)}
{data.interfaceKind} interface
); }; // Register custom node types const nodeTypes = { custom: CustomNode, interface: InterfaceNode, }; interface FlowClass { class?: Record; flow?: Record; } // Generate nodes from flow class processors const generateNodesFromFlowClass = (flowClass: FlowClass): Node[] => { const nodes: Node[] = []; // Add class processors Object.keys(flowClass.class || {}).forEach((processorName) => { // Strip template suffix to get base processor name for service map lookup const baseProcessorName = processorName.replace(/:\{[^}]+\}$/, ""); // Get connection info from service map - use role for connections, direction for positioning const processorInfo = serviceMap.processors[baseProcessorName] || { connections: [], }; const provides = processorInfo.connections ?.filter((conn) => conn.role === "provides") .map((conn) => conn.name) || []; const consumes = processorInfo.connections ?.filter((conn) => conn.role === "consumes") .map((conn) => conn.name) || []; nodes.push({ id: `class-${processorName}`, position: { x: 0, y: 0 }, // Will be calculated by dagre data: { label: processorName, type: "class", provides: provides, consumes: consumes, processorInfo: processorInfo, // Pass full processor info for direction lookup }, type: "custom", }); }); // Add flow processors Object.keys(flowClass.flow || {}).forEach((processorName) => { // Strip template suffix to get base processor name for service map lookup const baseProcessorName = processorName.replace(/:\{[^}]+\}$/, ""); // Get connection info from service map - use role for connections, direction for positioning const processorInfo = serviceMap.processors[baseProcessorName] || { connections: [], }; const provides = processorInfo.connections ?.filter((conn) => conn.role === "provides") .map((conn) => conn.name) || []; const consumes = processorInfo.connections ?.filter((conn) => conn.role === "consumes") .map((conn) => conn.name) || []; nodes.push({ id: `flow-${processorName}`, position: { x: 0, y: 0 }, // Will be calculated by dagre data: { label: processorName, type: "flow", provides: provides, consumes: consumes, processorInfo: processorInfo, // Pass full processor info for direction lookup }, type: "custom", }); }); // Add interface nodes Object.entries(flowClass.interfaces || {}).forEach( ([interfaceName, interfaceQueues]) => { // Look up interface definition in service map const interfaceDefinition = serviceMap.interfaces?.[interfaceName]; nodes.push({ id: `interface-${interfaceName}`, position: { x: 0, y: 0 }, // Will be calculated by dagre data: { label: interfaceName, type: "interface", interfaceKind: interfaceDefinition?.kind || "unknown", description: interfaceDefinition?.description || "", visible: interfaceDefinition?.visible, queues: interfaceQueues, }, type: "interface", // Use a different node type for interfaces }); }, ); return nodes; }; // Apply dagre layout to nodes and edges for better positioning const applyDagreLayout = (nodes: Node[], edges: Edge[]): Node[] => { const nodeWidth = 200; const nodeHeight = 120; // Increased for interface nodes // Create a new directed graph const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); dagreGraph.setGraph({ rankdir: "LR", // Left to right layout nodesep: 80, // Increased horizontal spacing between nodes ranksep: 500, // Extra 50% left-right spacing between ranks marginx: 40, // Increased margins marginy: 40, align: "UL", // Align ranks upward-left for better interface positioning acyclicer: "greedy", // Better cycle removal ranker: "tight-tree", // Better ranking algorithm }); // Add nodes to dagre graph nodes.forEach((node) => { dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); }); // Add edges to dagre graph edges.forEach((edge) => { dagreGraph.setEdge(edge.source, edge.target); }); // Calculate the layout dagre.layout(dagreGraph); // Apply the calculated positions back to the nodes return nodes.map((node) => { const nodeWithPosition = dagreGraph.node(node.id); return { ...node, position: { x: nodeWithPosition.x - nodeWidth / 2, y: nodeWithPosition.y - nodeHeight / 2, }, }; }); }; // Generate edges from flow class connections using three-way matching algorithm const generateEdgesFromFlowClass = (flowClass: FlowClass): Edge[] => { const edges: Edge[] = []; let edgeIndex = 0; // Build maps of providers and consumers by connection type const providersByType = new Map< string, Array<{ processorId: string; processorName: string; connectionName: string; queues: Record; }> >(); const consumersByType = new Map< string, Array<{ processorId: string; processorName: string; connectionName: string; queues: Record; }> >(); // Collect all processors and their connections from service map + flow class queues const allProcessors = [ ...Object.keys(flowClass.class || {}).map((name) => ({ name, type: "class", baseProcessorName: name.replace(/:\{[^}]+\}$/, ""), flowClassConnections: flowClass.class[name], })), ...Object.keys(flowClass.flow || {}).map((name) => ({ name, type: "flow", baseProcessorName: name.replace(/:\{[^}]+\}$/, ""), flowClassConnections: flowClass.flow[name], })), ]; allProcessors.forEach( ({ name, type, baseProcessorName, flowClassConnections }) => { const processorInfo = serviceMap.processors[baseProcessorName]; if (!processorInfo?.connections) return; const processorId = `${type}-${name}`; processorInfo.connections.forEach((connection) => { const connectionType = connection.type; const connectionKind = serviceMap.connection_types[connectionType]?.kind; // Extract queues based on connection kind let queues: Record = {}; if (connectionKind === "service") { // For service: look for {connection.name}-request and {connection.name}-response for consumers // For providers: look for request and response if (connection.role === "provides") { queues = { request: flowClassConnections.request, response: flowClassConnections.response, }; } else if (connection.role === "consumes") { queues = { request: flowClassConnections[`${connection.name}-request`], response: flowClassConnections[`${connection.name}-response`], }; } } else if (connectionKind === "flow") { // For flow: single queue value at connection.name queues = { value: flowClassConnections[connection.name] }; } else if (connectionKind === "passive") { // For passive: both consumer and provider use single queue value queues = { value: flowClassConnections[connection.name] }; } // Only add if we found valid queues if (Object.values(queues).some((q) => q !== undefined)) { if (connection.role === "provides") { if (!providersByType.has(connectionType)) { providersByType.set(connectionType, []); } providersByType.get(connectionType)!.push({ processorId, processorName: name, connectionName: connection.name, queues, }); } else if (connection.role === "consumes") { if (!consumersByType.has(connectionType)) { consumersByType.set(connectionType, []); } consumersByType.get(connectionType)!.push({ processorId, processorName: name, connectionName: connection.name, queues, }); } } }); }, ); // Create edges by matching providers and consumers using the three algorithms consumersByType.forEach((consumers, connectionType) => { const providers = providersByType.get(connectionType) || []; const connectionKind = serviceMap.connection_types[connectionType]?.kind; if (connectionKind === "passive") { // Passive connections - no special handling needed } consumers.forEach((consumer) => { providers.forEach((provider) => { // Skip self-connections if (consumer.processorId === provider.processorId) return; let isMatch = false; if (connectionKind === "service") { // Service: consumer's {connection-name}-request/response = provider's request/response isMatch = consumer.queues.request === provider.queues.request && consumer.queues.response === provider.queues.response; } else if (connectionKind === "flow") { // Flow: same queue value isMatch = consumer.queues.value === provider.queues.value; } else if (connectionKind === "passive") { // Passive: consumer's single queue = provider's single queue isMatch = consumer.queues.value === provider.queues.value; } if (isMatch) { // Determine edge styling let edgeColor = "#666666"; if (connectionKind === "service") edgeColor = "#2563eb"; else if (connectionKind === "flow") edgeColor = "#16a34a"; else if (connectionKind === "passive") edgeColor = "#dc2626"; // For logical flow direction (consumer requests → provider responds): // Use correct logical direction for both animation and layout edges.push({ id: `edge-${edgeIndex++}`, source: consumer.processorId, // Logical source (consumer makes request) target: provider.processorId, // Logical target (provider receives request) sourceHandle: `consume-${consumer.connectionName}`, // Consumer's outgoing handle targetHandle: `provide-${provider.connectionName}`, // Provider's incoming handle animated: connectionKind === "service", style: { stroke: edgeColor, strokeWidth: connectionKind === "passive" ? 1 : 2, }, label: connectionType, type: connectionKind === "passive" ? "step" : "default", }); } }); }); }); // Connect interfaces to their implementing processors Object.entries(flowClass.interfaces || {}).forEach( ([interfaceName, interfaceQueues]) => { const interfaceDefinition = serviceMap.interfaces?.[interfaceName]; const interfaceKind = interfaceDefinition?.kind; if (!interfaceKind) { return; } // Find processors that match this interface's queue pattern allProcessors.forEach( ({ name, type, baseProcessorName, flowClassConnections }) => { const processorId = `${type}-${name}`; const processorInfo = serviceMap.processors[baseProcessorName]; if (!processorInfo?.connections) return; let isMatch = false; let matchingConnection: Connection | null = null; if (interfaceKind === "service") { // For service interfaces: check if processor PROVIDES this service const interfaceRequest = ( interfaceQueues as Record ).request; const interfaceResponse = ( interfaceQueues as Record ).response; // Check if this processor provides this service if ( flowClassConnections.request === interfaceRequest && flowClassConnections.response === interfaceResponse ) { // Find the service connection that provides matchingConnection = processorInfo.connections.find( (c) => c.role === "provides" && c.name === "service", ); if (matchingConnection) { isMatch = true; } } } else if (interfaceKind === "flow") { // For flow interfaces: check if processor PROVIDES this flow const interfaceQueue = interfaceQueues as string; // Check only provider connections for matching queue processorInfo.connections.forEach((connection) => { if (connection.role === "provides") { const connectionQueue = flowClassConnections[connection.name]; if (connectionQueue === interfaceQueue) { matchingConnection = connection; isMatch = true; } } }); } if (isMatch && matchingConnection) { // Create edge from interface to processor const edgeColor = interfaceKind === "service" ? "#8b5cf6" : "#ec4899"; edges.push({ id: `interface-edge-${edgeIndex++}`, source: `interface-${interfaceName}`, target: processorId, sourceHandle: `interface-${interfaceName}`, targetHandle: matchingConnection.role === "provides" ? `provide-${matchingConnection.name}` : `consume-${matchingConnection.name}`, animated: true, style: { stroke: edgeColor, strokeWidth: 2, strokeDasharray: "5,5", }, label: `implements ${interfaceName}`, type: "default", }); } }, ); }, ); return edges; }; export const FlowClassEditorView: React.FC = ({ flowClassId, onBack, }) => { const { flowClasses } = useFlowClasses(); const flowClass = flowClasses.find((fc) => fc.id === flowClassId); // Generate nodes and edges from flow class data using useMemo - must be before early return const initialNodes = useMemo(() => { if (!flowClass) return []; const nodes = generateNodesFromFlowClass(flowClass); return nodes; }, [flowClass]); const generatedEdges = useMemo(() => { if (!flowClass) return []; const edges = generateEdgesFromFlowClass(flowClass); return edges; }, [flowClass]); const layoutedNodes = useMemo(() => { const layouted = applyDagreLayout(initialNodes, generatedEdges); return layouted; }, [initialNodes, generatedEdges]); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); // Update nodes and edges when the data changes useEffect(() => { setNodes(layoutedNodes); }, [layoutedNodes, setNodes]); useEffect(() => { setEdges(generatedEdges); }, [generatedEdges, setEdges]); const onConnect = useCallback( (params: Connection) => setEdges((eds) => addEdge(params, eds)), [setEdges], ); if (!flowClass) { return ( Flow class not found. ); } return ( {/* Header */} {/* */} {flowClass.name} {/* ReactFlow Canvas */} { return node.data?.type === "class" ? "#2563eb" : "#16a34a"; }} position="top-right" style={{ backgroundColor: "rgba(255, 255, 255, 0.8)", }} /> ); };