refactor(ui): server metrics page uses global time range
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m31s
CI / docker (push) Successful in 1m10s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 44s

Drop the page-local DS Select window picker. Drive from() / to() off
useGlobalFilters().timeRange so the dashboard tracks the same TopBar range
as Exchanges / Dashboard / Runtime. Bucket size auto-scales via
stepSecondsFor(windowSeconds) (10 s for ≤30 min → 1 h for >48 h). Query
hooks now take ServerMetricsRange = { from: Date; to: Date } instead of a
windowSeconds number, so they support arbitrary absolute or rolling ranges
the TopBar may supply (not just "now − N"). Toolbar collapses to just the
server-instance badges.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-24 09:19:20 +02:00
parent 3c2409ed6e
commit 35319dc666
4 changed files with 97 additions and 72 deletions

View File

@@ -49,30 +49,47 @@ export interface ServerMetricQueryRequest {
serverInstanceIds?: string[] | null;
}
// ── Range helper ───────────────────────────────────────────────────────
/**
* Time range driving every hook below. Callers pass the window they want
* to render; the hooks never invent their own "now" — that's the job of
* the global time-range control.
*/
export interface ServerMetricsRange {
from: Date;
to: Date;
}
function serializeRange(range: ServerMetricsRange) {
return {
from: range.from.toISOString(),
to: range.to.toISOString(),
};
}
// ── Query Hooks ────────────────────────────────────────────────────────
export function useServerMetricsCatalog(windowSeconds = 3600) {
export function useServerMetricsCatalog(range: ServerMetricsRange) {
const refetchInterval = useRefreshInterval(60_000);
const { from, to } = serializeRange(range);
return useQuery({
queryKey: ['admin', 'server-metrics', 'catalog', windowSeconds],
queryFn: async () => {
const to = new Date();
const from = new Date(to.getTime() - windowSeconds * 1000);
const params = new URLSearchParams({ from: from.toISOString(), to: to.toISOString() });
queryKey: ['admin', 'server-metrics', 'catalog', from, to],
queryFn: () => {
const params = new URLSearchParams({ from, to });
return adminFetch<ServerMetricCatalogEntry[]>(`/server-metrics/catalog?${params}`);
},
refetchInterval,
});
}
export function useServerMetricsInstances(windowSeconds = 3600) {
export function useServerMetricsInstances(range: ServerMetricsRange) {
const refetchInterval = useRefreshInterval(60_000);
const { from, to } = serializeRange(range);
return useQuery({
queryKey: ['admin', 'server-metrics', 'instances', windowSeconds],
queryFn: async () => {
const to = new Date();
const from = new Date(to.getTime() - windowSeconds * 1000);
const params = new URLSearchParams({ from: from.toISOString(), to: to.toISOString() });
queryKey: ['admin', 'server-metrics', 'instances', from, to],
queryFn: () => {
const params = new URLSearchParams({ from, to });
return adminFetch<ServerInstanceInfo[]>(`/server-metrics/instances?${params}`);
},
refetchInterval,
@@ -80,28 +97,23 @@ export function useServerMetricsInstances(windowSeconds = 3600) {
}
/**
* Run a time-series query against the server_metrics table.
* Generic time-series query against the server_metrics table.
*
* The window [from, to) is supplied in seconds of "now minus N" so the panel
* refreshes automatically at the polling interval without the caller
* recomputing timestamps.
* The caller owns the window — passing the globally-selected range keeps
* every panel aligned with the app-wide time control and allows inspection
* of historical windows, not just "last N seconds from now".
*/
export function useServerMetricsSeries(
request: Omit<ServerMetricQueryRequest, 'from' | 'to'>,
windowSeconds: number,
range: ServerMetricsRange,
opts?: { enabled?: boolean },
) {
const refetchInterval = useRefreshInterval(30_000);
const { from, to } = serializeRange(range);
return useQuery({
queryKey: ['admin', 'server-metrics', 'query', request, windowSeconds],
queryFn: async () => {
const to = new Date();
const from = new Date(to.getTime() - windowSeconds * 1000);
const body: ServerMetricQueryRequest = {
...request,
from: from.toISOString(),
to: to.toISOString(),
};
queryKey: ['admin', 'server-metrics', 'query', request, from, to],
queryFn: () => {
const body: ServerMetricQueryRequest = { ...request, from, to };
return adminFetch<ServerMetricQueryResponse>('/server-metrics/query', {
method: 'POST',
body: JSON.stringify(body),

View File

@@ -1,7 +1,7 @@
import { useMemo, useState } from 'react';
import { useMemo } from 'react';
import {
ThemedChart, Area, Line, CHART_COLORS,
Badge, EmptyState, Spinner, Select,
Badge, EmptyState, Spinner, useGlobalFilters,
} from '@cameleer/design-system';
import {
useServerMetricsCatalog,
@@ -9,19 +9,28 @@ import {
useServerMetricsSeries,
type ServerMetricQueryResponse,
type ServerMetricSeries,
type ServerMetricsRange,
} from '../../api/queries/admin/serverMetrics';
import chartCardStyles from '../../styles/chart-card.module.css';
import styles from './ServerMetricsAdminPage.module.css';
// ── Window options ─────────────────────────────────────────────────────
// ── Step picker ────────────────────────────────────────────────────────
const WINDOWS: { label: string; seconds: number; step: number }[] = [
{ label: 'Last 15 min', seconds: 15 * 60, step: 60 },
{ label: 'Last 1 h', seconds: 60 * 60, step: 60 },
{ label: 'Last 6 h', seconds: 6 * 60 * 60, step: 300 },
{ label: 'Last 24 h', seconds: 24 * 60 * 60, step: 300 },
{ label: 'Last 7 d', seconds: 7 * 24 * 60 * 60, step: 3600 },
];
/**
* Choose a bucket width that keeps the rendered series readable regardless
* of the window size the global time-range control hands us.
*
* Targets roughly 30120 points per series — any denser and the chart
* becomes a blur; any sparser and short windows look empty. Clamped to the
* [10, 3600] range the backend accepts.
*/
function stepSecondsFor(windowSeconds: number): number {
if (windowSeconds <= 30 * 60) return 10; // ≤ 30 min → 10 s buckets
if (windowSeconds <= 2 * 60 * 60) return 60; // ≤ 2 h → 1 min
if (windowSeconds <= 12 * 60 * 60) return 300; // ≤ 12 h → 5 min
if (windowSeconds <= 48 * 60 * 60) return 900; // ≤ 48 h → 15 min
return 3600; // longer → 1 h
}
// ── Panel component ────────────────────────────────────────────────────
@@ -36,7 +45,7 @@ interface PanelProps {
mode?: 'raw' | 'delta';
yLabel?: string;
asArea?: boolean;
windowSeconds: number;
range: ServerMetricsRange;
stepSeconds: number;
formatValue?: (v: number) => string;
}
@@ -44,11 +53,11 @@ interface PanelProps {
function Panel({
title, subtitle, metric, statistic, groupByTags, filterTags,
aggregation, mode = 'raw', yLabel, asArea = false,
windowSeconds, stepSeconds, formatValue,
range, stepSeconds, formatValue,
}: PanelProps) {
const { data, isLoading, isError, error } = useServerMetricsSeries(
{ metric, statistic, groupByTags, filterTags, aggregation, mode, stepSeconds },
windowSeconds,
range,
);
return (
@@ -157,20 +166,28 @@ function formatPct(frac: number): string {
// ── Page ───────────────────────────────────────────────────────────────
export default function ServerMetricsAdminPage() {
const [windowIdx, setWindowIdx] = useState(1); // default: last 1 h
const windowOpt = WINDOWS[windowIdx];
const windowSeconds = windowOpt.seconds;
const stepSeconds = windowOpt.step;
// Drive the entire page from the global time-range control in the TopBar.
const { timeRange } = useGlobalFilters();
const range: ServerMetricsRange = useMemo(
() => ({ from: timeRange.start, to: timeRange.end }),
[timeRange.start, timeRange.end],
);
const windowSeconds = Math.max(
1,
Math.round((range.to.getTime() - range.from.getTime()) / 1000),
);
const stepSeconds = stepSecondsFor(windowSeconds);
const { data: catalog } = useServerMetricsCatalog(windowSeconds);
const { data: instances } = useServerMetricsInstances(windowSeconds);
const { data: catalog } = useServerMetricsCatalog(range);
const { data: instances } = useServerMetricsInstances(range);
const has = (metricName: string) =>
(catalog ?? []).some((c) => c.metricName === metricName);
return (
<div className={styles.page}>
{/* Toolbar */}
{/* Toolbar — just server-instance badges. Time range is driven by
the global time-range control in the TopBar. */}
<div className={styles.toolbar}>
<div className={styles.instanceStrip}>
{(instances ?? []).slice(0, 8).map((i) => (
@@ -183,11 +200,6 @@ export default function ServerMetricsAdminPage() {
<Badge label="no samples in window" variant="outlined" />
)}
</div>
<Select
value={String(windowIdx)}
onChange={(e) => setWindowIdx(Number(e.target.value))}
options={WINDOWS.map((w, i) => ({ value: String(i), label: w.label }))}
/>
</div>
{/* Row 1: Server health */}
@@ -205,7 +217,7 @@ export default function ServerMetricsAdminPage() {
groupByTags={['state']}
aggregation="avg"
asArea
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
<Panel
@@ -215,7 +227,7 @@ export default function ServerMetricsAdminPage() {
statistic="value"
groupByTags={['type']}
aggregation="avg"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
</div>
@@ -227,7 +239,7 @@ export default function ServerMetricsAdminPage() {
statistic="count"
groupByTags={['reason']}
mode="delta"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
<Panel
@@ -237,7 +249,7 @@ export default function ServerMetricsAdminPage() {
statistic="count"
groupByTags={['reason']}
mode="delta"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
</div>
@@ -260,7 +272,7 @@ export default function ServerMetricsAdminPage() {
asArea
yLabel="MB"
formatValue={formatMB}
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
<Panel
@@ -272,7 +284,7 @@ export default function ServerMetricsAdminPage() {
asArea
yLabel="%"
formatValue={formatPct}
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
<Panel
@@ -282,7 +294,7 @@ export default function ServerMetricsAdminPage() {
statistic="max"
groupByTags={['cause']}
aggregation="max"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
</div>
@@ -293,7 +305,7 @@ export default function ServerMetricsAdminPage() {
metric="jvm.threads.live"
statistic="value"
aggregation="avg"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
<Panel
@@ -305,7 +317,7 @@ export default function ServerMetricsAdminPage() {
aggregation="sum"
yLabel="MB"
formatValue={formatMB}
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
</div>
@@ -327,7 +339,7 @@ export default function ServerMetricsAdminPage() {
filterTags={{ outcome: 'SUCCESS' }}
aggregation="avg"
yLabel="s"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
<Panel
@@ -337,7 +349,7 @@ export default function ServerMetricsAdminPage() {
statistic="count"
mode="delta"
aggregation="sum"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
<Panel
@@ -347,7 +359,7 @@ export default function ServerMetricsAdminPage() {
statistic="value"
groupByTags={['pool']}
aggregation="avg"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
</div>
@@ -359,7 +371,7 @@ export default function ServerMetricsAdminPage() {
statistic="count"
groupByTags={['pool']}
mode="delta"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
<Panel
@@ -369,7 +381,7 @@ export default function ServerMetricsAdminPage() {
statistic="count"
groupByTags={['level']}
mode="delta"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
</div>
@@ -394,7 +406,7 @@ export default function ServerMetricsAdminPage() {
groupByTags={['state']}
aggregation="avg"
asArea
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
)}
@@ -406,7 +418,7 @@ export default function ServerMetricsAdminPage() {
statistic="count"
groupByTags={['kind']}
mode="delta"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
)}
@@ -418,7 +430,7 @@ export default function ServerMetricsAdminPage() {
statistic="max"
aggregation="max"
yLabel="s"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
)}
@@ -442,7 +454,7 @@ export default function ServerMetricsAdminPage() {
statistic="count"
groupByTags={['status']}
mode="delta"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
)}
@@ -454,7 +466,7 @@ export default function ServerMetricsAdminPage() {
statistic="mean"
aggregation="avg"
yLabel="s"
windowSeconds={windowSeconds}
range={range}
stepSeconds={stepSeconds}
/>
)}