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 1045cca81ccf CAMEL-22616: Endpoints tab doc panel and HTTP tab title 
cleanup (#24472)
1045cca81ccf is described below

commit 1045cca81ccf5105bd4f6d2fd22dd19b60c597d4
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jul 6 21:38:23 2026 +0200

    CAMEL-22616: Endpoints tab doc panel and HTTP tab title cleanup (#24472)
    
    * CAMEL-22616: camel-jbang - Endpoints tab detail panel with catalog 
documentation
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-22616: camel-jbang - Move HTTP server info into table title
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/tui/EndpointsTab.java  | 223 ++++++++++++++++++++-
 .../camel/dsl/jbang/core/commands/tui/HttpTab.java |  63 +++---
 2 files changed, 251 insertions(+), 35 deletions(-)

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 eaa0bdd5692c..48faaea58ca1 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
@@ -16,7 +16,10 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.net.URISyntaxException;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
@@ -25,6 +28,7 @@ import java.util.Map;
 import dev.tamboui.layout.Constraint;
 import dev.tamboui.layout.Layout;
 import dev.tamboui.layout.Rect;
+import dev.tamboui.markdown.MarkdownView;
 import dev.tamboui.style.AnsiColor;
 import dev.tamboui.style.Color;
 import dev.tamboui.style.Style;
@@ -32,6 +36,7 @@ import dev.tamboui.terminal.Frame;
 import dev.tamboui.text.CharWidth;
 import dev.tamboui.text.Line;
 import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.MouseEvent;
 import dev.tamboui.widgets.block.Block;
@@ -43,6 +48,10 @@ import dev.tamboui.widgets.sparkline.DualSparkline;
 import dev.tamboui.widgets.table.Cell;
 import dev.tamboui.widgets.table.Row;
 import dev.tamboui.widgets.table.Table;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
+import org.apache.camel.tooling.model.BaseOptionModel;
+import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
@@ -55,6 +64,9 @@ class EndpointsTab extends AbstractTableTab {
     private static final int CHART_SINGLE = 1;
     private static final int CHART_OFF = 2;
 
+    private static final int PANEL_CHART = 0;
+    private static final int PANEL_DETAIL = 1;
+
     private final Map<String, LinkedList<Long>> endpointInHistory;
     private final Map<String, LinkedList<Long>> endpointOutHistory;
     private final Map<String, LinkedList<Long>> endpointRemoteInHistory;
@@ -68,11 +80,15 @@ class EndpointsTab extends AbstractTableTab {
 
     private int filter;
     private int chartMode = CHART_ALL;
+    private int panelMode = PANEL_CHART;
+    private int detailScroll;
     private int chartPanelHeight = 16;
     private final DragSplit vSplit = new DragSplit();
     private int flowPanelWidth = 38;
     private final DragSplit hSplit = new DragSplit();
 
+    private final Map<String, CamelCatalog> catalogCache = new HashMap<>();
+
     EndpointsTab(MonitorContext ctx, MetricsCollector metrics) {
         super(ctx, "component", "route", "dir", "total", "body", "hdr", "uri");
         sortIndex = 1;
@@ -105,6 +121,21 @@ class EndpointsTab extends AbstractTableTab {
             chartMode = (chartMode + 1) % 3;
             return true;
         }
+        if (ke.isCharIgnoreCase('d')) {
+            panelMode = panelMode == PANEL_CHART ? PANEL_DETAIL : PANEL_CHART;
+            detailScroll = 0;
+            return true;
+        }
+        if (panelMode == PANEL_DETAIL) {
+            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;
+            }
+        }
         return false;
     }
 
@@ -116,7 +147,7 @@ class EndpointsTab extends AbstractTableTab {
             }
             return true;
         }
-        if (hSplit.handleMouse(me, me.x())) {
+        if (panelMode == PANEL_CHART && hSplit.handleMouse(me, me.x())) {
             if (hSplit.isDragging()) {
                 flowPanelWidth = Math.max(20, Math.min(me.x() - area.x(), 
area.width() - 20));
             }
@@ -131,12 +162,19 @@ class EndpointsTab extends AbstractTableTab {
                 filtered.removeIf(ep -> !ep.remote && !ep.stub);
             }
             if (handleTableClick(me, lastTableArea, tableState, 
filtered.size())) {
+                detailScroll = 0;
                 return true;
             }
         }
         return false;
     }
 
+    @Override
+    public void navigateUp() {
+        tableState.selectPrevious();
+        detailScroll = 0;
+    }
+
     @Override
     public void navigateDown() {
         IntegrationInfo info = ctx.findSelectedIntegration();
@@ -148,6 +186,7 @@ class EndpointsTab extends AbstractTableTab {
                 filtered.removeIf(ep -> !ep.remote && !ep.stub);
             }
             tableState.selectNext(filtered.size());
+            detailScroll = 0;
         }
     }
 
@@ -246,11 +285,18 @@ class EndpointsTab extends AbstractTableTab {
             hasSizeHistory = false;
         }
 
-        boolean showChart = chartMode != CHART_OFF && ctx.shellPercent < 50;
+        boolean showPanel = panelMode == PANEL_DETAIL
+                || (chartMode != CHART_OFF && ctx.shellPercent < 50);
         List<Rect> chunks;
-        if (showChart) {
-            chartPanelHeight = Math.max(5, Math.min(chartPanelHeight, 
area.height() - 5));
-            chunks = Layout.vertical().constraints(Constraint.fill(), 
Constraint.length(chartPanelHeight)).split(area);
+        if (showPanel) {
+            if (panelMode == PANEL_DETAIL) {
+                // doc panel gets 60% of area
+                int detailH = Math.max(5, area.height() * 60 / 100);
+                chunks = Layout.vertical().constraints(Constraint.fill(), 
Constraint.length(detailH)).split(area);
+            } else {
+                chartPanelHeight = Math.max(5, Math.min(chartPanelHeight, 
area.height() - 5));
+                chunks = Layout.vertical().constraints(Constraint.fill(), 
Constraint.length(chartPanelHeight)).split(area);
+            }
             vSplit.setBorderPos(chunks.get(1).y());
         } else {
             chunks = List.of(area);
@@ -262,7 +308,10 @@ class EndpointsTab extends AbstractTableTab {
         frame.renderStatefulWidget(table, chunks.get(0), tableState);
         renderScrollbar(frame, sortedEndpoints.size());
 
-        if (showChart) {
+        if (showPanel && panelMode == PANEL_DETAIL) {
+            hSplit.clearBorderPos();
+            renderDetail(frame, chunks.get(1), sortedEndpoints, info);
+        } else if (showPanel) {
             // Determine selected endpoint URI for single-endpoint chart
             String selectedUri = null;
             if (chartMode == CHART_SINGLE) {
@@ -310,6 +359,10 @@ class EndpointsTab extends AbstractTableTab {
             default -> "[off]";
         };
         hint(spans, "a", "chart " + chartLabel);
+        hint(spans, "d", "doc " + (panelMode == PANEL_DETAIL ? "[on]" : 
"[off]"));
+        if (panelMode == PANEL_DETAIL) {
+            hintLast(spans, "PgUp/Dn", "scroll");
+        }
     }
 
     int getFilter() {
@@ -626,6 +679,145 @@ class EndpointsTab extends AbstractTableTab {
                 .build(), area);
     }
 
+    private void renderDetail(Frame frame, Rect area, List<EndpointInfo> 
sortedEndpoints, IntegrationInfo info) {
+        Integer sel = tableState.selected();
+        if (sel == null || sel < 0 || sel >= sortedEndpoints.size()) {
+            frame.renderWidget(
+                    MarkdownView.builder()
+                            .source("*Select an endpoint*")
+                            
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(" Endpoint Detail ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        EndpointInfo ep = sortedEndpoints.get(sel);
+        String uri = ep.uri;
+        String component = ep.component;
+        if (uri == null || component == null) {
+            frame.renderWidget(
+                    MarkdownView.builder()
+                            .source("*No endpoint URI*")
+                            
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(" Endpoint Detail ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        CamelCatalog catalog = getCatalog(info);
+        StringBuilder md = new StringBuilder();
+
+        if (catalog != null) {
+            ComponentModel model = catalog.componentModel(component);
+            if (model != null) {
+                // component title and description
+                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");
+                }
+
+                // parse the URI into option name/value pairs
+                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 {
+                    // build lookup map from endpoint option models
+                    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()) {
+                        String optName = entry.getKey();
+                        String optValue = entry.getValue();
+                        BaseOptionModel doc = optionDocs.get(optName);
+
+                        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");
+                            }
+                        }
+                    }
+                }
+            } else {
+                md.append("*No catalog documentation for: 
").append(component).append("*\n");
+            }
+        } else {
+            md.append("*Loading catalog...*\n");
+        }
+
+        String title = " " + uri + " ";
+        if (CharWidth.of(title) > area.width() - 4) {
+            title = " " + CharWidth.truncateWithEllipsis(uri, 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())
+                        .build(),
+                area);
+    }
+
+    private CamelCatalog getCatalog(IntegrationInfo info) {
+        String version = info.camelVersion;
+        if (version == null) {
+            return null;
+        }
+        return catalogCache.computeIfAbsent(version, v -> {
+            try {
+                return CatalogLoader.loadCatalog(null, v, true);
+            } catch (Exception e) {
+                return null;
+            }
+        });
+    }
+
     private static long unbox(Long value) {
         return value != null ? value : 0L;
     }
@@ -746,12 +938,31 @@ class EndpointsTab extends AbstractTableTab {
                 - **remote** — show only remote endpoints (external 
connections)
                 - **stub** — show only stubbed endpoints
 
+                ## Endpoint Documentation
+
+                Press `d` to toggle the documentation panel. When enabled, the
+                bottom panel shows detailed documentation for the selected 
endpoint's
+                configured options — parsed from the endpoint URI and looked 
up in
+                the Camel catalog matching the integration's Camel version.
+
+                For each configured option you'll see:
+                - **Name** and **current value** (highlighted)
+                - **Description** — what the option does
+                - **Type** — the expected value type
+                - **Default** — the default value if not set
+                - **Enum values** — valid choices for enum options
+                - **Group** — whether it's a common, consumer, producer, or 
advanced option
+
+                Use `PgUp/PgDn` to scroll the documentation panel.
+
                 ## Keys
 
                 - `Up/Down` — select endpoint
                 - `s` — cycle sort column
                 - `S` — reverse sort order
                 - `f` — cycle filter mode
+                - `d` — toggle endpoint documentation panel
+                - `PgUp/PgDn` — scroll documentation (when doc panel is open)
                 """;
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
index 69e2a35e6f21..23d65ebbb0e6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
@@ -277,13 +277,12 @@ class HttpTab extends AbstractTableTab {
 
         detailPanelHeight = Math.max(3, Math.min(detailPanelHeight, 
area.height() - 5));
         List<Rect> chunks = Layout.vertical()
-                .constraints(Constraint.length(1), Constraint.fill(), 
Constraint.length(detailPanelHeight))
+                .constraints(Constraint.fill(), 
Constraint.length(detailPanelHeight))
                 .split(area);
 
-        renderServerInfo(frame, chunks.get(0), info, visible);
-        renderTable(frame, chunks.get(1), visible);
-        vSplit.setBorderPos(chunks.get(2).y());
-        renderDetail(frame, chunks.get(2), visible);
+        renderTable(frame, chunks.get(0), visible, info);
+        vSplit.setBorderPos(chunks.get(1).y());
+        renderDetail(frame, chunks.get(1), visible);
     }
 
     @Override
@@ -1208,52 +1207,58 @@ class HttpTab extends AbstractTableTab {
         return result;
     }
 
-    private void renderServerInfo(Frame frame, Rect area, IntegrationInfo 
info, List<HttpEndpointInfo> visible) {
+    static Style methodStyle(String method) {
+        if (method == null) {
+            return Style.EMPTY;
+        }
+        String m = method.split(",")[0].trim().toUpperCase(Locale.ENGLISH);
+        return switch (m) {
+            case "GET" -> Style.EMPTY.fg(Color.GREEN);
+            case "POST" -> Style.EMPTY.fg(Color.YELLOW);
+            case "PUT" -> Style.EMPTY.fg(Color.CYAN);
+            case "DELETE" -> Style.EMPTY.fg(Color.LIGHT_RED);
+            case "PATCH" -> Style.EMPTY.fg(Color.rgb(0xFF, 0x80, 0x00));
+            default -> Style.EMPTY.dim();
+        };
+    }
+
+    private Title buildTableTitle(IntegrationInfo info, List<HttpEndpointInfo> 
visible) {
         List<Span> spans = new ArrayList<>();
+        spans.add(Span.raw(" HTTP Services [" + visible.size() + "]"));
         if (info.httpServer != null) {
-            spans.add(Span.styled(" Server: ", 
Style.EMPTY.fg(Color.YELLOW).bold()));
+            spans.add(Span.raw("  "));
+            spans.add(Span.styled("Server: ", 
Style.EMPTY.fg(Color.YELLOW).bold()));
             spans.add(Span.styled(info.httpServer, 
Style.EMPTY.fg(Color.CYAN)));
-            spans.add(Span.raw("    "));
         }
         long restCount = info.httpEndpoints.stream().filter(e -> e.fromRest && 
!e.specification).count();
         long specCount = info.httpEndpoints.stream().filter(e -> 
e.specification).count();
         long httpCount = info.httpEndpoints.stream().filter(e -> !e.fromRest 
&& !e.management).count();
         long mgmtCount = info.httpEndpoints.stream().filter(e -> 
e.management).count();
         if (restCount > 0) {
+            spans.add(Span.raw("  "));
             spans.add(Span.styled("REST: ", Style.EMPTY.fg(Color.GREEN)));
-            spans.add(Span.raw(restCount + "  "));
+            spans.add(Span.raw(restCount + ""));
         }
         if (specCount > 0) {
+            spans.add(Span.raw("  "));
             spans.add(Span.styled("Spec: ", Style.EMPTY.fg(Color.MAGENTA)));
-            spans.add(Span.raw(specCount + "  "));
+            spans.add(Span.raw(specCount + ""));
         }
         if (httpCount > 0) {
+            spans.add(Span.raw("  "));
             spans.add(Span.styled("HTTP: ", Style.EMPTY.fg(Color.CYAN)));
-            spans.add(Span.raw(httpCount + "  "));
+            spans.add(Span.raw(httpCount + ""));
         }
         if (mgmtCount > 0) {
+            spans.add(Span.raw("  "));
             spans.add(Span.styled("Mgmt: ", 
Style.EMPTY.fg(Color.YELLOW).dim()));
             spans.add(Span.raw(mgmtCount + ""));
         }
-        frame.renderWidget(Paragraph.from(Line.from(spans)), area);
-    }
-
-    static Style methodStyle(String method) {
-        if (method == null) {
-            return Style.EMPTY;
-        }
-        String m = method.split(",")[0].trim().toUpperCase(Locale.ENGLISH);
-        return switch (m) {
-            case "GET" -> Style.EMPTY.fg(Color.GREEN);
-            case "POST" -> Style.EMPTY.fg(Color.YELLOW);
-            case "PUT" -> Style.EMPTY.fg(Color.CYAN);
-            case "DELETE" -> Style.EMPTY.fg(Color.LIGHT_RED);
-            case "PATCH" -> Style.EMPTY.fg(Color.rgb(0xFF, 0x80, 0x00));
-            default -> Style.EMPTY.dim();
-        };
+        spans.add(Span.raw(" "));
+        return Title.from(Line.from(spans));
     }
 
-    private void renderTable(Frame frame, Rect area, List<HttpEndpointInfo> 
visible) {
+    private void renderTable(Frame frame, Rect area, List<HttpEndpointInfo> 
visible, IntegrationInfo info) {
         List<Row> rows = new ArrayList<>();
         for (HttpEndpointInfo ep : visible) {
             String method = ep.method != null ? ep.method : "";
@@ -1286,7 +1291,7 @@ class HttpTab extends AbstractTableTab {
                             "Stopped".equals(state) ? 
Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY))));
         }
 
-        String title = String.format(" HTTP Services [%d] ", visible.size());
+        Title title = buildTableTitle(info, visible);
 
         Row header = Row.from(
                 Cell.from(Span.styled(sortLabel("METHOD", "method"), 
sortStyle("method"))),

Reply via email to