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