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 59b040573e10 CAMEL-23976: TUI - add percentile range bar visualization
in info panels
59b040573e10 is described below
commit 59b040573e10d3496268e9c4a20d0d66aa74e600
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Jul 10 16:02:34 2026 +0200
CAMEL-23976: TUI - add percentile range bar visualization in info panels
CAMEL-23976: TUI - streamline Routes/Overview tables and add percentile bar
to processors
- Remove COVER, AGE, INFLIGHT, SINCE-LAST columns from Routes and Overview
tables
- Merge since-last timing into TOTAL/FAIL columns as dimmed suffix
- Right-align TOTAL/FAIL numbers across rows using max-width padding
- Add inline percentile bar column after P50/P95/P99 numbers in Routes table
- Add percentile bar to Processor table for route and processor rows
- Add MSG/S column to Processor table for consistency with Routes table
- Widen MSG/S column spacing to separate from TOTAL
CAMEL-23976: TUI - right-align numbers, center headers, add percentile bar
to Overview
CAMEL-23976: TUI - widen READY column in Overview tab for spacing
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/tui/DiagramTab.java | 10 ++-
.../dsl/jbang/core/commands/tui/OverviewTab.java | 55 +++++++------
.../dsl/jbang/core/commands/tui/RoutesTab.java | 91 +++++++++++-----------
.../dsl/jbang/core/commands/tui/TuiHelper.java | 38 +++++++++
4 files changed, 123 insertions(+), 71 deletions(-)
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 90472df22908..33901d72f665 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
@@ -487,6 +487,8 @@ class DiagramTab extends AbstractTab {
lines.add(Line.from(
Span.styled(" p99: ", Theme.muted()),
Span.raw(String.format("%" + pw + "d ms",
route.p99Time))));
+ lines.add(buildPercentileBarLine(
+ route.p50Time, route.p95Time, route.p99Time,
area.width() - 3));
}
}
@@ -586,18 +588,17 @@ class DiagramTab extends AbstractTab {
Span.raw(ln.id)));
}
- lines.add(Line.from(Span.raw("")));
String linkedRoute = diagram.findLinkedRouteId(drillDownRouteId);
if (linkedRoute != null && diagram.getRouteLayout(linkedRoute) !=
null) {
+ lines.add(Line.from(Span.raw("")));
lines.add(Line.from(
Span.styled(" ↵ ", Theme.label().bold()),
Span.styled(linkedRoute,
Style.EMPTY.fg(Theme.baseFg()))));
} else if (ln.treeNode != null && ln.treeNode.info.remote) {
+ lines.add(Line.from(Span.raw("")));
String arrow = "from".equals(ln.type) ? " external → " : " →
external";
lines.add(Line.from(
Span.styled(arrow, Theme.muted())));
- } else {
- lines.add(Line.from(Span.raw("")));
}
if (ln.treeNode != null && ln.treeNode.info.stat != null) {
@@ -645,6 +646,9 @@ class DiagramTab extends AbstractTab {
lines.add(Line.from(
Span.styled(" p99: ", Theme.muted()),
Span.raw(String.format("%" + pw + "d ms",
stat.p99ProcessingTime))));
+ lines.add(buildPercentileBarLine(
+ stat.p50ProcessingTime, stat.p95ProcessingTime,
+ stat.p99ProcessingTime, area.width() - 3));
}
if (stat.lastCompletedExchangeTimestamp > 0 ||
stat.lastFailedExchangeTimestamp > 0) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index 899023dfdc25..d887f6d3b06e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -434,6 +434,10 @@ class OverviewTab extends AbstractTab {
};
} else {
boolean hasPercentiles = infos.stream().anyMatch(i -> i.p50Time >=
0);
+ long maxTotal = infos.stream().mapToLong(i ->
i.exchangesTotal).max().orElse(0);
+ long maxFailed = infos.stream().mapToLong(i ->
i.failed).max().orElse(0);
+ int tw = Math.max(numWidth(maxTotal), 6);
+ int fw = Math.max(numWidth(maxFailed), 6);
for (IntegrationInfo info : infos) {
boolean isEven = (rowIndex++ % 2 == 0);
Style rowBg = isEven ? Style.EMPTY.bg(Theme.zebra()) :
Style.EMPTY;
@@ -451,8 +455,6 @@ class OverviewTab extends AbstractTab {
Cell.from(Span.styled("", dimStyle)),
Cell.from(Span.styled("", dimStyle)),
Cell.from(Span.styled(TuiIcons.STOPPED + "
Stopped", Theme.error().dim())),
- Cell.from(Span.styled(info.ago != null ? info.ago
: "", dimStyle)),
- Cell.from(Span.styled("", dimStyle)),
Cell.from(Span.styled("", dimStyle)),
Cell.from(Span.styled("", dimStyle)),
Cell.from(Span.styled("", dimStyle)),
@@ -476,8 +478,6 @@ class OverviewTab extends AbstractTab {
Style failStyle = info.failed > 0 ? Theme.error().bold() :
Style.EMPTY;
- String sinceLastDisplay = formatSinceLast(info);
-
boolean hasDoc = info.readmeFiles != null &&
!info.readmeFiles.isEmpty();
if (!hasDoc) {
hasDoc = hasReadmeInSourceDir(info);
@@ -511,20 +511,27 @@ class OverviewTab extends AbstractTab {
} else {
timingCol = "";
}
+ Line totalCell = info.sinceLastCompleted != null
+ ? Line.from(Span.raw(String.format("%" + tw + "d",
info.exchangesTotal)),
+ Span.styled(" (" + info.sinceLastCompleted
+ ")", Theme.muted()))
+ : Line.from(Span.raw(String.format("%" + tw + "d",
info.exchangesTotal)));
+ Line failCell = info.sinceLastFailed != null
+ ? Line.from(Span.styled(String.format("%" + fw +
"d", info.failed), failStyle),
+ Span.styled(" (" + info.sinceLastFailed +
")", Theme.muted()))
+ : Line.from(Span.styled(String.format("%" + fw +
"d", info.failed), failStyle));
+
rows.add(Row.from(
Cell.from(info.pid),
Cell.from(nameLine),
Cell.from(info.camelVersion != null ?
info.camelVersion : ""),
- centerCell(info.ready != null ? info.ready : "",
5),
+ centerCell(info.ready != null ? info.ready : "",
7),
Cell.from(Span.styled(stateText, statusStyle)),
- Cell.from(info.ago != null ? info.ago : ""),
rightCell(info.routeStarted + "/" +
info.routeTotal, 7),
rightCell(throughputDisplay != null ?
throughputDisplay : "", 8),
- rightCell(String.valueOf(info.exchangesTotal), 8),
- rightCell(String.valueOf(info.failed), 6,
failStyle),
- rightCell(String.valueOf(info.inflight), 8),
+ Cell.from(totalCell),
+ Cell.from(failCell),
rightCell(timingCol, 14),
- Cell.from(sinceLastDisplay)).style(rowBg));
+ Cell.from(buildPercentileBarLine(info.p50Time,
info.p95Time, info.p99Time, 10))).style(rowBg));
}
}
@@ -533,31 +540,27 @@ class OverviewTab extends AbstractTab {
Cell.from(Span.styled(sortLabel("PID", "pid"),
sortStyle("pid"))),
Cell.from(Span.styled(sortLabel("NAME", "name"),
sortStyle("name"))),
Cell.from(Span.styled(sortLabel("VERSION", "version"),
sortStyle("version"))),
- centerCell("READY", 5, Style.EMPTY.bold()),
+ centerCell("READY", 7, Style.EMPTY.bold()),
Cell.from(Span.styled(sortLabel("STATUS", "status"),
sortStyle("status"))),
- Cell.from(Span.styled("AGE", Style.EMPTY.bold())),
rightCell("ROUTE", 7, Style.EMPTY.bold()),
rightCell("MSG/S", 8, Style.EMPTY.bold()),
- rightCell(sortLabel("TOTAL", "total"), 8,
sortStyle("total")),
- rightCell(sortLabel("FAIL", "fail"), 6, sortStyle("fail")),
- rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
+ centerCell(sortLabel("TOTAL", "total"), 14,
sortStyle("total")),
+ centerCell(sortLabel("FAIL", "fail"), 10,
sortStyle("fail")),
rightCell(timingHeader, 14, Style.EMPTY.bold()),
- Cell.from(Span.styled("SINCE-LAST", Style.EMPTY.bold())));
+ Cell.from(""));
widths = new Constraint[] {
Constraint.length(8),
Constraint.fill(),
Constraint.length(16),
- Constraint.length(5),
+ Constraint.length(7),
Constraint.length(10),
- Constraint.length(8),
Constraint.length(7),
Constraint.length(8),
- Constraint.length(8),
- Constraint.length(6),
- Constraint.length(8),
Constraint.length(14),
- Constraint.length(13)
+ Constraint.length(10),
+ Constraint.length(14),
+ Constraint.fill()
};
}
@@ -1153,6 +1156,14 @@ class OverviewTab extends AbstractTab {
return sortStyle(column, topSort);
}
+ private static int numWidth(long... values) {
+ long max = 0;
+ for (long v : values) {
+ max = Math.max(max, Math.abs(v));
+ }
+ return Math.max(String.valueOf(max).length(), 1);
+ }
+
private int infraSortCompare(InfraInfo a, InfraInfo b) {
if (a.vanishing != b.vanishing) {
return a.vanishing ? 1 : -1;
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 ce3a7251eaab..975fb02b21e5 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
@@ -648,6 +648,11 @@ class RoutesTab extends AbstractTab {
} else {
boolean hasPercentiles = sortedRoutes.stream().anyMatch(r ->
r.p50Time >= 0);
+ long maxTotal = sortedRoutes.stream().mapToLong(r ->
r.total).max().orElse(0);
+ long maxFailed = sortedRoutes.stream().mapToLong(r ->
r.failed).max().orElse(0);
+ int tw = Math.max(numWidth(maxTotal), 6);
+ int fw = Math.max(numWidth(maxFailed), 6);
+
List<Row> routeRows = new ArrayList<>();
for (RouteInfo route : sortedRoutes) {
Style stateStyle = "Started".equals(route.state)
@@ -658,8 +663,6 @@ class RoutesTab extends AbstractTab {
? Theme.error().bold()
: Style.EMPTY;
- String sinceLastRoute = formatSinceLastRoute(route);
-
String timingCol;
if (hasPercentiles && route.p50Time >= 0) {
timingCol = route.p50Time + "/" + route.p95Time + "/" +
route.p99Time;
@@ -669,18 +672,24 @@ class RoutesTab extends AbstractTab {
timingCol = "";
}
+ Line totalCell = route.sinceLastCompleted != null
+ ? Line.from(Span.raw(String.format("%" + tw + "d",
route.total)),
+ Span.styled(" (" + route.sinceLastCompleted +
")", Theme.muted()))
+ : Line.from(Span.raw(String.format("%" + tw + "d",
route.total)));
+ Line failCell = route.sinceLastFailed != null
+ ? Line.from(Span.styled(String.format("%" + fw + "d",
route.failed), failStyle),
+ Span.styled(" (" + route.sinceLastFailed +
")", Theme.muted()))
+ : Line.from(Span.styled(String.format("%" + fw + "d",
route.failed), failStyle));
+
routeRows.add(Row.from(
Cell.from(Span.styled(route.routeId != null ?
route.routeId : "", Style.EMPTY.fg(Theme.accent()))),
Cell.from(routeFromLabel(route)),
Cell.from(Span.styled(route.state != null ?
route.state : "", stateStyle)),
- Cell.from(route.uptime != null ? route.uptime : ""),
- rightCell(route.coverage != null ? route.coverage :
"", 6),
rightCell(route.throughput != null ? route.throughput
: "", 8),
- rightCell(String.valueOf(route.total), 8),
- rightCell(String.valueOf(route.failed), 6, failStyle),
- rightCell(String.valueOf(route.inflight), 8),
+ Cell.from(totalCell),
+ Cell.from(failCell),
rightCell(timingCol, 14),
- Cell.from(sinceLastRoute)));
+ Cell.from(buildPercentileBarLine(route.p50Time,
route.p95Time, route.p99Time, 10))));
}
IntegrationInfo selDef = ctx.findSelectedIntegration();
@@ -696,15 +705,12 @@ class RoutesTab extends AbstractTab {
Cell.from(Span.styled("TOTAL", ts)),
Cell.from(""),
Cell.from(""),
- Cell.from(""),
- Cell.from(""),
rightCell(selDef.throughput != null ?
selDef.throughput : "", 8, ts),
- rightCell(String.valueOf(selDef.exchangesTotal), 8,
ts),
- rightCell(String.valueOf(selDef.failed), 6,
- selDef.failed > 0 ? Theme.error().bold() : ts),
- rightCell(String.valueOf(selDef.inflight), 8, ts),
+ Cell.from(Span.styled(String.format("%" + tw + "d",
selDef.exchangesTotal), ts)),
+ Cell.from(Span.styled(String.format("%" + fw + "d",
selDef.failed),
+ selDef.failed > 0 ? Theme.error().bold() :
ts)),
rightCell(totalTimingCol, 14, ts),
- Cell.from("")));
+ Cell.from(buildPercentileBarLine(selDef.p50Time,
selDef.p95Time, selDef.p99Time, 10))));
}
String timingHeader = hasPercentiles ? "P50/P95/P99" :
"MIN/MAX/MEAN";
@@ -715,26 +721,20 @@ class RoutesTab extends AbstractTab {
Cell.from(Span.styled(routeSortLabel("ROUTE",
"name"), routeSortStyle("name"))),
Cell.from(Span.styled(routeSortLabel("FROM",
"from"), routeSortStyle("from"))),
Cell.from(Span.styled(routeSortLabel("STATUS",
"status"), routeSortStyle("status"))),
- Cell.from(Span.styled("AGE", Style.EMPTY.bold())),
- rightCell("COVER", 6, Style.EMPTY.bold()),
rightCell("MSG/S", 8, Style.EMPTY.bold()),
- rightCell(routeSortLabel("TOTAL", "total"), 8,
routeSortStyle("total")),
- rightCell(routeSortLabel("FAIL", "failed"), 6,
routeSortStyle("failed")),
- rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
+ centerCell(routeSortLabel("TOTAL", "total"), 14,
routeSortStyle("total")),
+ centerCell(routeSortLabel("FAIL", "failed"), 14,
routeSortStyle("failed")),
rightCell(timingHeader, 14, Style.EMPTY.bold()),
- Cell.from(Span.styled("SINCE-LAST",
Style.EMPTY.bold()))))
+ Cell.from("")))
.widths(
Constraint.length(24),
Constraint.fill(),
Constraint.length(10),
- Constraint.length(8),
- Constraint.length(6),
- Constraint.length(8),
- Constraint.length(8),
- Constraint.length(6),
- Constraint.length(8),
+ Constraint.length(10),
Constraint.length(14),
- Constraint.length(13))
+ Constraint.length(14),
+ Constraint.length(14),
+ Constraint.length(12))
.highlightStyle(Theme.selectionBg())
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
@@ -960,6 +960,8 @@ class RoutesTab extends AbstractTab {
lines.add(Line.from(
Span.styled(" p99: ", Theme.muted()),
Span.raw(String.format("%" + pw + "d ms",
route.p99Time))));
+ lines.add(buildPercentileBarLine(
+ route.p50Time, route.p95Time, route.p99Time,
area.width() - 3));
}
}
@@ -1055,18 +1057,17 @@ class RoutesTab extends AbstractTab {
Span.raw(ln.id)));
}
- lines.add(Line.from(Span.raw("")));
String linkedRoute = diagram.findLinkedRouteId(drillDownRouteId);
if (linkedRoute != null && diagram.getRouteLayout(linkedRoute) !=
null) {
+ lines.add(Line.from(Span.raw("")));
lines.add(Line.from(
Span.styled(" ↵ ", Theme.label().bold()),
Span.styled(linkedRoute,
Style.EMPTY.fg(Theme.baseFg()))));
} else if (ln.treeNode != null && ln.treeNode.info.remote) {
+ lines.add(Line.from(Span.raw("")));
String arrow = "from".equals(ln.type) ? " external → " : " →
external";
lines.add(Line.from(
Span.styled(arrow, Theme.muted())));
- } else {
- lines.add(Line.from(Span.raw("")));
}
if (ln.treeNode != null && ln.treeNode.info.stat != null) {
@@ -1114,6 +1115,9 @@ class RoutesTab extends AbstractTab {
lines.add(Line.from(
Span.styled(" p99: ", Style.EMPTY.dim()),
Span.raw(String.format("%" + pw + "d ms",
stat.p99ProcessingTime))));
+ lines.add(buildPercentileBarLine(
+ stat.p50ProcessingTime, stat.p95ProcessingTime,
+ stat.p99ProcessingTime, area.width() - 3));
}
if (stat.lastCompletedExchangeTimestamp > 0 ||
stat.lastFailedExchangeTimestamp > 0) {
@@ -1272,13 +1276,13 @@ class RoutesTab extends AbstractTab {
rows.add(Row.from(
Cell.from(" route"),
Cell.from(Span.styled(route.from != null ? route.from :
route.routeId, routeStyle)),
- Cell.from(""), Cell.from(""), Cell.from(""), Cell.from(""),
+ rightCell(route.throughput != null ? route.throughput :
"", 8),
rightCell(String.valueOf(route.total), 8),
rightCell(String.valueOf(route.failed), 6,
route.failed > 0 ? Theme.error() : Style.EMPTY),
rightCell(String.valueOf(route.inflight), 8),
rightCell(routeTimingCol, 14),
- Cell.from("")));
+ Cell.from(buildPercentileBarLine(route.p50Time,
route.p95Time, route.p99Time, 10))));
for (ProcessorInfo proc : route.processors) {
String indent = " ".repeat(proc.level);
@@ -1296,13 +1300,13 @@ class RoutesTab extends AbstractTab {
rows.add(Row.from(
Cell.from(" " + (proc.processor != null ?
proc.processor : "")),
Cell.from(Span.styled(indent + (proc.id != null ?
proc.id : ""), nameStyle)),
- Cell.from(""), Cell.from(""), Cell.from(""),
Cell.from(""),
+ Cell.from(""),
rightCell(String.valueOf(proc.total), 8),
rightCell(String.valueOf(proc.failed), 6,
proc.failed > 0 ? Theme.error() : Style.EMPTY),
rightCell(String.valueOf(proc.inflight), 8),
rightCell(procTimingCol, 14),
- Cell.from("")));
+ Cell.from(buildPercentileBarLine(proc.p50Time,
proc.p95Time, proc.p99Time, 10))));
}
String procTimingHeader = hasProcPercentiles ? "P50/P95/P99" :
"MIN/MAX/MEAN";
@@ -1311,7 +1315,7 @@ class RoutesTab extends AbstractTab {
.header(Row.from(
Cell.from(Span.styled(" TYPE",
Style.EMPTY.bold())),
Cell.from(Span.styled("PROCESSOR",
Style.EMPTY.bold())),
- Cell.from(""), Cell.from(""), Cell.from(""),
Cell.from(""),
+ rightCell("MSG/S", 8, Style.EMPTY.bold()),
rightCell("TOTAL", 8, Style.EMPTY.bold()),
rightCell("FAIL", 6, Style.EMPTY.bold()),
rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
@@ -1324,11 +1328,8 @@ class RoutesTab extends AbstractTab {
Constraint.length(8),
Constraint.length(6),
Constraint.length(8),
- Constraint.length(8),
- Constraint.length(6),
- Constraint.length(8),
Constraint.length(14),
- Constraint.length(13))
+ Constraint.length(12))
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
.title(" Processors [" + route.routeId + "] ")
.build())
@@ -1707,11 +1708,9 @@ class RoutesTab extends AbstractTab {
- **ROUTE** — Unique route identifier (e.g., `timer-to-log`,
`seda-consumer`)
- **FROM** — The endpoint that triggers this route (e.g.,
`timer`, `kafka`, `file`). This is the source of messages
- **STATUS** — Route state: `Started` (running), `Stopped`
(not running), or `Suspended` (paused, can be resumed)
- - **COVER** — Node coverage: what percentage of route nodes
have processed at least one message. Shows as `5/10` meaning 5 of 10 nodes were
reached. Helps find dead code paths — nodes that are defined but never reached.
100% coverage means all branches in the route have been exercised
- **MSG/S** — Current message throughput (messages per second)
for this route
- **TOTAL** — Total exchanges processed by this route since
startup
- **FAIL** — Exchanges that ended with an unhandled error in
this route
- - **INFLIGHT** — Exchanges currently being processed by this
route
- **MIN** — Fastest exchange processing time in milliseconds.
This is the time from when the exchange entered the route until it completed
- **MEAN** — Average exchange processing time in milliseconds.
A rising MEAN may indicate a downstream service getting slower
- **MAX** — Slowest exchange processing time in milliseconds.
A very high MAX compared to MEAN suggests occasional slow outliers
@@ -1740,10 +1739,10 @@ class RoutesTab extends AbstractTab {
## Example Screen
```
- ROUTE FROM STATUS COVER MSG/S
TOTAL FAIL INFLIGHT P50/P95/P99 SINCE-LAST
- timer-to-log timer://hello?p=2000 Started 5/5 0.50
142 0 0 1/10/31 1s
- timer-to-seda timer://pump?p=3000 Started 2/2 0.33
95 0 0 0/1/2 2s
- seda-consumer seda://queue Started 1/1 0.33
95 0 0 0/0/1 2s
+ ROUTE FROM STATUS MSG/S TOTAL
FAIL P50/P95/P99 SINCE-LAST
+ timer-to-log timer://hello?p=2000 Started 0.50 142
0 1/10/31 ███▒▒▒▒▒░ 1s
+ timer-to-seda timer://pump?p=3000 Started 0.33 95
0 0/1/2 ▒░░░░░░░░ 2s
+ seda-consumer seda://queue Started 0.33 95
0 0/0/1 2s
```
## Top Mode
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
index 240c57e12334..beebc406e1ff 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
@@ -599,6 +599,44 @@ final class TuiHelper {
return "█".repeat(len);
}
+ static Line buildPercentileBarLine(long p50, long p95, long p99, int
barWidth) {
+ if (p99 <= 0 || barWidth <= 1) {
+ return Line.from(Span.raw(""));
+ }
+ int segments = (p50 > 0 ? 1 : 0) + (p95 > p50 ? 1 : 0) + (p99 > p95 ?
1 : 0);
+ if (segments < 2) {
+ return Line.from(Span.raw(""));
+ }
+ int greenWidth = (int) Math.round((double) p50 / p99 * barWidth);
+ int yellowWidth = (int) Math.round((double) (p95 - p50) / p99 *
barWidth);
+ int redWidth = barWidth - greenWidth - yellowWidth;
+ if (greenWidth <= 0 && p50 > 0) {
+ greenWidth = 1;
+ }
+ if (yellowWidth <= 0 && p95 > p50) {
+ yellowWidth = 1;
+ }
+ if (redWidth <= 0 && p99 > p95) {
+ redWidth = 1;
+ }
+ int total = greenWidth + yellowWidth + redWidth;
+ if (total > barWidth) {
+ redWidth = Math.max(0, barWidth - greenWidth - yellowWidth);
+ }
+ List<Span> spans = new ArrayList<>();
+ spans.add(Span.raw(" "));
+ if (greenWidth > 0) {
+ spans.add(Span.styled("█".repeat(greenWidth), Theme.success()));
+ }
+ if (yellowWidth > 0) {
+ spans.add(Span.styled("▒".repeat(yellowWidth), Theme.warning()));
+ }
+ if (redWidth > 0) {
+ spans.add(Span.styled("░".repeat(redWidth), Theme.error()));
+ }
+ return Line.from(spans);
+ }
+
static String shortTypeName(String type) {
if (type == null) {
return "null";