This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-26864-tui-walk-inspect
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 50f60d3541918d9c3226ca1c52e38f2aef2b4765
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 11 20:56:06 2026 +0100

    CAMEL-26864: camel-jbang - Consumers has the total of an event-driven 
consumer, and the Info panel of Inspect says what a step is and changed
    
    - Consumers: a TOTAL column with the messages each consumer received (the 
exchanges of its route), also for direct and seda, which have no POLLS
    - Inspect diagram Info panel: message in / message out / to a remote 
endpoint instead of the marker *->, and Changed in words: (first step), 
nothing, or body, headers
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ConsumerInfo.java  |  2 ++
 .../dsl/jbang/core/commands/tui/ConsumersTab.java  | 11 ++++--
 .../dsl/jbang/core/commands/tui/HistoryTab.java    | 41 +++++++++++++++++-----
 .../dsl/jbang/core/commands/tui/StatusParser.java  |  5 +++
 .../src/main/resources/tui/help/consumers.md       | 11 +++---
 .../core/commands/tui/ConsumersTabRenderTest.java  | 28 +++++++++++++++
 .../core/commands/tui/HistoryTabRenderTest.java    | 15 ++++++++
 7 files changed, 98 insertions(+), 15 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
index 403c83db4ed8..89290f9374ca 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
@@ -28,6 +28,8 @@ class ConsumerInfo {
     int inflight;
     Boolean polling;
     Long totalCounter;
+    // the messages its route received (the exchanges of the route), also for 
an event-driven consumer (CAMEL-26864)
+    long total = -1;
     Long delay;
     Long period;
     String schedule;
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 ab0e5a153642..d7985be91af8 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
@@ -42,7 +42,7 @@ import static 
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
 class ConsumersTab extends AbstractTableTab {
 
     ConsumersTab(MonitorContext ctx) {
-        super(ctx, "id", "status", "type", "remote", "inflight", "polls", 
"uri");
+        super(ctx, "id", "status", "type", "remote", "inflight", "polls", 
"total", "uri");
     }
 
     @Override
@@ -82,7 +82,7 @@ class ConsumersTab extends AbstractTableTab {
 
             // 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 - 9 - (20 + 10 + 
16 + 8 + 8 + 22 + 8 + 10 + 10));
+            int uriWidth = Math.max(0, area.width() - 2 - 3 - 10 - (20 + 10 + 
16 + 8 + 8 + 22 + 8 + 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)),
@@ -91,6 +91,7 @@ class ConsumersTab extends AbstractTableTab {
                     rightCell(String.valueOf(ci.inflight), 8),
                     Cell.from(schedule),
                     rightCell(ci.totalCounter != null ? 
String.valueOf(ci.totalCounter) : "", 8),
+                    rightCell(ci.total >= 0 ? String.valueOf(ci.total) : "", 
8),
                     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))));
@@ -110,6 +111,7 @@ 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")),
+                        rightCell(sortLabel("TOTAL", "total"), 8, 
sortStyle("total")),
                         rightCell("LAST", 10, Style.EMPTY.bold()),
                         rightCell("LAST FAIL", 10, Style.EMPTY.bold()),
                         Cell.from(Span.styled(sortLabel("URI", "uri"), 
sortStyle("uri")))))
@@ -121,6 +123,7 @@ class ConsumersTab extends AbstractTableTab {
                         Constraint.length(8),
                         Constraint.length(22),
                         Constraint.length(8),
+                        Constraint.length(8),
                         Constraint.length(10),
                         Constraint.length(10),
                         Constraint.fill())
@@ -154,6 +157,7 @@ class ConsumersTab extends AbstractTableTab {
                 long lb = b.totalCounter != null ? b.totalCounter : 0;
                 yield Long.compare(lb, la);
             }
