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 38e4e54e..a1b20137 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 @@ -109,7 +109,8 @@ public class ElkDiagramRenderer implements DiagramRenderer { NodeType.DO_TRY, NodeType.DO_CATCH, NodeType.DO_FINALLY, NodeType.EIP_LOOP, NodeType.EIP_MULTICAST, NodeType.EIP_AGGREGATE, NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER, - NodeType.ON_COMPLETION, NodeType.EIP_CIRCUIT_BREAKER + NodeType.ON_COMPLETION, NodeType.EIP_CIRCUIT_BREAKER, + NodeType.EIP_FILTER, NodeType.EIP_IDEMPOTENT_CONSUMER, NodeType.EIP_RECIPIENT_LIST ); /** Top-level handler types laid out in their own separate ELK graph. */ @@ -185,15 +186,16 @@ public class ElkDiagramRenderer implements DiagramRenderer { // ---------------------------------------------------------------- private LayoutResult computeLayout(RouteGraph graph, Direction rootDirection) { - LayoutContext ctx = new LayoutContext(ElkGraphFactory.eINSTANCE); - - // 1. Partition graph nodes into main flow vs handler sections + // 1. Build node index from root tree (preserves children) with flat-list fallback Map nodeById = buildNodeIndex(graph); + LayoutContext ctx = new LayoutContext(ElkGraphFactory.eINSTANCE, nodeById); + + // 2. Partition graph nodes into main flow vs handler sections List mainNodes = new ArrayList<>(); List handlerNodes = new ArrayList<>(); partitionNodes(graph, nodeById, mainNodes, handlerNodes); - // 2. Build ELK graphs — main flow + separate handler roots + // 3. Build ELK graphs — main flow + separate handler roots ElkNode rootNode = createElkRoot("root", rootDirection, 1.0, ctx); for (RouteNode rn : mainNodes) { if (!ctx.elkNodeMap.containsKey(rn.getId())) { @@ -208,34 +210,53 @@ public class ElkDiagramRenderer implements DiagramRenderer { handlerRoots.add(hr); } - // 3. Create ELK edges (filtering DO_TRY internals and cross-root edges) + // 4. Create ELK edges (filtering DO_TRY internals and cross-root edges) createElkEdges(graph, ctx); - // 4. Run layout engine + // 5. Run layout engine RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine(); engine.layout(rootNode, new BasicProgressMonitor()); for (ElkNode hr : handlerRoots) { engine.layout(hr, new BasicProgressMonitor()); } - // 5. Post-process: fix DO_TRY section ordering and widths + // 6. Post-process: fix DO_TRY section ordering and widths postProcessDoTrySections(ctx); - // 6. Extract positioned result + // 7. Extract positioned result return extractLayout(graph, rootNode, handlerRoots, ctx); } - /** Build a lookup from the flat nodes list (deduplicates nested + top-level entries). */ + /** + * Build a node lookup by walking the root tree (which preserves children for + * compound nodes). Falls back to the flat nodes list for any nodes not in the + * tree (backward compatibility when root is not set). + */ private Map buildNodeIndex(RouteGraph graph) { Map nodeById = new HashMap<>(); + if (graph.getRoot() != null) { + collectNodesRecursive(graph.getRoot(), nodeById); + } if (graph.getNodes() != null) { for (RouteNode rn : graph.getNodes()) { - nodeById.put(rn.getId(), rn); + nodeById.putIfAbsent(rn.getId(), rn); } } return nodeById; } + /** Recursively collect RouteNodes from the tree into a map (preserves children). */ + private void collectNodesRecursive(RouteNode node, Map map) { + if (node.getId() != null) { + map.put(node.getId(), node); + } + if (node.getChildren() != null) { + for (RouteNode child : node.getChildren()) { + collectNodesRecursive(child, map); + } + } + } + /** * Separate main flow nodes from handler sections by walking FLOW edges * from the graph root. Handler sections (onException, onCompletion, etc.) @@ -266,16 +287,14 @@ public class ElkDiagramRenderer implements DiagramRenderer { } Set seen = new HashSet<>(); - if (graph.getNodes() != null) { - for (RouteNode rn : graph.getNodes()) { - if (seen.contains(rn.getId())) continue; - seen.add(rn.getId()); - if (rn.getType() != null && HANDLER_SECTION_TYPES.contains(rn.getType()) - && rn.getChildren() != null && !rn.getChildren().isEmpty()) { - handlerNodes.add(rn); - } else if (mainNodeIds.isEmpty() || mainNodeIds.contains(rn.getId())) { - mainNodes.add(rn); - } + for (RouteNode rn : nodeById.values()) { + if (seen.contains(rn.getId())) continue; + seen.add(rn.getId()); + if (rn.getType() != null && HANDLER_SECTION_TYPES.contains(rn.getType()) + && rn.getChildren() != null && !rn.getChildren().isEmpty()) { + handlerNodes.add(rn); + } else if (mainNodeIds.isEmpty() || mainNodeIds.contains(rn.getId())) { + mainNodes.add(rn); } } } @@ -378,15 +397,13 @@ public class ElkDiagramRenderer implements DiagramRenderer { /** Extract positioned nodes, edges, and bounding box from the ELK layout result. */ private LayoutResult extractLayout(RouteGraph graph, ElkNode rootNode, List handlerRoots, LayoutContext ctx) { - // Extract positioned nodes + // Extract positioned nodes (uses tree-aware nodeById for compound children) List positionedNodes = new ArrayList<>(); - if (graph.getNodes() != null) { - for (RouteNode rn : graph.getNodes()) { - if (ctx.childNodeIds.contains(rn.getId())) continue; - ElkNode elkNode = ctx.elkNodeMap.get(rn.getId()); - if (elkNode == null) continue; - positionedNodes.add(extractPositionedNode(rn, elkNode, getElkRoot(elkNode), ctx)); - } + for (RouteNode rn : ctx.nodeById.values()) { + if (ctx.childNodeIds.contains(rn.getId())) continue; + ElkNode elkNode = ctx.elkNodeMap.get(rn.getId()); + if (elkNode == null) continue; + positionedNodes.add(extractPositionedNode(rn, elkNode, getElkRoot(elkNode), ctx)); } // Extract edges from main root + handler roots @@ -977,6 +994,7 @@ public class ElkDiagramRenderer implements DiagramRenderer { /** Mutable state accumulated during ELK graph construction and extraction. */ private static class LayoutContext { final ElkGraphFactory factory; + final Map nodeById; final Map elkNodeMap = new HashMap<>(); final Map nodeColors = new HashMap<>(); final Set compoundNodeIds = new HashSet<>(); @@ -985,8 +1003,9 @@ public class ElkDiagramRenderer implements DiagramRenderer { final Map> doTrySectionOrder = new LinkedHashMap<>(); final Map compoundInfos = new HashMap<>(); - LayoutContext(ElkGraphFactory factory) { + LayoutContext(ElkGraphFactory factory, Map nodeById) { this.factory = factory; + this.nodeById = nodeById; } } } diff --git a/ui/src/components/ProcessDiagram/node-colors.ts b/ui/src/components/ProcessDiagram/node-colors.ts index 78488fc1..17f6f3dd 100644 --- a/ui/src/components/ProcessDiagram/node-colors.ts +++ b/ui/src/components/ProcessDiagram/node-colors.ts @@ -65,6 +65,7 @@ const COMPOUND_TYPES = new Set([ 'ON_EXCEPTION', 'ERROR_HANDLER', 'ON_COMPLETION', 'EIP_CIRCUIT_BREAKER', '_CB_MAIN', '_CB_FALLBACK', + 'EIP_FILTER', 'EIP_IDEMPOTENT_CONSUMER', 'EIP_RECIPIENT_LIST', ]); const ERROR_COMPOUND_TYPES = new Set([