Preserve History pointer inspection during matching live refreshes

Reset pointer state only when the selection changes, not on supplied sample updates. Add a mounted pointer-refresh regression and recheck delayed-target browser states.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot] 2026-10-02 15:23:47 +01:00
parent 93531c9cd3
commit 1177e4d143
4 changed files with 47 additions and 6 deletions

View file

@ -7675,7 +7675,8 @@ metric, range, sampling cap, supplied-data mode or access state changes. Late
successes and failures cannot replace the current selection's samples, loading
or error state, even if cancellation is ignored. Selection changes clear old
readings and hover state while loading; current initial failures remain visible.
Matching background refresh failures retain already loaded samples. Polls never
Matching supplied-sample refreshes retain pointer inspection; changing the selection
clears it. Matching background refresh failures retain already loaded samples. Polls never
overlap and stop for supplied data (including empty arrays), unavailable targets,
locked ranges and unmount.

View file

@ -1,13 +1,13 @@
{
"version": 1,
"base_sha": "9dc6bd0ffe4de520bf9afc43ad10fb169026325d",
"verified_at": "2026-10-02T14:15:24.462877Z",
"base_sha": "93531c9cd3bfc9278633476ba26a1c77e66bc802",
"verified_at": "2026-10-02T14:23:47.145834Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/shared/useHistoryChartState.ts"
],
"content_sha256": {
"frontend-modern/src/components/shared/useHistoryChartState.ts": "ff9dff0db01ae3141b3a130477cb8128ea4da8ea4e93f23c360278f6a47b152a"
"frontend-modern/src/components/shared/useHistoryChartState.ts": "f10b5cfc7cb296c60ad7e87bf7c3001c4308d29af702b8c7a7e77e7c4c4c3fb6"
},
"routes": [
"/browser-tests/history-selection.html"

View file

@ -233,6 +233,32 @@ describe('HistoryChart', () => {
rectSpy.mockRestore();
});
it('keeps pointer inspection when matching supplied samples refresh', () => {
const rectSpy = vi.spyOn(HTMLCanvasElement.prototype, 'getBoundingClientRect').mockReturnValue({
x: 0,
y: 0,
left: 0,
top: 0,
right: 400,
bottom: 120,
width: 400,
height: 120,
toJSON: () => ({}),
});
const samples = (value: number) =>
[1000, 2000, 3000].map((timestamp) => ({ timestamp, value, min: value, max: value }));
const [data, setData] = createSignal(samples(10));
const { container } = render(() => (
<HistoryChart resourceType="agent" resourceId="a" metric="cpu" data={data()} />
));
fireEvent.mouseMove(container.querySelector('canvas')!, { clientX: 220 });
expect(container.querySelector('[data-history-chart-tooltip="true"]')).not.toBeNull();
setData(samples(20));
expect(container.querySelector('[data-history-chart-tooltip="true"]')).not.toBeNull();
expect(container.querySelector('[data-history-chart-tooltip="true"]')).toHaveTextContent('20');
rectSpy.mockRestore();
});
it('exposes the sub-day and Relay history ranges as first-class chart options', () => {
expect(HISTORY_CHART_RANGES).toEqual(['1h', '6h', '12h', '24h', '7d', '14d', '30d', '90d']);
});

View file

@ -90,6 +90,8 @@ export function useHistoryChartState(
const dataMin = createMemo(() => getHistoryChartDataMin(data()));
const dataMax = createMemo(() => getHistoryChartDataMax(data()));
let previousSelection: string | undefined;
// One effect owns a selection, its request and its polling timer. Cleanup
// invalidates completions even when a transport ignores cancellation.
createEffect(() => {
@ -117,8 +119,20 @@ export function useHistoryChartState(
setSource(suppliedData !== undefined ? 'live' : null);
setError(null);
setLoading(false);
setHoveredPoint(null);
setHoveredTimestamp(null);
const selection = JSON.stringify([
resourceType,
resourceId,
metric,
chartRange,
pointsCap,
locked,
suppliedData !== undefined,
]);
if (selection !== previousSelection) {
setHoveredPoint(null);
setHoveredTimestamp(null);
}
previousSelection = selection;
if (suppliedData !== undefined || locked || !resourceId || !resourceType) return;
const loadData = async () => {