v2.2 extended analysis render update

This commit is contained in:
2026-06-15 16:37:19 +02:00
parent 71bcfe65d7
commit bd44049e5e
37 changed files with 1964 additions and 836 deletions

View File

@@ -0,0 +1,386 @@
{
"metadata" : {
"triggers" : [ {
"event" : "AUDIT_EVENT",
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
"methodName" : "preHandle",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 17
}, {
"event" : "PLACE_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
"methodName" : "place",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 16
}, {
"event" : "CANCEL_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
"methodName" : "cancel",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 21
}, {
"event" : "PAY_ORDER",
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
"methodName" : "processPayment",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 18
}, {
"event" : "SHIP_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
"methodName" : "onShippingReady",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 17
}, {
"event" : "RETURN_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
"methodName" : "onReturn",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 17
} ],
"entryPoints" : [ {
"type" : "REST",
"name" : "POST /api/enterprise/orders/place",
"className" : "click.kamil.examples.enterprise.api.OrderController",
"methodName" : "placeOrder",
"metadata" : {
"path" : "/api/enterprise/orders/place",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/enterprise/orders/{id}/cancel",
"className" : "click.kamil.examples.enterprise.api.OrderController",
"methodName" : "cancelOrder",
"metadata" : {
"path" : "/api/enterprise/orders/{id}/cancel",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
}, {
"type" : "CUSTOM",
"name" : "INTERCEPTOR: SecurityInterceptor.preHandle",
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
"methodName" : "preHandle",
"metadata" : {
"interceptorType" : "Spring MVC Interceptor"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/enterprise/payments",
"className" : "click.kamil.examples.enterprise.api.ReactivePaymentController",
"methodName" : "pay",
"metadata" : {
"path" : "/api/enterprise/payments",
"verb" : "POST"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ "RequestBody" ]
} ]
}, {
"type" : "JMS",
"name" : "JMS: shipping.queue",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
"methodName" : "onShippingReady",
"metadata" : {
"protocol" : "JMS",
"destination" : "shipping.queue"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ ]
} ]
}, {
"type" : "RABBIT",
"name" : "RABBIT: returns.queue",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
"methodName" : "onReturn",
"metadata" : {
"protocol" : "RABBIT",
"destination" : "returns.queue"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ ]
} ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/enterprise/orders/place",
"className" : "click.kamil.examples.enterprise.api.OrderController",
"methodName" : "placeOrder",
"metadata" : {
"path" : "/api/enterprise/orders/place",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.placeOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.place" ],
"triggerPoint" : {
"event" : "PLACE_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
"methodName" : "place",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 16
}
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/enterprise/orders/{id}/cancel",
"className" : "click.kamil.examples.enterprise.api.OrderController",
"methodName" : "cancelOrder",
"metadata" : {
"path" : "/api/enterprise/orders/{id}/cancel",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.cancelOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.cancel" ],
"triggerPoint" : {
"event" : "CANCEL_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
"methodName" : "cancel",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 21
}
}, {
"entryPoint" : {
"type" : "CUSTOM",
"name" : "INTERCEPTOR: SecurityInterceptor.preHandle",
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
"methodName" : "preHandle",
"metadata" : {
"interceptorType" : "Spring MVC Interceptor"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.enterprise.api.SecurityInterceptor.preHandle" ],
"triggerPoint" : {
"event" : "AUDIT_EVENT",
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
"methodName" : "preHandle",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 17
}
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/enterprise/payments",
"className" : "click.kamil.examples.enterprise.api.ReactivePaymentController",
"methodName" : "pay",
"metadata" : {
"path" : "/api/enterprise/payments",
"verb" : "POST"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ "RequestBody" ]
} ]
},
"methodChain" : [ "click.kamil.examples.enterprise.api.ReactivePaymentController.pay", "click.kamil.examples.enterprise.service.ReactivePaymentService.processPayment" ],
"triggerPoint" : {
"event" : "PAY_ORDER",
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
"methodName" : "processPayment",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 18
}
}, {
"entryPoint" : {
"type" : "JMS",
"name" : "JMS: shipping.queue",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
"methodName" : "onShippingReady",
"metadata" : {
"protocol" : "JMS",
"destination" : "shipping.queue"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ ]
} ]
},
"methodChain" : [ "click.kamil.examples.enterprise.messaging.ShippingJmsListener.onShippingReady" ],
"triggerPoint" : {
"event" : "SHIP_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
"methodName" : "onShippingReady",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 17
}
}, {
"entryPoint" : {
"type" : "RABBIT",
"name" : "RABBIT: returns.queue",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
"methodName" : "onReturn",
"metadata" : {
"protocol" : "RABBIT",
"destination" : "returns.queue"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ ]
} ]
},
"methodChain" : [ "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener.onReturn" ],
"triggerPoint" : {
"event" : "RETURN_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
"methodName" : "onReturn",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 17
}
} ],
"properties" : { }
},
"name" : "click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig",
"renderChoicesAsDiamonds" : true,
"startStates" : [ "NEW" ],
"transitions" : [ {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"NEW\"",
"fullIdentifier" : "NEW"
} ],
"targetStates" : [ {
"rawName" : "\"CHECK_AVAILABILITY\"",
"fullIdentifier" : "CHECK_AVAILABILITY"
} ],
"event" : "PLACE_ORDER",
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "CHOICE",
"sourceStates" : [ {
"rawName" : "\"CHECK_AVAILABILITY\"",
"fullIdentifier" : "CHECK_AVAILABILITY"
} ],
"targetStates" : [ {
"rawName" : "\"PENDING_PAYMENT\"",
"fullIdentifier" : "PENDING_PAYMENT"
} ],
"event" : null,
"guard" : {
"expression" : "(c) -> true",
"isLambda" : true
},
"actions" : [ ],
"order" : 0
}, {
"type" : "CHOICE",
"sourceStates" : [ {
"rawName" : "\"CHECK_AVAILABILITY\"",
"fullIdentifier" : "CHECK_AVAILABILITY"
} ],
"targetStates" : [ {
"rawName" : "\"CANCELLED\"",
"fullIdentifier" : "CANCELLED"
} ],
"event" : null,
"guard" : null,
"actions" : [ ],
"order" : 1
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"PENDING_PAYMENT\"",
"fullIdentifier" : "PENDING_PAYMENT"
} ],
"targetStates" : [ {
"rawName" : "\"PAID\"",
"fullIdentifier" : "PAID"
} ],
"event" : "PAY_ORDER",
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"PAID\"",
"fullIdentifier" : "PAID"
} ],
"targetStates" : [ {
"rawName" : "\"SHIPPED\"",
"fullIdentifier" : "SHIPPED"
} ],
"event" : "SHIP_ORDER",
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"SHIPPED\"",
"fullIdentifier" : "SHIPPED"
} ],
"targetStates" : [ {
"rawName" : "\"DELIVERED\"",
"fullIdentifier" : "DELIVERED"
} ],
"event" : "FINALIZE",
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"PAID\"",
"fullIdentifier" : "PAID"
} ],
"targetStates" : [ {
"rawName" : "\"CANCELLED\"",
"fullIdentifier" : "CANCELLED"
} ],
"event" : "CANCEL_ORDER",
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"DELIVERED\"",
"fullIdentifier" : "DELIVERED"
} ],
"targetStates" : [ {
"rawName" : "\"RETURNED\"",
"fullIdentifier" : "RETURNED"
} ],
"event" : "RETURN_ORDER",
"guard" : null,
"actions" : [ ],
"order" : null
} ],
"endStates" : [ "CANCELLED", "DELIVERED", "RETURNED" ]
}

