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 62406fe880ee 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 (#29500)
62406fe880ee is described below
commit 62406fe880ee84ab40462de6437c970327d4a655
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 23:14:26 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 (#29500)
Co-Authored-By: Claude Opus 5.5 <[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 | 28 +++++++++++++++++++++
.../core/commands/tui/ConsumersTabRenderTest.java | 15 +++++++++++
.../core/commands/tui/OverviewTabRenderTest.java | 15 +++++++++++
8 files changed, 116 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 b1589490e721..c0ffd00c9067 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
@@ -48,9 +48,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;
@@ -209,11 +211,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);
@@ -537,11 +541,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 8d8e42995e13..aaad494296c4 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
@@ -630,6 +630,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;
@@ -639,7 +640,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())),
@@ -707,7 +708,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);
@@ -771,7 +772,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]),
@@ -779,9 +780,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);
}
@@ -789,6 +789,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 b1339720846c..41318670edc2 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..cd891b9551dd 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,8 @@ 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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -74,6 +76,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);
+ assertFalse(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;