This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch quick-fix/tui-sub-view-bar in repository https://gitbox.apache.org/repos/asf/camel.git
commit dc9c5a49af0ecd9a1835c66dc406625949fef0b7 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Oct 1 14:41:17 2026 +0200 chore: camel-jbang - a view bar on the tabs with views: Inspect, Errors, Route, Endpoint, HTTP, Metrics and Memory Leak, as the Diagram tab The level bar of the Diagram tab becomes a shared SubViewBar the monitor draws on the first row of any tab that has views: the views on the left (the one shown highlighted, a view that does not apply greyed out), the view settings with their state on the right. A click on a view goes there, a click on a setting presses its key as a footer hint does; the settings shown in the bar are left out of the footer and still listed for MCP agents. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[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(" › "); + } +}
