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 24360ad8a1c6 CAMEL-23569: camel-tui: Add support for camel-test-infra
services (#23379)
24360ad8a1c6 is described below
commit 24360ad8a1c65fb00ea7e15088aba4b778533824
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed May 20 18:08:55 2026 +0200
CAMEL-23569: camel-tui: Add support for camel-test-infra services (#23379)
* CAMEL-23569: camel-tui: Add support for camel-test-infra services
Add infrastructure service management to the TUI monitor including:
- Discover running infra services by scanning ~/.camel/infra-*.json files
- Show infra services in a separate table below integrations in the overview
- Display connection details (host, port, credentials) in the info panel
- Support infra service logs in the Log tab
- Add stop/kill (x/X keys) for both infra services and integrations
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23569: Fix infra layout and log tab for infra services
Collapse integrations table when empty so infra table fills the space,
show info panel for selected infra without sparkline, and allow Log tab
to display container logs for infra services.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23569: Toggle between integrations and infra views with 'i' key
Show one table at a time on the overview instead of stacking both.
Press 'i' to swap views. Auto-focus infra when no integrations exist.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23569: Polish infra UI - tab bar, hints, and selected color
Show only Overview and Log tabs when infra is selected. Use magenta
for infra selection in header. Hide Enter hint and log level shortcut
for infra. Fix infra count to show "infra(s)".
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23569: Add kill confirm dialog and clean up orphaned files
Show confirm dialog before force-killing a process. Clean up all
orphaned files (.log, -status.json, -action.json, -output.json,
-trace.json, -history.json, -debug.json, -receive.json) after
force kill for both integrations and infra services.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 674 ++++++++++++++++++---
1 file changed, 577 insertions(+), 97 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 805ee1327737..9676520457ae 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
@@ -36,6 +36,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@@ -163,8 +164,12 @@ public class CamelMonitor extends CamelCommand {
// State
private final AtomicReference<List<IntegrationInfo>> data = new
AtomicReference<>(Collections.emptyList());
+ private final AtomicReference<List<InfraInfo>> infraData = new
AtomicReference<>(Collections.emptyList());
private final Map<String, VanishingInfo> vanishing = new
ConcurrentHashMap<>();
+ private final Map<String, VanishingInfraInfo> vanishingInfra = new
ConcurrentHashMap<>();
private final TableState overviewTableState = new TableState();
+ private final TableState infraTableState = new TableState();
+ private boolean infraTableFocused;
private final TableState routeTableState = new TableState();
private final TableState consumerTableState = new TableState();
private final TableState healthTableState = new TableState();
@@ -356,6 +361,7 @@ public class CamelMonitor extends CamelCommand {
private static final String[] LOG_LEVELS = { "ERROR", "WARN", "INFO",
"DEBUG", "TRACE" };
private boolean showLogLevelPopup;
private final ListState logLevelListState = new ListState();
+ private boolean showKillConfirm;
private final AtomicBoolean refreshInProgress = new AtomicBoolean(false);
private final AtomicBoolean diagramLoading = new AtomicBoolean(false);
@@ -409,6 +415,17 @@ public class CamelMonitor extends CamelCommand {
private boolean handleEvent(Event event, TuiRunner runner) {
if (event instanceof KeyEvent ke) {
+ // Kill confirm dialog: Enter to confirm, Esc/any other key to
cancel
+ if (showKillConfirm) {
+ if (ke.isConfirm()) {
+ showKillConfirm = false;
+ stopSelectedProcess(true);
+ } else {
+ showKillConfirm = false;
+ }
+ return true;
+ }
+
// Escape: navigate back
if (ke.isCancel()) {
if (showLogLevelPopup) {
@@ -439,6 +456,11 @@ public class CamelMonitor extends CamelCommand {
tabsState.select(TAB_OVERVIEW);
return true;
}
+ if (infraTableFocused) {
+ infraTableFocused = false;
+ syncSelectedPidFromOverview();
+ return true;
+ }
if (selectedPid != null) {
selectedPid = null;
return true;
@@ -451,49 +473,63 @@ public class CamelMonitor extends CamelCommand {
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.isChar('3')) {
- return handleTabKey(TAB_ROUTES);
- }
- if (ke.isChar('4')) {
- return handleTabKey(TAB_CONSUMERS);
- }
- if (ke.isChar('5')) {
- return handleTabKey(TAB_ENDPOINTS);
- }
- if (ke.isChar('6')) {
- return handleTabKey(TAB_HTTP);
- }
- if (ke.isChar('7')) {
- return handleTabKey(TAB_HEALTH);
- }
- if (ke.isChar('8')) {
- return handleTabKey(TAB_HISTORY);
- }
- if (ke.isChar('9')) {
- return handleTabKey(TAB_CIRCUIT_BREAKER);
+ if (!isInfraSelected()) {
+ if (ke.isChar('3')) {
+ return handleTabKey(TAB_ROUTES);
+ }
+ if (ke.isChar('4')) {
+ return handleTabKey(TAB_CONSUMERS);
+ }
+ if (ke.isChar('5')) {
+ return handleTabKey(TAB_ENDPOINTS);
+ }
+ if (ke.isChar('6')) {
+ return handleTabKey(TAB_HTTP);
+ }
+ if (ke.isChar('7')) {
+ return handleTabKey(TAB_HEALTH);
+ }
+ if (ke.isChar('8')) {
+ return handleTabKey(TAB_HISTORY);
+ }
+ if (ke.isChar('9')) {
+ return handleTabKey(TAB_CIRCUIT_BREAKER);
+ }
}
// Tab cycling (check Shift+Tab before Tab since Tab binding also
matches Shift+Tab)
if (ke.isFocusPrevious()) {
- int prev = (tabsState.selected() - 1 + NUM_TABS) % NUM_TABS;
- if (prev != TAB_OVERVIEW) {
- selectCurrentIntegration();
+ if (isInfraSelected()) {
+ // Cycle between Overview and Log only
+ int prev = tabsState.selected() == TAB_OVERVIEW ? TAB_LOG
: TAB_OVERVIEW;
+ tabsState.select(prev);
+ } else {
+ int prev = (tabsState.selected() - 1 + NUM_TABS) %
NUM_TABS;
+ if (prev != TAB_OVERVIEW) {
+ selectCurrentIntegration();
+ }
+ tabsState.select(prev);
}
- tabsState.select(prev);
return true;
}
if (ke.isFocusNext()) {
- int next = (tabsState.selected() + 1) % NUM_TABS;
- if (next != TAB_OVERVIEW) {
- selectCurrentIntegration();
+ if (isInfraSelected()) {
+ int next = tabsState.selected() == TAB_OVERVIEW ? TAB_LOG
: TAB_OVERVIEW;
+ tabsState.select(next);
+ } else {
+ int next = (tabsState.selected() + 1) % NUM_TABS;
+ if (next != TAB_OVERVIEW) {
+ selectCurrentIntegration();
+ }
+ tabsState.select(next);
}
- tabsState.select(next);
return true;
}
@@ -625,8 +661,25 @@ public class CamelMonitor extends CamelCommand {
chartMode = (chartMode + 1) % 3;
return true;
}
- // Overview tab: start/stop all routes for selected integration
- if (tab == TAB_OVERVIEW && ke.isChar('p') && selectedPid != null) {
+ // Overview tab: toggle focus between integrations and infra tables
+ if (tab == TAB_OVERVIEW && ke.isChar('i') &&
!infraData.get().isEmpty()) {
+ infraTableFocused = !infraTableFocused;
+ if (infraTableFocused) {
+ if (infraTableState.selected() == null) {
+ infraTableState.select(0);
+ }
+ syncSelectedPidFromInfra();
+ } else {
+ List<IntegrationInfo> intInfos = sortedOverviewInfos();
+ if (!intInfos.isEmpty() && overviewTableState.selected()
== null) {
+ overviewTableState.select(0);
+ }
+ syncSelectedPidFromOverview();
+ }
+ return true;
+ }
+ // Overview tab: start/stop all routes for selected integration
(not infra)
+ if (tab == TAB_OVERVIEW && ke.isChar('p') && selectedPid != null
&& !infraTableFocused) {
IntegrationInfo selInfo = findSelectedIntegration();
if (selInfo != null) {
String cmd = selInfo.routeStarted > 0 ? "stop" : "start";
@@ -634,6 +687,16 @@ public class CamelMonitor extends CamelCommand {
}
return true;
}
+ // Overview tab: stop process (SIGTERM) for selected integration
or infra
+ if (tab == TAB_OVERVIEW && ke.isChar('x') && selectedPid != null) {
+ stopSelectedProcess(false);
+ return true;
+ }
+ // Overview tab: kill process (SIGKILL) — show confirm dialog first
+ if (tab == TAB_OVERVIEW && ke.isChar('X') && selectedPid != null) {
+ showKillConfirm = true;
+ return true;
+ }
// Consumers tab: sort
if (tab == TAB_CONSUMERS && ke.isChar('s')) {
@@ -1038,12 +1101,20 @@ public class CamelMonitor extends CamelCommand {
if (selectedPid != null) {
return;
}
- List<IntegrationInfo> infos = sortedOverviewInfos();
- Integer sel = overviewTableState.selected();
- if (sel != null && sel >= 0 && sel < infos.size()) {
- selectedPid = infos.get(sel).pid;
- } else if (infos.size() == 1) {
- selectedPid = infos.get(0).pid;
+ if (infraTableFocused) {
+ List<InfraInfo> infras = infraData.get();
+ Integer sel = infraTableState.selected();
+ if (sel != null && sel >= 0 && sel < infras.size()) {
+ selectedPid = infras.get(sel).pid;
+ }
+ } else {
+ List<IntegrationInfo> infos = sortedOverviewInfos();
+ Integer sel = overviewTableState.selected();
+ if (sel != null && sel >= 0 && sel < infos.size()) {
+ selectedPid = infos.get(sel).pid;
+ } else if (infos.size() == 1) {
+ selectedPid = infos.get(0).pid;
+ }
}
}
@@ -1062,6 +1133,19 @@ public class CamelMonitor extends CamelCommand {
}
}
+ private void syncSelectedPidFromInfra() {
+ List<InfraInfo> infras = infraData.get();
+ Integer sel = infraTableState.selected();
+ String newPid = null;
+ if (sel != null && sel >= 0 && sel < infras.size()) {
+ newPid = infras.get(sel).pid;
+ }
+ if (newPid != null && !newPid.equals(selectedPid)) {
+ selectedPid = newPid;
+ resetIntegrationTabState();
+ }
+ }
+
// NOTE: When adding a new tab, reset its view state here too so switching
integrations
// on the Overview always shows a clean slate for the newly selected
integration.
private void resetIntegrationTabState() {
@@ -1105,8 +1189,13 @@ public class CamelMonitor extends CamelCommand {
private void navigateUp() {
switch (tabsState.selected()) {
case TAB_OVERVIEW -> {
- overviewTableState.selectPrevious();
- syncSelectedPidFromOverview();
+ if (infraTableFocused) {
+ infraTableState.selectPrevious();
+ syncSelectedPidFromInfra();
+ } else {
+ overviewTableState.selectPrevious();
+ syncSelectedPidFromOverview();
+ }
}
case TAB_ROUTES -> routeTableState.selectPrevious();
case TAB_CIRCUIT_BREAKER -> cbTableState.selectPrevious();
@@ -1132,11 +1221,15 @@ public class CamelMonitor extends CamelCommand {
}
private void navigateDown() {
- List<IntegrationInfo> infos = data.get().stream().filter(i ->
!i.vanishing).toList();
switch (tabsState.selected()) {
case TAB_OVERVIEW -> {
- overviewTableState.selectNext(sortedOverviewInfos().size());
- syncSelectedPidFromOverview();
+ if (infraTableFocused) {
+ infraTableState.selectNext(infraData.get().size());
+ syncSelectedPidFromInfra();
+ } else {
+
overviewTableState.selectNext(sortedOverviewInfos().size());
+ syncSelectedPidFromOverview();
+ }
}
case TAB_ROUTES -> {
IntegrationInfo info = findSelectedIntegration();
@@ -1190,6 +1283,9 @@ public class CamelMonitor extends CamelCommand {
renderTabs(frame, mainChunks.get(2));
// mainChunks.get(3) is the empty spacer row between tabs and content
renderContent(frame, mainChunks.get(4));
+ if (showKillConfirm) {
+ renderKillConfirm(frame, mainChunks.get(4));
+ }
renderFooter(frame, mainChunks.get(5));
}
@@ -1204,9 +1300,19 @@ public class CamelMonitor extends CamelCommand {
titleSpans.add(Span.styled(camelVersion != null ? "v" + camelVersion :
"", Style.EMPTY.fg(Color.GREEN)));
titleSpans.add(Span.raw(" "));
titleSpans.add(Span.styled(activeCount + " integration(s)",
Style.EMPTY.fg(Color.CYAN)));
+ long activeInfra = infraData.get().stream().filter(i ->
!i.vanishing).count();
+ if (activeInfra > 0) {
+ titleSpans.add(Span.raw(" "));
+ titleSpans.add(Span.styled(activeInfra + " infra(s)",
Style.EMPTY.fg(Color.MAGENTA)));
+ }
if (selectedPid != null) {
titleSpans.add(Span.raw(" "));
- titleSpans.add(Span.styled("selected: " + selectedName(),
Style.EMPTY.fg(Color.YELLOW)));
+ InfraInfo selInfra = findSelectedInfra();
+ if (selInfra != null) {
+ titleSpans.add(Span.styled("selected: " + selectedName(),
Style.EMPTY.fg(Color.MAGENTA)));
+ } else {
+ titleSpans.add(Span.styled("selected: " + selectedName(),
Style.EMPTY.fg(Color.YELLOW)));
+ }
}
Line titleLine = Line.from(titleSpans);
@@ -1216,6 +1322,32 @@ public class CamelMonitor extends CamelCommand {
}
private void renderTabs(Frame frame, Rect area) {
+ boolean infraSelected = isInfraSelected();
+
+ if (infraSelected) {
+ // Infra mode: only Overview and Log tabs
+ Line[] labels = {
+ Line.from(" 1 Overview "),
+ Line.from(" 2 Log "),
+ };
+
+ // Map real tab index to infra tab index for highlight
+ int infraTabIdx = tabsState.selected() == TAB_LOG ? 1 : 0;
+ TabsState infraTabsState = new TabsState(infraTabIdx);
+
+ Tabs tabs = Tabs.builder()
+ .titles(labels)
+ .highlightStyle(Style.EMPTY.fg(Color.rgb(0xF6, 0x91,
0x23)).bold())
+ .divider(Span.styled(" | ", Style.EMPTY.dim()))
+ .build();
+
+ Rect labelsArea = area.height() >= 2
+ ? new Rect(area.x(), area.y() + 1, area.width(), 1)
+ : area;
+ frame.renderStatefulWidget(tabs, labelsArea, infraTabsState);
+ return;
+ }
+
// Compute notification counts (0 if no integration selected)
List<IntegrationInfo> infos = data.get();
long activeCount = infos.stream().filter(i -> !i.vanishing).count();
@@ -1349,9 +1481,10 @@ public class CamelMonitor extends CamelCommand {
private void renderOverview(Frame frame, Rect area) {
List<IntegrationInfo> infos = sortedOverviewInfos();
+ List<InfraInfo> infraInfos = infraData.get();
// Keep the table selection index tracking the same PID across sort
changes and data refreshes
- if (selectedPid != null) {
+ if (selectedPid != null && !infraTableFocused) {
for (int i = 0; i < infos.size(); i++) {
if (selectedPid.equals(infos.get(i).pid)) {
overviewTableState.select(i);
@@ -1359,17 +1492,28 @@ public class CamelMonitor extends CamelCommand {
}
}
}
+ if (selectedPid != null && infraTableFocused) {
+ for (int i = 0; i < infraInfos.size(); i++) {
+ if (selectedPid.equals(infraInfos.get(i).pid)) {
+ infraTableState.select(i);
+ break;
+ }
+ }
+ }
- // Split: table (fill) + chart (14 rows: 13 chart + 1 x-axis) if we
have data and chart is on
- boolean hasSparkline = chartMode != CHART_OFF &&
!throughputHistory.isEmpty();
- List<Rect> chunks;
+ // Split: one table (integrations or infra, toggled by 'i') + chart or
info panel
+ boolean hasSparkline = chartMode != CHART_OFF &&
!throughputHistory.isEmpty() && !infraTableFocused;
+ boolean showInfoPanel = infraTableFocused && findSelectedInfra() !=
null && !hasSparkline;
+ List<Constraint> constraints = new ArrayList<>();
+ constraints.add(Constraint.fill());
if (hasSparkline) {
- chunks = Layout.vertical()
- .constraints(Constraint.fill(), Constraint.length(14))
- .split(area);
- } else {
- chunks = List.of(area);
+ constraints.add(Constraint.length(14));
+ } else if (showInfoPanel) {
+ constraints.add(Constraint.length(10));
}
+ List<Rect> chunks = Layout.vertical()
+ .constraints(constraints)
+ .split(area);
// Integration table
List<Row> rows = new ArrayList<>();
@@ -1439,32 +1583,39 @@ public class CamelMonitor extends CamelCommand {
rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
Cell.from(Span.styled("SINCE-LAST", Style.EMPTY.bold())));
- Table table = Table.builder()
- .rows(rows)
- .header(header)
- .widths(
- Constraint.length(8),
- Constraint.fill(),
- Constraint.length(16),
- Constraint.length(5),
- Constraint.length(10),
- Constraint.length(8),
- Constraint.length(7),
- Constraint.length(8),
- Constraint.length(8),
- Constraint.length(6),
- Constraint.length(8),
- Constraint.length(12))
- .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
- .highlightSpacing(Table.HighlightSpacing.ALWAYS)
- .block(Block.builder().borderType(BorderType.ROUNDED).title("
Integrations ").build())
- .build();
+ if (infraTableFocused) {
+ // Show infra table only
+ renderInfraTable(frame, chunks.get(0), infraInfos);
+ } else {
+ // Show integrations table only
+ Style integrationHighlight =
Style.EMPTY.fg(Color.WHITE).bold().onBlue();
+ Table table = Table.builder()
+ .rows(rows)
+ .header(header)
+ .widths(
+ Constraint.length(8),
+ Constraint.fill(),
+ Constraint.length(16),
+ Constraint.length(5),
+ Constraint.length(10),
+ Constraint.length(8),
+ Constraint.length(7),
+ Constraint.length(8),
+ Constraint.length(8),
+ Constraint.length(6),
+ Constraint.length(8),
+ Constraint.length(12))
+ .highlightStyle(integrationHighlight)
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+
.block(Block.builder().borderType(BorderType.ROUNDED).title(" Integrations
").build())
+ .build();
- frame.renderStatefulWidget(table, chunks.get(0), overviewTableState);
+ frame.renderStatefulWidget(table, chunks.get(0),
overviewTableState);
+ }
// Split green/red throughput bar chart with Y and X axes
if (hasSparkline && chunks.size() > 1) {
- Rect chartTotalArea = chunks.get(1);
+ Rect chartTotalArea = chunks.get(chunks.size() - 1);
// Split chart area horizontally: bar chart (fill) + info panel
(30 cols)
List<Rect> chartHSplit = Layout.horizontal()
@@ -1607,10 +1758,19 @@ public class CamelMonitor extends CamelCommand {
// Info panel: heap and threads for the selected integration
renderOverviewInfoPanel(frame, infoArea);
+ } else if (showInfoPanel) {
+ renderOverviewInfoPanel(frame, chunks.get(chunks.size() - 1));
}
}
private void renderOverviewInfoPanel(Frame frame, Rect area) {
+ // Check if an infra service is selected — show connection details
instead
+ InfraInfo infraSel = infraTableFocused ? findSelectedInfra() : null;
+ if (infraSel != null) {
+ renderInfraInfoPanel(frame, area, infraSel);
+ return;
+ }
+
IntegrationInfo sel = findSelectedIntegration();
// Fall back to the single active integration when nothing is
explicitly selected
if (sel == null) {
@@ -1713,6 +1873,89 @@ public class CamelMonitor extends CamelCommand {
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(),
inner);
}
+ // ---- Infra table (overview sub-section) ----
+
+ private void renderInfraTable(Frame frame, Rect area, List<InfraInfo>
infraInfos) {
+ List<Row> infraRows = new ArrayList<>();
+ for (InfraInfo info : infraInfos) {
+ if (info.vanishing) {
+ long elapsed = System.currentTimeMillis() - info.vanishStart;
+ float fade = 1.0f - Math.min(1.0f, (float) elapsed /
VANISH_DURATION_MS);
+ int gray = (int) (100 * fade);
+ Style dimStyle = Style.EMPTY.fg(Color.indexed(232 +
Math.min(gray / 4, 23)));
+ infraRows.add(Row.from(
+ Cell.from(Span.styled(info.pid, dimStyle)),
+ Cell.from(Span.styled(info.alias, dimStyle)),
+ Cell.from(Span.styled("✖ Stopped",
Style.EMPTY.fg(Color.LIGHT_RED).dim())),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle))));
+ } else {
+ Style statusStyle = info.alive ? Style.EMPTY.fg(Color.GREEN) :
Style.EMPTY.fg(Color.LIGHT_RED);
+ String statusText = info.alive ? "Running" : "Stopped";
+ String port = objToString(info.properties.get("getPort"));
+ String host = objToString(info.properties.get("getHost"));
+ if (host.isEmpty()) {
+ host = objToString(info.properties.get("getHostname"));
+ }
+ infraRows.add(Row.from(
+ Cell.from(info.pid),
+ Cell.from(Span.styled(info.alias,
Style.EMPTY.fg(Color.MAGENTA))),
+ Cell.from(Span.styled(statusText, statusStyle)),
+ Cell.from(port),
+ Cell.from(host)));
+ }
+ }
+
+ Row infraHeader = Row.from(
+ Cell.from(Span.styled("PID", Style.EMPTY.bold())),
+ Cell.from(Span.styled("SERVICE", Style.EMPTY.bold())),
+ Cell.from(Span.styled("STATUS", Style.EMPTY.bold())),
+ Cell.from(Span.styled("PORT", Style.EMPTY.bold())),
+ Cell.from(Span.styled("HOST", Style.EMPTY.bold())));
+
+ Style infraHighlight = Style.EMPTY.fg(Color.WHITE).bold().onBlue();
+ Table infraTable = Table.builder()
+ .rows(infraRows)
+ .header(infraHeader)
+ .widths(
+ Constraint.length(8),
+ Constraint.fill(),
+ Constraint.length(10),
+ Constraint.length(8),
+ Constraint.length(20))
+ .highlightStyle(infraHighlight)
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+ .block(Block.builder().borderType(BorderType.ROUNDED).title("
Infrastructure ").build())
+ .build();
+
+ frame.renderStatefulWidget(infraTable, area, infraTableState);
+ }
+
+ private void renderInfraInfoPanel(Frame frame, Rect area, InfraInfo infra)
{
+ Block infoBlock =
Block.builder().borderType(BorderType.ROUNDED).build();
+ frame.renderWidget(infoBlock, area);
+ Rect inner = infoBlock.inner(area);
+ List<Line> lines = new ArrayList<>();
+ Style dim = Style.EMPTY.dim();
+ lines.add(Line.from(
+ Span.styled("Service: ", dim),
+ Span.styled(infra.alias, Style.EMPTY.fg(Color.MAGENTA))));
+ lines.add(Line.from(Span.raw("")));
+ // Show connection properties with cleaned-up key names
+ for (Map.Entry<String, Object> e : infra.properties.entrySet()) {
+ String key = e.getKey();
+ // Strip "get" prefix and capitalize
+ if (key.startsWith("get") && key.length() > 3) {
+ key = key.substring(3);
+ }
+ String value = String.valueOf(e.getValue());
+ lines.add(Line.from(
+ Span.styled(key + ": ", dim),
+ Span.raw(TuiHelper.truncate(value, inner.width() -
key.length() - 2))));
+ }
+ frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(),
inner);
+ }
+
// ---- Tab 2: Routes ----
private void renderRoutes(Frame frame, Rect area) {
@@ -2855,6 +3098,49 @@ public class CamelMonitor extends CamelCommand {
sendRouteCommand(selectedPid, route.routeId, command);
}
+ private void stopSelectedProcess(boolean forceKill) {
+ if (selectedPid == null) {
+ return;
+ }
+ long pid;
+ try {
+ pid = Long.parseLong(selectedPid);
+ } catch (NumberFormatException e) {
+ return;
+ }
+ if (infraTableFocused) {
+ // For infra services: delete the JSON and log files to trigger
graceful shutdown
+ InfraInfo infra = findSelectedInfra();
+ if (infra != null) {
+ Path camelDir = CommandLineHelper.getCamelDir();
+ PathUtils.deleteFile(camelDir.resolve("infra-" + infra.alias +
"-" + infra.pid + ".json"));
+ PathUtils.deleteFile(camelDir.resolve("infra-" + infra.alias +
"-" + infra.pid + ".log"));
+ }
+ if (forceKill) {
+
ProcessHandle.of(pid).ifPresent(ProcessHandle::destroyForcibly);
+ }
+ } else {
+ // For integrations: signal the process directly
+ ProcessHandle.of(pid).ifPresent(ph -> {
+ if (forceKill) {
+ ph.destroyForcibly();
+ // Clean up orphaned files after force kill
+ Path camelDir = CommandLineHelper.getCamelDir();
+ PathUtils.deleteFile(camelDir.resolve(selectedPid +
".log"));
+ PathUtils.deleteFile(camelDir.resolve(selectedPid +
"-status.json"));
+ PathUtils.deleteFile(camelDir.resolve(selectedPid +
"-action.json"));
+ PathUtils.deleteFile(camelDir.resolve(selectedPid +
"-output.json"));
+ PathUtils.deleteFile(camelDir.resolve(selectedPid +
"-trace.json"));
+ PathUtils.deleteFile(camelDir.resolve(selectedPid +
"-history.json"));
+ PathUtils.deleteFile(camelDir.resolve(selectedPid +
"-debug.json"));
+ PathUtils.deleteFile(camelDir.resolve(selectedPid +
"-receive.json"));
+ } else {
+ ph.destroy();
+ }
+ });
+ }
+ }
+
private void sendRouteCommand(String pid, String routeId, String command) {
JsonObject root = new JsonObject();
root.put("action", "route");
@@ -3830,7 +4116,8 @@ public class CamelMonitor extends CamelCommand {
private void renderLog(Frame frame, Rect area) {
IntegrationInfo info = findSelectedIntegration();
- if (info == null) {
+ InfraInfo infraSel = info == null ? findSelectedInfra() : null;
+ if (info == null && infraSel == null) {
renderNoSelection(frame, area);
return;
}
@@ -3842,9 +4129,14 @@ public class CamelMonitor extends CamelCommand {
String chunkSuffix = totalRead > entries.size()
? " #" + (totalRead - entries.size() + 1) + "-" + totalRead
: "";
- String logTitle = info.rootLogLevel != null
- ? " Log level:" + info.rootLogLevel + chunkSuffix + " "
- : " Log" + chunkSuffix + " ";
+ String logTitle;
+ if (infraSel != null) {
+ logTitle = " Log [" + infraSel.alias + "]" + chunkSuffix + " ";
+ } else if (info.rootLogLevel != null) {
+ logTitle = " Log level:" + info.rootLogLevel + chunkSuffix + " ";
+ } else {
+ logTitle = " Log" + chunkSuffix + " ";
+ }
Block block = Block.builder()
.borderType(BorderType.ROUNDED)
.title(logTitle)
@@ -3950,6 +4242,39 @@ public class CamelMonitor extends CamelCommand {
frame.renderStatefulWidget(list, popup, logLevelListState);
}
+ private void renderKillConfirm(Frame frame, Rect area) {
+ String name = selectedName();
+ String msg = " Kill " + name + " (PID: " + selectedPid + ")? ";
+ int popupW = Math.max(34, msg.length() + 4);
+ int popupH = 6;
+ int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+ int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+ Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
+
+ frame.renderWidget(Clear.INSTANCE, popup);
+ Block block = Block.builder()
+ .borderType(BorderType.ROUNDED)
+ .borderStyle(Style.EMPTY.fg(Color.LIGHT_RED))
+ .title(" Confirm Kill ")
+ .build();
+ frame.renderWidget(block, popup);
+ Rect inner = block.inner(popup);
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(
+ Line.from(Span.raw("")),
+ Line.from(Span.styled(msg,
Style.EMPTY.fg(Color.LIGHT_RED).bold())),
+ Line.from(Span.raw("")),
+ Line.from(
+ Span.raw(" "),
+ Span.styled("Enter",
Style.EMPTY.bold()),
+ Span.raw(" confirm "),
+ Span.styled("Esc", Style.EMPTY.bold()),
+ Span.raw(" cancel"))))
+ .build(),
+ inner);
+ }
+
private void sendLoggerLevelAction(String pid, String level) {
JsonObject root = new JsonObject();
root.put("action", "logger");
@@ -3961,7 +4286,11 @@ public class CamelMonitor extends CamelCommand {
}
private void readNewLogLines(String pid, List<String> newLines) {
- Path logFile = CommandLineHelper.getCamelDir().resolve(pid + ".log");
+ readNewLogLinesFromFile(pid, pid + ".log", newLines);
+ }
+
+ private void readNewLogLinesFromFile(String pid, String fileName,
List<String> newLines) {
+ Path logFile = CommandLineHelper.getCamelDir().resolve(fileName);
if (!Files.exists(logFile)) {
logFilePid = pid;
logFilePos = -1;
@@ -5099,23 +5428,31 @@ public class CamelMonitor extends CamelCommand {
if (tab == TAB_OVERVIEW) {
hint(spans, "q", "quit");
if (selectedPid != null) {
- hint(spans, "Esc", "unselect");
+ hint(spans, "Esc", infraTableFocused ? "integrations" :
"unselect");
}
hint(spans, "\u2191\u2193", "navigate");
- hint(spans, "s", "sort");
- hint(spans, "a", "chart " + switch (chartMode) {
- case CHART_ALL -> "[all]";
- case CHART_SINGLE -> "[single]";
- default -> "[off]";
- });
- hint(spans, "Enter", "details");
- if (selectedPid != null) {
+ if (!infraData.get().isEmpty()) {
+ hint(spans, "i", infraTableFocused ? "integrations" : "infra");
+ }
+ if (!infraTableFocused) {
+ hint(spans, "s", "sort");
+ hint(spans, "a", "chart " + switch (chartMode) {
+ case CHART_ALL -> "[all]";
+ case CHART_SINGLE -> "[single]";
+ default -> "[off]";
+ });
+ }
+ if (selectedPid != null && !infraTableFocused) {
IntegrationInfo selInfo = findSelectedIntegration();
if (selInfo != null) {
- hint(spans, "p", selInfo.routeStarted > 0 ? "stop" :
"start");
+ hint(spans, "p", selInfo.routeStarted > 0 ? "stop routes"
: "start routes");
}
}
- hint(spans, "1-9", "tabs");
+ if (selectedPid != null) {
+ hint(spans, "x", "stop");
+ hint(spans, "X", "kill");
+ }
+ hint(spans, isInfraSelected() ? "1-2" : "1-9", "tabs");
} else if (tab == TAB_ROUTES && showSource) {
hint(spans, "c/Esc", "close");
hint(spans, "\u2191\u2193\u2190\u2192", "scroll");
@@ -5193,8 +5530,11 @@ public class CamelMonitor extends CamelCommand {
if (!logWordWrap) {
hint(spans, "\u2190\u2192", "h-scroll");
}
- hint(spans, "l", "level");
- hintLast(spans, "f", "follow" + (logFollowMode ? " [on]" : "
[off]"));
+ if (!isInfraSelected()) {
+ hint(spans, "l", "level");
+ }
+ hint(spans, "f", "follow" + (logFollowMode ? " [on]" : " [off]"));
+ hint(spans, isInfraSelected() ? "1-2" : "1-9", "tabs");
} else if (tab == TAB_HTTP && showHttpSpec) {
hint(spans, "c/Esc", "close");
hint(spans, "↑↓", "scroll");
@@ -5394,12 +5734,37 @@ public class CamelMonitor extends CamelCommand {
data.set(infos);
+ // Discover running infra services
+ refreshInfraData();
+
+ // Auto-focus infra table when no active integrations exist
+ if (!infraTableFocused && !infraData.get().isEmpty()
+ && infos.stream().noneMatch(i -> !i.vanishing)) {
+ infraTableFocused = true;
+ if (infraTableState.selected() == null) {
+ infraTableState.select(0);
+ }
+ syncSelectedPidFromInfra();
+ }
+
// Refresh log data only when the Log tab is visible
if (tabsState.selected() == TAB_LOG) {
- IntegrationInfo selected = findSelectedIntegration();
- if (selected != null) {
- if (!selected.pid.equals(logFilePid)) {
- // Integration changed: reset all incremental log state
+ // Determine which log file to tail: infra or integration
+ String logPid = null;
+ String logFileName = null;
+ InfraInfo selInfra = findSelectedInfra();
+ if (selInfra != null) {
+ logPid = selInfra.pid;
+ logFileName = "infra-" + selInfra.alias + "-" +
selInfra.pid + ".log";
+ } else {
+ IntegrationInfo selected = findSelectedIntegration();
+ if (selected != null) {
+ logPid = selected.pid;
+ logFileName = selected.pid + ".log";
+ }
+ }
+ if (logPid != null) {
+ if (!logPid.equals(logFilePid)) {
mutableFilteredEntries.clear();
logFilePos = -1;
logTotalLinesRead = 0;
@@ -5407,7 +5772,7 @@ public class CamelMonitor extends CamelCommand {
logLineBuffer.setLength(0);
}
List<String> newRawLines = new ArrayList<>();
- readNewLogLines(selected.pid, newRawLines);
+ readNewLogLinesFromFile(logPid, logFileName, newRawLines);
if (!newRawLines.isEmpty()) {
logTotalLinesRead += newRawLines.size();
for (String line : newRawLines) {
@@ -5430,6 +5795,88 @@ public class CamelMonitor extends CamelCommand {
}
}
+ @SuppressWarnings("unchecked")
+ private void refreshInfraData() {
+ List<InfraInfo> infraInfos = new ArrayList<>();
+ try {
+ Path camelDir = CommandLineHelper.getCamelDir();
+ if (Files.isDirectory(camelDir)) {
+ try (var files = Files.list(camelDir)) {
+ List<Path> jsonFiles = files
+ .filter(p -> {
+ String n = p.getFileName().toString();
+ return n.startsWith("infra-") &&
n.endsWith(".json");
+ })
+ .toList();
+ for (Path jsonFile : jsonFiles) {
+ String fn = jsonFile.getFileName().toString();
+ // Format: infra-{alias}-{pid}.json
+ String withoutExt = fn.substring(0,
fn.lastIndexOf('.'));
+ int lastDash = withoutExt.lastIndexOf('-');
+ if (lastDash <= 6) {
+ continue;
+ }
+ String alias = withoutExt.substring(6, lastDash);
+ String pidStr = withoutExt.substring(lastDash + 1);
+ long pid;
+ try {
+ pid = Long.parseLong(pidStr);
+ } catch (NumberFormatException e) {
+ continue;
+ }
+ boolean alive =
ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false);
+
+ InfraInfo info = new InfraInfo();
+ info.pid = pidStr;
+ info.alias = alias;
+ info.alive = alive;
+ try {
+ String json = Files.readString(jsonFile);
+ Object parsed = Jsoner.deserialize(json);
+ if (parsed instanceof Map<?, ?> map) {
+ for (Map.Entry<?, ?> e : map.entrySet()) {
+
info.properties.put(String.valueOf(e.getKey()), e.getValue());
+ }
+ }
+ } catch (Exception e) {
+ // ignore parse errors
+ }
+ infraInfos.add(info);
+ }
+ }
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+
+ // Handle vanishing infra services
+ Set<String> liveInfraPids = infraInfos.stream().map(i ->
i.pid).collect(Collectors.toSet());
+ List<InfraInfo> previousInfra = infraData.get();
+ for (InfraInfo prev : previousInfra) {
+ if (!prev.vanishing && !liveInfraPids.contains(prev.pid) &&
!vanishingInfra.containsKey(prev.pid)) {
+ vanishingInfra.put(prev.pid, new VanishingInfraInfo(prev,
System.currentTimeMillis()));
+ }
+ }
+ long now = System.currentTimeMillis();
+ Iterator<Map.Entry<String, VanishingInfraInfo>> infraIt =
vanishingInfra.entrySet().iterator();
+ while (infraIt.hasNext()) {
+ Map.Entry<String, VanishingInfraInfo> entry = infraIt.next();
+ if (now - entry.getValue().startTime > VANISH_DURATION_MS) {
+ infraIt.remove();
+ } else if (!liveInfraPids.contains(entry.getKey())) {
+ InfraInfo ghost = entry.getValue().info;
+ ghost.vanishing = true;
+ ghost.vanishStart = entry.getValue().startTime;
+ infraInfos.add(ghost);
+ } else {
+ infraIt.remove();
+ }
+ }
+
+ infraInfos.sort((a, b) -> a.alias.compareToIgnoreCase(b.alias));
+ infraData.set(infraInfos);
+ }
+
private void updateThroughputHistory(IntegrationInfo info) {
// Track exchangesTotal and exchangesFailed over a 1-second sliding
window
long currentTotal = info.exchangesTotal;
@@ -6408,9 +6855,29 @@ public class CamelMonitor extends CamelCommand {
.findFirst().orElse(null);
}
+ private InfraInfo findSelectedInfra() {
+ if (selectedPid == null) {
+ return null;
+ }
+ return infraData.get().stream()
+ .filter(i -> selectedPid.equals(i.pid) && !i.vanishing)
+ .findFirst().orElse(null);
+ }
+
+ private boolean isInfraSelected() {
+ return infraTableFocused && findSelectedInfra() != null;
+ }
+
private String selectedName() {
IntegrationInfo info = findSelectedIntegration();
- return info != null ? truncate(info.name, 20) : "?";
+ if (info != null) {
+ return truncate(info.name, 20);
+ }
+ InfraInfo infra = findSelectedInfra();
+ if (infra != null) {
+ return truncate(infra.alias, 20);
+ }
+ return "?";
}
private List<Long> findPids(String name) {
@@ -6754,6 +7221,19 @@ public class CamelMonitor extends CamelCommand {
Map<String, String> exchangeVariableTypes;
}
+ static class InfraInfo {
+ String pid;
+ String alias;
+ String description;
+ Map<String, Object> properties = new TreeMap<>();
+ boolean alive;
+ boolean vanishing;
+ long vanishStart;
+ }
+
record VanishingInfo(IntegrationInfo info, long startTime) {
}
+
+ record VanishingInfraInfo(InfraInfo info, long startTime) {
+ }
}