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 9cb00776336b CAMEL-26864: camel-jbang - tui_get_diagram returns the
diagram of the Diagram tab as JSON (#29661)
9cb00776336b is described below
commit 9cb00776336bfb16bbf1f98dae224e50175c698d
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Nov 12 07:38:28 2026 +0100
CAMEL-26864: camel-jbang - tui_get_diagram returns the diagram of the
Diagram tab as JSON (#29661)
* CAMEL-26864: camel-jbang - tui_get_diagram returns the diagram of the
Diagram tab as JSON
It returned "No diagram available" since the text diagram was removed. It
now returns what the tab draws, at its level: the groups of the architecture or
the routes of the topology with their links, or the steps of a route with their
parent and children in order. Each box says where it is drawn (row, col,
height, width in characters), so an agent can sketch the diagram in any way it
likes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-26864: camel-jbang - a shorter description of tui_get_diagram, so
the prompt of the AI panel stays within its budget with the other PRs of
CAMEL-26864
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../jbang/core/commands/tui/ArchitectureView.java | 34 +++++
.../dsl/jbang/core/commands/tui/DiagramJson.java | 164 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/DiagramTab.java | 22 ++-
.../core/commands/tui/TuiToolDefinitions.java | 4 +-
.../jbang/core/commands/tui/TuiToolRegistry.java | 2 +-
.../jbang/core/commands/tui/DiagramJsonTest.java | 131 ++++++++++++++++
6 files changed, 353 insertions(+), 4 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
index 1cddd092eec4..d3bc17e738c4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
@@ -58,6 +58,8 @@ import
org.apache.camel.dsl.jbang.core.commands.ai.ProjectOverview;
import org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDensity;
import
org.apache.camel.dsl.jbang.core.commands.tui.diagram.TopologyDiagramWidget;
import
org.apache.camel.dsl.jbang.core.commands.tui.diagram.TopologyDiagramWidget.NodeLine;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
@@ -422,6 +424,38 @@ final class ArchitectureView {
// ---- for the Diagram tab ----
+ /**
+ * The groups as the Diagram tab draws them, for tui_get_diagram: the
boxes and links, each group with its name and
+ * routes. Null while the architecture is not shown.
+ */
+ JsonObject getDataAsJson() {
+ Capabilities caps = capabilities;
+ JsonObject json = active && !suspended ? DiagramJson.boxes(support) :
null;
+ if (json == null || caps == null) {
+ return json;
+ }
+ for (Object o : (JsonArray) json.get("nodes")) {
+ JsonObject n = (JsonObject) o;
+ String id = (String) n.remove("routeId");
+ n.put("id", id);
+ Group g = caps.group(id);
+ if (g != null) {
+ n.put("name", g.name());
+ n.put("routes", new JsonArray(g.routes()));
+ if (!g.entryPoints().isEmpty()) {
+ n.put("entryPoints", new JsonArray(g.entryPoints()));
+ }
+ if (!g.systems().isEmpty()) {
+ n.put("systems", new JsonArray(g.systems()));
+ }
+ if (g.text() != null && !g.text().isBlank()) {
+ n.put("text", g.text().strip());
+ }
+ }
+ }
+ return json;
+ }
+
/** Whether the utility routes are shown; the topology follows the same
setting. */
boolean isShowUtility() {
return showUtility;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramJson.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramJson.java
new file mode 100644
index 000000000000..49435d9ee950
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramJson.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.TreeNode;
+import org.apache.camel.diagram.TopologyAsciiRenderer;
+import
org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget.EipNodeBox;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * The diagram the Diagram tab draws, as data an agent can read or sketch in
any way it likes (tui_get_diagram): its
+ * boxes, where each one is drawn (in characters, the top left box at 0,0) and
how they connect.
+ */
+final class DiagramJson {
+
+ private DiagramJson() {
+ }
+
+ /**
+ * The boxes and links of a graph of boxes: the topology of the routes, or
the architecture of their groups. Each
+ * node is as tui_get_topology has it, with where it is drawn.
+ */
+ static JsonObject boxes(DiagramSupport support) {
+ JsonObject json = support.getTopologyDataAsJson();
+ if (json == null) {
+ return null;
+ }
+ List<int[]> at = new ArrayList<>();
+ Map<String, int[]> byId = new HashMap<>();
+ for (TopologyAsciiRenderer.NodeBox b : support.getNodeBoxes()) {
+ int[] box = { b.startRow(), b.startCol(), b.endRow() -
b.startRow() + 1, b.endCol() - b.startCol() + 1 };
+ at.add(box);
+ byId.putIfAbsent(b.routeId(), box);
+ }
+ int[] origin = origin(at);
+ for (Object o : (JsonArray) json.get("nodes")) {
+ JsonObject n = (JsonObject) o;
+ putBox(n, byId.get(n.getString("routeId")), origin);
+ }
+ return json;
+ }
+
+ /**
+ * The steps of a route, in the order they are drawn: each with its id,
type and label, the step it is in
+ * ({@code parent}) and the steps in it in order ({@code children}), where
it is drawn, and its numbers.
+ */
+ static JsonObject route(LayoutRoute route, List<EipNodeBox> drawn) {
+ if (route == null) {
+ return null;
+ }
+ List<int[]> at = new ArrayList<>();
+ Map<LayoutNode, int[]> byNode = new HashMap<>();
+ for (EipNodeBox b : drawn) {
+ int[] box = { b.startRow(), b.startCol(), b.endRow() -
b.startRow() + 1, b.endCol() - b.startCol() + 1 };
+ at.add(box);
+ byNode.putIfAbsent(b.layoutNode(), box);
+ }
+ int[] origin = origin(at);
+ JsonArray nodes = new JsonArray();
+ for (LayoutNode ln : route.nodes) {
+ if ("route".equals(ln.type)) {
+ continue;
+ }
+ JsonObject n = new JsonObject();
+ n.put("id", ln.id);
+ n.put("type", ln.type);
+ String label = ln.wrappedLines != null ? String.join(" ",
ln.wrappedLines).strip() : "";
+ if (!label.isEmpty()) {
+ n.put("label", label);
+ }
+ TreeNode tn = ln.treeNode;
+ if (tn != null) {
+ addStep(n, tn);
+ }
+ putBox(n, byNode.get(ln), origin);
+ nodes.add(n);
+ }
+ JsonObject json = new JsonObject();
+ json.put("routeId", route.routeId);
+ if (route.source != null) {
+ json.put("source", route.source);
+ }
+ json.put("nodes", nodes);
+ json.put("totalNodes", nodes.size());
+ return json;
+ }
+
+ private static void addStep(JsonObject n, TreeNode tn) {
+ NodeInfo info = tn.info;
+ if (tn.parent != null && tn.parent.info != null &&
!"route".equals(tn.parent.info.type)) {
+ n.put("parent", tn.parent.info.id);
+ }
+ if (!tn.children.isEmpty()) {
+ JsonArray children = new JsonArray();
+ tn.children.forEach(c -> children.add(c.info.id));
+ n.put("children", children);
+ }
+ if (info.uri != null) {
+ n.put("uri", info.uri);
+ n.put("remote", info.remote);
+ }
+ if (info.description != null) {
+ n.put("description", info.description);
+ }
+ if (info.line > 0) {
+ n.put("line", info.line);
+ }
+ if (info.stat != null) {
+ n.put("exchangesTotal", info.stat.exchangesTotal);
+ n.put("exchangesFailed", info.stat.exchangesFailed);
+ n.put("meanProcessingTime", info.stat.meanProcessingTime);
+ }
+ if (!info.bodyTypes.isEmpty()) {
+ n.put("bodyTypes", new JsonArray(info.bodyTypes));
+ }
+ if (!info.problems.isEmpty()) {
+ n.put("problems", new JsonArray(info.problems));
+ }
+ }
+
+ /** The top left of the drawing: the first row and column with a box. */
+ private static int[] origin(List<int[]> boxes) {
+ int row = Integer.MAX_VALUE;
+ int col = Integer.MAX_VALUE;
+ for (int[] b : boxes) {
+ row = Math.min(row, b[0]);
+ col = Math.min(col, b[1]);
+ }
+ return boxes.isEmpty() ? new int[] { 0, 0 } : new int[] { row, col };
+ }
+
+ /** Where a box is drawn: row and col from the top left box, and its size;
nothing for a box not drawn (folded). */
+ private static void putBox(JsonObject n, int[] box, int[] origin) {
+ if (box != null) {
+ n.put("row", box[0] - origin[0]);
+ n.put("col", box[1] - origin[1]);
+ n.put("height", box[2]);
+ n.put("width", box[3]);
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index 44200a466296..8af2ca74aff3 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -761,7 +762,26 @@ class DiagramTab extends AbstractDiagramTab {
@Override
public JsonObject getTableDataAsJson() {
- return null;
+ return getDiagramAsJson();
+ }
+
+ /**
+ * What the tab draws now, as data an agent can read or sketch
(tui_get_diagram): the groups of the architecture,
+ * the topology of the routes, or the steps of a route, each box with
where it is drawn.
+ */
+ JsonObject getDiagramAsJson() {
+ Level level = level();
+ JsonObject json = switch (level) {
+ case ARCHITECTURE -> architecture.getDataAsJson();
+ case TOPOLOGY -> diagram.isShowDiagram() ?
DiagramJson.boxes(diagram) : null;
+ case ROUTE -> drillDownRouteId != null
+ ?
DiagramJson.route(diagram.getRouteLayout(drillDownRouteId),
diagram.getEipNodeBoxes()) : null;
+ };
+ if (json != null) {
+ json.put("tab", "Diagram");
+ json.put("level", level.name().toLowerCase(Locale.ROOT));
+ }
+ return json;
}
JsonObject getTopologyDataAsJson() {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
index 465fb7370674..a1a6570bd2f7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
@@ -366,8 +366,8 @@ final class TuiToolDefinitions {
List.of("theme")), r::callSetTheme));
tools.add(tool(toolDef(
"tui_get_diagram",
- "Returns the route topology diagram as text. "
- + "Shows the ASCII/Unicode art diagram of
routes and their connections.",
+ "The Diagram tab's diagram as JSON: level, boxes (row, col,
height, width) and links, "
+ + "or a route's steps.",
Map.of()), args -> r.callGetDiagram(), READ_ONLY));
tools.add(tool(toolDef(
"tui_get_history",
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index bd1277247f67..7817fd1cd633 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -1223,7 +1223,7 @@ class TuiToolRegistry {
String callGetDiagram() {
JsonObject data = facade.getDiagramData();
if (data == null) {
- return "No diagram available. Navigate to the Diagram tab first.";
+ return "No diagram yet: the Diagram tab draws one when it is open
(tui_navigate tab=Diagram).";
}
return Jsoner.serialize(data);
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramJsonTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramJsonTest.java
new file mode 100644
index 000000000000..96c5059903eb
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramJsonTest.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyEdgeInfo;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyNodeInfo;
+import org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-26864: tui_get_diagram returned "No diagram available" since the text
diagram was removed. It returns the
+ * diagram the Diagram tab draws as JSON: the boxes, where each is drawn, and
how they connect.
+ */
+class DiagramJsonTest {
+
+ @Test
+ void theTopologyHasItsBoxesAndWhereTheyAreDrawn() {
+ DiagramTab tab = tab();
+ TopologyEdgeInfo edge = new TopologyEdgeInfo();
+ edge.fromRouteId = "hello";
+ edge.toRouteId = "seven";
+ edge.endpoint = "direct://seven";
+ tab.diagram.showCustomTopology(List.of(node("hello"), node("seven")),
List.of(edge), Map.of(), null);
+
+ JsonObject json = tab.getDiagramAsJson();
+
+ assertThat(json.getString("level")).isEqualTo("topology");
+ JsonArray nodes = (JsonArray) json.get("nodes");
+ assertThat(nodes).hasSize(2);
+ JsonObject hello = (JsonObject) nodes.stream().filter(n ->
"hello".equals(((JsonObject) n).getString("routeId")))
+ .findFirst().orElseThrow();
+ JsonObject seven = (JsonObject) nodes.stream().filter(n ->
"seven".equals(((JsonObject) n).getString("routeId")))
+ .findFirst().orElseThrow();
+ assertThat(hello.getInteger("width")).isPositive();
+ assertThat(hello.getInteger("height")).isPositive();
+ assertThat(seven.getInteger("row")).as("seven is drawn below hello,
which sends to it")
+ .isGreaterThan(hello.getInteger("row"));
+ JsonObject e = (JsonObject) ((JsonArray) json.get("edges")).get(0);
+ assertThat(e.getString("from")).isEqualTo("hello");
+ assertThat(e.getString("to")).isEqualTo("seven");
+ }
+
+ @Test
+ void aRouteHasItsStepsInOrderWithTheirBranches() {
+ RouteInfo route = new RouteInfo();
+ route.routeId = "hello";
+ route.nodes.add(step("route", "hello", 0));
+ route.nodes.add(step("from", "hello", 0));
+ route.nodes.add(step("setBody", "setBody1", 1));
+ route.nodes.add(step("choice", "choice1", 1));
+ route.nodes.add(step("when", "when1", 2));
+ route.nodes.add(step("to", "to1", 3));
+ route.nodes.add(step("otherwise", "otherwise1", 2));
+ route.nodes.add(step("log", "log1", 3));
+ RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+ LayoutRoute layout = engine.layoutRoute(route, 0);
+ RouteDiagramWidget widget = new RouteDiagramWidget(layout,
engine.getNodeWidth(), -1, 0, 0, false, Map.of());
+ widget.render(new Rect(0, 0, 200, 60), Buffer.empty(new Rect(0, 0,
200, 60)));
+
+ JsonObject json = DiagramJson.route(layout, widget.getNodeBoxes());
+
+ assertThat(json.getString("routeId")).isEqualTo("hello");
+ JsonArray nodes = (JsonArray) json.get("nodes");
+ assertThat(nodes.stream().map(n -> ((JsonObject)
n).getString("id")).toList())
+ .containsExactly("hello", "setBody1", "choice1", "when1",
"to1", "otherwise1", "log1");
+ JsonObject from = (JsonObject) nodes.get(0);
+ assertThat(from.getString("type")).isEqualTo("from");
+ assertThat(from.getInteger("row")).isZero();
+ JsonObject choice = (JsonObject) nodes.get(2);
+ assertThat(((JsonArray)
choice.get("children")).stream().map(Object::toString).toList())
+ .containsExactly("when1", "otherwise1");
+ JsonObject to = (JsonObject) nodes.get(4);
+ assertThat(to.getString("parent")).isEqualTo("when1");
+
assertThat(to.getInteger("row")).isGreaterThan(choice.getInteger("row"));
+ assertThat(to.getInteger("width")).isPositive();
+ }
+
+ private static DiagramTab tab() {
+ IntegrationInfo info = new IntegrationInfo();
+ info.pid = "1234";
+ info.name = "hello";
+ MonitorContext ctx = new MonitorContext(new
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+ ctx.selectedPid = "1234";
+ return new DiagramTab(ctx);
+ }
+
+ private static TopologyNodeInfo node(String id) {
+ TopologyNodeInfo n = new TopologyNodeInfo();
+ n.routeId = id;
+ n.from = "";
+ n.nodeType = "route";
+ return n;
+ }
+
+ private static NodeInfo step(String type, String id, int level) {
+ NodeInfo n = new NodeInfo();
+ n.type = type;
+ n.id = id;
+ n.code = type;
+ n.level = level;
+ return n;
+ }
+}