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 18c687c491f0 CAMEL-24233: Add EIP detail panel to Diagram tab
drill-down view
18c687c491f0 is described below
commit 18c687c491f00abf4c9ced5fc29a5d45061feb6c
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Jul 23 16:24:48 2026 +0200
CAMEL-24233: Add EIP detail panel to Diagram tab drill-down view
New processor-detail dev console returns all configured options for
every processor in a route (including from endpoint). Parses route XML
to extract attributes and expression children. DiagramTab shows an
inline detail panel (press d) with catalog-enriched option descriptions.
One async IPC call per route, cached for instant navigation.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../apache/camel/catalog/dev-consoles.properties | 1 +
.../catalog/dev-consoles/processor-detail.json | 31 ++
.../apache/camel/dev-console/processor-detail.json | 31 ++
.../org/apache/camel/dev-console/processor-detail | 2 +
.../org/apache/camel/dev-consoles.properties | 2 +-
.../impl/console/ProcessorDetailDevConsole.java | 243 +++++++++++++
.../camel/cli/connector/LocalCliConnector.java | 16 +
.../dsl/jbang/core/commands/tui/DiagramTab.java | 380 ++++++++++++++++++++-
.../dsl/jbang/core/commands/tui/EndpointsTab.java | 2 +-
9 files changed, 703 insertions(+), 5 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
index 434dd5ca3d55..716fed55a924 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
@@ -42,6 +42,7 @@ micrometer
opentelemetry
platform-http
processor
+processor-detail
producer
properties
quartz
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/processor-detail.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/processor-detail.json
new file mode 100644
index 000000000000..2ea6f8d4be50
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/processor-detail.json
@@ -0,0 +1,31 @@
+{
+ "console": {
+ "kind": "console",
+ "group": "camel",
+ "name": "processor-detail",
+ "title": "Processor Detail",
+ "description": "Show configured options for all processors in a route",
+ "deprecated": false,
+ "javaType": "org.apache.camel.impl.console.ProcessorDetailDevConsole",
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-console",
+ "version": "4.22.0-SNAPSHOT"
+ },
+ "options": {
+ "routeId": {
+ "index": 0,
+ "kind": "option",
+ "displayName": "Route Id",
+ "group": "query",
+ "label": "query",
+ "required": true,
+ "type": "string",
+ "javaType": "java.lang.String",
+ "deprecated": false,
+ "autowired": false,
+ "secret": false,
+ "description": "The route id to get processor details for"
+ }
+ }
+}
+
diff --git
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/processor-detail.json
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/processor-detail.json
new file mode 100644
index 000000000000..2ea6f8d4be50
--- /dev/null
+++
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/processor-detail.json
@@ -0,0 +1,31 @@
+{
+ "console": {
+ "kind": "console",
+ "group": "camel",
+ "name": "processor-detail",
+ "title": "Processor Detail",
+ "description": "Show configured options for all processors in a route",
+ "deprecated": false,
+ "javaType": "org.apache.camel.impl.console.ProcessorDetailDevConsole",
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-console",
+ "version": "4.22.0-SNAPSHOT"
+ },
+ "options": {
+ "routeId": {
+ "index": 0,
+ "kind": "option",
+ "displayName": "Route Id",
+ "group": "query",
+ "label": "query",
+ "required": true,
+ "type": "string",
+ "javaType": "java.lang.String",
+ "deprecated": false,
+ "autowired": false,
+ "secret": false,
+ "description": "The route id to get processor details for"
+ }
+ }
+}
+
diff --git
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/processor-detail
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/processor-detail
new file mode 100644
index 000000000000..db1623e64a0f
--- /dev/null
+++
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/processor-detail
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.ProcessorDetailDevConsole
diff --git
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
index 10e14834d7af..6fd92f94c447 100644
---
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
+++
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
@@ -1,5 +1,5 @@
# Generated by camel build tools - do NOT edit this file!
-dev-consoles=activity api bean blocked browse circuit-breaker consumer context
datasource debug endpoint errors eval-language event gc health heap-dump
heap-histogram inflight internal-tasks java-security jfr-memory-leak jvm log
memory message-history processor producer properties receive reload rest
rest-spec route route-controller route-dump route-group route-structure
route-topology send service simple-language source sql-query sql-trace
startup-recorder system-properties thread top t [...]
+dev-consoles=activity api bean blocked browse circuit-breaker consumer context
datasource debug endpoint errors eval-language event gc health heap-dump
heap-histogram inflight internal-tasks java-security jfr-memory-leak jvm log
memory message-history processor processor-detail producer properties receive
reload rest rest-spec route route-controller route-dump route-group
route-structure route-topology send service simple-language source sql-query
sql-trace startup-recorder system-proper [...]
groupId=org.apache.camel
artifactId=camel-console
version=4.22.0-SNAPSHOT
diff --git
a/core/camel-console/src/main/java/org/apache/camel/impl/console/ProcessorDetailDevConsole.java
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ProcessorDetailDevConsole.java
new file mode 100644
index 000000000000..186cdd488486
--- /dev/null
+++
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ProcessorDetailDevConsole.java
@@ -0,0 +1,243 @@
+/*
+ * 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.impl.console;
+
+import java.io.StringReader;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import org.xml.sax.InputSource;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.api.management.ManagedCamelContext;
+import org.apache.camel.api.management.mbean.ManagedRouteMBean;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+@DevConsole(name = "processor-detail", description = "Show configured options
for all processors in a route")
+public class ProcessorDetailDevConsole extends AbstractDevConsole {
+
+ @Metadata(label = "query", required = true,
+ description = "The route id to get processor details for",
+ javaType = "java.lang.String")
+ public static final String ROUTE_ID = "routeId";
+
+ public ProcessorDetailDevConsole() {
+ super("camel", "processor-detail", "Processor Detail", "Show
configured options for all processors in a route");
+ }
+
+ @Override
+ protected String doCallText(Map<String, Object> options) {
+ JsonObject json = doCallJson(options);
+ StringBuilder sb = new StringBuilder();
+
+ String routeId = json.getString("routeId");
+ if (routeId != null) {
+ sb.append(String.format("Route: %s%n", routeId));
+ JsonArray processors = (JsonArray) json.get("processors");
+ if (processors != null) {
+ for (Object obj : processors) {
+ JsonObject p = (JsonObject) obj;
+ sb.append(String.format(" %s (%s)%n", p.getString("id"),
p.getString("type")));
+ JsonObject opts = p.getMap("options");
+ if (opts != null) {
+ for (Map.Entry<String, Object> e : opts.entrySet()) {
+ sb.append(String.format(" %s = %s%n",
e.getKey(), e.getValue()));
+ }
+ }
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+ @Override
+ protected JsonObject doCallJson(Map<String, Object> options) {
+ String path = (String) options.get(Exchange.HTTP_PATH);
+ String subPath = path != null ? StringHelper.after(path, "/") : null;
+ String routeId = optionString(options, ROUTE_ID);
+ if (routeId == null || routeId.isBlank()) {
+ routeId = subPath;
+ }
+
+ JsonObject root = new JsonObject();
+ if (routeId == null || routeId.isBlank()) {
+ return root;
+ }
+
+ ManagedCamelContext mcc
+ =
getCamelContext().getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+ if (mcc == null) {
+ return root;
+ }
+
+ ManagedRouteMBean mr = mcc.getManagedRoute(routeId);
+ if (mr == null) {
+ return root;
+ }
+
+ root.put("routeId", routeId);
+ JsonArray processors = new JsonArray();
+
+ // add the "from" node (uses the route ID as its processor ID in the
diagram)
+ JsonObject fromEntry = new JsonObject();
+ fromEntry.put("id", routeId);
+ fromEntry.put("type", "from");
+ fromEntry.put("endpointUri", mr.getEndpointUri());
+ fromEntry.put("options", new JsonObject());
+ processors.add(fromEntry);
+
+ // parse the route XML to extract all processor elements with their
configured options
+ try {
+ String xml = mr.dumpRouteAsXml();
+ if (xml != null && !xml.isBlank()) {
+ DocumentBuilderFactory factory =
DocumentBuilderFactory.newInstance();
+
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",
true);
+
factory.setFeature("http://xml.org/sax/features/external-general-entities",
false);
+
factory.setFeature("http://xml.org/sax/features/external-parameter-entities",
false);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document doc = builder.parse(new InputSource(new
StringReader(xml)));
+ Element routeElement = doc.getDocumentElement();
+
+ collectProcessors(routeElement, processors);
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+
+ root.put("processors", processors);
+ return root;
+ }
+
+ private static void collectProcessors(Element parent, JsonArray
processors) {
+ NodeList children = parent.getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+ Element elem = (Element) node;
+ String id = elem.getAttribute("id");
+ if (id == null || id.isEmpty()) {
+ // recurse into structural elements that may contain processors
+ collectProcessors(elem, processors);
+ continue;
+ }
+
+ String type = elem.getTagName();
+ JsonObject entry = new JsonObject();
+ entry.put("id", id);
+ entry.put("type", type);
+
+ JsonObject opts = new JsonObject();
+ NamedNodeMap attrs = elem.getAttributes();
+ for (int j = 0; j < attrs.getLength(); j++) {
+ Attr attr = (Attr) attrs.item(j);
+ String name = attr.getName();
+ if (!"id".equals(name) && !"customId".equals(name) &&
!name.startsWith("xmlns")) {
+ opts.put(name, attr.getValue());
+ }
+ }
+
+ // collect expression/language child elements (e.g. <simple>,
<jsonpath>, <header>,
+ // <correlationExpression>, <completionPredicate>, etc.)
+ collectExpressionChildren(elem, opts);
+
+ entry.put("options", opts);
+ processors.add(entry);
+
+ // recurse into child elements (nested EIPs like split > to,
choice > when > to)
+ collectProcessors(elem, processors);
+ }
+ }
+
+ private static void collectExpressionChildren(Element parent, JsonObject
opts) {
+ NodeList children = parent.getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+ Element child = (Element) node;
+
+ // skip child elements that are processors (have an id or contain
processors with ids)
+ if (hasProcessorDescendants(child)) {
+ continue;
+ }
+
+ String tag = child.getTagName();
+ String text = child.getTextContent();
+ if (text != null) {
+ text = text.trim();
+ }
+
+ // check if this is a wrapper element (e.g.
<correlationExpression>, <completionPredicate>)
+ // that contains a language element inside
+ Element langChild = findLanguageChild(child);
+ if (langChild != null) {
+ String langTag = langChild.getTagName();
+ String langText = langChild.getTextContent();
+ if (langText != null) {
+ langText = langText.trim();
+ }
+ opts.put(tag, langTag + (langText != null &&
!langText.isEmpty() ? "(" + langText + ")" : ""));
+ } else if (text != null && !text.isEmpty()) {
+ // direct expression element (e.g. <simple>..., <jsonpath>...,
<header>...)
+ opts.put(tag, text);
+ }
+ }
+ }
+
+ private static boolean hasProcessorDescendants(Element elem) {
+ String id = elem.getAttribute("id");
+ if (id != null && !id.isEmpty()) {
+ return true;
+ }
+ NodeList children = elem.getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE &&
hasProcessorDescendants((Element) node)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static Element findLanguageChild(Element parent) {
+ NodeList children = parent.getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ return (Element) node;
+ }
+ }
+ return null;
+ }
+}
diff --git
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index 9ac743df5eac..de21d1a064d7 100644
---
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -352,6 +352,8 @@ public class LocalCliConnector extends ServiceSupport
implements CliConnector, C
doActionRouteStructureTask(root);
} else if ("route-topology".equals(action)) {
doActionRouteTopologyTask(root);
+ } else if ("processor-detail".equals(action)) {
+ doActionProcessorDetailTask(root);
} else if ("route-controller".equals(action)) {
doActionRouteControllerTask(root);
} else if ("startup-recorder".equals(action)) {
@@ -782,6 +784,20 @@ public class LocalCliConnector extends ServiceSupport
implements CliConnector, C
}
}
+ private void doActionProcessorDetailTask(JsonObject root) throws Exception
{
+ DevConsole dc =
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+ .resolveById("processor-detail");
+ if (dc != null) {
+ String routeId = root.getString("routeId");
+ JsonObject json
+ = (JsonObject) dc.call(DevConsole.MediaType.JSON,
Map.of("routeId", routeId != null ? routeId : ""));
+ LOG.trace("Updating output file: {}", outputFile);
+ IOHelper.writeText(json.toJson(), outputFile);
+ } else {
+ IOHelper.writeText("{}", outputFile);
+ }
+ }
+
private void doActionSourceTask(JsonObject root) throws Exception {
DevConsole dc =
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
.resolveById("source");
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 bce158122b1f..15eca4f9c1c2 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
@@ -16,26 +16,43 @@
*/
package org.apache.camel.dsl.jbang.core.commands.tui;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import dev.tamboui.layout.Constraint;
import dev.tamboui.layout.Layout;
import dev.tamboui.layout.Rect;
+import dev.tamboui.markdown.MarkdownView;
import dev.tamboui.style.Color;
import dev.tamboui.style.Style;
import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.CharWidth;
import dev.tamboui.text.Line;
import dev.tamboui.text.Span;
import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.tui.event.MouseEventKind;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.tooling.model.BaseOptionModel;
+import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.tooling.model.EipModel;
import org.apache.camel.util.TimeUtils;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
@@ -55,6 +72,18 @@ class DiagramTab extends AbstractTab {
private int infoPanelWidth = 30;
private final DragSplit hSplit = new DragSplit();
+ private boolean detailMode;
+ private int detailScroll;
+ private Rect lastDetailArea;
+ private final DragSplit vSplit = new DragSplit();
+ private final Map<String, CamelCatalog> catalogCache = new HashMap<>();
+ private final Set<String> catalogLoadFailed = new HashSet<>();
+ private volatile JsonObject cachedRouteDetail;
+ private volatile String cachedRouteDetailId;
+ private volatile boolean detailLoading;
+ private volatile String detailLoadingRouteId;
+ private String lastDetailNodeId;
+
DiagramTab(MonitorContext ctx) {
super(ctx);
}
@@ -151,12 +180,41 @@ class DiagramTab extends AbstractTab {
}
}
+ // Toggle detail panel in drill-down mode
+ if (!topologyMode && diagram.isShowDiagram() &&
!diagram.getEipNodeBoxes().isEmpty()
+ && ke.isCharIgnoreCase('d')) {
+ detailMode = !detailMode;
+ detailScroll = 0;
+ cachedRouteDetail = null;
+ cachedRouteDetailId = null;
+ detailLoadingRouteId = null;
+ detailLoading = false;
+ return true;
+ }
+
+ // Detail panel scroll
+ if (!topologyMode && detailMode && diagram.isShowDiagram()) {
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+ detailScroll = Math.max(0, detailScroll - 10);
+ return true;
+ }
+ if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+ detailScroll += 10;
+ return true;
+ }
+ }
+
// Jump back to topology from any depth
if (!topologyMode && diagram.isShowDiagram() && ke.isChar('t')) {
routeNavigationStack.clear();
diagram.setPendingSelectionRouteId(drillDownRouteId);
drillDownRouteId = null;
topologyMode = true;
+ detailMode = false;
+ cachedRouteDetail = null;
+ cachedRouteDetailId = null;
+ detailLoadingRouteId = null;
+ detailLoading = false;
diagram.setTopologyMode(true);
diagram.setSelectedEipNodeIndex(-1);
diagram.resetScroll();
@@ -248,6 +306,19 @@ class DiagramTab extends AbstractTab {
@Override
public boolean handleMouseEvent(MouseEvent me, Rect area) {
+ if (detailMode && lastDetailArea != null &&
lastDetailArea.contains(me.x(), me.y())) {
+ if (me.kind() == MouseEventKind.SCROLL_UP) {
+ detailScroll = Math.max(0, detailScroll - 3);
+ return true;
+ }
+ if (me.kind() == MouseEventKind.SCROLL_DOWN) {
+ detailScroll += 3;
+ return true;
+ }
+ }
+ if (vSplit.handleMouse(me, me.y())) {
+ return true;
+ }
if (hSplit.handleMouse(me, me.x())) {
if (hSplit.isDragging()) {
infoPanelWidth = Math.max(10, Math.min(me.x() - area.x(),
area.width() - 20));
@@ -290,6 +361,11 @@ class DiagramTab extends AbstractTab {
// Go back to topology
diagram.setPendingSelectionRouteId(drillDownRouteId);
topologyMode = true;
+ detailMode = false;
+ cachedRouteDetail = null;
+ cachedRouteDetailId = null;
+ detailLoadingRouteId = null;
+ detailLoading = false;
diagram.setTopologyMode(true);
diagram.setSelectedEipNodeIndex(-1);
diagram.resetScroll();
@@ -334,6 +410,12 @@ class DiagramTab extends AbstractTab {
topologyMode = true;
drillDownRouteId = null;
routeNavigationStack.clear();
+ detailMode = false;
+ cachedRouteDetail = null;
+ cachedRouteDetailId = null;
+ detailLoadingRouteId = null;
+ detailLoading = false;
+ lastDetailNodeId = null;
diagram.reset();
diagram.setTopologyMode(true);
}
@@ -393,10 +475,33 @@ class DiagramTab extends AbstractTab {
.split(area);
hSplit.setBorderPos(hChunks.get(1).x());
renderEipInfoPanel(frame, hChunks.get(0));
- diagram.renderNativeRouteDiagram(
- frame, hChunks.get(1), title, diagramMetrics,
drillDownRouteId, routeLayout);
+ if (detailMode) {
+ int detailH = Math.max(5, hChunks.get(1).height() * 60
/ 100);
+ List<Rect> vChunks = Layout.vertical()
+ .constraints(Constraint.fill(),
Constraint.length(detailH))
+ .split(hChunks.get(1));
+ vSplit.setBorderPos(vChunks.get(1).y());
+ diagram.renderNativeRouteDiagram(
+ frame, vChunks.get(0), title, diagramMetrics,
drillDownRouteId, routeLayout);
+ renderDetail(frame, vChunks.get(1), info);
+ } else {
+ diagram.renderNativeRouteDiagram(
+ frame, hChunks.get(1), title, diagramMetrics,
drillDownRouteId, routeLayout);
+ }
} else {
- diagram.renderNativeRouteDiagram(frame, area, title,
diagramMetrics, drillDownRouteId, routeLayout);
+ if (detailMode) {
+ int detailH = Math.max(5, area.height() * 60 / 100);
+ List<Rect> vChunks = Layout.vertical()
+ .constraints(Constraint.fill(),
Constraint.length(detailH))
+ .split(area);
+ vSplit.setBorderPos(vChunks.get(1).y());
+ diagram.renderNativeRouteDiagram(
+ frame, vChunks.get(0), title, diagramMetrics,
drillDownRouteId, routeLayout);
+ renderDetail(frame, vChunks.get(1), info);
+ } else {
+ diagram.renderNativeRouteDiagram(
+ frame, area, title, diagramMetrics,
drillDownRouteId, routeLayout);
+ }
}
return;
}
@@ -712,6 +817,7 @@ class DiagramTab extends AbstractTab {
hint(spans, TuiIcons.HINT_NAV, "navigate");
hint(spans, "PgUp/PgDn", "page");
hint(spans, "c", "source");
+ hint(spans, "d", "detail" + (detailMode ? " [on]" : " [off]"));
} else if (!topologyMode) {
hint(spans, "Esc", "back");
hint(spans, "t", "topology");
@@ -887,6 +993,7 @@ class DiagramTab extends AbstractTab {
- `↑↓←→` — navigate between EIP nodes
- `Enter` — jump to linked route (when `↵` indicator shown)
- `c` — show source code at selected node
+ - `d` — toggle EIP detail panel (shows configured options)
- `Esc` — go back (previous route or topology)
- `t` — jump back to topology view
@@ -1025,6 +1132,273 @@ class DiagramTab extends AbstractTab {
return bestBeforeIdx >= 0 ? bestBeforeIdx : bestAfterIdx;
}
+ private void renderDetail(Frame frame, Rect area, IntegrationInfo info) {
+ lastDetailArea = area;
+ var selected = diagram.getSelectedEipNodeBox();
+ if (selected == null || selected.layoutNode() == null) {
+ frame.renderWidget(
+ MarkdownView.builder()
+ .source("*Select an EIP node*")
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" EIP Detail ").build())
+ .styles(Theme.markdownStyles())
+ .build(),
+ area);
+ return;
+ }
+
+ String nodeId = selected.layoutNode().id;
+ String eipType = selected.layoutNode().type;
+ if (nodeId == null || eipType == null) {
+ frame.renderWidget(
+ MarkdownView.builder()
+ .source("*No detail available*")
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" EIP Detail ").build())
+ .styles(Theme.markdownStyles())
+ .build(),
+ area);
+ return;
+ }
+
+ // reset scroll when navigating to a different node
+ if (!nodeId.equals(lastDetailNodeId)) {
+ lastDetailNodeId = nodeId;
+ detailScroll = 0;
+ }
+
+ // load route detail once per route (covers all processors)
+ if (drillDownRouteId != null &&
!drillDownRouteId.equals(cachedRouteDetailId)
+ && !drillDownRouteId.equals(detailLoadingRouteId)) {
+ detailLoadingRouteId = drillDownRouteId;
+ detailLoading = true;
+ String rid = drillDownRouteId;
+ if (ctx.runner != null) {
+ ctx.runner.scheduler().execute(() -> {
+ JsonObject result = requestRouteProcessorDetail(rid);
+ cachedRouteDetail = result;
+ cachedRouteDetailId = rid;
+ detailLoading = false;
+ });
+ }
+ }
+
+ // find the selected processor in the cached route data
+ JsonObject processorEntry = findProcessorEntry(nodeId);
+
+ StringBuilder md = new StringBuilder();
+ CamelCatalog catalog = getCatalog(info);
+
+ if (processorEntry != null) {
+ String type = processorEntry.getString("type");
+ String endpointUri = processorEntry.getString("endpointUri");
+
+ if ("from".equals(type) && endpointUri != null && catalog != null)
{
+ renderEndpointDetail(md, catalog, endpointUri);
+ } else if (type != null) {
+ renderEipDetail(md, catalog, type,
processorEntry.getMap("options"));
+ } else {
+ md.append("*No detail available*\n");
+ }
+ } else if (detailLoading) {
+ md.append("*Loading...*\n");
+ } else {
+ md.append("*No detail available*\n");
+ }
+
+ String title = " " + eipType + " [" + nodeId + "] ";
+ if (CharWidth.of(title) > area.width() - 4) {
+ title = " " + CharWidth.truncateWithEllipsis(
+ eipType + " [" + nodeId + "]", area.width() - 6,
CharWidth.TruncatePosition.MIDDLE) + " ";
+ }
+
+ frame.renderWidget(
+ MarkdownView.builder()
+ .source(md.toString())
+ .scroll(detailScroll)
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(title).build())
+ .styles(Theme.markdownStyles())
+ .build(),
+ area);
+ }
+
+ private void renderEipDetail(StringBuilder md, CamelCatalog catalog,
String type, JsonObject opts) {
+ EipModel model = catalog != null ? catalog.eipModel(type) : null;
+
+ if (model != null && model.getTitle() != null) {
+ md.append("## ").append(model.getTitle()).append("\n\n");
+ if (model.getDescription() != null &&
!model.getDescription().isEmpty()) {
+ md.append(model.getDescription()).append("\n\n");
+ }
+ } else {
+ md.append("## ").append(type).append("\n\n");
+ }
+
+ if (opts != null && !opts.isEmpty()) {
+ Map<String, BaseOptionModel> optionDocs = new LinkedHashMap<>();
+ if (model != null) {
+ for (BaseOptionModel opt : model.getOptions()) {
+ if (opt.getName() != null) {
+ optionDocs.put(opt.getName(), opt);
+ }
+ }
+ }
+
+ for (Map.Entry<String, Object> entry : opts.entrySet()) {
+ String optName = entry.getKey();
+ String optValue = String.valueOf(entry.getValue());
+ appendOptionDetail(md, optName, optValue,
optionDocs.get(optName));
+ }
+ } else {
+ md.append("*No configured options*\n");
+ }
+ }
+
+ private void renderEndpointDetail(StringBuilder md, CamelCatalog catalog,
String uri) {
+ String component = uri.contains(":") ? uri.substring(0,
uri.indexOf(':')) : uri;
+ ComponentModel model = catalog.componentModel(component);
+
+ if (model != null) {
+ String compTitle = model.getTitle() != null ? model.getTitle() :
component;
+ md.append("## ").append(compTitle).append("\n\n");
+ if (model.getDescription() != null &&
!model.getDescription().isEmpty()) {
+ md.append(model.getDescription()).append("\n\n");
+ }
+
+ Map<String, String> parsedOptions;
+ try {
+ parsedOptions = catalog.endpointProperties(uri);
+ } catch (URISyntaxException e) {
+ parsedOptions = Map.of();
+ }
+
+ if (parsedOptions.isEmpty()) {
+ md.append("*No configured options*\n");
+ } else {
+ Map<String, BaseOptionModel> optionDocs = new
LinkedHashMap<>();
+ for (ComponentModel.EndpointOptionModel opt :
model.getEndpointOptions()) {
+ if (opt.getName() != null) {
+ optionDocs.put(opt.getName(), opt);
+ }
+ }
+
+ for (Map.Entry<String, String> entry :
parsedOptions.entrySet()) {
+ appendOptionDetail(md, entry.getKey(), entry.getValue(),
optionDocs.get(entry.getKey()));
+ }
+ }
+ } else {
+ md.append("## ").append(component).append("\n\n");
+ md.append("*No catalog documentation for:
").append(component).append("*\n");
+ }
+ }
+
+ private static void appendOptionDetail(StringBuilder md, String optName,
String optValue, BaseOptionModel doc) {
+ md.append("---\n\n");
+ md.append("**").append(optName).append("** =
**").append(optValue).append("**\n\n");
+
+ if (doc != null) {
+ if (doc.getDescription() != null &&
!doc.getDescription().isEmpty()) {
+ md.append(doc.getDescription()).append("\n\n");
+ }
+ List<String> meta = new ArrayList<>();
+ if (doc.getType() != null) {
+ meta.add("Type: `" + doc.getType() + "`");
+ }
+ if (doc.getDefaultValue() != null) {
+ meta.add("Default: `" + doc.getDefaultValue() + "`");
+ }
+ if (doc.isRequired()) {
+ meta.add("Required: yes");
+ }
+ if (doc.getEnums() != null && !doc.getEnums().isEmpty()) {
+ meta.add("Enum: " + String.join(", ", doc.getEnums()));
+ }
+ if (doc.getGroup() != null && !doc.getGroup().isEmpty()) {
+ meta.add("Group: " + doc.getGroup());
+ }
+ if (doc.isDeprecated()) {
+ String depText = "Deprecated";
+ if (doc.getDeprecationNote() != null &&
!doc.getDeprecationNote().isEmpty()) {
+ depText += " — " + doc.getDeprecationNote();
+ }
+ meta.add(depText);
+ }
+ if (!meta.isEmpty()) {
+ for (String m : meta) {
+ md.append("- ").append(m).append("\n");
+ }
+ md.append("\n");
+ }
+ }
+ }
+
+ private CamelCatalog getCatalog(IntegrationInfo info) {
+ String version = info.camelVersion;
+ if (version == null) {
+ return null;
+ }
+ CamelCatalog cached = catalogCache.get(version);
+ if (cached != null) {
+ return cached;
+ }
+ if (catalogLoadFailed.contains(version)) {
+ return null;
+ }
+ try {
+ cached = CatalogLoader.loadCatalog(null, version, true);
+ if (cached != null) {
+ catalogCache.put(version, cached);
+ } else {
+ catalogLoadFailed.add(version);
+ }
+ return cached;
+ } catch (Exception e) {
+ catalogLoadFailed.add(version);
+ return null;
+ }
+ }
+
+ private JsonObject findProcessorEntry(String nodeId) {
+ if (cachedRouteDetail == null || nodeId == null) {
+ return null;
+ }
+ JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
+ if (processors == null) {
+ return null;
+ }
+ for (Object obj : processors) {
+ JsonObject p = (JsonObject) obj;
+ if (nodeId.equals(p.getString("id"))) {
+ return p;
+ }
+ }
+ return null;
+ }
+
+ private JsonObject requestRouteProcessorDetail(String routeId) {
+ if (ctx.selectedPid == null) {
+ return null;
+ }
+ try {
+ Path outputFile = ctx.getOutputFile(ctx.selectedPid);
+ PathUtils.deleteFile(outputFile);
+
+ JsonObject root = new JsonObject();
+ root.put("action", "processor-detail");
+ root.put("routeId", routeId);
+
+ Path actionFile = ctx.getActionFile(ctx.selectedPid);
+ PathUtils.writeTextSafely(root.toJson(), actionFile);
+
+ JsonObject jo = pollJsonResponse(outputFile, 5000);
+ PathUtils.deleteFile(outputFile);
+ return jo;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
private static int numWidth(long... values) {
long max = 0;
for (long v : values) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index f98881645a27..089af5847cc2 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -374,7 +374,7 @@ class EndpointsTab extends AbstractTableTab {
default -> "[off]";
};
hint(spans, "a", "chart " + chartLabel);
- hint(spans, "d", "doc " + (panelMode == PANEL_DETAIL ? "[on]" :
"[off]"));
+ hint(spans, "d", "detail " + (panelMode == PANEL_DETAIL ? "[on]" :
"[off]"));
if (panelMode == PANEL_DETAIL) {
hintLast(spans, "PgUp/Dn", "scroll");
}