mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 21:20:00 +00:00
Show one chart panel in phone drawers and the rest on request (#2923)
Stacked on rcourtman/Pulse#2921, which sits on rcourtman/Pulse#2920; this branch contains both. It lands after them or together with them. With history charts on every drawer's Overview, a phone stacked three or four chart panels before a guest's facts, a node's hardware or an agent's details. At 390px a guest drawer was 1043px tall. Below the `sm` breakpoint, the shared drawer chart component (`GuestDrawerHistory`, used by the guest, node, Docker host and resource-detail drawers) now shows: - the first panel (Utilization), plus any panel carrying an active alert; - one full-width "Show Network I/O, Disk I/O, Thermals" button (`aria-expanded`) that reveals the rest and then reads "Show fewer charts". Wider layouts are unchanged. It is a CSS class on panels that stay mounted, so it adds no read. Measured at 390px on the built preview: | Drawer | Height | Notes | | --- | --- | --- | | Guest | 657px (was 1043px) | | | Node | 597px | 3 panels collapsed | | Agent | 715px | 3 panels collapsed | At 1440px every panel shows and there is no toggle. Tests: the new cases in `GuestDrawerHistory.inspection.test.tsx` cover the collapse, the toggle, and an alerting panel staying visible. Typecheck plus the Workloads, Infrastructure, Docker and Storage suites pass on pulse-dev.
This commit is contained in:
@@ -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 <panel names>" 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -71,6 +71,8 @@ interface GuestDrawerHistoryRangeSelectProps {
|
||||
|
||||
interface GuestDrawerHistoryGroupChartProps {
|
||||
alerts?: Record<string, GuestDrawerHistoryAlert | undefined>;
|
||||
/** Collapsed below the sm breakpoint until the reader asks for more charts. */
|
||||
hiddenOnPhone?: boolean;
|
||||
currentMetrics?: Record<string, number | null | undefined>;
|
||||
deferredMetrics?: Record<string, GuestDrawerHistoryDeferredMetric | undefined>;
|
||||
group: GuestDrawerHistoryGroupConfig;
|
||||
@@ -472,8 +474,9 @@ const GuestDrawerHistoryGroupChart: Component<GuestDrawerHistoryGroupChartProps>
|
||||
|
||||
return (
|
||||
<section
|
||||
class="flex min-h-[154px] flex-col rounded-xs border border-border bg-surface p-2.5"
|
||||
class={`flex min-h-[154px] flex-col rounded-xs border border-border bg-surface p-2.5 ${props.hiddenOnPhone ? 'max-sm:hidden' : ''}`}
|
||||
data-testid="guest-history-group-chart"
|
||||
data-phone-collapsed={props.hiddenOnPhone ? 'true' : undefined}
|
||||
data-history-group={props.group.id}
|
||||
>
|
||||
<div class="mb-2 flex flex-wrap items-start justify-between gap-x-4 gap-y-1">
|
||||
@@ -806,6 +809,12 @@ const GuestDrawerHistoryGroupChart: Component<GuestDrawerHistoryGroupChartProps>
|
||||
|
||||
export const GuestDrawerHistory: Component<GuestDrawerHistoryProps> = (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<GuestDrawerHistoryProps> = (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<GuestDrawerHistoryProps> = (props) =>
|
||||
>
|
||||
<Show when={!historyQuery.error() || hasHistoryPoints()}>
|
||||
<div
|
||||
id={gridId}
|
||||
class={`grid gap-3 md:grid-cols-2 ${groups().length > 3 ? 'xl:grid-cols-4' : 'xl:grid-cols-3'}`}
|
||||
>
|
||||
<For each={groups()}>
|
||||
{(group) => (
|
||||
{(group, index) => (
|
||||
<GuestDrawerHistoryGroupChart
|
||||
alerts={props.alerts}
|
||||
hiddenOnPhone={isPhoneCollapsed(group, index())}
|
||||
group={group}
|
||||
loading={historyQuery.loading() && !historyQuery.resolvedOnce()}
|
||||
metrics={metrics()}
|
||||
@@ -922,6 +940,22 @@ export const GuestDrawerHistory: Component<GuestDrawerHistoryProps> = (props) =>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={phoneCollapsedGroups().length > 0 || showAllOnPhone()}>
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-h-11 w-full items-center justify-center gap-1 rounded-xs border border-border bg-surface px-3 text-xs font-medium text-base-content sm:hidden"
|
||||
aria-controls={gridId}
|
||||
aria-expanded={showAllOnPhone()}
|
||||
data-testid="history-phone-toggle"
|
||||
onClick={() => setShowAllOnPhone((open) => !open)}
|
||||
>
|
||||
{showAllOnPhone()
|
||||
? 'Show fewer charts'
|
||||
: `Show ${phoneCollapsedGroups()
|
||||
.map((group) => group.label)
|
||||
.join(', ')}`}
|
||||
</button>
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
+44
@@ -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(() => <GuestDrawerHistory target={target} range="24h" />);
|
||||
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(() => (
|
||||
<GuestDrawerHistory
|
||||
target={target}
|
||||
range="24h"
|
||||
alerts={{ diskread: { severity: 'warning', value: 1024 } }}
|
||||
/>
|
||||
));
|
||||
await screen.findAllByTestId('guest-history-group-chart');
|
||||
expect(groupIds()).toEqual(['utilization', 'disk-io']);
|
||||
expect(screen.getByTestId('history-phone-toggle')).toHaveTextContent('Show Network I/O');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user