This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch chore-tui-tables-polish in repository https://gitbox.apache.org/repos/asf/camel.git
commit e0ffd570bd34a9101faef6b686420623c19f1041 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 9 17:05:45 2026 +0100 chore: camel-jbang - Beans starts with the beans of the integration, Consumers says LAST and LAST FAIL, and the numbers of Integrations stand next to the names - Beans: the scope starts at user, the beans of the integration itself: the 11 or so of Camel are the same in every integration. With none of its own, the table says how many Camel has and that f shows them; tui_get_table gives the scope and how many beans there are in all. - Consumers: SINCE-LAST was three times in one cell (0s/0s/-, started, completed, failed). It is LAST (the last exchange done) and LAST FAIL (in red, blank when none failed), as "1s ago". - Integrations: NAME took the rest of the width, so on a wide screen the numbers stood far from the names. NAME is as wide as the names need, and the percentile bar after the timings gets the rest. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/tui/BeansTab.java | 17 +++++++++---- .../dsl/jbang/core/commands/tui/ConsumersTab.java | 24 ++++++++++-------- .../dsl/jbang/core/commands/tui/OverviewTab.java | 29 ++++++++++++++++++---- .../src/main/resources/tui/help/beans.md | 4 ++- .../src/main/resources/tui/help/consumers.md | 11 ++++---- .../core/commands/tui/BeansTabRenderTest.java | 27 ++++++++++++++++++++ .../core/commands/tui/ConsumersTabRenderTest.java | 15 +++++++++++ .../core/commands/tui/OverviewTabRenderTest.java | 15 +++++++++++ 8 files changed, 115 insertions(+), 27 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java index 426f77ec21de..a4e70a9eeab4 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java @@ -49,9 +49,11 @@ class BeansTab extends AbstractTableTab { private final AtomicBoolean loading = new AtomicBoolean(false); - private int filterIndex; + // the scope starts with the beans of the integration itself (user): the ones of Camel are many and the same in + // every integration + private int filterIndex = 1; private final FilterField filter = new FilterField(this::filterChanged); - private List<BeanData> allBeans = Collections.emptyList(); + List<BeanData> allBeans = Collections.emptyList(); private int detailScroll; private boolean detailFocused; private String lastPid; @@ -204,11 +206,13 @@ class BeansTab extends AbstractTableTab { Cell.from(Span.styled(shortType, Style.EMPTY)))); } + String mode = filterModes()[filterIndex]; if (rows.isEmpty()) { - rows.add(emptyRow("No beans", 2)); + rows.add(emptyRow("user".equals(mode) && !allBeans.isEmpty() && filter.term() == null + ? "No beans of its own: f shows the " + allBeans.size() + " of Camel" + : "No beans", 2)); } - String mode = filterModes()[filterIndex]; String title = filter.term() != null ? String.format(" Beans [%d] scope:%s filter:\"%s\" ", visible.size(), mode, filter.term()) : String.format(" Beans [%d] scope:%s ", visible.size(), mode); @@ -531,11 +535,14 @@ class BeansTab extends AbstractTableTab { @Override public JsonObject getTableDataAsJson() { List<BeanData> beans = sortedBeans(); - if (beans.isEmpty()) { + if (allBeans.isEmpty()) { return null; } JsonObject result = new JsonObject(); result.put("tab", "Beans"); + // the rows are those of the scope (f): the beans of the integration itself at first, not those of Camel + result.put("scope", filterModes()[filterIndex]); + result.put("allBeans", allBeans.size()); JsonArray rows = new JsonArray(); for (BeanData b : beans) { 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/ConsumersTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java index 4f504c25bc0c..ab0e5a153642 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java @@ -74,14 +74,15 @@ class ConsumersTab extends AbstractTableTab { String statusText = healthDown ? TuiIcons.HEALTH_WARN + " " + status : status; String type = consumerType(ci); String schedule = consumerSchedule(ci); - String sinceLast = consumerSinceLast(ci); + String last = ago(ci.sinceLastCompleted != null ? ci.sinceLastCompleted : ci.sinceLastStarted); + String lastFail = ago(ci.sinceLastFailed); String uri = healthDown && hc.message != null ? hc.message : (ci.uri != null ? ci.uri : ""); // the URI gets the rest of the width: the borders, ">> ", 8 gaps and the other columns; a long id or URI // ends with an ellipsis where it is cut - int uriWidth = Math.max(0, area.width() - 2 - 3 - 8 - (20 + 10 + 16 + 8 + 8 + 22 + 8 + 22)); + int uriWidth = Math.max(0, area.width() - 2 - 3 - 9 - (20 + 10 + 16 + 8 + 8 + 22 + 8 + 10 + 10)); rows.add(Row.from( Cell.from(Span.styled(" " + TuiHelper.truncate(ci.id, 19), Style.EMPTY.fg(Theme.accent()))), Cell.from(Span.styled(statusText, statusStyle)), @@ -90,12 +91,13 @@ class ConsumersTab extends AbstractTableTab { rightCell(String.valueOf(ci.inflight), 8), Cell.from(schedule), rightCell(ci.totalCounter != null ? String.valueOf(ci.totalCounter) : "", 8), - Cell.from(sinceLast), + rightCell(last, 10), + lastFail.isEmpty() ? rightCell("", 10) : rightCell(lastFail, 10, Theme.error()), Cell.from(Span.styled(TuiHelper.truncate(uri, uriWidth), healthDown ? Theme.error() : Style.EMPTY)))); } if (rows.isEmpty()) { - rows.add(emptyRow("No consumers", 9)); + rows.add(emptyRow("No consumers", 10)); } Table table = Table.builder() @@ -108,7 +110,8 @@ class ConsumersTab extends AbstractTableTab { rightCell(sortLabel("INFLIGHT", "inflight"), 8, sortStyle("inflight")), Cell.from(Span.styled("SCHEDULE", Style.EMPTY.bold())), rightCell(sortLabel("POLLS", "polls"), 8, sortStyle("polls")), - Cell.from(Span.styled("SINCE-LAST", Style.EMPTY.bold())), + rightCell("LAST", 10, Style.EMPTY.bold()), + rightCell("LAST FAIL", 10, Style.EMPTY.bold()), Cell.from(Span.styled(sortLabel("URI", "uri"), sortStyle("uri"))))) .widths( Constraint.length(20), @@ -118,7 +121,8 @@ class ConsumersTab extends AbstractTableTab { Constraint.length(8), Constraint.length(22), Constraint.length(8), - Constraint.length(22), + Constraint.length(10), + Constraint.length(10), Constraint.fill()) .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) @@ -278,11 +282,9 @@ class ConsumersTab extends AbstractTableTab { return ms + "ms"; } - private static String consumerSinceLast(ConsumerInfo ci) { - String s1 = ci.sinceLastStarted != null ? ci.sinceLastStarted : "-"; - String s2 = ci.sinceLastCompleted != null ? ci.sinceLastCompleted : "-"; - String s3 = ci.sinceLastFailed != null ? ci.sinceLastFailed : "-"; - return s1 + "/" + s2 + "/" + s3; + /** How long ago, from the time since the last exchange the integration says (1m5s): blank when there was none. */ + static String ago(String since) { + return since != null && !since.isBlank() && !"-".equals(since) ? since + " ago" : ""; } @Override 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 2774d4fd5e72..7fa332e94ce0 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 @@ -626,6 +626,7 @@ class OverviewTab extends AbstractTab { fixed[drop] = 0; nameWidth = overviewNameWidth(areaWidth, fixed); } + int nameColumn = Math.min(nameWidth, Math.max(MIN_NAME_WIDTH, namesWidth(infos) + 2)); for (IntegrationInfo info : infos) { boolean isEven = (rowIndex++ % 2 == 0); Style rowBg = isEven ? Style.EMPTY.bg(Theme.zebra()) : Style.EMPTY; @@ -635,7 +636,7 @@ class OverviewTab extends AbstractTab { String nameText = platformIcon + " " + (info.name != null ? info.name : ""); rows.add(Row.from( Cell.from(Span.styled("-", Theme.muted())), - Cell.from(Span.styled(TuiHelper.truncate(nameText, nameWidth), Theme.info())), + Cell.from(Span.styled(TuiHelper.truncate(nameText, nameColumn), Theme.info())), Cell.from(Span.styled("", Theme.muted())), Cell.from(Span.styled("", Theme.muted())), Cell.from(projectStatus(info, MonotonicClock.millis())), @@ -703,7 +704,7 @@ class OverviewTab extends AbstractTab { if (hasDoc) { nameSpans.add(Span.styled(" " + TuiIcons.README, Style.EMPTY)); } - Line nameLine = TuiHelper.fitLine(nameSpans, nameWidth); + Line nameLine = TuiHelper.fitLine(nameSpans, nameColumn); String throughputDisplay = info.throughput; if (throughputDisplay == null || "0.00".equals(throughputDisplay)) { LinkedList<Long> tpHist = throughputHistory.get(info.pid); @@ -767,7 +768,7 @@ class OverviewTab extends AbstractTab { Constraint[] widths = new Constraint[] { Constraint.length(fixed[0]), - Constraint.fill(), + Constraint.length(nameColumn), Constraint.length(fixed[1]), Constraint.length(fixed[2]), Constraint.length(fixed[3]), @@ -775,9 +776,8 @@ class OverviewTab extends AbstractTab { Constraint.length(fixed[5]), Constraint.length(fixed[6]), Constraint.length(fixed[7]), - // fixed, so NAME gets the rest of the width, as overviewNameWidth counts it Constraint.length(fixed[8]), - Constraint.length(0) + Constraint.fill() }; return new TableSpec(rows, header, widths); } @@ -785,6 +785,25 @@ class OverviewTab extends AbstractTab { // the narrowest NAME before other columns give way: room for an icon and a name static final int MIN_NAME_WIDTH = 20; + /** + * The width the names take: the runtime icon, the name, the project it was opened as, [dev] and the readme mark. + * NAME is as wide as that, so the numbers stand next to the names on a wide screen; the rest goes after the last + * column. + */ + static int namesWidth(List<IntegrationInfo> infos) { + int width = 0; + for (IntegrationInfo info : infos) { + String name = info.name != null ? info.name : ""; + int w = CharWidth.of(TuiIcons.runtimeIcon(info.platform != null ? info.platform : "")) + 1 + CharWidth.of(name); + if (info.openedAs != null && !info.openedAs.equalsIgnoreCase(info.name)) { + w += 3 + CharWidth.of(info.openedAs); + } + w += (info.devMode ? 6 : 0) + 1 + CharWidth.of(TuiIcons.README); + width = Math.max(width, w); + } + return width; + } + /** The width the NAME column gets beside the other columns: the table's borders, ">> " and 1 between columns. */ static int overviewNameWidth(int areaWidth, int[] fixed) { int used = 2 + 3 + 10; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/beans.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/beans.md index 13df2295d85b..08cf4b22214f 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/beans.md +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/beans.md @@ -27,7 +27,9 @@ right options. ## Filter Modes -Press `f` to cycle through filter modes: +The tab starts with **user**, the beans of your application: the ones of +Camel are many and the same in every integration. When there are none, +the table says how many Camel has. Press `f` to cycle through filter modes: - **all** — show all beans (including framework internals) - **user** — only your application beans (excludes Camel, Spring, Quarkus, and JDK beans) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/consumers.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/consumers.md index 91deb593108a..34bf23e72b4c 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/consumers.md +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/consumers.md @@ -18,16 +18,17 @@ There are two types of consumers: - **TYPE** — The Camel component type (e.g., `kafka`, `file`, `timer`, `cron`, `seda`) - **POLLS** — Total number of poll cycles the scheduler has executed. This counts poll **attempts**, not messages received — a poll may check for new files and find none. For event-driven consumers (like `seda`), this column is empty - **SCHEDULE** — How often the consumer polls: a fixed interval like `1s` or `5000ms`, or a cron expression like `0 0/5 * * *`. Only shown for polling consumers -- **SINCE-LAST** — Three timestamps showing **started** / **completed** / **failed**: when the last exchange was created, when the last exchange completed successfully, and when the last exchange failed. Helps identify stale consumers or recurring failures +- **LAST** — How long ago the consumer's last exchange completed (or started, when none has completed yet). A consumer that should be busy but has a long LAST is stale +- **LAST FAIL** — How long ago its last exchange failed, in red; blank when none did - **URI** — The full endpoint URI (e.g., `timer://hello?period=2000`). If the consumer health check is DOWN, the failure message is shown here instead ## Example Screen ``` - ROUTE STATUS TYPE POLLS SCHEDULE SINCE-LAST URI - timer-to-log Started timer 17 2s 1s/1s/- timer://hello?period=2000 - timer-to-seda Started timer 12 3s 0s/0s/- timer://pump?period=3000 - seda-consumer Started seda 0s/0s/- seda://queue + ROUTE STATUS TYPE POLLS SCHEDULE LAST LAST FAIL URI + timer-to-log Started timer 17 2s 1s ago timer://hello?period=2000 + timer-to-seda Started timer 12 3s 0s ago 5m ago timer://pump?period=3000 + seda-consumer Started seda 0s ago seda://queue ``` Notice that `seda-consumer` has no POLLS or SCHEDULE because it is diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTabRenderTest.java index 422feb74d9d4..2b0fe9407d05 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTabRenderTest.java @@ -24,6 +24,7 @@ import dev.tamboui.text.Span; 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; /** @@ -74,6 +75,32 @@ class BeansTabRenderTest { assertTrue(rendered.contains("No beans"), "Should show 'No beans' when no bean data is loaded"); } + @Test + void theBeansOfTheIntegrationComeFirst() { + BeansTab tab = new BeansTab(ctx); + tab.allBeans = List.of(bean("global-variable-repository", "org.apache.camel.impl.engine.GlobalVariableRepository"), + bean("DependencyDownloaderStrategy", "org.apache.camel.main.download.DependencyDownloaderStrategy")); + + // none of its own: the table says so, and how to see the ones of Camel + String rendered = TuiTestHelper.renderToString(tab, 120, 20); + assertTrue(rendered.contains("scope:user"), rendered); + assertTrue(rendered.contains("No beans of its own: f shows the 2 of Camel"), rendered); + assertEquals("user", tab.getTableDataAsJson().getString("scope")); + assertEquals(2, tab.getTableDataAsJson().getInteger("allBeans")); + + tab.allBeans = List.of(tab.allBeans.get(0), bean("orderService", "com.acme.OrderService")); + rendered = TuiTestHelper.renderToString(tab, 120, 20); + assertTrue(rendered.contains("orderService"), rendered); + assertTrue(!rendered.contains("global-variable-repository"), rendered); + } + + private static BeansTab.BeanData bean(String name, String type) { + BeansTab.BeanData b = new BeansTab.BeanData(); + b.name = name; + b.type = type; + return b; + } + @Test void renderFooterHintsContainExpectedKeys() { BeansTab tab = new BeansTab(ctx); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java index 9d4cea9a0c35..5f1273f0c2e2 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java @@ -228,6 +228,21 @@ class ConsumersTabRenderTest { // ---- Helper methods ---- + @Test + void lastAndLastFailSayHowLongAgo() { + ConsumerInfo ci = addConsumer("stock-check", "timer://stock", "Started", "TimerConsumer"); + ci.sinceLastStarted = "0s"; + ci.sinceLastCompleted = "1s"; + ci.sinceLastFailed = "5m2s"; + + String rendered = TuiTestHelper.renderToString(new ConsumersTab(ctx), 200, 10); + + assertThat(rendered).contains("LAST FAIL", "1s ago", "5m2s ago").doesNotContain("0s/1s/5m2s"); + // none failed: blank + assertThat(ConsumersTab.ago(null)).isEmpty(); + assertThat(ConsumersTab.ago("-")).isEmpty(); + } + private ConsumerInfo addConsumer(String id, String uri, String state, String className) { ConsumerInfo ci = new ConsumerInfo(); ci.id = id; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java index 3e16f4a1d3e9..11e31f58c5e7 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java @@ -85,6 +85,21 @@ class OverviewTabRenderTest { assertTrue(rendered.contains("TOTAL"), "Should show TOTAL header"); } + @Test + void theNumbersStandNextToTheNamesOnAWideScreen() { + info.state = 5; + info.name = "circuit-breaker"; + + OverviewTab tab = new OverviewTab(ctx, new MetricsCollector(), new HashSet<>(), () -> { + }).showIntegrations(); + String header = TuiTestHelper.renderToString(tab, 200, 30).lines() + .filter(l -> l.contains("VERSION")).findFirst().orElseThrow(); + + // NAME is as wide as the names need, not the rest of a 200 wide screen + int name = header.indexOf("NAME"); + assertTrue(header.indexOf("VERSION") - name < 30, header); + } + @Test void tableDataExportsUptimeAsDurationNotStartTimestamp() { info.state = 5;
