This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-23631-route-diagram-highlight-error-path in repository https://gitbox.apache.org/repos/asf/camel.git
commit 8666c8651e0c45ff76d1d186fb17160bfd92af8b Author: Claus Ibsen <[email protected]> AuthorDate: Wed May 27 21:45:45 2026 +0200 CAMEL-23631: camel-diagram - Add highlight support for route paths Add the ability to highlight specific node paths in route diagrams, supporting both success (green) and fail (red) styles. This enables visualizing error paths from ErrorRegistry and trace paths from message history. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../camel/diagram/DefaultRouteDiagramDumper.java | 21 +++- .../camel/diagram/RouteDiagramAsciiRenderer.java | 71 +++++++++++- .../apache/camel/diagram/RouteDiagramHelper.java | 93 +++++++++++++++ .../camel/diagram/RouteDiagramLayoutEngine.java | 3 + .../apache/camel/diagram/RouteDiagramRenderer.java | 128 ++++++++++++++++++--- .../camel-jbang-cmd-route-diagram.adoc | 2 + .../META-INF/camel-jbang-commands-metadata.json | 2 +- .../commands/action/CamelRouteDiagramAction.java | 32 +++++- 8 files changed, 324 insertions(+), 28 deletions(-) diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java index ba0d83311d38..920588cb328e 100644 --- a/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java +++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java @@ -141,7 +141,8 @@ public class DefaultRouteDiagramDumper extends ServiceSupport implements CamelCo private static BufferedImage renderImage( List<RouteDiagramLayoutEngine.RouteInfo> routes, String theme, int fontSize, int nodeWidth, - String nodeLabel, boolean metrics) { + String nodeLabel, boolean metrics, + Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle highlightStyle) { RouteDiagramRenderer renderer = new RouteDiagramRenderer( nodeWidth * RouteDiagramLayoutEngine.SCALE, fontSize * RouteDiagramLayoutEngine.SCALE, metrics); RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine( @@ -159,11 +160,18 @@ public class DefaultRouteDiagramDumper extends ServiceSupport implements CamelCo } theme = theme.toLowerCase(); RouteDiagramRenderer.DiagramColors colors = RouteDiagramRenderer.DiagramColors.parse(theme); - return renderer.renderDiagram(layoutRoutes, currentY, colors); + return renderer.renderDiagram(layoutRoutes, currentY, colors, highlightedNodeIds, highlightStyle); + } + + private static BufferedImage renderImage( + List<RouteDiagramLayoutEngine.RouteInfo> routes, String theme, int fontSize, int nodeWidth, + String nodeLabel, boolean metrics) { + return renderImage(routes, theme, fontSize, nodeWidth, nodeLabel, metrics, null, null); } private static String renderAscii( - List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth, String nodeLabel, boolean unicode) { + List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth, String nodeLabel, boolean unicode, + Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle highlightStyle) { RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine( nodeWidth, RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE, RouteDiagramLayoutEngine.NodeLabelMode.valueOf(nodeLabel.toUpperCase())); @@ -178,7 +186,12 @@ public class DefaultRouteDiagramDumper extends ServiceSupport implements CamelCo RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer( nodeWidth * RouteDiagramLayoutEngine.SCALE, unicode); - return renderer.renderDiagram(layoutRoutes, currentY); + return renderer.renderDiagram(layoutRoutes, currentY, highlightedNodeIds, highlightStyle); + } + + private static String renderAscii( + List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth, String nodeLabel, boolean unicode) { + return renderAscii(routes, nodeWidth, nodeLabel, unicode, null, null); } } diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java index 5d0f9bb0173a..150fb77a736a 100644 --- a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java +++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java @@ -19,6 +19,7 @@ package org.apache.camel.diagram; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import org.apache.camel.diagram.RouteDiagramLayoutEngine.Bounds; import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode; @@ -62,7 +63,9 @@ public class RouteDiagramAsciiRenderer { public enum CounterType { OK, - FAIL + FAIL, + HIGHLIGHT_SUCCESS, + HIGHLIGHT_FAIL } public record CounterPos(int row, int col, int length, CounterType type) { @@ -91,8 +94,19 @@ public class RouteDiagramAsciiRenderer { return counterPositions; } + public String renderDiagramAnsi( + List<LayoutRoute> layoutRoutes, int totalHeight, + Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle highlightStyle) { + String plain = renderDiagram(layoutRoutes, totalHeight, highlightedNodeIds, highlightStyle); + return applyAnsiColors(plain); + } + public String renderDiagramAnsi(List<LayoutRoute> layoutRoutes, int totalHeight) { String plain = renderDiagram(layoutRoutes, totalHeight); + return applyAnsiColors(plain); + } + + private String applyAnsiColors(String plain) { if (counterPositions.isEmpty()) { return plain; } @@ -104,7 +118,11 @@ public class RouteDiagramAsciiRenderer { String before = line.substring(0, cp.col); String counter = line.substring(cp.col, cp.col + cp.length); String after = line.substring(cp.col + cp.length); - String color = cp.type == CounterType.OK ? "\033[32m" : "\033[31m"; + String color; + switch (cp.type) { + case OK, HIGHLIGHT_SUCCESS -> color = "\033[32m"; + default -> color = "\033[31m"; + } lines[cp.row] = before + color + counter + "\033[0m" + after; } } @@ -112,6 +130,27 @@ public class RouteDiagramAsciiRenderer { return String.join("\n", lines); } + public String renderDiagram( + List<LayoutRoute> layoutRoutes, int totalHeight, + Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle highlightStyle) { + counterPositions.clear(); + int maxPixelX = layoutRoutes.stream() + .mapToInt(lr -> lr.maxX).max().orElse(nodeWidth) + PADDING; + int gridWidth = toCol(maxPixelX) + boxWidth + 4; + int gridHeight = totalHeight / Y_SCALE + 20; + + char[][] grid = new char[gridHeight][gridWidth]; + for (char[] row : grid) { + Arrays.fill(row, ' '); + } + + for (LayoutRoute lr : layoutRoutes) { + drawRoute(grid, lr, highlightedNodeIds, highlightStyle); + } + + return gridToString(grid); + } + public String renderDiagram(List<LayoutRoute> layoutRoutes, int totalHeight) { counterPositions.clear(); int maxPixelX = layoutRoutes.stream() @@ -131,7 +170,9 @@ public class RouteDiagramAsciiRenderer { return gridToString(grid); } - private void drawRoute(char[][] grid, LayoutRoute lr) { + private void drawRoute( + char[][] grid, LayoutRoute lr, + Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle highlightStyle) { int labelRow = toRow(lr.labelY); String label = lr.routeId; if (lr.source != null && !lr.source.isEmpty()) { @@ -157,9 +198,16 @@ public class RouteDiagramAsciiRenderer { for (LayoutNode ln : lr.nodes) { drawNode(grid, ln); + if (highlightedNodeIds != null && ln.id != null && highlightedNodeIds.contains(ln.id)) { + recordHighlightPositions(grid, ln, highlightStyle); + } } } + private void drawRoute(char[][] grid, LayoutRoute lr) { + drawRoute(grid, lr, null, null); + } + private void drawNode(char[][] grid, LayoutNode node) { int col = toCol(node.x); int row = toRow(node.y); @@ -203,6 +251,23 @@ public class RouteDiagramAsciiRenderer { } } + private void recordHighlightPositions( + char[][] grid, LayoutNode node, RouteDiagramHelper.HighlightStyle style) { + int col = toCol(node.x); + int row = toRow(node.y); + int innerWidth = boxWidth - 4; + List<String> lines = rewrapText(node, innerWidth); + int height = 2 + lines.size(); + + CounterType ct = style == RouteDiagramHelper.HighlightStyle.FAIL + ? CounterType.HIGHLIGHT_FAIL + : CounterType.HIGHLIGHT_SUCCESS; + + for (int r = row; r < row + height && r < grid.length; r++) { + counterPositions.add(new CounterPos(r, col, boxWidth, ct)); + } + } + private void drawArrow(char[][] grid, LayoutNode from, LayoutNode to) { int fromCx = centerCol(from); int fromBottom = toRow(from.y) + boxHeight(from); 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 645110f0ca9b..5ad89d4b11e4 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 @@ -17,7 +17,9 @@ package org.apache.camel.diagram; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo; import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo; @@ -66,6 +68,7 @@ public final class RouteDiagramHelper { for (JsonObject line : lines) { NodeInfo node = new NodeInfo(); node.type = line.getString("type"); + node.id = line.getString("id"); node.code = Jsoner.unescape(line.getString("code")); node.description = line.getString("description"); Integer level = line.getInteger("level"); @@ -109,6 +112,96 @@ public final class RouteDiagramHelper { return routes; } + public enum HighlightStyle { + SUCCESS, + FAIL + } + + public static class HighlightInfo { + private final Set<String> nodeIds; + private final List<String> routeOrder; + private final HighlightStyle style; + + public HighlightInfo(Set<String> nodeIds, List<String> routeOrder, HighlightStyle style) { + this.nodeIds = nodeIds; + this.routeOrder = routeOrder; + this.style = style; + } + + public Set<String> getNodeIds() { + return nodeIds; + } + + public List<String> getRouteOrder() { + return routeOrder; + } + + public HighlightStyle getStyle() { + return style; + } + } + + /** + * Parses message history entries into a {@link HighlightInfo} containing the node IDs to highlight and the route + * ordering (by first visit). + * + * @param messageHistory array of history entries in the format {@code "routeId[nodeId] (elapsed ms)"} + * @param style the highlight style to use + * @return highlight info with node IDs and route ordering + */ + public static HighlightInfo parseMessageHistory(String[] messageHistory, HighlightStyle style) { + Set<String> nodeIds = new LinkedHashSet<>(); + Set<String> routeOrderSet = new LinkedHashSet<>(); + if (messageHistory != null) { + for (String h : messageHistory) { + int bracket = h.indexOf('['); + int end = h.indexOf(']'); + if (bracket >= 0 && end > bracket) { + routeOrderSet.add(h.substring(0, bracket)); + nodeIds.add(h.substring(bracket + 1, end)); + } + } + } + return new HighlightInfo(nodeIds, new ArrayList<>(routeOrderSet), style); + } + + /** + * Filters and orders routes based on the highlight info. Only routes that contain at least one highlighted node are + * included, and they are ordered by the sequence in which the message first visited each route. + * + * @param routes the full list of parsed routes + * @param highlight the highlight info containing route ordering + * @return filtered and ordered list of routes + */ + public static List<RouteInfo> filterAndOrderRoutes(List<RouteInfo> routes, HighlightInfo highlight) { + if (highlight == null || highlight.routeOrder.isEmpty()) { + return routes; + } + Set<String> nodeIds = highlight.nodeIds; + List<String> order = highlight.routeOrder; + + List<RouteInfo> result = new ArrayList<>(); + for (RouteInfo route : routes) { + boolean hasHighlightedNode = route.nodes.stream() + .anyMatch(n -> n.id != null && nodeIds.contains(n.id)); + if (hasHighlightedNode) { + result.add(route); + } + } + result.sort((a, b) -> { + int ia = order.indexOf(a.routeId); + int ib = order.indexOf(b.routeId); + if (ia < 0) { + ia = Integer.MAX_VALUE; + } + if (ib < 0) { + ib = Integer.MAX_VALUE; + } + return Integer.compare(ia, ib); + }); + return result; + } + /** * Extracts a short display name from a source path. For example, {@code "file:/path/to/my-route.yaml"} becomes * {@code "my-route.yaml"}. 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 f052e68126c8..7681aba8e338 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 @@ -152,6 +152,7 @@ public class RouteDiagramLayoutEngine { public static class NodeInfo { public String type; + public String id; public String code; public String description; public int level; @@ -179,6 +180,7 @@ public class RouteDiagramLayoutEngine { public static class LayoutNode { public String type; + public String id; public int x; public int y; public int height; @@ -405,6 +407,7 @@ public class RouteDiagramLayoutEngine { LayoutNode ln = new LayoutNode(); ln.type = node.info.type; + ln.id = node.info.id; ln.x = nodeX; ln.y = y; ln.wrappedLines = resolveLabel(node.info, nodeLabelMode).stream() 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 89f4839c5158..a87b85001636 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 @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.camel.diagram.RouteDiagramLayoutEngine.Bounds; import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode; @@ -48,6 +49,10 @@ public class RouteDiagramRenderer { private static final int LABEL_TEXT_BASELINE = 14 * SCALE; public static final int MAX_IMAGE_DIMENSION = 16384; + private static final float HIGHLIGHT_STROKE_WIDTH = 3.0f * SCALE; + private static final Color HIGHLIGHT_SUCCESS_COLOR = new Color(0x43a047); + private static final Color HIGHLIGHT_FAIL_COLOR = new Color(0xe53935); + private static final float[] DASH_PATTERN = { 10f, 5f }; // 10 pixels on, 5 pixels off private static final Stroke DASHED_STROKE = new BasicStroke( STROKE_WIDTH, // line width @@ -218,6 +223,41 @@ public class RouteDiagramRenderer { } } + public BufferedImage renderDiagram( + List<LayoutRoute> layoutRoutes, int totalHeight, DiagramColors colors, + Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle highlightStyle) { + int imgWidth = layoutRoutes.stream().mapToInt(lr -> lr.maxX).max().orElse(400) + PADDING; + int imgHeight = totalHeight + PADDING; + + if (imgWidth > MAX_IMAGE_DIMENSION || imgHeight > MAX_IMAGE_DIMENSION) { + throw new IllegalStateException( + "Diagram too large (" + imgWidth / SCALE + "x" + imgHeight / SCALE + + " logical pixels). Use --filter to narrow the routes displayed."); + } + + int imageType = colors.getBg() == null ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB; + BufferedImage image = new BufferedImage(imgWidth, imgHeight, imageType); + Graphics2D g = image.createGraphics(); + try { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + + if (colors.getBg() != null) { + g.setColor(colors.getBg()); + g.fillRect(0, 0, imgWidth, imgHeight); + } + + for (LayoutRoute lr : layoutRoutes) { + drawRoute(g, lr, colors, highlightedNodeIds, highlightStyle); + } + } finally { + g.dispose(); + } + return image; + } + public BufferedImage renderDiagram(List<LayoutRoute> layoutRoutes, int totalHeight, DiagramColors colors) { int imgWidth = layoutRoutes.stream().mapToInt(lr -> lr.maxX).max().orElse(400) + PADDING; int imgHeight = totalHeight + PADDING; @@ -251,7 +291,9 @@ public class RouteDiagramRenderer { return image; } - private void drawRoute(Graphics2D g, LayoutRoute lr, DiagramColors colors) { + private void drawRoute( + Graphics2D g, LayoutRoute lr, DiagramColors colors, + Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle highlightStyle) { g.setColor(colors.getRouteLabel()); g.setFont(new Font("SansSerif", Font.BOLD, fontSizeLabel)); String label = lr.routeId; @@ -270,19 +312,36 @@ public class RouteDiagramRenderer { for (LayoutNode ln : lr.nodes) { if (ln.parentNode != null) { + boolean highlightArrow = highlightedNodeIds != null + && isHighlighted(ln, highlightedNodeIds) + && isHighlighted(ln.parentNode, highlightedNodeIds); if (ln.connectFromMerge) { - drawArrowFromMerge(g, ln, colors); + drawArrowFromMerge(g, ln, colors, highlightArrow, highlightStyle); } else { - drawArrow(g, ln.parentNode, ln, colors); + drawArrow(g, ln.parentNode, ln, colors, highlightArrow, highlightStyle); } } } for (LayoutNode ln : lr.nodes) { - drawNode(g, ln, colors); + drawNode(g, ln, colors, highlightedNodeIds, highlightStyle); } } + private void drawRoute(Graphics2D g, LayoutRoute lr, DiagramColors colors) { + drawRoute(g, lr, colors, null, null); + } + + private static boolean isHighlighted(LayoutNode node, Set<String> highlightedNodeIds) { + return node.id != null && highlightedNodeIds.contains(node.id); + } + + private static Color highlightColor(RouteDiagramHelper.HighlightStyle style) { + return style == RouteDiagramHelper.HighlightStyle.FAIL + ? HIGHLIGHT_FAIL_COLOR + : HIGHLIGHT_SUCCESS_COLOR; + } + private void drawScopeBox(Graphics2D g, LayoutNode scopeNode, DiagramColors colors) { TreeNode tn = scopeNode.treeNode; Bounds bounds = new Bounds( @@ -308,17 +367,24 @@ public class RouteDiagramRenderer { ? node.y - SCOPE_BOX_PAD : node.y; } - private void drawArrowFromMerge(Graphics2D g, LayoutNode to, DiagramColors colors) { + private void drawArrowFromMerge( + Graphics2D g, LayoutNode to, DiagramColors colors, + boolean highlighted, RouteDiagramHelper.HighlightStyle highlightStyle) { var stat = to.treeNode.info.stat; long total = stat != null ? stat.exchangesTotal : 0; long failed = stat != null ? stat.exchangesFailed : 0; long ok = total - failed; - g.setColor(colors.getArrow()); - if (!metrics || total > 0) { - g.setStroke(new BasicStroke(STROKE_WIDTH)); + if (highlighted) { + g.setColor(highlightColor(highlightStyle)); + g.setStroke(new BasicStroke(HIGHLIGHT_STROKE_WIDTH)); } else { - g.setStroke(DASHED_STROKE); + g.setColor(colors.getArrow()); + if (!metrics || total > 0) { + g.setStroke(new BasicStroke(STROKE_WIDTH)); + } else { + g.setStroke(DASHED_STROKE); + } } int toCx = to.x + nodeWidth / 2; @@ -347,14 +413,26 @@ public class RouteDiagramRenderer { } } - private void drawNode(Graphics2D g, LayoutNode node, DiagramColors colors) { + private void drawArrowFromMerge(Graphics2D g, LayoutNode to, DiagramColors colors) { + drawArrowFromMerge(g, to, colors, false, null); + } + + private void drawNode( + Graphics2D g, LayoutNode node, DiagramColors colors, + Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle highlightStyle) { Color color = getNodeColor(node.type, colors); g.setColor(color); g.fillRoundRect(node.x, node.y, nodeWidth, node.height, ARC, ARC); - g.setColor(color.brighter()); - g.setStroke(new BasicStroke(BORDER_STROKE_WIDTH)); + boolean highlighted = highlightedNodeIds != null && isHighlighted(node, highlightedNodeIds); + if (highlighted) { + g.setColor(highlightColor(highlightStyle)); + g.setStroke(new BasicStroke(HIGHLIGHT_STROKE_WIDTH)); + } else { + g.setColor(color.brighter()); + g.setStroke(new BasicStroke(BORDER_STROKE_WIDTH)); + } g.drawRoundRect(node.x, node.y, nodeWidth, node.height, ARC, ARC); g.setColor(colors.getText()); @@ -380,21 +458,31 @@ public class RouteDiagramRenderer { } } - private void drawArrow(Graphics2D g, LayoutNode from, LayoutNode to, DiagramColors colors) { + private void drawNode(Graphics2D g, LayoutNode node, DiagramColors colors) { + drawNode(g, node, colors, null, null); + } + + private void drawArrow( + Graphics2D g, LayoutNode from, LayoutNode to, DiagramColors colors, + boolean highlighted, RouteDiagramHelper.HighlightStyle highlightStyle) { var stat = metrics ? to.treeNode.info.stat : null; if (metrics && BRANCH_CHILD_TYPES.contains(to.type) && !to.treeNode.children.isEmpty()) { - // grab stat from first child (for example choice to have counters for when/otherwise) stat = to.treeNode.children.get(0).info.stat; } long total = stat != null ? stat.exchangesTotal : 0; long failed = stat != null ? stat.exchangesFailed : 0; long ok = total - failed; - g.setColor(colors.getArrow()); - if (!metrics || total > 0) { - g.setStroke(new BasicStroke(STROKE_WIDTH)); + if (highlighted) { + g.setColor(highlightColor(highlightStyle)); + g.setStroke(new BasicStroke(HIGHLIGHT_STROKE_WIDTH)); } else { - g.setStroke(DASHED_STROKE); + g.setColor(colors.getArrow()); + if (!metrics || total > 0) { + g.setStroke(new BasicStroke(STROKE_WIDTH)); + } else { + g.setStroke(DASHED_STROKE); + } } int fromCx = from.x + nodeWidth / 2; @@ -423,6 +511,10 @@ public class RouteDiagramRenderer { } } + private void drawArrow(Graphics2D g, LayoutNode from, LayoutNode to, DiagramColors colors) { + drawArrow(g, from, to, colors, false, null); + } + private void drawArrowHead(Graphics2D g, int x, int y) { int[] xPoints = { x - ARROW_SIZE, x, x + ARROW_SIZE }; int[] yPoints = { y - ARROW_SIZE, y, y - ARROW_SIZE }; 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 354db16e98f4..eaa8d056e71f 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 @@ -22,6 +22,8 @@ camel cmd route-diagram [options] | `--box-width` | Node box width in logical pixels | 180 | int | `--filter` | Filter route by filename or route id | | String | `--font-size` | Font size in logical pixels for node text | 12 | int +| `--highlight` | Comma-separated node IDs to highlight in the diagram | | String +| `--highlight-style` | Highlight style: success (green) or fail (red) | success | String | `--ignore-loading-error` | Whether to ignore route loading and compilation errors (use this with care!) | false | boolean | `--metric` | Whether to include live metrics (only possible for running Camel application) | true | boolean | `--node-label` | What text to display in diagram nodes: code, description, or both (default) | both | String 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 d0d78976a693..eebf916394be 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 @@ -3,7 +3,7 @@ { "name": "ask", "fullName": "ask", "description": "Ask a question about a running Camel application using AI", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": "--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama', 'openai', or 'anthropic'", "javaType": "LlmClient.ApiType", "type" [...] { "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 c29cb7b8df37..fa89ddca2317 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 @@ -22,7 +22,10 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import javax.imageio.ImageIO; @@ -98,6 +101,14 @@ public class CamelRouteDiagramAction extends ActionWatchCommand { description = "Whether to include live metrics (only possible for running Camel application)") boolean metric; + @CommandLine.Option(names = { "--highlight" }, + description = "Comma-separated node IDs to highlight in the diagram") + String highlight; + + @CommandLine.Option(names = { "--highlight-style" }, + description = "Highlight style: success (green) or fail (red)", defaultValue = "success") + String highlightStyle; + @CommandLine.Option(names = { "--ignore-loading-error" }, defaultValue = "false", description = "Whether to ignore route loading and compilation errors (use this with care!)") boolean ignoreLoadingError; @@ -198,6 +209,23 @@ public class CamelRouteDiagramAction extends ActionWatchCommand { return 1; } + // parse highlight info + Set<String> highlightedNodeIds = null; + RouteDiagramHelper.HighlightStyle hlStyle = null; + RouteDiagramHelper.HighlightInfo highlightInfo = null; + if (highlight != null && !highlight.isBlank()) { + highlightedNodeIds = new LinkedHashSet<>(Arrays.asList(highlight.split(","))); + hlStyle = "fail".equalsIgnoreCase(highlightStyle) + ? RouteDiagramHelper.HighlightStyle.FAIL + : RouteDiagramHelper.HighlightStyle.SUCCESS; + highlightInfo = new RouteDiagramHelper.HighlightInfo(highlightedNodeIds, List.of(), hlStyle); + routes = RouteDiagramHelper.filterAndOrderRoutes(routes, highlightInfo); + if (routes.isEmpty()) { + printer().println("No routes contain highlighted nodes"); + return 1; + } + } + NodeLabelMode labelMode = parseNodeLabelMode(nodeLabel); RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(boxWidth, fontSize, labelMode); @@ -212,7 +240,7 @@ public class CamelRouteDiagramAction extends ActionWatchCommand { if (isTextTheme()) { RouteDiagramAsciiRenderer asciiRenderer = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), isUnicodeTheme(), pid > 0 && metric); - String ascii = asciiRenderer.renderDiagramAnsi(layoutRoutes, currentY); + String ascii = asciiRenderer.renderDiagramAnsi(layoutRoutes, currentY, highlightedNodeIds, hlStyle); if (output != null) { String fileName = output.endsWith(".png") @@ -237,7 +265,7 @@ public class CamelRouteDiagramAction extends ActionWatchCommand { BufferedImage image; try { - image = renderer.renderDiagram(layoutRoutes, currentY, colors); + image = renderer.renderDiagram(layoutRoutes, currentY, colors, highlightedNodeIds, hlStyle); } catch (IllegalStateException e) { printer().println(e.getMessage()); return 1;
