mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-25 12:52:25 +00:00
Extract alert overrides projection model owner
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>): 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,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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<string, unknown> | undefined =>
|
||||
value && typeof value === 'object' ? (value as Record<string, unknown>) : 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<string>();
|
||||
|
||||
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<string, RawOverrideConfig>,
|
||||
): Record<string, RawOverrideConfig> => {
|
||||
const cleanedOverrides: Record<string, RawOverrideConfig> = {};
|
||||
|
||||
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<string, RawOverrideConfig>;
|
||||
nodeResources: Resource[];
|
||||
vmResources: Resource[];
|
||||
containerResources: Resource[];
|
||||
storageResources: Resource[];
|
||||
agentResourceList: Resource[];
|
||||
dockerHostResources: Resource[];
|
||||
getChildren: (resourceId: string) => Resource[];
|
||||
pbsInstanceById: Map<string, PBSInstance>;
|
||||
}
|
||||
|
||||
export const buildProjectedOverrides = ({
|
||||
rawConfig,
|
||||
nodeResources,
|
||||
vmResources,
|
||||
containerResources,
|
||||
storageResources,
|
||||
agentResourceList,
|
||||
dockerHostResources,
|
||||
getChildren,
|
||||
pbsInstanceById,
|
||||
}: BuildProjectedOverridesArgs): Override[] => {
|
||||
const overridesList: Override[] = [];
|
||||
const dockerHostMap = new Map<string, Resource>();
|
||||
const dockerContainerMap = new Map<
|
||||
string,
|
||||
{ host: Resource; container: Resource; containerShortId: string }
|
||||
>();
|
||||
const agentMap = new Map<string, Resource>();
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -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<string, unknown> | undefined =>
|
||||
value && typeof value === 'object' ? (value as Record<string, unknown>) : 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<string>();
|
||||
|
||||
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<string, RawOverrideConfig>,
|
||||
): Record<string, RawOverrideConfig> => {
|
||||
const cleanedOverrides: Record<string, RawOverrideConfig> = {};
|
||||
|
||||
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<Override[]>([]);
|
||||
const [rawOverridesConfig, setRawOverridesConfig] = createSignal<
|
||||
Record<string, RawOverrideConfig>
|
||||
>({});
|
||||
|
||||
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<string, Resource>();
|
||||
const dockerContainerMap = new Map<
|
||||
string,
|
||||
{ host: Resource; container: Resource; containerShortId: string }
|
||||
>();
|
||||
const agentMap = new Map<string, Resource>();
|
||||
|
||||
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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user