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 3ce1f92a6fb094bbe15ca7281bcf1005d71c5347 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Jun 4 08:49:13 2026 +0200 CAMEL-23672: camel-tui - Enrich route structure with remote flag and UI improvements Add remote boolean to ModelDumpLine and route structure dev console JSON, so consumers (TUI, CLI, 3rd party) can identify remote endpoints without cross-referencing topology data. Route diagram now uses this to draw dashed borders for remote endpoints. Also fix topology arrow positioning when toggling description mode, and remove GROUP column from routes tab. Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../apache/camel/diagram/RouteDiagramHelper.java | 2 + .../camel/diagram/RouteDiagramLayoutEngine.java | 1 + .../java/org/apache/camel/spi/ModelDumpLine.java | 3 +- .../impl/console/RouteStructureDevConsole.java | 3 ++ .../camel/impl/DefaultModelToStructureDumper.java | 51 +++++++++++++++++++--- .../jbang/core/commands/tui/DiagramSupport.java | 5 +-- .../dsl/jbang/core/commands/tui/RoutesTab.java | 12 +---- .../commands/tui/diagram/RouteDiagramWidget.java | 22 +++++++--- .../tui/diagram/TopologyDiagramWidget.java | 16 ++++++- 9 files changed, 88 insertions(+), 27 deletions(-) diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java index 6fdd0d2028cd..1d298aa1f792 100644 --- a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java +++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java @@ -75,6 +75,8 @@ public final class RouteDiagramHelper { node.uri = Jsoner.unescape(uri); } node.description = line.getString("description"); + Boolean rem = line.getBoolean("remote"); + node.remote = rem != null && rem; Integer level = line.getInteger("level"); node.level = level != null ? level : 0; Integer lineNum = line.getInteger("line"); diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java index b234db8f1552..de3656d6b085 100644 --- a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java +++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java @@ -158,6 +158,7 @@ public class RouteDiagramLayoutEngine { public String description; public int level; public int line; + public boolean remote; public StatInfo stat; } diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/ModelDumpLine.java b/core/camel-api/src/main/java/org/apache/camel/spi/ModelDumpLine.java index a5e0ba00eb7b..3b4070bde6cc 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/ModelDumpLine.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/ModelDumpLine.java @@ -28,8 +28,9 @@ import org.jspecify.annotations.Nullable; * @param code EIP code such as label or short name that is human-readable or pseudocode * @param description optional description of the EIP node * @param uri the raw endpoint URI for endpoint-producing EIP nodes (from, to, toD, wireTap, enrich, etc.) + * @param remote whether the endpoint connects to a remote system (true) or is local/in-JVM only (false) * @since 4.16 */ public record ModelDumpLine(@Nullable String location, String type, String id, int level, String code, - @Nullable String description, @Nullable String uri) { + @Nullable String description, @Nullable String uri, boolean remote) { } diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteStructureDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteStructureDevConsole.java index f8ab84fe6859..18166b40f9d7 100644 --- a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteStructureDevConsole.java +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteStructureDevConsole.java @@ -204,6 +204,9 @@ public class RouteStructureDevConsole extends AbstractDevConsole { c.put("code", Jsoner.escape(line.code())); if (line.uri() != null) { c.put("uri", Jsoner.escape(line.uri())); + if (line.remote()) { + c.put("remote", true); + } } if (metric && mcc != null) { diff --git a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModelToStructureDumper.java b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModelToStructureDumper.java index e980ced54f7d..eca8d90ce6a3 100644 --- a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModelToStructureDumper.java +++ b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModelToStructureDumper.java @@ -17,9 +17,12 @@ package org.apache.camel.impl; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.camel.CamelContext; +import org.apache.camel.Endpoint; import org.apache.camel.NamedNode; import org.apache.camel.model.EndpointRequiredDefinition; import org.apache.camel.model.Model; @@ -39,6 +42,8 @@ public class DefaultModelToStructureDumper implements ModelToStructureDumper { // dump in text format padded by level List<ModelDumpLine> answer = new ArrayList<>(); + Map<String, Boolean> schemeRemoteMap = buildSchemeRemoteMap(context); + // lookup model and runtime route final Model model = context.getCamelContextExtension().getContextPlugin(Model.class); final RouteDefinition def = model.getRouteDefinition(routeId); @@ -48,20 +53,23 @@ public class DefaultModelToStructureDumper implements ModelToStructureDumper { answer.add( new ModelDumpLine( loc, "route", def.getRouteId(), 0, "route[" + def.getRouteId() + "]", def.getDescription(), - null)); + null, false)); String uri = def.getInput().getLabel(); if (brief) { uri = StringHelper.before(uri, "?", uri); } String fromUri = def.getInput().getEndpointUri(); - answer.add(new ModelDumpLine(loc, "from", routeId, 1, "from[" + uri + "]", def.getDescription(), fromUri)); + boolean fromRemote = isRemoteUri(fromUri, schemeRemoteMap); + answer.add(new ModelDumpLine(loc, "from", routeId, 1, "from[" + uri + "]", def.getDescription(), fromUri, fromRemote)); - dumpChildren(def, scheme, brief, 2, answer); + dumpChildren(def, scheme, brief, 2, answer, schemeRemoteMap); return answer; } - private static void dumpChildren(NamedNode parent, String scheme, boolean brief, int level, List<ModelDumpLine> answer) { + private static void dumpChildren( + NamedNode parent, String scheme, boolean brief, int level, + List<ModelDumpLine> answer, Map<String, Boolean> schemeRemoteMap) { for (NamedNode child : parent.getChildren()) { if (child instanceof OptionalIdentifiedDefinition<?> output) { String loc = scheme + ":" + output.getLocation(); @@ -73,17 +81,48 @@ public class DefaultModelToStructureDumper implements ModelToStructureDumper { boolean choice = "choice".equals(kind); String code = choice || brief ? output.getShortName() : output.getLabel(); String endpointUri = null; + boolean remote = false; if (output instanceof EndpointRequiredDefinition erd) { endpointUri = erd.getEndpointUri(); + remote = isRemoteUri(endpointUri, schemeRemoteMap); if (brief) { String uri = StringHelper.before(endpointUri, "?", endpointUri); code = output.getShortName() + "[" + uri + "]"; } } - answer.add(new ModelDumpLine(loc, kind, id, level, code, output.getDescription(), endpointUri)); + answer.add(new ModelDumpLine(loc, kind, id, level, code, output.getDescription(), endpointUri, remote)); + } + dumpChildren(child, scheme, brief, level + 1, answer, schemeRemoteMap); + } + } + + private static Map<String, Boolean> buildSchemeRemoteMap(CamelContext context) { + Map<String, Boolean> map = new HashMap<>(); + for (Endpoint ep : context.getEndpoints()) { + if ("StubEndpoint".equals(ep.getClass().getSimpleName())) { + continue; } - dumpChildren(child, scheme, brief, level + 1, answer); + String uri = ep.getEndpointUri(); + int colonIdx = uri.indexOf(':'); + if (colonIdx > 0) { + String s = uri.substring(0, colonIdx); + map.putIfAbsent(s, ep.isRemote()); + } + } + return map; + } + + private static boolean isRemoteUri(String uri, Map<String, Boolean> schemeRemoteMap) { + if (uri == null) { + return false; + } + int colonIdx = uri.indexOf(':'); + if (colonIdx <= 0) { + return false; } + String scheme = uri.substring(0, colonIdx); + Boolean remote = schemeRemoteMap.get(scheme); + return remote != null && remote; } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java index fcfcbb61bfc7..30d2de167198 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java @@ -1240,9 +1240,8 @@ class DiagramSupport { JsonObject root = new JsonObject(); root.put("action", "route-topology"); root.put("metric", "true"); - if (external) { - root.put("external", "true"); - } + // Always request external endpoints so route diagrams can show dashed borders + root.put("external", "true"); if (routes) { root.put("routes", "true"); } 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 3b457829afa4..24c72adc6df5 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 @@ -45,7 +45,7 @@ import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*; class RoutesTab implements MonitorTab { - private static final String[] ROUTE_SORT_COLUMNS = { "name", "group", "from", "status", "total", "failed" }; + private static final String[] ROUTE_SORT_COLUMNS = { "name", "from", "status", "total", "failed" }; private static final String[] ROUTE_TOP_SORT_COLUMNS = { "mean", "max", "min", "last", "delta" }; private final MonitorContext ctx; @@ -334,7 +334,6 @@ class RoutesTab implements MonitorTab { routeRows.add(Row.from( Cell.from(Span.styled(route.routeId != null ? route.routeId : "", Style.EMPTY.fg(Color.CYAN))), - Cell.from(Span.styled(route.group != null ? route.group : "", Style.EMPTY.dim())), Cell.from(route.from != null ? route.from : ""), Cell.from(Span.styled(route.state != null ? route.state : "", stateStyle)), Cell.from(route.uptime != null ? route.uptime : ""), @@ -353,7 +352,6 @@ class RoutesTab implements MonitorTab { .rows(routeRows) .header(Row.from( Cell.from(Span.styled(routeSortLabel("ROUTE", "name"), routeSortStyle("name"))), - Cell.from(Span.styled(routeSortLabel("GROUP", "group"), routeSortStyle("group"))), Cell.from(Span.styled(routeSortLabel("FROM", "from"), routeSortStyle("from"))), Cell.from(Span.styled(routeSortLabel("STATUS", "status"), routeSortStyle("status"))), Cell.from(Span.styled("AGE", Style.EMPTY.bold())), @@ -365,8 +363,7 @@ class RoutesTab implements MonitorTab { rightCell("MIN/MAX/MEAN", 14, Style.EMPTY.bold()), Cell.from(Span.styled("SINCE-LAST", Style.EMPTY.bold())))) .widths( - Constraint.length(12), - Constraint.length(14), + Constraint.length(24), Constraint.fill(), Constraint.length(10), Constraint.length(8), @@ -668,11 +665,6 @@ class RoutesTab implements MonitorTab { String sb2 = b.state != null ? b.state : ""; yield sa.compareToIgnoreCase(sb2); } - case "group" -> { - String ga = a.group != null ? a.group : ""; - String gb = b.group != null ? b.group : ""; - yield ga.compareToIgnoreCase(gb); - } case "from" -> { String fa = a.from != null ? a.from : ""; String fb = b.from != null ? b.from : ""; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java index 68f24106f2e5..97e3cfc8367b 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java @@ -68,7 +68,8 @@ public class RouteDiagramWidget implements Widget { LayoutRoute layoutRoute, int nodeWidth, int selectedNodeIndex, int scrollX, int scrollY, boolean showMetrics) { - this(layoutRoute, nodeWidth, selectedNodeIndex, scrollX, scrollY, showMetrics, Collections.emptyMap()); + this(layoutRoute, nodeWidth, selectedNodeIndex, scrollX, scrollY, showMetrics, + Collections.emptyMap()); } public RouteDiagramWidget( @@ -138,6 +139,7 @@ public class RouteDiagramWidget implements Widget { int nodeIdx = nodeBoxes.size(); boolean selected = nodeIdx == selectedNodeIndex; + boolean external = isExternalEndpoint(node); Color eipColor = getEipColor(node.type); Style borderStyle = Style.EMPTY.fg(eipColor); @@ -145,10 +147,13 @@ public class RouteDiagramWidget implements Widget { borderStyle = borderStyle.patch(SELECTION_STYLE); } + char hChar = external ? DASH_H : H; + char vChar = external ? DASH_V : V; + // Top border setChar(buffer, area, row, col, TL, borderStyle); for (int c = col + 1; c < col + boxWidth - 1; c++) { - setChar(buffer, area, row, c, H, borderStyle); + setChar(buffer, area, row, c, hChar, borderStyle); } setChar(buffer, area, row, col + boxWidth - 1, TR, borderStyle); @@ -156,15 +161,15 @@ public class RouteDiagramWidget implements Widget { int bottom = row + height - 1; setChar(buffer, area, bottom, col, BL, borderStyle); for (int c = col + 1; c < col + boxWidth - 1; c++) { - setChar(buffer, area, bottom, c, H, borderStyle); + setChar(buffer, area, bottom, c, hChar, borderStyle); } setChar(buffer, area, bottom, col + boxWidth - 1, BR, borderStyle); // Content rows for (int i = 0; i < lines.size(); i++) { int r = row + 1 + i; - setChar(buffer, area, r, col, V, borderStyle); - setChar(buffer, area, r, col + boxWidth - 1, V, borderStyle); + setChar(buffer, area, r, col, vChar, borderStyle); + setChar(buffer, area, r, col + boxWidth - 1, vChar, borderStyle); Style bgStyle = selected ? SELECTION_STYLE : Style.EMPTY; for (int c = col + 1; c < col + boxWidth - 1; c++) { @@ -414,6 +419,13 @@ public class RouteDiagramWidget implements Widget { return pixelX * boxWidth / nodeWidth; } + private boolean isExternalEndpoint(LayoutNode node) { + if (node.treeNode == null) { + return false; + } + return node.treeNode.info.remote; + } + private String findLinkedRouteId(LayoutNode node) { if (linkableEndpoints.isEmpty() || node.treeNode == null) { return null; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java index 8fb84a3e9f05..7b4030d1e02a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java @@ -290,11 +290,23 @@ public class TopologyDiagramWidget implements Widget { } return 2 + lines; } - int lines = 3; + String label = showDescription && node.description != null && !node.description.isBlank() + ? node.description : node.routeId; + int lines = wrapText(label, boxWidth - 4).size(); + if (!showDescription) { + List<String> fromLines = wrapText("(" + node.from + ")", boxWidth - 4); + lines += fromLines.size(); + if (fromLines.size() < 2) { + lines++; + } + } if (showMetrics) { lines++; } - return 2 + Math.min(lines, MAX_WRAP_LINES + 1); + while (lines > MAX_WRAP_LINES + 1) { + lines--; + } + return 2 + lines; } private void setChar(Buffer buffer, Rect area, int gridRow, int gridCol, char ch, Style style) {
