feat: render EIP_CIRCUIT_BREAKER as compound container with main/fallback lanes
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:
@@ -109,7 +109,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
NodeType.DO_TRY, NodeType.DO_CATCH, NodeType.DO_FINALLY,
|
NodeType.DO_TRY, NodeType.DO_CATCH, NodeType.DO_FINALLY,
|
||||||
NodeType.EIP_LOOP, NodeType.EIP_MULTICAST,
|
NodeType.EIP_LOOP, NodeType.EIP_MULTICAST,
|
||||||
NodeType.EIP_AGGREGATE, NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER,
|
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. */
|
/** 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());
|
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);
|
ctx.doTrySectionOrder.put(rn.getId(), sectionOrder);
|
||||||
} else if (isCompound) {
|
} else if (isCompound) {
|
||||||
ctx.compoundNodeIds.add(rn.getId());
|
ctx.compoundNodeIds.add(rn.getId());
|
||||||
@@ -690,6 +768,55 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
children.add(extractPositionedNode(handler, childElk, rootNode, ctx));
|
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 {
|
} else {
|
||||||
for (RouteNode child : rn.getChildren()) {
|
for (RouteNode child : rn.getChildren()) {
|
||||||
ElkNode childElk = ctx.elkNodeMap.get(child.getId());
|
ElkNode childElk = ctx.elkNodeMap.get(child.getId());
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ export function CompoundNode({
|
|||||||
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
||||||
};
|
};
|
||||||
|
|
||||||
// _TRY_BODY: transparent wrapper — no header, no border, just layout
|
// _TRY_BODY / _CB_MAIN: transparent wrapper — no header, no border, just layout
|
||||||
if (node.type === '_TRY_BODY') {
|
if (node.type === '_TRY_BODY' || node.type === '_CB_MAIN') {
|
||||||
return (
|
return (
|
||||||
<g transform={`translate(${x}, ${y})`}>
|
<g transform={`translate(${x}, ${y})`}>
|
||||||
{renderInternalEdges(internalEdges, absX, absY, executionOverlay)}
|
{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)
|
// DO_CATCH / DO_FINALLY: section-like styling (tinted bg, thin border, label)
|
||||||
if (node.type === 'DO_CATCH' || node.type === 'DO_FINALLY') {
|
if (node.type === 'DO_CATCH' || node.type === 'DO_FINALLY') {
|
||||||
const sectionLabel = node.type === 'DO_CATCH'
|
const sectionLabel = node.type === 'DO_CATCH'
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const COMPOUND_TYPES = new Set([
|
|||||||
'EIP_LOOP', 'EIP_MULTICAST', 'EIP_AGGREGATE',
|
'EIP_LOOP', 'EIP_MULTICAST', 'EIP_AGGREGATE',
|
||||||
'ON_EXCEPTION', 'ERROR_HANDLER',
|
'ON_EXCEPTION', 'ERROR_HANDLER',
|
||||||
'ON_COMPLETION',
|
'ON_COMPLETION',
|
||||||
|
'EIP_CIRCUIT_BREAKER', '_CB_MAIN', '_CB_FALLBACK',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ERROR_COMPOUND_TYPES = new Set([
|
const ERROR_COMPOUND_TYPES = new Set([
|
||||||
|
|||||||
Reference in New Issue
Block a user