+            case "total" -> Long.compare(b.total, a.total);
             case "uri" -> {
                 String ua = a.uri != null ? a.uri : "";
                 String ub = b.uri != null ? b.uri : "";
@@ -356,6 +360,9 @@ class ConsumersTab extends AbstractTableTab {
             if (ci.totalCounter != null) {
                 row.put("totalCounter", ci.totalCounter);
             }
+            if (ci.total >= 0) {
+                row.put("total", ci.total);
+            }
             if (ci.polling != null) {
                 row.put("polling", ci.polling);
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index 6345bae4b9dd..2395a6f2b725 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
@@ -25,6 +25,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.StringJoiner;
 import java.util.concurrent.atomic.AtomicReference;
 
 import dev.tamboui.layout.Alignment;
@@ -981,7 +982,7 @@ class HistoryTab extends AbstractTab {
             routeId = e.routeId;
             nodeId = e.nodeId;
             processor = e.processor;
-            direction = e.direction;
+            direction = stepKind(e.first, e.last, e.remoteEndpoint, 
e.stubEndpoint);
             timestamp = e.timestamp;
             threadName = e.threadName;
             elapsed = e.elapsed;
@@ -1005,7 +1006,7 @@ class HistoryTab extends AbstractTab {
             routeId = e.routeId;
             nodeId = e.nodeId;
             processor = e.processor;
-            direction = e.direction;
+            direction = stepKind(e.first, e.last, e.remoteEndpoint, 
e.stubEndpoint);
             timestamp = e.timestamp;
             threadName = e.threadName;
             elapsed = e.elapsed;
@@ -1091,12 +1092,7 @@ class HistoryTab extends AbstractTab {
         boolean propsChanged = changes.length() > 2 && changes.charAt(2) == 
'P';
         boolean varsChanged = changes.length() > 3 && changes.charAt(3) == 'V';
 
-        List<Span> changeSpans = new ArrayList<>();
-        changeSpans.add(Span.styled(" Changed:  ", Theme.muted()));
-        if (!changes.isBlank()) {
-            changeSpans.addAll(buildChangeSpans(changes));
-        }
-        lines.add(Line.from(changeSpans));
+        lines.add(Line.from(Span.styled(" Changed:  ", Theme.muted()), 
changedWords(changes, stepIdx == 0)));
 
         boolean isTraceMode = !diagramTraceSteps.isEmpty();
         boolean showBody = isTraceMode ? showTraceBody : showHistoryBody;
@@ -1931,6 +1927,35 @@ class HistoryTab extends AbstractTab {
                 rightCell(elapsedStr, 10));
     }
 
+    /**
+     * What a step is, in words, for the Info panel; the table has the markers 
of camel get history ({@code *->},
+     * {@code <-*}, {@code --->}, {@code ~-->}) in a column of its own. Empty 
for a step inside the route.
+     */
+    static String stepKind(boolean first, boolean last, boolean remote, 
boolean stub) {
+        String where = stub ? " (stub)" : remote ? " (remote)" : "";
+        if (first) {
+            return "message in" + where;
+        } else if (last) {
+            return "message out" + where;
+        }
+        return stub ? "to a stub" : remote ? "to a remote endpoint" : "";
+    }
+
+    /** What the step changed, in words (the table has the letters B, H, P and 
V): "body, headers", or nothing. */
+    static Span changedWords(String changes, boolean firstStep) {
+        if (firstStep) {
+            return Span.styled("(first step)", Style.EMPTY.dim());
+        }
+        String[] words = { "body", "headers", "properties", "variables" };
+        StringJoiner sj = new StringJoiner(", ");
+        for (int i = 0; i < Math.min(changes.length(), words.length); i++) {
+            if (changes.charAt(i) != ' ') {
+                sj.add(words[i]);
+            }
+        }
+        return sj.length() > 0 ? Span.styled(sj.toString(), Theme.change()) : 
Span.styled("nothing", Style.EMPTY.dim());
+    }
+
     private static List<Span> buildChangeSpans(String changes) {
         if (changes == null || changes.isEmpty()) {
             return List.of(Span.raw(""));
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 b549c4d05c8b..2434fb444959 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
@@ -478,6 +478,11 @@ final class StatusParser {
                     ci.inflight = cj.getIntegerOrDefault("inflight", 0);
                     ci.polling = Boolean.TRUE.equals(cj.get("polling"));
                     ci.totalCounter = cj.getLong("totalCounter");
+                    for (RouteInfo r : info.routes) {
+                        if (r.routeId != null && r.routeId.equals(ci.id)) {
+                            ci.total = r.total;
+                        }
+                    }
                     ci.delay = cj.getLong("delay");
                     ci.period = cj.getLong("period");
                     JsonObject cStats = (JsonObject) cj.get("statistics");
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 34bf23e72b4c..e59e3c30dee3 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
@@ -17,6 +17,7 @@ There are two types of consumers:
 - **STATUS** — Consumer state: `Started` (running normally), `Polling` 
(currently in a poll cycle). Shows a health warning icon if the consumer health 
check is `DOWN`
 - **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
+- **TOTAL** — The messages the consumer received: the exchanges of its route. 
It is there for every consumer, also an event-driven one (`direct`, `seda`) 
that has no POLLS
 - **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
 - **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
@@ -25,13 +26,13 @@ There are two types of consumers:
 ## Example Screen
 
 ```
- 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
+ ROUTE           STATUS   TYPE   REMOTE  INFLIGHT  SCHEDULE  POLLS  TOTAL    
LAST  LAST FAIL  URI
+ timer-to-log    Started  timer                 0  2s           17     17  1s 
ago             timer://hello?period=2000
+ timer-to-seda   Started  timer                 0  3s           12     12  0s 
ago     5m ago  timer://pump?period=3000
+ seda-consumer   Started  seda                  0                      12  0s 
ago             seda://queue
 ```
 
-Notice that `seda-consumer` has no POLLS or SCHEDULE because it is
+Notice that `seda-consumer` has no POLLS or SCHEDULE, only a TOTAL, because it 
is
 event-driven — it reacts immediately when a message is put into the
 SEDA queue by another route.
 
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 5f1273f0c2e2..68d492f63336 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
@@ -27,6 +27,9 @@ import dev.tamboui.terminal.Frame;
 import dev.tamboui.text.Span;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.KeyModifiers;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -243,6 +246,31 @@ class ConsumersTabRenderTest {
         assertThat(ConsumersTab.ago("-")).isEmpty();
     }
 
+    @Test
+    void anEventDrivenConsumerHasTheTotalOfItsRoute() throws Exception {
+        // CAMEL-26864: the direct consumer of route seven received 200 
messages, but had no POLLS and no count at all
+        JsonObject root = (JsonObject) Jsoner.deserialize("""
+                {"context": {"name": "hello"},
+                 "routes": [{"routeId": "seven", "statistics": 
{"exchangesTotal": 200}},
+                            {"routeId": "hello", "statistics": 
{"exchangesTotal": 1131}}],
+                 "consumers": {"consumers": [
+                    {"id": "seven", "uri": "direct://seven", "state": 
"Started"},
+                    {"id": "hello", "uri": "timer://tick", "state": "Started", 
"totalCounter": 1131}]}}
+                """);
+        IntegrationInfo parsed = 
StatusParser.parseIntegration(ProcessHandle.current(), root);
+        info.consumers.addAll(parsed.consumers);
+
+        ConsumersTab tab = new ConsumersTab(ctx);
+        String rendered = TuiTestHelper.renderToString(tab, 200, 10);
+        assertThat(rendered).contains("TOTAL");
+        String seven = rendered.lines().filter(l -> 
l.contains("direct://seven")).findFirst().orElseThrow();
+        assertThat(seven).contains(" 200 ");
+        JsonObject row = (JsonObject) ((JsonArray) 
tab.getTableDataAsJson().get("rows")).get(1);
+        assertThat(row.getString("id")).isEqualTo("seven");
+        assertThat(row.getLong("total")).isEqualTo(200L);
+        assertThat(row.containsKey("totalCounter")).as("a direct consumer does 
not poll").isFalse();
+    }
+
     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/HistoryTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java
index 92d762487bc7..b36a21353f6d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java
@@ -289,4 +289,19 @@ class HistoryTabRenderTest {
     private static HistoryTab.WaterfallStep step(String id, boolean first, 
boolean last, long elapsed, long start) {
         return new HistoryTab.WaterfallStep(id, id, "-->", first, last, 0, 
elapsed, 0, start);
     }
+
+    @Test
+    void theInfoPanelSaysWhatAStepIsAndWhatItChangedInWords() {
+        // CAMEL-26864: it showed the marker of the table ("Step: 1/6 *->") 
and an empty "Changed:"
+        assertEquals("message in", HistoryTab.stepKind(true, false, false, 
false));
+        assertEquals("message in (remote)", HistoryTab.stepKind(true, false, 
true, false));
+        assertEquals("message out", HistoryTab.stepKind(false, true, false, 
false));
+        assertEquals("to a remote endpoint", HistoryTab.stepKind(false, false, 
true, false));
+        assertEquals("to a stub", HistoryTab.stepKind(false, false, true, 
true));
+        assertEquals("", HistoryTab.stepKind(false, false, false, false));
+
+        assertEquals("(first step)", HistoryTab.changedWords("", 
true).content());
+        assertEquals("nothing", HistoryTab.changedWords("    ", 
false).content());
+        assertEquals("body, properties", HistoryTab.changedWords("B P ", 
false).content());
+    }
 }

Reply via email to