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

davsclaus pushed a commit to branch topology-external-endpoints
in repository https://gitbox.apache.org/repos/asf/camel.git

commit b9b31650b2534ec5fac60ea19931f57e24fd1402
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Jun 2 12:02:40 2026 +0200

    Add external systems toggle to TUI diagram tab
    
    Press 'e' to toggle external systems on/off in the topology diagram.
    When enabled, the diagram shows a three-band layout with external
    consumers at the top, routes in the middle, and external producers
    at the bottom. External node names are colored cyan in the TUI.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../camel/diagram/RouteDiagramAsciiRenderer.java   |  3 +-
 dsl/camel-jbang/camel-jbang-core/pom.xml           |  4 +
 .../examples/camel-jbang-example-catalog.json      | 23 +++++
 .../resources/examples/route-topology/README.md    | 61 ++++++++++++++
 .../examples/route-topology/application.properties |  3 +
 .../route-topology/route-topology.camel.yaml       | 97 ++++++++++++++++++++++
 .../jbang/core/commands/tui/DiagramSupport.java    | 17 ++--
 .../dsl/jbang/core/commands/tui/DiagramTab.java    | 25 +++++-
 8 files changed, 226 insertions(+), 7 deletions(-)

diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
index 654c166a7114..d32e850300c0 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
@@ -65,7 +65,8 @@ public class RouteDiagramAsciiRenderer {
         OK,
         FAIL,
         HIGHLIGHT_SUCCESS,
-        HIGHLIGHT_FAIL
+        HIGHLIGHT_FAIL,
+        EXTERNAL
     }
 
     public record CounterPos(int row, int col, int length, CounterType type) {
diff --git a/dsl/camel-jbang/camel-jbang-core/pom.xml 
b/dsl/camel-jbang/camel-jbang-core/pom.xml
index b0b9f0c33b89..1b0e2716e499 100644
--- a/dsl/camel-jbang/camel-jbang-core/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-core/pom.xml
@@ -308,6 +308,10 @@
                                         <sync-example name="rest-api" 
file="README.md"/>
                                         <sync-example name="rest-api" 
file="application.properties"/>
                                         <sync-example name="rest-api" 
file="rest-api.camel.yaml"/>
+                                        <!-- route-topology -->
+                                        <sync-example name="route-topology" 
file="README.md"/>
+                                        <sync-example name="route-topology" 
file="application.properties"/>
+                                        <sync-example name="route-topology" 
file="route-topology.camel.yaml"/>
                                         <!-- routes -->
                                         <sync-example name="routes" 
file="Greeter.java"/>
                                         <sync-example name="routes" 
file="README.md"/>
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
index 10c4a6e65692..1d5361fa33f6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
@@ -372,6 +372,29 @@
             "rest-api.camel.yaml"
         ]
     },
