diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index ee7417f984..96f4a1bd69 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -5478,7 +5478,10 @@ accessible value states each reading's average and range. Every history range picker (these drawers, storage disk and pool details) names its options through `formatHistoryRangeOptionLabel` in `historyChartModel.ts` ("1 hour", "24 hours", "7 days"), so one range is never shown as "24h", "24 hours" and "Last 24 hours" -in three places. Any +in three places. Below the `sm` breakpoint the chart panels stack, so a phone +shows the first panel (Utilization) plus any panel carrying an active alert, +and one full-width "Show " button (`aria-expanded`, hidden from +`sm` up) reveals the rest; wider layouts always show every panel. Any warning- or danger-toned detail row is lifted into a "Worth checking" section in front, whichever section owns it, so demotion never hides a reason a reading cannot be trusted. Guest diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 650e679ffa..4a7f3d2170 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -4019,8 +4019,10 @@ the compact "Worth checking" list reuses rows already derived, and an alert's dashed trigger line reuses the alert the drawer already holds. The min-max band reads the `min`/`max` each stored bucket already carries: one extra path per series with any spread (none for flat data). Range picker option labels come -from one lookup table (`formatHistoryRangeOptionLabel`). None adds a read or -timer. +from one lookup table (`formatHistoryRangeOptionLabel`). The phone collapse of +later chart panels is a CSS class below `sm` on panels that stay mounted: it +hides nothing from the shared history read and adds no read or timer. None +adds a read or timer. The default stored-history legend resolves one latest timestamp from that same bounded, normalized panel index, then looks up each configured series at diff --git a/frontend-modern/browser-verification/ec5c5b20f28306bf.json b/frontend-modern/browser-verification/ec5c5b20f28306bf.json new file mode 100644 index 0000000000..93554da94e --- /dev/null +++ b/frontend-modern/browser-verification/ec5c5b20f28306bf.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "base_sha": "96d307dcbe2042d70d981fe7ebc3dd2b6f35162f", + "verified_at": "2026-10-13T23:18:00Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx" + ], + "content_sha256": { + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "11ffac0be9469fc1107e63c4e8c99fcd7df26fd401b76ac29664c29cde8a680a" + }, + "routes": [ + "/proxmox/overview", + "/standalone/machines" + ], + "viewports": [ + { + "width": 1440, + "height": 900 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "final built bytes of this tree (current sidecar head with PRs 2920/2921 and this change) embedded in a darwin binary, mock mode, headless Chromium on pulse-dev through a reverse tunnel, full-resolution element screenshots read by eye", + "390px: guest drawer (artifact-cache-310) shows only the Utilization panel, then a full-width \"Show Network I/O, Disk I/O\" button, then the facts strip; drawer height 657px against 1043px with every panel shown", + "390px: node drawer (Analytics A) and agent machine drawer each show Utilization and \"Show Network I/O, Disk I/O, Thermals\" (597px and 715px tall)", + "1440px: guest, node and agent drawers show every panel (3, 4 and 4) and no toggle; layout matches the previous pass" + ], + "interactions": [ + "clicked \"Show Network I/O, Disk I/O\" in the phone guest drawer: Network I/O and Disk I/O panels appeared, the button read \"Show fewer charts\" and the drawer grew to 1043px", + "measured visible panel boxes per drawer at both widths" + ] +} diff --git a/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx b/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx index a47a3a8ed7..95f723e349 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx @@ -71,6 +71,8 @@ interface GuestDrawerHistoryRangeSelectProps { interface GuestDrawerHistoryGroupChartProps { alerts?: Record; + /** Collapsed below the sm breakpoint until the reader asks for more charts. */ + hiddenOnPhone?: boolean; currentMetrics?: Record; deferredMetrics?: Record; group: GuestDrawerHistoryGroupConfig; @@ -472,8 +474,9 @@ const GuestDrawerHistoryGroupChart: Component return (
@@ -806,6 +809,12 @@ const GuestDrawerHistoryGroupChart: Component export const GuestDrawerHistory: Component = (props) => { const [touchInspection, setTouchInspection] = createSignal(false); + // On a phone every panel stacks, so a drawer's Overview ran four charts + // deep before its disks and details. Phones show the first panel, plus any + // panel with an active alert, and the rest on request. Wider layouts show + // every panel; the class only applies below the sm breakpoint. + const [showAllOnPhone, setShowAllOnPhone] = createSignal(false); + const gridId = `history-groups-${createUniqueId()}`; onMount(() => { void loadRuntimeCapabilities(); }); @@ -830,6 +839,13 @@ export const GuestDrawerHistory: Component = (props) => historyQuery.value(), ), ); + const groupHasAlert = (group: GuestDrawerHistoryGroupConfig) => + group.series.some((series) => Boolean(props.alerts?.[series.metric])); + const isPhoneCollapsed = (group: GuestDrawerHistoryGroupConfig, index: number) => + !showAllOnPhone() && index > 0 && !groupHasAlert(group); + const phoneCollapsedGroups = createMemo(() => + groups().filter((group, index) => index > 0 && !groupHasAlert(group)), + ); const hasHistoryPoints = createMemo(() => groups().some((group) => group.series.some( @@ -903,12 +919,14 @@ export const GuestDrawerHistory: Component = (props) => >
3 ? 'xl:grid-cols-4' : 'xl:grid-cols-3'}`} > - {(group) => ( + {(group, index) => ( = (props) => )}
+ 0 || showAllOnPhone()}> + +
diff --git a/frontend-modern/src/components/Workloads/__tests__/GuestDrawerHistory.inspection.test.tsx b/frontend-modern/src/components/Workloads/__tests__/GuestDrawerHistory.inspection.test.tsx index 853e320d4b..9984768f48 100644 --- a/frontend-modern/src/components/Workloads/__tests__/GuestDrawerHistory.inspection.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/GuestDrawerHistory.inspection.test.tsx @@ -408,3 +408,47 @@ describe('drawer History range picker', () => { ]); }); }); + +describe('drawer History on a phone', () => { + const groupIds = () => + screen + .getAllByTestId('guest-history-group-chart') + .filter((chart) => chart.dataset.phoneCollapsed !== 'true') + .map((chart) => chart.dataset.historyGroup); + + it('keeps the first panel and collapses the rest behind one toggle', async () => { + vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue(response()); + render(() => ); + await screen.findAllByTestId('guest-history-group-chart'); + expect(groupIds()).toEqual(['utilization']); + for (const chart of screen.getAllByTestId('guest-history-group-chart').slice(1)) { + expect(chart).toHaveClass('max-sm:hidden'); + } + const toggle = screen.getByTestId('history-phone-toggle'); + expect(toggle).toHaveClass('sm:hidden'); + expect(toggle).toHaveAttribute('aria-expanded', 'false'); + expect(toggle).toHaveTextContent('Show Network I/O, Disk I/O'); + + fireEvent.click(toggle); + expect(toggle).toHaveAttribute('aria-expanded', 'true'); + expect(toggle).toHaveTextContent('Show fewer charts'); + expect(groupIds()).toEqual(['utilization', 'network', 'disk-io']); + for (const chart of screen.getAllByTestId('guest-history-group-chart')) { + expect(chart).not.toHaveClass('max-sm:hidden'); + } + }); + + it('keeps a panel with an active alert visible on a phone', async () => { + vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue(response()); + render(() => ( + + )); + await screen.findAllByTestId('guest-history-group-chart'); + expect(groupIds()).toEqual(['utilization', 'disk-io']); + expect(screen.getByTestId('history-phone-toggle')).toHaveTextContent('Show Network I/O'); + }); +});