This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch feature/CAMEL-23672-diagram-external-modes
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 07b35fec56b7cd895a4bd78b56f03c5c48bed613
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jun 6 17:25:59 2026 +0200

    CAMEL-23672: TUI - Diagram external toggle with three modes (off/edges/all)
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/diagram/TopologyHelper.java   | 69 ++++++++++++++++++++++
 .../jbang/core/commands/tui/DiagramSupport.java    | 43 +++++++++++++-
 .../dsl/jbang/core/commands/tui/DiagramTab.java    | 43 ++++++++------
 .../dsl/jbang/core/commands/tui/RoutesTab.java     | 11 ++--
 .../tui/diagram/TopologyDiagramWidget.java         |  3 +-
 .../tui/diagram/TopologyMinimapWidget.java         | 13 ++--
 6 files changed, 147 insertions(+), 35 deletions(-)

diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
index ce48c0d105a5..d59a2a3bb1ad 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
@@ -17,7 +17,11 @@
 package org.apache.camel.diagram;
 
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import org.apache.camel.diagram.TopologyLayoutEngine.TopologyEdgeInfo;
 import org.apache.camel.diagram.TopologyLayoutEngine.TopologyNodeInfo;
@@ -121,6 +125,71 @@ public final class TopologyHelper {
         }
     }
 
