This is an automated email from the ASF dual-hosted git repository.
gnodet 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 314c463c55be CAMEL-23424: Add --node-label option to route-diagram for
description display
314c463c55be is described below
commit 314c463c55bee041cd942a84d9a0edcdf8af7158
Author: Guillaume Nodet <[email protected]>
AuthorDate: Tue May 5 22:18:07 2026 +0200
CAMEL-23424: Add --node-label option to route-diagram for description
display
- Add --node-label option with code/description/both modes (default: both)
- Parse description field from route structure JSON
- Fix DefaultDumpRoutesStrategy to include description in JSON
- Expose nodeLabel parameter in MCP tool
---
.../apache/camel/diagram/RouteDiagramHelper.java | 1 +
.../camel/diagram/RouteDiagramLayoutEngine.java | 48 +++++++-
.../apache/camel/diagram/RouteDiagramRenderer.java | 19 ++--
.../org/apache/camel/diagram/RouteDiagramTest.java | 123 ++++++++++++++++++++-
.../camel/impl/DefaultDumpRoutesStrategy.java | 3 +
.../camel-jbang-cmd-route-diagram.adoc | 1 +
.../META-INF/camel-jbang-commands-metadata.json | 2 +-
.../commands/action/CamelRouteDiagramAction.java | 27 ++++-
.../action/CamelRouteDiagramActionTest.java | 49 ++++++++
.../jbang/core/commands/mcp/RouteDiagramTools.java | 5 +-
.../core/commands/mcp/RouteDiagramToolsTest.java | 6 +-
11 files changed, 260 insertions(+), 24 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 8eef000a444c..75dd4faeaa4f 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
@@ -60,6 +60,7 @@ public final class RouteDiagramHelper {
NodeInfo node = new NodeInfo();
node.type = line.getString("type");
node.code = Jsoner.unescape(line.getString("code"));
+ node.description = line.getString("description");
Integer level = line.getInteger("level");
node.level = level != null ? level : 0;
route.nodes.add(node);
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 3fe193394239..a139fab5aa52 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
@@ -38,9 +38,16 @@ public class RouteDiagramLayoutEngine {
private static final int NODE_TEXT_PADDING = 16 * SCALE;
private static final int MAX_WRAP_LINES = 3;
+ public enum NodeLabelMode {
+ CODE,
+ DESCRIPTION,
+ BOTH
+ }
+
private final int nodeWidth;
private final int baseNodeHeight;
private final FontMetrics fontMetrics;
+ private final NodeLabelMode nodeLabelMode;
private static final Set<String> BRANCHING_EIPS = Set.of(
"choice", "multicast", "doTry", "loadBalance", "recipientList",
"circuitBreaker");
@@ -60,8 +67,18 @@ public class RouteDiagramLayoutEngine {
* @param fontSize font size in logical pixels (before SCALE)
*/
public RouteDiagramLayoutEngine(int boxWidth, int fontSize) {
+ this(boxWidth, fontSize, NodeLabelMode.CODE);
+ }
+
+ /**
+ * @param boxWidth logical node width in pixels (before SCALE)
+ * @param fontSize font size in logical pixels (before SCALE)
+ * @param nodeLabelMode controls what text is displayed in node labels
+ */
+ public RouteDiagramLayoutEngine(int boxWidth, int fontSize, NodeLabelMode
nodeLabelMode) {
this.nodeWidth = boxWidth * SCALE;
this.baseNodeHeight = Math.max(32, fontSize * 32 / 12) * SCALE;
+ this.nodeLabelMode = nodeLabelMode != null ? nodeLabelMode :
NodeLabelMode.CODE;
BufferedImage scratch = new BufferedImage(1, 1,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = scratch.createGraphics();
g.setFont(new Font("SansSerif", Font.PLAIN, fontSize * SCALE));
@@ -80,6 +97,7 @@ public class RouteDiagramLayoutEngine {
public static class NodeInfo {
public String type;
public String code;
+ public String description;
public int level;
}
@@ -102,7 +120,6 @@ public class RouteDiagramLayoutEngine {
}
public static class LayoutNode {
- public String label;
public String type;
public int x;
public int y;
@@ -216,8 +233,7 @@ public class RouteDiagramLayoutEngine {
return lines;
}
- int computeNodeHeight(String label) {
- List<String> lines = wrapLabel(label);
+ int computeNodeHeight(List<String> lines) {
int lineCount = Math.min(lines.size(), MAX_WRAP_LINES);
if (lineCount <= 1) {
return baseNodeHeight;
@@ -226,6 +242,26 @@ public class RouteDiagramLayoutEngine {
return baseNodeHeight + (lineCount - 1) * lineSpacing;
}
+ List<String> resolveLabel(NodeInfo info) {
+ return resolveLabel(info, nodeLabelMode);
+ }
+
+ public static List<String> resolveLabel(NodeInfo info, NodeLabelMode mode)
{
+ String code = cleanLabel(info.code);
+ boolean hasDesc = info.description != null &&
!info.description.isBlank();
+ switch (mode) {
+ case DESCRIPTION:
+ return List.of(hasDesc ? info.description : code);
+ case BOTH:
+ if (hasDesc && !info.description.equals(code)) {
+ return List.of(info.description, code);
+ }
+ return List.of(code);
+ default:
+ return List.of(code);
+ }
+ }
+
public LayoutRoute layoutRoute(RouteInfo route, int startY) {
LayoutRoute lr = new LayoutRoute();
lr.routeId = route.routeId;
@@ -296,12 +332,12 @@ public class RouteDiagramLayoutEngine {
int nodeX = x + (availableWidth - nodeWidth) / 2;
LayoutNode ln = new LayoutNode();
- ln.label = cleanLabel(node.info.code);
ln.type = node.info.type;
ln.x = nodeX;
ln.y = y;
- ln.wrappedLines = wrapLabel(ln.label);
- ln.height = computeNodeHeight(ln.label);
+ ln.wrappedLines = resolveLabel(node.info).stream()
+ .flatMap(s -> wrapLabel(s).stream()).toList();
+ ln.height = computeNodeHeight(ln.wrappedLines);
ln.treeNode = node;
node.layoutNode = ln;
lr.nodes.add(ln);
diff --git
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
index 84bf0d358e23..2a04dfec5396 100644
---
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
+++
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
@@ -307,9 +307,6 @@ public class RouteDiagramRenderer {
FontMetrics fm = g.getFontMetrics();
List<String> lines = node.wrappedLines;
- if (lines == null || lines.isEmpty()) {
- lines = List.of(node.label);
- }
int lineHeight = fm.getHeight();
int totalTextHeight = lines.size() * lineHeight;
@@ -379,6 +376,10 @@ public class RouteDiagramRenderer {
}
public List<String> printTextDiagram(List<RouteInfo> routes) {
+ return printTextDiagram(routes,
RouteDiagramLayoutEngine.NodeLabelMode.CODE);
+ }
+
+ public List<String> printTextDiagram(List<RouteInfo> routes,
RouteDiagramLayoutEngine.NodeLabelMode mode) {
List<String> lines = new ArrayList<>();
for (RouteInfo route : routes) {
lines.add("");
@@ -390,14 +391,16 @@ public class RouteDiagramRenderer {
TreeNode tree = RouteDiagramLayoutEngine.buildTree(route.nodes);
if (tree != null) {
- printTreeNode(tree, "", true, true, lines);
+ printTreeNode(tree, "", true, true, lines, mode);
}
lines.add("");
}
return lines;
}
- private void printTreeNode(TreeNode node, String prefix, boolean isLast,
boolean isRoot, List<String> lines) {
+ private void printTreeNode(
+ TreeNode node, String prefix, boolean isLast, boolean isRoot,
List<String> lines,
+ RouteDiagramLayoutEngine.NodeLabelMode mode) {
String connector;
if (isRoot) {
connector = " ";
@@ -408,9 +411,9 @@ public class RouteDiagramRenderer {
}
String typeTag = node.info.type != null ? "[" + node.info.type + "] "
: "";
- String code = RouteDiagramLayoutEngine.cleanLabel(node.info.code);
+ String label = String.join(" | ",
RouteDiagramLayoutEngine.resolveLabel(node.info, mode));
- lines.add(prefix + connector + typeTag + code);
+ lines.add(prefix + connector + typeTag + label);
String childPrefix;
if (isRoot) {
@@ -422,7 +425,7 @@ public class RouteDiagramRenderer {
}
for (int i = 0; i < node.children.size(); i++) {
- printTreeNode(node.children.get(i), childPrefix, i ==
node.children.size() - 1, false, lines);
+ printTreeNode(node.children.get(i), childPrefix, i ==
node.children.size() - 1, false, lines, mode);
}
}
}
diff --git
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
index ae16b66aca0b..5c75685598fa 100644
---
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
+++
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
@@ -410,7 +410,7 @@ class RouteDiagramTest {
LayoutRoute lr = engine.layoutRoute(route,
RouteDiagramLayoutEngine.PADDING);
LayoutNode logAfter = lr.nodes.get(lr.nodes.size() - 1);
- assertEquals("log[Do other processing...]", logAfter.label);
+ assertEquals("log[Do other processing...]", String.join("",
logAfter.wrappedLines));
assertTrue(logAfter.connectFromMerge, "Should connect from doTry merge
point");
LayoutNode choiceNode = lr.nodes.stream()
@@ -437,7 +437,7 @@ class RouteDiagramTest {
assertEquals(5, lr.nodes.size());
LayoutNode toNode = lr.nodes.get(4);
- assertEquals("to[log:after]", toNode.label);
+ assertEquals("to[log:after]", String.join("", toNode.wrappedLines));
assertTrue(toNode.connectFromMerge, "Node after filter should connect
from merge point");
LayoutNode filterNode = lr.nodes.get(2);
@@ -593,10 +593,129 @@ class RouteDiagramTest {
assertTrue(image.getHeight() > 0);
}
+ @Test
+ void testNodeLabelModeDescription() {
+ RouteInfo route = new RouteInfo();
+ route.routeId = "route1";
+ route.nodes.add(node("from", "timer:tick?period=1000", 0, "Poll every
second"));
+ route.nodes.add(node("to", "log:a", 1, null));
+
+ RouteDiagramLayoutEngine engine
+ = new RouteDiagramLayoutEngine(180, 12,
RouteDiagramLayoutEngine.NodeLabelMode.DESCRIPTION);
+ LayoutRoute lr = engine.layoutRoute(route,
RouteDiagramLayoutEngine.PADDING);
+
+ assertEquals("Poll every second", String.join("",
lr.nodes.get(0).wrappedLines));
+ assertEquals("log:a", String.join("", lr.nodes.get(1).wrappedLines));
+ }
+
+ @Test
+ void testNodeLabelModeDescriptionFallsBackToCode() {
+ RouteInfo route = new RouteInfo();
+ route.routeId = "route1";
+ route.nodes.add(node("from", "timer:tick", 0, null));
+
+ RouteDiagramLayoutEngine engine
+ = new RouteDiagramLayoutEngine(180, 12,
RouteDiagramLayoutEngine.NodeLabelMode.DESCRIPTION);
+ LayoutRoute lr = engine.layoutRoute(route,
RouteDiagramLayoutEngine.PADDING);
+
+ assertEquals("timer:tick", String.join("",
lr.nodes.get(0).wrappedLines));
+ }
+
+ @Test
+ void testNodeLabelModeBoth() {
+ RouteInfo route = new RouteInfo();
+ route.routeId = "route1";
+ route.nodes.add(node("from", "timer:tick?period=1000", 0, "Poll every
second"));
+ route.nodes.add(node("to", "log:a", 1, null));
+
+ RouteDiagramLayoutEngine engine
+ = new RouteDiagramLayoutEngine(180, 12,
RouteDiagramLayoutEngine.NodeLabelMode.BOTH);
+ LayoutRoute lr = engine.layoutRoute(route,
RouteDiagramLayoutEngine.PADDING);
+
+ List<String> fromLines = lr.nodes.get(0).wrappedLines;
+ assertTrue(fromLines.size() >= 2, "Both mode should produce at least 2
lines when description differs from code");
+ String allFromText = String.join("", fromLines);
+ assertTrue(allFromText.contains("Poll every second"));
+ assertTrue(allFromText.contains("timer:tick"));
+
+ assertEquals("log:a", String.join("", lr.nodes.get(1).wrappedLines));
+ }
+
+ @Test
+ void testNodeLabelModeCodeDefault() {
+ RouteInfo route = new RouteInfo();
+ route.routeId = "route1";
+ route.nodes.add(node("from", "timer:tick", 0, "Poll every second"));
+
+ RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+ LayoutRoute lr = engine.layoutRoute(route,
RouteDiagramLayoutEngine.PADDING);
+
+ assertEquals("timer:tick", String.join("",
lr.nodes.get(0).wrappedLines));
+ }
+
+ @Test
+ void testTextDiagramWithDescriptionMode() {
+ RouteInfo route = new RouteInfo();
+ route.routeId = "route1";
+ route.nodes.add(node("from", "timer:tick?period=1000", 0, "Poll every
second"));
+ route.nodes.add(node("to", "log:a", 1, null));
+
+ RouteDiagramRenderer renderer = new RouteDiagramRenderer();
+ List<String> lines
+ = renderer.printTextDiagram(List.of(route),
RouteDiagramLayoutEngine.NodeLabelMode.DESCRIPTION);
+
+ String output = String.join("\n", lines);
+ assertTrue(output.contains("Poll every second"));
+ assertTrue(output.contains("log:a"));
+ }
+
+ @Test
+ void testTextDiagramWithBothMode() {
+ RouteInfo route = new RouteInfo();
+ route.routeId = "route1";
+ route.nodes.add(node("from", "timer:tick", 0, "Poll every second"));
+ route.nodes.add(node("to", "log:a", 1, null));
+
+ RouteDiagramRenderer renderer = new RouteDiagramRenderer();
+ List<String> lines = renderer.printTextDiagram(List.of(route),
RouteDiagramLayoutEngine.NodeLabelMode.BOTH);
+
+ String output = String.join("\n", lines);
+ assertTrue(output.contains("Poll every second"));
+ assertTrue(output.contains("timer:tick"));
+ assertTrue(output.contains("log:a"));
+ }
+
+ @Test
+ void testRenderDiagramWithDescriptionMode() {
+ System.setProperty("java.awt.headless", "true");
+
+ RouteInfo route = new RouteInfo();
+ route.routeId = "route1";
+ route.nodes.add(node("from", "timer:tick?period=1000", 0, "Poll every
second"));
+ route.nodes.add(node("to", "log:a", 1, null));
+
+ RouteDiagramLayoutEngine engine
+ = new RouteDiagramLayoutEngine(180, 12,
RouteDiagramLayoutEngine.NodeLabelMode.DESCRIPTION);
+ LayoutRoute lr = engine.layoutRoute(route,
RouteDiagramLayoutEngine.PADDING);
+
+ RouteDiagramRenderer renderer = new RouteDiagramRenderer();
+ DiagramColors colors = DiagramColors.parse("dark");
+ BufferedImage image = renderer.renderDiagram(List.of(lr), lr.maxY +
RouteDiagramLayoutEngine.V_GAP, colors);
+
+ assertNotNull(image);
+ assertTrue(image.getWidth() > 0);
+ assertTrue(image.getHeight() > 0);
+ }
+
private static NodeInfo node(String type, String code, int level) {
+ return node(type, code, level, null);
+ }
+
+ private static NodeInfo node(String type, String code, int level, String
description) {
NodeInfo n = new NodeInfo();
n.type = type;
n.code = code;
+ n.description = description;
n.level = level;
return n;
}
diff --git
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultDumpRoutesStrategy.java
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultDumpRoutesStrategy.java
index aa0642a404b4..a380537ba119 100644
---
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultDumpRoutesStrategy.java
+++
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultDumpRoutesStrategy.java
@@ -448,6 +448,9 @@ public class DefaultDumpRoutesStrategy extends
ServiceSupport implements DumpRou
c.put("type", line.type());
c.put("id", line.id());
c.put("level", line.level());
+ if (line.description() != null) {
+ c.put("description", line.description());
+ }
c.put("code", Jsoner.escape(line.code()));
code.add(c);
}
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
index 1909b47dc09f..5741b65b6c0a 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
@@ -23,6 +23,7 @@ camel cmd route-diagram [options]
| `--filter` | Filter route by filename or route id | | String
| `--font-size` | Font size in logical pixels for node text | 12 | int
| `--ignore-loading-error` | Whether to ignore route loading and compilation
errors (use this with care!) | false | boolean
+| `--node-label` | What text to display in diagram nodes: code, description,
or both (default) | both | String
| `--output` | Save diagram to a PNG file instead of displaying in terminal |
| String
| `--theme,--colors` | Color theme preset (dark, light, transparent) or custom
colors (e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or ANSI
color names (e.g. from=seagreen:to=steelblue). Use bg= for transparent. Can
also be set via DIAGRAM_COLORS env var. | dark | String
| `--width` | Image width in pixels (0 = auto) | 0 | int
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 43700abf34c1..86ff873fd444 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -2,7 +2,7 @@
"commands": [
{ "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind
source and sink Kamelets as a new Camel integration", "deprecated": true,
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options":
[ { "names": "--error-handler", "description": "Add error handler
(none|log|sink:<endpoint>). Sink endpoints are expected in the format
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet
name.", "javaType": "java.lang.String", "type": "stri [...]
{ "name": "catalog", "fullName": "catalog", "description": "List artifacts
from Camel Catalog", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [
{ "names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"component", "fullName": "catalog component", "description": "List components
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
- { "name": "cmd", "fullName": "cmd", "description": "Performs commands in
the running Camel integrations, such as start\/stop route, or change logging
levels.", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"browse", "fullName": "cmd browse", "description": "Browse pending messages on
endpoints [...]
+ { "name": "cmd", "fullName": "cmd", "description": "Performs commands in
the running Camel integrations, such as start\/stop route, or change logging
levels.", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"browse", "fullName": "cmd browse", "description": "Browse pending messages on
endpoints [...]
{ "name": "completion", "fullName": "completion", "description": "Generate
completion script for bash\/zsh", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ] },
{ "name": "config", "fullName": "config", "description": "Get and set user
configuration values", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get",
"fullName": "config get", "description": "Display user configuration value",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
{ "name": "debug", "fullName": "debug", "description": "Debug local Camel
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug",
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names":
"--background", "description": "Run in the background", "defaultValue":
"false", "javaType": "boolean", "type": "boolean" }, { "names":
"--background-wait", "description": "To [...]
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
index 269cfdc25917..3b84256441bc 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
@@ -29,6 +29,7 @@ import javax.imageio.ImageIO;
import org.apache.camel.diagram.RouteDiagramHelper;
import org.apache.camel.diagram.RouteDiagramLayoutEngine;
import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeLabelMode;
import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
import org.apache.camel.diagram.RouteDiagramRenderer;
import org.apache.camel.diagram.RouteDiagramRenderer.DiagramColors;
@@ -82,6 +83,11 @@ public class CamelRouteDiagramAction extends
ActionBaseCommand {
description = "Node box width in logical pixels",
defaultValue = "180")
int boxWidth;
+ @CommandLine.Option(names = { "--node-label" },
+ description = "What text to display in diagram nodes:
code, description, or both (default)",
+ defaultValue = "both")
+ String nodeLabel;
+
@CommandLine.Option(names = { "--ignore-loading-error" }, defaultValue =
"false",
description = "Whether to ignore route loading and
compilation errors (use this with care!)")
boolean ignoreLoadingError;
@@ -109,7 +115,7 @@ public class CamelRouteDiagramAction extends
ActionBaseCommand {
public Integer renderSourceToFile(
String sourceFile, String outputFile, String theme, String filter,
int width, boolean ignoreLoadingError,
- int fontSize, int boxWidth)
+ int fontSize, int boxWidth, String nodeLabel)
throws Exception {
this.name = sourceFile;
this.output = outputFile;
@@ -121,6 +127,9 @@ public class CamelRouteDiagramAction extends
ActionBaseCommand {
this.ignoreLoadingError = ignoreLoadingError;
this.fontSize = fontSize;
this.boxWidth = boxWidth;
+ if (nodeLabel != null && !nodeLabel.isBlank()) {
+ this.nodeLabel = nodeLabel;
+ }
return doCall();
}
@@ -179,7 +188,8 @@ public class CamelRouteDiagramAction extends
ActionBaseCommand {
return 0;
}
- RouteDiagramLayoutEngine engine = new
RouteDiagramLayoutEngine(boxWidth, fontSize);
+ NodeLabelMode labelMode = parseNodeLabelMode(nodeLabel);
+ RouteDiagramLayoutEngine engine = new
RouteDiagramLayoutEngine(boxWidth, fontSize, labelMode);
RouteDiagramRenderer renderer = new RouteDiagramRenderer(
engine.getNodeWidth(), fontSize *
RouteDiagramLayoutEngine.SCALE);
@@ -225,7 +235,7 @@ public class CamelRouteDiagramAction extends
ActionBaseCommand {
"Terminal does not support graphics
protocols (Kitty, iTerm2, or Sixel).");
printer().println(
"Try running in a supported terminal:
Kitty, iTerm2, WezTerm, Ghostty, or VS Code.");
- for (String line :
renderer.printTextDiagram(routes)) {
+ for (String line :
renderer.printTextDiagram(routes, labelMode)) {
printer().println(line);
}
}
@@ -293,4 +303,15 @@ public class CamelRouteDiagramAction extends
ActionBaseCommand {
List<RouteInfo> parseRoutes(JsonObject jo) {
return RouteDiagramHelper.parseRoutes(jo);
}
+
+ static NodeLabelMode parseNodeLabelMode(String value) {
+ if (value == null || value.isBlank() ||
"code".equalsIgnoreCase(value)) {
+ return NodeLabelMode.CODE;
+ } else if ("description".equalsIgnoreCase(value)) {
+ return NodeLabelMode.DESCRIPTION;
+ } else if ("both".equalsIgnoreCase(value)) {
+ return NodeLabelMode.BOTH;
+ }
+ return NodeLabelMode.CODE;
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramActionTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramActionTest.java
index aa83ac006945..008e9b8f08d5 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramActionTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramActionTest.java
@@ -18,10 +18,12 @@ package org.apache.camel.dsl.jbang.core.commands.action;
import java.util.List;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeLabelMode;
import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CamelRouteDiagramActionTest {
@@ -69,4 +71,51 @@ class CamelRouteDiagramActionTest {
assertEquals("from", routes.get(0).nodes.get(0).type);
assertEquals("timer:tick", routes.get(0).nodes.get(0).code);
}
+
+ @Test
+ void testParseRoutesWithDescription() {
+ org.apache.camel.util.json.JsonObject line1 = new
org.apache.camel.util.json.JsonObject();
+ line1.put("type", "from");
+ line1.put("code", "timer:tick?period=1000");
+ line1.put("description", "Poll every second");
+ line1.put("level", 0);
+
+ org.apache.camel.util.json.JsonObject line2 = new
org.apache.camel.util.json.JsonObject();
+ line2.put("type", "to");
+ line2.put("code", "log:a");
+ line2.put("level", 1);
+
+ org.apache.camel.util.json.JsonArray code = new
org.apache.camel.util.json.JsonArray();
+ code.add(line1);
+ code.add(line2);
+
+ org.apache.camel.util.json.JsonObject routeObj = new
org.apache.camel.util.json.JsonObject();
+ routeObj.put("routeId", "route1");
+ routeObj.put("code", code);
+
+ org.apache.camel.util.json.JsonArray routesArr = new
org.apache.camel.util.json.JsonArray();
+ routesArr.add(routeObj);
+
+ org.apache.camel.util.json.JsonObject jo = new
org.apache.camel.util.json.JsonObject();
+ jo.put("routes", routesArr);
+
+ CamelRouteDiagramAction action = new CamelRouteDiagramAction(null);
+ List<RouteInfo> routes = action.parseRoutes(jo);
+
+ assertEquals("Poll every second",
routes.get(0).nodes.get(0).description);
+ assertNull(routes.get(0).nodes.get(1).description);
+ }
+
+ @Test
+ void testParseNodeLabelMode() {
+ assertEquals(NodeLabelMode.CODE,
CamelRouteDiagramAction.parseNodeLabelMode(null));
+ assertEquals(NodeLabelMode.CODE,
CamelRouteDiagramAction.parseNodeLabelMode(""));
+ assertEquals(NodeLabelMode.CODE,
CamelRouteDiagramAction.parseNodeLabelMode("code"));
+ assertEquals(NodeLabelMode.CODE,
CamelRouteDiagramAction.parseNodeLabelMode("CODE"));
+ assertEquals(NodeLabelMode.DESCRIPTION,
CamelRouteDiagramAction.parseNodeLabelMode("description"));
+ assertEquals(NodeLabelMode.DESCRIPTION,
CamelRouteDiagramAction.parseNodeLabelMode("Description"));
+ assertEquals(NodeLabelMode.BOTH,
CamelRouteDiagramAction.parseNodeLabelMode("both"));
+ assertEquals(NodeLabelMode.BOTH,
CamelRouteDiagramAction.parseNodeLabelMode("BOTH"));
+ assertEquals(NodeLabelMode.CODE,
CamelRouteDiagramAction.parseNodeLabelMode("invalid"));
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
index 5dab71e25f65..452ee8892eaf 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
@@ -51,6 +51,8 @@ public class RouteDiagramTools {
@ToolArg(description = "Image width in pixels; 0 (or unset) =
auto") Integer width,
@ToolArg(description = "Font size in logical pixels for node text
(default 12)") Integer fontSize,
@ToolArg(description = "Node box width in logical pixels (default
180)") Integer boxWidth,
+ @ToolArg(description = "What text to display in diagram nodes:
'code' (default), 'description' (prefer "
+ + "description over code if available), or
'both' (show description and code)") String nodeLabel,
@ToolArg(description = "Whether to ignore route loading and
compilation errors (use with care)") Boolean ignoreLoadingError) {
if (sourceFile == null || sourceFile.isBlank()) {
@@ -88,7 +90,8 @@ public class RouteDiagramTools {
width != null ? width : 0,
ignoreLoadingError != null && ignoreLoadingError,
fontSize != null ? fontSize : 12,
- boxWidth != null ? boxWidth : 180);
+ boxWidth != null ? boxWidth : 180,
+ nodeLabel);
File out = new File(resolvedOutput);
boolean success = exit == 0 && out.isFile() && out.length() > 0;
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramToolsTest.java
index 1554ace299fe..fcceec06a600 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramToolsTest.java
@@ -27,7 +27,7 @@ class RouteDiagramToolsTest {
void missingSourceFileThrows() {
RouteDiagramTools tools = new RouteDiagramTools();
- assertThatThrownBy(() -> tools.camel_render_route_diagram(null, null,
null, null, null, null, null, null))
+ assertThatThrownBy(() -> tools.camel_render_route_diagram(null, null,
null, null, null, null, null, null, null))
.isInstanceOf(ToolCallException.class)
.hasMessageContaining("sourceFile");
}
@@ -36,7 +36,7 @@ class RouteDiagramToolsTest {
void blankSourceFileThrows() {
RouteDiagramTools tools = new RouteDiagramTools();
- assertThatThrownBy(() -> tools.camel_render_route_diagram(" ", null,
null, null, null, null, null, null))
+ assertThatThrownBy(() -> tools.camel_render_route_diagram(" ", null,
null, null, null, null, null, null, null))
.isInstanceOf(ToolCallException.class)
.hasMessageContaining("sourceFile");
}
@@ -46,7 +46,7 @@ class RouteDiagramToolsTest {
RouteDiagramTools tools = new RouteDiagramTools();
assertThatThrownBy(() -> tools.camel_render_route_diagram(
- "/no/such/file.yaml", null, null, null, null, null, null,
null))
+ "/no/such/file.yaml", null, null, null, null, null, null,
null, null))
.isInstanceOf(ToolCallException.class)
.hasMessageContaining("does not exist");
}