View File

@@ -36,13 +36,28 @@
}
#sidebar {
width: 420px;
width: 480px;
min-width: 350px;
max-width: 900px;
background: var(--sidebar-bg);
border-right: 1px solid var(--border);
border-right: 2px solid var(--border);
display: flex;
flex-direction: column;
box-shadow: 10px 0 15px rgba(0,0,0,0.02);
z-index: 10;
overflow-x: hidden;
resize: horizontal;
position: relative;
}
#sidebar::after {
content: '⋮';
position: absolute;
right: 2px; top: 50%;
transform: translateY(-50%);
color: var(--border);
font-size: 1.2rem;
pointer-events: none;
}
.sidebar-header {
@@ -52,7 +67,6 @@
header h1 { font-weight: 900; font-size: 1.6rem; color: var(--text); margin: 0 0 5px 0; letter-spacing: -0.5px; }
header p { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--text-muted); margin: 0 0 15px 0; opacity: 0.7; }
/* Tabs */
.tabs {
display: flex;
padding: 0 30px;
@@ -99,6 +113,8 @@
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
position: relative;
word-break: break-all;
overflow-wrap: anywhere;
}
.ep-card:hover, .flow-card:hover {
@@ -254,7 +270,6 @@
function setupTabs() {
const flows = metadata.flows || [];
if (flows.length === 0) {
// No flows? Hide the entire tab system to keep the UI clean
document.querySelector('.tabs').style.display = 'none';
document.querySelector('.tab-content[data-tab="flows"]')?.remove();
return;
@@ -272,8 +287,18 @@
function populateSidebar() {
const list = document.getElementById('ep-list');
const entryPoints = metadata.metadata.entryPoints || [];
const machineEvents = new Set(metadata.transitions.map(t => t.event).filter(Boolean));
entryPoints.forEach(ep => {
// Quality Filter: Hide if it doesn't trigger a machine event
const chains = (metadata.metadata.callChains || []).filter(c =>
c.entryPoint.className === ep.className &&
c.entryPoint.methodName === ep.methodName &&
machineEvents.has(c.triggerPoint.event)
);
if (chains.length === 0) return;
const card = document.createElement('div');
card.className = 'ep-card';
card.innerHTML = `
@@ -292,7 +317,7 @@
const flows = metadata.flows || [];
if (flows.length === 0) {
list.innerHTML = '<p style="color:var(--text-muted); font-size:0.8rem; font-style:italic;">No business flows defined. Add a flows.json to your project to see them here.</p>';
list.innerHTML = '<p style="color:var(--text-muted); font-size:0.8rem; font-style:italic;">No business flows defined.</p>';
return;
}
@@ -311,9 +336,10 @@
function highlightEntryPoint(ep) {
clearHighlights();
const machineEvents = new Set(metadata.transitions.map(t => t.event).filter(Boolean));
const chains = metadata.metadata.callChains || [];
const relevantEvents = chains
.filter(c => c.entryPoint.className === ep.className && c.entryPoint.methodName === ep.methodName)
.filter(c => c.entryPoint.className === ep.className && c.entryPoint.methodName === ep.methodName && machineEvents.has(c.triggerPoint.event))
.map(c => c.triggerPoint.event);
if (relevantEvents.length === 0) return;
@@ -326,7 +352,7 @@
highlightEvents(flow.steps);
}
function highlightEvents(events) {
function highlightEvents(steps) {
const svg = document.querySelector('svg');
// Dim everything first
@@ -334,44 +360,66 @@
el.classList.add('dimmed');
});
events.forEach(evt => {
const normalized = normalize(evt);
const linkId = "#link_" + normalized;
steps.forEach(step => {
let linkId = "";
if (step.includes("->")) {
// Named Choice Branch: "SRC->TGT"
const parts = step.split("->");
linkId = "#link_anon_" + normalize(parts[0]) + "_" + normalize(parts[1]);
} else {
// Standard Event
linkId = "#link_" + normalize(step);
}
svg.querySelectorAll(`a[*|href="${linkId}"], a[href="${linkId}"]`).forEach(link => {
link.classList.add('active-path');
link.querySelectorAll('*').forEach(child => child.classList.remove('dimmed'));
// Surgical Selection
let prev = link.previousElementSibling;
let foundPath = false;
let foundPolygon = false;
while (prev) {
if (!foundPath && prev.tagName === 'path') {
prev.classList.remove('dimmed');
prev.classList.add('active-path');
foundPath = true;
} else if (!foundPolygon && prev.tagName === 'polygon') {
prev.classList.remove('dimmed');
prev.classList.add('active-path');
foundPolygon = true;
} else if (['rect', 'circle', 'ellipse', 'text'].includes(prev.tagName)) {
break;
}
if (foundPath && foundPolygon) break;
prev = prev.previousElementSibling;
svg.querySelectorAll('a').forEach(link => {
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
if (href === linkId) {
highlightLinkGroup(link);
}
});
});
// Un-dim all state nodes for context
svg.querySelectorAll('rect, circle, ellipse').forEach(el => {
el.classList.remove('dimmed');
let next = el.nextElementSibling;
if (next && next.tagName === 'text') next.classList.remove('dimmed');
svg.querySelectorAll('rect, circle, ellipse, polygon').forEach(el => {
const isChoice = el.tagName === 'polygon' && el.points?.numberOfItems === 4;
if (isChoice || ['rect', 'circle', 'ellipse'].includes(el.tagName)) {
el.classList.remove('dimmed');
let next = el.nextElementSibling;
while(next && next.tagName === 'text') {
next.classList.remove('dimmed');
next = next.nextElementSibling;
}
}
});
}
function highlightLinkGroup(link) {
link.classList.add('active-path');
link.querySelectorAll('*').forEach(child => child.classList.remove('dimmed'));
// Find path and polygon immediately before the <a> tag (interleaved)
let prev = link.previousElementSibling;
let foundPath = false;
let foundPolygon = false;
while (prev) {
if (prev.tagName === 'path' && !foundPath) {
prev.classList.remove('dimmed');
prev.classList.add('active-path');
foundPath = true;
} else if (prev.tagName === 'polygon' && !foundPolygon && prev.points?.numberOfItems !== 4) {
prev.classList.remove('dimmed');
prev.classList.add('active-path');
foundPolygon = true;
} else if (['rect', 'circle', 'ellipse'].includes(prev.tagName) ||
(prev.tagName === 'polygon' && prev.points?.numberOfItems === 4)) {
// Hit a state node, stop traversal
break;
}
if (foundPath && foundPolygon) break;
prev = prev.previousElementSibling;
}
}
function clearHighlights() {
document.querySelectorAll('.dimmed, .active-path').forEach(el => {
el.classList.remove('dimmed', 'active-path');
@@ -386,27 +434,28 @@
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
if (!href || !href.startsWith('#link_')) return;
const eventName = href.replace('#link_', '');
const metaTrans = transitions.find(t => normalize(t.event) === eventName);
const chains = (metadata.metadata.callChains || []).filter(c => normalize(c.triggerPoint.event) === eventName);
const linkId = href.replace('#link_', '');
const isAnon = linkId.startsWith('anon_');
let metaTrans = null;
if (isAnon) {
const parts = linkId.split('_'); // link_anon_SRC_TGT
const src = parts[2];
const tgt = parts[3];
metaTrans = transitions.find(t =>
!t.event &&
normalize(t.sourceStates[0].fullIdentifier) === src &&
normalize(t.targetStates[0].fullIdentifier) === tgt
);
} else {
metaTrans = transitions.find(t => normalize(t.event) === linkId);
}
if (metaTrans || chains.length > 0) {
if (metaTrans || isAnon) {
link.style.cursor = 'pointer';
let pathToTrigger = null;
let prev = link.previousElementSibling;
while (prev) {
if (prev.tagName === 'path') {
pathToTrigger = prev;
break;
}
if (['rect', 'circle'].includes(prev.tagName)) break;
prev = prev.previousElementSibling;
}
const setupTippy = (el) => {
tippy(el, {
content: createTooltip(eventName, metaTrans, chains),
content: createTooltip(isAnon ? "Internal logic" : linkId, metaTrans),
allowHTML: true,
theme: 'modern',
interactive: true,
@@ -415,11 +464,12 @@
offset: [0, 20]
});
};
setupTippy(link);
if (pathToTrigger) {
pathToTrigger.style.cursor = 'pointer';
setupTippy(pathToTrigger);
let path = link.previousElementSibling;
while(path && path.tagName !== 'path') path = path.previousElementSibling;
if (path) {
path.style.cursor = 'pointer';
setupTippy(path);
}
}
});
@@ -430,10 +480,12 @@
return s.replace(/[^a-zA-Z0-9]/g, '_');
}
function createTooltip(name, trans, chains) {
function createTooltip(name, trans) {
const chains = (metadata.metadata.callChains || []).filter(c => normalize(c.triggerPoint.event) === name);
let html = `<div class="tooltip-content">
<div style="font-weight:900; font-size:1.1rem; margin-bottom:12px; border-bottom:1px solid #eee; padding-bottom:8px; line-height:1.3;">
Event: <span style="color:var(--accent)">${name}</span>
${name === 'Internal logic' ? name : 'Event: <span style="color:var(--accent)">' + name + '</span>'}
</div>`;
if (trans && trans.guard) {
@@ -446,37 +498,30 @@
chains.forEach(c => {
html += `<div style="margin-bottom:20px">
<div style="font-weight:700; font-size:0.85rem; color:var(--text);">${c.entryPoint.name}</div>
${renderParameters(c.entryPoint.parameters)}
<div style="margin-left:10px; border-left:2px solid #e2e8f0; padding-left:12px; margin-top:10px">
${c.methodChain.map((m, i) => `<div style="font-size:0.7rem; color:#475569; margin-top:4px; font-family:'JetBrains Mono', monospace;"><b>${i+1}</b> ${m}</div>`).join('')}
</div>
</div>`;
});
}
html += `</div>`;
return html;
}
function renderParameters(params) {
if (!params || params.length === 0) return "";
let html = `<div style="font-size:0.65rem; font-weight:700; color:#94a3b8; margin-top:8px; margin-bottom:4px; text-transform:uppercase;">Input Data</div>`;
html += `<div class="payload-box">`;
params.forEach(p => {
const annotation = (p.annotations || []).find(a => a.endsWith("RequestBody") || a.endsWith("PathVariable") || a.endsWith("RequestParam"));
const cleanAnn = annotation ? "@" + annotation.substring(annotation.lastIndexOf('.') + 1) : "";
html += `<div class="payload-param">
<span style="color:#f43f5e; font-size:0.65rem; font-weight:bold;">${cleanAnn}</span>
<span style="color:#38bdf8;">${p.type}</span>
<span style="color:#a3e635;">${p.name}</span>
</div>`;
});
html += `</div>`;
return html;
}