mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-05 20:32:59 +00:00
Split thresholds family data owners
This commit is contained in:
@@ -130,8 +130,17 @@ Alert threshold tables now route their visible resource row labels, search
|
||||
labels, and persisted override display names through the same shared helper
|
||||
so governed agent, guest, and storage rows do not leak raw names when the
|
||||
threshold editor saves or re-renders them.
|
||||
That threshold editor data shaping now lives under
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts`,
|
||||
That threshold editor data shaping now routes through
|
||||
`frontend-modern/src/features/alerts/thresholds/thresholdsResourceModel.ts`
|
||||
for shared override-ID compatibility, grouped resource normalization, and
|
||||
storage status policy, while
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts`
|
||||
stays the composition owner for the family-specific threshold projectors in
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsHostData.ts`,
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsDockerData.ts`,
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsGuestData.ts`,
|
||||
and
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsInfrastructureData.ts`.
|
||||
backup and snapshot default sanitization plus factory-drift policy now live in
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts`,
|
||||
while `frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts`
|
||||
@@ -143,8 +152,8 @@ effects now route through
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTable.tsx` is now limited to
|
||||
table interaction and presentation. New threshold row grouping,
|
||||
override-ID compatibility, resource normalization, or thresholds-table
|
||||
controller logic should land in those hooks rather than being rebuilt inside
|
||||
the table component.
|
||||
controller logic should land in those threshold model and family hooks rather
|
||||
than being rebuilt inside the table component.
|
||||
The alert resource thresholds editor now follows the same shape: shared metric
|
||||
normalization, bounds, value-resolution, and override-label logic live in
|
||||
`frontend-modern/src/components/Alerts/alertResourceTableModel.ts`, render-heavy
|
||||
|
||||
@@ -236,12 +236,15 @@ inline.
|
||||
The thresholds editor now follows that same split more tightly:
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts`
|
||||
must stay the table-shell owner for route sync and local UI state, while
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts`
|
||||
stays the composition shell for threshold resource-family projectors,
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts`
|
||||
owns backup/snapshot default sanitization and factory-drift policy, and
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts`
|
||||
owns override save/bulk/toggle persistence and alert-removal side effects. The
|
||||
table-shell hook should not re-accumulate raw override mutation logic or
|
||||
recovery-threshold defaults policy inline.
|
||||
table-shell hook should not re-accumulate raw override mutation logic,
|
||||
recovery-threshold defaults policy, or resource-family projection engines
|
||||
inline.
|
||||
|
||||
The updates settings surface now follows the same presentation-owner rule.
|
||||
`frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx` stays the
|
||||
@@ -430,10 +433,15 @@ thresholds-table adapter logic should stay feature-owned unless it graduates
|
||||
into a shared primitive used by more than one alert surface.
|
||||
Within that thresholds surface, `frontend-modern/src/components/Alerts/ThresholdsTable.tsx`
|
||||
is now explicitly a feature consumer rather than the data or controller owner.
|
||||
Canonical threshold row shaping, override-ID compatibility, grouped resource
|
||||
normalization, and thresholds-table controller state live in
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts`,
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts`,
|
||||
Canonical threshold row shaping now routes through
|
||||
`frontend-modern/src/features/alerts/thresholds/thresholdsResourceModel.ts`
|
||||
plus the family-owned feature hooks
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsHostData.ts`,
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsDockerData.ts`,
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsGuestData.ts`,
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsInfrastructureData.ts`,
|
||||
with `frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts`
|
||||
limited to composing them. Thresholds-table controller state lives in
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts`,
|
||||
so future cleanup should extend those feature hooks instead of rebuilding
|
||||
resource normalization or thresholds-table runtime state inside the table
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { Resource } from '@/types/resource';
|
||||
|
||||
import {
|
||||
agentDiskResourceId,
|
||||
hostOverrideIdCandidates,
|
||||
normalizeStorageStatus,
|
||||
uniqueIds,
|
||||
} from '../thresholdsResourceModel';
|
||||
|
||||
describe('thresholdsResourceModel', () => {
|
||||
it('deduplicates candidate identifiers while preserving order', () => {
|
||||
expect(uniqueIds(' agent-1 ', undefined, 'agent-1', 'agent-2', '')).toEqual([
|
||||
'agent-1',
|
||||
'agent-2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('builds host override candidates from the canonical resource identifiers', () => {
|
||||
const resource = {
|
||||
id: 'agent-runtime',
|
||||
type: 'agent',
|
||||
discoveryTarget: {
|
||||
resourceType: 'agent',
|
||||
resourceId: 'agent-discovery',
|
||||
agentId: 'agent-discovery',
|
||||
},
|
||||
agent: {
|
||||
agentId: 'agent-runtime',
|
||||
},
|
||||
platformData: {
|
||||
agent: {
|
||||
agentId: 'agent-platform',
|
||||
},
|
||||
agentId: 'agent-platform',
|
||||
},
|
||||
} as unknown as Resource;
|
||||
|
||||
expect(hostOverrideIdCandidates(resource)).toEqual([
|
||||
'agent-discovery',
|
||||
'agent-runtime',
|
||||
'agent-platform',
|
||||
]);
|
||||
});
|
||||
|
||||
it('sanitizes agent disk ids with the backend-compatible label rules', () => {
|
||||
expect(agentDiskResourceId('agent-1', '/var/lib/docker', '')).toBe(
|
||||
'agent:agent-1/disk:var-lib-docker',
|
||||
);
|
||||
expect(agentDiskResourceId('agent-1', '', '/dev/sda1')).toBe('agent:agent-1/disk:dev-sda1');
|
||||
});
|
||||
|
||||
it('normalizes storage status to the table availability contract', () => {
|
||||
expect(normalizeStorageStatus('online')).toBe('available');
|
||||
expect(normalizeStorageStatus('running')).toBe('available');
|
||||
expect(normalizeStorageStatus('offline')).toBe('offline');
|
||||
expect(normalizeStorageStatus(undefined)).toBe('offline');
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,264 @@
|
||||
import { createMemo } from 'solid-js';
|
||||
|
||||
import { getPreferredResourceHostname } from '@/utils/resourceIdentity';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { getAlertResourceDisplayLabel } from '@/features/alerts/helpers';
|
||||
|
||||
import type { GroupHeaderMeta, Resource as TableResource } from '../tableTypes';
|
||||
import { ThresholdsDataInputs } from '../thresholdsResourceModel';
|
||||
import {
|
||||
createOverridesMap,
|
||||
dockerContainerOverrideIdCandidates,
|
||||
dockerHostOverrideIdCandidates,
|
||||
findOverrideByCandidates,
|
||||
getFriendlyAlertNodeName,
|
||||
getFriendlyNodeName,
|
||||
hasThresholdDiff,
|
||||
platformData,
|
||||
} from '../thresholdsResourceModel';
|
||||
|
||||
export function useThresholdsDockerData(inputs: ThresholdsDataInputs) {
|
||||
const { props, editingId, searchTerm } = inputs;
|
||||
|
||||
const dockerHostsWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
const seen = new Set<string>();
|
||||
|
||||
const hosts: TableResource[] = (props.dockerHosts ?? []).map((host) => {
|
||||
const idCandidates = dockerHostOverrideIdCandidates(host);
|
||||
const originalName = getAlertResourceDisplayLabel(host);
|
||||
const friendlyName = getFriendlyAlertNodeName(originalName, host.policy);
|
||||
const override = findOverrideByCandidates(overridesMap, idCandidates);
|
||||
const resourceId = override?.id || idCandidates[0] || host.id;
|
||||
const disableConnectivity = override?.disableConnectivity || false;
|
||||
|
||||
seen.add(resourceId);
|
||||
|
||||
return {
|
||||
id: resourceId,
|
||||
name: friendlyName,
|
||||
displayName: friendlyName,
|
||||
rawName: originalName,
|
||||
type: 'dockerHost' as const,
|
||||
resourceType: 'Container Runtime',
|
||||
node: getPreferredResourceHostname(host),
|
||||
instance: (platformData(host)?.platform as string) || (platformData(host)?.osName as string) || '',
|
||||
status: host.status,
|
||||
hasOverride: disableConnectivity,
|
||||
disableConnectivity,
|
||||
thresholds: override?.thresholds || {},
|
||||
defaults: {},
|
||||
editable: false,
|
||||
} satisfies TableResource;
|
||||
});
|
||||
|
||||
(props.overrides() ?? [])
|
||||
.filter((override) => override.type === 'dockerHost' && !seen.has(override.id))
|
||||
.forEach((override) => {
|
||||
const originalName = override.name || override.id;
|
||||
const friendlyName = getFriendlyNodeName(originalName);
|
||||
hosts.push({
|
||||
id: override.id,
|
||||
name: friendlyName,
|
||||
displayName: friendlyName,
|
||||
rawName: originalName,
|
||||
type: 'dockerHost',
|
||||
resourceType: 'Container Runtime',
|
||||
node: override.node || '',
|
||||
instance: override.instance || '',
|
||||
status: 'unknown',
|
||||
hasOverride: true,
|
||||
disableConnectivity: override.disableConnectivity || false,
|
||||
thresholds: override.thresholds || {},
|
||||
defaults: {},
|
||||
editable: false,
|
||||
});
|
||||
});
|
||||
|
||||
return search ? hosts.filter((host) => host.name.toLowerCase().includes(search)) : hosts;
|
||||
}, []);
|
||||
|
||||
const dockerContainersByHostId = createMemo(() => {
|
||||
const map = new Map<string, Resource[]>();
|
||||
(props.allResources ?? []).forEach((resource) => {
|
||||
if (resource.type !== 'app-container') return;
|
||||
const parentId = resource.parentId;
|
||||
if (!parentId) return;
|
||||
const existing = map.get(parentId);
|
||||
if (existing) {
|
||||
existing.push(resource);
|
||||
} else {
|
||||
map.set(parentId, [resource]);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const dockerContainersGroupedByHost = createMemo<Record<string, TableResource[]>>((prev = {}) => {
|
||||
if (editingId()) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
const groups: Record<string, TableResource[]> = {};
|
||||
const seen = new Set<string>();
|
||||
|
||||
(props.dockerHosts ?? []).forEach((host) => {
|
||||
const dockerHostIds = dockerHostOverrideIdCandidates(host);
|
||||
const dockerHostIdForActions = dockerHostIds[0] || host.id;
|
||||
const hostLabel = getAlertResourceDisplayLabel(host);
|
||||
const friendlyHostName = getFriendlyAlertNodeName(hostLabel, host.policy);
|
||||
const hostLabelLower = hostLabel.toLowerCase();
|
||||
const friendlyHostNameLower = friendlyHostName.toLowerCase();
|
||||
const hostHostname = getPreferredResourceHostname(host);
|
||||
const containers = dockerContainersByHostId().get(host.id) ?? [];
|
||||
|
||||
containers.forEach((container) => {
|
||||
const shortId = container.id.includes('/')
|
||||
? (container.id.split('/').pop() ?? container.id)
|
||||
: container.id;
|
||||
const resourceIdCandidates = dockerContainerOverrideIdCandidates(host, shortId);
|
||||
const override = findOverrideByCandidates(overridesMap, resourceIdCandidates);
|
||||
const resourceId =
|
||||
override?.id || resourceIdCandidates[0] || `docker:${dockerHostIdForActions}/${shortId}`;
|
||||
const overrideSeverity = override?.poweredOffSeverity;
|
||||
const hasCustomThresholds = hasThresholdDiff(
|
||||
override,
|
||||
props.dockerDefaults as Record<string, number | undefined>,
|
||||
);
|
||||
const hasOverride =
|
||||
hasCustomThresholds ||
|
||||
Boolean(override?.disabled) ||
|
||||
Boolean(override?.disableConnectivity) ||
|
||||
overrideSeverity !== undefined;
|
||||
const containerName = getAlertResourceDisplayLabel(container, shortId);
|
||||
const image = (platformData(container)?.image as string) ?? '';
|
||||
|
||||
const matchesSearch =
|
||||
!search ||
|
||||
containerName.toLowerCase().includes(search) ||
|
||||
hostLabelLower.includes(search) ||
|
||||
friendlyHostNameLower.includes(search) ||
|
||||
image.toLowerCase().includes(search);
|
||||
if (!matchesSearch) return;
|
||||
|
||||
const groupKey = friendlyHostName || hostLabel;
|
||||
const resource: TableResource = {
|
||||
id: resourceId,
|
||||
name: containerName,
|
||||
type: 'dockerContainer',
|
||||
resourceType: 'Container',
|
||||
node: groupKey,
|
||||
instance: hostHostname,
|
||||
status: container.status,
|
||||
hasOverride,
|
||||
disabled: override?.disabled || false,
|
||||
disableConnectivity: override?.disableConnectivity || false,
|
||||
thresholds: override?.thresholds || {},
|
||||
defaults: props.dockerDefaults,
|
||||
hostId: dockerHostIdForActions,
|
||||
image,
|
||||
poweredOffSeverity: overrideSeverity,
|
||||
};
|
||||
|
||||
if (!groups[groupKey]) {
|
||||
groups[groupKey] = [];
|
||||
}
|
||||
groups[groupKey].push(resource);
|
||||
seen.add(resourceId);
|
||||
});
|
||||
});
|
||||
|
||||
(props.overrides() ?? [])
|
||||
.filter((override) => override.type === 'dockerContainer' && !seen.has(override.id))
|
||||
.forEach((override) => {
|
||||
const fallbackName = override.name || override.id.split('/').pop() || override.id;
|
||||
const group = 'Unassigned Containers';
|
||||
if (!groups[group]) {
|
||||
groups[group] = [];
|
||||
}
|
||||
groups[group].push({
|
||||
id: override.id,
|
||||
name: fallbackName,
|
||||
type: 'dockerContainer',
|
||||
resourceType: 'Container',
|
||||
status: 'unknown',
|
||||
hasOverride: true,
|
||||
disabled: override.disabled || false,
|
||||
disableConnectivity: override.disableConnectivity || false,
|
||||
thresholds: override.thresholds || {},
|
||||
defaults: props.dockerDefaults,
|
||||
poweredOffSeverity: override.poweredOffSeverity,
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(groups).forEach((group) => {
|
||||
groups[group].sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
if (!search) {
|
||||
return groups;
|
||||
}
|
||||
|
||||
const filteredGroups: Record<string, TableResource[]> = {};
|
||||
Object.entries(groups).forEach(([group, resources]) => {
|
||||
if (resources.length > 0) {
|
||||
filteredGroups[group] = resources;
|
||||
}
|
||||
});
|
||||
return filteredGroups;
|
||||
}, {});
|
||||
|
||||
const dockerContainersFlat = createMemo<TableResource[]>(() =>
|
||||
Object.values(dockerContainersGroupedByHost() ?? {}).flat(),
|
||||
);
|
||||
|
||||
const totalDockerContainers = createMemo(() =>
|
||||
(props.dockerHosts ?? []).reduce(
|
||||
(sum, host) => sum + (dockerContainersByHostId().get(host.id)?.length ?? 0),
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
const dockerHostGroupMeta = createMemo<Record<string, GroupHeaderMeta>>(() => {
|
||||
const meta: Record<string, GroupHeaderMeta> = {};
|
||||
(props.dockerHosts ?? []).forEach((host) => {
|
||||
const originalName = getAlertResourceDisplayLabel(host);
|
||||
const friendlyName = getFriendlyAlertNodeName(originalName, host.policy);
|
||||
const headerMeta: GroupHeaderMeta = {
|
||||
displayName: friendlyName,
|
||||
rawName: originalName,
|
||||
status: host.status,
|
||||
};
|
||||
|
||||
const hostname = getPreferredResourceHostname(host);
|
||||
[friendlyName, originalName, hostname, host.id]
|
||||
.filter((key: string | undefined): key is string => Boolean(key && key.trim()))
|
||||
.forEach((key: string) => {
|
||||
meta[key.trim()] = headerMeta;
|
||||
});
|
||||
});
|
||||
|
||||
meta['Unassigned Containers'] = {
|
||||
displayName: 'Unassigned Containers',
|
||||
status: 'unknown',
|
||||
};
|
||||
|
||||
return meta;
|
||||
});
|
||||
|
||||
return {
|
||||
dockerHostsWithOverrides,
|
||||
dockerContainersByHostId,
|
||||
dockerContainersGroupedByHost,
|
||||
dockerContainersFlat,
|
||||
totalDockerContainers,
|
||||
dockerHostGroupMeta,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { createMemo } from 'solid-js';
|
||||
|
||||
import { getAlertResourceDisplayLabel } from '@/features/alerts/helpers';
|
||||
|
||||
import type { GroupHeaderMeta, Resource as TableResource } from '../tableTypes';
|
||||
import { ThresholdsDataInputs } from '../thresholdsResourceModel';
|
||||
import {
|
||||
buildNodeHeaderMeta,
|
||||
createOverridesMap,
|
||||
hasThresholdDiff,
|
||||
platformData,
|
||||
} from '../thresholdsResourceModel';
|
||||
|
||||
export function useThresholdsGuestData(inputs: ThresholdsDataInputs) {
|
||||
const { props, editingId, searchTerm } = inputs;
|
||||
|
||||
const guestsGroupedByNode = createMemo<Record<string, TableResource[]>>((prev = {}) => {
|
||||
if (editingId()) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
|
||||
const guests = (props.allGuests() ?? []).map((guest) => {
|
||||
const data = platformData(guest);
|
||||
const vmid = (data?.vmid as number | undefined) ?? undefined;
|
||||
const node = (data?.node as string | undefined) ?? '';
|
||||
const instance = (data?.instance as string | undefined) ?? guest.platformId ?? '';
|
||||
const override = overridesMap.get(guest.id);
|
||||
const overrideSeverity = override?.poweredOffSeverity;
|
||||
const hasCustomThresholds = hasThresholdDiff(
|
||||
override,
|
||||
props.guestDefaults as Record<string, number | undefined>,
|
||||
);
|
||||
const hasOverride =
|
||||
hasCustomThresholds ||
|
||||
Boolean(override?.disabled) ||
|
||||
Boolean(override?.disableConnectivity) ||
|
||||
overrideSeverity !== undefined;
|
||||
|
||||
return {
|
||||
id: guest.id,
|
||||
name: getAlertResourceDisplayLabel(guest),
|
||||
displayName: getAlertResourceDisplayLabel(guest),
|
||||
rawName: guest.name,
|
||||
type: 'guest' as const,
|
||||
resourceType: guest.type === 'vm' ? 'VM' : 'Container',
|
||||
vmid,
|
||||
node,
|
||||
instance,
|
||||
status: guest.status,
|
||||
hasOverride,
|
||||
disabled: override?.disabled || false,
|
||||
disableConnectivity: override?.disableConnectivity || false,
|
||||
thresholds: override?.thresholds || {},
|
||||
defaults: props.guestDefaults,
|
||||
backup: override?.backup || props.backupDefaults(),
|
||||
snapshot: override?.snapshot || props.snapshotDefaults(),
|
||||
poweredOffSeverity: overrideSeverity,
|
||||
};
|
||||
});
|
||||
|
||||
const filteredGuests = search
|
||||
? guests.filter(
|
||||
(guest) =>
|
||||
guest.name.toLowerCase().includes(search) ||
|
||||
guest.vmid?.toString().includes(search) ||
|
||||
guest.node?.toLowerCase().includes(search),
|
||||
)
|
||||
: guests;
|
||||
|
||||
const grouped: Record<string, TableResource[]> = {};
|
||||
filteredGuests.forEach((guest) => {
|
||||
const groupKey = guest.instance || guest.node || 'Unknown';
|
||||
if (!grouped[groupKey]) {
|
||||
grouped[groupKey] = [];
|
||||
}
|
||||
grouped[groupKey].push(guest);
|
||||
});
|
||||
|
||||
Object.keys(grouped).forEach((node) => {
|
||||
grouped[node].sort((a, b) => {
|
||||
if (a.vmid && b.vmid) return a.vmid - b.vmid;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}, {});
|
||||
|
||||
const guestsFlat = createMemo<TableResource[]>(() =>
|
||||
Object.values(guestsGroupedByNode() ?? {}).flat(),
|
||||
);
|
||||
|
||||
const guestGroupHeaderMeta = createMemo<Record<string, GroupHeaderMeta>>(() => {
|
||||
const meta: Record<string, GroupHeaderMeta> = {};
|
||||
(props.nodes ?? []).forEach((node) => {
|
||||
const { headerMeta, keys } = buildNodeHeaderMeta(node);
|
||||
keys.forEach((key: string) => {
|
||||
meta[key] = headerMeta;
|
||||
});
|
||||
});
|
||||
return meta;
|
||||
});
|
||||
|
||||
return {
|
||||
guestsGroupedByNode,
|
||||
guestsFlat,
|
||||
guestGroupHeaderMeta,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import { createMemo } from 'solid-js';
|
||||
|
||||
import { getAlertResourceDisplayLabel } from '@/features/alerts/helpers';
|
||||
|
||||
import type { Resource as TableResource } from '../tableTypes';
|
||||
import { ThresholdsDataInputs } from '../thresholdsResourceModel';
|
||||
import {
|
||||
agentDiskResourceId,
|
||||
createOverridesMap,
|
||||
findOverrideByCandidates,
|
||||
getFriendlyAlertNodeName,
|
||||
hasThresholdDiff,
|
||||
hostActionId,
|
||||
hostOverrideIdCandidates,
|
||||
platformData,
|
||||
readRecord,
|
||||
readString,
|
||||
} from '../thresholdsResourceModel';
|
||||
|
||||
export function useThresholdsHostData(inputs: ThresholdsDataInputs) {
|
||||
const { props, editingId, searchTerm } = inputs;
|
||||
|
||||
const nodesWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
|
||||
const nodes = (props.nodes ?? []).map((node) => {
|
||||
const override = overridesMap.get(node.id);
|
||||
const data = platformData(node);
|
||||
const clusterName = (data?.clusterName as string | undefined) ?? undefined;
|
||||
const isClusterMember =
|
||||
(data?.isClusterMember as boolean | undefined) ?? Boolean(node.clusterId);
|
||||
const hasCustomThresholds = hasThresholdDiff(
|
||||
override,
|
||||
props.nodeDefaults as Record<string, number | undefined>,
|
||||
);
|
||||
const note = typeof override?.note === 'string' ? override.note : undefined;
|
||||
const hasNote = Boolean(note && note.trim().length > 0);
|
||||
|
||||
const originalDisplayName = getAlertResourceDisplayLabel(node);
|
||||
const friendlyName = getFriendlyAlertNodeName(originalDisplayName, node.policy, clusterName);
|
||||
const rawName = node.name;
|
||||
const sanitizedName = friendlyName || originalDisplayName || rawName.split('.')[0] || rawName;
|
||||
const guestUrlValue = typeof data?.guestURL === 'string' ? data.guestURL.trim() : '';
|
||||
const hostValue = (typeof data?.host === 'string' ? data.host.trim() : '') || rawName;
|
||||
const normalizedHost =
|
||||
guestUrlValue && guestUrlValue !== ''
|
||||
? guestUrlValue.startsWith('http')
|
||||
? guestUrlValue
|
||||
: `https://${guestUrlValue}`
|
||||
: hostValue.startsWith('http://') || hostValue.startsWith('https://')
|
||||
? hostValue
|
||||
: `https://${hostValue.includes(':') ? hostValue : `${hostValue}:8006`}`;
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
name: sanitizedName,
|
||||
displayName: sanitizedName,
|
||||
rawName: originalDisplayName,
|
||||
host: normalizedHost,
|
||||
type: 'agent' as const,
|
||||
resourceType: 'Agent',
|
||||
status: node.status,
|
||||
uptime: node.uptime,
|
||||
cpu: (node.cpu?.current ?? 0) / 100,
|
||||
memory: node.memory?.current,
|
||||
hasOverride: hasCustomThresholds || hasNote || Boolean(override?.disableConnectivity),
|
||||
disabled: false,
|
||||
disableConnectivity: override?.disableConnectivity || false,
|
||||
thresholds: override?.thresholds || {},
|
||||
defaults: props.nodeDefaults,
|
||||
clusterName: isClusterMember ? clusterName?.trim() : undefined,
|
||||
isClusterMember,
|
||||
instance: node.platformId,
|
||||
note,
|
||||
} satisfies TableResource;
|
||||
});
|
||||
|
||||
return search ? nodes.filter((node) => node.name.toLowerCase().includes(search)) : nodes;
|
||||
}, []);
|
||||
|
||||
const agentsWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
const seen = new Set<string>();
|
||||
|
||||
const agents: TableResource[] = (props.agents ?? []).map((agentResource) => {
|
||||
const idCandidates = hostOverrideIdCandidates(agentResource);
|
||||
const override = findOverrideByCandidates(overridesMap, idCandidates);
|
||||
const resourceId = override?.id || idCandidates[0] || agentResource.id;
|
||||
const hasCustomThresholds = hasThresholdDiff(
|
||||
override,
|
||||
props.agentDefaults as Record<string, number | undefined>,
|
||||
);
|
||||
const displayName = getAlertResourceDisplayLabel(agentResource);
|
||||
const data = platformData(agentResource);
|
||||
const agentData = readRecord(data?.agent);
|
||||
|
||||
seen.add(resourceId);
|
||||
|
||||
return {
|
||||
id: resourceId,
|
||||
name: displayName,
|
||||
displayName,
|
||||
rawName: agentResource.identity?.hostname ?? agentResource.name,
|
||||
type: 'agent' as const,
|
||||
resourceType: 'Agent',
|
||||
node: displayName,
|
||||
instance:
|
||||
readString(agentData?.platform) ||
|
||||
readString(agentData?.osName) ||
|
||||
readString(data?.platform) ||
|
||||
readString(data?.osName) ||
|
||||
'',
|
||||
status: agentResource.status,
|
||||
hasOverride:
|
||||
hasCustomThresholds || Boolean(override?.disabled) || Boolean(override?.disableConnectivity),
|
||||
disabled: override?.disabled || false,
|
||||
disableConnectivity: override?.disableConnectivity || false,
|
||||
thresholds: override?.thresholds || {},
|
||||
defaults: props.agentDefaults,
|
||||
} satisfies TableResource;
|
||||
});
|
||||
|
||||
(props.overrides() ?? [])
|
||||
.filter((override) => override.type === 'agent' && !seen.has(override.id))
|
||||
.forEach((override) => {
|
||||
const name = override.name?.trim() || override.id;
|
||||
agents.push({
|
||||
id: override.id,
|
||||
name,
|
||||
displayName: name,
|
||||
rawName: name,
|
||||
type: 'agent' as const,
|
||||
resourceType: 'Agent',
|
||||
node: '',
|
||||
instance: '',
|
||||
status: 'unknown',
|
||||
hasOverride: true,
|
||||
disabled: override.disabled || false,
|
||||
disableConnectivity: override.disableConnectivity || false,
|
||||
thresholds: override.thresholds || {},
|
||||
defaults: props.agentDefaults,
|
||||
} satisfies TableResource);
|
||||
});
|
||||
|
||||
return search ? agents.filter((agent) => agent.name.toLowerCase().includes(search)) : agents;
|
||||
}, []);
|
||||
|
||||
const agentDisksWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
const seen = new Set<string>();
|
||||
const disks: TableResource[] = [];
|
||||
|
||||
(props.agents ?? []).forEach((agentResource) => {
|
||||
const agentDisplayName = getAlertResourceDisplayLabel(agentResource);
|
||||
const agentIdCandidates = hostOverrideIdCandidates(agentResource);
|
||||
const agentIdForActions = hostActionId(agentResource);
|
||||
const data = platformData(agentResource);
|
||||
const platformAgent = readRecord(data?.agent);
|
||||
const disksFromPlatformRoot = Array.isArray(data?.disks) ? data.disks : null;
|
||||
const disksFromPlatformAgent = Array.isArray(platformAgent?.disks) ? platformAgent.disks : null;
|
||||
const disksFromResourceAgent = Array.isArray(agentResource.agent?.disks)
|
||||
? agentResource.agent.disks
|
||||
: null;
|
||||
const disksForAgent = (disksFromPlatformRoot ||
|
||||
disksFromPlatformAgent ||
|
||||
disksFromResourceAgent ||
|
||||
[]) as Array<{
|
||||
mountpoint?: string;
|
||||
device?: string;
|
||||
used?: number;
|
||||
total?: number;
|
||||
type?: string;
|
||||
}>;
|
||||
|
||||
disksForAgent.forEach((disk) => {
|
||||
const diskLabel = disk.mountpoint?.trim() || disk.device?.trim() || 'disk';
|
||||
const resourceIdCandidates = agentIdCandidates.map((agentId) =>
|
||||
agentDiskResourceId(agentId, disk.mountpoint || '', disk.device),
|
||||
);
|
||||
const override = findOverrideByCandidates(overridesMap, resourceIdCandidates);
|
||||
const resourceId = override?.id || resourceIdCandidates[0];
|
||||
if (!resourceId) return;
|
||||
|
||||
const hasCustomThresholds = hasThresholdDiff(override, {
|
||||
disk: props.agentDefaults.disk,
|
||||
});
|
||||
|
||||
seen.add(resourceId);
|
||||
|
||||
disks.push({
|
||||
id: resourceId,
|
||||
name: diskLabel,
|
||||
displayName: diskLabel,
|
||||
rawName: disk.device || diskLabel,
|
||||
type: 'agentDisk' as const,
|
||||
resourceType: 'Agent Disk',
|
||||
host: agentIdForActions,
|
||||
node: agentDisplayName,
|
||||
instance: disk.type || '',
|
||||
status: agentResource.status,
|
||||
hasOverride: hasCustomThresholds || Boolean(override?.disabled),
|
||||
disabled: override?.disabled || false,
|
||||
thresholds: override?.thresholds || {},
|
||||
defaults: { disk: props.agentDefaults.disk },
|
||||
subtitle: `${((disk.used || 0) / 1024 / 1024 / 1024).toFixed(1)} / ${((disk.total || 0) / 1024 / 1024 / 1024).toFixed(1)} GB`,
|
||||
} satisfies TableResource);
|
||||
});
|
||||
});
|
||||
|
||||
(props.overrides() ?? [])
|
||||
.filter((override) => override.type === 'agentDisk' && !seen.has(override.id))
|
||||
.forEach((override) => {
|
||||
const name = override.name || override.id;
|
||||
disks.push({
|
||||
id: override.id,
|
||||
name,
|
||||
displayName: name,
|
||||
rawName: name,
|
||||
type: 'agentDisk' as const,
|
||||
resourceType: 'Agent Disk',
|
||||
host: '',
|
||||
node: 'Unknown Agent',
|
||||
instance: '',
|
||||
status: 'unknown',
|
||||
hasOverride: true,
|
||||
disabled: override.disabled || false,
|
||||
thresholds: override.thresholds || {},
|
||||
defaults: { disk: props.agentDefaults.disk },
|
||||
});
|
||||
});
|
||||
|
||||
return search
|
||||
? disks.filter(
|
||||
(disk) => disk.name.toLowerCase().includes(search) || disk.node?.toLowerCase().includes(search),
|
||||
)
|
||||
: disks;
|
||||
}, []);
|
||||
|
||||
const agentDisksGroupedByAgent = createMemo<Record<string, TableResource[]>>(() => {
|
||||
const grouped: Record<string, TableResource[]> = {};
|
||||
agentDisksWithOverrides().forEach((disk) => {
|
||||
const key = disk.node?.trim() || 'Unknown Agent';
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = [];
|
||||
}
|
||||
grouped[key].push(disk);
|
||||
});
|
||||
|
||||
Object.values(grouped).forEach((resources) => {
|
||||
resources.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
return grouped;
|
||||
});
|
||||
|
||||
return {
|
||||
nodesWithOverrides,
|
||||
agentsWithOverrides,
|
||||
agentDisksWithOverrides,
|
||||
agentDisksGroupedByAgent,
|
||||
};
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import { createMemo } from 'solid-js';
|
||||
|
||||
import type { PMGThresholdDefaults } from '@/types/alerts';
|
||||
import { getAlertResourceDisplayLabel } from '@/features/alerts/helpers';
|
||||
|
||||
import { PMG_KEY_TO_NORMALIZED, PMG_NORMALIZED_TO_KEY, PMG_THRESHOLD_COLUMNS } from '../constants';
|
||||
import type { Resource as TableResource } from '../tableTypes';
|
||||
import { ThresholdsDataInputs } from '../thresholdsResourceModel';
|
||||
import {
|
||||
createOverridesMap,
|
||||
hasThresholdDiff,
|
||||
normalizeStorageStatus,
|
||||
storageCoords,
|
||||
} from '../thresholdsResourceModel';
|
||||
|
||||
export function useThresholdsInfrastructureData(inputs: ThresholdsDataInputs) {
|
||||
const { props, editingId, searchTerm } = inputs;
|
||||
|
||||
const pbsServersWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
const pbsServers = (props.pbsInstances || []).map((pbs) => {
|
||||
const override = overridesMap.get(pbs.id);
|
||||
const hasCustomThresholds = hasThresholdDiff(override, {
|
||||
cpu: props.pbsDefaults?.cpu ?? 80,
|
||||
memory: props.pbsDefaults?.memory ?? 85,
|
||||
});
|
||||
const disableConnectivity = override?.disableConnectivity || false;
|
||||
|
||||
return {
|
||||
id: pbs.id,
|
||||
name: pbs.name,
|
||||
type: 'pbs' as const,
|
||||
resourceType: 'PBS',
|
||||
host: pbs.host,
|
||||
status: pbs.status,
|
||||
cpu: pbs.cpu,
|
||||
memory: pbs.memory,
|
||||
memoryUsed: pbs.memoryUsed,
|
||||
memoryTotal: pbs.memoryTotal,
|
||||
uptime: pbs.uptime,
|
||||
hasOverride: hasCustomThresholds || disableConnectivity,
|
||||
disabled: false,
|
||||
disableConnectivity,
|
||||
thresholds: override?.thresholds || {},
|
||||
defaults: {
|
||||
cpu: props.pbsDefaults?.cpu ?? 80,
|
||||
memory: props.pbsDefaults?.memory ?? 85,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return search
|
||||
? pbsServers.filter(
|
||||
(pbs) => pbs.name.toLowerCase().includes(search) || pbs.host?.toLowerCase().includes(search),
|
||||
)
|
||||
: pbsServers;
|
||||
}, []);
|
||||
|
||||
const pmgGlobalDefaults = createMemo<Record<string, number>>(() => {
|
||||
const defaults = props.pmgThresholds();
|
||||
const record: Record<string, number> = {};
|
||||
PMG_THRESHOLD_COLUMNS.forEach(({ key, normalized }) => {
|
||||
const value = defaults[key as keyof PMGThresholdDefaults];
|
||||
record[normalized] = typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
||||
});
|
||||
return record;
|
||||
});
|
||||
|
||||
const pmgServersWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
const defaultThresholds = pmgGlobalDefaults();
|
||||
|
||||
const pmgServers = (props.pmgInstances || []).map((pmg) => {
|
||||
const override = overridesMap.get(pmg.id);
|
||||
const thresholdOverrides: Record<string, number> = {};
|
||||
const overrideThresholds = (override?.thresholds ?? {}) as Record<string, unknown>;
|
||||
|
||||
Object.entries(overrideThresholds).forEach(([rawKey, rawValue]) => {
|
||||
if (typeof rawValue !== 'number' || Number.isNaN(rawValue)) return;
|
||||
const normalizedKey =
|
||||
PMG_KEY_TO_NORMALIZED.get(rawKey as keyof PMGThresholdDefaults) ||
|
||||
(PMG_NORMALIZED_TO_KEY.has(rawKey) ? rawKey : undefined);
|
||||
if (!normalizedKey) return;
|
||||
thresholdOverrides[normalizedKey] = rawValue;
|
||||
});
|
||||
|
||||
const hasOverride =
|
||||
Boolean(override?.disableConnectivity) ||
|
||||
Boolean(override?.disabled) ||
|
||||
Object.keys(thresholdOverrides).length > 0;
|
||||
|
||||
return {
|
||||
id: pmg.id,
|
||||
name: pmg.name,
|
||||
type: 'pmg' as const,
|
||||
resourceType: 'PMG',
|
||||
host: pmg.host,
|
||||
status: pmg.status,
|
||||
hasOverride,
|
||||
disabled: override?.disabled || false,
|
||||
disableConnectivity: override?.disableConnectivity || false,
|
||||
thresholds: thresholdOverrides,
|
||||
defaults: { ...defaultThresholds },
|
||||
};
|
||||
});
|
||||
|
||||
return search
|
||||
? pmgServers.filter(
|
||||
(pmg) => pmg.name.toLowerCase().includes(search) || pmg.host?.toLowerCase().includes(search),
|
||||
)
|
||||
: pmgServers;
|
||||
}, []);
|
||||
|
||||
const storageWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
|
||||
const storageDevices = (props.storage ?? []).map((storage) => {
|
||||
const override = overridesMap.get(storage.id);
|
||||
const coords = storageCoords(storage);
|
||||
const hasCustomThresholds = hasThresholdDiff(override, { usage: props.storageDefault() });
|
||||
|
||||
return {
|
||||
id: storage.id,
|
||||
name: getAlertResourceDisplayLabel(storage),
|
||||
displayName: getAlertResourceDisplayLabel(storage),
|
||||
rawName: storage.name,
|
||||
type: 'storage' as const,
|
||||
resourceType: 'Storage',
|
||||
node: coords.node,
|
||||
instance: coords.instance,
|
||||
status: normalizeStorageStatus(storage.status),
|
||||
hasOverride: hasCustomThresholds || Boolean(override?.disabled),
|
||||
disabled: override?.disabled || false,
|
||||
thresholds: override?.thresholds || {},
|
||||
defaults: { usage: props.storageDefault() },
|
||||
};
|
||||
});
|
||||
|
||||
return search
|
||||
? storageDevices.filter(
|
||||
(storage) =>
|
||||
storage.name.toLowerCase().includes(search) || storage.node?.toLowerCase().includes(search),
|
||||
)
|
||||
: storageDevices;
|
||||
}, []);
|
||||
|
||||
const storageGroupedByNode = createMemo<Record<string, TableResource[]>>(() => {
|
||||
const grouped: Record<string, TableResource[]> = {};
|
||||
storageWithOverrides().forEach((storage) => {
|
||||
const key = storage.node?.trim() || 'Unassigned';
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = [];
|
||||
}
|
||||
grouped[key].push(storage);
|
||||
});
|
||||
|
||||
Object.values(grouped).forEach((resources) => {
|
||||
resources.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
return grouped;
|
||||
});
|
||||
|
||||
return {
|
||||
pbsServersWithOverrides,
|
||||
pmgGlobalDefaults,
|
||||
pmgServersWithOverrides,
|
||||
storageWithOverrides,
|
||||
storageGroupedByNode,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import { unwrap } from 'solid-js/store';
|
||||
|
||||
import { requiresGovernedResourceDisplay } from '@/types/resource';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import {
|
||||
getAgentDiscoveryResourceId,
|
||||
isAppContainerDiscoveryResourceType,
|
||||
} from '@/utils/discoveryTarget';
|
||||
import { getAlertResourceDisplayLabel } from '@/features/alerts/helpers';
|
||||
|
||||
import type { GroupHeaderMeta } from './tableTypes';
|
||||
import type { Override, ThresholdsTableProps } from './types';
|
||||
|
||||
export interface ThresholdsDataInputs {
|
||||
props: ThresholdsTableProps;
|
||||
editingId: () => string | null;
|
||||
searchTerm: () => string;
|
||||
}
|
||||
|
||||
export const platformData = (resource: Resource): Record<string, unknown> | undefined =>
|
||||
resource.platformData ? (unwrap(resource.platformData) as Record<string, unknown>) : undefined;
|
||||
|
||||
export const readRecord = (value: unknown): Record<string, unknown> | undefined =>
|
||||
value && typeof value === 'object' ? (value as Record<string, unknown>) : undefined;
|
||||
|
||||
export const readString = (value: unknown): string | undefined =>
|
||||
typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
||||
|
||||
export const uniqueIds = (...values: unknown[]): string[] => {
|
||||
const ids: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
values.forEach((value) => {
|
||||
const normalized = readString(value);
|
||||
if (!normalized || seen.has(normalized)) return;
|
||||
seen.add(normalized);
|
||||
ids.push(normalized);
|
||||
});
|
||||
return ids;
|
||||
};
|
||||
|
||||
export const createOverridesMap = (overrides: Override[] | undefined) =>
|
||||
new Map((overrides ?? []).map((override) => [override.id, override]));
|
||||
|
||||
export const hasThresholdDiff = (
|
||||
override: Override | undefined,
|
||||
defaults: Record<string, number | undefined>,
|
||||
) =>
|
||||
Boolean(
|
||||
override?.thresholds &&
|
||||
Object.keys(override.thresholds).some((key) => {
|
||||
const thresholdKey = key as keyof Override['thresholds'];
|
||||
return (
|
||||
override.thresholds[thresholdKey] !== undefined &&
|
||||
override.thresholds[thresholdKey] !== defaults[key]
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
export function hostOverrideIdCandidates(resource: Resource): string[] {
|
||||
const data = platformData(resource);
|
||||
const agent = readRecord(data?.agent);
|
||||
const discoveryTarget = resource.discoveryTarget ?? null;
|
||||
return uniqueIds(
|
||||
getAgentDiscoveryResourceId(discoveryTarget),
|
||||
discoveryTarget?.agentId,
|
||||
resource.agent?.agentId,
|
||||
agent?.agentId,
|
||||
data?.agentId,
|
||||
resource.id,
|
||||
);
|
||||
}
|
||||
|
||||
export const hostActionId = (resource: Resource): string =>
|
||||
hostOverrideIdCandidates(resource)[0] || resource.id;
|
||||
|
||||
export const dockerHostOverrideIdCandidates = (resource: Resource): string[] => {
|
||||
const data = platformData(resource);
|
||||
const docker = readRecord(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}`),
|
||||
);
|
||||
|
||||
export const findOverrideByCandidates = (
|
||||
overridesMap: Map<string, Override>,
|
||||
candidates: string[],
|
||||
): Override | undefined => {
|
||||
for (const candidate of candidates) {
|
||||
const override = overridesMap.get(candidate);
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getFriendlyNodeName = (value: string, clusterName?: string): string => {
|
||||
if (!value) return value;
|
||||
|
||||
const clusterLower = clusterName?.toLowerCase().trim();
|
||||
|
||||
const normalizeToken = (token?: string | null): string => {
|
||||
if (!token) return '';
|
||||
let result = token
|
||||
.replace(/\(.*?\)/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
if (clusterLower) {
|
||||
result = result
|
||||
.split(' ')
|
||||
.filter((part) => part.toLowerCase() !== clusterLower)
|
||||
.join(' ')
|
||||
.trim();
|
||||
}
|
||||
if (!result) return '';
|
||||
const firstWord = result.split(/\s+/)[0] || result;
|
||||
const withoutDomain = firstWord.includes('.') ? (firstWord.split('.')[0] ?? firstWord) : firstWord;
|
||||
return withoutDomain.trim();
|
||||
};
|
||||
|
||||
const parentheticalMatch = value.match(/\(([^)]+)\)/);
|
||||
const parentheticalRaw = parentheticalMatch?.[1]?.trim();
|
||||
|
||||
let base = normalizeToken(value);
|
||||
if (!base) {
|
||||
base = value.trim();
|
||||
}
|
||||
|
||||
const parenthetical = normalizeToken(parentheticalRaw);
|
||||
if (parenthetical && parenthetical.toLowerCase() !== base.toLowerCase()) {
|
||||
return parenthetical;
|
||||
}
|
||||
|
||||
return base;
|
||||
};
|
||||
|
||||
export const getFriendlyAlertNodeName = (
|
||||
value: string,
|
||||
policy?: Resource['policy'],
|
||||
clusterName?: string,
|
||||
): string => (requiresGovernedResourceDisplay(policy) ? value : getFriendlyNodeName(value, clusterName));
|
||||
|
||||
export function buildNodeHeaderMeta(node: Resource) {
|
||||
const data = platformData(node);
|
||||
const clusterName = (data?.clusterName as string | undefined) ?? undefined;
|
||||
const isClusterMember =
|
||||
(data?.isClusterMember as boolean | undefined) ?? Boolean(node.clusterId);
|
||||
|
||||
const originalDisplayName = getAlertResourceDisplayLabel(node);
|
||||
const friendlyName = getFriendlyAlertNodeName(originalDisplayName, node.policy, clusterName);
|
||||
|
||||
const guestUrlValue = typeof data?.guestURL === 'string' ? data.guestURL.trim() : '';
|
||||
const hostValue = typeof data?.host === 'string' ? data.host.trim() : '';
|
||||
|
||||
let host: string | undefined;
|
||||
if (guestUrlValue && guestUrlValue !== '') {
|
||||
host = guestUrlValue.startsWith('http') ? guestUrlValue : `https://${guestUrlValue}`;
|
||||
} else if (hostValue && hostValue !== '') {
|
||||
host = hostValue.startsWith('http')
|
||||
? hostValue
|
||||
: `https://${hostValue.includes(':') ? hostValue : `${hostValue}:8006`}`;
|
||||
} else if (node.name) {
|
||||
host = `https://${node.name.includes(':') ? node.name : `${node.name}:8006`}`;
|
||||
}
|
||||
|
||||
const headerMeta: GroupHeaderMeta = {
|
||||
type: 'node',
|
||||
displayName: friendlyName,
|
||||
rawName: originalDisplayName,
|
||||
host,
|
||||
status: node.status,
|
||||
clusterName: isClusterMember ? clusterName?.trim() || 'Cluster' : undefined,
|
||||
isClusterMember,
|
||||
};
|
||||
|
||||
const keys = new Set<string>();
|
||||
[node.name, originalDisplayName, friendlyName].forEach((value) => {
|
||||
if (value && value.trim()) {
|
||||
keys.add(value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
return { headerMeta, keys };
|
||||
}
|
||||
|
||||
export const agentDiskResourceId = (agentId: string, mountpoint: string, device?: string): string => {
|
||||
let label = (mountpoint?.trim() || device?.trim() || 'disk').toLowerCase();
|
||||
label = label
|
||||
.replace(/[^a-z0-9]/g, '-')
|
||||
.replace(/-{2,}/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!label) label = 'unknown';
|
||||
return `agent:${agentId}/disk:${label}`;
|
||||
};
|
||||
|
||||
export 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 || '',
|
||||
};
|
||||
};
|
||||
|
||||
export const normalizeStorageStatus = (status: string | undefined): string => {
|
||||
switch ((status ?? '').toLowerCase()) {
|
||||
case 'online':
|
||||
case 'running':
|
||||
case 'available':
|
||||
return 'available';
|
||||
default:
|
||||
return 'offline';
|
||||
}
|
||||
};
|
||||
@@ -20,9 +20,14 @@ import alertThresholdsTabSource from '@/features/alerts/tabs/ThresholdsTab.tsx?r
|
||||
import recentAlertsPanelSource from '@/components/Alerts/RecentAlertsPanel.tsx?raw';
|
||||
import thresholdsTableSource from '@/components/Alerts/ThresholdsTable.tsx?raw';
|
||||
import thresholdsDataHookSource from '@/features/alerts/thresholds/hooks/useThresholdsData.ts?raw';
|
||||
import thresholdsHostDataHookSource from '@/features/alerts/thresholds/hooks/useThresholdsHostData.ts?raw';
|
||||
import thresholdsDockerDataHookSource from '@/features/alerts/thresholds/hooks/useThresholdsDockerData.ts?raw';
|
||||
import thresholdsGuestDataHookSource from '@/features/alerts/thresholds/hooks/useThresholdsGuestData.ts?raw';
|
||||
import thresholdsInfrastructureDataHookSource from '@/features/alerts/thresholds/hooks/useThresholdsInfrastructureData.ts?raw';
|
||||
import thresholdsRecoveryDefaultsStateHookSource from '@/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts?raw';
|
||||
import thresholdsTableStateHookSource from '@/features/alerts/thresholds/hooks/useThresholdsTableState.ts?raw';
|
||||
import thresholdsOverrideMutationsHookSource from '@/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts?raw';
|
||||
import thresholdsResourceModelSource from '@/features/alerts/thresholds/thresholdsResourceModel.ts?raw';
|
||||
|
||||
import {
|
||||
ALERT_TAB_SEGMENTS,
|
||||
@@ -325,6 +330,23 @@ describe('tab path helpers', () => {
|
||||
expect(thresholdsTableSource).not.toContain('const [searchTerm, setSearchTerm] = createSignal');
|
||||
expect(thresholdsTableSource).not.toContain('const handleTabClick =');
|
||||
expect(thresholdsDataHookSource).toContain('export function useThresholdsData');
|
||||
expect(thresholdsDataHookSource).toContain('useThresholdsHostData(inputs)');
|
||||
expect(thresholdsDataHookSource).toContain('useThresholdsDockerData(inputs)');
|
||||
expect(thresholdsDataHookSource).toContain('useThresholdsGuestData(inputs)');
|
||||
expect(thresholdsDataHookSource).toContain('useThresholdsInfrastructureData(inputs)');
|
||||
expect(thresholdsDataHookSource).not.toContain('const hostOverrideIdCandidates =');
|
||||
expect(thresholdsDataHookSource).not.toContain('const dockerContainersGroupedByHost = createMemo');
|
||||
expect(thresholdsHostDataHookSource).toContain('export function useThresholdsHostData');
|
||||
expect(thresholdsHostDataHookSource).toContain('hostOverrideIdCandidates(agentResource)');
|
||||
expect(thresholdsDockerDataHookSource).toContain('export function useThresholdsDockerData');
|
||||
expect(thresholdsDockerDataHookSource).toContain('dockerContainerOverrideIdCandidates');
|
||||
expect(thresholdsGuestDataHookSource).toContain('export function useThresholdsGuestData');
|
||||
expect(thresholdsInfrastructureDataHookSource).toContain(
|
||||
'export function useThresholdsInfrastructureData',
|
||||
);
|
||||
expect(thresholdsResourceModelSource).toContain('export function hostOverrideIdCandidates');
|
||||
expect(thresholdsResourceModelSource).toContain('export function buildNodeHeaderMeta');
|
||||
expect(thresholdsResourceModelSource).toContain('export const normalizeStorageStatus');
|
||||
expect(thresholdsTableStateHookSource).toContain('export function useThresholdsTableState');
|
||||
expect(thresholdsTableStateHookSource).toContain('useThresholdsData(props, editingId, searchTerm)');
|
||||
expect(thresholdsTableStateHookSource).toContain('useThresholdsRecoveryDefaultsState(props)');
|
||||
|
||||
@@ -342,9 +342,14 @@ import alertScheduleTabSource from '@/features/alerts/tabs/ScheduleTab.tsx?raw';
|
||||
import alertThresholdsTabSource from '@/features/alerts/tabs/ThresholdsTab.tsx?raw';
|
||||
import thresholdsTableSource from '@/components/Alerts/ThresholdsTable.tsx?raw';
|
||||
import thresholdsDataHookSource from '@/features/alerts/thresholds/hooks/useThresholdsData.ts?raw';
|
||||
import thresholdsHostDataHookSource from '@/features/alerts/thresholds/hooks/useThresholdsHostData.ts?raw';
|
||||
import thresholdsDockerDataHookSource from '@/features/alerts/thresholds/hooks/useThresholdsDockerData.ts?raw';
|
||||
import thresholdsGuestDataHookSource from '@/features/alerts/thresholds/hooks/useThresholdsGuestData.ts?raw';
|
||||
import thresholdsInfrastructureDataHookSource from '@/features/alerts/thresholds/hooks/useThresholdsInfrastructureData.ts?raw';
|
||||
import thresholdsRecoveryDefaultsStateHookSource from '@/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts?raw';
|
||||
import thresholdsTableStateHookSource from '@/features/alerts/thresholds/hooks/useThresholdsTableState.ts?raw';
|
||||
import thresholdsOverrideMutationsHookSource from '@/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts?raw';
|
||||
import thresholdsResourceModelSource from '@/features/alerts/thresholds/thresholdsResourceModel.ts?raw';
|
||||
import alertIncidentPresentationSource from '@/utils/alertIncidentPresentation.ts?raw';
|
||||
import alertHistoryPresentationSource from '@/utils/alertHistoryPresentation.ts?raw';
|
||||
import bulkEditDialogSource from '@/components/Alerts/BulkEditDialog.tsx?raw';
|
||||
@@ -2918,6 +2923,21 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(thresholdsTableSource).not.toContain('const [searchTerm, setSearchTerm] = createSignal');
|
||||
expect(thresholdsTableSource).not.toContain('const handleTabClick =');
|
||||
expect(thresholdsDataHookSource).toContain('export function useThresholdsData');
|
||||
expect(thresholdsDataHookSource).toContain('useThresholdsHostData(inputs)');
|
||||
expect(thresholdsDataHookSource).toContain('useThresholdsDockerData(inputs)');
|
||||
expect(thresholdsDataHookSource).toContain('useThresholdsGuestData(inputs)');
|
||||
expect(thresholdsDataHookSource).toContain('useThresholdsInfrastructureData(inputs)');
|
||||
expect(thresholdsDataHookSource).not.toContain('const hostOverrideIdCandidates =');
|
||||
expect(thresholdsDataHookSource).not.toContain('const dockerContainersGroupedByHost = createMemo');
|
||||
expect(thresholdsHostDataHookSource).toContain('export function useThresholdsHostData');
|
||||
expect(thresholdsDockerDataHookSource).toContain('export function useThresholdsDockerData');
|
||||
expect(thresholdsGuestDataHookSource).toContain('export function useThresholdsGuestData');
|
||||
expect(thresholdsInfrastructureDataHookSource).toContain(
|
||||
'export function useThresholdsInfrastructureData',
|
||||
);
|
||||
expect(thresholdsResourceModelSource).toContain('export function hostOverrideIdCandidates');
|
||||
expect(thresholdsResourceModelSource).toContain('export function buildNodeHeaderMeta');
|
||||
expect(thresholdsResourceModelSource).toContain('export const normalizeStorageStatus');
|
||||
expect(thresholdsTableStateHookSource).toContain('export function useThresholdsTableState');
|
||||
expect(thresholdsTableStateHookSource).toContain('useCollapsedSections()');
|
||||
expect(thresholdsTableStateHookSource).toContain('useThresholdsData(props, editingId, searchTerm)');
|
||||
|
||||
Reference in New Issue
Block a user