This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch 
feature/CAMEL-23898-configuration-detail-panel
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 3e9d41a15f7f7dab64633db6372cc85fc68cff23
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jul 4 11:13:17 2026 +0200

    CAMEL-23898: Configuration tab with row selection and property detail panel 
in camel-tui
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../jbang/core/commands/tui/ConfigurationTab.java  | 429 +++++++++++++--------
 1 file changed, 277 insertions(+), 152 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
index d164e936cf72..1ac4aef18b68 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
@@ -17,7 +17,9 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import dev.tamboui.layout.Constraint;
 import dev.tamboui.layout.Layout;
@@ -31,71 +33,52 @@ 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 dev.tamboui.widgets.scrollbar.Scrollbar;
-import dev.tamboui.widgets.scrollbar.ScrollbarState;
+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.catalog.DefaultCamelCatalog;
+import org.apache.camel.tooling.model.BaseOptionModel;
+import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.tooling.model.MainModel;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
 import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
 
-class ConfigurationTab extends AbstractTab {
+class ConfigurationTab extends AbstractTableTab {
 
-    private static final int MOUSE_SCROLL_LINES = 3;
-    private static final Style KEY_STYLE = Style.EMPTY.fg(Color.CYAN);
-    private static final Style VALUE_STYLE = Style.EMPTY.fg(Color.WHITE);
     private static final Style SECRET_STYLE = Style.EMPTY.fg(Color.DARK_GRAY);
-    private static final Style SOURCE_STYLE = Style.EMPTY.dim();
 
-    private final ScrollbarState scrollbarState = new ScrollbarState();
-    private int scrollOffset;
+    private int detailScroll;
+
+    private CamelCatalog catalog;
+    private Map<String, BaseOptionModel> mainOptionsMap;
+    private final Map<String, Map<String, BaseOptionModel>> 
componentOptionsCache = new HashMap<>();
 
     ConfigurationTab(MonitorContext ctx) {
-        super(ctx);
+        super(ctx, "key", "value", "source");
     }
 
     @Override
-    public boolean handleKeyEvent(KeyEvent ke) {
-        if (ke.isUp()) {
-            scrollOffset = Math.max(0, scrollOffset - 1);
-            return true;
-        }
-        if (ke.isDown()) {
-            scrollOffset++;
-            return true;
-        }
-        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
-            scrollOffset = Math.max(0, scrollOffset - 20);
-            return true;
-        }
-        if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
-            scrollOffset += 20;
-            return true;
-        }
-        if (ke.isHome()) {
-            scrollOffset = 0;
-            return true;
-        }
-        if (ke.isEnd()) {
-            scrollOffset = Integer.MAX_VALUE;
-            return true;
-        }
-        return false;
+    protected int getRowCount() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        return info != null ? info.configProperties.size() : 0;
     }
 
     @Override
