diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java index 930088ce..a64272ce 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java @@ -265,10 +265,11 @@ public class ElkDiagramRenderer implements DiagramRenderer { } // Process main flow nodes into the root ELK graph + Set doTryNodeIds = new HashSet<>(); for (RouteNode rn : mainNodes) { if (!elkNodeMap.containsKey(rn.getId())) { createElkNodeRecursive(rn, rootNode, factory, elkNodeMap, nodeColors, - compoundNodeIds, childNodeIds); + compoundNodeIds, childNodeIds, doTryNodeIds); } } @@ -287,13 +288,20 @@ public class ElkDiagramRenderer implements DiagramRenderer { NodePlacementStrategy.LINEAR_SEGMENTS); createElkNodeRecursive(rn, handlerRoot, factory, elkNodeMap, nodeColors, - compoundNodeIds, childNodeIds); + compoundNodeIds, childNodeIds, doTryNodeIds); handlerRoots.add(handlerRoot); } // Create ELK edges — skip edges that cross between different ELK root graphs if (graph.getEdges() != null) { for (RouteEdge re : graph.getEdges()) { + // Skip all edges originating from DO_TRY nodes — these are entry/handler + // edges that don't need layout (try body has its own internal flow, + // handler edges are like route-level ON_EXCEPTION edges) + if (doTryNodeIds.contains(re.getSource())) { + continue; + } + ElkNode sourceElk = elkNodeMap.get(re.getSource()); ElkNode targetElk = elkNodeMap.get(re.getTarget()); if (sourceElk == null || targetElk == null) { @@ -322,17 +330,6 @@ public class ElkDiagramRenderer implements DiagramRenderer { engine.layout(handlerRoot, new BasicProgressMonitor()); } - // Debug: log root dimensions and children after layout - System.out.println("[ELK-v2] rootNode " + rootNode.getWidth() + "x" + rootNode.getHeight() - + " children=" + rootNode.getChildren().size() - + " mainNodes=" + mainNodes.size() + " handlerNodes=" + handlerNodes.size()); - for (ElkNode child : rootNode.getChildren()) { - System.out.println("[ELK-v2] " + child.getIdentifier() - + " x=" + String.format("%.1f", child.getX()) - + " y=" + String.format("%.1f", child.getY()) - + " parent=" + (child.getParent() != null ? child.getParent().getIdentifier() : "null")); - } - // Extract positioned nodes List positionedNodes = new ArrayList<>(); Map compoundInfos = new HashMap<>(); @@ -533,7 +530,8 @@ public class ElkDiagramRenderer implements DiagramRenderer { private void createElkNodeRecursive( RouteNode rn, ElkNode parentElk, ElkGraphFactory factory, Map elkNodeMap, Map nodeColors, - Set compoundNodeIds, Set childNodeIds) { + Set compoundNodeIds, Set childNodeIds, + Set doTryNodeIds) { boolean isCompound = rn.getType() != null && COMPOUND_TYPES.contains(rn.getType()) && rn.getChildren() != null && !rn.getChildren().isEmpty(); @@ -542,7 +540,63 @@ public class ElkDiagramRenderer implements DiagramRenderer { elkNode.setIdentifier(rn.getId()); elkNode.setParent(parentElk); - if (isCompound) { + if (isCompound && rn.getType() == NodeType.DO_TRY) { + // DO_TRY: vertical container with a virtual _TRY_BODY wrapper for the try body + // and DO_CATCH/DO_FINALLY as separate children below + doTryNodeIds.add(rn.getId()); + compoundNodeIds.add(rn.getId()); + elkNode.setWidth(200); + elkNode.setHeight(100); + elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered"); + elkNode.setProperty(CoreOptions.DIRECTION, Direction.DOWN); + elkNode.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.6); + elkNode.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5); + elkNode.setProperty(CoreOptions.PADDING, + new org.eclipse.elk.core.math.ElkPadding(COMPOUND_TOP_PADDING, + COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING)); + + // Separate try body children from handler children + List tryBodyChildren = new ArrayList<>(); + List handlerChildren = new ArrayList<>(); + for (RouteNode child : rn.getChildren()) { + if (child.getType() == NodeType.DO_CATCH || child.getType() == NodeType.DO_FINALLY) { + handlerChildren.add(child); + } else { + tryBodyChildren.add(child); + } + } + + // Virtual _TRY_BODY wrapper with LR direction for horizontal try body chain + if (!tryBodyChildren.isEmpty()) { + String wrapperId = rn.getId() + "._try_body"; + ElkNode wrapper = factory.createElkNode(); + wrapper.setIdentifier(wrapperId); + wrapper.setParent(elkNode); + wrapper.setWidth(200); + wrapper.setHeight(40); + wrapper.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered"); + wrapper.setProperty(CoreOptions.DIRECTION, Direction.RIGHT); + wrapper.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5); + wrapper.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5); + wrapper.setProperty(CoreOptions.PADDING, + new org.eclipse.elk.core.math.ElkPadding(8, 8, 8, 8)); + compoundNodeIds.add(wrapperId); + elkNodeMap.put(wrapperId, wrapper); + + for (RouteNode child : tryBodyChildren) { + childNodeIds.add(child.getId()); + createElkNodeRecursive(child, wrapper, factory, elkNodeMap, nodeColors, + compoundNodeIds, childNodeIds, doTryNodeIds); + } + } + + // Handler children are direct children of DO_TRY (stacked below) + for (RouteNode child : handlerChildren) { + childNodeIds.add(child.getId()); + createElkNodeRecursive(child, elkNode, factory, elkNodeMap, nodeColors, + compoundNodeIds, childNodeIds, doTryNodeIds); + } + } else if (isCompound) { compoundNodeIds.add(rn.getId()); elkNode.setWidth(200); elkNode.setHeight(100); @@ -557,7 +611,7 @@ public class ElkDiagramRenderer implements DiagramRenderer { for (RouteNode child : rn.getChildren()) { childNodeIds.add(child.getId()); createElkNodeRecursive(child, elkNode, factory, elkNodeMap, nodeColors, - compoundNodeIds, childNodeIds); + compoundNodeIds, childNodeIds, doTryNodeIds); } } else { elkNode.setWidth(NODE_WIDTH); @@ -583,11 +637,47 @@ public class ElkDiagramRenderer implements DiagramRenderer { List children = List.of(); if (compoundNodeIds.contains(rn.getId()) && rn.getChildren() != null) { children = new ArrayList<>(); - for (RouteNode child : rn.getChildren()) { - ElkNode childElk = elkNodeMap.get(child.getId()); - if (childElk != null) { - children.add(extractPositionedNode(child, childElk, elkNodeMap, - compoundNodeIds, compoundInfos, rootNode)); + + if (rn.getType() == NodeType.DO_TRY) { + // DO_TRY: extract virtual _TRY_BODY wrapper first, then handler children + String wrapperId = rn.getId() + "._try_body"; + ElkNode wrapperElk = elkNodeMap.get(wrapperId); + if (wrapperElk != null) { + List wrapperChildren = new ArrayList<>(); + for (RouteNode child : rn.getChildren()) { + if (child.getType() != NodeType.DO_CATCH && child.getType() != NodeType.DO_FINALLY) { + ElkNode childElk = elkNodeMap.get(child.getId()); + if (childElk != null) { + wrapperChildren.add(extractPositionedNode(child, childElk, elkNodeMap, + compoundNodeIds, compoundInfos, rootNode)); + } + } + } + children.add(new PositionedNode( + wrapperId, "", "_TRY_BODY", + getAbsoluteX(wrapperElk, rootNode), + getAbsoluteY(wrapperElk, rootNode), + wrapperElk.getWidth(), wrapperElk.getHeight(), + wrapperChildren)); + compoundInfos.put(wrapperId, new CompoundInfo(wrapperId, Color.WHITE)); + } + // Handler children (DO_CATCH, DO_FINALLY) + for (RouteNode child : rn.getChildren()) { + if (child.getType() == NodeType.DO_CATCH || child.getType() == NodeType.DO_FINALLY) { + ElkNode childElk = elkNodeMap.get(child.getId()); + if (childElk != null) { + children.add(extractPositionedNode(child, childElk, elkNodeMap, + compoundNodeIds, compoundInfos, rootNode)); + } + } + } + } else { + for (RouteNode child : rn.getChildren()) { + ElkNode childElk = elkNodeMap.get(child.getId()); + if (childElk != null) { + children.add(extractPositionedNode(child, childElk, elkNodeMap, + compoundNodeIds, compoundInfos, rootNode)); + } } } compoundInfos.put(rn.getId(), new CompoundInfo(rn.getId(), colorForType(rn.getType()))); diff --git a/ui/src/components/ProcessDiagram/CompoundNode.tsx b/ui/src/components/ProcessDiagram/CompoundNode.tsx index 2692a2bf..7c065bac 100644 --- a/ui/src/components/ProcessDiagram/CompoundNode.tsx +++ b/ui/src/components/ProcessDiagram/CompoundNode.tsx @@ -59,6 +59,47 @@ export function CompoundNode({ e => descendantIds.has(e.sourceId) && descendantIds.has(e.targetId), ); + const childProps = { + edges, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay, + overlayActive, iterationState, onIterationChange, + onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave, + }; + + // _TRY_BODY: transparent wrapper — no header, no border, just layout + if (node.type === '_TRY_BODY') { + return ( + + {renderInternalEdges(internalEdges, absX, absY, executionOverlay)} + {renderChildren(node, absX, absY, childProps)} + + ); + } + + // DO_CATCH / DO_FINALLY: section-like styling (tinted bg, thin border, label) + if (node.type === 'DO_CATCH' || node.type === 'DO_FINALLY') { + const sectionLabel = node.type === 'DO_CATCH' + ? (node.label ? `catch: ${node.label}` : 'catch') + : (node.label ? `finally: ${node.label}` : 'finally'); + + return ( + + {/* Tinted background */} + + {/* Border */} + + {/* Section label */} + + {sectionLabel} + + {renderInternalEdges(internalEdges, absX, absY, executionOverlay)} + {renderChildren(node, absX, absY, childProps)} + + ); + } + + // Default compound rendering (DO_TRY, EIP_CHOICE, etc.) return ( {/* Container body */} @@ -110,46 +151,56 @@ export function CompoundNode({ )} - {/* Internal edges (rendered after background, before children) */} - - {internalEdges.map((edge, i) => { - const isTraversed = executionOverlay - ? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId)) - : undefined; - return ( - [p[0] - absX, p[1] - absY]), - }} - traversed={isTraversed} - /> - ); - })} - + {renderInternalEdges(internalEdges, absX, absY, executionOverlay)} + {renderChildren(node, absX, absY, childProps)} + + ); +} - {/* Children — recurse into compound children, render leaves as DiagramNode */} +/** Render internal edges adjusted for compound coordinates */ +function renderInternalEdges( + internalEdges: DiagramEdgeType[], + absX: number, absY: number, + executionOverlay?: Map, +) { + return ( + + {internalEdges.map((edge, i) => { + const isTraversed = executionOverlay + ? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId)) + : undefined; + return ( + [p[0] - absX, p[1] - absY]), + }} + traversed={isTraversed} + /> + ); + })} + + ); +} + +/** Render children — compounds recurse, leaves become DiagramNode */ +function renderChildren( + node: DiagramNodeType, + absX: number, absY: number, + props: Omit, +) { + return ( + <> {node.children?.map(child => { if (isCompoundType(child.type) && child.children && child.children.length > 0) { return ( ); } @@ -161,19 +212,19 @@ export function CompoundNode({ x: (child.x ?? 0) - absX, y: (child.y ?? 0) - absY, }} - isHovered={hoveredNodeId === child.id} - isSelected={selectedNodeId === child.id} - config={child.id ? nodeConfigs?.get(child.id) : undefined} - executionState={executionOverlay?.get(child.id ?? '')} - overlayActive={overlayActive} - onClick={() => child.id && onNodeClick(child.id)} - onDoubleClick={() => child.id && onNodeDoubleClick?.(child.id)} - onMouseEnter={() => child.id && onNodeEnter(child.id)} - onMouseLeave={onNodeLeave} + isHovered={props.hoveredNodeId === child.id} + isSelected={props.selectedNodeId === child.id} + config={child.id ? props.nodeConfigs?.get(child.id) : undefined} + executionState={props.executionOverlay?.get(child.id ?? '')} + overlayActive={props.overlayActive} + onClick={() => child.id && props.onNodeClick(child.id)} + onDoubleClick={() => child.id && props.onNodeDoubleClick?.(child.id)} + onMouseEnter={() => child.id && props.onNodeEnter(child.id)} + onMouseLeave={props.onNodeLeave} /> ); })} - + ); } diff --git a/ui/src/components/ProcessDiagram/node-colors.ts b/ui/src/components/ProcessDiagram/node-colors.ts index d62d0c47..614a6b67 100644 --- a/ui/src/components/ProcessDiagram/node-colors.ts +++ b/ui/src/components/ProcessDiagram/node-colors.ts @@ -48,7 +48,7 @@ const TYPE_MAP: Record = { TRY_CATCH: ERROR_COLOR, DO_TRY: ERROR_COLOR, DO_CATCH: ERROR_COLOR, - DO_FINALLY: ERROR_COLOR, + DO_FINALLY: '#1A7F8E', // teal — completion handler, not error ON_COMPLETION: '#1A7F8E', // --running (teal, lifecycle handler) @@ -60,7 +60,7 @@ const TYPE_MAP: Record = { const COMPOUND_TYPES = new Set([ 'EIP_CHOICE', 'EIP_WHEN', 'EIP_OTHERWISE', 'EIP_SPLIT', 'TRY_CATCH', - 'DO_TRY', 'DO_CATCH', 'DO_FINALLY', + 'DO_TRY', 'DO_CATCH', 'DO_FINALLY', '_TRY_BODY', 'EIP_LOOP', 'EIP_MULTICAST', 'EIP_AGGREGATE', 'ON_EXCEPTION', 'ERROR_HANDLER', 'ON_COMPLETION',