+    {
+        "name": "route-topology",
+        "title": "Route Topology",
+        "description": "Demonstrates inter-route topology with triggers, 
shared routes, and external systems",
+        "level": "intermediate",
+        "tags": [
+            "topology",
+            "direct",
+            "kafka",
+            "timer"
+        ],
+        "bundled": true,
+        "requiresDocker": false,
+        "hasCitrusTests": false,
+        "files": [
+            "README.md",
+            "application.properties",
+            "route-topology.camel.yaml"
+        ],
+        "infraServices": [
+            "kafka"
+        ]
+    },
     {
         "name": "routes",
         "title": "Routes",
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/README.md
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/README.md
new file mode 100644
index 000000000000..194f09d6ca33
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/README.md
@@ -0,0 +1,61 @@
+## Route Topology
+
+This example demonstrates inter-route topology in an order processing system.
+It showcases how multiple routes connect through shared endpoints — both 
internal (direct)
+and external (kafka) — making it a good example for the `route-topology` 
command.
+
+### How to run
+
+    camel run route-topology.camel.yaml
+
+You can use `--stub` to run without a Kafka broker installed.
+This replaces Kafka with an internal in-memory queue, so the routes still 
connect and messages flow end-to-end:
+
+    camel run route-topology.camel.yaml --stub=kafka
+
+### View the route topology
+
+    camel cmd route-topology
+
+View as a Unicode diagram with live metrics and route descriptions:
+
+    camel cmd route-topology --theme=unicode --metric --description
+
+Sample output:
+
+```
+    ┌──────────────────────┐    ┌──────────────────────┐
+    │   Generate Orders    │    │    Order REST API    │
+    │          54          │    └──────────────────────┘
+    └──────────────────────┘
+                │                           │
+                │             ┬─────────────┘
+                └─────────────│
+                              ▼
+                  ┌──────────────────────┐
+                  │    Process Order     │
+                  │          54          │
+                  └──────────────────────┘
+                              │
+                ┬─────────────┴─────────────┬
+                ▼                           ▼
+    ┌──────────────────────┐    ┌──────────────────────┐
+    │    Dispatch Order    │    │    Validate Order    │
+    │          54          │    │          54          │
+    └──────────────────────┘    └──────────────────────┘
+                │
+                └───────────────────────────┬
+                ▼                           ▼
+    ┌──────────────────────┐    ┌──────────────────────┐
+    │    Fulfill Order     │    │  Send Notification   │
+    │          54          │    │          54          │
+    └──────────────────────┘    └──────────────────────┘
+```
+
+Save as PNG image:
+
+    camel cmd route-topology --theme=dark --output=topology.png
+
+### View as JSON
+
+    camel cmd route-topology --json
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/application.properties
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/application.properties
new file mode 100644
index 000000000000..663bdcf00ac3
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/application.properties
@@ -0,0 +1,3 @@
+# Kafka defaults to localhost:9092
+# To use a different broker, uncomment and adjust:
+# camel.component.kafka.brokers=localhost:9092
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/route-topology.camel.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/route-topology.camel.yaml
new file mode 100644
index 000000000000..a89d27b967de
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/route-topology.camel.yaml
@@ -0,0 +1,97 @@
+# Order Processing System
+# This example demonstrates inter-route topology with triggers, shared routes, 
and external systems.
+# Use "camel cmd route-topology" to visualize how the routes connect to each 
other.
+
+# Trigger: generates a new order every 5 seconds
+- route:
+    id: order-generator
+    description: Generate Orders
+    from:
+      uri: timer
+      parameters:
+        timerName: orders
+        period: 5000
+      steps:
+        - setBody:
+            expression:
+              simple:
+                expression: '{"orderId": "${exchangeId}", "item": "Camel 
T-Shirt", "quantity": 1}'
+        - to:
+            uri: direct:process-order
+
+# HTTP entry point: receives orders from external clients
+- route:
+    id: order-api
+    description: Order REST API
+    from:
+      uri: platform-http
+      parameters:
+        path: /api/orders
+        httpMethodRestrict: POST
+      steps:
+        - to:
+            uri: direct:process-order
+
+# Shared route: validates and publishes orders (used by both order-generator 
and order-api)
+- route:
+    id: process-order
+    description: Process Order
+    from:
+      uri: direct:process-order
+      steps:
+        - to:
+            uri: direct:validate-order
+        - log:
+            message: "Processing order: ${body}"
+        - to:
+            uri: kafka:orders
+
+# Validation: checks order contents
+- route:
+    id: validate-order
+    description: Validate Order
+    from:
+      uri: direct:validate-order
+      steps:
+        - log:
+            message: "Validating order: ${body}"
+
+# Kafka consumer: picks up orders and fans out to fulfillment and notifications
+- route:
+    id: order-dispatcher
+    description: Dispatch Order
+    from:
+      uri: kafka:orders
+      steps:
+        - log:
+            message: "Dispatching order: ${body}"
+        - multicast:
+            steps:
+              - to:
+                  uri: kafka:fulfillment
+              - to:
+                  uri: kafka:notifications
+
+# Kafka consumer: handles fulfillment and reports to warehouse
+- route:
+    id: fulfillment
+    description: Fulfill Order
+    from:
+      uri: kafka:fulfillment
+      steps:
+        - log:
+            message: "Fulfilling order: ${body}"
+        - to:
+            uri: kafka:warehouse-shipments
+
+# Kafka consumer: sends notifications via external email service
+- route:
+    id: notification
+    description: Send Notification
+    from:
+      uri: kafka:notifications
+      steps:
+        - log:
+            message: "Sending notification for: ${body}"
+        - to:
+            uri: kafka:email-outbox
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 7d67c3475565..43985f4db3fc 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
@@ -431,8 +431,8 @@ class DiagramSupport {
     }
 
     void loadTopologyDiagramInBackground(
-            MonitorContext ctx, String pid, boolean textMode, boolean metrics) 
{
-        JsonObject jo = requestRouteTopology(ctx, pid);
+            MonitorContext ctx, String pid, boolean textMode, boolean metrics, 
boolean external) {
+        JsonObject jo = requestRouteTopology(ctx, pid, external);
         if (jo == null) {
             applyResult(ctx, List.of("(No response from integration)"), null, 
null, null);
             return;
@@ -440,6 +440,9 @@ class DiagramSupport {
 
         List<TopologyNodeInfo> nodes = TopologyHelper.parseNodes(jo);
         List<TopologyEdgeInfo> edges = TopologyHelper.parseEdges(jo);
+        if (external) {
+            TopologyHelper.addExternalEndpoints(nodes, edges, jo);
+        }
         if (nodes.isEmpty()) {
             applyResult(ctx, List.of("(No routes in response)"), null, null, 
null);
             return;
@@ -473,7 +476,7 @@ class DiagramSupport {
                         case OK -> RouteDiagramAsciiRenderer.CounterType.OK;
                         case FAIL -> 
RouteDiagramAsciiRenderer.CounterType.FAIL;
                         case TRIGGER -> 
RouteDiagramAsciiRenderer.CounterType.HIGHLIGHT_SUCCESS;
-                        case EXTERNAL -> 
RouteDiagramAsciiRenderer.CounterType.OK;
+                        case EXTERNAL -> 
RouteDiagramAsciiRenderer.CounterType.EXTERNAL;
                     };
                     positions.add(new RouteDiagramAsciiRenderer.CounterPos(
                             rowMapping[cp.row()], cp.col(), cp.length(), 
mapped));
@@ -501,13 +504,16 @@ class DiagramSupport {
         }
     }
 
-    private JsonObject requestRouteTopology(MonitorContext ctx, String pid) {
+    private JsonObject requestRouteTopology(MonitorContext ctx, String pid, 
boolean external) {
         Path outputFile = ctx.getOutputFile(pid);
         PathUtils.deleteFile(outputFile);
 
         JsonObject root = new JsonObject();
         root.put("action", "route-topology");
         root.put("metric", "true");
+        if (external) {
+            root.put("external", "true");
+        }
 
         Path actionFile = ctx.getActionFile(pid);
         PathUtils.writeTextSafely(root.toJson(), actionFile);
@@ -689,6 +695,7 @@ class DiagramSupport {
                     end = Math.min(end, text.length());
                     int colorFlag = switch (cp.type()) {
                         case OK, HIGHLIGHT_SUCCESS -> 1; // green
+                        case EXTERNAL -> 3; // cyan
                         default -> 2; // red
                     };
                     counterRanges.add(new int[] { start, end, colorFlag });
@@ -737,7 +744,7 @@ class DiagramSupport {
                     spans.add(Span.styled(text.substring(pos, cr[0]), 
Style.EMPTY.fg(defaultColor)));
                 }
                 int counterEnd = Math.min(cr[1], to);
-                Color counterColor = cr[2] == 1 ? Color.GREEN : 
Color.LIGHT_RED;
+                Color counterColor = cr[2] == 1 ? Color.GREEN : cr[2] == 3 ? 
Color.CYAN : Color.LIGHT_RED;
                 spans.add(Span.styled(text.substring(cr[0], counterEnd), 
Style.EMPTY.fg(counterColor).bold()));
                 pos = counterEnd;
             } else {
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 0311dbad42ef..3c08ef6fbbee 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
@@ -36,6 +36,7 @@ class DiagramTab implements MonitorTab {
     private final MonitorContext ctx;
     private final DiagramSupport diagram = new DiagramSupport();
     private boolean diagramMetrics = true;
+    private boolean showExternal;
     private boolean topologyMode = true;
     private String drillDownRouteId;
 
@@ -61,6 +62,14 @@ class DiagramTab implements MonitorTab {
             return true;
         }
 
+        // Toggle external systems
+        if (diagram.isShowDiagram() && topologyMode && 
ke.isCharIgnoreCase('e')) {
+            showExternal = !showExternal;
+            diagram.endLoad();
+            reloadDiagram();
+            return true;
+        }
+
         // Toggle description
         if (diagram.isShowDiagram() && ke.isCharIgnoreCase('n')) {
             diagram.setShowDescription(!diagram.isShowDescription());
@@ -151,6 +160,7 @@ class DiagramTab implements MonitorTab {
         if (diagram.isShowDiagram()) {
             diagram.renderFooterHints(spans);
             hint(spans, "m", "metrics" + (diagramMetrics ? " [on]" : " 
[off]"));
+            hint(spans, "e", "external" + (showExternal ? " [on]" : " [off]"));
             hint(spans, "n", "description" + (diagram.isShowDescription() ? " 
[on]" : " [off]"));
         }
     }
@@ -179,6 +189,7 @@ class DiagramTab implements MonitorTab {
 
         String pid = ctx.selectedPid;
         boolean showMetrics = diagramMetrics;
+        boolean external = showExternal;
 
         if (showPlaceholder) {
             diagram.setLoadingPlaceholder();
@@ -188,7 +199,7 @@ class DiagramTab implements MonitorTab {
             try {
                 if (topologyMode) {
                     diagram.setTopologyMode(true);
-                    diagram.loadTopologyDiagramInBackground(ctx, pid, true, 
showMetrics);
+                    diagram.loadTopologyDiagramInBackground(ctx, pid, true, 
showMetrics, external);
                 } else {
                     diagram.setTopologyMode(false);
                     diagram.loadRouteDiagramInBackground(ctx, pid, true, 
drillDownRouteId, showMetrics);
@@ -244,9 +255,21 @@ class DiagramTab implements MonitorTab {
                                 - **Red** number with `!` — failed exchanges
                                 - Combined as `3748/12!` means 3748 ok and 12 
failed
 
+                                ## 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
+
+                                External system boxes are drawn with dashed 
borders to distinguish
+                                them from route boxes. Dashed edges connect 
routes to external systems.
+
                                 ## Keys
 
                                 - `m` — toggle metrics on/off (default: on)
+                                - `e` — toggle external systems on/off 
(default: off)
+                                - `n` — toggle description labels on/off 
(default: off)
                                 - `↑↓←→` — scroll diagram
                                 - `PgUp/PgDn` — page scroll
                                 - `Home/End` — top/end

Reply via email to