This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch quick-fix/polish-tables in repository https://gitbox.apache.org/repos/asf/camel.git
commit 443a1a487c052a8d387d5f0d658912776cb76e17 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Oct 1 17:33:08 2026 +0200 chore: camel-jbang - tables read better: a first row selected for the detail, URIs and durations as people write them, Activity newest first with a true rate, one row per REST service and health check - Errors, Endpoints and HTTP select their first row, so the detail panel shows something - endpoint URIs are shown decoded ({sku}, ${header...}) in Routes, Activity and Endpoints - durations read 2.02s rather than 2s20ms - Activity: newest first, the rate over the time up to now (a burst was thousands a minute), a short exchange id, and a detail panel as tall as its content - HTTP: a REST service and its platform-http consumer are one row - Health: a check reported for readiness and liveness is one row; the title says what R and L are - Endpoints: a check mark for stub and remote - tui_navigate finds the tabs by the label the bar shows (Route, Endpoint) and by number Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../jbang/core/commands/tui/AbstractTableTab.java | 10 ++++ .../dsl/jbang/core/commands/tui/ActivityTab.java | 68 ++++++++++++++-------- .../dsl/jbang/core/commands/tui/EndpointsTab.java | 11 +++- .../dsl/jbang/core/commands/tui/ErrorsTab.java | 5 ++ .../dsl/jbang/core/commands/tui/HealthTab.java | 5 +- .../dsl/jbang/core/commands/tui/HistoryTab.java | 2 +- .../camel/dsl/jbang/core/commands/tui/HttpTab.java | 32 +++++++++- .../dsl/jbang/core/commands/tui/McpFacade.java | 25 ++++++++ .../dsl/jbang/core/commands/tui/RoutesTab.java | 5 +- .../dsl/jbang/core/commands/tui/StatusParser.java | 22 ++++++- .../dsl/jbang/core/commands/tui/TuiHelper.java | 22 ++++++- .../core/commands/tui/DisplayFormatsTest.java | 58 ++++++++++++++++++ .../core/commands/tui/EndpointsTabRenderTest.java | 4 ++ .../core/commands/tui/ErrorsTabRenderTest.java | 14 +++++ .../jbang/core/commands/tui/HttpTabRenderTest.java | 18 ++++++ .../jbang/core/commands/tui/TablePolishTest.java | 64 ++++++++++++++++++++ 16 files changed, 331 insertions(+), 34 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTableTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTableTab.java index 223193e2fa74..b839273fadbd 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTableTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTableTab.java @@ -52,6 +52,11 @@ abstract class AbstractTableTab extends AbstractTab { protected abstract int getRowCount(); + /** Whether the table selects its first row by itself: a table with a detail panel of the selected row does. */ + protected boolean selectsFirstRow() { + return false; + } + protected abstract void renderContent(Frame frame, Rect area, IntegrationInfo info); @Override @@ -120,6 +125,11 @@ abstract class AbstractTableTab extends AbstractTab { renderNoSelection(frame, area); return; } + // a table with a detail panel has a row selected once it has rows, so the detail shows something + int rows = getRowCount(); + if (selectsFirstRow() && rows > 0 && (tableState.selected() == null || tableState.selected() >= rows)) { + tableState.select(tableState.selected() == null ? 0 : rows - 1); + } renderContent(frame, area, info); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java index 0cfbdca0549c..59b5bbcccb9c 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java @@ -54,7 +54,8 @@ class ActivityTab extends AbstractTableTab { private static final long[] TIME_FILTER_MILLIS = { 0, 60_000, 300_000, 900_000, 1_800_000, 3_600_000 }; ActivityTab(MonitorContext ctx) { - super(ctx, "exchange", "route", "elapsed", "since"); + // newest first, as a live feed reads + super(ctx, "since", "exchange", "route", "elapsed"); } @Override @@ -144,13 +145,13 @@ class ActivityTab extends AbstractTableTab { rows.add(Row .from( - Cell.from(ae.exchangeId != null ? ae.exchangeId : ""), + Cell.from(shortExchangeId(ae.exchangeId)), Cell.from(Span.styled(ae.routeId != null ? ae.routeId : "", Style.EMPTY.fg(Theme.accent()))), Cell.from(Span.styled(status, statusStyle)), Cell.from(elapsed), Cell.from(sends), Cell.from(ago), - Cell.from(ae.fromEndpointUri != null ? ae.fromEndpointUri : ""))); + Cell.from(ae.fromEndpointUri != null ? TuiHelper.displayUri(ae.fromEndpointUri) : ""))); } if (rows.isEmpty()) { @@ -166,11 +167,12 @@ class ActivityTab extends AbstractTableTab { List<Constraint> constraints = new ArrayList<>(); constraints.add(Constraint.length(4)); + constraints.add(Constraint.fill()); + List<Line> detailLines = showDetail ? detailLines(selectedEntry) : List.of(); if (showDetail) { - constraints.add(Constraint.length(13)); - constraints.add(Constraint.fill()); - } else { - constraints.add(Constraint.fill()); + // the detail is as tall as what it has to say, up to half of the tab; the table takes the rest + int detailHeight = Math.max(6, Math.min(detailLines.size() + 2, (area.height() - 4) / 2)); + constraints.add(Constraint.length(detailHeight)); } List<Rect> chunks = Layout.vertical() .constraints(constraints) @@ -189,7 +191,7 @@ class ActivityTab extends AbstractTableTab { Cell.from(Span.styled(sortLabel("SINCE", "since"), sortStyle("since"))), Cell.from(Span.styled("ENDPOINT", Style.EMPTY.bold())))) .widths( - Constraint.length(38), + Constraint.length(16), Constraint.length(20), Constraint.length(8), Constraint.length(10), @@ -207,7 +209,7 @@ class ActivityTab extends AbstractTableTab { renderScrollbar(frame, table, sorted.size()); if (showDetail) { - renderDetail(frame, chunks.get(2), selectedEntry); + renderDetail(frame, chunks.get(2), detailLines); } } @@ -243,11 +245,11 @@ class ActivityTab extends AbstractTableTab { String errorRate = total > 0 ? String.format(Locale.US, "%.1f%%", (failed * 100.0) / total) : "0%"; String rate = ""; - if (oldestTs < Long.MAX_VALUE && newestTs > oldestTs) { - double minutes = (newestTs - oldestTs) / 60_000.0; - if (minutes > 0) { - rate = String.format(Locale.US, "%.1f/min", total / minutes); - } + if (oldestTs < Long.MAX_VALUE) { + // over the time up to now, at least a second: a burst of exchanges within one second is not thousands a + // minute + double minutes = Math.max(1000, System.currentTimeMillis() - oldestTs) / 60_000.0; + rate = String.format(Locale.US, "%.1f/min", total / minutes); } int sends = entries.stream().mapToInt(ae -> ae.endpointSends.size()).sum(); @@ -300,11 +302,19 @@ class ActivityTab extends AbstractTableTab { TuiHelper.hintLast(spans, "w", "wrap" + (wordWrap ? " [on]" : " [off]")); } - private void renderDetail(Frame frame, Rect area, ActivityEntry ae) { + private void renderDetail(Frame frame, Rect area, List<Line> lines) { + int[] scroll = { detailScroll }; + int[] hScroll = { detailHScroll }; + HistoryTab.renderDetailPanel(frame, area, lines, wordWrap, hScroll, scroll, detailScrollState, " Detail "); + detailScroll = scroll[0]; + detailHScroll = hScroll[0]; + } + + private static List<Line> detailLines(ActivityEntry ae) { List<Line> lines = new ArrayList<>(); HistoryTab.addExchangeInfoLines(lines, - ae.exchangeId, ae.routeId, null, null, "Endpoint", ae.fromEndpointUri, + ae.exchangeId, ae.routeId, null, null, "Endpoint", TuiHelper.displayUri(ae.fromEndpointUri), ae.elapsed, null, ae.failed); if (ae.exceptionMessage != null) { @@ -316,16 +326,12 @@ class ActivityTab extends AbstractTableTab { for (ActivityEntry.EndpointSendEntry se : ae.endpointSends) { lines.add(Line .from( - Span.styled(" " + (se.endpointUri != null ? se.endpointUri : ""), Style.EMPTY), + Span.styled(" " + (se.endpointUri != null ? TuiHelper.displayUri(se.endpointUri) : ""), + Style.EMPTY), Span.styled(" " + se.elapsed + "ms", Theme.muted()))); } } - - int[] scroll = { detailScroll }; - int[] hScroll = { detailHScroll }; - HistoryTab.renderDetailPanel(frame, area, lines, wordWrap, hScroll, scroll, detailScrollState, " Detail "); - detailScroll = scroll[0]; - detailHScroll = hScroll[0]; + return lines; } private List<ActivityEntry> filteredActivity(IntegrationInfo info) { @@ -340,6 +346,22 @@ class ActivityTab extends AbstractTableTab { return result; } + /** + * The part of an exchange id that tells the exchanges apart: Camel ids are the context's prefix and a counter + * (AD50EEC2B14A8A3-0000000000000001), so the counter without its leading zeros (…-1). The detail shows the full id. + */ + static String shortExchangeId(String id) { + if (id == null) { + return ""; + } + int dash = id.lastIndexOf('-'); + if (dash < 0 || dash == id.length() - 1) { + return id; + } + String counter = id.substring(dash + 1).replaceFirst("^0+(?=.)", ""); + return id.substring(0, Math.min(dash, 6)) + "…-" + counter; + } + private int sortActivity(ActivityEntry a, ActivityEntry b) { int result = switch (sort) { case "exchange" -> TuiHelper.compareStr(a.exchangeId, b.exchangeId); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java index 0976b4c7b94e..136a02e2b69d 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java @@ -105,6 +105,11 @@ class EndpointsTab extends AbstractTableTab { this.perEndpointOutSizeHistory = metrics.getPerEndpointOutSizeHistory(); } + @Override + protected boolean selectsFirstRow() { + return true; + } + @Override protected int getRowCount() { IntegrationInfo info = ctx.findSelectedIntegration(); @@ -249,9 +254,9 @@ class EndpointsTab extends AbstractTableTab { cells.add(rightCell(FlowHelper.sizeToString(ep.meanBodySize), 10)); cells.add(rightCell(FlowHelper.sizeToString(ep.meanHeadersSize), 10)); } - cells.add(centerCell(ep.stub ? "x" : "", 6)); - cells.add(centerCell(ep.remote ? "x" : "", 8)); - cells.add(Cell.from(ep.uri != null ? ep.uri : "")); + cells.add(centerCell(ep.stub ? TuiIcons.CHECK : "", 6)); + cells.add(centerCell(ep.remote ? TuiIcons.CHECK : "", 8)); + cells.add(Cell.from(ep.uri != null ? TuiHelper.displayUri(ep.uri) : "")); rows.add(Row.from(cells)); } 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 ab5923980a1e..fe735789bf79 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 @@ -73,6 +73,11 @@ class ErrorsTab extends AbstractTableTab { super(ctx, "id", "age", "route", "node", "exception"); } + @Override + protected boolean selectsFirstRow() { + return true; + } + @Override protected int getRowCount() { return filteredSize(); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java index fdf62c8560cf..7de92f93a077 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java @@ -113,9 +113,10 @@ class HealthTab extends AbstractTableTab { Cell.from(""))); } + // KIND says what the check is for: R readiness (may take traffic), L liveness (is alive) String title = showOnlyDown ? " Health [DOWN only] " - : " Health "; + : " Health ─ KIND: R readiness, L liveness "; Table table = Table.builder() .rows(rows) @@ -144,7 +145,7 @@ class HealthTab extends AbstractTableTab { @Override public void renderFooter(List<Span> spans) { super.renderFooter(spans); - hint(spans, "d", "toggle DOWN"); + hint(spans, "d", showOnlyDown ? "show all" : "only DOWN"); } boolean isShowOnlyDown() { 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 4b6fed1c60ac..f16e8018fb42 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 @@ -2133,7 +2133,7 @@ class HistoryTab extends AbstractTab { spans.add(Span.styled("status:" + (failed ? "failed" : "ok"), failed ? Theme.error().bold() : Theme.success().bold())); if (last.elapsed >= 0) { - spans.add(Span.raw(" elapsed:" + TimeUtils.printDuration(last.elapsed, true))); + spans.add(Span.raw(" elapsed:" + TuiHelper.formatDurationMs(last.elapsed))); } if (first.epochMs > 0) { String ago = TimeUtils.printSince(first.epochMs); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java index b7a465333646..4ad817609b03 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -73,6 +74,11 @@ class HttpTab extends AbstractTableTab { this.probe = new HttpProbe(ctx); } + @Override + protected boolean selectsFirstRow() { + return true; + } + @Override protected int getRowCount() { return sortedVisibleEndpoints(ctx.findSelectedIntegration()).size(); @@ -314,6 +320,14 @@ class HttpTab extends AbstractTableTab { if (ep.management && !showManagement) { continue; } + if (filter == 0 && !ep.fromRest) { + // a REST service is served by a platform-http consumer of its own: one row for both + HttpEndpointInfo rest = restFor(info, ep); + if (rest != null) { + rest.hits = Math.max(rest.hits, ep.hits); + continue; + } + } if (filter == 1 && !ep.fromRest) { continue; } @@ -325,6 +339,21 @@ class HttpTab extends AbstractTableTab { return result; } + /** The REST service with the same method and path as a plain HTTP endpoint, or null. */ + static HttpEndpointInfo restFor(IntegrationInfo info, HttpEndpointInfo http) { + for (HttpEndpointInfo ep : info.httpEndpoints) { + if (ep.fromRest && !ep.specification && Objects.equals(ep.path, http.path) + && Objects.equals(normalizeMethod(ep.method), normalizeMethod(http.method))) { + return ep; + } + } + return null; + } + + private static String normalizeMethod(String method) { + return method != null ? method.trim().toUpperCase(Locale.ENGLISH) : ""; + } + static Style methodStyle(String method) { if (method == null) { return Style.EMPTY; @@ -350,7 +379,8 @@ class HttpTab extends AbstractTableTab { } long restCount = info.httpEndpoints.stream().filter(e -> e.fromRest && !e.specification).count(); long specCount = info.httpEndpoints.stream().filter(e -> e.specification).count(); - long httpCount = info.httpEndpoints.stream().filter(e -> !e.fromRest && !e.management).count(); + long httpCount = info.httpEndpoints.stream() + .filter(e -> !e.fromRest && !e.management && restFor(info, e) == null).count(); long mgmtCount = info.httpEndpoints.stream().filter(e -> e.management).count(); if (restCount > 0) { spans.add(Span.raw(" ")); 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 071a4624249b..c4457e82549d 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 @@ -159,6 +159,26 @@ class McpFacade { } // Tab name constants + /** + * The index of a main tab by its name, by the label the tab bar shows (Route, Endpoint) or by its number key, else + * -1. + */ + static int tabIndex(String tabName) { + if (tabName == null) { + return -1; + } + String name = tabName.trim(); + for (int i = 0; i < TAB_NAMES.length; i++) { + String tab = TAB_NAMES[i]; + String label = tab.endsWith("s") && !"Errors".equals(tab) ? tab.substring(0, tab.length() - 1) : tab; + String key = String.valueOf((i + 1) % 10); + if (tab.equalsIgnoreCase(name) || label.equalsIgnoreCase(name) || key.equals(name)) { + return i; + } + } + return -1; + } + static final String[] TAB_NAMES = { "Overview", "Source", "Log", "Activity", "Diagram", "Routes", "Endpoints", "Inspect", "Errors", "More" @@ -375,6 +395,11 @@ class McpFacade { // ---- Tab navigation ---- String navigateToTab(String tabName) { + int index = tabIndex(tabName); + if (index >= 0) { + bridge.handleTabKey(index); + return TAB_NAMES[index]; + } for (int i = 0; i < TAB_NAMES.length; i++) { if (TAB_NAMES[i].equalsIgnoreCase(tabName)) { bridge.handleTabKey(i); 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 8b3cc0321f01..e0f121cdc16d 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 @@ -962,7 +962,7 @@ class RoutesTab extends AbstractTab { if (showDescription && route.description != null && !route.description.isBlank()) { return route.description; } - return route.from != null ? route.from : ""; + return route.from != null ? TuiHelper.displayUri(route.from) : ""; } /** @@ -1140,7 +1140,8 @@ class RoutesTab extends AbstractTab { } rows.add(Row.from( Cell.from(" route"), - Cell.from(Span.styled(route.from != null ? route.from : route.routeId, routeStyle)), + Cell.from(Span.styled(route.from != null ? TuiHelper.displayUri(route.from) : route.routeId, + routeStyle)), rightCell(formatThroughput(route.throughput), 8), rightCell(String.valueOf(route.total), 8), rightCell(String.valueOf(route.failed), 6, diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java index e9cebdb3cdeb..c41fc8e2a530 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java @@ -25,6 +25,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.apache.camel.dsl.jbang.core.common.ProcessHelper; @@ -362,12 +363,31 @@ final class StatusParser { hc.message = details.getString("failure.error.message"); } } - info.healthChecks.add(hc); + addHealthCheck(info.healthChecks, hc); } } } } + /** + * A check that runs for both readiness and liveness (context) is reported once per kind: one row for it, with the + * worse state. + */ + static void addHealthCheck(List<HealthCheckInfo> checks, HealthCheckInfo hc) { + for (HealthCheckInfo known : checks) { + if (Objects.equals(known.group, hc.group) && Objects.equals(known.name, hc.name)) { + known.readiness |= hc.readiness; + known.liveness |= hc.liveness; + if (!"DOWN".equals(known.state) && hc.state != null && !"UP".equals(hc.state)) { + known.state = hc.state; + known.message = hc.message != null ? hc.message : known.message; + } + return; + } + } + checks.add(hc); + } + /** * Consumers. */ diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java index 209997c8038c..4fd9123896f8 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java @@ -17,6 +17,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.io.IOException; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -146,6 +147,22 @@ final class TuiHelper { return slash >= 0 ? name.substring(slash + 1) : name; } + /** + * An endpoint URI as people write it: Camel normalizes URIs with their placeholders and spaces percent-encoded + * (platform-http:///stock/%7Bsku%7D), which reads poorly in a table. Only for display; the URI itself stays the + * key. + */ + static String displayUri(String uri) { + if (uri == null || uri.indexOf('%') < 0) { + return uri; + } + try { + return URLDecoder.decode(uri.replace("+", "%2B"), StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + return uri; + } + } + static String truncate(String s, int max) { if (s == null) { return ""; @@ -583,7 +600,10 @@ final class TuiHelper { if (minutes > 0) { return minutes + "m" + (seconds % 60) + "s"; } - return seconds + "s" + (ms % 1000) + "ms"; + // 2.02s reads better than 2s20ms; a tenth is enough from 10 seconds on + return seconds < 10 + ? String.format(Locale.US, "%.2fs", ms / 1000.0) + : String.format(Locale.US, "%.1fs", ms / 1000.0); } static String formatLoad(String l1, String l5, String l15) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DisplayFormatsTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DisplayFormatsTest.java new file mode 100644 index 000000000000..22b98d2f5908 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DisplayFormatsTest.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.tui; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * How the tables show URIs, durations and exchange ids. + */ +class DisplayFormatsTest { + + @Test + void endpointUrisReadAsWritten() { + assertThat(TuiHelper.displayUri("platform-http:///stock/%7Bsku%7D?httpMethodRestrict=GET")) + .isEqualTo("platform-http:///stock/{sku}?httpMethodRestrict=GET"); + assertThat(TuiHelper.displayUri("file://parked?fileName=%24%7Bheader.CamelFileName%7D")) + .isEqualTo("file://parked?fileName=${header.CamelFileName}"); + // a plus stays a plus, and a URI that is not encoded is kept + assertThat(TuiHelper.displayUri("sql:select a+b from t%20x")).isEqualTo("sql:select a+b from t x"); + assertThat(TuiHelper.displayUri("timer:tick?period=1000")).isEqualTo("timer:tick?period=1000"); + assertThat(TuiHelper.displayUri("bad%zz")).isEqualTo("bad%zz"); + assertThat(TuiHelper.displayUri(null)).isNull(); + } + + @Test + void durationsReadAsSeconds() { + assertThat(TuiHelper.formatDurationMs(17)).isEqualTo("17ms"); + assertThat(TuiHelper.formatDurationMs(2020)).isEqualTo("2.02s"); + assertThat(TuiHelper.formatDurationMs(12345)).isEqualTo("12.3s"); + assertThat(TuiHelper.formatDurationMs(125_000)).isEqualTo("2m5s"); + } + + @Test + void exchangeIdsByTheirCounter() { + assertThat(ActivityTab.shortExchangeId("AD50EEC2B14A8A3-0000000000000001")).isEqualTo("AD50EE…-1"); + assertThat(ActivityTab.shortExchangeId("AD50EEC2B14A8A3-000000000000000A")).isEqualTo("AD50EE…-A"); + assertThat(ActivityTab.shortExchangeId("AD50EEC2B14A8A3-0000000000000000")).isEqualTo("AD50EE…-0"); + assertThat(ActivityTab.shortExchangeId("no-dash-")).isEqualTo("no-dash-"); + assertThat(ActivityTab.shortExchangeId("plain")).isEqualTo("plain"); + assertThat(ActivityTab.shortExchangeId(null)).isEmpty(); + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java index d9f09dc77c34..5c99b27db1a6 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java @@ -83,6 +83,8 @@ class EndpointsTabRenderTest { @Test void renderComponentNameInCyan() { addEndpoint("http", "http://example.com/api", "out", "http-route", 5); + // a second row: the first is selected, and the selection highlight hides its colors + addEndpoint("http", "http://example.com/api2", "out", "http-route", 5); EndpointsTab tab = new EndpointsTab(ctx, new MetricsCollector()); @@ -98,6 +100,7 @@ class EndpointsTabRenderTest { @Test void renderInDirectionColor() { addEndpoint("timer", "timer://tick", "in", "route1", 10); + addEndpoint("timer", "timer://tock", "in", "route1", 10); EndpointsTab tab = new EndpointsTab(ctx, new MetricsCollector()); @@ -114,6 +117,7 @@ class EndpointsTabRenderTest { @Test void renderOutDirectionInCyan() { addEndpoint("log", "log://output", "out", "route1", 10); + addEndpoint("log", "log://output2", "out", "route1", 10); EndpointsTab tab = new EndpointsTab(ctx, new MetricsCollector()); 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 f69d33048add..3c42792afda3 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 @@ -30,6 +30,7 @@ import dev.tamboui.tui.event.KeyModifiers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -93,6 +94,8 @@ class ErrorsTabRenderTest { @Test void renderErrorRouteIdInCyan() { addError("ID-001", "my-route", "to1", "Exception", "fail", false); + // a second row: the first is selected, and the selection highlight hides its colors + addError("ID-002", "my-route", "to1", "Exception", "fail", false); ErrorsTab tab = new ErrorsTab(ctx); @@ -108,6 +111,7 @@ class ErrorsTabRenderTest { @Test void renderHandledTrueUsesGreenColor() { addError("ID-001", "route1", "to1", "Exception", "handled", true); + addError("ID-002", "route1", "to1", "Exception", "handled", true); ErrorsTab tab = new ErrorsTab(ctx); @@ -123,6 +127,7 @@ class ErrorsTabRenderTest { @Test void renderHandledFalseUsesRedColor() { addError("ID-001", "route1", "to1", "Exception", "unhandled", false); + addError("ID-002", "route1", "to1", "Exception", "unhandled", false); ErrorsTab tab = new ErrorsTab(ctx); @@ -135,6 +140,15 @@ class ErrorsTabRenderTest { assertTrue(foundRed, "handled=false should be rendered in LIGHT_RED"); } + @Test + void theFirstErrorIsSelectedSoItsDetailShows() { + addError("ID-001", "route1", "to1", "Exception", "fail", false); + ErrorsTab tab = new ErrorsTab(ctx); + String rendered = renderToString(tab, 160, 30); + assertEquals(0, tab.tableState.selected()); + assertTrue(rendered.contains("ID-001"), rendered); + } + @Test void renderMultipleErrorsAllAppear() { addError("ID-AAA", "route-a", "to1", "IOException", "fail1", false); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTabRenderTest.java index 978e06f304f6..2b8eb52a2f56 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTabRenderTest.java @@ -30,6 +30,7 @@ import dev.tamboui.tui.event.KeyModifiers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -195,6 +196,23 @@ class HttpTabRenderTest { // ---- Helper methods ---- + @Test + void aRestServiceAndItsHttpConsumerAreOneRow() { + HttpEndpointInfo rest = addHttpEndpoint("GET", "/stock/{sku}", "http://0.0.0.0:8080/stock/{sku}"); + rest.fromRest = true; + HttpEndpointInfo http = addHttpEndpoint("GET", "/stock/{sku}", "http://0.0.0.0:8080/stock/{sku}"); + http.hits = 6; + addHttpEndpoint("POST", "/orders", "http://0.0.0.0:8080/orders"); + + HttpTab tab = new HttpTab(ctx); + List<HttpEndpointInfo> rows = tab.sortedVisibleEndpoints(info); + assertEquals(2, rows.size(), "the REST service and the plain POST"); + assertTrue(rows.contains(rest)); + assertEquals(6, rest.hits, "the hits of its consumer"); + String rendered = TuiTestHelper.renderToString(tab, 140, 30); + assertTrue(rendered.contains("REST: 1 HTTP: 1"), rendered); + } + private HttpEndpointInfo addHttpEndpoint(String method, String path, String url) { HttpEndpointInfo ep = new HttpEndpointInfo(); ep.method = method; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TablePolishTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TablePolishTest.java new file mode 100644 index 000000000000..206c48b95ae0 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TablePolishTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.tui; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Small things that make the tables read right: one row per health check, and tabs found by the label they show. + */ +class TablePolishTest { + + @Test + void aCheckReportedForReadinessAndLivenessIsOneRow() { + List<HealthCheckInfo> checks = new ArrayList<>(); + StatusParser.addHealthCheck(checks, check("context", "UP", true, false)); + StatusParser.addHealthCheck(checks, check("route-controller", "UP", true, false)); + StatusParser.addHealthCheck(checks, check("context", "DOWN", false, true)); + + assertThat(checks).hasSize(2); + HealthCheckInfo context = checks.get(0); + assertThat(context.readiness).isTrue(); + assertThat(context.liveness).isTrue(); + assertThat(context.state).as("the worse state").isEqualTo("DOWN"); + } + + @Test + void tabsAreFoundByTheLabelTheBarShows() { + assertThat(McpFacade.tabIndex("Route")).isEqualTo(McpFacade.tabIndex("Routes")).isEqualTo(5); + assertThat(McpFacade.tabIndex("endpoint")).isEqualTo(6); + assertThat(McpFacade.tabIndex("Errors")).isEqualTo(8); + assertThat(McpFacade.tabIndex("1")).isZero(); + assertThat(McpFacade.tabIndex("0")).isEqualTo(9); + assertThat(McpFacade.tabIndex("Health")).as("a More tab").isEqualTo(-1); + } + + private static HealthCheckInfo check(String name, String state, boolean readiness, boolean liveness) { + HealthCheckInfo hc = new HealthCheckInfo(); + hc.group = "camel"; + hc.name = name; + hc.state = state; + hc.readiness = readiness; + hc.liveness = liveness; + return hc; + } +}
