diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 1465e603f..14db02fad 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -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. diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index ded7b81d0..5342ba6fd 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -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" diff --git a/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx b/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx index e8f2f7c27..d90be587f 100644 --- a/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx @@ -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(() => ( + + )); + 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']); }); diff --git a/frontend-modern/src/components/shared/useHistoryChartState.ts b/frontend-modern/src/components/shared/useHistoryChartState.ts index e32f39d77..51328aaa4 100644 --- a/frontend-modern/src/components/shared/useHistoryChartState.ts +++ b/frontend-modern/src/components/shared/useHistoryChartState.ts @@ -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 () => {