1342 lines
46 KiB
HTML
1342 lines
46 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Cameleer3 — Route Diagram: split-and-multicast (Light)</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
background: #f7f5f2;
|
|
color: #57534e;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 32px 16px;
|
|
}
|
|
|
|
header {
|
|
max-width: 960px;
|
|
width: 100%;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
header h1 {
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
color: #1c1917;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
header .meta {
|
|
font-size: 13px;
|
|
color: #a8a29e;
|
|
display: flex;
|
|
gap: 20px;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
header .meta span { display: inline-flex; align-items: center; gap: 5px; }
|
|
header .meta .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
header .meta .dot.green { background: #047857; }
|
|
|
|
/* Toolbar */
|
|
.toolbar {
|
|
max-width: 960px;
|
|
width: 100%;
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn {
|
|
padding: 7px 16px;
|
|
border-radius: 6px;
|
|
border: 1px solid #d4cfc8;
|
|
background: #ffffff;
|
|
color: #57534e;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
transition: background 0.15s, border-color 0.15s;
|
|
user-select: none;
|
|
}
|
|
|
|
.btn:hover { background: #f3f1ee; border-color: #d4cfc8; color: #1c1917; }
|
|
|
|
.btn.active {
|
|
background: rgba(180, 83, 9, 0.07);
|
|
border-color: rgba(180, 83, 9, 0.3);
|
|
color: #b45309;
|
|
}
|
|
|
|
.btn .kbd {
|
|
font-size: 10px;
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
background: #f3f1ee;
|
|
border: 1px solid #d4cfc8;
|
|
color: #a8a29e;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.exec-badge {
|
|
font-size: 12px;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
font-weight: 500;
|
|
display: none;
|
|
}
|
|
|
|
.exec-badge.visible { display: inline-flex; align-items: center; gap: 5px; }
|
|
|
|
.exec-badge.completed { background: rgba(4, 120, 87, 0.08); color: #047857; border: 1px solid rgba(4, 120, 87, 0.3); }
|
|
.exec-badge.failed { background: rgba(190, 18, 60, 0.08); color: #be123c; border: 1px solid rgba(190, 18, 60, 0.3); }
|
|
|
|
.canvas-wrapper {
|
|
background: #ffffff;
|
|
border: 1px solid #e4e0db;
|
|
border-radius: 14px;
|
|
overflow: auto;
|
|
max-width: 960px;
|
|
width: 100%;
|
|
position: relative;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
|
}
|
|
|
|
svg text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; }
|
|
|
|
/* Hover glow on node groups */
|
|
.node-group { cursor: default; transition: opacity 0.3s; }
|
|
.node-group:hover .node-rect { filter: drop-shadow(0 0 8px var(--glow-color, rgba(29, 78, 216, 0.3))); }
|
|
|
|
/* Execution overlay states */
|
|
.overlay-active .node-group.dimmed { opacity: 0.3; }
|
|
.overlay-active .node-group.dimmed:hover { opacity: 0.5; }
|
|
.overlay-active .edge-path.dimmed { opacity: 0.15; }
|
|
|
|
.overlay-active .node-group.executed .node-rect {
|
|
filter: drop-shadow(0 0 8px var(--glow-color, rgba(4, 120, 87, 0.4)));
|
|
}
|
|
|
|
.overlay-active .edge-path.executed {
|
|
stroke-width: 2.5 !important;
|
|
filter: drop-shadow(0 0 4px var(--edge-glow));
|
|
}
|
|
|
|
/* Animated flow particles */
|
|
@keyframes flowParticle {
|
|
0% { offset-distance: 0%; opacity: 0; }
|
|
5% { opacity: 1; }
|
|
95% { opacity: 1; }
|
|
100% { offset-distance: 100%; opacity: 0; }
|
|
}
|
|
|
|
.flow-particle {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: #047857;
|
|
position: absolute;
|
|
offset-rotate: 0deg;
|
|
animation: flowParticle 1.5s linear infinite;
|
|
box-shadow: 0 0 8px #04785766, 0 0 16px #04785733;
|
|
display: none;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.overlay-active .flow-particle { display: block; }
|
|
|
|
/* Duration badge on nodes */
|
|
.duration-badge { display: none; }
|
|
.overlay-active .duration-badge { display: block; }
|
|
|
|
/* Iteration count badge on split nodes */
|
|
.iter-badge { display: none; }
|
|
.overlay-active .iter-badge { display: block; }
|
|
|
|
/* Execution detail panel */
|
|
.exec-panel {
|
|
max-width: 960px;
|
|
width: 100%;
|
|
margin-top: 16px;
|
|
background: #ffffff;
|
|
border: 1px solid #e4e0db;
|
|
border-radius: 14px;
|
|
overflow: hidden;
|
|
display: none;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
|
}
|
|
|
|
.exec-panel.visible { display: block; }
|
|
|
|
.exec-panel-header {
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid #e4e0db;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.exec-panel-header h3 {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #1c1917;
|
|
}
|
|
|
|
.exec-panel-header .timing {
|
|
font-size: 12px;
|
|
color: #a8a29e;
|
|
}
|
|
|
|
.exec-steps {
|
|
display: flex;
|
|
overflow-x: auto;
|
|
padding: 16px;
|
|
gap: 0;
|
|
}
|
|
|
|
.exec-step {
|
|
flex-shrink: 0;
|
|
width: 180px;
|
|
position: relative;
|
|
}
|
|
|
|
.exec-step::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 18px;
|
|
right: -1px;
|
|
width: 100%;
|
|
height: 2px;
|
|
background: rgba(4, 120, 87, 0.3);
|
|
z-index: 0;
|
|
}
|
|
|
|
.exec-step:last-child::after { display: none; }
|
|
|
|
.step-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: #047857;
|
|
border: 2px solid #ffffff;
|
|
position: relative;
|
|
z-index: 1;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.step-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: #1c1917;
|
|
margin-bottom: 2px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 160px;
|
|
}
|
|
|
|
.step-type {
|
|
font-size: 10px;
|
|
color: #a8a29e;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.step-duration {
|
|
font-size: 10px;
|
|
color: #047857;
|
|
font-weight: 500;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.step-body {
|
|
font-size: 10px;
|
|
color: #a8a29e;
|
|
margin-top: 4px;
|
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
line-height: 1.4;
|
|
max-width: 160px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.step-body .arrow { color: #d4cfc8; }
|
|
|
|
/* Iteration selector tabs */
|
|
.iter-tabs {
|
|
display: flex;
|
|
gap: 0;
|
|
padding: 0 16px;
|
|
border-bottom: 1px solid #e4e0db;
|
|
}
|
|
|
|
.iter-tab {
|
|
padding: 8px 16px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: #a8a29e;
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
transition: color 0.15s, border-color 0.15s;
|
|
user-select: none;
|
|
}
|
|
|
|
.iter-tab:hover { color: #57534e; }
|
|
|
|
.iter-tab.active {
|
|
color: #047857;
|
|
border-bottom-color: #047857;
|
|
}
|
|
|
|
.iter-tab .iter-idx {
|
|
font-size: 10px;
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
background: #f3f1ee;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
.iter-tab.active .iter-idx {
|
|
background: rgba(4, 120, 87, 0.08);
|
|
color: #047857;
|
|
}
|
|
|
|
/* Legend */
|
|
.legend {
|
|
max-width: 960px;
|
|
width: 100%;
|
|
margin-top: 16px;
|
|
background: #ffffff;
|
|
border: 1px solid #e4e0db;
|
|
border-radius: 14px;
|
|
padding: 16px 20px;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 28px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
|
}
|
|
|
|
.legend-section h3 {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.8px;
|
|
color: #a8a29e;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.legend-items { display: flex; flex-wrap: wrap; gap: 14px; }
|
|
|
|
.legend-item {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
font-size: 12px;
|
|
color: #57534e;
|
|
}
|
|
|
|
.legend-swatch {
|
|
width: 28px;
|
|
height: 16px;
|
|
border-radius: 4px;
|
|
border: 1.5px solid;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.legend-line {
|
|
width: 28px;
|
|
height: 2px;
|
|
flex-shrink: 0;
|
|
border-radius: 1px;
|
|
}
|
|
|
|
.legend-line.dashed {
|
|
height: 0;
|
|
border-top: 2px dashed;
|
|
}
|
|
|
|
.legend-swatch.exec-glow {
|
|
background: rgba(4, 120, 87, 0.08);
|
|
border-color: rgba(4, 120, 87, 0.5);
|
|
box-shadow: 0 0 6px rgba(4, 120, 87, 0.25);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<h1>split-and-multicast</h1>
|
|
<div class="meta">
|
|
<span><span class="dot green"></span> Route active</span>
|
|
<span>From: <code style="color:#1d4ed8">direct:splitAndMulticast</code></span>
|
|
<span>Nodes: 8</span>
|
|
<span>Split: 3 iterations</span>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="toolbar">
|
|
<button class="btn" id="toggleOverlay" onclick="toggleExecution()">
|
|
<span id="overlayIcon">\u25b6</span> Show Execution
|
|
<span class="kbd">E</span>
|
|
</button>
|
|
<span class="exec-badge" id="execBadge">
|
|
<span class="dot green"></span>
|
|
COMPLETED in 85ms (3 iterations)
|
|
</span>
|
|
<span style="flex:1"></span>
|
|
<span style="font-size:12px;color:#d4cfc8" id="execMeta"></span>
|
|
</div>
|
|
|
|
<div class="canvas-wrapper" id="canvas-wrapper">
|
|
<svg id="diagram" xmlns="http://www.w3.org/2000/svg"></svg>
|
|
</div>
|
|
|
|
<!-- Execution detail panel -->
|
|
<div class="exec-panel" id="execPanel">
|
|
<div class="exec-panel-header">
|
|
<h3>Execution Trace</h3>
|
|
<span class="timing" id="execTiming"></span>
|
|
</div>
|
|
<div class="iter-tabs" id="iterTabs"></div>
|
|
<div class="exec-steps" id="execSteps"></div>
|
|
</div>
|
|
|
|
<div class="legend">
|
|
<div class="legend-section">
|
|
<h3>Node Types</h3>
|
|
<div class="legend-items">
|
|
<span class="legend-item"><span class="legend-swatch" style="background:rgba(29,78,216,0.06);border-color:rgba(29,78,216,0.35)"></span>Endpoint</span>
|
|
<span class="legend-item"><span class="legend-swatch" style="background:rgba(124,58,237,0.06);border-color:rgba(124,58,237,0.35)"></span>EIP Pattern</span>
|
|
<span class="legend-item"><span class="legend-swatch" style="background:rgba(4,120,87,0.06);border-color:rgba(4,120,87,0.35)"></span>Processor</span>
|
|
<span class="legend-item"><span class="legend-swatch" style="background:rgba(190,18,60,0.05);border-color:rgba(190,18,60,0.3)"></span>Error</span>
|
|
<span class="legend-item"><span class="legend-swatch" style="background:transparent;border-color:rgba(14,116,144,0.35);border-style:dashed"></span>Cross-route</span>
|
|
</div>
|
|
</div>
|
|
<div class="legend-section">
|
|
<h3>Edge Types</h3>
|
|
<div class="legend-items">
|
|
<span class="legend-item"><span class="legend-line" style="background:#c4bfb8"></span>Flow</span>
|
|
<span class="legend-item"><span class="legend-line" style="background:#b45309"></span>Branch</span>
|
|
<span class="legend-item"><span class="legend-line dashed" style="border-color:#be123c"></span>Error</span>
|
|
<span class="legend-item"><span class="legend-line dashed" style="border-color:#0e7490"></span>Cross-route</span>
|
|
</div>
|
|
</div>
|
|
<div class="legend-section">
|
|
<h3>Execution Overlay</h3>
|
|
<div class="legend-items">
|
|
<span class="legend-item"><span class="legend-swatch exec-glow"></span>Executed node</span>
|
|
<span class="legend-item"><span class="legend-line" style="background:#047857"></span>Execution path</span>
|
|
<span class="legend-item"><span style="width:6px;height:6px;border-radius:50%;background:#047857;box-shadow:0 0 6px rgba(4,120,87,0.5);flex-shrink:0"></span>Flow particle</span>
|
|
<span class="legend-item"><span class="legend-swatch" style="background:#f3f1ee;border-color:#e4e0db;opacity:0.35"></span>Not executed</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// -- Route diagram data ----------------------------------------------------------
|
|
// Split-and-multicast route: receives an order, logs it, splits the items
|
|
// array, processes each item (log + setHeader + to mock:warehouse), then
|
|
// aggregates and sends to direct:audit.
|
|
const routeData = {
|
|
routeId: "split-and-multicast",
|
|
from: "direct:splitAndMulticast",
|
|
nodes: [
|
|
{ id: "n1", type: "DIRECT", label: "direct:splitAndMulticast", endpointUri: "direct:splitAndMulticast" },
|
|
{ id: "n2", type: "LOG", label: "Processing order items" },
|
|
{ id: "n3", type: "EIP_SPLIT", label: "split", expression: "${body.items}" },
|
|
{ id: "n4", type: "LOG", label: "Item: ${body.name}" },
|
|
{ id: "n5", type: "SET_HEADER", label: "setHeader: X-ItemId" },
|
|
{ id: "n6", type: "TO", label: "mock:warehouse", endpointUri: "mock:warehouse" },
|
|
{ id: "n7", type: "LOG", label: "All items processed" },
|
|
{ id: "n8", type: "TO", label: "direct:audit", endpointUri: "direct:audit", crossRoute: true }
|
|
],
|
|
edges: [
|
|
{ source: "n1", target: "n2", edgeType: "FLOW" },
|
|
{ source: "n2", target: "n3", edgeType: "FLOW" },
|
|
{ source: "n3", target: "n4", edgeType: "FLOW" },
|
|
{ source: "n4", target: "n5", edgeType: "FLOW" },
|
|
{ source: "n5", target: "n6", edgeType: "FLOW" },
|
|
{ source: "n3", target: "n7", edgeType: "FLOW" },
|
|
{ source: "n7", target: "n8", edgeType: "FLOW" },
|
|
{ source: "n8", target: "direct:audit", edgeType: "CROSS_ROUTE", label: "\u2192 audit" }
|
|
]
|
|
};
|
|
|
|
// -- Execution data (3-item split) ------------------------------------------------
|
|
// Simulates a real CAMELEER3_EXECUTION JSON for an order with 3 items
|
|
// flowing through the split-and-multicast route.
|
|
const executionData = {
|
|
routeId: "split-and-multicast",
|
|
exchangeId: "ID-cameleer-dev-1741443600-0-2",
|
|
correlationId: "b4f9d3e2-8c5e-4f0b-a7d4-3e2g1f9b6d58",
|
|
status: "COMPLETED",
|
|
startTime: "2026-03-08T14:00:01.200Z",
|
|
endTime: "2026-03-08T14:00:01.285Z",
|
|
durationMs: 85,
|
|
inputSnapshot: {
|
|
body: '{"orderId":"ORD-2048","items":[{"name":"Widget A","qty":2},{"name":"Gadget B","qty":1},{"name":"Gizmo C","qty":5}]}',
|
|
headers: { "Content-Type": "application/json", "X-Cameleer-CorrelationId": "b4f9d3e2-8c5e-4f0b-a7d4-3e2g1f9b6d58" }
|
|
},
|
|
outputSnapshot: {
|
|
body: '{"orderId":"ORD-2048","items":[...],"processed":true}',
|
|
headers: { "Content-Type": "application/json", "X-Cameleer-CorrelationId": "b4f9d3e2-8c5e-4f0b-a7d4-3e2g1f9b6d58" }
|
|
},
|
|
// Processors before the split, then the split itself (with iterations as children)
|
|
processors: [
|
|
{
|
|
nodeId: "n1", processorType: "from", label: "direct:splitAndMulticast",
|
|
status: "COMPLETED", durationMs: 1,
|
|
inputBody: '{"orderId":"ORD-2048","items":[...3 items]}',
|
|
outputBody: '{"orderId":"ORD-2048","items":[...3 items]}'
|
|
},
|
|
{
|
|
nodeId: "n2", processorType: "log", label: "Processing order items",
|
|
status: "COMPLETED", durationMs: 2,
|
|
inputBody: '{"orderId":"ORD-2048","items":[...3 items]}',
|
|
outputBody: '{"orderId":"ORD-2048","items":[...3 items]}'
|
|
},
|
|
{
|
|
nodeId: "n3", processorType: "split", label: "split: ${body.items} \u00d7 3",
|
|
status: "COMPLETED", durationMs: 62,
|
|
splitSize: 3,
|
|
inputBody: '{"orderId":"ORD-2048","items":[...3 items]}',
|
|
outputBody: '{"orderId":"ORD-2048","items":[...3 items]}',
|
|
// Each iteration shows processors within that split sub-exchange
|
|
iterations: [
|
|
{
|
|
splitIndex: 0, splitSize: 3, status: "COMPLETED", durationMs: 18,
|
|
inputBody: '{"name":"Widget A","qty":2}',
|
|
processors: [
|
|
{ nodeId: "n4", processorType: "log", label: "Item: Widget A", status: "COMPLETED", durationMs: 2, inputBody: '{"name":"Widget A","qty":2}', outputBody: '{"name":"Widget A","qty":2}' },
|
|
{ nodeId: "n5", processorType: "setHeader", label: "X-ItemId = WIDGET-A", status: "COMPLETED", durationMs: 1, inputBody: '{"name":"Widget A","qty":2}', outputBody: '{"name":"Widget A","qty":2}', inputHeaders: {}, outputHeaders: { "X-ItemId": "WIDGET-A" } },
|
|
{ nodeId: "n6", processorType: "to", label: "\u2192 mock:warehouse", status: "COMPLETED", durationMs: 12, inputBody: '{"name":"Widget A","qty":2}', outputBody: '{"name":"Widget A","qty":2,"shipped":true}', endpointUri: "mock:warehouse" }
|
|
]
|
|
},
|
|
{
|
|
splitIndex: 1, splitSize: 3, status: "COMPLETED", durationMs: 20,
|
|
inputBody: '{"name":"Gadget B","qty":1}',
|
|
processors: [
|
|
{ nodeId: "n4", processorType: "log", label: "Item: Gadget B", status: "COMPLETED", durationMs: 2, inputBody: '{"name":"Gadget B","qty":1}', outputBody: '{"name":"Gadget B","qty":1}' },
|
|
{ nodeId: "n5", processorType: "setHeader", label: "X-ItemId = GADGET-B", status: "COMPLETED", durationMs: 1, inputBody: '{"name":"Gadget B","qty":1}', outputBody: '{"name":"Gadget B","qty":1}', inputHeaders: {}, outputHeaders: { "X-ItemId": "GADGET-B" } },
|
|
{ nodeId: "n6", processorType: "to", label: "\u2192 mock:warehouse", status: "COMPLETED", durationMs: 15, inputBody: '{"name":"Gadget B","qty":1}', outputBody: '{"name":"Gadget B","qty":1,"shipped":true}', endpointUri: "mock:warehouse" }
|
|
]
|
|
},
|
|
{
|
|
splitIndex: 2, splitSize: 3, status: "COMPLETED", durationMs: 16,
|
|
inputBody: '{"name":"Gizmo C","qty":5}',
|
|
processors: [
|
|
{ nodeId: "n4", processorType: "log", label: "Item: Gizmo C", status: "COMPLETED", durationMs: 1, inputBody: '{"name":"Gizmo C","qty":5}', outputBody: '{"name":"Gizmo C","qty":5}' },
|
|
{ nodeId: "n5", processorType: "setHeader", label: "X-ItemId = GIZMO-C", status: "COMPLETED", durationMs: 1, inputBody: '{"name":"Gizmo C","qty":5}', outputBody: '{"name":"Gizmo C","qty":5}', inputHeaders: {}, outputHeaders: { "X-ItemId": "GIZMO-C" } },
|
|
{ nodeId: "n6", processorType: "to", label: "\u2192 mock:warehouse", status: "COMPLETED", durationMs: 11, inputBody: '{"name":"Gizmo C","qty":5}', outputBody: '{"name":"Gizmo C","qty":5,"shipped":true}', endpointUri: "mock:warehouse" }
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
nodeId: "n7", processorType: "log", label: "All items processed",
|
|
status: "COMPLETED", durationMs: 2,
|
|
inputBody: '{"orderId":"ORD-2048","items":[...3 items]}',
|
|
outputBody: '{"orderId":"ORD-2048","items":[...3 items]}'
|
|
},
|
|
{
|
|
nodeId: "n8", processorType: "to", label: "\u2192 direct:audit",
|
|
status: "COMPLETED", durationMs: 15,
|
|
inputBody: '{"orderId":"ORD-2048","items":[...3 items]}',
|
|
outputBody: '{"orderId":"ORD-2048","items":[...3 items],"audited":true}',
|
|
endpointUri: "direct:audit"
|
|
}
|
|
]
|
|
};
|
|
|
|
// -- Build executed node/edge sets (handles split iterations) ---------------------
|
|
const executedNodes = new Set();
|
|
const executedEdges = new Set();
|
|
// Track per-node execution count for iteration badges
|
|
const nodeExecCounts = {};
|
|
// Aggregate duration per node (sum across iterations)
|
|
const nodeAggregateDuration = {};
|
|
|
|
function collectExecutedFromProcessors(procs) {
|
|
for (let i = 0; i < procs.length; i++) {
|
|
const p = procs[i];
|
|
executedNodes.add(p.nodeId);
|
|
nodeExecCounts[p.nodeId] = (nodeExecCounts[p.nodeId] || 0) + 1;
|
|
nodeAggregateDuration[p.nodeId] = (nodeAggregateDuration[p.nodeId] || 0) + p.durationMs;
|
|
if (i > 0) {
|
|
executedEdges.add(procs[i - 1].nodeId + "->" + p.nodeId);
|
|
}
|
|
// Recurse into split iterations
|
|
if (p.iterations) {
|
|
for (const iter of p.iterations) {
|
|
collectExecutedFromProcessors(iter.processors);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Connect sequential processors at the top level
|
|
for (let i = 0; i < executionData.processors.length - 1; i++) {
|
|
executedEdges.add(executionData.processors[i].nodeId + "->" + executionData.processors[i + 1].nodeId);
|
|
}
|
|
// Also connect split node to its children and back to post-split
|
|
const splitProc = executionData.processors.find(p => p.iterations);
|
|
if (splitProc && splitProc.iterations.length > 0) {
|
|
const firstIterProc = splitProc.iterations[0].processors[0];
|
|
if (firstIterProc) executedEdges.add(splitProc.nodeId + "->" + firstIterProc.nodeId);
|
|
// Connect last iteration processor back to post-split
|
|
const splitIdx = executionData.processors.indexOf(splitProc);
|
|
const postSplit = executionData.processors[splitIdx + 1];
|
|
if (postSplit) {
|
|
for (const iter of splitProc.iterations) {
|
|
const lastP = iter.processors[iter.processors.length - 1];
|
|
if (lastP) executedEdges.add(lastP.nodeId + "->" + postSplit.nodeId);
|
|
}
|
|
executedEdges.add(splitProc.nodeId + "->" + postSplit.nodeId);
|
|
}
|
|
}
|
|
|
|
collectExecutedFromProcessors(executionData.processors);
|
|
|
|
// Current iteration index for detail panel (null = "All")
|
|
let currentIteration = null;
|
|
|
|
// -- Node style config (light theme) ---------------------------------------------
|
|
const STYLES = {
|
|
endpoint: { fill: "rgba(29, 78, 216, 0.06)", stroke: "rgba(29, 78, 216, 0.35)", text: "#1d4ed8", glow: "rgba(29, 78, 216, 0.3)", icon: "\u25c9" },
|
|
eip: { fill: "rgba(124, 58, 237, 0.06)", stroke: "rgba(124, 58, 237, 0.35)", text: "#7c3aed", glow: "rgba(124, 58, 237, 0.3)", icon: "\u25c6" },
|
|
processor: { fill: "rgba(4, 120, 87, 0.06)", stroke: "rgba(4, 120, 87, 0.35)", text: "#047857", glow: "rgba(4, 120, 87, 0.3)", icon: "\u25b8" },
|
|
error: { fill: "rgba(190, 18, 60, 0.05)", stroke: "rgba(190, 18, 60, 0.3)", text: "#be123c", glow: "rgba(190, 18, 60, 0.3)", icon: "\u26a0" },
|
|
crossRoute:{ fill: "rgba(14, 116, 144, 0.06)", stroke: "rgba(14, 116, 144, 0.35)", text: "#0e7490", glow: "rgba(14, 116, 144, 0.3)", icon: "\u2197" }
|
|
};
|
|
|
|
function nodeCategory(node) {
|
|
if (node.crossRoute) return "crossRoute";
|
|
const t = node.type;
|
|
if (["DIRECT","ENDPOINT","SEDA"].includes(t)) return "endpoint";
|
|
if (t.startsWith("EIP_")) return "eip";
|
|
if (["ERROR_HANDLER","ON_EXCEPTION","TRY_CATCH","DO_TRY","DO_CATCH","DO_FINALLY"].includes(t)) return "error";
|
|
if (t === "TO" || t === "TO_DYNAMIC") return "endpoint";
|
|
return "processor";
|
|
}
|
|
|
|
const EDGE_COLORS = {
|
|
FLOW: "#c4bfb8",
|
|
BRANCH: "#b45309",
|
|
ERROR: "#be123c",
|
|
CROSS_ROUTE: "#0e7490"
|
|
};
|
|
|
|
// -- Layout constants -------------------------------------------------------------
|
|
const NODE_W = 200;
|
|
const NODE_H = 40;
|
|
const V_GAP = 50;
|
|
const H_GAP = 40;
|
|
const PAD = 60;
|
|
|
|
// -- Build node lookup ------------------------------------------------------------
|
|
const nodeMap = {};
|
|
routeData.nodes.forEach(n => nodeMap[n.id] = n);
|
|
|
|
// -- Tree layout for split route --------------------------------------------------
|
|
// Linear chain: n1 -> n2 -> n3(split) -> n4 -> n5 -> n6 (split body)
|
|
// Then: n3 -> n7 -> n8 (post-split)
|
|
// Layout: vertical chain with split body indented to the right
|
|
|
|
const positions = {};
|
|
|
|
function layoutSplitRoute() {
|
|
const centerX = PAD + NODE_W / 2 + 60;
|
|
|
|
// Pre-split: n1, n2 centered
|
|
positions["n1"] = { x: centerX - NODE_W / 2, y: PAD };
|
|
positions["n2"] = { x: centerX - NODE_W / 2, y: PAD + (NODE_H + V_GAP) };
|
|
|
|
// Split node: n3
|
|
positions["n3"] = { x: centerX - NODE_W / 2, y: PAD + 2 * (NODE_H + V_GAP) };
|
|
|
|
// Split body (indented right): n4, n5, n6
|
|
const splitBodyX = centerX + NODE_W / 2 + H_GAP / 2;
|
|
const splitBodyStartY = PAD + 2 * (NODE_H + V_GAP) + 10;
|
|
positions["n4"] = { x: splitBodyX, y: splitBodyStartY + 0 * (NODE_H + V_GAP) };
|
|
positions["n5"] = { x: splitBodyX, y: splitBodyStartY + 1 * (NODE_H + V_GAP) };
|
|
positions["n6"] = { x: splitBodyX, y: splitBodyStartY + 2 * (NODE_H + V_GAP) };
|
|
|
|
// Post-split: n7, n8 centered (below split body)
|
|
const postSplitY = splitBodyStartY + 3 * (NODE_H + V_GAP);
|
|
positions["n7"] = { x: centerX - NODE_W / 2, y: postSplitY };
|
|
positions["n8"] = { x: centerX - NODE_W / 2, y: postSplitY + (NODE_H + V_GAP) };
|
|
}
|
|
|
|
layoutSplitRoute();
|
|
|
|
// -- Compute SVG dimensions -------------------------------------------------------
|
|
let maxX = 0, maxY = 0;
|
|
Object.values(positions).forEach(p => {
|
|
if (p.x + NODE_W > maxX) maxX = p.x + NODE_W;
|
|
if (p.y + NODE_H > maxY) maxY = p.y + NODE_H;
|
|
});
|
|
const svgW = maxX + PAD;
|
|
const svgH = maxY + PAD + 30;
|
|
|
|
// -- SVG rendering ----------------------------------------------------------------
|
|
const NS = "http://www.w3.org/2000/svg";
|
|
const svg = document.getElementById("diagram");
|
|
svg.setAttribute("width", svgW);
|
|
svg.setAttribute("height", svgH);
|
|
svg.setAttribute("viewBox", `0 0 ${svgW} ${svgH}`);
|
|
|
|
// Defs: arrow markers + execution glow filter
|
|
const defs = document.createElementNS(NS, "defs");
|
|
|
|
function createMarker(id, color) {
|
|
const marker = document.createElementNS(NS, "marker");
|
|
marker.setAttribute("id", id);
|
|
marker.setAttribute("viewBox", "0 0 10 7");
|
|
marker.setAttribute("refX", "10");
|
|
marker.setAttribute("refY", "3.5");
|
|
marker.setAttribute("markerWidth", "8");
|
|
marker.setAttribute("markerHeight", "6");
|
|
marker.setAttribute("orient", "auto");
|
|
const poly = document.createElementNS(NS, "polygon");
|
|
poly.setAttribute("points", "0 0, 10 3.5, 0 7");
|
|
poly.setAttribute("fill", color);
|
|
marker.appendChild(poly);
|
|
defs.appendChild(marker);
|
|
}
|
|
|
|
Object.entries(EDGE_COLORS).forEach(([type, color]) => {
|
|
createMarker("arrow-" + type, color);
|
|
});
|
|
// Green execution arrow
|
|
createMarker("arrow-exec", "#047857");
|
|
|
|
svg.appendChild(defs);
|
|
|
|
// -- Draw edges -------------------------------------------------------------------
|
|
const edgeGroup = document.createElementNS(NS, "g");
|
|
edgeGroup.setAttribute("class", "edges");
|
|
|
|
// Store edge elements and paths for overlay
|
|
const edgeElements = [];
|
|
|
|
routeData.edges.forEach(edge => {
|
|
const sp = positions[edge.source];
|
|
const tp = positions[edge.target];
|
|
if (!sp || !tp) return;
|
|
|
|
const color = EDGE_COLORS[edge.edgeType] || EDGE_COLORS.FLOW;
|
|
const isDashed = edge.edgeType === "ERROR" || edge.edgeType === "CROSS_ROUTE";
|
|
const edgeKey = edge.source + "->" + edge.target;
|
|
const isExecuted = executedEdges.has(edgeKey);
|
|
|
|
const sx = sp.x + NODE_W / 2;
|
|
const sy = sp.y + NODE_H;
|
|
const tx = tp.x + NODE_W / 2;
|
|
const ty = tp.y;
|
|
|
|
const path = document.createElementNS(NS, "path");
|
|
path.setAttribute("class", "edge-path");
|
|
path.dataset.edgeKey = edgeKey;
|
|
|
|
let d;
|
|
if (edge.edgeType === "CROSS_ROUTE") {
|
|
const sx2 = sp.x + NODE_W / 2;
|
|
const sy2 = sp.y + NODE_H;
|
|
const tx2 = tp.x + NODE_W / 2;
|
|
const ty2 = tp.y + NODE_H;
|
|
const midY = Math.max(sy2, ty2) + 25;
|
|
d = `M ${sx2} ${sy2} C ${sx2} ${midY}, ${tx2} ${midY}, ${tx2} ${ty2}`;
|
|
} else if (Math.abs(sx - tx) < 2) {
|
|
d = `M ${sx} ${sy} L ${tx} ${ty}`;
|
|
} else {
|
|
const midY = sy + (ty - sy) * 0.5;
|
|
d = `M ${sx} ${sy} C ${sx} ${midY}, ${tx} ${midY}, ${tx} ${ty}`;
|
|
}
|
|
|
|
path.setAttribute("d", d);
|
|
path.setAttribute("fill", "none");
|
|
path.setAttribute("stroke", color);
|
|
path.setAttribute("stroke-width", edge.edgeType === "BRANCH" ? "2" : "1.5");
|
|
if (isDashed) path.setAttribute("stroke-dasharray", "6 3");
|
|
path.setAttribute("marker-end", `url(#arrow-${edge.edgeType})`);
|
|
path.style.setProperty("--edge-glow", color);
|
|
path.style.transition = "opacity 0.3s, stroke 0.3s, stroke-width 0.3s";
|
|
edgeGroup.appendChild(path);
|
|
|
|
edgeElements.push({ el: path, edge, d, isExecuted });
|
|
|
|
// Edge label
|
|
if (edge.label && edge.edgeType !== "CROSS_ROUTE") {
|
|
const lx = (sx + tx) / 2 + 8;
|
|
const ly = (sy + ty) / 2;
|
|
const text = document.createElementNS(NS, "text");
|
|
text.setAttribute("x", lx);
|
|
text.setAttribute("y", ly);
|
|
text.setAttribute("fill", color);
|
|
text.setAttribute("font-size", "11");
|
|
text.setAttribute("font-weight", "500");
|
|
text.setAttribute("class", "edge-label");
|
|
text.dataset.edgeKey = edgeKey;
|
|
text.style.transition = "opacity 0.3s";
|
|
text.textContent = edge.label;
|
|
edgeGroup.appendChild(text);
|
|
}
|
|
});
|
|
|
|
svg.appendChild(edgeGroup);
|
|
|
|
// -- Execution path overlay edges (drawn on top, hidden by default) ---------------
|
|
const execEdgeGroup = document.createElementNS(NS, "g");
|
|
execEdgeGroup.setAttribute("class", "exec-edges");
|
|
execEdgeGroup.style.display = "none";
|
|
|
|
edgeElements.filter(e => e.isExecuted).forEach(({ d, edge }) => {
|
|
const path = document.createElementNS(NS, "path");
|
|
path.setAttribute("d", d);
|
|
path.setAttribute("fill", "none");
|
|
path.setAttribute("stroke", "#047857");
|
|
path.setAttribute("stroke-width", "2.5");
|
|
path.setAttribute("marker-end", "url(#arrow-exec)");
|
|
path.style.setProperty("--edge-glow", "#047857");
|
|
path.style.filter = "drop-shadow(0 0 4px rgba(4, 120, 87, 0.4))";
|
|
execEdgeGroup.appendChild(path);
|
|
});
|
|
|
|
svg.appendChild(execEdgeGroup);
|
|
|
|
// -- Draw nodes -------------------------------------------------------------------
|
|
const nodeGroup = document.createElementNS(NS, "g");
|
|
nodeGroup.setAttribute("class", "nodes");
|
|
|
|
const nodeElements = {};
|
|
|
|
routeData.nodes.forEach(node => {
|
|
const pos = positions[node.id];
|
|
if (!pos) return;
|
|
|
|
const cat = nodeCategory(node);
|
|
const style = STYLES[cat];
|
|
const isExec = executedNodes.has(node.id);
|
|
|
|
const g = document.createElementNS(NS, "g");
|
|
g.setAttribute("class", "node-group");
|
|
g.dataset.nodeId = node.id;
|
|
g.style.setProperty("--glow-color", style.glow);
|
|
|
|
// Tooltip -- enhanced with execution data when available
|
|
const title = document.createElementNS(NS, "title");
|
|
let tip = `${node.type}: ${node.label}`;
|
|
if (node.expression) tip += `\nExpression: ${node.expression}`;
|
|
if (node.endpointUri) tip += `\nURI: ${node.endpointUri}`;
|
|
// Find execution data (top-level processor or aggregated from iterations)
|
|
const execProc = executionData.processors.find(p => p.nodeId === node.id);
|
|
const execCount = nodeExecCounts[node.id] || 0;
|
|
const aggDuration = nodeAggregateDuration[node.id] || 0;
|
|
if (execProc) {
|
|
tip += `\n\u2500\u2500\u2500 Execution \u2500\u2500\u2500`;
|
|
tip += `\nStatus: ${execProc.status}`;
|
|
tip += `\nDuration: ${execProc.durationMs}ms`;
|
|
if (execProc.splitSize) tip += `\nSplit: ${execProc.splitSize} iterations, ${aggDuration}ms total`;
|
|
if (execProc.inputBody) tip += `\nInput: ${execProc.inputBody}`;
|
|
if (execProc.outputBody) tip += `\nOutput: ${execProc.outputBody}`;
|
|
} else if (execCount > 1) {
|
|
tip += `\n\u2500\u2500\u2500 Execution \u2500\u2500\u2500`;
|
|
tip += `\nExecuted ${execCount}\u00d7 across split iterations`;
|
|
tip += `\nAggregate duration: ${aggDuration}ms`;
|
|
}
|
|
title.textContent = tip;
|
|
g.appendChild(title);
|
|
|
|
// Rect
|
|
const rect = document.createElementNS(NS, "rect");
|
|
rect.setAttribute("class", "node-rect");
|
|
rect.setAttribute("x", pos.x);
|
|
rect.setAttribute("y", pos.y);
|
|
rect.setAttribute("width", NODE_W);
|
|
rect.setAttribute("height", NODE_H);
|
|
rect.setAttribute("rx", "8");
|
|
rect.setAttribute("fill", style.fill);
|
|
rect.setAttribute("stroke", style.stroke);
|
|
rect.setAttribute("stroke-width", "1.5");
|
|
if (node.crossRoute) rect.setAttribute("stroke-dasharray", "5 3");
|
|
g.appendChild(rect);
|
|
|
|
// Icon
|
|
const icon = document.createElementNS(NS, "text");
|
|
icon.setAttribute("x", pos.x + 14);
|
|
icon.setAttribute("y", pos.y + NODE_H / 2 + 1);
|
|
icon.setAttribute("fill", style.text);
|
|
icon.setAttribute("font-size", "14");
|
|
icon.setAttribute("dominant-baseline", "central");
|
|
icon.textContent = style.icon;
|
|
g.appendChild(icon);
|
|
|
|
// Label
|
|
let labelText = node.label;
|
|
if (labelText.length > 24) labelText = labelText.substring(0, 22) + "\u2026";
|
|
|
|
const label = document.createElementNS(NS, "text");
|
|
label.setAttribute("x", pos.x + 30);
|
|
label.setAttribute("y", pos.y + NODE_H / 2 + 1);
|
|
label.setAttribute("fill", "#1c1917");
|
|
label.setAttribute("font-size", "12.5");
|
|
label.setAttribute("font-weight", "500");
|
|
label.setAttribute("dominant-baseline", "central");
|
|
label.textContent = labelText;
|
|
g.appendChild(label);
|
|
|
|
// Type badge below node
|
|
const badge = document.createElementNS(NS, "text");
|
|
badge.setAttribute("x", pos.x + NODE_W / 2);
|
|
badge.setAttribute("y", pos.y + NODE_H + 14);
|
|
badge.setAttribute("fill", style.text);
|
|
badge.setAttribute("font-size", "9.5");
|
|
badge.setAttribute("text-anchor", "middle");
|
|
badge.textContent = node.type.replace("EIP_", "");
|
|
g.appendChild(badge);
|
|
|
|
// Duration badge (only visible during execution overlay)
|
|
const showDuration = execProc || execCount > 0;
|
|
if (showDuration) {
|
|
const displayDuration = execProc ? execProc.durationMs : aggDuration;
|
|
const dbg = document.createElementNS(NS, "g");
|
|
dbg.setAttribute("class", "duration-badge");
|
|
|
|
const pillW = 42;
|
|
const pillH = 16;
|
|
const px = pos.x + NODE_W - pillW - 4;
|
|
const py = pos.y - pillH - 3;
|
|
|
|
const pill = document.createElementNS(NS, "rect");
|
|
pill.setAttribute("x", px);
|
|
pill.setAttribute("y", py);
|
|
pill.setAttribute("width", pillW);
|
|
pill.setAttribute("height", pillH);
|
|
pill.setAttribute("rx", "8");
|
|
pill.setAttribute("fill", "#ffffff");
|
|
pill.setAttribute("stroke", "#e4e0db");
|
|
pill.setAttribute("stroke-width", "1");
|
|
dbg.appendChild(pill);
|
|
|
|
const dtext = document.createElementNS(NS, "text");
|
|
dtext.setAttribute("x", px + pillW / 2);
|
|
dtext.setAttribute("y", py + pillH / 2 + 1);
|
|
// Color based on speed: green < 10ms, amber 10-50ms, rose > 50ms
|
|
let durColor = "#047857";
|
|
if (displayDuration > 50) durColor = "#be123c";
|
|
else if (displayDuration >= 10) durColor = "#b45309";
|
|
dtext.setAttribute("fill", durColor);
|
|
dtext.setAttribute("font-size", "10");
|
|
dtext.setAttribute("font-weight", "600");
|
|
dtext.setAttribute("text-anchor", "middle");
|
|
dtext.setAttribute("dominant-baseline", "central");
|
|
dtext.setAttribute("font-variant-numeric", "tabular-nums");
|
|
dtext.textContent = `${displayDuration}ms`;
|
|
dbg.appendChild(dtext);
|
|
|
|
g.appendChild(dbg);
|
|
}
|
|
|
|
// Iteration count badge for split nodes (e.g., "x3")
|
|
if (execProc && execProc.splitSize) {
|
|
const ibg = document.createElementNS(NS, "g");
|
|
ibg.setAttribute("class", "iter-badge");
|
|
|
|
const ibW = 28;
|
|
const ibH = 16;
|
|
const ibx = pos.x + 4;
|
|
const iby = pos.y - ibH - 3;
|
|
|
|
const ibPill = document.createElementNS(NS, "rect");
|
|
ibPill.setAttribute("x", ibx);
|
|
ibPill.setAttribute("y", iby);
|
|
ibPill.setAttribute("width", ibW);
|
|
ibPill.setAttribute("height", ibH);
|
|
ibPill.setAttribute("rx", "8");
|
|
ibPill.setAttribute("fill", "#047857");
|
|
ibPill.setAttribute("stroke", "none");
|
|
ibg.appendChild(ibPill);
|
|
|
|
const ibText = document.createElementNS(NS, "text");
|
|
ibText.setAttribute("x", ibx + ibW / 2);
|
|
ibText.setAttribute("y", iby + ibH / 2 + 1);
|
|
ibText.setAttribute("fill", "#ffffff");
|
|
ibText.setAttribute("font-size", "10");
|
|
ibText.setAttribute("font-weight", "700");
|
|
ibText.setAttribute("text-anchor", "middle");
|
|
ibText.setAttribute("dominant-baseline", "central");
|
|
ibText.textContent = `\u00d7${execProc.splitSize}`;
|
|
ibg.appendChild(ibText);
|
|
|
|
g.appendChild(ibg);
|
|
}
|
|
|
|
// Iteration count badge for nodes executed multiple times (inside split body)
|
|
if (!execProc && execCount > 1) {
|
|
const ibg = document.createElementNS(NS, "g");
|
|
ibg.setAttribute("class", "iter-badge");
|
|
|
|
const ibW = 28;
|
|
const ibH = 16;
|
|
const ibx = pos.x + 4;
|
|
const iby = pos.y - ibH - 3;
|
|
|
|
const ibPill = document.createElementNS(NS, "rect");
|
|
ibPill.setAttribute("x", ibx);
|
|
ibPill.setAttribute("y", iby);
|
|
ibPill.setAttribute("width", ibW);
|
|
ibPill.setAttribute("height", ibH);
|
|
ibPill.setAttribute("rx", "8");
|
|
ibPill.setAttribute("fill", "#047857");
|
|
ibPill.setAttribute("stroke", "none");
|
|
ibg.appendChild(ibPill);
|
|
|
|
const ibText = document.createElementNS(NS, "text");
|
|
ibText.setAttribute("x", ibx + ibW / 2);
|
|
ibText.setAttribute("y", iby + ibH / 2 + 1);
|
|
ibText.setAttribute("fill", "#ffffff");
|
|
ibText.setAttribute("font-size", "10");
|
|
ibText.setAttribute("font-weight", "700");
|
|
ibText.setAttribute("text-anchor", "middle");
|
|
ibText.setAttribute("dominant-baseline", "central");
|
|
ibText.textContent = `\u00d7${execCount}`;
|
|
ibg.appendChild(ibText);
|
|
|
|
g.appendChild(ibg);
|
|
}
|
|
|
|
// Cross-route external route pill
|
|
if (node.crossRoute) {
|
|
const pillW = 130;
|
|
const pillH = 18;
|
|
const px = pos.x + (NODE_W - pillW) / 2;
|
|
const py = pos.y + NODE_H + 22;
|
|
|
|
const pill = document.createElementNS(NS, "rect");
|
|
pill.setAttribute("x", px);
|
|
pill.setAttribute("y", py);
|
|
pill.setAttribute("width", pillW);
|
|
pill.setAttribute("height", pillH);
|
|
pill.setAttribute("rx", "9");
|
|
pill.setAttribute("fill", "rgba(14, 116, 144, 0.06)");
|
|
pill.setAttribute("stroke", "rgba(14, 116, 144, 0.35)");
|
|
pill.setAttribute("stroke-width", "1");
|
|
pill.setAttribute("stroke-dasharray", "3 2");
|
|
g.appendChild(pill);
|
|
|
|
const pillText = document.createElementNS(NS, "text");
|
|
pillText.setAttribute("x", px + pillW / 2);
|
|
pillText.setAttribute("y", py + pillH / 2 + 1);
|
|
pillText.setAttribute("fill", "#0e7490");
|
|
pillText.setAttribute("font-size", "9.5");
|
|
pillText.setAttribute("text-anchor", "middle");
|
|
pillText.setAttribute("dominant-baseline", "central");
|
|
pillText.textContent = "\u2197 external route";
|
|
g.appendChild(pillText);
|
|
}
|
|
|
|
nodeElements[node.id] = g;
|
|
nodeGroup.appendChild(g);
|
|
});
|
|
|
|
svg.appendChild(nodeGroup);
|
|
|
|
// -- Execution sequence number overlay (hidden by default) ------------------------
|
|
const seqGroup = document.createElementNS(NS, "g");
|
|
seqGroup.setAttribute("class", "seq-numbers");
|
|
seqGroup.style.display = "none";
|
|
|
|
// Flatten top-level processor sequence (skip iteration children for seq numbers)
|
|
const topLevelSeq = executionData.processors.filter(p => !p.iterations || true);
|
|
const seenSeqNodes = new Set();
|
|
|
|
topLevelSeq.forEach((proc, i) => {
|
|
if (seenSeqNodes.has(proc.nodeId)) return;
|
|
seenSeqNodes.add(proc.nodeId);
|
|
const pos = positions[proc.nodeId];
|
|
if (!pos) return;
|
|
|
|
const cx = pos.x + 10;
|
|
const cy = pos.y + 10;
|
|
|
|
const circle = document.createElementNS(NS, "circle");
|
|
circle.setAttribute("cx", cx);
|
|
circle.setAttribute("cy", cy);
|
|
circle.setAttribute("r", "9");
|
|
circle.setAttribute("fill", "#047857");
|
|
circle.setAttribute("stroke", "#ffffff");
|
|
circle.setAttribute("stroke-width", "2");
|
|
seqGroup.appendChild(circle);
|
|
|
|
const num = document.createElementNS(NS, "text");
|
|
num.setAttribute("x", cx);
|
|
num.setAttribute("y", cy + 1);
|
|
num.setAttribute("fill", "#ffffff");
|
|
num.setAttribute("font-size", "10");
|
|
num.setAttribute("font-weight", "700");
|
|
num.setAttribute("text-anchor", "middle");
|
|
num.setAttribute("dominant-baseline", "central");
|
|
num.textContent = i + 1;
|
|
seqGroup.appendChild(num);
|
|
});
|
|
|
|
// Add sequence numbers for split body nodes (using sub-numbering like 3a, 3b, 3c)
|
|
if (splitProc && splitProc.iterations.length > 0) {
|
|
const splitIdx = executionData.processors.indexOf(splitProc);
|
|
const firstIter = splitProc.iterations[0];
|
|
firstIter.processors.forEach((proc, j) => {
|
|
if (seenSeqNodes.has(proc.nodeId)) return;
|
|
seenSeqNodes.add(proc.nodeId);
|
|
const pos = positions[proc.nodeId];
|
|
if (!pos) return;
|
|
|
|
const cx = pos.x + 10;
|
|
const cy = pos.y + 10;
|
|
|
|
const circle = document.createElementNS(NS, "circle");
|
|
circle.setAttribute("cx", cx);
|
|
circle.setAttribute("cy", cy);
|
|
circle.setAttribute("r", "9");
|
|
circle.setAttribute("fill", "#047857");
|
|
circle.setAttribute("stroke", "#ffffff");
|
|
circle.setAttribute("stroke-width", "2");
|
|
seqGroup.appendChild(circle);
|
|
|
|
const num = document.createElementNS(NS, "text");
|
|
num.setAttribute("x", cx);
|
|
num.setAttribute("y", cy + 1);
|
|
num.setAttribute("fill", "#ffffff");
|
|
num.setAttribute("font-size", "9");
|
|
num.setAttribute("font-weight", "700");
|
|
num.setAttribute("text-anchor", "middle");
|
|
num.setAttribute("dominant-baseline", "central");
|
|
num.textContent = `${splitIdx + 1}${String.fromCharCode(97 + j)}`;
|
|
seqGroup.appendChild(num);
|
|
});
|
|
}
|
|
|
|
svg.appendChild(seqGroup);
|
|
|
|
// -- Build execution detail panel with iteration tabs -----------------------------
|
|
function buildExecPanel() {
|
|
const tabsEl = document.getElementById("iterTabs");
|
|
const stepsEl = document.getElementById("execSteps");
|
|
const timingEl = document.getElementById("execTiming");
|
|
|
|
timingEl.textContent = `Exchange: ${executionData.exchangeId} \u00b7 ${executionData.startTime} \u00b7 ${executionData.durationMs}ms total`;
|
|
|
|
// Build iteration tabs if there's a split processor
|
|
if (splitProc && splitProc.iterations.length > 0) {
|
|
// "All" tab
|
|
const allTab = document.createElement("div");
|
|
allTab.className = "iter-tab active";
|
|
allTab.textContent = "All Steps";
|
|
allTab.onclick = () => selectIteration(null);
|
|
tabsEl.appendChild(allTab);
|
|
|
|
splitProc.iterations.forEach((iter, i) => {
|
|
const tab = document.createElement("div");
|
|
tab.className = "iter-tab";
|
|
tab.innerHTML = `Iteration <span class="iter-idx">${i}</span>`;
|
|
tab.title = `Split item: ${iter.inputBody}`;
|
|
tab.onclick = () => selectIteration(i);
|
|
tabsEl.appendChild(tab);
|
|
});
|
|
}
|
|
|
|
renderSteps(null);
|
|
}
|
|
|
|
function selectIteration(iterIdx) {
|
|
currentIteration = iterIdx;
|
|
// Update tab active state
|
|
document.querySelectorAll(".iter-tab").forEach((tab, i) => {
|
|
tab.classList.toggle("active", iterIdx === null ? i === 0 : i === iterIdx + 1);
|
|
});
|
|
renderSteps(iterIdx);
|
|
}
|
|
|
|
function renderSteps(iterIdx) {
|
|
const stepsEl = document.getElementById("execSteps");
|
|
stepsEl.innerHTML = "";
|
|
|
|
let procs;
|
|
if (iterIdx === null) {
|
|
// Show all top-level processors (split shows as aggregate)
|
|
procs = executionData.processors;
|
|
} else {
|
|
// Show pre-split procs + specific iteration procs + post-split procs
|
|
const splitIndex = executionData.processors.indexOf(splitProc);
|
|
const preSplit = executionData.processors.slice(0, splitIndex);
|
|
const iterProcs = splitProc.iterations[iterIdx].processors;
|
|
const postSplit = executionData.processors.slice(splitIndex + 1);
|
|
procs = [...preSplit, ...iterProcs, ...postSplit];
|
|
}
|
|
|
|
procs.forEach((proc, i) => {
|
|
const step = document.createElement("div");
|
|
step.className = "exec-step";
|
|
|
|
const dot = document.createElement("div");
|
|
dot.className = "step-dot";
|
|
if (proc.status === "FAILED") dot.style.background = "#be123c";
|
|
step.appendChild(dot);
|
|
|
|
const lbl = document.createElement("div");
|
|
lbl.className = "step-label";
|
|
let labelPrefix = `${i + 1}. `;
|
|
if (proc.splitIndex !== undefined) labelPrefix = `${i + 1}. [${proc.splitIndex}] `;
|
|
lbl.textContent = labelPrefix + proc.label;
|
|
lbl.title = proc.label;
|
|
step.appendChild(lbl);
|
|
|
|
const type = document.createElement("div");
|
|
type.className = "step-type";
|
|
let typeText = proc.processorType;
|
|
if (proc.iterations) typeText += ` (\u00d7${proc.splitSize})`;
|
|
type.textContent = typeText;
|
|
step.appendChild(type);
|
|
|
|
const dur = document.createElement("div");
|
|
dur.className = "step-duration";
|
|
dur.textContent = `${proc.durationMs}ms`;
|
|
step.appendChild(dur);
|
|
|
|
// Show body details
|
|
if (proc.inputBody && proc.outputBody) {
|
|
const bodyDiv = document.createElement("div");
|
|
bodyDiv.className = "step-body";
|
|
|
|
if (proc.outputHeaders && proc.inputHeaders) {
|
|
const newKeys = Object.keys(proc.outputHeaders).filter(k => !(k in proc.inputHeaders));
|
|
if (newKeys.length > 0) {
|
|
bodyDiv.textContent = `+${newKeys.map(k => `${k}: ${proc.outputHeaders[k]}`).join(", ")}`;
|
|
bodyDiv.style.color = "#047857";
|
|
}
|
|
}
|
|
|
|
if (proc.processorType === "to" && proc.inputBody !== proc.outputBody) {
|
|
const outObj = proc.outputBody;
|
|
bodyDiv.innerHTML = `<span class="arrow">\u2192</span> body changed`;
|
|
bodyDiv.style.color = "#047857";
|
|
}
|
|
|
|
if (proc.iterations) {
|
|
bodyDiv.textContent = `${proc.splitSize} iterations, ${proc.iterations.reduce((s, it) => s + it.durationMs, 0)}ms total`;
|
|
bodyDiv.style.color = "#1d4ed8";
|
|
}
|
|
|
|
if (bodyDiv.textContent || bodyDiv.innerHTML) step.appendChild(bodyDiv);
|
|
}
|
|
|
|
stepsEl.appendChild(step);
|
|
});
|
|
}
|
|
|
|
buildExecPanel();
|
|
|
|
// -- Toggle execution overlay -----------------------------------------------------
|
|
let overlayActive = false;
|
|
|
|
function toggleExecution() {
|
|
overlayActive = !overlayActive;
|
|
const svgEl = document.getElementById("diagram");
|
|
const btn = document.getElementById("toggleOverlay");
|
|
const badge = document.getElementById("execBadge");
|
|
const panel = document.getElementById("execPanel");
|
|
const meta = document.getElementById("execMeta");
|
|
const iconSpan = document.getElementById("overlayIcon");
|
|
|
|
if (overlayActive) {
|
|
// Activate overlay
|
|
svgEl.classList.add("overlay-active");
|
|
btn.classList.add("active");
|
|
btn.innerHTML = '<span id="overlayIcon">\u25a0</span> Hide Execution <span class="kbd">E</span>';
|
|
badge.className = "exec-badge visible completed";
|
|
panel.classList.add("visible");
|
|
meta.textContent = `Correlation: ${executionData.correlationId}`;
|
|
|
|
// Dim non-executed nodes and edges
|
|
routeData.nodes.forEach(node => {
|
|
const el = nodeElements[node.id];
|
|
if (!el) return;
|
|
if (executedNodes.has(node.id)) {
|
|
el.classList.add("executed");
|
|
el.classList.remove("dimmed");
|
|
} else {
|
|
el.classList.add("dimmed");
|
|
el.classList.remove("executed");
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll(".edge-path").forEach(el => {
|
|
const key = el.dataset.edgeKey;
|
|
if (executedEdges.has(key)) {
|
|
el.classList.add("executed");
|
|
el.classList.remove("dimmed");
|
|
} else {
|
|
el.classList.add("dimmed");
|
|
el.classList.remove("executed");
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll(".edge-label").forEach(el => {
|
|
const key = el.dataset.edgeKey;
|
|
el.style.opacity = executedEdges.has(key) ? "1" : "0.15";
|
|
});
|
|
|
|
// Show execution edges + sequence numbers
|
|
execEdgeGroup.style.display = "";
|
|
seqGroup.style.display = "";
|
|
|
|
} else {
|
|
// Deactivate overlay
|
|
svgEl.classList.remove("overlay-active");
|
|
btn.classList.remove("active");
|
|
btn.innerHTML = '<span id="overlayIcon">\u25b6</span> Show Execution <span class="kbd">E</span>';
|
|
badge.className = "exec-badge";
|
|
panel.classList.remove("visible");
|
|
meta.textContent = "";
|
|
|
|
// Remove dim/executed classes
|
|
routeData.nodes.forEach(node => {
|
|
const el = nodeElements[node.id];
|
|
if (!el) return;
|
|
el.classList.remove("executed", "dimmed");
|
|
});
|
|
|
|
document.querySelectorAll(".edge-path").forEach(el => {
|
|
el.classList.remove("executed", "dimmed");
|
|
});
|
|
|
|
document.querySelectorAll(".edge-label").forEach(el => {
|
|
el.style.opacity = "";
|
|
});
|
|
|
|
execEdgeGroup.style.display = "none";
|
|
seqGroup.style.display = "none";
|
|
}
|
|
}
|
|
|
|
// Keyboard shortcut: E to toggle
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "e" || e.key === "E") {
|
|
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
|
|
toggleExecution();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|