v2 extended analysis render update
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -23,6 +23,9 @@ public class AnalysisResult {
|
|||||||
private final Set<String> endStates;
|
private final Set<String> endStates;
|
||||||
private final boolean renderChoicesAsDiamonds;
|
private final boolean renderChoicesAsDiamonds;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private final List<BusinessFlow> flows = java.util.Collections.emptyList();
|
||||||
|
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private CodebaseMetadata metadata = CodebaseMetadata.empty();
|
private CodebaseMetadata metadata = CodebaseMetadata.empty();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.jackson.Jacksonized;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@Jacksonized
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class BusinessFlow {
|
||||||
|
private final String name;
|
||||||
|
private final String description;
|
||||||
|
private final List<String> steps; // List of Event names in order
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnrich
|
|||||||
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
|
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
|
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.BusinessFlow;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider;
|
import click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider;
|
||||||
@@ -24,10 +25,7 @@ import java.io.IOException;
|
|||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Collections;
|
import java.util.*;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ExportService {
|
public class ExportService {
|
||||||
@@ -72,10 +70,25 @@ public class ExportService {
|
|||||||
context.loadLibraryHints(hintsFile);
|
context.loadLibraryHints(hintsFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<BusinessFlow> flows = new ArrayList<>();
|
||||||
|
Path flowsFile = inputDir.resolve("flows.json");
|
||||||
|
if (!Files.exists(flowsFile)) {
|
||||||
|
flowsFile = inputDir.resolve("src/main/resources/flows.json");
|
||||||
|
}
|
||||||
|
if (Files.exists(flowsFile)) {
|
||||||
|
log.info("Loading business flows from {}", flowsFile.toAbsolutePath());
|
||||||
|
try {
|
||||||
|
flows = new com.fasterxml.jackson.databind.ObjectMapper()
|
||||||
|
.readValue(flowsFile.toFile(), new com.fasterxml.jackson.core.type.TypeReference<List<BusinessFlow>>() {});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load flows.json", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
context.scan(inputDir);
|
context.scan(inputDir);
|
||||||
|
|
||||||
CodebaseIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, inputDir);
|
CodebaseIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, inputDir);
|
||||||
exportAll(context, intelligence, outputDir, selectedFormats, renderChoicesAsDiamonds);
|
exportAll(context, intelligence, outputDir, selectedFormats, renderChoicesAsDiamonds, flows);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats) throws IOException {
|
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats) throws IOException {
|
||||||
@@ -85,7 +98,7 @@ public class ExportService {
|
|||||||
generateOutputs(outputDir, result, selectedFormats);
|
generateOutputs(outputDir, result, selectedFormats);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows) throws IOException {
|
||||||
Set<String> processedLocations = new HashSet<>();
|
Set<String> processedLocations = new HashSet<>();
|
||||||
|
|
||||||
// 1. Find entry point classes (annotated or extending adapter)
|
// 1. Find entry point classes (annotated or extending adapter)
|
||||||
@@ -93,7 +106,7 @@ public class ExportService {
|
|||||||
for (TypeDeclaration td : entryPoints) {
|
for (TypeDeclaration td : entryPoints) {
|
||||||
String fqn = context.getFqn(td);
|
String fqn = context.getFqn(td);
|
||||||
if (processedLocations.add(fqn)) {
|
if (processedLocations.add(fqn)) {
|
||||||
processEntryPoint(td, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds);
|
processEntryPoint(td, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds, flows);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,12 +119,12 @@ public class ExportService {
|
|||||||
String uniqueId = parentFqn + "#" + methodName;
|
String uniqueId = parentFqn + "#" + methodName;
|
||||||
|
|
||||||
if (processedLocations.add(uniqueId)) {
|
if (processedLocations.add(uniqueId)) {
|
||||||
processBeanMethod(m, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds);
|
processBeanMethod(m, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds, flows);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows) throws IOException {
|
||||||
String className = context.getFqn(td);
|
String className = context.getFqn(td);
|
||||||
log.info("Processing state machine config: {}", className);
|
log.info("Processing state machine config: {}", className);
|
||||||
|
|
||||||
@@ -134,6 +147,7 @@ public class ExportService {
|
|||||||
.startStates(startStates)
|
.startStates(startStates)
|
||||||
.endStates(endStates)
|
.endStates(endStates)
|
||||||
.renderChoicesAsDiamonds(renderChoicesAsDiamonds)
|
.renderChoicesAsDiamonds(renderChoicesAsDiamonds)
|
||||||
|
.flows(flows)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
enrichmentService.enrich(result, context, intelligence);
|
enrichmentService.enrich(result, context, intelligence);
|
||||||
@@ -141,7 +155,7 @@ public class ExportService {
|
|||||||
generateOutputs(outputDir, result, selectedFormats);
|
generateOutputs(outputDir, result, selectedFormats);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows) throws IOException {
|
||||||
TypeDeclaration parentClass = (TypeDeclaration) m.getParent();
|
TypeDeclaration parentClass = (TypeDeclaration) m.getParent();
|
||||||
String parentFqn = context.getFqn(parentClass);
|
String parentFqn = context.getFqn(parentClass);
|
||||||
String methodName = m.getName().getIdentifier();
|
String methodName = m.getName().getIdentifier();
|
||||||
@@ -160,6 +174,7 @@ public class ExportService {
|
|||||||
.startStates(startStates)
|
.startStates(startStates)
|
||||||
.endStates(endStates)
|
.endStates(endStates)
|
||||||
.renderChoicesAsDiamonds(renderChoicesAsDiamonds)
|
.renderChoicesAsDiamonds(renderChoicesAsDiamonds)
|
||||||
|
.flows(flows)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
enrichmentService.enrich(result, context, intelligence);
|
enrichmentService.enrich(result, context, intelligence);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -41,16 +41,55 @@
|
|||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 30px;
|
|
||||||
overflow-y: auto;
|
|
||||||
box-shadow: 10px 0 15px rgba(0,0,0,0.02);
|
box-shadow: 10px 0 15px rgba(0,0,0,0.02);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
header h1 { font-weight: 900; font-size: 1.6rem; color: var(--text); margin: 0 0 5px 0; letter-spacing: -0.5px; }
|
.sidebar-header {
|
||||||
header p { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--text-muted); margin: 0 0 35px 0; opacity: 0.7; }
|
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;
|
padding: 18px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -59,9 +98,10 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
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);
|
border-color: var(--primary);
|
||||||
transform: translateX(5px);
|
transform: translateX(5px);
|
||||||
box-shadow: 0 15px 30px -10px rgba(59, 130, 246, 0.15);
|
box-shadow: 0 15px 30px -10px rgba(59, 130, 246, 0.15);
|
||||||
@@ -70,7 +110,8 @@
|
|||||||
.ep-type {
|
.ep-type {
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
padding: 3px 8px; border-radius: 6px;
|
padding: 3px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -84,8 +125,8 @@
|
|||||||
.type-kafka { background: #fefce8; color: #854d0e; }
|
.type-kafka { background: #fefce8; color: #854d0e; }
|
||||||
.type-rabbit { background: #f0fdf4; color: #166534; }
|
.type-rabbit { background: #f0fdf4; color: #166534; }
|
||||||
|
|
||||||
.ep-name { font-weight: 700; font-size: 0.95rem; margin-bottom: 6px; color: var(--text); }
|
.ep-name, .flow-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-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; }
|
#main { flex-grow: 1; position: relative; background: var(--main-bg); overflow: hidden; }
|
||||||
#svg-container {
|
#svg-container {
|
||||||
@@ -165,12 +206,25 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
<header>
|
<header>
|
||||||
<h1>Explorer</h1>
|
<h1>Explorer</h1>
|
||||||
<p>{{MACHINE_NAME}}</p>
|
<p>{{MACHINE_NAME}}</p>
|
||||||
</header>
|
</header>
|
||||||
|
</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 id="ep-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-flows" class="tab-content">
|
||||||
|
<div id="flow-list"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="main">
|
<div id="main">
|
||||||
@@ -191,10 +245,30 @@
|
|||||||
const svg = document.querySelector('svg');
|
const svg = document.querySelector('svg');
|
||||||
panZoomInstance = svgPanZoom(svg, { zoomEnabled: true, controlIconsEnabled: true, fit: true, center: true });
|
panZoomInstance = svgPanZoom(svg, { zoomEnabled: true, controlIconsEnabled: true, fit: true, center: true });
|
||||||
|
|
||||||
|
setupTabs();
|
||||||
populateSidebar();
|
populateSidebar();
|
||||||
|
populateFlows();
|
||||||
attachInteractivity();
|
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() {
|
function populateSidebar() {
|
||||||
const list = document.getElementById('ep-list');
|
const list = document.getElementById('ep-list');
|
||||||
const entryPoints = metadata.metadata.entryPoints || [];
|
const entryPoints = metadata.metadata.entryPoints || [];
|
||||||
@@ -207,23 +281,52 @@
|
|||||||
<div class="ep-name">${ep.name}</div>
|
<div class="ep-name">${ep.name}</div>
|
||||||
<div class="ep-fqn">${ep.className}.${ep.methodName}</div>
|
<div class="ep-fqn">${ep.className}.${ep.methodName}</div>
|
||||||
`;
|
`;
|
||||||
card.onmouseenter = () => highlightPaths(ep);
|
card.onmouseenter = () => highlightEntryPoint(ep);
|
||||||
card.onmouseleave = clearHighlights;
|
card.onmouseleave = clearHighlights;
|
||||||
list.appendChild(card);
|
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();
|
clearHighlights();
|
||||||
const chains = metadata.metadata.callChains || [];
|
const chains = metadata.metadata.callChains || [];
|
||||||
|
|
||||||
// Surgical Filter: Match by full class and method name
|
|
||||||
const relevantEvents = chains
|
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)
|
||||||
.map(c => c.triggerPoint.event);
|
.map(c => c.triggerPoint.event);
|
||||||
|
|
||||||
if (relevantEvents.length === 0) return;
|
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');
|
const svg = document.querySelector('svg');
|
||||||
|
|
||||||
// Dim everything first
|
// Dim everything first
|
||||||
@@ -231,16 +334,15 @@
|
|||||||
el.classList.add('dimmed');
|
el.classList.add('dimmed');
|
||||||
});
|
});
|
||||||
|
|
||||||
relevantEvents.forEach(evt => {
|
events.forEach(evt => {
|
||||||
const normalized = normalize(evt);
|
const normalized = normalize(evt);
|
||||||
const linkId = "#link_" + normalized;
|
const linkId = "#link_" + normalized;
|
||||||
|
|
||||||
// Find <a> tags via hyperlinks
|
|
||||||
svg.querySelectorAll(`a[*|href="${linkId}"], a[href="${linkId}"]`).forEach(link => {
|
svg.querySelectorAll(`a[*|href="${linkId}"], a[href="${linkId}"]`).forEach(link => {
|
||||||
link.classList.add('active-path');
|
link.classList.add('active-path');
|
||||||
link.querySelectorAll('*').forEach(child => child.classList.remove('dimmed'));
|
link.querySelectorAll('*').forEach(child => child.classList.remove('dimmed'));
|
||||||
|
|
||||||
// Surgical Selection: Grab ONLY the immediate path and polygon
|
// Surgical Selection
|
||||||
let prev = link.previousElementSibling;
|
let prev = link.previousElementSibling;
|
||||||
let foundPath = false;
|
let foundPath = false;
|
||||||
let foundPolygon = 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 => {
|
svg.querySelectorAll('rect, circle, ellipse').forEach(el => {
|
||||||
el.classList.remove('dimmed');
|
el.classList.remove('dimmed');
|
||||||
let next = el.nextElementSibling;
|
let next = el.nextElementSibling;
|
||||||
@@ -280,7 +382,6 @@
|
|||||||
const svg = document.querySelector('svg');
|
const svg = document.querySelector('svg');
|
||||||
const transitions = metadata.transitions || [];
|
const transitions = metadata.transitions || [];
|
||||||
|
|
||||||
// Attach tooltips to hyperlink groups
|
|
||||||
svg.querySelectorAll('a').forEach(link => {
|
svg.querySelectorAll('a').forEach(link => {
|
||||||
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
|
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
|
||||||
if (!href || !href.startsWith('#link_')) return;
|
if (!href || !href.startsWith('#link_')) return;
|
||||||
@@ -292,7 +393,6 @@
|
|||||||
if (metaTrans || chains.length > 0) {
|
if (metaTrans || chains.length > 0) {
|
||||||
link.style.cursor = 'pointer';
|
link.style.cursor = 'pointer';
|
||||||
|
|
||||||
// Map to the associated path
|
|
||||||
let pathToTrigger = null;
|
let pathToTrigger = null;
|
||||||
let prev = link.previousElementSibling;
|
let prev = link.previousElementSibling;
|
||||||
while (prev) {
|
while (prev) {
|
||||||
@@ -338,7 +438,7 @@
|
|||||||
|
|
||||||
if (trans && trans.guard) {
|
if (trans && trans.guard) {
|
||||||
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:4px">Guard</div>
|
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) {
|
if (chains && chains.length > 0) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnrich
|
|||||||
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
|
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
|
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.BusinessFlow;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
||||||
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
|
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
@@ -23,23 +24,19 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
public class HtmlExporterTest {
|
public class HtmlExporterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void verifyFirstStepOfGeneration() throws IOException {
|
void shouldGenerateCompleteInteractivePortal() throws IOException {
|
||||||
// 1. Setup paths
|
// 1. Setup paths
|
||||||
Path projectRoot = Path.of("..").toAbsolutePath().normalize();
|
Path projectRoot = Path.of("..").toAbsolutePath().normalize();
|
||||||
Path inputDir = projectRoot.resolve("state_machines/extended_analysis_sample");
|
Path inputDir = projectRoot.resolve("state_machines/ultimate_ecosystem_sm");
|
||||||
Path outputDir = projectRoot.resolve("state_machine_exporter/out_html_verify");
|
|
||||||
|
|
||||||
Files.createDirectories(outputDir);
|
// 2. Perform Analysis
|
||||||
|
|
||||||
// 2. Perform Analysis (Same as core Main)
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
CodebaseContext context = new CodebaseContext();
|
||||||
context.setSourcepath(List.of(inputDir.toString()));
|
context.setSourcepath(List.of(inputDir.toString()));
|
||||||
context.setResolveBindings(true);
|
context.setResolveBindings(true);
|
||||||
context.scan(inputDir);
|
context.scan(inputDir);
|
||||||
|
|
||||||
// Find the ExtendedStateMachineConfig class
|
|
||||||
TypeDeclaration td = context.getTypeDeclarations().stream()
|
TypeDeclaration td = context.getTypeDeclarations().stream()
|
||||||
.filter(t -> context.getFqn(t).equals("click.kamil.examples.statemachine.extended.config.ExtendedStateMachineConfig"))
|
.filter(t -> context.getFqn(t).equals("click.kamil.ultimate.config.UltimateStateMachineConfig"))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
|
|
||||||
@@ -47,14 +44,19 @@ public class HtmlExporterTest {
|
|||||||
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||||
aggregator.aggregateStates(td);
|
aggregator.aggregateStates(td);
|
||||||
|
|
||||||
|
// 3. Documented Flows
|
||||||
|
List<BusinessFlow> flows = List.of(
|
||||||
|
BusinessFlow.builder().name("Test Flow").description("Desc").steps(List.of("CREATE")).build()
|
||||||
|
);
|
||||||
|
|
||||||
AnalysisResult result = AnalysisResult.builder()
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
.name(context.getFqn(td))
|
.name("UltimatePortal")
|
||||||
.transitions(transitions)
|
.transitions(transitions)
|
||||||
.startStates(aggregator.getInitialStates())
|
.startStates(aggregator.getInitialStates())
|
||||||
.endStates(aggregator.getEndStates())
|
.endStates(aggregator.getEndStates())
|
||||||
|
.flows(flows)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 3. Perform Enrichment
|
|
||||||
EnrichmentService enrichmentService = new EnrichmentService(List.of(
|
EnrichmentService enrichmentService = new EnrichmentService(List.of(
|
||||||
new TriggerEnricher(),
|
new TriggerEnricher(),
|
||||||
new EntryPointEnricher(),
|
new EntryPointEnricher(),
|
||||||
@@ -63,20 +65,24 @@ public class HtmlExporterTest {
|
|||||||
));
|
));
|
||||||
enrichmentService.enrich(result, context, new click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider(context, inputDir));
|
enrichmentService.enrich(result, context, new click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider(context, inputDir));
|
||||||
|
|
||||||
// 4. Run HtmlExporter
|
// 4. Export
|
||||||
HtmlExporter exporter = new HtmlExporter();
|
HtmlExporter exporter = new HtmlExporter();
|
||||||
String html = exporter.export(result, ExportOptions.builder().build());
|
String html = exporter.export(result, ExportOptions.builder().build());
|
||||||
|
|
||||||
Path outputFile = outputDir.resolve("Verification.html");
|
// 5. Assertions - Structural Integrity
|
||||||
Files.writeString(outputFile, html);
|
|
||||||
System.out.println("Verification HTML generated at: " + outputFile.toAbsolutePath());
|
|
||||||
|
|
||||||
// 5. Verification
|
|
||||||
assertThat(html).contains("<!DOCTYPE html>");
|
assertThat(html).contains("<!DOCTYPE html>");
|
||||||
assertThat(html).contains("Explorer");
|
assertThat(html).contains("UltimatePortal");
|
||||||
assertThat(html).contains(result.getName());
|
assertThat(html).contains("Test Flow"); // Flows tab check
|
||||||
assertThat(html).contains("<svg");
|
|
||||||
// We will improve the "Decorator" to inject identifying classes more robustly
|
// 6. Assertions - Data Payloads
|
||||||
// assertThat(html).contains("link_SUBMIT_EVENT");
|
assertThat(html).contains("TicketRequest"); // @RequestBody extraction
|
||||||
|
assertThat(html).contains("ticketId"); // JMS param extraction
|
||||||
|
assertThat(html).contains("ticket.close.queue"); // Destination extraction
|
||||||
|
|
||||||
|
// 7. Assertions - Surgical SVG Identification
|
||||||
|
// Every event should be wrapped in an identifying hyperlink for robust JS selection
|
||||||
|
assertThat(html).contains("#link_CREATE");
|
||||||
|
assertThat(html).contains("#link_ASSIGN");
|
||||||
|
assertThat(html).contains("#link_CLOSE_TICKET");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.html.exporter;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class SvgDecoratorTest {
|
||||||
|
|
||||||
|
private final SvgDecorator decorator = new SvgDecorator();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMakeSvgResponsive() {
|
||||||
|
String rawSvg = "<svg width=\"500px\" height=\"300px\" style=\"width:500px;height:300px;background:#FFF;\" version=\"1.1\"><defs/></svg>";
|
||||||
|
String decorated = decorator.decorate(rawSvg);
|
||||||
|
|
||||||
|
assertThat(decorated).contains("width=\"100%\"");
|
||||||
|
assertThat(decorated).contains("height=\"100%\"");
|
||||||
|
assertThat(decorated).doesNotContain("width=\"500px\"");
|
||||||
|
assertThat(decorated).doesNotContain("style=\"width:500px;height:300px;background:#FFF;\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotTouchInternalElementDimensions() {
|
||||||
|
String rawSvg = "<svg width=\"500\"><rect width=\"10\" height=\"10\"/></svg>";
|
||||||
|
String decorated = decorator.decorate(rawSvg);
|
||||||
|
|
||||||
|
assertThat(decorated).contains("<svg width=\"100%\"");
|
||||||
|
assertThat(decorated).contains("<rect width=\"10\" height=\"10\"/>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldInjectInteractivityStyles() {
|
||||||
|
String rawSvg = "<svg><defs/></svg>";
|
||||||
|
String decorated = decorator.decorate(rawSvg);
|
||||||
|
|
||||||
|
assertThat(decorated).contains(".active-path");
|
||||||
|
assertThat(decorated).contains(".dimmed");
|
||||||
|
assertThat(decorated).contains("@keyframes sm-flow");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Standard Ticket Lifecycle",
|
||||||
|
"description": "The typical path for a ticket: Creation -> Triage -> Progress -> Resolution -> Closing.",
|
||||||
|
"steps": ["CREATE", "ASSIGN", "RESOLVE", "CLOSE_TICKET"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AWS-Triggered Escalation",
|
||||||
|
"description": "Emergency flow triggered by AWS infrastructure: SNS Update -> SQS Escalation.",
|
||||||
|
"steps": ["SNS_UPDATE", "SQS_UPDATE", "ESCALATE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reactive Creation Path",
|
||||||
|
"description": "Modern non-blocking ticket creation flow via WebFlux.",
|
||||||
|
"steps": ["CREATE_REACTIVE", "ASSIGN"]
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user