feat: render EIP_CIRCUIT_BREAKER as compound container with main/fallback lanes
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 58s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 41s

Follow the DO_TRY pattern: virtual _CB_MAIN wrapper for main path children,
onFallback rendered as _CB_FALLBACK section with purple dashed border.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-29 17:40:28 +02:00
parent 9eb2c2692b
commit e8039f9cc4
3 changed files with 149 additions and 3 deletions

View File

@@ -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<RouteNode> mainChildren = new ArrayList<>();
RouteNode fallbackNode = null;
for (RouteNode child : rn.getChildren()) {
if ("onFallback".equals(child.getLabel())) {
fallbackNode = child;
} else {
mainChildren.add(child);
}
}
List<String> 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<PositionedNode> 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<PositionedNode> 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());

View File

@@ -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 (
<g transform={`translate(${x}, ${y})`}>
{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 (
<g data-node-id={node.id} transform={`translate(${x}, ${y})`}>
<rect x={0} y={0} width={w} height={h} rx={CORNER_RADIUS}
fill={fallbackColor} fillOpacity={0.06} />
<rect x={0} y={0} width={w} height={h} rx={CORNER_RADIUS}
fill="none" stroke={fallbackColor} strokeWidth={1} strokeOpacity={0.4} strokeDasharray="4 2" />
<text x={8} y={12} fill={fallbackColor} fontSize={10} fontWeight={600}>
fallback
</text>
{renderInternalEdges(internalEdges, absX, absY, executionOverlay)}
{renderChildren(node, absX, absY, childProps)}
</g>
);
}
// 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'

View File

@@ -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([