From 5168ae6460521b8cf98149835037802b0c5e457f Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:30:46 +0100 Subject: [PATCH] Chart history on Overview in Docker host and resource drawers PR 2913 moved the Proxmox guest and node drawers' charts onto Overview and removed their History tab. The Docker host drawer and the shared resource-detail drawer (agent machines, containers, PBS, TrueNAS, Kubernetes and other platform rows) still kept the charts a tab away, so the same kind of drawer behaved two ways depending on the page. Both now render the history charts on Overview under the attention list, opening on 24 hours with the range picker beside them, and drop the History tab. Charts stop polling while another tab shows, and an active alert on a charted metric draws its trigger the same way as in the Proxmox drawers. A resource whose metrics target drops for one snapshot simply loses its charts until the target returns; the selected tab is kept. Storage page disk and pool details keep their own History tab; they are separate components and their Overview has no charts to repeat. E2E specs 27 and 63 no longer click History; the drawer read starts when it opens. The manual tab-retention proof now selects Manage. Its fixture page fails before the drawer renders (no WebSocket provider) on main as well, so it was not run. --- .../subsystems/frontend-primitives.md | 7 ++- .../subsystems/performance-and-scalability.md | 9 ++-- .../internal/subsystems/unified-resources.md | 19 +++++--- .../f401b95ebed9ca10.json | 48 +++++++++++++++++++ .../Infrastructure/ResourceDetailDrawer.tsx | 32 ------------- .../ResourceDetailDrawerOverviewTab.tsx | 33 +++++++++++-- ...urceDetailDrawer.docker-container.test.tsx | 9 ++-- .../ResourceDetailDrawer.history.test.tsx | 14 +++--- ...ourceDetailDrawer.machine-history.test.tsx | 24 ++++------ .../ResourceDetailDrawer.snapshot.test.tsx | 28 ++++++----- .../useResourceDetailDrawerDerivedState.ts | 6 +-- .../useResourceDetailDrawerState.ts | 3 +- .../SharedPrimitives.guardrails.test.ts | 9 ++-- .../features/docker/DockerHostDrawer.test.tsx | 28 ++++++++++- .../src/features/docker/DockerHostDrawer.tsx | 31 ++++-------- .../docker/DockerHostDrawerOverview.tsx | 41 ++++++++++++++-- ...rContainersTable.cpu-presentation.test.tsx | 1 - .../__tests__/DockerHostsTable.test.tsx | 3 +- .../__tests__/DockerNativeTables.test.tsx | 9 ++-- scripts/check-drawer-tab-retention.cjs | 40 ++++++++-------- .../27-truenas-storage-disk-history.spec.ts | 4 +- .../tests/63-pbs-history-targets.spec.ts | 2 +- 22 files changed, 247 insertions(+), 153 deletions(-) create mode 100644 frontend-modern/browser-verification/f401b95ebed9ca10.json diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 3cf539efc4..d0c1bab67b 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -1243,7 +1243,7 @@ display identity. The presence of discovery routing does not enable Agent-only disk-throughput charts. Explicit Agent linkage owns that capability distinction. `nodeDrawerModel.branchcov0713.test.ts` pins target precedence and unsupported target fallback, while `NodeDrawer.test.tsx` verifies the chart request and -API-only chart groups through the rendered History tab. +API-only chart groups through the rendered Overview charts. Platform inventory presentation has one structural owner across provider pages, drawers, and inline detail rows. `PlatformTableShell` owns framed page tables; @@ -5457,7 +5457,10 @@ The Proxmox guest and node drawers (`GuestDrawerOverview.tsx`, a named bet): a grid of equal-weight inventory cards buried the drawer's job. Their Overview leads with active alerts (or one "No active alerts" line), then the resource's history charts (`GuestDrawerHistory`, lazy-loaded) opening on -24 hours with the range picker beside them. These drawers have no History tab: +24 hours with the range picker beside them. These drawers have no History tab +(nor, since the same change was carried to them, do the Docker host drawer and +the shared resource-detail drawer for agents, containers and other platform +rows): by maintainer decision on 2026-10-13 the tab was removed because it repeated Overview's charts at another range in another style. The charts poll only while Overview shows, and before any history is stored they show the live 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 bb7beb15bb..8a3f81c6f9 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -2900,8 +2900,11 @@ and pass `enabled={false}` while Manage shows, so one query from `guestDrawerHistoryQuery.ts` polls only while the charts are visible. These drawers have no History tab, so there is no second read of the same resource at another range (the deleted `DrawerTrendCards.tsx` and the History tab each -held one). Docker host and resource-detail drawers keep their History tab on -the same 24h default. +held one). The Docker host drawer and the shared resource-detail drawer +(agents, containers, PBS, TrueNAS, Kubernetes and other platform rows) follow +the same rule: `DockerHostDrawerOverview.tsx` and +`ResourceDetailDrawerOverviewTab.tsx` render `GuestDrawerHistory` on Overview +on the same 24h default and pass `enabled={false}` while another tab shows. Panel width alone does not make mount paths readable. Filesystem disk rows must opt into the shared `layout: 'stacked'` presentation so their full mount path wraps above usage and progress at desktop and phone widths. Shared-prefix paths @@ -3745,7 +3748,7 @@ internal/mock/platform_fixtures_test.go pin the generation behavior. The workload adapter copies resource.parentId into parentResourceId. vSphere workload groups use that server parent identity while cluster, connection and host display labels are enriched. A metadata update therefore keeps the row -and its drawer mounted, including its selected History tab. The workload +and its drawer mounted, including its selected tab. The workload instance fallback uses an explicit cluster and never the workload's own name. Legacy rows without a canonical parent retain the existing scoped fallback. diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index f860737813..6c2a39468b 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -704,7 +704,7 @@ API-only Proxmox hosts remain `agent` resources but their metrics target is linked Agent source retains priority and its `agent` store ID. Consumers must use that target rather than reconstructing storage coordinates from display identity or discovery routing. The node view carries the canonical target into -its History tab. Discovery can route by a host name without an installed Agent, +its Overview charts. Discovery can route by a host name without an installed Agent, so it does not establish a `linkedAgentId` or Agent-only disk-throughput coverage. The explicit Agent facet/link owns that evidence. @@ -7696,14 +7696,19 @@ live snapshot is being replaced. `useResourceDetailDrawerState` falls back to Overview only when the drawer starts showing a different resource; it must not reset merely because the current tab is momentarily absent from the derived tab list. A merged host's metrics target can be omitted for one snapshot during a -refresh, and treating that as a permanent loss of the History tab made the -Proxmox -> Backups drawer silently jump back to Overview. Each tab body already -renders its own availability notice while its tab is unavailable, so the -selection can be retained and recovered without manufacturing a target. The +refresh, and treating that as a permanent loss of the then History tab made +the Proxmox -> Backups drawer silently jump back to Overview. History charts +now sit on Overview and simply withdraw while the target is absent; the other +tab bodies render their own availability notice while their tab is +unavailable, so the selection can be retained and recovered without +manufacturing a target. The `ResourceDetailDrawer.history.test.tsx` guard pins the single-resource reset boundary, and `scripts/check-drawer-tab-retention.cjs` exercises the rendered -drawer at desktop and narrow widths: selecting History, dropping the merged -metrics target, and restoring it must keep the selection and recover the chart. +drawer at desktop and narrow widths: selecting Manage, dropping the merged +metrics target, and restoring it must keep the selection and bring the Overview +charts back. Its fixture page currently fails before the drawer renders +(no WebSocket provider), so that proof does not run until the fixture is +repaired. ### Canonical reference resolution uses a derived alias index diff --git a/frontend-modern/browser-verification/f401b95ebed9ca10.json b/frontend-modern/browser-verification/f401b95ebed9ca10.json new file mode 100644 index 0000000000..3e01723dd3 --- /dev/null +++ b/frontend-modern/browser-verification/f401b95ebed9ca10.json @@ -0,0 +1,48 @@ +{ + "version": 1, + "base_sha": "170f726c9330684c6240203071e106dad63ce1e7", + "verified_at": "2026-10-13T22:28:00Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx", + "frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx", + "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts", + "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts", + "frontend-modern/src/features/docker/DockerHostDrawer.tsx", + "frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx" + ], + "content_sha256": { + "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx": "a9f6116d18eeb473aa86dd91c4b24b98fb1a2e59dda35347718bfb5dbb770f2b", + "frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx": "8df81a27afadf825285b5142c3e4ef7a48a7379b43a6bae1ee46c83b0e921984", + "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts": "864681ed7644f33777d40b0019b405d28c1a0e023f3eb7b8c64a66d984a4f610", + "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts": "ddb5c9e4506ca9cd81034a6759edd2bcf1869f3b4997646e9d4ba15e92ef2e64", + "frontend-modern/src/features/docker/DockerHostDrawer.tsx": "770745fc8b6346218b294957525d35adece0a7076b4c5ea3d90574ed15a1a252", + "frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx": "6d7137c5c6c4fbb868a1d6b87aaa6c79ebce298540df9b8d6d957f9fdc22c989" + }, + "routes": [ + "/docker/overview", + "/docker/containers", + "/standalone/machines" + ], + "viewports": [ + { + "width": 1440, + "height": 900 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "final built bytes of this tree (origin/main 170f726c93 plus this change) embedded in a darwin binary, mock mode with 7 days of seeded metrics, headless Chromium on pulse-dev through a reverse tunnel, full-resolution element screenshots read by eye", + "Docker host drawer (App Platform 01, /docker/overview): tabs read Overview, Manage; Needs attention with 4 active alerts, then the range picker and Refresh history, then Utilization, Network I/O, Disk I/O and Thermals charts at 24 hours, then the operator context, runtime, memory, storage and telemetry cards", + "container drawer (loki, /docker/containers, the shared resource-detail drawer): tabs read Overview, Manage; the CPU-scale note, then three charts (Utilization, Network I/O, Disk I/O) at 24 hours, then runtime, container and identity cards and the relationship map", + "agent machine drawer (Apollo-114, /standalone/machines, the shared resource-detail drawer): tabs read Overview, Manage; four charts including Thermals at 24 hours above the operator, runtime and identity cards and the closed Platform details", + "390px phone: each drawer stacks its charts in one column under the toolbar with no horizontal overflow; 1440px: charts in a row of three or four" + ], + "interactions": [ + "expanded the first Docker host row, the first container row and the first agent machine row by their expand buttons at 1440 and 390px", + "read each drawer tab list (Overview, Manage) and counted the chart panels (4, 3 and 4)" + ] +} diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx index cce4ceb12e..3886c49c83 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx @@ -9,10 +9,6 @@ import { ProxmoxMailGatewayDrawer } from '@/features/proxmox/ProxmoxMailGatewayD import { K8sDeploymentsDrawer } from '@/components/Kubernetes/K8sDeploymentsDrawer'; import { K8sNamespacesDrawer } from '@/components/Kubernetes/K8sNamespacesDrawer'; import { SwarmServicesDrawer } from '@/components/Docker/SwarmServicesDrawer'; -import { - GuestDrawerHistory, - GuestDrawerHistoryRangeSelect, -} from '@/components/Workloads/GuestDrawerHistory'; import { ResourceDetailDrawerDebugTab } from './ResourceDetailDrawerDebugTab'; import { ResourceAccessDisclosure, @@ -193,34 +189,6 @@ const DrawerContent: Component = (props) => { /> - {/* Agent Machine Metrics History Tab */} -
- - } - > - {(target) => ( -
-
- -
- -
- )} -
-
-
-
diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx index 8aab60fc3f..d2b0287b0c 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx @@ -11,6 +11,8 @@ import { useWebSocket } from '@/contexts/appRuntime'; import { alertTypeDisplayLabel } from '@/features/alerts/helpers'; import { useAlertsActivation } from '@/stores/alertsActivation'; import { getAlertsForUnifiedResource } from '@/utils/alerts'; +import { GuestDrawerHistory } from '@/components/Workloads/GuestDrawerHistory'; +import { getDrawerChartAlerts } from '@/components/Workloads/drawerOverviewModel'; import { formatUptime, formatRelativeTime } from '@/utils/format'; import { useRelativeTimeNow } from '@/utils/relativeTimeClock'; import { SystemInfoCard } from '@/components/shared/cards/SystemInfoCard'; @@ -328,12 +330,15 @@ export const ResourceDetailDrawerOverviewTab: Component { - const alerts = getAlertsForUnifiedResource( - resource(), - activeAlerts, - alertsActivation.detectionEnabled(), + const resourceAlerts = () => + getAlertsForUnifiedResource(resource(), activeAlerts, alertsActivation.detectionEnabled()); + const chartAlerts = () => + getDrawerChartAlerts( + resourceAlerts(), + drawer.metricsHistoryGroups().flatMap((group) => group.series.map((series) => series.metric)), ); + const attentionItems = () => { + const alerts = resourceAlerts(); const items: DrawerAttentionItem[] = alerts.map((alert) => ({ id: alert.id, message: alert.message, @@ -379,6 +384,24 @@ export const ResourceDetailDrawerOverviewTab: Component + {/* Stored history leads the reading path: the charts answer "is this + normal or getting worse?" before the inventory below. */} + + {(target) => ( +
+ +
+ )} +
{ expect(onResourceActionSettled).not.toHaveBeenCalled(); }); - it('adds a metrics history tab for app-containers with a metrics target', async () => { + it('charts metrics history on Overview for app-containers with a metrics target', async () => { render(() => ( { /> )); - await fireEvent.click(screen.getByRole('tab', { name: 'History' })); - + expect(screen.queryByRole('tab', { name: 'History' })).not.toBeInTheDocument(); const history = screen.getByTestId('container-history'); expect(history).toHaveAttribute('data-resource-type', 'app-container'); expect(history).toHaveAttribute('data-resource-id', 'abc123def456'); expect(history).toHaveAttribute('data-cpu', '75'); }); - it('omits the history tab when no metrics target resolves', () => { + it('omits history charts when no metrics target resolves', () => { render(() => ( { /> )); - expect(screen.queryByRole('tab', { name: 'History' })).not.toBeInTheDocument(); + expect(screen.queryByTestId('container-history')).not.toBeInTheDocument(); }); it('renders container runtime facts in the table-row summary', () => { diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx index 38b47d45af..3ba2931efb 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx @@ -162,7 +162,7 @@ const baseResource = (overrides: Partial): Resource => ({ }); describe('ResourceDetailDrawer change history section', () => { - it('keeps retained guest memory and paused filesystems out of current History fallbacks', async () => { + it('keeps retained guest memory and paused filesystems out of current chart fallbacks', async () => { resetCreateNonSuspendingQueryCacheForTest(); const source = baseResource({ id: 'provenance-vm', @@ -194,7 +194,6 @@ describe('ResourceDetailDrawer change history section', () => { )); try { - fireEvent.click(screen.getByRole('tab', { name: 'History' })); await waitFor(() => expect(read).toHaveBeenCalledTimes(1)); const chart = view.container.querySelector('[data-history-group="utilization"]')!; expect(chart.querySelector('[data-history-current="memory"]')).toBeNull(); @@ -249,7 +248,7 @@ describe('ResourceDetailDrawer change history section', () => { } }); - it('reactively withdraws stopped VM disk carriers without discarding History or independent readings', async () => { + it('reactively withdraws stopped VM disk carriers without discarding the Overview charts or independent readings', async () => { resetCreateNonSuspendingQueryCacheForTest(); const source = baseResource({ id: 'native-power-vm', @@ -278,7 +277,6 @@ describe('ResourceDetailDrawer change history section', () => { )); try { - fireEvent.click(screen.getByRole('tab', { name: 'History' })); await waitFor(() => expect(read).toHaveBeenCalledTimes(1)); const chart = view.container.querySelector('[data-history-group="utilization"]')!; expect(chart.querySelector('[data-history-current="disk"]')).toHaveTextContent( @@ -308,7 +306,10 @@ describe('ResourceDetailDrawer change history section', () => { expect(view.container.querySelector('[data-history-group="utilization"]')).toBe(chart); expect(chart.querySelector('path[data-history-series]')).toBeNull(); expect(read).toHaveBeenCalledTimes(1); - expect(screen.getByRole('tab', { name: 'History' })).toHaveAttribute('aria-selected', 'true'); + expect(screen.getByRole('tab', { name: 'Overview' })).toHaveAttribute( + 'aria-selected', + 'true', + ); } finally { view.unmount(); read.mockRestore(); @@ -317,7 +318,7 @@ describe('ResourceDetailDrawer change history section', () => { }); it.each(['vm', 'system-container'] as const)( - 'keeps the mounted %s History on the selected filesystem producer', + 'keeps the mounted %s Overview charts on the selected filesystem producer', async (type) => { resetCreateNonSuspendingQueryCacheForTest(); const disks = [{ mountpoint: '/', total: 100, used: 62, free: 38 }]; @@ -350,7 +351,6 @@ describe('ResourceDetailDrawer change history section', () => { )); try { - fireEvent.click(screen.getByRole('tab', { name: 'History' })); await waitFor(() => expect(read).toHaveBeenCalledTimes(1)); const chart = view.container.querySelector('[data-history-group="utilization"]')!; expect(chart.querySelector('[data-history-current="disk"]')).toHaveTextContent( diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.machine-history.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.machine-history.test.tsx index f8e75133db..8f7c9bb1a5 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.machine-history.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.machine-history.test.tsx @@ -78,7 +78,7 @@ afterEach(() => { }); describe('ResourceDetailDrawer machine metrics history', () => { - it('adds a metrics history tab for Pulse Agent machines', async () => { + it('charts metrics history on Overview for Pulse Agent machines, with no History tab', async () => { render(() => ( { /> )); - await fireEvent.click(screen.getByRole('tab', { name: 'History' })); - + expect(screen.queryByRole('tab', { name: 'History' })).not.toBeInTheDocument(); const history = screen.getByTestId('machine-history'); expect(history).toHaveAttribute('data-resource-type', 'agent'); expect(history).toHaveAttribute('data-resource-id', 'agent-mac-mini'); @@ -113,6 +112,7 @@ describe('ResourceDetailDrawer machine metrics history', () => { )); expect(screen.queryByRole('tab', { name: 'History' })).not.toBeInTheDocument(); + expect(screen.queryByTestId('machine-history')).not.toBeInTheDocument(); }); it('renders storage history with the capacity catalog instead of workload metrics', async () => { @@ -127,8 +127,6 @@ describe('ResourceDetailDrawer machine metrics history', () => { /> )); - await fireEvent.click(screen.getByRole('tab', { name: 'History' })); - const history = screen.getByTestId('machine-history'); expect(history).toHaveAttribute('data-resource-type', 'storage'); expect(history).toHaveAttribute('data-groups', 'capacity'); @@ -148,8 +146,6 @@ describe('ResourceDetailDrawer machine metrics history', () => { /> )); - await fireEvent.click(screen.getByRole('tab', { name: 'History' })); - const history = screen.getByTestId('machine-history'); expect(history).toHaveAttribute('data-resource-type', 'disk'); expect(history).toHaveAttribute('data-groups', 'disk-activity,disk-io,disk-thermal'); @@ -168,8 +164,6 @@ describe('ResourceDetailDrawer machine metrics history', () => { /> )); - await fireEvent.click(screen.getByRole('tab', { name: 'History' })); - const history = screen.getByTestId('machine-history'); expect(history).toHaveAttribute('data-resource-type', 'docker-host'); expect(history).toHaveAttribute('data-groups', 'utilization,network,disk-io,thermals'); @@ -270,7 +264,7 @@ describe('ResourceDetailDrawer machine metrics history', () => { expect(within(hostSection).queryByText('Hardware')).toBeNull(); }); - it('keeps the selected History tab across a snapshot refresh that briefly drops the metrics target', async () => { + it('withdraws and restores the Overview charts when a snapshot briefly drops the metrics target', async () => { const initial = resource({ id: 'pbs-1', type: 'pbs', @@ -279,18 +273,16 @@ describe('ResourceDetailDrawer machine metrics history', () => { }); const [pbs, setPbs] = createStore(initial); render(() => ); - - await fireEvent.click(screen.getByRole('tab', { name: 'History' })); expect(screen.getByTestId('machine-history')).toHaveAttribute( 'data-resource-id', 'agent-pbs-1', ); - // A snapshot refresh arrives without the merged metrics target. The drawer - // must not silently discard the user's History selection: it should show - // the in-tab notice and recover when the target returns on the next poll. + // A snapshot refresh arrives without the merged metrics target. Overview + // stays selected without charts, and they return when the target does. setPbs(reconcile({ ...initial, metricsTarget: undefined }, { merge: false })); - expect(screen.getByText('Metrics history is unavailable.')).toBeInTheDocument(); + expect(screen.queryByTestId('machine-history')).not.toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'Overview' })).toHaveAttribute('aria-selected', 'true'); setPbs( reconcile( diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.snapshot.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.snapshot.test.tsx index 25603f651d..106d356864 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.snapshot.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.snapshot.test.tsx @@ -21,13 +21,23 @@ vi.mock('@/components/Workloads/GuestDrawerHistory', () => ({ target: { resourceType: string; resourceId: string }; currentMetrics?: Record; range: string; + onRangeChange?: (range: string) => void; }) => (
+ > + +
), GuestDrawerHistoryRangeSelect: (props: { range: string; @@ -126,7 +136,6 @@ describe('ResourceDetailDrawer snapshot ownership', () => { it('updates derived identity, current metrics and the History target without remounting a same-ID drawer', () => { const [resource, setResource] = createSignal(snapshot()); render(() => ); - fireEvent.click(screen.getByRole('tab', { name: 'History' })); fireEvent.change(screen.getByRole('combobox', { name: 'History range' }), { target: { value: '7d' }, }); @@ -145,16 +154,16 @@ describe('ResourceDetailDrawer snapshot ownership', () => { expect(history).toHaveAttribute('data-range', '7d'); }); - it('withdraws a lost metrics target on an immutable snapshot and recovers the selected tab when it returns', () => { + it('withdraws Overview charts for a lost metrics target on an immutable snapshot and restores them when it returns', () => { const [resource, setResource] = createSignal(snapshot()); render(() => ); - fireEvent.click(screen.getByRole('tab', { name: 'History' })); + expect(screen.getByTestId('snapshot-history')).toHaveAttribute('data-target', 'history-a'); setResource(snapshot({ metricsTarget: undefined })); expect(screen.queryByTestId('snapshot-history')).not.toBeInTheDocument(); - expect(screen.getByText('Metrics history is unavailable.')).toBeInTheDocument(); + expect(screen.queryByRole('tab', { name: 'History' })).not.toBeInTheDocument(); setResource(snapshot()); expect(screen.getByTestId('snapshot-history')).toHaveAttribute('data-target', 'history-a'); - expect(screen.getByRole('tab', { name: 'History' })).toHaveAttribute('aria-selected', 'true'); + expect(screen.getByRole('tab', { name: 'Overview' })).toHaveAttribute('aria-selected', 'true'); }); it('updates availability evidence in Overview instead of retaining the initial snapshot', () => { @@ -188,7 +197,7 @@ describe('ResourceDetailDrawer snapshot ownership', () => { requests.facets.mockReturnValueOnce(new Promise((resolve) => (completeOld = resolve))); const [resource, setResource] = createSignal(snapshot()); const { container } = render(() => ); - fireEvent.click(screen.getByRole('tab', { name: 'History' })); + fireEvent.click(screen.getByRole('tab', { name: 'Manage' })); setResource( snapshot({ id: 'pbs-b', @@ -199,7 +208,7 @@ describe('ResourceDetailDrawer snapshot ownership', () => { ); expect(screen.getByRole('heading', { name: 'PBS B' })).toBeInTheDocument(); expect(screen.getByRole('tab', { name: 'Overview' })).toHaveAttribute('aria-selected', 'true'); - expect(screen.queryByTestId('snapshot-history')).not.toBeInTheDocument(); + expect(screen.getByTestId('snapshot-history')).toHaveAttribute('data-target', 'history-b'); await waitFor(() => expect(requests.facets).toHaveBeenCalledWith('pbs-b', { limit: 25 })); completeOld({ capabilities: [], @@ -211,14 +220,11 @@ describe('ResourceDetailDrawer snapshot ownership', () => { await Promise.resolve(); expect(container).not.toHaveTextContent('Former resource change'); expect(container).not.toHaveTextContent('PBS A'); - fireEvent.click(screen.getByRole('tab', { name: 'History' })); - expect(screen.getByTestId('snapshot-history')).toHaveAttribute('data-target', 'history-b'); }); it('keeps the reconciled-store client and same-ID tab continuity working', () => { const [resource, setResource] = createStore(snapshot()); render(() => ); - fireEvent.click(screen.getByRole('tab', { name: 'History' })); const history = screen.getByTestId('snapshot-history'); setResource(reconcile(snapshot({ cpu: { current: 35 } }))); expect(screen.getByTestId('snapshot-history')).toBe(history); diff --git a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts index 4e3d5097a2..f80753c422 100644 --- a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts +++ b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts @@ -89,8 +89,7 @@ import { } from './resourceDetailDrawerKubernetesModel'; import { getResourceHealthIssuePresentation } from './resourceHealthPresentation'; -type DrawerTab = - 'overview' | 'history' | 'manage' | 'mail' | 'namespaces' | 'deployments' | 'swarm' | 'debug'; +type DrawerTab = 'overview' | 'manage' | 'mail' | 'namespaces' | 'deployments' | 'swarm' | 'debug'; interface UseResourceDetailDrawerDerivedStateOptions { resource: Resource; @@ -376,8 +375,9 @@ export const useResourceDetailDrawerDerivedState = ( const tabs = createMemo(() => { const base = [ + // A resource with stored history charts it on Overview; there is no + // separate History tab repeating those charts at another range. { id: 'overview' as DrawerTab, label: 'Overview' }, - ...(hasMetricsHistory() ? [{ id: 'history' as DrawerTab, label: 'History' }] : []), { id: 'manage' as DrawerTab, label: 'Manage' }, ...(resource().type === 'pmg' ? [{ id: 'mail' as DrawerTab, label: 'Mail' }] : []), ...(resource().type === 'k8s-cluster' diff --git a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts index 2f455c2601..0f43140e7c 100644 --- a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts +++ b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts @@ -8,8 +8,7 @@ import { useResourceDetailDrawerHistoryState } from './useResourceDetailDrawerHi import { useResourceDetailDrawerDerivedState } from './useResourceDetailDrawerDerivedState'; import type { ResourceDetailDrawerPresentation } from './resourceDetailDrawerPresentation'; -type DrawerTab = - 'overview' | 'history' | 'manage' | 'mail' | 'namespaces' | 'deployments' | 'swarm' | 'debug'; +type DrawerTab = 'overview' | 'manage' | 'mail' | 'namespaces' | 'deployments' | 'swarm' | 'debug'; export interface UseResourceDetailDrawerStateOptions { resource: Resource; diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 1d2ce1acc5..6605d8a41a 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -1309,14 +1309,15 @@ describe('shared primitive guardrails', () => { expect(source).toContain("value: 'overview', label: 'Overview'"); expect(source).toContain("value: 'history', label: 'History'"); } - // Guest and node drawers chart their history on Overview itself, with the - // range picker beside the charts. A History tab there repeated Overview's - // charts at another range. - for (const source of [guestDrawerSource, nodeDrawerSource]) { + // Guest, node, Docker host and resource-detail drawers chart their history + // on Overview itself, with the range picker beside the charts. A History + // tab there repeated Overview's charts, or hid them a tab away. + for (const source of [guestDrawerSource, nodeDrawerSource, dockerHostDrawerSource]) { expect(source).toContain('@/components/shared/Subtabs'); expect(source).toContain("value: 'overview', label: 'Overview'"); expect(source).not.toContain("value: 'history', label: 'History'"); } + expect(resourceDetailDrawerSource).not.toContain("activeTab() === 'history'"); expect(resourceDetailDrawerSource).toContain('@/components/shared/Subtabs'); expect(resourceDetailDrawerSource).toContain('tabs={drawer.tabs().map'); diff --git a/frontend-modern/src/features/docker/DockerHostDrawer.test.tsx b/frontend-modern/src/features/docker/DockerHostDrawer.test.tsx index c6497994cf..479d6f3423 100644 --- a/frontend-modern/src/features/docker/DockerHostDrawer.test.tsx +++ b/frontend-modern/src/features/docker/DockerHostDrawer.test.tsx @@ -13,7 +13,20 @@ vi.mock('@/contexts/appRuntime', async (importOriginal) => ({ })); vi.mock('@/components/Workloads/GuestDrawerHistory', () => ({ - GuestDrawerHistory: () =>
, + GuestDrawerHistory: (props: { + target: { resourceType: string; resourceId: string }; + range: string; + enabled?: boolean; + onRangeChange?: (range: string) => void; + }) => ( +
+ ), GuestDrawerHistoryRangeSelect: () =>