This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new c6d626d9edf7 chore: camel-jbang - tables read better: a first row
selected, URIs and durations as written, Activity newest first with a true rate
(#27224)
c6d626d9edf7 is described below
commit c6d626d9edf7161c5f22294ced19668a9ea52d9d
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 19:48:21 2026 +0200
chore: camel-jbang - tables read better: a first row selected, URIs and
durations as written, Activity newest first with a true rate (#27224)
Co-Authored-By: Claude Opus 5.5 (1M context) <[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 5d41814776ac..09d6b152d7ac 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
@@ -2134,7 +2134,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 c326da71300b..e4293c712ae8 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 f7330766e055..9df917f0bdc1 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;
+ }
+}