refactor(ui): server metrics page uses global time range
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:
@@ -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),
|
||||
|
||||
@@ -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 30–120 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}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user