Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3ab8b5b
CAMEL-23636: add frontend-maven-plugin build infrastructure for web c…
ammachado Jun 15, 2026
ed13855
CAMEL-23636: add JS layout algorithm ported from RouteDiagramLayoutEn…
ammachado Jun 15, 2026
aa46fe7
CAMEL-23636: implement camel-route-diagram Lit web component with SVG…
ammachado Jun 15, 2026
21a4410
CAMEL-23636: fix duplicate license header in smoke-test.html
ammachado Jun 15, 2026
b1d465e
CAMEL-23636: document camel-route-diagram web component
ammachado Jun 15, 2026
4410a80
CAMEL-23636: fix dark-mode background and Safari file:// loading in s…
ammachado Jun 15, 2026
a01d9cc
CAMEL-23636: fix smoke-test server instructions — serve from src/ not…
ammachado Jun 15, 2026
a5d5a1e
CAMEL-23636: fix edges passing through intermediate nodes in linear c…
ammachado Jun 15, 2026
257b645
CAMEL-23636: move smoke-test.html to src/main/frontend/ where it belongs
ammachado Jun 15, 2026
97a6576
CAMEL-23636: address code review findings
ammachado Jun 15, 2026
a1c0418
CAMEL-23636: fix broken xref in upgrade guide
ammachado Jun 15, 2026
c81c027
CAMEL-23636: fix 15 code-review findings in camel-diagram web component
ammachado Jun 17, 2026
be8f4ee
CAMEL-23636: add Lucide icons with ISC license attribution
ammachado Jun 17, 2026
1ee4a72
CAMEL-23636: replace Lit/npm toolchain with vanilla Web Component
ammachado Jun 17, 2026
afd4844
CAMEL-23636: fix 8 code-review findings in camel-diagram
ammachado Jun 17, 2026
5c8973c
CAMEL-23636: fix broken xref in upgrade guide
ammachado Jun 17, 2026
6996616
CAMEL-23636: replace SVG marker arrowheads with inline polygons and u…
ammachado Jun 17, 2026
14398f2
CAMEL-23636: fix node label rendering and multi-route layout in camel…
ammachado Jun 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions components/camel-diagram/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
limitations under the License.

-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
Expand Down Expand Up @@ -99,7 +100,26 @@
<scope>test</scope>
</dependency>


</dependencies>

<build>
<plugins>
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<configuration>
<licenseSets>
<licenseSet>
<excludes combine.children="append">
<!-- third-party attribution notice (plain text, no license header) -->
<exclude>src/main/resources/META-INF/resources/camel/diagram/THIRD-PARTY-NOTICES.txt
</exclude>
</excludes>
</licenseSet>
</licenseSets>
</configuration>
</plugin>
</plugins>
</build>

