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");
         }


Reply via email to