diff --git a/apps/webapp/app/components/queues/QueueMetricCards.tsx b/apps/webapp/app/components/queues/QueueMetricCards.tsx index 98525d23e..9f26894a0 100644 --- a/apps/webapp/app/components/queues/QueueMetricCards.tsx +++ b/apps/webapp/app/components/queues/QueueMetricCards.tsx @@ -104,6 +104,20 @@ type QueueMetricChartProps = { * are config values that existed all along, so carry the first value backward instead. */ carryBackfill?: string[]; + /** Show the series legend below the chart (use for multi-series charts). */ + showLegend?: boolean; + /** + * Recolour a series' stroke above a threshold with a gradient split (colour only above the + * line). `value` sets a constant threshold; `valueFromSeries` reads a (roughly constant) + * threshold off another series — e.g. the concurrency limit. `series` targets which line is + * recoloured; the others keep their own colour. + */ + thresholdStroke?: { + aboveColor: string; + series?: string; + value?: number; + valueFromSeries?: string; + }; }; // Bare chart (no card chrome) so it can live inside a shared card, e.g. a tabbed panel. @@ -118,6 +132,8 @@ export function QueueMetricChart({ defaultPeriod, warningOverlay, carryBackfill, + showLegend, + thresholdStroke, }: QueueMetricChartProps) { const { rows, showLoading, failed } = useQueueMetric(query, { ids, @@ -163,6 +179,26 @@ export function QueueMetricChart({ [data] ); + // Resolve the threshold value: a constant, or the max of another series (e.g. the limit line, + // which is effectively constant). A gradient split then colours the target series only above it. + // `valueFromSeries` targets integer-count series (concurrency limit), so split half a unit below + // the limit — that way the line renders warning *at or above* the limit (saturated), matching + // "turns yellow at the limit", rather than only when it strictly exceeds it. + const resolvedThresholdStroke = useMemo(() => { + if (!thresholdStroke) return undefined; + let value = thresholdStroke.value; + if (value == null && thresholdStroke.valueFromSeries) { + let max = -Infinity; + for (const p of data) { + const v = Number(p[thresholdStroke.valueFromSeries]); + if (Number.isFinite(v) && v > max) max = v; + } + value = max > 0 ? max - 0.5 : undefined; + } + if (value == null || !Number.isFinite(value)) return undefined; + return { value, aboveColor: thresholdStroke.aboveColor, series: thresholdStroke.series }; + }, [thresholdStroke, data]); + const state: ChartState = showLoading ? "loading" : failed ? "invalid" : undefined; return ( @@ -173,6 +209,7 @@ export function QueueMetricChart({ series={series.map((s) => s.key)} state={state} fillContainer + showLegend={showLegend} > ); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index 9be77036b..d390a4721 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -348,6 +348,17 @@ export default function Page() { ); } +// Inline colour swatch for tooltip copy — matches the chart legend swatch (rounded-[2px]) and is +// nudged up 1px so it sits on the text baseline. +function ColorSwatch({ color }: { color: string }) { + return ( + + ); +} + function OverviewCharts({ ids, timeRange, @@ -363,7 +374,14 @@ function OverviewCharts({
+ How many runs are going at once ( color) versus + the queue's limit ( color). Turns{" "} + color when it reaches the limit. + + } + showLegend className="aspect-[2/1]" query={`SELECT timeBucket() AS t, max(max_running) AS running, max(max_limit) AS limit\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} fillGaps @@ -375,15 +393,26 @@ function OverviewCharts({ { key: "limit", label: "Limit", color: COLORS.limit }, { key: "running", label: "Running", color: COLORS.running }, ]} - // Recolour Running warning where it reaches the limit (saturated), matching the tooltip. - warningOverlay={{ series: "running", atOrAbove: "limit" }} + // Recolour Running above the limit line with a gradient split, so it's orange only where + // it's actually over the limit — not on the way up. The threshold reads off the (roughly + // constant) limit series. + thresholdStroke={{ + series: "running", + valueFromSeries: "limit", + aboveColor: "var(--color-warning)", + }} // The limit is a config value emitted only while the queue is active; back-fill its // leading zeros so the reference line doesn't start with a false 0→limit step. carryBackfill={["limit"]} /> + How many runs are waiting in this queue over time ( + color). + + } className="aspect-[2/1]" query={`SELECT timeBucket() AS t, max(max_queued) AS queued\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} fillGaps @@ -394,7 +423,14 @@ function OverviewCharts({ /> + Runs arriving (Enqueued, color) versus starting + (Started, color). Turns{" "} + color when Started falls behind. + + } + showLegend className="aspect-[2/1]" query={`SELECT timeBucket() AS t,\n deltaSumTimestampMerge(enqueue_delta) AS enqueued,\n deltaSumTimestampMerge(started_delta) AS started\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} fillGaps @@ -411,7 +447,14 @@ function OverviewCharts({ /> + How long runs wait before they start: p50 ( color), + p95 ( color), and p99 ( + color). + + } + showLegend className="aspect-[2/1]" query={`SELECT timeBucket() AS t,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[1]) AS p50,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[3]) AS p95,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[4]) AS p99\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} fillGaps @@ -427,7 +470,12 @@ function OverviewCharts({ /> + How often runs were held back by a limit ({" "} + color). + + } className="aspect-[2/1] sm:col-span-2 sm:aspect-[4/1]" query={`SELECT timeBucket() AS t, sum(throttled_count) AS throttled\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} fillGaps @@ -844,7 +892,12 @@ function KeyDrilldown({
+ This key: waiting (Queued, color) vs running ( + color). + + } className="aspect-[2/1]" query={`SELECT timeBucket() AS t, max(max_queued) AS queued, max(max_running) AS running\nFROM queue_metrics_by_key\nWHERE ${pin}\nGROUP BY t\nORDER BY t`} fillGaps