v2 extended analysis render update

This commit is contained in:
2026-06-14 21:32:13 +02:00
parent 2b6b727de6
commit 38d708f85b
10 changed files with 254 additions and 2299 deletions

View File

@@ -41,16 +41,55 @@
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 30px;
overflow-y: auto;
box-shadow: 10px 0 15px rgba(0,0,0,0.02);
z-index: 10;
}
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 35px 0; opacity: 0.7; }
.sidebar-header {
padding: 30px 30px 10px;
}
.ep-card {
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;
border-bottom: 1px solid var(--border);
gap: 20px;
}
.tab {
padding: 10px 0;
font-size: 0.85rem;
font-weight: 600;
color: var(--text-muted);
cursor: pointer;
position: relative;
transition: color 0.2s;
}
.tab:hover { color: var(--text); }
.tab.active { color: var(--primary); }
.tab.active::after {
content: '';
position: absolute;
bottom: -1px; left: 0; right: 0;
height: 2px;
background: var(--primary);
}
.tab-content {
flex-grow: 1;
padding: 25px 30px;
overflow-y: auto;
display: none;
}
.tab-content.active { display: block; }
.ep-card, .flow-card {
padding: 18px;
background: #fff;
border: 1px solid var(--border);
@@ -59,9 +98,10 @@
cursor: pointer;
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;
}
.ep-card:hover {
.ep-card:hover, .flow-card:hover {
border-color: var(--primary);
transform: translateX(5px);
box-shadow: 0 15px 30px -10px rgba(59, 130, 246, 0.15);
@@ -70,7 +110,8 @@
.ep-type {
font-size: 0.6rem;
font-weight: 900;
padding: 3px 8px; border-radius: 6px;
padding: 3px 8px;
border-radius: 6px;
display: inline-block;
margin-bottom: 10px;
text-transform: uppercase;
@@ -84,8 +125,8 @@
.type-kafka { background: #fefce8; color: #854d0e; }
.type-rabbit { background: #f0fdf4; color: #166534; }
.ep-name { font-weight: 700; font-size: 0.95rem; margin-bottom: 6px; color: var(--text); }
.ep-fqn { font-family: 'JetBrains Mono', monospace; font-size: 0.65rem; color: var(--text-muted); opacity: 0.8; }
.ep-name, .flow-name { font-weight: 700; font-size: 0.95rem; margin-bottom: 6px; color: var(--text); }
.ep-fqn, .flow-desc { font-family: 'Inter', sans-serif; font-size: 0.75rem; color: var(--text-muted); opacity: 0.9; line-height: 1.4; }
#main { flex-grow: 1; position: relative; background: var(--main-bg); overflow: hidden; }
#svg-container {
@@ -165,12 +206,25 @@
<body>
<div id="sidebar">
<header>
<h1>Explorer</h1>
<p>{{MACHINE_NAME}}</p>
</header>
<div class="sidebar-header">
<header>
<h1>Explorer</h1>
<p>{{MACHINE_NAME}}</p>
</header>
</div>
<div id="ep-list"></div>
<div class="tabs">
<div class="tab active" data-tab="explorer">Explorer</div>
<div class="tab" data-tab="flows">Business Flows</div>
</div>
<div id="tab-explorer" class="tab-content active">
<div id="ep-list"></div>
</div>
<div id="tab-flows" class="tab-content">
<div id="flow-list"></div>
</div>
</div>
<div id="main">
@@ -191,10 +245,30 @@
const svg = document.querySelector('svg');
panZoomInstance = svgPanZoom(svg, { zoomEnabled: true, controlIconsEnabled: true, fit: true, center: true });
setupTabs();
populateSidebar();
populateFlows();
attachInteractivity();
});
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;
}
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab, .tab-content').forEach(el => el.classList.remove('active'));
tab.classList.add('active');
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
});
});
}
function populateSidebar() {
const list = document.getElementById('ep-list');
const entryPoints = metadata.metadata.entryPoints || [];
@@ -207,23 +281,52 @@
<div class="ep-name">${ep.name}</div>
<div class="ep-fqn">${ep.className}.${ep.methodName}</div>
`;
card.onmouseenter = () => highlightPaths(ep);
card.onmouseenter = () => highlightEntryPoint(ep);
card.onmouseleave = clearHighlights;
list.appendChild(card);
});
}
function highlightPaths(ep) {
function populateFlows() {
const list = document.getElementById('flow-list');
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>';
return;
}
flows.forEach(flow => {
const card = document.createElement('div');
card.className = 'flow-card';
card.innerHTML = `
<div class="flow-name">${flow.name}</div>
<div class="flow-desc">${flow.description}</div>
`;
card.onmouseenter = () => highlightFlow(flow);
card.onmouseleave = clearHighlights;
list.appendChild(card);
});
}
function highlightEntryPoint(ep) {
clearHighlights();
const chains = metadata.metadata.callChains || [];
// Surgical Filter: Match by full class and method name
const relevantEvents = chains
.filter(c => c.entryPoint.className === ep.className && c.entryPoint.methodName === ep.methodName)
.map(c => c.triggerPoint.event);
if (relevantEvents.length === 0) return;
highlightEvents(relevantEvents);
}
function highlightFlow(flow) {
clearHighlights();
if (!flow.steps || flow.steps.length === 0) return;
highlightEvents(flow.steps);
}
function highlightEvents(events) {
const svg = document.querySelector('svg');
// Dim everything first
@@ -231,16 +334,15 @@
el.classList.add('dimmed');
});
relevantEvents.forEach(evt => {
events.forEach(evt => {
const normalized = normalize(evt);
const linkId = "#link_" + normalized;
// Find <a> tags via hyperlinks
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: Grab ONLY the immediate path and polygon
// Surgical Selection
let prev = link.previousElementSibling;
let foundPath = false;
let foundPolygon = false;
@@ -262,7 +364,7 @@
});
});
// Keep all nodes visible for context
// Un-dim all state nodes for context
svg.querySelectorAll('rect, circle, ellipse').forEach(el => {
el.classList.remove('dimmed');
let next = el.nextElementSibling;
@@ -280,7 +382,6 @@
const svg = document.querySelector('svg');
const transitions = metadata.transitions || [];
// Attach tooltips to hyperlink groups
svg.querySelectorAll('a').forEach(link => {
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
if (!href || !href.startsWith('#link_')) return;
@@ -292,7 +393,6 @@
if (metaTrans || chains.length > 0) {
link.style.cursor = 'pointer';
// Map to the associated path
let pathToTrigger = null;
let prev = link.previousElementSibling;
while (prev) {
@@ -338,7 +438,7 @@
if (trans && trans.guard) {
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:4px">Guard</div>
<div style="background:#f1f5f9; padding:8px; border-radius:6px; font-family:'JetBrains Mono', monospace; font-size:0.8rem; margin-bottom:12px; border:1px solid #e2e8f0;">${trans.guard.expression}</div>`;
<div style="background:#f1f5f9; padding:6px; border-radius:4px; font-family:monospace; font-size:0.8rem; margin-bottom:12px; border:1px solid #e2e8f0;">${trans.guard.expression}</div>`;
}
if (chains && chains.length > 0) {