This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feat/camel-tui in repository https://gitbox.apache.org/repos/asf/camel.git
commit e2fab33616c92be4dec6599ed90900c3ea0c75db Author: Claus Ibsen <[email protected]> AuthorDate: Mon May 18 15:35:26 2026 +0200 TUI: HTTP tab - press 'c' to view OpenAPI spec; auto-scroll to operationId Pressing 'c' on a selected REST contract-first endpoint fetches the full OpenAPI spec via the rest-spec dev console action, opens a full-screen spec viewer, and scrolls to the first line containing the endpoint's operationId. c/Esc closes the viewer. Added rest-spec action handler to LocalCliConnector. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --- .../camel/cli/connector/LocalCliConnector.java | 16 ++ .../dsl/jbang/core/commands/tui/CamelMonitor.java | 179 ++++++++++++++++++++- 2 files changed, 194 insertions(+), 1 deletion(-) 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 438e7d426006..3ea9020b287b 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 @@ -308,6 +308,8 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C doActionTopProcessorsTask(); } else if ("source".equals(action)) { doActionSourceTask(root); + } else if ("rest-spec".equals(action)) { + doActionRestSpecTask(root); } else if ("route-dump".equals(action)) { doActionRouteDumpTask(root); } else if ("route-structure".equals(action)) { @@ -707,6 +709,20 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C } } + private void doActionRestSpecTask(JsonObject root) throws Exception { + DevConsole dc = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class) + .resolveById("rest-spec"); + if (dc != null) { + String filter = root.getString("filter"); + Map<String, Object> options = filter != null ? Map.of("filter", filter) : Map.of(); + JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON, options); + LOG.trace("Updating output file: {}", outputFile); + IOHelper.writeText(json.toJson(), outputFile); + } else { + IOHelper.writeText("{}", outputFile); + } + } + private void doActionTopProcessorsTask() throws IOException { DevConsole dc = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class).resolveById("top"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index ad69260b49d0..999e696bfa6a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -235,6 +235,12 @@ public class CamelMonitor extends CamelCommand { // httpFilter: 0=all, 1=REST DSL only, 2=Platform-HTTP only private int httpFilter; private boolean httpShowManagement; + // HTTP spec viewer state + private boolean showHttpSpec; + private List<String> httpSpecLines = Collections.emptyList(); + private String httpSpecTitle; + private int httpSpecScroll; + private final AtomicBoolean specLoading = new AtomicBoolean(false); // Health filter state private boolean showOnlyDown; @@ -382,6 +388,10 @@ public class CamelMonitor extends CamelCommand { showSource = false; return true; } + if (showHttpSpec) { + showHttpSpec = false; + return true; + } if (showDiagram) { showDiagram = false; diagramImageData = null; @@ -621,6 +631,30 @@ public class CamelMonitor extends CamelCommand { httpShowManagement = !httpShowManagement; return true; } + if (tab == TAB_HTTP && !showHttpSpec && ke.isChar('c')) { + loadSpecForSelectedHttpEndpoint(); + return true; + } + if (tab == TAB_HTTP && showHttpSpec) { + if (ke.isChar('c')) { + showHttpSpec = false; + } else if (ke.isUp()) { + httpSpecScroll = Math.max(0, httpSpecScroll - 1); + } else if (ke.isDown()) { + httpSpecScroll++; + } else if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) { + httpSpecScroll = Math.max(0, httpSpecScroll - 20); + } else if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) { + httpSpecScroll += 20; + } else if (ke.isHome()) { + httpSpecScroll = 0; + } else if (ke.isEnd()) { + httpSpecScroll = Integer.MAX_VALUE; + } else { + return false; + } + return true; + } // Endpoints tab: sort and filter if (tab == TAB_ENDPOINTS && ke.isChar('s')) { @@ -972,6 +1006,11 @@ public class CamelMonitor extends CamelCommand { sourceTitle = null; sourceScroll = 0; sourceScrollX = 0; + // Spec viewer (TAB_HTTP) + showHttpSpec = false; + httpSpecLines = Collections.emptyList(); + httpSpecTitle = null; + httpSpecScroll = 0; // Diagram (TAB_ROUTES) showDiagram = false; diagramImageData = null; @@ -3511,6 +3550,11 @@ public class CamelMonitor extends CamelCommand { return; } + if (showHttpSpec) { + renderHttpSpec(frame, area); + return; + } + List<HttpEndpointInfo> visible = visibleHttpEndpoints(info); // Sort @@ -3726,6 +3770,127 @@ public class CamelMonitor extends CamelCommand { Span.raw(value))); } + private void loadSpecForSelectedHttpEndpoint() { + if (selectedPid == null || runner == null) { + return; + } + List<HttpEndpointInfo> visible = visibleHttpEndpoints(findSelectedIntegration()); + Integer sel = httpTableState.selected(); + if (sel == null || sel < 0 || sel >= visible.size()) { + return; + } + HttpEndpointInfo ep = visible.get(sel); + if (ep.specificationUri == null) { + return; + } + if (!specLoading.compareAndSet(false, true)) { + return; + } + + httpSpecLines = List.of("(Loading spec...)"); + httpSpecTitle = ep.specificationUri; + httpSpecScroll = 0; + showHttpSpec = true; + + String pid = selectedPid; + String specUri = ep.specificationUri; + String operationId = ep.operationId; + + runner.scheduler().execute(() -> { + try { + loadSpecInBackground(pid, specUri, operationId); + } finally { + specLoading.set(false); + } + }); + } + + private void loadSpecInBackground(String pid, String specUri, String operationId) { + Path outputFile = getOutputFile(pid); + PathUtils.deleteFile(outputFile); + + JsonObject root = new JsonObject(); + root.put("action", "rest-spec"); + root.put("filter", specUri); + + Path actionFile = getActionFile(pid); + org.apache.camel.dsl.jbang.core.common.PathUtils.writeTextSafely(root.toJson(), actionFile); + + JsonObject jo = pollJsonResponse(outputFile, 5000); + PathUtils.deleteFile(outputFile); + + if (jo == null) { + applySpecResult(specUri, List.of("(No response from integration)"), 0); + return; + } + + JsonArray specs = (JsonArray) jo.get("specs"); + if (specs == null || specs.isEmpty()) { + applySpecResult(specUri, List.of("(No spec content available for: " + specUri + ")"), 0); + return; + } + + JsonObject specObj = (JsonObject) specs.get(0); + String content = specObj.getString("content"); + if (content == null || content.isBlank()) { + applySpecResult(specUri, List.of("(Empty spec content for: " + specUri + ")"), 0); + return; + } + + List<String> lines = List.of(content.split("\n", -1)); + + // scroll to the line containing operationId + int scrollTo = 0; + if (operationId != null) { + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).contains(operationId)) { + scrollTo = Math.max(0, i - 2); + break; + } + } + } + + applySpecResult(specUri, lines, scrollTo); + } + + private void applySpecResult(String specUri, List<String> lines, int scrollTo) { + if (runner == null) { + return; + } + runner.runOnRenderThread(() -> { + if (!showHttpSpec) { + return; + } + httpSpecTitle = specUri; + httpSpecLines = lines; + httpSpecScroll = scrollTo; + }); + } + + private void renderHttpSpec(Frame frame, Rect area) { + String title = " Spec [" + (httpSpecTitle != null ? httpSpecTitle : "") + "] "; + + int visibleLines = area.height() - 2; // border top+bottom + if (visibleLines < 1) { + visibleLines = 1; + } + int maxScroll = Math.max(0, httpSpecLines.size() - visibleLines); + httpSpecScroll = Math.min(httpSpecScroll, maxScroll); + + int end = Math.min(httpSpecScroll + visibleLines, httpSpecLines.size()); + List<Line> visible = new ArrayList<>(); + for (int i = httpSpecScroll; i < end; i++) { + visible.add(Line.from(Span.raw(httpSpecLines.get(i)))); + } + + frame.renderWidget( + Paragraph.builder() + .text(Text.from(visible)) + .block(Block.builder().borderType(BorderType.ROUNDED).title(title).build()) + .build(), + area); + } + // ---- Tab 7: Inspect (merged Last + Tracer) ---- private void renderInspect(Frame frame, Rect area) { @@ -4395,13 +4560,25 @@ public class CamelMonitor extends CamelCommand { } hint(spans, "l", "level"); hintLast(spans, "f", "follow" + (logFollowMode ? " [on]" : " [off]")); + } else if (tab == TAB_HTTP && showHttpSpec) { + hint(spans, "c/Esc", "close"); + hint(spans, "↑↓", "scroll"); + hintLast(spans, "PgUp/PgDn", "page"); } else if (tab == TAB_HTTP) { hint(spans, "Esc", "back"); hint(spans, "↑↓", "navigate"); hint(spans, "s", "sort"); String[] filterLabels = { "all", "rest", "http" }; hint(spans, "f", "filter [" + filterLabels[httpFilter] + "]"); - hintLast(spans, "m", "management" + (httpShowManagement ? " [on]" : " [off]")); + hint(spans, "m", "management" + (httpShowManagement ? " [on]" : " [off]")); + // show spec key only when selected row has a spec + List<HttpEndpointInfo> hVisible = visibleHttpEndpoints(findSelectedIntegration()); + Integer hSel = httpTableState.selected(); + if (hSel != null && hSel >= 0 && hSel < hVisible.size() && hVisible.get(hSel).specificationUri != null) { + hintLast(spans, "c", "spec"); + } else { + hintLast(spans, "1-9", "tabs"); + } } else if (tab == TAB_HISTORY) { boolean tracerActive = !traces.get().isEmpty(); if (tracerActive && traceDetailView) {
