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 76b5067f..38e4e54e 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,7 @@ 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.ON_COMPLETION, NodeType.EIP_CIRCUIT_BREAKER ); /** Top-level handler types laid out in their own separate ELK graph. */ @@ -621,6 +621,84 @@ public class ElkDiagramRenderer implements DiagramRenderer { sectionOrder.add(handler.getId()); } + ctx.doTrySectionOrder.put(rn.getId(), sectionOrder); + } else if (isCompound && rn.getType() == NodeType.EIP_CIRCUIT_BREAKER) { + // CIRCUIT_BREAKER: vertical container with _CB_MAIN for main path + // and onFallback as a compound section below (like DO_TRY pattern) + ctx.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.4); + elkNode.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.3); + elkNode.setProperty(CoreOptions.PADDING, + new org.eclipse.elk.core.math.ElkPadding(COMPOUND_TOP_PADDING, + COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING)); + + // Separate main path children from onFallback child + List mainChildren = new ArrayList<>(); + RouteNode fallbackNode = null; + for (RouteNode child : rn.getChildren()) { + if ("onFallback".equals(child.getLabel())) { + fallbackNode = child; + } else { + mainChildren.add(child); + } + } + + List sectionOrder = new ArrayList<>(); + + // Virtual _CB_MAIN wrapper for main path (horizontal flow) + if (!mainChildren.isEmpty()) { + String wrapperId = rn.getId() + "._cb_main"; + ElkNode wrapper = ctx.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)); + ctx.compoundNodeIds.add(wrapperId); + ctx.elkNodeMap.put(wrapperId, wrapper); + sectionOrder.add(wrapperId); + + for (RouteNode child : mainChildren) { + ctx.childNodeIds.add(child.getId()); + createElkNodeRecursive(child, wrapper, ctx); + } + } + + // onFallback as compound section containing its children + if (fallbackNode != null) { + ctx.childNodeIds.add(fallbackNode.getId()); + ElkNode fallbackElk = ctx.factory.createElkNode(); + fallbackElk.setIdentifier(fallbackNode.getId()); + fallbackElk.setParent(elkNode); + fallbackElk.setWidth(200); + fallbackElk.setHeight(40); + fallbackElk.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered"); + fallbackElk.setProperty(CoreOptions.DIRECTION, Direction.RIGHT); + fallbackElk.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5); + fallbackElk.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5); + fallbackElk.setProperty(CoreOptions.PADDING, + new org.eclipse.elk.core.math.ElkPadding(18, 8, 8, 8)); + ctx.compoundNodeIds.add(fallbackNode.getId()); + ctx.elkNodeMap.put(fallbackNode.getId(), fallbackElk); + sectionOrder.add(fallbackNode.getId()); + + if (fallbackNode.getChildren() != null) { + for (RouteNode child : fallbackNode.getChildren()) { + ctx.childNodeIds.add(child.getId()); + createElkNodeRecursive(child, fallbackElk, ctx); + } + } + } + ctx.doTrySectionOrder.put(rn.getId(), sectionOrder); } else if (isCompound) { ctx.compoundNodeIds.add(rn.getId()); @@ -690,6 +768,55 @@ public class ElkDiagramRenderer implements DiagramRenderer { children.add(extractPositionedNode(handler, childElk, rootNode, ctx)); } } + } else if (rn.getType() == NodeType.EIP_CIRCUIT_BREAKER) { + // CIRCUIT_BREAKER: extract _CB_MAIN wrapper, then onFallback section + String mainWrapperId = rn.getId() + "._cb_main"; + ElkNode mainWrapperElk = ctx.elkNodeMap.get(mainWrapperId); + if (mainWrapperElk != null) { + List wrapperChildren = new ArrayList<>(); + for (RouteNode child : rn.getChildren()) { + if (!"onFallback".equals(child.getLabel())) { + ElkNode childElk = ctx.elkNodeMap.get(child.getId()); + if (childElk != null) { + wrapperChildren.add(extractPositionedNode(child, childElk, rootNode, ctx)); + } + } + } + children.add(new PositionedNode( + mainWrapperId, "", "_CB_MAIN", + getAbsoluteX(mainWrapperElk, rootNode), + getAbsoluteY(mainWrapperElk, rootNode), + mainWrapperElk.getWidth(), mainWrapperElk.getHeight(), + wrapperChildren, null)); + ctx.compoundInfos.put(mainWrapperId, new CompoundInfo(mainWrapperId, Color.WHITE)); + } + // onFallback section with its children, type overridden to _CB_FALLBACK + for (RouteNode child : rn.getChildren()) { + if ("onFallback".equals(child.getLabel())) { + ElkNode childElk = ctx.elkNodeMap.get(child.getId()); + if (childElk != null) { + List fallbackChildren = new ArrayList<>(); + if (child.getChildren() != null) { + for (RouteNode fc : child.getChildren()) { + ElkNode fcElk = ctx.elkNodeMap.get(fc.getId()); + if (fcElk != null) { + fallbackChildren.add(extractPositionedNode(fc, fcElk, rootNode, ctx)); + } + } + } + children.add(new PositionedNode( + child.getId(), + child.getLabel() != null ? child.getLabel() : "", + "_CB_FALLBACK", + getAbsoluteX(childElk, rootNode), + getAbsoluteY(childElk, rootNode), + childElk.getWidth(), childElk.getHeight(), + fallbackChildren, null)); + ctx.compoundInfos.put(child.getId(), + new CompoundInfo(child.getId(), colorForType(rn.getType()))); + } + } + } } else { for (RouteNode child : rn.getChildren()) { ElkNode childElk = ctx.elkNodeMap.get(child.getId()); diff --git a/ui/src/components/ProcessDiagram/CompoundNode.tsx b/ui/src/components/ProcessDiagram/CompoundNode.tsx index aa7e0a30..00850621 100644 --- a/ui/src/components/ProcessDiagram/CompoundNode.tsx +++ b/ui/src/components/ProcessDiagram/CompoundNode.tsx @@ -65,8 +65,8 @@ export function CompoundNode({ onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave, }; - // _TRY_BODY: transparent wrapper — no header, no border, just layout - if (node.type === '_TRY_BODY') { + // _TRY_BODY / _CB_MAIN: transparent wrapper — no header, no border, just layout + if (node.type === '_TRY_BODY' || node.type === '_CB_MAIN') { return ( {renderInternalEdges(internalEdges, absX, absY, executionOverlay)} @@ -75,6 +75,24 @@ export function CompoundNode({ ); } + // _CB_FALLBACK: section styling with EIP purple + if (node.type === '_CB_FALLBACK') { + const fallbackColor = '#7C3AED'; // EIP purple + return ( + + + + + fallback + + {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' diff --git a/ui/src/components/ProcessDiagram/node-colors.ts b/ui/src/components/ProcessDiagram/node-colors.ts index f14c1db6..78488fc1 100644 --- a/ui/src/components/ProcessDiagram/node-colors.ts +++ b/ui/src/components/ProcessDiagram/node-colors.ts @@ -64,6 +64,7 @@ const COMPOUND_TYPES = new Set([ 'EIP_LOOP', 'EIP_MULTICAST', 'EIP_AGGREGATE', 'ON_EXCEPTION', 'ERROR_HANDLER', 'ON_COMPLETION', + 'EIP_CIRCUIT_BREAKER', '_CB_MAIN', '_CB_FALLBACK', ]); const ERROR_COMPOUND_TYPES = new Set([