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 12f0ef22011b183aefe5b686f494a46c0407f09e Author: Claus Ibsen <[email protected]> AuthorDate: Mon May 18 10:06:08 2026 +0200 TUI: add HTTP Services tab (REST DSL + Platform-HTTP unified view) New tab 9 "HTTP" shows all HTTP endpoints exposed by the selected integration — both REST DSL service definitions and Platform-HTTP bindings — in a unified table with a detail panel. Layout: - Server info row: binding address, REST/HTTP/Mgmt endpoint counts - Table: METHOD (color-coded), PATH, PRODUCES, SOURCE, STATE - Detail panel: full URL, consumes/produces, source type, in/out types, description (REST DSL fields shown when applicable) Method colors: GET=green, POST=yellow, PUT=cyan, DELETE=red, PATCH=orange Data model: - HttpEndpointInfo class with fields from both REST DSL and Platform-HTTP - Parsed from "rests" and "platform-http" JSON sections in status file - IntegrationInfo.httpEndpoints + httpServer fields added Controls: - s/S: sort by method/path/source (toggle, reverse) - f: cycle filter — all / REST DSL only / Platform-HTTP only - m: toggle management endpoint visibility (off by default) Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 390 ++++++++++++++++++++- 1 file changed, 381 insertions(+), 9 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 5cef03fcc771..4bb2862a8527 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 @@ -31,6 +31,7 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -114,7 +115,7 @@ public class CamelMonitor extends CamelCommand { private static final int MAX_ENDPOINT_CHART_POINTS = 60; private static final int MAX_LOG_LINES = 3000; private static final int MAX_TRACES = 200; - private static final int NUM_TABS = 8; + private static final int NUM_TABS = 9; // Tab indices private static final int TAB_OVERVIEW = 0; @@ -125,6 +126,7 @@ public class CamelMonitor extends CamelCommand { private static final int TAB_HEALTH = 5; private static final int TAB_HISTORY = 6; private static final int TAB_CIRCUIT_BREAKER = 7; + private static final int TAB_HTTP = 8; // Overview sort columns private static final String[] OVERVIEW_SORT_COLUMNS = { "pid", "name", "version", "status", "total", "fail" }; @@ -141,6 +143,9 @@ public class CamelMonitor extends CamelCommand { // Circuit breaker sort columns (order matches table column order) private static final String[] CB_SORT_COLUMNS = { "route", "id", "component", "state" }; + // HTTP sort columns + private static final String[] HTTP_SORT_COLUMNS = { "method", "path", "source" }; + @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1") String name = "*"; @@ -158,6 +163,7 @@ public class CamelMonitor extends CamelCommand { private final TableState healthTableState = new TableState(); private final TableState endpointTableState = new TableState(); private final TableState cbTableState = new TableState(); + private final TableState httpTableState = new TableState(); private final TableState processorTableState = new TableState(); private final TableState routeHeaderTableState = new TableState(); private final TabsState tabsState = new TabsState(TAB_OVERVIEW); @@ -222,6 +228,14 @@ public class CamelMonitor extends CamelCommand { private int cbSortIndex = 0; private boolean cbSortReversed; + // HTTP tab sort/filter state + private String httpSort = "method"; + private int httpSortIndex = 0; + private boolean httpSortReversed; + // httpFilter: 0=all, 1=REST DSL only, 2=Platform-HTTP only + private int httpFilter; + private boolean httpShowManagement; + // Health filter state private boolean showOnlyDown; @@ -420,6 +434,9 @@ public class CamelMonitor extends CamelCommand { if (ke.isChar('8')) { return handleTabKey(TAB_CIRCUIT_BREAKER); } + if (ke.isChar('9')) { + return handleTabKey(TAB_HTTP); + } // Tab cycling if (ke.isFocusNext()) { @@ -585,6 +602,26 @@ public class CamelMonitor extends CamelCommand { return true; } + // HTTP tab: sort, filter, management toggle + if (tab == TAB_HTTP && ke.isChar('s')) { + httpSortIndex = (httpSortIndex + 1) % HTTP_SORT_COLUMNS.length; + httpSort = HTTP_SORT_COLUMNS[httpSortIndex]; + httpSortReversed = false; + return true; + } + if (tab == TAB_HTTP && ke.isChar('S')) { + httpSortReversed = !httpSortReversed; + return true; + } + if (tab == TAB_HTTP && ke.isCharIgnoreCase('f')) { + httpFilter = (httpFilter + 1) % 3; + return true; + } + if (tab == TAB_HTTP && ke.isCharIgnoreCase('m')) { + httpShowManagement = !httpShowManagement; + return true; + } + // Endpoints tab: sort and filter if (tab == TAB_ENDPOINTS && ke.isChar('s')) { endpointSortIndex = (endpointSortIndex + 1) % ENDPOINT_SORT_COLUMNS.length; @@ -969,6 +1006,7 @@ public class CamelMonitor extends CamelCommand { } case TAB_ROUTES -> routeTableState.selectPrevious(); case TAB_CIRCUIT_BREAKER -> cbTableState.selectPrevious(); + case TAB_HTTP -> httpTableState.selectPrevious(); case TAB_LOG -> { logFollowMode = false; logScroll = Math.max(0, logScroll - 1); @@ -1004,6 +1042,10 @@ public class CamelMonitor extends CamelCommand { IntegrationInfo info = findSelectedIntegration(); cbTableState.selectNext(info != null ? info.circuitBreakers.size() : 0); } + case TAB_HTTP -> { + List<HttpEndpointInfo> visible = visibleHttpEndpoints(findSelectedIntegration()); + httpTableState.selectNext(visible.size()); + } case TAB_LOG -> logScroll++; case TAB_HISTORY -> { if (!traces.get().isEmpty()) { @@ -1090,6 +1132,7 @@ public class CamelMonitor extends CamelCommand { ? sel.healthChecks.stream().filter(hc -> "DOWN".equals(hc.state)).count() : 0; int historyCount = hasSelection ? historyEntries.size() : 0; boolean hasTraces = hasSelection && !traces.get().isEmpty(); + int httpCount = hasSelection ? sel.httpEndpoints.size() : 0; // Row 0: label-only titles — fixed width so the tab bar never shifts when badges appear Line[] labels = { @@ -1101,6 +1144,7 @@ public class CamelMonitor extends CamelCommand { Line.from(" 6 Health "), Line.from(" 7 Inspect "), Line.from(" 8 Circuit Breaker "), + Line.from(" 9 HTTP "), }; Tabs tabs = Tabs.builder() @@ -1120,7 +1164,7 @@ public class CamelMonitor extends CamelCommand { int badgeY = area.y(); int dividerW = CharWidth.of(" | "); - String[] badgeTexts = { "", "", "", "", "", "", "", "" }; + String[] badgeTexts = { "", "", "", "", "", "", "", "", "" }; Style[] badgeStyles = new Style[labels.length]; Style yellow = Style.EMPTY.fg(Color.YELLOW).bold(); Style cyan = Style.EMPTY.fg(Color.CYAN).bold(); @@ -1161,6 +1205,9 @@ public class CamelMonitor extends CamelCommand { } else if (cbCount > 0) { badgeTexts[7] = "(" + cbCount + ")"; } + if (httpCount > 0) { + badgeTexts[8] = "(" + httpCount + ")"; + } int tabX = 0; for (int i = 0; i < labels.length; i++) { @@ -1192,6 +1239,7 @@ public class CamelMonitor extends CamelCommand { case TAB_HEALTH -> renderHealth(frame, area); case TAB_LOG -> renderLog(frame, area); case TAB_HISTORY -> renderInspect(frame, area); + case TAB_HTTP -> renderHttp(frame, area); } } @@ -3426,6 +3474,233 @@ public class CamelMonitor extends CamelCommand { return entry; } + // ---- Tab 9: HTTP Services ---- + + private List<HttpEndpointInfo> visibleHttpEndpoints(IntegrationInfo info) { + if (info == null) { + return Collections.emptyList(); + } + List<HttpEndpointInfo> result = new ArrayList<>(); + for (HttpEndpointInfo ep : info.httpEndpoints) { + if (ep.management && !httpShowManagement) { + continue; + } + if (httpFilter == 1 && !ep.fromRest) { + continue; + } + if (httpFilter == 2 && ep.fromRest) { + continue; + } + result.add(ep); + } + return result; + } + + private void renderHttp(Frame frame, Rect area) { + IntegrationInfo info = findSelectedIntegration(); + if (info == null) { + renderNoSelection(frame, area); + return; + } + + List<HttpEndpointInfo> visible = visibleHttpEndpoints(info); + + // Sort + visible.sort((a, b) -> { + int result = switch (httpSort) { + case "path" -> { + String pa = a.path != null ? a.path : ""; + String pb = b.path != null ? b.path : ""; + yield pa.compareToIgnoreCase(pb); + } + case "source" -> Boolean.compare(b.fromRest, a.fromRest); + default -> { // method + String ma = a.method != null ? a.method : ""; + String mb = b.method != null ? b.method : ""; + yield ma.compareToIgnoreCase(mb); + } + }; + return httpSortReversed ? -result : result; + }); + + // Layout: server info row (1) + table (fill) + detail (10) + List<Rect> chunks = Layout.vertical() + .constraints(Constraint.length(1), Constraint.fill(), Constraint.length(10)) + .split(area); + + // Server info row + renderHttpServerInfo(frame, chunks.get(0), info, visible); + + // Table + renderHttpTable(frame, chunks.get(1), visible); + + // Detail panel + renderHttpDetail(frame, chunks.get(2), visible); + } + + private void renderHttpServerInfo(Frame frame, Rect area, IntegrationInfo info, List<HttpEndpointInfo> visible) { + List<Span> spans = new ArrayList<>(); + if (info.httpServer != null) { + spans.add(Span.styled(" Server: ", Style.EMPTY.fg(Color.YELLOW).bold())); + spans.add(Span.styled(info.httpServer, Style.EMPTY.fg(Color.CYAN))); + spans.add(Span.raw(" ")); + } + long restCount = info.httpEndpoints.stream().filter(e -> e.fromRest).count(); + long httpCount = info.httpEndpoints.stream().filter(e -> !e.fromRest && !e.management).count(); + long mgmtCount = info.httpEndpoints.stream().filter(e -> e.management).count(); + if (restCount > 0) { + spans.add(Span.styled("REST: ", Style.EMPTY.fg(Color.GREEN))); + spans.add(Span.raw(restCount + " ")); + } + if (httpCount > 0) { + spans.add(Span.styled("HTTP: ", Style.EMPTY.fg(Color.CYAN))); + spans.add(Span.raw(httpCount + " ")); + } + if (mgmtCount > 0) { + spans.add(Span.styled("Mgmt: ", Style.EMPTY.fg(Color.YELLOW).dim())); + spans.add(Span.raw(mgmtCount + "")); + } + frame.renderWidget(Paragraph.from(Line.from(spans)), area); + } + + private static Style methodStyle(String method) { + if (method == null) { + return Style.EMPTY; + } + // Use first verb if comma-separated + String m = method.split(",")[0].trim().toUpperCase(Locale.ENGLISH); + return switch (m) { + case "GET" -> Style.EMPTY.fg(Color.GREEN); + case "POST" -> Style.EMPTY.fg(Color.YELLOW); + case "PUT" -> Style.EMPTY.fg(Color.CYAN); + case "DELETE" -> Style.EMPTY.fg(Color.LIGHT_RED); + case "PATCH" -> Style.EMPTY.fg(Color.rgb(0xFF, 0x80, 0x00)); + default -> Style.EMPTY.dim(); + }; + } + + private void renderHttpTable(Frame frame, Rect area, List<HttpEndpointInfo> visible) { + List<Row> rows = new ArrayList<>(); + for (HttpEndpointInfo ep : visible) { + String method = ep.method != null ? ep.method : ""; + String path = ep.path != null ? ep.path : (ep.url != null ? ep.url : ""); + String produces = ep.produces != null ? ep.produces : ""; + String source; + if (ep.management) { + source = "Mgmt"; + } else if (ep.fromRest) { + source = ep.contractFirst ? "REST(contract)" : "REST(code)"; + } else { + source = "HTTP"; + } + String state = ep.state != null ? ep.state : ""; + rows.add(Row.from( + Cell.from(Span.styled(method, methodStyle(method))), + Cell.from(path), + Cell.from(produces), + Cell.from(Span.styled(source, + ep.fromRest ? Style.EMPTY.fg(Color.GREEN) : Style.EMPTY.fg(Color.CYAN))), + Cell.from(Span.styled(state, + "Stopped".equals(state) ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY)))); + } + + String sortLabel = httpSort + (httpSortReversed ? " ↓" : " ↑"); + String title = String.format(" HTTP Services [%d] sort:%s ", visible.size(), sortLabel); + + Row header = Row.from( + Cell.from(Span.styled("METHOD", Style.EMPTY.bold())), + Cell.from(Span.styled("PATH", Style.EMPTY.bold())), + Cell.from(Span.styled("PRODUCES", Style.EMPTY.bold())), + Cell.from(Span.styled("SOURCE", Style.EMPTY.bold())), + Cell.from(Span.styled("STATE", Style.EMPTY.bold()))); + + Table table = Table.builder() + .rows(rows) + .header(header) + .widths( + Constraint.length(14), + Constraint.fill(), + Constraint.length(24), + Constraint.length(15), + Constraint.length(8)) + .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) + .highlightSpacing(Table.HighlightSpacing.ALWAYS) + .block(Block.builder().borderType(BorderType.ROUNDED).title(title).build()) + .build(); + + frame.renderStatefulWidget(table, area, httpTableState); + } + + private void renderHttpDetail(Frame frame, Rect area, List<HttpEndpointInfo> visible) { + Integer sel = httpTableState.selected(); + if (sel == null || sel < 0 || sel >= visible.size()) { + frame.renderWidget( + Paragraph.builder() + .text(Text.from(Line.from( + Span.styled(" Select an endpoint to view details", + Style.EMPTY.dim())))) + .block(Block.builder().borderType(BorderType.ROUNDED) + .title(" Detail ").build()) + .build(), + area); + return; + } + + HttpEndpointInfo ep = visible.get(sel); + List<Span> titleSpans = new ArrayList<>(); + if (ep.method != null) { + titleSpans.add(Span.raw(" ")); + titleSpans.add(Span.styled(ep.method, methodStyle(ep.method).bold())); + titleSpans.add(Span.raw(" ")); + } + if (ep.path != null) { + titleSpans.add(Span.raw(ep.path + " ")); + } + Title detailTitle = Title.from(Line.from(titleSpans)); + + List<Line> lines = new ArrayList<>(); + addDetailLine(lines, "URL", ep.url); + addDetailLine(lines, "Consumes", ep.consumes); + addDetailLine(lines, "Produces", ep.produces); + String sourceStr; + if (ep.management) { + sourceStr = "Platform-HTTP (management)"; + } else if (ep.fromRest) { + sourceStr = "REST DSL (" + (ep.contractFirst ? "contract-first" : "code-first") + ")"; + } else { + sourceStr = "Platform-HTTP"; + } + addDetailLine(lines, "Source", sourceStr); + if (ep.state != null) { + addDetailLine(lines, "State", ep.state); + } + if (ep.inType != null) { + addDetailLine(lines, "In type", ep.inType); + } + if (ep.outType != null) { + addDetailLine(lines, "Out type", ep.outType); + } + if (ep.description != null) { + addDetailLine(lines, "Desc", ep.description); + } + + frame.renderWidget( + Paragraph.builder() + .text(Text.from(lines)) + .block(Block.builder().borderType(BorderType.ROUNDED).title(detailTitle).build()) + .build(), + area); + } + + private static void addDetailLine(List<Line> lines, String label, String value) { + if (value == null || value.isEmpty()) { + return; + } + lines.add(Line.from( + Span.styled(String.format(" %-10s ", label + ":"), Style.EMPTY.fg(Color.YELLOW).bold()), + Span.raw(value))); + } + // ---- Tab 7: Inspect (merged Last + Tracer) ---- private void renderInspect(Frame frame, Rect area) { @@ -4019,7 +4294,7 @@ public class CamelMonitor extends CamelCommand { hint(spans, "p", selInfo.routeStarted > 0 ? "stop" : "start"); } } - hint(spans, "1-8", "tabs"); + hint(spans, "1-9", "tabs"); } else if (tab == TAB_ROUTES && showSource) { hint(spans, "c/Esc", "close"); hint(spans, "\u2191\u2193\u2190\u2192", "scroll"); @@ -4059,27 +4334,27 @@ public class CamelMonitor extends CamelCommand { } else if (routeState != null) { hint(spans, "p", "start"); } - hint(spans, "1-8", "tabs"); + hint(spans, "1-9", "tabs"); } else if (tab == TAB_CONSUMERS) { hint(spans, "Esc", "back"); hint(spans, "s", "sort"); - hint(spans, "1-8", "tabs"); + hint(spans, "1-9", "tabs"); } else if (tab == TAB_ENDPOINTS) { hint(spans, "Esc", "back"); hint(spans, "s", "sort"); String[] filterLabels = { "all", "remote", "remote+stub" }; hint(spans, "f", "filter [" + filterLabels[endpointFilter] + "]"); hint(spans, "a", "chart " + (showEndpointChart ? "[all]" : "[off]")); - hint(spans, "1-8", "tabs"); + hint(spans, "1-9", "tabs"); } else if (tab == TAB_CIRCUIT_BREAKER) { hint(spans, "Esc", "back"); hint(spans, "\u2191\u2193", "navigate"); hint(spans, "s", "sort"); - hint(spans, "1-8", "tabs"); + hint(spans, "1-9", "tabs"); } else if (tab == TAB_HEALTH) { hint(spans, "Esc", "back"); hint(spans, "d", "toggle DOWN"); - hint(spans, "1-8", "tabs"); + hint(spans, "1-9", "tabs"); } else if (tab == TAB_LOG && showLogLevelPopup) { hint(spans, "\u2191\u2193", "navigate"); hint(spans, "Enter", "set level"); @@ -4095,6 +4370,13 @@ public class CamelMonitor extends CamelCommand { } hint(spans, "l", "level"); hintLast(spans, "f", "follow" + (logFollowMode ? " [on]" : " [off]")); + } 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]")); } else if (tab == TAB_HISTORY) { boolean tracerActive = !traces.get().isEmpty(); if (tracerActive && traceDetailView) { @@ -4132,7 +4414,7 @@ public class CamelMonitor extends CamelCommand { } else { hint(spans, "Esc", "back"); hint(spans, "\u2191\u2193", "navigate"); - hint(spans, "1-8", "tabs"); + hint(spans, "1-9", "tabs"); } frame.renderWidget(Paragraph.from(Line.from(spans)), area); @@ -5098,9 +5380,78 @@ public class CamelMonitor extends CamelCommand { parseCbSection(root, "fault-tolerance", info); parseCbSection(root, "circuit-breaker", info); + // Parse REST DSL services + JsonObject restsObj = (JsonObject) root.get("rests"); + if (restsObj != null) { + JsonArray restList = (JsonArray) restsObj.get("rests"); + if (restList != null) { + for (Object r : restList) { + JsonObject rj = (JsonObject) r; + HttpEndpointInfo ep = new HttpEndpointInfo(); + ep.fromRest = true; + ep.url = rj.getString("url"); + ep.method = rj.getString("method"); + if (ep.method != null) { + ep.method = ep.method.toUpperCase(Locale.ENGLISH); + } + ep.consumes = rj.getString("consumes"); + ep.produces = rj.getString("produces"); + ep.description = rj.getString("description"); + ep.contractFirst = Boolean.TRUE.equals(rj.get("contractFirst")); + ep.state = rj.getString("state"); + ep.inType = rj.getString("inType"); + ep.outType = rj.getString("outType"); + // derive path from url (strip scheme+host+port) + ep.path = extractPath(ep.url); + info.httpEndpoints.add(ep); + } + } + } + + // Parse Platform-HTTP services + JsonObject phpObj = (JsonObject) root.get("platform-http"); + if (phpObj != null) { + info.httpServer = phpObj.getString("server"); + parseHttpEndpoints(phpObj, "endpoints", false, info); + parseHttpEndpoints(phpObj, "managementEndpoints", true, info); + } + return info; } + private static void parseHttpEndpoints(JsonObject phpObj, String key, boolean management, IntegrationInfo info) { + JsonArray arr = (JsonArray) phpObj.get(key); + if (arr == null) { + return; + } + for (Object e : arr) { + JsonObject ej = (JsonObject) e; + HttpEndpointInfo ep = new HttpEndpointInfo(); + ep.fromRest = false; + ep.management = management; + ep.server = phpObj.getString("server"); + ep.url = ej.getString("url"); + ep.path = ej.getString("path"); + ep.method = ej.getString("verbs"); + ep.consumes = ej.getString("consumes"); + ep.produces = ej.getString("produces"); + info.httpEndpoints.add(ep); + } + } + + private static String extractPath(String url) { + if (url == null) { + return null; + } + // strip scheme://host:port prefix — find third '/' or return url as-is + int idx = url.indexOf("://"); + if (idx < 0) { + return url; + } + int slash = url.indexOf('/', idx + 3); + return slash >= 0 ? url.substring(slash) : "/"; + } + private static void parseCbSection(JsonObject root, String key, IntegrationInfo info) { JsonObject section = (JsonObject) root.get(key); if (section == null) { @@ -5291,6 +5642,8 @@ public class CamelMonitor extends CamelCommand { final List<HealthCheckInfo> healthChecks = new ArrayList<>(); final List<EndpointInfo> endpoints = new ArrayList<>(); final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>(); + final List<HttpEndpointInfo> httpEndpoints = new ArrayList<>(); + String httpServer; // platform-http server binding, e.g. http://0.0.0.0:8080 } static class RouteInfo { @@ -5373,6 +5726,25 @@ public class CamelMonitor extends CamelCommand { double failureRate; // -1 means not available } + // HTTP endpoint from REST DSL or Platform-HTTP + static class HttpEndpointInfo { + String method; // HTTP method(s), e.g. "GET" or "GET,POST" + String path; // URI path, e.g. /api/orders + String url; // full URL including server + String consumes; + String produces; + // REST DSL only + boolean fromRest; + boolean contractFirst; + String description; + String inType; + String outType; + String state; + // Platform-HTTP only + boolean management; + String server; // e.g. http://0.0.0.0:8080 + } + static class TraceEntry { String pid; String uid;
