From 19fddb2cc1c4ba19414d33729cde2a99487bdee3 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 19 Mar 2026 22:23:36 +0000 Subject: [PATCH] Demote resource drawer host detail cards --- .../internal/subsystems/unified-resources.md | 4 + .../Infrastructure/ResourceDetailDrawer.tsx | 113 +++++++++++++----- .../ResourceDetailDrawer.history.test.tsx | 1 + ...urceDetailDrawer.identity-runtime.test.tsx | 55 +++++++++ 4 files changed, 146 insertions(+), 27 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 209bc15a7..7b0a049f7 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -264,6 +264,10 @@ That discovery surface is now a compact support row with a one-line summary and an on-demand metadata panel, so the actionable web-interface path stays primary while deeper discovery inspection remains available without reading like a second peer overview surface. +Host and node system or hardware cards now also live behind a collapsed +`Host details` support block instead of rendering before the primary overview +cards, so runtime status, identity, and next investigation steps stay first +while deeper machine detail remains available on demand. The same facet bundle now also returns grouped recent-change counts by canonical change kind, so the detail drawer can surface the distribution of state transitions, restarts, config updates, and anomalies without diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx index c433491e7..d0405dbc9 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx @@ -143,6 +143,7 @@ const DrawerContent: Component = (props) => { const [showInvestigationContext, setShowInvestigationContext] = createSignal(false); const [showCorrelationContext, setShowCorrelationContext] = createSignal(false); const [showDiscoveryContext, setShowDiscoveryContext] = createSignal(false); + const [showHostDetails, setShowHostDetails] = createSignal(false); const displayName = createMemo(() => getPreferredResourceDisplayName(props.resource)); const kubernetesClusterName = createMemo(() => @@ -550,6 +551,38 @@ const DrawerContent: Component = (props) => { return config.hostname ? `${discoveryMode} via ${config.hostname}` : discoveryMode; }); + const hostDetailCards = createMemo(() => { + const cards: string[] = []; + + if (proxmoxNode()) { + cards.push('system', 'hardware', 'storage'); + } + + const agent = agentInfo(); + if (agent) { + cards.push('system', 'hardware'); + if ((agent.networkInterfaces?.length ?? 0) > 0) cards.push('network'); + if ((agent.disks?.length ?? 0) > 0) cards.push('disks'); + if ((agentMeta()?.raid?.length ?? 0) > 0) cards.push('raid'); + if (temperatureRows().length > 0) cards.push('temperatures'); + } + + return cards; + }); + const hasHostDetails = createMemo(() => hostDetailCards().length > 0); + const hostDetailSummary = createMemo(() => { + const labels = Array.from(new Set(hostDetailCards())); + if (labels.length === 0) return null; + + const categories = + labels.length === 1 + ? labels[0] + : labels.length === 2 + ? `${labels[0]} and ${labels[1]}` + : `${labels.slice(0, -1).join(', ')}, and ${labels[labels.length - 1]}`; + + return `${hostDetailCards().length} detail card${hostDetailCards().length === 1 ? '' : 's'} covering ${categories}.`; + }); const workloadsHref = createMemo(() => buildWorkloadsHref(props.resource)); const headerIdentity = createMemo(() => getPrimaryResourceIdentity(props.resource)); const relatedLinks = createMemo(() => { @@ -769,33 +802,7 @@ const DrawerContent: Component = (props) => { {/* Overview Tab */}
- -
- - {(node) => ( - <> - - - - - )} - - - {(agent) => ( - <> - - - - - - - - )} - -
-
- -
+
Runtime @@ -1349,6 +1356,58 @@ const DrawerContent: Component = (props) => {
+ +
+
+
+
+ Host details +
+
+ Secondary system and hardware detail for deeper inspection. +
+ +
{hostDetailSummary()}
+
+
+ + +
+ + +
+ + {(node) => ( + <> + + + + + )} + + + {(agent) => ( + <> + + + + + + + + )} + +
+
+
+
+
{ const changeHistorySection = screen.getByTestId('resource-change-history-section'); expect(screen.queryByRole('button', { name: 'Discovery' })).toBeNull(); expect(screen.getByText('Change history')).toBeInTheDocument(); + expect(screen.queryByText('Host details')).toBeNull(); expect(screen.getByText('Discovery context')).toBeInTheDocument(); expect( screen.getByText('Supporting metadata only. The web interface path above stays primary.'), diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx index 58c47b1ca..6ad791789 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx @@ -147,6 +147,61 @@ describe('ResourceDetailDrawer runtime and identity cards', () => { }); }); + it('keeps host detail cards behind a secondary overview disclosure', async () => { + const resource = baseResource({ + platformData: { + sources: ['agent'], + agent: { + agentId: 'agent-1', + hostname: 'host-1', + platform: 'linux', + osName: 'Ubuntu', + osVersion: '24.04', + kernelVersion: '6.8.0', + architecture: 'x86_64', + uptimeSeconds: 7200, + cpuCount: 8, + agentVersion: '1.2.3', + memory: { total: 16 * 1024 * 1024 * 1024 }, + networkInterfaces: [ + { + name: 'eth0', + mac: '00:11:22:33:44:55', + addresses: ['192.0.2.10'], + }, + ], + disks: [ + { + mountpoint: '/', + total: 100 * 1024 * 1024 * 1024, + used: 50 * 1024 * 1024 * 1024, + }, + ], + }, + }, + }); + + const { getByRole, getByText, queryByText } = render(() => ( + + )); + + expect(getByText('Host details')).toBeInTheDocument(); + expect( + getByText('4 detail cards covering system, hardware, network, and disks.'), + ).toBeInTheDocument(); + expect(queryByText('Hardware')).toBeNull(); + expect(queryByText('Network')).toBeNull(); + + fireEvent.click(getByRole('button', { name: 'Show host details' })); + + await waitFor(() => { + expect(getByText('Hardware')).toBeInTheDocument(); + }); + expect(getByText('Network')).toBeInTheDocument(); + expect(getByText('Disks')).toBeInTheDocument(); + expect(getByText('eth0')).toBeInTheDocument(); + }); + it('falls back to source list summary when per-source health is unavailable', () => { const resource = baseResource({ sourceType: 'api',