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 817ca08ec940 chore: camel-jbang - a view bar on every tab with views
(Inspect, Errors, Route, Endpoint, HTTP, Metrics, Memory Leak), as the Diagram
tab (#27203)
817ca08ec940 is described below
commit 817ca08ec9404252692f44ae6c9ada02b992ecd3
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 15:12:21 2026 +0200
chore: camel-jbang - a view bar on every tab with views (Inspect, Errors,
Route, Endpoint, HTTP, Metrics, Memory Leak), as the Diagram tab (#27203)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../dsl/jbang/core/commands/tui/AbstractTab.java | 6 +
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 27 ++++
.../jbang/core/commands/tui/DiagramLevelBar.java | 131 ----------------
.../dsl/jbang/core/commands/tui/DiagramTab.java | 110 ++++++-------
.../dsl/jbang/core/commands/tui/EndpointsTab.java | 30 ++--
.../dsl/jbang/core/commands/tui/ErrorsTab.java | 42 ++++-
.../dsl/jbang/core/commands/tui/HistoryTab.java | 54 +++++--
.../camel/dsl/jbang/core/commands/tui/HttpTab.java | 28 +++-
.../dsl/jbang/core/commands/tui/MemoryLeakTab.java | 20 ++-
.../dsl/jbang/core/commands/tui/MetricsTab.java | 28 +++-
.../dsl/jbang/core/commands/tui/MonitorTab.java | 19 ++-
.../dsl/jbang/core/commands/tui/RoutesTab.java | 40 ++++-
.../dsl/jbang/core/commands/tui/SubViewBar.java | 173 +++++++++++++++++++++
.../core/commands/tui/DiagramLevelBarTest.java | 67 --------
.../core/commands/tui/EndpointsTabRenderTest.java | 6 +-
.../core/commands/tui/ErrorsTabRenderTest.java | 7 +-
.../core/commands/tui/MetricsTabRenderTest.java | 5 +-
.../jbang/core/commands/tui/SubViewBarTest.java | 98 ++++++++++++
18 files changed, 583 insertions(+), 308 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java
index fd1249cc3673..3ccc74ce9fe1 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java
@@ -26,6 +26,7 @@ import dev.tamboui.terminal.Frame;
import dev.tamboui.text.Line;
import dev.tamboui.text.Span;
import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.tui.event.MouseEvent;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
@@ -41,6 +42,11 @@ import dev.tamboui.widgets.table.TableState;
abstract class AbstractTab implements MonitorTab {
+ /** Presses a key of the tab, as a click on a view of the view bar does. */
+ protected void pressKey(char key) {
+ handleKeyEvent(KeyEvent.ofChar(key));
+ }
+
protected final MonitorContext ctx;
protected AbstractTab(MonitorContext ctx) {
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 61bd5a2aac2d..32fec82f9560 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
@@ -222,6 +222,8 @@ public class CamelMonitor extends CamelCommand {
private String lastTabDivider;
// Panel resize drag state
private final DragSplit panelSplit = new DragSplit();
+ /** The views and view settings of the active tab, on the first row of its
content. */
+ private final SubViewBar subViewBar = new SubViewBar();
// Footer key-binding hit-testing: each clickable hint records its
[startX, endX) column range on
// the footer row and the KeyEvent to synthesize when clicked.
private int footerRowY = -1;
@@ -1664,6 +1666,23 @@ public class CamelMonitor extends CamelCommand {
return true;
}
+ // The view bar of the tab: a click on a view goes there, a click on a
view setting presses its key
+ if (subViewBar.isOnRow(me.y()) && TuiHelper.contains(lastContentArea,
me.x(), me.y())
+ && !popupManager.isMorePopupVisible() &&
!popupManager.isSwitchPopupVisible()) {
+ if (me.isClick()) {
+ SubViewBar.View view = subViewBar.viewAt(me.x(), me.y());
+ if (view != null) {
+ view.select().run();
+ return true;
+ }
+ KeyEvent key = footerKeyEvent(subViewBar.keyAt(me.x(),
me.y()));
+ if (key != null) {
+ return handleEvent(key, runner);
+ }
+ }
+ return true;
+ }
+
// Mouse events in the content area: delegate to the active tab
if (TuiHelper.contains(lastContentArea, me.x(), me.y())) {
if (popupManager.isMorePopupVisible() ||
popupManager.isSwitchPopupVisible()) {
@@ -2283,6 +2302,14 @@ public class CamelMonitor extends CamelCommand {
frame.buffer().clear(area);
MonitorTab tab = tabRegistry.activeTab();
if (tab != null) {
+ SubViewBar.Spec spec = tab.subViewBar();
+ if (spec != null && area.height() > 3) {
+ List<Rect> rows =
Layout.vertical().constraints(Constraint.length(1),
Constraint.fill()).split(area);
+ subViewBar.render(frame, rows.get(0), spec);
+ area = rows.get(1);
+ } else {
+ subViewBar.clear();
+ }
tab.render(frame, area);
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLevelBar.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLevelBar.java
deleted file mode 100644
index f1fa9b4ade97..000000000000
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLevelBar.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.camel.dsl.jbang.core.commands.tui;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import dev.tamboui.layout.Rect;
-import dev.tamboui.style.Style;
-import dev.tamboui.terminal.Frame;
-import dev.tamboui.text.CharWidth;
-import dev.tamboui.text.Line;
-import dev.tamboui.text.Span;
-import dev.tamboui.text.Text;
-import dev.tamboui.widgets.paragraph.Paragraph;
-
-/**
- * The zoom levels of the Diagram tab as one line above the diagram
(CAMEL-25147): Architecture › Topology › Route. The
- * level shown is highlighted, the others are dim but visible so the user
knows they exist, and a level that does not
- * apply yet (no route selected) is greyed out. The labels carry the path: the
capability a topology is filtered to, the
- * route a diagram shows. A click on a level goes there.
- */
-final class DiagramLevelBar {
-
- enum Level {
- ARCHITECTURE,
- TOPOLOGY,
- ROUTE
- }
-
- /** One level as the bar shows it. */
- record Segment(Level level, String label, boolean enabled) {
- }
-
- /** A view setting of the level shown: its key, name and state (on, off,
or a mode such as edges). */
- record Toggle(String key, String label, String state) {
-
- boolean on() {
- return !"off".equals(state);
- }
- }
-
- private static final String SEPARATOR = " › ";
-
- private final List<int[]> hitBoxes = new ArrayList<>();
- private final List<Level> hitLevels = new ArrayList<>();
- private int row = -1;
-
- /** Draws the bar in a one-row area, with the view settings of the level
at the right. */
- void render(Frame frame, Rect area, List<Segment> segments, Level current,
List<Toggle> toggles) {
- hitBoxes.clear();
- hitLevels.clear();
- row = area.y();
- List<Span> spans = new ArrayList<>();
- // the key that moves through the levels, as the settings on the right
show theirs
- String key = " v ";
- spans.add(Span.raw(" "));
- spans.add(Span.styled(key, Theme.hintKey()));
- spans.add(Span.raw(" "));
- int x = area.x() + 1 + CharWidth.of(key) + 1;
- for (int i = 0; i < segments.size(); i++) {
- Segment s = segments.get(i);
- if (i > 0) {
- spans.add(Span.styled(SEPARATOR, Theme.muted()));
- x += CharWidth.of(SEPARATOR);
- }
- boolean active = s.level() == current;
- String text = (active ? "◆ " : "◇ ") + s.label();
- Style style = active ? Theme.accentBg()
- : s.enabled() ? Style.EMPTY.fg(Theme.baseFg()) :
Theme.muted().dim();
- String shown = active ? " " + text + " " : text;
- spans.add(Span.styled(shown, style));
- int w = CharWidth.of(shown);
- hitBoxes.add(new int[] { x, x + w });
- hitLevels.add(s.enabled() ? s.level() : null);
- x += w;
- }
- int used = x - area.x();
- // the settings at the right, as many as fit
- List<Span> right = new ArrayList<>();
- int rightWidth = 0;
- for (Toggle t : toggles) {
- // an on/off setting shows its state as a mark, a setting with
modes names the mode
- boolean onOff = "on".equals(t.state()) || "off".equals(t.state());
- String chip = " " + t.key() + " ";
- String label = " " + t.label() + (onOff ? "" : ": " + t.state()) +
" ";
- String mark = (onOff ? (t.on() ? "\u25cf" : "\u25cb") : "") + " ";
- int w = CharWidth.of(chip) + CharWidth.of(label) +
CharWidth.of(mark);
- if (used + rightWidth + w + 2 > area.width()) {
- break;
- }
- right.add(Span.styled(chip, Theme.hintKey()));
- right.add(Span.raw(label));
- right.add(Span.styled(mark, t.on() ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted()));
- rightWidth += w;
- }
- if (!right.isEmpty()) {
- spans.add(Span.raw(" ".repeat(area.width() - used - rightWidth)));
- spans.addAll(right);
- }
-
frame.renderWidget(Paragraph.builder().text(Text.from(Line.from(spans))).build(),
area);
- }
-
- /** The level under a click on the bar, or null when the click is
elsewhere or on a level that does not apply. */
- Level hit(int x, int y) {
- if (y != row) {
- return null;
- }
- for (int i = 0; i < hitBoxes.size(); i++) {
- int[] box = hitBoxes.get(i);
- if (x >= box[0] && x < box[1]) {
- return hitLevels.get(i);
- }
- }
- return null;
- }
-}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index fe251b887402..cdee18e4f199 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -48,6 +48,13 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
class DiagramTab extends AbstractTab {
+ /** The zoom levels of the tab (CAMEL-25147): Architecture › Topology ›
Route. */
+ enum Level {
+ ARCHITECTURE,
+ TOPOLOGY,
+ ROUTE
+ }
+
private final DiagramSupport diagram = new DiagramSupport();
private final SourceViewer sourceViewer = new SourceViewer();
private final GotoNodePopup gotoNodePopup = new GotoNodePopup();
@@ -64,7 +71,6 @@ class DiagramTab extends AbstractTab {
private final DiagramDetailSupport detail = new DiagramDetailSupport(ctx,
diagram);
private final ArchitectureView architecture
= new ArchitectureView(ctx, this::focusGroup, () ->
diagram.isShowDescription());
- private final DiagramLevelBar levelBar = new DiagramLevelBar();
/** Whether the user chose descriptions on or off with n; until then they
are on when the routes have any. */
private boolean descriptionChosen;
/** Whether the topology shows each route's group; null until the user
presses g (then on when there are groups). */
@@ -385,13 +391,6 @@ class DiagramTab extends AbstractTab {
if (gotoNodePopup.isVisible()) {
return true;
}
- if (me.isClick()) {
- DiagramLevelBar.Level clicked = levelBar.hit(me.x(), me.y());
- if (clicked != null) {
- goToLevel(clicked);
- return true;
- }
- }
if (detailMode && detail.containsMouse(me.x(), me.y())) {
if (me.kind() == MouseEventKind.SCROLL_UP) {
detail.scrollBy(-3);
@@ -445,7 +444,7 @@ class DiagramTab extends AbstractTab {
}
if (topologyMode && diagram.isShowDiagram()) {
// Esc zooms out: from the topology up to the architecture
- goToLevel(DiagramLevelBar.Level.ARCHITECTURE);
+ goToLevel(Level.ARCHITECTURE);
return true;
}
if (!topologyMode) {
@@ -526,11 +525,11 @@ class DiagramTab extends AbstractTab {
// ---- zoom levels (CAMEL-25147) ----
/** The level shown: the capability groups, the topology of all routes, or
one route's diagram. */
- DiagramLevelBar.Level level() {
+ Level level() {
if (architecture.isActive()) {
- return DiagramLevelBar.Level.ARCHITECTURE;
+ return Level.ARCHITECTURE;
}
- return topologyMode ? DiagramLevelBar.Level.TOPOLOGY :
DiagramLevelBar.Level.ROUTE;
+ return topologyMode ? Level.TOPOLOGY : Level.ROUTE;
}
/**
@@ -550,20 +549,20 @@ class DiagramTab extends AbstractTab {
/** v: architecture, topology, route, and round again; the route level is
skipped when no route is selected. */
private void cycleLevel() {
goToLevel(switch (level()) {
- case ARCHITECTURE -> DiagramLevelBar.Level.TOPOLOGY;
- case TOPOLOGY -> routeCandidate() != null ?
DiagramLevelBar.Level.ROUTE : DiagramLevelBar.Level.ARCHITECTURE;
- case ROUTE -> DiagramLevelBar.Level.ARCHITECTURE;
+ case ARCHITECTURE -> Level.TOPOLOGY;
+ case TOPOLOGY -> routeCandidate() != null ? Level.ROUTE :
Level.ARCHITECTURE;
+ case ROUTE -> Level.ARCHITECTURE;
});
}
- void goToLevel(DiagramLevelBar.Level target) {
- DiagramLevelBar.Level now = level();
+ void goToLevel(Level target) {
+ Level now = level();
if (target == now) {
return;
}
switch (target) {
case ARCHITECTURE -> {
- if (now == DiagramLevelBar.Level.ROUTE) {
+ if (now == Level.ROUTE) {
leaveRoute();
}
diagram.setFocus(null, null);
@@ -574,7 +573,7 @@ class DiagramTab extends AbstractTab {
}
}
case TOPOLOGY -> {
- if (now == DiagramLevelBar.Level.ROUTE) {
+ if (now == Level.ROUTE) {
leaveRoute();
} else if (!architecture.openSelectedGroup()) {
architecture.close();
@@ -633,19 +632,25 @@ class DiagramTab extends AbstractTab {
}
}
- private void renderLevelBar(Frame frame, Rect row) {
- DiagramLevelBar.Level current = level();
+ @Override
+ public SubViewBar.Spec subViewBar() {
+ if (ctx.findSelectedIntegration() == null || sourceViewer.isVisible()
|| !diagram.isShowDiagram()) {
+ return null;
+ }
+ Level current = level();
String group = diagram.getFocusName();
String route = routeCandidate();
- List<DiagramLevelBar.Segment> segments = List.of(
- new
DiagramLevelBar.Segment(DiagramLevelBar.Level.ARCHITECTURE, "Architecture",
true),
- new DiagramLevelBar.Segment(
- DiagramLevelBar.Level.TOPOLOGY,
- group != null ? "Topology \u00b7 " + group :
"Topology", true),
- new DiagramLevelBar.Segment(
- DiagramLevelBar.Level.ROUTE,
- route != null ? "Route: " + route : "Route", route !=
null));
- levelBar.render(frame, row, segments, current, viewToggles());
+ List<SubViewBar.View> views = List.of(
+ new SubViewBar.View(
+ "Architecture", current == Level.ARCHITECTURE, true,
+ () -> goToLevel(Level.ARCHITECTURE)),
+ new SubViewBar.View(
+ group != null ? "Topology \u00b7 " + group :
"Topology", current == Level.TOPOLOGY,
+ true, () -> goToLevel(Level.TOPOLOGY)),
+ new SubViewBar.View(
+ route != null ? "Route: " + route : "Route", current
== Level.ROUTE, route != null,
+ () -> goToLevel(Level.ROUTE)));
+ return new SubViewBar.Spec("v", views, viewToggles(), true);
}
/**
@@ -685,34 +690,34 @@ class DiagramTab extends AbstractTab {
}
/** The view settings of the level shown, with their state: they sit on
the level bar, beside the diagram. */
- List<DiagramLevelBar.Toggle> viewToggles() {
- List<DiagramLevelBar.Toggle> toggles = new ArrayList<>();
+ List<SubViewBar.Toggle> viewToggles() {
+ List<SubViewBar.Toggle> toggles = new ArrayList<>();
if (IntegrationSummaryHints.settingEnabled()) {
// without a summary yet the setting is off, and a has the AI
write one
- toggles.add(new DiagramLevelBar.Toggle(
+ toggles.add(new SubViewBar.Toggle(
"a", "ai", hasAiHints() &&
IntegrationSummaryHints.isShown() ? "on" : "off"));
}
toggles.addAll(levelToggles());
return toggles;
}
- private List<DiagramLevelBar.Toggle> levelToggles() {
- DiagramLevelBar.Toggle view
- = new DiagramLevelBar.Toggle("b", "view",
diagram.isShowDescription() ? "business" : "technical");
+ private List<SubViewBar.Toggle> levelToggles() {
+ SubViewBar.Toggle view
+ = new SubViewBar.Toggle("b", "view",
diagram.isShowDescription() ? "business" : "technical");
String metrics = diagramMetrics ? "on" : "off";
return switch (level()) {
case ARCHITECTURE -> List.of(view,
- new DiagramLevelBar.Toggle("u", "utility",
architecture.isShowUtility() ? "on" : "off"),
- new DiagramLevelBar.Toggle("e", "external",
architecture.isShowExternal() ? "edges" : "off"));
+ new SubViewBar.Toggle("u", "utility",
architecture.isShowUtility() ? "on" : "off"),
+ new SubViewBar.Toggle("e", "external",
architecture.isShowExternal() ? "edges" : "off"));
case TOPOLOGY -> {
- List<DiagramLevelBar.Toggle> toggles = new ArrayList<>();
+ List<SubViewBar.Toggle> toggles = new ArrayList<>();
toggles.add(view);
- toggles.add(new DiagramLevelBar.Toggle("g", "group",
isShowGroups() ? "on" : "off"));
+ toggles.add(new SubViewBar.Toggle("g", "group", isShowGroups()
? "on" : "off"));
if (hasUtilityRoutes()) {
- toggles.add(new DiagramLevelBar.Toggle("u", "utility",
architecture.isShowUtility() ? "on" : "off"));
+ toggles.add(new SubViewBar.Toggle("u", "utility",
architecture.isShowUtility() ? "on" : "off"));
}
- toggles.add(new DiagramLevelBar.Toggle("m", "metrics",
metrics));
- toggles.add(new DiagramLevelBar.Toggle(
+ toggles.add(new SubViewBar.Toggle("m", "metrics", metrics));
+ toggles.add(new SubViewBar.Toggle(
"e", "external",
switch (externalMode) {
case 1 -> "edges";
@@ -721,20 +726,11 @@ class DiagramTab extends AbstractTab {
}));
yield toggles;
}
- case ROUTE -> List.of(view, new DiagramLevelBar.Toggle("m",
"metrics", metrics),
- new DiagramLevelBar.Toggle("d", "detail", detailMode ?
"on" : "off"));
+ case ROUTE -> List.of(view, new SubViewBar.Toggle("m", "metrics",
metrics),
+ new SubViewBar.Toggle("d", "detail", detailMode ? "on" :
"off"));
};
}
- @Override
- public void renderViewToggles(List<Span> spans) {
- if (diagram.isShowDiagram() && !sourceViewer.isVisible()) {
- for (DiagramLevelBar.Toggle t : viewToggles()) {
- hint(spans, t.key(), t.label() + " [" + t.state() + "]");
- }
- }
- }
-
/**
* Opens the route diagram of a route (the Route level), when the
integration runs that route.
*
@@ -792,12 +788,6 @@ class DiagramTab extends AbstractTab {
return;
}
- if (diagram.isShowDiagram() && area.height() > 3) {
- List<Rect> rows =
Layout.vertical().constraints(Constraint.length(1),
Constraint.fill()).split(area);
- renderLevelBar(frame, rows.get(0));
- area = rows.get(1);
- }
-
if (architecture.isActive()) {
architecture.render(frame, area, info.name);
return;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index c17bdd22c20b..0976b4c7b94e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -384,19 +384,31 @@ class EndpointsTab extends AbstractTableTab {
}
}
+ @Override
+ public SubViewBar.Spec subViewBar() {
+ if (ctx.findSelectedIntegration() == null) {
+ return null;
+ }
+ String[] filterLabels = { "all", "remote", "remote+stub" };
+ List<SubViewBar.View> views = List.of(
+ new SubViewBar.View("Chart", panelMode == PANEL_CHART, true,
() -> pressKey('d')),
+ new SubViewBar.View("Detail", panelMode == PANEL_DETAIL, true,
() -> pressKey('d')));
+ List<SubViewBar.Toggle> toggles = new ArrayList<>();
+ toggles.add(new SubViewBar.Toggle("f", "filter",
filterLabels[filter]));
+ if (panelMode == PANEL_CHART) {
+ toggles.add(new SubViewBar.Toggle("a", "chart", switch (chartMode)
{
+ case CHART_ALL -> "all";
+ case CHART_SINGLE -> "single";
+ default -> "off";
+ }));
+ }
+ return new SubViewBar.Spec("d", views, toggles, false);
+ }
+
@Override
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
hint(spans, "s", "sort");
- String[] filterLabels = { "all", "remote", "remote+stub" };
- hint(spans, "f", "filter [" + filterLabels[filter] + "]");
- String chartLabel = switch (chartMode) {
- case CHART_ALL -> "[all]";
- case CHART_SINGLE -> "[single]";
- default -> "[off]";
- };
- hint(spans, "a", "chart " + chartLabel);
- hint(spans, "d", "detail " + (panelMode == PANEL_DETAIL ? "[on]" :
"[off]"));
if (panelMode == PANEL_DETAIL) {
hint(spans, "Tab", detailFocused ? "table" : "detail");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
index d5490b6074b2..ab5923980a1e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
@@ -401,6 +401,29 @@ class ErrorsTab extends AbstractTableTab {
}
}
+ @Override
+ public SubViewBar.Spec subViewBar() {
+ if (ctx.findSelectedIntegration() == null) {
+ return null;
+ }
+ boolean diagramShown = diagram.isShowDiagram();
+ boolean historyDiagram = diagramShown && diagram.isHistoryMode() &&
diagram.hasHistoryData();
+ List<SubViewBar.View> views = List.of(
+ new SubViewBar.View("Errors", !diagramShown, true, () ->
pressKey('d')),
+ new SubViewBar.View("Diagram", diagramShown, diagramShown ||
selectedErrorHasHistory(), () -> pressKey('d')));
+ List<SubViewBar.Toggle> toggles = new ArrayList<>();
+ if (!diagramShown) {
+ toggles.add(new SubViewBar.Toggle("f", "handled", handledFilter));
+ }
+ if (historyDiagram) {
+ toggles.add(new SubViewBar.Toggle("n", "description",
diagram.isShowDescription() ? "on" : "off"));
+ }
+ if (!diagramShown || historyDiagram) {
+ toggles.add(new SubViewBar.Toggle("w", "wrap", wordWrap ? "on" :
"off"));
+ }
+ return new SubViewBar.Spec("d", views, toggles, false);
+ }
+
@Override
public void renderFooter(List<Span> spans) {
if (diagram.isShowDiagram()) {
@@ -415,9 +438,7 @@ class ErrorsTab extends AbstractTableTab {
hint(spans, TuiIcons.HINT_NAV, "navigate");
hint(spans, "Enter", "drill-down");
hint(spans, "i", infoLabel);
- hint(spans, "n", "description" +
(diagram.isShowDescription() ? " [on]" : ""));
hintShowBhpv(spans, showBody, showHeaders, showProperties,
showVariables);
- hintLast(spans, "w", "wrap" + (wordWrap ? " [on]" : "
[off]"));
} else {
hint(spans, "d", "close");
hint(spans, "Esc", "back");
@@ -425,9 +446,7 @@ class ErrorsTab extends AbstractTableTab {
hint(spans, TuiIcons.HINT_H, "h-scroll");
hint(spans, "t", "topology");
hint(spans, "i", infoLabel);
- hint(spans, "n", "description" +
(diagram.isShowDescription() ? " [on]" : ""));
hintShowBhpv(spans, showBody, showHeaders, showProperties,
showVariables);
- hintLast(spans, "w", "wrap" + (wordWrap ? " [on]" : "
[off]"));
}
return;
}
@@ -441,10 +460,7 @@ class ErrorsTab extends AbstractTableTab {
}
hint(spans, "Home/End", "top/end");
hint(spans, "s", "sort");
- hint(spans, "d", "diagram");
- hint(spans, "f", "handled [" + handledFilter + "]");
hintShowBhpv(spans, showBody, showHeaders, showProperties,
showVariables);
- hint(spans, "w", "wrap [" + (wordWrap ? "on" : "off") + "]");
}
/** How often this kind of error happened, and how long it has been going
on (CAMEL-24911). */
@@ -750,6 +766,18 @@ class ErrorsTab extends AbstractTableTab {
// ---- Diagram ----
+ /** Whether the selected error has the message history its diagram is
drawn from. */
+ private boolean selectedErrorHasHistory() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || info.errors.isEmpty()) {
+ return false;
+ }
+ List<ErrorInfo> sorted = applyFilter(info.errors);
+ Integer sel = tableState.selected();
+ ErrorInfo selected = sel != null && sel >= 0 && sel < sorted.size() ?
sorted.get(sel) : null;
+ return selected != null && selected.messageHistory != null &&
selected.messageHistory.length > 0;
+ }
+
private void loadDiagramForSelectedError() {
if (ctx.selectedPid == null || ctx.runner == null) {
return;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index 083ef5512c1d..4b6fed1c60ac 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -791,6 +791,46 @@ class HistoryTab extends AbstractTab {
}
}
+ @Override
+ public SubViewBar.Spec subViewBar() {
+ if (ctx.findSelectedIntegration() == null) {
+ return null;
+ }
+ boolean diagramShown = diagram.isShowDiagram();
+ boolean historyDiagram = diagramShown && diagram.isHistoryMode() &&
diagram.hasHistoryData();
+ boolean tracerActive = !traces.get().isEmpty();
+ boolean exchangeList = tracerActive && !traceDetailView;
+ boolean waterfallApplies = !exchangeList;
+ boolean waterfall = !diagramShown && showWaterfall && waterfallApplies;
+ List<SubViewBar.View> views = List.of(
+ new SubViewBar.View(exchangeList ? "Exchanges" : "Steps",
!diagramShown && !waterfall, true, () -> {
+ if (diagram.isShowDiagram()) {
+ pressKey('d');
+ }
+ if (showWaterfall) {
+ pressKey('g');
+ }
+ }),
+ new SubViewBar.View("Waterfall", waterfall, waterfallApplies,
() -> {
+ if (diagram.isShowDiagram()) {
+ pressKey('d');
+ }
+ if (!showWaterfall) {
+ pressKey('g');
+ }
+ }),
+ new SubViewBar.View("Diagram", diagramShown, true, () ->
pressKey('d')));
+ List<SubViewBar.Toggle> toggles = new ArrayList<>();
+ if (!diagramShown || historyDiagram) {
+ toggles.add(new SubViewBar.Toggle("n", "description",
showDescription ? "on" : "off"));
+ }
+ boolean traceWrap = historyDiagram ? !diagramTraceSteps.isEmpty() :
tracerActive;
+ if (historyDiagram || !diagramShown && !waterfall && !exchangeList) {
+ toggles.add(new SubViewBar.Toggle("w", "wrap", (traceWrap ?
traceWordWrap : historyWordWrap) ? "on" : "off"));
+ }
+ return new SubViewBar.Spec(null, views, toggles, false);
+ }
+
@Override
public void renderFooter(List<Span> spans) {
if (diagram.isShowDiagram()) {
@@ -800,7 +840,6 @@ class HistoryTab extends AbstractTab {
boolean sh = isTraceMode ? showTraceHeaders :
showHistoryHeaders;
boolean sp = isTraceMode ? showTraceProperties :
showHistoryProperties;
boolean sv = isTraceMode ? showTraceVariables :
showHistoryVariables;
- boolean sw = isTraceMode ? traceWordWrap : historyWordWrap;
String infoLabel = switch (infoPanelSize) {
case INFO_WIDE -> "info [wide]";
case INFO_FULL -> "info [full]";
@@ -811,9 +850,7 @@ class HistoryTab extends AbstractTab {
hint(spans, TuiIcons.HINT_NAV, "navigate");
hint(spans, "Enter", "drill-down");
hint(spans, "i", infoLabel);
- hint(spans, "n", "description" + (showDescription ? "
[on]" : ""));
hintShowBhpv(spans, sb, sh, sp, sv);
- hintLast(spans, "w", "wrap" + (sw ? " [on]" : " [off]"));
} else {
hint(spans, "d", "close");
hint(spans, "Esc", "back");
@@ -821,9 +858,7 @@ class HistoryTab extends AbstractTab {
hint(spans, TuiIcons.HINT_H, "h-scroll");
hint(spans, "t", "topology");
hint(spans, "i", infoLabel);
- hint(spans, "n", "description" + (showDescription ? "
[on]" : ""));
hintShowBhpv(spans, sb, sh, sp, sv);
- hintLast(spans, "w", "wrap" + (sw ? " [on]" : " [off]"));
}
return;
}
@@ -839,17 +874,14 @@ class HistoryTab extends AbstractTab {
if (!showWaterfall && !traceWordWrap) {
hint(spans, TuiIcons.HINT_H, "h-scroll");
}
- hint(spans, "n", "description" + (showDescription ? " [on]" : ""));
- hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : ""));
+ hint(spans, "g", "waterfall");
hint(spans, "d", "diagram");
if (!showWaterfall) {
hintShowBhpv(spans, showTraceBody, showTraceHeaders,
showTraceProperties, showTraceVariables);
}
- hintLast(spans, "w", "wrap" + (traceWordWrap ? " [on]" : "
[off]"));
} else if (tracerActive) {
hint(spans, "Esc", "back");
hint(spans, "s", "sort");
- hint(spans, "n", "description" + (showDescription ? " [on]" : ""));
hint(spans, "d", "diagram");
hint(spans, "Enter", "details");
hintLast(spans, "F5", "refresh");
@@ -861,12 +893,10 @@ class HistoryTab extends AbstractTab {
if (!showWaterfall && !historyWordWrap) {
hint(spans, TuiIcons.HINT_H, "h-scroll");
}
- hint(spans, "n", "description" + (showDescription ? " [on]" : ""));
- hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : ""));
+ hint(spans, "g", "waterfall");
hint(spans, "d", "diagram");
if (!showWaterfall) {
hintShowBhpv(spans, showHistoryBody, showHistoryHeaders,
showHistoryProperties, showHistoryVariables);
- hint(spans, "w", "wrap" + (historyWordWrap ? " [on]" : "
[off]"));
}
hintLast(spans, "F5", "refresh");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
index b53942844fc5..b7a465333646 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
@@ -226,6 +226,31 @@ class HttpTab extends AbstractTableTab {
renderDetail(frame, chunks.get(1), visible);
}
+ @Override
+ public SubViewBar.Spec subViewBar() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ return null;
+ }
+ List<HttpEndpointInfo> visible = sortedVisibleEndpoints(info);
+ Integer sel = tableState.selected();
+ HttpEndpointInfo selected = sel != null && sel >= 0 && sel <
visible.size() ? visible.get(sel) : null;
+ boolean table = !probe.isActive() && !showSpec;
+ List<SubViewBar.View> views = List.of(
+ new SubViewBar.View("Endpoints", table, true,
this::handleEscape),
+ new SubViewBar.View("Probe", probe.isActive(), table &&
selected != null, this::enterProbeModeFromTable),
+ new SubViewBar.View(
+ "Spec", showSpec, table && selected != null &&
selected.specificationUri != null,
+ this::loadSpecForSelectedEndpoint));
+ List<SubViewBar.Toggle> toggles = new ArrayList<>();
+ if (table) {
+ String[] filterLabels = { "all", "rest", "http" };
+ toggles.add(new SubViewBar.Toggle("f", "filter",
filterLabels[filter]));
+ toggles.add(new SubViewBar.Toggle("m", "management",
showManagement ? "on" : "off"));
+ }
+ return new SubViewBar.Spec(null, views, toggles, false);
+ }
+
@Override
public void renderFooter(List<Span> spans) {
if (probe.isActive()) {
@@ -240,9 +265,6 @@ class HttpTab extends AbstractTableTab {
hint(spans, "Esc", "back");
hint(spans, "Enter", "probe");
hint(spans, "s", "sort");
- String[] filterLabels = { "all", "rest", "http" };
- hint(spans, "f", "filter [" + filterLabels[filter] + "]");
- hint(spans, "m", "management" + (showManagement ? " [on]" : " [off]"));
List<HttpEndpointInfo> hVisible =
sortedVisibleEndpoints(ctx.findSelectedIntegration());
Integer hSel = tableState.selected();
if (hSel != null && hSel >= 0 && hSel < hVisible.size() &&
hVisible.get(hSel).specificationUri != null) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
index 564dfdd58608..24488ece1d90 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
@@ -1175,6 +1175,23 @@ class MemoryLeakTab extends AbstractTab {
};
}
+ @Override
+ public SubViewBar.Spec subViewBar() {
+ if (state != State.HAS_RESULTS) {
+ return null;
+ }
+ String modeLabel = recordingMode == RecordingMode.DUAL ? "dual" :
"single";
+ List<SubViewBar.View> views = List.of(
+ new SubViewBar.View("Samples", !dominatorView, true, () ->
pressKey('v')),
+ new SubViewBar.View("Dominators", dominatorView, true, () ->
pressKey('v')));
+ List<SubViewBar.Toggle> toggles = new ArrayList<>();
+ if (comparisons == null && !dominatorView) {
+ toggles.add(new SubViewBar.Toggle("m", "min-size",
MIN_SIZE_LABELS[minSizeIndex]));
+ }
+ toggles.add(new SubViewBar.Toggle("d", "mode", modeLabel));
+ return new SubViewBar.Spec("v", views, toggles, false);
+ }
+
@Override
public void renderFooter(List<Span> spans) {
String modeLabel = recordingMode == RecordingMode.DUAL ? "dual" :
"single";
@@ -1195,11 +1212,8 @@ class MemoryLeakTab extends AbstractTab {
hint(spans, "Esc", "back");
if (comparisons == null && !dominatorView) {
hint(spans, "s", "sort");
- hint(spans, "m", "min-size [" +
MIN_SIZE_LABELS[minSizeIndex] + "]");
}
- hint(spans, "v", dominatorView ? "samples" : "dominators");
hint(spans, "r", "new recording");
- hint(spans, "d", "mode [" + modeLabel + "]");
hint(spans, "+/-", "duration [" + duration + "s]");
hint(spans, "h", "heap dump");
hintLast(spans, "PgUp/Dn", "detail");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
index 88457d728c6d..1733228dbd60 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
@@ -612,6 +612,32 @@ class MetricsTab extends AbstractTableTab {
});
}
+ @Override
+ public SubViewBar.Spec subViewBar() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ return null;
+ }
+ boolean raw = findMetricsUrl(info) != null;
+ List<SubViewBar.View> views = List.of(
+ new SubViewBar.View("Dashboard", !showRaw && !tableMode, true,
() -> {
+ handleEscape();
+ if (tableMode) {
+ pressKey('d');
+ }
+ }),
+ new SubViewBar.View("Table", !showRaw && tableMode, true, ()
-> {
+ handleEscape();
+ if (!tableMode) {
+ pressKey('d');
+ }
+ }),
+ new SubViewBar.View("Prometheus", showRaw, raw, () ->
pressKey('r')));
+ List<SubViewBar.Toggle> toggles = !showRaw && tableMode
+ ? List.of(new SubViewBar.Toggle("f", "filter", filterType)) :
List.of();
+ return new SubViewBar.Spec("d", views, toggles, false);
+ }
+
@Override
public void renderFooter(List<Span> spans) {
if (showRaw) {
@@ -621,10 +647,8 @@ class MetricsTab extends AbstractTableTab {
return;
}
hint(spans, "Esc", "back");
- hint(spans, "d", tableMode ? "dashboard" : "table");
if (tableMode) {
hint(spans, "s", "sort");
- hint(spans, "f", "filter:" + filterType);
}
IntegrationInfo info = ctx.findSelectedIntegration();
if (info != null && findMetricsUrl(info) != null) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
index cd62efd24b7f..7aff33679d51 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
@@ -56,10 +56,25 @@ interface MonitorTab {
void render(Frame frame, Rect area);
/**
- * The view settings of the tab and their state, as key hints with {@code
[state]}: shown in the tab itself (the
- * Diagram tab's level bar) rather than the footer, and listed with the
footer actions for MCP agents.
+ * The views of the tab and its view settings, shown as a bar on the first
row of the tab (the levels of the Diagram
+ * tab, the views of the Inspect tab); null for a tab without views. The
settings shown there are left out of the
+ * footer.
+ */
+ default SubViewBar.Spec subViewBar() {
+ return null;
+ }
+
+ /**
+ * The view settings of the tab and their state, as key hints with {@code
[state]}: shown in the tab's view bar
+ * rather than the footer, and listed with the footer actions for MCP
agents.
*/
default void renderViewToggles(List<Span> spans) {
+ SubViewBar.Spec spec = subViewBar();
+ if (spec != null) {
+ for (SubViewBar.Toggle t : spec.toggles()) {
+ TuiHelper.hint(spans, t.key(), t.label() + " [" + t.state() +
"]");
+ }
+ }
}
default void renderFooter(List<Span> spans) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 3a3b6d807bb1..8b3cc0321f01 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -869,6 +869,38 @@ class RoutesTab extends AbstractTab {
.build();
}
+ @Override
+ public SubViewBar.Spec subViewBar() {
+ if (ctx.findSelectedIntegration() == null || sourceViewer.isVisible())
{
+ return null;
+ }
+ boolean diagramShown = diagram.isShowDiagram();
+ List<SubViewBar.View> views = List.of(
+ new SubViewBar.View("Routes", !diagramShown, true, () -> {
+ // Esc goes back a level at a time: from a route's diagram
to the topology, then to the table
+ for (int i = 0; i < 3 && diagram.isShowDiagram(); i++) {
+ handleEscape();
+ }
+ }),
+ new SubViewBar.View("Diagram", diagramShown, true, () ->
handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER))));
+ List<SubViewBar.Toggle> toggles = new ArrayList<>();
+ if (diagramShown) {
+ toggles.add(new SubViewBar.Toggle("m", "metrics", diagramMetrics ?
"on" : "off"));
+ if (topologyMode) {
+ toggles.add(new SubViewBar.Toggle(
+ "e", "external",
+ EXTERNAL_LABELS[externalMode].trim().replace("[",
"").replace("]", "")));
+ } else if (!diagram.getEipNodeBoxes().isEmpty()) {
+ toggles.add(new SubViewBar.Toggle("d", "detail", detailMode ?
"on" : "off"));
+ }
+ toggles.add(new SubViewBar.Toggle("n", "description",
diagram.isShowDescription() ? "on" : "off"));
+ } else {
+ toggles.add(new SubViewBar.Toggle("n", "description",
showDescription ? "on" : "off"));
+ toggles.add(new SubViewBar.Toggle("t", "top", routeTopMode ? "on"
: "off"));
+ }
+ return new SubViewBar.Spec(null, views, toggles, false);
+ }
+
@Override
public void renderFooter(List<Span> spans) {
if (sourceViewer.isVisible()) {
@@ -878,7 +910,6 @@ class RoutesTab extends AbstractTab {
hint(spans, "Esc", "back");
hint(spans, "t", "topology");
hint(spans, TuiIcons.HINT_NAV, "navigate");
- hint(spans, "d", "detail" + (detailMode ? " [on]" : " [off]"));
hint(spans, "g", "go to");
hint(spans, "PgUp/PgDn", detailMode ? "detail" : "page");
hint(spans, "c", "source");
@@ -896,17 +927,10 @@ class RoutesTab extends AbstractTab {
} else {
diagram.renderFooterHints(spans);
}
- hint(spans, "m", "metrics" + (diagramMetrics ? " [on]" : "
[off]"));
- if (topologyMode) {
- hint(spans, "e", "external" + EXTERNAL_LABELS[externalMode]);
- }
- hint(spans, "n", "description" + (diagram.isShowDescription() ? "
[on]" : " [off]"));
} else {
hint(spans, "Esc", "back");
hint(spans, "Enter", "diagram");
hint(spans, "s", "sort");
- hint(spans, "n", "description" + (showDescription ? " [on]" : "
[off]"));
- hint(spans, "t", routeTopMode ? "top [on]" : "top [off]");
if (!routeTopMode) {
hint(spans, "c", "source");
String routeState = selectedRouteState();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
new file mode 100644
index 000000000000..4867202f8045
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.CharWidth;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.widgets.paragraph.Paragraph;
+
+/**
+ * The views of a tab as one line above its content, with the view settings at
the right: Architecture › Topology ›
+ * Route in the Diagram tab, History │ Waterfall │ Diagram in the Inspect tab.
The view shown is highlighted, the others
+ * are dim but visible so the user knows they exist, and a view that does not
apply yet is greyed out. A click on a view
+ * goes there; a click on a setting presses its key, as a click on a footer
hint does.
+ */
+final class SubViewBar {
+
+ /**
+ * One view of a tab.
+ *
+ * @param label what the bar shows
+ * @param active whether it is the view shown
+ * @param enabled whether it applies now (a route view needs a route)
+ * @param select goes to the view; null when it cannot be chosen by a
click
+ */
+ record View(String label, boolean active, boolean enabled, Runnable
select) {
+ }
+
+ /** A view setting: its key, name and state (on, off, or a mode such as a
filter value). */
+ record Toggle(String key, String label, String state) {
+
+ boolean on() {
+ return !"off".equals(state);
+ }
+ }
+
+ /**
+ * What a tab shows in the bar.
+ *
+ * @param cycleKey the key that moves through the views, or null
+ * @param levels whether the views are levels of one another (separated
by ›) rather than peers (│)
+ */
+ record Spec(String cycleKey, List<View> views, List<Toggle> toggles,
boolean levels) {
+ }
+
+ private record Hit(int from, int to, View view, String key) {
+ }
+
+ private final List<Hit> hits = new ArrayList<>();
+ private int row = -1;
+
+ /** Draws the bar in a one-row area. */
+ void render(Frame frame, Rect area, Spec spec) {
+ hits.clear();
+ row = area.y();
+ List<Span> spans = new ArrayList<>();
+ spans.add(Span.raw(" "));
+ int x = area.x() + 1;
+ if (spec.cycleKey() != null) {
+ String key = " " + spec.cycleKey() + " ";
+ spans.add(Span.styled(key, Theme.hintKey()));
+ spans.add(Span.raw(" "));
+ x += CharWidth.of(key) + 1;
+ }
+ String separator = spec.levels() ? " › " : " │ ";
+ List<View> views = spec.views();
+ for (int i = 0; i < views.size(); i++) {
+ View v = views.get(i);
+ if (i > 0) {
+ spans.add(Span.styled(separator, Theme.muted()));
+ x += CharWidth.of(separator);
+ }
+ String text = (v.active() ? "◆ " : "◇ ") + v.label();
+ Style style = v.active() ? Theme.accentBg()
+ : v.enabled() ? Style.EMPTY.fg(Theme.baseFg()) :
Theme.muted().dim();
+ String shown = v.active() ? " " + text + " " : text;
+ spans.add(Span.styled(shown, style));
+ int w = CharWidth.of(shown);
+ if (v.enabled() && !v.active() && v.select() != null) {
+ hits.add(new Hit(x, x + w, v, null));
+ }
+ x += w;
+ }
+ int used = x - area.x();
+ // the settings at the right, as many as fit
+ List<Span> right = new ArrayList<>();
+ List<int[]> rightBoxes = new ArrayList<>();
+ List<String> rightKeys = new ArrayList<>();
+ int rightWidth = 0;
+ for (Toggle t : spec.toggles()) {
+ // an on/off setting shows its state as a mark, a setting with
modes names the mode
+ boolean onOff = "on".equals(t.state()) || "off".equals(t.state());
+ String chip = " " + t.key() + " ";
+ String label = " " + t.label() + (onOff ? "" : ": " + t.state()) +
" ";
+ String mark = (onOff ? (t.on() ? "●" : "○") : "") + " ";
+ int w = CharWidth.of(chip) + CharWidth.of(label) +
CharWidth.of(mark);
+ if (used + rightWidth + w + 2 > area.width()) {
+ break;
+ }
+ right.add(Span.styled(chip, Theme.hintKey()));
+ right.add(Span.raw(label));
+ right.add(Span.styled(mark, t.on() ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted()));
+ rightBoxes.add(new int[] { rightWidth, rightWidth + w - 2 });
+ rightKeys.add(t.key());
+ rightWidth += w;
+ }
+ if (!right.isEmpty()) {
+ int start = area.x() + area.width() - rightWidth;
+ spans.add(Span.raw(" ".repeat(area.width() - used - rightWidth)));
+ spans.addAll(right);
+ for (int i = 0; i < rightBoxes.size(); i++) {
+ hits.add(new Hit(start + rightBoxes.get(i)[0], start +
rightBoxes.get(i)[1], null, rightKeys.get(i)));
+ }
+ }
+
frame.renderWidget(Paragraph.builder().text(Text.from(Line.from(spans))).build(),
area);
+ }
+
+ /** The view under a click on the bar, or null. */
+ View viewAt(int x, int y) {
+ Hit h = hitAt(x, y);
+ return h != null ? h.view() : null;
+ }
+
+ /** The key of the setting under a click on the bar, or null. */
+ String keyAt(int x, int y) {
+ Hit h = hitAt(x, y);
+ return h != null ? h.key() : null;
+ }
+
+ /** Whether the bar was drawn on the row: a click there is the bar's, even
between its items. */
+ boolean isOnRow(int y) {
+ return row >= 0 && y == row;
+ }
+
+ /** Forgets where the bar was, when a tab without one is shown. */
+ void clear() {
+ hits.clear();
+ row = -1;
+ }
+
+ private Hit hitAt(int x, int y) {
+ if (y != row) {
+ return null;
+ }
+ for (Hit h : hits) {
+ if (x >= h.from() && x < h.to()) {
+ return h;
+ }
+ }
+ return null;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLevelBarTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLevelBarTest.java
deleted file mode 100644
index bb31eff1471d..000000000000
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLevelBarTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.camel.dsl.jbang.core.commands.tui;
-
-import java.util.List;
-
-import dev.tamboui.buffer.Buffer;
-import dev.tamboui.layout.Rect;
-import dev.tamboui.terminal.Frame;
-import org.apache.camel.dsl.jbang.core.commands.tui.DiagramLevelBar.Level;
-import org.apache.camel.dsl.jbang.core.commands.tui.DiagramLevelBar.Segment;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * The Diagram tab's level bar (CAMEL-25147): every level shown, the current
one marked, the path in the labels, and a
- * click goes to a level unless it does not apply.
- */
-class DiagramLevelBarTest {
-
- @Test
- void showsAllLevelsAndTheCurrentOne() {
- DiagramLevelBar bar = new DiagramLevelBar();
- Rect area = new Rect(0, 0, 160, 1);
- Buffer buffer = Buffer.empty(area);
- bar.render(Frame.forTesting(buffer), area, List.of(
- new Segment(Level.ARCHITECTURE, "Architecture", true),
- new Segment(Level.TOPOLOGY, "Topology: Order intake", true),
- new Segment(Level.ROUTE, "Route", false)), Level.TOPOLOGY,
- List.of(new DiagramLevelBar.Toggle("b", "view", "business"),
- new DiagramLevelBar.Toggle("e", "external", "edges"),
- new DiagramLevelBar.Toggle("m", "metrics", "off")));
- String line = TuiTestHelper.bufferToString(buffer);
- assertTrue(line.contains("◇ Architecture"), line);
- assertTrue(line.indexOf(" v ") >= 0 && line.indexOf(" v ") <
line.indexOf("Architecture"),
- "the key that moves through the levels comes first: " + line);
- assertTrue(line.contains("◆ Topology: Order intake"), "the current
level: " + line);
- assertTrue(line.contains("◇ Route"), line);
- // the view settings of the level, with their state, beside the diagram
- assertTrue(line.contains(" b view: business "), "a mode is named: " +
line);
- assertTrue(line.contains(" e external: edges "), line);
- assertTrue(line.contains(" m metrics \u25cb"), line);
-
- int capabilities = line.indexOf("Architecture");
- assertEquals(Level.ARCHITECTURE, bar.hit(capabilities, 0));
- assertEquals(Level.TOPOLOGY, bar.hit(line.indexOf("Order intake"), 0));
- assertNull(bar.hit(line.indexOf("Route"), 0), "a level that does not
apply");
- assertNull(bar.hit(capabilities, 1), "another row");
- }
-}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
index 8c766dabe5b1..d9f09dc77c34 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
@@ -193,7 +193,11 @@ class EndpointsTabRenderTest {
assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
assertTrue(footer.contains("sort"), "Footer should contain sort hint");
- assertTrue(footer.contains("filter"), "Footer should contain filter
hint");
+ // the filter is a view setting: in the view bar, with the Chart and
Detail views
+ SubViewBar.Spec bar = tab.subViewBar();
+ assertTrue(bar.toggles().stream().anyMatch(t -> t.key().equals("f") &&
t.state().equals("all")),
+ "View bar should contain the filter");
+ assertTrue(bar.views().stream().anyMatch(v ->
v.label().equals("Detail")), "View bar should list Detail");
}
// ---- Helper methods ----
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
index 30bf5da8c013..f69d33048add 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
@@ -248,8 +248,11 @@ class ErrorsTabRenderTest {
assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
assertTrue(footer.contains("sort"), "Footer should contain sort hint");
- assertTrue(footer.contains("handled"), "Footer should contain handled
filter hint");
- assertTrue(footer.contains("wrap"), "Footer should contain wrap hint");
+ // the handled filter and wrap are view settings: in the view bar,
with the Errors and Diagram views
+ SubViewBar.Spec bar = tab.subViewBar();
+ assertTrue(bar.toggles().stream().anyMatch(t -> t.key().equals("f") &&
t.label().equals("handled")),
+ "View bar should contain the handled filter");
+ assertTrue(bar.toggles().stream().anyMatch(t -> t.key().equals("w")),
"View bar should contain wrap");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTabRenderTest.java
index 069c92547083..a7dcb4357219 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTabRenderTest.java
@@ -146,7 +146,10 @@ class MetricsTabRenderTest {
.reduce("", String::concat);
assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
- assertTrue(footer.contains("d"), "Footer should contain 'd' toggle
hint");
+ // dashboard and table are views of the view bar, d moves between them
+ SubViewBar.Spec bar = tab.subViewBar();
+ assertTrue(bar != null && "d".equals(bar.cycleKey()), "View bar should
be moved through with d");
+ assertTrue(bar.views().stream().anyMatch(v ->
v.label().equals("Table")), "View bar should list Table");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
new file mode 100644
index 000000000000..7216366dc5f0
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import org.apache.camel.dsl.jbang.core.commands.tui.SubViewBar.Spec;
+import org.apache.camel.dsl.jbang.core.commands.tui.SubViewBar.Toggle;
+import org.apache.camel.dsl.jbang.core.commands.tui.SubViewBar.View;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The view bar of a tab: every view shown, the current one marked, a click
goes to a view unless it does not apply, and
+ * a click on a view setting presses its key.
+ */
+class SubViewBarTest {
+
+ private final AtomicReference<String> selected = new AtomicReference<>();
+
+ private Spec levels() {
+ return new Spec(
+ "v", List.of(
+ new View("Architecture", false, true, () ->
selected.set("architecture")),
+ new View("Topology · Order intake", true, true, () ->
selected.set("topology")),
+ new View("Route", false, false, () ->
selected.set("route"))),
+ List.of(new Toggle("b", "view", "business"), new Toggle("e",
"external", "edges"),
+ new Toggle("m", "metrics", "off")),
+ true);
+ }
+
+ private static String render(SubViewBar bar, Spec spec, int y) {
+ Rect area = new Rect(0, y, 160, 1);
+ Buffer buffer = Buffer.empty(new Rect(0, 0, 160, y + 1));
+ bar.render(Frame.forTesting(buffer), area, spec);
+ String all = TuiTestHelper.bufferToString(buffer);
+ String[] rows = all.split("\n");
+ return rows[rows.length - 1];
+ }
+
+ @Test
+ void showsAllViewsTheCurrentOneAndTheSettings() {
+ SubViewBar bar = new SubViewBar();
+ String line = render(bar, levels(), 3);
+
+ assertThat(line).contains("◇ Architecture", "◆ Topology · Order
intake", "◇ Route", " › ");
+ assertThat(line.indexOf(" v ")).isBetween(0,
line.indexOf("Architecture"));
+ // the view settings, with their state
+ assertThat(line).contains(" b view: business ", " e external: edges
", " m metrics ○");
+ }
+
+ @Test
+ void aClickGoesToAViewOrPressesTheKeyOfASetting() {
+ SubViewBar bar = new SubViewBar();
+ String line = render(bar, levels(), 3);
+
+ bar.viewAt(line.indexOf("Architecture"), 3).select().run();
+ assertThat(selected.get()).isEqualTo("architecture");
+ assertThat(bar.viewAt(line.indexOf("Order intake"), 3)).as("the view
shown").isNull();
+ assertThat(bar.viewAt(line.indexOf("Route"), 3)).as("a view that does
not apply").isNull();
+ assertThat(bar.viewAt(line.indexOf("Architecture"), 2)).as("another
row").isNull();
+
+ assertThat(bar.keyAt(line.indexOf("metrics"), 3)).isEqualTo("m");
+ assertThat(bar.keyAt(line.indexOf(" e external"), 3)).isEqualTo("e");
+ assertThat(bar.isOnRow(3)).isTrue();
+ }
+
+ @Test
+ void peerViewsAreSeparatedByABar() {
+ SubViewBar bar = new SubViewBar();
+ String line = render(bar, new Spec(
+ null, List.of(
+ new View("History", true, true, null), new
View("Waterfall", false, true, () -> {
+ })),
+ List.of(), false), 0);
+
+ assertThat(line).contains("◆ History", " │ ", "◇
Waterfall").doesNotContain(" › ");
+ }
+}