From a2225b05b7c9cab6206fc6577df6302579c0d45a Mon Sep 17 00:00:00 2001 From: Fabzerito Date: Fri, 22 May 2026 13:58:42 +0200 Subject: [PATCH] feat: add granularity options to instance metrics --- src/intl/en.json | 2 + src/modules/metrics/metrics-helpers.ts | 31 +++++++ src/modules/metrics/use-metrics.ts | 19 ++--- .../service/metrics/service-metrics.page.tsx | 85 ++++++++++++++----- .../_main/services/$serviceId/metrics.tsx | 19 ++--- 5 files changed, 108 insertions(+), 48 deletions(-) diff --git a/src/intl/en.json b/src/intl/en.json index 1108854e0..71b62b613 100644 --- a/src/intl/en.json +++ b/src/intl/en.json @@ -2665,6 +2665,8 @@ }, "metrics": { "title": "Metrics", + "timeFrame": "Time frame", + "step": "Step", "cpu": { "label": "CPU usage", "tooltip": "Percentage of used CPU for the selected instance" diff --git a/src/modules/metrics/metrics-helpers.ts b/src/modules/metrics/metrics-helpers.ts index 7e46cd4a3..cea2aa5fd 100644 --- a/src/modules/metrics/metrics-helpers.ts +++ b/src/modules/metrics/metrics-helpers.ts @@ -3,6 +3,37 @@ import { DataPoint, GraphDataPoint } from './metrics-types'; export const metricsTimeFrames = ['5m', '15m', '1h', '6h', '1d', '2d', '7d'] as const; export type MetricsTimeFrame = (typeof metricsTimeFrames)[number]; +export const metricsSteps = ['1m', '5m', '15m', '30m', '1h', '2h', '3h', '6h', '12h'] as const; +export type MetricsStep = (typeof metricsSteps)[number]; + +export function getDefaultStep(timeFrame: MetricsTimeFrame): MetricsStep { + const defaultSteps: Record = { + '5m': '1m', + '15m': '1m', + '1h': '1m', + '6h': '5m', + '1d': '30m', + '2d': '1h', + '7d': '3h', + }; + + return defaultSteps[timeFrame]; +} + +export function getValidStepsForTimeFrame(timeFrame: MetricsTimeFrame): MetricsStep[] { + const validSteps: Record = { + '5m': ['1m'], + '15m': ['1m', '5m'], + '1h': ['1m', '5m'], + '6h': ['1m', '5m', '15m', '30m'], + '1d': ['5m', '15m', '30m', '1h'], + '2d': ['15m', '30m', '1h', '2h'], + '7d': ['30m', '1h', '2h', '3h', '6h', '12h'], + }; + + return validSteps[timeFrame]; +} + export function toGraph({ date, value }: DataPoint): GraphDataPoint { const result: GraphDataPoint = { x: date, y: null }; diff --git a/src/modules/metrics/use-metrics.ts b/src/modules/metrics/use-metrics.ts index c5f4796fd..052199c16 100644 --- a/src/modules/metrics/use-metrics.ts +++ b/src/modules/metrics/use-metrics.ts @@ -7,7 +7,7 @@ import { getApiQueryKey, refetchInterval, useApi } from 'src/api'; import { identity } from 'src/utils/generic'; import { toObject } from 'src/utils/object'; -import { MetricsTimeFrame } from './metrics-helpers'; +import { MetricsStep, MetricsTimeFrame, getDefaultStep } from './metrics-helpers'; import { DataPoint, Metric } from './metrics-types'; const timeFrameToDuration: Record = { @@ -20,34 +20,27 @@ const timeFrameToDuration: Record = { '7d': { days: 7 }, }; -const timeFrameToStep: Record = { - '5m': '1m', - '15m': '1m', - '1h': '1m', - '6h': '5m', - '1d': '30m', - '2d': '1h', - '7d': '3h', -}; - type UseMetricsOptions = { serviceId?: string; instanceId?: string; metrics: API.MetricName[]; timeFrame: MetricsTimeFrame; + step?: MetricsStep; }; -export function useMetricsQueries({ serviceId, instanceId, metrics, timeFrame }: UseMetricsOptions) { +export function useMetricsQueries({ serviceId, instanceId, metrics, timeFrame, step }: UseMetricsOptions) { const api = useApi(); const { getAccessToken } = useAuth(); + const resolvedStep = step ?? getDefaultStep(timeFrame); + return useQueries({ queries: metrics.map((name) => { const query = { name, service_id: serviceId, instance_id: instanceId, - step: timeFrameToStep[timeFrame], + step: resolvedStep, time_frame: timeFrame, }; diff --git a/src/pages/service/metrics/service-metrics.page.tsx b/src/pages/service/metrics/service-metrics.page.tsx index a2a87a578..c6d16fb33 100644 --- a/src/pages/service/metrics/service-metrics.page.tsx +++ b/src/pages/service/metrics/service-metrics.page.tsx @@ -13,7 +13,13 @@ import { HttpThroughputGraph } from 'src/modules/metrics/graphs/http-throughput- import { MemoryGraph } from 'src/modules/metrics/graphs/memory-graph'; import { PublicDataTransferGraph } from 'src/modules/metrics/graphs/public-data-transfer-graph'; import { ResponseTimeGraph } from 'src/modules/metrics/graphs/response-time-graph'; -import { MetricsTimeFrame, metricsTimeFrames } from 'src/modules/metrics/metrics-helpers'; +import { + MetricsStep, + MetricsTimeFrame, + getDefaultStep, + getValidStepsForTimeFrame, + metricsTimeFrames, +} from 'src/modules/metrics/metrics-helpers'; import { useMetricsQueries } from 'src/modules/metrics/use-metrics'; import { inArray } from 'src/utils/arrays'; @@ -22,31 +28,60 @@ const T = createTranslate('pages.service.metrics'); type MetricsPageProps = { serviceId: string; timeFrame: MetricsTimeFrame; + step?: MetricsStep; }; -export function ServiceMetricsPage({ serviceId, timeFrame }: MetricsPageProps) { +export function ServiceMetricsPage({ serviceId, timeFrame, step: stepProp }: MetricsPageProps) { + const validSteps = getValidStepsForTimeFrame(timeFrame); + const step: MetricsStep = stepProp && validSteps.includes(stepProp) ? stepProp : getDefaultStep(timeFrame); + return ( <> - {metricsTimeFrames.map((s) => ( - <LinkButton - key={s} - from="/services/$serviceId/metrics" - search={{ 'time-frame': s }} - type="button" - variant={s === timeFrame ? 'solid' : 'outline'} - > - {s.toUpperCase()} - </LinkButton> - ))} - </ButtonGroup> + <div className="col gap-2"> + <div className="col gap-1"> + <span className="px-1 text-xs text-dim"> + <T id="timeFrame" /> + </span> + <ButtonGroup> + {metricsTimeFrames.map((s) => ( + <LinkButton + key={s} + from="/services/$serviceId/metrics" + search={{ 'time-frame': s }} + type="button" + variant={s === timeFrame ? 'solid' : 'outline'} + > + {s.toUpperCase()} + </LinkButton> + ))} + </ButtonGroup> + </div> + <div className="col gap-1"> + <span className="px-1 text-xs text-dim"> + <T id="step" /> + </span> + <ButtonGroup> + {validSteps.map((s) => ( + <LinkButton + key={s} + from="/services/$serviceId/metrics" + search={{ 'time-frame': timeFrame, step: s }} + type="button" + variant={s === step ? 'solid' : 'outline'} + > + {s.toUpperCase()} + </LinkButton> + ))} + </ButtonGroup> + </div> + </div> } /> - <ServiceMetrics serviceId={serviceId} timeFrame={timeFrame} /> + <ServiceMetrics serviceId={serviceId} timeFrame={timeFrame} step={step} /> </> ); } @@ -63,15 +98,23 @@ const metrics: API.MetricName[] = [ 'PUBLIC_DATA_TRANSFER_OUT', ] as const; -function ServiceMetrics({ serviceId, timeFrame }: { serviceId: string; timeFrame: MetricsTimeFrame }) { +function ServiceMetrics({ + serviceId, + timeFrame, + step, +}: { + serviceId: string; + timeFrame: MetricsTimeFrame; + step: MetricsStep; +}) { const service = useService(serviceId); - const queries = useMetricsQueries({ serviceId, metrics, timeFrame }); + const queries = useMetricsQueries({ serviceId, metrics, timeFrame, step }); const instance = useServiceInstanceType(serviceId); return ( <> - <div className="col gap-4 lg:row"> - <GraphCard label={<T id="cpu.label" />} tooltip={<T id="cpu.tooltip" />} className="flex-1"> + <div className="col gap-4"> + <GraphCard label={<T id="cpu.label" />} tooltip={<T id="cpu.tooltip" />}> <CpuGraph loading={queries.isPending} error={queries.error['CPU_TOTAL_PERCENT']} @@ -79,7 +122,7 @@ function ServiceMetrics({ serviceId, timeFrame }: { serviceId: string; timeFrame /> </GraphCard> - <GraphCard label={<T id="memory.label" />} tooltip={<T id="memory.tooltip" />} className="flex-1"> + <GraphCard label={<T id="memory.label" />} tooltip={<T id="memory.tooltip" />}> <MemoryGraph loading={queries.isPending} error={queries.error['MEM_RSS']} diff --git a/src/routes/_main/services/$serviceId/metrics.tsx b/src/routes/_main/services/$serviceId/metrics.tsx index 139d72466..f12f54282 100644 --- a/src/routes/_main/services/$serviceId/metrics.tsx +++ b/src/routes/_main/services/$serviceId/metrics.tsx @@ -2,29 +2,20 @@ import { createFileRoute } from '@tanstack/react-router'; import z from 'zod'; import { CrumbLink } from 'src/layouts/main/app-breadcrumbs'; +import { metricsSteps, metricsTimeFrames } from 'src/modules/metrics/metrics-helpers'; import { ServiceMetricsPage } from 'src/pages/service/metrics/service-metrics.page'; export const Route = createFileRoute('/_main/services/$serviceId/metrics')({ component: function Component() { const { serviceId } = Route.useParams(); - const { 'time-frame': timeFrame } = Route.useSearch(); + const { 'time-frame': timeFrame, step } = Route.useSearch(); - return <ServiceMetricsPage serviceId={serviceId} timeFrame={timeFrame} />; + return <ServiceMetricsPage serviceId={serviceId} timeFrame={timeFrame} step={step} />; }, validateSearch: z.object({ - 'time-frame': z - .union([ - z.literal('5m'), - z.literal('15m'), - z.literal('1h'), - z.literal('6h'), - z.literal('1d'), - z.literal('2d'), - z.literal('7d'), - ]) - .default('5m') - .catch('5m'), + 'time-frame': z.enum(metricsTimeFrames).default('5m').catch('5m'), + step: z.enum(metricsSteps).optional().catch(undefined), }), beforeLoad: ({ params }) => ({