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) {

Reply via email to