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 74ee085f7d64 chore: camel-jbang - the processors of a route show their 
line and what they do, not the ids Camel generates (#28193)
74ee085f7d64 is described below

commit 74ee085f7d646ea0bb5d278508b12350beed7cee
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 20 22:13:04 2026 +0200

    chore: camel-jbang - the processors of a route show their line and what 
they do, not the ids Camel generates (#28193)
    
    The table of the processors in the Routes view showed the id of each step. 
An id
    Camel generates (to1, setBody2) only repeats the type in the column before 
it.
    The table now shows:
    
    - TYPE indented by level, so the tree of the steps shows there;
    - LINE, the line of the route source the step is on, when the runtime 
records
      source locations;
    - PROCESSOR, what the step does as the route structure has it: log:outbox 
for
      to[log:outbox], jackson for unmarshal[jackson]. It comes from the 
structure
      the diagrams already ask for. An id the route sets is shown first.
    
    Top mode shows the line and what the steps do as well. PROCESSOR gets its 
room
    before the body types, which the Info panel of the diagram has in full.
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../images/jbang/camel-tui-routes-body-types.png   | Bin 350752 -> 344730 bytes
 .../jbang/core/commands/tui/DiagramTopology.java   |  74 ++++++-
 .../dsl/jbang/core/commands/tui/ProcessorInfo.java |   2 +
 .../jbang/core/commands/tui/ProcessorLabels.java   | 115 ++++++++++
 .../dsl/jbang/core/commands/tui/RoutesTab.java     | 236 ++++++++++++++-------
 .../dsl/jbang/core/commands/tui/StatusParser.java  |   1 +
 .../src/main/resources/tui/help/routes.md          |   9 +
 .../commands/tui/RoutesProcessorLabelsTest.java    | 188 ++++++++++++++++
 8 files changed, 544 insertions(+), 81 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-routes-body-types.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-routes-body-types.png
index d4f882ffd2b5..a71da341b68a 100644
Binary files 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-routes-body-types.png 
and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-routes-body-types.png 
differ
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
index c564f37a957d..b56c095b5bdf 100644
--- 
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
@@ -25,6 +25,7 @@ import java.util.function.Consumer;
 
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 
 /**
  * The route topology the diagrams draw: the routes, their steps, the routes 
they call and the systems they reach, with
@@ -40,16 +41,51 @@ 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) {
+    /**
+     * The structure of the routes of an integration, and which routes they 
were, with its steps by route and step id.
+     */
+    private record Structure(
+            String pid, int reloaded, List<String> routeIds, JsonObject 
topology, Map<String, Step> steps) {
 
         boolean isOf(String pid, int reloaded, List<String> routeIds) {
             return this.pid.equals(pid) && this.reloaded == reloaded && 
this.routeIds.equals(routeIds);
         }
     }
 
+    /** A step as the route structure has it: its code (to[log:outbox]) and 
the line of the source it is on. */
+    record Step(String code, int line) {
+    }
+
     private volatile Structure structure;
 
+    /**
+     * The step of a route by its id, from the structure asked for last (the 
Routes tab shows what each step does); null
+     * until the structure of the integration has been asked for, or when it 
has no such step.
+     */
+    Step step(String pid, String routeId, String id) {
+        Structure s = structure;
+        if (s == null || !s.pid().equals(pid) || routeId == null || id == 
null) {
+            return null;
+        }
+        return s.steps().get(stepKey(routeId, id));
+    }
+
+    /**
+     * Asks for the structure when the routes of the integration have changed 
since it was asked for, as {@link #load}
+     * does without making the copy with the numbers. On the background thread 
only.
+     */
+    void refreshStructure(MonitorContext ctx, String pid) {
+        IntegrationInfo info = integration(ctx, pid);
+        if (info == null) {
+            return;
+        }
+        List<String> routeIds = routeIds(info);
+        Structure s = structure;
+        if (s == null || !s.isOf(pid, info.reloaded, routeIds)) {
+            fetch(ctx, pid, info.reloaded, routeIds);
+        }
+    }
+
     /**
      * The topology of the integration, with its numbers as of its status 
file; null when the integration does not
      * answer.
@@ -60,7 +96,7 @@ final class DiagramTopology {
             // 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();
+        List<String> routeIds = routeIds(info);
         Structure s = structure;
         if (s == null || !s.isOf(pid, info.reloaded, routeIds)) {
             s = fetch(ctx, pid, info.reloaded, routeIds);
@@ -71,6 +107,10 @@ final class DiagramTopology {
         return withNumbers(s.topology(), info);
     }
 
+    private static List<String> routeIds(IntegrationInfo info) {
+        return info.routes.stream().map(r -> 
r.routeId).filter(Objects::nonNull).sorted().toList();
+    }
+
     /**
      * 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.
@@ -84,11 +124,37 @@ final class DiagramTopology {
         if (topology == null) {
             return null;
         }
-        s = new Structure(pid, reloaded, routeIds, topology);
+        s = new Structure(pid, reloaded, routeIds, topology, steps(topology));
         structure = s;
         return s;
     }
 
+    /** The steps of the routes of the topology by route and step id, with 
their code unescaped as the diagrams do. */
+    private static Map<String, Step> steps(JsonObject topology) {
+        Map<String, Step> answer = new HashMap<>();
+        Collection<?> routes = topology.getCollection("routes");
+        if (routes == null) {
+            return answer;
+        }
+        for (Object r : routes) {
+            if (r instanceof JsonObject route && route.getCollection("code") 
!= null) {
+                String routeId = route.getString("routeId");
+                for (Object o : route.getCollection("code")) {
+                    if (o instanceof JsonObject line && routeId != null && 
line.getString("id") != null) {
+                        String code = line.getString("code");
+                        answer.put(stepKey(routeId, line.getString("id")),
+                                new Step(code != null ? Jsoner.unescape(code) 
: null, line.getIntegerOrDefault("line", 0)));
+                    }
+                }
+            }
+        }
+        return answer;
+    }
+
+    private static String stepKey(String routeId, String id) {
+        return routeId + '/' + id;
+    }
+
     /** 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();
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 d193eb03c797..926ac8c30f91 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
@@ -21,6 +21,8 @@ import java.util.Map;
 
 class ProcessorInfo {
     String id;
+    /** The prefix of the ids of the steps of a route template or kamelet 
(only when it has one). */
+    String nodePrefixId;
     String processor;
     /** Where the processor is in the source, file:line (only when the runtime 
records source locations). */
     String source;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorLabels.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorLabels.java
new file mode 100644
index 000000000000..387398994ba9
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorLabels.java
@@ -0,0 +1,115 @@
+/*
+ * 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.List;
+
+import dev.tamboui.style.Style;
+import dev.tamboui.text.CharWidth;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+
+/**
+ * What the processors table of the Routes tab shows of a step: the line of 
the route source it is on, and what it does
+ * as the route structure has it (log:outbox for to[log:outbox]), with its id 
only when the route sets one. An id Camel
+ * generates (to1, setBody2) repeats the type the table already shows.
+ */
+final class ProcessorLabels {
+
+    private ProcessorLabels() {
+    }
+
+    /** The line of the source, from a source location such as 
orders.camel.yaml:21; null when it has none. */
+    static Integer line(String source) {
+        if (source == null) {
+            return null;
+        }
+        int colon = source.lastIndexOf(':');
+        if (colon < 0 || colon == source.length() - 1) {
+            return null;
+        }
+        for (int i = colon + 1; i < source.length(); i++) {
+            if (!Character.isDigit(source.charAt(i))) {
+                return null;
+            }
+        }
+        return Integer.valueOf(source.substring(colon + 1));
+    }
+
+    /**
+     * Whether the route sets the id of the step: one Camel generates is the 
type with a counter (to1, setBody2), after
+     * the node prefix of a route template or kamelet.
+     */
+    static boolean customId(ProcessorInfo proc) {
+        String id = proc.id;
+        if (id == null || id.isEmpty()) {
+            return false;
+        }
+        String type = proc.processor;
+        if (type == null) {
+            return true;
+        }
+        if (proc.nodePrefixId != null && id.startsWith(proc.nodePrefixId)) {
+            id = id.substring(proc.nodePrefixId.length());
+        }
+        if (!id.startsWith(type) || id.length() == type.length()) {
+            return true;
+        }
+        for (int i = type.length(); i < id.length(); i++) {
+            if (!Character.isDigit(id.charAt(i))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * What the step does, from its code in the route structure without the 
type in front: log:outbox for
+     * to[log:outbox], jackson for unmarshal[jackson], nothing for choice. 
Without the structure, where it sends to.
+     */
+    static String detail(ProcessorInfo proc, DiagramTopology.Step step) {
+        String code = step != null ? step.code() : null;
+        if (code == null) {
+            return proc.uri != null ? TuiHelper.displayUri(proc.uri) : "";
+        }
+        String type = proc.processor;
+        if (type != null && code.startsWith(type + "[") && code.endsWith("]")) 
{
+            return code.substring(type.length() + 1, code.length() - 1);
+        }
+        return code.equals(type) ? "" : code;
+    }
+
+    /**
+     * The step in at most the width: its id when the route sets one, then 
what it does, cut at the end with an
+     * ellipsis.
+     */
+    static Line label(ProcessorInfo proc, DiagramTopology.Step step, int 
width, Style style) {
+        List<Span> spans = new ArrayList<>();
+        int room = Math.max(0, width);
+        if (customId(proc)) {
+            String id = TuiHelper.truncate(proc.id, room);
+            spans.add(Span.styled(id, Theme.label().bold()));
+            room -= CharWidth.of(id) + 2;
+        }
+        String detail = detail(proc, step);
+        if (!detail.isEmpty() && room > 0) {
+            spans.add(Span.styled((spans.isEmpty() ? "" : "  ") + 
TuiHelper.truncate(detail, room), style));
+        }
+        return Line.from(spans);
+    }
+}
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 b08eb9a78d8b..60c1a8f75ccf 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
@@ -51,6 +51,12 @@ class RoutesTab extends AbstractDiagramTab {
 
     /** The widest the body types of a step are in the processors table: the 
info panel of the diagram has them all. */
     private static final int MAX_BODY_TYPES_WIDTH = 40;
+    /** The widest the TYPE column of the processors table grows to for the 
tree of the steps. */
+    private static final int MAX_TYPE_WIDTH = 30;
+    /** The room what a step does has in the processors table before the body 
types take the rest. */
+    private static final int MIN_PROCESSOR_WIDTH = 30;
+    /** What a step does in the processors table of Top mode, where the bar of 
the times takes the rest. */
+    private static final int TOP_PROCESSOR_WIDTH = 30;
 
     private static final String[] ROUTE_SORT_COLUMNS = { "name", "from", 
"status", "total", "failed" };
     private static final String[] ROUTE_TOP_SORT_COLUMNS = { "mean", "max", 
"min", "last", "delta", "p50", "p95", "p99" };
@@ -90,6 +96,16 @@ class RoutesTab extends AbstractDiagramTab {
         }
     }
 
+    @Override
+    public void refreshWhileVisible() {
+        // what the steps do, for the table of the processors: the route 
structure the diagrams use, asked of the
+        // integration again only when its routes change
+        String pid = ctx.selectedPid;
+        if (pid != null && !isShowDiagram()) {
+            ctx.diagramTopology.refreshStructure(ctx, pid);
+        }
+    }
+
     boolean isDiagramMetrics() {
         return diagramMetrics;
     }
@@ -856,9 +872,11 @@ class RoutesTab extends AbstractDiagramTab {
             if (maxValue <= 0) {
                 maxValue = 1;
             }
+            // sorted by time, not a tree: the steps are known by their line 
and what they do
+            int lineWidth = lineWidth(route);
 
             for (ProcessorInfo proc : sorted) {
-                Style nameStyle = proc.failed > 0 ? Theme.error() : 
Style.EMPTY.fg(Theme.accent());
+                Style nameStyle = proc.failed > 0 ? Theme.error() : 
Style.EMPTY;
                 long chartVal = procChartValue(proc);
                 String bar;
                 if (chartVal > 0) {
@@ -873,9 +891,13 @@ class RoutesTab extends AbstractDiagramTab {
                     barStyle = Style.EMPTY.fg(Theme.accent());
                 }
 
-                rows.add(Row.from(
-                        Cell.from("   " + (proc.processor != null ? 
proc.processor : "")),
-                        Cell.from(Span.styled(proc.id != null ? proc.id : "", 
nameStyle)),
+                List<Cell> cells = new ArrayList<>();
+                cells.add(Cell.from("   " + (proc.processor != null ? 
proc.processor : "")));
+                if (lineWidth > 0) {
+                    cells.add(lineCell(ProcessorLabels.line(proc.source), 
lineWidth));
+                }
+                cells.addAll(List.of(
+                        Cell.from(ProcessorLabels.label(proc, step(route, 
proc), TOP_PROCESSOR_WIDTH, nameStyle)),
                         Cell.from(Span.styled(bar, barStyle)),
                         rightCell(proc.total > 0 ? 
String.valueOf(proc.meanTime) : "", 6, topTimeStyle(proc.meanTime)),
                         rightCell(proc.total > 0 ? 
String.valueOf(proc.maxTime) : "", 6, topTimeStyle(proc.maxTime)),
@@ -890,40 +912,50 @@ class RoutesTab extends AbstractDiagramTab {
                         rightCell(String.valueOf(proc.failed), 6,
                                 proc.failed > 0 ? Theme.error() : Style.EMPTY),
                         rightCell(String.valueOf(proc.inflight), 8)));
+                rows.add(Row.from(cells));
             }
 
+            List<Cell> header = new ArrayList<>();
+            List<Constraint> widths = new ArrayList<>();
+            header.add(Cell.from(Span.styled("   TYPE", Style.EMPTY.bold())));
+            widths.add(Constraint.length(20));
+            if (lineWidth > 0) {
+                header.add(rightCell("LINE", lineWidth, Style.EMPTY.bold()));
+                widths.add(Constraint.length(lineWidth));
+            }
+            header.addAll(List.of(
+                    Cell.from(Span.styled("PROCESSOR", Style.EMPTY.bold())),
+                    Cell.from(""),
+                    rightCell(routeTopSortLabel("MEAN", "mean"), 6, 
routeTopSortStyle("mean")),
+                    rightCell(routeTopSortLabel("MAX", "max"), 6, 
routeTopSortStyle("max")),
+                    rightCell(routeTopSortLabel("MIN", "min"), 6, 
routeTopSortStyle("min")),
+                    rightCell(routeTopSortLabel("LAST", "last"), 6, 
routeTopSortStyle("last")),
+                    rightCell(routeTopSortLabel("DELTA", "delta"), 6, 
routeTopSortStyle("delta")),
+                    rightCell(routeTopSortLabel("P50", "p50"), 6, 
routeTopSortStyle("p50")),
+                    rightCell(routeTopSortLabel("P95", "p95"), 6, 
routeTopSortStyle("p95")),
+                    rightCell(routeTopSortLabel("P99", "p99"), 6, 
routeTopSortStyle("p99")),
+                    rightCell("TOTAL", 8, Style.EMPTY.bold()),
+                    rightCell("FAIL", 6, Style.EMPTY.bold()),
+                    rightCell("INFLIGHT", 8, Style.EMPTY.bold())));
+            widths.addAll(List.of(
+                    Constraint.length(TOP_PROCESSOR_WIDTH),
+                    Constraint.fill(),
+                    Constraint.length(6),
+                    Constraint.length(6),
+                    Constraint.length(6),
+                    Constraint.length(6),
+                    Constraint.length(6),
+                    Constraint.length(6),
+                    Constraint.length(6),
+                    Constraint.length(6),
+                    Constraint.length(8),
+                    Constraint.length(6),
+                    Constraint.length(9)));
+
             table = Table.builder()
                     .rows(rows)
-                    .header(Row.from(
-                            Cell.from(Span.styled("   TYPE", 
Style.EMPTY.bold())),
-                            Cell.from(Span.styled("PROCESSOR", 
Style.EMPTY.bold())),
-                            Cell.from(""),
-                            rightCell(routeTopSortLabel("MEAN", "mean"), 6, 
routeTopSortStyle("mean")),
-                            rightCell(routeTopSortLabel("MAX", "max"), 6, 
routeTopSortStyle("max")),
-                            rightCell(routeTopSortLabel("MIN", "min"), 6, 
routeTopSortStyle("min")),
-                            rightCell(routeTopSortLabel("LAST", "last"), 6, 
routeTopSortStyle("last")),
-                            rightCell(routeTopSortLabel("DELTA", "delta"), 6, 
routeTopSortStyle("delta")),
-                            rightCell(routeTopSortLabel("P50", "p50"), 6, 
routeTopSortStyle("p50")),
-                            rightCell(routeTopSortLabel("P95", "p95"), 6, 
routeTopSortStyle("p95")),
-                            rightCell(routeTopSortLabel("P99", "p99"), 6, 
routeTopSortStyle("p99")),
-                            rightCell("TOTAL", 8, Style.EMPTY.bold()),
-                            rightCell("FAIL", 6, Style.EMPTY.bold()),
-                            rightCell("INFLIGHT", 8, Style.EMPTY.bold())))
-                    .widths(
-                            Constraint.length(20),
-                            Constraint.length(14),
-                            Constraint.fill(),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(8),
-                            Constraint.length(6),
-                            Constraint.length(9))
+                    .header(Row.from(header))
+                    .widths(widths.toArray(Constraint[]::new))
                     
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
                             .title(" Processors [" + route.routeId + "] ")
                             .build())
@@ -957,6 +989,19 @@ class RoutesTab extends AbstractDiagramTab {
             bodyColWidth = Math.min(bodyColWidth, MAX_BODY_TYPES_WIDTH) + 1;
         }
 
+        // the tree of the steps is in TYPE, and LINE says where a step is in 
the source (when the runtime records it)
+        int typeWidth = typeWidth(route);
+        int lineWidth = lineWidth(route);
+        // PROCESSOR and BODY TYPES share what the borders, the gaps and the 
other columns leave: what a step does comes
+        // first, the body types get the rest up to their width (the info 
panel of the diagram has them all)
+        int columns = 8 + (lineWidth > 0 ? 1 : 0) + (bodyTypes ? 1 : 0);
+        int rest = Math.max(0,
+                area.width() - 2 - (columns - 1) - (typeWidth + lineWidth + 10 
+ 8 + 6 + 8 + 20 + 12));
+        if (bodyTypes) {
+            bodyColWidth = Math.max(CharWidth.of(" BODY TYPES"), 
Math.min(bodyColWidth, rest - MIN_PROCESSOR_WIDTH));
+        }
+        int procWidth = Math.max(0, rest - bodyColWidth);
+
         List<Row> rows = new ArrayList<>();
 
         // Synthetic top row representing the route itself
@@ -971,21 +1016,20 @@ class RoutesTab extends AbstractDiagramTab {
         } else {
             routeTimingCol = "";
         }
-        // PROCESSOR gets the rest of the width: the borders, ">> ", 7 gaps 
and the other columns; a long URI
-        // ends with an ellipsis where it is cut
-        int procWidth = Math.max(0, area.width() - 2 - 3 - 7 - (20 + 10 + 8 + 
6 + 8 + 20 + 12)
-                                    - (bodyTypes ? bodyColWidth + 1 : 0));
-        List<Cell> routeCells = new ArrayList<>(
-                List.of(
-                        Cell.from("   route"),
-                        Cell.from(Span.styled(TuiHelper.truncate(
-                                route.from != null ? 
TuiHelper.displayUri(route.from) : route.routeId, procWidth),
-                                routeStyle)),
-                        rightCell(formatThroughput(route.throughput), 8),
-                        rightCell(String.valueOf(route.total), 8),
-                        rightCell(String.valueOf(route.failed), 6,
-                                route.failed > 0 ? Theme.error() : 
Style.EMPTY),
-                        rightCell(String.valueOf(route.inflight), 8)));
+        List<Cell> routeCells = new ArrayList<>();
+        routeCells.add(Cell.from("   route"));
+        if (lineWidth > 0) {
+            routeCells.add(lineCell(ProcessorLabels.line(route.source), 
lineWidth));
+        }
+        routeCells.addAll(List.of(
+                Cell.from(Span.styled(TuiHelper.truncate(
+                        route.from != null ? TuiHelper.displayUri(route.from) 
: route.routeId, procWidth),
+                        routeStyle)),
+                rightCell(formatThroughput(route.throughput), 8),
+                rightCell(String.valueOf(route.total), 8),
+                rightCell(String.valueOf(route.failed), 6,
+                        route.failed > 0 ? Theme.error() : Style.EMPTY),
+                rightCell(String.valueOf(route.inflight), 8)));
         if (bodyTypes) {
             routeCells.add(Cell.from(""));
         }
@@ -994,8 +1038,7 @@ class RoutesTab extends AbstractDiagramTab {
         rows.add(Row.from(routeCells));
 
         for (ProcessorInfo proc : route.processors) {
-            String indent = "  ".repeat(proc.level);
-            Style nameStyle = proc.failed > 0 ? Theme.error() : 
Style.EMPTY.fg(Theme.accent());
+            Style nameStyle = proc.failed > 0 ? Theme.error() : Style.EMPTY;
 
             String procTimingCol;
             if (hasProcPercentiles && proc.p50Time >= 0) {
@@ -1008,16 +1051,18 @@ class RoutesTab extends AbstractDiagramTab {
                 procTimingCol = "";
             }
 
-            List<Cell> cells = new ArrayList<>(
-                    List.of(
-                            Cell.from("   " + (proc.processor != null ? 
proc.processor : "")),
-                            Cell.from(Span.styled(TuiHelper.truncate(indent + 
(proc.id != null ? proc.id : ""), procWidth),
-                                    nameStyle)),
-                            rightCell(formatThroughput(proc.throughput), 8),
-                            rightCell(String.valueOf(proc.total), 8),
-                            rightCell(String.valueOf(proc.failed), 6,
-                                    proc.failed > 0 ? Theme.error() : 
Style.EMPTY),
-                            rightCell(String.valueOf(proc.inflight), 8)));
+            List<Cell> cells = new ArrayList<>();
+            cells.add(Cell.from(TuiHelper.truncate(typeCell(proc), 
typeWidth)));
+            if (lineWidth > 0) {
+                cells.add(lineCell(ProcessorLabels.line(proc.source), 
lineWidth));
+            }
+            cells.addAll(List.of(
+                    Cell.from(ProcessorLabels.label(proc, step(route, proc), 
procWidth, nameStyle)),
+                    rightCell(formatThroughput(proc.throughput), 8),
+                    rightCell(String.valueOf(proc.total), 8),
+                    rightCell(String.valueOf(proc.failed), 6,
+                            proc.failed > 0 ? Theme.error() : Style.EMPTY),
+                    rightCell(String.valueOf(proc.inflight), 8)));
             if (bodyTypes) {
                 cells.add(Cell.from(Span.styled(
                         " " + 
TuiHelper.truncate(BodyTypes.shortLabel(proc.bodyTypes, proc.bodyKinds), 
bodyColWidth - 1),
@@ -1029,22 +1074,26 @@ class RoutesTab extends AbstractDiagramTab {
         }
         String procTimingHeader = hasProcPercentiles ? "P50/P95/P99" : 
"MIN/MAX/MEAN";
 
-        List<Cell> header = new ArrayList<>(
-                List.of(
-                        Cell.from(Span.styled("   TYPE", Style.EMPTY.bold())),
-                        Cell.from(Span.styled("PROCESSOR", 
Style.EMPTY.bold())),
-                        rightCell(ctx.ratePerMinute ? "MSG/M" : "MSG/S", 8, 
Style.EMPTY.bold()),
-                        rightCell("TOTAL", 8, Style.EMPTY.bold()),
-                        rightCell("FAIL", 6, Style.EMPTY.bold()),
-                        rightCell("INFLIGHT", 8, Style.EMPTY.bold())));
-        List<Constraint> widths = new ArrayList<>(
-                List.of(
-                        Constraint.length(20),
-                        Constraint.fill(),
-                        Constraint.length(10),
-                        Constraint.length(8),
-                        Constraint.length(6),
-                        Constraint.length(8)));
+        List<Cell> header = new ArrayList<>();
+        List<Constraint> widths = new ArrayList<>();
+        header.add(Cell.from(Span.styled("   TYPE", Style.EMPTY.bold())));
+        widths.add(Constraint.length(typeWidth));
+        if (lineWidth > 0) {
+            header.add(rightCell("LINE", lineWidth, Style.EMPTY.bold()));
+            widths.add(Constraint.length(lineWidth));
+        }
+        header.addAll(List.of(
+                Cell.from(Span.styled("PROCESSOR", Style.EMPTY.bold())),
+                rightCell(ctx.ratePerMinute ? "MSG/M" : "MSG/S", 8, 
Style.EMPTY.bold()),
+                rightCell("TOTAL", 8, Style.EMPTY.bold()),
+                rightCell("FAIL", 6, Style.EMPTY.bold()),
+                rightCell("INFLIGHT", 8, Style.EMPTY.bold())));
+        widths.addAll(List.of(
+                Constraint.fill(),
+                Constraint.length(10),
+                Constraint.length(8),
+                Constraint.length(6),
+                Constraint.length(8)));
         if (bodyTypes) {
             header.add(Cell.from(Span.styled(" BODY TYPES", 
Style.EMPTY.bold())));
             widths.add(Constraint.length(bodyColWidth));
@@ -1064,6 +1113,39 @@ class RoutesTab extends AbstractDiagramTab {
                 .build();
     }
 
+    /** The type of a step, indented by its level: the tree of the steps of 
the route. */
+    private static String typeCell(ProcessorInfo proc) {
+        return "   " + "  ".repeat(Math.max(0, proc.level - 1)) + 
(proc.processor != null ? proc.processor : "");
+    }
+
+    /** As wide as the deepest step needs, up to a limit. */
+    private static int typeWidth(RouteInfo route) {
+        int width = CharWidth.of("   route");
+        for (ProcessorInfo proc : route.processors) {
+            width = Math.max(width, CharWidth.of(typeCell(proc)));
+        }
+        return Math.min(width, MAX_TYPE_WIDTH);
+    }
+
+    /** As wide as the highest line needs, or 0 when the runtime records no 
source locations (no LINE column). */
+    private static int lineWidth(RouteInfo route) {
+        int max = 0;
+        for (ProcessorInfo proc : route.processors) {
+            Integer line = ProcessorLabels.line(proc.source);
+            max = line != null ? Math.max(max, line) : max;
+        }
+        return max > 0 ? Math.max(CharWidth.of("LINE"), 
String.valueOf(max).length()) : 0;
+    }
+
+    private static Cell lineCell(Integer line, int width) {
+        return rightCell(line != null ? String.valueOf(line) : "", width, 
Theme.muted());
+    }
+
+    /** The step as the route structure has it, from what the diagrams asked 
of the integration. */
+    private DiagramTopology.Step step(RouteInfo route, ProcessorInfo proc) {
+        return ctx.diagramTopology.step(ctx.selectedPid, route.routeId, 
proc.id);
+    }
+
     private String formatThroughput(String throughput) {
         if (throughput == null || throughput.isEmpty()) {
             return "";
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 e523c2bd131f..1ba8daab4040 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
@@ -302,6 +302,7 @@ final class StatusParser {
                         JsonObject pj = (JsonObject) p;
                         ProcessorInfo pi = new ProcessorInfo();
                         pi.id = pj.getString("id");
+                        pi.nodePrefixId = pj.getString("nodePrefixId");
                         pi.processor = pj.getString("processor");
                         pi.source = pj.getString("source");
                         pi.level = pj.getIntegerOrDefault("level", 0);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
index 0be659c4e1a6..fc49acbdb236 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
@@ -18,6 +18,15 @@ they are sent to. A typical integration has multiple routes 
working together.
 - **MAX** — Slowest exchange processing time in milliseconds. A very high MAX 
compared to MEAN suggests occasional slow outliers
 - **SINCE-LAST** — Time since the last exchange activity on this route, shown 
as up to three values separated by `/`: started/completed/failed (e.g., 
`1s/3s/1m14s`). Values are omitted when there is no activity of that type
 
+## Processors
+
+Below the routes are the steps of the selected route, with the same numbers:
+
+- **TYPE** — The kind of step (`to`, `setBody`, `choice`), indented under the 
step it is inside, such as the steps of a branch of a choice
+- **LINE** — The line of the route source the step is on (when the runtime 
records the source locations)
+- **PROCESSOR** — What the step does, as the route diagram shows it: where it 
sends to (`log:outbox`), its expression (`simple{${body}}`) or its data format 
(`jackson`). An id the route gives a step (`id: audit`) is shown first; the ids 
Camel generates (`to1`, `setBody2`) are left out, as they only repeat the type
+- **BODY TYPES** — The body types the step has been reached with, across the 
messages (with the message history on)
+
 ## Percentile Latency (P50/P95/P99)
 
 When **Extended** statistics level is enabled, the timing columns show
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesProcessorLabelsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesProcessorLabelsTest.java
new file mode 100644
index 000000000000..8e8ce14d9204
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesProcessorLabelsTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.style.Style;
+import dev.tamboui.text.Line;
+import dev.tamboui.tui.event.KeyEvent;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The processors table of the Routes tab shows where each step is in the 
source and what it does, as the route
+ * structure has it, and an id only when the route sets one: one Camel 
generates (to1, setBody1) repeats the type.
+ */
+class RoutesProcessorLabelsTest {
+
+    /** The structure of the orders route, as the route-topology console 
answers with the routes. */
+    private static final String TOPOLOGY = """
+            {"nodes": [], "edges": [], "routes": [{"routeId": "orders", 
"code": [
+              {"line": 3, "type": "route", "id": "orders", "level": 0, "code": 
"route[orders]"},
+              {"line": 4, "type": "from", "id": "orders", "level": 0, "code": 
"from[timer:orders]"},
+              {"line": 8, "type": "setBody", "id": "setBody1", "level": 1, 
"code": "setBody[constant{hello}]"},
+              {"line": 10, "type": "unmarshal", "id": "unmarshal1", "level": 
1, "code": "unmarshal[jackson]"},
+              {"line": 12, "type": "choice", "id": "choice1", "level": 1, 
"code": "choice"},
+              {"line": 16, "type": "marshal", "id": "marshal1", "level": 3, 
"code": "marshal[jackson]"},
+              {"line": 21, "type": "to", "id": "to1", "level": 1, "code": 
"to[log:outbox]", "uri": "log:outbox"},
+              {"line": 22, "type": "multicast", "id": "audit-fanout", "level": 
1, "code": "multicast"}]}]}
+            """;
+
+    private StructureContext ctx;
+    private RouteInfo route;
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+        IntegrationInfo info = new IntegrationInfo();
+        info.pid = "5678";
+        info.name = "orders";
+        route = new RouteInfo();
+        route.routeId = "orders";
+        route.from = "timer://orders";
+        route.state = "Started";
+        route.source = "orders.camel.yaml:3";
+        route.processors.add(processor("setBody1", "setBody", 1, 8, null));
+        route.processors.add(processor("unmarshal1", "unmarshal", 1, 10, 
null));
+        route.processors.add(processor("choice1", "choice", 1, 12, null));
+        route.processors.add(processor("marshal1", "marshal", 2, 16, null));
+        route.processors.add(processor("to1", "to", 1, 21, "log://outbox"));
+        route.processors.add(processor("audit-fanout", "multicast", 1, 22, 
null));
+        info.routes.add(route);
+        ctx = new StructureContext(info);
+        ctx.selectedPid = "5678";
+    }
+
+    @Test
+    void theStepsShowTheirLineAndWhatTheyDo() {
+        ctx.diagramTopology.refreshStructure(ctx, "5678");
+
+        String rendered = TuiTestHelper.renderToString(new RoutesTab(ctx), 
160, 30);
+
+        assertThat(rendered).contains("LINE", "log:outbox", "jackson", 
"constant{hello}");
+        // the tree of the steps is in TYPE: the marshal is inside the choice
+        assertThat(rendered).contains("     marshal");
+        assertThat(line(rendered, "log:outbox")).contains(" 21 ");
+        // ids Camel generated are left out, one the route sets is shown
+        assertThat(rendered).doesNotContain("to1", "setBody1", "unmarshal1", 
"marshal1", "choice1");
+        assertThat(rendered).contains("audit-fanout");
+    }
+
+    @Test
+    void beforeTheStructureIsThereASendingStepShowsWhereItSends() {
+        String rendered = TuiTestHelper.renderToString(new RoutesTab(ctx), 
160, 30);
+
+        assertThat(rendered).contains("log://outbox").doesNotContain("to1", 
"jackson");
+    }
+
+    @Test
+    void noLineColumnWithoutSourceLocations() {
+        route.processors.forEach(p -> p.source = null);
+
+        String rendered = TuiTestHelper.renderToString(new RoutesTab(ctx), 
160, 30);
+
+        assertThat(rendered).doesNotContain("LINE");
+    }
+
+    @Test
+    void topModeShowsThemToo() {
+        ctx.diagramTopology.refreshStructure(ctx, "5678");
+        RoutesTab tab = new RoutesTab(ctx);
+        tab.handleKeyEvent(KeyEvent.ofChar('t'));
+
+        String rendered = TuiTestHelper.renderToString(tab, 160, 30);
+
+        assertThat(rendered).contains("LINE", "log:outbox", 
"audit-fanout").doesNotContain("unmarshal1");
+    }
+
+    @Test
+    void theLineOfASourceLocation() {
+        assertThat(ProcessorLabels.line("orders.camel.yaml:21")).isEqualTo(21);
+        
assertThat(ProcessorLabels.line("file:/work/Orders.java:7")).isEqualTo(7);
+        assertThat(ProcessorLabels.line("orders.camel.yaml")).isNull();
+        assertThat(ProcessorLabels.line("orders.camel.yaml:")).isNull();
+        assertThat(ProcessorLabels.line(null)).isNull();
+    }
+
+    @Test
+    void anIdTheRouteSets() {
+        assertThat(ProcessorLabels.customId(processor("to1", "to", 1, 0, 
null))).isFalse();
+        assertThat(ProcessorLabels.customId(processor("setBody12", "setBody", 
1, 0, null))).isFalse();
+        assertThat(ProcessorLabels.customId(processor("audit", "to", 1, 0, 
null))).isTrue();
+        assertThat(ProcessorLabels.customId(processor("to", "to", 1, 0, 
null))).isTrue();
+        assertThat(ProcessorLabels.customId(processor("toAudit1", "to", 1, 0, 
null))).isTrue();
+        // a route template or kamelet puts its prefix in front of the ids it 
generates
+        ProcessorInfo prefixed = processor("orders-to3", "to", 1, 0, null);
+        prefixed.nodePrefixId = "orders-";
+        assertThat(ProcessorLabels.customId(prefixed)).isFalse();
+    }
+
+    @Test
+    void whatAStepDoes() {
+        ProcessorInfo to = processor("to1", "to", 1, 0, "log://outbox");
+        assertThat(ProcessorLabels.detail(to, new 
DiagramTopology.Step("to[log:outbox]", 21))).isEqualTo("log:outbox");
+        assertThat(ProcessorLabels.detail(to, null)).isEqualTo("log://outbox");
+        ProcessorInfo choice = processor("choice1", "choice", 1, 0, null);
+        assertThat(ProcessorLabels.detail(choice, new 
DiagramTopology.Step("choice", 12))).isEmpty();
+        assertThat(ProcessorLabels.detail(choice, null)).isEmpty();
+
+        ProcessorInfo audit = processor("audit", "to", 1, 0, null);
+        Line label = ProcessorLabels.label(audit, new 
DiagramTopology.Step("to[log:audit]", 30), 40, Style.EMPTY);
+        assertThat(text(label)).isEqualTo("audit  log:audit");
+        assertThat(text(ProcessorLabels.label(audit, new 
DiagramTopology.Step("to[log:audit]", 30), 12, Style.EMPTY)))
+                .isEqualTo("audit  lo...");
+    }
+
+    private static ProcessorInfo processor(String id, String type, int level, 
int line, String uri) {
+        ProcessorInfo proc = new ProcessorInfo();
+        proc.id = id;
+        proc.processor = type;
+        proc.level = level;
+        proc.source = line > 0 ? "orders.camel.yaml:" + line : null;
+        proc.uri = uri;
+        return proc;
+    }
+
+    private static String line(String rendered, String text) {
+        return rendered.lines().filter(l -> 
l.contains(text)).findFirst().orElse("");
+    }
+
+    private static String text(Line line) {
+        StringBuilder sb = new StringBuilder();
+        line.spans().forEach(span -> sb.append(span.content()));
+        return sb.toString();
+    }
+
+    /** Answers the route-topology action with the structure of the orders 
route. */
+    private static final class StructureContext extends MonitorContext {
+
+        StructureContext(IntegrationInfo info) {
+            super(new AtomicReference<>(List.of(info)), new 
AtomicReference<>(List.of()));
+        }
+
+        @Override
+        JsonObject executeAction(String pid, JsonObject request, long 
timeoutMs) {
+            return Jsoner.deserialize(TOPOLOGY, new JsonObject());
+        }
+    }
+}

Reply via email to