+    /**
+     * Expands external-type route-to-route edges into intermediary external 
nodes. For each shared external endpoint
+     * (e.g., kafka:foo used by both a producer and consumer route), the 
direct edge is replaced with a dashed external
+     * box and two edges passing through it.
+     */
+    public static void expandExternalEdges(List<TopologyNodeInfo> nodes, 
List<TopologyEdgeInfo> edges) {
+        // Only consider route nodes — exclude external-in/external-out nodes 
already added
+        Set<String> routeIds = nodes.stream()
+                .filter(n -> n.nodeType == null || 
!n.nodeType.startsWith("external"))
+                .map(n -> n.routeId)
+                .collect(Collectors.toSet());
+
+        // Group external edges by endpoint URI (only edges between two route 
nodes)
+        Map<String, List<TopologyEdgeInfo>> byEndpoint = new LinkedHashMap<>();
+        for (TopologyEdgeInfo edge : edges) {
+            if ("external".equals(edge.connectionType)
+                    && routeIds.contains(edge.fromRouteId) && 
routeIds.contains(edge.toRouteId)) {
+                byEndpoint.computeIfAbsent(edge.endpoint, k -> new 
ArrayList<>()).add(edge);
+            }
+        }
+        if (byEndpoint.isEmpty()) {
+            return;
+        }
+
+        int idx = 0;
+        for (Map.Entry<String, List<TopologyEdgeInfo>> entry : 
byEndpoint.entrySet()) {
+            String uri = entry.getKey();
+            List<TopologyEdgeInfo> group = entry.getValue();
+
+            // Create intermediary external node
+            TopologyNodeInfo extNode = new TopologyNodeInfo();
+            extNode.routeId = "ext-" + idx++;
+            extNode.from = uri;
+            extNode.nodeType = "external";
+            int colonIdx = uri.indexOf(':');
+            extNode.description = colonIdx > 0 ? uri.substring(colonIdx + 1) : 
uri;
+
+            // Determine scheme from URI
+            if (colonIdx > 0) {
+                extNode.fromScheme = uri.substring(0, colonIdx);
+            }
+
+            nodes.add(extNode);
+
+            // Replace each original edge with two edges through the 
intermediary node
+            for (TopologyEdgeInfo orig : group) {
+                edges.remove(orig);
+
+                TopologyEdgeInfo toExt = new TopologyEdgeInfo();
+                toExt.fromRouteId = orig.fromRouteId;
+                toExt.toRouteId = extNode.routeId;
+                toExt.endpoint = uri;
+                toExt.connectionType = "external";
+                edges.add(toExt);
+
+                TopologyEdgeInfo fromExt = new TopologyEdgeInfo();
+                fromExt.fromRouteId = extNode.routeId;
+                fromExt.toRouteId = orig.toRouteId;
+                fromExt.endpoint = uri;
+                fromExt.connectionType = "external";
+                edges.add(fromExt);
+            }
+        }
+    }
+
     public static void enrichWithMetrics(List<TopologyNodeInfo> nodes, 
JsonObject routeStructureJson) {
         if (routeStructureJson == null) {
             return;
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 c31681f97321..a3fc734acf7b 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
@@ -409,7 +409,7 @@ class DiagramSupport {
             ctx.runner.scheduler().execute(() -> {
                 try {
                     setTopologyMode(true);
-                    loadAllDiagramsInBackground(ctx, pid, false, false);
+                    loadAllDiagramsInBackground(ctx, pid, false, 0);
                 } finally {
                     endLoad();
                 }
@@ -640,7 +640,8 @@ class DiagramSupport {
         for (TopologyLayoutNode node : result.nodes) {
             int col = nodeW == 0 ? 0 : node.x * bw / nodeW;
             int row = node.y / 20;
-            boolean ext = "external-in".equals(node.nodeType) || 
"external-out".equals(node.nodeType);
+            boolean ext = "external-in".equals(node.nodeType) || 
"external-out".equals(node.nodeType)
+                    || "external".equals(node.nodeType);
             int contentLines;
             if (ext) {
                 contentLines = 1;
@@ -828,6 +829,10 @@ class DiagramSupport {
             if ("from".equals(type)) {
                 // "from" node: find route that sends TO this endpoint
                 if (currentRouteId.equals(edge.to.routeId) && 
!currentRouteId.equals(edge.from.routeId)) {
+                    String resolved = resolveThrough(edge.from.routeId, 
currentRouteId);
+                    if (resolved != null) {
+                        return resolved;
+                    }
                     return edge.from.routeId;
                 }
             } else {
@@ -835,6 +840,10 @@ class DiagramSupport {
                 if (currentRouteId.equals(edge.from.routeId) && 
!currentRouteId.equals(edge.to.routeId)) {
                     String targetFrom = stripQueryParams(edge.to.from);
                     if (baseUri.equals(targetFrom)) {
+                        String resolved = resolveThrough(edge.to.routeId, 
currentRouteId);
+                        if (resolved != null) {
+                            return resolved;
+                        }
                         return edge.to.routeId;
                     }
                 }
@@ -856,6 +865,30 @@ class DiagramSupport {
         return null;
     }
 
+    private String resolveThrough(String nodeId, String excludeRouteId) {
+        if (!"external".equals(findNodeType(nodeId))) {
+            return null;
+        }
+        for (TopologyLayoutEdge e : topologyEdges) {
+            if (nodeId.equals(e.from.routeId) && 
!excludeRouteId.equals(e.to.routeId)) {
+                return e.to.routeId;
+            }
+            if (nodeId.equals(e.to.routeId) && 
!excludeRouteId.equals(e.from.routeId)) {
+                return e.from.routeId;
+            }
+        }
+        return null;
+    }
+
+    private String findNodeType(String nodeId) {
+        for (TopologyLayoutNode n : topologyNodes) {
+            if (nodeId.equals(n.routeId)) {
+                return n.nodeType;
+            }
+        }
+        return null;
+    }
+
     static String getBaseUri(RouteDiagramLayoutEngine.NodeInfo info) {
         String uri = info.uri;
         if (uri == null) {
@@ -1720,8 +1753,9 @@ class DiagramSupport {
     }
 
     void loadAllDiagramsInBackground(
-            MonitorContext ctx, String pid, boolean metrics, boolean external) 
{
+            MonitorContext ctx, String pid, boolean metrics, int externalMode) 
{
         // Single IPC call: topology + route structures
+        boolean external = externalMode > 0;
         JsonObject topoJson = requestRouteTopology(ctx, pid, external, true);
 
         TopologyLayoutResult topoResult = null;
@@ -1735,6 +1769,9 @@ class DiagramSupport {
             if (external) {
                 TopologyHelper.addExternalEndpoints(nodes, edges, topoJson);
             }
+            if (externalMode == 2) {
+                TopologyHelper.expandExternalEdges(nodes, edges);
+            }
             if (!nodes.isEmpty()) {
                 TopologyLayoutEngine engine = new TopologyLayoutEngine();
                 topoResult = engine.layout(nodes, edges);
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 aeea11c9205d..01dcc181258c 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
@@ -45,7 +45,8 @@ class DiagramTab implements MonitorTab {
     private final DiagramSupport diagram = new DiagramSupport();
     private final SourceViewer sourceViewer = new SourceViewer();
     private boolean diagramMetrics = true;
-    private boolean showExternal;
+    private static final String[] EXTERNAL_LABELS = { " [off]", " [edges]", " 
[all]" };
+    private int externalMode;
     private boolean topologyMode = true;
     private String drillDownRouteId;
     private final Deque<String> routeNavigationStack = new ArrayDeque<>();
@@ -175,9 +176,9 @@ class DiagramTab implements MonitorTab {
             return true;
         }
 
-        // Toggle external systems
+        // Cycle external systems: off → edges → all → off
         if (diagram.isShowDiagram() && topologyMode && 
ke.isCharIgnoreCase('e')) {
-            showExternal = !showExternal;
+            externalMode = (externalMode + 1) % 3;
             diagram.endLoad();
             reloadDiagram();
             return true;
@@ -465,9 +466,10 @@ class DiagramTab implements MonitorTab {
             var topoNode = diagram.getSelectedTopologyNode();
             if (topoNode != null) {
                 boolean isInbound = "external-in".equals(topoNode.nodeType);
+                boolean isBridge = "external".equals(topoNode.nodeType);
+                String label = isBridge ? " External" : isInbound ? " Inbound" 
: " Outbound";
                 lines.add(Line.from(
-                        Span.styled(isInbound ? " Inbound" : " Outbound",
-                                Style.EMPTY.fg(Color.CYAN).bold())));
+                        Span.styled(label, 
Style.EMPTY.fg(Color.CYAN).bold())));
                 lines.add(Line.from(Span.raw("")));
                 lines.add(Line.from(
                         Span.styled(" URI: ", Style.EMPTY.dim()),
@@ -477,12 +479,14 @@ class DiagramTab implements MonitorTab {
                             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 (!isBridge) {
+                    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("")));
@@ -645,7 +649,7 @@ class DiagramTab implements MonitorTab {
             }
             hint(spans, "m", "metrics" + (diagramMetrics ? " [on]" : " 
[off]"));
             if (topologyMode) {
-                hint(spans, "e", "external" + (showExternal ? " [on]" : " 
[off]"));
+                hint(spans, "e", "external" + EXTERNAL_LABELS[externalMode]);
             }
             hint(spans, "n", "description" + (diagram.isShowDescription() ? " 
[on]" : " [off]"));
         }
@@ -675,7 +679,7 @@ class DiagramTab implements MonitorTab {
 
         String pid = ctx.selectedPid;
         boolean showMetrics = diagramMetrics;
-        boolean external = showExternal;
+        int external = externalMode;
 
         if (showPlaceholder) {
             diagram.setLoadingPlaceholder();
@@ -738,10 +742,15 @@ class DiagramTab implements MonitorTab {
 
                                 ## External Systems
 
-                                When external systems are enabled, the diagram 
shows a three-band layout:
-                                - **Top band** — external consumers sending 
messages INTO Camel
-                                - **Middle band** — the Camel routes and their 
internal connections
-                                - **Bottom band** — external producers where 
Camel sends messages OUT
+                                Press `e` to cycle through three external 
modes:
+
+                                - **off** — no external endpoints shown
+                                - **edges** — external endpoints that are 
truly outside Camel are shown
+                                  as dashed boxes in top/bottom bands. Routes 
sharing an external
+                                  endpoint (e.g. kafka) are connected with a 
direct arrow.
+                                - **all** — same as edges, but routes sharing 
an external endpoint
+                                  are connected through an intermediary dashed 
box showing the
+                                  endpoint name, instead of a direct arrow.
 
                                 External system boxes are drawn with dashed 
borders to distinguish
                                 them from route boxes. Dashed edges connect 
routes to external systems.
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 49c3453de8df..88ab80b9d2c7 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
@@ -71,7 +71,8 @@ class RoutesTab implements MonitorTab {
     private final SourceViewer sourceViewer = new SourceViewer();
     private boolean diagramMetrics = true;
     private boolean showDescription;
-    private boolean showExternal;
+    private static final String[] EXTERNAL_LABELS = { " [off]", " [edges]", " 
[all]" };
+    private int externalMode;
     private boolean topologyMode = true;
     private String drillDownRouteId;
     private final Deque<String> routeNavigationStack = new ArrayDeque<>();
@@ -214,9 +215,9 @@ class RoutesTab implements MonitorTab {
             return true;
         }
 
-        // Toggle external systems (topology mode only)
+        // Cycle external systems: off → edges → all → off (topology mode only)
         if (diagram.isShowDiagram() && topologyMode && 
ke.isCharIgnoreCase('e')) {
-            showExternal = !showExternal;
+            externalMode = (externalMode + 1) % 3;
             diagram.endLoad();
             reloadDiagram();
             return true;
@@ -666,7 +667,7 @@ class RoutesTab implements MonitorTab {
             }
             hint(spans, "m", "metrics" + (diagramMetrics ? " [on]" : " 
[off]"));
             if (topologyMode) {
-                hint(spans, "e", "external" + (showExternal ? " [on]" : " 
[off]"));
+                hint(spans, "e", "external" + EXTERNAL_LABELS[externalMode]);
             }
             hint(spans, "n", "description" + (diagram.isShowDescription() ? " 
[on]" : " [off]"));
         } else {
@@ -1333,7 +1334,7 @@ class RoutesTab implements MonitorTab {
 
         String pid = ctx.selectedPid;
         boolean showMetrics = diagramMetrics;
-        boolean external = showExternal;
+        int external = externalMode;
 
         if (showPlaceholder) {
             diagram.setLoadingPlaceholder();
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 d2187803f491..c53d2ec04205 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
@@ -388,7 +388,8 @@ public class TopologyDiagramWidget implements Widget {
     }
 
     private static boolean isExternal(TopologyLayoutNode node) {
-        return "external-in".equals(node.nodeType) || 
"external-out".equals(node.nodeType);
+        return "external-in".equals(node.nodeType) || 
"external-out".equals(node.nodeType)
+                || "external".equals(node.nodeType);
     }
 
     static List<String> wrapText(String text, int maxWidth) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyMinimapWidget.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyMinimapWidget.java
index f18ae2803244..0ebeeb90afd5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyMinimapWidget.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyMinimapWidget.java
@@ -54,8 +54,10 @@ public class TopologyMinimapWidget implements Widget {
         }
 
         for (TopologyLayoutNode node : layout.nodes) {
+            if (node.nodeType != null && node.nodeType.startsWith("external")) 
{
+                continue;
+            }
             boolean isCurrent = node.routeId != null && 
node.routeId.equals(currentRouteId);
-            boolean isExternal = "external-in".equals(node.nodeType) || 
"external-out".equals(node.nodeType);
 
             int col = node.x * mapW / totalW;
             int row = node.y * mapH / totalH;
@@ -67,14 +69,7 @@ public class TopologyMinimapWidget implements Widget {
             col = Math.max(0, col);
             row = Math.max(0, row);
 
-            Style style;
-            if (isCurrent) {
-                style = CURRENT_STYLE;
-            } else if (isExternal) {
-                style = EXTERNAL_STYLE;
-            } else {
-                style = OTHER_STYLE;
-            }
+            Style style = isCurrent ? CURRENT_STYLE : OTHER_STYLE;
 
             drawMiniBox(buffer, area, col, row, nodeW, nodeH, style, 
isCurrent);
         }

Reply via email to