-    public boolean handleMouseEvent(MouseEvent me, Rect area) {
-        if (me.kind() == MouseEventKind.SCROLL_UP) {
-            scrollOffset = Math.max(0, scrollOffset - MOUSE_SCROLL_LINES);
+    protected boolean handleTabKeyEvent(KeyEvent ke) {
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            detailScroll = Math.max(0, detailScroll - 10);
             return true;
         }
-        if (me.kind() == MouseEventKind.SCROLL_DOWN) {
-            scrollOffset += MOUSE_SCROLL_LINES;
+        if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            detailScroll += 10;
             return true;
         }
         return false;
@@ -103,24 +86,30 @@ class ConfigurationTab extends AbstractTab {
 
     @Override
     public void navigateUp() {
-        scrollOffset = Math.max(0, scrollOffset - 1);
+        tableState.selectPrevious();
+        detailScroll = 0;
     }
 
     @Override
     public void navigateDown() {
-        scrollOffset++;
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        int count = info != null ? info.configProperties.size() : 0;
+        tableState.selectNext(count);
+        detailScroll = 0;
     }
 
     @Override
-    public void render(Frame frame, Rect area) {
-        IntegrationInfo info = ctx.findSelectedIntegration();
-        if (info == null) {
-            renderNoSelection(frame, area);
-            return;
+    public boolean handleMouseEvent(MouseEvent me, Rect area) {
+        boolean handled = super.handleMouseEvent(me, area);
+        if (handled) {
+            detailScroll = 0;
         }
+        return handled;
+    }
 
-        List<ConfigProperty> props = info.configProperties;
-        if (props.isEmpty()) {
+    @Override
+    protected void renderContent(Frame frame, Rect area, IntegrationInfo info) 
{
+        if (info.configProperties.isEmpty()) {
             frame.renderWidget(
                     Paragraph.builder()
                             .text(Text.from(Line.from(
@@ -132,113 +121,259 @@ class ConfigurationTab extends AbstractTab {
             return;
         }
 
-        // Find divider position: index of first non-camel property
-        int dividerIndex = -1;
-        for (int i = 0; i < props.size(); i++) {
-            if (!props.get(i).key.startsWith("camel.")) {
-                dividerIndex = i;
-                break;
+        List<ConfigProperty> props = sortedProperties(info);
+
+        List<Rect> chunks = Layout.vertical()
+                .constraints(Constraint.fill(), Constraint.percentage(30))
+                .split(area);
+        renderTable(frame, chunks.get(0), props);
+        renderDetail(frame, chunks.get(1), props);
+    }
+
+    private List<ConfigProperty> sortedProperties(IntegrationInfo info) {
+        List<ConfigProperty> result = new ArrayList<>(info.configProperties);
+        result.sort((a, b) -> {
+            int cmp = switch (sort) {
+                case "value" -> compareStr(a.value, b.value);
+                case "source" -> compareStr(a.source, b.source);
+                default -> compareCamelFirst(a, b);
+            };
+            return sortReversed ? -cmp : cmp;
+        });
+        return result;
+    }
+
+    private void renderTable(Frame frame, Rect area, List<ConfigProperty> 
props) {
+        List<Row> rows = new ArrayList<>();
+        for (ConfigProperty p : props) {
+            boolean secret = "xxxxxx".equals(p.value);
+            String value = p.value != null ? p.value : "";
+            Style valStyle = secret ? SECRET_STYLE : Style.EMPTY;
+
+            String source = "";
+            if (p.source != null && !p.source.isEmpty()) {
+                source = FileUtil.stripPath(p.source);
             }
-        }
-        boolean hasDivider = dividerIndex > 0 && dividerIndex < props.size();
-        int totalLines = props.size() + (hasDivider ? 1 : 0);
 
-        String title = String.format(" Configuration — %d properties ", 
props.size());
-        Block block = Block.builder()
-                .borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                .title(title)
-                .build();
-        Rect inner = block.inner(area);
-        frame.renderWidget(block, area);
+            rows.add(Row.from(
+                    Cell.from(Span.styled(p.key, Style.EMPTY.fg(Color.CYAN))),
+                    Cell.from(Span.styled(value, valStyle)),
+                    Cell.from(Span.styled(source, Style.EMPTY.dim()))));
+        }
 
-        if (inner.height() < 1 || inner.width() < 10) {
-            return;
+        if (rows.isEmpty()) {
+            rows.add(emptyRow("No properties", 3));
         }
 
-        int visibleLines = inner.height();
-        int maxScroll = Math.max(0, totalLines - visibleLines);
-        scrollOffset = Math.min(scrollOffset, maxScroll);
+        String title = String.format(" Configuration [%d] sort:%s ", 
props.size(), sort);
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(sortLabel("KEY", "key"), 
sortStyle("key"))),
+                        Cell.from(Span.styled(sortLabel("VALUE", "value"), 
sortStyle("value"))),
+                        Cell.from(Span.styled(sortLabel("SOURCE", "source"), 
sortStyle("source")))))
+                .widths(
+                        Constraint.percentage(35),
+                        Constraint.fill(),
+                        Constraint.length(20))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
+                .build();
 
-        // Compute max key length across visible properties for alignment
-        int maxKeyLen = 0;
-        for (ConfigProperty p : props) {
-            maxKeyLen = Math.max(maxKeyLen, p.key.length());
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderScrollbar(frame, props.size());
+    }
+
+    private void renderDetail(Frame frame, Rect area, List<ConfigProperty> 
props) {
+        Integer sel = tableState.selected();
+        if (sel == null || sel < 0 || sel >= props.size()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(Span.styled(" Select a 
property", Style.EMPTY.dim()))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(" Property Detail ").build())
+                            .build(),
+                    area);
+            return;
         }
-        int keyWidth = Math.min(maxKeyLen, inner.width() / 2);
 
-        List<Rect> hChunks = Layout.horizontal()
-                .constraints(Constraint.fill(), Constraint.length(1))
-                .split(inner);
-        int lineWidth = hChunks.get(0).width();
+        ConfigProperty prop = props.get(sel);
+        BaseOptionModel option = lookupPropertyDoc(prop.key);
+        String title = " " + prop.key + " ";
 
-        // Build visible lines, inserting divider at the right display position
         List<Line> lines = new ArrayList<>();
-        int displayRow = 0;
-        for (int i = 0; i < props.size() && lines.size() < visibleLines; i++) {
-            if (hasDivider && i == dividerIndex) {
-                if (displayRow >= scrollOffset) {
-                    String divText = "─".repeat(Math.max(1, inner.width() - 
2));
-                    lines.add(Line.from(Span.styled(" " + divText, 
Style.EMPTY.dim())));
-                }
-                displayRow++;
-                if (lines.size() >= visibleLines) {
-                    break;
+
+        if (option != null) {
+            if (option.getDescription() != null && 
!option.getDescription().isEmpty()) {
+                addDetailField(lines, "Description", option.getDescription(), 
area.width());
+            }
+            if (option.getType() != null) {
+                addDetailField(lines, "Type", option.getType(), area.width());
+            }
+            if (option.getDefaultValue() != null) {
+                addDetailField(lines, "Default", 
option.getDefaultValue().toString(), area.width());
+            }
+            if (option.getEnums() != null && !option.getEnums().isEmpty()) {
+                addDetailField(lines, "Enum values", String.join(", ", 
option.getEnums()), area.width());
+            }
+            if (option.isRequired()) {
+                addDetailField(lines, "Required", "true", area.width());
+            }
+            if (option.isDeprecated()) {
+                String depText = "true";
+                if (option.getDeprecationNote() != null && 
!option.getDeprecationNote().isEmpty()) {
+                    depText += " — " + option.getDeprecationNote();
                 }
+                addDetailField(lines, "Deprecated", depText, area.width());
             }
-            if (displayRow >= scrollOffset) {
-                lines.add(renderProperty(props.get(i), keyWidth, lineWidth));
+            if (option.isSecret()) {
+                addDetailField(lines, "Secret", "true", area.width());
             }
-            displayRow++;
+            if (option.getGroup() != null && !option.getGroup().isEmpty()) {
+                addDetailField(lines, "Group", option.getGroup(), 
area.width());
+            }
+            lines.add(Line.from(Span.raw("")));
+        }
+
+        // current value and source
+        String value = prop.value != null ? prop.value : "";
+        boolean secret = "xxxxxx".equals(value);
+        addDetailField(lines, "Current value", secret ? "xxxxxx (masked)" : 
value, area.width());
+        if (prop.source != null && !prop.source.isEmpty()) {
+            addDetailField(lines, "Source", FileUtil.stripPath(prop.source), 
area.width());
         }
 
-        frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(), 
hChunks.get(0));
+        if (option == null) {
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(Span.styled("  No documentation available for 
this property.", Style.EMPTY.dim())));
+        }
 
-        if (totalLines > visibleLines) {
-            scrollbarState
-                    .contentLength(totalLines)
-                    .viewportContentLength(visibleLines)
-                    .position(scrollOffset);
-            frame.renderStatefulWidget(Scrollbar.builder().build(), 
hChunks.get(1), scrollbarState);
+        int visibleLines = area.height() - 2;
+        if (visibleLines < 1) {
+            visibleLines = 1;
         }
+        int maxScroll = Math.max(0, lines.size() - visibleLines);
+        detailScroll = Math.min(detailScroll, maxScroll);
+        int end = Math.min(detailScroll + visibleLines, lines.size());
+        List<Line> visible = lines.subList(detailScroll, end);
+
+        frame.renderWidget(
+                Paragraph.builder()
+                        .text(Text.from(visible))
+                        
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                .title(title).build())
+                        .build(),
+                area);
     }
 
-    private Line renderProperty(ConfigProperty prop, int keyWidth, int 
lineWidth) {
-        String key = prop.key;
-        if (key.length() > keyWidth) {
-            key = key.substring(0, keyWidth - 1) + "…";
+    private void addDetailField(List<Line> lines, String label, String value, 
int width) {
+        String prefix = "  " + label + ": ";
+        int maxValueLen = width - prefix.length() - 2;
+        if (maxValueLen <= 0) {
+            maxValueLen = 40;
+        }
+
+        if (value.length() <= maxValueLen) {
+            lines.add(Line.from(
+                    Span.styled(prefix, Style.EMPTY.dim()),
+                    Span.styled(value, Style.EMPTY.fg(Color.WHITE))));
         } else {
-            key = String.format("%-" + keyWidth + "s", key);
+            // wrap long values
+            lines.add(Line.from(Span.styled(prefix, Style.EMPTY.dim())));
+            int indent = 6;
+            String indentStr = " ".repeat(indent);
+            int wrapWidth = width - indent - 2;
+            if (wrapWidth <= 0) {
+                wrapWidth = 40;
+            }
+            int pos = 0;
+            while (pos < value.length()) {
+                int end = Math.min(pos + wrapWidth, value.length());
+                lines.add(Line.from(Span.styled(indentStr + 
value.substring(pos, end), Style.EMPTY.fg(Color.WHITE))));
+                pos = end;
+            }
         }
+    }
 
-        boolean secret = "xxxxxx".equals(prop.value);
-        Style valStyle = secret ? SECRET_STYLE : VALUE_STYLE;
-        String value = prop.value != null ? prop.value : "";
+    private BaseOptionModel lookupPropertyDoc(String key) {
+        if (key == null) {
+            return null;
+        }
+        initCatalog();
+        if (mainOptionsMap != null) {
+            BaseOptionModel option = mainOptionsMap.get(key);
+            if (option != null) {
+                return option;
+            }
+        }
+        // try camel.component.<name>.<option>
+        if (key.startsWith("camel.component.")) {
+            String rest = key.substring("camel.component.".length());
+            int dot = rest.indexOf('.');
+            if (dot > 0) {
+                String componentName = rest.substring(0, dot);
+                String optionName = rest.substring(dot + 1);
+                Map<String, BaseOptionModel> compOptions = 
getComponentOptions(componentName);
+                if (compOptions != null) {
+                    return compOptions.get(optionName);
+                }
+            }
+        }
+        return null;
+    }
 
-        List<Span> spans = new ArrayList<>();
-        String keyPart = "  " + key + "  ";
-        spans.add(Span.styled(keyPart, KEY_STYLE));
-        spans.add(Span.styled(value, valStyle));
-        if (prop.source != null && !prop.source.isEmpty()) {
-            String displaySource = FileUtil.stripPath(prop.source);
-            String sourceText = "[" + displaySource + "]";
-            int used = keyPart.length() + value.length();
-            int gap = Math.max(2, lineWidth - used - sourceText.length());
-            spans.add(Span.styled(" ".repeat(gap) + sourceText, SOURCE_STYLE));
+    private void initCatalog() {
+        if (catalog != null) {
+            return;
+        }
+        catalog = new DefaultCamelCatalog();
+        mainOptionsMap = new HashMap<>();
+        MainModel mainModel = catalog.mainModel();
+        if (mainModel != null) {
+            for (MainModel.MainOptionModel opt : mainModel.getOptions()) {
+                if (opt.getName() != null) {
+                    mainOptionsMap.put(opt.getName(), opt);
+                }
+            }
         }
+    }
 
-        return Line.from(spans);
+    private Map<String, BaseOptionModel> getComponentOptions(String 
componentName) {
+        return componentOptionsCache.computeIfAbsent(componentName, name -> {
+            initCatalog();
+            ComponentModel model = catalog.componentModel(name);
+            if (model == null) {
+                return null;
+            }
+            Map<String, BaseOptionModel> map = new HashMap<>();
+            for (ComponentModel.ComponentOptionModel opt : 
model.getComponentOptions()) {
+                if (opt.getName() != null) {
+                    map.put(opt.getName(), opt);
+                }
+            }
+            return map.isEmpty() ? null : map;
+        });
     }
 
     @Override
     public void renderFooter(List<Span> spans) {
-        hint(spans, "Esc", "back");
-        hint(spans, "↑↓", "scroll");
-        hintLast(spans, "PgUp/Dn", "page");
+        super.renderFooter(spans);
+        hint(spans, "↑↓", "navigate");
+        hintLast(spans, "PgUp/Dn", "scroll");
     }
 
     @Override
     public SelectionContext getSelectionContext() {
-        return null;
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.configProperties.isEmpty()) {
+            return null;
+        }
+        List<String> items = info.configProperties.stream().map(p -> 
p.key).toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Configuration");
     }
 
     static int compareCamelFirst(ConfigProperty a, ConfigProperty b) {
@@ -280,47 +415,35 @@ class ConfigurationTab extends AbstractTab {
 
                 - **KEY** — Property name following Camel's naming convention 
(e.g., `camel.main.name`, `camel.component.kafka.brokers`, `greeting.message`)
                 - **VALUE** — Current resolved property value. Sensitive 
values (passwords, tokens) are masked as `xxxxxx` for security
-                - **DEFAULT** — Default value (shown only if different from 
current value). Helps identify which properties were explicitly configured vs 
using defaults
                 - **SOURCE** — Where the property was set:
                   - `application.properties` — from the main properties file
                   - `ENV` — from an environment variable
-                  - `SYS` — from a Java system property (`-D`)
+                  - `JVM` — from a Java system property (`-D`)
+                  - `Spring Boot` — from Spring Boot configuration
                   - `camel-component` — default from a Camel component
                   - `override` — set programmatically in code
                   - `initial` — set during context initialization
 
-                ## Example Screen
-
-                ```
-                 KEY                              VALUE              DEFAULT   
   SOURCE
-                 camel.main.name                  camel-demo                   
   application.properties
-                 camel.main.shutdownTimeout       30                 300       
   application.properties
-                 camel.component.kafka.brokers    localhost:9092               
    application.properties
-                 greeting.message                 Hello World                  
   application.properties
-                ```
-
-                ## Property Namespaces
-
-                Common property prefixes and what they configure:
-
-                - `camel.main.*` — Core Camel settings (name, shutdown 
timeout, tracing, etc.)
-                - `camel.component.*` — Component-level defaults (applied to 
all endpoints of that component)
-                - `camel.dataformat.*` — Data format defaults (JSON, XML, CSV 
serialization options)
-                - `camel.language.*` — Expression language settings
-                - `camel.server.*` — Embedded HTTP server configuration (port, 
host, CORS)
+                ## Detail View
 
-                ## Use Cases
+                Press `Enter` on a property to see its documentation from the 
Camel
+                catalog. For `camel.main.*` and `camel.component.*` 
properties, the
+                detail panel shows:
 
-                - **Verify configuration**: Confirm that properties from 
files, environment variables, and system properties are being applied correctly
-                - **Debug overrides**: When a property has an unexpected 
value, check the SOURCE column to see where it was set
-                - **Audit security**: Verify that sensitive properties 
(passwords, API keys) are properly masked
+                - **Description** — what the property does
+                - **Type** — expected value type (string, boolean, integer, 
etc.)
+                - **Default** — default value if not explicitly set
+                - **Enum values** — allowed values for enumerated properties
+                - **Required** / **Deprecated** / **Secret** flags
+                - **Group** — the configuration group this property belongs to
 
                 ## Keys
 
-                - `Up/Down` — navigate properties
+                - `Up/Down` — select property
+                - `Enter` — view property detail / documentation
                 - `s` — cycle sort column
                 - `S` — reverse sort order
-                - `Esc` — back
+                - `Esc` — close detail / back
                 """;
     }
 
@@ -350,6 +473,8 @@ class ConfigurationTab extends AbstractTab {
         }
         result.put("rows", rows);
         result.put("totalRows", info.configProperties.size());
+        Integer sel = tableState.selected();
+        result.put("selectedIndex", sel != null ? sel : -1);
         return result;
     }
 }

Reply via email to