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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 1de5ec5135f8 CAMEL-25630: camel-jbang - the route diagram takes its 
numbers from the status file
1de5ec5135f8 is described below

commit 1de5ec5135f865070739c872eeb672093e7d5879
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 13 22:03:58 2026 +0200

    CAMEL-25630: camel-jbang - the route diagram takes its numbers from the 
status file
    
    While a diagram was on screen, the Diagram and Routes tabs asked the 
integration
    for its route topology with the numbers every second: a request the 
connector
    answers at its next poll, and an answer of hundreds of KB for a big 
integration.
    
    The structure of the routes changes only when routes are added, removed or
    reloaded, so it is now asked for without numbers, and again only when the 
status
    file shows another reload count or other route ids. The numbers come from 
the
    status file, which the TUI reads every second anyway, and go where the
    route-topology console puts them: the route totals on the routes, the 
route's
    statistics on its route and from lines, each step's by its id, the route's
    messages on a system it reads from, and on a system it sends to the sum of 
the
    steps that send to it (the steps with a destination, which the console 
counts).
    
    DiagramTopology keeps the structure and is shared through MonitorContext, 
so the
    Routes and Diagram tabs and the message diagrams of the Inspect and Errors 
tabs
    ask once between them, and toggling a diagram option no longer waits for an
    answer. Without a status for the integration the numbers are asked for with 
the
    structure, as before. StatusParser keeps the statistics of each route and 
step,
    and the destination of a step.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 dsl/camel-jbang/camel-jbang-plugin-tui/AGENTS.md   |   2 +-
 .../jbang/core/commands/tui/DiagramSupport.java    |  28 +--
 .../jbang/core/commands/tui/DiagramTopology.java   | 251 +++++++++++++++++++
 .../jbang/core/commands/tui/MonitorContext.java    |   2 +
 .../dsl/jbang/core/commands/tui/ProcessorInfo.java |   6 +
 .../dsl/jbang/core/commands/tui/RouteInfo.java     |   3 +
 .../dsl/jbang/core/commands/tui/StatusParser.java  |   3 +
 .../core/commands/tui/DiagramTopologyTest.java     | 268 +++++++++++++++++++++
 .../jbang/core/commands/tui/SourceSizeTest.java    |   2 +-
 9 files changed, 539 insertions(+), 26 deletions(-)

diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/AGENTS.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/AGENTS.md
index 15f750ec1c97..6fc2b821ec3d 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/AGENTS.md
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/AGENTS.md
@@ -33,7 +33,7 @@ and use each other's package-private members, so there are no 
sub-packages. Clas
 | `*Popup`, `PopupManager` | popups; `PopupManager` has the switch integration 
(F3), More and confirm popups |
 | `FilterListPopup`, `ChoicePopup`, `ActivityLogPopup`, `DialogHelper` | what 
popups are built on: a list narrowed down by typing, a short list of choices, a 
log with a detail panel, and centered confirm and input dialogs |
 | `SourceTab`, `SourceViewer`, `SourceEditAssist`, `*Completions`, `*QuickDoc` 
| the source editor; its Tab completion is `SourceCompletion`, its Ctrl+R 
refactorings `SourceRefactorActions`, the live run data at the end of the lines 
`SourceLiveColumn` |
-| `DiagramSupport`, `DiagramDetailSupport`, `ArchitectureView`, `diagram.*` | 
route and architecture diagrams |
+| `DiagramSupport`, `DiagramDetailSupport`, `ArchitectureView`, `diagram.*` | 
route and architecture diagrams; what they draw comes from `DiagramTopology` 
(`ctx.diagramTopology`), which asks an integration for the structure of its 
routes only when they change and takes the numbers from the status file |
 | `AbstractDiagramTab` | what the Diagram and Routes tabs share of the route 
diagram: its state, keys, drilling into a route and back |
 | `AiPanel`, `AiSlashCommandRegistry` | the F8 AI panel; its input line is 
`AiInputLine`, its tools and system prompt `AiToolSet`, its usage statistics 
`AiUsageStats`, and an external coding agent (ACP) `AcpSession` over 
`AcpAgentClient` |
 | `LiveEdit`, `EditReplay` | `/write live`: a file an AI tool writes is typed 
