feat(webapp): default the queue metrics period to 1 hour and remember it (#4438)
## Summary
The Queues list and queue detail pages opened on a 1 day window, and
went back to it every time you navigated between queues or reloaded.
They now default to the last hour, and the period you pick is remembered
across navigations and refreshes.
## Design
The last period is stored in a `queueMetricsPeriod` cookie, written
client-side whenever a `period` lands in the URL and read by both
loaders. A cookie rather than localStorage because the queues list
renders its per-queue metrics columns server-side: with localStorage the
page would paint the 1 hour default and then re-fetch, and the picker
would flash the wrong window.
Both pages resolve the window once, in one place, and pass it down:
```ts
period: resolveQueueMetricsPeriod({
period: value("period"), // a usable period in the URL wins
from: value("from"), // an absolute range means "no period"
to: value("to"),
defaultPeriod, // otherwise the remembered default from the loader
}),
```
That keeps the picker pill and every chart query on the same value, so
no call site falls back to its own default. Periods the picker could
never produce (a hand-edited `?period=garbage`, or a window past the 30
day retention) fall back to the default, and the picker renders the
resolved window rather than the raw search param so the label can't
disagree with the data. Absolute from/to ranges, including drag-to-zoom,
are not remembered, since they would pin later visits to a window that
has gone stale.
While wiring that up: the two queue-metric queries that go straight to
ClickHouse (the list table and the concurrency-keys endpoint) never
applied the org's `queryPeriodDays` limit, so a hand-typed `?period=`
read further back than the plan allows. Everything behind
`/resources/metric` is already clipped that way by `executeQuery`; both
of these now clip with the same limit, capped at the retention window,
and the plan cap is resolved once per load and handed to the page
instead of each route deriving its own copy from the client-side
subscription.
Verified on both pages: default with no cookie is 1 hr, picking 6 hrs
survives navigating away and back to a param-free URL and a hard reload,
clearing the cookie returns to 1 hr, an oversized period falls back
without being remembered, and an absolute range still renders as a
range.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
area: webapp
|
||||
type: improvement
|
||||
---
|
||||
|
||||
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.
|
||||
@@ -15,6 +15,7 @@ import { Header3 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { InfoIconTooltip } from "~/components/primitives/Tooltip";
|
||||
import { useSearchParams } from "~/hooks/useSearchParam";
|
||||
import { QUEUE_METRICS_DEFAULT_PERIOD } from "~/components/queues/queueMetricsPeriod";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { formatNumberCompact } from "~/utils/numberFormatter";
|
||||
|
||||
@@ -34,8 +35,6 @@ export const QUEUE_METRIC_COLORS = {
|
||||
ckWait: "#F59E0B",
|
||||
};
|
||||
|
||||
export const QUEUE_METRICS_DEFAULT_PERIOD = "1d";
|
||||
|
||||
export type QueueMetricIds = {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { getCachedLimit } from "~/services/platform.v3.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { QUEUE_METRICS_RETENTION_DAYS } from "./queueMetricsPeriod";
|
||||
|
||||
/**
|
||||
* The furthest back this org can query queue metrics: their plan's query period, capped at the
|
||||
* 30 day retention. Same limit `executeQuery` enforces, so the queue-metric queries that bypass it
|
||||
* and go straight to ClickHouse stay in step with the ones that don't.
|
||||
*
|
||||
* Read through the limit cache: the queues page revalidates on an interval, so this runs far more
|
||||
* often than a one-off page load. Never throws, so a cache or platform outage costs the caller its
|
||||
* time filter rather than the whole page: the retention cap is the widest window the data can cover
|
||||
* anyway, and the queries stay tenant-scoped either way.
|
||||
*/
|
||||
export async function queueMetricsMaxPeriodDays(organizationId: string): Promise<number> {
|
||||
try {
|
||||
const cached = await getCachedLimit(
|
||||
organizationId,
|
||||
"queryPeriodDays",
|
||||
QUEUE_METRICS_RETENTION_DAYS
|
||||
);
|
||||
const planPeriodDays = cached.val ?? QUEUE_METRICS_RETENTION_DAYS;
|
||||
return Math.min(planPeriodDays, QUEUE_METRICS_RETENTION_DAYS);
|
||||
} catch (error) {
|
||||
logger.warn("Queue metrics query period limit unavailable, falling back to retention", {
|
||||
organizationId,
|
||||
error,
|
||||
});
|
||||
return QUEUE_METRICS_RETENTION_DAYS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import parse from "parse-duration";
|
||||
import { useEffect } from "react";
|
||||
|
||||
/**
|
||||
* The time window the queue-metrics pages (queues list + queue detail) use when the URL carries
|
||||
* no explicit period, and the memory that makes the user's last pick stick.
|
||||
*
|
||||
* The last period picked is stored in a cookie rather than localStorage so the loaders can read it
|
||||
* and the first render already uses the remembered window (with localStorage the page would paint
|
||||
* the default and then re-fetch). Absolute from/to ranges are never remembered: they'd pin later
|
||||
* visits to a window that goes stale.
|
||||
*/
|
||||
export const QUEUE_METRICS_DEFAULT_PERIOD = "1h";
|
||||
|
||||
const COOKIE_NAME = "queueMetricsPeriod";
|
||||
const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
|
||||
|
||||
/**
|
||||
* The shape TimeFilter writes: a count plus a minute/hour/day unit. The count is unbounded here
|
||||
* because the picker accepts any positive integer for a custom duration (`10000m` is a little under
|
||||
* 7 days); the retention bound below is what rules a window out.
|
||||
*/
|
||||
const PERIOD_PATTERN = /^\d+[mhd]$/;
|
||||
|
||||
/** Queue metrics are retained for 30 days, so a longer window can only ever render empty. */
|
||||
export const QUEUE_METRICS_RETENTION_DAYS = 30;
|
||||
|
||||
const MINUTE_MS = 60 * 1000;
|
||||
const HOUR_MS = 60 * MINUTE_MS;
|
||||
const DAY_MS = 24 * HOUR_MS;
|
||||
const MAX_PERIOD_MS = QUEUE_METRICS_RETENTION_DAYS * DAY_MS;
|
||||
|
||||
function isPeriod(value: string | undefined | null): value is string {
|
||||
if (typeof value !== "string" || !PERIOD_PATTERN.test(value)) return false;
|
||||
const ms = parse(value);
|
||||
return typeof ms === "number" && ms > 0 && ms <= MAX_PERIOD_MS;
|
||||
}
|
||||
|
||||
/** Loader side: the remembered period, falling back to the default when nothing usable is stored. */
|
||||
export function queueMetricsPeriodFromRequest(request: Request): string {
|
||||
const header = request.headers.get("cookie");
|
||||
if (!header) return QUEUE_METRICS_DEFAULT_PERIOD;
|
||||
|
||||
for (const part of header.split(";")) {
|
||||
const separator = part.indexOf("=");
|
||||
if (separator === -1) continue;
|
||||
if (part.slice(0, separator).trim() !== COOKIE_NAME) continue;
|
||||
const value = part.slice(separator + 1).trim();
|
||||
return isPeriod(value) ? value : QUEUE_METRICS_DEFAULT_PERIOD;
|
||||
}
|
||||
|
||||
return QUEUE_METRICS_DEFAULT_PERIOD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember the period currently in the URL so the next visit to a queue-metrics page opens on it.
|
||||
* Pass the raw `period` search param: an absent one (the page is on its default) or an absolute
|
||||
* from/to range leaves the stored value alone.
|
||||
*/
|
||||
export function useRememberQueueMetricsPeriod(period: string | undefined) {
|
||||
useEffect(() => {
|
||||
if (!isPeriod(period)) return;
|
||||
document.cookie = `${COOKIE_NAME}=${period}; path=/; max-age=${COOKIE_MAX_AGE_SECONDS}; samesite=lax`;
|
||||
}, [period]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The window the page should show: a usable period in the URL wins, an absolute range means "no
|
||||
* period", and everything else (including a period the picker could never produce, e.g. a
|
||||
* hand-edited `?period=garbage`) falls back to the remembered default the loader resolved. The
|
||||
* result is held inside the org's plan query period, since that is the window the data will cover.
|
||||
*
|
||||
* Both the loaders and the client-side chart queries resolve through here, so they can't disagree
|
||||
* about the window.
|
||||
*/
|
||||
export function resolveQueueMetricsPeriod({
|
||||
period,
|
||||
from,
|
||||
to,
|
||||
defaultPeriod,
|
||||
maxPeriodDays,
|
||||
}: {
|
||||
period: string | undefined;
|
||||
from: string | undefined;
|
||||
to: string | undefined;
|
||||
defaultPeriod: string;
|
||||
maxPeriodDays: number;
|
||||
}): string | null {
|
||||
if (isPeriod(period)) return clampQueueMetricsPeriod(period, maxPeriodDays);
|
||||
if (from || to) return null;
|
||||
return clampQueueMetricsPeriod(defaultPeriod, maxPeriodDays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hold a period inside a day budget (the org's plan query period). A period longer than the plan
|
||||
* allows becomes the plan's maximum, so the picker shows the window the data covers.
|
||||
*
|
||||
* The budget is whatever the plan says, not necessarily a whole number of days, so the replacement
|
||||
* is expressed in the largest unit that divides it: rounding down keeps the period inside the
|
||||
* budget rather than a hair over it.
|
||||
*/
|
||||
export function clampQueueMetricsPeriod(period: string, maxPeriodDays: number): string {
|
||||
const maxMs = maxPeriodDays * DAY_MS;
|
||||
const ms = parse(period);
|
||||
if (typeof ms === "number" && ms > 0 && ms <= maxMs) return period;
|
||||
|
||||
const days = Math.floor(maxMs / DAY_MS);
|
||||
if (days >= 1) return `${days}d`;
|
||||
const hours = Math.floor(maxMs / HOUR_MS);
|
||||
if (hours >= 1) return `${hours}h`;
|
||||
return `${Math.max(1, Math.floor(maxMs / MINUTE_MS))}m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull a window forward to the earliest time the org's plan can query, the same clip `executeQuery`
|
||||
* applies to every metric query. Queue-metric queries that go straight to ClickHouse (the queues
|
||||
* list table, the concurrency-keys endpoint) have to apply it themselves, otherwise a hand-typed
|
||||
* `?period=` reaches further back than the plan allows.
|
||||
*
|
||||
* A range that ends before the plan's earliest queryable time collapses to an empty window rather
|
||||
* than an inverted one, which is what the enforced lower bound in `executeQuery` yields for the
|
||||
* same request: no rows.
|
||||
*/
|
||||
export function clipQueueMetricsWindow(
|
||||
window: { from: Date; to: Date },
|
||||
maxPeriodDays: number
|
||||
): { from: Date; to: Date } {
|
||||
const earliest = new Date(Date.now() - maxPeriodDays * DAY_MS);
|
||||
const from = window.from < earliest ? earliest : window.from;
|
||||
return { from, to: window.to < from ? from : window.to };
|
||||
}
|
||||
+43
-14
@@ -102,6 +102,16 @@ import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { BigNumber } from "~/components/metrics/BigNumber";
|
||||
import { canAccessQueueMetricsUi } from "~/v3/canAccessQueueMetricsUi.server";
|
||||
import { QueueAllocationPresenter } from "~/presenters/v3/QueueAllocationPresenter.server";
|
||||
import {
|
||||
QUEUE_METRICS_DEFAULT_PERIOD,
|
||||
QUEUE_METRICS_RETENTION_DAYS,
|
||||
clampQueueMetricsPeriod,
|
||||
clipQueueMetricsWindow,
|
||||
queueMetricsPeriodFromRequest,
|
||||
resolveQueueMetricsPeriod,
|
||||
useRememberQueueMetricsPeriod,
|
||||
} from "~/components/queues/queueMetricsPeriod";
|
||||
import { queueMetricsMaxPeriodDays } from "~/components/queues/queueMetricsPeriod.server";
|
||||
|
||||
const SearchParamsSchema = z.object({
|
||||
query: z.string().optional(),
|
||||
@@ -112,8 +122,6 @@ const SearchParamsSchema = z.object({
|
||||
sort: z.enum(["busiest", "queued", "name"]).optional(),
|
||||
});
|
||||
|
||||
const QUEUE_METRICS_DEFAULT_PERIOD = "1d";
|
||||
|
||||
// The live "Queued" / "Running" header blocks poll ClickHouse on a short cadence so they stay
|
||||
// current after first paint. They read the env-wide gauges from env_metrics (the env-level rollup
|
||||
// of queue_metrics, cheapest for a dimension-free query), always over a fixed 15m window regardless
|
||||
@@ -163,6 +171,14 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
// no metrics query fires.
|
||||
const queueMetricsUiEnabled = await canAccessQueueMetricsUi({ userId, organizationSlug });
|
||||
|
||||
const maxPeriodDays = queueMetricsUiEnabled
|
||||
? await queueMetricsMaxPeriodDays(environment.organizationId)
|
||||
: QUEUE_METRICS_RETENTION_DAYS;
|
||||
const defaultPeriod = clampQueueMetricsPeriod(
|
||||
queueMetricsPeriodFromRequest(request),
|
||||
maxPeriodDays
|
||||
);
|
||||
|
||||
try {
|
||||
const queueListPresenter = new QueueListPresenter();
|
||||
const queues = await queueListPresenter.call({
|
||||
@@ -194,12 +210,17 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const queueNames = queues.queues.map((q) =>
|
||||
q.type === "task" ? `task/${q.name}` : q.name
|
||||
);
|
||||
const timeRange = timeFilterFromTo({
|
||||
period,
|
||||
from: parseFiniteInt(from),
|
||||
to: parseFiniteInt(to),
|
||||
defaultPeriod: QUEUE_METRICS_DEFAULT_PERIOD,
|
||||
});
|
||||
const timeRange = clipQueueMetricsWindow(
|
||||
timeFilterFromTo({
|
||||
period:
|
||||
resolveQueueMetricsPeriod({ period, from, to, defaultPeriod, maxPeriodDays }) ??
|
||||
undefined,
|
||||
from: parseFiniteInt(from),
|
||||
to: parseFiniteInt(to),
|
||||
defaultPeriod,
|
||||
}),
|
||||
maxPeriodDays
|
||||
);
|
||||
const queueMetrics =
|
||||
queueNames.length > 0
|
||||
? await presenter.getQueueListMetrics({
|
||||
@@ -239,6 +260,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
metrics,
|
||||
allocation,
|
||||
queueMetricsUiEnabled,
|
||||
defaultPeriod,
|
||||
maxPeriodDays,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -362,6 +385,8 @@ function QueuesWithMetricsView() {
|
||||
autoReloadPollIntervalMs,
|
||||
metrics,
|
||||
allocation,
|
||||
defaultPeriod,
|
||||
maxPeriodDays,
|
||||
} = useTypedLoaderData<typeof loader>();
|
||||
|
||||
const metricsByQueue = metrics?.byQueue ?? {};
|
||||
@@ -377,18 +402,21 @@ function QueuesWithMetricsView() {
|
||||
const project = useProject();
|
||||
const env = useEnvironment();
|
||||
const plan = useCurrentPlan();
|
||||
// Queue metrics are retained for 30 days in ClickHouse, so cap the picker there even for
|
||||
// plans whose query-period limit was raised above it — a longer window would render empty.
|
||||
const planPeriodDays = plan?.v3Subscription?.plan?.limits?.queryPeriodDays?.number;
|
||||
const maxPeriodDays = Math.min(planPeriodDays ?? 30, 30);
|
||||
|
||||
// The header tiles fetch client-side with the same period/from/to the TimeFilter writes.
|
||||
const { value } = useSearchParams();
|
||||
const timeRange = {
|
||||
period: value("period") ?? null,
|
||||
period: resolveQueueMetricsPeriod({
|
||||
period: value("period"),
|
||||
from: value("from"),
|
||||
to: value("to"),
|
||||
defaultPeriod,
|
||||
maxPeriodDays,
|
||||
}),
|
||||
from: value("from") ?? null,
|
||||
to: value("to") ?? null,
|
||||
};
|
||||
useRememberQueueMetricsPeriod(value("period"));
|
||||
|
||||
useAutoRevalidate({ interval: autoReloadPollIntervalMs, onFocus: true });
|
||||
|
||||
@@ -473,7 +501,8 @@ function QueuesWithMetricsView() {
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<TimeFilter
|
||||
defaultPeriod={QUEUE_METRICS_DEFAULT_PERIOD}
|
||||
period={timeRange.period ?? undefined}
|
||||
defaultPeriod={defaultPeriod}
|
||||
labelName="Period"
|
||||
maxPeriodDays={maxPeriodDays}
|
||||
shortcut={{ key: "d" }}
|
||||
|
||||
+23
-9
@@ -22,7 +22,6 @@ import { ChartSyncProvider } from "~/components/primitives/charts/ChartSyncConte
|
||||
import { useZoomToTimeFilter } from "~/hooks/useZoomToTimeFilter";
|
||||
import {
|
||||
QUEUE_METRIC_COLORS as COLORS,
|
||||
QUEUE_METRICS_DEFAULT_PERIOD,
|
||||
QueueMetricChartCard as QueueDetailChartCard,
|
||||
type QueueMetricIds as Ids,
|
||||
type QueueMetricTimeRange as TimeRangeParams,
|
||||
@@ -55,7 +54,6 @@ import type {
|
||||
ConcurrencyKeyRow,
|
||||
ConcurrencyKeysResponse,
|
||||
} from "~/routes/resources.queues.concurrency-keys";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { canAccessQueueMetricsUi } from "~/v3/canAccessQueueMetricsUi.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { docsPath, EnvironmentParamSchema, v3RunsPath } from "~/utils/pathBuilder";
|
||||
@@ -67,6 +65,13 @@ import {
|
||||
QueueOverrideConcurrencyButton,
|
||||
QueuePauseResumeButton,
|
||||
} from "~/components/queues/QueueControls";
|
||||
import {
|
||||
clampQueueMetricsPeriod,
|
||||
queueMetricsPeriodFromRequest,
|
||||
resolveQueueMetricsPeriod,
|
||||
useRememberQueueMetricsPeriod,
|
||||
} from "~/components/queues/queueMetricsPeriod";
|
||||
import { queueMetricsMaxPeriodDays } from "~/components/queues/queueMetricsPeriod.server";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { RunsIcon } from "~/assets/icons/RunsIcon";
|
||||
import { InfoPanel } from "~/components/primitives/InfoPanel";
|
||||
@@ -106,6 +111,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const queue = retrieve.queue;
|
||||
const fullName = queue.type === "task" ? `task/${queue.name}` : queue.name;
|
||||
|
||||
const maxPeriodDays = await queueMetricsMaxPeriodDays(environment.organizationId);
|
||||
|
||||
const [ckBreakdown, oldestQueuedAt] = await Promise.all([
|
||||
engine.concurrencyKeyBreakdown(environment, fullName, { limit: CK_LIVE_LIMIT }),
|
||||
// Enqueue time of the oldest run still waiting in the queue right now (any queue, keyed or
|
||||
@@ -134,6 +141,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
oldestQueuedAt: oldestQueuedAt ?? null,
|
||||
loadedAt: Date.now(),
|
||||
backPath: url.pathname.replace(/\/[^/]+$/, ""),
|
||||
defaultPeriod: clampQueueMetricsPeriod(queueMetricsPeriodFromRequest(request), maxPeriodDays),
|
||||
maxPeriodDays,
|
||||
ids: {
|
||||
organizationId: environment.organizationId,
|
||||
projectId: environment.projectId,
|
||||
@@ -210,19 +219,23 @@ export default function Page() {
|
||||
loadedAt,
|
||||
backPath,
|
||||
ids,
|
||||
defaultPeriod,
|
||||
maxPeriodDays,
|
||||
} = useTypedLoaderData<typeof loader>();
|
||||
const plan = useCurrentPlan();
|
||||
// Queue metrics are retained for 30 days in ClickHouse, so cap the picker there even for
|
||||
// plans whose query-period limit was raised above it — a longer window would render empty.
|
||||
const planPeriodDays = plan?.v3Subscription?.plan?.limits?.queryPeriodDays?.number;
|
||||
const maxPeriodDays = Math.min(planPeriodDays ?? 30, 30);
|
||||
|
||||
const { value, replace } = useSearchParams();
|
||||
const timeRange: TimeRangeParams = {
|
||||
period: value("period") ?? null,
|
||||
period: resolveQueueMetricsPeriod({
|
||||
period: value("period"),
|
||||
from: value("from"),
|
||||
to: value("to"),
|
||||
defaultPeriod,
|
||||
maxPeriodDays,
|
||||
}),
|
||||
from: value("from") ?? null,
|
||||
to: value("to") ?? null,
|
||||
};
|
||||
useRememberQueueMetricsPeriod(value("period"));
|
||||
|
||||
// The Concurrency keys tab exists only for queues with key activity: live keys in the
|
||||
// ckIndex, or nonzero CK history in the selected range (one cached scalar query decides).
|
||||
@@ -283,7 +296,8 @@ export default function Page() {
|
||||
/>
|
||||
) : null}
|
||||
<TimeFilter
|
||||
defaultPeriod={QUEUE_METRICS_DEFAULT_PERIOD}
|
||||
period={timeRange.period ?? undefined}
|
||||
defaultPeriod={defaultPeriod}
|
||||
labelName="Period"
|
||||
maxPeriodDays={maxPeriodDays}
|
||||
shortcut={{ key: "d" }}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { timeFilterFromTo } from "~/components/runs/v3/SharedFilters";
|
||||
import {
|
||||
QUEUE_METRICS_DEFAULT_PERIOD,
|
||||
clipQueueMetricsWindow,
|
||||
} from "~/components/queues/queueMetricsPeriod";
|
||||
import { queueMetricsMaxPeriodDays } from "~/components/queues/queueMetricsPeriod.server";
|
||||
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
|
||||
import { findEnvironmentById, hasAccessToEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
@@ -13,8 +18,7 @@ import { engine } from "~/v3/runEngine.server";
|
||||
// Redis (O(page), independent of total key cardinality). This replaces the old top-50 cap.
|
||||
export const CONCURRENCY_KEYS_PER_PAGE = 25;
|
||||
|
||||
// Matches QUEUE_METRICS_DEFAULT_PERIOD (the detail page's TimeFilter default).
|
||||
const DEFAULT_PERIOD = "1d";
|
||||
const DEFAULT_PERIOD = QUEUE_METRICS_DEFAULT_PERIOD;
|
||||
|
||||
const Body = z.object({
|
||||
organizationId: z.string(),
|
||||
@@ -110,12 +114,15 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
return json<ConcurrencyKeysResponse>({ success: false, error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const range = timeFilterFromTo({
|
||||
period: period ?? undefined,
|
||||
from: from ?? undefined,
|
||||
to: to ?? undefined,
|
||||
defaultPeriod: DEFAULT_PERIOD,
|
||||
});
|
||||
const range = clipQueueMetricsWindow(
|
||||
timeFilterFromTo({
|
||||
period: period ?? undefined,
|
||||
from: from ?? undefined,
|
||||
to: to ?? undefined,
|
||||
defaultPeriod: DEFAULT_PERIOD,
|
||||
}),
|
||||
await queueMetricsMaxPeriodDays(organizationId)
|
||||
);
|
||||
const startTime = formatClickhouseDateTime(new Date(floorToMinute(range.from.getTime())));
|
||||
const endTime = formatClickhouseDateTime(new Date(ceilToMinute(range.to.getTime())));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user