You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/06/04 10:36:56 UTC
(camel) 25/33: CAMEL-23672: camel-tui - Unify routes tab diagram with topology drill-down
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch feature/CAMEL-23672-tui-diagram
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 189fb63b6c3e9977a02b90473371b77d8b00e6c7
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu Jun 4 10:56:49 2026 +0200
CAMEL-23672: camel-tui - Unify routes tab diagram with topology drill-down
Replace legacy ASCII diagram in the Routes tab with the same native
topology and route drill-down experience from the Diagram tab. Users
now get consistent navigation (arrow keys, Enter to drill down, Esc
to go back, t for topology) across both tabs. Drop the a (all/single)
diagram toggle.
Also swap F2 example browser keys so r runs directly and Enter opens
the tweak dialog, and move the run options dialog higher up.
Signed-off-by: Claus Ibsen <da...@apache.org>
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 8 +-
.../dsl/jbang/core/commands/tui/RoutesTab.java | 860 ++++++++++++++++-----
.../jbang/core/commands/tui/RunOptionsForm.java | 4 +-
3 files changed, 669 insertions(+), 203 deletions(-)
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index 2404676351ce..8f9a52013245 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -502,11 +502,11 @@ class ActionsPopup {
} else if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
navigateExampleBrowser(10);
} else if (ke.isChar('r')) {
- openNameInput();
+ launchSelectedExample();
} else if (ke.isChar('d')) {
loadDocFromExample();
} else if (ke.isConfirm()) {
- launchSelectedExample();
+ openNameInput();
}
return true;
}
@@ -687,8 +687,8 @@ class ActionsPopup {
}
if (showExampleBrowser) {
hint(spans, "↑↓", "navigate");
- hint(spans, "Enter", "run");
- hint(spans, "r", "run...");
+ hint(spans, "r", "run");
+ hint(spans, "Enter", "run...");
hint(spans, "d", "docs");
hintLast(spans, "Esc", "back");
return;
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 0b61a0283652..e23ac133d74d 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
@@ -17,7 +17,9 @@
package org.apache.camel.dsl.jbang.core.commands.tui;
import java.nio.file.Path;
+import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Deque;
import java.util.List;
import dev.tamboui.layout.Constraint;
@@ -38,6 +40,7 @@ import dev.tamboui.widgets.table.Row;
import dev.tamboui.widgets.table.Table;
import dev.tamboui.widgets.table.TableState;
import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.TimeUtils;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
@@ -62,15 +65,16 @@ class RoutesTab implements MonitorTab {
// Table states
private final TableState routeTableState = new TableState();
private final TableState processorTableState = new TableState();
- private final TableState routeHeaderTableState = new TableState();
// Diagram support (shared rendering/loading logic)
private final DiagramSupport diagram = new DiagramSupport();
private final SourceViewer sourceViewer = new SourceViewer();
- private boolean diagramAllRoutes;
private boolean diagramMetrics = true;
private boolean showDescription;
- private String diagramRouteId;
+ private boolean showExternal;
+ private boolean topologyMode = true;
+ private String drillDownRouteId;
+ private final Deque<String> routeNavigationStack = new ArrayDeque<>();
RoutesTab(MonitorContext ctx) {
this.ctx = ctx;
@@ -88,38 +92,185 @@ class RoutesTab implements MonitorTab {
return diagramMetrics;
}
- boolean isDiagramAllRoutes() {
- return diagramAllRoutes;
- }
-
boolean isShowSource() {
return sourceViewer.isVisible();
}
@Override
public boolean handleKeyEvent(KeyEvent ke) {
- // Source view scrolling
+ // Source view scrolling (takes priority when active)
if (sourceViewer.handleKeyEvent(ke)) {
return true;
}
- // Diagram scrolling
+ // Source viewer toggle (drill-down mode)
+ if (!topologyMode && diagram.isShowDiagram() && ke.isChar('c')) {
+ loadSourceForSelectedNode();
+ return true;
+ }
+
+ // Topology node navigation
+ if (diagram.isShowDiagram() && topologyMode && diagram.hasDiagramData()
+ && !diagram.getNodeBoxes().isEmpty()) {
+ if (ke.isUp()) {
+ diagram.selectNodeUp();
+ diagram.scrollToSelectedNode();
+ return true;
+ }
+ if (ke.isDown()) {
+ diagram.selectNodeDown();
+ diagram.scrollToSelectedNode();
+ return true;
+ }
+ if (ke.isLeft()) {
+ diagram.selectNodeLeft();
+ diagram.scrollToSelectedNode();
+ return true;
+ }
+ if (ke.isRight()) {
+ diagram.selectNodeRight();
+ diagram.scrollToSelectedNode();
+ return true;
+ }
+ if (ke.isHome()) {
+ diagram.selectFirstNode();
+ diagram.scrollToSelectedNode();
+ return true;
+ }
+ if (ke.isEnd()) {
+ diagram.selectLastNode();
+ diagram.scrollToSelectedNode();
+ return true;
+ }
+ }
+
+ // EIP node navigation in route drill-down mode
+ if (diagram.isShowDiagram() && !topologyMode && !diagram.getEipNodeBoxes().isEmpty()) {
+ if (ke.isUp()) {
+ diagram.selectEipNodeUp();
+ diagram.scrollToSelectedEipNode();
+ return true;
+ }
+ if (ke.isDown()) {
+ diagram.selectEipNodeDown();
+ diagram.scrollToSelectedEipNode();
+ return true;
+ }
+ if (ke.isLeft()) {
+ diagram.selectEipNodeLeft();
+ diagram.scrollToSelectedEipNode();
+ return true;
+ }
+ if (ke.isRight()) {
+ diagram.selectEipNodeRight();
+ diagram.scrollToSelectedEipNode();
+ return true;
+ }
+ if (ke.isHome()) {
+ diagram.selectFirstEipNode();
+ diagram.scrollToSelectedEipNode();
+ return true;
+ }
+ if (ke.isEnd()) {
+ diagram.selectLastEipNode();
+ diagram.scrollToSelectedEipNode();
+ return true;
+ }
+ }
+
+ // Jump back to topology from drill-down
+ if (diagram.isShowDiagram() && !topologyMode && ke.isChar('t')) {
+ routeNavigationStack.clear();
+ diagram.setPendingSelectionRouteId(drillDownRouteId);
+ drillDownRouteId = null;
+ topologyMode = true;
+ diagram.setTopologyMode(true);
+ diagram.setSelectedEipNodeIndex(-1);
+ diagram.resetScroll();
+ if (diagram.hasNativeLayout()) {
+ return true;
+ }
+ diagram.endLoad();
+ reloadDiagram();
+ return true;
+ }
+
+ // Diagram scrolling (PgUp/PgDn etc)
if (diagram.handleScrollKeys(ke)) {
return true;
}
- // Diagram-specific controls (only when diagram is showing)
- if (diagram.isShowDiagram()) {
- if (ke.isCharIgnoreCase('m')) {
- diagramMetrics = !diagramMetrics;
- diagram.endLoad();
- reloadDiagramQuietly();
+ // Toggle metrics (diagram mode)
+ if (diagram.isShowDiagram() && ke.isCharIgnoreCase('m')) {
+ diagramMetrics = !diagramMetrics;
+ diagram.endLoad();
+ reloadDiagram();
+ return true;
+ }
+
+ // Toggle external systems (topology mode only)
+ if (diagram.isShowDiagram() && topologyMode && ke.isCharIgnoreCase('e')) {
+ showExternal = !showExternal;
+ diagram.endLoad();
+ reloadDiagram();
+ return true;
+ }
+
+ // Toggle description (diagram mode)
+ if (diagram.isShowDiagram() && ke.isCharIgnoreCase('n')) {
+ diagram.setShowDescription(!diagram.isShowDescription());
+ diagram.endLoad();
+ reloadDiagram();
+ return true;
+ }
+
+ // Jump to linked route from EIP node (Enter in route mode)
+ if (diagram.isShowDiagram() && !topologyMode && ke.isConfirm() && !diagram.getEipNodeBoxes().isEmpty()) {
+ String linkedRouteId = diagram.findLinkedRouteId(drillDownRouteId);
+ if (linkedRouteId != null && diagram.getRouteLayout(linkedRouteId) != null) {
+ if (linkedRouteId.equals(drillDownRouteId)) {
+ return true;
+ }
+ if (routeNavigationStack.contains(linkedRouteId)) {
+ while (!routeNavigationStack.isEmpty() && !linkedRouteId.equals(routeNavigationStack.peek())) {
+ routeNavigationStack.pop();
+ }
+ routeNavigationStack.pop();
+ } else {
+ routeNavigationStack.push(drillDownRouteId);
+ }
+ drillDownRouteId = linkedRouteId;
+ diagram.selectFromNode(linkedRouteId);
+ diagram.resetScroll();
return true;
}
+ return true;
+ }
+
+ // Drill down into route diagram (Enter from topology)
+ if (diagram.isShowDiagram() && topologyMode && ke.isConfirm()) {
+ String selectedRouteId = diagram.getSelectedRouteId();
+ if (selectedRouteId != null) {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info != null && info.routes.stream().anyMatch(r -> selectedRouteId.equals(r.routeId))) {
+ routeNavigationStack.clear();
+ drillDownRouteId = selectedRouteId;
+ topologyMode = false;
+ diagram.setTopologyMode(false);
+ diagram.selectFromNode(selectedRouteId);
+ diagram.resetScroll();
+ diagram.endLoad();
+ if (diagram.getRouteLayout(selectedRouteId) != null) {
+ return true;
+ }
+ reloadDiagram();
+ }
+ }
+ return true;
}
- // Sort
- if (ke.isChar('s')) {
+ // Sort (only when not in diagram)
+ if (!diagram.isShowDiagram() && ke.isChar('s')) {
if (routeTopMode) {
routeTopSortIndex = (routeTopSortIndex + 1) % ROUTE_TOP_SORT_COLUMNS.length;
routeTopSort = ROUTE_TOP_SORT_COLUMNS[routeTopSortIndex];
@@ -131,7 +282,7 @@ class RoutesTab implements MonitorTab {
}
return true;
}
- if (ke.isChar('S')) {
+ if (!diagram.isShowDiagram() && ke.isChar('S')) {
if (routeTopMode) {
routeTopSortReversed = !routeTopSortReversed;
} else {
@@ -146,34 +297,24 @@ class RoutesTab implements MonitorTab {
return true;
}
- // Toggle all routes diagram flag
- if (!sourceViewer.isVisible() && !diagram.isShowDiagram() && ke.isCharIgnoreCase('a')) {
- diagramAllRoutes = !diagramAllRoutes;
- return true;
- }
-
- // Toggle description in route table
+ // Toggle description in route table (only when not in diagram)
if (!sourceViewer.isVisible() && !diagram.isShowDiagram() && ke.isCharIgnoreCase('n')) {
showDescription = !showDescription;
return true;
}
- // Text diagram toggle
+ // Diagram toggle
if (ke.isCharIgnoreCase('d')) {
- diagram.toggleDiagram(this::loadDiagramForSelectedRoute);
- return true;
- }
-
- // Toggle description in diagram
- if (diagram.isShowDiagram() && ke.isCharIgnoreCase('n')) {
- diagram.setShowDescription(!diagram.isShowDescription());
- diagram.endLoad();
- loadDiagramForSelectedRoute();
+ if (diagram.isShowDiagram()) {
+ closeDiagram();
+ } else {
+ openDiagram();
+ }
return true;
}
- // Source viewer toggle
- if (ke.isChar('c')) {
+ // Source viewer toggle (table mode — route source)
+ if (!diagram.isShowDiagram() && ke.isChar('c')) {
if (sourceViewer.isVisible()) {
sourceViewer.hide();
} else {
@@ -203,24 +344,56 @@ class RoutesTab implements MonitorTab {
sourceViewer.hide();
return true;
}
- return diagram.handleEscape();
+ if (diagram.isShowDiagram()) {
+ if (!topologyMode) {
+ if (!routeNavigationStack.isEmpty()) {
+ drillDownRouteId = routeNavigationStack.pop();
+ diagram.selectFromNode(drillDownRouteId);
+ diagram.resetScroll();
+ return true;
+ }
+ // Go back to topology
+ diagram.setPendingSelectionRouteId(drillDownRouteId);
+ topologyMode = true;
+ diagram.setTopologyMode(true);
+ diagram.setSelectedEipNodeIndex(-1);
+ diagram.resetScroll();
+ if (diagram.hasNativeLayout()) {
+ return true;
+ }
+ diagram.endLoad();
+ reloadDiagram();
+ return true;
+ }
+ // Close diagram entirely from topology
+ closeDiagram();
+ return true;
+ }
+ return false;
}
@Override
public void navigateUp() {
- routeTableState.selectPrevious();
+ if (!diagram.isShowDiagram()) {
+ routeTableState.selectPrevious();
+ }
}
@Override
public void navigateDown() {
- IntegrationInfo info = ctx.findSelectedIntegration();
- routeTableState.selectNext(info != null ? info.routes.size() : 0);
+ if (!diagram.isShowDiagram()) {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ routeTableState.selectNext(info != null ? info.routes.size() : 0);
+ }
}
@Override
public void onIntegrationChanged() {
sourceViewer.reset();
diagram.reset();
+ topologyMode = true;
+ drillDownRouteId = null;
+ routeNavigationStack.clear();
routeTableState.select(0);
}
@@ -232,36 +405,77 @@ class RoutesTab implements MonitorTab {
return;
}
- // Fullscreen source view
- if (sourceViewer.isVisible()) {
- List<Rect> fullChunks = Layout.vertical()
- .constraints(Constraint.length(4), Constraint.fill())
- .split(area);
- renderRouteHeader(frame, fullChunks.get(0), info);
- sourceViewer.render(frame, fullChunks.get(1));
+ // Fullscreen source view (from table mode)
+ if (sourceViewer.isVisible() && !diagram.isShowDiagram()) {
+ sourceViewer.render(frame, area);
+ return;
+ }
+
+ // Fullscreen source view (from drill-down mode)
+ if (sourceViewer.isVisible() && diagram.isShowDiagram()) {
+ sourceViewer.render(frame, area);
return;
}
// Fullscreen diagram mode
if (diagram.isShowDiagram() && diagram.hasDiagramData()) {
- String title = " Diagram [" + diagramRouteId + "] ";
- if (diagramAllRoutes) {
- diagram.renderDiagram(frame, area, title);
- } else {
- List<Rect> fullChunks = Layout.vertical()
- .constraints(Constraint.length(4), Constraint.fill())
- .split(area);
- renderRouteHeader(frame, fullChunks.get(0), info);
- diagram.renderDiagram(frame, fullChunks.get(1), title);
+ if (topologyMode && diagram.hasNativeLayout()) {
+ String selectedRouteId = diagram.getSelectedRouteId();
+ Line title;
+ if (info.name != null) {
+ title = Line.from(
+ Span.raw(" Topology ["),
+ Span.styled(info.name, Style.EMPTY.fg(Color.YELLOW).bold()),
+ Span.raw("] "));
+ } else {
+ title = Line.from(Span.raw(" Topology "));
+ }
+ if (selectedRouteId != null && area.width() > 60) {
+ int panelWidth = 30;
+ List<Rect> hChunks = Layout.horizontal()
+ .constraints(Constraint.length(panelWidth), Constraint.fill())
+ .split(area);
+ renderInfoPanel(frame, hChunks.get(0), info, selectedRouteId);
+ diagram.renderNativeDiagram(frame, hChunks.get(1), title, diagramMetrics);
+ } else {
+ diagram.renderNativeDiagram(frame, area, title, diagramMetrics);
+ }
+ return;
+ } else if (!topologyMode && drillDownRouteId != null
+ && diagram.getRouteLayout(drillDownRouteId) != null) {
+ Line title = buildBreadcrumbTitle();
+ var routeLayout = diagram.getRouteLayout(drillDownRouteId);
+ if (area.width() > 60) {
+ int panelWidth = 30;
+ List<Rect> hChunks = Layout.horizontal()
+ .constraints(Constraint.length(panelWidth), Constraint.fill())
+ .split(area);
+ renderEipInfoPanel(frame, hChunks.get(0));
+ diagram.renderNativeRouteDiagram(
+ frame, hChunks.get(1), title, diagramMetrics, drillDownRouteId, routeLayout);
+ } else {
+ diagram.renderNativeRouteDiagram(frame, area, title, diagramMetrics, drillDownRouteId, routeLayout);
+ }
+ return;
}
+
+ // Fallback: loading or no native layout yet
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(Line.from(Span.styled(
+ "Loading diagram...",
+ Style.EMPTY.dim()))))
+ .block(Block.builder().borderType(BorderType.ROUNDED)
+ .title(" Diagram ").build())
+ .build(),
+ area);
return;
}
- // Sort routes
+ // Normal table view
List<RouteInfo> sortedRoutes = new ArrayList<>(info.routes);
sortedRoutes.sort(this::sortRoute);
- // Split: routes table (top half) + processors table (bottom half)
List<Rect> chunks = Layout.vertical()
.constraints(Constraint.percentage(45), Constraint.percentage(55))
.split(area);
@@ -390,25 +604,20 @@ class RoutesTab implements MonitorTab {
frame.renderStatefulWidget(routeTable, chunks.get(0), routeTableState);
- // Bottom panel: diagram or processors
- if (diagram.isShowDiagram() && diagram.hasDiagramData()) {
- String title = " Diagram [" + diagramRouteId + "] ";
- diagram.renderDiagram(frame, chunks.get(1), title);
+ // Bottom panel: processors
+ Integer selectedRoute = routeTableState.selected();
+ if (selectedRoute != null && selectedRoute >= 0 && selectedRoute < sortedRoutes.size()) {
+ RouteInfo route = sortedRoutes.get(selectedRoute);
+ renderProcessors(frame, chunks.get(1), route);
+ } else if (!sortedRoutes.isEmpty()) {
+ renderProcessors(frame, chunks.get(1), sortedRoutes.get(0));
} else {
- Integer selectedRoute = routeTableState.selected();
- if (selectedRoute != null && selectedRoute >= 0 && selectedRoute < sortedRoutes.size()) {
- RouteInfo route = sortedRoutes.get(selectedRoute);
- renderProcessors(frame, chunks.get(1), route);
- } else if (!sortedRoutes.isEmpty()) {
- renderProcessors(frame, chunks.get(1), sortedRoutes.get(0));
- } else {
- frame.renderWidget(
- Paragraph.builder()
- .text(Text.from(Line.from(Span.styled("No routes", Style.EMPTY.dim()))))
- .block(Block.builder().borderType(BorderType.ROUNDED).title(" Processors ").build())
- .build(),
- chunks.get(1));
- }
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(Line.from(Span.styled("No routes", Style.EMPTY.dim()))))
+ .block(Block.builder().borderType(BorderType.ROUNDED).title(" Processors ").build())
+ .build(),
+ chunks.get(1));
}
}
@@ -417,9 +626,30 @@ class RoutesTab implements MonitorTab {
if (sourceViewer.isVisible()) {
sourceViewer.renderFooter(spans);
} else if (diagram.isShowDiagram()) {
- diagram.renderFooterHints(spans);
+ if (!topologyMode && !diagram.getEipNodeBoxes().isEmpty()) {
+ hint(spans, "Esc", "back");
+ hint(spans, "t", "topology");
+ hint(spans, "↑↓←→", "navigate");
+ hint(spans, "PgUp/PgDn", "page");
+ hint(spans, "c", "source");
+ } else if (!topologyMode) {
+ hint(spans, "Esc", "back");
+ hint(spans, "t", "topology");
+ hint(spans, "↑↓←→", "scroll");
+ hint(spans, "PgUp/PgDn", "page");
+ } else if (!diagram.getNodeBoxes().isEmpty()) {
+ hint(spans, "Esc", "close");
+ hint(spans, "↑↓←→", "navigate");
+ hint(spans, "Enter", "drill-down");
+ hint(spans, "PgUp/PgDn", "page");
+ } else {
+ diagram.renderFooterHints(spans);
+ }
hint(spans, "m", "metrics" + (diagramMetrics ? " [on]" : " [off]"));
- hintLast(spans, "n", "description" + (diagram.isShowDescription() ? " [on]" : " [off]"));
+ if (topologyMode) {
+ hint(spans, "e", "external" + (showExternal ? " [on]" : " [off]"));
+ }
+ hint(spans, "n", "description" + (diagram.isShowDescription() ? " [on]" : " [off]"));
} else {
hint(spans, "Esc", "back");
hint(spans, "↑↓", "navigate");
@@ -429,7 +659,6 @@ class RoutesTab implements MonitorTab {
if (!routeTopMode) {
hint(spans, "c", "source");
hint(spans, "d", "diagram");
- hint(spans, "a", "diagram " + (diagramAllRoutes ? "[all]" : "[single]"));
String routeState = selectedRouteState();
boolean supSus = selectedRouteSupportsSuspension();
if ("Started".equals(routeState)) {
@@ -451,7 +680,7 @@ class RoutesTab implements MonitorTab {
void refreshDiagramIfNeeded() {
if (diagram.isShowDiagram() && diagramMetrics) {
- reloadDiagramQuietly();
+ reloadDiagram();
}
}
@@ -462,6 +691,284 @@ class RoutesTab implements MonitorTab {
return route.from != null ? route.from : "";
}
+ // ---- Diagram open/close ----
+
+ private void openDiagram() {
+ topologyMode = true;
+ drillDownRouteId = null;
+ routeNavigationStack.clear();
+ diagram.setTopologyMode(true);
+
+ // Pre-select the currently highlighted route from the table
+ String selectedId = selectedRouteId();
+ if (selectedId != null) {
+ diagram.setPendingSelectionRouteId(selectedId);
+ }
+
+ loadDiagram(true);
+ }
+
+ private void closeDiagram() {
+ topologyMode = true;
+ drillDownRouteId = null;
+ routeNavigationStack.clear();
+ diagram.close();
+ }
+
+ // ---- Info panels (mirrored from DiagramTab) ----
+
+ private void renderInfoPanel(Frame frame, Rect area, IntegrationInfo info, String routeId) {
+ RouteInfo route = null;
+ for (RouteInfo r : info.routes) {
+ if (routeId.equals(r.routeId)) {
+ route = r;
+ break;
+ }
+ }
+
+ List<Line> lines = new ArrayList<>();
+ if (route != null) {
+ lines.add(Line.from(
+ Span.styled(" Route: ", Style.EMPTY.fg(Color.YELLOW).bold()),
+ Span.styled(route.routeId, Style.EMPTY.fg(Color.WHITE).bold())));
+ lines.add(Line.from(
+ Span.styled(" From: ", Style.EMPTY.dim()),
+ Span.raw(route.from != null ? route.from : "")));
+ String stateLabel = route.state != null ? route.state : "";
+ Style stateStyle = "Started".equals(route.state) ? Style.EMPTY.fg(Color.GREEN) : Style.EMPTY.fg(Color.LIGHT_RED);
+ lines.add(Line.from(
+ Span.styled(" State: ", Style.EMPTY.dim()),
+ Span.styled(stateLabel, stateStyle)));
+
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" Uptime: ", Style.EMPTY.dim()),
+ Span.raw(route.uptime != null ? route.uptime : "")));
+ lines.add(Line.from(
+ Span.styled(" Throughput: ", Style.EMPTY.dim()),
+ Span.raw(route.throughput != null ? route.throughput : "")));
+ if (route.coverage != null) {
+ lines.add(Line.from(
+ Span.styled(" Coverage: ", Style.EMPTY.dim()),
+ Span.raw(route.coverage)));
+ }
+
+ lines.add(Line.from(Span.raw("")));
+ int w = numWidth(route.total, route.failed, route.inflight);
+ lines.add(Line.from(
+ Span.styled(" Total: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + w + "d", route.total))));
+ Style failStyle = route.failed > 0 ? Style.EMPTY.fg(Color.LIGHT_RED).bold() : Style.EMPTY;
+ lines.add(Line.from(
+ Span.styled(" Failed: ", Style.EMPTY.dim()),
+ Span.styled(String.format("%" + w + "d", route.failed), failStyle)));
+ lines.add(Line.from(
+ Span.styled(" Inflight: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + w + "d", route.inflight))));
+
+ lines.add(Line.from(Span.raw("")));
+ if (route.total > 0) {
+ int tw = numWidth(route.meanTime, route.maxTime, route.minTime);
+ lines.add(Line.from(
+ Span.styled(" Mean: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + tw + "d ms", route.meanTime))));
+ lines.add(Line.from(
+ Span.styled(" Max: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + tw + "d ms", route.maxTime))));
+ lines.add(Line.from(
+ Span.styled(" Min: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + tw + "d ms", route.minTime))));
+ }
+
+ if (route.sinceLastCompleted != null || route.sinceLastFailed != null) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" Since last:", Style.EMPTY.dim())));
+ if (route.sinceLastCompleted != null) {
+ lines.add(Line.from(
+ Span.styled(" success: ", Style.EMPTY.dim()),
+ Span.raw(route.sinceLastCompleted)));
+ }
+ if (route.sinceLastFailed != null) {
+ lines.add(Line.from(
+ Span.styled(" fail: ", Style.EMPTY.dim()),
+ Span.styled(route.sinceLastFailed,
+ Style.EMPTY.fg(Color.LIGHT_RED))));
+ }
+ }
+
+ } else {
+ var topoNode = diagram.getSelectedTopologyNode();
+ if (topoNode != null) {
+ boolean isInbound = "external-in".equals(topoNode.nodeType);
+ lines.add(Line.from(
+ Span.styled(isInbound ? " Inbound" : " Outbound",
+ Style.EMPTY.fg(Color.CYAN).bold())));
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" URI: ", Style.EMPTY.dim()),
+ Span.raw(topoNode.from != null ? topoNode.from : "")));
+ if (topoNode.description != null && !topoNode.description.isBlank()) {
+ lines.add(Line.from(
+ Span.styled(" Path: ", Style.EMPTY.dim()),
+ Span.raw(topoNode.description)));
+ }
+ String connectedRoute = diagram.getConnectedRouteId(routeId);
+ if (connectedRoute != null) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(isInbound ? " To route: " : " From route: ", Style.EMPTY.dim()),
+ Span.styled(connectedRoute, Style.EMPTY.fg(Color.WHITE))));
+ }
+ if (topoNode.exchangesTotal > 0 || topoNode.exchangesFailed > 0) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" Total: ", Style.EMPTY.dim()),
+ Span.raw(String.valueOf(topoNode.exchangesTotal))));
+ if (topoNode.exchangesFailed > 0) {
+ lines.add(Line.from(
+ Span.styled(" Failed: ", Style.EMPTY.dim()),
+ Span.styled(String.valueOf(topoNode.exchangesFailed),
+ Style.EMPTY.fg(Color.LIGHT_RED).bold())));
+ }
+ }
+ } else {
+ lines.add(Line.from(
+ Span.styled(" " + routeId, Style.EMPTY.fg(Color.CYAN).bold())));
+ lines.add(Line.from(
+ Span.styled(" (external endpoint)", Style.EMPTY.dim())));
+ }
+ }
+
+ Paragraph paragraph = Paragraph.builder()
+ .text(Text.from(lines))
+ .block(Block.builder().borderType(BorderType.ROUNDED)
+ .title(" Info ").build())
+ .build();
+ frame.renderWidget(paragraph, area);
+ }
+
+ private void renderEipInfoPanel(Frame frame, Rect area) {
+ List<Line> lines = new ArrayList<>();
+ var selected = diagram.getSelectedEipNodeBox();
+ if (selected != null && selected.layoutNode() != null) {
+ var ln = selected.layoutNode();
+
+ String typeLabel = ln.type != null ? ln.type : "unknown";
+ Color eipColor = org.apache.camel.dsl.jbang.core.commands.tui.diagram.DiagramColors.getEipColor(typeLabel);
+ lines.add(Line.from(
+ Span.styled(" [" + typeLabel + "]", Style.EMPTY.fg(eipColor).bold())));
+
+ String label = String.join("", ln.wrappedLines);
+ if (!label.isBlank()) {
+ lines.add(Line.from(
+ Span.styled(" ", Style.EMPTY.dim()),
+ Span.raw(label)));
+ }
+
+ if (ln.id != null) {
+ lines.add(Line.from(
+ Span.styled(" ID: ", Style.EMPTY.dim()),
+ Span.raw(ln.id)));
+ }
+
+ lines.add(Line.from(Span.raw("")));
+ String linkedRoute = diagram.findLinkedRouteId(drillDownRouteId);
+ if (linkedRoute != null && diagram.getRouteLayout(linkedRoute) != null) {
+ lines.add(Line.from(
+ Span.styled(" ↵ ", Style.EMPTY.fg(Color.YELLOW).bold()),
+ Span.styled(linkedRoute, Style.EMPTY.fg(Color.WHITE))));
+ } else if (ln.treeNode != null && ln.treeNode.info.remote) {
+ String arrow = "from".equals(ln.type) ? " external → " : " → external";
+ lines.add(Line.from(
+ Span.styled(arrow, Style.EMPTY.fg(Color.DARK_GRAY))));
+ } else {
+ lines.add(Line.from(Span.raw("")));
+ }
+
+ if (ln.treeNode != null && ln.treeNode.info.stat != null) {
+ var stat = ln.treeNode.info.stat;
+ lines.add(Line.from(Span.raw("")));
+ int w = numWidth(stat.exchangesTotal, stat.exchangesFailed, stat.exchangesInflight);
+ lines.add(Line.from(
+ Span.styled(" Total: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + w + "d", stat.exchangesTotal))));
+ Style failStyle = stat.exchangesFailed > 0
+ ? Style.EMPTY.fg(Color.LIGHT_RED).bold() : Style.EMPTY;
+ lines.add(Line.from(
+ Span.styled(" Failed: ", Style.EMPTY.dim()),
+ Span.styled(String.format("%" + w + "d", stat.exchangesFailed), failStyle)));
+ lines.add(Line.from(
+ Span.styled(" Inflight: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + w + "d", stat.exchangesInflight))));
+
+ if (stat.exchangesTotal > 0) {
+ lines.add(Line.from(Span.raw("")));
+ int tw = numWidth(stat.meanProcessingTime, stat.maxProcessingTime,
+ stat.minProcessingTime, stat.lastProcessingTime);
+ lines.add(Line.from(
+ Span.styled(" Mean: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + tw + "d ms", stat.meanProcessingTime))));
+ lines.add(Line.from(
+ Span.styled(" Max: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + tw + "d ms", stat.maxProcessingTime))));
+ lines.add(Line.from(
+ Span.styled(" Min: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + tw + "d ms", stat.minProcessingTime))));
+ lines.add(Line.from(
+ Span.styled(" Last: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + tw + "d ms", stat.lastProcessingTime))));
+
+ if (stat.lastCompletedExchangeTimestamp > 0 || stat.lastFailedExchangeTimestamp > 0) {
+ long now = System.currentTimeMillis();
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" Since last:", Style.EMPTY.dim())));
+ if (stat.lastCompletedExchangeTimestamp > 0) {
+ long ago = now - stat.lastCompletedExchangeTimestamp;
+ lines.add(Line.from(
+ Span.styled(" success: ", Style.EMPTY.dim()),
+ Span.raw(TimeUtils.printDuration(ago, false))));
+ }
+ if (stat.lastFailedExchangeTimestamp > 0) {
+ long ago = now - stat.lastFailedExchangeTimestamp;
+ lines.add(Line.from(
+ Span.styled(" fail: ", Style.EMPTY.dim()),
+ Span.styled(TimeUtils.printDuration(ago, false),
+ Style.EMPTY.fg(Color.LIGHT_RED))));
+ }
+ }
+ }
+ }
+ } else {
+ lines.add(Line.from(Span.styled(" (no node selected)", Style.EMPTY.dim())));
+ }
+
+ Paragraph paragraph = Paragraph.builder()
+ .text(Text.from(lines))
+ .block(Block.builder().borderType(BorderType.ROUNDED)
+ .title(" Info ").build())
+ .build();
+ frame.renderWidget(paragraph, area);
+ }
+
+ private Line buildBreadcrumbTitle() {
+ Style nameStyle = Style.EMPTY.fg(Color.YELLOW).bold();
+ List<Span> spans = new ArrayList<>();
+ spans.add(Span.raw(" Route ["));
+ if (routeNavigationStack.isEmpty()) {
+ spans.add(Span.styled(drillDownRouteId, nameStyle));
+ } else {
+ for (var it = routeNavigationStack.descendingIterator(); it.hasNext();) {
+ spans.add(Span.styled(it.next(), nameStyle));
+ spans.add(Span.raw(" → "));
+ }
+ spans.add(Span.styled(drillDownRouteId, nameStyle));
+ }
+ spans.add(Span.raw("] "));
+ return Line.from(spans);
+ }
+
// ---- Rendering helpers ----
private void renderProcessors(Frame frame, Rect area, RouteInfo route) {
@@ -606,64 +1113,6 @@ class RoutesTab implements MonitorTab {
frame.renderStatefulWidget(table, area, processorTableState);
}
- private void renderRouteHeader(Frame frame, Rect area, IntegrationInfo info) {
- RouteInfo route = null;
- if (diagramRouteId != null) {
- for (RouteInfo r : info.routes) {
- if (diagramRouteId.equals(r.routeId)) {
- route = r;
- break;
- }
- }
- }
-
- List<Row> rows = new ArrayList<>();
- if (route != null) {
- Style stateStyle = "Started".equals(route.state)
- ? Style.EMPTY.fg(Color.GREEN)
- : Style.EMPTY.fg(Color.LIGHT_RED);
- Style failStyle = route.failed > 0
- ? Style.EMPTY.fg(Color.LIGHT_RED).bold()
- : Style.EMPTY;
- rows.add(Row.from(
- Cell.from(Span.styled(route.routeId != null ? route.routeId : "", Style.EMPTY.fg(Color.CYAN))),
- Cell.from(routeFromLabel(route)),
- Cell.from(Span.styled(route.state != null ? route.state : "", stateStyle)),
- Cell.from(route.uptime != null ? route.uptime : ""),
- rightCell(route.throughput != null ? route.throughput : "", 8),
- rightCell(String.valueOf(route.total), 8),
- rightCell(String.valueOf(route.failed), 6, failStyle),
- rightCell(String.valueOf(route.inflight), 8)));
- }
-
- Table table = Table.builder()
- .rows(rows)
- .header(Row.from(
- Cell.from(Span.styled("ROUTE", Style.EMPTY.bold())),
- Cell.from(Span.styled("FROM", Style.EMPTY.bold())),
- Cell.from(Span.styled("STATUS", Style.EMPTY.bold())),
- Cell.from(Span.styled("AGE", Style.EMPTY.bold())),
- rightCell("MSG/S", 8, Style.EMPTY.bold()),
- rightCell("TOTAL", 8, Style.EMPTY.bold()),
- rightCell("FAIL", 6, Style.EMPTY.bold()),
- rightCell("INFLIGHT", 8, Style.EMPTY.bold())))
- .widths(
- Constraint.length(12),
- Constraint.fill(),
- Constraint.length(10),
- Constraint.length(8),
- Constraint.length(8),
- Constraint.length(8),
- Constraint.length(6),
- Constraint.length(8))
- .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
- .block(Block.builder().borderType(BorderType.ROUNDED)
- .title(" Route ").build())
- .build();
-
- frame.renderStatefulWidget(table, area, routeHeaderTableState);
- }
-
// ---- Sorting ----
private int sortRoute(RouteInfo a, RouteInfo b) {
@@ -831,15 +1280,7 @@ class RoutesTab implements MonitorTab {
// ---- Async loading ----
- private void loadDiagramForSelectedRoute() {
- loadDiagramForSelectedRoute(true);
- }
-
- private void reloadDiagramQuietly() {
- loadDiagramForSelectedRoute(false);
- }
-
- private void loadDiagramForSelectedRoute(boolean showPlaceholder) {
+ private void loadDiagram(boolean showPlaceholder) {
if (ctx.selectedPid == null || ctx.runner == null) {
return;
}
@@ -847,41 +1288,28 @@ class RoutesTab implements MonitorTab {
return;
}
- IntegrationInfo info = ctx.findSelectedIntegration();
- if (info == null || info.routes.isEmpty()) {
- diagram.endLoad();
- return;
- }
-
- List<RouteInfo> sortedRoutes = new ArrayList<>(info.routes);
- sortedRoutes.sort(this::sortRoute);
-
- Integer sel = routeTableState.selected();
- RouteInfo selectedRoute;
- if (sel != null && sel >= 0 && sel < sortedRoutes.size()) {
- selectedRoute = sortedRoutes.get(sel);
- } else {
- selectedRoute = sortedRoutes.get(0);
- }
-
String pid = ctx.selectedPid;
boolean showMetrics = diagramMetrics;
- String routeId = diagramAllRoutes ? null : selectedRoute.routeId;
+ boolean external = showExternal;
- diagramRouteId = routeId != null ? routeId : "all";
if (showPlaceholder) {
diagram.setLoadingPlaceholder();
}
ctx.runner.scheduler().execute(() -> {
try {
- diagram.loadRouteDiagramInBackground(ctx, pid, routeId, showMetrics);
+ diagram.setTopologyMode(topologyMode);
+ diagram.loadAllDiagramsInBackground(ctx, pid, showMetrics, external);
} finally {
diagram.endLoad();
}
});
}
+ private void reloadDiagram() {
+ loadDiagram(false);
+ }
+
private void loadSourceForSelectedRoute() {
IntegrationInfo info = ctx.findSelectedIntegration();
if (info == null || info.routes.isEmpty()) {
@@ -895,6 +1323,19 @@ class RoutesTab implements MonitorTab {
sourceViewer.loadSource(ctx, selectedRoute.routeId, 0);
}
+ private void loadSourceForSelectedNode() {
+ if (drillDownRouteId == null) {
+ return;
+ }
+ int targetLine = 0;
+ var selected = diagram.getSelectedEipNodeBox();
+ if (selected != null && selected.layoutNode() != null
+ && selected.layoutNode().treeNode != null) {
+ targetLine = selected.layoutNode().treeNode.info.line;
+ }
+ sourceViewer.loadSource(ctx, drillDownRouteId, targetLine);
+ }
+
@Override
public SelectionContext getSelectionContext() {
IntegrationInfo info = ctx.findSelectedIntegration();
@@ -951,51 +1392,68 @@ class RoutesTab implements MonitorTab {
## Route Diagram
- Press `d` to see a visual flow chart of the selected route. The diagram
- shows every EIP node and how messages flow between them. Numbers on
- each node show how many exchanges passed through it.
+ Press `d` to see a topology diagram showing how all routes connect to each
+ other. This is the same view as the Diagram tab. Use arrow keys to navigate
+ between route boxes and press `Enter` to drill down into a route's internal
+ EIP structure.
- Scroll down to view the diagram example:
+ ## Navigation
- ```
- ┌──────────────────────┐
- │ from[timer:hello?..] │
- └──────────────────────┘
- │
- ▼ 29
- ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
- ╎ ┌────────────────────────┐ ╎
- ╎ │ choice │ ╎
- ╎ └────────────────────────┘ ╎
- ╎ │ │ ╎
- ╎ ▼ 9 ▼ 20 ╎
- ╎ ┌──────────┐ ┌──────────┐ ╎
- ╎ │ log[HI] │ │ log[LO] │ ╎
- ╎ └──────────┘ └──────────┘ ╎
- ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
- ```
+ In the topology view, use arrow keys to select route boxes:
+ - `↑↓` moves between layers (upstream/downstream routes)
+ - `←→` moves between routes in the same layer
+
+ When a route is selected, an **Info panel** appears on the left
+ showing key metrics: state, uptime, throughput, exchange counts,
+ and processing times.
+
+ Press `Enter` on a selected route to **drill down** into its
+ internal EIP structure (the route diagram). Press `Esc` to
+ return to the topology view.
- The dotted border groups nodes that belong to the same EIP block
- (like `choice`, `split`, `multicast`). The numbers show how many
- exchanges took each branch — useful for verifying routing logic.
+ ## Route Diagram (drill-down)
+
+ In the route diagram, each EIP node shows its type tag (colored)
+ and endpoint URI or description. Nodes that connect to other routes
+ display a `↵` indicator — press `Enter` to jump directly to the
+ linked route's diagram.
+
+ Navigation history is maintained as a stack: pressing `Esc` goes
+ back to the previous route, and eventually back to the topology view.
## Source View
- Press `s` to see the original route source code (YAML, XML, or Java).
+ Press `c` to see the original route source code (YAML, XML, or Java).
## Keys
+ **Route table:**
- `Up/Down` — select route
- `p` — start/stop selected route
- `P` — suspend/resume selected route
- - `d` — show route diagram
- - `a` — toggle diagram scope (single route or all routes)
+ - `d` — show topology diagram
- `c` — show route source code
- - `m` — toggle metrics in diagram
+ - `n` — toggle description labels
- `s` — cycle sort column
- `S` — reverse sort order
- `t` — toggle Top mode
- - `Esc` — back to route list
+
+ **Topology view:**
+ - `↑↓←→` — navigate between route boxes
+ - `Enter` — drill down into selected route
+ - `Esc` — close diagram (back to route table)
+ - `m` — toggle metrics on/off
+ - `e` — toggle external systems on/off
+ - `n` — toggle description labels
+
+ **Route diagram (drill-down):**
+ - `↑↓←→` — navigate between EIP nodes
+ - `Enter` — jump to linked route (when `↵` indicator shown)
+ - `Esc` — go back (previous route or topology)
+ - `t` — jump back to topology view
+ - `c` — show source code
+ - `m` — toggle metrics
+ - `n` — toggle description labels
""";
}
@@ -1033,4 +1491,12 @@ class RoutesTab implements MonitorTab {
result.put("selectedIndex", sel != null ? sel : -1);
return result;
}
+
+ private static int numWidth(long... values) {
+ long max = 0;
+ for (long v : values) {
+ max = Math.max(max, Math.abs(v));
+ }
+ return Math.max(1, String.valueOf(max).length());
+ }
}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
index cbfc5e124b25..3e4f6b0ae5af 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
@@ -378,7 +378,7 @@ class RunOptionsForm {
int popupW = Math.min(56, area.width() - 4);
int popupH = 11;
int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
- int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+ int y = area.top() + Math.max(0, (area.height() - popupH) / 4);
Rect popup = new Rect(x, y, Math.min(popupW, area.width()), Math.min(popupH, area.height()));
frame.renderWidget(Clear.INSTANCE, popup);
@@ -448,7 +448,7 @@ class RunOptionsForm {
int propCount = properties != null ? properties.size() : 0;
int popupH = Math.min(propCount + 2, Math.min(20, area.height() - 4));
int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
- int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+ int y = area.top() + Math.max(0, (area.height() - popupH) / 4);
Rect popup = new Rect(x, y, Math.min(popupW, area.width()), Math.min(popupH, area.height()));
frame.renderWidget(Clear.INSTANCE, popup);