into the source editor, to watch and then save or discard |
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 c9aed9871a3d..c4b5c1ca97b7 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
@@ -1850,8 +1850,8 @@ class DiagramSupport {
     void loadHighlightedNativeDiagramInBackground(
             MonitorContext ctx, String pid,
             String[] messageHistory, boolean failed, int initialStepIndex) {
-        // Single IPC call: topology + route structures
-        JsonObject jo = requestRouteTopology(ctx, pid, false, true);
+        // the topology and the structure of the routes, with the numbers of 
the status file
+        JsonObject jo = ctx.diagramTopology.load(ctx, pid);
         if (jo == null) {
             close();
             return;
@@ -2389,11 +2389,11 @@ class DiagramSupport {
 
     void loadAllDiagramsInBackground(
             MonitorContext ctx, String pid, boolean metrics, int externalMode) 
{
-        // Single IPC call: topology + route structures
         boolean external = externalMode > 0;
         Set<String> hidden = hiddenRouteIds;
         int boxColumns = wantedBoxColumns();
-        JsonObject topoJson = requestRouteTopology(ctx, pid, external, true);
+        // the topology and the structure of the routes, with the numbers of 
the status file
+        JsonObject topoJson = ctx.diagramTopology.load(ctx, pid);
 
         TopologyLayoutResult topoResult = null;
         int nodeW = 0;
@@ -2570,26 +2570,6 @@ class DiagramSupport {
         });
     }
 
-    private JsonObject requestRouteTopology(MonitorContext ctx, String pid, 
boolean external, boolean routes) {
-        JsonObject root = new JsonObject();
-        root.put("action", "route-topology");
-        root.put("metric", "true");
-        root.put("external", "true");
-        if (routes) {
-            root.put("routes", "true");
-        }
-        return ctx.executeAction(pid, root, 5000);
-    }
-
-    private JsonObject requestRouteStructure(MonitorContext ctx, String pid) {
-        JsonObject root = new JsonObject();
-        root.put("action", "route-structure");
-        root.put("filter", "*");
-        root.put("brief", false);
-        root.put("metric", true);
-        return ctx.executeAction(pid, root, 5000);
-    }
-
     static void addParentNodes(List<RouteDiagramLayoutEngine.NodeInfo> nodes, 
Set<String> nodeIds) {
         for (int i = 0; i < nodes.size(); i++) {
             RouteDiagramLayoutEngine.NodeInfo node = nodes.get(i);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopology.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopology.java
new file mode 100644
index 000000000000..e556d6b4bb09
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopology.java
@@ -0,0 +1,251 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * The route topology the diagrams draw: the routes, their steps, the routes 
they call and the systems they reach, with
+ * the messages of each.
+ * <p>
+ * Its structure changes only when routes are added, removed or reloaded, so 
it is asked of the integration without
+ * numbers, and asked again only then. The numbers come from the status file 
the TUI reads every second anyway: the
+ * totals of each route, and the statistics of each step by its id. Asking for 
the topology with its numbers every
+ * second took a request the connector answers at its next poll, and an answer 
of hundreds of KB from a big integration.
+ */
+final class DiagramTopology {
+
+    /** What the route-structure console leaves out of the statistics of the 
from line, which are the route's. */
+    private static final List<String> ROUTE_ONLY_STATISTICS = 
List.of("coverage", "load01", "load05", "load15");
+
+    /** The structure of the routes of an integration, and which routes they 
were. */
+    private record Structure(String pid, int reloaded, List<String> routeIds, 
JsonObject topology) {
+
+        boolean isOf(String pid, int reloaded, List<String> routeIds) {
+            return this.pid.equals(pid) && this.reloaded == reloaded && 
this.routeIds.equals(routeIds);
+        }
+    }
+
+    private volatile Structure structure;
+
+    /**
+     * The topology of the integration, with its numbers as of its status 
file; null when the integration does not
+     * answer.
+     */
+    JsonObject load(MonitorContext ctx, String pid) {
+        IntegrationInfo info = integration(ctx, pid);
+        if (info == null) {
+            // no status to take the numbers from: they come with the structure
+            return request(ctx, pid, true);
+        }
+        List<String> routeIds = info.routes.stream().map(r -> 
r.routeId).filter(Objects::nonNull).sorted().toList();
+        Structure s = structure;
+        if (s == null || !s.isOf(pid, info.reloaded, routeIds)) {
+            s = fetch(ctx, pid, info.reloaded, routeIds);
+            if (s == null) {
+                return null;
+            }
+        }
+        return withNumbers(s.topology(), info);
+    }
+
+    /**
+     * Asks for the structure, one tab at a time: the Routes and Diagram tabs 
load together, and the second takes what
+     * the first was answered.
+     */
+    private synchronized Structure fetch(MonitorContext ctx, String pid, int 
reloaded, List<String> routeIds) {
+        Structure s = structure;
+        if (s != null && s.isOf(pid, reloaded, routeIds)) {
+            return s;
+        }
+        JsonObject topology = request(ctx, pid, false);
+        if (topology == null) {
+            return null;
+        }
+        s = new Structure(pid, reloaded, routeIds, topology);
+        structure = s;
+        return s;
+    }
+
+    /** Asks the integration for its topology, with the structure of its 
routes and the systems they reach. */
+    private static JsonObject request(MonitorContext ctx, String pid, boolean 
metric) {
+        JsonObject root = new JsonObject();
+        root.put("action", "route-topology");
+        root.put("metric", Boolean.toString(metric));
+        root.put("external", "true");
+        root.put("routes", "true");
+        return ctx.executeAction(pid, root, 5000);
+    }
+
+    /**
+     * A copy of the topology with the numbers of the status of the 
integration where the route-topology console puts
+     * them; the topology is left as it is, for the next time.
+     */
+    static JsonObject withNumbers(JsonObject topology, IntegrationInfo info) {
+        Map<String, RouteInfo> routes = new HashMap<>();
+        for (RouteInfo r : info.routes) {
+            routes.put(r.routeId, r);
+        }
+        JsonObject answer = new JsonObject(topology);
+        copyEach(answer, "nodes", node -> {
+            RouteInfo r = routes.get(node.getString("routeId"));
+            if (r != null) {
+                node.put("exchangesTotal", r.total);
+                node.put("exchangesFailed", r.failed);
+            }
+        });
+        copyEach(answer, "externalEndpoints", endpoint -> {
+            RouteInfo r = routes.get(endpoint.getString("routeId"));
+            if (r != null) {
+                addEndpointNumbers(endpoint, r);
+            }
+        });
+        copyEach(answer, "routes", route -> {
+            RouteInfo r = routes.get(route.getString("routeId"));
+            if (r != null) {
+                addStepStatistics(route, r);
+            }
+        });
+        return answer;
+    }
+
+    /**
+     * A system the route reads from has the messages of the route; one it 
sends to has those of the steps of the route
+     * that send to it.
+     */
+    private static void addEndpointNumbers(JsonObject endpoint, RouteInfo 
route) {
+        if ("in".equals(endpoint.getString("direction"))) {
+            endpoint.put("exchangesTotal", route.total);
+            endpoint.put("exchangesFailed", route.failed);
+            return;
+        }
+        String uri = endpoint.getString("uri");
+        if (uri == null) {
+            return;
+        }
+        String key = endpointKey(uri);
+        long total = 0;
+        long failed = 0;
+        boolean sends = false;
+        for (ProcessorInfo p : route.processors) {
+            if (p.uri != null && key.equals(endpointKey(p.uri))) {
+                total += p.total;
+                failed += p.failed;
+                sends = true;
+            }
+        }
+        if (sends) {
+            endpoint.put("exchangesTotal", total);
+            endpoint.put("exchangesFailed", failed);
+        }
+    }
+
+    /**
+     * The statistics of each line of the route's code: the first two lines 
(the route and its from) have the route's,
+     * the others those of their step.
+     */
+    private static void addStepStatistics(JsonObject route, RouteInfo info) {
+        Collection<?> code = route.getCollection("code");
+        if (code == null) {
+            return;
+        }
+        Map<String, Map<String, ?>> steps = new HashMap<>();
+        for (ProcessorInfo p : info.processors) {
+            if (p.id != null && p.statistics != null) {
+                steps.put(p.id, p.statistics);
+            }
+        }
+        JsonArray lines = new JsonArray();
+        int index = 0;
+        for (Object o : code) {
+            if (o instanceof Map<?, ?> m) {
+                JsonObject line = copy(m);
+                Map<String, ?> statistics = switch (index) {
+                    case 0 -> info.statistics;
+                    case 1 -> fromStatistics(info.statistics);
+                    default -> steps.get(line.getString("id"));
+                };
+                if (statistics != null) {
+                    line.put("statistics", statistics instanceof JsonObject jo 
? jo : new JsonObject(statistics));
+                }
+                lines.add(line);
+            } else {
+                lines.add(o);
+            }
+            index++;
+        }
+        route.put("code", lines);
+    }
+
+    private static Map<String, ?> fromStatistics(Map<String, ?> route) {
+        if (route == null) {
+            return null;
+        }
+        JsonObject answer = new JsonObject(route);
+        ROUTE_ONLY_STATISTICS.forEach(answer::remove);
+        return answer;
+    }
+
+    /** The endpoint without its options, and without the // after the scheme, 
as the console matches them. */
+    static String endpointKey(String uri) {
+        String answer = DiagramSupport.stripQueryParams(uri);
+        int idx = answer.indexOf("://");
+        return idx > 0 ? answer.substring(0, idx + 1) + answer.substring(idx + 
3) : answer;
+    }
+
+    /** Puts copies of the objects of the array under the key in their place, 
changed. */
+    private static void copyEach(JsonObject topology, String key, 
Consumer<JsonObject> change) {
+        Collection<?> array = topology.getCollection(key);
+        if (array == null) {
+            return;
+        }
+        JsonArray copies = new JsonArray();
+        for (Object o : array) {
+            if (o instanceof Map<?, ?> m) {
+                JsonObject copy = copy(m);
+                change.accept(copy);
+                copies.add(copy);
+            } else {
+                copies.add(o);
+            }
+        }
+        topology.put(key, copies);
+    }
+
+    private static JsonObject copy(Map<?, ?> map) {
+        JsonObject answer = new JsonObject();
+        map.forEach((k, v) -> answer.put(String.valueOf(k), v));
+        return answer;
+    }
+
+    private static IntegrationInfo integration(MonitorContext ctx, String pid) 
{
+        for (IntegrationInfo info : ctx.data.get()) {
+            if (pid.equals(info.pid) && !info.vanishing) {
+                return info;
+            }
+        }
+        return null;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index b88ccc8cbd83..311d4c85eacb 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -58,6 +58,8 @@ class MonitorContext {
         t.setDaemon(true);
         return t;
     });
+    /** The structure of the routes the diagrams draw, asked of an integration 
again only when its routes change. */
+    final DiagramTopology diagramTopology = new DiagramTopology();
 
     volatile String selectedPid;
     volatile String lastSelectedName;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
index d1a6226fd6ad..38763a71ae0e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
@@ -16,6 +16,8 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.util.Map;
+
 class ProcessorInfo {
     String id;
     String processor;
@@ -37,4 +39,8 @@ class ProcessorInfo {
     String sinceLastStarted;
     String sinceLastCompleted;
     String sinceLastFailed;
+    /** The endpoint the step sends to (to, toD, wireTap, enrich, poll), as 
the status file has it. */
+    String uri;
+    /** The statistics as the status file has them, for the route diagram 
(DiagramTopology). */
+    Map<String, ?> statistics;
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
index 20b0490827aa..0cea43e82b58 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 class RouteInfo {
     String routeId;
@@ -48,5 +49,7 @@ class RouteInfo {
     String sinceLastStarted;
     String sinceLastCompleted;
     String sinceLastFailed;
+    /** The statistics as the status file has them, for the route diagram 
(DiagramTopology). */
+    Map<String, ?> statistics;
     final List<ProcessorInfo> processors = new ArrayList<>();
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
index eb467ec3eed8..777a76008d72 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
@@ -259,6 +259,7 @@ final class StatusParser {
                 ri.uptime = rj.getString("uptime");
 
                 Map<String, ?> rs = rj.getMap("statistics");
+                ri.statistics = rs;
                 if (rs != null) {
                     ri.coverage = objToString(rs.get("coverage"));
                     ri.throughput = objToString(rs.get("exchangesThroughput"));
@@ -300,8 +301,10 @@ final class StatusParser {
                         pi.processor = pj.getString("processor");
                         pi.source = pj.getString("source");
                         pi.level = pj.getIntegerOrDefault("level", 0);
+                        pi.uri = pj.getString("uri");
 
                         Map<String, ?> ps = pj.getMap("statistics");
+                        pi.statistics = ps;
                         if (ps != null) {
                             pi.total = objToLong(ps.get("exchangesTotal"));
                             pi.failed = objToLong(ps.get("exchangesFailed"));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopologyTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopologyTest.java
new file mode 100644
index 000000000000..75b0c1d114c1
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopologyTest.java
@@ -0,0 +1,268 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.diagram.RouteDiagramHelper;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine;
+import org.apache.camel.diagram.TopologyHelper;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyEdgeInfo;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyNodeInfo;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * The diagrams ask for the structure of the routes only when the routes 
change, and take the numbers from the status
+ * file (CAMEL-25630).
+ */
+class DiagramTopologyTest {
+
+    /** As the route-topology console answers without numbers: route a reads 
kafka:in, sends to kafka:out and b. */
+    private static final String TOPOLOGY
+            = """
+                    {
+                      "nodes": [
+                        { "routeId": "a", "from": "kafka:in", "fromScheme": 
"kafka", "nodeType": "route" },
+                        { "routeId": "b", "from": "direct:b", "fromScheme": 
"direct", "nodeType": "route" }
+                      ],
+                      "edges": [
+                        { "fromRouteId": "a", "toRouteId": "b", "endpoint": 
"direct:b", "connectionType": "direct" }
+                      ],
+                      "externalEndpoints": [
+                        { "id": "ext-in", "uri": "kafka:in", "scheme": 
"kafka", "direction": "in", "routeId": "a" },
+                        { "id": "ext-out", "uri": "kafka:out", "scheme": 
"kafka", "direction": "out", "routeId": "a" }
+                      ],
+                      "routes": [
+                        { "routeId": "a", "from": "kafka:in", "code": [
+                          { "line": 1, "type": "route", "id": "a", "level": 0, 
"code": "route" },
+                          { "line": 2, "type": "from", "id": "a", "level": 0, 
"code": "from(\\"kafka:in\\")", "uri": "kafka:in" },
+                          { "line": 3, "type": "to", "id": "to1", "level": 1, 
"code": "to(\\"kafka:out\\")", "uri": "kafka:out" },
+                          { "line": 4, "type": "wireTap", "id": "tap1", 
"level": 1, "code": "wireTap(\\"kafka:out\\")", "uri": "kafka:out" },
+                          { "line": 5, "type": "to", "id": "to2", "level": 1, 
"code": "to(\\"direct:b\\")", "uri": "direct:b" }
+                        ] }
+                      ]
+                    }""";
+
+    /** The status file of the integration, as the route console writes it 
with the processors. */
+    private static final String STATUS = """
+            {
+              "context": { "name": "orders", "statistics": { "reload": { 
"reloaded": 0 } } },
+              "routes": [
+                { "routeId": "a", "state": "Started",
+                  "statistics": { "exchangesTotal": 10, "exchangesFailed": 1, 
"coverage": "4/4",
+                                  "load01": "0.10", "load05": "0.05", 
"load15": "0.01", "meanProcessingTime": 4 },
+                  "processors": [
+                    { "id": "to1", "processor": "to", "uri": 
"kafka://out?brokers=localhost:9092",
+                      "statistics": { "exchangesTotal": 9, "exchangesFailed": 
1, "meanProcessingTime": 3 } },
+                    { "id": "tap1", "processor": "wireTap", "uri": "kafka:out",
+                      "statistics": { "exchangesTotal": 2, "exchangesFailed": 
0 } },
+                    { "id": "to2", "processor": "to", "uri": "direct://b",
+                      "statistics": { "exchangesTotal": 8, "exchangesFailed": 
0 } }
+                  ] },
+                { "routeId": "b", "state": "Started", "statistics": { 
"exchangesTotal": 8, "exchangesFailed": 0 } }
+              ]
+            }""";
+
+    private static JsonObject json(String text) {
+        JsonObject answer = Jsoner.deserialize(text, new JsonObject());
+        assertThat(answer).as("the fixture is JSON").isNotEmpty();
+        return answer;
+    }
+
+    private static IntegrationInfo status() {
+        return StatusParser.parseIntegration(ProcessHandle.current(), 
json(STATUS));
+    }
+
+    @Test
+    void theRoutesHaveTheirTotalsFromTheStatusFile() {
+        JsonObject topology = DiagramTopology.withNumbers(json(TOPOLOGY), 
status());
+
+        assertThat(TopologyHelper.parseNodes(topology))
+                .extracting(n -> n.routeId, n -> n.exchangesTotal, n -> 
n.exchangesFailed)
+                .containsExactly(tuple("a", 10L, 1L), tuple("b", 8L, 0L));
+    }
+
+    @Test
+    void 
aSystemReadFromHasTheRoutesMessagesAndOneSentToThoseOfTheStepsSendingToIt() {
+        JsonObject topology = DiagramTopology.withNumbers(json(TOPOLOGY), 
status());
+
+        List<TopologyNodeInfo> nodes = new ArrayList<>();
+        List<TopologyEdgeInfo> edges = new ArrayList<>();
+        TopologyHelper.addExternalEndpoints(nodes, edges, topology);
+
+        // the to and the wireTap both send to kafka:out, one with options and 
// in its uri
+        assertThat(nodes)
+                .extracting(n -> n.routeId, n -> n.exchangesTotal, n -> 
n.exchangesFailed)
+                .containsExactly(tuple("ext-in", 10L, 1L), tuple("ext-out", 
11L, 1L));
+    }
+
+    @Test
+    void 
theStepsHaveTheirStatisticsByIdAndTheFromLineTheRoutesWithoutItsLoad() {
+        JsonObject topology = DiagramTopology.withNumbers(json(TOPOLOGY), 
status());
+
+        List<RouteDiagramLayoutEngine.NodeInfo> lines = 
RouteDiagramHelper.parseRoutes(topology).get(0).nodes;
+
+        assertThat(lines).extracting(n -> n.id, n -> n.stat.exchangesTotal, n 
-> n.stat.exchangesFailed)
+                .containsExactly(tuple("a", 10L, 1L), tuple("a", 10L, 1L), 
tuple("to1", 9L, 1L), tuple("tap1", 2L, 0L),
+                        tuple("to2", 8L, 0L));
+        
assertThat(lines.get(0).stat).isInstanceOf(RouteDiagramLayoutEngine.RouteStatInfo.class);
+        assertThat(((RouteDiagramLayoutEngine.RouteStatInfo) 
lines.get(0).stat).coverage).isEqualTo("4/4");
+        assertThat(lines.get(2).stat.meanProcessingTime).isEqualTo(3);
+        JsonObject from = 
topology.getJsonArray("routes").getJsonObject(0).getJsonArray("code").getJsonObject(1);
+        
assertThat(from.getJsonObject("statistics")).containsKey("meanProcessingTime")
+                .doesNotContainKeys("coverage", "load01", "load05", "load15");
+    }
+
+    @Test
+    void theStructureKeptForTheNextTimeIsLeftWithoutNumbers() {
+        JsonObject structure = json(TOPOLOGY);
+
+        DiagramTopology.withNumbers(structure, status());
+
+        assertThat(structure).isEqualTo(json(TOPOLOGY));
+    }
+
+    @Test
+    void theStructureIsAskedForOnceAndAgainWhenTheRoutesChange() {
+        IntegrationInfo info = status();
+        AnsweringContext ctx = new AnsweringContext(info);
+        DiagramTopology topology = new DiagramTopology();
+
+        assertThat(topology.load(ctx, info.pid)).isNotNull();
+        assertThat(topology.load(ctx, info.pid)).isNotNull();
+        // the numbers are not asked for: they are in the status file
+        assertThat(ctx.metrics).containsExactly("false");
+
+        // the same routes, listed in another order
+        Collections.reverse(info.routes);
+        topology.load(ctx, info.pid);
+        assertThat(ctx.metrics).hasSize(1);
+
+        info.reloaded = 1;
+        topology.load(ctx, info.pid);
+        assertThat(ctx.metrics).hasSize(2);
+
+        RouteInfo added = new RouteInfo();
+        added.routeId = "c";
+        info.routes.add(added);
+        topology.load(ctx, info.pid);
+        assertThat(ctx.metrics).hasSize(3);
+
+        topology.load(ctx, info.pid);
+        assertThat(ctx.metrics).hasSize(3);
+    }
+
+    @Test
+    void withoutAStatusTheNumbersComeWithTheStructure() {
+        AnsweringContext ctx = new AnsweringContext(status());
+        ctx.data.set(List.of());
+        DiagramTopology topology = new DiagramTopology();
+
+        topology.load(ctx, "123");
+        topology.load(ctx, "123");
+
+        assertThat(ctx.metrics).containsExactly("true", "true");
+    }
+
+    @Test
+    void aStructureNotAnsweredIsAskedForAgain() {
+        IntegrationInfo info = status();
+        AnsweringContext ctx = new AnsweringContext(info);
+        ctx.unanswered = 1;
+        DiagramTopology topology = new DiagramTopology();
+
+        assertThat(topology.load(ctx, info.pid)).isNull();
+        assertThat(topology.load(ctx, info.pid)).isNotNull();
+
+        assertThat(ctx.metrics).containsExactly("false", "false");
+    }
+
+    @Test
+    void tabsLoadingTogetherAskOnce() throws InterruptedException {
+        IntegrationInfo info = status();
+        AnsweringContext ctx = new AnsweringContext(info);
+        ctx.answer = new CountDownLatch(1);
+        DiagramTopology topology = new DiagramTopology();
+        List<JsonObject> loaded = new CopyOnWriteArrayList<>();
+
+        Thread routesTab = new Thread(() -> loaded.add(topology.load(ctx, 
info.pid)));
+        routesTab.start();
+        await().atMost(5, TimeUnit.SECONDS).until(() -> ctx.metrics.size() == 
1);
+        Thread diagramTab = new Thread(() -> loaded.add(topology.load(ctx, 
info.pid)));
+        diagramTab.start();
+        // the second waits for the answer to the first, or asks as well
+        await().atMost(5, TimeUnit.SECONDS)
+                .until(() -> diagramTab.getState() == Thread.State.BLOCKED || 
ctx.metrics.size() > 1);
+        ctx.answer.countDown();
+        routesTab.join(5000);
+        diagramTab.join(5000);
+
+        assertThat(ctx.metrics).containsExactly("false");
+        assertThat(loaded).hasSize(2).doesNotContainNull();
+    }
+
+    @Test
+    void endpointsMatchWithoutTheirOptionsAndTheSlashesAfterTheScheme() {
+        
assertThat(DiagramTopology.endpointKey("kafka://out?brokers=localhost:9092")).isEqualTo("kafka:out");
+        
assertThat(DiagramTopology.endpointKey("kafka:out")).isEqualTo("kafka:out");
+        
assertThat(DiagramTopology.endpointKey("http://host:8080/api?q=1";)).isEqualTo("http:host:8080/api");
+    }
+
+    /** An integration whose status is the one given, and that answers the 
topology request. */
+    private static final class AnsweringContext extends MonitorContext {
+
+        final List<String> metrics = new CopyOnWriteArrayList<>();
+        int unanswered;
+        /** When set, the answer waits for it. */
+        CountDownLatch answer;
+
+        AnsweringContext(IntegrationInfo info) {
+            super(new AtomicReference<>(List.of(info)), new 
AtomicReference<>(List.of()));
+        }
+
+        @Override
+        JsonObject executeAction(String pid, JsonObject request, long 
timeoutMs) {
+            
assertThat(request.getString("action")).isEqualTo("route-topology");
+            assertThat(request.getString("routes")).isEqualTo("true");
+            metrics.add(request.getString("metric"));
+            if (answer != null) {
+                try {
+                    answer.await(5, TimeUnit.SECONDS);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            }
+            if (unanswered > 0) {
+                unanswered--;
+                return null;
+            }
+            return json(TOPOLOGY);
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
index 70619005c4c9..2f73df7c7763 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
@@ -62,7 +62,7 @@ class SourceSizeTest {
             CamelMonitor.java 2789
             DiagramDetailSupport.renderEipInfoPanel 159
             DiagramDetailSupport.renderRouteInfoPanel 154
-            DiagramSupport.java 2697
+            DiagramSupport.java 2677
             DiagramSupport.loadAllDiagramsInBackground 182
             DiagramSupport.loadHighlightedNativeDiagramInBackground 212
             EndpointsTab.renderContent 170

Reply via email to