From d19c5dbf06b851d2a657cad8bef4e80db188ffb5 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 22 Mar 2026 22:14:31 +0000 Subject: [PATCH] Extract alert overrides projection model owner --- .../v6/internal/subsystems/alerts.md | 6 +- .../subsystems/frontend-primitives.md | 6 +- .../__tests__/alertOverridesModel.test.ts | 75 ++++ .../features/alerts/alertOverridesModel.ts | 352 ++++++++++++++++++ .../features/alerts/useAlertOverridesState.ts | 332 +---------------- .../pages/__tests__/Alerts.helpers.test.ts | 10 +- .../frontendResourceTypeBoundaries.test.ts | 10 +- 7 files changed, 465 insertions(+), 326 deletions(-) create mode 100644 frontend-modern/src/features/alerts/__tests__/alertOverridesModel.test.ts create mode 100644 frontend-modern/src/features/alerts/alertOverridesModel.ts diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index d50378fc2..00a92efb6 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -328,8 +328,10 @@ ownership, `frontend-modern/src/features/alerts/alertsConfigurationModel.ts` for backend config normalization, factory defaults, docker-gap validation, and save-payload serialization, -`frontend-modern/src/features/alerts/useAlertOverridesState.ts` for raw -override normalization plus resource-backed override projection, and +`frontend-modern/src/features/alerts/alertOverridesModel.ts` for raw override +normalization plus resource-backed override projection, and +`frontend-modern/src/features/alerts/useAlertOverridesState.ts` for reactive +override state, derived resource lists, and overview handoff, and `frontend-modern/src/features/alerts/useAlertDestinationsState.ts` for notification destination reload and persistence. `frontend-modern/src/features/alerts/useAlertWebhookDestinationsState.ts` now diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 6587663b0..86bdabaa4 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -498,8 +498,10 @@ for the default-backed mutable configuration snapshot plus apply/capture/reset ownership, `frontend-modern/src/features/alerts/alertsConfigurationModel.ts` for config normalization, factory defaults, docker-gap validation, and payload -serialization, `frontend-modern/src/features/alerts/useAlertOverridesState.ts` -for override projection and thresholds-facing resource selectors, and +serialization, `frontend-modern/src/features/alerts/alertOverridesModel.ts` +for override normalization and resource-backed projection, and +`frontend-modern/src/features/alerts/useAlertOverridesState.ts` +for reactive override state and thresholds-facing resource selectors, and `frontend-modern/src/features/alerts/useAlertDestinationsState.ts` for notification destination reload and persistence. `frontend-modern/src/features/alerts/useAlertWebhookDestinationsState.ts` now diff --git a/frontend-modern/src/features/alerts/__tests__/alertOverridesModel.test.ts b/frontend-modern/src/features/alerts/__tests__/alertOverridesModel.test.ts new file mode 100644 index 000000000..9b0e88e7c --- /dev/null +++ b/frontend-modern/src/features/alerts/__tests__/alertOverridesModel.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest'; + +import type { Resource } from '@/types/resource'; + +import { + buildProjectedOverrides, + normalizeRawOverridesConfig, +} from '../alertOverridesModel'; + +const makeResource = (overrides: Partial): Resource => + ({ + id: 'resource-1', + name: 'resource-1', + type: 'vm', + ...overrides, + }) as Resource; + +describe('alertOverridesModel', () => { + it('normalizes disk override ids into canonical agent disk keys', () => { + expect( + normalizeRawOverridesConfig({ + 'agent:agent-1/disk:NVMe 0n1': { + disk: { trigger: 90, clear: 85 }, + } as any, + }), + ).toEqual({ + 'agent:agent-1/disk:nvme-0n1': { + disk: { trigger: 90, clear: 85 }, + }, + }); + }); + + it('projects guest overrides without requiring agent-backed resources', () => { + const guest = makeResource({ + id: 'vm-100', + name: 'db-01', + type: 'vm', + platformId: 'qemu/100', + platformData: { + vmid: 100, + node: 'pve-1', + instance: 'qemu/100', + }, + }); + + expect( + buildProjectedOverrides({ + rawConfig: { + 'vm-100': { + cpu: { trigger: 95, clear: 90 }, + disabled: true, + } as any, + }, + nodeResources: [], + vmResources: [guest], + containerResources: [], + storageResources: [], + agentResourceList: [], + dockerHostResources: [], + getChildren: () => [], + pbsInstanceById: new Map(), + }), + ).toEqual([ + expect.objectContaining({ + id: 'vm-100', + type: 'guest', + resourceType: 'VM', + disabled: true, + thresholds: { + cpu: 95, + }, + }), + ]); + }); +}); diff --git a/frontend-modern/src/features/alerts/alertOverridesModel.ts b/frontend-modern/src/features/alerts/alertOverridesModel.ts new file mode 100644 index 000000000..836079335 --- /dev/null +++ b/frontend-modern/src/features/alerts/alertOverridesModel.ts @@ -0,0 +1,352 @@ +import type { PBSInstance } from '@/types/api'; +import type { RawOverrideConfig } from '@/types/alerts'; +import type { Resource } from '@/types/resource'; +import { getActionableAgentIdFromResource } from '@/utils/agentResources'; +import { isAppContainerDiscoveryResourceType } from '@/utils/discoveryTarget'; + +import { + extractTriggerValues, + getAlertResourceDisplayLabel, + guessNumericId, + platformData, +} from './helpers'; +import type { Override } from './types'; + +const asRecord = (value: unknown): Record | undefined => + value && typeof value === 'object' ? (value as Record) : undefined; + +const asString = (value: unknown): string | undefined => + typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined; + +const uniqueIds = (...values: unknown[]): string[] => { + const ids: string[] = []; + const seen = new Set(); + + values.forEach((value) => { + const normalized = asString(value); + if (!normalized || seen.has(normalized)) return; + seen.add(normalized); + ids.push(normalized); + }); + + return ids; +}; + +export const normalizeRawOverridesConfig = ( + rawOverrides: Record, +): Record => { + const cleanedOverrides: Record = {}; + + for (const [key, value] of Object.entries(rawOverrides)) { + const diskMatch = key.match(/^(agent:.+\/disk:)(.+)$/); + if (diskMatch) { + const normalized = + diskMatch[2] + .toLowerCase() + .replace(/[^a-z0-9]/g, '-') + .replace(/-{2,}/g, '-') + .replace(/^-|-$/g, '') || 'unknown'; + cleanedOverrides[diskMatch[1] + normalized] = value; + continue; + } + + cleanedOverrides[key] = value; + } + + return cleanedOverrides; +}; + +export const hostOverrideIdCandidates = (resource: Resource): string[] => { + const data = platformData(resource); + const agent = asRecord(data?.agent); + return uniqueIds( + getActionableAgentIdFromResource(resource), + resource.discoveryTarget?.agentId, + resource.agent?.agentId, + agent?.agentId, + data?.agentId, + resource.id, + ); +}; + +export const dockerHostOverrideIdCandidates = (resource: Resource): string[] => { + const data = platformData(resource); + const docker = asRecord(data?.docker); + const discoveryTarget = resource.discoveryTarget; + return uniqueIds( + isAppContainerDiscoveryResourceType(discoveryTarget?.resourceType) + ? discoveryTarget?.resourceId + : undefined, + docker?.hostSourceId, + data?.hostSourceId, + discoveryTarget?.agentId, + resource.id, + ); +}; + +export const dockerContainerOverrideIdCandidates = ( + host: Resource, + shortId: string, +): string[] => + uniqueIds( + ...dockerHostOverrideIdCandidates(host).map((hostId) => `docker:${hostId}/${shortId}`), + ); + +interface BuildProjectedOverridesArgs { + rawConfig: Record; + nodeResources: Resource[]; + vmResources: Resource[]; + containerResources: Resource[]; + storageResources: Resource[]; + agentResourceList: Resource[]; + dockerHostResources: Resource[]; + getChildren: (resourceId: string) => Resource[]; + pbsInstanceById: Map; +} + +export const buildProjectedOverrides = ({ + rawConfig, + nodeResources, + vmResources, + containerResources, + storageResources, + agentResourceList, + dockerHostResources, + getChildren, + pbsInstanceById, +}: BuildProjectedOverridesArgs): Override[] => { + const overridesList: Override[] = []; + const dockerHostMap = new Map(); + const dockerContainerMap = new Map< + string, + { host: Resource; container: Resource; containerShortId: string } + >(); + const agentMap = new Map(); + + const storageCoords = (resource: Resource): { node: string; instance: string } => { + const data = platformData(resource); + if (resource.type === 'datastore') { + const instance = + (data?.pbsInstanceId as string | undefined) || + resource.parentId || + resource.platformId || + 'pbs'; + const node = (data?.pbsInstanceName as string | undefined) || instance; + return { node, instance }; + } + + return { + node: (data?.node as string | undefined) || '', + instance: (data?.instance as string | undefined) || resource.platformId || '', + }; + }; + + dockerHostResources.forEach((host) => { + dockerHostOverrideIdCandidates(host).forEach((id) => { + dockerHostMap.set(id, host); + }); + + const containers = getChildren(host.id).filter((resource) => resource.type === 'app-container'); + + containers.forEach((container) => { + const shortId = container.id.includes('/') ? container.id.split('/').pop()! : container.id; + dockerContainerOverrideIdCandidates(host, shortId).forEach((resourceId) => { + dockerContainerMap.set(resourceId, { host, container, containerShortId: shortId }); + }); + }); + }); + + agentResourceList.forEach((agentResource) => { + hostOverrideIdCandidates(agentResource).forEach((id) => { + agentMap.set(id, agentResource); + }); + }); + + Object.entries(rawConfig).forEach(([key, thresholds]) => { + const dockerHost = dockerHostMap.get(key); + if (dockerHost) { + overridesList.push({ + id: key, + name: getAlertResourceDisplayLabel(dockerHost), + type: 'dockerHost', + resourceType: 'Container Runtime', + disableConnectivity: thresholds.disableConnectivity || false, + thresholds: extractTriggerValues(thresholds), + }); + return; + } + + const dockerContainer = dockerContainerMap.get(key); + if (dockerContainer) { + const { host, container, containerShortId } = dockerContainer; + const containerName = getAlertResourceDisplayLabel(container, containerShortId); + overridesList.push({ + id: key, + name: containerName, + type: 'dockerContainer', + resourceType: 'Container', + node: getAlertResourceDisplayLabel(host), + instance: getAlertResourceDisplayLabel(host), + disabled: thresholds.disabled || false, + disableConnectivity: thresholds.disableConnectivity || false, + poweredOffSeverity: + thresholds.poweredOffSeverity === 'critical' + ? 'critical' + : thresholds.poweredOffSeverity === 'warning' + ? 'warning' + : undefined, + thresholds: extractTriggerValues(thresholds), + }); + return; + } + + if (key.startsWith('docker:')) { + const [, rest] = key.split(':', 2); + const [hostId, containerId] = (rest || '').split('/', 2); + if (containerId) { + overridesList.push({ + id: key, + name: containerId, + type: 'dockerContainer', + resourceType: 'Container', + node: hostId, + disabled: thresholds.disabled || false, + disableConnectivity: thresholds.disableConnectivity || false, + poweredOffSeverity: + thresholds.poweredOffSeverity === 'critical' + ? 'critical' + : thresholds.poweredOffSeverity === 'warning' + ? 'warning' + : undefined, + thresholds: extractTriggerValues(thresholds), + }); + return; + } + + overridesList.push({ + id: key, + name: hostId || key, + type: 'dockerHost', + resourceType: 'Container Runtime', + disableConnectivity: thresholds.disableConnectivity || false, + thresholds: extractTriggerValues(thresholds), + }); + return; + } + + const diskMatch = key.match(/^agent:(.+)\/disk:(.+)$/); + if (diskMatch) { + const [, agentId, diskLabel] = diskMatch; + const agent = agentMap.get(agentId); + overridesList.push({ + id: key, + name: diskLabel.replace(/-/g, '/'), + type: 'agentDisk', + resourceType: 'Agent Disk', + node: agent ? getAlertResourceDisplayLabel(agent) : agentId, + disabled: thresholds.disabled || false, + thresholds: extractTriggerValues(thresholds), + }); + return; + } + + const agentResource = agentMap.get(key); + if (agentResource) { + const displayName = getAlertResourceDisplayLabel(agentResource); + const data = platformData(agentResource); + const agent = asRecord(data?.agent); + overridesList.push({ + id: key, + name: displayName, + type: 'agent', + resourceType: 'Agent', + node: displayName, + instance: + asString(agent?.platform) || + asString(agent?.osName) || + asString(data?.platform) || + asString(data?.osName) || + '', + disabled: thresholds.disabled || false, + disableConnectivity: thresholds.disableConnectivity || false, + thresholds: extractTriggerValues(thresholds), + }); + return; + } + + if (key.startsWith('pbs-')) { + const pbs = pbsInstanceById.get(key); + if (pbs) { + overridesList.push({ + id: key, + name: pbs.name, + type: 'pbs', + resourceType: 'PBS', + disableConnectivity: thresholds.disableConnectivity || false, + thresholds: extractTriggerValues(thresholds), + }); + } + return; + } + + const node = nodeResources.find((resource) => resource.id === key); + if (node) { + overridesList.push({ + id: key, + name: getAlertResourceDisplayLabel(node), + type: 'agent', + resourceType: 'Agent', + disableConnectivity: thresholds.disableConnectivity || false, + thresholds: extractTriggerValues(thresholds), + }); + return; + } + + const storage = storageResources.find((resource) => resource.id === key); + if (storage) { + const coords = storageCoords(storage); + overridesList.push({ + id: key, + name: getAlertResourceDisplayLabel(storage), + type: 'storage', + resourceType: 'Storage', + node: coords.node, + instance: coords.instance, + disabled: thresholds.disabled || false, + thresholds: extractTriggerValues(thresholds), + }); + return; + } + + const guest = + vmResources.find((resource) => resource.id === key) || + containerResources.find((resource) => resource.id === key); + if (!guest) { + return; + } + + const data = platformData(guest); + overridesList.push({ + id: key, + name: getAlertResourceDisplayLabel(guest), + type: 'guest', + resourceType: guest.type === 'vm' ? 'VM' : 'Container', + vmid: (data?.vmid as number | undefined) ?? guessNumericId(guest.id), + node: (data?.node as string | undefined) ?? '', + instance: (data?.instance as string | undefined) ?? guest.platformId, + disabled: thresholds.disabled || false, + disableConnectivity: thresholds.disableConnectivity || false, + poweredOffSeverity: + thresholds.poweredOffSeverity === 'critical' + ? 'critical' + : thresholds.poweredOffSeverity === 'warning' + ? 'warning' + : undefined, + thresholds: extractTriggerValues(thresholds), + backup: thresholds.backup, + snapshot: thresholds.snapshot, + }); + }); + + return overridesList; +}; diff --git a/frontend-modern/src/features/alerts/useAlertOverridesState.ts b/frontend-modern/src/features/alerts/useAlertOverridesState.ts index 8dd30d34e..9d4822d7c 100644 --- a/frontend-modern/src/features/alerts/useAlertOverridesState.ts +++ b/frontend-modern/src/features/alerts/useAlertOverridesState.ts @@ -3,16 +3,10 @@ import { createEffect, createMemo, createSignal, type Accessor } from 'solid-js' import type { PBSInstance, PMGInstance } from '@/types/api'; import type { RawOverrideConfig } from '@/types/alerts'; import type { Resource, ResourceType } from '@/types/resource'; -import { getActionableAgentIdFromResource, hasAgentFacet } from '@/utils/agentResources'; -import { isAppContainerDiscoveryResourceType } from '@/utils/discoveryTarget'; +import { hasAgentFacet } from '@/utils/agentResources'; import { pbsInstanceFromResource, pmgInstanceFromResource } from '@/utils/resourceStateAdapters'; -import { - extractTriggerValues, - getAlertResourceDisplayLabel, - guessNumericId, - platformData, -} from './helpers'; +import { buildProjectedOverrides, normalizeRawOverridesConfig } from './alertOverridesModel'; import type { Override } from './types'; export interface AlertOverridesStateProps { @@ -23,91 +17,12 @@ export interface AlertOverridesStateProps { setOverviewOverrides: (value: Override[]) => void; } -const asRecord = (value: unknown): Record | undefined => - value && typeof value === 'object' ? (value as Record) : undefined; - -const asString = (value: unknown): string | undefined => - typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined; - -const uniqueIds = (...values: unknown[]): string[] => { - const ids: string[] = []; - const seen = new Set(); - - values.forEach((value) => { - const normalized = asString(value); - if (!normalized || seen.has(normalized)) return; - seen.add(normalized); - ids.push(normalized); - }); - - return ids; -}; - -const normalizeRawOverridesConfig = ( - rawOverrides: Record, -): Record => { - const cleanedOverrides: Record = {}; - - for (const [key, value] of Object.entries(rawOverrides)) { - const diskMatch = key.match(/^(agent:.+\/disk:)(.+)$/); - if (diskMatch) { - const normalized = - diskMatch[2] - .toLowerCase() - .replace(/[^a-z0-9]/g, '-') - .replace(/-{2,}/g, '-') - .replace(/^-|-$/g, '') || 'unknown'; - cleanedOverrides[diskMatch[1] + normalized] = value; - continue; - } - - cleanedOverrides[key] = value; - } - - return cleanedOverrides; -}; - export function useAlertOverridesState(props: AlertOverridesStateProps) { const [overrides, setOverrides] = createSignal([]); const [rawOverridesConfig, setRawOverridesConfig] = createSignal< Record >({}); - const pd = platformData; - - const hostOverrideIdCandidates = (resource: Resource): string[] => { - const data = pd(resource); - const agent = asRecord(data?.agent); - return uniqueIds( - getActionableAgentIdFromResource(resource), - resource.discoveryTarget?.agentId, - resource.agent?.agentId, - agent?.agentId, - data?.agentId, - resource.id, - ); - }; - - const dockerHostOverrideIdCandidates = (resource: Resource): string[] => { - const data = pd(resource); - const docker = asRecord(data?.docker); - const discoveryTarget = resource.discoveryTarget; - return uniqueIds( - isAppContainerDiscoveryResourceType(discoveryTarget?.resourceType) - ? discoveryTarget?.resourceId - : undefined, - docker?.hostSourceId, - data?.hostSourceId, - discoveryTarget?.agentId, - resource.id, - ); - }; - - const dockerContainerOverrideIdCandidates = (host: Resource, shortId: string): string[] => - uniqueIds( - ...dockerHostOverrideIdCandidates(host).map((hostId) => `docker:${hostId}/${shortId}`), - ); - const allGuests = createMemo( () => [ ...props.byType('vm'), @@ -180,239 +95,16 @@ export function useAlertOverridesState(props: AlertOverridesStateProps) { .filter((resource) => resource.type === 'storage' || resource.type === 'datastore'); const agentResourceList = agentResources(); const dockerHostResources = props.byType('docker-host'); - const overridesList: Override[] = []; - const dockerHostMap = new Map(); - const dockerContainerMap = new Map< - string, - { host: Resource; container: Resource; containerShortId: string } - >(); - const agentMap = new Map(); - - const storageCoords = (resource: Resource): { node: string; instance: string } => { - const data = pd(resource); - if (resource.type === 'datastore') { - const instance = - (data?.pbsInstanceId as string | undefined) || - resource.parentId || - resource.platformId || - 'pbs'; - const node = (data?.pbsInstanceName as string | undefined) || instance; - return { node, instance }; - } - - return { - node: (data?.node as string | undefined) || '', - instance: (data?.instance as string | undefined) || resource.platformId || '', - }; - }; - - dockerHostResources.forEach((host) => { - dockerHostOverrideIdCandidates(host).forEach((id) => { - dockerHostMap.set(id, host); - }); - - const containers = props - .children(host.id) - .filter((resource) => resource.type === 'app-container'); - - containers.forEach((container) => { - const shortId = container.id.includes('/') ? container.id.split('/').pop()! : container.id; - dockerContainerOverrideIdCandidates(host, shortId).forEach((resourceId) => { - dockerContainerMap.set(resourceId, { host, container, containerShortId: shortId }); - }); - }); - }); - - agentResourceList.forEach((agentResource) => { - hostOverrideIdCandidates(agentResource).forEach((id) => { - agentMap.set(id, agentResource); - }); - }); - - Object.entries(rawConfig).forEach(([key, thresholds]) => { - const dockerHost = dockerHostMap.get(key); - if (dockerHost) { - overridesList.push({ - id: key, - name: getAlertResourceDisplayLabel(dockerHost), - type: 'dockerHost', - resourceType: 'Container Runtime', - disableConnectivity: thresholds.disableConnectivity || false, - thresholds: extractTriggerValues(thresholds), - }); - return; - } - - const dockerContainer = dockerContainerMap.get(key); - if (dockerContainer) { - const { host, container, containerShortId } = dockerContainer; - const containerName = getAlertResourceDisplayLabel(container, containerShortId); - overridesList.push({ - id: key, - name: containerName, - type: 'dockerContainer', - resourceType: 'Container', - node: getAlertResourceDisplayLabel(host), - instance: getAlertResourceDisplayLabel(host), - disabled: thresholds.disabled || false, - disableConnectivity: thresholds.disableConnectivity || false, - poweredOffSeverity: - thresholds.poweredOffSeverity === 'critical' - ? 'critical' - : thresholds.poweredOffSeverity === 'warning' - ? 'warning' - : undefined, - thresholds: extractTriggerValues(thresholds), - }); - return; - } - - if (key.startsWith('docker:')) { - const [, rest] = key.split(':', 2); - const [hostId, containerId] = (rest || '').split('/', 2); - if (containerId) { - overridesList.push({ - id: key, - name: containerId, - type: 'dockerContainer', - resourceType: 'Container', - node: hostId, - disabled: thresholds.disabled || false, - disableConnectivity: thresholds.disableConnectivity || false, - poweredOffSeverity: - thresholds.poweredOffSeverity === 'critical' - ? 'critical' - : thresholds.poweredOffSeverity === 'warning' - ? 'warning' - : undefined, - thresholds: extractTriggerValues(thresholds), - }); - return; - } - - overridesList.push({ - id: key, - name: hostId || key, - type: 'dockerHost', - resourceType: 'Container Runtime', - disableConnectivity: thresholds.disableConnectivity || false, - thresholds: extractTriggerValues(thresholds), - }); - return; - } - - const diskMatch = key.match(/^agent:(.+)\/disk:(.+)$/); - if (diskMatch) { - const [, agentId, diskLabel] = diskMatch; - const agent = agentMap.get(agentId); - overridesList.push({ - id: key, - name: diskLabel.replace(/-/g, '/'), - type: 'agentDisk', - resourceType: 'Agent Disk', - node: agent ? getAlertResourceDisplayLabel(agent) : agentId, - disabled: thresholds.disabled || false, - thresholds: extractTriggerValues(thresholds), - }); - return; - } - - const agentResource = agentMap.get(key); - if (agentResource) { - const displayName = getAlertResourceDisplayLabel(agentResource); - const data = pd(agentResource); - const agent = asRecord(data?.agent); - overridesList.push({ - id: key, - name: displayName, - type: 'agent', - resourceType: 'Agent', - node: displayName, - instance: - asString(agent?.platform) || - asString(agent?.osName) || - asString(data?.platform) || - asString(data?.osName) || - '', - disabled: thresholds.disabled || false, - disableConnectivity: thresholds.disableConnectivity || false, - thresholds: extractTriggerValues(thresholds), - }); - return; - } - - if (key.startsWith('pbs-')) { - const pbs = pbsInstanceById().get(key); - if (pbs) { - overridesList.push({ - id: key, - name: pbs.name, - type: 'pbs', - resourceType: 'PBS', - disableConnectivity: thresholds.disableConnectivity || false, - thresholds: extractTriggerValues(thresholds), - }); - } - return; - } - - const node = nodeResources.find((resource) => resource.id === key); - if (node) { - overridesList.push({ - id: key, - name: getAlertResourceDisplayLabel(node), - type: 'agent', - resourceType: 'Agent', - disableConnectivity: thresholds.disableConnectivity || false, - thresholds: extractTriggerValues(thresholds), - }); - return; - } - - const storage = storageResources.find((resource) => resource.id === key); - if (storage) { - const coords = storageCoords(storage); - overridesList.push({ - id: key, - name: getAlertResourceDisplayLabel(storage), - type: 'storage', - resourceType: 'Storage', - node: coords.node, - instance: coords.instance, - disabled: thresholds.disabled || false, - thresholds: extractTriggerValues(thresholds), - }); - return; - } - - const guest = - vmResources.find((resource) => resource.id === key) || - containerResources.find((resource) => resource.id === key); - if (!guest) { - return; - } - - const data = pd(guest); - overridesList.push({ - id: key, - name: getAlertResourceDisplayLabel(guest), - type: 'guest', - resourceType: guest.type === 'vm' ? 'VM' : 'Container', - vmid: (data?.vmid as number | undefined) ?? guessNumericId(guest.id), - node: (data?.node as string | undefined) ?? '', - instance: (data?.instance as string | undefined) ?? guest.platformId, - disabled: thresholds.disabled || false, - disableConnectivity: thresholds.disableConnectivity || false, - poweredOffSeverity: - thresholds.poweredOffSeverity === 'critical' - ? 'critical' - : thresholds.poweredOffSeverity === 'warning' - ? 'warning' - : undefined, - thresholds: extractTriggerValues(thresholds), - backup: thresholds.backup, - snapshot: thresholds.snapshot, - }); + const overridesList = buildProjectedOverrides({ + rawConfig, + nodeResources, + vmResources, + containerResources, + storageResources, + agentResourceList, + dockerHostResources, + getChildren: props.children, + pbsInstanceById: pbsInstanceById(), }); const currentOverrides = overrides(); diff --git a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts index 3a389ccb6..3a338cae6 100644 --- a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts +++ b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts @@ -4,6 +4,7 @@ import alertsConfigurationSurfaceSource from '@/features/alerts/AlertsConfigurat import alertsConfigurationStateSource from '@/features/alerts/useAlertsConfigurationState.ts?raw'; import alertsConfigurationSnapshotStateSource from '@/features/alerts/useAlertsConfigurationSnapshotState.ts?raw'; import alertsConfigurationModelSource from '@/features/alerts/alertsConfigurationModel.ts?raw'; +import alertOverridesModelSource from '@/features/alerts/alertOverridesModel.ts?raw'; import alertOverridesStateSource from '@/features/alerts/useAlertOverridesState.ts?raw'; import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw'; import alertDestinationsTabStateSource from '@/features/alerts/useAlertDestinationsTabState.ts?raw'; @@ -278,9 +279,16 @@ describe('tab path helpers', () => { expect(alertsConfigurationModelSource).toContain('const createHysteresisThreshold ='); expect(alertsConfigurationModelSource).toContain('const normalizeGap ='); expect(alertOverridesStateSource).toContain('export function useAlertOverridesState'); - expect(alertOverridesStateSource).toContain('getActionableAgentIdFromResource'); expect(alertOverridesStateSource).toContain('pbsInstanceFromResource'); + expect(alertOverridesStateSource).toContain('buildProjectedOverrides'); + expect(alertOverridesStateSource).not.toContain('getActionableAgentIdFromResource'); + expect(alertOverridesStateSource).not.toContain('const hostOverrideIdCandidates ='); expect(alertOverridesStateSource).toContain('props.setOverviewOverrides(overrides())'); + expect(alertOverridesModelSource).toContain('export const normalizeRawOverridesConfig ='); + expect(alertOverridesModelSource).toContain('export const hostOverrideIdCandidates ='); + expect(alertOverridesModelSource).toContain('export const dockerHostOverrideIdCandidates ='); + expect(alertOverridesModelSource).toContain('export const buildProjectedOverrides ='); + expect(alertOverridesModelSource).toContain('getActionableAgentIdFromResource'); expect(alertDestinationsStateSource).toContain('export function useAlertDestinationsState'); expect(alertDestinationsStateSource).toContain('NotificationsAPI.getEmailConfig'); expect(alertDestinationsStateSource).toContain('NotificationsAPI.updateEmailConfig'); diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index ad10ed159..f4e71004b 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -328,6 +328,7 @@ import alertsConfigurationSurfaceSource from '@/features/alerts/AlertsConfigurat import alertsConfigurationStateSource from '@/features/alerts/useAlertsConfigurationState.ts?raw'; import alertsConfigurationSnapshotStateSource from '@/features/alerts/useAlertsConfigurationSnapshotState.ts?raw'; import alertsConfigurationModelSource from '@/features/alerts/alertsConfigurationModel.ts?raw'; +import alertOverridesModelSource from '@/features/alerts/alertOverridesModel.ts?raw'; import alertOverridesStateSource from '@/features/alerts/useAlertOverridesState.ts?raw'; import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw'; import alertDestinationsTabStateSource from '@/features/alerts/useAlertDestinationsTabState.ts?raw'; @@ -2862,9 +2863,16 @@ describe('frontend resource type boundaries', () => { expect(alertsConfigurationModelSource).toContain('const createHysteresisThreshold ='); expect(alertsConfigurationModelSource).toContain('const normalizeGap ='); expect(alertOverridesStateSource).toContain('export function useAlertOverridesState'); - expect(alertOverridesStateSource).toContain('getActionableAgentIdFromResource'); expect(alertOverridesStateSource).toContain('pbsInstanceFromResource'); + expect(alertOverridesStateSource).toContain('buildProjectedOverrides'); + expect(alertOverridesStateSource).not.toContain('getActionableAgentIdFromResource'); + expect(alertOverridesStateSource).not.toContain('const hostOverrideIdCandidates ='); expect(alertOverridesStateSource).toContain('props.setOverviewOverrides(overrides())'); + expect(alertOverridesModelSource).toContain('export const normalizeRawOverridesConfig ='); + expect(alertOverridesModelSource).toContain('export const hostOverrideIdCandidates ='); + expect(alertOverridesModelSource).toContain('export const dockerHostOverrideIdCandidates ='); + expect(alertOverridesModelSource).toContain('export const buildProjectedOverrides ='); + expect(alertOverridesModelSource).toContain('getActionableAgentIdFromResource'); expect(alertDestinationsStateSource).toContain('getAlertDestinationsConfigLoadError'); expect(alertDestinationsStateSource).toContain('NotificationsAPI.getEmailConfig'); expect(alertDestinationsStateSource).toContain('NotificationsAPI.updateEmailConfig');