This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch obs3 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 2a54e4ef7cb04fc00057388715b91561cbea7bc3 Author: Claus Ibsen <[email protected]> AuthorDate: Sat May 30 15:49:28 2026 +0200 CAMEL-23648: camel-jbang - TUI HTTP tab add probe view for sending requests Add a Postman-like HTTP Probe sub-view accessible via Enter on any endpoint in the HTTP tab. Features include method selector, path/headers/body input, response display with status/headers/body, request history with replay, pretty print toggle for JSON/XML responses, and proper key handling isolation from global shortcuts. Co-Authored-By: Claude <[email protected]> --- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 123 ++- .../camel/dsl/jbang/core/commands/tui/HttpTab.java | 976 ++++++++++++++++++++- 2 files changed, 1063 insertions(+), 36 deletions(-) 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 e70e6364e3fd..f07a205ad802 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 @@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -483,48 +484,56 @@ public class CamelMonitor extends CamelCommand { } return true; } - // Quit: q or Ctrl+c - if (ke.isCharIgnoreCase('q') || ke.isCtrlC()) { + // Quit: q or Ctrl+c (skip when probe is editing text) + boolean probeEditing = tabsState.selected() == TAB_HTTP && httpTab.isProbeMode(); + if (!probeEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) { runner.quit(); return true; } - // Tab switching with number keys - // When infra is selected, only Overview (1) and Log (2) are available - if (ke.isChar('1')) { - return handleTabKey(TAB_OVERVIEW); - } - if (ke.isChar('2')) { - return handleTabKey(TAB_LOG); + if (ke.isCtrlC()) { + runner.quit(); + return true; } - if (!isInfraSelected()) { - if (ke.isChar('3')) { - return handleTabKey(TAB_ROUTES); - } - if (ke.isChar('4')) { - return handleTabKey(TAB_ENDPOINTS); - } - if (ke.isChar('5')) { - return handleTabKey(TAB_HTTP); - } - if (ke.isChar('6')) { - return handleTabKey(TAB_HEALTH); - } - if (ke.isChar('7')) { - return handleTabKey(TAB_HISTORY); - } - if (ke.isChar('8')) { - return handleTabKey(TAB_ERRORS); + // Tab switching with number keys (skip when probe is editing text) + // When infra is selected, only Overview (1) and Log (2) are available + if (!probeEditing) { + if (ke.isChar('1')) { + return handleTabKey(TAB_OVERVIEW); } - if (ke.isChar('9')) { - return handleTabKey(TAB_METRICS); + if (ke.isChar('2')) { + return handleTabKey(TAB_LOG); } - if (ke.isChar('0')) { - return handleTabKey(TAB_MORE); + if (!isInfraSelected()) { + if (ke.isChar('3')) { + return handleTabKey(TAB_ROUTES); + } + if (ke.isChar('4')) { + return handleTabKey(TAB_ENDPOINTS); + } + if (ke.isChar('5')) { + return handleTabKey(TAB_HTTP); + } + if (ke.isChar('6')) { + return handleTabKey(TAB_HEALTH); + } + if (ke.isChar('7')) { + return handleTabKey(TAB_HISTORY); + } + if (ke.isChar('8')) { + return handleTabKey(TAB_ERRORS); + } + if (ke.isChar('9')) { + return handleTabKey(TAB_METRICS); + } + if (ke.isChar('0')) { + return handleTabKey(TAB_MORE); + } } } // Tab cycling (check Shift+Tab before Tab since Tab binding also matches Shift+Tab) - if (ke.isFocusPrevious()) { + // Skip tab cycling when HTTP probe is active (Tab navigates fields) + if (ke.isFocusPrevious() && !(tabsState.selected() == TAB_HTTP && httpTab.isProbeMode())) { if (isInfraSelected()) { // Cycle between Overview and Log only int prev = tabsState.selected() == TAB_OVERVIEW ? TAB_LOG : TAB_OVERVIEW; @@ -538,7 +547,7 @@ public class CamelMonitor extends CamelCommand { } return true; } - if (ke.isFocusNext()) { + if (ke.isFocusNext() && !(tabsState.selected() == TAB_HTTP && httpTab.isProbeMode())) { if (isInfraSelected()) { int next = tabsState.selected() == TAB_OVERVIEW ? TAB_LOG : TAB_OVERVIEW; tabsState.select(next); @@ -678,6 +687,10 @@ public class CamelMonitor extends CamelCommand { actionsPopup.handlePaste(pe.text()); return true; } + if (httpTab.isProbeMode()) { + httpTab.handlePaste(pe.text()); + return true; + } } if (event instanceof TickEvent) { long now = System.currentTimeMillis(); @@ -3361,6 +3374,9 @@ public class CamelMonitor extends CamelCommand { parseHttpEndpoints(phpObj, "managementEndpoints", true, info); } + // Fix REST DSL URLs that are missing the port by resolving from platform-http endpoints + fixRestUrlPorts(info); + // Parse configuration properties (from PropertiesDevConsole) JsonObject propsObj = (JsonObject) root.get("properties"); if (propsObj != null) { @@ -3406,6 +3422,47 @@ public class CamelMonitor extends CamelCommand { } } + private static void fixRestUrlPorts(IntegrationInfo info) { + // Find a base URL with port from platform-http endpoints + String baseWithPort = null; + for (HttpEndpointInfo ep : info.httpEndpoints) { + if (!ep.fromRest && ep.url != null) { + try { + URI uri = URI.create(ep.url); + if (uri.getPort() > 0) { + baseWithPort = uri.getScheme() + "://" + uri.getHost() + ":" + uri.getPort(); + break; + } + } catch (Exception e) { + // ignore + } + } + } + if (baseWithPort == null) { + return; + } + // Fix REST DSL endpoints missing port + for (HttpEndpointInfo ep : info.httpEndpoints) { + if (ep.fromRest && ep.url != null) { + String path = extractPath(ep.url); + if (path != null) { + // Check if URL has no port by looking at the host portion + int schemeEnd = ep.url.indexOf("://"); + if (schemeEnd > 0) { + String hostPart = ep.url.substring(schemeEnd + 3); + int slash = hostPart.indexOf('/'); + if (slash > 0) { + hostPart = hostPart.substring(0, slash); + } + if (!hostPart.contains(":")) { + ep.url = baseWithPort + path; + } + } + } + } + } + } + private static String extractPath(String url) { if (url == null) { return null; 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 06389239b243..1e9fbd0f83e8 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 @@ -16,11 +16,17 @@ */ package org.apache.camel.dsl.jbang.core.commands.tui; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -38,11 +44,14 @@ import dev.tamboui.tui.event.KeyEvent; import dev.tamboui.widgets.block.Block; import dev.tamboui.widgets.block.BorderType; import dev.tamboui.widgets.block.Title; +import dev.tamboui.widgets.input.TextInput; +import dev.tamboui.widgets.input.TextInputState; import dev.tamboui.widgets.paragraph.Paragraph; import dev.tamboui.widgets.table.Cell; import dev.tamboui.widgets.table.Row; import dev.tamboui.widgets.table.Table; import dev.tamboui.widgets.table.TableState; +import org.apache.camel.dsl.jbang.core.common.CamelCommandHelper; import org.apache.camel.dsl.jbang.core.common.PathUtils; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; @@ -55,6 +64,16 @@ class HttpTab implements MonitorTab { private static final Set<String> OPENAPI_HTTP_VERBS = Set.of("get", "post", "put", "delete", "patch", "options", "head", "trace"); + // Probe field constants + private static final int PROBE_METHOD = 0; + private static final int PROBE_PATH = 1; + private static final int PROBE_HEADERS = 2; + private static final int PROBE_BODY = 3; + private static final int PROBE_HISTORY = 4; + + private static final String[] PROBE_METHODS = { "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS" }; + private static final int MAX_PROBE_HISTORY = 20; + private final MonitorContext ctx; private final TableState tableState = new TableState(); private final AtomicBoolean specLoading = new AtomicBoolean(false); @@ -70,12 +89,43 @@ class HttpTab implements MonitorTab { private String specTitle; private int specScroll; + // Probe mode state + private boolean probeMode; + private String probeRouteId; + private String probeBaseUrl; + private int probeField = PROBE_PATH; + private int probeMethodIndex; + private final TextInputState probePathState = new TextInputState(""); + private final TextInputState probeBodyState = new TextInputState(""); + private List<HeaderEntry> probeHeaders; + private int probeSelectedHeader; + private boolean probeEditingHeaderKey; + private final AtomicBoolean probeSending = new AtomicBoolean(false); + private String probeResponseStatus; + private long probeResponseElapsed; + private List<String> probeResponseLines; + private boolean probeResponseError; + private int probeResponseScroll; + private final List<ProbeHistoryEntry> probeHistory = new ArrayList<>(); + private int probeHistoryIndex; + private boolean probePrettyPrint; + private String probeResponseRawBody; + private List<String> probeResponseHeaderLines; + HttpTab(MonitorContext ctx) { this.ctx = ctx; } + boolean isProbeMode() { + return probeMode; + } + @Override public boolean handleKeyEvent(KeyEvent ke) { + if (probeMode) { + return handleProbeKeyEvent(ke); + } + if (showSpec) { if (ke.isChar('c') || ke.isCancel()) { showSpec = false; @@ -97,6 +147,10 @@ class HttpTab implements MonitorTab { return true; } + if (ke.isConfirm()) { + enterProbeModeFromTable(); + return true; + } if (ke.isChar('s')) { sortIndex = (sortIndex + 1) % SORT_COLUMNS.length; sort = SORT_COLUMNS[sortIndex]; @@ -124,6 +178,10 @@ class HttpTab implements MonitorTab { @Override public boolean handleEscape() { + if (probeMode) { + probeMode = false; + return true; + } if (showSpec) { showSpec = false; return true; @@ -133,11 +191,23 @@ class HttpTab implements MonitorTab { @Override public void navigateUp() { + if (probeMode) { + if (probeField == PROBE_HISTORY && !probeHistory.isEmpty()) { + probeHistoryIndex = Math.max(0, probeHistoryIndex - 1); + } + return; + } tableState.selectPrevious(); } @Override public void navigateDown() { + if (probeMode) { + if (probeField == PROBE_HISTORY && !probeHistory.isEmpty()) { + probeHistoryIndex = Math.min(probeHistory.size() - 1, probeHistoryIndex + 1); + } + return; + } List<HttpEndpointInfo> visible = sortedVisibleEndpoints(ctx.findSelectedIntegration()); tableState.selectNext(visible.size()); } @@ -148,6 +218,7 @@ class HttpTab implements MonitorTab { specLines = Collections.emptyList(); specTitle = null; specScroll = 0; + probeMode = false; } @Override @@ -158,6 +229,11 @@ class HttpTab implements MonitorTab { return; } + if (probeMode) { + renderProbe(frame, area); + return; + } + if (showSpec) { renderSpec(frame, area); return; @@ -176,6 +252,19 @@ class HttpTab implements MonitorTab { @Override public void renderFooter(List<Span> spans) { + if (probeMode) { + hint(spans, "Esc", "back"); + hint(spans, "Tab", "fields"); + hint(spans, "Enter", "send"); + hint(spans, "+", "header"); + hint(spans, "p", "pretty" + (probePrettyPrint ? " [on]" : "")); + if (!probeHistory.isEmpty()) { + hintLast(spans, "↑↓", "history"); + } else { + hintLast(spans, "1-9", "tabs"); + } + return; + } if (showSpec) { hint(spans, "c/Esc", "close"); hint(spans, "↑↓", "scroll"); @@ -183,7 +272,7 @@ class HttpTab implements MonitorTab { return; } hint(spans, "Esc", "back"); - hint(spans, "↑↓", "navigate"); + hint(spans, "Enter", "probe"); hint(spans, "s", "sort"); String[] filterLabels = { "all", "rest", "http" }; hint(spans, "f", "filter [" + filterLabels[filter] + "]"); @@ -197,6 +286,852 @@ class HttpTab implements MonitorTab { } } + void handlePaste(String text) { + if (!probeMode || probeSending.get() || text == null || text.isEmpty()) { + return; + } + TextInputState target = probeActiveTextInput(); + if (target != null) { + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + if (ch != '\n' && ch != '\r') { + target.insert(ch); + } + } + } + } + + // ---- Probe mode ---- + + private void enterProbeModeFromTable() { + List<HttpEndpointInfo> visible = sortedVisibleEndpoints(ctx.findSelectedIntegration()); + Integer sel = tableState.selected(); + if (sel == null || sel < 0 || sel >= visible.size()) { + return; + } + enterProbeMode(visible.get(sel)); + } + + private void enterProbeMode(HttpEndpointInfo ep) { + probeMode = true; + probeField = PROBE_PATH; + probeRouteId = ep.routeId; + probeBaseUrl = extractBaseUrl(ep.url); + + // Pre-fill method + probeMethodIndex = 0; + if (ep.method != null) { + String m = ep.method.split(",")[0].trim().toUpperCase(Locale.ENGLISH); + for (int i = 0; i < PROBE_METHODS.length; i++) { + if (PROBE_METHODS[i].equals(m)) { + probeMethodIndex = i; + break; + } + } + } + + // Pre-fill path + String path = ep.path != null ? ep.path : (ep.url != null ? ep.url : "/"); + probePathState.clear(); + for (int i = 0; i < path.length(); i++) { + probePathState.insert(path.charAt(i)); + } + + // Pre-fill headers from endpoint metadata + probeHeaders = null; + if (ep.consumes != null && !ep.consumes.isEmpty()) { + addProbeHeader("Content-Type", ep.consumes); + } + if (ep.produces != null && !ep.produces.isEmpty()) { + addProbeHeader("Accept", ep.produces); + } + probeSelectedHeader = 0; + probeEditingHeaderKey = true; + + // Clear body and response + probeBodyState.clear(); + probeResponseStatus = null; + probeResponseElapsed = 0; + probeResponseLines = null; + probeResponseHeaderLines = null; + probeResponseRawBody = null; + probeResponseError = false; + probeResponseScroll = 0; + probeHistoryIndex = 0; + } + + private void addProbeHeader(String key, String value) { + if (probeHeaders == null) { + probeHeaders = new ArrayList<>(); + } + TextInputState keyState = new TextInputState(""); + TextInputState valState = new TextInputState(""); + for (int i = 0; i < key.length(); i++) { + keyState.insert(key.charAt(i)); + } + for (int i = 0; i < value.length(); i++) { + valState.insert(value.charAt(i)); + } + probeHeaders.add(new HeaderEntry(keyState, valState)); + } + + private boolean handleProbeKeyEvent(KeyEvent ke) { + if (probeSending.get()) { + return true; + } + if (ke.isConfirm()) { + if (probeField == PROBE_HISTORY && !probeHistory.isEmpty()) { + replayHistoryEntry(probeHistory.get(probeHistoryIndex)); + } else { + doProbeRequest(); + } + return true; + } + + // PgUp/PgDn scroll response + if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) { + probeResponseScroll = Math.max(0, probeResponseScroll - 10); + return true; + } + if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) { + probeResponseScroll += 10; + return true; + } + + // Toggle pretty print for response body + if (ke.isChar('p') && probeField != PROBE_PATH && probeField != PROBE_BODY + && probeField != PROBE_HEADERS) { + probePrettyPrint = !probePrettyPrint; + if (probeResponseLines != null) { + reformatResponseBody(); + } + return true; + } + + // Field-specific handling + if (probeField == PROBE_METHOD) { + if (ke.isKey(KeyCode.TAB) || ke.isDown()) { + probeField = PROBE_PATH; + return true; + } + if (ke.isUp()) { + if (!probeHistory.isEmpty()) { + probeField = PROBE_HISTORY; + } else { + probeField = PROBE_BODY; + } + return true; + } + if (ke.isLeft()) { + probeMethodIndex = (probeMethodIndex - 1 + PROBE_METHODS.length) % PROBE_METHODS.length; + return true; + } + if (ke.isRight()) { + probeMethodIndex = (probeMethodIndex + 1) % PROBE_METHODS.length; + return true; + } + return true; + } + if (probeField == PROBE_PATH) { + if (ke.isKey(KeyCode.TAB) || ke.isDown()) { + if (hasProbeHeaders()) { + probeField = PROBE_HEADERS; + probeSelectedHeader = 0; + probeEditingHeaderKey = true; + } else { + probeField = PROBE_BODY; + } + return true; + } + if (ke.isUp()) { + probeField = PROBE_METHOD; + return true; + } + if (ke.isChar('+')) { + addProbeHeaderEmpty(); + return true; + } + handleTextInput(ke, probePathState); + return true; + } + if (probeField == PROBE_HEADERS) { + return handleProbeHeaderKeyEvent(ke); + } + if (probeField == PROBE_BODY) { + if (ke.isKey(KeyCode.TAB) || ke.isDown()) { + if (!probeHistory.isEmpty()) { + probeField = PROBE_HISTORY; + } else { + probeField = PROBE_METHOD; + } + return true; + } + if (ke.isUp()) { + if (hasProbeHeaders()) { + probeField = PROBE_HEADERS; + probeSelectedHeader = probeHeaders.size() - 1; + probeEditingHeaderKey = false; + } else { + probeField = PROBE_PATH; + } + return true; + } + if (ke.isChar('+')) { + addProbeHeaderEmpty(); + return true; + } + handleTextInput(ke, probeBodyState); + return true; + } + if (probeField == PROBE_HISTORY) { + if (ke.isKey(KeyCode.TAB)) { + probeField = PROBE_METHOD; + return true; + } + if (ke.isUp()) { + if (probeHistoryIndex > 0) { + probeHistoryIndex--; + } else { + probeField = PROBE_BODY; + } + return true; + } + if (ke.isDown()) { + if (probeHistoryIndex < probeHistory.size() - 1) { + probeHistoryIndex++; + } + return true; + } + return true; + } + return true; + } + + private boolean handleProbeHeaderKeyEvent(KeyEvent ke) { + HeaderEntry current = probeHeaders.get(probeSelectedHeader); + TextInputState activeInput = probeEditingHeaderKey ? current.keyInput : current.valueInput; + + if (ke.isChar('+')) { + addProbeHeaderEmpty(); + return true; + } + if (ke.isKey(KeyCode.TAB) || ke.isDown()) { + if (probeEditingHeaderKey) { + probeEditingHeaderKey = false; + } else if (probeSelectedHeader < probeHeaders.size() - 1) { + probeSelectedHeader++; + probeEditingHeaderKey = true; + } else { + probeField = PROBE_BODY; + } + return true; + } + if (ke.isUp()) { + if (probeEditingHeaderKey) { + if (probeSelectedHeader > 0) { + probeSelectedHeader--; + probeEditingHeaderKey = false; + } else { + probeField = PROBE_PATH; + } + } else { + probeEditingHeaderKey = true; + } + return true; + } + if (ke.isDeleteBackward()) { + if (probeEditingHeaderKey && current.keyInput.text().isEmpty()) { + probeHeaders.remove(probeSelectedHeader); + if (probeHeaders.isEmpty()) { + probeHeaders = null; + probeField = PROBE_PATH; + } else if (probeSelectedHeader >= probeHeaders.size()) { + probeSelectedHeader = probeHeaders.size() - 1; + } + return true; + } + activeInput.deleteBackward(); + return true; + } + if (ke.isDeleteForward()) { + activeInput.deleteForward(); + return true; + } + if (ke.isLeft()) { + if (!probeEditingHeaderKey && activeInput.cursorPosition() == 0) { + probeEditingHeaderKey = true; + } else { + activeInput.moveCursorLeft(); + } + return true; + } + if (ke.isRight()) { + if (probeEditingHeaderKey && activeInput.cursorPosition() == activeInput.text().length()) { + probeEditingHeaderKey = false; + } else { + activeInput.moveCursorRight(); + } + return true; + } + if (ke.isHome()) { + activeInput.moveCursorToStart(); + return true; + } + if (ke.isEnd()) { + activeInput.moveCursorToEnd(); + return true; + } + if (ke.code() == KeyCode.CHAR) { + activeInput.insert(ke.character()); + return true; + } + return true; + } + + private void addProbeHeaderEmpty() { + if (probeHeaders == null) { + probeHeaders = new ArrayList<>(); + } + probeHeaders.add(new HeaderEntry(new TextInputState(""), new TextInputState(""))); + probeField = PROBE_HEADERS; + probeSelectedHeader = probeHeaders.size() - 1; + probeEditingHeaderKey = true; + } + + private boolean hasProbeHeaders() { + return probeHeaders != null && !probeHeaders.isEmpty(); + } + + private TextInputState probeActiveTextInput() { + if (probeField == PROBE_PATH) { + return probePathState; + } + if (probeField == PROBE_BODY) { + return probeBodyState; + } + if (probeField == PROBE_HEADERS && hasProbeHeaders()) { + HeaderEntry he = probeHeaders.get(probeSelectedHeader); + return probeEditingHeaderKey ? he.keyInput : he.valueInput; + } + return null; + } + + private void replayHistoryEntry(ProbeHistoryEntry entry) { + // Fill fields from history + for (int i = 0; i < PROBE_METHODS.length; i++) { + if (PROBE_METHODS[i].equals(entry.method)) { + probeMethodIndex = i; + break; + } + } + probePathState.clear(); + for (int i = 0; i < entry.path.length(); i++) { + probePathState.insert(entry.path.charAt(i)); + } + probeBodyState.clear(); + if (entry.body != null) { + for (int i = 0; i < entry.body.length(); i++) { + probeBodyState.insert(entry.body.charAt(i)); + } + } + probeHeaders = null; + if (entry.headers != null) { + for (HeaderEntry he : entry.headers) { + addProbeHeader(he.keyInput.text(), he.valueInput.text()); + } + } + probeField = PROBE_BODY; + doProbeRequest(); + } + + private void doProbeRequest() { + if (ctx.runner == null || probeBaseUrl == null) { + return; + } + if (!probeSending.compareAndSet(false, true)) { + return; + } + + probeResponseStatus = "Sending..."; + probeResponseElapsed = 0; + probeResponseLines = null; + probeResponseError = false; + probeResponseScroll = 0; + + String method = PROBE_METHODS[probeMethodIndex]; + String path = probePathState.text(); + String body = probeBodyState.text(); + String baseUrl = probeBaseUrl; + + // Snapshot headers + List<HeaderEntry> headerSnapshot = null; + if (hasProbeHeaders()) { + headerSnapshot = new ArrayList<>(); + for (HeaderEntry he : probeHeaders) { + headerSnapshot.add(new HeaderEntry( + new TextInputState(he.keyInput.text()), + new TextInputState(he.valueInput.text()))); + } + } + List<HeaderEntry> hdrs = headerSnapshot; + + ctx.runner.scheduler().execute(() -> { + try { + doProbeRequestInBackground(baseUrl, method, path, body, hdrs); + } finally { + probeSending.set(false); + } + }); + } + + private void doProbeRequestInBackground( + String baseUrl, String method, String path, String body, List<HeaderEntry> hdrs) { + + String url = baseUrl + path; + String statusText; + long elapsed = 0; + boolean error = false; + List<String> headerLines = new ArrayList<>(); + String rawBody = null; + int httpStatus = 0; + + try { + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + boolean hasBody = body != null && !body.isEmpty(); + HttpRequest.BodyPublisher bodyPublisher = hasBody + ? HttpRequest.BodyPublishers.ofString(body) + : HttpRequest.BodyPublishers.noBody(); + + HttpRequest.Builder reqBuilder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(20)) + .method(method, bodyPublisher); + + // Add user headers + if (hdrs != null) { + for (HeaderEntry he : hdrs) { + String k = he.keyInput.text().trim(); + String v = he.valueInput.text(); + if (!k.isEmpty()) { + reqBuilder.header(k, v); + } + } + } + + long start = System.currentTimeMillis(); + HttpResponse<String> response = client.send(reqBuilder.build(), + HttpResponse.BodyHandlers.ofString()); + elapsed = System.currentTimeMillis() - start; + + httpStatus = response.statusCode(); + statusText = String.valueOf(httpStatus); + + // Response headers + for (Map.Entry<String, List<String>> entry : response.headers().map().entrySet()) { + String k = entry.getKey(); + if (k == null || k.startsWith(":")) { + continue; + } + for (String v : entry.getValue()) { + headerLines.add(k + ": " + v); + } + } + + // Response body + String responseBody = response.body(); + if (responseBody != null && !responseBody.isEmpty()) { + rawBody = responseBody; + } + + if (httpStatus >= 400) { + error = true; + } + } catch (Exception e) { + statusText = "Error"; + error = true; + String msg = e.getMessage(); + headerLines.add(msg != null ? msg : e.getClass().getSimpleName()); + } + + // Build history entry + List<HeaderEntry> histHeaders = null; + if (hdrs != null && !hdrs.isEmpty()) { + histHeaders = new ArrayList<>(hdrs); + } + ProbeHistoryEntry histEntry = new ProbeHistoryEntry( + method, path, histHeaders, body, + httpStatus, elapsed, statusText, error); + + // Apply results on render thread + String finalStatus = statusText; + long finalElapsed = elapsed; + boolean finalError = error; + List<String> finalHeaderLines = headerLines; + String finalRawBody = rawBody; + + if (ctx.runner != null) { + ctx.runner.runOnRenderThread(() -> { + probeResponseStatus = finalStatus; + probeResponseElapsed = finalElapsed; + probeResponseError = finalError; + probeResponseHeaderLines = finalHeaderLines; + probeResponseRawBody = finalRawBody; + probeResponseScroll = 0; + rebuildResponseLines(); + + // Add to history (most recent first) + probeHistory.add(0, histEntry); + if (probeHistory.size() > MAX_PROBE_HISTORY) { + probeHistory.remove(probeHistory.size() - 1); + } + probeHistoryIndex = 0; + }); + } + } + + private static String extractBaseUrl(String url) { + if (url == null) { + return "http://localhost:8080"; + } + // Extract scheme + host + port from full URL like http://0.0.0.0:8080/api/hello + try { + URI uri = URI.create(url); + String host = uri.getHost(); + if ("0.0.0.0".equals(host)) { + host = "localhost"; + } + int port = uri.getPort(); + String scheme = uri.getScheme() != null ? uri.getScheme() : "http"; + if (port > 0) { + return scheme + "://" + host + ":" + port; + } + return scheme + "://" + host; + } catch (Exception e) { + return "http://localhost:8080"; + } + } + + private void rebuildResponseLines() { + List<String> lines = new ArrayList<>(); + if (probeResponseHeaderLines != null) { + lines.addAll(probeResponseHeaderLines); + } + if (probeResponseRawBody != null && !probeResponseRawBody.isEmpty()) { + lines.add(""); + String body = probeResponseRawBody; + if (probePrettyPrint) { + body = prettyFormat(body); + } + for (String line : body.split("\n", -1)) { + lines.add(line); + } + } + probeResponseLines = lines; + } + + private void reformatResponseBody() { + rebuildResponseLines(); + probeResponseScroll = 0; + } + + private static String prettyFormat(String text) { + if (text == null || text.isEmpty()) { + return text; + } + return CamelCommandHelper.valueAsStringPretty(text, false); + } + + // ---- Probe rendering ---- + + private void renderProbe(Frame frame, Rect area) { + int headerCount = hasProbeHeaders() ? probeHeaders.size() : 0; + int requestHeight = 7 + headerCount + (headerCount > 0 ? 1 : 0); + int historyHeight = Math.min(4 + 2, probeHistory.size() + 2); + if (historyHeight < 3) { + historyHeight = 3; + } + + List<Rect> chunks = Layout.vertical() + .constraints( + Constraint.length(requestHeight), + Constraint.fill(), + Constraint.length(historyHeight)) + .split(area); + + renderProbeRequest(frame, chunks.get(0)); + renderProbeResponse(frame, chunks.get(1)); + renderProbeHistory(frame, chunks.get(2)); + } + + private void renderProbeRequest(Frame frame, Rect area) { + String method = PROBE_METHODS[probeMethodIndex]; + Title title = Title.from(Line.from( + Span.styled(" HTTP Probe ", Style.EMPTY.bold()), + Span.styled(method, methodStyle(method).bold()), + Span.raw(" " + probePathState.text() + " "))); + + Block block = Block.builder().borderType(BorderType.ROUNDED).title(title).build(); + frame.renderWidget(block, area); + + int innerX = area.left() + 2; + int innerW = area.width() - 4; + int labelW = 10; + int fieldW = innerW - labelW; + int row = area.top() + 1; + + // Method selector + renderProbeLabel(frame, innerX, row, labelW, "Method:", probeField == PROBE_METHOD); + Rect methodArea = new Rect(innerX + labelW, row, fieldW, 1); + Style methodSt = methodStyle(method); + String leftArr = probeField == PROBE_METHOD ? "◀ " : " "; + String rightArr = probeField == PROBE_METHOD ? " ▶" : ""; + frame.renderWidget(Paragraph.from(Line.from( + Span.styled(leftArr, methodSt), + Span.styled(method, methodSt.bold()), + Span.styled(rightArr, methodSt))), methodArea); + + // Full URL (read-only) + row++; + renderProbeLabel(frame, innerX, row, labelW, "URL:", false); + String fullUrl = (probeBaseUrl != null ? probeBaseUrl : "") + probePathState.text(); + Rect urlArea = new Rect(innerX + labelW, row, fieldW, 1); + frame.renderWidget(Paragraph.from(Line.from( + Span.styled(fullUrl, Style.EMPTY.dim()))), urlArea); + + // Path input + row++; + renderProbeLabel(frame, innerX, row, labelW, "Path:", probeField == PROBE_PATH); + Rect pathArea = new Rect(innerX + labelW, row, fieldW, 1); + if (probeField == PROBE_PATH && !probeSending.get()) { + TextInput textInput = TextInput.builder().cursorStyle(Style.EMPTY.reversed()).build(); + frame.renderStatefulWidget(textInput, pathArea, probePathState); + } else { + String pathText = probePathState.text(); + frame.renderWidget(Paragraph.from(Line.from( + Span.styled(pathText.isEmpty() ? "/" : pathText, + pathText.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY))), + pathArea); + } + + // Headers + if (hasProbeHeaders()) { + int keyW = Math.min(20, fieldW / 3); + int valW = fieldW - keyW - 3; + for (int i = 0; i < probeHeaders.size(); i++) { + row++; + boolean isSelected = probeField == PROBE_HEADERS && probeSelectedHeader == i; + String label = i == 0 ? "Headers:" : ""; + renderProbeLabel(frame, innerX, row, labelW, label, + isSelected || (i == 0 && probeField == PROBE_HEADERS)); + + HeaderEntry he = probeHeaders.get(i); + int fieldX = innerX + labelW; + + Rect keyArea = new Rect(fieldX, row, keyW, 1); + if (isSelected && probeEditingHeaderKey && !probeSending.get()) { + TextInput keyInput = TextInput.builder().cursorStyle(Style.EMPTY.reversed()).build(); + frame.renderStatefulWidget(keyInput, keyArea, he.keyInput); + } else { + String keyText = he.keyInput.text(); + Style keyStyle = keyText.isEmpty() ? Style.EMPTY.dim() + : isSelected ? Style.EMPTY.bold() : Style.EMPTY; + frame.renderWidget(Paragraph.from(Line.from( + Span.styled(keyText.isEmpty() ? "<key>" : keyText, keyStyle))), keyArea); + } + + Rect sepArea = new Rect(fieldX + keyW, row, 3, 1); + frame.renderWidget(Paragraph.from(Line.from( + Span.styled(" : ", Style.EMPTY.dim()))), sepArea); + + Rect valArea = new Rect(fieldX + keyW + 3, row, valW, 1); + if (isSelected && !probeEditingHeaderKey && !probeSending.get()) { + TextInput valInput = TextInput.builder().cursorStyle(Style.EMPTY.reversed()).build(); + frame.renderStatefulWidget(valInput, valArea, he.valueInput); + } else { + String valText = he.valueInput.text(); + Style valStyle = valText.isEmpty() ? Style.EMPTY.dim() + : isSelected ? Style.EMPTY.bold() : Style.EMPTY; + frame.renderWidget(Paragraph.from(Line.from( + Span.styled(valText.isEmpty() ? "<value>" : valText, valStyle))), valArea); + } + } + } + + // Body input + row++; + renderProbeLabel(frame, innerX, row, labelW, "Body:", probeField == PROBE_BODY); + Rect bodyArea = new Rect(innerX + labelW, row, fieldW, 1); + if (probeField == PROBE_BODY && !probeSending.get()) { + TextInput textInput = TextInput.builder().cursorStyle(Style.EMPTY.reversed()).build(); + frame.renderStatefulWidget(textInput, bodyArea, probeBodyState); + } else { + String bodyText = probeBodyState.text(); + frame.renderWidget(Paragraph.from(Line.from( + Span.styled(bodyText.isEmpty() ? "—" : bodyText, + bodyText.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY))), + bodyArea); + } + } + + private void renderProbeResponse(Frame frame, Rect area) { + String titleStr; + Style titleStyle = Style.EMPTY.bold(); + if (probeResponseStatus == null) { + titleStr = " Response "; + } else if (probeResponseError) { + titleStr = " Response " + probeResponseStatus + " "; + titleStyle = Style.EMPTY.fg(Color.LIGHT_RED).bold(); + } else if ("Sending...".equals(probeResponseStatus)) { + titleStr = " Sending... "; + titleStyle = Style.EMPTY.fg(Color.YELLOW).bold(); + } else { + titleStr = " Response " + probeResponseStatus; + if (probeResponseElapsed > 0) { + titleStr += " (" + probeResponseElapsed + "ms)"; + } + titleStr += " "; + titleStyle = statusStyle(probeResponseStatus); + } + + Title title = Title.from(Line.from(Span.styled(titleStr, titleStyle))); + + if (probeResponseLines == null || probeResponseLines.isEmpty()) { + String placeholder = probeResponseStatus == null + ? " Press Enter to send request" + : " No response content"; + frame.renderWidget( + Paragraph.builder() + .text(Text.from(Line.from(Span.styled(placeholder, Style.EMPTY.dim())))) + .block(Block.builder().borderType(BorderType.ROUNDED).title(title).build()) + .build(), + area); + return; + } + + int visibleLines = area.height() - 2; + if (visibleLines < 1) { + visibleLines = 1; + } + int maxScroll = Math.max(0, probeResponseLines.size() - visibleLines); + probeResponseScroll = Math.min(probeResponseScroll, maxScroll); + + int end = Math.min(probeResponseScroll + visibleLines, probeResponseLines.size()); + List<Line> lines = new ArrayList<>(); + for (int i = probeResponseScroll; i < end; i++) { + String line = probeResponseLines.get(i); + if (line.isEmpty()) { + lines.add(Line.from(Span.raw(""))); + } else if (line.contains(": ") && !line.startsWith(" ") && !line.startsWith("{") + && !line.startsWith("[") && !line.startsWith("\"")) { + // Header line + int colon = line.indexOf(": "); + lines.add(Line.from( + Span.styled(line.substring(0, colon + 1), Style.EMPTY.fg(Color.YELLOW)), + Span.raw(line.substring(colon + 1)))); + } else { + lines.add(Line.from(Span.raw(line))); + } + } + + frame.renderWidget( + Paragraph.builder() + .text(Text.from(lines)) + .block(Block.builder().borderType(BorderType.ROUNDED).title(title).build()) + .build(), + area); + } + + private void renderProbeHistory(Frame frame, Rect area) { + String title = " History [" + probeHistory.size() + "] "; + + if (probeHistory.isEmpty()) { + frame.renderWidget( + Paragraph.builder() + .text(Text.from(Line.from( + Span.styled(" No requests yet", Style.EMPTY.dim())))) + .block(Block.builder().borderType(BorderType.ROUNDED).title(title).build()) + .build(), + area); + return; + } + + int visibleLines = area.height() - 2; + if (visibleLines < 1) { + visibleLines = 1; + } + + // Ensure selected index is visible + int start = 0; + if (probeHistoryIndex >= visibleLines) { + start = probeHistoryIndex - visibleLines + 1; + } + int end = Math.min(start + visibleLines, probeHistory.size()); + + List<Line> lines = new ArrayList<>(); + for (int i = start; i < end; i++) { + ProbeHistoryEntry entry = probeHistory.get(i); + boolean selected = probeField == PROBE_HISTORY && i == probeHistoryIndex; + String pointer = selected ? "► " : " "; + String methodStr = String.format("%-8s", entry.method); + String statusStr = entry.error ? "ERR" : entry.statusText; + String elapsedStr = entry.elapsed > 0 ? entry.elapsed + "ms" : ""; + String bodySnippet = entry.body != null && !entry.body.isEmpty() + ? " " + truncate(entry.body, 30) + : ""; + + Style lineStyle = selected ? Style.EMPTY.bold() : Style.EMPTY; + lines.add(Line.from( + Span.styled(pointer, lineStyle), + Span.styled(methodStr, methodStyle(entry.method)), + Span.styled(entry.path + " ", lineStyle), + Span.styled(statusStr, selected ? statusStyle(statusStr) : statusStyle(statusStr).dim()), + Span.styled(" " + elapsedStr, Style.EMPTY.dim()), + Span.styled(bodySnippet, Style.EMPTY.dim()))); + } + + frame.renderWidget( + Paragraph.builder() + .text(Text.from(lines)) + .block(Block.builder().borderType(BorderType.ROUNDED).title(title).build()) + .build(), + area); + } + + private void renderProbeLabel(Frame frame, int x, int y, int w, String label, boolean selected) { + Style style = selected ? Style.EMPTY.bold() : Style.EMPTY.dim(); + Rect labelArea = new Rect(x, y, w, 1); + frame.renderWidget(Paragraph.from(Line.from(Span.styled(label, style))), labelArea); + } + + private static Style statusStyle(String status) { + if (status == null) { + return Style.EMPTY.bold(); + } + try { + int code = Integer.parseInt(status); + if (code >= 200 && code < 300) { + return Style.EMPTY.fg(Color.GREEN).bold(); + } + if (code >= 300 && code < 400) { + return Style.EMPTY.fg(Color.CYAN).bold(); + } + if (code >= 400 && code < 500) { + return Style.EMPTY.fg(Color.YELLOW).bold(); + } + if (code >= 500) { + return Style.EMPTY.fg(Color.LIGHT_RED).bold(); + } + } catch (NumberFormatException e) { + // not a number — treat as error + } + return Style.EMPTY.fg(Color.LIGHT_RED).bold(); + } + + // ---- Existing table/spec methods ---- + List<HttpEndpointInfo> sortedVisibleEndpoints(IntegrationInfo info) { List<HttpEndpointInfo> visible = visibleEndpoints(info); visible.sort((a, b) -> { @@ -263,7 +1198,7 @@ class HttpTab implements MonitorTab { frame.renderWidget(Paragraph.from(Line.from(spans)), area); } - private static Style methodStyle(String method) { + static Style methodStyle(String method) { if (method == null) { return Style.EMPTY; } @@ -466,7 +1401,7 @@ class HttpTab implements MonitorTab { root.put("filter", specUri); Path actionFile = ctx.getActionFile(pid); - org.apache.camel.dsl.jbang.core.common.PathUtils.writeTextSafely(root.toJson(), actionFile); + PathUtils.writeTextSafely(root.toJson(), actionFile); JsonObject jo = pollJsonResponse(outputFile, 5000); PathUtils.deleteFile(outputFile); @@ -594,6 +1529,31 @@ class HttpTab implements MonitorTab { return MonitorContext.sortStyle(column, sort); } + private static void handleTextInput(KeyEvent ke, TextInputState state) { + if (ke.isDeleteBackward()) { + state.deleteBackward(); + } else if (ke.isDeleteForward()) { + state.deleteForward(); + } else if (ke.isLeft()) { + state.moveCursorLeft(); + } else if (ke.isRight()) { + state.moveCursorRight(); + } else if (ke.isHome()) { + state.moveCursorToStart(); + } else if (ke.isEnd()) { + state.moveCursorToEnd(); + } else if (ke.code() == KeyCode.CHAR) { + state.insert(ke.character()); + } + } + + private static String truncate(String s, int max) { + if (s == null) { + return ""; + } + return s.length() <= max ? s : s.substring(0, max - 1) + "…"; + } + @Override public SelectionContext getSelectionContext() { IntegrationInfo info = ctx.findSelectedIntegration(); @@ -607,4 +1567,14 @@ class HttpTab implements MonitorTab { Integer sel = tableState.selected(); return new SelectionContext("table", items, sel != null ? sel : -1, items.size(), "HTTP"); } + + record HeaderEntry(TextInputState keyInput, TextInputState valueInput) { + } + + record ProbeHistoryEntry( + String method, String path, + List<HeaderEntry> headers, String body, + int statusCode, long elapsed, + String statusText, boolean error) { + } }