</project>
69 changes: 69 additions & 0 deletions components/camel-diagram/src/main/docs/diagram.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,72 @@ String diagram = renderer.renderDiagramAnsi(layoutRoutes, totalHeight, highlight
RouteDiagramRenderer pngRenderer = new RouteDiagramRenderer(nodeWidth, fontSize);
BufferedImage image = pngRenderer.renderDiagram(layoutRoutes, totalHeight, colors, highlightedNodes, style);
----

== Embeddable Web Component

`camel-diagram` ships a lightweight `<camel-route-diagram>` web component that renders
interactive route diagrams as SVG directly in the browser.
Any application with `camel-diagram` on the classpath automatically serves the component
as a static resource — no extra server configuration needed.

=== Usage

Include the bundled script served from `META-INF/resources/camel/diagram/camel-route-diagram.js`
(automatically exposed by Servlet 3 containers and Quarkus/Spring Boot static-resource mechanisms):

[source,html]
----
<script type="module" src="/camel/diagram/camel-route-diagram.js"></script>

<camel-route-diagram
src="/q/dev/route-structure"
refresh="5000"
filter="my-route">
</camel-route-diagram>
----

The `src` attribute must point to an endpoint returning the `route-structure` dev console JSON
(for example the Quarkus Dev UI endpoint `/q/dev/route-structure`).
The component automatically appends `?metric=true` so that per-processor exchange statistics
are included in the diagram.

=== Attributes

[width="100%",cols="2,5,2",options="header"]
|===
| Attribute | Description | Default
| `src` | URL to fetch the route-structure JSON from (required) | —
| `refresh` | Polling interval in milliseconds; `0` disables polling | `0`
| `filter` | Route ID filter, forwarded as `?filter=` query parameter | (all routes)
|===

=== Theming

The component is theme-agnostic.
It respects `prefers-color-scheme` automatically for dark/light mode,
and exposes CSS custom properties so the host application can override every visual aspect:

[source,css]
----
camel-route-diagram {
--crd-bg: #ffffff; /* canvas background */
--crd-fg: #1e293b; /* text colour */
--crd-edge: #94a3b8; /* edge/arrow colour */
--crd-stat: #64748b; /* metric overlay text */
--crd-font: system-ui; /* font family */
--crd-font-size: 12px; /* base font size */
--crd-color-route: #6366f1; /* "route" node */
--crd-color-from: #0ea5e9; /* "from" node */
--crd-color-to: #0ea5e9; /* "to" node */
--crd-color-log: #64748b; /* "log" node */
--crd-color-choice: #f59e0b; /* "choice" node */
--crd-color-when: #fbbf24; /* "when" branch */
--crd-color-otherwise: #fbbf24; /* "otherwise" branch */
--crd-color-doTry: #f59e0b; /* "doTry" scope */
--crd-color-doCatch: #fbbf24; /* "doCatch" clause */
--crd-color-doFinally: #fbbf24; /* "doFinally" clause */
--crd-color-multicast: #8b5cf6; /* "multicast" node */
--crd-color-circuitBreaker: #ef4444; /* "circuitBreaker" node */
--crd-color-default: #6366f1; /* all other EIP nodes */
}
----
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
*/
public class RouteDiagramAsciiRenderer {

private static final int MAX_WRAP_LINES = 3;
private static final int Y_SCALE = 20;
private static final int MIN_BOX_WIDTH = 16;
private static final int X_DIVISOR = 15;
Expand Down Expand Up @@ -484,51 +483,7 @@ private int boxHeight(LayoutNode node) {

private List<String> rewrapText(LayoutNode node, int maxWidth) {
String label = String.join("", node.wrappedLines);
return wrapText(label, maxWidth);
}

static List<String> wrapText(String text, int maxWidth) {
if (maxWidth <= 0 || text.length() <= maxWidth) {
return List.of(text);
}

List<String> lines = new ArrayList<>();
String remaining = text;

while (!remaining.isEmpty() && lines.size() < MAX_WRAP_LINES) {
if (remaining.length() <= maxWidth) {
lines.add(remaining);
remaining = "";
break;
}

int breakAt = -1;
for (int i = 0; i < maxWidth && i < remaining.length(); i++) {
char c = remaining.charAt(i);
if (c == ' ' || c == ':' || c == '/' || c == '.' || c == ',' || c == '&' || c == '?') {
breakAt = i + 1;
}
}
if (breakAt <= 0) {
breakAt = maxWidth;
}

lines.add(remaining.substring(0, breakAt).stripTrailing());
remaining = remaining.substring(breakAt).stripLeading();
}

if (!remaining.isEmpty()) {
int lastIdx = lines.size() - 1;
String lastLine = lines.get(lastIdx);
if (lastLine.length() + remaining.length() <= maxWidth) {
lines.set(lastIdx, lastLine + remaining);
} else {
String combined = lastLine + remaining;
lines.set(lastIdx, combined.substring(0, Math.max(1, maxWidth - 3)) + "...");
}
}

return lines;
return RouteDiagramHelper.wrapText(label, maxWidth);
}

private int toCol(int pixelX) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
*/
public final class RouteDiagramHelper {

static final int MAX_WRAP_LINES = 3;

private RouteDiagramHelper() {
}

Expand All @@ -52,12 +54,10 @@ public static List<RouteInfo> parseRoutes(JsonObject jo) {
return routes;
}

for (int i = 0; i < arr.size(); i++) {
Object item = arr.get(i);
if (!(item instanceof JsonObject)) {
for (Object item : arr) {
if (!(item instanceof JsonObject o)) {
continue;
}
JsonObject o = (JsonObject) item;
RouteInfo route = new RouteInfo();
route.routeId = o.getString("routeId");
String source = o.getString("source");
Expand Down Expand Up @@ -120,6 +120,50 @@ public static List<RouteInfo> parseRoutes(JsonObject jo) {
return routes;
}

static List<String> wrapText(String text, int maxWidth) {
if (maxWidth <= 0 || text.length() <= maxWidth) {
return List.of(text);
}

List<String> lines = new ArrayList<>();
String remaining = text;

while (!remaining.isEmpty() && lines.size() < MAX_WRAP_LINES) {
if (remaining.length() <= maxWidth) {
lines.add(remaining);
remaining = "";
break;
}

int breakAt = -1;
for (int i = 0; i < maxWidth && i < remaining.length(); i++) {
char c = remaining.charAt(i);
if (c == ' ' || c == ':' || c == '/' || c == '.' || c == ',' || c == '&' || c == '?') {
breakAt = i + 1;
}
}
if (breakAt <= 0) {
breakAt = maxWidth;
}

lines.add(remaining.substring(0, breakAt).stripTrailing());
remaining = remaining.substring(breakAt).stripLeading();
}

if (!remaining.isEmpty()) {
int lastIdx = lines.size() - 1;
String lastLine = lines.get(lastIdx);
if (lastLine.length() + remaining.length() <= maxWidth) {
lines.set(lastIdx, lastLine + remaining);
} else {
String combined = lastLine + remaining;
lines.set(lastIdx, combined.substring(0, Math.max(1, maxWidth - 3)) + "...");
}
}

return lines;
}

public enum HighlightStyle {
SUCCESS,
FAIL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public RouteDiagramRenderer(int nodeWidth, int fontSizeScaled, boolean metrics)
public RouteDiagramRenderer(int nodeWidth, int fontSizeScaled, int nodeTextPadding, boolean metrics) {
this.nodeWidth = nodeWidth;
this.fontSizeNode = fontSizeScaled;
this.fontSizeLabel = fontSizeScaled + 1 * SCALE;
this.fontSizeLabel = fontSizeScaled + SCALE;
this.nodeTextPadding = nodeTextPadding;
this.metrics = metrics;
}
Expand Down Expand Up @@ -167,7 +167,7 @@ private static Color parseColor(String value) {
}
Integer idx = Colors.rgbColor(value);
if (idx != null) {
return new Color(Colors.rgbColor(idx.intValue()));
return new Color(Colors.rgbColor(idx));
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutNode;
import org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutResult;

import static org.apache.camel.diagram.RouteDiagramHelper.wrapText;

/**
* Renders topology diagrams as ASCII art or Unicode box-drawing text.
*/
Expand All @@ -33,7 +35,7 @@ public class TopologyAsciiRenderer {
private static final int Y_SCALE = 20;
private static final int MIN_BOX_WIDTH = 16;
private static final int X_DIVISOR = 15;
private static final int MAX_WRAP_LINES = 3;
private static final int MAX_WRAP_LINES = RouteDiagramHelper.MAX_WRAP_LINES;

private static final char UNI_H = '─';
private static final char UNI_V = '│';
Expand Down Expand Up @@ -151,8 +153,7 @@ private void drawNode(char[][] grid, TopologyLayoutNode node) {
line1 = node.routeId;
}

List<String> lines = new ArrayList<>();
lines.addAll(wrapText(line1, boxWidth - 4));
List<String> lines = new ArrayList<>(wrapText(line1, boxWidth - 4));
if (!isExternalNode(node) && !showDescription) {
String line2 = "(" + node.from + ")";
List<String> fromLines = wrapText(line2, boxWidth - 4);
Expand Down Expand Up @@ -331,46 +332,6 @@ private int boxHeight(TopologyLayoutNode node) {
return 2 + Math.min(lines, MAX_WRAP_LINES + 1);
}

static List<String> wrapText(String text, int maxWidth) {
if (maxWidth <= 0 || text.length() <= maxWidth) {
return new ArrayList<>(List.of(text));
}

List<String> lines = new ArrayList<>();
String remaining = text;

while (!remaining.isEmpty() && lines.size() < MAX_WRAP_LINES) {
if (remaining.length() <= maxWidth) {
lines.add(remaining);
remaining = "";
break;
}

int breakAt = -1;
for (int i = 0; i < maxWidth && i < remaining.length(); i++) {
char c = remaining.charAt(i);
if (c == ' ' || c == ':' || c == '/' || c == '.' || c == ',' || c == '&' || c == '?') {
breakAt = i + 1;
}
}
if (breakAt <= 0) {
breakAt = maxWidth;
}

lines.add(remaining.substring(0, breakAt).stripTrailing());
remaining = remaining.substring(breakAt).stripLeading();
}

if (!remaining.isEmpty()) {
int lastIdx = lines.size() - 1;
String lastLine = lines.get(lastIdx);
String combined = lastLine + remaining;
lines.set(lastIdx, combined.substring(0, Math.max(1, maxWidth - 3)) + "...");
}

return lines;
}

private String applyAnsiColors(String plain) {
if (counterPositions.isEmpty()) {
return plain;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
camel-route-diagram bundles the following third-party content.

--------------------------------------------------------------------------------
Lucide (icon SVG paths inlined in camel-route-diagram.js):
--------------------------------------------------------------------------------

ISC License
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT).
All other copyright (c) for Lucide are held by Lucide Contributors 2022.
SPDX-License-Identifier: ISC
https://lucide.dev

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above copyright
notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
Loading