mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-02 02:58:10 +02:00
Merge commit 'a8390532f7' as 'ai-context/workbench-ui'
This commit is contained in:
commit
1a72bfdec0
310 changed files with 56430 additions and 0 deletions
|
|
@ -0,0 +1,827 @@
|
|||
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 (
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 20px",
|
||||
border: `2px solid ${borderColor}`,
|
||||
borderRadius: "6px",
|
||||
background: backgroundColor,
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
position: "relative",
|
||||
minWidth: "150px",
|
||||
minHeight: `${nodeHeight}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{/* 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 (
|
||||
<React.Fragment
|
||||
key={`${isProvides ? "provide" : "consume"}-${connection}`}
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={`${isProvides ? "provide" : "consume"}-${connection}`}
|
||||
style={{
|
||||
background: isProvides ? "#16a34a" : "#dc2626",
|
||||
top: `${((index + 1) / (leftConnections.length + 1)) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: `calc(100% + 15px)`,
|
||||
top: `calc(${((index + 1) / (leftConnections.length + 1)) * 100}% - 8px)`,
|
||||
transform: "translateY(-50%)",
|
||||
fontSize: "9px",
|
||||
color: isProvides ? "#16a34a" : "#dc2626",
|
||||
fontWeight: "normal",
|
||||
whiteSpace: "nowrap",
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{connection}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 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 (
|
||||
<React.Fragment
|
||||
key={`${isProvides ? "provide" : "consume"}-${connection}`}
|
||||
>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={`${isProvides ? "provide" : "consume"}-${connection}`}
|
||||
style={{
|
||||
background: isProvides ? "#16a34a" : "#dc2626",
|
||||
top: `${((index + 1) / (rightConnections.length + 1)) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `calc(100% + 15px)`,
|
||||
top: `calc(${((index + 1) / (rightConnections.length + 1)) * 100}% - 8px)`,
|
||||
transform: "translateY(-50%)",
|
||||
fontSize: "9px",
|
||||
color: isProvides ? "#16a34a" : "#dc2626",
|
||||
fontWeight: "normal",
|
||||
whiteSpace: "nowrap",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
{connection}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
<div style={{ fontSize: "12px", fontWeight: "600" }}>{data.label}</div>
|
||||
{data.type && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
color: borderColor,
|
||||
fontWeight: "normal",
|
||||
marginTop: "2px",
|
||||
}}
|
||||
>
|
||||
{data.type}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Interface node component - visually distinct from processors
|
||||
const InterfaceNode = ({
|
||||
data,
|
||||
}: {
|
||||
data: {
|
||||
label: string;
|
||||
interfaceKind?: string;
|
||||
description?: string;
|
||||
visible?: boolean;
|
||||
queues?: Record<string, unknown>;
|
||||
};
|
||||
}) => {
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
padding: "12px 20px",
|
||||
border: `2px dashed ${borderColor}`,
|
||||
borderRadius: "12px",
|
||||
background: backgroundColor,
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
minWidth: "180px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Connection handle on the right side */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={`interface-${data.label}`}
|
||||
style={{
|
||||
background: borderColor,
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
border: "2px solid white",
|
||||
right: "-6px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
fontSize: "16px",
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
<span>{icon}</span>
|
||||
<span>{data.label}</span>
|
||||
</div>
|
||||
{data.description && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: "#6b7280",
|
||||
fontStyle: "italic",
|
||||
textAlign: "center",
|
||||
maxWidth: "200px",
|
||||
}}
|
||||
>
|
||||
{data.description}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
color: borderColor,
|
||||
fontWeight: "bold",
|
||||
textTransform: "uppercase",
|
||||
marginTop: "4px",
|
||||
}}
|
||||
>
|
||||
{data.interfaceKind} interface
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Register custom node types
|
||||
const nodeTypes = {
|
||||
custom: CustomNode,
|
||||
interface: InterfaceNode,
|
||||
};
|
||||
|
||||
interface FlowClass {
|
||||
class?: Record<string, unknown>;
|
||||
flow?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// 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<string, unknown>;
|
||||
}>
|
||||
>();
|
||||
const consumersByType = new Map<
|
||||
string,
|
||||
Array<{
|
||||
processorId: string;
|
||||
processorName: string;
|
||||
connectionName: string;
|
||||
queues: Record<string, unknown>;
|
||||
}>
|
||||
>();
|
||||
|
||||
// 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<string, unknown> = {};
|
||||
|
||||
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<string, unknown>
|
||||
).request;
|
||||
const interfaceResponse = (
|
||||
interfaceQueues as Record<string, unknown>
|
||||
).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<FlowClassEditorViewProps> = ({
|
||||
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 (
|
||||
<Box p={6}>
|
||||
<HStack spacing={4} mb={4}>
|
||||
<Button
|
||||
onClick={onBack}
|
||||
leftIcon={<ArrowLeft size={16} />}
|
||||
variant="ghost"
|
||||
>
|
||||
Back to Flow Classes
|
||||
</Button>
|
||||
</HStack>
|
||||
<Text>Flow class not found.</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box h="100vh" display="flex" flexDirection="column">
|
||||
{/* Header */}
|
||||
<VStack
|
||||
spacing={4}
|
||||
p={6}
|
||||
bg="white"
|
||||
borderBottom="1px"
|
||||
borderColor="gray.200"
|
||||
>
|
||||
<HStack justifyContent="space-between" w="100%">
|
||||
<HStack spacing={4}>
|
||||
<Button
|
||||
onClick={onBack}
|
||||
leftIcon={<ArrowLeft size={16} />}
|
||||
variant="ghost"
|
||||
>
|
||||
Back to Flow Classes
|
||||
</Button>
|
||||
</HStack>
|
||||
<HStack spacing={4}>
|
||||
{/*
|
||||
<Button leftIcon={<FileCode size={16} />} variant="outline" size="sm">
|
||||
Export
|
||||
</Button>
|
||||
<Button leftIcon={<Construction size={16} />} variant="outline" size="sm">
|
||||
Build
|
||||
</Button>
|
||||
*/}
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
<VStack spacing={2} align="start" w="100%">
|
||||
<Heading size="lg">{flowClass.name}</Heading>
|
||||
</VStack>
|
||||
|
||||
<Separator />
|
||||
</VStack>
|
||||
|
||||
{/* ReactFlow Canvas */}
|
||||
<Box flex={1} position="relative">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
fitView
|
||||
attributionPosition="bottom-left"
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<MiniMap
|
||||
nodeColor={(node) => {
|
||||
return node.data?.type === "class" ? "#2563eb" : "#16a34a";
|
||||
}}
|
||||
position="top-right"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
||||
}}
|
||||
/>
|
||||
</ReactFlow>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue