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 b465f30a8240 CAMEL-23809: fix garbage chars in route-diagram dev 
console metrics
b465f30a8240 is described below

commit b465f30a82409687803284c0d1a42852ccd65cec
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jun 22 07:00:34 2026 +0200

    CAMEL-23809: fix garbage chars in route-diagram dev console metrics
    
    Replace literal Unicode characters in the web component JS with \u
    escape sequences so they survive encoding when the JS is inlined into
    the HTML response. Add <meta charset="utf-8"> to the HTML head.
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    CAMEL-23809: reduce top padding in route-diagram web component
    
    The route label already sits above the SVG, so the 30px top padding
    inside the SVG pushed the first node too far below the label. Use a
    6px top padding so the first node renders right under its route id.
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/diagram/DiagramDevConsole.java    | 25 ++++++++++++------
 .../resources/camel/diagram/camel-route-diagram.js | 30 ++++++++++++++--------
 .../camel/diagram/DiagramDevConsoleTest.java       |  1 +
 3 files changed, 37 insertions(+), 19 deletions(-)

diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/DiagramDevConsole.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/DiagramDevConsole.java
index b2b7391f531b..67dcf8b59cd6 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/DiagramDevConsole.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/DiagramDevConsole.java
@@ -73,8 +73,8 @@ public class DiagramDevConsole extends AbstractDevConsole {
     public static final String MODE = "mode";
 
     /**
-     * Output format for the HTML rendering: html (default, interactive web 
component) or png (legacy inline image).
-     * Only applies to image themes; ascii/unicode themes always render as 
text.
+     * Output format: html (default, interactive web component), png (inline 
image), text or ascii (ASCII art), unicode
+     * (box-drawing characters).
      */
     public static final String FORMAT = "format";
 
@@ -100,12 +100,14 @@ public class DiagramDevConsole extends AbstractDevConsole 
{
 
         try {
             RouteDiagramDumper dumper = 
PluginHelper.getRouteDiagramDumper(getCamelContext());
+            boolean textFormat = isTextFormat(format);
             if ("topology".equalsIgnoreCase(mode)) {
                 sj.add(doCallTopologyText(dumper, theme, nodeWidth, metric, 
fontSize, refresh));
-            } else if (isTextTheme(theme)) {
+            } else if (isTextTheme(theme) || textFormat) {
+                boolean unicode = isUnicodeTheme(theme) || 
"unicode".equalsIgnoreCase(format);
                 String text = dumper.dumpRoutesAsAsciiArt(filter,
                         
RouteDiagramDumper.NodeLabelMode.valueOf(nodeLabel.toUpperCase()),
-                        nodeWidth, isUnicodeTheme(theme));
+                        nodeWidth, unicode);
                 sj.add(text);
             } else if ("png".equalsIgnoreCase(format)) {
                 BufferedImage image = dumper.dumpRoutesAsImage(filter,
@@ -121,7 +123,7 @@ public class DiagramDevConsole extends AbstractDevConsole {
                 html = "<html>\n" + html + "</html>\n";
                 sj.add(html);
             } else {
-                sj.add(buildRouteWebComponentHtml(filter, refresh));
+                sj.add(buildRouteWebComponentHtml(filter, metric, refresh));
             }
         } catch (Exception e) {
             // ignore
@@ -210,18 +212,21 @@ public class DiagramDevConsole extends AbstractDevConsole 
{
 
     private static final String WEB_COMPONENT_JS = loadWebComponentJs();
 
-    private static String buildRouteWebComponentHtml(String filter, boolean 
refresh) {
+    private static String buildRouteWebComponentHtml(String filter, boolean 
metric, boolean refresh) {
         String f = filter == null ? "*" : filter;
+        String metricAttr = metric ? "" : " metric=\"false\"";
         String refreshAttr = refresh ? " refresh=\"5000\"" : "";
         // inline the web component script: static resource serving is not 
available when only the developer
         // console is enabled (camel run --console). route-structure is a 
sibling console on the same origin.
         return "<html>\n"
                + "  <head>\n"
+               + "    <meta charset=\"utf-8\">\n"
                + "    <script type=\"module\">\n" + WEB_COMPONENT_JS + "\n    
</script>\n"
                + "  </head>\n"
                + "  <body>\n"
-               + String.format("    <camel-route-diagram 
src=\"route-structure\" filter=\"%s\"%s></camel-route-diagram>%n",
-                       escapeAttr(f), refreshAttr)
+               + String.format(
+                       "    <camel-route-diagram src=\"route-structure\" 
filter=\"%s\"%s%s></camel-route-diagram>%n",
+                       escapeAttr(f), metricAttr, refreshAttr)
                + "  </body>\n"
                + "</html>\n";
     }
@@ -247,4 +252,8 @@ public class DiagramDevConsole extends AbstractDevConsole {
         return "unicode".equalsIgnoreCase(theme);
     }
 
+    private static boolean isTextFormat(String format) {
+        return "text".equalsIgnoreCase(format) || 
"ascii".equalsIgnoreCase(format) || "unicode".equalsIgnoreCase(format);
+    }
+
 }
diff --git 
a/components/camel-diagram/src/main/resources/META-INF/resources/camel/diagram/camel-route-diagram.js
 
b/components/camel-diagram/src/main/resources/META-INF/resources/camel/diagram/camel-route-diagram.js
index bf83d53f7d54..48724cae91cd 100644
--- 
a/components/camel-diagram/src/main/resources/META-INF/resources/camel/diagram/camel-route-diagram.js
+++ 
b/components/camel-diagram/src/main/resources/META-INF/resources/camel/diagram/camel-route-diagram.js
@@ -22,6 +22,7 @@ const NODE_H = 36;
 const H_GAP = NODE_W / 2;
 const V_GAP = 40;
 const PADDING = 30;
+const PADDING_TOP = 0;
 const ARROW_SIZE = 6;
 
 const BRANCHING_EIPS = new Set([
@@ -148,16 +149,16 @@ function assignPositions(node, x, y, parentWidth, 
positions) {
 }
 
 function layoutRoute(route) {
-    const nodes = route.code ?? [];
+    const nodes = (route.code ?? []).filter(n => n.type !== 'route');
     if (!nodes.length) {
-        return { positions: {}, width: NODE_W + PADDING * 2, height: NODE_H + 
PADDING * 2 };
+        return { positions: {}, width: NODE_W + PADDING * 2, height: NODE_H + 
PADDING_TOP + PADDING };
     }
 
     const tree = buildTree(nodes);
     computeSubtreeWidth(tree);
 
     const positions = {};
-    assignPositions(tree, PADDING, PADDING, tree.subtreeWidth, positions);
+    assignPositions(tree, PADDING, PADDING_TOP, tree.subtreeWidth, positions);
 
     let maxX = 0;
     let maxYVal = 0;
@@ -221,14 +222,14 @@ function nodeColor(type) {
 function truncate(text, maxLen = 28) {
     if (!text) return '';
     const clean = text.replace(/^\.+/, '');
-    return clean.length > maxLen ? clean.slice(0, maxLen - 1) + '…' : clean;
+    return clean.length > maxLen ? clean.slice(0, maxLen - 1) + '\u2026' : 
clean;
 }
 
 function formatStat(stats) {
     if (!stats) return null;
     const total = stats.exchangesTotal ?? 0;
     const failed = stats.exchangesFailed ?? 0;
-    return `✓${total} ✗${failed}`;
+    return { total, failed };
 }
 
 function esc(s) {
@@ -286,6 +287,7 @@ const COMPONENT_STYLE = `
     font-size: 0.9em;
     padding: 4px 0 2px 0;
     opacity: .8;
+    text-align: center;
   }
   svg { display: block; overflow: visible; }
 `;
@@ -305,11 +307,12 @@ const COMPONENT_STYLE = `
  * @since 4.21
  */
 class CamelRouteDiagram extends HTMLElement {
-    static observedAttributes = ['src', 'refresh', 'filter'];
+    static observedAttributes = ['src', 'refresh', 'filter', 'metric'];
 
     #src = '';
     #refresh = 0;
     #filter = '';
+    #metric = true;
     #timer = null;
     #uid = Math.random().toString(36).slice(2);
     #controller = null;
@@ -347,6 +350,10 @@ class CamelRouteDiagram extends HTMLElement {
                 this.#filter = newValue ?? '';
                 if (this.isConnected) this.#doFetch();
                 break;
+            case 'metric':
+                this.#metric = newValue !== 'false';
+                if (this.isConnected) this.#doFetch();
+                break;
             case 'refresh':
                 this.#refresh = Number(newValue) || 0;
                 if (this.isConnected) this.#scheduleRefresh();
@@ -371,7 +378,7 @@ class CamelRouteDiagram extends HTMLElement {
         try {
             const url = new URL(src, location.href);
             if (this.#filter) url.searchParams.set('filter', this.#filter);
-            url.searchParams.set('metric', 'true');
+            url.searchParams.set('metric', String(this.#metric));
             // Ask for JSON explicitly; the dev console serves plain text for 
a default */* Accept.
             const res = await fetch(url, {
                 signal: this.#controller.signal,
@@ -409,8 +416,8 @@ class CamelRouteDiagram extends HTMLElement {
 
     #buildHTML() {
         const style = `<style>${COMPONENT_STYLE}</style>`;
-        if (this.#error) return `${style}<div class="wrap"><p class="error">⚠ 
${esc(this.#error)}</p></div>`;
-        if (!this.#data) return `${style}<div class="wrap"><p 
class="loading">Loading diagram…</p></div>`;
+        if (this.#error) return `${style}<div class="wrap"><p 
class="error">\u26A0 ${esc(this.#error)}</p></div>`;
+        if (!this.#data) return `${style}<div class="wrap"><p 
class="loading">Loading diagram\u2026</p></div>`;
         return style + `<div class="wrap">${this.#data.routes.map((r, i) => 
this.#routeHTML(r, i)).join('')}</div>`;
     }
 
@@ -484,9 +491,10 @@ class CamelRouteDiagram extends HTMLElement {
         </text>
         ${stat ? `
         <text x="${textX}" y="${pos.y + NODE_H - 3}"
-              text-anchor="start" fill="var(--crd-stat, #64748b)" font-size="9"
+              text-anchor="start" font-size="9"
               clip-path="url(#${clipId})">
-          ${esc(stat)}
+          <tspan fill="#22c55e">${stat.total}</tspan>${stat.failed > 0
+            ? `<tspan dx="6" fill="#ef4444">${stat.failed}</tspan>` : ''}
         </text>` : ''}
         <g transform="translate(${pos.x + 12},${pos.y + (NODE_H - 14) / 2}) 
scale(0.5833)"
               fill="none" stroke="${fill}" stroke-width="2.4"
diff --git 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/DiagramDevConsoleTest.java
 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/DiagramDevConsoleTest.java
index 18ce8c25e205..3dd56acbfb5c 100644
--- 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/DiagramDevConsoleTest.java
+++ 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/DiagramDevConsoleTest.java
@@ -70,6 +70,7 @@ class DiagramDevConsoleTest extends CamelTestSupport {
         String text = (String) console.call(DevConsole.MediaType.TEXT);
         assertThat(text).isNotNull();
         assertThat(text).contains("<html>");
+        assertThat(text).contains("<meta charset=\"utf-8\">");
         assertThat(text).contains("<camel-route-diagram");
         assertThat(text).contains("src=\"route-structure\"");
         assertThat(text).contains("customElements.define");

Reply via email to