Skip to content

Commit ddae31f

Browse files
committed
refactor(webapp): flatten the run inspector queue block and tab the task detail queue chart
The run inspector's "Waiting in queue" section now leads with two borderless stat + last-30-minutes sparklines (backlog and scheduling delay), matching the inspector's divider-and-rows layout instead of nested cards. On the task detail page the queue backlog moves behind a tab in the activity card rather than sitting beside the runs-by-status chart, so it does not crowd the agent-task hero charts.
1 parent 61931ab commit ddae31f

5 files changed

Lines changed: 245 additions & 85 deletions

File tree

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export type UsageSparklineProps = {
3333
formatTotal?: (total: number) => string;
3434
/** Class for the trailing total label. */
3535
totalClassName?: string;
36+
/** Size of the bar chart. Defaults to the list-cell size (`h-6 w-28`). */
37+
chartClassName?: string;
38+
/** Hide the trailing total (e.g. when the caller shows its own headline). */
39+
hideTotal?: boolean;
3640
};
3741

3842
/**
@@ -49,6 +53,8 @@ export function UsageSparkline({
4953
total: totalOverride,
5054
formatTotal,
5155
totalClassName = "text-blue-400",
56+
chartClassName = "h-6 w-28",
57+
hideTotal = false,
5258
}: UsageSparklineProps) {
5359
const hasTotalOverride = totalOverride !== undefined;
5460
if (!data || data.length === 0 || (data.every((v) => v === 0) && !hasTotalOverride)) {
@@ -70,7 +76,7 @@ export function UsageSparkline({
7076

7177
return (
7278
<div className="flex items-start gap-2">
73-
<div className="h-6 w-28 rounded-sm">
79+
<div className={cn("rounded-sm", chartClassName)}>
7480
<ResponsiveContainer width="100%" height="100%">
7581
<BarChart data={chartData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
7682
<YAxis domain={[0, max || 1]} hide />
@@ -100,9 +106,11 @@ export function UsageSparkline({
100106
</BarChart>
101107
</ResponsiveContainer>
102108
</div>
103-
<span className={cn("-mt-1 text-xs tabular-nums", totalClassName)}>
104-
{formatTotal ? formatTotal(total) : total.toLocaleString()}
105-
</span>
109+
{hideTotal ? null : (
110+
<span className={cn("-mt-1 text-xs tabular-nums", totalClassName)}>
111+
{formatTotal ? formatTotal(total) : total.toLocaleString()}
112+
</span>
113+
)}
106114
</div>
107115
);
108116
}

apps/webapp/app/components/queues/QueueMetricCards.tsx

Lines changed: 111 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type ChartState,
77
} from "~/components/primitives/charts/ChartCompound";
88
import { ChartCard } from "~/components/primitives/charts/ChartCard";
9+
import { UsageSparkline } from "~/components/primitives/UsageSparkline";
910
import {
1011
useMetricResourceQuery,
1112
type MetricResourceTimeRange,
@@ -79,19 +80,7 @@ export function formatWaitMs(ms: number): string {
7980

8081
export type QueueMetricSeriesConfig = { key: string; label: string; color: string };
8182

82-
export function QueueMetricChartCard({
83-
title,
84-
query,
85-
series,
86-
ids,
87-
timeRange,
88-
queueName,
89-
valueFormat,
90-
fillGaps,
91-
defaultPeriod,
92-
className,
93-
}: {
94-
title: string;
83+
type QueueMetricChartProps = {
9584
query: string;
9685
series: QueueMetricSeriesConfig[];
9786
ids: QueueMetricIds;
@@ -100,8 +89,19 @@ export function QueueMetricChartCard({
10089
valueFormat?: (value: number) => string;
10190
fillGaps?: boolean;
10291
defaultPeriod?: string;
103-
className?: string;
104-
}) {
92+
};
93+
94+
// Bare chart (no card chrome) so it can live inside a shared card, e.g. a tabbed panel.
95+
export function QueueMetricChart({
96+
query,
97+
series,
98+
ids,
99+
timeRange,
100+
queueName,
101+
valueFormat,
102+
fillGaps,
103+
defaultPeriod,
104+
}: QueueMetricChartProps) {
105105
const { rows, showLoading, failed } = useQueueMetric(query, {
106106
ids,
107107
timeRange,
@@ -135,25 +135,35 @@ export function QueueMetricChartCard({
135135

136136
const state: ChartState = showLoading ? "loading" : failed ? "invalid" : undefined;
137137

138+
return (
139+
<Chart.Root
140+
config={chartConfig}
141+
data={data}
142+
dataKey="bucket"
143+
series={series.map((s) => s.key)}
144+
state={state}
145+
fillContainer
146+
>
147+
<Chart.Line
148+
lineType="monotone"
149+
xAxisProps={{ tickFormatter }}
150+
yAxisProps={valueFormat ? { tickFormatter: (v: number) => valueFormat(v) } : undefined}
151+
tooltipLabelFormatter={tooltipLabelFormatter}
152+
tooltipValueFormatter={valueFormat}
153+
/>
154+
</Chart.Root>
155+
);
156+
}
157+
158+
export function QueueMetricChartCard({
159+
title,
160+
className,
161+
...chart
162+
}: QueueMetricChartProps & { title: string; className?: string }) {
138163
return (
139164
<div className={className ?? "h-64"}>
140165
<ChartCard title={title}>
141-
<Chart.Root
142-
config={chartConfig}
143-
data={data}
144-
dataKey="bucket"
145-
series={series.map((s) => s.key)}
146-
state={state}
147-
fillContainer
148-
>
149-
<Chart.Line
150-
lineType="monotone"
151-
xAxisProps={{ tickFormatter }}
152-
yAxisProps={valueFormat ? { tickFormatter: (v: number) => valueFormat(v) } : undefined}
153-
tooltipLabelFormatter={tooltipLabelFormatter}
154-
tooltipValueFormatter={valueFormat}
155-
/>
156-
</Chart.Root>
166+
<QueueMetricChart {...chart} />
157167
</ChartCard>
158168
</div>
159169
);
@@ -203,6 +213,76 @@ export function QueueSidebarStats({
203213
);
204214
}
205215

216+
// A compact stat card with a recent trend sparkline underneath, for the run inspector.
217+
// The headline is a live "now" value from the loader; the sparkline pulls its own series.
218+
const SPARKLINE_PERIOD = "30m";
219+
220+
export function QueueSparklineStat({
221+
title,
222+
headline,
223+
headlineClassName,
224+
query,
225+
color,
226+
ids,
227+
queueName,
228+
formatPeak,
229+
}: {
230+
title: string;
231+
headline: string;
232+
headlineClassName?: string;
233+
query: string;
234+
color: string;
235+
ids: QueueMetricIds;
236+
queueName: string;
237+
formatPeak?: (peak: number) => string;
238+
}) {
239+
const timeRange: QueueMetricTimeRange = { period: SPARKLINE_PERIOD, from: null, to: null };
240+
const { rows } = useQueueMetric(query, {
241+
ids,
242+
timeRange,
243+
queueName,
244+
fillGaps: true,
245+
defaultPeriod: SPARKLINE_PERIOD,
246+
});
247+
248+
const { data, bucketStartMs, bucketIntervalMs, peak } = useMemo(() => {
249+
const points = rows
250+
.map((r) => ({ bucket: clickhouseTimeToMs(r.t), v: toNumber(r.v) }))
251+
.filter((p) => Number.isFinite(p.bucket))
252+
.sort((a, b) => a.bucket - b.bucket);
253+
return {
254+
data: points.map((p) => p.v),
255+
bucketStartMs: points[0]?.bucket,
256+
bucketIntervalMs: points.length > 1 ? points[1]!.bucket - points[0]!.bucket : undefined,
257+
peak: points.reduce((m, p) => Math.max(m, p.v), 0),
258+
};
259+
}, [rows]);
260+
261+
return (
262+
<div className="flex flex-col gap-1">
263+
<div className="flex items-baseline justify-between gap-2">
264+
<span className="text-xs uppercase tracking-wide text-text-dimmed">{title}</span>
265+
{data.length > 0 && peak > 0 ? (
266+
<span className="text-xs tabular-nums text-text-dimmed">
267+
peak {formatPeak ? formatPeak(peak) : peak.toLocaleString()}
268+
</span>
269+
) : null}
270+
</div>
271+
<div className={cn("text-xl tabular-nums text-text-bright", headlineClassName)}>
272+
{headline}
273+
</div>
274+
<UsageSparkline
275+
data={data}
276+
bucketStartMs={bucketStartMs}
277+
bucketIntervalMs={bucketIntervalMs}
278+
color={color}
279+
chartClassName="h-6 w-full"
280+
hideTotal
281+
/>
282+
</div>
283+
);
284+
}
285+
206286
export function QueueMetricStat({
207287
label,
208288
value,

apps/webapp/app/presenters/v3/RunQueueMetricsPresenter.server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type RunQueueWaiting = {
1313
delayP50Ms: number | null;
1414
delayP95Ms: number | null;
1515
loadedAt: number;
16+
ids: { organizationId: string; projectId: string; environmentId: string };
1617
concurrencyKey: {
1718
key: string;
1819
queued: number;
@@ -23,6 +24,7 @@ export type RunQueueWaiting = {
2324

2425
export type RunQueueMetrics = {
2526
queueFriendlyId: string | null;
27+
queueName: string;
2628
paused: boolean;
2729
waiting: RunQueueWaiting | null;
2830
};
@@ -64,6 +66,7 @@ export async function resolveRunQueueMetrics(options: {
6466

6567
const base: RunQueueMetrics = {
6668
queueFriendlyId: taskQueue?.friendlyId ?? null,
69+
queueName: run.queue.name,
6770
paused: taskQueue?.paused ?? false,
6871
waiting: null,
6972
};
@@ -106,6 +109,11 @@ export async function resolveRunQueueMetrics(options: {
106109
delayP50Ms: delays.p50,
107110
delayP95Ms: delays.p95,
108111
loadedAt,
112+
ids: {
113+
organizationId: project.organizationId,
114+
projectId: project.id,
115+
environmentId: environment.id,
116+
},
109117
concurrencyKey: ck
110118
? {
111119
key: ck,

0 commit comments

Comments
 (0)