Skip to content

Commit a585dea

Browse files
authored
Queue metrics & health: UI polish (TRI-12068) (#4354)
**Stacked on #4131** — targets `feat/queue-metrics-and-health`, not `main`. GitHub will auto-retarget this to `main` once #4131 merges. (Targeting `main` now would wrongly include all of #4131's own commits.) UI polish for the Queue metrics & health pages (TRI-12068). 49 commits, all under `apps/webapp/app/` — no engine/ClickHouse/package changes. **Highlights** - **Layout** — consolidated queue-detail header into one filter bar; shared `MetricsLayout` spacing (2.5 scale); paused-queue banner mirroring the environment-paused banner. - **Charts** — inline legends under titles; threshold gradient coloring (Env saturation orange only >100%; Concurrency orange only at/over the limit); fullscreen legend spacing; result caching so tab switches don't reload; brighter hover cursor + full x-axis labels. - **Tables** — whole-row click via stretched link; custom Queues icon; removed column sorting; Limit-cell spacing + hover-bright; "Limited by" tooltip sizing. - **Controls** — Pause moved into the filter bar (orange label + accurate tooltip copy); restyled override-concurrency modal. - **Copy** — plain-language pass; inline colour swatches; Throttled reads "% of current period". Also consolidated the three queue-metrics `.server-changes` notes into one. **Known follow-up (not in this PR):** the Concurrency-keys table is capped at 50 keys (union of live Redis backlog + ClickHouse history, no single cursor across both) and truncates silently — fine for most queues, a real limit for high-cardinality per-tenant keys. Verified locally against the `queue-metrics-demo` seed; `pnpm typecheck --filter webapp` passes.
1 parent 0dfaec2 commit a585dea

27 files changed

Lines changed: 1252 additions & 613 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
The concurrency keys table on a queue's page is now paginated, so queues with thousands of keys can page through all of them instead of only showing the top 50.

.server-changes/queue-metrics-dashboard.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ area: webapp
33
type: feature
44
---
55

6-
Queue metrics and health on the Queues page: per-queue depth, throughput, concurrency, throttling, and scheduling-delay charts, plus a per-queue detail view, live queue stats and a backlog chart on the task detail page, and a waiting-in-queue explainer in the run inspector for runs that have not started yet. Off by default; enabled per organization.
6+
New Queue metrics & health dashboard (per-org opt-in): per-queue depth, throughput, concurrency, throttling and scheduling-delay charts, a per-queue detail view, and live queue stats. Queue concurrency limits set above the environment limit are now rejected instead of being silently capped.

.server-changes/queue-override-reject-above-limit.md

Lines changed: 0 additions & 6 deletions
This file was deleted.

.server-changes/queue-pages-live-and-clarity.md

Lines changed: 0 additions & 6 deletions
This file was deleted.

apps/webapp/app/components/layout/MetricsLayout.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ function MetricsLayoutMain({ children, scroll }: { children: ReactNode; scroll:
178178
<div
179179
className={
180180
scroll === "page"
181-
? "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"
181+
? "flex min-h-0 flex-1 flex-col gap-2.5 overflow-y-auto py-2.5 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"
182182
: "flex min-h-0 flex-1 flex-col overflow-hidden"
183183
}
184184
>
@@ -275,9 +275,21 @@ function MetricsLayoutRoot({
275275
* standard page insets. Compose left/right clusters as child divs — `justify-between` spreads them
276276
* (a single child sits at the start).
277277
*/
278-
function MetricsLayoutFilters({ children }: { children: ReactNode }) {
278+
function MetricsLayoutFilters({
279+
children,
280+
className,
281+
}: {
282+
children: ReactNode;
283+
/** Override the baked horizontal padding (the two queue pages want slightly different insets). */
284+
className?: string;
285+
}) {
279286
return (
280-
<div className="flex h-10 shrink-0 items-center justify-between gap-2 border-b border-grid-dimmed pl-2.5 pr-3">
287+
<div
288+
className={cn(
289+
"flex h-10 shrink-0 items-center justify-between gap-2 border-b border-grid-dimmed pl-2.5 pr-3",
290+
className
291+
)}
292+
>
281293
{children}
282294
</div>
283295
);
@@ -306,7 +318,7 @@ function MetricsLayoutGrid({
306318
return (
307319
<div
308320
className={cn(
309-
"grid gap-3 px-3",
321+
"grid gap-2.5 px-2.5",
310322
// `shrink-0` is load-bearing: the grid sits in Root's flex-col scroll container, where the
311323
// default flex-shrink would collapse a fixed-height row whose chart cards have ~no
312324
// intrinsic height. Pin it so the charts keep their row height and the page scrolls past.
@@ -325,8 +337,8 @@ function MetricsLayoutGrid({
325337
/**
326338
* The content region below the tiles (tabs / table / list). Full-bleed by default so a list table
327339
* spans edge to edge with its own top border; pass `inset` for a padded column (the detail page's
328-
* tabs + charts). Either way Content bakes a doubled separation above it, so the tile blocks read
329-
* as a distinct band from the content below.
340+
* tabs + charts). Separation from the tiles above comes from the scroll column's gap alone (no
341+
* extra top margin), so the tile → content step matches the gap between tile rows.
330342
*/
331343
function MetricsLayoutContent({
332344
children,
@@ -336,7 +348,7 @@ function MetricsLayoutContent({
336348
/** Pad the content into a column (page gutter) instead of letting it span edge to edge. */
337349
inset?: boolean;
338350
}) {
339-
return <div className={cn("mt-3 flex flex-col gap-3", inset && "px-3")}>{children}</div>;
351+
return <div className={cn("flex flex-col gap-2.5", inset && "px-2.5")}>{children}</div>;
340352
}
341353

342354
export const MetricsLayout = {

apps/webapp/app/components/metrics/BigNumber.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function BigNumber({
4040
typeof compactThreshold === "number" && v !== undefined && v >= compactThreshold;
4141

4242
return (
43-
<div className="flex flex-col justify-between gap-4 rounded-sm border border-grid-dimmed bg-background-bright p-4">
43+
<div className="group flex flex-col justify-between gap-4 rounded-lg border border-grid-bright bg-background-bright pb-4 pl-4 pr-3 pt-3">
4444
<div className="flex flex-wrap items-center justify-between gap-2">
4545
<Header3 className="leading-6">{title}</Header3>
4646
{accessory && <div className="shrink-0">{accessory}</div>}
@@ -56,7 +56,7 @@ export function BigNumber({
5656
) : formattedValue !== undefined ? (
5757
<div className="flex flex-wrap items-baseline gap-2">
5858
{formattedValue}
59-
{suffix && <div className={cn("text-xs", suffixClassName)}>{suffix}</div>}
59+
{suffix && <div className={cn("text-xs tabular-nums", suffixClassName)}>{suffix}</div>}
6060
</div>
6161
) : v !== undefined ? (
6262
<div className="flex flex-wrap items-baseline gap-2">
@@ -70,7 +70,7 @@ export function BigNumber({
7070
) : (
7171
formatNumber(v)
7272
)}
73-
{suffix && <div className={cn("text-xs", suffixClassName)}>{suffix}</div>}
73+
{suffix && <div className={cn("text-xs tabular-nums", suffixClassName)}>{suffix}</div>}
7474
</div>
7575
) : (
7676
"–"

apps/webapp/app/components/metrics/MiniLineChart.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export function MiniLineChart({
138138
type="monotone"
139139
dataKey="count"
140140
stroke={color}
141-
strokeWidth={1.5}
141+
strokeWidth={1}
142142
dot={false}
143143
activeDot={{ r: 2.5, fill: color, strokeWidth: 0 }}
144144
isAnimationActive={false}
@@ -148,7 +148,7 @@ export function MiniLineChart({
148148
type="monotone"
149149
dataKey="throttledOverlay"
150150
stroke="var(--color-warning)"
151-
strokeWidth={1.5}
151+
strokeWidth={1}
152152
dot={false}
153153
activeDot={{ r: 2.5, fill: "var(--color-warning)", strokeWidth: 0 }}
154154
connectNulls={false}

apps/webapp/app/components/primitives/SegmentedControl.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ type SegmentedControlProps = {
7676
variant?: VariantType;
7777
fullWidth?: boolean;
7878
onChange?: (value: string) => void;
79+
/** Override the control's outer styling (e.g. a height that matches an adjacent input). */
80+
className?: string;
7981
};
8082

8183
export default function SegmentedControl({
@@ -86,6 +88,7 @@ export default function SegmentedControl({
8688
variant = "secondary/medium",
8789
fullWidth,
8890
onChange,
91+
className,
8992
}: SegmentedControlProps) {
9093
const variantStyle = variants[variant];
9194
const _isPrimary = variant.startsWith("primary");
@@ -95,7 +98,8 @@ export default function SegmentedControl({
9598
className={cn(
9699
"flex rounded text-text-bright",
97100
variantStyle.base,
98-
fullWidth ? "w-full" : "w-fit"
101+
fullWidth ? "w-full" : "w-fit",
102+
className
99103
)}
100104
>
101105
<RadioGroup

apps/webapp/app/components/primitives/Table.tsx

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ type TableCellBasicProps = {
181181
type TableHeaderCellProps = TableCellBasicProps & {
182182
hiddenLabel?: boolean;
183183
tooltip?: ReactNode;
184+
/** Extra class merged onto the tooltip content — e.g. widen it past the default max-width. */
185+
tooltipContentClassName?: string;
184186
disableTooltipHoverableContent?: boolean;
185187
/**
186188
* When set (together with `onSort`), the header renders a sort indicator and becomes clickable.
@@ -202,6 +204,7 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
202204
colSpan,
203205
hiddenLabel = false,
204206
tooltip,
207+
tooltipContentClassName,
205208
disableTooltipHoverableContent = false,
206209
sortDirection,
207210
onSort,
@@ -226,7 +229,7 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
226229
const tooltipNode = tooltip ? (
227230
<InfoIconTooltip
228231
content={tooltip}
229-
contentClassName="normal-case tracking-normal"
232+
contentClassName={cn("normal-case tracking-normal", tooltipContentClassName)}
230233
disableHoverableContent={disableTooltipHoverableContent}
231234
/>
232235
) : null;
@@ -271,40 +274,19 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
271274
tabIndex={-1}
272275
>
273276
{sortable ? (
274-
// Order is always title → info icon → sort arrows. The info trigger is itself a <button>,
275-
// so it can't nest inside the sort <button> (invalid DOM). Without a tooltip the arrows
276-
// ride inside the full-width label button (unchanged). With a tooltip, the info sits
277-
// between the label and a separate arrows button — both buttons toggle the sort.
277+
// Only the sort arrows toggle sorting — the label (and info tooltip) are not clickable, so
278+
// clicking the header text does nothing. Order is always title → info icon → sort arrows.
278279
<div className={rowClassName}>
280+
{label}
281+
{tooltip ? tooltipNode : null}
279282
<button
280283
type="button"
281284
onClick={onSort}
282-
className={cn(
283-
"group/sort flex cursor-pointer select-none items-center gap-1 rounded-sm font-medium text-text-bright focus-custom",
284-
tooltip
285-
? undefined
286-
: cn("w-full", {
287-
"justify-center": alignment === "center",
288-
"justify-end": alignment === "right",
289-
})
290-
)}
285+
aria-label="Toggle sort"
286+
className="group/sort flex cursor-pointer select-none items-center rounded-sm focus-custom"
291287
>
292-
{label}
293-
{!tooltip && sortIndicator}
288+
{sortIndicator}
294289
</button>
295-
{tooltip ? (
296-
<>
297-
{tooltipNode}
298-
<button
299-
type="button"
300-
onClick={onSort}
301-
aria-label="Toggle sort"
302-
className="group/sort flex cursor-pointer select-none items-center rounded-sm focus-custom"
303-
>
304-
{sortIndicator}
305-
</button>
306-
</>
307-
) : null}
308290
</div>
309291
) : tooltip ? (
310292
<div className={rowClassName}>
@@ -407,19 +389,27 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
407389
// With leading/trailing content, the link is content-sized and the adornments sit beside
408390
// it (still inside the td) so interactive triggers never nest inside the <a>.
409391
leadingContent || trailingContent ? (
410-
<div className={cn(flexClasses, "gap-2")}>
411-
{leadingContent}
392+
// Stretched link: the <a> covers the whole cell via an inset ::before overlay, so the
393+
// entire cell is clickable — not just the text. The interactive adornments (tooltip
394+
// icons, badge buttons) sit above the overlay (relative z-10) and stay clickable, and
395+
// never nest inside the <a>.
396+
<div className={cn(flexClasses, "relative gap-2")}>
397+
{leadingContent ? (
398+
<span className="relative z-10 flex items-center">{leadingContent}</span>
399+
) : null}
412400
<Link
413401
to={to}
414402
className={cn(
415-
"inline-flex items-center gap-2 focus:outline-hidden",
403+
"inline-flex items-center gap-2 before:absolute before:inset-0 before:content-[''] focus:outline-hidden",
416404
actionClassName
417405
)}
418406
tabIndex={isTabbableCell ? 0 : -1}
419407
>
420408
{children}
421409
</Link>
422-
{trailingContent}
410+
{trailingContent ? (
411+
<span className="relative z-10 flex items-center">{trailingContent}</span>
412+
) : null}
423413
</div>
424414
) : (
425415
<Link

apps/webapp/app/components/primitives/Tooltip.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const TooltipContent = React.forwardRef<
4242
ref={ref}
4343
sideOffset={sideOffset}
4444
className={cn(
45-
"z-50 overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 focus-visible:outline-hidden",
45+
"z-50 max-w-[230px] overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 focus-visible:outline-hidden",
4646
variantClasses[variant],
4747
className
4848
)}

0 commit comments

Comments
 (0)