You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/07/06 18:39:24 UTC
(camel) 01/02: CAMEL-22616: camel-jbang - Endpoints tab detail panel with catalog documentation
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch worktree-tui
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 1f8a28a4e3ab4e60b02274cde9694e8c4d73c222
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Mon Jul 6 20:29:39 2026 +0200
CAMEL-22616: camel-jbang - Endpoints tab detail panel with catalog documentation
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../dsl/jbang/core/commands/tui/EndpointsTab.java | 223 ++++++++++++++++++++-
1 file changed, 217 insertions(+), 6 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)
""";
}