Merge remote-tracking branch 'origin/main' into feat/docker-silent-hosts

This commit is contained in:
rcourtman
2026-10-05 03:50:49 +01:00
5 changed files with 121 additions and 1 deletions
@@ -8100,6 +8100,14 @@ read, confirm clear, then release the obsolete response at desktop and phone
widths. Scripted API responses establish component behaviour, not installed
backend deletion or destination delivery.
### 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.
### Large platform notices keep the inventory in view
`PlatformOutdatedAgentNotice` is maintenance guidance, not an incident, so it
@@ -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."
]
}
@@ -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<VmwarePageTabId>(() =>
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() {
/>
<Show when={activeTab() === 'overview'}>
<div class="space-y-4">
<Show when={overviewIncidents().length > 0}>
<VsphereAlertsTable
incidents={overviewIncidents()}
emptyIcon={vmwareIcon()}
emptyTitle="No active vSphere health signals"
emptyDescription="vSphere triggered alarms and overall health signals appear here when vCenter reports them."
showToolbar={false}
/>
</Show>
<VmwareOverview
model={model}
metricDisplayMode={metricDisplayMode}
@@ -186,6 +186,50 @@ describe('VmwarePageSurface contract', () => {
);
});
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<Resource> & Pick<Resource, 'id' | 'type'>);
// 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(() => <VmwarePageSurface />);
expect(screen.queryByTestId('alerts-table')).not.toBeInTheDocument();
unmount();
byCacheKey([host, alarmedDatastore]);
render(() => <VmwarePageSurface />);
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 = [
@@ -42,6 +42,24 @@ afterEach(() => {
});
describe('VsphereAlertsTable', () => {
it('renders every signal without the filter toolbar for the overview lead-in', () => {
render(() => (
<VsphereAlertsTable
incidents={buildVmwareIncidentRows([makeHost()])}
emptyIcon={<span />}
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({