From 7e968dc06bd60ee40e8b79803c60cea224d2fc3a Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:34:40 +0200 Subject: [PATCH] fix: use root tree for compound node detection instead of flat nodes list The agent now sends shallow copies (without children) in the flat nodes list. Build nodeById map by walking graph.getRoot() tree which preserves children, falling back to flat list via putIfAbsent for compatibility. Also adds EIP_FILTER, EIP_IDEMPOTENT_CONSUMER, EIP_RECIPIENT_LIST as new compound container types per updated DIAGRAMS.md. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/diagram/ElkDiagramRenderer.java | 79 ++++++++++++------- .../components/ProcessDiagram/node-colors.ts | 1 + 2 files changed, 50 insertions(+), 30 deletions(-) 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([