This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new f57bf9309579 CAMEL-23831: Refactor Beans tab with always-visible
detail panel and smart filtering in camel-tui CAMEL-23831: Add --scope option
to camel cmd bean and / text filter to Beans tab in camel-tui CAMEL-23831:
Auto-refresh Threads tab every 5 seconds in camel-tui CAMEL-23831:
Always-visible stack trace detail panel in Threads tab in camel-tui
CAMEL-23831: Fix BeansTabRenderTest for updated footer hints in camel-tui
f57bf9309579 is described below
commit f57bf93095797e6182cbf71762eedd1e033beb3d
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jul 4 13:37:51 2026 +0200
CAMEL-23831: Refactor Beans tab with always-visible detail panel and smart
filtering in camel-tui
CAMEL-23831: Add --scope option to camel cmd bean and / text filter to
Beans tab in camel-tui
CAMEL-23831: Auto-refresh Threads tab every 5 seconds in camel-tui
CAMEL-23831: Always-visible stack trace detail panel in Threads tab in
camel-tui
CAMEL-23831: Fix BeansTabRenderTest for updated footer hints in camel-tui
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../pages/jbang-commands/camel-jbang-get-bean.adoc | 1 +
.../META-INF/camel-jbang-commands-metadata.json | 2 +-
.../jbang/core/commands/action/CamelBeanDump.java | 65 +++-
.../dsl/jbang/core/commands/tui/BeansTab.java | 331 +++++++++++++--------
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 5 +-
.../dsl/jbang/core/commands/tui/TabRegistry.java | 4 +
.../dsl/jbang/core/commands/tui/ThreadsTab.java | 133 +++------
.../core/commands/tui/BeansTabRenderTest.java | 8 +-
8 files changed, 334 insertions(+), 215 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-bean.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-bean.adoc
index 32f601f4b709..d3574f8838e4 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-bean.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-bean.adoc
@@ -24,6 +24,7 @@ camel get bean [options]
| `--internal` | Include internal Camel beans | false | boolean
| `--nulls` | Include null values | true | boolean
| `--properties` | Show bean properties | true | boolean
+| `--scope` | Filter beans by scope: all, user (excludes
Camel/Spring/Quarkus/JDK), camel, spring, quarkus | all | String
| `--sort` | Sort by name or type | name | String
| `-h,--help` | Display the help and sub-commands | | boolean
|===
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index d30315094616..9a674d88c01b 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -15,7 +15,7 @@
{ "name": "eval", "fullName": "eval", "description": "Evaluate Camel
expressions and scripts", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression",
"fullName": "eval expression", "description": "Evaluates Camel expression",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...]
{ "name": "explain", "fullName": "explain", "description": "Explain what a
Camel route does using AI\/LLM", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names":
"--api-key", "description": "API key for authentication. Also reads
ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType":
"java.lang.String", "type": "string" }, { "names": "--api-type", "description":
"API type: 'ollama', 'openai' (OpenAI-compatible), or 'anthropic' (A [...]
{ "name": "export", "fullName": "export", "description": "Export to other
runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names":
"--build-property", "description": "Maven build properties, ex.
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, {
"names": "--camel-spring-boot-version", "description": "Camel version to use
with Spring Boot", "javaType": "java.lang.String", "ty [...]
- { "name": "get", "fullName": "get", "description": "Get status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "bean",
"fullName": "get [...]
+ { "name": "get", "fullName": "get", "description": "Get status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "bean",
"fullName": "get [...]
{ "name": "harden", "fullName": "harden", "description": "Suggest security
hardening for Camel routes using AI\/LLM", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Harden", "options": [ { "names":
"--api-key", "description": "API key for authentication. Also reads
OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String",
"type": "string" }, { "names": "--api-type", "description": "API type: 'ollama'
or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", [...]
{ "name": "hawtio", "fullName": "hawtio", "description": "Launch Hawtio
web console", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.Hawtio", "options": [ {
"names": "--host", "description": "Hostname to bind the Hawtio web console to",
"defaultValue": "127.0.0.1", "javaType": "java.lang.String", "type": "string"
}, { "names": "--openUrl", "description": "To automatic open Hawtio web console
in the web browser", "defaultValue": "true", "javaType": "boolean", "type":
[...]
{ "name": "infra", "fullName": "infra", "description": "List and Run
external services for testing and prototyping", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.infra.InfraCommand", "options": [ {
"names": "--json", "description": "Output in JSON Format", "javaType":
"boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display
the help and sub-commands", "javaType": "boolean", "type": "boolean" } ],
"subcommands": [ { "name": "get", "fullName": "infra [...]
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelBeanDump.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelBeanDump.java
index 4ffcff7cb5f9..11f68876e3c3 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelBeanDump.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelBeanDump.java
@@ -39,7 +39,8 @@ import picocli.CommandLine.Command;
footer = {
"%nExamples:",
" camel cmd bean",
- " camel cmd bean --filter=myBean" })
+ " camel cmd bean --filter=myBean",
+ " camel cmd bean --scope=user" })
public class CamelBeanDump extends ActionBaseCommand {
public static class NameTypeCompletionCandidates implements
Iterable<String> {
@@ -54,6 +55,18 @@ public class CamelBeanDump extends ActionBaseCommand {
}
+ public static class ScopeCompletionCandidates implements Iterable<String> {
+
+ public ScopeCompletionCandidates() {
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ return List.of("all", "user", "camel", "spring",
"quarkus").iterator();
+ }
+
+ }
+
@CommandLine.Parameters(description = "Name or pid of running Camel
integration", arity = "0..1")
String name = "*";
@@ -73,6 +86,11 @@ public class CamelBeanDump extends ActionBaseCommand {
description = "Include null values", defaultValue =
"true")
boolean nulls;
+ @CommandLine.Option(names = { "--scope" }, completionCandidates =
ScopeCompletionCandidates.class,
+ description = "Filter beans by scope: all, user
(excludes Camel/Spring/Quarkus/JDK), camel, spring, quarkus",
+ defaultValue = "all")
+ String scope;
+
@CommandLine.Option(names = { "--internal" },
description = "Include internal Camel beans",
defaultValue = "false")
boolean internal;
@@ -113,7 +131,7 @@ public class CamelBeanDump extends ActionBaseCommand {
}
root.put("properties", properties);
root.put("nulls", nulls);
- root.put("internal", internal);
+ root.put("internal", !"all".equals(scope) || internal);
Path f = getActionFile(Long.toString(pid));
PathUtils.writeTextSafely(root.toJson(), f);
@@ -157,6 +175,11 @@ public class CamelBeanDump extends ActionBaseCommand {
return 1;
}
+ // filter by scope
+ if (!"all".equals(scope)) {
+ rows.removeIf(r -> !matchesScope(r, scope));
+ }
+
// sort rows
rows.sort(this::sortRow);
if (properties) {
@@ -225,6 +248,44 @@ public class CamelBeanDump extends ActionBaseCommand {
return "null";
}
+ private static boolean matchesScope(Row r, String scope) {
+ return switch (scope) {
+ case "user" -> !isFrameworkBean(r);
+ case "camel" -> isCamelBean(r);
+ case "spring" -> isSpringBean(r);
+ case "quarkus" -> isQuarkusBean(r);
+ default -> true;
+ };
+ }
+
+ private static boolean isFrameworkBean(Row r) {
+ if (isCamelBean(r) || isSpringBean(r) || isQuarkusBean(r)) {
+ return true;
+ }
+ return r.type != null && r.type.startsWith("java.");
+ }
+
+ private static boolean isCamelBean(Row r) {
+ if (r.name != null && (r.name.startsWith("camel-") ||
r.name.startsWith("org.apache.camel"))) {
+ return true;
+ }
+ return r.type != null && r.type.startsWith("org.apache.camel");
+ }
+
+ private static boolean isSpringBean(Row r) {
+ if (r.name != null && r.name.startsWith("org.springframework")) {
+ return true;
+ }
+ return r.type != null && r.type.startsWith("org.springframework");
+ }
+
+ private static boolean isQuarkusBean(Row r) {
+ if (r.name != null && r.name.startsWith("io.quarkus")) {
+ return true;
+ }
+ return r.type != null && r.type.startsWith("io.quarkus");
+ }
+
protected int sortRow(Row o1, Row o2) {
String s = sort;
int negate = 1;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
index 072850109e1a..35a7e3de6824 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
@@ -37,6 +37,7 @@ import dev.tamboui.tui.event.MouseEvent;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.input.TextInputState;
import dev.tamboui.widgets.paragraph.Paragraph;
import dev.tamboui.widgets.table.Cell;
import dev.tamboui.widgets.table.Row;
@@ -51,9 +52,11 @@ class BeansTab extends AbstractTableTab {
private final AtomicBoolean loading = new AtomicBoolean(false);
- private boolean showInternal;
+ private int filterIndex;
+ private boolean filterInputActive;
+ private TextInputState filterInputState = new TextInputState("");
+ private String filterTerm;
private List<BeanData> allBeans = Collections.emptyList();
- private boolean showDetail;
private int detailScroll;
private String lastPid;
@@ -81,66 +84,67 @@ class BeansTab extends AbstractTableTab {
@Override
public void onIntegrationChanged() {
allBeans = Collections.emptyList();
- showDetail = false;
detailScroll = 0;
lastPid = null;
}
@Override
public boolean handleKeyEvent(KeyEvent ke) {
- if (showDetail) {
- if (ke.isUp()) {
- detailScroll = Math.max(0, detailScroll - 1);
- return true;
- }
- if (ke.isDown()) {
- detailScroll++;
- return true;
- }
- if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
- detailScroll = Math.max(0, detailScroll - 20);
- return true;
- }
- if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
- detailScroll += 20;
- return true;
- }
- return false;
+ if (filterInputActive) {
+ return handleFilterInput(ke);
+ }
+ if (ke.isChar('/')) {
+ filterInputActive = true;
+ filterInputState = new TextInputState(filterTerm != null ?
filterTerm : "");
+ return true;
}
return super.handleKeyEvent(ke);
}
- @Override
- protected boolean handleTabKeyEvent(KeyEvent ke) {
- if (ke.isConfirm()) {
- showDetail = !showDetail;
- detailScroll = 0;
- return true;
- }
- if (ke.isCharIgnoreCase('i')) {
- showInternal = !showInternal;
+ private boolean handleFilterInput(KeyEvent ke) {
+ if (ke.isKey(KeyCode.ESCAPE)) {
+ filterInputActive = false;
return true;
}
- if (ke.isCharIgnoreCase('r')) {
- loadBeans();
+ if (ke.isConfirm()) {
+ String text = filterInputState.text().trim();
+ filterTerm = text.isEmpty() ? null : text;
+ filterInputActive = false;
+ tableState.select(0);
+ detailScroll = 0;
return true;
}
- return false;
+ FormHelper.handleTextInput(ke, filterInputState);
+ return true;
+ }
+
+ boolean isFilterInputActive() {
+ return filterInputActive;
}
@Override
- public boolean handleMouseEvent(MouseEvent me, Rect area) {
- if (!showDetail) {
- List<BeanData> visible = sortedBeans();
- return handleTableClick(me, lastTableArea, tableState,
visible.size());
+ public boolean handleEscape() {
+ if (filterTerm != null) {
+ filterTerm = null;
+ tableState.select(0);
+ detailScroll = 0;
+ return true;
}
return false;
}
@Override
- public boolean handleEscape() {
- if (showDetail) {
- showDetail = false;
+ protected boolean handleTabKeyEvent(KeyEvent ke) {
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+ detailScroll = Math.max(0, detailScroll - 10);
+ return true;
+ }
+ if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+ detailScroll += 10;
+ return true;
+ }
+ if (ke.isCharIgnoreCase('f')) {
+ filterIndex = (filterIndex + 1) % filterModes().length;
return true;
}
return false;
@@ -148,17 +152,24 @@ class BeansTab extends AbstractTableTab {
@Override
public void navigateUp() {
- if (!showDetail) {
- tableState.selectPrevious();
- }
+ tableState.selectPrevious();
+ detailScroll = 0;
}
@Override
public void navigateDown() {
- if (!showDetail) {
- List<BeanData> visible = sortedBeans();
- tableState.selectNext(visible.size());
+ List<BeanData> visible = sortedBeans();
+ tableState.selectNext(visible.size());
+ detailScroll = 0;
+ }
+
+ @Override
+ public boolean handleMouseEvent(MouseEvent me, Rect area) {
+ boolean handled = super.handleMouseEvent(me, area);
+ if (handled) {
+ detailScroll = 0;
}
+ return handled;
}
@Override
@@ -175,15 +186,11 @@ class BeansTab extends AbstractTableTab {
List<BeanData> visible = sortedBeans();
- if (showDetail) {
- List<Rect> chunks = Layout.vertical()
- .constraints(Constraint.percentage(40), Constraint.fill())
- .split(area);
- renderTable(frame, chunks.get(0), visible);
- renderDetail(frame, chunks.get(1), visible);
- } else {
- renderTable(frame, area, visible);
- }
+ List<Rect> chunks = Layout.vertical()
+ .constraints(Constraint.fill(), Constraint.percentage(30))
+ .split(area);
+ renderTable(frame, chunks.get(0), visible);
+ renderDetail(frame, chunks.get(1), visible);
}
private void renderTable(Frame frame, Rect area, List<BeanData> visible) {
@@ -195,28 +202,25 @@ class BeansTab extends AbstractTableTab {
rows.add(Row.from(
Cell.from(Span.styled(b.name != null ? b.name : "",
Style.EMPTY.fg(Color.CYAN))),
- Cell.from(Span.styled(shortType, Style.EMPTY)),
- Cell.from(Span.styled(type, Style.EMPTY.dim()))));
+ Cell.from(Span.styled(shortType, Style.EMPTY))));
}
if (rows.isEmpty()) {
- rows.add(emptyRow("No beans", 3));
+ rows.add(emptyRow("No beans", 2));
}
- String title = String.format(" Beans [%d] sort:%s ", visible.size(),
sort);
- if (showInternal) {
- title = String.format(" Beans [%d] sort:%s internal:on ",
visible.size(), sort);
- }
+ String mode = filterModes()[filterIndex];
+ String title = filterTerm != null
+ ? String.format(" Beans [%d] sort:%s scope:%s filter:\"%s\" ",
visible.size(), sort, mode, filterTerm)
+ : String.format(" Beans [%d] sort:%s scope:%s ",
visible.size(), sort, mode);
Table table = Table.builder()
.rows(rows)
.header(Row.from(
Cell.from(Span.styled(sortLabel("NAME", "name"),
sortStyle("name"))),
- Cell.from(Span.styled(sortLabel("TYPE", "type"),
sortStyle("type"))),
- Cell.from(Span.styled("PACKAGE", Style.EMPTY.bold()))))
+ Cell.from(Span.styled(sortLabel("TYPE", "type"),
sortStyle("type")))))
.widths(
- Constraint.length(30),
- Constraint.length(30),
+ Constraint.percentage(55),
Constraint.fill())
.highlightStyle(Theme.selectionBg())
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
@@ -234,52 +238,67 @@ class BeansTab extends AbstractTableTab {
frame.renderWidget(
Paragraph.builder()
.text(Text.from(Line.from(Span.styled(" Select a
bean", Style.EMPTY.dim()))))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title("
Properties ")
- .build())
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Bean Detail ").build())
.build(),
area);
return;
}
BeanData bean = visible.get(sel);
- String title = " " + bean.name + " (" + (bean.type != null ? bean.type
: "") + ") ";
+ String title = " " + (bean.name != null ? bean.name : "") + " ";
- if (bean.properties == null || bean.properties.isEmpty()) {
- frame.renderWidget(
- Paragraph.builder()
- .text(Text.from(Line.from(Span.styled(" No
properties", Style.EMPTY.dim()))))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
- .build(),
- area);
- return;
+ List<Line> lines = new ArrayList<>();
+
+ // full type
+ if (bean.type != null && !bean.type.isEmpty()) {
+ lines.add(Line.from(
+ Span.styled(" Type: ", Style.EMPTY.dim()),
+ Span.styled(bean.type, Style.EMPTY.fg(Color.WHITE))));
+ }
+
+ // properties
+ if (bean.properties != null && !bean.properties.isEmpty()) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(Span.styled(" Properties:",
Style.EMPTY.dim())));
+
+ int maxNameLen = 0;
+ for (BeanData.Property prop : bean.properties) {
+ if (prop.name != null) {
+ maxNameLen = Math.max(maxNameLen, prop.name.length());
+ }
+ }
+ int nameWidth = Math.max(15, maxNameLen + 1);
+
+ for (BeanData.Property prop : bean.properties) {
+ String propType = prop.type != null ? prop.type : "";
+ int dot = propType.lastIndexOf('.');
+ String shortPropType = dot >= 0 ? propType.substring(dot + 1)
: propType;
+ String value = prop.value != null ? prop.value : "null";
+
+ lines.add(Line.from(
+ Span.styled(" " + String.format("%-" + nameWidth +
"s", prop.name), Style.EMPTY.fg(Color.CYAN)),
+ Span.styled(String.format("%-15s", shortPropType),
Style.EMPTY.dim()),
+ Span.styled(" = ", Style.EMPTY.dim()),
+ Span.styled(value, "null".equals(value) ?
Style.EMPTY.dim() : Style.EMPTY.fg(Color.WHITE))));
+ }
+ } else {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(Span.styled(" No properties",
Style.EMPTY.dim())));
}
int visibleLines = area.height() - 2;
if (visibleLines < 1) {
visibleLines = 1;
}
- int maxScroll = Math.max(0, bean.properties.size() - visibleLines);
+ int maxScroll = Math.max(0, lines.size() - visibleLines);
detailScroll = Math.min(detailScroll, maxScroll);
-
- int end = Math.min(detailScroll + visibleLines,
bean.properties.size());
- List<Line> lines = new ArrayList<>();
- for (int i = detailScroll; i < end; i++) {
- BeanData.Property prop = bean.properties.get(i);
- String propType = prop.type != null ? prop.type : "";
- int dot = propType.lastIndexOf('.');
- String shortPropType = dot >= 0 ? propType.substring(dot + 1) :
propType;
- String value = prop.value != null ? prop.value : "null";
-
- lines.add(Line.from(
- Span.styled(" " + String.format("%-25s", prop.name),
Style.EMPTY.fg(Color.CYAN)),
- Span.styled(String.format("%-15s", shortPropType),
Style.EMPTY.dim()),
- Span.styled(" = ", Style.EMPTY.dim()),
- Span.styled(value, "null".equals(value) ?
Style.EMPTY.dim() : Style.EMPTY.fg(Color.WHITE))));
- }
+ int end = Math.min(detailScroll + visibleLines, lines.size());
+ List<Line> visibleContent = lines.subList(detailScroll, end);
frame.renderWidget(
Paragraph.builder()
- .text(Text.from(lines))
+ .text(Text.from(visibleContent))
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
.build(),
area);
@@ -287,14 +306,23 @@ class BeansTab extends AbstractTableTab {
@Override
public void renderFooter(List<Span> spans) {
- super.renderFooter(spans);
- hint(spans, "i", "internal" + (showInternal ? " [on]" : ""));
- hint(spans, "r", "refresh");
- if (showDetail) {
- hintLast(spans, "↑↓", "scroll");
+ if (filterInputActive) {
+ spans.add(Span.styled(" /", Style.EMPTY.fg(Color.YELLOW).bold()));
+ spans.add(Span.raw(filterInputState.text() + "█ "));
+ hint(spans, "Enter", "filter");
+ hintLast(spans, "Esc", "cancel");
+ return;
+ }
+ hint(spans, "Esc", filterTerm != null ? "clear" : "back");
+ hint(spans, "f", "scope [" + filterModes()[filterIndex] + "]");
+ if (filterTerm != null) {
+ spans.add(Span.styled(" /", Style.EMPTY.fg(Color.YELLOW).bold()));
+ spans.add(Span.raw("\"" + filterTerm + "\" "));
} else {
- hintLast(spans, "Enter", "detail");
+ hint(spans, "/", "filter");
}
+ hint(spans, "↑↓", "navigate");
+ hintLast(spans, "PgUp/Dn", "scroll");
}
@Override
@@ -308,10 +336,39 @@ class BeansTab extends AbstractTableTab {
return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Beans");
}
+ private String[] filterModes() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ String platform = info != null ? info.platform : null;
+ if ("Spring Boot".equals(platform)) {
+ return new String[] { "all", "user", "camel", "spring" };
+ } else if ("Quarkus".equals(platform)) {
+ return new String[] { "all", "user", "camel", "quarkus" };
+ }
+ return new String[] { "all", "user", "camel" };
+ }
+
private List<BeanData> sortedBeans() {
+ String[] modes = filterModes();
+ if (filterIndex >= modes.length) {
+ filterIndex = 0;
+ }
+ String mode = modes[filterIndex];
+ String ft = filterTerm != null ? filterTerm.toLowerCase() : null;
List<BeanData> result = new ArrayList<>();
for (BeanData b : allBeans) {
- if (!showInternal && b.internal) {
+ if ("user".equals(mode) && isFrameworkBean(b)) {
+ continue;
+ }
+ if ("camel".equals(mode) && !b.internal) {
+ continue;
+ }
+ if ("spring".equals(mode) && !isSpringBean(b)) {
+ continue;
+ }
+ if ("quarkus".equals(mode) && !isQuarkusBean(b)) {
+ continue;
+ }
+ if (ft != null && !matchesFilter(b, ft)) {
continue;
}
result.add(b);
@@ -400,6 +457,42 @@ class BeansTab extends AbstractTableTab {
}
}
+ private static boolean matchesFilter(BeanData b, String filter) {
+ if (b.name != null && b.name.toLowerCase().contains(filter)) {
+ return true;
+ }
+ return b.type != null && b.type.toLowerCase().contains(filter);
+ }
+
+ private static boolean isFrameworkBean(BeanData b) {
+ if (isInternalBean(b.name, b.type)) {
+ return true;
+ }
+ if (b.name != null && (b.name.startsWith("org.springframework") ||
b.name.startsWith("io.quarkus"))) {
+ return true;
+ }
+ if (b.type != null
+ && (b.type.startsWith("org.springframework") ||
b.type.startsWith("io.quarkus")
+ || b.type.startsWith("java."))) {
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean isSpringBean(BeanData b) {
+ if (b.name != null && b.name.startsWith("org.springframework")) {
+ return true;
+ }
+ return b.type != null && b.type.startsWith("org.springframework");
+ }
+
+ private static boolean isQuarkusBean(BeanData b) {
+ if (b.name != null && b.name.startsWith("io.quarkus")) {
+ return true;
+ }
+ return b.type != null && b.type.startsWith("io.quarkus");
+ }
+
private static boolean isInternalBean(String name, String type) {
if (name == null) {
return false;
@@ -451,33 +544,33 @@ class BeansTab extends AbstractTableTab {
## Table Columns
- **NAME** — Bean name used to look it up from routes. In a
route you reference this with `.bean("myService")` or `${bean:myService}`
- - **TYPE** — Java class of the bean (e.g.,
`com.example.MyService`, `java.util.HashMap`)
-
- ## Example Screen
-
- ```
- NAME TYPE
- greeter com.example.Greeter
- myDataSource com.zaxxer.hikari.HikariDataSource
- restConfiguration org.apache.camel.spi.RestConfiguration
- json-jackson
org.apache.camel.component.jackson.JacksonDataFormat
- ```
+ - **TYPE** — Short Java class name of the bean (e.g.,
`Greeter`, `HikariDataSource`)
## Detail View
- Press `Enter` on a bean to inspect its properties and current
values.
- Camel uses Java bean introspection to read property values,
showing
- you the runtime state of each bean. This is useful for
verifying
- configuration — for example, checking that a database
connection pool
- has the correct URL, or that a data format is configured with
the
+ The detail panel at the bottom shows the full type (including
package)
+ and the bean's properties with their current values. This is
useful for
+ verifying configuration — for example, checking that a
database connection
+ pool has the correct URL, or that a data format is configured
with the
right options.
+ ## Filter Modes
+
+ Press `f` to cycle through filter modes:
+
+ - **all** — show all beans (including framework internals)
+ - **user** — only your application beans (excludes Camel,
Spring, Quarkus, and JDK beans)
+ - **camel** — only Camel internal beans
+ - **spring** — only Spring Framework beans (Spring Boot apps
only)
+ - **quarkus** — only Quarkus beans (Quarkus apps only)
+
## Keys
- `Up/Down` — select bean
- - `Enter` — view bean properties
+ - `PgUp/PgDn` — scroll detail panel
- `s` — cycle sort column
- `S` — reverse sort order
+ - `f` — cycle filter
- `Esc` — back
""";
}
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 32a1d23a9dac..cd21ef80a14d 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
@@ -566,10 +566,13 @@ public class CamelMonitor extends CamelCommand {
boolean spanFilterActive = tabRegistry.selectedTabIndex() == TAB_MORE
&& tabRegistry.getActiveMoreTab() == tabRegistry.spansTab()
&& tabRegistry.spansTab().isFilterInputActive();
+ boolean beanFilterActive = tabRegistry.selectedTabIndex() == TAB_MORE
+ && tabRegistry.getActiveMoreTab() == tabRegistry.beansTab()
+ && tabRegistry.beansTab().isFilterInputActive();
boolean sqlInputActive = tabRegistry.selectedTabIndex() == TAB_MORE
&& tabRegistry.getActiveMoreTab() == tabRegistry.sqlQueryTab()
&& tabRegistry.sqlQueryTab().isInputActive();
- boolean textEditing = probeEditing || logSearchActive ||
spanFilterActive || sqlInputActive;
+ boolean textEditing = probeEditing || logSearchActive ||
spanFilterActive || beanFilterActive || sqlInputActive;
if (!textEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) {
runner.quit();
return true;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index d8fd97204900..db4d61526229 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -305,6 +305,10 @@ class TabRegistry {
return errorsTab;
}
+ BeansTab beansTab() {
+ return beansTab;
+ }
+
SpansTab spansTab() {
return spansTab;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
index 04aad8b40ad8..5d48490bde09 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
@@ -51,15 +51,17 @@ class ThreadsTab extends AbstractTableTab {
private static final String[] FILTER_LABELS = { "camel", "all" };
+ private static final long AUTO_REFRESH_INTERVAL_MS = 5000;
+
private final AtomicBoolean loading = new AtomicBoolean(false);
private int filter; // 0=camel, 1=all
private List<ThreadData> allThreads = Collections.emptyList();
private int threadCount;
private int peakThreadCount;
- private boolean showTrace;
private int traceScroll;
private String lastPid;
+ private long lastRefreshTime;
ThreadsTab(MonitorContext ctx) {
super(ctx, "id", "name", "state");
@@ -85,48 +87,27 @@ class ThreadsTab extends AbstractTableTab {
@Override
public void onIntegrationChanged() {
allThreads = Collections.emptyList();
- showTrace = false;
traceScroll = 0;
lastPid = null;
}
@Override
public boolean handleKeyEvent(KeyEvent ke) {
- if (showTrace) {
- if (ke.isUp()) {
- traceScroll = Math.max(0, traceScroll - 1);
- return true;
- }
- if (ke.isDown()) {
- traceScroll++;
- return true;
- }
- if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
- traceScroll = Math.max(0, traceScroll - 20);
- return true;
- }
- if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
- traceScroll += 20;
- return true;
- }
- return false;
- }
return super.handleKeyEvent(ke);
}
@Override
protected boolean handleTabKeyEvent(KeyEvent ke) {
- if (ke.isConfirm()) {
- showTrace = !showTrace;
- traceScroll = 0;
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+ traceScroll = Math.max(0, traceScroll - 10);
return true;
}
- if (ke.isCharIgnoreCase('f')) {
- filter = (filter + 1) % FILTER_LABELS.length;
+ if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+ traceScroll += 10;
return true;
}
- if (ke.isCharIgnoreCase('r')) {
- loadThreads();
+ if (ke.isCharIgnoreCase('f')) {
+ filter = (filter + 1) % FILTER_LABELS.length;
return true;
}
return false;
@@ -134,37 +115,32 @@ class ThreadsTab extends AbstractTableTab {
@Override
public boolean handleMouseEvent(MouseEvent me, Rect area) {
- if (!showTrace) {
- return super.handleMouseEvent(me, area);
- }
- return false;
- }
-
- @Override
- public boolean handleEscape() {
- if (showTrace) {
- showTrace = false;
- return true;
+ boolean handled = super.handleMouseEvent(me, area);
+ if (handled) {
+ traceScroll = 0;
}
- return false;
+ return handled;
}
@Override
public void navigateUp() {
- if (!showTrace) {
- super.navigateUp();
- }
+ super.navigateUp();
+ traceScroll = 0;
}
@Override
public void navigateDown() {
- if (!showTrace) {
- super.navigateDown();
- }
+ super.navigateDown();
+ traceScroll = 0;
}
@Override
protected void renderContent(Frame frame, Rect area, IntegrationInfo info)
{
+ long now = System.currentTimeMillis();
+ if (!allThreads.isEmpty() && now - lastRefreshTime >=
AUTO_REFRESH_INTERVAL_MS) {
+ loadThreads();
+ }
+
if (loading.get() && allThreads.isEmpty()) {
frame.renderWidget(
Paragraph.builder()
@@ -178,33 +154,11 @@ class ThreadsTab extends AbstractTableTab {
List<ThreadData> visible = sortedThreads();
- if (showTrace) {
- List<Rect> chunks = Layout.vertical()
- .constraints(Constraint.length(1),
Constraint.percentage(40), Constraint.fill())
- .split(area);
- renderSummary(frame, chunks.get(0), visible);
- renderTable(frame, chunks.get(1), visible);
- renderTrace(frame, chunks.get(2), visible);
- } else {
- List<Rect> chunks = Layout.vertical()
- .constraints(Constraint.length(1), Constraint.fill())
- .split(area);
- renderSummary(frame, chunks.get(0), visible);
- renderTable(frame, chunks.get(1), visible);
- }
- }
-
- private void renderSummary(Frame frame, Rect area, List<ThreadData>
visible) {
- List<Span> spans = new ArrayList<>();
- spans.add(Span.styled(" Threads: ",
Style.EMPTY.fg(Color.YELLOW).bold()));
- spans.add(Span.styled(String.valueOf(threadCount),
Style.EMPTY.fg(Color.WHITE)));
- spans.add(Span.raw(" "));
- spans.add(Span.styled("Peak: ", Style.EMPTY.fg(Color.YELLOW).bold()));
- spans.add(Span.styled(String.valueOf(peakThreadCount),
Style.EMPTY.fg(Color.WHITE)));
- spans.add(Span.raw(" "));
- spans.add(Span.styled("Showing: ",
Style.EMPTY.fg(Color.YELLOW).bold()));
- spans.add(Span.styled(visible.size() + "/" + allThreads.size(),
Style.EMPTY.fg(Color.WHITE)));
- frame.renderWidget(Paragraph.from(Line.from(spans)), area);
+ List<Rect> chunks = Layout.vertical()
+ .constraints(Constraint.fill(), Constraint.percentage(40))
+ .split(area);
+ renderTable(frame, chunks.get(0), visible);
+ renderTrace(frame, chunks.get(1), visible);
}
private void renderTable(Frame frame, Rect area, List<ThreadData> visible)
{
@@ -230,7 +184,8 @@ class ThreadsTab extends AbstractTableTab {
rows.add(emptyRow("No threads", 5));
}
- String title = String.format(" Threads [%d] sort:%s filter:%s ",
visible.size(), sort, FILTER_LABELS[filter]);
+ String title = String.format(" Threads [%d/%d] peak:%d sort:%s
filter:%s ",
+ visible.size(), threadCount, peakThreadCount, sort,
FILTER_LABELS[filter]);
Table table = Table.builder()
.rows(rows)
@@ -314,12 +269,8 @@ class ThreadsTab extends AbstractTableTab {
hint(spans, "Esc", "back");
hint(spans, "s", "sort");
hint(spans, "f", "filter [" + FILTER_LABELS[filter] + "]");
- hint(spans, "r", "refresh");
- if (showTrace) {
- hintLast(spans, "↑↓", "scroll");
- } else {
- hintLast(spans, "Enter", "trace");
- }
+ hint(spans, "↑↓", "navigate");
+ hintLast(spans, "PgUp/Dn", "scroll");
}
@Override
@@ -450,6 +401,7 @@ class ThreadsTab extends AbstractTableTab {
threadCount = tc;
peakThreadCount = peak;
lastPid = pid;
+ lastRefreshTime = System.currentTimeMillis();
});
}
}
@@ -519,26 +471,31 @@ class ThreadsTab extends AbstractTableTab {
## Stack Trace View
- Press `Enter` to see the full stack trace of the selected
thread.
- The stack trace shows exactly what code the thread is
executing (or
- waiting in). Lines containing `org.apache.camel` are
highlighted in
- yellow to help you find Camel-related frames.
+ The detail panel at the bottom always shows the stack trace of
the
+ currently selected thread. As you navigate threads with
`Up/Down`,
+ the stack trace updates automatically. Lines containing
+ `org.apache.camel` are highlighted in yellow to help you find
+ Camel-related frames.
- Use `Up/Down` or `PgUp/PgDn` to scroll through long stack
traces.
+ Use `PgUp/PgDn` to scroll through long stack traces.
## Filter Modes
- **camel** (default) — Show only Camel-related threads (names
containing `camel`, `vertx`, or `netty`)
- **all** — Show all JVM threads including system threads, GC
threads, JIT compiler threads
+ ## Auto-Refresh
+
+ Thread data is automatically refreshed every 5 seconds so you
can
+ watch BLOCKED/WAITED counters change over time without manual
action.
+
## Keys
- - `Up/Down` — select thread
- - `Enter` — view/hide stack trace
+ - `Up/Down` — select thread (detail updates automatically)
+ - `PgUp/PgDn` — scroll stack trace detail
- `s` — cycle sort column
- `S` — reverse sort order
- `f` — toggle filter (camel / all)
- - `r` — refresh thread data
- `Esc` — back
""";
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTabRenderTest.java
index 3d5d2bcec75b..428a4ca38c5c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTabRenderTest.java
@@ -85,10 +85,10 @@ class BeansTabRenderTest {
.reduce("", String::concat);
assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
- assertTrue(footer.contains("sort"), "Footer should contain sort hint");
- assertTrue(footer.contains("internal"), "Footer should contain
internal toggle hint");
- assertTrue(footer.contains("refresh"), "Footer should contain refresh
hint");
- assertTrue(footer.contains("detail"), "Footer should contain detail
hint");
+ assertTrue(footer.contains("scope"), "Footer should contain scope
hint");
+ assertTrue(footer.contains("filter"), "Footer should contain filter
hint");
+ assertTrue(footer.contains("navigate"), "Footer should contain
navigate hint");
+ assertTrue(footer.contains("scroll"), "Footer should contain scroll
hint");
}
}