mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 05:29:45 +00:00
Show one chart panel in phone drawers and the rest on request
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: a guest drawer ran 1043px tall at 390px wide. Below the sm breakpoint the shared chart component now shows the first panel (Utilization) and any panel carrying an active alert, then one full-width "Show Network I/O, Disk I/O, Thermals" button that reveals the rest. The guest drawer drops to 657px. Wider layouts are unchanged. It is a CSS class on panels that stay mounted, so it adds no read and the hidden panels keep their data for when they are shown.
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