This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch tui-about-route-names in repository https://gitbox.apache.org/repos/asf/camel.git
commit 35ce17843caae1c713ddca8ad5c264580596527c Author: Claus Ibsen <[email protected]> AuthorDate: Tue Nov 10 23:03:19 2026 +0100 chore: camel-jbang - About names a long route and the route of the API description in full, and an encoded out is the one the routes name From walking the route, connect and contracts examples: - What happened shows content-based-router in full when the strips leave room, not content-based-rout.... - The route Camel adds to serve the API description of the REST DSL (route1 from rest-api://openapi) is named by its endpoint on its strip. - AboutReport.baseUri decodes the escapes Camel normalizes a URI with, so rest-openapi://stock-api.json%23reserveStock is the rest-openapi:stock-api.json#reserveStock the route names: openapi-client had its out twice, once without its calls. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/ai/AboutReport.java | 18 ++++++ .../dsl/jbang/core/commands/ai/AboutTest.java | 5 ++ .../dsl/jbang/core/commands/tui/AboutView.java | 26 ++++++++- .../dsl/jbang/core/commands/tui/AboutViewTest.java | 67 ++++++++++++++++++++++ .../jbang/core/commands/tui/ObservedOutsTest.java | 16 ++++++ 5 files changed, 130 insertions(+), 2 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutReport.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutReport.java index a99b40430c9e..45d5396e885f 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutReport.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutReport.java @@ -17,6 +17,8 @@ package org.apache.camel.dsl.jbang.core.commands.ai; import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -347,9 +349,25 @@ public final class AboutReport { int q = uri.indexOf('?'); String answer = q >= 0 ? uri.substring(0, q) : uri; answer = answer.replaceFirst("^([a-zA-Z0-9+.-]+):/{0,3}", "$1:"); + // the runtime has the URI encoded as Camel normalizes it (rest-openapi://stock-api.json%23reserveStock, + // platform-http:///stock/%7Bsku%7D), the sources as written + answer = decoded(answer); return answer.endsWith("/") ? answer.substring(0, answer.length() - 1) : answer; } + private static String decoded(String uri) { + if (uri.indexOf('%') < 0) { + return uri; + } + try { + // a + is a plus in a path, not a space + return URLDecoder.decode(uri.replace("+", "%2B"), StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + // a % that is no escape + return uri; + } + } + private static String total(JsonObject jo) { if (jo.get("exchangesTotal") == null) { return ""; diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java index 73b26d7c27c3..ec0fdf4ff262 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java @@ -176,6 +176,11 @@ class AboutTest { assertThat(AboutReport.baseUri("file:/tmp/in?noop=true")).isEqualTo(AboutReport.baseUri("file:///tmp/in")); assertThat(AboutReport.baseUri("kafka://orders")).isEqualTo("kafka:orders"); assertThat(AboutReport.baseUri("direct:orders")).isEqualTo("direct:orders"); + // the runtime has it encoded, the sources as written (contracts/openapi-client, connect/stock-api) + assertThat(AboutReport.baseUri("rest-openapi://stock-api.json%23reserveStock")) + .isEqualTo(AboutReport.baseUri("rest-openapi:stock-api.json#reserveStock")); + assertThat(AboutReport.baseUri("platform-http:///stock/%7Bsku%7D")).isEqualTo("platform-http:stock/{sku}"); + assertThat(AboutReport.baseUri("http:shop/a+b/100%")).isEqualTo("http:shop/a+b/100%"); } @Test diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java index 80058d5e2230..fc122d35962e 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java @@ -485,6 +485,24 @@ final class AboutView { * The routes in the order of the Routes panel, where a route comes after the one that calls it, then those it does * not have; the integration lists its routes by id, which puts a called route before its caller. */ + /** + * The name of a route on its strip: its id, or for a route the sources do not have and Camel named route1, its + * endpoint (rest-api:openapi, the route that serves the API description of the REST DSL). + */ + private String stripName(IntegrationInfo info, String routeId) { + About.Model m = model; + if (m == null || !routeId.matches("route\\d+") || m.routes().stream().anyMatch(r -> routeId.equals(r.routeId()))) { + return routeId; + } + String from = info.routes.stream().filter(r -> routeId.equals(r.routeId) && r.from != null).map(r -> r.from) + .findFirst().orElse(null); + if (from == null) { + return routeId; + } + int q = from.indexOf('?'); + return (q >= 0 ? from.substring(0, q) : from).replaceFirst("://", ":"); + } + private List<String> routeOrder(IntegrationInfo info) { List<String> running = info.routes.stream().map(r -> r.routeId).filter(id -> id != null).toList(); List<String> order = new ArrayList<>(); @@ -720,7 +738,8 @@ final class AboutView { int seconds = seconds(); List<String> order = routeOrder(info); // the names, then one space: every line starts its columns at the same place - int label = Math.min(18, Math.max(8, order.stream().mapToInt(String::length).max().orElse(8))) + 4; + int longest = Math.max(8, order.stream().mapToInt(id -> stripName(info, id).length()).max().orElse(8)); + int label = Math.min(18, longest) + 4; List<Timeline.RouteSeries> series = ctx.timeline.routes(info.pid, order, end, seconds); // what the line of a route has after its strip (its exchanges, an open circuit breaker, the worst that // happened, with the cause of a recovery up to CAUSE_WIDTH) takes the room it needs, and the strips the rest @@ -729,6 +748,8 @@ final class AboutView { notes = Math.max(notes, width(stripNotes(info, r, last, end, seconds, CAUSE_WIDTH))); } int columns = stripColumns(seconds, width - label - notes - 1); + // a longer name in full, in what the strips leave (content-based-router) + label += Math.max(0, Math.min(longest + 4 - label, width - label - notes - 1 - columns)); // the cause gets what the strips leave too int causeRoom = CAUSE_WIDTH + Math.max(0, width - label - notes - 1 - columns); stripColumns = columns; @@ -741,7 +762,8 @@ final class AboutView { if (spaced && !lines.isEmpty()) { lines.add(Line.from(Span.raw(""))); } - String name = r.route().length() > label - 3 ? r.route().substring(0, label - 4) + "…" : r.route(); + String name = stripName(info, r.route()); + name = name.length() > label - 3 ? name.substring(0, label - 4) + "…" : name; long max = 1; // completed, recovered, handled, failed, down, redelivery attempts: the height is the exchanges done, the // attempts only colour the column (an exchange retried many times is still one exchange) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java index f748a70566ba..1f8c6b769ee4 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java @@ -444,6 +444,73 @@ class AboutViewTest { assertThat(about.tableJson(info).getString("keys")).contains("r the README, s the summary"); } + @Test + void theRouteCamelAddsForTheApiDescriptionIsNamedByItsEndpoint() throws Exception { + // contracts/rest-code-first: the REST DSL serves /openapi from a route Camel adds and names route1 + Files.writeString(dir.resolve("rest-code-first.camel.yaml"), """ + - route: + id: list-orders + from: + uri: direct:list-orders + steps: + - log: listed + """); + IntegrationInfo info = new IntegrationInfo(); + info.pid = "1234"; + info.name = "rest-code-first"; + info.directory = dir.toString(); + for (String[] r : List.of(new String[] { "list-orders", "direct://list-orders" }, + new String[] { "route1", "rest-api://openapi" })) { + RouteInfo route = new RouteInfo(); + route.routeId = r[0]; + route.from = r[1]; + route.state = "Started"; + info.routes.add(route); + } + TimelineJson t = new TimelineJson(T0 / 1000 + 59, 60); + t.route("list-orders", "completed", 55, 1).route("route1", "completed", 56, 1); + MonitorContext ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new AtomicReference<>(List.of())); + ctx.timeline.merge("1234", t.json(), true); + AboutView about = new AboutView(ctx); + + await().atMost(20, TimeUnit.SECONDS).untilAsserted(() -> { + List<String> strips = render(about, info, 200, 34).lines().dropWhile(l -> !l.contains("What happened")) + .filter(l -> l.contains(" list-orders ") || l.contains(" rest-api:openapi ")).toList(); + assertThat(strips).hasSize(2); + }); + assertThat(render(about, info, 200, 34)).doesNotContain(" route1 "); + } + + @Test + void aLongRouteNameIsShownInFullWhenTheStripsLeaveRoom() throws Exception { + // route/content-based-router: its one route has the name of the example + Files.writeString(dir.resolve("content-based-router.camel.yaml"), """ + - route: + id: content-based-router + from: + uri: file:orders + steps: + - log: routed + """); + IntegrationInfo info = new IntegrationInfo(); + info.pid = "1234"; + info.name = "content-based-router"; + info.directory = dir.toString(); + RouteInfo route = new RouteInfo(); + route.routeId = "content-based-router"; + route.state = "Started"; + info.routes.add(route); + TimelineJson t = new TimelineJson(T0 / 1000 + 59, 60); + t.route("content-based-router", "completed", 55, 3); + MonitorContext ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new AtomicReference<>(List.of())); + ctx.timeline.merge("1234", t.json(), true); + AboutView about = new AboutView(ctx); + + await().atMost(20, TimeUnit.SECONDS).untilAsserted(() -> assertThat(render(about, info, 200, 34).lines() + .dropWhile(l -> !l.contains("What happened")).filter(l -> l.contains("content-based-rout")).toList()) + .singleElement().satisfies(l -> assertThat(l).contains("content-based-router ").doesNotContain("…"))); + } + @Test void lastFiveMinutesCountsAnExchangeOnceNotOncePerRouteItPasses() throws Exception { // fail-well/circuit-breaker: stock-check calls supplier with direct, once a second diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ObservedOutsTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ObservedOutsTest.java index d7f9c3f9af73..3985234d4034 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ObservedOutsTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ObservedOutsTest.java @@ -64,6 +64,22 @@ class ObservedOutsTest { assertThat(ObservedOuts.of(info, List.of())).extracting(About.Out::uri).containsExactly("kafka:audit"); } + @Test + void anOutTheRuntimeHasEncodedIsTheOneOfTheRoutes() { + // contracts/openapi-client: the route names rest-openapi:stock-api.json#reserveStock, the runtime counts it + // as rest-openapi://stock-api.json%23reserveStock + IntegrationInfo info = new IntegrationInfo(); + info.endpoints.add(endpoint("reserve-order-lines", "out", "rest-openapi://stock-api.json%23reserveStock", + "remote", 6)); + List<About.Out> known = List.of(new About.Out( + "http", "rest-openapi:stock-api.json#reserveStock", "out", + "reserve-order-lines")); + + assertThat(ObservedOuts.of(info, known)).isEmpty(); + assertThat(AboutView.outHits(info, "rest-openapi:stock-api.json#reserveStock", "reserve-order-lines")) + .isEqualTo(6); + } + @Test void theServiceOfAUriIsAllButItsLastSegment() { assertThat(ObservedOuts.group("http://localhost:8080/stock/CAMEL-MUG")).isEqualTo("http://localhost:8080/stock/");
