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 8b88859eca01 chore: camel-jbang - Timeline: room around the panels,
and taller charts and strips on a tall screen (#29457)
8b88859eca01 is described below
commit 8b88859eca01da73d7475f0210143dcbefb3d411
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 11:05:43 2026 +0100
chore: camel-jbang - Timeline: room around the panels, and taller charts
and strips on a tall screen (#29457)
* chore: camel-jbang - Timeline: room around the panels, and taller charts
and strips on a tall screen
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../ROOT/pages/camel-jbang-tui-observe.adoc | 4 +-
.../dsl/jbang/core/commands/tui/TimelineTab.java | 228 +++++++++++++++------
.../src/main/resources/tui/help/timeline.md | 2 +
.../core/commands/tui/TimelineTabRenderTest.java | 73 +++++++
4 files changed, 238 insertions(+), 69 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index c4b256c593d8..e72d184e60fc 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -53,8 +53,8 @@ camel-chaos, the faults of each chaos rule are a layer of
their own, marked ๐ฒ
apart. When no exchange failed, the WARN and ERROR lines of the log are the
cause instead (`log ERROR`, `log WARN`),
read from the log file of the integration: something that went wrong outside
an exchange, such as a consumer that
cannot connect. When nothing went wrong the band is one line.
-* *Routes* has a line per route. The height is the exchanges done that second,
and the colour the worst that happened to
-them: completed, recovered by redelivery, handled by the error handler (such
as the dead letter channel), or failed. A
+* *Routes* has a line per route (on a tall screen two rows high, with room
around the panels and taller charts). The
+height is the exchanges done that second, and the colour the worst that
happened to them: completed, recovered by redelivery, handled by the error
handler (such as the dead letter channel), or failed. A
second a chaos rule had the route down is shaded, and a mark says what
happened to the route then: `โถ` started, `โ `
stopped, `โป` reloaded, `โ` removed. A `โ` is a second a circuit breaker of the
route was open (the Circuit Breaker EIP
with camel-resilience4j or camel-microprofile-fault-tolerance, or the
throttling exception route policy), and the route
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
index 6875f44e8452..3de12c0904b1 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
@@ -25,6 +25,7 @@ import java.util.function.LongSupplier;
import dev.tamboui.layout.Constraint;
import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Padding;
import dev.tamboui.layout.Rect;
import dev.tamboui.style.Style;
import dev.tamboui.terminal.Frame;
@@ -62,6 +63,12 @@ class TimelineTab extends AbstractTableTab {
private static final int DETAIL_LINES = DETAIL_ROWS + 5;
private static final int SUFFIX = 30;
private static final int MAX_LABEL = 18;
+ // a tall screen (this many rows to spare) has room around the panels, and
its charts and strips grow
+ static final int ROOMY_ROWS = 12;
+ // the rows the room around takes: the padding of both panels, a blank
line in each, and the line between them
+ private static final int ROOMY_EXTRA = 9;
+ private static final int MAX_CHART_ROWS = 6;
+ private static final int MAX_STRIP_ROWS = 2;
private final LongSupplier clock;
// the seconds a column has, and the characters a column is wide, as the
last render worked them out
@@ -72,7 +79,11 @@ class TimelineTab extends AbstractTableTab {
private long pausedAt = -1;
// the column of the cursor while paused
private int cursor = -1;
- private int routeLinesY = -1;
+ // where the strips of the routes are, for the clicks: the first line, the
lines from one route to the next, the
+ // lines of a strip
+ private int routeTop = -1;
+ private int routePitch = 1;
+ private int routeStrip = 1;
private final TimelineExchanges drill = new TimelineExchanges();
TimelineTab(MonitorContext ctx) {
@@ -285,10 +296,50 @@ class TimelineTab extends AbstractTableTab {
@Override
public boolean handleMouseEvent(MouseEvent me, Rect area) {
- if (routeLinesY < 0 || lastTableArea == null) {
+ if (routeTop < 0 || !me.isClick() || lastTableArea == null ||
!lastTableArea.contains(me.x(), me.y())) {
return false;
}
- return handleTableClick(me, lastTableArea, tableState, getRowCount());
+ // a click on a strip of a route selects it, not on the blank line
between two routes
+ int dy = me.y() - routeTop;
+ int i = dy / routePitch;
+ if (dy >= 0 && dy % routePitch < routeStrip && i < getRowCount()) {
+ tableState.select(i);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * How the screen is laid out: compact, or on a tall screen with room
around the panels, the charts this many rows
+ * high, the strips of the routes this many rows high and a blank line
between them.
+ */
+ record Room(boolean roomy, int causeRows, int detailRows, int strip,
boolean spaced) {
+ static final Room COMPACT = new Room(false, CAUSE_ROWS, DETAIL_ROWS,
1, false);
+ }
+
+ /**
+ * The room the screen has: the rows to spare go first to the room around
the panels, then a blank line between the
+ * routes, then taller strips, and the rest to the charts (a line kept
free at the bottom).
+ */
+ static Room room(int height, int routes, boolean anyCause) {
+ int compact = (anyCause ? CAUSE_ROWS + 1 : 1) + routes + 4 +
DETAIL_LINES + 2;
+ int spare = height - compact;
+ if (spare < ROOMY_ROWS) {
+ return Room.COMPACT;
+ }
+ spare -= ROOMY_EXTRA + 1;
+ boolean spaced = routes > 1 && spare >= routes - 1;
+ if (spaced) {
+ spare -= routes - 1;
+ }
+ int strip = 1;
+ if (routes > 0) {
+ strip = Math.min(MAX_STRIP_ROWS, 1 + spare / routes);
+ spare -= (strip - 1) * routes;
+ }
+ int cause = anyCause ? Math.min(MAX_CHART_ROWS - CAUSE_ROWS, spare /
2) : 0;
+ int detail = Math.min(MAX_CHART_ROWS - DETAIL_ROWS, spare - cause);
+ return new Room(true, CAUSE_ROWS + cause, DETAIL_ROWS + detail, strip,
spaced);
}
int seconds() {
@@ -323,20 +374,27 @@ class TimelineTab extends AbstractTableTab {
List<Timeline.RouteSeries> routes = routes(info);
List<Timeline.CauseSeries> causes = ctx.timeline.causes(info.pid,
endSecond(), seconds());
boolean anyCause = causes.stream().anyMatch(c -> sum(c.values()) > 0);
- int causeHeight = anyCause ? CAUSE_ROWS + 1 : 1;
- // the cause, the legend of the routes, a line per route and the time
axis, in a border
- int detailHeight = DETAIL_LINES + 2;
- int topHeight = Math.max(6, Math.min(area.height() - detailHeight,
causeHeight + routes.size() + 4));
+ Room room = room(area.height(), routes.size(), anyCause);
+ // on a tall screen: padding in the panels, a blank line after the
cause, after the legend and in the detail,
+ // and a line between the panels
+ int gap = room.roomy() ? 1 : 0;
+ int causeHeight = anyCause ? room.causeRows() + 1 : 1;
+ int routeHeight = routes.size() * room.strip() + (room.spaced() ?
Math.max(0, routes.size() - 1) : 0);
+ // the cause, the legend of the routes, a strip per route and the time
axis, in a border
+ int detailHeight = DETAIL_LINES + 2 + room.detailRows() - DETAIL_ROWS
+ 4 * gap;
+ int topHeight = Math.max(6,
+ Math.min(area.height() - detailHeight - gap, causeHeight +
Math.max(1, routeHeight) + 4 + 4 * gap));
List<Rect> rows = Layout.vertical()
- .constraints(Constraint.length(topHeight),
Constraint.length(detailHeight), Constraint.fill())
+ .constraints(Constraint.length(topHeight),
Constraint.length(gap), Constraint.length(detailHeight),
+ Constraint.fill())
.split(area);
- int inner = Math.max(20, area.width() - 2);
+ int inner = Math.max(20, area.width() - 2 - 2 * gap);
int label = Math.min(MAX_LABEL,
Math.max(11, routes.stream().mapToInt(r ->
r.route().length()).max().orElse(0))) + 4;
// a second is 1 to 3 characters wide as the screen allows, or a
character is several seconds
- int room = Math.max(10, inner - label - SUFFIX - 1);
- int per = room >= seconds() ? 1 : (int) Math.ceil((double) seconds() /
room);
- width = per == 1 ? Math.max(1, Math.min(3, room / seconds())) : 1;
+ int space = Math.max(10, inner - label - SUFFIX - 1);
+ int per = space >= seconds() ? 1 : (int) Math.ceil((double) seconds()
/ space);
+ width = per == 1 ? Math.max(1, Math.min(3, space / seconds())) : 1;
this.per = per;
int columns = (seconds() + per - 1) / per * width;
if (cursor >= 0) {
@@ -345,42 +403,60 @@ class TimelineTab extends AbstractTableTab {
}
List<Line> lines = new ArrayList<>();
- renderCauses(lines, causes, anyCause, info, columns, per, label);
- boolean anyOpen = routes.stream().anyMatch(r -> r.total(r.open()) > 0);
- lines.add(Line.from(Span.raw(pad("", label)), Span.styled("Routes ",
Theme.label()),
- Span.styled("โ", Theme.success()), Span.raw(" completed "),
- Span.styled("โ", Theme.warning()), Span.raw(" recovered "),
- Span.styled("โ", handledStyle()), Span.raw(" handled "),
- Span.styled("โ", Theme.error()), Span.raw(" failed "),
- Span.styled("โ", Theme.error()), Span.raw(" down"),
- Span.raw(anyOpen ? " " : ""), Span.styled(anyOpen ? "โ" : "",
Theme.notice()),
- Span.raw(anyOpen ? " circuit breaker open" : ""),
- Span.raw(routes.stream().anyMatch(r ->
Arrays.stream(r.events()).anyMatch(e -> e != null))
- ? " โถ started โ stopped โป reloaded" : "")));
+ renderCauses(lines, causes, anyCause, info, columns, room.causeRows(),
label);
+ blank(lines, room.roomy());
+ lines.add(legend(routes, label));
+ blank(lines, room.roomy());
Integer sel = tableState.selected();
- routeLinesY = lines.size();
+ int routeLinesY = lines.size();
for (int i = 0; i < routes.size(); i++) {
- lines.add(routeLine(routes.get(i), sel != null && sel == i,
columns, per, label));
+ blank(lines, room.spaced() && i > 0);
+ lines.addAll(routeLines(routes.get(i), sel != null && sel == i,
columns, per, label, room.strip()));
}
if (routes.isEmpty()) {
lines.add(Line.from(Span.styled(pad("", label) + "no routes yet",
Theme.muted())));
}
lines.add(axis(columns, per, label));
Rect top = rows.get(0);
- // the rows of the routes, as a table with a header line of two would
have them, for the clicks
- lastTableArea = new Rect(top.x(), top.y() + 1 + routeLinesY - 2,
top.width(), routes.size() + 2);
-
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block(title(info))).build(),
top);
+ // the lines of the routes, for the clicks
+ routeTop = top.y() + 1 + gap + routeLinesY;
+ routePitch = room.strip() + (room.spaced() ? 1 : 0);
+ routeStrip = room.strip();
+ lastTableArea = new Rect(top.x(), routeTop, top.width(), Math.max(1,
routeHeight));
+ Padding padding = room.roomy() ? Padding.uniform(1) : Padding.NONE;
+
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block(title(info),
padding)).build(), top);
if (drill.isOpen()) {
List<Timeline.Kept> kept = keptAtCursor();
-
frame.renderWidget(Paragraph.builder().text(Text.from(drill.lines(kept,
DETAIL_LINES)))
- .block(block(drill.title(kept, ctx.timeline.done(info.pid,
drill.from(), drill.to())))).build(),
- rows.get(1));
+
frame.renderWidget(Paragraph.builder().text(Text.from(drill.lines(kept,
detailHeight - 2 - 2 * gap)))
+ .block(block(drill.title(kept, ctx.timeline.done(info.pid,
drill.from(), drill.to())), padding))
+ .build(),
+ rows.get(2));
return;
}
Timeline.RouteSeries selected = sel != null && sel >= 0 && sel <
routes.size() ? routes.get(sel) : null;
- renderDetail(frame, rows.get(1), selected, columns, per, label, info);
+ renderDetail(frame, rows.get(2), selected, columns, label, info, room);
+ }
+
+ private static void blank(List<Line> lines, boolean when) {
+ if (when) {
+ lines.add(Line.from(Span.raw("")));
+ }
+ }
+ /** The legend of the strips of the routes. */
+ private static Line legend(List<Timeline.RouteSeries> routes, int label) {
+ boolean anyOpen = routes.stream().anyMatch(r -> r.total(r.open()) > 0);
+ return Line.from(Span.raw(pad("", label)), Span.styled("Routes ",
Theme.label()),
+ Span.styled("โ", Theme.success()), Span.raw(" completed "),
+ Span.styled("โ", Theme.warning()), Span.raw(" recovered "),
+ Span.styled("โ", handledStyle()), Span.raw(" handled "),
+ Span.styled("โ", Theme.error()), Span.raw(" failed "),
+ Span.styled("โ", Theme.error()), Span.raw(" down"),
+ Span.raw(anyOpen ? " " : ""), Span.styled(anyOpen ? "โ" : "",
Theme.notice()),
+ Span.raw(anyOpen ? " circuit breaker open" : ""),
+ Span.raw(routes.stream().anyMatch(r ->
Arrays.stream(r.events()).anyMatch(e -> e != null))
+ ? " โถ started โ stopped โป reloaded" : ""));
}
private String title(IntegrationInfo info) {
@@ -406,7 +482,7 @@ class TimelineTab extends AbstractTableTab {
private void renderCauses(
List<Line> lines, List<Timeline.CauseSeries> causes, boolean
anyCause, IntegrationInfo info, int columns,
- int per, int label) {
+ int rows, int label) {
List<Span> legend = new ArrayList<>();
legend.add(Span.raw(pad("", label)));
legend.add(Span.styled("Errors/s ", Theme.label()));
@@ -433,7 +509,7 @@ class TimelineTab extends AbstractTableTab {
return;
}
lines.add(Line.from(legend));
- lines.addAll(stacked(values, styles, CAUSE_ROWS, columns, label));
+ lines.addAll(stacked(values, styles, rows, columns, label));
}
private static Style causeStyle(int i) {
@@ -445,12 +521,12 @@ class TimelineTab extends AbstractTableTab {
// ---- the routes ----
- private Line routeLine(Timeline.RouteSeries r, boolean selected, int
columns, int per, int label) {
- List<Span> spans = new ArrayList<>();
+ /**
+ * The strip of a route, {@code strip} rows high (the blocks stacked, so a
strip of 2 rows has 16 levels), its name
+ * and its summary on the bottom row, where the blocks stand.
+ */
+ private List<Line> routeLines(Timeline.RouteSeries r, boolean selected,
int columns, int per, int label, int strip) {
String name = r.route().length() > label - 3 ? r.route().substring(0,
label - 4) + "โฆ" : r.route();
- spans.add(selected
- ? Span.styled(pad(">> " + name, label), Theme.selectionBg())
- : Span.raw(pad(" " + name, label)));
long[] completed = cols(r.completed(), false);
long[] recovered = cols(r.recovered(), false);
long[] handled = cols(r.handled(), false);
@@ -463,32 +539,47 @@ class TimelineTab extends AbstractTableTab {
max = Math.max(max, total(completed[c], recovered[c], handled[c],
failed[c]));
}
String[] events = eventsOf(r.events(), per);
+ List<List<Span>> rows = new ArrayList<>();
+ for (int row = 0; row < strip; row++) {
+ boolean bottom = row == strip - 1;
+ List<Span> spans = new ArrayList<>();
+ String text = bottom ? (selected ? ">> " : " ") + name : "";
+ spans.add(selected ? Span.styled(pad(text, label),
Theme.selectionBg()) : Span.raw(pad(text, label)));
+ rows.add(spans);
+ }
for (int c = 0; c < columns; c++) {
long t = total(completed[c], recovered[c], handled[c], failed[c]);
String event = c / width < events.length && c % width == 0 ?
events[c / width] : null;
- if (event != null) {
- // what happened to the route then, in place of its exchanges
- spans.add(eventMark(event));
- } else if (down[c] > 0) {
- spans.add(Span.styled("โ", Theme.error()));
- } else if (open[c] > 0) {
- // the circuit breaker was open (half open dim): the calls
went to the fallback, or failed fast
- spans.add(Span.styled("โ", open[c] >= 2 ? Theme.notice() :
Theme.muted()));
- } else if (completed[c] < 0) {
- spans.add(Span.raw(" "));
- } else if (t == 0) {
- spans.add(Span.styled("ยท", Theme.muted()));
- } else {
- int level = (int) Math.max(1, Math.min(8, Math.ceil(t * 8.0 /
max)));
- Style style = failed[c] > 0 ? Theme.error()
- : handled[c] > 0 ? handledStyle()
- : recovered[c] > 0 || attempts[c] > 0 ?
Theme.warning() : Theme.success();
- spans.add(Span.styled(String.valueOf(BLOCKS.charAt(level -
1)), style));
+ Style style = failed[c] > 0 ? Theme.error()
+ : handled[c] > 0 ? handledStyle()
+ : recovered[c] > 0 || attempts[c] > 0 ? Theme.warning() :
Theme.success();
+ int level = t == 0 ? 0 : (int) Math.max(1, Math.min(8L * strip,
Math.ceil(t * 8.0 * strip / max)));
+ for (int row = 0; row < strip; row++) {
+ boolean bottom = row == strip - 1;
+ Span cell;
+ if (event != null) {
+ // what happened to the route then, in place of its
exchanges
+ cell = bottom ? eventMark(event) : Span.raw(" ");
+ } else if (down[c] > 0) {
+ cell = Span.styled("โ", Theme.error());
+ } else if (open[c] > 0) {
+ // the circuit breaker was open (half open dim): the calls
went to the fallback, or failed fast
+ cell = Span.styled("โ", open[c] >= 2 ? Theme.notice() :
Theme.muted());
+ } else if (completed[c] < 0) {
+ cell = Span.raw(" ");
+ } else if (t == 0) {
+ cell = bottom ? Span.styled("ยท", Theme.muted()) :
Span.raw(" ");
+ } else {
+ int here = Math.max(0, Math.min(8, level - (strip - 1 -
row) * 8));
+ cell = here == 0 ? Span.raw(" ") :
Span.styled(String.valueOf(BLOCKS.charAt(here - 1)), style);
+ }
+ rows.get(row).add(cell);
}
}
- spans.add(Span.raw(" "));
- spans.addAll(summary(r));
- return Line.from(spans);
+ List<Span> last = rows.get(strip - 1);
+ last.add(Span.raw(" "));
+ last.addAll(summary(r));
+ return rows.stream().map(Line::from).toList();
}
/** The event of each column of several seconds: the last in the column. */
@@ -609,18 +700,20 @@ class TimelineTab extends AbstractTableTab {
// ---- the selected route ----
private void renderDetail(
- Frame frame, Rect area, Timeline.RouteSeries r, int columns, int
per, int label, IntegrationInfo info) {
+ Frame frame, Rect area, Timeline.RouteSeries r, int columns, int
label, IntegrationInfo info, Room room) {
List<Line> lines = new ArrayList<>();
+ Padding padding = room.roomy() ? Padding.uniform(1) : Padding.NONE;
if (r == null) {
frame.renderWidget(Paragraph.builder().text(Text.from(Line.from(Span.raw(""))))
- .block(block(" Route ")).build(), area);
+ .block(block(" Route ", padding)).build(), area);
return;
}
List<long[]> values = List.of(cols(r.completed(), false),
cols(r.recovered(), false),
cols(r.handled(), false), cols(r.failed(), false));
List<Style> styles = List.of(Theme.success(), Theme.warning(),
handledStyle(), Theme.error());
- int rows = Math.max(2, Math.min(DETAIL_ROWS, area.height() - 7));
+ int rows = Math.max(2, Math.min(room.detailRows(), area.height() - 7 -
(room.roomy() ? 4 : 0)));
lines.addAll(stacked(values, styles, rows, columns, label));
+ blank(lines, room.roomy());
long[] mean = cols(r.mean(), true);
long maxMean = 0;
@@ -685,6 +778,7 @@ class TimelineTab extends AbstractTableTab {
callLine.addAll(calls);
}
lines.add(Line.from(callLine));
+ blank(lines, room.roomy());
List<Span> totals = new ArrayList<>();
totals.add(Span.raw(pad("", label)));
@@ -694,7 +788,7 @@ class TimelineTab extends AbstractTableTab {
addTotal(totals, r.total(r.failed()), "failed", Theme.error());
lines.add(Line.from(totals));
String title = " " + r.route() + " โ per second ";
-
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block(title)).build(),
area);
+
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block(title,
padding)).build(), area);
}
private static void addTotal(List<Span> spans, long count, String words,
Style style) {
@@ -823,8 +917,8 @@ class TimelineTab extends AbstractTableTab {
return s.length() >= width ? s : " ".repeat(width - s.length()) + s;
}
- private static Block block(String title) {
- return
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build();
+ private static Block block(String title, Padding padding) {
+ return
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).padding(padding).build();
}
@Override
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/timeline.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/timeline.md
index c430256bfaac..59ce27f2c213 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/timeline.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/timeline.md
@@ -40,6 +40,8 @@ When nothing went wrong, the band is one line: `Errors/s
none`.
A line per route, one character per second. The height is the number of
exchanges done that second, relative to the busiest second of the route.
+On a tall screen the lines are two rows high with a blank line between
+them, the charts are taller, and there is room around the panels.
The colour is the worst that happened to them that second:
- green: completed
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java
index 257389c061fd..ee381eead6d1 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java
@@ -22,10 +22,13 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
+import dev.tamboui.layout.Rect;
import dev.tamboui.text.Span;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.tui.event.KeyModifiers;
+import dev.tamboui.tui.event.MouseButton;
+import dev.tamboui.tui.event.MouseEvent;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import org.junit.jupiter.api.BeforeEach;
@@ -319,4 +322,74 @@ class TimelineTabRenderTest {
spans.forEach(s -> sb.append(s.content()));
return sb.toString();
}
+
+ @Test
+ void aTallScreenHasRoomAroundThePanelsAndTallerStrips() {
+ TimelineTab tab = new TimelineTab(ctx, now::get);
+ List<String> lines = TuiTestHelper.renderToString(tab, 198,
48).lines().toList();
+
+ // a line between the panels
+ int bottom = indexOf(lines, "โฐ");
+ assertThat(lines.get(bottom + 1)).isBlank();
+ assertThat(lines.get(bottom + 2)).contains("orders โ per second");
+ // a strip is 2 rows: the blocks of a busy second stand on the row
with the name
+ int orders = indexOf(lines, ">> orders");
+ assertThat(lines.get(orders - 1)).contains("โโโโ");
+ // and a blank line between the routes
+ int shipping = indexOf(lines, " shipping");
+ assertThat(shipping).isEqualTo(orders + 3);
+ assertThat(lines.get(orders + 1).replace("โ", "")).isBlank();
+ // the charts grow: the scale of the errors per second is 6 rows from
top to bottom
+ int top = indexOf(lines, "2 โค");
+ assertThat(lines.get(top + 5)).contains("0 โค");
+ }
+
+ @Test
+ void aSmallScreenStaysCompact() {
+ TimelineTab tab = new TimelineTab(ctx, now::get);
+ List<String> lines = TuiTestHelper.renderToString(tab, 160,
30).lines().toList();
+
+ int bottom = indexOf(lines, "โฐ");
+ assertThat(lines.get(bottom + 1)).contains("orders โ per second");
+ int orders = indexOf(lines, ">> orders");
+ assertThat(lines.get(orders + 1)).contains(" shipping");
+ }
+
+ @Test
+ void theRoomGoesToTheSpaceAroundThenTheRoutesThenTheCharts() {
+ assertThat(TimelineTab.room(30, 3,
true)).isEqualTo(TimelineTab.Room.COMPACT);
+ assertThat(TimelineTab.room(48, 3, true)).isEqualTo(new
TimelineTab.Room(true, 6, 6, 2, true));
+ // the routes need the rows: one row each, no blank line between them
+ TimelineTab.Room many = TimelineTab.room(48, 10, true);
+ assertThat(many.roomy()).isTrue();
+ assertThat(many.strip()).isEqualTo(1);
+ assertThat(many.spaced()).isFalse();
+ // no error: the detail has the rows the errors would have
+ assertThat(TimelineTab.room(48, 3, false).causeRows()).isEqualTo(4);
+ }
+
+ @Test
+ void aClickOnTheStripOfARouteSelectsIt() {
+ TimelineTab tab = new TimelineTab(ctx, now::get);
+ Rect area = new Rect(0, 0, 198, 48);
+ List<String> lines = TuiTestHelper.renderToString(tab, 198,
48).lines().toList();
+ int shipping = indexOf(lines, " shipping");
+
+ // the blank line between orders and shipping is no route
+ assertThat(tab.handleMouseEvent(MouseEvent.press(MouseButton.LEFT, 40,
shipping - 2), area)).isFalse();
+ // the upper row of the strip of shipping is shipping
+ assertThat(tab.handleMouseEvent(MouseEvent.press(MouseButton.LEFT, 40,
shipping - 1), area)).isTrue();
+
assertThat(tab.getTableDataAsJson().getInteger("selectedIndex")).isEqualTo(1);
+ assertThat(tab.handleMouseEvent(MouseEvent.press(MouseButton.LEFT, 40,
shipping + 3), area)).isTrue();
+
assertThat(tab.getTableDataAsJson().getInteger("selectedIndex")).isEqualTo(2);
+ }
+
+ private static int indexOf(List<String> lines, String text) {
+ for (int i = 0; i < lines.size(); i++) {
+ if (lines.get(i).contains(text)) {
+ return i;
+ }
+ }
+ throw new AssertionError("no line with " + text);
+ }
}