diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index f6fda0fd90..2c8c2f6652 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -8304,3 +8304,12 @@ chain at desktop and phone widths with keyboard/touch refresh, class-dark CSS, 403/503 controls and pending recovery. Unit controls separately cover current request ownership and disposal. No mutation, route, navigation entry, queue policy, permission or recipient-delivery contract is added by these states. + +### Platform overviews may lead with their own incident table + +A platform Overview that leads with its provider's health signals reuses +that platform's existing incident table rather than a new summary card: +the vSphere Overview renders `VsphereAlertsTable` with `showToolbar={false}` +above its hosts table, only when signals exist, matching the TrueNAS +Overview's health-alert table. Filtering stays on the platform's Health tab. + diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index ae04260b9a..5b32979bee 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -5859,3 +5859,16 @@ The Actions page now hides its Patrol Watch only guidance while the presentation only: the action audit list, approval counts, review dialog, routing and every recorded action outcome are read and rendered exactly as before, and no resource or action identity changes. + +### vSphere Overview leads with vCenter health signals + +The vSphere Overview answers "is anything in trouble?" before inventory, as +the TrueNAS Overview does. While the Overview is active the page also +hydrates the Health tab's resource query (hosts, VMs, datastores and +networks) and, when `buildVmwareIncidentRows` returns any rows from those +resources' canonical `incidents`, renders the shared `VsphereAlertsTable` +above the hosts table without its filter toolbar. Nothing renders when +vCenter reports no signals. The Overview's own model, workload snapshot and +navigation facets still come from the Overview query; the Health tab keeps +the filterable table. + diff --git a/frontend-modern/browser-verification/6c5983f639b94947.json b/frontend-modern/browser-verification/6c5983f639b94947.json new file mode 100644 index 0000000000..5bd288246b --- /dev/null +++ b/frontend-modern/browser-verification/6c5983f639b94947.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "base_sha": "efe6fc21ae10a6bf173cc17e6203aaf6c97dc130", + "verified_at": "2026-10-05T00:25:56Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/features/vmware/VmwarePageSurface.tsx" + ], + "content_sha256": { + "frontend-modern/src/features/vmware/VmwarePageSurface.tsx": "cab90be0a9a6868aebac459576f63b93cd4700a8c08fc3ddae4092b7e1fc04bf" + }, + "routes": [ + "/vmware/overview", + "/vmware/health" + ], + "viewports": [ + { + "width": 1440, + "height": 1000 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "Mock Lab vCenter with five warning signals (two datastores, one host, two networks). Before: the Overview showed only hosts and VMs, esxi-07 tinted yellow with no reason; the alarms lived on the fourth tab.", + "After at 1440: a Health Signals table (Resource, Severity, Signal, vCenter, Entity, Started) leads the Overview above Hosts, with no filter toolbar; the yellow esxi-07 row is explained by the Host has VMware overall status yellow signal.", + "Phone 390x844: the same table shows Resource, Signal, VC and Age above the hosts table; rows open the existing detail drawer." + ], + "interactions": [ + "Loaded the Overview and Health tabs signed in at both widths and inspected the screenshots; component tests cover the toolbar-less table and the overview hydrating the Health query only on Overview and Health." + ] +} diff --git a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx index 704ad5107e..2f2468ae51 100644 --- a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx +++ b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx @@ -53,6 +53,7 @@ import type { Resource } from '@/types/resource'; import { VsphereHostsTable } from './VsphereHostsTable'; import { VMWARE_TAB_SPECS, + buildVmwareIncidentRows, buildVmwarePageModel, getVmwarePageTabSpecs, type VmwarePageModel, @@ -207,7 +208,10 @@ export function VmwarePageSurface() { const activeTab = createMemo(() => tabs().some((tab) => tab.id === requestedTab()) ? requestedTab() : 'overview', ); - const shouldHydrateTab = (tab: VmwarePageTabId) => activeTab() === tab; + // The overview leads with vCenter's own health signals, so it reads the + // Health tab's resource set as well as its own hosts and VMs. + const shouldHydrateTab = (tab: VmwarePageTabId) => + activeTab() === tab || (tab === 'health' && activeTab() === 'overview'); const createTabResources = (tab: VmwarePageTabId) => useUnifiedResources({ query: VMWARE_RESOURCE_QUERY_BY_TAB[tab], @@ -258,6 +262,9 @@ export function VmwarePageSurface() { }, ); const model = createMemo(() => buildVmwarePageModel(resources(), activityTimeline() ?? [])); + const overviewIncidents = createMemo(() => + activeTab() === 'overview' ? buildVmwareIncidentRows(healthResources.resources()) : [], + ); const agentUpdateTargetVersion = createMemo( () => updateStore.versionInfo()?.agentUpdateTargetVersion, ); @@ -343,6 +350,15 @@ export function VmwarePageSurface() { />
+ 0}> + + { ); }); + it('leads the overview with vCenter health signals only when vCenter reports any', () => { + const host = makeResource({ + id: 'esxi-host-1', + type: 'agent', + vmware: { entityType: 'host', managedObjectId: 'host-1' }, + }); + const alarmedDatastore = makeResource({ + id: 'datastore-1', + type: 'storage', + vmware: { entityType: 'datastore', managedObjectId: 'datastore-1' }, + incidents: [{ code: 'vmware_alarm', severity: 'warning', summary: 'Datastore latency' }], + } as Partial & Pick); + // Each query gets its own fixture so the test proves the signals come + // from the Health query while the overview model keeps its own resources. + const byCacheKey = (healthResources: Resource[]) => + mockUseUnifiedResources.mockImplementation((options: { cacheKey?: string }) => { + const resources = options.cacheKey === 'vmware-health' ? healthResources : [host]; + return { + resources: () => resources, + loading: () => false, + error: () => null, + refetch: vi.fn().mockResolvedValue(resources), + }; + }); + + byCacheKey([host]); + const { unmount } = render(() => ); + expect(screen.queryByTestId('alerts-table')).not.toBeInTheDocument(); + unmount(); + + byCacheKey([host, alarmedDatastore]); + render(() => ); + expect(screen.getByTestId('platform-section-tabs')).toHaveAttribute('data-active', 'overview'); + expect(screen.getByTestId('alerts-table')).toBeInTheDocument(); + expect(screen.getByTestId('hosts-table')).toHaveAttribute('data-rows', '1'); + expect( + mockUseUnifiedResources.mock.calls.some( + ([options]) => + (options as { cacheKey?: string }).cacheKey === 'vmware-health' && + (options as { enabled: () => boolean }).enabled(), + ), + ).toBe(true); + }); + it('reuses the source-scoped page snapshot for the vSphere workload surface', async () => { const refetch = vi.fn().mockResolvedValue([]); const resources = [ diff --git a/frontend-modern/src/features/vmware/__tests__/VsphereAlertsTable.test.tsx b/frontend-modern/src/features/vmware/__tests__/VsphereAlertsTable.test.tsx index 81c10176d5..389dd05d76 100644 --- a/frontend-modern/src/features/vmware/__tests__/VsphereAlertsTable.test.tsx +++ b/frontend-modern/src/features/vmware/__tests__/VsphereAlertsTable.test.tsx @@ -42,6 +42,24 @@ afterEach(() => { }); describe('VsphereAlertsTable', () => { + it('renders every signal without the filter toolbar for the overview lead-in', () => { + render(() => ( + } + emptyTitle="No signals" + emptyDescription="No signals" + showToolbar={false} + /> + )); + + // The overview shows the signals as they are; filtering stays on Health. + expect(screen.queryByRole('button', { name: /Attention/ })).not.toBeInTheDocument(); + expect(screen.queryByPlaceholderText('Search vSphere health')).not.toBeInTheDocument(); + expect(document.querySelectorAll('[data-vsphere-alert-row]')).toHaveLength(1); + expect(screen.getByText('Health Signals')).toBeInTheDocument(); + }); + it('renders native vSphere health signal rows with inline details', async () => { const host = makeHost(); const informational = makeHost({