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;

Reply via email to