This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-23862 in repository https://gitbox.apache.org/repos/asf/camel.git
commit ad0718430ff50eef4fe32068df2151ed55cfa449 Author: Claus Ibsen <[email protected]> AuthorDate: Tue Jun 30 19:25:07 2026 +0200 CAMEL-23862: SQL Trace - master/detail panel, borders, and resource file resolution Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/impl/console/SqlTraceDevConsole.java | 17 +++ .../dsl/jbang/core/commands/tui/SqlTraceTab.java | 151 ++++++++++++++++++++- 2 files changed, 164 insertions(+), 4 deletions(-) diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlTraceDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlTraceDevConsole.java index b20ff8fd9929..63809d27570d 100644 --- a/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlTraceDevConsole.java +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlTraceDevConsole.java @@ -16,6 +16,7 @@ */ package org.apache.camel.impl.console; +import java.io.InputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -31,6 +32,7 @@ import org.apache.camel.spi.Configurer; import org.apache.camel.spi.Metadata; import org.apache.camel.spi.annotations.DevConsole; import org.apache.camel.support.EventNotifierSupport; +import org.apache.camel.support.ResourceHelper; import org.apache.camel.support.console.AbstractDevConsole; import org.apache.camel.util.StringHelper; import org.apache.camel.util.json.JsonArray; @@ -204,6 +206,17 @@ public class SqlTraceDevConsole extends AbstractDevConsole { return null; } + private String resolveResource(String uri) { + try (InputStream is = ResourceHelper.resolveResourceAsInputStream(getCamelContext(), uri)) { + if (is != null) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8).strip(); + } + } catch (Exception e) { + // ignore + } + return "resource:" + uri; + } + private static String detectCategory(String query) { if (query != null && !query.isEmpty()) { String upper = query.stripLeading().toUpperCase(Locale.ENGLISH); @@ -252,6 +265,10 @@ public class SqlTraceDevConsole extends AbstractDevConsole { if (query == null) { query = extractQuery(uri); } + // resolve resource: references to actual SQL content + if (query != null && query.startsWith("resource:")) { + query = resolveResource(query.substring("resource:".length())); + } JsonObject jo = new JsonObject(); jo.put("timestamp", event.getTimestamp()); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java index 37fe1c062a63..b68b8acf49af 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java @@ -35,7 +35,9 @@ import dev.tamboui.text.Text; import dev.tamboui.tui.event.KeyEvent; import dev.tamboui.widgets.block.Block; import dev.tamboui.widgets.block.BorderType; +import dev.tamboui.widgets.block.Borders; import dev.tamboui.widgets.paragraph.Paragraph; +import dev.tamboui.widgets.scrollbar.ScrollbarState; import dev.tamboui.widgets.table.Cell; import dev.tamboui.widgets.table.Row; import dev.tamboui.widgets.table.Table; @@ -52,14 +54,26 @@ class SqlTraceTab implements MonitorTab { private final MonitorContext ctx; private final TableState tableState = new TableState(); + private final ScrollbarState detailScrollState = new ScrollbarState(); private String sort = "time"; private int sortIndex; private boolean sortReversed; + private int detailScroll; + private boolean wordWrap = true; + private String selectedKey; SqlTraceTab(MonitorContext ctx) { this.ctx = ctx; } + @Override + public void onTabSelected() { + IntegrationInfo info = ctx.findSelectedIntegration(); + if (info != null && !info.sqlTraceStatements.isEmpty() && tableState.selected() == null) { + tableState.select(0); + } + } + @Override public boolean handleKeyEvent(KeyEvent ke) { if (ke.isChar('s')) { @@ -72,6 +86,26 @@ class SqlTraceTab implements MonitorTab { sortReversed = !sortReversed; return true; } + if (ke.isCharIgnoreCase('w')) { + wordWrap = !wordWrap; + return true; + } + if (ke.isHome()) { + detailScroll = 0; + return true; + } + if (ke.isEnd()) { + detailScroll = Integer.MAX_VALUE; + return true; + } + if (ke.isPageUp()) { + detailScroll = Math.max(0, detailScroll - 5); + return true; + } + if (ke.isPageDown()) { + detailScroll += 5; + return true; + } return false; } @@ -82,10 +116,17 @@ class SqlTraceTab implements MonitorTab { @Override public void navigateUp() { + detailScroll = 0; + tableState.selectPrevious(); } @Override public void navigateDown() { + detailScroll = 0; + IntegrationInfo info = ctx.findSelectedIntegration(); + if (info != null) { + tableState.selectNext(info.sqlTraceStatements.size()); + } } @Override @@ -101,7 +142,7 @@ class SqlTraceTab implements MonitorTab { .split(area); renderKpiStrip(frame, layout.get(0), info); - renderTable(frame, layout.get(1), info); + renderMasterDetail(frame, layout.get(1), info); } private void renderKpiStrip(Frame frame, Rect area, IntegrationInfo info) { @@ -127,16 +168,52 @@ class SqlTraceTab implements MonitorTab { Paragraph kpi = Paragraph.builder() .text(Text.from(Line.from(spans))) - .block(Block.builder().borderType(BorderType.ROUNDED) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) .title(" SQL Trace ").build()) .build(); frame.renderWidget(kpi, area); } - private void renderTable(Frame frame, Rect area, IntegrationInfo info) { + private static String traceKey(SqlTraceInfo si) { + return si.exchangeId + "@" + si.timestamp; + } + + private void renderMasterDetail(Frame frame, Rect area, IntegrationInfo info) { List<SqlTraceInfo> sorted = new ArrayList<>(info.sqlTraceStatements); sorted.sort(this::sortTrace); + // restore selection by identity so cursor follows the same row when new data arrives + if (selectedKey != null) { + for (int i = 0; i < sorted.size(); i++) { + if (selectedKey.equals(traceKey(sorted.get(i)))) { + tableState.select(i); + break; + } + } + } + + SqlTraceInfo selected = null; + Integer sel = tableState.selected(); + if (sel != null && sel >= 0 && sel < sorted.size()) { + selected = sorted.get(sel); + selectedKey = traceKey(selected); + } + boolean showDetail = selected != null; + + List<Rect> chunks = showDetail + ? Layout.vertical() + .constraints(Constraint.length(13), Constraint.length(1), Constraint.fill()) + .split(area) + : List.of(area); + + renderTable(frame, chunks.get(0), sorted); + + if (showDetail) { + renderDetail(frame, chunks.get(2), selected); + } + } + + private void renderTable(Frame frame, Rect area, List<SqlTraceInfo> sorted) { List<Row> rows = new ArrayList<>(); for (SqlTraceInfo si : sorted) { Style durStyle = si.duration >= 100 ? Style.EMPTY.fg(Color.YELLOW) : Style.EMPTY; @@ -191,13 +268,67 @@ class SqlTraceTab implements MonitorTab { Constraint.length(10), Constraint.length(8), Constraint.length(8)) - .block(Block.builder().borderType(BorderType.ROUNDED) + .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) + .highlightSpacing(Table.HighlightSpacing.ALWAYS) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) .title(" Statements sort:" + sort + " ").build()) .build(); frame.renderStatefulWidget(table, area, tableState); } + private void renderDetail(Frame frame, Rect area, SqlTraceInfo si) { + List<Line> lines = new ArrayList<>(); + Style labelStyle = Style.EMPTY.fg(Color.CYAN).bold(); + Style valueStyle = Style.EMPTY; + + lines.add(Line.from( + Span.styled(" Category: ", labelStyle), + Span.styled(si.category != null ? si.category : "", categoryStyle(si.category)))); + lines.add(Line.from( + Span.styled(" Route: ", labelStyle), + Span.styled(si.routeId != null ? si.routeId : "", valueStyle))); + lines.add(Line.from( + Span.styled(" Exchange: ", labelStyle), + Span.styled(si.exchangeId != null ? si.exchangeId : "", valueStyle))); + + String time = ""; + if (si.timestamp > 0) { + time = LocalDateTime.ofInstant(Instant.ofEpochMilli(si.timestamp), ZoneId.systemDefault()) + .format(TIME_FMT); + } + Style durStyle = si.duration >= 100 ? Style.EMPTY.fg(Color.YELLOW) : Style.EMPTY; + Style statusStyle = si.failed ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY.fg(Color.GREEN); + lines.add(Line.from( + Span.styled(" Time: ", labelStyle), + Span.styled(time, valueStyle), + Span.styled(" Duration: ", labelStyle), + Span.styled(si.duration + " ms", durStyle), + Span.styled(" Status: ", labelStyle), + Span.styled(si.failed ? "FAILED" : "OK", statusStyle))); + + String rowsStr = ""; + if (si.rowCount > 0) { + rowsStr = si.rowCount + " rows"; + } else if (si.updateCount > 0) { + rowsStr = si.updateCount + " updated"; + } + if (!rowsStr.isEmpty()) { + lines.add(Line.from( + Span.styled(" Rows: ", labelStyle), + Span.styled(rowsStr, valueStyle))); + } + + lines.add(Line.from(Span.raw(""))); + lines.add(Line.from(Span.styled(" SQL:", labelStyle))); + lines.add(Line.from(Span.styled(" " + (si.query != null ? si.query : ""), valueStyle))); + + int[] scroll = { detailScroll }; + int[] hScroll = { 0 }; + HistoryTab.renderDetailPanel(frame, area, lines, wordWrap, hScroll, scroll, detailScrollState); + detailScroll = scroll[0]; + } + private static Style categoryStyle(String category) { if (category == null) { return Style.EMPTY; @@ -214,7 +345,11 @@ class SqlTraceTab implements MonitorTab { @Override public void renderFooter(List<Span> spans) { hint(spans, "Esc", "back"); + hint(spans, "↑↓", "navigate"); + hint(spans, "PgUp/Dn", "scroll detail"); + hint(spans, "Home/End", "top/end"); hint(spans, "s", "sort"); + hint(spans, "w", "wrap [" + (wordWrap ? "on" : "off") + "]"); } private String sortLabel(String label, String column) { @@ -296,11 +431,19 @@ class SqlTraceTab implements MonitorTab { - **ROWS** — Row count (for SELECT) or update count (for INSERT/UPDATE/DELETE) - **STATUS** — OK (green) or FAIL (red) + ## Detail Panel + + Select a statement with Up/Down to see full details below the table: + SQL text, endpoint URI, route, exchange ID, timing, and row counts. + ## Keys - `Up/Down` — select statement + - `PgUp/PgDn` — scroll detail panel + - `Home/End` — jump to top/end of detail - `s` — cycle sort column - `S` — reverse sort order + - `w` — toggle word wrap """; }
