Skip to content

Commit b5bca95

Browse files
committed
feat(webapp): default the queue metrics period to 1 hour and remember it
The Queues list and queue detail pages opened on a 1 day window and reset to it on every navigation. They now default to 1 hour, and the last period picked is kept in a cookie the loaders read, so the first render is already on the remembered window instead of painting the default and re-fetching. Both pages resolve the period once (URL param, then absolute range, then the remembered default) and pass it down, so the picker and every chart query agree on one value. Absolute from/to ranges are not remembered.
1 parent 6e5f0f0 commit b5bca95

6 files changed

Lines changed: 113 additions & 12 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 Queues pages now open on the last hour instead of the last day, and remember the time period you picked when you navigate between queues or reload the page.

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Header3 } from "~/components/primitives/Headers";
1515
import { Paragraph } from "~/components/primitives/Paragraph";
1616
import { InfoIconTooltip } from "~/components/primitives/Tooltip";
1717
import { useSearchParams } from "~/hooks/useSearchParam";
18+
import { QUEUE_METRICS_DEFAULT_PERIOD } from "~/components/queues/queueMetricsPeriod";
1819
import { cn } from "~/utils/cn";
1920
import { formatNumberCompact } from "~/utils/numberFormatter";
2021

@@ -34,8 +35,6 @@ export const QUEUE_METRIC_COLORS = {
3435
ckWait: "#F59E0B",
3536
};
3637

