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 f6301f02280a CAMEL-25892: camel-jbang - show the body types each step
has been reached with in the Routes and Diagram views (#28192)
f6301f02280a is described below
commit f6301f02280a6d31e861ecfcab3c71de7f5df5ec
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 20 22:11:10 2026 +0200
CAMEL-25892: camel-jbang - show the body types each step has been reached
with in the Routes and Diagram views (#28192)
* CAMEL-25892: camel-diagram - the steps of a route diagram have the body
types they have been reached with
The route-structure console has the body types each step has been reached
with,
across the messages, and their kinds (CAMEL-25750). The node of a route
diagram
now keeps them, so a renderer can draw them: RouteDiagramHelper reads them,
and
the steps of an onException block copied into the error frame keep them.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-25892: camel-jbang - show the body types each step has been reached
with in the Routes and Diagram views
Routes: a BODY TYPES column says how many steps of a route have been reached
with more than one body type, and how many of them with bodies of more than
one
kind, which need attention; the processors of the selected route have the
types
of each step.
Diagram: y shows the types beside the arrow into each step of a route, on
the
shaft above the head, cut to the room there is where the arrows of a branch
are
side by side (the first types and how many more). A step reached with
bodies of
more than one kind has a warning sign in its top border. The Info panel of
a step
lists all its types with their kind, the Info panel of a route in the
topology
its counts, and the diagram state of the MCP tools has them. The types
follow the
status file while they are shown, as the metrics do.
DiagramSupport was at its size limit: its layout helpers moved to
DiagramLayouts.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../apache/camel/diagram/RouteDiagramHelper.java | 16 ++
.../camel/diagram/RouteDiagramLayoutEngine.java | 10 +
.../org/apache/camel/diagram/RouteErrorFrame.java | 2 +
.../camel/diagram/RouteDiagramHelperTest.java | 29 ++
.../apache/camel/diagram/RouteErrorFrameTest.java | 15 +
.../images/jbang/camel-tui-diagram-body-types.png | Bin 0 -> 329211 bytes
.../images/jbang/camel-tui-routes-body-types.png | Bin 0 -> 350752 bytes
.../ROOT/pages/camel-jbang-tui-diagram.adoc | 41 ++-
.../core/commands/tui/AbstractDiagramTab.java | 13 +-
.../dsl/jbang/core/commands/tui/BodyTypes.java | 122 ++++++++
.../core/commands/tui/DiagramDetailSupport.java | 285 +++++++++++--------
.../jbang/core/commands/tui/DiagramLayouts.java | 95 +++++++
.../jbang/core/commands/tui/DiagramSupport.java | 99 ++-----
.../dsl/jbang/core/commands/tui/DiagramTab.java | 7 +
.../jbang/core/commands/tui/DiagramTopology.java | 12 +-
.../dsl/jbang/core/commands/tui/ProcessorInfo.java | 5 +
.../dsl/jbang/core/commands/tui/RouteInfo.java | 4 +
.../dsl/jbang/core/commands/tui/RoutesTab.java | 316 +++++++++++++--------
.../dsl/jbang/core/commands/tui/StatusParser.java | 20 +-
.../commands/tui/diagram/RouteDiagramWidget.java | 149 +++++++++-
.../src/main/resources/tui/help/diagram.md | 35 ++-
.../src/main/resources/tui/help/routes.md | 2 +
.../core/commands/tui/AbstractDiagramTabTest.java | 16 ++
.../core/commands/tui/BodyTypesViewsTest.java | 198 +++++++++++++
.../core/commands/tui/DiagramTopologyTest.java | 19 ++
.../jbang/core/commands/tui/SourceSizeTest.java | 5 +-
.../tui/diagram/BodyTypesOnArrowsTest.java | 142 +++++++++
27 files changed, 1328 insertions(+), 329 deletions(-)
diff --git
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
index 48f94cb6e637..f7964e845675 100644
---
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
+++
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
@@ -81,6 +81,8 @@ public final class RouteDiagramHelper {
node.level = level != null ? level : 0;
Integer lineNum = line.getInteger("line");
node.line = lineNum != null ? lineNum : 0;
+ node.bodyTypes = strings(line.get("bodyTypes"));
+ node.bodyKinds = strings(line.get("bodyKinds"));
if (line.containsKey("statistics")) {
JsonObject ls = line.getJsonObject("statistics");
@@ -125,6 +127,20 @@ public final class RouteDiagramHelper {
return routes;
}
+ /** The strings of a JSON array, such as the body types of a step; empty
when there are none. */
+ private static List<String> strings(Object array) {
+ if (!(array instanceof List<?> list) || list.isEmpty()) {
+ return List.of();
+ }
+ List<String> answer = new ArrayList<>(list.size());
+ for (Object o : list) {
+ if (o != null) {
+ answer.add(o.toString());
+ }
+ }
+ return List.copyOf(answer);
+ }
+
static List<String> wrapText(String text, int maxWidth) {
if (maxWidth <= 0 || text.length() <= maxWidth) {
return List.of(text);
diff --git
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
index 24bea721acf7..85f31f1a2415 100644
---
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
+++
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
@@ -186,6 +186,16 @@ public class RouteDiagramLayoutEngine {
public int line;
public boolean remote;
public StatInfo stat;
+ /**
+ * The body types the step has been reached with, across the messages,
when the message history recorded them:
+ * the class names, at most a few.
+ */
+ public List<String> bodyTypes = List.of();
+ /**
+ * The kinds of those bodies: null, payload, data and collection. More
than one is a step that needs attention,
+ * as a step that writes the body fails for the data, and one that
reads fields fails for the payload.
+ */
+ public List<String> bodyKinds = List.of();
}
public static class RouteInfo {
diff --git
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteErrorFrame.java
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteErrorFrame.java
index 336c771d1a64..91293c416c32 100644
---
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteErrorFrame.java
+++
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteErrorFrame.java
@@ -87,6 +87,8 @@ public record RouteErrorFrame(int topY, int bottomY,
List<String> lines) {
copy.line = n.line;
copy.remote = n.remote;
copy.stat = n.stat;
+ copy.bodyTypes = n.bodyTypes;
+ copy.bodyKinds = n.bodyKinds;
return copy;
}
diff --git
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramHelperTest.java
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramHelperTest.java
index f1415f6bf9e8..c932f0d9aa7a 100644
---
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramHelperTest.java
+++
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramHelperTest.java
@@ -18,12 +18,41 @@ package org.apache.camel.diagram;
import java.util.List;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+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;
class RouteDiagramHelperTest {
+ @Test
+ void parseRoutesKeepsTheBodyTypesOfEachStep() {
+ JsonObject structure = (JsonObject) Jsoner.deserialize("""
+ {"routes": [{"routeId": "orders", "code": [
+ {"type": "route", "id": "orders", "level": 0, "code":
"route"},
+ {"type": "from", "id": "from1", "level": 0, "code":
"from[platform-http:/orders]"},
+ {"type": "unmarshal", "id": "unmarshal1", "level": 1,
"code": "unmarshal[json]",
+ "bodyTypes":
["org.apache.camel.converter.stream.ByteArrayInputStreamCache"], "bodyKinds":
["payload"]},
+ {"type": "to", "id": "to1", "level": 1, "code":
"to[file:outbox]",
+ "bodyTypes": ["byte[]", "java.util.LinkedHashMap"],
"bodyKinds": ["payload", "data"]}]}]}
+ """, (JsonObject) null);
+
+ RouteInfo route = RouteDiagramHelper.parseRoutes(structure).get(0);
+
+ NodeInfo from = route.nodes.get(1);
+ NodeInfo to = route.nodes.get(3);
+ // the from has none: the body it gave is what the first step was
reached with
+ assertThat(from.bodyTypes).isEmpty();
+ assertThat(from.bodyKinds).isEmpty();
+ assertThat(route.nodes.get(2).bodyTypes)
+
.containsExactly("org.apache.camel.converter.stream.ByteArrayInputStreamCache");
+ assertThat(to.bodyTypes).containsExactly("byte[]",
"java.util.LinkedHashMap");
+ assertThat(to.bodyKinds).containsExactly("payload", "data");
+ }
+
@Test
void wrapTextShortReturnedAsIs() {
assertThat(RouteDiagramHelper.wrapText("short",
20)).containsExactly("short");
diff --git
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteErrorFrameTest.java
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteErrorFrameTest.java
index 86a214f716b9..4ae95ec3d485 100644
---
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteErrorFrameTest.java
+++
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteErrorFrameTest.java
@@ -65,6 +65,21 @@ class RouteErrorFrameTest {
.extracting(n -> n.id + "@" +
n.level).containsExactly("onException1@0", "log1@1", "to1@1"));
}
+ @Test
+ void theStepsOfAnOnExceptionKeepTheirBodyTypes() {
+ RouteInfo route = checkout();
+ NodeInfo log = route.nodes.get(2);
+ log.bodyTypes = List.of("java.lang.String", "java.util.LinkedHashMap");
+ log.bodyKinds = List.of("payload", "data");
+
+ RouteErrorFrame.Split split = RouteErrorFrame.split(route);
+
+ NodeInfo copy = split.blocks().get(0).nodes.get(1);
+ assertThat(copy.id).isEqualTo("log1");
+ assertThat(copy.bodyTypes).containsExactly("java.lang.String",
"java.util.LinkedHashMap");
+ assertThat(copy.bodyKinds).containsExactly("payload", "data");
+ }
+
@Test
void theLinesSayWhereTheFailuresGoAndComeFrom() {
assertThat(RouteErrorFrame.lines("checkout", PATHS, LineStyle.UNICODE))
diff --git
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-diagram-body-types.png
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-diagram-body-types.png
new file mode 100644
index 000000000000..0b3807880a4a
Binary files /dev/null and
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-diagram-body-types.png
differ
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
new file mode 100644
index 000000000000..d4f882ffd2b5
Binary files /dev/null and
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-routes-body-types.png
differ
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
index e0ae315405d3..33693e0a9107 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
@@ -16,6 +16,7 @@ See xref:camel-jbang-tui.adoc[Camel TUI] for getting started
and the other pages
topology and in a route, and in a box of its own in the architecture
* <<_external_endpoints,*External endpoints*>> -- what the integration
consumes from and sends to outside
* <<_metrics_and_source_code,*Metrics and source*>> -- throughput and timing
on the boxes, and the source a box comes from
+* <<_body_types,*Body types*>> -- the types of the bodies each step has been
reached with, on the arrows into the steps
== Route Topology Diagram
@@ -84,8 +85,8 @@ the summary once it is saved. *b* switches every level
between the *business* vi
labels and what capabilities achieve (the AI-assisted parts marked ✦), or
route ids with their
endpoints and the entry points and systems of each capability. The business
view is the default when
the routes have descriptions, from their source or the AI project overview;
*n*, the key of the former
-description toggle, does the same. *m* metrics and *e* external systems apply
to the topology, *m* and
-*d* detail to a route. *g* group, in the topology, shows the architecture one
level down: each route
+description toggle, does the same. *m* metrics and *e* external systems apply
to the topology, *m*,
+*y* body types and *d* detail to a route. *g* group, in the topology, shows
the architecture one level down: each route
box names its group (a route group, an AI capability marked ✦, shared services
or utility), each group
has the border colour of its box in the architecture, and a legend at the
bottom of the topology lists
the groups in their colours; it is on when the project has groups. *u* shows
or hides the utility
@@ -181,6 +182,41 @@ Press *e* to cycle through three modes for external
endpoints:
* Press *c* to view the source code of the selected route, with syntax
highlighting
* Press *b* (or *n*) to switch between the business view (labels) and the
technical view (route ids and endpoints)
+=== Body Types
+
+When the integration runs with the message history on, such as with `camel run
--dev`, Camel keeps the body types
+each step has been reached with, across the messages (see
+xref:camel-jbang-diagnostics.adoc#_body_types[Body types] for the same in the
Camel CLI). In a route, press *y* to show
+them beside the arrow into each step, so the arrows show how the body changes
along the route:
+
+[source,text]
+----
+ ┌──────────────────────┐
+ │ unmarshal[json] │
+ └──────────────────────┘
+ │
+ │ ⚠ byte[] | LinkedHashMap (payload and data)
+ 3 ▼ 12
+ ┌────────────────────⚠─┐
+ │ to[file:outbox] │
+ └──────────────────────┘
+----
+
+A step reached with more than one type is a part of the route that is dynamic.
When the types are of more than one
+kind (`null`, `payload`, `data` or `collection`), the step needs attention, as
its body depends on the path a message
+takes: its box has a ⚠ in its top border, whether the types are shown or not.
Here some messages reach the file
+endpoint with parsed data that was never marshalled.
+
+image::jbang/camel-tui-diagram-body-types.png[The orders route with its body
types on the arrows: only one branch of the choice marshals, so the step after
it is reached with a stream and a map, and its Info panel says the body there
depends on the path a message takes]
+
+The arrows of a branch are side by side, so the types beside them are cut to
the room there is: the first ones and how
+many more (`ByteArrayInputStreamCache +2`). The Info panel of the selected
step lists them all with their kind. In the
+topology, the Info panel of a route says how many of its steps were reached
with more than one type
+(`⚠ 1 mixed · 2 varied`), and the Routes tab has the same in its BODY TYPES
column, with the types of each step in the
+table of the processors of the selected route.
+
+image::jbang/camel-tui-routes-body-types.png[The Routes tab: the orders route
has four steps reached with bodies of more than one kind, and its processors
list the types of each step]
+
== Keyboard Shortcuts
=== Diagram Tab
@@ -194,6 +230,7 @@ Press *e* to cycle through three modes for external
endpoints:
| *Esc* | Go back (previous route or topology)
| *t* | Jump to topology view
| *m* | Toggle metrics overlay
+| *y* | In a route: show the body types each step has been reached with,
beside the arrows into the steps
| *e* | External systems: in the topology off, edges, all; in the architecture
off or edges
| *a* | AI-assisted parts shown, or the facts alone; without a
`camel-summary.md` yet, has the AI write one
| *b* (or *n*) | Business view (labels, what capabilities achieve) or
technical view (route ids, endpoints)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
index f60cd28aa4ff..7cd960f2cdc2 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
@@ -31,7 +31,8 @@ import dev.tamboui.tui.event.KeyEvent;
* and Esc walks back along the routes followed, then to the topology; t jumps
back to the topology</li>
* <li>d shows the detail panel of the selected node (PgUp and PgDn scroll
it), g goes to a node by name, c shows the
* source of the selected node or route</li>
- * <li>m shows the metrics, e the external systems of the topology</li>
+ * <li>m shows the metrics, e the external systems of the topology, y the body
types each step of a route has been
+ * reached with</li>
* </ul>
* A tab offers a key to {@link #handleGotoNodeKeys(KeyEvent)}, then to its
own keys and those of the source, and to
* {@link #handleDiagramKeys(KeyEvent)}; Esc to {@link #handleDiagramEscape()}.
@@ -156,6 +157,13 @@ abstract class AbstractDiagramTab extends AbstractTab {
reloadDiagram();
return true;
}
+ if (!topologyMode && ke.isCharIgnoreCase('y')) {
+ // the body types each step has been reached with, beside the
arrows into the steps (CAMEL-25750)
+ diagram.setShowBodyTypes(!diagram.isShowBodyTypes());
+ diagram.endLoad();
+ reloadDiagram();
+ return true;
+ }
if (topologyMode && ke.isCharIgnoreCase('e')) {
// external systems: off, edges, all
externalMode = (externalMode + 1) % 3;
@@ -254,8 +262,9 @@ abstract class AbstractDiagramTab extends AbstractTab {
}
}
+ /** The diagram again with the numbers and the body types of the status
file, while they are shown. */
void refreshDiagramIfNeeded() {
- if (diagram.isShowDiagram() && diagramMetrics) {
+ if (diagram.isShowDiagram() && (diagramMetrics ||
diagram.isShowBodyTypes())) {
reloadDiagram();
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypes.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypes.java
new file mode 100644
index 000000000000..47034b59dbdd
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypes.java
@@ -0,0 +1,122 @@
+/*
+ * 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.StringJoiner;
+
+import dev.tamboui.style.Style;
+import dev.tamboui.text.CharWidth;
+
+/**
+ * The body types a step has been reached with, across the messages, as words
for the views (CAMEL-25750): their simple
+ * class names, and their kinds (null, payload, data or collection) when there
is more than one kind, which is what
+ * needs attention.
+ */
+public final class BodyTypes {
+
+ /** The mark of a step reached with bodies of more than one kind. */
+ public static final String MARK = TuiIcons.WARN;
+
+ private BodyTypes() {
+ }
+
+ /** Whether the bodies were of more than one kind: a step that needs
attention. */
+ public static boolean mixed(List<String> kinds) {
+ return kinds != null && kinds.size() > 1;
+ }
+
+ /** The simple class name of a type: LinkedHashMap, byte[]. */
+ public static String simpleName(String type) {
+ return type.substring(type.lastIndexOf('.') + 1);
+ }
+
+ /** The types by their simple class names: String | LinkedHashMap. */
+ public static String names(List<String> types) {
+ StringJoiner sj = new StringJoiner(" | ");
+ for (String type : types) {
+ sj.add(simpleName(type));
+ }
+ return sj.toString();
+ }
+
+ /** The kinds in words: payload and data, or null, payload and data. */
+ public static String kinds(List<String> kinds) {
+ if (kinds.size() < 2) {
+ return String.join("", kinds);
+ }
+ return String.join(", ", kinds.subList(0, kinds.size() - 1)) + " and "
+ kinds.get(kinds.size() - 1);
+ }
+
+ /**
+ * The types, with a warning sign and the kinds when there is more than
one kind: "⚠ String | LinkedHashMap (payload
+ * and data)". Empty when the step has not been reached.
+ */
+ public static String label(List<String> types, List<String> kinds) {
+ if (types == null || types.isEmpty()) {
+ return "";
+ }
+ return mixed(kinds) ? MARK + " " + names(types) + " (" + kinds(kinds)
+ ")" : names(types);
+ }
+
+ /**
+ * The types, with a warning sign when there is more than one kind, for a
table where the room is short: "⚠ String |
+ * LinkedHashMap". Empty when the step has not been reached.
+ */
+ public static String shortLabel(List<String> types, List<String> kinds) {
+ if (types == null || types.isEmpty()) {
+ return "";
+ }
+ return mixed(kinds) ? MARK + " " + names(types) : names(types);
+ }
+
+ /**
+ * The types in at most the width, the most that fits of: all of them with
their kinds, all of them, the first ones
+ * and how many more ("⚠ byte[] +1"), the first one cut and how many more.
A view that cuts them shows them all
+ * elsewhere (the diagram in its Info panel).
+ */
+ public static String fit(List<String> types, List<String> kinds, int
width) {
+ if (types == null || types.isEmpty() || width <= 0) {
+ return "";
+ }
+ String all = label(types, kinds);
+ if (CharWidth.of(all) <= width) {
+ return all;
+ }
+ String mark = mixed(kinds) ? MARK + " " : "";
+ for (int n = types.size(); n >= 1; n--) {
+ String more = n < types.size() ? " +" + (types.size() - n) : "";
+ String text = mark + names(types.subList(0, n)) + more;
+ if (CharWidth.of(text) <= width) {
+ return text;
+ }
+ }
+ String more = types.size() > 1 ? " +" + (types.size() - 1) : "";
+ int room = width - CharWidth.of(mark) - CharWidth.of(more);
+ if (room < 2) {
+ // no room for a name: the mark alone says the step needs attention
+ return mark.isEmpty() ? "" : MARK;
+ }
+ return mark + CharWidth.truncateWithEllipsis(simpleName(types.get(0)),
room, "…", CharWidth.TruncatePosition.END)
+ + more;
+ }
+
+ /** The style of the types: a warning for more than one kind, muted
otherwise. */
+ public static Style style(List<String> kinds) {
+ return mixed(kinds) ? Theme.warning() : Theme.muted();
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java
index 4fed814873fd..60d3358369a8 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java
@@ -35,6 +35,7 @@ import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
import dev.tamboui.widgets.paragraph.Paragraph;
import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine;
import org.apache.camel.dsl.jbang.core.commands.tui.diagram.DiagramColors;
import org.apache.camel.util.TimeUtils;
import org.apache.camel.util.json.JsonObject;
@@ -295,6 +296,11 @@ final class DiagramDetailSupport {
Span.styled(stateLabel, stateStyle)));
addGroup(lines, info, route, diagram.isShowErrorPaths());
addDescription(lines, info, route, area.width() - 3);
+ if (route.variedBodyTypes > 0) {
+ // the steps reached with more than one body type
(CAMEL-25750): the route diagram shows which, with y
+ lines.add(Line.from(Span.styled(" Body: ", Theme.muted()),
Span.styled(RoutesTab.bodyTypesLabel(route),
+ route.mixedBodyKinds > 0 ? Theme.warning() :
Style.EMPTY.fg(Theme.baseFg()))));
+ }
lines.add(Line.from(Span.raw("")));
lines.add(Line.from(
@@ -372,50 +378,7 @@ final class DiagramDetailSupport {
}
} else {
- // External endpoint — show topology node data
- var topoNode = diagram.getSelectedTopologyNode();
- if (topoNode != null) {
- boolean isInbound = "external-in".equals(topoNode.nodeType);
- boolean isBridge = "external".equals(topoNode.nodeType);
- String label = isBridge ? " External" : isInbound ? " Inbound"
: " Outbound";
- lines.add(Line.from(
- Span.styled(label,
Style.EMPTY.fg(Theme.accent()).bold())));
- lines.add(Line.from(Span.raw("")));
- lines.add(Line.from(
- Span.styled(" URI: ", Theme.muted()),
- Span.raw(topoNode.from != null ? topoNode.from : "")));
- if (topoNode.description != null &&
!topoNode.description.isBlank()) {
- lines.add(Line.from(
- Span.styled(" Path: ", Theme.muted()),
- Span.raw(topoNode.description)));
- }
- if (!isBridge) {
- String connectedRoute =
diagram.getConnectedRouteId(routeId);
- if (connectedRoute != null) {
- lines.add(Line.from(Span.raw("")));
- lines.add(Line.from(
- Span.styled(isInbound ? " To route: " : " From
route: ", Theme.muted()),
- Span.styled(connectedRoute,
Style.EMPTY.fg(Theme.baseFg()))));
- }
- }
- if (topoNode.exchangesTotal > 0 || topoNode.exchangesFailed >
0) {
- lines.add(Line.from(Span.raw("")));
- lines.add(Line.from(
- Span.styled(" Total: ", Theme.muted()),
-
Span.raw(String.valueOf(topoNode.exchangesTotal))));
- if (topoNode.exchangesFailed > 0) {
- lines.add(Line.from(
- Span.styled(" Failed: ", Theme.muted()),
-
Span.styled(String.valueOf(topoNode.exchangesFailed),
- Theme.error().bold())));
- }
- }
- } else {
- lines.add(Line.from(
- Span.styled(" " + routeId,
Style.EMPTY.fg(Theme.accent()).bold())));
- lines.add(Line.from(
- Span.styled(" (external endpoint)",
Style.EMPTY.dim())));
- }
+ addExternalEndpoint(lines, routeId);
}
Paragraph paragraph = Paragraph.builder()
@@ -426,6 +389,56 @@ final class DiagramDetailSupport {
frame.renderWidget(paragraph, area);
}
+ /**
+ * A system outside the integration, selected in the topology: where it
is, the route it is linked to, its counts.
+ */
+ private void addExternalEndpoint(List<Line> lines, String routeId) {
+ // External endpoint — show topology node data
+ var topoNode = diagram.getSelectedTopologyNode();
+ if (topoNode != null) {
+ boolean isInbound = "external-in".equals(topoNode.nodeType);
+ boolean isBridge = "external".equals(topoNode.nodeType);
+ String label = isBridge ? " External" : isInbound ? " Inbound" : "
Outbound";
+ lines.add(Line.from(
+ Span.styled(label,
Style.EMPTY.fg(Theme.accent()).bold())));
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" URI: ", Theme.muted()),
+ Span.raw(topoNode.from != null ? topoNode.from : "")));
+ if (topoNode.description != null &&
!topoNode.description.isBlank()) {
+ lines.add(Line.from(
+ Span.styled(" Path: ", Theme.muted()),
+ Span.raw(topoNode.description)));
+ }
+ if (!isBridge) {
+ String connectedRoute = diagram.getConnectedRouteId(routeId);
+ if (connectedRoute != null) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(isInbound ? " To route: " : " From
route: ", Theme.muted()),
+ Span.styled(connectedRoute,
Style.EMPTY.fg(Theme.baseFg()))));
+ }
+ }
+ if (topoNode.exchangesTotal > 0 || topoNode.exchangesFailed > 0) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" Total: ", Theme.muted()),
+ Span.raw(String.valueOf(topoNode.exchangesTotal))));
+ if (topoNode.exchangesFailed > 0) {
+ lines.add(Line.from(
+ Span.styled(" Failed: ", Theme.muted()),
+
Span.styled(String.valueOf(topoNode.exchangesFailed),
+ Theme.error().bold())));
+ }
+ }
+ } else {
+ lines.add(Line.from(
+ Span.styled(" " + routeId,
Style.EMPTY.fg(Theme.accent()).bold())));
+ lines.add(Line.from(
+ Span.styled(" (external endpoint)", Style.EMPTY.dim())));
+ }
+ }
+
void renderEipInfoPanel(Frame frame, Rect area, String drillDownRouteId) {
List<Line> lines = new ArrayList<>();
var selected = diagram.getSelectedEipNodeBox();
@@ -497,82 +510,11 @@ final class DiagramDetailSupport {
Span.styled(arrow, Theme.muted())));
}
+ if (ln.treeNode != null) {
+ addBodyTypes(lines, ln.treeNode.info, area.width() - 3);
+ }
if (ln.treeNode != null && ln.treeNode.info.stat != null) {
- var stat = ln.treeNode.info.stat;
- lines.add(Line.from(Span.raw("")));
- int w = numWidth(stat.exchangesTotal, stat.exchangesFailed,
stat.exchangesInflight);
- lines.add(Line.from(
- Span.styled(" Total: ", Theme.muted()),
- Span.raw(String.format("%" + w + "d",
stat.exchangesTotal))));
- Style failStyle = stat.exchangesFailed > 0
- ? Theme.error().bold() : Style.EMPTY;
- lines.add(Line.from(
- Span.styled(" Failed: ", Theme.muted()),
- Span.styled(String.format("%" + w + "d",
stat.exchangesFailed), failStyle)));
- lines.add(Line.from(
- Span.styled(" Inflight: ", Theme.muted()),
- Span.raw(String.format("%" + w + "d",
stat.exchangesInflight))));
- if (stat.exchangesThroughput != null &&
!stat.exchangesThroughput.isEmpty()) {
- String tpUnit = ctx.ratePerMinute ? " msg/m" : " msg/s";
- String tpValue = ctx.ratePerMinute
- ?
TuiHelper.throughputPerMinute(stat.exchangesThroughput)
- : stat.exchangesThroughput;
- lines.add(Line.from(
- Span.styled(" Rate: ", Theme.muted()),
- Span.raw(tpValue),
- Span.styled(tpUnit, Theme.muted())));
- }
-
- if (stat.exchangesTotal > 0) {
- lines.add(Line.from(Span.raw("")));
- lines.add(Line.from(
- Span.styled(" Mean: ", Theme.muted()),
-
Span.raw(formatDurationMs(stat.meanProcessingTime))));
- lines.add(Line.from(
- Span.styled(" Max: ", Theme.muted()),
-
Span.raw(formatDurationMs(stat.maxProcessingTime))));
- lines.add(Line.from(
- Span.styled(" Min: ", Theme.muted()),
-
Span.raw(formatDurationMs(stat.minProcessingTime))));
- lines.add(Line.from(
- Span.styled(" Last: ", Theme.muted()),
-
Span.raw(formatDurationMs(stat.lastProcessingTime))));
- if (stat.p50ProcessingTime >= 0) {
- lines.add(Line.from(Span.raw("")));
- lines.add(Line.from(
- Span.styled(" p50: ", Theme.muted()),
-
Span.raw(formatDurationMs(stat.p50ProcessingTime))));
- lines.add(Line.from(
- Span.styled(" p95: ", Theme.muted()),
-
Span.raw(formatDurationMs(stat.p95ProcessingTime))));
- lines.add(Line.from(
- Span.styled(" p99: ", Theme.muted()),
-
Span.raw(formatDurationMs(stat.p99ProcessingTime))));
- lines.add(buildPercentileBarLine(
- stat.p50ProcessingTime, stat.p95ProcessingTime,
- stat.p99ProcessingTime, area.width() - 3));
- }
-
- if (stat.lastCompletedExchangeTimestamp > 0 ||
stat.lastFailedExchangeTimestamp > 0) {
- long now = System.currentTimeMillis();
- lines.add(Line.from(Span.raw("")));
- lines.add(Line.from(
- Span.styled(" Since last:", Theme.muted())));
- if (stat.lastCompletedExchangeTimestamp > 0) {
- long ago = now -
stat.lastCompletedExchangeTimestamp;
- lines.add(Line.from(
- Span.styled(" ok: ", Theme.muted()),
- Span.raw(TimeUtils.printDuration(ago,
false))));
- }
- if (stat.lastFailedExchangeTimestamp > 0) {
- long ago = now - stat.lastFailedExchangeTimestamp;
- lines.add(Line.from(
- Span.styled(" fail: ", Theme.muted()),
- Span.styled(TimeUtils.printDuration(ago,
false),
- Theme.error())));
- }
- }
- }
+ addStepStatistics(lines, ln.treeNode.info.stat, area.width() -
3);
}
} else {
lines.add(Line.from(Span.styled(" (no node selected)",
Style.EMPTY.dim())));
@@ -586,6 +528,111 @@ final class DiagramDetailSupport {
frame.renderWidget(paragraph, area);
}
+ /**
+ * The body types the step has been reached with, across the messages
(CAMEL-25750), one per line, with their kind;
+ * a step reached with bodies of more than one kind needs attention. The
diagram shows them cut, and with y only.
+ */
+ static void addBodyTypes(List<Line> lines,
RouteDiagramLayoutEngine.NodeInfo info, int width) {
+ if (info.bodyTypes.isEmpty()) {
+ return;
+ }
+ boolean mixed = BodyTypes.mixed(info.bodyKinds);
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(Span.styled(
+ mixed || info.bodyKinds.isEmpty() ? " Body types:" : " Body
types (" + info.bodyKinds.get(0) + "):",
+ Theme.muted())));
+ for (String type : info.bodyTypes) {
+ lines.add(Line.from(Span.raw(" " +
truncate(BodyTypes.simpleName(type), Math.max(4, width - 2)))));
+ }
+ if (mixed) {
+ List<String> warning = wrapWords(
+ BodyTypes.kinds(info.bodyKinds) + ": the body here depends
on the path a message takes",
+ Math.max(10, width - 2));
+ for (int i = 0; i < warning.size(); i++) {
+ lines.add(Line.from(Span.styled((i == 0 ? " " + BodyTypes.MARK
+ " " : " ") + warning.get(i),
+ Theme.warning())));
+ }
+ }
+ }
+
+ /** The statistics of a step: its counts, its rate, its times, and how
long since its last message. */
+ private void addStepStatistics(List<Line> lines,
RouteDiagramLayoutEngine.StatInfo stat, int width) {
+ lines.add(Line.from(Span.raw("")));
+ int w = numWidth(stat.exchangesTotal, stat.exchangesFailed,
stat.exchangesInflight);
+ lines.add(Line.from(
+ Span.styled(" Total: ", Theme.muted()),
+ Span.raw(String.format("%" + w + "d", stat.exchangesTotal))));
+ Style failStyle = stat.exchangesFailed > 0
+ ? Theme.error().bold() : Style.EMPTY;
+ lines.add(Line.from(
+ Span.styled(" Failed: ", Theme.muted()),
+ Span.styled(String.format("%" + w + "d",
stat.exchangesFailed), failStyle)));
+ lines.add(Line.from(
+ Span.styled(" Inflight: ", Theme.muted()),
+ Span.raw(String.format("%" + w + "d",
stat.exchangesInflight))));
+ if (stat.exchangesThroughput != null &&
!stat.exchangesThroughput.isEmpty()) {
+ String tpUnit = ctx.ratePerMinute ? " msg/m" : " msg/s";
+ String tpValue = ctx.ratePerMinute
+ ? TuiHelper.throughputPerMinute(stat.exchangesThroughput)
+ : stat.exchangesThroughput;
+ lines.add(Line.from(
+ Span.styled(" Rate: ", Theme.muted()),
+ Span.raw(tpValue),
+ Span.styled(tpUnit, Theme.muted())));
+ }
+
+ if (stat.exchangesTotal > 0) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" Mean: ", Theme.muted()),
+ Span.raw(formatDurationMs(stat.meanProcessingTime))));
+ lines.add(Line.from(
+ Span.styled(" Max: ", Theme.muted()),
+ Span.raw(formatDurationMs(stat.maxProcessingTime))));
+ lines.add(Line.from(
+ Span.styled(" Min: ", Theme.muted()),
+ Span.raw(formatDurationMs(stat.minProcessingTime))));
+ lines.add(Line.from(
+ Span.styled(" Last: ", Theme.muted()),
+ Span.raw(formatDurationMs(stat.lastProcessingTime))));
+ if (stat.p50ProcessingTime >= 0) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" p50: ", Theme.muted()),
+ Span.raw(formatDurationMs(stat.p50ProcessingTime))));
+ lines.add(Line.from(
+ Span.styled(" p95: ", Theme.muted()),
+ Span.raw(formatDurationMs(stat.p95ProcessingTime))));
+ lines.add(Line.from(
+ Span.styled(" p99: ", Theme.muted()),
+ Span.raw(formatDurationMs(stat.p99ProcessingTime))));
+ lines.add(buildPercentileBarLine(
+ stat.p50ProcessingTime, stat.p95ProcessingTime,
+ stat.p99ProcessingTime, width));
+ }
+
+ if (stat.lastCompletedExchangeTimestamp > 0 ||
stat.lastFailedExchangeTimestamp > 0) {
+ long now = System.currentTimeMillis();
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" Since last:", Theme.muted())));
+ if (stat.lastCompletedExchangeTimestamp > 0) {
+ long ago = now - stat.lastCompletedExchangeTimestamp;
+ lines.add(Line.from(
+ Span.styled(" ok: ", Theme.muted()),
+ Span.raw(TimeUtils.printDuration(ago, false))));
+ }
+ if (stat.lastFailedExchangeTimestamp > 0) {
+ long ago = now - stat.lastFailedExchangeTimestamp;
+ lines.add(Line.from(
+ Span.styled(" fail: ", Theme.muted()),
+ Span.styled(TimeUtils.printDuration(ago, false),
+ Theme.error())));
+ }
+ }
+ }
+ }
+
void renderDetail(Frame frame, Rect area, IntegrationInfo info, String
drillDownRouteId) {
lastDetailArea = area;
var selected = diagram.getSelectedEipNodeBox();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLayouts.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLayouts.java
new file mode 100644
index 000000000000..a8042a40efda
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLayouts.java
@@ -0,0 +1,95 @@
+/*
+ * 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.Set;
+
+import org.apache.camel.diagram.RouteDiagramLayoutEngine;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutNode;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutResult;
+
+/**
+ * What {@link DiagramSupport} does to a layout before it draws it: the
diagram moved up to the top, and the steps a
+ * highlighted step is inside highlighted too.
+ */
+final class DiagramLayouts {
+
+ private DiagramLayouts() {
+ }
+
+ static void addParentNodes(List<RouteDiagramLayoutEngine.NodeInfo> nodes,
Set<String> nodeIds) {
+ for (int i = 0; i < nodes.size(); i++) {
+ RouteDiagramLayoutEngine.NodeInfo node = nodes.get(i);
+ if (node.id == null || nodeIds.contains(node.id)) {
+ continue;
+ }
+ boolean hasHighlightedChild = false;
+ for (int j = i + 1; j < nodes.size(); j++) {
+ RouteDiagramLayoutEngine.NodeInfo child = nodes.get(j);
+ if (child.level <= node.level) {
+ break;
+ }
+ if (child.id != null && nodeIds.contains(child.id)) {
+ hasHighlightedChild = true;
+ break;
+ }
+ }
+ if (hasHighlightedChild) {
+ nodeIds.add(node.id);
+ }
+ }
+ }
+
+ static void normalizeTopologyLayoutY(TopologyLayoutResult result) {
+ if (result.nodes.isEmpty()) {
+ return;
+ }
+ int minY = Integer.MAX_VALUE;
+ for (TopologyLayoutNode node : result.nodes) {
+ minY = Math.min(minY, node.y);
+ }
+ if (minY <= 0) {
+ return;
+ }
+ int shift = minY - 40;
+ for (TopologyLayoutNode node : result.nodes) {
+ node.y -= shift;
+ }
+ }
+
+ static void normalizeRouteLayoutY(RouteDiagramLayoutEngine.LayoutRoute lr)
{
+ int minY = Integer.MAX_VALUE;
+ for (RouteDiagramLayoutEngine.LayoutNode ln : lr.nodes) {
+ if (!"route".equals(ln.type)) {
+ minY = Math.min(minY, ln.y);
+ }
+ }
+ if (minY <= 0 || minY == Integer.MAX_VALUE) {
+ return;
+ }
+ int shift = minY - 40;
+ for (RouteDiagramLayoutEngine.LayoutNode ln : lr.nodes) {
+ ln.y -= shift;
+ if (ln.connectFromMerge) {
+ ln.mergeY -= shift;
+ }
+ }
+ lr.labelY -= shift;
+ lr.maxY -= shift;
+ }
+}
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 01d70c13f353..7866de503162 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
@@ -29,6 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
import dev.tamboui.layout.Constraint;
import dev.tamboui.layout.Layout;
@@ -63,10 +64,14 @@ import
org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutNode;
import org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutResult;
import org.apache.camel.diagram.TopologyLayoutEngine.TopologyNodeInfo;
import org.apache.camel.dsl.jbang.core.commands.ai.IntegrationSummary;
+import org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget;
import
org.apache.camel.dsl.jbang.core.commands.tui.diagram.TopologyDiagramWidget;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
+import static
org.apache.camel.dsl.jbang.core.commands.tui.DiagramLayouts.addParentNodes;
+import static
org.apache.camel.dsl.jbang.core.commands.tui.DiagramLayouts.normalizeRouteLayoutY;
+import static
org.apache.camel.dsl.jbang.core.commands.tui.DiagramLayouts.normalizeTopologyLayoutY;
import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
@@ -80,6 +85,8 @@ class DiagramSupport {
private boolean showDiagram;
private boolean topologyMode;
private boolean showDescription;
+ /** The body types each step has been reached with, beside the arrows of
the route diagram (CAMEL-25750). */
+ private boolean showBodyTypes;
private Path aiSourceDirectory;
/** The AI labels of the decision points of the routes shown, by their
node (CAMEL-25161). */
private volatile Map<String, IntegrationSummary.StepLabel> stepLabels =
Map.of();
@@ -168,6 +175,14 @@ class DiagramSupport {
this.showDescription = showDescription;
}
+ boolean isShowBodyTypes() {
+ return showBodyTypes;
+ }
+
+ void setShowBodyTypes(boolean showBodyTypes) {
+ this.showBodyTypes = showBodyTypes;
+ }
+
List<TopologyAsciiRenderer.NodeBox> getNodeBoxes() {
return nodeBoxes;
}
@@ -1195,11 +1210,11 @@ class DiagramSupport {
// ---- Route EIP node selection ----
- private
List<org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget.EipNodeBox>
eipNodeBoxes
+ private List<RouteDiagramWidget.EipNodeBox> eipNodeBoxes
= Collections.emptyList();
private int selectedEipNodeIndex = -1;
-
List<org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget.EipNodeBox>
getEipNodeBoxes() {
+ List<RouteDiagramWidget.EipNodeBox> getEipNodeBoxes() {
return eipNodeBoxes;
}
@@ -1215,7 +1230,7 @@ class DiagramSupport {
selectedEipNodeIndex = 0;
}
-
org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget.EipNodeBox
getSelectedEipNodeBox() {
+ RouteDiagramWidget.EipNodeBox getSelectedEipNodeBox() {
if (selectedEipNodeIndex >= 0 && selectedEipNodeIndex <
eipNodeBoxes.size()) {
return eipNodeBoxes.get(selectedEipNodeIndex);
}
@@ -1532,11 +1547,14 @@ class DiagramSupport {
Map<String, String> linkable =
computeLinkableEndpoints(currentRouteId);
Map<String, String> routeDescs = showDescription ?
computeRouteDescriptions() : Collections.emptyMap();
- var widget = new
org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget(
+ // built again once the scroll is clamped to its size
+ Supplier<RouteDiagramWidget> widgets = () -> new RouteDiagramWidget(
routeLayout, nw, selectedEipNodeIndex, scrollX, scrollY,
metrics, linkable,
showDescription, routeDescs)
.withErrorFrame(layoutShowErrorPaths ?
routeErrorFrames.get(currentRouteId) : null)
- .withErrorMark(errorMarkRouteIds().contains(currentRouteId));
+ .withErrorMark(errorMarkRouteIds().contains(currentRouteId))
+ .withBodyTypes(showBodyTypes).withBodyKindMarks(true);
+ var widget = widgets.get();
int totalRows = widget.getTotalRows();
int totalCols = widget.getTotalCols();
@@ -1550,11 +1568,7 @@ class DiagramSupport {
scrollY = Math.min(scrollY, maxVScroll);
scrollX = Math.min(scrollX, maxHScroll);
- var finalWidget = new
org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget(
- routeLayout, nw, selectedEipNodeIndex, scrollX, scrollY,
metrics, linkable,
- showDescription, routeDescs)
- .withErrorFrame(layoutShowErrorPaths ?
routeErrorFrames.get(currentRouteId) : null)
- .withErrorMark(errorMarkRouteIds().contains(currentRouteId));
+ var finalWidget = widgets.get();
List<Rect> vChunks = Layout.vertical()
.constraints(Constraint.fill(), Constraint.length(1))
@@ -2105,7 +2119,7 @@ class DiagramSupport {
Map<String, String> linkable = computeLinkableEndpoints(routeId);
Map<String, String> routeDescs = showDescription ?
computeRouteDescriptions() : Collections.emptyMap();
- var widget = new
org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget(
+ var widget = new RouteDiagramWidget(
routeLayout, nw, selIdx, scrollX, scrollY, false, linkable,
showDescription, routeDescs, activeHighlight, historyFailed);
@@ -2136,7 +2150,7 @@ class DiagramSupport {
}
}
- var finalWidget = new
org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget(
+ var finalWidget = new RouteDiagramWidget(
routeLayout, nw, selIdx, scrollX, scrollY, false, linkable,
showDescription, routeDescs, activeHighlight, historyFailed);
@@ -2353,67 +2367,6 @@ class DiagramSupport {
});
}
- static void addParentNodes(List<RouteDiagramLayoutEngine.NodeInfo> nodes,
Set<String> nodeIds) {
- for (int i = 0; i < nodes.size(); i++) {
- RouteDiagramLayoutEngine.NodeInfo node = nodes.get(i);
- if (node.id == null || nodeIds.contains(node.id)) {
- continue;
- }
- boolean hasHighlightedChild = false;
- for (int j = i + 1; j < nodes.size(); j++) {
- RouteDiagramLayoutEngine.NodeInfo child = nodes.get(j);
- if (child.level <= node.level) {
- break;
- }
- if (child.id != null && nodeIds.contains(child.id)) {
- hasHighlightedChild = true;
- break;
- }
- }
- if (hasHighlightedChild) {
- nodeIds.add(node.id);
- }
- }
- }
-
- private static void normalizeTopologyLayoutY(TopologyLayoutResult result) {
- if (result.nodes.isEmpty()) {
- return;
- }
- int minY = Integer.MAX_VALUE;
- for (TopologyLayoutNode node : result.nodes) {
- minY = Math.min(minY, node.y);
- }
- if (minY <= 0) {
- return;
- }
- int shift = minY - 40;
- for (TopologyLayoutNode node : result.nodes) {
- node.y -= shift;
- }
- }
-
- private static void
normalizeRouteLayoutY(RouteDiagramLayoutEngine.LayoutRoute lr) {
- int minY = Integer.MAX_VALUE;
- for (RouteDiagramLayoutEngine.LayoutNode ln : lr.nodes) {
- if (!"route".equals(ln.type)) {
- minY = Math.min(minY, ln.y);
- }
- }
- if (minY <= 0 || minY == Integer.MAX_VALUE) {
- return;
- }
- int shift = minY - 40;
- for (RouteDiagramLayoutEngine.LayoutNode ln : lr.nodes) {
- ln.y -= shift;
- if (ln.connectFromMerge) {
- ln.mergeY -= shift;
- }
- }
- lr.labelY -= shift;
- lr.maxY -= shift;
- }
-
/**
* Finds the index of the EIP node box whose source line is closest to the
given line, preferring the closest node
* at or before the line. Returns -1 when there are no boxes with line
information.
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index e20050189142..3856d58299f3 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -512,6 +512,7 @@ class DiagramTab extends AbstractDiagramTab {
}
case ROUTE -> List.of(view, new SubViewBar.Toggle("x", "errors",
errorsToggle()),
new SubViewBar.Toggle("m", "metrics", metrics),
+ new SubViewBar.Toggle("y", "types",
diagram.isShowBodyTypes() ? "on" : "off"),
new SubViewBar.Toggle("d", "detail", detailMode ? "on" :
"off"));
};
}
@@ -822,6 +823,12 @@ class DiagramTab extends AbstractDiagramTab {
if (linkedRoute != null) {
node.put("linkedRoute", linkedRoute);
}
+ var treeNode = eipBox.layoutNode().treeNode;
+ if (treeNode != null && !treeNode.info.bodyTypes.isEmpty()) {
+ // the body types the step has been reached with; more
than one kind is a step that needs attention
+ node.put("bodyTypes", new
JsonArray(treeNode.info.bodyTypes));
+ node.put("bodyKinds", new
JsonArray(treeNode.info.bodyKinds));
+ }
result.put("selectedNode", node);
}
}
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 e556d6b4bb09..c564f37a957d 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
@@ -164,7 +164,8 @@ final class DiagramTopology {
/**
* 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.
+ * the others those of their step, with the body types the step has been
reached with so far (CAMEL-25750): the
+ * structure has those it was asked with.
*/
private static void addStepStatistics(JsonObject route, RouteInfo info) {
Collection<?> code = route.getCollection("code");
@@ -172,10 +173,14 @@ final class DiagramTopology {
return;
}
Map<String, Map<String, ?>> steps = new HashMap<>();
+ Map<String, ProcessorInfo> bodies = new HashMap<>();
for (ProcessorInfo p : info.processors) {
if (p.id != null && p.statistics != null) {
steps.put(p.id, p.statistics);
}
+ if (p.id != null && !p.bodyTypes.isEmpty()) {
+ bodies.put(p.id, p);
+ }
}
JsonArray lines = new JsonArray();
int index = 0;
@@ -190,6 +195,11 @@ final class DiagramTopology {
if (statistics != null) {
line.put("statistics", statistics instanceof JsonObject jo
? jo : new JsonObject(statistics));
}
+ ProcessorInfo body = index > 1 ?
bodies.get(line.getString("id")) : null;
+ if (body != null) {
+ line.put("bodyTypes", new JsonArray(body.bodyTypes));
+ line.put("bodyKinds", new JsonArray(body.bodyKinds));
+ }
lines.add(line);
} else {
lines.add(o);
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 38763a71ae0e..d193eb03c797 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,7 @@
*/
package org.apache.camel.dsl.jbang.core.commands.tui;
+import java.util.List;
import java.util.Map;
class ProcessorInfo {
@@ -43,4 +44,8 @@ class ProcessorInfo {
String uri;
/** The statistics as the status file has them, for the route diagram
(DiagramTopology). */
Map<String, ?> statistics;
+ /** The body types the step has been reached with, across the messages, in
the order first seen (CAMEL-25750). */
+ List<String> bodyTypes = List.of();
+ /** The kinds of those bodies: null, payload, data or collection. More
than one needs attention. */
+ List<String> bodyKinds = List.of();
}
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 0cea43e82b58..cbb595c5f1a0 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
@@ -51,5 +51,9 @@ class RouteInfo {
String sinceLastFailed;
/** The statistics as the status file has them, for the route diagram
(DiagramTopology). */
Map<String, ?> statistics;
+ /** The steps reached with more than one body type, across the messages
(CAMEL-25750). */
+ int variedBodyTypes;
+ /** The steps reached with bodies of more than one kind, which need
attention (CAMEL-25750). */
+ int mixedBodyKinds;
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/RoutesTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 15bcaa96969a..b08eb9a78d8b 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
@@ -49,6 +49,9 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
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;
+
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" };
@@ -535,8 +538,18 @@ class RoutesTab extends AbstractDiagramTab {
totalColWidth = Math.max(totalColWidth, tw + since + 1);
failColWidth = Math.max(failColWidth, fw + failSince + 1);
}
+ // the steps reached with more than one body type (CAMEL-25750): a
column only when a route has some
+ boolean bodyTypes = sortedRoutes.stream().anyMatch(r ->
r.variedBodyTypes > 0);
+ int bodyColWidth = 0;
+ if (bodyTypes) {
+ bodyColWidth = CharWidth.of("BODY TYPES");
+ for (RouteInfo route : sortedRoutes) {
+ bodyColWidth = Math.max(bodyColWidth,
CharWidth.of(bodyTypesLabel(route)));
+ }
+ }
// FROM gets the rest of the width: the borders, ">> ", 7 gaps and the
other columns
- int fromWidth = Math.max(0, areaWidth - 2 - 3 - 7 - (24 + 10 + 10 +
totalColWidth + failColWidth + 20 + 12));
+ int fromWidth = Math.max(0, areaWidth - 2 - 3 - 7 - (24 + 10 + 10 +
totalColWidth + failColWidth + 20 + 12)
+ - (bodyTypes ? bodyColWidth + 1 : 0));
List<Row> routeRows = new ArrayList<>();
Map<String, String> aiDescriptions = aiDescriptions();
@@ -569,15 +582,21 @@ class RoutesTab extends AbstractDiagramTab {
Span.styled(" (" + route.sinceLastFailed + ")",
Theme.muted()))
: Line.from(Span.styled(String.format("%" + fw + "d",
route.failed), failStyle));
- routeRows.add(Row.from(
- Cell.from(Span.styled(TuiHelper.truncate(route.routeId,
24), Style.EMPTY.fg(Theme.accent()))),
- routeFromCell(route, aiDescriptions, fromWidth),
- Cell.from(Span.styled(route.state != null ? route.state :
"", stateStyle)),
- rightCell(formatThroughput(route.throughput), 8),
- Cell.from(totalCell),
- Cell.from(failCell),
- rightCell(timingCol, 20),
- Cell.from(buildPercentileBarLine(route.p50Time,
route.p95Time, route.p99Time, 10))));
+ List<Cell> cells = new ArrayList<>(
+ List.of(
+
Cell.from(Span.styled(TuiHelper.truncate(route.routeId, 24),
Style.EMPTY.fg(Theme.accent()))),
+ routeFromCell(route, aiDescriptions, fromWidth),
+ Cell.from(Span.styled(route.state != null ?
route.state : "", stateStyle)),
+ rightCell(formatThroughput(route.throughput), 8),
+ Cell.from(totalCell),
+ Cell.from(failCell)));
+ if (bodyTypes) {
+ cells.add(Cell.from(Span.styled(bodyTypesLabel(route),
+ route.mixedBodyKinds > 0 ? Theme.warning() :
Theme.muted())));
+ }
+ cells.add(rightCell(timingCol, 20));
+ cells.add(Cell.from(buildPercentileBarLine(route.p50Time,
route.p95Time, route.p99Time, 10)));
+ routeRows.add(Row.from(cells));
}
IntegrationInfo selDef = ctx.findSelectedIntegration();
@@ -591,40 +610,54 @@ class RoutesTab extends AbstractDiagramTab {
totalTimingCol = formatDurationMs(selDef.minTime) + "/" +
formatDurationMs(selDef.maxTime) + "/"
+ formatDurationMs(selDef.meanTime);
}
- routeRows.add(Row.from(
- Cell.from(Span.styled("GLOBAL", ts)),
- Cell.from(""),
- Cell.from(""),
- rightCell(formatThroughput(selDef.throughput), 8, ts),
- Cell.from(Span.styled(String.format("%" + tw + "d",
selDef.exchangesTotal), ts)),
- Cell.from(Span.styled(String.format("%" + fw + "d",
selDef.failed),
- selDef.failed > 0 ? Theme.error().bold() : ts)),
- rightCell(totalTimingCol, 20, ts),
- Cell.from(buildPercentileBarLine(selDef.p50Time,
selDef.p95Time, selDef.p99Time, 10))));
+ List<Cell> cells = new ArrayList<>(
+ List.of(
+ Cell.from(Span.styled("GLOBAL", ts)),
+ Cell.from(""),
+ Cell.from(""),
+ rightCell(formatThroughput(selDef.throughput), 8,
ts),
+ Cell.from(Span.styled(String.format("%" + tw +
"d", selDef.exchangesTotal), ts)),
+ Cell.from(Span.styled(String.format("%" + fw +
"d", selDef.failed),
+ selDef.failed > 0 ? Theme.error().bold() :
ts))));
+ if (bodyTypes) {
+ cells.add(Cell.from(""));
+ }
+ cells.add(rightCell(totalTimingCol, 20, ts));
+ cells.add(Cell.from(buildPercentileBarLine(selDef.p50Time,
selDef.p95Time, selDef.p99Time, 10)));
+ routeRows.add(Row.from(cells));
}
String timingHeader = hasPercentiles ? "P50/P95/P99" : "MIN/MAX/MEAN";
- return Table.builder()
- .rows(routeRows)
- .header(Row.from(
+ List<Cell> header = new ArrayList<>(
+ List.of(
Cell.from(Span.styled(routeSortLabel("ROUTE", "name"),
routeSortStyle("name"))),
Cell.from(Span.styled(routeSortLabel("FROM", "from"),
routeSortStyle("from"))),
Cell.from(Span.styled(routeSortLabel("STATUS",
"status"), routeSortStyle("status"))),
rightCell(ctx.ratePerMinute ? "MSG/M" : "MSG/S", 8,
Style.EMPTY.bold()),
centerCell(routeSortLabel("TOTAL", "total"), 14,
routeSortStyle("total")),
- centerCell(routeSortLabel("FAIL", "failed"), 14,
routeSortStyle("failed")),
- rightCell(timingHeader, 20, Style.EMPTY.bold()),
- Cell.from("")))
- .widths(
+ centerCell(routeSortLabel("FAIL", "failed"), 14,
routeSortStyle("failed"))));
+ List<Constraint> widths = new ArrayList<>(
+ List.of(
Constraint.length(24),
Constraint.fill(),
Constraint.length(10),
Constraint.length(10),
Constraint.length(totalColWidth),
- Constraint.length(failColWidth),
- Constraint.length(20),
- Constraint.length(12))
+ Constraint.length(failColWidth)));
+ if (bodyTypes) {
+ header.add(Cell.from(Span.styled("BODY TYPES",
Style.EMPTY.bold())));
+ widths.add(Constraint.length(bodyColWidth));
+ }
+ header.add(rightCell(timingHeader, 20, Style.EMPTY.bold()));
+ header.add(Cell.from(""));
+ widths.add(Constraint.length(20));
+ widths.add(Constraint.length(12));
+
+ return Table.builder()
+ .rows(routeRows)
+ .header(Row.from(header))
+ .widths(widths.toArray(Constraint[]::new))
.highlightStyle(Theme.selectionBg())
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
@@ -632,6 +665,21 @@ class RoutesTab extends AbstractDiagramTab {
.build();
}
+ /**
+ * The steps of a route reached with more than one body type, across the
messages (CAMEL-25750): "⚠ 1 mixed · 2
+ * varied" when some are reached with bodies of more than one kind, which
need attention; empty when none varied.
+ */
+ static String bodyTypesLabel(RouteInfo route) {
+ if (route.variedBodyTypes <= 0) {
+ return "";
+ }
+ if (route.mixedBodyKinds <= 0) {
+ return route.variedBodyTypes + " varied";
+ }
+ return TuiIcons.WARN + " " + route.mixedBodyKinds + " mixed"
+ + (route.variedBodyTypes > route.mixedBodyKinds ? " · " +
route.variedBodyTypes + " varied" : "");
+ }
+
@Override
public SubViewBar.Spec subViewBar() {
if (ctx.findSelectedIntegration() == null || sourceViewer.isVisible())
{
@@ -654,6 +702,7 @@ class RoutesTab extends AbstractDiagramTab {
"e", "external",
EXTERNAL_LABELS[externalMode].trim().replace("[",
"").replace("]", "")));
} else if (!diagram.getEipNodeBoxes().isEmpty()) {
+ toggles.add(new SubViewBar.Toggle("y", "types",
diagram.isShowBodyTypes() ? "on" : "off"));
toggles.add(new SubViewBar.Toggle("d", "detail", detailMode ?
"on" : "off"));
}
toggles.add(new SubViewBar.Toggle("n", "description",
diagram.isShowDescription() ? "on" : "off"));
@@ -880,98 +929,139 @@ class RoutesTab extends AbstractDiagramTab {
.build())
.build();
} else {
- boolean hasProcPercentiles = route.p50Time >= 0
- || route.processors.stream().anyMatch(p -> p.p50Time >= 0);
-
- List<Row> rows = new ArrayList<>();
+ table = buildProcessorTable(area, route);
+ }
- // Synthetic top row representing the route itself
- Style routeStyle = route.failed > 0 ? Theme.error() :
Style.EMPTY.fg(Theme.accent());
- String routeTimingCol;
- if (hasProcPercentiles && route.p50Time >= 0) {
- routeTimingCol = formatDurationMs(route.p50Time) + "/" +
formatDurationMs(route.p95Time) + "/"
- + formatDurationMs(route.p99Time);
- } else if (route.total > 0) {
- routeTimingCol = formatDurationMs(route.minTime) + "/" +
formatDurationMs(route.maxTime) + "/"
- + formatDurationMs(route.meanTime);
- } 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));
- rows.add(Row.from(
- 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),
- rightCell(routeTimingCol, 20),
- Cell.from(buildPercentileBarLine(route.p50Time,
route.p95Time, route.p99Time, 10))));
+ frame.renderStatefulWidget(table, area, processorTableState);
+ int processorRowCount = routeTopMode ? route.processors.size() :
route.processors.size() + 1;
+ renderTableScrollbar(frame, area, table, processorTableState,
processorTableScrollState,
+ processorRowCount);
+ }
+ /**
+ * The processors of a route in the default mode (counters, timing), with
the body types each step has been reached
+ * with, when the message history recorded them (CAMEL-25750).
+ */
+ private Table buildProcessorTable(Rect area, RouteInfo route) {
+ boolean hasProcPercentiles = route.p50Time >= 0
+ || route.processors.stream().anyMatch(p -> p.p50Time >= 0);
+ // a column only when a step has body types, as wide as the longest up
to a limit: the info panel of the
+ // diagram has them all; a space before them sets them apart from the
inflight count
+ boolean bodyTypes = route.processors.stream().anyMatch(p ->
!p.bodyTypes.isEmpty());
+ int bodyColWidth = 0;
+ if (bodyTypes) {
+ bodyColWidth = CharWidth.of("BODY TYPES");
for (ProcessorInfo proc : route.processors) {
- String indent = " ".repeat(proc.level);
- Style nameStyle = proc.failed > 0 ? Theme.error() :
Style.EMPTY.fg(Theme.accent());
+ bodyColWidth = Math.max(bodyColWidth,
CharWidth.of(BodyTypes.shortLabel(proc.bodyTypes, proc.bodyKinds)));
+ }
+ bodyColWidth = Math.min(bodyColWidth, MAX_BODY_TYPES_WIDTH) + 1;
+ }
- String procTimingCol;
- if (hasProcPercentiles && proc.p50Time >= 0) {
- procTimingCol = formatDurationMs(proc.p50Time) + "/" +
formatDurationMs(proc.p95Time) + "/"
- + formatDurationMs(proc.p99Time);
- } else if (proc.total > 0) {
- procTimingCol = formatDurationMs(proc.minTime) + "/" +
formatDurationMs(proc.maxTime) + "/"
- + formatDurationMs(proc.meanTime);
- } else {
- procTimingCol = "";
- }
+ List<Row> rows = new ArrayList<>();
+
+ // Synthetic top row representing the route itself
+ Style routeStyle = route.failed > 0 ? Theme.error() :
Style.EMPTY.fg(Theme.accent());
+ String routeTimingCol;
+ if (hasProcPercentiles && route.p50Time >= 0) {
+ routeTimingCol = formatDurationMs(route.p50Time) + "/" +
formatDurationMs(route.p95Time) + "/"
+ + formatDurationMs(route.p99Time);
+ } else if (route.total > 0) {
+ routeTimingCol = formatDurationMs(route.minTime) + "/" +
formatDurationMs(route.maxTime) + "/"
+ + formatDurationMs(route.meanTime);
+ } 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)));
+ if (bodyTypes) {
+ routeCells.add(Cell.from(""));
+ }
+ routeCells.add(rightCell(routeTimingCol, 20));
+ routeCells.add(Cell.from(buildPercentileBarLine(route.p50Time,
route.p95Time, route.p99Time, 10)));
+ 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());
+
+ String procTimingCol;
+ if (hasProcPercentiles && proc.p50Time >= 0) {
+ procTimingCol = formatDurationMs(proc.p50Time) + "/" +
formatDurationMs(proc.p95Time) + "/"
+ + formatDurationMs(proc.p99Time);
+ } else if (proc.total > 0) {
+ procTimingCol = formatDurationMs(proc.minTime) + "/" +
formatDurationMs(proc.maxTime) + "/"
+ + formatDurationMs(proc.meanTime);
+ } else {
+ procTimingCol = "";
+ }
- rows.add(Row.from(
- 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),
- rightCell(procTimingCol, 20),
- Cell.from(buildPercentileBarLine(proc.p50Time,
proc.p95Time, proc.p99Time, 10))));
+ 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)));
+ if (bodyTypes) {
+ cells.add(Cell.from(Span.styled(
+ " " +
TuiHelper.truncate(BodyTypes.shortLabel(proc.bodyTypes, proc.bodyKinds),
bodyColWidth - 1),
+ BodyTypes.style(proc.bodyKinds))));
}
- String procTimingHeader = hasProcPercentiles ? "P50/P95/P99" :
"MIN/MAX/MEAN";
+ cells.add(rightCell(procTimingCol, 20));
+ cells.add(Cell.from(buildPercentileBarLine(proc.p50Time,
proc.p95Time, proc.p99Time, 10)));
+ rows.add(Row.from(cells));
+ }
+ String procTimingHeader = hasProcPercentiles ? "P50/P95/P99" :
"MIN/MAX/MEAN";
- table = Table.builder()
- .rows(rows)
- .header(Row.from(
- 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()),
- rightCell(procTimingHeader, 20,
Style.EMPTY.bold()),
- Cell.from("")))
- .widths(
- Constraint.length(20),
- Constraint.fill(),
- Constraint.length(10),
- Constraint.length(8),
- Constraint.length(6),
- Constraint.length(8),
- Constraint.length(20),
- Constraint.length(12))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Processors [" + route.routeId + "] ")
- .build())
- .build();
+ 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)));
+ if (bodyTypes) {
+ header.add(Cell.from(Span.styled(" BODY TYPES",
Style.EMPTY.bold())));
+ widths.add(Constraint.length(bodyColWidth));
}
+ header.add(rightCell(procTimingHeader, 20, Style.EMPTY.bold()));
+ header.add(Cell.from(""));
+ widths.add(Constraint.length(20));
+ widths.add(Constraint.length(12));
- frame.renderStatefulWidget(table, area, processorTableState);
- int processorRowCount = routeTopMode ? route.processors.size() :
route.processors.size() + 1;
- renderTableScrollbar(frame, area, table, processorTableState,
processorTableScrollState,
- processorRowCount);
+ return Table.builder()
+ .rows(rows)
+ .header(Row.from(header))
+ .widths(widths.toArray(Constraint[]::new))
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Processors [" + route.routeId + "] ")
+ .build())
+ .build();
}
private String formatThroughput(String throughput) {
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 bd714eeee0fd..e523c2bd131f 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
@@ -244,7 +244,7 @@ final class StatusParser {
/**
* Routes and their processors with statistics.
*/
- private static void parseRoutes(IntegrationInfo info, JsonObject root) {
+ static void parseRoutes(IntegrationInfo info, JsonObject root) {
JsonArray routes = (JsonArray) root.get("routes");
if (routes != null) {
for (Object r : routes) {
@@ -258,6 +258,9 @@ final class StatusParser {
ri.state = rj.getString("state");
ri.supportsSuspension =
rj.getBooleanOrDefault("supportsSuspension", false);
ri.uptime = rj.getString("uptime");
+ // the steps reached with more than one body type, and with
more than one kind (CAMEL-25750)
+ ri.variedBodyTypes = rj.getIntegerOrDefault("variedBodyTypes",
0);
+ ri.mixedBodyKinds = rj.getIntegerOrDefault("mixedBodyKinds",
0);
Map<String, ?> rs = rj.getMap("statistics");
ri.statistics = rs;
@@ -303,6 +306,9 @@ final class StatusParser {
pi.source = pj.getString("source");
pi.level = pj.getIntegerOrDefault("level", 0);
pi.uri = pj.getString("uri");
+ // the body types the step has been reached with, and
their kinds (CAMEL-25750)
+ pi.bodyTypes = stringList(pj.get("bodyTypes"));
+ pi.bodyKinds = stringList(pj.get("bodyKinds"));
Map<String, ?> ps = pj.getMap("statistics");
pi.statistics = ps;
@@ -1579,6 +1585,18 @@ final class StatusParser {
return TuiHelper.objToLong(o);
}
+ /** The strings of a JSON array, or an empty list. */
+ static List<String> stringList(Object o) {
+ if (!(o instanceof List<?> list) || list.isEmpty()) {
+ return List.of();
+ }
+ List<String> answer = new ArrayList<>(list.size());
+ for (Object item : list) {
+ answer.add(String.valueOf(item));
+ }
+ return answer;
+ }
+
private static void parseVaultSecrets(
JsonObject vaults, String key, String vaultName, IntegrationInfo
info, boolean hasRegion) {
JsonObject vo = (JsonObject) vaults.get(key);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java
index ab4efc2867b5..b3b3d01a6611 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui.diagram;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -36,6 +37,7 @@ import
org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
import org.apache.camel.diagram.RouteDiagramLayoutEngine.StatInfo;
import org.apache.camel.diagram.RouteDiagramLayoutEngine.TreeNode;
import org.apache.camel.diagram.RouteErrorFrame;
+import org.apache.camel.dsl.jbang.core.commands.tui.BodyTypes;
import org.apache.camel.dsl.jbang.core.commands.tui.BusinessEndpointLabels;
import org.apache.camel.dsl.jbang.core.commands.tui.Theme;
@@ -50,6 +52,10 @@ public class RouteDiagramWidget implements Widget {
private static final int MIN_BOX_WIDTH = 16;
private static final int X_DIVISOR = 15;
private static final int MAX_WRAP_LINES = 3;
+ /** The widest the body types beside an arrow are: the Info panel has them
all. */
+ private static final int MAX_BODY_TYPES_WIDTH = 48;
+ /** The room a count at the head of an arrow takes before the body types,
at most. */
+ private static final int COUNT_WIDTH = 10;
// Dashed scope box characters
private static final char SCOPE_H = '╌';
@@ -151,6 +157,8 @@ public class RouteDiagramWidget implements Widget {
@Override
public void render(Rect area, Buffer buffer) {
nodeBoxes.clear();
+ arrowCells.clear();
+ arrowEnds.clear();
if (errorFrame != null) {
drawErrorFrame(buffer, area);
@@ -189,6 +197,10 @@ public class RouteDiagramWidget implements Widget {
drawNode(buffer, area, ln);
}
}
+ // the body types beside the arrows, where the boxes, the arrows and
the markers leave room for them
+ for (ArrowEnd end : arrowEnds) {
+ drawBodyTypes(buffer, area, end);
+ }
// the markers beside the boxes last, so a long one stops at a box
instead of drawing over it
for (Marker m : markers) {
writeMarker(buffer, area, m);
@@ -261,6 +273,94 @@ public class RouteDiagramWidget implements Widget {
return this;
}
+ private boolean bodyTypes;
+ private boolean bodyKindMarks;
+ /** The cells of the arrows and their counts, which the body types beside
an arrow stop at. */
+ private final Set<Long> arrowCells = new HashSet<>();
+ private final List<ArrowEnd> arrowEnds = new ArrayList<>();
+
+ /** Where an arrow goes into a step, for the body types beside it: its
columns and rows, the width of its count. */
+ private record ArrowEnd(LayoutNode to, int fromCx, int fromRow, int toCx,
int toTop, int countWidth) {
+ }
+
+ /**
+ * The body types each step has been reached with, across the messages
(CAMEL-25750), beside the arrow into it: as
+ * much of them as there is room for, as the arrows of a branch are side
by side; the Info panel has them all.
+ */
+ public RouteDiagramWidget withBodyTypes(boolean show) {
+ this.bodyTypes = show;
+ return this;
+ }
+
+ /**
+ * A ⚠ on the top border of a step reached with bodies of more than one
kind (CAMEL-25750), which needs attention,
+ * whether the body types are shown or not.
+ */
+ public RouteDiagramWidget withBodyKindMarks(boolean marks) {
+ this.bodyKindMarks = marks;
+ return this;
+ }
+
+ private void addArrowEnd(LayoutNode to, int fromCx, int fromRow, int toCx,
int toTop, int countWidth) {
+ if (bodyTypes && to.treeNode != null &&
!to.treeNode.info.bodyTypes.isEmpty()) {
+ arrowEnds.add(new ArrowEnd(to, fromCx, fromRow, toCx, toTop,
countWidth));
+ }
+ }
+
+ /**
+ * The body types beside the arrow into a step: on its shaft above the
head, or after the count at the head where
+ * the arrow bends right above it (a branch); cut to the room before the
next box, arrow or marker.
+ */
+ private void drawBodyTypes(Buffer buffer, Rect area, ArrowEnd end) {
+ int shaftTop = end.fromCx() == end.toCx() ? end.fromRow() :
end.fromRow() + (end.toTop() - end.fromRow()) / 2 + 1;
+ int row = end.toTop() - 2;
+ int col = end.toCx() + 2;
+ if (row < shaftTop) {
+ row = end.toTop() - 1;
+ col += end.countWidth() > 0 ? end.countWidth() + 2 : 0;
+ }
+ var info = end.to().treeNode.info;
+ String text = BodyTypes.fit(info.bodyTypes, info.bodyKinds, room(row,
col));
+ if (!text.isEmpty()) {
+ writeText(buffer, area, row, col, text,
BodyTypes.style(info.bodyKinds));
+ }
+ }
+
+ /**
+ * The columns free on the row from the column, two left blank before a
box, an arrow or a marker so the types do
+ * not read as part of it.
+ */
+ private int room(int row, int col) {
+ for (int c = col; c < col + MAX_BODY_TYPES_WIDTH; c++) {
+ if (arrowCells.contains(cell(row, c)) || inBox(row, c) ||
markerAt(row, c)) {
+ return Math.max(0, c - col - 2);
+ }
+ }
+ return MAX_BODY_TYPES_WIDTH;
+ }
+
+ private boolean markerAt(int row, int col) {
+ for (Marker m : markers) {
+ if (m.row() == row && col >= m.col() && col < m.col() +
CharWidth.of(m.text())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static long cell(int row, int col) {
+ return ((long) row << 32) | (col & 0xffffffffL);
+ }
+
+ /** A cell of an arrow or of its count: the body types beside another
arrow stop there. */
+ private void occupy(int row, int col, int width) {
+ if (bodyTypes) {
+ for (int c = col; c < col + width; c++) {
+ arrowCells.add(cell(row, c));
+ }
+ }
+ }
+
/** The mark on the top border of the from box, at its right end (the
route has error handling). */
private void drawErrorMark(Buffer buffer, Rect area) {
for (LayoutNode ln : layoutRoute.nodes) {
@@ -317,6 +417,15 @@ public class RouteDiagramWidget implements Widget {
cols = Math.max(cols, CharWidth.of(line) + 6);
}
}
+ if (bodyTypes) {
+ // the body types beside the arrows can be scrolled to: as wide as
they are before something cuts them
+ for (LayoutNode ln : layoutRoute.nodes) {
+ if (!ln.tableRow && ln.treeNode != null &&
!ln.treeNode.info.bodyTypes.isEmpty()) {
+ int label =
CharWidth.of(BodyTypes.label(ln.treeNode.info.bodyTypes,
ln.treeNode.info.bodyKinds));
+ cols = Math.max(cols, centerCol(ln) + 2 + COUNT_WIDTH +
Math.min(MAX_BODY_TYPES_WIDTH, label) + 2);
+ }
+ }
+ }
return cols;
}
@@ -388,6 +497,11 @@ public class RouteDiagramWidget implements Widget {
}
}
+ if (bodyKindMarks && node.treeNode != null &&
BodyTypes.mixed(node.treeNode.info.bodyKinds)) {
+ // bodies of more than one kind reach the step: it needs attention
+ writeText(buffer, area, row, col + boxWidth - 3, BodyTypes.MARK,
Theme.warning());
+ }
+
// Link indicator for nodes that connect to other routes
String linkedRouteId = findLinkedRouteId(node);
if (linkedRouteId != null) {
@@ -440,7 +554,7 @@ public class RouteDiagramWidget implements Widget {
&& to.id != null && highlightNodeIds.contains(to.id);
drawArrowPath(buffer, area, fromCx, fromBottom, toCx, toTop, dashed,
external, bothHighlighted);
- drawCounters(buffer, area, toCx, toTop, stat);
+ addArrowEnd(to, fromCx, fromBottom, toCx, toTop, drawCounters(buffer,
area, toCx, toTop, stat));
}
private void drawMergeArrow(Buffer buffer, Rect area, LayoutNode to) {
@@ -457,7 +571,7 @@ public class RouteDiagramWidget implements Widget {
&& to.id != null && highlightNodeIds.contains(to.id);
drawArrowPath(buffer, area, fromCx, fromRow, toCx, toTop, dashed,
false, highlighted);
- drawCounters(buffer, area, toCx, toTop, stat);
+ addArrowEnd(to, fromCx, fromRow, toCx, toTop, drawCounters(buffer,
area, toCx, toTop, stat));
}
private void drawArrowPath(
@@ -482,48 +596,59 @@ public class RouteDiagramWidget implements Widget {
if (fromCx == toCx) {
for (int r = fromRow; r < toRow - 1; r++) {
- setChar(buffer, area, r, fromCx, vChar, edgeStyle);
+ arrowChar(buffer, area, r, fromCx, vChar, edgeStyle);
}
- setChar(buffer, area, toRow - 1, toCx, ARROW, edgeStyle);
+ arrowChar(buffer, area, toRow - 1, toCx, ARROW, edgeStyle);
} else {
int midRow = fromRow + (toRow - fromRow) / 2;
for (int r = fromRow; r < midRow; r++) {
- setChar(buffer, area, r, fromCx, vChar, edgeStyle);
+ arrowChar(buffer, area, r, fromCx, vChar, edgeStyle);
}
int minC = Math.min(fromCx, toCx);
int maxC = Math.max(fromCx, toCx);
for (int c = minC; c <= maxC; c++) {
- setChar(buffer, area, midRow, c, hChar, edgeStyle);
+ arrowChar(buffer, area, midRow, c, hChar, edgeStyle);
}
- setChar(buffer, area, midRow, fromCx, T_UP, edgeStyle);
- setChar(buffer, area, midRow, toCx, T_DOWN, edgeStyle);
+ arrowChar(buffer, area, midRow, fromCx, T_UP, edgeStyle);
+ arrowChar(buffer, area, midRow, toCx, T_DOWN, edgeStyle);
for (int r = midRow + 1; r < toRow - 1; r++) {
- setChar(buffer, area, r, toCx, vChar, edgeStyle);
+ arrowChar(buffer, area, r, toCx, vChar, edgeStyle);
}
- setChar(buffer, area, toRow - 1, toCx, ARROW, edgeStyle);
+ arrowChar(buffer, area, toRow - 1, toCx, ARROW, edgeStyle);
}
}
- private void drawCounters(Buffer buffer, Rect area, int toCx, int toTop,
StatInfo stat) {
+ private void arrowChar(Buffer buffer, Rect area, int row, int col, char
ch, Style style) {
+ setChar(buffer, area, row, col, ch, style);
+ occupy(row, col, 1);
+ }
+
+ /** The counts at the head of an arrow, failed left of it and ok right of
it; returns the width of the ok count. */
+ private int drawCounters(Buffer buffer, Rect area, int toCx, int toTop,
StatInfo stat) {
if (!showMetrics || stat == null) {
- return;
+ return 0;
}
long total = stat.exchangesTotal;
long failed = stat.exchangesFailed;
long ok = total - failed;
+ int okWidth = 0;
if (ok > 0) {
String okStr = String.valueOf(ok);
writeText(buffer, area, toTop - 1, toCx + 2, okStr,
metricsOkStyle());
+ okWidth = okStr.length();
+ occupy(toTop - 1, toCx + 2, okWidth);
}
if (failed > 0) {
String failStr = String.valueOf(failed);
int col = toCx - 1 - failStr.length();
writeText(buffer, area, toTop - 1, col, failStr,
metricsFailStyle());
+ occupy(toTop - 1, col, failStr.length());
}
+ return okWidth;
}
private void drawScopeBox(Buffer buffer, Rect area, LayoutNode scopeNode) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
index fc89f6e876ed..5d10011bb852 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
@@ -81,8 +81,8 @@ state: `a` ai (off shows only what the sources and the runtime
say: no AI labels, notes or capabilities; it does not change the AI
overview setting; with no summary yet it has the AI write one, as
`/overview` does), `b` view (business: labels and what capabilities achieve;
-technical: route ids and endpoints), `m` metrics, `e` external, `d`
-detail, `g` group, `u` utility (one setting for the architecture
+technical: route ids and endpoints), `m` metrics, `e` external, `y` types
+(the body types on the arrows of a route), `d` detail, `g` group, `u` utility
(one setting for the architecture
and the topology, so both show the same groups), `x` errors (the
error handling in a frame below the routes; in the architecture
a box of the routes reached only on error). The footer lists
@@ -149,6 +149,35 @@ its failures go, is in a frame below the route when `x` is
on. The
history view always shows the onException clauses, so a replay can
step into them.
+## Body Types
+
+With the message history on (such as `camel run --dev`), Camel keeps the
+body types each step has been reached with, across the messages. In the
+route diagram, `y` shows them beside the arrow into each step, so the
+arrows show how the body changes along the route:
+
+```
+ ┌──────────────────────┐
+ │ unmarshal[json] │
+ └──────────────────────┘
+ │
+ │ ⚠ byte[] | LinkedHashMap (payload and data)
+ 3 ▼ 12
+ ┌────────────────────⚠─┐
+ │ to[file:outbox] │
+ └──────────────────────┘
+```
+
+A step reached with bodies of more than one kind (`null`, `payload`,
+`data` or `collection`) needs attention, as its body depends on the path
+a message takes: its box has a `⚠` in its top border, whether the types
+are shown or not. Where the arrows of a branch are side by side, the
+types are cut to the room there is: the first ones and how many more
+(`ByteArrayInputStreamCache +2`). The Info panel of the selected step
+lists them all with their kind, and in the topology the Info panel of a
+route says how many of its steps were reached with more than one type
+(`⚠ 1 mixed · 2 varied`).
+
## Route Structure Preview
A compact tree structure preview appears in the bottom-right corner
@@ -199,6 +228,8 @@ you an at-a-glance view of where you are in the route.
- `c` — show source code at selected node
- `d` — toggle EIP detail panel (shows configured options)
- `x` — show or hide the route's error handling, in a frame below it
+- `y` — show the body types each step has been reached with, beside the
+ arrows into the steps
- `g` — go to node (fuzzy search popup)
- `Esc` — go back (previous route or topology)
- `t` — jump back to topology view
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 65a3fbb49da6..0be659c4e1a6 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
@@ -12,6 +12,7 @@ they are sent to. A typical integration has multiple routes
working together.
- **MSG/S** or **MSG/M** — Current message throughput (per second or per
minute) for this route. Configure in Settings (F2)
- **TOTAL** — Total exchanges processed by this route since startup
- **FAIL** — Exchanges that ended with an unhandled error in this route
+- **BODY TYPES** — Shown when steps of a route have been reached with more
than one body type, across the messages (with the message history on, such as
`camel run --dev`): how many steps varied, and how many of them with bodies of
more than one kind, which need attention (`⚠ 1 mixed · 2 varied`). The
processors of the selected route then have a BODY TYPES column too, with the
types each step has been reached with
- **MIN** — Fastest exchange processing time in milliseconds. This is the time
from when the exchange entered the route until it completed
- **MEAN** — Average exchange processing time in milliseconds. A rising MEAN
may indicate a downstream service getting slower
- **MAX** — Slowest exchange processing time in milliseconds. A very high MAX
compared to MEAN suggests occasional slow outliers
@@ -147,6 +148,7 @@ in the diagram.
- `Esc` — go back (previous route or topology)
- `t` — jump back to topology view
- `m` — toggle metrics
+- `y` — toggle the body types each step has been reached with, beside the
arrows
- `n` — toggle description labels
- `PgUp/PgDn` — scroll detail panel (when detail is on)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTabTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTabTest.java
index 98f3e8e5e85b..1bfe1f67065b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTabTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTabTest.java
@@ -92,6 +92,22 @@ class AbstractDiagramTabTest {
assertThat(tab.detailMode).isFalse();
}
+ @ParameterizedTest
+ @MethodSource("tabs")
+ void yShowsTheBodyTypesOnTheArrowsOfARoute(Function<MonitorContext,
AbstractDiagramTab> factory) {
+ AbstractDiagramTab tab = inRoute(factory, false);
+
+ assertThat(tab.handleKeyEvent(KeyEvent.ofChar('y'))).isTrue();
+ assertThat(tab.diagram.isShowBodyTypes()).isTrue();
+ assertThat(tab.handleKeyEvent(KeyEvent.ofChar('y'))).isTrue();
+ assertThat(tab.diagram.isShowBodyTypes()).isFalse();
+
+ // the topology has no steps to show them for
+ tab.topologyMode = true;
+ tab.handleKeyEvent(KeyEvent.ofChar('y'));
+ assertThat(tab.diagram.isShowBodyTypes()).isFalse();
+ }
+
@ParameterizedTest
@MethodSource("tabs")
void mAndEToggleWhatTheDiagramShows(Function<MonitorContext,
AbstractDiagramTab> factory) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypesViewsTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypesViewsTest.java
new file mode 100644
index 000000000000..a27775637353
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypesViewsTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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 java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.text.Line;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine;
+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 body types each step has been reached with, in the Routes view and the
Info panel of the diagram (CAMEL-25892).
+ */
+class BodyTypesViewsTest {
+
+ private MonitorContext ctx;
+ private IntegrationInfo info;
+
+ @BeforeEach
+ void setUp() {
+ Theme.resetForTesting();
+ info = new IntegrationInfo();
+ info.pid = "5678";
+ info.name = "orders";
+ ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new
AtomicReference<>(List.of()));
+ ctx.selectedPid = "5678";
+ }
+
+ @Test
+ void theRoutesWithStepsThatVaried() {
+ RouteInfo orders = route("orders", "platform-http:/orders");
+ orders.variedBodyTypes = 2;
+ orders.mixedBodyKinds = 1;
+ RouteInfo stock = route("stock", "timer:stock");
+ RouteInfo invoices = route("invoices", "file:inbox");
+ invoices.variedBodyTypes = 1;
+
+ String rendered = TuiTestHelper.renderToString(new RoutesTab(ctx),
160, 30);
+
+ assertThat(rendered).contains("BODY TYPES");
+ assertThat(rendered).contains(TuiIcons.WARN + " 1 mixed · 2 varied");
+ assertThat(rendered).contains("1 varied");
+ assertThat(RoutesTab.bodyTypesLabel(stock)).isEmpty();
+ }
+
+ @Test
+ void noColumnWhenNothingVaried() {
+ route("stock", "timer:stock");
+
+ String rendered = TuiTestHelper.renderToString(new RoutesTab(ctx),
160, 30);
+
+ assertThat(rendered).doesNotContain("BODY TYPES");
+ }
+
+ @Test
+ void theBodyTypesOfEachStep() {
+ RouteInfo orders = route("orders", "platform-http:/orders");
+ orders.variedBodyTypes = 1;
+ orders.mixedBodyKinds = 1;
+ orders.processors.add(processor("unmarshal1", "unmarshal", 1,
+
List.of("org.apache.camel.converter.stream.ByteArrayInputStreamCache"),
List.of("payload")));
+ orders.processors.add(processor("to1", "to", 1,
+ List.of("byte[]", "java.util.LinkedHashMap"),
List.of("payload", "data")));
+
+ String rendered = TuiTestHelper.renderToString(new RoutesTab(ctx),
160, 40);
+
+ assertThat(rendered).contains("ByteArrayInputStreamCache");
+ assertThat(rendered).contains(TuiIcons.WARN + " byte[] |
LinkedHashMap");
+ }
+
+ @Test
+ void theStatusFileHasThem() {
+ JsonObject status = Jsoner.deserialize("""
+ {"routes": [{"routeId": "orders", "from":
"platform-http:/orders", "state": "Started",
+ "variedBodyTypes": 1, "mixedBodyKinds": 1,
+ "processors": [{"id": "to1", "processor": "to", "level": 1,
+ "bodyTypes": ["byte[]", "java.util.LinkedHashMap"],
"bodyKinds": ["payload", "data"]}]}]}
+ """, (JsonObject) null);
+ IntegrationInfo parsed = new IntegrationInfo();
+
+ StatusParser.parseRoutes(parsed, status);
+
+ RouteInfo route = parsed.routes.get(0);
+ assertThat(route.variedBodyTypes).isEqualTo(1);
+ assertThat(route.mixedBodyKinds).isEqualTo(1);
+
assertThat(route.processors.get(0).bodyTypes).containsExactly("byte[]",
"java.util.LinkedHashMap");
+
assertThat(route.processors.get(0).bodyKinds).containsExactly("payload",
"data");
+ }
+
+ @Test
+ void theWords() {
+ assertThat(BodyTypes.label(List.of("java.lang.String", "byte[]"),
List.of("payload")))
+ .isEqualTo("String | byte[]");
+ assertThat(BodyTypes.label(List.of("null", "java.util.LinkedHashMap"),
List.of("null", "data")))
+ .isEqualTo(TuiIcons.WARN + " null | LinkedHashMap (null and
data)");
+ assertThat(BodyTypes.label(List.of(), List.of())).isEmpty();
+ assertThat(BodyTypes.shortLabel(List.of("null",
"java.util.LinkedHashMap"), List.of("null", "data")))
+ .isEqualTo(TuiIcons.WARN + " null | LinkedHashMap");
+ assertThat(BodyTypes.kinds(List.of("null", "payload",
"data"))).isEqualTo("null, payload and data");
+ }
+
+ @Test
+ void theTypesCutToTheRoomThereIs() {
+ List<String> types =
List.of("org.apache.camel.converter.stream.ByteArrayInputStreamCache",
"java.util.LinkedHashMap");
+ List<String> kinds = List.of("payload", "data");
+
+ assertThat(BodyTypes.fit(types, kinds, 80))
+ .isEqualTo(TuiIcons.WARN + " ByteArrayInputStreamCache |
LinkedHashMap (payload and data)");
+ assertThat(BodyTypes.fit(types, kinds, 45)).isEqualTo(TuiIcons.WARN +
" ByteArrayInputStreamCache | LinkedHashMap");
+ // the first type, and how many more
+ assertThat(BodyTypes.fit(types, kinds, 30)).isEqualTo(TuiIcons.WARN +
" ByteArrayInputStreamCache +1");
+ assertThat(BodyTypes.fit(types, kinds, 16)).isEqualTo(TuiIcons.WARN +
" ByteArrayI… +1");
+ // no room for a name: the step still says it needs attention
+ assertThat(BodyTypes.fit(types, kinds, 3)).isEqualTo(TuiIcons.WARN);
+ assertThat(BodyTypes.fit(List.of("java.lang.String"),
List.of("payload"), 3)).isEqualTo("St…");
+ assertThat(BodyTypes.fit(List.of("java.lang.String"),
List.of("payload"), 1)).isEmpty();
+ assertThat(BodyTypes.fit(List.of(), List.of(), 40)).isEmpty();
+ }
+
+ @Test
+ void theInfoPanelHasThemAll() {
+ RouteDiagramLayoutEngine.NodeInfo to = new
RouteDiagramLayoutEngine.NodeInfo();
+ to.bodyTypes =
List.of("org.apache.camel.converter.stream.ByteArrayInputStreamCache",
"java.util.LinkedHashMap");
+ to.bodyKinds = List.of("payload", "data");
+ List<Line> lines = new ArrayList<>();
+
+ DiagramDetailSupport.addBodyTypes(lines, to, 27);
+
+ assertThat(lines).extracting(BodyTypesViewsTest::text).containsExactly(
+ "",
+ " Body types:",
+ " ByteArrayInputStreamCache",
+ " LinkedHashMap",
+ " " + TuiIcons.WARN + " payload and data: the",
+ " body here depends on the",
+ " path a message takes");
+
+ RouteDiagramLayoutEngine.NodeInfo log = new
RouteDiagramLayoutEngine.NodeInfo();
+ log.bodyTypes = List.of("java.lang.String", "byte[]");
+ log.bodyKinds = List.of("payload");
+ lines.clear();
+ DiagramDetailSupport.addBodyTypes(lines, log, 27);
+ assertThat(lines).extracting(BodyTypesViewsTest::text)
+ .containsExactly("", " Body types (payload):", " String", "
byte[]");
+
+ // not reached: nothing
+ lines.clear();
+ DiagramDetailSupport.addBodyTypes(lines, new
RouteDiagramLayoutEngine.NodeInfo(), 27);
+ assertThat(lines).isEmpty();
+ }
+
+ private static String text(Line line) {
+ StringBuilder sb = new StringBuilder();
+ line.spans().forEach(span -> sb.append(span.content()));
+ return sb.toString();
+ }
+
+ private RouteInfo route(String id, String from) {
+ RouteInfo route = new RouteInfo();
+ route.routeId = id;
+ route.from = from;
+ route.state = "Started";
+ info.routes.add(route);
+ return route;
+ }
+
+ private static ProcessorInfo processor(
+ String id, String type, int level, List<String> bodyTypes,
List<String> bodyKinds) {
+ ProcessorInfo proc = new ProcessorInfo();
+ proc.id = id;
+ proc.processor = type;
+ proc.level = level;
+ proc.bodyTypes = bodyTypes;
+ proc.bodyKinds = bodyKinds;
+ return proc;
+ }
+}
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
index 75b0c1d114c1..02ff1dd67f03 100644
---
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
@@ -139,6 +139,25 @@ class DiagramTopologyTest {
.doesNotContainKeys("coverage", "load01", "load05", "load15");
}
+ @Test
+ void theStepsHaveTheBodyTypesOfTheStatusFileAsMoreMessagesCome() {
+ IntegrationInfo info = status();
+ ProcessorInfo to1 = info.routes.get(0).processors.get(0);
+ to1.bodyTypes = List.of("byte[]", "java.util.LinkedHashMap");
+ to1.bodyKinds = List.of("payload", "data");
+
+ JsonObject topology = DiagramTopology.withNumbers(json(TOPOLOGY),
info);
+
+ List<RouteDiagramLayoutEngine.NodeInfo> lines =
RouteDiagramHelper.parseRoutes(topology).get(0).nodes;
+ assertThat(lines.get(2).id).isEqualTo("to1");
+ assertThat(lines.get(2).bodyTypes).containsExactly("byte[]",
"java.util.LinkedHashMap");
+ assertThat(lines.get(2).bodyKinds).containsExactly("payload", "data");
+ // a step not reached yet has none, nor have the route and its from
+ assertThat(lines.get(3).bodyTypes).isEmpty();
+ assertThat(lines.get(0).bodyTypes).isEmpty();
+ assertThat(lines.get(1).bodyTypes).isEmpty();
+ }
+
@Test
void theStructureKeptForTheNextTimeIsLeftWithoutNumbers() {
JsonObject structure = 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 a80d8046e18e..c36832270de2 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
@@ -60,9 +60,7 @@ class SourceSizeTest {
AiPanel.runAgentLoop 159
AiUsageStats.render 166
CamelMonitor.java 2761
- DiagramDetailSupport.renderEipInfoPanel 159
- DiagramDetailSupport.renderRouteInfoPanel 154
- DiagramSupport.java 2460
+ DiagramSupport.java 2413
DiagramSupport.loadAllDiagramsInBackground 180
DiagramSupport.loadHighlightedNativeDiagramInBackground 212
EndpointsTab.renderContent 170
@@ -74,7 +72,6 @@ class SourceSizeTest {
OllamaMonitor.toJson 200
OverviewTab.buildOverviewTable 185
OverviewTab.renderThroughputChart 158
- RoutesTab.renderProcessors 179
SendMessagePopup.doSend 156
SendMessagePopup.handleKeyEvent 220
SourceEditAssist.java 2006
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/BodyTypesOnArrowsTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/BodyTypesOnArrowsTest.java
new file mode 100644
index 000000000000..4751dff700ca
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/BodyTypesOnArrowsTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.diagram;
+
+import java.util.List;
+import java.util.Map;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.apache.camel.dsl.jbang.core.commands.tui.BodyTypes;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The body types each step has been reached with, beside the arrow into it
(CAMEL-25892): on the shaft above the head,
+ * cut where the arrows of a branch are side by side, and a ⚠ on a step
reached with bodies of more than one kind.
+ */
+class BodyTypesOnArrowsTest {
+
+ private static final String STREAM =
"org.apache.camel.converter.stream.ByteArrayInputStreamCache";
+
+ @Test
+ void theTypesAreOnTheShaftAboveTheHeadOfTheArrow() {
+ RouteInfo route = route();
+ route.nodes.add(node("unmarshal", "unmarshal1", 1, List.of(STREAM),
List.of("payload")));
+ route.nodes.add(node("to", "to1", 1, List.of("byte[]",
"java.util.LinkedHashMap"), List.of("payload", "data")));
+
+ Buffer buffer = render(route, true);
+
+ int[] stream = find(buffer, "ByteArrayInputStreamCache");
+ assertThat(stream).isNotNull();
+ // the shaft is two columns left of it, and the head of the arrow
right below
+ assertThat(symbol(buffer, stream[0] - 2, stream[1])).isEqualTo("│");
+ assertThat(symbol(buffer, stream[0] - 2, stream[1] +
1)).isEqualTo("▼");
+ // with more than one kind: the kinds as well, as there is room
+ int[] mixed = find(buffer, BodyTypes.MARK + " byte[] | LinkedHashMap
(payload and data)");
+ assertThat(mixed).isNotNull();
+ assertThat(symbol(buffer, mixed[0] - 2, mixed[1] + 1)).isEqualTo("▼");
+ // and its box has a ⚠ in its top border
+ assertThat(row(buffer, mixed[1] + 2)).contains(BodyTypes.MARK);
+ }
+
+ @Test
+ void theArrowsOfABranchSideBySideCutTheTypes() {
+ RouteInfo route = route();
+ route.nodes.add(node("multicast", "multicast1", 1, List.of(),
List.of()));
+ route.nodes.add(node("to", "to1", 2,
+ List.of(STREAM, "java.util.LinkedHashMap",
"java.util.ArrayList"), List.of("payload", "data", "collection")));
+ route.nodes.add(node("to", "to2", 2, List.of("java.lang.String"),
List.of("payload")));
+
+ Buffer buffer = render(route, true);
+
+ // the first one stops before the arrow of the second: the first type,
and how many more
+ int[] left = find(buffer, BodyTypes.MARK + " ByteArrayInputStreamCache
+2 ");
+ assertThat(left).isNotNull();
+ assertThat(row(buffer,
left[1])).doesNotContain("LinkedHashMap").contains("▼ String");
+ }
+
+ @Test
+ void offTheStepsNeedingAttentionStillHaveTheirMark() {
+ RouteInfo route = route();
+ route.nodes.add(node("to", "to1", 1, List.of("byte[]",
"java.util.LinkedHashMap"), List.of("payload", "data")));
+
+ Buffer buffer = render(route, false);
+
+ assertThat(find(buffer, "LinkedHashMap")).isNull();
+ assertThat(find(buffer, BodyTypes.MARK)).isNotNull();
+ }
+
+ private static Buffer render(RouteInfo route, boolean bodyTypes) {
+ RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+ LayoutRoute layout = engine.layoutRoute(route, 0);
+ RouteDiagramWidget widget = new RouteDiagramWidget(layout,
engine.getNodeWidth(), -1, 0, 0, false, Map.of())
+ .withBodyTypes(bodyTypes).withBodyKindMarks(true);
+ Rect area = new Rect(0, 0, 160, 50);
+ Buffer buffer = Buffer.empty(area);
+ widget.render(area, buffer);
+ return buffer;
+ }
+
+ private static RouteInfo route() {
+ RouteInfo route = new RouteInfo();
+ route.routeId = "orders";
+ NodeInfo r = node("route", "orders", 0, List.of(), List.of());
+ r.code = "route[orders]";
+ route.nodes.add(r);
+ route.nodes.add(node("from", "from1", 0, List.of(), List.of()));
+ return route;
+ }
+
+ private static NodeInfo node(String type, String id, int level,
List<String> bodyTypes, List<String> bodyKinds) {
+ NodeInfo n = new NodeInfo();
+ n.type = type;
+ n.id = id;
+ n.code = type;
+ n.level = level;
+ n.bodyTypes = bodyTypes;
+ n.bodyKinds = bodyKinds;
+ return n;
+ }
+
+ private static String symbol(Buffer buffer, int x, int y) {
+ return buffer.get(x, y).symbol();
+ }
+
+ private static String row(Buffer buffer, int y) {
+ StringBuilder sb = new StringBuilder();
+ for (int x = 0; x < buffer.width(); x++) {
+ sb.append(buffer.get(x, y).symbol());
+ }
+ return sb.toString();
+ }
+
+ private static int[] find(Buffer buffer, String text) {
+ for (int y = 0; y < buffer.height(); y++) {
+ int x = row(buffer, y).indexOf(text);
+ if (x >= 0) {
+ return new int[] { x, y };
+ }
+ }
+ return null;
+ }
+}