mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 05:29:45 +00:00
Name where History readings came from instead of always "stored" (#2933)
A History reply carries its source: the history store, readings Pulse holds in memory because the store has none for the range, or a lone current reading when no history exists yet. The drawer readout ignored it and always said "Latest stored", so memory and live readings were presented as stored history. The canvas charts did badge the source, but as "Buffer" and "Live", words a reader has no reason to know. One shared vocabulary now covers both. Stored history (and demo data) keeps "Latest stored" / "Stored". Memory readings read "Latest recorded" / "Recorded", with one line above the drawer charts saying the store has none for this range yet, and a "Recent only" badge on canvas charts. A lone current reading reads "Current", badged "Current only". Stacked on #2932 (same chart files); it carries that commit until #2932 lands.
This commit is contained in:
@@ -9215,6 +9215,15 @@ is dropped (`withoutRepeatedHistoryTimeTicks`). Phone chart folds share one
|
|||||||
button, `PhoneChartsToggle` ("Show <names>" / "Show fewer charts",
|
button, `PhoneChartsToggle` ("Show <names>" / "Show fewer charts",
|
||||||
`aria-controls` naming the folded group, hidden from `sm` up); the drawer
|
`aria-controls` naming the folded group, hidden from `sm` up); the drawer
|
||||||
History and the disk detail both use it.
|
History and the disk detail both use it.
|
||||||
|
History names where its readings came from, from the reply's `source`, in
|
||||||
|
one shared vocabulary (`getHistoryReadoutLabel`, `getHistorySourceNotice`):
|
||||||
|
stored history (and demo data) reads "Latest stored · <time>" / "Stored ·";
|
||||||
|
readings Pulse holds in memory because the store has none for the range read
|
||||||
|
"Latest recorded" / "Recorded", with one line above the drawer charts ("Recent
|
||||||
|
readings only. The history store has none for this range yet.") and a "Recent
|
||||||
|
only" badge on a canvas chart (it said "Buffer"); a lone current reading
|
||||||
|
reads "Current" with "Current only" (it said "Live"). A drawer readout never
|
||||||
|
calls memory or live readings stored.
|
||||||
|
|
||||||
### Shared History single observations
|
### Shared History single observations
|
||||||
|
|
||||||
|
|||||||
@@ -4033,7 +4033,8 @@ handoff reads `document.activeElement` once, as the Retry branch is disposed.
|
|||||||
Repeated gridline labels are blanked from the three labels each panel already
|
Repeated gridline labels are blanked from the three labels each panel already
|
||||||
formats, and the canvas chart measures the time labels it already draws to
|
formats, and the canvas chart measures the time labels it already draws to
|
||||||
choose how many fit. None adds a read or timer. The disk detail's phone fold
|
choose how many fit. None adds a read or timer. The disk detail's phone fold
|
||||||
removes reads rather than adding them: folded charts stay paused until opened.
|
removes reads rather than adding them: folded charts stay paused until opened. The readout and notice
|
||||||
|
naming a reply's source read the `source` field the reply already carries.
|
||||||
|
|
||||||
The default stored-history legend resolves one latest timestamp from that
|
The default stored-history legend resolves one latest timestamp from that
|
||||||
same bounded, normalized panel index, then looks up each configured series at
|
same bounded, normalized panel index, then looks up each configured series at
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ const parent = false;
|
|||||||
async () => {
|
async () => {
|
||||||
assert.match(await description(), /42°C/);
|
assert.match(await description(), /42°C/);
|
||||||
assert.equal(await disk.locator('[data-history-chart-tooltip]').count(), 1);
|
assert.equal(await disk.locator('[data-history-chart-tooltip]').count(), 1);
|
||||||
assert.match(await disk.innerText(), /Buffer/i);
|
assert.match(await disk.innerText(), /Recent only/i);
|
||||||
assert(requests.some((r) => r.org === 'org-a' && r.resourceType === 'disk'));
|
assert(requests.some((r) => r.org === 'org-a' && r.resourceType === 'disk'));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -243,7 +243,7 @@ const parent = false;
|
|||||||
'Pending org-b removes org-a values, min/max, provenance and grouped inspection',
|
'Pending org-b removes org-a values, min/max, provenance and grouped inspection',
|
||||||
async () => {
|
async () => {
|
||||||
assert.doesNotMatch(await description(), /42°C/);
|
assert.doesNotMatch(await description(), /42°C/);
|
||||||
assert.doesNotMatch(await disk.innerText(), /Buffer|42°C/i);
|
assert.doesNotMatch(await disk.innerText(), /Recent only|42°C/i);
|
||||||
assert.equal(await disk.locator('[data-history-chart-tooltip]').count(), 0);
|
assert.equal(await disk.locator('[data-history-chart-tooltip]').count(), 0);
|
||||||
assert.equal(await pool.locator('[data-history-chart-tooltip]').count(), 0);
|
assert.equal(await pool.locator('[data-history-chart-tooltip]').count(), 0);
|
||||||
assert.equal(await service.getByRole('slider').count(), 0);
|
assert.equal(await service.getByRole('slider').count(), 0);
|
||||||
@@ -257,7 +257,7 @@ const parent = false;
|
|||||||
'Late pre-switch completion cannot resurrect readings or settle the new org',
|
'Late pre-switch completion cannot resurrect readings or settle the new org',
|
||||||
async () => {
|
async () => {
|
||||||
assert.doesNotMatch(await description(), /42°C|99°C/);
|
assert.doesNotMatch(await description(), /42°C|99°C/);
|
||||||
assert.doesNotMatch(await disk.innerText(), /Buffer|99°C/i);
|
assert.doesNotMatch(await disk.innerText(), /Recent only|99°C/i);
|
||||||
assert.equal(await service.getByRole('slider').count(), 0);
|
assert.equal(await service.getByRole('slider').count(), 0);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ const { chromium } = require('playwright');
|
|||||||
range,
|
range,
|
||||||
start: 1_700_000_000_000,
|
start: 1_700_000_000_000,
|
||||||
end: 1_700_000_060_000,
|
end: 1_700_000_060_000,
|
||||||
source: 'store',
|
// The service target has no stored history yet: Pulse answers from memory.
|
||||||
|
source: id === 'pbs-three' ? 'memory' : 'store',
|
||||||
metrics: Object.fromEntries(
|
metrics: Object.fromEntries(
|
||||||
(id === 'pbs-three'
|
(id === 'pbs-three'
|
||||||
? ['cpu', 'memory']
|
? ['cpu', 'memory']
|
||||||
@@ -229,6 +230,16 @@ const { chromium } = require('playwright');
|
|||||||
await waitForPaths(2);
|
await waitForPaths(2);
|
||||||
assert.equal(requests.at(-1).id, 'pbs-three');
|
assert.equal(requests.at(-1).id, 'pbs-three');
|
||||||
assert.equal(requests.at(-1).range, '24h');
|
assert.equal(requests.at(-1).range, '24h');
|
||||||
|
// Readings from memory are named as recorded, not stored, and say why.
|
||||||
|
assert.match(
|
||||||
|
await detail.getByTestId('guest-history-hover-time').first().innerText(),
|
||||||
|
/^Latest recorded · /,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
await detail.getByTestId('history-source-notice').innerText(),
|
||||||
|
'Recent readings only. The history store has none for this range yet.',
|
||||||
|
);
|
||||||
|
await screenshot('replacement-recorded');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await detail.locator('[data-history-group="network"] path[data-history-series]').count(),
|
await detail.locator('[data-history-group="network"] path[data-history-series]').count(),
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"base_sha": "8ae88cb6d1845d690d534084aaf374b55c91c5ed",
|
||||||
|
"verified_at": "2026-10-14T12:04:15Z",
|
||||||
|
"result": "passed",
|
||||||
|
"changed_paths": [
|
||||||
|
"frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx",
|
||||||
|
"frontend-modern/src/components/shared/HistoryChartHeader.tsx",
|
||||||
|
"frontend-modern/src/components/shared/historyChartModel.ts"
|
||||||
|
],
|
||||||
|
"content_sha256": {
|
||||||
|
"frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "0ec0c06291cce526988749094bceafcb78a947f896968570b3001e3ed8eb0ab2",
|
||||||
|
"frontend-modern/src/components/shared/HistoryChartHeader.tsx": "46a353942d31d318125a0ee7bc41232cfecadb7c1bb16e6733ba5c04291df367",
|
||||||
|
"frontend-modern/src/components/shared/historyChartModel.ts": "a8b8894cfa937ce26299d72eae9d1cbebd17b977e0f8f2d12943cf7324d22a1e"
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
"/browser-tests/pbs-history-refresh.html (production GuestDrawerHistory renderer, Chromium via Playwright 1.56.1 on pulse-dev)",
|
||||||
|
"/browser-tests/history-org.html (production HistoryChart canvas with a memory-sourced reply)"
|
||||||
|
],
|
||||||
|
"viewports": [
|
||||||
|
{
|
||||||
|
"width": 1365,
|
||||||
|
"height": 900
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"width": 390,
|
||||||
|
"height": 844
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"states": [
|
||||||
|
"drawer target answered from memory: readout \"Latest recorded \u00b7 <time>\", one line \"Recent readings only. The history store has none for this range yet.\" above the charts, light and dark",
|
||||||
|
"drawer target answered from the store: \"Latest stored\", no notice",
|
||||||
|
"canvas chart answered from memory: \"Recent only\" badge (was \"Buffer\"), withdrawn on an organisation switch"
|
||||||
|
],
|
||||||
|
"interactions": [
|
||||||
|
"drawer poll, failure, retry and target switch",
|
||||||
|
"organisation switch mid-read on the canvas chart"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -23,6 +23,9 @@ import {
|
|||||||
formatHistoryChartTooltipValue,
|
formatHistoryChartTooltipValue,
|
||||||
formatHistoryRangeOptionLabel,
|
formatHistoryRangeOptionLabel,
|
||||||
formatHistoryChartTimeLabel,
|
formatHistoryChartTimeLabel,
|
||||||
|
getHistoryReadoutLabel,
|
||||||
|
getHistorySourceNotice,
|
||||||
|
type HistorySource,
|
||||||
withoutRepeatedHistoryAxisLabels,
|
withoutRepeatedHistoryAxisLabels,
|
||||||
} from '@/components/shared/historyChartModel';
|
} from '@/components/shared/historyChartModel';
|
||||||
import { isRangeLocked, loadRuntimeCapabilities, maxHistoryDays } from '@/stores/license';
|
import { isRangeLocked, loadRuntimeCapabilities, maxHistoryDays } from '@/stores/license';
|
||||||
@@ -82,6 +85,8 @@ interface GuestDrawerHistoryGroupChartProps {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
metrics: Record<string, AggregatedMetricPoint[] | undefined>;
|
metrics: Record<string, AggregatedMetricPoint[] | undefined>;
|
||||||
range: HistoryTimeRange;
|
range: HistoryTimeRange;
|
||||||
|
/** Where the reply's readings came from; names the readout's time. */
|
||||||
|
source?: HistorySource;
|
||||||
sourceKey: string;
|
sourceKey: string;
|
||||||
timeBounds: GuestDrawerHistoryTimeBounds | null;
|
timeBounds: GuestDrawerHistoryTimeBounds | null;
|
||||||
touchInspection: boolean;
|
touchInspection: boolean;
|
||||||
@@ -506,9 +511,10 @@ const GuestDrawerHistoryGroupChart: Component<GuestDrawerHistoryGroupChartProps>
|
|||||||
data-testid="guest-history-hover-time"
|
data-testid="guest-history-hover-time"
|
||||||
dateTime={time().dateTime}
|
dateTime={time().dateTime}
|
||||||
title={time().fullLabel}
|
title={time().fullLabel}
|
||||||
aria-label={`Stored observation: ${time().fullLabel}`}
|
aria-label={`${getHistoryReadoutLabel(props.source, true)} observation: ${time().fullLabel}`}
|
||||||
>
|
>
|
||||||
{activeTimestamp() === null ? 'Latest stored · ' : 'Stored · '}
|
{getHistoryReadoutLabel(props.source, activeTimestamp() !== null)}
|
||||||
|
{' · '}
|
||||||
{time().label}
|
{time().label}
|
||||||
</time>
|
</time>
|
||||||
)}
|
)}
|
||||||
@@ -986,6 +992,14 @@ export const GuestDrawerHistory: Component<GuestDrawerHistoryProps> = (props) =>
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show when={!historyQuery.error() || hasHistoryPoints()}>
|
<Show when={!historyQuery.error() || hasHistoryPoints()}>
|
||||||
|
{/* A reply that is not stored history says so once, above its charts. */}
|
||||||
|
<Show when={hasHistoryPoints() && getHistorySourceNotice(historyQuery.value().source)}>
|
||||||
|
{(notice) => (
|
||||||
|
<p class="text-xs text-muted" data-testid="history-source-notice">
|
||||||
|
{notice().detail}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
<div
|
<div
|
||||||
id={gridId}
|
id={gridId}
|
||||||
class={`grid gap-3 md:grid-cols-2 ${groups().length > 3 ? 'xl:grid-cols-4' : 'xl:grid-cols-3'}`}
|
class={`grid gap-3 md:grid-cols-2 ${groups().length > 3 ? 'xl:grid-cols-4' : 'xl:grid-cols-3'}`}
|
||||||
@@ -1001,6 +1015,7 @@ export const GuestDrawerHistory: Component<GuestDrawerHistoryProps> = (props) =>
|
|||||||
currentMetrics={props.currentMetrics}
|
currentMetrics={props.currentMetrics}
|
||||||
deferredMetrics={props.deferredMetrics}
|
deferredMetrics={props.deferredMetrics}
|
||||||
range={props.range}
|
range={props.range}
|
||||||
|
source={historyQuery.value().source}
|
||||||
sourceKey={`${props.target?.resourceType}:${props.target?.resourceId}:${props.range}`}
|
sourceKey={`${props.target?.resourceType}:${props.target?.resourceId}:${props.range}`}
|
||||||
timeBounds={timeBounds()}
|
timeBounds={timeBounds()}
|
||||||
touchInspection={touchInspection()}
|
touchInspection={touchInspection()}
|
||||||
|
|||||||
@@ -71,6 +71,30 @@ const selectSharedTime = (chart: HTMLElement) => {
|
|||||||
return chart.querySelector('[data-history-hover-marker]')!;
|
return chart.querySelector('[data-history-hover-marker]')!;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
describe('GuestDrawerHistory reading source', () => {
|
||||||
|
const readout = () => within(group('utilization')).getByTestId('guest-history-hover-time');
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['store', 'Latest stored', 'Stored', null],
|
||||||
|
['memory', 'Latest recorded', 'Recorded', 'Recent readings only.'],
|
||||||
|
['live', 'Current', 'Current', 'No history for this range yet.'],
|
||||||
|
] as const)('names %s readings as such', async (source, latest, inspected, notice) => {
|
||||||
|
vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue(response({ source }));
|
||||||
|
mount();
|
||||||
|
const slider = await screen.findByRole('slider', { name: 'Inspect Utilization history' });
|
||||||
|
expect(readout().textContent).toMatch(new RegExp(`^${latest} · `));
|
||||||
|
if (notice) expect(screen.getByTestId('history-source-notice')).toHaveTextContent(notice);
|
||||||
|
else expect(screen.queryByTestId('history-source-notice')).toBeNull();
|
||||||
|
slider.focus();
|
||||||
|
fireEvent.input(slider, { target: { value: '0' } });
|
||||||
|
expect(readout().textContent).toMatch(new RegExp(`^${inspected} · `));
|
||||||
|
expect(readout()).toHaveAttribute(
|
||||||
|
'aria-label',
|
||||||
|
expect.stringMatching(new RegExp(`^${inspected} observation: `)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('GuestDrawerHistory value axis', () => {
|
describe('GuestDrawerHistory value axis', () => {
|
||||||
it('labels a quiet link\'s rate axis once, not "<1K/s" on two gridlines', async () => {
|
it('labels a quiet link\'s rate axis once, not "<1K/s" on two gridlines', async () => {
|
||||||
vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue(
|
vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, Show } from 'solid-js';
|
import { Component, Show } from 'solid-js';
|
||||||
import { formatHistoryChartTooltipValue } from './historyChartModel';
|
import { formatHistoryChartTooltipValue, getHistorySourceNotice } from './historyChartModel';
|
||||||
import type { HistoryChartState } from './useHistoryChartState';
|
import type { HistoryChartState } from './useHistoryChartState';
|
||||||
|
|
||||||
interface HistoryChartHeaderProps {
|
interface HistoryChartHeaderProps {
|
||||||
@@ -20,21 +20,19 @@ export const HistoryChartHeader: Component<HistoryChartHeaderProps> = (props) =>
|
|||||||
<Show when={props.unit}>
|
<Show when={props.unit}>
|
||||||
<span class="text-xs text-slate-400">({props.unit})</span>
|
<span class="text-xs text-slate-400">({props.unit})</span>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.chart.source() && props.chart.source() !== 'store'}>
|
<Show when={getHistorySourceNotice(props.chart.source())}>
|
||||||
<span
|
{(notice) => (
|
||||||
class={`text-[10px] font-semibold px-2 py-0.5 rounded-full uppercase tracking-wide ${
|
<span
|
||||||
props.chart.source() === 'live'
|
class={`text-[10px] font-semibold px-2 py-0.5 rounded-full uppercase tracking-wide ${
|
||||||
? 'bg-red-100 text-red-700 dark:bg-red-900/25 dark:text-red-300'
|
props.chart.source() === 'live'
|
||||||
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/25 dark:text-amber-300'
|
? 'bg-red-100 text-red-700 dark:bg-red-900/25 dark:text-red-300'
|
||||||
}`}
|
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/25 dark:text-amber-300'
|
||||||
title={
|
}`}
|
||||||
props.chart.source() === 'live'
|
title={notice().detail}
|
||||||
? 'Live sample shown because history is not available yet.'
|
>
|
||||||
: 'In-memory buffer shown while history is warming up.'
|
{notice().badge}
|
||||||
}
|
</span>
|
||||||
>
|
)}
|
||||||
{props.chart.source() === 'live' ? 'Live' : 'Buffer'}
|
|
||||||
</span>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
getHistoryChartTimeTickCount,
|
getHistoryChartTimeTickCount,
|
||||||
getHistoryChartTooltipLayout,
|
getHistoryChartTooltipLayout,
|
||||||
getHistoryChartYAxisLabels,
|
getHistoryChartYAxisLabels,
|
||||||
|
getHistoryReadoutLabel,
|
||||||
|
getHistorySourceNotice,
|
||||||
hasHistoryChartSpread,
|
hasHistoryChartSpread,
|
||||||
HISTORY_CHART_RANGES,
|
HISTORY_CHART_RANGES,
|
||||||
withoutRepeatedHistoryAxisLabels,
|
withoutRepeatedHistoryAxisLabels,
|
||||||
@@ -131,6 +133,22 @@ vi.mock('@/api/charts', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
describe('History source wording', () => {
|
||||||
|
it('names the readout and badge by where the readings came from', () => {
|
||||||
|
expect(getHistoryReadoutLabel('store', false)).toBe('Latest stored');
|
||||||
|
expect(getHistoryReadoutLabel('store', true)).toBe('Stored');
|
||||||
|
expect(getHistoryReadoutLabel('mock_synthetic', false)).toBe('Latest stored');
|
||||||
|
expect(getHistoryReadoutLabel(undefined, false)).toBe('Latest stored');
|
||||||
|
expect(getHistoryReadoutLabel('memory', false)).toBe('Latest recorded');
|
||||||
|
expect(getHistoryReadoutLabel('memory', true)).toBe('Recorded');
|
||||||
|
expect(getHistoryReadoutLabel('live', false)).toBe('Current');
|
||||||
|
expect(getHistorySourceNotice('store')).toBeNull();
|
||||||
|
expect(getHistorySourceNotice('mock_synthetic')).toBeNull();
|
||||||
|
expect(getHistorySourceNotice('memory')?.badge).toBe('Recent only');
|
||||||
|
expect(getHistorySourceNotice('live')?.badge).toBe('Current only');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('History chart min-max band', () => {
|
describe('History chart min-max band', () => {
|
||||||
const at = (timestamp: number, value: number, min = value, max = value) => ({
|
const at = (timestamp: number, value: number, min = value, max = value) => ({
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -845,11 +863,13 @@ describe('History organisation boundary rendering', () => {
|
|||||||
touchPointer(canvas, 'pointerup');
|
touchPointer(canvas, 'pointerup');
|
||||||
}
|
}
|
||||||
expect(container.querySelector('[data-history-chart-tooltip]')).toHaveTextContent('42°C');
|
expect(container.querySelector('[data-history-chart-tooltip]')).toHaveTextContent('42°C');
|
||||||
|
// Readings from memory say so in words a reader knows, not "Buffer".
|
||||||
|
expect(container).toHaveTextContent('Recent only');
|
||||||
eventBus.emit('org_switched', 'org-b');
|
eventBus.emit('org_switched', 'org-b');
|
||||||
expect(container.querySelector('[data-history-chart-tooltip]')).toBeNull();
|
expect(container.querySelector('[data-history-chart-tooltip]')).toBeNull();
|
||||||
expect(container.querySelector('[aria-live="polite"]')?.textContent).toBe('');
|
expect(container.querySelector('[aria-live="polite"]')?.textContent).toBe('');
|
||||||
expect(container).not.toHaveTextContent('42°C');
|
expect(container).not.toHaveTextContent('42°C');
|
||||||
expect(container).not.toHaveTextContent('Buffer');
|
expect(container).not.toHaveTextContent('Recent only');
|
||||||
expect(canvas).toHaveAccessibleDescription(/Loading 1-hour history data/);
|
expect(canvas).toHaveAccessibleDescription(/Loading 1-hour history data/);
|
||||||
complete({
|
complete({
|
||||||
points: [{ timestamp: 1000, value: 80, min: 80, max: 80 }],
|
points: [{ timestamp: 1000, value: 80, min: 80, max: 80 }],
|
||||||
|
|||||||
@@ -181,6 +181,42 @@ export function getHistoryChartDataMax(points: AggregatedMetricPoint[]) {
|
|||||||
return max;
|
return max;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HistorySource = 'store' | 'memory' | 'live' | 'mock_synthetic';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Names where a History reply's readings came from. `store` is the history
|
||||||
|
* store; `memory` means the store had nothing for the range, so the readings
|
||||||
|
* are the recent ones Pulse holds in memory; `live` is one current reading
|
||||||
|
* because no history exists yet. Demo data reads like stored history.
|
||||||
|
*/
|
||||||
|
export function getHistoryReadoutLabel(
|
||||||
|
source: HistorySource | null | undefined,
|
||||||
|
inspected: boolean,
|
||||||
|
): string {
|
||||||
|
if (source === 'live') return 'Current';
|
||||||
|
if (source === 'memory') return inspected ? 'Recorded' : 'Latest recorded';
|
||||||
|
return inspected ? 'Stored' : 'Latest stored';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A short badge and its explanation for a reply that is not stored history. */
|
||||||
|
export function getHistorySourceNotice(
|
||||||
|
source: HistorySource | null | undefined,
|
||||||
|
): { badge: string; detail: string } | null {
|
||||||
|
if (source === 'memory') {
|
||||||
|
return {
|
||||||
|
badge: 'Recent only',
|
||||||
|
detail: 'Recent readings only. The history store has none for this range yet.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (source === 'live') {
|
||||||
|
return {
|
||||||
|
badge: 'Current only',
|
||||||
|
detail: 'No history for this range yet. Showing the current reading.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/** True when any stored bucket's lowest and highest readings differ. */
|
/** True when any stored bucket's lowest and highest readings differ. */
|
||||||
export function hasHistoryChartSpread(points: readonly AggregatedMetricPoint[]): boolean {
|
export function hasHistoryChartSpread(points: readonly AggregatedMetricPoint[]): boolean {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user