37-
export const QUEUE_METRICS_DEFAULT_PERIOD = "1d";
38-
3938
export type QueueMetricIds = {
4039
organizationId: string;
4140
projectId: string;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useEffect } from "react";
2+
3+
/**
4+
* The time window the queue-metrics pages (queues list + queue detail) use when the URL carries
5+
* no explicit period, and the memory that makes the user's last pick stick.
6+
*
7+
* The last period picked is stored in a cookie rather than localStorage so the loaders can read it
8+
* and the first render already uses the remembered window (with localStorage the page would paint
9+
* the default and then re-fetch). Absolute from/to ranges are never remembered: they'd pin later
10+
* visits to a window that goes stale.
11+
*/
12+
export const QUEUE_METRICS_DEFAULT_PERIOD = "1h";
13+
14+
const COOKIE_NAME = "queueMetricsPeriod";
15+
const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
16+
17+
/** The shape TimeFilter writes: a count plus a minute/hour/day unit (presets and custom durations). */
18+
const PERIOD_PATTERN = /^\d{1,4}[mhd]$/;
19+
20+
function isPeriod(value: string | undefined | null): value is string {
21+
return typeof value === "string" && PERIOD_PATTERN.test(value);
22+
}
23+
24+
/** Loader side: the remembered period, falling back to the default when nothing usable is stored. */
25+
export function queueMetricsPeriodFromRequest(request: Request): string {
26+
const header = request.headers.get("cookie");
27+
if (!header) return QUEUE_METRICS_DEFAULT_PERIOD;
28+
29+
for (const part of header.split(";")) {
30+
const separator = part.indexOf("=");
31+
if (separator === -1) continue;
32+
if (part.slice(0, separator).trim() !== COOKIE_NAME) continue;
33+
const value = part.slice(separator + 1).trim();
34+
return isPeriod(value) ? value : QUEUE_METRICS_DEFAULT_PERIOD;
35+
}
36+
37+
return QUEUE_METRICS_DEFAULT_PERIOD;
38+
}
39+
40+
/**
41+
* Remember the period currently in the URL so the next visit to a queue-metrics page opens on it.
42+
* Pass the raw `period` search param: an absent one (the page is on its default) or an absolute
43+
* from/to range leaves the stored value alone.
44+
*/
45+
export function useRememberQueueMetricsPeriod(period: string | undefined) {
46+
useEffect(() => {
47+
if (!isPeriod(period)) return;
48+
document.cookie = `${COOKIE_NAME}=${period}; path=/; max-age=${COOKIE_MAX_AGE_SECONDS}; samesite=lax`;
49+
}, [period]);
50+
}
51+
52+
/**
53+
* The window the page should show: an explicit period wins, an absolute range means "no period",
54+
* and everything else falls back to the remembered default the loader resolved.
55+
*/
56+
export function resolveQueueMetricsPeriod({
57+
period,
58+
from,
59+
to,
60+
defaultPeriod,
61+
}: {
62+
period: string | undefined;
63+
from: string | undefined;
64+
to: string | undefined;
65+
defaultPeriod: string;
66+
}): string | null {
67+
if (period) return period;
68+
if (from || to) return null;
69+
return defaultPeriod;
70+
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
102102
import { BigNumber } from "~/components/metrics/BigNumber";
103103
import { canAccessQueueMetricsUi } from "~/v3/canAccessQueueMetricsUi.server";
104104
import { QueueAllocationPresenter } from "~/presenters/v3/QueueAllocationPresenter.server";
105+
import {
106+
QUEUE_METRICS_DEFAULT_PERIOD,
107+
queueMetricsPeriodFromRequest,
108+
resolveQueueMetricsPeriod,
109+
useRememberQueueMetricsPeriod,
110+
} from "~/components/queues/queueMetricsPeriod";
105111

106112
const SearchParamsSchema = z.object({
107113
query: z.string().optional(),
@@ -112,8 +118,6 @@ const SearchParamsSchema = z.object({
112118
sort: z.enum(["busiest", "queued", "name"]).optional(),
113119
});
114120

115-
const QUEUE_METRICS_DEFAULT_PERIOD = "1d";
116-
117121
// The live "Queued" / "Running" header blocks poll ClickHouse on a short cadence so they stay
118122
// current after first paint. They read the env-wide gauges from env_metrics (the env-level rollup
119123
// of queue_metrics, cheapest for a dimension-free query), always over a fixed 15m window regardless
@@ -143,6 +147,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
143147
Object.fromEntries(url.searchParams)
144148
);
145149

150+
const defaultPeriod = queueMetricsPeriodFromRequest(request);
151+
146152
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
147153
if (!project) {
148154
throw new Response(undefined, {
@@ -198,7 +204,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
198204
period,
199205
from: parseFiniteInt(from),
200206
to: parseFiniteInt(to),
201-
defaultPeriod: QUEUE_METRICS_DEFAULT_PERIOD,
207+
defaultPeriod,
202208
});
203209
const queueMetrics =
204210
queueNames.length > 0
@@ -239,6 +245,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
239245
metrics,
240246
allocation,
241247
queueMetricsUiEnabled,
248+
defaultPeriod,
242249
});
243250
} catch (error) {
244251
console.error(error);
@@ -362,6 +369,7 @@ function QueuesWithMetricsView() {
362369
autoReloadPollIntervalMs,
363370
metrics,
364371
allocation,
372+
defaultPeriod,
365373
} = useTypedLoaderData<typeof loader>();
366374

367375
const metricsByQueue = metrics?.byQueue ?? {};
@@ -385,10 +393,16 @@ function QueuesWithMetricsView() {
385393
// The header tiles fetch client-side with the same period/from/to the TimeFilter writes.
386394
const { value } = useSearchParams();
387395
const timeRange = {
388-
period: value("period") ?? null,
396+
period: resolveQueueMetricsPeriod({
397+
period: value("period"),
398+
from: value("from"),
399+
to: value("to"),
400+
defaultPeriod,
401+
}),
389402
from: value("from") ?? null,
390403
to: value("to") ?? null,
391404
};
405+
useRememberQueueMetricsPeriod(value("period"));
392406

393407
useAutoRevalidate({ interval: autoReloadPollIntervalMs, onFocus: true });
394408

@@ -473,7 +487,7 @@ function QueuesWithMetricsView() {
473487
</div>
474488
<div className="flex items-center gap-1.5">
475489
<TimeFilter
476-
defaultPeriod={QUEUE_METRICS_DEFAULT_PERIOD}
490+
defaultPeriod={defaultPeriod}
477491
labelName="Period"
478492
maxPeriodDays={maxPeriodDays}
479493
shortcut={{ key: "d" }}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { ChartSyncProvider } from "~/components/primitives/charts/ChartSyncConte
2222
import { useZoomToTimeFilter } from "~/hooks/useZoomToTimeFilter";
2323
import {
2424
QUEUE_METRIC_COLORS as COLORS,
25-
QUEUE_METRICS_DEFAULT_PERIOD,
2625
QueueMetricChartCard as QueueDetailChartCard,
2726
type QueueMetricIds as Ids,
2827
type QueueMetricTimeRange as TimeRangeParams,
@@ -67,6 +66,11 @@ import {
6766
QueueOverrideConcurrencyButton,
6867
QueuePauseResumeButton,
6968
} from "~/components/queues/QueueControls";
69+
import {
70+
queueMetricsPeriodFromRequest,
71+
resolveQueueMetricsPeriod,
72+
useRememberQueueMetricsPeriod,
73+
} from "~/components/queues/queueMetricsPeriod";
7074
import { LinkButton } from "~/components/primitives/Buttons";
7175
import { RunsIcon } from "~/assets/icons/RunsIcon";
7276
import { InfoPanel } from "~/components/primitives/InfoPanel";
@@ -134,6 +138,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
134138
oldestQueuedAt: oldestQueuedAt ?? null,
135139
loadedAt: Date.now(),
136140
backPath: url.pathname.replace(/\/[^/]+$/, ""),
141+
defaultPeriod: queueMetricsPeriodFromRequest(request),
137142
ids: {
138143
organizationId: environment.organizationId,
139144
projectId: environment.projectId,
@@ -210,6 +215,7 @@ export default function Page() {
210215
loadedAt,
211216
backPath,
212217
ids,
218+
defaultPeriod,
213219
} = useTypedLoaderData<typeof loader>();
214220
const plan = useCurrentPlan();
215221
// Queue metrics are retained for 30 days in ClickHouse, so cap the picker there even for
@@ -219,10 +225,16 @@ export default function Page() {
219225

220226
const { value, replace } = useSearchParams();
221227
const timeRange: TimeRangeParams = {
222-
period: value("period") ?? null,
228+
period: resolveQueueMetricsPeriod({
229+
period: value("period"),
230+
from: value("from"),
231+
to: value("to"),
232+
defaultPeriod,
233+
}),
223234
from: value("from") ?? null,
224235
to: value("to") ?? null,
225236
};
237+
useRememberQueueMetricsPeriod(value("period"));
226238

227239
// The Concurrency keys tab exists only for queues with key activity: live keys in the
228240
// ckIndex, or nonzero CK history in the selected range (one cached scalar query decides).
@@ -283,7 +295,7 @@ export default function Page() {
283295
/>
284296
) : null}
285297
<TimeFilter
286-
defaultPeriod={QUEUE_METRICS_DEFAULT_PERIOD}
298+
defaultPeriod={defaultPeriod}
287299
labelName="Period"
288300
maxPeriodDays={maxPeriodDays}
289301
shortcut={{ key: "d" }}

apps/webapp/app/routes/resources.queues.concurrency-keys.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
22
import { z } from "zod";
33
import { timeFilterFromTo } from "~/components/runs/v3/SharedFilters";
4+
import { QUEUE_METRICS_DEFAULT_PERIOD } from "~/components/queues/queueMetricsPeriod";
45
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
56
import { findEnvironmentById, hasAccessToEnvironment } from "~/models/runtimeEnvironment.server";
67
import { requireUserId } from "~/services/session.server";
@@ -13,8 +14,7 @@ import { engine } from "~/v3/runEngine.server";
1314
// Redis (O(page), independent of total key cardinality). This replaces the old top-50 cap.
1415
export const CONCURRENCY_KEYS_PER_PAGE = 25;
1516

16-
// Matches QUEUE_METRICS_DEFAULT_PERIOD (the detail page's TimeFilter default).
17-
const DEFAULT_PERIOD = "1d";
17+
const DEFAULT_PERIOD = QUEUE_METRICS_DEFAULT_PERIOD;
1818

1919
const Body = z.object({
2020
organizationId: z.string(),

0 commit comments

Comments
 (0)