This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26864-tui-walk-fixes in repository https://gitbox.apache.org/repos/asf/camel.git
commit a357f174dbe93c2853e98dd57b95cde15398599a Author: Claus Ibsen <[email protected]> AuthorDate: Wed Nov 11 14:53:53 2026 +0100 CAMEL-26864: camel-jbang - Startup total and step parents, tui_get_history on Inspect, tab names, small display fixes - Startup: the total is from the first step to the end of the last one; each step is under its own parent (depth-first by id); a step with no name has no leading space. - tui_get_history reads the Inspect tab (it looked up "History"), and with an exchangeId returns its steps without changing the user's tab. - tui_get_table names the tabs as tui_get_options lists them; sub-views are given as "view". - Log title "level: INFO"; TOTAL (AGO) / FAIL (AGO) headers; internal entry points "timer:tick (period=2000, internal)"; Errors empty state in the first column. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../core/commands/ai/ProjectCapabilities.java | 14 ++- .../core/commands/ai/ProjectCapabilitiesTest.java | 13 ++ .../dsl/jbang/core/commands/tui/BrowseTab.java | 5 +- .../jbang/core/commands/tui/CircuitBreakerTab.java | 2 +- .../dsl/jbang/core/commands/tui/ErrorsTab.java | 3 +- .../jbang/core/commands/tui/HeapHistogramTab.java | 2 +- .../dsl/jbang/core/commands/tui/HistoryTab.java | 136 +++++++++++---------- .../camel/dsl/jbang/core/commands/tui/LogTab.java | 2 +- .../dsl/jbang/core/commands/tui/McpFacade.java | 6 +- .../dsl/jbang/core/commands/tui/MemoryLeakTab.java | 3 +- .../dsl/jbang/core/commands/tui/MemoryTab.java | 2 +- .../dsl/jbang/core/commands/tui/OverviewTab.java | 5 +- .../dsl/jbang/core/commands/tui/RoutesTab.java | 5 +- .../dsl/jbang/core/commands/tui/StartupTab.java | 95 ++++++++++++-- .../jbang/core/commands/tui/TransformersTab.java | 2 +- .../core/commands/tui/TuiToolDefinitions.java | 4 +- .../jbang/core/commands/tui/TuiToolRegistry.java | 17 ++- .../src/main/resources/tui/help/overview.md | 4 +- .../src/main/resources/tui/help/routes.md | 4 +- .../core/commands/tui/ErrorsTabRenderTest.java | 6 +- .../core/commands/tui/HistoryTabRenderTest.java | 19 +++ .../jbang/core/commands/tui/LogTabRenderTest.java | 2 +- .../core/commands/tui/StartupTabRenderTest.java | 45 +++++++ 23 files changed, 290 insertions(+), 106 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectCapabilities.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectCapabilities.java index e777e08093ec..56081997352f 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectCapabilities.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectCapabilities.java @@ -302,10 +302,22 @@ public final class ProjectCapabilities { private static List<String> entryPoints(Overview overview, List<String> routes) { List<String> answer = new ArrayList<>(); overview.entryPoints().stream().filter(e -> e.route() != null && routes.contains(e.route())) - .forEach(e -> answer.add(ProjectOverview.isInternal(e) ? e.label() + " (internal)" : e.label())); + .forEach(e -> answer.add(entryPointLabel(e))); return answer; } + /** + * An entry point as a group's box shows it: internal ones say so, inside the brackets of the label when it has some + * ("timer:tick (period=2000, internal)"). + */ + static String entryPointLabel(ProjectOverview.EntryPoint e) { + String label = e.label(); + if (!ProjectOverview.isInternal(e)) { + return label; + } + return label.endsWith(")") ? label.substring(0, label.length() - 1) + ", internal)" : label + " (internal)"; + } + private static List<String> systems(Overview overview, List<String> routes) { Set<String> answer = new LinkedHashSet<>(); overview.systems().stream().filter(s -> routes.contains(s.route())).forEach(s -> answer.add(s.name())); diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectCapabilitiesTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectCapabilitiesTest.java index 1cfd23101788..447406a85051 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectCapabilitiesTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectCapabilitiesTest.java @@ -36,6 +36,19 @@ import static org.assertj.core.api.Assertions.assertThat; */ class ProjectCapabilitiesTest { + @Test + void anInternalEntryPointSaysSoInsideTheBracketsOfItsLabel() { + assertThat(ProjectCapabilities.entryPointLabel( + new ProjectOverview.EntryPoint("timer", "timer:tick (period=2000)", "hello", false))) + .isEqualTo("timer:tick (period=2000, internal)"); + assertThat(ProjectCapabilities.entryPointLabel( + new ProjectOverview.EntryPoint("timer", "timer:tick", "hello", false))) + .isEqualTo("timer:tick (internal)"); + assertThat(ProjectCapabilities.entryPointLabel( + new ProjectOverview.EntryPoint("http", "platform-http:/orders", "order", true))) + .isEqualTo("platform-http:/orders"); + } + static final String ROUTES = """ - route: id: invoice diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java index 36ae33785b10..395f06925294 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java @@ -747,7 +747,8 @@ class BrowseTab extends AbstractTab { public JsonObject getTableDataAsJson() { if (selectedEndpoint != null && !messages.isEmpty()) { JsonObject result = new JsonObject(); - result.put("tab", "Browse Messages"); + result.put("tab", "Browse Endpoints"); + result.put("view", "Messages"); result.put("endpoint", selectedEndpoint.uri); JsonArray rows = new JsonArray(); for (MessageData m : messages) { @@ -775,7 +776,7 @@ class BrowseTab extends AbstractTab { return null; } JsonObject result = new JsonObject(); - result.put("tab", "Browse"); + result.put("tab", "Browse Endpoints"); JsonArray rows = new JsonArray(); for (EndpointData e : endpoints) { JsonObject row = new JsonObject(); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java index 4550c9580a34..c5869c5b9be1 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java @@ -414,7 +414,7 @@ class CircuitBreakerTab extends AbstractTableTab { return null; } JsonObject result = new JsonObject(); - result.put("tab", "CircuitBreaker"); + result.put("tab", "Circuit Breaker"); JsonArray rows = new JsonArray(); List<CircuitBreakerInfo> sorted = new ArrayList<>(info.circuitBreakers); sorted.sort(this::sortCb); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java index 33cac7128064..58760c6bd024 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java @@ -365,7 +365,8 @@ class ErrorsTab extends AbstractTableTab { } if (rows.isEmpty()) { - rows.add(emptyRow("No errors captured", 8, 7)); + // in the first column, as the empty state of the other tabs, short enough not to be cut there + rows.add(emptyRow("No errors", 8)); } ErrorInfo selectedError = shownError(info); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java index 4a2acf52d232..b07424246436 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java @@ -304,7 +304,7 @@ class HeapHistogramTab extends AbstractTableTab { return null; } JsonObject result = new JsonObject(); - result.put("tab", "Heap Histogram"); + result.put("tab", "Heap Memory Histogram"); JsonArray rows = new JsonArray(); for (HeapEntry e : entries) { JsonObject row = new JsonObject(); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java index 6345bae4b9dd..d4d8355be238 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java @@ -2327,15 +2327,6 @@ class HistoryTab extends AbstractTab { return DocHelper.loadHelpText("history"); } - void selectTraceExchange(String exchangeId) { - if (exchangeId != null && traceSortedExchangeIds.contains(exchangeId)) { - traceSelectedExchangeId = exchangeId; - traceDetailView = true; - traceStepTableState.select(0); - traceDetailScroll = 0; - } - } - String toggleDisplaySection(String section, Boolean enabled) { boolean tracerActive = !traces.get().isEmpty(); boolean newValue; @@ -2392,71 +2383,83 @@ class HistoryTab extends AbstractTab { return section + "=" + (newValue ? "on" : "off"); } + /** + * The trace steps of an exchange with all their details, as tui_get_history returns them, without changing what the + * tab shows. + */ + JsonObject traceStepsAsJson(String exchangeId) { + JsonObject result = new JsonObject(); + result.put("tab", "Inspect"); + result.put("view", "Trace Steps"); + List<TraceEntry> steps = getTraceStepsDepthFirst(exchangeId); + JsonArray rows = new JsonArray(); + for (TraceEntry t : steps) { + JsonObject row = new JsonObject(); + row.put("exchangeId", t.exchangeId); + row.put("routeId", t.routeId); + row.put("nodeId", t.nodeId); + row.put("processor", t.processor); + row.put("elapsed", t.elapsed); + row.put("timestamp", t.timestamp); + row.put("direction", t.direction); + row.put("status", t.status); + row.put("failed", t.failed); + row.put("first", t.first); + row.put("last", t.last); + if (t.body != null) { + row.put("body", t.body); + } + if (t.bodyType != null) { + row.put("bodyType", t.bodyType); + } + if (t.exception != null) { + row.put("exception", t.exception); + } + if (t.nodeLabel != null) { + row.put("nodeLabel", t.nodeLabel); + } + if (t.nodeShortName != null) { + row.put("nodeShortName", t.nodeShortName); + } + if (t.location != null) { + row.put("location", t.location); + } + if (t.threadName != null) { + row.put("threadName", t.threadName); + } + row.put("nodeLevel", t.nodeLevel); + if (t.headers != null && !t.headers.isEmpty()) { + row.put("headers", new JsonObject(t.headers)); + } + if (t.headerTypes != null && !t.headerTypes.isEmpty()) { + row.put("headerTypes", new JsonObject(t.headerTypes)); + } + if (t.exchangeProperties != null && !t.exchangeProperties.isEmpty()) { + row.put("exchangeProperties", new JsonObject(t.exchangeProperties)); + } + if (t.exchangeVariables != null && !t.exchangeVariables.isEmpty()) { + row.put("exchangeVariables", new JsonObject(t.exchangeVariables)); + } + rows.add(row); + } + result.put("rows", rows); + result.put("totalRows", steps.size()); + result.put("exchangeId", exchangeId); + return result; + } + @Override public JsonObject getTableDataAsJson() { JsonObject result = new JsonObject(); boolean tracerActive = !traces.get().isEmpty(); if (tracerActive) { if (traceDetailView && traceSelectedExchangeId != null) { - result.put("tab", "Trace Steps"); - List<TraceEntry> steps = getTraceStepsDepthFirst(traceSelectedExchangeId); - JsonArray rows = new JsonArray(); - for (TraceEntry t : steps) { - JsonObject row = new JsonObject(); - row.put("exchangeId", t.exchangeId); - row.put("routeId", t.routeId); - row.put("nodeId", t.nodeId); - row.put("processor", t.processor); - row.put("elapsed", t.elapsed); - row.put("timestamp", t.timestamp); - row.put("direction", t.direction); - row.put("status", t.status); - row.put("failed", t.failed); - row.put("first", t.first); - row.put("last", t.last); - if (t.body != null) { - row.put("body", t.body); - } - if (t.bodyType != null) { - row.put("bodyType", t.bodyType); - } - if (t.exception != null) { - row.put("exception", t.exception); - } - if (t.nodeLabel != null) { - row.put("nodeLabel", t.nodeLabel); - } - if (t.nodeShortName != null) { - row.put("nodeShortName", t.nodeShortName); - } - if (t.location != null) { - row.put("location", t.location); - } - if (t.threadName != null) { - row.put("threadName", t.threadName); - } - row.put("nodeLevel", t.nodeLevel); - if (t.headers != null && !t.headers.isEmpty()) { - row.put("headers", new JsonObject(t.headers)); - } - if (t.headerTypes != null && !t.headerTypes.isEmpty()) { - row.put("headerTypes", new JsonObject(t.headerTypes)); - } - if (t.exchangeProperties != null && !t.exchangeProperties.isEmpty()) { - row.put("exchangeProperties", new JsonObject(t.exchangeProperties)); - } - if (t.exchangeVariables != null && !t.exchangeVariables.isEmpty()) { - row.put("exchangeVariables", new JsonObject(t.exchangeVariables)); - } - rows.add(row); - } - result.put("rows", rows); - result.put("totalRows", steps.size()); - result.put("exchangeId", traceSelectedExchangeId); + result = traceStepsAsJson(traceSelectedExchangeId); Integer sel = traceStepTableState.selected(); result.put("selectedIndex", sel != null ? sel : -1); } else { - result.put("tab", "Traces"); + result.put("tab", "Inspect"); + result.put("view", "Traces"); List<String> exchangeIds = getTraceExchangeIds(); JsonArray rows = new JsonArray(); for (String eid : exchangeIds) { @@ -2470,7 +2473,8 @@ class HistoryTab extends AbstractTab { result.put("selectedIndex", sel != null ? sel : -1); } } else { - result.put("tab", "History"); + result.put("tab", "Inspect"); + result.put("view", "History"); List<HistoryEntry> entries = reorderHistoryDepthFirst(historyEntries); JsonArray rows = new JsonArray(); for (int i = 0; i < entries.size(); i++) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java index 8442ece204b3..5b77c1478fed 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java @@ -350,7 +350,7 @@ class LogTab extends AbstractTab { titleSpans.add(Span.styled(ctx.selectedName(), Theme.label().bold())); titleSpans.add(Span.raw("] Log ")); if (!ctx.logPinVisible && info != null && info.rootLogLevel != null) { - titleSpans.add(Span.raw("level:" + info.rootLogLevel + " ")); + titleSpans.add(Span.raw("level: " + info.rootLogLevel + " ")); } } else { titleSpans.add(Span.raw(" Log ")); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java index 486c1750122e..073f6de05e15 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java @@ -807,8 +807,10 @@ class McpFacade { return tabRegistry.diagramTab().getTableDataAsJson(); } - void selectTraceExchange(String exchangeId) { - tabRegistry.historyTab().selectTraceExchange(exchangeId); + /** The trace steps of an exchange, or null when it has none; the Inspect tab's view stays as it is. */ + JsonObject getTraceSteps(String exchangeId) { + JsonObject steps = tabRegistry.historyTab().traceStepsAsJson(exchangeId); + return steps.getIntegerOrDefault("totalRows", 0) > 0 ? steps : null; } JsonObject getTopologyData() { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java index a40614f73a37..ff35827b07fd 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java @@ -1347,8 +1347,7 @@ class MemoryLeakTab extends AbstractTab { } private JsonObject getComparisonDataAsJson() { - JsonObject result = new JsonObject(); - result.put("tab", "Memory Leak Comparison"); + JsonObject result = new JsonObject(Map.of("tab", "Memory Leak", "view", "Comparison")); result.put("baselineDurationMs", baselineDurationMs); result.put("currentDurationMs", currentDurationMs); result.put("durationRatio", durationRatio); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java index 1e78cad795b7..59affed2cf6b 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java @@ -500,7 +500,7 @@ class MemoryTab extends AbstractTab { return null; } JsonObject result = new JsonObject(); - result.put("tab", "Memory"); + result.put("tab", "Memory Usage"); JsonObject data = new JsonObject(); data.put("heapMemUsed", info.heapMemUsed); data.put("heapMemCommitted", info.heapMemCommitted); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java index a40f6f5079f8..c9b4246981d6 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java @@ -764,8 +764,9 @@ class OverviewTab extends AbstractTab { Cell.from(Span.styled(sortLabel("STATUS", "status"), sortStyle("status"))), rightCell("ROUTE", 7, Style.EMPTY.bold()), rightCell(ctx.ratePerMinute ? "MSG/M" : "MSG/S", 8, Style.EMPTY.bold()), - centerCell(sortLabel("TOTAL", "total"), 14, sortStyle("total")), - centerCell(sortLabel("FAIL", "fail"), 14, sortStyle("fail")), + // the time in brackets is since the last one + centerCell(sortLabel("TOTAL (AGO)", "total"), 14, sortStyle("total")), + centerCell(sortLabel("FAIL (AGO)", "fail"), 14, sortStyle("fail")), rightCell(timingHeader, 20, Style.EMPTY.bold()), Cell.from("")); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java index 1c841f510c32..c510691eb068 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java @@ -656,8 +656,9 @@ class RoutesTab extends AbstractDiagramTab { Cell.from(Span.styled(routeSortLabel("FROM", "from"), routeSortStyle("from"))), Cell.from(Span.styled(routeSortLabel("STATUS", "status"), routeSortStyle("status"))), rightCell(ctx.ratePerMinute ? "MSG/M" : "MSG/S", 8, Style.EMPTY.bold()), - centerCell(routeSortLabel("TOTAL", "total"), 14, routeSortStyle("total")), - centerCell(routeSortLabel("FAIL", "failed"), 14, routeSortStyle("failed")))); + // the time in brackets is since the last one + centerCell(routeSortLabel("TOTAL (AGO)", "total"), 14, routeSortStyle("total")), + centerCell(routeSortLabel("FAIL (AGO)", "failed"), 14, routeSortStyle("failed")))); List<Constraint> widths = new ArrayList<>( List.of( Constraint.length(24), diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java index a781bc0ad99e..fcd43aac9df7 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java @@ -16,10 +16,16 @@ */ package org.apache.camel.dsl.jbang.core.commands.tui; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import dev.tamboui.layout.Constraint; @@ -275,25 +281,98 @@ class StartupTab extends AbstractTab { Span.styled("█".repeat(barWidth), bandStyle)); } - private static String label(StartupStep step) { - String label = step.name != null ? step.name : ""; - if (step.description != null && !step.description.isEmpty() && !step.description.equals(label)) { - label += " " + step.description; + static String label(StartupStep step) { + String name = step.name != null ? step.name : ""; + String description = step.description != null ? step.description : ""; + if (name.isEmpty() || description.equals(name)) { + // a step with no name (Init CamelContext) is its description alone, without a leading space + return name.isEmpty() ? description : name; } - return label; + return description.isEmpty() ? name : name + " " + description; } - /** The steps in the order they started (by begin time, the outer step first), or the slowest first. */ + /** + * The steps in the order they started, each under its own parent (depth-first by parent id), or the slowest first. + */ static List<StartupStep> ordered(List<StartupStep> steps, boolean slowestFirst) { List<StartupStep> ordered = new ArrayList<>(steps); if (slowestFirst) { ordered.sort(Comparator.comparingLong((StartupStep st) -> st.duration).reversed()); + } else if (hasIds(steps)) { + ordered = depthFirst(steps); } else if (steps.stream().anyMatch(st -> st.beginTime > 0)) { ordered.sort(Comparator.comparingLong((StartupStep st) -> st.beginTime).thenComparingInt(st -> st.level)); } return ordered; } + /** Whether every step has its own id, so the steps can be put under their parents. */ + private static boolean hasIds(List<StartupStep> steps) { + Set<Integer> ids = new HashSet<>(); + for (StartupStep st : steps) { + if (st.id <= 0 || !ids.add(st.id)) { + return false; + } + } + return true; + } + + /** + * Each step followed by its children: the recorder gives the ids in the order the steps begin, so ordering by id + * keeps the begin order even where two steps began in the same millisecond, and a step whose parent is not in the + * list is a root. + */ + private static List<StartupStep> depthFirst(List<StartupStep> steps) { + Set<Integer> ids = new HashSet<>(); + steps.forEach(st -> ids.add(st.id)); + List<StartupStep> byId = new ArrayList<>(steps); + byId.sort(Comparator.comparingInt(st -> st.id)); + Map<Integer, List<StartupStep>> children = new HashMap<>(); + List<StartupStep> roots = new ArrayList<>(); + for (StartupStep st : byId) { + if (st.parentId != st.id && ids.contains(st.parentId)) { + children.computeIfAbsent(st.parentId, k -> new ArrayList<>()).add(st); + } else { + roots.add(st); + } + } + List<StartupStep> answer = new ArrayList<>(steps.size()); + Set<Integer> visited = new HashSet<>(); + Deque<StartupStep> stack = new ArrayDeque<>(); + for (StartupStep root : roots) { + stack.push(root); + while (!stack.isEmpty()) { + StartupStep st = stack.pop(); + if (visited.add(st.id)) { + answer.add(st); + List<StartupStep> kids = children.getOrDefault(st.id, List.of()); + for (int i = kids.size() - 1; i >= 0; i--) { + stack.push(kids.get(i)); + } + } + } + } + if (answer.size() < byId.size()) { + // steps in a parent loop: after the others, in id order + byId.stream().filter(st -> !visited.contains(st.id)).forEach(answer::add); + } + return answer; + } + + /** + * The time the startup took: from the first step to the end of the last one, or (without begin times, as an older + * Camel has them) the longest step. + */ + static long totalDuration(List<StartupStep> steps) { + long t0 = steps.stream().mapToLong(st -> st.beginTime).filter(b -> b > 0).min().orElse(0); + if (t0 > 0) { + return steps.stream().filter(st -> st.beginTime > 0).mapToLong(st -> st.beginTime + st.duration).max() + .orElse(t0) + - t0; + } + return steps.stream().mapToLong(s -> s.duration).max().orElse(0); + } + private Style colorForDuration(long duration) { return TuiHelper.colorForDuration(duration, minDurationColor, maxDurationColor); } @@ -366,8 +445,8 @@ class StartupTab extends AbstractTab { dataLoaded = true; if (!steps.isEmpty()) { - totalDuration = steps.stream().mapToLong(s -> s.duration).max().orElse(0); - maxDuration = totalDuration; + totalDuration = totalDuration(steps); + maxDuration = steps.stream().mapToLong(s -> s.duration).max().orElse(0); minDurationColor = steps.stream().filter(s -> s.level > 0).mapToLong(s -> s.duration).min().orElse(0); maxDurationColor diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java index abb2ad200fda..7394d1c8feec 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java @@ -228,7 +228,7 @@ class TransformersTab extends AbstractTableTab { return null; } JsonObject result = new JsonObject(); - result.put("tab", "Data Type Transformers"); + result.put("tab", "Transformers"); JsonArray rows = new JsonArray(); for (TransformerData t : transformers) { JsonObject row = new JsonObject(); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java index 965dc9246727..465fb7370674 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java @@ -371,11 +371,11 @@ final class TuiToolDefinitions { Map.of()), args -> r.callGetDiagram(), READ_ONLY)); tools.add(tool(toolDef( "tui_get_history", - "Trace and history data of the History tab with all exchange details (body and headers with types, " + "Trace and history data of the Inspect tab with all exchange details (body and headers with types, " + "properties, variables, thread, source location, node labels); richer than " + "tui_get_table. The shape follows the tab's mode: traces, trace steps or history.", Map.of("exchangeId", propDef("string", - "Trace steps of this exchange; otherwise the current History tab view"))), + "Trace steps of this exchange (the tab's view does not change); otherwise the current Inspect tab view"))), r::callGetHistory, READ_ONLY, CORE)); tools.add(tool(toolDef( "tui_get_topology", diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java index 2761f41ef244..bd1277247f67 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java @@ -1229,14 +1229,19 @@ class TuiToolRegistry { } String callGetHistory(Map<String, Object> args) { - String exchangeId = args.get("exchangeId") instanceof String s ? s : null; - if (exchangeId != null && !exchangeId.isBlank()) { - facade.navigateToTab("History"); - facade.selectTraceExchange(exchangeId); + String exchangeId = args.get("exchangeId") instanceof String s && !s.isBlank() ? s : null; + if (exchangeId != null) { + // read only: the steps of that exchange, without changing the tab or the view the user sees + JsonObject steps = facade.getTraceSteps(exchangeId); + if (steps == null) { + return "No trace steps for exchange " + exchangeId + ". Message tracing must be on, and the exchange " + + "one of the traced ones (tui_get_history without exchangeId lists them)."; + } + return Jsoner.serialize(steps); } - JsonObject data = facade.getTableData("History"); + JsonObject data = facade.getTableData("Inspect"); if (data == null) { - return "No history data available. Ensure the History tab has data."; + return "No history data available. Ensure the Inspect tab has data."; } return Jsoner.serialize(data); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md index 4ed570918470..2624bb9801ae 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md @@ -91,8 +91,8 @@ Each row represents one running Camel integration: - **STATUS** — Current lifecycle state: `Running` (processing messages), `Started` (ready), or `Stopped`; `No status` when it wrote no status for 15 seconds while its process runs (its MSG/S is then `-`) - **AGE** — How long the integration has been running (e.g., `2m30s`, `1h15m`) - **MSG/S** or **MSG/M** — Messages processed per second or per minute (current throughput). The rate unit can be configured in settings. This is the overall rate across all routes -- **TOTAL** — Total number of exchanges (messages) processed since the integration started -- **FAIL** — Number of exchanges that ended with an unhandled error +- **TOTAL (AGO)** — Total number of exchanges (messages) processed since the integration started, and in brackets how long ago the last one completed (`1128 (1s)`) +- **FAIL (AGO)** — Number of exchanges that ended with an unhandled error, and in brackets how long ago the last one failed - **INFLIGHT** — Exchanges currently being processed right now. A consistently high inflight count may indicate slow downstream services - **SINCE-LAST** — Time since the last exchange activity, shown as up to three values separated by `/`: started/completed/failed. For example, `1s/3s/1m14s` means the last exchange started 1s ago, the last completed 3s ago, and the last failure was 1m14s ago. Values are omitted when there is no activity of that type diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md index 84bce008e709..4bbdefa53e28 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md @@ -10,8 +10,8 @@ they are sent to. A typical integration has multiple routes working together. - **FROM** — The endpoint that triggers this route (e.g., `timer`, `kafka`, `file`). This is the source of messages - **STATUS** — Route state: `Started` (running), `Stopped` (not running), or `Suspended` (paused, can be resumed) - **MSG/S** or **MSG/M** — Current message throughput (per second or per minute) for this route. Configure in Settings (F2) -- **TOTAL** — Total exchanges processed by this route since startup -- **FAIL** — Exchanges that ended with an unhandled error in this route +- **TOTAL (AGO)** — Total exchanges processed by this route since startup, and in brackets how long ago the last one completed (`201 (13s)`) +- **FAIL (AGO)** — Exchanges that ended with an unhandled error in this route, and in brackets how long ago the last one failed - **BODY TYPES** — Shown when steps of a route have been reached with more than one body type, across the messages (with the message history on, such as `camel run --dev`): how many steps varied, and how many of them with bodies of more than one kind, which need attention (`⚠ 1 mixed · 2 varied`). The processors of the selected route then have a BODY TYPES column too, with the types each step has been reached with - **MIN** — Fastest exchange processing time in milliseconds. This is the time from when the exchange entered the route until it completed - **MEAN** — Average exchange processing time in milliseconds. A rising MEAN may indicate a downstream service getting slower diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java index 87c7baaf3c91..38f9ee0b031b 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java @@ -215,8 +215,10 @@ class ErrorsTabRenderTest { ErrorsTab tab = new ErrorsTab(ctx); String rendered = renderToString(tab, 160, 20); - assertTrue(rendered.contains("No errors captured"), - "Should show placeholder when no errors exist"); + // in the first column, under ID, as the empty state of the other tabs (it was under MESSAGE) + String header = rendered.lines().filter(l -> l.contains("EXCEPTION")).findFirst().orElseThrow(); + String empty = rendered.lines().filter(l -> l.contains("No errors")).findFirst().orElseThrow(); + assertEquals(header.indexOf("ID"), empty.indexOf("No errors"), rendered); } @Test diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java index 92d762487bc7..9a75933dcf5f 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java @@ -28,6 +28,7 @@ import dev.tamboui.style.Color; import dev.tamboui.terminal.Frame; import dev.tamboui.text.Span; import dev.tamboui.tui.event.KeyEvent; +import org.apache.camel.util.json.JsonObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -191,6 +192,24 @@ class HistoryTabRenderTest { assertTrue(rendered.contains("STATUS"), "Should show STATUS header"); } + @Test + void theTableDataNamesTheInspectTabAndReadingStepsKeepsTheView() { + traces.set(List.of(createTrace("EX-010", "route1", "Done", true, true))); + HistoryTab tab = new HistoryTab(ctx, traces, new HashMap<>()); + TuiTestHelper.renderToString(tab, 120, 30); + + JsonObject table = tab.getTableDataAsJson(); + assertEquals("Inspect", table.getString("tab"), "the name tui_get_options lists"); + assertEquals("Traces", table.getString("view")); + + // tui_get_history with an exchangeId: the steps of that exchange, while the tab still lists the traces + JsonObject steps = tab.traceStepsAsJson("EX-010"); + assertEquals("Trace Steps", steps.getString("view")); + assertEquals("EX-010", steps.getString("exchangeId")); + assertEquals(1, steps.getInteger("totalRows")); + assertEquals("Traces", tab.getTableDataAsJson().getString("view")); + } + // ---- Helper methods ---- private TraceEntry createTrace(String exchangeId, String routeId, String status, boolean first, boolean last) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabRenderTest.java index 64e020e845a6..a871c6e9c700 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabRenderTest.java @@ -164,7 +164,7 @@ class LogTabRenderTest { info.rootLogLevel = "INFO"; LogTab tab = new LogTab(ctx); String rendered = TuiTestHelper.renderToString(tab, 120, 20); - assertTrue(rendered.contains("[test-app] Log level:INFO"), rendered); + assertTrue(rendered.contains("[test-app] Log level: INFO"), rendered); } @Test diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTabRenderTest.java index 0c709b0b983e..76ad5347c9c7 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTabRenderTest.java @@ -97,6 +97,51 @@ class StartupTabRenderTest { assertEquals(steps, StartupTab.ordered(steps, false)); } + @Test + void eachStepIsUnderItsOwnParentWhenTwoBeganInTheSameMillisecond() { + // CAMEL-26864: "seven Create Route" (19) and "hello Init Route" (22) began in the same millisecond, and the + // children of 19 were shown under 22 + StartupTab.StartupStep routes = step(18, 0, "Init Routes", 1, 100, 20); + StartupTab.StartupStep create = step(19, 18, "seven Create Route", 2, 105, 5); + StartupTab.StartupStep endpoint = step(20, 19, "direct:seven Get Endpoint", 3, 105, 1); + StartupTab.StartupStep log = step(21, 19, "log2 Create Log Processor", 3, 106, 1); + StartupTab.StartupStep init = step(22, 18, "hello Init Route", 2, 105, 10); + List<StartupTab.StartupStep> steps = List.of(endpoint, log, create, init, routes); + + assertEquals(List.of(routes, create, endpoint, log, init), StartupTab.ordered(steps, false)); + } + + @Test + void theTotalIsFromTheFirstStepToTheEndOfTheLast() { + StartupTab.StartupStep init = step(1, 0, "Init CamelContext", 0, 1000, 222); + StartupTab.StartupStep start = step(2, 0, "Start CamelContext", 0, 1282, 182); + assertEquals(464, StartupTab.totalDuration(List.of(init, start))); + // without begin times (an older Camel): the longest step + init.beginTime = 0; + start.beginTime = 0; + assertEquals(222, StartupTab.totalDuration(List.of(init, start))); + } + + @Test + void aStepWithNoNameIsItsDescription() { + StartupTab.StartupStep st = step("", 0, 0, 1); + st.description = "Init CamelContext"; + assertEquals("Init CamelContext", StartupTab.label(st)); + st.name = null; + assertEquals("Init CamelContext", StartupTab.label(st)); + st.name = "hello"; + st.description = "Init Route"; + assertEquals("hello Init Route", StartupTab.label(st)); + } + + private static StartupTab.StartupStep step( + int id, int parentId, String name, int level, long begin, long duration) { + StartupTab.StartupStep st = step(name, level, begin, duration); + st.id = id; + st.parentId = parentId; + return st; + } + private static StartupTab.StartupStep step(String name, int level, long begin, long duration) { StartupTab.StartupStep st = new StartupTab.StartupStep(); st.name = name;
