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/05/30 14:15:33 UTC
(camel) 02/03: CAMEL-23648: camel-jbang - TUI HTTP tab add probe view for sending requests
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 <cl...@gmail.com>
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 <no...@anthropic.com>
---
.../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) {
+ }
}