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 bc74c9cad682 CAMEL-24259: TUI - Add Producers, Events, Route
Controller tabs and fix inline docs
bc74c9cad682 is described below
commit bc74c9cad6825930b3786ae1abf030e01222e1ed
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jul 26 15:54:38 2026 +0200
CAMEL-24259: TUI - Add Producers, Events, Route Controller tabs and fix
inline docs
Add three new TUI tabs in the More submenu: Producers, Events (exchange),
and Route Controller. Fix inline doc scroll bug where toggling docs caused
cursor-down to scroll selected line out of view. Fix multi-route inline
docs to load processor detail for all routes. Fix YAML parameters block
to document individual endpoint options with component name resolution.
Closes #25114
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../camel/cli/connector/LocalCliConnector.java | 8 +
.../dsl/jbang/core/commands/tui/DiagramTab.java | 37 ++-
.../dsl/jbang/core/commands/tui/EventInfo.java | 25 ++
.../dsl/jbang/core/commands/tui/EventTab.java | 234 ++++++++++++++++
.../jbang/core/commands/tui/IntegrationInfo.java | 7 +
.../dsl/jbang/core/commands/tui/ProducerInfo.java | 27 ++
.../dsl/jbang/core/commands/tui/ProducersTab.java | 234 ++++++++++++++++
.../core/commands/tui/RouteControllerInfo.java | 29 ++
.../core/commands/tui/RouteControllerTab.java | 293 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/RoutesTab.java | 178 +++++++++++--
.../dsl/jbang/core/commands/tui/SourceViewer.java | 40 ++-
.../dsl/jbang/core/commands/tui/StatusParser.java | 70 +++++
.../dsl/jbang/core/commands/tui/TabRegistry.java | 11 +
.../dsl/jbang/core/commands/tui/TuiIcons.java | 3 +
.../jbang/core/commands/tui/TabRegistryTest.java | 8 +-
15 files changed, 1149 insertions(+), 55 deletions(-)
diff --git
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index 290f2ec1c903..ad33fe6a3e93 100644
---
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -1529,6 +1529,14 @@ public class LocalCliConnector extends ServiceSupport
implements CliConnector, C
root.put("producers", json);
}
}
+ DevConsole dc14c = dcr.resolveById("route-controller");
+ if (dc14c != null) {
+ JsonObject json
+ = (JsonObject)
dc14c.call(DevConsole.MediaType.JSON, Map.of("stacktrace", "false"));
+ if (json != null && !json.isEmpty()) {
+ root.put("routeController", json);
+ }
+ }
DevConsole dc15 = dcr.resolveById("variables");
if (dc15 != null) {
JsonObject json = (JsonObject)
dc15.call(DevConsole.MediaType.JSON);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index 3e33db3fbddc..7c7ac2116bac 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -1201,17 +1201,16 @@ class DiagramTab extends AbstractTab {
detailScroll = 0;
}
- // load route detail once per route (covers all processors)
- if (drillDownRouteId != null &&
!drillDownRouteId.equals(cachedRouteDetailId)
- && !drillDownRouteId.equals(detailLoadingRouteId)) {
- detailLoadingRouteId = drillDownRouteId;
+ // load route detail once (covers all routes and processors)
+ if (drillDownRouteId != null && cachedRouteDetail == null
+ && !"*".equals(detailLoadingRouteId)) {
+ detailLoadingRouteId = "*";
detailLoading = true;
- String rid = drillDownRouteId;
if (ctx.runner != null) {
ctx.backgroundExecutor.execute(() -> {
- JsonObject result = requestRouteProcessorDetail(rid);
+ JsonObject result = requestRouteProcessorDetail("*");
cachedRouteDetail = result;
- cachedRouteDetailId = rid;
+ cachedRouteDetailId = "*";
detailLoading = false;
});
}
@@ -1397,12 +1396,7 @@ class DiagramTab extends AbstractTab {
if (cachedRouteDetail == null || nodeId == null) {
return null;
}
- JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
- if (processors == null) {
- return null;
- }
- for (Object obj : processors) {
- JsonObject p = (JsonObject) obj;
+ for (JsonObject p : RoutesTab.getAllProcessors(cachedRouteDetail)) {
if (nodeId.equals(p.getString("id"))) {
return p;
}
@@ -1417,7 +1411,7 @@ class DiagramTab extends AbstractTab {
try {
JsonObject root = new JsonObject();
root.put("action", "processor-detail");
- root.put("routeId", routeId);
+ root.put("routeId", "*");
return ctx.executeAction(ctx.selectedPid, root, 5000);
} catch (Exception e) {
return null;
@@ -1431,8 +1425,8 @@ class DiagramTab extends AbstractTab {
return Map.of();
}
- JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
- if (processors == null || processors.isEmpty()) {
+ List<JsonObject> processors =
RoutesTab.getAllProcessors(cachedRouteDetail);
+ if (processors.isEmpty()) {
return Map.of();
}
@@ -1440,8 +1434,7 @@ class DiagramTab extends AbstractTab {
CamelCatalog catalog = info != null ? getCatalog(info) : null;
Map<Integer, List<String>> result = new LinkedHashMap<>();
- for (Object obj : processors) {
- JsonObject proc = (JsonObject) obj;
+ for (JsonObject proc : processors) {
Integer line = proc.getInteger("line");
if (line == null || line <= 0) {
continue;
@@ -1465,14 +1458,14 @@ class DiagramTab extends AbstractTab {
private void ensureProcessorDetailLoaded(String routeId) {
if (routeId != null && cachedRouteDetail == null
- && !routeId.equals(detailLoadingRouteId)) {
- detailLoadingRouteId = routeId;
+ && !"*".equals(detailLoadingRouteId)) {
+ detailLoadingRouteId = "*";
detailLoading = true;
if (ctx.runner != null) {
ctx.backgroundExecutor.execute(() -> {
- JsonObject result = requestRouteProcessorDetail(routeId);
+ JsonObject result = requestRouteProcessorDetail("*");
cachedRouteDetail = result;
- cachedRouteDetailId = routeId;
+ cachedRouteDetailId = "*";
detailLoading = false;
});
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventInfo.java
new file mode 100644
index 000000000000..ed3893d11b79
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventInfo.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+class EventInfo {
+ String type;
+ String category;
+ long timestamp;
+ String exchangeId;
+ String message;
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
new file mode 100644
index 000000000000..0a9ad6be45cc
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
@@ -0,0 +1,234 @@
+/*
+ * 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 dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class EventTab extends AbstractTableTab {
+
+ EventTab(MonitorContext ctx) {
+ super(ctx, "timestamp", "category", "type", "message");
+ this.sort = "timestamp";
+ }
+
+ @Override
+ public void navigateUp() {
+ }
+
+ @Override
+ public void navigateDown() {
+ }
+
+ @Override
+ public boolean handleKeyEvent(KeyEvent ke) {
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)
+ || ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)
+ || ke.isHome() || ke.isEnd()) {
+ return false;
+ }
+ return super.handleKeyEvent(ke);
+ }
+
+ @Override
+ public boolean handleMouseEvent(MouseEvent me, Rect area) {
+ return false;
+ }
+
+ @Override
+ protected int getRowCount() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ return info != null ? info.events.size() : 0;
+ }
+
+ @Override
+ protected void renderContent(Frame frame, Rect area, IntegrationInfo info)
{
+ List<EventInfo> sorted = new ArrayList<>(info.events);
+ sorted.sort(this::sortEvent);
+
+ List<Row> rows = new ArrayList<>();
+ for (EventInfo ei : sorted) {
+ String ts = ei.timestamp > 0 ? TimeUtils.printSince(ei.timestamp)
: "";
+ Style categoryStyle = categoryStyle(ei.category);
+
+ rows.add(Row.from(
+ rightCell(ts, 12),
+ Cell.from(Span.styled(ei.category != null ? ei.category :
"", categoryStyle)),
+ Cell.from(Span.styled(ei.type != null ? ei.type : "",
Style.EMPTY.fg(Theme.accent()))),
+ Cell.from(ei.message != null ? ei.message : "")));
+ }
+
+ if (rows.isEmpty()) {
+ rows.add(emptyRow("No events", 4));
+ }
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ rightCell(sortLabel("AGO", "timestamp"), 12,
sortStyle("timestamp")),
+ Cell.from(Span.styled(sortLabel("CATEGORY",
"category"), sortStyle("category"))),
+ Cell.from(Span.styled(sortLabel("TYPE", "type"),
sortStyle("type"))),
+ Cell.from(Span.styled(sortLabel("MESSAGE", "message"),
sortStyle("message")))))
+ .widths(
+ Constraint.length(12),
+ Constraint.length(12),
+ Constraint.length(30),
+ Constraint.fill())
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Events ").build())
+ .build();
+
+ lastTableArea = area;
+ frame.renderStatefulWidget(table, area, tableState);
+ renderScrollbar(frame, sorted.size());
+ }
+
+ private int sortEvent(EventInfo a, EventInfo b) {
+ int result = switch (sort) {
+ case "category" -> {
+ String ca = a.category != null ? a.category : "";
+ String cb = b.category != null ? b.category : "";
+ yield ca.compareToIgnoreCase(cb);
+ }
+ case "type" -> {
+ String ta = a.type != null ? a.type : "";
+ String tb = b.type != null ? b.type : "";
+ yield ta.compareToIgnoreCase(tb);
+ }
+ case "message" -> {
+ String ma = a.message != null ? a.message : "";
+ String mb = b.message != null ? b.message : "";
+ yield ma.compareToIgnoreCase(mb);
+ }
+ default -> // "timestamp" — newest first by default
+ Long.compare(b.timestamp, a.timestamp);
+ };
+ return sortReversed ? -result : result;
+ }
+
+ private static Style categoryStyle(String category) {
+ if (category == null) {
+ return Style.EMPTY;
+ }
+ return switch (category) {
+ case "route" -> Style.EMPTY.fg(Theme.accent());
+ case "exchange" -> Theme.success();
+ default -> Style.EMPTY;
+ };
+ }
+
+ @Override
+ public SelectionContext getSelectionContext() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || info.events.isEmpty()) {
+ return null;
+ }
+ List<EventInfo> sorted = new ArrayList<>(info.events);
+ sorted.sort(this::sortEvent);
+ List<String> items = sorted.stream().map(e -> e.type != null ? e.type
: "").toList();
+ Integer sel = tableState.selected();
+ return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Events");
+ }
+
+ @Override
+ public String description() {
+ return "Camel lifecycle events (context, route, and exchange events)";
+ }
+
+ @Override
+ public String getHelpText() {
+ return """
+ # Events
+
+ Shows Camel lifecycle events captured by the event notifier.
Events are
+ grouped into three categories:
+
+ - **general** — Context-level events: CamelContext
starting/started/stopping,
+ service add/remove, component add/remove
+ - **route** — Route lifecycle events: route
added/removed/started/stopped/reloaded
+ - **exchange** — Exchange events: exchange
created/completed/failed/sending/sent
+
+ Events are stored in a circular buffer (default capacity 25
per category).
+ Only the most recent events are shown.
+
+ ## Table Columns
+
+ - **AGO** — How long ago the event occurred (e.g., `2s`,
`1m30s`)
+ - **CATEGORY** — Event category: `general`, `route`, or
`exchange`
+ - **TYPE** — The specific event type (e.g.,
`CamelContextStartedEvent`,
+ `RouteStartedEvent`, `ExchangeCompletedEvent`)
+ - **MESSAGE** — Human-readable event description
+
+ ## Keys
+
+ - `Up/Down` — select event
+ - `s` — cycle sort column
+ - `S` — reverse sort order
+ """;
+ }
+
+ @Override
+ public JsonObject getTableDataAsJson() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ return null;
+ }
+ JsonObject result = new JsonObject();
+ result.put("tab", "Events");
+ JsonArray rows = new JsonArray();
+ List<EventInfo> sorted = new ArrayList<>(info.events);
+ sorted.sort(this::sortEvent);
+ for (EventInfo ei : sorted) {
+ JsonObject row = new JsonObject();
+ row.put("type", ei.type);
+ row.put("category", ei.category);
+ if (ei.timestamp > 0) {
+ row.put("timestamp", ei.timestamp);
+ }
+ if (ei.exchangeId != null) {
+ row.put("exchangeId", ei.exchangeId);
+ }
+ row.put("message", ei.message);
+ rows.add(row);
+ }
+ result.put("rows", rows);
+ result.put("totalRows", info.events.size());
+ Integer sel = tableState.selected();
+ result.put("selectedIndex", sel != null ? sel : -1);
+ return result;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index 6598567a4198..88a2b654a942 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -80,6 +80,13 @@ class IntegrationInfo {
long vanishStart;
final List<RouteInfo> routes = new ArrayList<>();
final List<ConsumerInfo> consumers = new ArrayList<>();
+ final List<ProducerInfo> producers = new ArrayList<>();
+ final List<EventInfo> events = new ArrayList<>();
+ String routeControllerType;
+ boolean routeControllerUnhealthy;
+ int routeControllerRestartingRoutes;
+ int routeControllerExhaustedRoutes;
+ final List<RouteControllerInfo> routeControllerRoutes = new ArrayList<>();
final List<HealthCheckInfo> healthChecks = new ArrayList<>();
final List<EndpointInfo> endpoints = new ArrayList<>();
final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducerInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducerInfo.java
new file mode 100644
index 000000000000..e1e697163eac
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducerInfo.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+class ProducerInfo {
+ String uri;
+ String state;
+ String className;
+ String routeId;
+ String stepId;
+ boolean remote;
+ boolean singleton;
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
new file mode 100644
index 000000000000..38c63698c796
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
@@ -0,0 +1,234 @@
+/*
+ * 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 dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class ProducersTab extends AbstractTableTab {
+
+ ProducersTab(MonitorContext ctx) {
+ super(ctx, "route", "status", "type", "uri");
+ }
+
+ @Override
+ public void navigateUp() {
+ }
+
+ @Override
+ public void navigateDown() {
+ }
+
+ @Override
+ public boolean handleKeyEvent(KeyEvent ke) {
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)
+ || ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)
+ || ke.isHome() || ke.isEnd()) {
+ return false;
+ }
+ return super.handleKeyEvent(ke);
+ }
+
+ @Override
+ public boolean handleMouseEvent(MouseEvent me, Rect area) {
+ return false;
+ }
+
+ @Override
+ protected int getRowCount() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ return info != null ? info.producers.size() : 0;
+ }
+
+ @Override
+ protected void renderContent(Frame frame, Rect area, IntegrationInfo info)
{
+ List<ProducerInfo> sorted = new ArrayList<>(info.producers);
+ sorted.sort(this::sortProducer);
+
+ List<Row> rows = new ArrayList<>();
+ for (ProducerInfo pi : sorted) {
+ String type = producerType(pi);
+ Style statusStyle = "Started".equals(pi.state) ? Theme.success() :
Theme.error();
+
+ rows.add(Row.from(
+ Cell.from(Span.styled(" " + (pi.routeId != null ?
pi.routeId : ""), Style.EMPTY.fg(Theme.accent()))),
+ Cell.from(Span.styled(pi.state != null ? pi.state : "",
statusStyle)),
+ Cell.from(type),
+ Cell.from(pi.remote ? "Yes" : "No"),
+ Cell.from(pi.uri != null ? pi.uri : "")));
+ }
+
+ if (rows.isEmpty()) {
+ rows.add(emptyRow("No producers", 5));
+ }
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ Cell.from(Span.styled(" " + sortLabel("ROUTE",
"route"), sortStyle("route"))),
+ Cell.from(Span.styled(sortLabel("STATUS", "status"),
sortStyle("status"))),
+ Cell.from(Span.styled(sortLabel("TYPE", "type"),
sortStyle("type"))),
+ Cell.from(Span.styled("REMOTE", Style.EMPTY.bold())),
+ Cell.from(Span.styled(sortLabel("URI", "uri"),
sortStyle("uri")))))
+ .widths(
+ Constraint.length(20),
+ Constraint.length(10),
+ Constraint.length(20),
+ Constraint.length(8),
+ Constraint.fill())
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Producers ").build())
+ .build();
+
+ lastTableArea = area;
+ frame.renderStatefulWidget(table, area, tableState);
+ renderScrollbar(frame, sorted.size());
+ }
+
+ private int sortProducer(ProducerInfo a, ProducerInfo b) {
+ int result = switch (sort) {
+ case "status" -> {
+ String sa = a.state != null ? a.state : "";
+ String sb = b.state != null ? b.state : "";
+ yield sa.compareToIgnoreCase(sb);
+ }
+ case "type" -> {
+ String ta = producerType(a);
+ String tb = producerType(b);
+ yield ta.compareToIgnoreCase(tb);
+ }
+ case "uri" -> {
+ String ua = a.uri != null ? a.uri : "";
+ String ub = b.uri != null ? b.uri : "";
+ yield ua.compareToIgnoreCase(ub);
+ }
+ default -> { // "route"
+ String ra = a.routeId != null ? a.routeId : "";
+ String rb = b.routeId != null ? b.routeId : "";
+ yield ra.compareToIgnoreCase(rb);
+ }
+ };
+ return sortReversed ? -result : result;
+ }
+
+ private static String producerType(ProducerInfo pi) {
+ if (pi.className == null) {
+ return "";
+ }
+ String s = pi.className;
+ if (s.endsWith("Producer")) {
+ s = s.substring(0, s.length() - 8);
+ }
+ int dot = s.lastIndexOf('.');
+ return dot >= 0 ? s.substring(dot + 1) : s;
+ }
+
+ @Override
+ public SelectionContext getSelectionContext() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || info.producers.isEmpty()) {
+ return null;
+ }
+ List<ProducerInfo> sorted = new ArrayList<>(info.producers);
+ sorted.sort(this::sortProducer);
+ List<String> items = sorted.stream().map(p -> p.routeId != null ?
p.routeId : "").toList();
+ Integer sel = tableState.selected();
+ return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Producers");
+ }
+
+ @Override
+ public String description() {
+ return "Producer statistics (output endpoints sending data to external
systems)";
+ }
+
+ @Override
+ public String getHelpText() {
+ return """
+ # Producers
+
+ Producers are the **output** side of a Camel route. They send
data to
+ external systems (message brokers, HTTP endpoints, databases,
files, etc.).
+
+ Unlike consumers (one per route), a route can have multiple
producers
+ — each `.to()` or `.toD()` call in the route creates a
producer.
+
+ ## Table Columns
+
+ - **ROUTE** — The route this producer belongs to
+ - **STATUS** — Producer state: `Started` (running normally) or
`Stopped`
+ - **TYPE** — The Camel component type (e.g., `Kafka`, `Http`,
`Log`, `Seda`)
+ - **REMOTE** — Whether this producer sends to a remote system
(`Yes`) or is in-process (`No`)
+ - **URI** — The full endpoint URI (e.g., `kafka://my-topic`,
`log://mylogger`)
+
+ ## Keys
+
+ - `Up/Down` — select producer
+ - `s` — cycle sort column
+ - `S` — reverse sort order
+ """;
+ }
+
+ @Override
+ public JsonObject getTableDataAsJson() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ return null;
+ }
+ JsonObject result = new JsonObject();
+ result.put("tab", "Producers");
+ JsonArray rows = new JsonArray();
+ List<ProducerInfo> sorted = new ArrayList<>(info.producers);
+ sorted.sort(this::sortProducer);
+ for (ProducerInfo pi : sorted) {
+ JsonObject row = new JsonObject();
+ row.put("routeId", pi.routeId);
+ row.put("uri", pi.uri);
+ row.put("state", pi.state);
+ row.put("className", pi.className);
+ row.put("remote", pi.remote);
+ row.put("singleton", pi.singleton);
+ if (pi.stepId != null) {
+ row.put("stepId", pi.stepId);
+ }
+ rows.add(row);
+ }
+ result.put("rows", rows);
+ result.put("totalRows", info.producers.size());
+ Integer sel = tableState.selected();
+ result.put("selectedIndex", sel != null ? sel : -1);
+ return result;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerInfo.java
new file mode 100644
index 000000000000..50d9c0296234
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerInfo.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+class RouteControllerInfo {
+ String routeId;
+ String status;
+ String uri;
+ String supervising;
+ long attempts;
+ long lastAttempt;
+ long nextAttempt;
+ long elapsed;
+ String error;
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
new file mode 100644
index 000000000000..492cfdb34ccf
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
@@ -0,0 +1,293 @@
+/*
+ * 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 dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class RouteControllerTab extends AbstractTableTab {
+
+ RouteControllerTab(MonitorContext ctx) {
+ super(ctx, "route", "status", "supervising", "attempts", "uri");
+ }
+
+ @Override
+ public void navigateUp() {
+ }
+
+ @Override
+ public void navigateDown() {
+ }
+
+ @Override
+ public boolean handleKeyEvent(KeyEvent ke) {
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)
+ || ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)
+ || ke.isHome() || ke.isEnd()) {
+ return false;
+ }
+ return super.handleKeyEvent(ke);
+ }
+
+ @Override
+ public boolean handleMouseEvent(MouseEvent me, Rect area) {
+ return false;
+ }
+
+ @Override
+ protected int getRowCount() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ return info != null ? info.routeControllerRoutes.size() : 0;
+ }
+
+ @Override
+ protected void renderContent(Frame frame, Rect area, IntegrationInfo info)
{
+ // Handle non-supervised controller
+ if ("DefaultRouteController".equals(info.routeControllerType)) {
+ renderEmptyState(frame, area, "Route controller: Default (not
supervised)", " Route Controller ");
+ return;
+ }
+
+ List<RouteControllerInfo> sorted = new
ArrayList<>(info.routeControllerRoutes);
+ sorted.sort(this::sortRouteController);
+
+ // Handle supervised controller with all routes started successfully
+ if ("SupervisingRouteController".equals(info.routeControllerType)
+ && !info.routeControllerUnhealthy
+ && info.routeControllerRestartingRoutes == 0
+ && info.routeControllerExhaustedRoutes == 0
+ && sorted.stream().allMatch(r -> "Started".equals(r.status))) {
+ renderEmptyState(frame, area, "All routes started successfully", "
Route Controller (Supervised) ");
+ return;
+ }
+
+ List<Row> rows = new ArrayList<>();
+ for (RouteControllerInfo rc : sorted) {
+ Style statusStyle = "Started".equals(rc.status) ? Theme.success()
: Theme.error();
+ String supervising = rc.supervising != null ? rc.supervising : "";
+ String attempts = rc.attempts > 0 ? String.valueOf(rc.attempts) :
"";
+ String lastAttempt = rc.lastAttempt > 0 ?
TimeUtils.printSince(rc.lastAttempt) : "";
+ String nextAttempt = rc.nextAttempt > 0 ?
TimeUtils.printSince(rc.nextAttempt) : "";
+ String uriOrError = rc.error != null ? rc.error : (rc.uri != null
? rc.uri : "");
+ Style uriStyle = rc.error != null ? Theme.error() : Style.EMPTY;
+
+ rows.add(Row.from(
+ Cell.from(Span.styled(" " + (rc.routeId != null ?
rc.routeId : ""), Style.EMPTY.fg(Theme.accent()))),
+ Cell.from(Span.styled(rc.status != null ? rc.status : "",
statusStyle)),
+ Cell.from(supervising),
+ rightCell(attempts, 10),
+ rightCell(lastAttempt, 12),
+ rightCell(nextAttempt, 12),
+ Cell.from(Span.styled(uriOrError, uriStyle))));
+ }
+
+ if (rows.isEmpty()) {
+ rows.add(emptyRow("No routes under supervision", 7));
+ }
+
+ String title =
"SupervisingRouteController".equals(info.routeControllerType)
+ ? " Route Controller (Supervised) "
+ : " Route Controller ";
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ Cell.from(Span.styled(" " + sortLabel("ROUTE",
"route"), sortStyle("route"))),
+ Cell.from(Span.styled(sortLabel("STATUS", "status"),
sortStyle("status"))),
+ Cell.from(Span.styled(sortLabel("SUPERVISING",
"supervising"), sortStyle("supervising"))),
+ rightCell(sortLabel("ATTEMPTS", "attempts"), 10,
sortStyle("attempts")),
+ rightCell("LAST", 12, Style.EMPTY.bold()),
+ rightCell("NEXT", 12, Style.EMPTY.bold()),
+ Cell.from(Span.styled(sortLabel("URI", "uri"),
sortStyle("uri")))))
+ .widths(
+ Constraint.length(20),
+ Constraint.length(10),
+ Constraint.length(14),
+ Constraint.length(10),
+ Constraint.length(12),
+ Constraint.length(12),
+ Constraint.fill())
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(title).build())
+ .build();
+
+ lastTableArea = area;
+ frame.renderStatefulWidget(table, area, tableState);
+ renderScrollbar(frame, sorted.size());
+ }
+
+ private void renderEmptyState(Frame frame, Rect area, String message,
String title) {
+ List<Row> rows = new ArrayList<>();
+ rows.add(Row.from(Cell.from(Span.styled(" " + message,
Theme.muted()))));
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(Cell.from(Span.styled(" INFO",
Style.EMPTY.bold()))))
+ .widths(Constraint.fill())
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(title).build())
+ .build();
+
+ lastTableArea = area;
+ frame.renderStatefulWidget(table, area, tableState);
+ }
+
+ private int sortRouteController(RouteControllerInfo a, RouteControllerInfo
b) {
+ int result = switch (sort) {
+ case "status" -> {
+ String sa = a.status != null ? a.status : "";
+ String sb = b.status != null ? b.status : "";
+ yield sa.compareToIgnoreCase(sb);
+ }
+ case "supervising" -> {
+ String sa = a.supervising != null ? a.supervising : "";
+ String sb = b.supervising != null ? b.supervising : "";
+ yield sa.compareToIgnoreCase(sb);
+ }
+ case "attempts" -> Long.compare(b.attempts, a.attempts);
+ case "uri" -> {
+ String ua = a.uri != null ? a.uri : "";
+ String ub = b.uri != null ? b.uri : "";
+ yield ua.compareToIgnoreCase(ub);
+ }
+ default -> { // "route"
+ String ra = a.routeId != null ? a.routeId : "";
+ String rb = b.routeId != null ? b.routeId : "";
+ yield ra.compareToIgnoreCase(rb);
+ }
+ };
+ return sortReversed ? -result : result;
+ }
+
+ @Override
+ public SelectionContext getSelectionContext() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || info.routeControllerRoutes.isEmpty()) {
+ return null;
+ }
+ List<RouteControllerInfo> sorted = new
ArrayList<>(info.routeControllerRoutes);
+ sorted.sort(this::sortRouteController);
+ List<String> items = sorted.stream().map(r -> r.routeId != null ?
r.routeId : "").toList();
+ Integer sel = tableState.selected();
+ return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Route Controller");
+ }
+
+ @Override
+ public String description() {
+ return "Route controller status (supervised route startup and restart
attempts)";
+ }
+
+ @Override
+ public String getHelpText() {
+ return """
+ # Route Controller
+
+ Shows the status of the Camel route controller. The route
controller manages
+ route startup and can automatically restart routes that fail
to start.
+
+ There are two types of route controllers:
+
+ - **Default** — Routes are started in order and a failure
stops the context.
+ This tab shows "Route controller: Default (not supervised)"
in this case.
+ - **Supervising** — Routes that fail to start are retried with
exponential
+ backoff. The controller tracks attempts and manages restart
scheduling.
+
+ When using the supervised controller and all routes start
successfully,
+ the tab shows "All routes started successfully".
+
+ ## Table Columns
+
+ - **ROUTE** — The route ID
+ - **STATUS** — Route state: `Started`, `Stopped`, or other
lifecycle states
+ - **SUPERVISING** — Supervision status (e.g., `Active` when
the route is being
+ restarted by the controller)
+ - **ATTEMPTS** — Number of restart attempts so far
+ - **LAST** — Time since the last restart attempt
+ - **NEXT** — Time until the next scheduled restart attempt
+ - **URI** — The route's consumer endpoint URI. If the route
has a startup error,
+ the error message is shown here instead (in red)
+
+ ## Keys
+
+ - `Up/Down` — select route
+ - `s` — cycle sort column
+ - `S` — reverse sort order
+ """;
+ }
+
+ @Override
+ public JsonObject getTableDataAsJson() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ return null;
+ }
+ JsonObject result = new JsonObject();
+ result.put("tab", "Route Controller");
+ result.put("controllerType", info.routeControllerType);
+ result.put("unhealthy", info.routeControllerUnhealthy);
+ result.put("restartingRoutes", info.routeControllerRestartingRoutes);
+ result.put("exhaustedRoutes", info.routeControllerExhaustedRoutes);
+ JsonArray rows = new JsonArray();
+ List<RouteControllerInfo> sorted = new
ArrayList<>(info.routeControllerRoutes);
+ sorted.sort(this::sortRouteController);
+ for (RouteControllerInfo rc : sorted) {
+ JsonObject row = new JsonObject();
+ row.put("routeId", rc.routeId);
+ row.put("status", rc.status);
+ row.put("uri", rc.uri);
+ if (rc.supervising != null) {
+ row.put("supervising", rc.supervising);
+ }
+ row.put("attempts", rc.attempts);
+ if (rc.lastAttempt > 0) {
+ row.put("lastAttempt", rc.lastAttempt);
+ }
+ if (rc.nextAttempt > 0) {
+ row.put("nextAttempt", rc.nextAttempt);
+ }
+ if (rc.error != null) {
+ row.put("error", rc.error);
+ }
+ rows.add(row);
+ }
+ result.put("rows", rows);
+ result.put("totalRows", info.routeControllerRoutes.size());
+ Integer sel = tableState.selected();
+ result.put("selectedIndex", sel != null ? sel : -1);
+ return result;
+ }
+}
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 ad5e22996c01..6590530ad796 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
@@ -1812,16 +1812,15 @@ class RoutesTab extends AbstractTab {
detailScroll = 0;
}
- if (drillDownRouteId != null &&
!drillDownRouteId.equals(cachedRouteDetailId)
- && !drillDownRouteId.equals(detailLoadingRouteId)) {
- detailLoadingRouteId = drillDownRouteId;
+ if (drillDownRouteId != null && cachedRouteDetail == null
+ && !"*".equals(detailLoadingRouteId)) {
+ detailLoadingRouteId = "*";
detailLoading = true;
- String rid = drillDownRouteId;
if (ctx.runner != null) {
ctx.backgroundExecutor.execute(() -> {
- JsonObject result = requestRouteProcessorDetail(rid);
+ JsonObject result = requestRouteProcessorDetail("*");
cachedRouteDetail = result;
- cachedRouteDetailId = rid;
+ cachedRouteDetailId = "*";
detailLoading = false;
});
}
@@ -2009,7 +2008,7 @@ class RoutesTab extends AbstractTab {
try {
JsonObject root = new JsonObject();
root.put("action", "processor-detail");
- root.put("routeId", routeId);
+ root.put("routeId", "*");
return ctx.executeAction(ctx.selectedPid, root, 5000);
} catch (Exception e) {
return null;
@@ -2020,12 +2019,7 @@ class RoutesTab extends AbstractTab {
if (cachedRouteDetail == null || nodeId == null) {
return null;
}
- JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
- if (processors == null) {
- return null;
- }
- for (Object obj : processors) {
- JsonObject p = (JsonObject) obj;
+ for (JsonObject p : getAllProcessors(cachedRouteDetail)) {
if (nodeId.equals(p.getString("id"))) {
return p;
}
@@ -2033,6 +2027,35 @@ class RoutesTab extends AbstractTab {
return null;
}
+ static List<JsonObject> getAllProcessors(JsonObject routeDetail) {
+ if (routeDetail == null) {
+ return List.of();
+ }
+ JsonArray routes = (JsonArray) routeDetail.get("routes");
+ if (routes != null) {
+ List<JsonObject> all = new ArrayList<>();
+ for (Object obj : routes) {
+ JsonObject route = (JsonObject) obj;
+ JsonArray procs = (JsonArray) route.get("processors");
+ if (procs != null) {
+ for (Object p : procs) {
+ all.add((JsonObject) p);
+ }
+ }
+ }
+ return all;
+ }
+ JsonArray procs = (JsonArray) routeDetail.get("processors");
+ if (procs != null) {
+ List<JsonObject> all = new ArrayList<>();
+ for (Object p : procs) {
+ all.add((JsonObject) p);
+ }
+ return all;
+ }
+ return List.of();
+ }
+
// ---- Quick doc (q toggle in source viewer) ----
private Map<Integer, List<String>> provideAllQuickDocs(List<JsonObject>
cd) {
@@ -2040,8 +2063,8 @@ class RoutesTab extends AbstractTab {
return Map.of();
}
- JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
- if (processors == null || processors.isEmpty()) {
+ List<JsonObject> processors = getAllProcessors(cachedRouteDetail);
+ if (processors.isEmpty()) {
return Map.of();
}
@@ -2049,8 +2072,7 @@ class RoutesTab extends AbstractTab {
CamelCatalog catalog = info != null ? getCatalog(info) : null;
Map<Integer, List<String>> result = new LinkedHashMap<>();
- for (Object obj : processors) {
- JsonObject proc = (JsonObject) obj;
+ for (JsonObject proc : processors) {
Integer line = proc.getInteger("line");
if (line == null || line <= 0) {
continue;
@@ -2074,14 +2096,14 @@ class RoutesTab extends AbstractTab {
private void ensureProcessorDetailLoaded(String routeId) {
if (routeId != null && cachedRouteDetail == null
- && !routeId.equals(detailLoadingRouteId)) {
- detailLoadingRouteId = routeId;
+ && !"*".equals(detailLoadingRouteId)) {
+ detailLoadingRouteId = "*";
detailLoading = true;
if (ctx.runner != null) {
ctx.backgroundExecutor.execute(() -> {
- JsonObject result = requestRouteProcessorDetail(routeId);
+ JsonObject result = requestRouteProcessorDetail("*");
cachedRouteDetail = result;
- cachedRouteDetailId = routeId;
+ cachedRouteDetailId = "*";
detailLoading = false;
});
}
@@ -2148,10 +2170,31 @@ class RoutesTab extends AbstractTab {
CamelCatalog catalog, String type, JsonObject opts, int eipIdx) {
EipModel model = catalog != null ? catalog.eipModel(type) : null;
+
+ // For endpoint-bearing EIPs, resolve the component from the uri option
+ ComponentModel compModel = null;
+ if (opts != null && catalog != null) {
+ Object uriObj = opts.get("uri");
+ if (uriObj != null) {
+ String uri = uriObj.toString();
+ String comp = uri.contains(":") ? uri.substring(0,
uri.indexOf(':')) : uri;
+ compModel = catalog.componentModel(comp);
+ }
+ }
+
List<String> titleLines = new ArrayList<>();
if (model != null && model.getTitle() != null) {
- String desc = model.getDescription() != null ?
truncateText(model.getDescription(), 80) : "";
- titleLines.add(model.getTitle() + " — " + desc);
+ String eipTitle = model.getTitle();
+ if (compModel != null && compModel.getTitle() != null) {
+ eipTitle += " (" + compModel.getTitle() + ")";
+ }
+ String desc;
+ if (compModel != null && compModel.getDescription() != null) {
+ desc = truncateText(compModel.getDescription(), 80);
+ } else {
+ desc = model.getDescription() != null ?
truncateText(model.getDescription(), 80) : "";
+ }
+ titleLines.add(eipTitle + " — " + desc);
} else {
titleLines.add(type);
}
@@ -2166,20 +2209,31 @@ class RoutesTab extends AbstractTab {
optionDocs.put(opt.getName(), opt);
}
}
+ if (compModel != null) {
+ for (ComponentModel.EndpointOptionModel opt :
compModel.getEndpointOptions()) {
+ if (opt.getName() != null &&
!optionDocs.containsKey(opt.getName())) {
+ optionDocs.put(opt.getName(), opt);
+ }
+ }
+ }
inlineParameterDocs(result, cd, eipIdx, optionDocs);
// For Java/XML where options are on the same line,
// cluster the option docs under the title
if (result.size() == beforeSize + 1 && opts != null) {
- clusterEipOptions(titleLines, opts, optionDocs);
+ boolean hasParams = hasParametersChild(cd, eipIdx);
+ clusterEipOptions(titleLines, opts, optionDocs, hasParams);
}
}
}
private static void clusterEipOptions(
List<String> docLines, JsonObject opts,
- Map<String, BaseOptionModel> optionDocs) {
+ Map<String, BaseOptionModel> optionDocs, boolean skipUri) {
for (Map.Entry<String, Object> entry : opts.entrySet()) {
+ if (skipUri && "uri".equals(entry.getKey())) {
+ continue;
+ }
BaseOptionModel optModel = optionDocs.get(entry.getKey());
if (optModel != null) {
String optDoc = formatOptionDoc(optModel);
@@ -2190,6 +2244,22 @@ class RoutesTab extends AbstractTab {
}
}
+ static boolean hasParametersChild(List<JsonObject> cd, int eipIdx) {
+ int eipIndent = lineIndent(cd, eipIdx);
+ for (int i = eipIdx + 1; i < cd.size(); i++) {
+ String code = cd.get(i).get("code") != null ?
cd.get(i).get("code").toString() : "";
+ int indent = leadingSpaces(code);
+ if (indent < eipIndent && !code.isBlank()) {
+ break;
+ }
+ String trimmed = code.stripLeading();
+ if (trimmed.startsWith("parameters:")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
static void inlineParameterDocs(
Map<Integer, List<String>> result, List<JsonObject> cd,
int eipIdx, Map<String, BaseOptionModel> optionDocs) {
@@ -2200,6 +2270,9 @@ class RoutesTab extends AbstractTab {
int eipIndent = lineIndent(cd, eipIdx);
int childIndent = -1;
+ boolean inParameters = false;
+ int paramIndent = -1;
+ int parametersLineIndent = -1;
for (int i = eipIdx + 1; i < cd.size(); i++) {
String code = cd.get(i).get("code") != null ?
cd.get(i).get("code").toString() : "";
@@ -2210,6 +2283,35 @@ class RoutesTab extends AbstractTab {
if (childIndent < 0 && indent > eipIndent) {
childIndent = indent;
}
+
+ if (inParameters) {
+ if (paramIndent < 0 && indent > parametersLineIndent &&
!code.isBlank()) {
+ paramIndent = indent;
+ }
+ if (paramIndent > 0 && indent <= parametersLineIndent &&
!code.isBlank()) {
+ inParameters = false;
+ paramIndent = -1;
+ parametersLineIndent = -1;
+ } else if (paramIndent > 0 && indent == paramIndent) {
+ String trimmed = code.stripLeading();
+ int colon = trimmed.indexOf(':');
+ if (colon > 0) {
+ String key = trimmed.substring(0, colon).strip();
+ BaseOptionModel doc = optionDocs.get(key);
+ if (doc != null) {
+ String docLine = formatOptionDoc(doc);
+ if (docLine != null) {
+ int docIdx = lastContinuationLine(cd, i,
indent);
+ result.put(docIdx, List.of(docLine));
+ }
+ }
+ }
+ continue;
+ } else {
+ continue;
+ }
+ }
+
if (childIndent > 0 && indent != childIndent) {
continue;
}
@@ -2219,7 +2321,12 @@ class RoutesTab extends AbstractTab {
continue;
}
String key = trimmed.substring(0, colon).strip();
- if ("parameters".equals(key) || "uri".equals(key) ||
"steps".equals(key)
+ if ("parameters".equals(key)) {
+ inParameters = true;
+ parametersLineIndent = indent;
+ continue;
+ }
+ if ("uri".equals(key) || "steps".equals(key)
|| "id".equals(key) || "description".equals(key)) {
continue;
}
@@ -2228,12 +2335,29 @@ class RoutesTab extends AbstractTab {
if (doc != null) {
String docLine = formatOptionDoc(doc);
if (docLine != null) {
- result.put(i, List.of(docLine));
+ int docIdx = lastContinuationLine(cd, i, indent);
+ result.put(docIdx, List.of(docLine));
}
}
}
}
+ static int lastContinuationLine(List<JsonObject> cd, int keyIdx, int
keyIndent) {
+ int last = keyIdx;
+ for (int j = keyIdx + 1; j < cd.size(); j++) {
+ String c = cd.get(j).get("code") != null ?
cd.get(j).get("code").toString() : "";
+ if (c.isBlank()) {
+ continue;
+ }
+ if (leadingSpaces(c) > keyIndent) {
+ last = j;
+ } else {
+ break;
+ }
+ }
+ return last;
+ }
+
static String formatOptionDoc(BaseOptionModel doc) {
StringBuilder sb = new StringBuilder();
if (doc.getDescription() != null && !doc.getDescription().isEmpty()) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index c685be6ed7fe..83bf0e67da04 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -363,7 +363,6 @@ class SourceViewer {
int visibleLines = inner.height();
lastVisibleLines = visibleLines;
- int maxScroll = Math.max(0, lines.size() - visibleLines);
// On initial load, position selected line at 2/3 of viewport
if (pendingScroll && selectedLine >= 0) {
@@ -372,14 +371,37 @@ class SourceViewer {
pendingScroll = false;
}
- // Auto-scroll to keep selected line visible
+ // Auto-scroll to keep selected line visible (accounting for inline
doc lines)
if (selectedLine >= 0) {
if (selectedLine < scrollY) {
scrollY = selectedLine;
+ } else if (quickDocEnabled && !quickDocEntries.isEmpty()) {
+ while (scrollY < selectedLine && countVisualRows(scrollY,
selectedLine) + 1 > visibleLines) {
+ scrollY++;
+ }
} else if (selectedLine >= scrollY + visibleLines) {
scrollY = selectedLine - visibleLines + 1;
}
}
+
+ int maxScroll;
+ if (quickDocEnabled && !quickDocEntries.isEmpty()) {
+ maxScroll = 0;
+ int visualFromEnd = 0;
+ for (int i = lines.size() - 1; i >= 0; i--) {
+ visualFromEnd++;
+ List<String> docs = quickDocEntries.get(i);
+ if (docs != null) {
+ visualFromEnd += docs.size();
+ }
+ if (visualFromEnd >= visibleLines) {
+ maxScroll = i;
+ break;
+ }
+ }
+ } else {
+ maxScroll = Math.max(0, lines.size() - visibleLines);
+ }
scrollY = Math.min(scrollY, maxScroll);
int hSkip = wordWrap ? 0 : scrollX;
@@ -1089,6 +1111,20 @@ class SourceViewer {
return result;
}
+ private int countVisualRows(int fromLine, int toLine) {
+ int count = 0;
+ for (int i = fromLine; i < toLine && i < lines.size(); i++) {
+ count++;
+ if (quickDocEnabled) {
+ List<String> docs = quickDocEntries.get(i);
+ if (docs != null) {
+ count += docs.size();
+ }
+ }
+ }
+ return count;
+ }
+
private static String objToString(Object o) {
return o != null ? o.toString() : "";
}
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 f0a3be50cd7a..db04860676c5 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
@@ -326,6 +326,60 @@ final class StatusParser {
}
}
+ // Parse producers
+ JsonObject producersObj = (JsonObject) root.get("producers");
+ if (producersObj != null) {
+ JsonArray producerList = (JsonArray) producersObj.get("producers");
+ if (producerList != null) {
+ for (Object p : producerList) {
+ JsonObject pj = (JsonObject) p;
+ ProducerInfo pi = new ProducerInfo();
+ pi.uri = pj.getString("uri");
+ pi.state = pj.getString("state");
+ pi.className = pj.getString("class");
+ pi.routeId = pj.getString("routeId");
+ pi.stepId = pj.getString("stepId");
+ pi.remote = Boolean.TRUE.equals(pj.get("remote"));
+ pi.singleton = Boolean.TRUE.equals(pj.get("singleton"));
+ info.producers.add(pi);
+ }
+ }
+ }
+
+ // Parse events
+ JsonObject eventsObj = (JsonObject) root.get("events");
+ if (eventsObj != null) {
+ parseEventArray(eventsObj, "events", "general", info);
+ parseEventArray(eventsObj, "routeEvents", "route", info);
+ parseEventArray(eventsObj, "exchangeEvents", "exchange", info);
+ }
+
+ // Parse route controller
+ JsonObject rcObj = (JsonObject) root.get("routeController");
+ if (rcObj != null) {
+ info.routeControllerType = rcObj.getString("controller");
+ info.routeControllerUnhealthy =
Boolean.TRUE.equals(rcObj.get("unhealthyRoutes"));
+ info.routeControllerRestartingRoutes =
rcObj.getIntegerOrDefault("restartingRoutes", 0);
+ info.routeControllerExhaustedRoutes =
rcObj.getIntegerOrDefault("exhaustedRoutes", 0);
+ JsonArray rcRoutes = (JsonArray) rcObj.get("routes");
+ if (rcRoutes != null) {
+ for (Object r : rcRoutes) {
+ JsonObject rj = (JsonObject) r;
+ RouteControllerInfo rc = new RouteControllerInfo();
+ rc.routeId = rj.getString("routeId");
+ rc.status = rj.getString("status");
+ rc.uri = rj.getString("uri");
+ rc.supervising = rj.getString("supervising");
+ rc.attempts = rj.getLongOrDefault("attempts", 0);
+ rc.lastAttempt = rj.getLongOrDefault("lastAttempt", 0);
+ rc.nextAttempt = rj.getLongOrDefault("nextAttempt", 0);
+ rc.elapsed = rj.getLongOrDefault("elapsed", 0);
+ rc.error = rj.getString("error");
+ info.routeControllerRoutes.add(rc);
+ }
+ }
+ }
+
// Parse endpoints (top-level "endpoints" is a JsonObject with nested
"endpoints" array)
JsonObject endpointsObj = (JsonObject) root.get("endpoints");
if (endpointsObj != null) {
@@ -1220,4 +1274,20 @@ final class StatusParser {
static long objToLong(Object o) {
return TuiHelper.objToLong(o);
}
+
+ private static void parseEventArray(JsonObject eventsObj, String arrayKey,
String category, IntegrationInfo info) {
+ JsonArray arr = (JsonArray) eventsObj.get(arrayKey);
+ if (arr != null) {
+ for (Object e : arr) {
+ JsonObject ej = (JsonObject) e;
+ EventInfo ei = new EventInfo();
+ ei.type = ej.getString("type");
+ ei.category = category;
+ ei.timestamp = ej.getLongOrDefault("timestamp", 0);
+ ei.exchangeId = ej.getString("exchangeId");
+ ei.message = ej.getString("message");
+ info.events.add(ei);
+ }
+ }
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index ab90e81eece4..0fdc0a31873c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -70,6 +70,9 @@ class TabRegistry {
private DiagramTab diagramTab;
private RoutesTab routesTab;
private ConsumersTab consumersTab;
+ private ProducersTab producersTab;
+ private EventTab eventTab;
+ private RouteControllerTab routeControllerTab;
private EndpointsTab endpointsTab;
private HttpTab httpTab;
private HealthTab healthTab;
@@ -115,6 +118,9 @@ class TabRegistry {
diagramTab = new DiagramTab(ctx);
routesTab = new RoutesTab(ctx);
consumersTab = new ConsumersTab(ctx);
+ producersTab = new ProducersTab(ctx);
+ eventTab = new EventTab(ctx);
+ routeControllerTab = new RouteControllerTab(ctx);
kafkaTab = new KafkaTab(ctx);
dataSourceTab = new DataSourceTab(ctx);
heapHistogramTab = new HeapHistogramTab(ctx);
@@ -160,9 +166,14 @@ class TabRegistry {
new MoreTab(TuiIcons.TAB_CIRCUIT_BREAKER, "Circuit Breaker",
"&Circuit Breaker", circuitBreakerTab, "Routing"),
new MoreTab(TuiIcons.TAB_CONSUMERS, "Consumers", "Co&nsumers",
consumersTab, "Routing"),
new MoreTab(TuiIcons.TAB_INFLIGHT, "Inflight", "In&flight",
inflightTab, "Routing"),
+ new MoreTab(TuiIcons.TAB_PRODUCERS, "Producers", "Prod&ucers",
producersTab, "Routing"),
+ new MoreTab(
+ TuiIcons.TAB_ROUTE_CONTROLLER, "Route Controller",
"Route Organi&zer", routeControllerTab,
+ "Routing"),
// Observability
new MoreTab(TuiIcons.TAB_HEALTH, "Health", "H&ealth",
healthTab, "Observability"),
new MoreTab(TuiIcons.TAB_METRICS, "Metrics", "Metr&ics",
metricsTab, "Observability"),
+ new MoreTab(TuiIcons.TAB_EVENTS, "Events", "E&xchange Events",
eventTab, "Observability"),
new MoreTab(TuiIcons.TAB_SPANS, "Spans", "&OTel Spans",
spansTab, "Observability"),
// Data
new MoreTab(TuiIcons.TAB_DATASOURCE, "JDBC DataSource", "&JDBC
DataSource", dataSourceTab, "Data"),
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 395d0a118149..ad41009894f1 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -146,6 +146,9 @@ final class TuiIcons {
static final String TAB_PROCESS = CLIPBOARD;
static final String TAB_STARTUP = QUARKUS;
static final String TAB_THREADS = "🧵";
+ static final String TAB_PRODUCERS = "📤";
+ static final String TAB_EVENTS = "📣";
+ static final String TAB_ROUTE_CONTROLLER = "🚦";
/** Icons for {@link TabRegistry#TAB_OVERVIEW}..{@link
TabRegistry#TAB_MORE} (in order). */
static final List<String> PRIMARY_TAB_ICONS = List.of(
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
index e4076c785e7b..86d153cac9c6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
@@ -82,8 +82,8 @@ class TabRegistryTest {
}
@Test
- void moreTabsHasTwentyTwoEntries() {
- assertEquals(23, registry.moreTabs().size());
+ void moreTabsHasTwentySixEntries() {
+ assertEquals(26, registry.moreTabs().size());
}
@Test
@@ -109,8 +109,8 @@ class TabRegistryTest {
// MORE_SHORTCUTS array carried before the MoreTab refactor. A label
edit that repoints a key must fail here.
List<Character> shortcuts =
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
assertEquals(
- List.of('W', 'C', 'N', 'F', 'E', 'I', 'O', 'J', 'K', 'Q', 'R',
'A', 'H', 'M', 'Y', 'P', 'S', 'T', 'B', 'L',
- 'G', 'V', 'D'),
+ List.of('W', 'C', 'N', 'F', 'U', 'Z', 'E', 'I', 'X', 'O', 'J',
'K', 'Q', 'R', 'A', 'H', 'M', 'Y', 'P', 'S',
+ 'T', 'B', 'L', 'G', 'V', 'D'),
shortcuts, "More tab shortcut letters must match the
historical sequence");
}