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); }
