@@ -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 @@
${ep.name}
${ep.className}.${ep.methodName}
`;
- 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 = '
No business flows defined. Add a flows.json to your project to see them here.
';
+ return;
+ }
+
+ flows.forEach(flow => {
+ const card = document.createElement('div');
+ card.className = 'flow-card';
+ card.innerHTML = `
+
${flow.name}
+
${flow.description}
+ `;
+ 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
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 += `Guard
- ${trans.guard.expression}
`;
+ ${trans.guard.expression}
`;
}
if (chains && chains.length > 0) {
diff --git a/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlExporterTest.java b/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlExporterTest.java
index 9a09998..561c98e 100644
--- a/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlExporterTest.java
+++ b/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlExporterTest.java
@@ -5,6 +5,7 @@ import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnrich
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
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.ast.app.StateMachineAggregator;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
@@ -23,23 +24,19 @@ import static org.assertj.core.api.Assertions.assertThat;
public class HtmlExporterTest {
@Test
- void verifyFirstStepOfGeneration() throws IOException {
+ void shouldGenerateCompleteInteractivePortal() throws IOException {
// 1. Setup paths
Path projectRoot = Path.of("..").toAbsolutePath().normalize();
- Path inputDir = projectRoot.resolve("state_machines/extended_analysis_sample");
- Path outputDir = projectRoot.resolve("state_machine_exporter/out_html_verify");
+ Path inputDir = projectRoot.resolve("state_machines/ultimate_ecosystem_sm");
- Files.createDirectories(outputDir);
-
- // 2. Perform Analysis (Same as core Main)
+ // 2. Perform Analysis
CodebaseContext context = new CodebaseContext();
context.setSourcepath(List.of(inputDir.toString()));
context.setResolveBindings(true);
context.scan(inputDir);
- // Find the ExtendedStateMachineConfig class
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()
.orElseThrow();
@@ -47,14 +44,19 @@ public class HtmlExporterTest {
List transitions = aggregator.aggregateTransitions(td);
aggregator.aggregateStates(td);
+ // 3. Documented Flows
+ List flows = List.of(
+ BusinessFlow.builder().name("Test Flow").description("Desc").steps(List.of("CREATE")).build()
+ );
+
AnalysisResult result = AnalysisResult.builder()
- .name(context.getFqn(td))
+ .name("UltimatePortal")
.transitions(transitions)
.startStates(aggregator.getInitialStates())
.endStates(aggregator.getEndStates())
+ .flows(flows)
.build();
- // 3. Perform Enrichment
EnrichmentService enrichmentService = new EnrichmentService(List.of(
new TriggerEnricher(),
new EntryPointEnricher(),
@@ -63,20 +65,24 @@ public class HtmlExporterTest {
));
enrichmentService.enrich(result, context, new click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider(context, inputDir));
- // 4. Run HtmlExporter
+ // 4. Export
HtmlExporter exporter = new HtmlExporter();
String html = exporter.export(result, ExportOptions.builder().build());
- Path outputFile = outputDir.resolve("Verification.html");
- Files.writeString(outputFile, html);
- System.out.println("Verification HTML generated at: " + outputFile.toAbsolutePath());
-
- // 5. Verification
+ // 5. Assertions - Structural Integrity
assertThat(html).contains("");
- assertThat(html).contains("Explorer");
- assertThat(html).contains(result.getName());
- assertThat(html).contains("";
+ 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 = "";
+ String decorated = decorator.decorate(rawSvg);
+
+ assertThat(decorated).contains("