mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Add Kubernetes, TrueNAS, and vSphere alert targets
This commit is contained in:
parent
75d1a0477b
commit
05abf07219
57 changed files with 4226 additions and 302 deletions
|
|
@ -84,6 +84,12 @@ operator-facing alert routing behavior for live runtime alerts.
|
|||
62. `frontend-modern/src/components/Alerts/IncidentAssistantHandoffButton.tsx`
|
||||
63. `frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts`
|
||||
64. `internal/alerts/storage_override_identity.go`
|
||||
65. `internal/alerts/unified_eval.go`
|
||||
66. `frontend-modern/src/components/Alerts/ThresholdsTable.tsx`
|
||||
67. `frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts`
|
||||
68. `frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsPlatformData.ts`
|
||||
69. `frontend-modern/src/utils/alertTargetTypes.ts`
|
||||
70. `frontend-modern/src/types/alerts.ts`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
|
|
@ -127,6 +133,12 @@ operator-facing alert routing behavior for live runtime alerts.
|
|||
handoffs must not add suggested prompt chips or route-owned remediation
|
||||
instructions; the configured model owns investigation and next-step
|
||||
reasoning after it receives the context.
|
||||
9. Add or change alert target resource types through
|
||||
`internal/alerts/specs/types.go`, `internal/alerts/config/identity.go`,
|
||||
`internal/alerts/unified_eval.go`, and
|
||||
`frontend-modern/src/utils/alertTargetTypes.ts`. Supported target types
|
||||
must share the unified evaluator, the canonical threshold/override identity
|
||||
chain, and the standard notification delivery path.
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
|
@ -167,6 +179,22 @@ alert feature gating from the shared entitlements layer, not from a per-surface
|
|||
Canonical alert identity and evaluation are the live runtime model. Remaining
|
||||
legacy references should exist only in explicit migration or negative test
|
||||
boundaries.
|
||||
All v6 platform alert targets must enter runtime threshold evaluation through
|
||||
`UnifiedResourceInput` and `internal/alerts/unified_eval.go`: Proxmox guests/
|
||||
nodes/storage, Docker hosts/containers/services, Kubernetes clusters/nodes/
|
||||
namespaces/deployments/pods, TrueNAS systems/pools/datasets/disks, VMware
|
||||
vSphere hosts/VMs/datastores/networks, PBS, PMG, and standalone host agents.
|
||||
Adding a platform-local evaluator branch for these resource families is
|
||||
forbidden.
|
||||
Per-platform defaults, per-resource overrides, global disables, active-alert
|
||||
reevaluation, history persistence, and notification delivery must use the same
|
||||
alert configuration shape rather than a platform-specific sidecar.
|
||||
The browser thresholds surface is also platform-shaped: Proxmox, Docker,
|
||||
Kubernetes, TrueNAS, vSphere, PBS, PMG, and Systems. It must use the shared
|
||||
FilterBar chip and "+ Filter" pattern for resource filtering, and alert tables
|
||||
must use the canonical platform table column-kind alignment helpers from
|
||||
`frontend-modern/src/features/platformPage/` rather than hard-coded table
|
||||
alignment classes.
|
||||
Guest metric canonical state remains resource-backed and therefore node-scoped
|
||||
for Proxmox guests, so node moves must not strand active alert state on the
|
||||
previous resource ID. When a guest metric alert survives a node move, alerts
|
||||
|
|
@ -625,19 +653,25 @@ shell composition for search/help/nav plus bulk-edit dialog flow, while the
|
|||
tab render owners live in
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTablePMGTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx`, and
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx`. New
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableKubernetesTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableTrueNASTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableVMwareTab.tsx`, and
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTablePBSTab.tsx`. New
|
||||
threshold row grouping, override-ID compatibility, resource normalization,
|
||||
thresholds-table controller logic, or per-tab runtime should land in those
|
||||
feature hooks and tab owners rather than being rebuilt inside the shell.
|
||||
The shell-owned thresholds sub-routes are now the neutral user-facing paths
|
||||
`/alerts/thresholds/infrastructure`, `/alerts/thresholds/systems`,
|
||||
`/alerts/thresholds/mail-gateway`, and `/alerts/thresholds/containers`.
|
||||
Legacy `/alerts/thresholds/proxmox` and `/alerts/thresholds/agents` links
|
||||
must redirect to the neutral infrastructure and systems routes so API-backed
|
||||
platforms like TrueNAS do not remain stranded behind provider-specific deep
|
||||
links.
|
||||
Within the infrastructure tab, render-heavy ownership now further routes through
|
||||
The shell-owned thresholds sub-routes are platform-shaped user-facing paths:
|
||||
`/alerts/thresholds/proxmox`, `/alerts/thresholds/docker`,
|
||||
`/alerts/thresholds/kubernetes`, `/alerts/thresholds/truenas`,
|
||||
`/alerts/thresholds/vmware`, `/alerts/thresholds/pbs`,
|
||||
`/alerts/thresholds/pmg`, and `/alerts/thresholds/systems`. Legacy neutral
|
||||
links like `/alerts/thresholds/infrastructure`,
|
||||
`/alerts/thresholds/containers`, and `/alerts/thresholds/mail-gateway` must
|
||||
redirect to the matching platform-shaped route. Legacy
|
||||
`/alerts/thresholds/agents` links must continue to resolve to Systems.
|
||||
Within the Proxmox tab, render-heavy ownership now further routes through
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestsSection.tsx`,
|
||||
|
|
@ -668,15 +702,18 @@ links into infrastructure, workloads, storage, and recovery. Pulse does not
|
|||
promise a TrueNAS-only alert workflow or provider-specific alert management
|
||||
surface beyond the shared alerts product.
|
||||
At the current locked VMware floor, alert support must mean the same shared
|
||||
alert surfaces can evaluate, show, and drill into vSphere alarm and health
|
||||
signals on canonical `agent`, `vm`, and `storage` resources, with related
|
||||
event/task context routed through the shared incident and resource links.
|
||||
Pulse must not grow a VMware-only alert shell, alarm editor, or direct alarm-
|
||||
control surface in phase 1.
|
||||
alert surfaces can evaluate, show, and drill into vSphere alarm, health, and
|
||||
threshold signals on canonical `agent`, `vm`, `storage`, and `network`
|
||||
resources, with related event/task context routed through the shared incident
|
||||
and resource links. This is the alerts support floor for admitted vSphere
|
||||
resources; it does not by itself promote the broader VMware platform readiness
|
||||
state beyond the separately governed platform-admission floor. Pulse must not
|
||||
grow a VMware-only alert shell, alarm editor, or direct alarm-control surface
|
||||
in phase 1.
|
||||
That same VMware alert rule now also includes the topology boundary. Alarm
|
||||
context that originates on a datacenter, cluster, folder, or resource pool may
|
||||
inform a shared incident, but it must still resolve onto canonical `agent`,
|
||||
`vm`, or `storage` investigation paths rather than creating synthetic
|
||||
`vm`, `storage`, or `network` investigation paths rather than creating synthetic
|
||||
top-level VMware incident resources. If that attachment cannot be done
|
||||
honestly for a given signal, the signal should remain supporting context
|
||||
instead of inflating the support claim.
|
||||
|
|
|
|||
|
|
@ -161,6 +161,13 @@ work extends shared components instead of creating new local variants.
|
|||
|
||||
## Shared Boundaries
|
||||
|
||||
Alert thresholds consume the shared FilterBar primitive and route state, while
|
||||
the alerts subsystem owns the resource data and platform-specific threshold
|
||||
tabs. The thresholds platform IA is platform-shaped: Proxmox, Docker,
|
||||
Kubernetes, TrueNAS, vSphere, PBS, PMG, and Systems. Frontend primitives own
|
||||
the chip, reset, "+ Filter", and route-backed shell pattern; alerts must not
|
||||
replace that with page-local search/tab chrome or legacy neutral buckets.
|
||||
|
||||
The System Updates install guide is a shared settings primitive, not a
|
||||
deployment-lane-only panel. `UpdateInstallGuide` must render the canonical
|
||||
update-plan readiness verdict inline with the update action, and a blocked
|
||||
|
|
@ -341,6 +348,10 @@ filter chip or an explicit page-owned advanced selector. Platform-owned filter
|
|||
selectors must also exclude facet options from other platform scopes, even when
|
||||
the underlying shared surface is mounted from the same Workloads or Storage
|
||||
component.
|
||||
Alert configuration tables follow the same primitive boundary: the alerts
|
||||
owner supplies platform-specific threshold groups and filter catalog values,
|
||||
while the shared FilterBar owns the chip, reset, and "+ Filter" interaction
|
||||
shape so thresholds do not reintroduce page-local search/tab chrome.
|
||||
Platform sub-routes that add native provider inventory must stay on the shared
|
||||
platform page and table primitives. The vSphere Networks surface routes through
|
||||
`/vmware/networks`, the shared platform tab model, the command palette
|
||||
|
|
@ -2946,17 +2957,23 @@ is now explicitly a shell consumer rather than the data or controller owner,
|
|||
and the tab render owners live in
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTablePMGTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx`, and
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx`.
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableKubernetesTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableTrueNASTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableVMwareTab.tsx`, and
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTablePBSTab.tsx`.
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts`
|
||||
owns the neutral thresholds sub-route contract:
|
||||
`/alerts/thresholds/infrastructure`, `/alerts/thresholds/systems`,
|
||||
`/alerts/thresholds/mail-gateway`, and `/alerts/thresholds/containers`.
|
||||
Legacy `/alerts/thresholds/proxmox` and `/alerts/thresholds/agents` links
|
||||
must redirect to the neutral infrastructure and systems routes so API-backed
|
||||
platforms such as TrueNAS stay on canonical page language rather than
|
||||
provider-specific aliases.
|
||||
The infrastructure tab is itself now a shell that composes
|
||||
owns the platform-shaped thresholds sub-route contract:
|
||||
`/alerts/thresholds/proxmox`, `/alerts/thresholds/docker`,
|
||||
`/alerts/thresholds/kubernetes`, `/alerts/thresholds/truenas`,
|
||||
`/alerts/thresholds/vmware`, `/alerts/thresholds/pbs`,
|
||||
`/alerts/thresholds/pmg`, and `/alerts/thresholds/systems`. Legacy neutral
|
||||
links such as `/alerts/thresholds/infrastructure`,
|
||||
`/alerts/thresholds/containers`, and `/alerts/thresholds/mail-gateway` must
|
||||
redirect to the matching platform-shaped route; legacy
|
||||
`/alerts/thresholds/agents` links must continue to resolve to Systems.
|
||||
The Proxmox tab is itself now a shell that composes
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestsSection.tsx`,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ truth for live infrastructure data.
|
|||
48. `internal/truenas/provider.go`
|
||||
49. `internal/models/ceph_cluster_identity.go`
|
||||
50. `internal/truenas/types.go`
|
||||
51. `internal/monitoring/monitor_alert_sync.go`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
|
|
@ -240,6 +241,12 @@ truth for live infrastructure data.
|
|||
fields locally. Monitoring must preserve those objects as native cluster
|
||||
inventory instead of flattening them into pods, deployments, or generic
|
||||
networking, storage, configuration, or controller rows.
|
||||
19. Add or change unified-resource alert synchronization through
|
||||
`internal/monitoring/monitor_alert_sync.go` and the alerts subsystem
|
||||
contract together. Monitoring may pass the current unified-resource snapshot
|
||||
into the alert manager, but threshold selection, override identity, active
|
||||
alert state, and notification delivery remain alerts-owned. The monitoring
|
||||
sync bridge must not introduce per-platform evaluator branches.
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
|
|
|||
|
|
@ -1228,6 +1228,7 @@
|
|||
"owned_files": [
|
||||
"frontend-modern/src/pages/Alerts.tsx",
|
||||
"frontend-modern/src/stores/alertsActivation.ts",
|
||||
"frontend-modern/src/types/alerts.ts",
|
||||
"frontend-modern/src/utils/alertActivationPresentation.ts",
|
||||
"frontend-modern/src/utils/alertAdministrationPresentation.ts",
|
||||
"frontend-modern/src/utils/alertBulkEditPresentation.ts",
|
||||
|
|
@ -1243,6 +1244,7 @@
|
|||
"frontend-modern/src/utils/alertSchedulePresentation.ts",
|
||||
"frontend-modern/src/utils/alertSeverityPresentation.ts",
|
||||
"frontend-modern/src/utils/alertTabsPresentation.ts",
|
||||
"frontend-modern/src/utils/alertTargetTypes.ts",
|
||||
"frontend-modern/src/utils/alertThresholdDefaults.ts",
|
||||
"frontend-modern/src/utils/alertThresholdsPresentation.ts",
|
||||
"frontend-modern/src/utils/alertThresholdsSectionPresentation.ts",
|
||||
|
|
@ -1303,6 +1305,7 @@
|
|||
],
|
||||
"match_files": [
|
||||
"frontend-modern/src/pages/Alerts.tsx",
|
||||
"frontend-modern/src/types/alerts.ts",
|
||||
"frontend-modern/src/utils/alertOverviewPresentation.ts"
|
||||
],
|
||||
"allow_same_subsystem_tests": false,
|
||||
|
|
@ -1325,7 +1328,21 @@
|
|||
"frontend-modern/src/features/alerts/thresholds/__tests__/helpers.test.ts",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/__tests__/useThresholdsTableState.test.tsx",
|
||||
"frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts",
|
||||
"frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts"
|
||||
"frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts",
|
||||
"frontend-modern/src/utils/__tests__/alertTargetTypes.test.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "alert-target-type-resolution",
|
||||
"label": "alert target type resolver proof",
|
||||
"match_prefixes": [],
|
||||
"match_files": [
|
||||
"frontend-modern/src/utils/alertTargetTypes.ts"
|
||||
],
|
||||
"allow_same_subsystem_tests": false,
|
||||
"test_prefixes": [],
|
||||
"exact_files": [
|
||||
"frontend-modern/src/utils/__tests__/alertTargetTypes.test.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ import {
|
|||
getGroupedTableRowCellClass,
|
||||
getGroupedTableRowClass,
|
||||
} from '@/components/shared/groupedTableRowPresentation';
|
||||
import {
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import { AlertResourceTableRow } from './AlertResourceTableRow';
|
||||
import { AlertResourceGroupHeader } from './AlertResourceGroupHeader';
|
||||
import {
|
||||
|
|
@ -31,6 +35,7 @@ import {
|
|||
getAlertResourceTableNoResultsState,
|
||||
} from '@/utils/alertResourceTablePresentation';
|
||||
import {
|
||||
getAlertResourceColumnKind,
|
||||
getAlertResourceColumnHeaderTooltip,
|
||||
getAlertResourceEnabledDefault,
|
||||
getAlertResourceMetricBounds,
|
||||
|
|
@ -115,7 +120,9 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
<TableHeader>
|
||||
<TableRow class="text-muted">
|
||||
<Show when={props.table.onBulkEdit}>
|
||||
<TableHead class="text-center w-10 px-2 border-r border-border">
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('badge')} w-10 border-r border-border`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.allSelected()}
|
||||
|
|
@ -130,12 +137,16 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
/>
|
||||
</TableHead>
|
||||
</Show>
|
||||
<TableHead class="text-center w-16">Alerts</TableHead>
|
||||
<TableHead class="text-left w-1/4">Resource</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('badge')} w-16`}>
|
||||
Alerts
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} w-1/4`}>
|
||||
Resource
|
||||
</TableHead>
|
||||
<For each={props.table.columns}>
|
||||
{(column) => (
|
||||
<TableHead
|
||||
class="text-center whitespace-normal break-words"
|
||||
class={`${getPlatformTableHeadClassForKind(getAlertResourceColumnKind(column))} whitespace-normal break-words`}
|
||||
title={getAlertResourceColumnHeaderTooltip(column)}
|
||||
>
|
||||
{column}
|
||||
|
|
@ -143,11 +154,14 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
)}
|
||||
</For>
|
||||
<Show when={props.table.showOfflineAlertsColumn}>
|
||||
<TableHead class="text-center" title={OFFLINE_ALERTS_TOOLTIP}>
|
||||
<TableHead
|
||||
class={getPlatformTableHeadClassForKind('badge')}
|
||||
title={OFFLINE_ALERTS_TOOLTIP}
|
||||
>
|
||||
Offline Alerts
|
||||
</TableHead>
|
||||
</Show>
|
||||
<TableHead class="text-center">Actions</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('badge')}>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody class="divide-y divide-border">
|
||||
|
|
@ -162,12 +176,18 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
class={`bg-surface-alt ${props.table.globalDisableFlag?.() ? 'opacity-40' : ''}`}
|
||||
>
|
||||
<Show when={props.table.onBulkEdit}>
|
||||
<TableCell class="p-1 px-2 border-r border-border" />
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('badge')} border-r border-border`}
|
||||
/>
|
||||
</Show>
|
||||
<TableCell class="p-1 px-2 text-center align-middle">
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('badge')} align-middle`}>
|
||||
<Show
|
||||
when={props.table.onToggleGlobalDisable}
|
||||
fallback={<span class="text-sm text-slate-400" aria-hidden="true">-</span>}
|
||||
fallback={
|
||||
<span class="text-sm text-slate-400" aria-hidden="true">
|
||||
-
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<TogglePrimitive
|
||||
|
|
@ -184,7 +204,7 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
</div>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell class="p-1 px-2">
|
||||
<TableCell class={getPlatformTableCellClassForKind('name')}>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-semibold text-base-content">Global Defaults</span>
|
||||
<Show when={props.hasCustomGlobalDefaults()}>
|
||||
|
|
@ -202,7 +222,9 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
const isOff = () => value() === -1;
|
||||
|
||||
return (
|
||||
<TableCell class="p-1 px-2 text-center align-middle">
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind(getAlertResourceColumnKind(column))} align-middle`}
|
||||
>
|
||||
<div class="relative flex justify-center w-full">
|
||||
<input
|
||||
type="number"
|
||||
|
|
@ -249,13 +271,17 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
}}
|
||||
</For>
|
||||
<Show when={props.table.showOfflineAlertsColumn}>
|
||||
<TableCell class="p-1 px-2 text-center align-middle">
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('badge')} align-middle`}>
|
||||
<Show
|
||||
when={props.table.onSetGlobalOfflineState}
|
||||
fallback={
|
||||
<Show
|
||||
when={props.table.onToggleGlobalDisableOffline}
|
||||
fallback={<span class="text-sm text-slate-400" aria-hidden="true">-</span>}
|
||||
fallback={
|
||||
<span class="text-sm text-slate-400" aria-hidden="true">
|
||||
-
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{(() => {
|
||||
const defaultDisabled = props.table.globalDisableOfflineFlag?.() ?? false;
|
||||
|
|
@ -280,8 +306,7 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
>
|
||||
{(() => {
|
||||
const disabledGlobally = props.table.globalDisableFlag?.() ?? false;
|
||||
const defaultDisabled =
|
||||
props.table.globalDisableOfflineFlag?.() ?? false;
|
||||
const defaultDisabled = props.table.globalDisableOfflineFlag?.() ?? false;
|
||||
const defaultSeverity = props.table.globalOfflineSeverity ?? 'warning';
|
||||
const state: OfflineState = defaultDisabled
|
||||
? 'off'
|
||||
|
|
@ -298,7 +323,7 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
</Show>
|
||||
</TableCell>
|
||||
</Show>
|
||||
<TableCell class="p-1 px-2 text-center align-middle">
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('badge')} align-middle`}>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Show
|
||||
when={
|
||||
|
|
@ -355,7 +380,9 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
</button>
|
||||
</Show>
|
||||
<Show when={!props.table.showDelayColumn && !props.hasCustomGlobalDefaults()}>
|
||||
<span class="text-sm text-slate-400" aria-hidden="true">-</span>
|
||||
<span class="text-sm text-slate-400" aria-hidden="true">
|
||||
-
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
|
@ -372,12 +399,16 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
class={`bg-surface-alt ${props.table.globalDisableFlag?.() ? 'opacity-40' : ''}`}
|
||||
>
|
||||
<Show when={props.table.onBulkEdit}>
|
||||
<TableCell class="p-1 px-2 border-r border-border" />
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('badge')} border-r border-border`}
|
||||
/>
|
||||
</Show>
|
||||
<TableCell class="p-1 px-2 text-center align-middle">
|
||||
<span class="text-sm" aria-hidden="true">-</span>
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('badge')} align-middle`}>
|
||||
<span class="text-sm" aria-hidden="true">
|
||||
-
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="p-1 px-2 align-middle">
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('name')} align-middle`}>
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-muted inline-flex items-center gap-1">
|
||||
{getAlertResourceTableAlertDelayLabel()}
|
||||
<HelpIcon contentId="alerts.thresholds.delay" size="xs" />
|
||||
|
|
@ -393,7 +424,9 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
);
|
||||
|
||||
return (
|
||||
<TableCell class="p-1 px-2 text-center align-middle">
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind(getAlertResourceColumnKind(column))} align-middle`}
|
||||
>
|
||||
<div class="relative flex justify-center w-full">
|
||||
<input
|
||||
type="number"
|
||||
|
|
@ -428,12 +461,16 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
}}
|
||||
</For>
|
||||
<Show when={props.table.showOfflineAlertsColumn}>
|
||||
<TableCell class="p-1 px-2 text-center align-middle">
|
||||
<span class="text-sm" aria-hidden="true">-</span>
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('badge')} align-middle`}>
|
||||
<span class="text-sm" aria-hidden="true">
|
||||
-
|
||||
</span>
|
||||
</TableCell>
|
||||
</Show>
|
||||
<TableCell class="p-1 px-2 text-center align-middle">
|
||||
<span class="text-sm" aria-hidden="true">-</span>
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('badge')} align-middle`}>
|
||||
<span class="text-sm" aria-hidden="true">
|
||||
-
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Show>
|
||||
|
|
@ -446,10 +483,7 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
{([nodeName, resources]) => (
|
||||
<>
|
||||
<TableRow class={getGroupedTableRowClass()}>
|
||||
<TableCell
|
||||
colspan={totalColumnCount()}
|
||||
class={getGroupedTableRowCellClass()}
|
||||
>
|
||||
<TableCell colspan={totalColumnCount()} class={getGroupedTableRowCellClass()}>
|
||||
<AlertResourceGroupHeader
|
||||
groupKey={nodeName}
|
||||
meta={props.table.groupHeaderMeta?.[nodeName]}
|
||||
|
|
@ -485,9 +519,7 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
setActiveMetricInput={props.setActiveMetricInput}
|
||||
showBulkSelection={Boolean(props.table.onBulkEdit)}
|
||||
selected={props.selectedIds().has(resource.id)}
|
||||
onToggleSelection={(checked) =>
|
||||
props.toggleSelection(resource.id, checked)
|
||||
}
|
||||
onToggleSelection={(checked) => props.toggleSelection(resource.id, checked)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
|
@ -528,9 +560,7 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
setActiveMetricInput={props.setActiveMetricInput}
|
||||
showBulkSelection={Boolean(props.table.onBulkEdit)}
|
||||
selected={props.selectedIds().has(resource.id)}
|
||||
onToggleSelection={(checked) =>
|
||||
props.toggleSelection(resource.id, checked)
|
||||
}
|
||||
onToggleSelection={(checked) => props.toggleSelection(resource.id, checked)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
|
@ -539,7 +569,7 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
<TableRow>
|
||||
<TableCell
|
||||
colspan={totalColumnCount()}
|
||||
class="px-4 py-8 text-center text-sm text-muted"
|
||||
class={`${getPlatformTableCellClassForKind('badge')} px-4 py-8 text-sm text-muted`}
|
||||
>
|
||||
{getAlertResourceTableNoResultsState(props.table.title)}
|
||||
</TableCell>
|
||||
|
|
@ -548,7 +578,10 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps)
|
|||
</Show>
|
||||
<Show when={props.hasRows() === false}>
|
||||
<TableRow>
|
||||
<TableCell colspan={totalColumnCount()} class="px-4 py-6 text-sm text-center text-muted">
|
||||
<TableCell
|
||||
colspan={totalColumnCount()}
|
||||
class={`${getPlatformTableCellClassForKind('badge')} px-4 py-6 text-sm text-muted`}
|
||||
>
|
||||
{getAlertResourceTableEmptyState(props.table.emptyMessage)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { TogglePrimitive } from '@/components/shared/Toggle';
|
|||
import { StatusBadge } from '@/components/shared/StatusBadge';
|
||||
import { ThresholdSlider } from '@/components/Workloads/ThresholdSlider';
|
||||
import { TableCell, TableRow } from '@/components/shared/Table';
|
||||
import { getPlatformTableCellClassForKind } from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
getAlertResourceTableCustomBadgeLabel,
|
||||
getAlertResourceTableEditMetricTitle,
|
||||
|
|
@ -18,6 +19,7 @@ import {
|
|||
import {
|
||||
ALERT_RESOURCE_TABLE_SLIDER_METRICS,
|
||||
alertResourceSupportsMetric,
|
||||
getAlertResourceColumnKind,
|
||||
getAlertResourceLabel,
|
||||
getAlertResourceMetricBounds,
|
||||
getAlertResourceMetricDisplayValue,
|
||||
|
|
@ -69,7 +71,8 @@ interface AlertResourceTableRowProps {
|
|||
|
||||
export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
||||
const isEditing = () => props.editingId() === props.resource.id;
|
||||
const thresholds = () => (isEditing() ? props.editingThresholds() : (props.resource.thresholds ?? {}));
|
||||
const thresholds = () =>
|
||||
isEditing() ? props.editingThresholds() : (props.resource.thresholds ?? {});
|
||||
const resourceLabel = () => getAlertResourceLabel(props.resource);
|
||||
const displayValue = (metric: string) =>
|
||||
getAlertResourceMetricDisplayValue(
|
||||
|
|
@ -78,8 +81,7 @@ export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
|||
props.editingThresholds(),
|
||||
isEditing(),
|
||||
);
|
||||
const isOverridden = (metric: string) =>
|
||||
isAlertResourceMetricOverridden(props.resource, metric);
|
||||
const isOverridden = (metric: string) => isAlertResourceMetricOverridden(props.resource, metric);
|
||||
|
||||
const getThresholds = (): AlertResourceThresholdMap => thresholds();
|
||||
|
||||
|
|
@ -196,7 +198,9 @@ export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
|||
class={`transition-colors ${props.resource.disabled || props.globalDisableFlag?.() ? 'opacity-40' : ''} ${props.resource.hasOverride ? 'bg-sky-50/50 hover:bg-sky-50/80 dark:bg-sky-900/20 dark:hover:bg-sky-900/30 border-l-[3px] border-l-sky-400 dark:border-l-sky-500' : 'hover:bg-surface-hover'}`}
|
||||
>
|
||||
<Show when={props.showBulkSelection}>
|
||||
<TableCell class="p-1 px-2 text-center align-middle border-r border-border">
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('badge')} align-middle border-r border-border`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.selected}
|
||||
|
|
@ -207,7 +211,7 @@ export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
|||
</TableCell>
|
||||
</Show>
|
||||
|
||||
<TableCell class="p-1 px-2 text-center align-middle">
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('badge')} align-middle`}>
|
||||
<Show when={props.onToggleDisabled}>
|
||||
{(() => {
|
||||
const globallyDisabled = props.globalDisableFlag?.() ?? false;
|
||||
|
|
@ -239,7 +243,7 @@ export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
|||
</Show>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="p-1 px-2">
|
||||
<TableCell class={getPlatformTableCellClassForKind('name')}>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<Show
|
||||
when={props.resource.type === 'agent'}
|
||||
|
|
@ -334,10 +338,14 @@ export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
|||
const titlePrefix = metric === 'backup' ? 'Backup' : 'Snapshot';
|
||||
|
||||
return (
|
||||
<TableCell class="p-1 px-2 text-center align-middle">
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('badge')} align-middle`}>
|
||||
<Show
|
||||
when={onToggle}
|
||||
fallback={<span class="text-sm text-slate-400" aria-hidden="true">-</span>}
|
||||
fallback={
|
||||
<span class="text-sm text-slate-400" aria-hidden="true">
|
||||
-
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<StatusBadge
|
||||
|
|
@ -357,10 +365,16 @@ export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<TableCell class="p-1 px-2 text-center align-middle">
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind(getAlertResourceColumnKind(column))} align-middle`}
|
||||
>
|
||||
<Show
|
||||
when={alertResourceSupportsMetric(props.resource.type, metric)}
|
||||
fallback={<span class="text-sm text-muted" aria-hidden="true">-</span>}
|
||||
fallback={
|
||||
<span class="text-sm text-muted" aria-hidden="true">
|
||||
-
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={isEditing()}
|
||||
|
|
@ -410,10 +424,7 @@ export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
|||
return Math.round(editingVal);
|
||||
}
|
||||
const currentDisplayValue = displayValue(metric);
|
||||
if (
|
||||
typeof currentDisplayValue === 'number' &&
|
||||
currentDisplayValue >= 0
|
||||
) {
|
||||
if (typeof currentDisplayValue === 'number' && currentDisplayValue >= 0) {
|
||||
return Math.round(currentDisplayValue);
|
||||
}
|
||||
return defaultSliderValue();
|
||||
|
|
@ -422,10 +433,7 @@ export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
|||
return (
|
||||
<div class="w-36">
|
||||
<ThresholdSlider
|
||||
value={Math.max(
|
||||
sliderMin,
|
||||
Math.min(sliderMax, currentSliderValue()),
|
||||
)}
|
||||
value={Math.max(sliderMin, Math.min(sliderMax, currentSliderValue()))}
|
||||
onChange={(val) => {
|
||||
props.setEditingThresholds({
|
||||
...props.editingThresholds(),
|
||||
|
|
@ -498,7 +506,7 @@ export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
|||
</For>
|
||||
|
||||
<Show when={props.showOfflineAlertsColumn}>
|
||||
<TableCell class="p-1 px-2 text-center align-middle">
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('badge')} align-middle`}>
|
||||
{(() => {
|
||||
const disabledGlobally = props.globalDisableFlag?.() ?? false;
|
||||
const supportsTriState =
|
||||
|
|
@ -528,7 +536,11 @@ export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
|||
}
|
||||
|
||||
if (!props.onToggleNodeConnectivity) {
|
||||
return <span class="text-sm text-slate-400" aria-hidden="true">-</span>;
|
||||
return (
|
||||
<span class="text-sm text-slate-400" aria-hidden="true">
|
||||
-
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const globalOfflineDisabled = props.globalDisableOfflineFlag?.() ?? false;
|
||||
|
|
@ -539,17 +551,15 @@ export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
|||
if (disabledGlobally) return;
|
||||
props.onToggleNodeConnectivity?.(props.resource.id);
|
||||
},
|
||||
titleEnabled:
|
||||
'Offline alerts enabled. Click to disable for this resource.',
|
||||
titleDisabled:
|
||||
'Offline alerts disabled. Click to enable for this resource.',
|
||||
titleEnabled: 'Offline alerts enabled. Click to disable for this resource.',
|
||||
titleDisabled: 'Offline alerts disabled. Click to enable for this resource.',
|
||||
titleWhenDisabled: 'Offline alerts controlled globally',
|
||||
});
|
||||
})()}
|
||||
</TableCell>
|
||||
</Show>
|
||||
|
||||
<TableCell class="p-1 px-2">
|
||||
<TableCell class={getPlatformTableCellClassForKind('badge')}>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Show
|
||||
when={!isEditing()}
|
||||
|
|
@ -581,7 +591,14 @@ export function AlertResourceTableRow(props: AlertResourceTableRowProps) {
|
|||
props.resource.disableConnectivity);
|
||||
|
||||
return (
|
||||
<Show when={showEdit || showRevert} fallback={<span class="text-xs text-muted" aria-hidden="true">—</span>}>
|
||||
<Show
|
||||
when={showEdit || showRevert}
|
||||
fallback={
|
||||
<span class="text-xs text-muted" aria-hidden="true">
|
||||
—
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Show when={showEdit}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -291,13 +291,9 @@ describe('ResourceTable', () => {
|
|||
);
|
||||
expect(alertResourceTableDesktopSource).toContain('AlertResourceTableRow');
|
||||
expect(alertResourceTableDesktopSource).toContain('AlertResourceGroupHeader');
|
||||
expect(alertResourceTableMobileSource).toContain(
|
||||
'export function AlertResourceTableMobile',
|
||||
);
|
||||
expect(alertResourceTableMobileSource).toContain('export function AlertResourceTableMobile');
|
||||
expect(alertResourceTableMobileSource).toContain('AlertResourceGroupHeader');
|
||||
expect(alertResourceGroupHeaderSource).toContain(
|
||||
'export function AlertResourceGroupHeader',
|
||||
);
|
||||
expect(alertResourceGroupHeaderSource).toContain('export function AlertResourceGroupHeader');
|
||||
expect(alertResourceTableRowSource).toContain('export function AlertResourceTableRow');
|
||||
expect(alertResourceTableRowSource).toContain('alertResourceSupportsMetric');
|
||||
expect(alertResourceTableStateSource).toContain('export function useAlertResourceTableState');
|
||||
|
|
@ -316,6 +312,10 @@ describe('ResourceTable', () => {
|
|||
expect(normalizeAlertResourceMetricKey('CPU %')).toBe('cpu');
|
||||
expect(alertResourceSupportsMetric('agent', 'networkIn')).toBe(false);
|
||||
expect(alertResourceSupportsMetric('pbs', 'disk')).toBe(false);
|
||||
expect(alertResourceSupportsMetric('vmwareHost', 'networkIn')).toBe(true);
|
||||
expect(alertResourceSupportsMetric('vmwareHost', 'disk')).toBe(false);
|
||||
expect(alertResourceSupportsMetric('vmwareDatastore', 'usage')).toBe(true);
|
||||
expect(alertResourceSupportsMetric('vmwareNetwork', 'cpu')).toBe(false);
|
||||
expect(
|
||||
getAlertResourceMetricDisplayValue(
|
||||
makeResource({
|
||||
|
|
@ -531,15 +531,11 @@ describe('ResourceTable', () => {
|
|||
expect(disabledOff).toBeTruthy();
|
||||
|
||||
// formatMetricValue should NOT be called for disabled metrics (they show "Off" directly)
|
||||
const cpuCalls = formatMetricValue.mock.calls.filter(
|
||||
([metric]) => metric === 'cpu',
|
||||
);
|
||||
const cpuCalls = formatMetricValue.mock.calls.filter(([metric]) => metric === 'cpu');
|
||||
expect(cpuCalls.length).toBe(0);
|
||||
|
||||
// Memory (85) should still be formatted normally
|
||||
const memoryCalls = formatMetricValue.mock.calls.filter(
|
||||
([metric]) => metric === 'memory',
|
||||
);
|
||||
const memoryCalls = formatMetricValue.mock.calls.filter(([metric]) => metric === 'memory');
|
||||
expect(memoryCalls.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
|
|
@ -954,9 +950,7 @@ describe('ResourceTable', () => {
|
|||
render(() => <ResourceTable {...props} />);
|
||||
|
||||
// Storage only supports 'usage' — cpu and memory should NOT be formatted
|
||||
const usageCalls = formatMetricValue.mock.calls.filter(
|
||||
(args) => args[0] === 'usage',
|
||||
);
|
||||
const usageCalls = formatMetricValue.mock.calls.filter((args) => args[0] === 'usage');
|
||||
expect(usageCalls.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const cpuCalls = formatMetricValue.mock.calls.filter((args) => args[0] === 'cpu');
|
||||
|
|
|
|||
|
|
@ -1,34 +1,91 @@
|
|||
import { Show } from 'solid-js';
|
||||
import { SearchInput } from '@/components/shared/SearchInput';
|
||||
import Server from 'lucide-solid/icons/server';
|
||||
import Mail from 'lucide-solid/icons/mail';
|
||||
import Users from 'lucide-solid/icons/users';
|
||||
import Boxes from 'lucide-solid/icons/boxes';
|
||||
import Cpu from 'lucide-solid/icons/cpu';
|
||||
import Database from 'lucide-solid/icons/database';
|
||||
import HardDrive from 'lucide-solid/icons/hard-drive';
|
||||
import Network from 'lucide-solid/icons/network';
|
||||
import { FilterBar, type FilterDef } from '@/components/shared/FilterBar';
|
||||
import { useBreakpoint } from '@/hooks/useBreakpoint';
|
||||
import { BulkEditDialog } from './BulkEditDialog';
|
||||
import { ThresholdsTableAgentsTab } from './ThresholdsTableAgentsTab';
|
||||
import { ThresholdsTableDockerTab } from './ThresholdsTableDockerTab';
|
||||
import { ThresholdsTableKubernetesTab } from './ThresholdsTableKubernetesTab';
|
||||
import { ThresholdsTablePBSTab } from './ThresholdsTablePBSTab';
|
||||
import { ThresholdsTablePMGTab } from './ThresholdsTablePMGTab';
|
||||
import { ThresholdsTableProxmoxTab } from './ThresholdsTableProxmoxTab';
|
||||
import { ThresholdsTableTrueNASTab } from './ThresholdsTableTrueNASTab';
|
||||
import { ThresholdsTableVMwareTab } from './ThresholdsTableVMwareTab';
|
||||
import type { ThresholdsTableProps } from '@/features/alerts/thresholds/types';
|
||||
import type { ThresholdsActiveTab } from '@/features/alerts/thresholds/tableTypes';
|
||||
import { useThresholdsTableState } from '@/features/alerts/thresholds/hooks/useThresholdsTableState';
|
||||
|
||||
export function ThresholdsTable(props: ThresholdsTableProps) {
|
||||
const state = useThresholdsTableState(props);
|
||||
const { isMobile } = useBreakpoint();
|
||||
|
||||
const filters = (): FilterDef[] => [
|
||||
{
|
||||
id: 'alerts-platform',
|
||||
label: 'Platform',
|
||||
group: 'scope',
|
||||
inline: true,
|
||||
value: state.activeTab,
|
||||
setValue: (value) => state.handleTabClick(value as ThresholdsActiveTab),
|
||||
defaultValue: 'proxmox',
|
||||
options: () => [
|
||||
{ value: 'proxmox', label: 'Proxmox', icon: Server },
|
||||
{ value: 'docker', label: 'Docker', icon: Boxes },
|
||||
{ value: 'kubernetes', label: 'Kubernetes', icon: Network },
|
||||
{ value: 'truenas', label: 'TrueNAS', icon: HardDrive },
|
||||
{ value: 'vmware', label: 'vSphere', icon: Cpu },
|
||||
{ value: 'pbs', label: 'PBS', icon: Database },
|
||||
{ value: 'pmg', label: 'PMG', icon: Mail },
|
||||
{ value: 'systems', label: 'Systems', icon: Users },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'alerts-overrides',
|
||||
label: 'Overrides',
|
||||
group: 'properties',
|
||||
value: state.overrideFilter,
|
||||
setValue: state.setOverrideFilter,
|
||||
defaultValue: 'all',
|
||||
options: () => [
|
||||
{ value: 'all', label: 'All resources' },
|
||||
{ value: 'custom', label: 'Custom only' },
|
||||
{ value: 'disabled', label: 'Disabled only' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
<div class="relative">
|
||||
<SearchInput
|
||||
value={state.searchTerm}
|
||||
onChange={state.setSearchTerm}
|
||||
placeholder={state.getAlertThresholdsSearchPlaceholder()}
|
||||
class="w-full"
|
||||
onBeforeAutoFocus={() => Boolean(state.editingId())}
|
||||
focusOnShortcut
|
||||
clearOnEscape
|
||||
shortcutHint="Ctrl+F"
|
||||
/>
|
||||
</div>
|
||||
<FilterBar
|
||||
role="group"
|
||||
ariaLabel="Alert threshold filters"
|
||||
isMobile={isMobile}
|
||||
search={{
|
||||
value: state.searchTerm,
|
||||
setValue: state.setSearchTerm,
|
||||
placeholder: state.getAlertThresholdsSearchPlaceholder(),
|
||||
clearOnEscape: true,
|
||||
onBeforeAutoFocus: () => Boolean(state.editingId()),
|
||||
}}
|
||||
filters={filters()}
|
||||
showClearAll={() =>
|
||||
state.searchTerm().trim().length > 0 ||
|
||||
state.overrideFilter() !== 'all' ||
|
||||
state.activeTab() !== 'proxmox'
|
||||
}
|
||||
onClearAll={() => {
|
||||
state.setSearchTerm('');
|
||||
state.setOverrideFilter('all');
|
||||
state.handleTabClick('proxmox');
|
||||
}}
|
||||
/>
|
||||
|
||||
<Show when={!state.helpBannerDismissed()}>
|
||||
<div class="rounded-md border border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900 p-3 relative group">
|
||||
|
|
@ -83,45 +140,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="border-b border-border">
|
||||
<nav class="-mb-px flex gap-4 sm:gap-6" aria-label="Tabs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => state.handleTabClick('infrastructure')}
|
||||
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5 ${state.activeTab() === 'infrastructure' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-muted hover:text-base-content hover:border-slate-300'}`}
|
||||
>
|
||||
<Server class="w-4 h-4" />
|
||||
<span>Infrastructure</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => state.handleTabClick('pmg')}
|
||||
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5 ${state.activeTab() === 'pmg' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-muted hover:text-base-content hover:border-slate-300'}`}
|
||||
>
|
||||
<Mail class="w-4 h-4" />
|
||||
<span class="hidden sm:inline">Mail Gateway</span>
|
||||
<span class="sm:hidden">Mail</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => state.handleTabClick('systems')}
|
||||
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5 ${state.activeTab() === 'systems' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-muted hover:text-base-content hover:border-slate-300'}`}
|
||||
>
|
||||
<Users class="w-4 h-4" />
|
||||
<span>Systems</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => state.handleTabClick('docker')}
|
||||
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5 ${state.activeTab() === 'docker' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-muted hover:text-base-content hover:border-slate-300'}`}
|
||||
>
|
||||
<Boxes class="w-4 h-4" />
|
||||
<span>Containers</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<Show when={state.activeTab() === 'infrastructure'}>
|
||||
<Show when={state.activeTab() === 'proxmox'}>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -142,10 +161,30 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
|
|||
</Show>
|
||||
|
||||
<div class="space-y-6">
|
||||
<Show when={state.activeTab() === 'infrastructure'}>
|
||||
<Show when={state.activeTab() === 'proxmox'}>
|
||||
<ThresholdsTableProxmoxTab state={state} tableProps={props} />
|
||||
</Show>
|
||||
|
||||
<Show when={state.activeTab() === 'docker'}>
|
||||
<ThresholdsTableDockerTab state={state} tableProps={props} />
|
||||
</Show>
|
||||
|
||||
<Show when={state.activeTab() === 'kubernetes'}>
|
||||
<ThresholdsTableKubernetesTab state={state} tableProps={props} />
|
||||
</Show>
|
||||
|
||||
<Show when={state.activeTab() === 'truenas'}>
|
||||
<ThresholdsTableTrueNASTab state={state} tableProps={props} />
|
||||
</Show>
|
||||
|
||||
<Show when={state.activeTab() === 'vmware'}>
|
||||
<ThresholdsTableVMwareTab state={state} tableProps={props} />
|
||||
</Show>
|
||||
|
||||
<Show when={state.activeTab() === 'pbs'}>
|
||||
<ThresholdsTablePBSTab state={state} tableProps={props} />
|
||||
</Show>
|
||||
|
||||
<Show when={state.activeTab() === 'pmg'}>
|
||||
<ThresholdsTablePMGTab state={state} tableProps={props} />
|
||||
</Show>
|
||||
|
|
@ -153,10 +192,6 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
|
|||
<Show when={state.activeTab() === 'systems'}>
|
||||
<ThresholdsTableAgentsTab state={state} tableProps={props} />
|
||||
</Show>
|
||||
|
||||
<Show when={state.activeTab() === 'docker'}>
|
||||
<ThresholdsTableDockerTab state={state} tableProps={props} />
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<BulkEditDialog
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
import { Show, type JSX } from 'solid-js';
|
||||
import Boxes from 'lucide-solid/icons/boxes';
|
||||
import Network from 'lucide-solid/icons/network';
|
||||
import Server from 'lucide-solid/icons/server';
|
||||
import SquareStack from 'lucide-solid/icons/square-stack';
|
||||
import Tags from 'lucide-solid/icons/tags';
|
||||
|
||||
import { ResourceTable } from './ResourceTable';
|
||||
import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection';
|
||||
import { formatMetricValue } from '@/features/alerts/thresholds/helpers';
|
||||
import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps';
|
||||
import type { Resource } from '@/features/alerts/thresholds/tableTypes';
|
||||
|
||||
const KUBERNETES_WORKLOAD_COLUMNS = [
|
||||
'CPU %',
|
||||
'Memory %',
|
||||
'Disk %',
|
||||
'Disk R MB/s',
|
||||
'Disk W MB/s',
|
||||
'Net In MB/s',
|
||||
'Net Out MB/s',
|
||||
];
|
||||
const KUBERNETES_NODE_COLUMNS = ['CPU %', 'Memory %', 'Disk %'];
|
||||
|
||||
function KubernetesResourceSection(
|
||||
props: ThresholdsTableSectionProps & {
|
||||
id: string;
|
||||
title: string;
|
||||
resources: () => Resource[];
|
||||
columns: string[];
|
||||
icon: JSX.Element;
|
||||
typeKey: 'k8s-cluster' | 'k8s-node' | 'k8s-namespace' | 'k8s-deployment' | 'pod';
|
||||
defaults?: Record<string, number | undefined>;
|
||||
},
|
||||
) {
|
||||
const { state, tableProps } = props;
|
||||
|
||||
return (
|
||||
<Show when={state.hasSection(props.id)}>
|
||||
<CollapsibleSection
|
||||
id={props.id}
|
||||
title={props.title}
|
||||
resourceCount={props.resources().length}
|
||||
collapsed={state.isCollapsed(props.id)}
|
||||
onToggle={() => state.toggleSection(props.id)}
|
||||
icon={props.icon}
|
||||
isGloballyDisabled={tableProps.disableAllKubernetes()}
|
||||
emptyMessage="No Kubernetes alert targets match the current filters."
|
||||
>
|
||||
<div ref={state.registerSection(props.id)} class="scroll-mt-24">
|
||||
<ResourceTable
|
||||
title=""
|
||||
resources={props.resources()}
|
||||
columns={props.columns}
|
||||
activeAlerts={tableProps.activeAlerts}
|
||||
emptyMessage="No Kubernetes alert targets match the current filters."
|
||||
onEdit={state.startEditing}
|
||||
onSaveEdit={state.saveEdit}
|
||||
onCancelEdit={state.cancelEdit}
|
||||
onRemoveOverride={state.removeOverride}
|
||||
onToggleDisabled={state.toggleDisabled}
|
||||
showOfflineAlertsColumn={false}
|
||||
editingId={state.editingId}
|
||||
editingThresholds={state.editingThresholds}
|
||||
setEditingThresholds={state.setEditingThresholds}
|
||||
editingNote={state.editingNote}
|
||||
setEditingNote={state.setEditingNote}
|
||||
onBulkEdit={(ids) => state.handleBulkEdit(ids, props.columns)}
|
||||
formatMetricValue={formatMetricValue}
|
||||
hasActiveAlert={state.hasActiveAlert}
|
||||
globalDefaults={props.defaults}
|
||||
setGlobalDefaults={props.defaults ? tableProps.setKubernetesDefaults : undefined}
|
||||
setHasUnsavedChanges={tableProps.setHasUnsavedChanges}
|
||||
globalDisableFlag={tableProps.disableAllKubernetes}
|
||||
onToggleGlobalDisable={() =>
|
||||
tableProps.setDisableAllKubernetes(!tableProps.disableAllKubernetes())
|
||||
}
|
||||
showDelayColumn={props.columns.length > 0}
|
||||
globalDelaySeconds={tableProps.timeThresholds()[props.typeKey]}
|
||||
metricDelaySeconds={tableProps.metricTimeThresholds()[props.typeKey] ?? {}}
|
||||
onMetricDelayChange={(metric, value) =>
|
||||
state.updateMetricDelay(props.typeKey, metric, value)
|
||||
}
|
||||
factoryDefaults={props.defaults ? tableProps.factoryKubernetesDefaults : undefined}
|
||||
onResetDefaults={props.defaults ? tableProps.resetKubernetesDefaults : undefined}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThresholdsTableKubernetesTab(props: ThresholdsTableSectionProps) {
|
||||
const defaults = () => props.tableProps.kubernetesDefaults ?? {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<KubernetesResourceSection
|
||||
{...props}
|
||||
id="kubernetesClusters"
|
||||
title="Clusters"
|
||||
resources={props.state.kubernetesClustersWithOverrides}
|
||||
columns={KUBERNETES_WORKLOAD_COLUMNS}
|
||||
icon={<Network class="w-5 h-5" />}
|
||||
typeKey="k8s-cluster"
|
||||
defaults={defaults()}
|
||||
/>
|
||||
<KubernetesResourceSection
|
||||
{...props}
|
||||
id="kubernetesNodes"
|
||||
title="Nodes"
|
||||
resources={props.state.kubernetesNodesWithOverrides}
|
||||
columns={KUBERNETES_NODE_COLUMNS}
|
||||
icon={<Server class="w-5 h-5" />}
|
||||
typeKey="k8s-node"
|
||||
defaults={defaults()}
|
||||
/>
|
||||
<KubernetesResourceSection
|
||||
{...props}
|
||||
id="kubernetesNamespaces"
|
||||
title="Namespaces"
|
||||
resources={props.state.kubernetesNamespacesWithOverrides}
|
||||
columns={[]}
|
||||
icon={<Tags class="w-5 h-5" />}
|
||||
typeKey="k8s-namespace"
|
||||
/>
|
||||
<KubernetesResourceSection
|
||||
{...props}
|
||||
id="kubernetesDeployments"
|
||||
title="Deployments"
|
||||
resources={props.state.kubernetesDeploymentsWithOverrides}
|
||||
columns={KUBERNETES_WORKLOAD_COLUMNS}
|
||||
icon={<SquareStack class="w-5 h-5" />}
|
||||
typeKey="k8s-deployment"
|
||||
defaults={defaults()}
|
||||
/>
|
||||
<KubernetesResourceSection
|
||||
{...props}
|
||||
id="kubernetesPods"
|
||||
title="Pods"
|
||||
resources={props.state.kubernetesPodsWithOverrides}
|
||||
columns={KUBERNETES_WORKLOAD_COLUMNS}
|
||||
icon={<Boxes class="w-5 h-5" />}
|
||||
typeKey="pod"
|
||||
defaults={defaults()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { ThresholdsTableProxmoxPBSSection } from './ThresholdsTableProxmoxPBSSection';
|
||||
import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps';
|
||||
|
||||
export function ThresholdsTablePBSTab(props: ThresholdsTableSectionProps) {
|
||||
return <ThresholdsTableProxmoxPBSSection {...props} />;
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { ThresholdsTableProxmoxBackupsSection } from './ThresholdsTableProxmoxBa
|
|||
import { ThresholdsTableProxmoxGuestFilteringSection } from './ThresholdsTableProxmoxGuestFilteringSection';
|
||||
import { ThresholdsTableProxmoxGuestsSection } from './ThresholdsTableProxmoxGuestsSection';
|
||||
import { ThresholdsTableProxmoxNodesSection } from './ThresholdsTableProxmoxNodesSection';
|
||||
import { ThresholdsTableProxmoxPBSSection } from './ThresholdsTableProxmoxPBSSection';
|
||||
import { ThresholdsTableProxmoxSnapshotsSection } from './ThresholdsTableProxmoxSnapshotsSection';
|
||||
import { ThresholdsTableProxmoxStorageSection } from './ThresholdsTableProxmoxStorageSection';
|
||||
import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps';
|
||||
|
|
@ -11,7 +10,6 @@ export function ThresholdsTableProxmoxTab(props: ThresholdsTableSectionProps) {
|
|||
return (
|
||||
<>
|
||||
<ThresholdsTableProxmoxNodesSection {...props} />
|
||||
<ThresholdsTableProxmoxPBSSection {...props} />
|
||||
<ThresholdsTableProxmoxGuestsSection {...props} />
|
||||
<ThresholdsTableProxmoxGuestFilteringSection {...props} />
|
||||
<ThresholdsTableProxmoxBackupsSection {...props} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
import { Show, type JSX } from 'solid-js';
|
||||
import Database from 'lucide-solid/icons/database';
|
||||
import HardDrive from 'lucide-solid/icons/hard-drive';
|
||||
import Server from 'lucide-solid/icons/server';
|
||||
|
||||
import { ResourceTable } from './ResourceTable';
|
||||
import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection';
|
||||
import { formatMetricValue } from '@/features/alerts/thresholds/helpers';
|
||||
import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps';
|
||||
import type { Resource } from '@/features/alerts/thresholds/tableTypes';
|
||||
|
||||
const TRUENAS_SYSTEM_COLUMNS = [
|
||||
'CPU %',
|
||||
'Memory %',
|
||||
'Disk %',
|
||||
'Temp °C',
|
||||
'Disk R MB/s',
|
||||
'Disk W MB/s',
|
||||
'Net In MB/s',
|
||||
'Net Out MB/s',
|
||||
];
|
||||
const TRUENAS_STORAGE_COLUMNS = ['Usage %'];
|
||||
const TRUENAS_DISK_COLUMNS = ['Temp °C'];
|
||||
|
||||
function TrueNASResourceSection(
|
||||
props: ThresholdsTableSectionProps & {
|
||||
id: string;
|
||||
title: string;
|
||||
resources: () => Resource[];
|
||||
columns: string[];
|
||||
icon: JSX.Element;
|
||||
typeKey: 'truenas-system' | 'truenas-pool' | 'truenas-dataset' | 'truenas-disk';
|
||||
defaults: Record<string, number | undefined>;
|
||||
factoryDefaults?: Record<string, number | undefined>;
|
||||
setGlobalDefaults?: (
|
||||
value:
|
||||
| Record<string, number | undefined>
|
||||
| ((prev: Record<string, number | undefined>) => Record<string, number | undefined>),
|
||||
) => void;
|
||||
onResetDefaults?: () => void;
|
||||
},
|
||||
) {
|
||||
const { state, tableProps } = props;
|
||||
|
||||
return (
|
||||
<Show when={state.hasSection(props.id)}>
|
||||
<CollapsibleSection
|
||||
id={props.id}
|
||||
title={props.title}
|
||||
resourceCount={props.resources().length}
|
||||
collapsed={state.isCollapsed(props.id)}
|
||||
onToggle={() => state.toggleSection(props.id)}
|
||||
icon={props.icon}
|
||||
isGloballyDisabled={tableProps.disableAllTrueNAS()}
|
||||
emptyMessage="No TrueNAS alert targets match the current filters."
|
||||
>
|
||||
<div ref={state.registerSection(props.id)} class="scroll-mt-24">
|
||||
<ResourceTable
|
||||
title=""
|
||||
resources={props.resources()}
|
||||
columns={props.columns}
|
||||
activeAlerts={tableProps.activeAlerts}
|
||||
emptyMessage="No TrueNAS alert targets match the current filters."
|
||||
onEdit={state.startEditing}
|
||||
onSaveEdit={state.saveEdit}
|
||||
onCancelEdit={state.cancelEdit}
|
||||
onRemoveOverride={state.removeOverride}
|
||||
onToggleDisabled={state.toggleDisabled}
|
||||
showOfflineAlertsColumn={false}
|
||||
editingId={state.editingId}
|
||||
editingThresholds={state.editingThresholds}
|
||||
setEditingThresholds={state.setEditingThresholds}
|
||||
editingNote={state.editingNote}
|
||||
setEditingNote={state.setEditingNote}
|
||||
onBulkEdit={(ids) => state.handleBulkEdit(ids, props.columns)}
|
||||
formatMetricValue={formatMetricValue}
|
||||
hasActiveAlert={state.hasActiveAlert}
|
||||
globalDefaults={props.defaults}
|
||||
setGlobalDefaults={props.setGlobalDefaults}
|
||||
setHasUnsavedChanges={tableProps.setHasUnsavedChanges}
|
||||
globalDisableFlag={tableProps.disableAllTrueNAS}
|
||||
onToggleGlobalDisable={() =>
|
||||
tableProps.setDisableAllTrueNAS(!tableProps.disableAllTrueNAS())
|
||||
}
|
||||
showDelayColumn={true}
|
||||
globalDelaySeconds={tableProps.timeThresholds()[props.typeKey]}
|
||||
metricDelaySeconds={tableProps.metricTimeThresholds()[props.typeKey] ?? {}}
|
||||
onMetricDelayChange={(metric, value) =>
|
||||
state.updateMetricDelay(props.typeKey, metric, value)
|
||||
}
|
||||
factoryDefaults={props.factoryDefaults}
|
||||
onResetDefaults={props.onResetDefaults}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThresholdsTableTrueNASTab(props: ThresholdsTableSectionProps) {
|
||||
const trueNASDefaults = () => props.tableProps.trueNASDefaults ?? {};
|
||||
const trueNASStorageDefaults = () => ({ usage: trueNASDefaults().usage ?? 85 });
|
||||
const trueNASDiskDefaults = () => props.tableProps.trueNASDiskDefaults ?? {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TrueNASResourceSection
|
||||
{...props}
|
||||
id="trueNASSystems"
|
||||
title="Systems"
|
||||
resources={props.state.trueNASSystemsWithOverrides}
|
||||
columns={TRUENAS_SYSTEM_COLUMNS}
|
||||
icon={<Server class="w-5 h-5" />}
|
||||
typeKey="truenas-system"
|
||||
defaults={trueNASDefaults()}
|
||||
factoryDefaults={props.tableProps.factoryTrueNASDefaults}
|
||||
setGlobalDefaults={props.tableProps.setTrueNASDefaults}
|
||||
onResetDefaults={props.tableProps.resetTrueNASDefaults}
|
||||
/>
|
||||
<TrueNASResourceSection
|
||||
{...props}
|
||||
id="trueNASPools"
|
||||
title="Pools"
|
||||
resources={props.state.trueNASPoolsWithOverrides}
|
||||
columns={TRUENAS_STORAGE_COLUMNS}
|
||||
icon={<Database class="w-5 h-5" />}
|
||||
typeKey="truenas-pool"
|
||||
defaults={trueNASStorageDefaults()}
|
||||
factoryDefaults={
|
||||
props.tableProps.factoryTrueNASDefaults?.usage !== undefined
|
||||
? { usage: props.tableProps.factoryTrueNASDefaults.usage }
|
||||
: undefined
|
||||
}
|
||||
setGlobalDefaults={(value) => {
|
||||
if (!props.tableProps.setTrueNASDefaults) return;
|
||||
props.tableProps.setTrueNASDefaults((prev) => {
|
||||
const next = typeof value === 'function' ? value({ usage: prev.usage }) : value;
|
||||
return { ...prev, usage: next.usage ?? 85 };
|
||||
});
|
||||
}}
|
||||
onResetDefaults={props.tableProps.resetTrueNASDefaults}
|
||||
/>
|
||||
<TrueNASResourceSection
|
||||
{...props}
|
||||
id="trueNASDatasets"
|
||||
title="Datasets"
|
||||
resources={props.state.trueNASDatasetsWithOverrides}
|
||||
columns={TRUENAS_STORAGE_COLUMNS}
|
||||
icon={<Database class="w-5 h-5" />}
|
||||
typeKey="truenas-dataset"
|
||||
defaults={trueNASStorageDefaults()}
|
||||
factoryDefaults={
|
||||
props.tableProps.factoryTrueNASDefaults?.usage !== undefined
|
||||
? { usage: props.tableProps.factoryTrueNASDefaults.usage }
|
||||
: undefined
|
||||
}
|
||||
setGlobalDefaults={(value) => {
|
||||
if (!props.tableProps.setTrueNASDefaults) return;
|
||||
props.tableProps.setTrueNASDefaults((prev) => {
|
||||
const next = typeof value === 'function' ? value({ usage: prev.usage }) : value;
|
||||
return { ...prev, usage: next.usage ?? 85 };
|
||||
});
|
||||
}}
|
||||
onResetDefaults={props.tableProps.resetTrueNASDefaults}
|
||||
/>
|
||||
<TrueNASResourceSection
|
||||
{...props}
|
||||
id="trueNASDisks"
|
||||
title="Disks"
|
||||
resources={props.state.trueNASDisksWithOverrides}
|
||||
columns={TRUENAS_DISK_COLUMNS}
|
||||
icon={<HardDrive class="w-5 h-5" />}
|
||||
typeKey="truenas-disk"
|
||||
defaults={trueNASDiskDefaults()}
|
||||
factoryDefaults={props.tableProps.factoryTrueNASDiskDefaults}
|
||||
setGlobalDefaults={props.tableProps.setTrueNASDiskDefaults}
|
||||
onResetDefaults={props.tableProps.resetTrueNASDiskDefaults}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
import { Show, type JSX } from 'solid-js';
|
||||
import Cpu from 'lucide-solid/icons/cpu';
|
||||
import Database from 'lucide-solid/icons/database';
|
||||
import Monitor from 'lucide-solid/icons/monitor';
|
||||
import Network from 'lucide-solid/icons/network';
|
||||
|
||||
import { ResourceTable } from './ResourceTable';
|
||||
import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection';
|
||||
import { formatMetricValue } from '@/features/alerts/thresholds/helpers';
|
||||
import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps';
|
||||
import type { Resource } from '@/features/alerts/thresholds/tableTypes';
|
||||
|
||||
const VMWARE_HOST_COLUMNS = [
|
||||
'CPU %',
|
||||
'Memory %',
|
||||
'Disk R MB/s',
|
||||
'Disk W MB/s',
|
||||
'Net In MB/s',
|
||||
'Net Out MB/s',
|
||||
];
|
||||
const VMWARE_VM_COLUMNS = [
|
||||
'CPU %',
|
||||
'Memory %',
|
||||
'Disk %',
|
||||
'Disk R MB/s',
|
||||
'Disk W MB/s',
|
||||
'Net In MB/s',
|
||||
'Net Out MB/s',
|
||||
];
|
||||
const VMWARE_DATASTORE_COLUMNS = ['Usage %'];
|
||||
|
||||
function VMwareResourceSection(
|
||||
props: ThresholdsTableSectionProps & {
|
||||
id: string;
|
||||
title: string;
|
||||
resources: () => Resource[];
|
||||
columns: string[];
|
||||
icon: JSX.Element;
|
||||
typeKey: 'vmware-host' | 'vmware-vm' | 'vmware-datastore' | 'vmware-network';
|
||||
defaults?: Record<string, number | undefined>;
|
||||
factoryDefaults?: Record<string, number | undefined>;
|
||||
setGlobalDefaults?: (
|
||||
value:
|
||||
| Record<string, number | undefined>
|
||||
| ((prev: Record<string, number | undefined>) => Record<string, number | undefined>),
|
||||
) => void;
|
||||
onResetDefaults?: () => void;
|
||||
},
|
||||
) {
|
||||
const { state, tableProps } = props;
|
||||
|
||||
return (
|
||||
<Show when={state.hasSection(props.id)}>
|
||||
<CollapsibleSection
|
||||
id={props.id}
|
||||
title={props.title}
|
||||
resourceCount={props.resources().length}
|
||||
collapsed={state.isCollapsed(props.id)}
|
||||
onToggle={() => state.toggleSection(props.id)}
|
||||
icon={props.icon}
|
||||
isGloballyDisabled={tableProps.disableAllVMware()}
|
||||
emptyMessage="No vSphere alert targets match the current filters."
|
||||
>
|
||||
<div ref={state.registerSection(props.id)} class="scroll-mt-24">
|
||||
<ResourceTable
|
||||
title=""
|
||||
resources={props.resources()}
|
||||
columns={props.columns}
|
||||
activeAlerts={tableProps.activeAlerts}
|
||||
emptyMessage="No vSphere alert targets match the current filters."
|
||||
onEdit={state.startEditing}
|
||||
onSaveEdit={state.saveEdit}
|
||||
onCancelEdit={state.cancelEdit}
|
||||
onRemoveOverride={state.removeOverride}
|
||||
onToggleDisabled={state.toggleDisabled}
|
||||
showOfflineAlertsColumn={false}
|
||||
editingId={state.editingId}
|
||||
editingThresholds={state.editingThresholds}
|
||||
setEditingThresholds={state.setEditingThresholds}
|
||||
editingNote={state.editingNote}
|
||||
setEditingNote={state.setEditingNote}
|
||||
onBulkEdit={(ids) => state.handleBulkEdit(ids, props.columns)}
|
||||
formatMetricValue={formatMetricValue}
|
||||
hasActiveAlert={state.hasActiveAlert}
|
||||
globalDefaults={props.defaults}
|
||||
setGlobalDefaults={props.setGlobalDefaults}
|
||||
setHasUnsavedChanges={tableProps.setHasUnsavedChanges}
|
||||
globalDisableFlag={tableProps.disableAllVMware}
|
||||
onToggleGlobalDisable={() =>
|
||||
tableProps.setDisableAllVMware(!tableProps.disableAllVMware())
|
||||
}
|
||||
showDelayColumn={props.columns.length > 0}
|
||||
globalDelaySeconds={tableProps.timeThresholds()[props.typeKey]}
|
||||
metricDelaySeconds={tableProps.metricTimeThresholds()[props.typeKey] ?? {}}
|
||||
onMetricDelayChange={(metric, value) =>
|
||||
state.updateMetricDelay(props.typeKey, metric, value)
|
||||
}
|
||||
factoryDefaults={props.factoryDefaults}
|
||||
onResetDefaults={props.onResetDefaults}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThresholdsTableVMwareTab(props: ThresholdsTableSectionProps) {
|
||||
const vmwareDefaults = () => props.tableProps.vmwareDefaults ?? {};
|
||||
const datastoreDefaults = () => ({ usage: vmwareDefaults().usage ?? 85 });
|
||||
|
||||
const setDatastoreDefaults = (
|
||||
value:
|
||||
| Record<string, number | undefined>
|
||||
| ((prev: Record<string, number | undefined>) => Record<string, number | undefined>),
|
||||
) => {
|
||||
if (!props.tableProps.setVMwareDefaults) return;
|
||||
props.tableProps.setVMwareDefaults((prev) => {
|
||||
const next = typeof value === 'function' ? value({ usage: prev.usage }) : value;
|
||||
return { ...prev, usage: next.usage ?? 85 };
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<VMwareResourceSection
|
||||
{...props}
|
||||
id="vmwareHosts"
|
||||
title="Hosts"
|
||||
resources={props.state.vmwareHostsWithOverrides}
|
||||
columns={VMWARE_HOST_COLUMNS}
|
||||
icon={<Cpu class="w-5 h-5" />}
|
||||
typeKey="vmware-host"
|
||||
defaults={vmwareDefaults()}
|
||||
factoryDefaults={props.tableProps.factoryVMwareDefaults}
|
||||
setGlobalDefaults={props.tableProps.setVMwareDefaults}
|
||||
onResetDefaults={props.tableProps.resetVMwareDefaults}
|
||||
/>
|
||||
<VMwareResourceSection
|
||||
{...props}
|
||||
id="vmwareVMs"
|
||||
title="Virtual Machines"
|
||||
resources={props.state.vmwareVMsWithOverrides}
|
||||
columns={VMWARE_VM_COLUMNS}
|
||||
icon={<Monitor class="w-5 h-5" />}
|
||||
typeKey="vmware-vm"
|
||||
defaults={vmwareDefaults()}
|
||||
factoryDefaults={props.tableProps.factoryVMwareDefaults}
|
||||
setGlobalDefaults={props.tableProps.setVMwareDefaults}
|
||||
onResetDefaults={props.tableProps.resetVMwareDefaults}
|
||||
/>
|
||||
<VMwareResourceSection
|
||||
{...props}
|
||||
id="vmwareDatastores"
|
||||
title="Datastores"
|
||||
resources={props.state.vmwareDatastoresWithOverrides}
|
||||
columns={VMWARE_DATASTORE_COLUMNS}
|
||||
icon={<Database class="w-5 h-5" />}
|
||||
typeKey="vmware-datastore"
|
||||
defaults={datastoreDefaults()}
|
||||
factoryDefaults={
|
||||
props.tableProps.factoryVMwareDefaults?.usage !== undefined
|
||||
? { usage: props.tableProps.factoryVMwareDefaults.usage }
|
||||
: undefined
|
||||
}
|
||||
setGlobalDefaults={setDatastoreDefaults}
|
||||
onResetDefaults={props.tableProps.resetVMwareDefaults}
|
||||
/>
|
||||
<VMwareResourceSection
|
||||
{...props}
|
||||
id="vmwareNetworks"
|
||||
title="Networks"
|
||||
resources={props.state.vmwareNetworksWithOverrides}
|
||||
columns={[]}
|
||||
icon={<Network class="w-5 h-5" />}
|
||||
typeKey="vmware-network"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import { normalizeDockerIgnoredInput } from '@/features/alerts/thresholds/helper
|
|||
import type { PMGThresholdDefaults, SnapshotAlertConfig, BackupAlertConfig } from '@/types/alerts';
|
||||
import type { Agent, Alert } from '@/types/api';
|
||||
|
||||
const [getPathname, setPathname] = createSignal('/alerts/thresholds/containers');
|
||||
const [getPathname, setPathname] = createSignal('/alerts/thresholds/docker');
|
||||
const mockNavigate = vi.fn();
|
||||
|
||||
vi.mock('@solidjs/router', () => ({
|
||||
|
|
@ -93,7 +93,7 @@ afterEach(() => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setPathname('/alerts/thresholds/containers');
|
||||
setPathname('/alerts/thresholds/docker');
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
|
|
@ -139,6 +139,7 @@ const baseProps = () => ({
|
|||
storage: [],
|
||||
containerRuntimes: [],
|
||||
dockerHosts: [],
|
||||
allResources: [],
|
||||
pbsInstances: [],
|
||||
pmgInstances: [],
|
||||
pmgThresholds: () => DEFAULT_PMG_THRESHOLDS,
|
||||
|
|
@ -151,6 +152,43 @@ const baseProps = () => ({
|
|||
setGuestPoweredOffSeverity: vi.fn(),
|
||||
nodeDefaults: {},
|
||||
setNodeDefaults: vi.fn(),
|
||||
pbsDefaults: { cpu: 80, memory: 85 },
|
||||
setPBSDefaults: vi.fn(),
|
||||
kubernetesDefaults: {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 90,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
},
|
||||
setKubernetesDefaults: vi.fn(),
|
||||
trueNASDefaults: {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 85,
|
||||
usage: 85,
|
||||
temperature: 80,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
},
|
||||
setTrueNASDefaults: vi.fn(),
|
||||
trueNASDiskDefaults: { temperature: 55 },
|
||||
setTrueNASDiskDefaults: vi.fn(),
|
||||
vmwareDefaults: {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 90,
|
||||
usage: 85,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
},
|
||||
setVMwareDefaults: vi.fn(),
|
||||
agentDefaults: { cpu: 80, memory: 85, disk: 90 },
|
||||
setAgentDefaults: vi.fn(),
|
||||
dockerDefaults: DEFAULT_DOCKER_DEFAULTS,
|
||||
|
|
@ -163,12 +201,49 @@ const baseProps = () => ({
|
|||
setStorageDefault: vi.fn(),
|
||||
resetGuestDefaults: vi.fn(),
|
||||
resetNodeDefaults: vi.fn(),
|
||||
resetPBSDefaults: vi.fn(),
|
||||
resetKubernetesDefaults: vi.fn(),
|
||||
resetTrueNASDefaults: vi.fn(),
|
||||
resetTrueNASDiskDefaults: vi.fn(),
|
||||
resetVMwareDefaults: vi.fn(),
|
||||
resetAgentDefaults: vi.fn(),
|
||||
resetDockerDefaults: vi.fn(),
|
||||
resetDockerIgnoredPrefixes: vi.fn(),
|
||||
resetStorageDefault: vi.fn(),
|
||||
factoryGuestDefaults: {},
|
||||
factoryNodeDefaults: {},
|
||||
factoryPBSDefaults: { cpu: 80, memory: 85 },
|
||||
factoryKubernetesDefaults: {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 90,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
},
|
||||
factoryTrueNASDefaults: {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 85,
|
||||
usage: 85,
|
||||
temperature: 80,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
},
|
||||
factoryTrueNASDiskDefaults: { temperature: 55 },
|
||||
factoryVMwareDefaults: {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 90,
|
||||
usage: 85,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
},
|
||||
factoryAgentDefaults: { cpu: 80, memory: 85, disk: 90 },
|
||||
factoryDockerDefaults: DEFAULT_DOCKER_DEFAULTS,
|
||||
factoryStorageDefault: 85,
|
||||
|
|
@ -192,7 +267,26 @@ const baseProps = () => ({
|
|||
criticalSizeGiB: 0,
|
||||
} as SnapshotAlertConfig,
|
||||
resetSnapshotDefaults: vi.fn(),
|
||||
timeThresholds: () => ({ guest: 5, node: 5, storage: 5, pbs: 5, agent: 5 }),
|
||||
timeThresholds: () => ({
|
||||
guest: 5,
|
||||
node: 5,
|
||||
storage: 5,
|
||||
pbs: 5,
|
||||
agent: 5,
|
||||
'k8s-cluster': 5,
|
||||
'k8s-node': 5,
|
||||
'k8s-deployment': 5,
|
||||
'k8s-namespace': 5,
|
||||
pod: 5,
|
||||
'truenas-system': 5,
|
||||
'truenas-pool': 5,
|
||||
'truenas-dataset': 5,
|
||||
'truenas-disk': 5,
|
||||
'vmware-host': 5,
|
||||
'vmware-vm': 5,
|
||||
'vmware-datastore': 5,
|
||||
'vmware-network': 5,
|
||||
}),
|
||||
metricTimeThresholds: () => ({}),
|
||||
setMetricTimeThresholds: vi.fn(),
|
||||
activeAlerts: {},
|
||||
|
|
@ -215,6 +309,12 @@ const baseProps = () => ({
|
|||
setDisableAllDockerServices: vi.fn(),
|
||||
disableAllDockerContainers: () => false,
|
||||
setDisableAllDockerContainers: vi.fn(),
|
||||
disableAllKubernetes: () => false,
|
||||
setDisableAllKubernetes: vi.fn(),
|
||||
disableAllTrueNAS: () => false,
|
||||
setDisableAllTrueNAS: vi.fn(),
|
||||
disableAllVMware: () => false,
|
||||
setDisableAllVMware: vi.fn(),
|
||||
disableAllNodesOffline: () => false,
|
||||
setDisableAllNodesOffline: vi.fn(),
|
||||
disableAllGuestsOffline: () => false,
|
||||
|
|
@ -259,18 +359,18 @@ describe('ThresholdsTable basics', () => {
|
|||
});
|
||||
|
||||
describe('ThresholdsTable navigation and redirection', () => {
|
||||
it('redirects from base path to infrastructure', () => {
|
||||
it('redirects from base path to Proxmox', () => {
|
||||
setPathname('/alerts/thresholds');
|
||||
render(() => <ThresholdsTable {...(baseProps() as any)} />);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/infrastructure', {
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/proxmox', {
|
||||
replace: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects legacy thresholds sub-routes onto canonical infrastructure and systems paths', () => {
|
||||
setPathname('/alerts/thresholds/proxmox');
|
||||
it('redirects legacy thresholds sub-routes onto canonical platform paths', () => {
|
||||
setPathname('/alerts/thresholds/infrastructure');
|
||||
render(() => <ThresholdsTable {...(baseProps() as any)} />);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/infrastructure', {
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/proxmox', {
|
||||
replace: true,
|
||||
});
|
||||
|
||||
|
|
@ -303,11 +403,11 @@ describe('ThresholdsTable navigation and redirection', () => {
|
|||
it('navigates to correct route when tabs are clicked', () => {
|
||||
render(() => <ThresholdsTable {...(baseProps() as any)} />);
|
||||
|
||||
const infrastructureTab = screen
|
||||
const proxmoxTab = screen
|
||||
.getAllByRole('button')
|
||||
.find((el) => el.textContent?.includes('Infrastructure'));
|
||||
if (infrastructureTab) fireEvent.click(infrastructureTab);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/infrastructure');
|
||||
.find((el) => el.textContent?.includes('Proxmox'));
|
||||
if (proxmoxTab) fireEvent.click(proxmoxTab);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/proxmox');
|
||||
|
||||
const systemsTab = screen
|
||||
.getAllByRole('button')
|
||||
|
|
@ -315,11 +415,15 @@ describe('ThresholdsTable navigation and redirection', () => {
|
|||
if (systemsTab) fireEvent.click(systemsTab);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/systems');
|
||||
|
||||
const mailTab = screen
|
||||
.getAllByRole('button')
|
||||
.find((el) => el.textContent?.includes('Mail Gateway'));
|
||||
const mailTab = screen.getAllByRole('button').find((el) => el.textContent?.includes('PMG'));
|
||||
if (mailTab) fireEvent.click(mailTab);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/mail-gateway');
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/pmg');
|
||||
|
||||
const vmwareTab = screen
|
||||
.getAllByRole('button')
|
||||
.find((el) => el.textContent?.includes('vSphere'));
|
||||
if (vmwareTab) fireEvent.click(vmwareTab);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/vmware');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -400,9 +504,7 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
});
|
||||
|
||||
expect(screen.getByTestId('group-header-0')).toHaveTextContent('TrueNAS Main');
|
||||
expect(
|
||||
screen.getByTestId('resource-row-agent:truenas-main/disk:mnt-tank'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('resource-row-agent:truenas-main/disk:mnt-tank')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders infrastructure hosts and guests correctly', async () => {
|
||||
|
|
@ -523,7 +625,9 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
});
|
||||
|
||||
expect(screen.getByTestId('resource-name-storage1')).toHaveTextContent('Secret Datastore');
|
||||
expect(screen.getByTestId('resource-name-storage1')).not.toHaveTextContent('redacted by policy');
|
||||
expect(screen.getByTestId('resource-name-storage1')).not.toHaveTextContent(
|
||||
'redacted by policy',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders policy-redacted docker containers with their raw display name in operator-local UI', async () => {
|
||||
|
|
@ -567,7 +671,9 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('resource-name-docker:docker-host-1/container-governed')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByTestId('resource-name-docker:docker-host-1/container-governed'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(
|
||||
|
|
@ -617,9 +723,9 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
});
|
||||
|
||||
expect(screen.getByTestId('resource-name-truenas-resource')).toHaveTextContent('TrueNAS');
|
||||
expect(screen.getByTestId('resource-name-docker:truenas-resource/ix-nextcloud')).toHaveTextContent(
|
||||
'Nextcloud',
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('resource-name-docker:truenas-resource/ix-nextcloud'),
|
||||
).toHaveTextContent('Nextcloud');
|
||||
expect(screen.queryByText('Ignored container prefixes')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Swarm service alerts')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -659,6 +765,106 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
screen.getByTestId('resource-node-agent:agent-governed/disk:var-lib'),
|
||||
).not.toHaveTextContent('redacted by policy');
|
||||
});
|
||||
|
||||
it('renders vSphere alert targets from canonical VMware resources', async () => {
|
||||
setPathname('/alerts/thresholds/vmware');
|
||||
const resources = [
|
||||
{
|
||||
id: 'vmware:vc-1:host:host-101',
|
||||
type: 'agent',
|
||||
name: 'esxi-01.lab.local',
|
||||
displayName: 'ESXi 01',
|
||||
platformId: 'vc-1',
|
||||
platformType: 'vmware-vsphere',
|
||||
sourceType: 'api',
|
||||
sources: ['vmware'],
|
||||
status: 'online',
|
||||
lastSeen: 123,
|
||||
vmware: {
|
||||
connectionName: 'Lab vCenter',
|
||||
clusterName: 'Prod Compute',
|
||||
managedObjectId: 'host-101',
|
||||
entityType: 'host',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'vmware:vc-1:vm:vm-201',
|
||||
type: 'vm',
|
||||
name: 'app-01',
|
||||
displayName: 'App 01',
|
||||
platformId: 'vc-1',
|
||||
platformType: 'vmware-vsphere',
|
||||
sourceType: 'api',
|
||||
sources: ['vmware'],
|
||||
status: 'online',
|
||||
lastSeen: 123,
|
||||
parentName: 'esxi-01.lab.local',
|
||||
vmware: {
|
||||
connectionName: 'Lab vCenter',
|
||||
runtimeHostName: 'esxi-01.lab.local',
|
||||
managedObjectId: 'vm-201',
|
||||
entityType: 'vm',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'vmware:vc-1:datastore:datastore-301',
|
||||
type: 'storage',
|
||||
name: 'nvme-primary',
|
||||
displayName: 'nvme-primary',
|
||||
platformId: 'vc-1',
|
||||
platformType: 'vmware-vsphere',
|
||||
sourceType: 'api',
|
||||
sources: ['vmware'],
|
||||
status: 'online',
|
||||
lastSeen: 123,
|
||||
storage: { platform: 'vmware-vsphere', topology: 'datastore' },
|
||||
vmware: {
|
||||
connectionName: 'Lab vCenter',
|
||||
datacenterName: 'Lab Datacenter',
|
||||
managedObjectId: 'datastore-301',
|
||||
entityType: 'datastore',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'vmware:vc-1:network:network-401',
|
||||
type: 'network',
|
||||
name: 'VM Network',
|
||||
displayName: 'VM Network',
|
||||
platformId: 'vc-1',
|
||||
platformType: 'vmware-vsphere',
|
||||
sourceType: 'api',
|
||||
sources: ['vmware'],
|
||||
status: 'online',
|
||||
lastSeen: 123,
|
||||
vmware: {
|
||||
connectionName: 'Lab vCenter',
|
||||
datacenterName: 'Lab Datacenter',
|
||||
managedObjectId: 'network-401',
|
||||
entityType: 'network',
|
||||
},
|
||||
},
|
||||
] as any[];
|
||||
|
||||
render(() => <ThresholdsTable {...(baseProps() as any)} allResources={resources} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('section-Hosts')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('section-Virtual Machines')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('section-Datastores')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('section-Networks')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('resource-name-vmware:vc-1:host:host-101')).toHaveTextContent(
|
||||
'ESXi 01',
|
||||
);
|
||||
expect(screen.getByTestId('resource-name-vmware:vc-1:vm:vm-201')).toHaveTextContent('App 01');
|
||||
expect(
|
||||
screen.getByTestId('resource-name-vmware:vc-1:datastore:datastore-301'),
|
||||
).toHaveTextContent('nvme-primary');
|
||||
expect(screen.getByTestId('resource-name-vmware:vc-1:network:network-401')).toHaveTextContent(
|
||||
'VM Network',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ThresholdsTable Metric Formatting', () => {
|
||||
|
|
@ -863,7 +1069,7 @@ describe('ThresholdsTable V6 ID compatibility', () => {
|
|||
});
|
||||
|
||||
it('removes PBS offline alerts using legacy compatibility IDs when disabled', async () => {
|
||||
setPathname('/alerts/thresholds/infrastructure');
|
||||
setPathname('/alerts/thresholds/pbs');
|
||||
const removeAlerts = vi.fn();
|
||||
const pbs = {
|
||||
id: 'pbs-main',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { Resource as UnifiedResource, ResourcePolicy } from '@/types/resource';
|
||||
import type { PlatformTableColumnKind } from '@/features/platformPage/columnAlignment';
|
||||
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
|
||||
|
||||
const COLUMN_TOOLTIP_LOOKUP: Record<string, string> = {
|
||||
|
|
@ -218,6 +219,14 @@ export function getAlertResourceColumnHeaderTooltip(column: string): string | un
|
|||
return COLUMN_TOOLTIP_LOOKUP[column] ?? COLUMN_TOOLTIP_LOOKUP[normalized];
|
||||
}
|
||||
|
||||
export function getAlertResourceColumnKind(column: string): PlatformTableColumnKind {
|
||||
const metric = normalizeAlertResourceMetricKey(column);
|
||||
if (metric === 'backup' || metric === 'snapshot') {
|
||||
return 'badge';
|
||||
}
|
||||
return 'numeric-value';
|
||||
}
|
||||
|
||||
export function alertResourceSupportsMetric(
|
||||
resourceType: string | undefined,
|
||||
metric: string,
|
||||
|
|
@ -235,6 +244,53 @@ export function alertResourceSupportsMetric(
|
|||
if (resourceType === 'storage') {
|
||||
return metric === 'usage';
|
||||
}
|
||||
if (resourceType === 'kubernetesNamespace') {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
resourceType === 'kubernetesCluster' ||
|
||||
resourceType === 'kubernetesDeployment' ||
|
||||
resourceType === 'kubernetesPod'
|
||||
) {
|
||||
return ['cpu', 'memory', 'disk', 'diskRead', 'diskWrite', 'networkIn', 'networkOut'].includes(
|
||||
metric,
|
||||
);
|
||||
}
|
||||
if (resourceType === 'kubernetesNode') {
|
||||
return ['cpu', 'memory', 'disk'].includes(metric);
|
||||
}
|
||||
if (resourceType === 'truenasSystem') {
|
||||
return [
|
||||
'cpu',
|
||||
'memory',
|
||||
'disk',
|
||||
'diskRead',
|
||||
'diskWrite',
|
||||
'networkIn',
|
||||
'networkOut',
|
||||
'temperature',
|
||||
].includes(metric);
|
||||
}
|
||||
if (resourceType === 'truenasPool' || resourceType === 'truenasDataset') {
|
||||
return metric === 'usage';
|
||||
}
|
||||
if (resourceType === 'truenasDisk') {
|
||||
return metric === 'temperature';
|
||||
}
|
||||
if (resourceType === 'vmwareHost') {
|
||||
return ['cpu', 'memory', 'diskRead', 'diskWrite', 'networkIn', 'networkOut'].includes(metric);
|
||||
}
|
||||
if (resourceType === 'vmwareVm') {
|
||||
return ['cpu', 'memory', 'disk', 'diskRead', 'diskWrite', 'networkIn', 'networkOut'].includes(
|
||||
metric,
|
||||
);
|
||||
}
|
||||
if (resourceType === 'vmwareDatastore') {
|
||||
return metric === 'usage';
|
||||
}
|
||||
if (resourceType === 'vmwareNetwork') {
|
||||
return false;
|
||||
}
|
||||
if (resourceType === 'dockerContainer') {
|
||||
return [
|
||||
'cpu',
|
||||
|
|
@ -267,7 +323,8 @@ export function getAlertResourceMetricDisplayValue(
|
|||
editingThresholds?: AlertResourceThresholdMap,
|
||||
isEditing = false,
|
||||
): number {
|
||||
const extract = (source: Record<string, unknown> | undefined) => parseAlertMetricNumber(source?.[metric]);
|
||||
const extract = (source: Record<string, unknown> | undefined) =>
|
||||
parseAlertMetricNumber(source?.[metric]);
|
||||
const defaults = resource.defaults as Record<string, unknown> | undefined;
|
||||
|
||||
if (isEditing) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Show } from 'solid-js';
|
|||
import { IncidentTimelinePanel } from '@/components/Alerts/IncidentTimelinePanel';
|
||||
import { InvestigateAlertButton } from '@/components/Alerts/InvestigateAlertButton';
|
||||
import { TableCell, TableRow } from '@/components/shared/Table';
|
||||
import { getPlatformTableCellClassForKind } from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
getAlertHistoryResourceTypeBadgeClass,
|
||||
getAlertHistorySourcePresentation,
|
||||
|
|
@ -32,61 +33,65 @@ export function AlertHistoryTableAlertRow(props: AlertHistoryTableAlertRowProps)
|
|||
<TableRow
|
||||
class={`border-b border-border hover:bg-surface-hover ${historyStatusPresentation().rowClassName}`}
|
||||
>
|
||||
<TableCell class="p-1 px-1 font-mono whitespace-nowrap text-muted sm:p-1.5 sm:px-2">
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} font-mono whitespace-nowrap text-muted`}
|
||||
>
|
||||
{new Date(props.alert.startTime).toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="p-1 px-1 text-center sm:p-1.5 sm:px-2">
|
||||
<TableCell class={getPlatformTableCellClassForKind('badge')}>
|
||||
<span class={sourcePresentation().className}>{sourcePresentation().label}</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
class="max-w-[150px] truncate p-1 px-1 font-medium text-base-content sm:p-1.5 sm:px-2"
|
||||
class={`${getPlatformTableCellClassForKind('name')} max-w-[150px] truncate font-medium text-base-content`}
|
||||
title={props.alert.resourceName}
|
||||
>
|
||||
{props.alert.resourceName}
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="p-1 px-1 sm:p-1.5 sm:px-2">
|
||||
<TableCell class={getPlatformTableCellClassForKind('badge')}>
|
||||
<span class={getAlertHistoryResourceTypeBadgeClass(props.alert.resourceType)}>
|
||||
{props.alert.resourceType}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="p-1 px-1 text-center sm:p-1.5 sm:px-2">
|
||||
<TableCell class={getPlatformTableCellClassForKind('badge')}>
|
||||
<span class={getAlertIncidentLevelBadgeClass(props.alert.severity)}>
|
||||
{props.alert.severity}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
class="max-w-[300px] truncate p-1 px-1 text-base-content sm:p-1.5 sm:px-2"
|
||||
class={`${getPlatformTableCellClassForKind('text')} max-w-[300px] truncate text-base-content`}
|
||||
title={props.alert.description}
|
||||
>
|
||||
{props.alert.description}
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="hidden p-1 px-1 text-center text-muted lg:table-cell sm:p-1.5 sm:px-2">
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-muted lg:table-cell`}
|
||||
>
|
||||
{props.alert.duration}
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="hidden p-1 px-1 text-center lg:table-cell sm:p-1.5 sm:px-2">
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('badge')} hidden lg:table-cell`}>
|
||||
<span class={historyStatusPresentation().className}>
|
||||
{historyStatusPresentation().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
class="hidden truncate p-1 px-1 text-muted lg:table-cell sm:p-1.5 sm:px-2"
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden truncate text-muted lg:table-cell`}
|
||||
title={props.alert.nodeDisplayName || props.alert.node || ''}
|
||||
>
|
||||
{props.alert.nodeDisplayName || props.alert.node || '—'}
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="p-1 px-1 text-center sm:p-1.5 sm:px-2">
|
||||
<TableCell class={getPlatformTableCellClassForKind('badge')}>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Show when={props.alert.source === 'alert'}>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { For, Show } from 'solid-js';
|
|||
|
||||
import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/shared/Table';
|
||||
import { TableCard } from '@/components/shared/TableCard';
|
||||
import { getPlatformTableHeadClassForKind } from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
getAlertHistoryEmptyState,
|
||||
getAlertHistoryLoadingState,
|
||||
|
|
@ -34,34 +35,54 @@ export function AlertHistoryTableSection(props: AlertHistoryTableSectionProps) {
|
|||
<Table class="w-full text-[11px] sm:text-sm">
|
||||
<TableHeader>
|
||||
<TableRow class="border-b border-border bg-surface-hover text-muted">
|
||||
<TableHead class="p-1 px-1 text-left text-[10px] font-medium uppercase tracking-wider sm:p-1.5 sm:px-2 sm:text-xs">
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} text-[10px] font-medium uppercase tracking-wider sm:text-xs`}
|
||||
>
|
||||
Timestamp
|
||||
</TableHead>
|
||||
<TableHead class="p-1 px-1 text-center text-[10px] font-medium uppercase tracking-wider sm:p-1.5 sm:px-2 sm:text-xs">
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('badge')} text-[10px] font-medium uppercase tracking-wider sm:text-xs`}
|
||||
>
|
||||
Source
|
||||
</TableHead>
|
||||
<TableHead class="p-1 px-1 text-left text-[10px] font-medium uppercase tracking-wider sm:p-1.5 sm:px-2 sm:text-xs">
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('name')} text-[10px] font-medium uppercase tracking-wider sm:text-xs`}
|
||||
>
|
||||
Resource
|
||||
</TableHead>
|
||||
<TableHead class="p-1 px-1 text-left text-[10px] font-medium uppercase tracking-wider sm:p-1.5 sm:px-2 sm:text-xs">
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('badge')} text-[10px] font-medium uppercase tracking-wider sm:text-xs`}
|
||||
>
|
||||
{getTypeColumnLabel()}
|
||||
</TableHead>
|
||||
<TableHead class="p-1 px-1 text-center text-[10px] font-medium uppercase tracking-wider sm:p-1.5 sm:px-2 sm:text-xs">
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('badge')} text-[10px] font-medium uppercase tracking-wider sm:text-xs`}
|
||||
>
|
||||
Severity
|
||||
</TableHead>
|
||||
<TableHead class="p-1 px-1 text-left text-[10px] font-medium uppercase tracking-wider sm:p-1.5 sm:px-2 sm:text-xs">
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} text-[10px] font-medium uppercase tracking-wider sm:text-xs`}
|
||||
>
|
||||
Message
|
||||
</TableHead>
|
||||
<TableHead class="hidden p-1 px-1 text-center text-[10px] font-medium uppercase tracking-wider lg:table-cell sm:p-1.5 sm:px-2 sm:text-xs">
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden text-[10px] font-medium uppercase tracking-wider lg:table-cell sm:text-xs`}
|
||||
>
|
||||
Duration
|
||||
</TableHead>
|
||||
<TableHead class="hidden p-1 px-1 text-center text-[10px] font-medium uppercase tracking-wider lg:table-cell sm:p-1.5 sm:px-2 sm:text-xs">
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('badge')} hidden text-[10px] font-medium uppercase tracking-wider lg:table-cell sm:text-xs`}
|
||||
>
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead class="hidden p-1 px-1 text-left text-[10px] font-medium uppercase tracking-wider lg:table-cell sm:p-1.5 sm:px-2 sm:text-xs">
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden text-[10px] font-medium uppercase tracking-wider lg:table-cell sm:text-xs`}
|
||||
>
|
||||
Node
|
||||
</TableHead>
|
||||
<TableHead class="p-1 px-1 text-center text-[10px] font-medium uppercase tracking-wider sm:p-1.5 sm:px-2 sm:text-xs">
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('badge')} text-[10px] font-medium uppercase tracking-wider sm:text-xs`}
|
||||
>
|
||||
Actions
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
|
|
|
|||
|
|
@ -107,6 +107,14 @@ export function AlertsConfigurationSurface(props: AlertsConfigurationSurfaceProp
|
|||
setAgentDefaults={state.setAgentDefaults}
|
||||
pbsDefaults={state.pbsDefaults}
|
||||
setPBSDefaults={state.setPBSDefaults}
|
||||
kubernetesDefaults={state.kubernetesDefaults}
|
||||
setKubernetesDefaults={state.setKubernetesDefaults}
|
||||
trueNASDefaults={state.trueNASDefaults}
|
||||
setTrueNASDefaults={state.setTrueNASDefaults}
|
||||
trueNASDiskDefaults={state.trueNASDiskDefaults}
|
||||
setTrueNASDiskDefaults={state.setTrueNASDiskDefaults}
|
||||
vmwareDefaults={state.vmwareDefaults}
|
||||
setVMwareDefaults={state.setVMwareDefaults}
|
||||
dockerDefaults={state.dockerDefaults}
|
||||
dockerDisableConnectivity={state.dockerDisableConnectivity}
|
||||
setDockerDisableConnectivity={state.setDockerDisableConnectivity}
|
||||
|
|
@ -156,6 +164,12 @@ export function AlertsConfigurationSurface(props: AlertsConfigurationSurfaceProp
|
|||
setDisableAllDockerServices={state.setDisableAllDockerServices}
|
||||
disableAllDockerContainers={state.disableAllDockerContainers}
|
||||
setDisableAllDockerContainers={state.setDisableAllDockerContainers}
|
||||
disableAllKubernetes={state.disableAllKubernetes}
|
||||
setDisableAllKubernetes={state.setDisableAllKubernetes}
|
||||
disableAllTrueNAS={state.disableAllTrueNAS}
|
||||
setDisableAllTrueNAS={state.setDisableAllTrueNAS}
|
||||
disableAllVMware={state.disableAllVMware}
|
||||
setDisableAllVMware={state.setDisableAllVMware}
|
||||
disableAllNodesOffline={state.disableAllNodesOffline}
|
||||
setDisableAllNodesOffline={state.setDisableAllNodesOffline}
|
||||
disableAllGuestsOffline={state.disableAllGuestsOffline}
|
||||
|
|
@ -169,6 +183,10 @@ export function AlertsConfigurationSurface(props: AlertsConfigurationSurfaceProp
|
|||
disableAllDockerHostsOffline={state.disableAllDockerHostsOffline}
|
||||
setDisableAllDockerHostsOffline={state.setDisableAllDockerHostsOffline}
|
||||
resetPBSDefaults={state.resetPBSDefaults}
|
||||
resetKubernetesDefaults={state.resetKubernetesDefaults}
|
||||
resetTrueNASDefaults={state.resetTrueNASDefaults}
|
||||
resetTrueNASDiskDefaults={state.resetTrueNASDiskDefaults}
|
||||
resetVMwareDefaults={state.resetVMwareDefaults}
|
||||
resetDockerDefaults={state.resetDockerDefaults}
|
||||
resetDockerIgnoredPrefixes={state.resetDockerIgnoredPrefixes}
|
||||
resetStorageDefault={state.resetStorageDefault}
|
||||
|
|
@ -177,6 +195,10 @@ export function AlertsConfigurationSurface(props: AlertsConfigurationSurfaceProp
|
|||
factoryGuestDefaults={state.factoryGuestDefaults}
|
||||
factoryNodeDefaults={state.factoryNodeDefaults}
|
||||
factoryPBSDefaults={state.factoryPBSDefaults}
|
||||
factoryKubernetesDefaults={state.factoryKubernetesDefaults}
|
||||
factoryTrueNASDefaults={state.factoryTrueNASDefaults}
|
||||
factoryTrueNASDiskDefaults={state.factoryTrueNASDiskDefaults}
|
||||
factoryVMwareDefaults={state.factoryVMwareDefaults}
|
||||
factoryAgentDefaults={state.factoryAgentDefaults}
|
||||
factoryDockerDefaults={state.factoryDockerDefaults}
|
||||
factoryStorageDefault={state.factoryStorageDefault}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,41 @@ const buildProps = (): ThresholdsTabProps =>
|
|||
setNodeDefaults: vi.fn(),
|
||||
pbsDefaults: () => ({}),
|
||||
setPBSDefaults: vi.fn(),
|
||||
kubernetesDefaults: () => ({
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 90,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
}),
|
||||
setKubernetesDefaults: vi.fn(),
|
||||
trueNASDefaults: () => ({
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 85,
|
||||
usage: 85,
|
||||
temperature: 80,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
}),
|
||||
setTrueNASDefaults: vi.fn(),
|
||||
trueNASDiskDefaults: () => ({ temperature: 55 }),
|
||||
setTrueNASDiskDefaults: vi.fn(),
|
||||
vmwareDefaults: () => ({
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 90,
|
||||
usage: 85,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
}),
|
||||
setVMwareDefaults: vi.fn(),
|
||||
agentDefaults: () => ({ cpu: 80 }),
|
||||
setAgentDefaults: vi.fn(),
|
||||
dockerDefaults: () => ({
|
||||
|
|
@ -71,6 +106,10 @@ const buildProps = (): ThresholdsTabProps =>
|
|||
resetGuestDefaults: vi.fn(),
|
||||
resetNodeDefaults: vi.fn(),
|
||||
resetPBSDefaults: vi.fn(),
|
||||
resetKubernetesDefaults: vi.fn(),
|
||||
resetTrueNASDefaults: vi.fn(),
|
||||
resetTrueNASDiskDefaults: vi.fn(),
|
||||
resetVMwareDefaults: vi.fn(),
|
||||
resetAgentDefaults: vi.fn(),
|
||||
resetDockerDefaults: vi.fn(),
|
||||
resetDockerIgnoredPrefixes: vi.fn(),
|
||||
|
|
@ -78,6 +117,37 @@ const buildProps = (): ThresholdsTabProps =>
|
|||
factoryGuestDefaults: {},
|
||||
factoryNodeDefaults: {},
|
||||
factoryPBSDefaults: {},
|
||||
factoryKubernetesDefaults: {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 90,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
},
|
||||
factoryTrueNASDefaults: {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 85,
|
||||
usage: 85,
|
||||
temperature: 80,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
},
|
||||
factoryTrueNASDiskDefaults: { temperature: 55 },
|
||||
factoryVMwareDefaults: {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 90,
|
||||
usage: 85,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
},
|
||||
factoryAgentDefaults: { cpu: 80 },
|
||||
factoryDockerDefaults: {
|
||||
cpu: 80,
|
||||
|
|
@ -91,7 +161,26 @@ const buildProps = (): ThresholdsTabProps =>
|
|||
serviceCriticalGapPercent: 50,
|
||||
},
|
||||
factoryStorageDefault: 85,
|
||||
timeThresholds: () => ({ guest: 5, node: 5, storage: 5, pbs: 5, agent: 5 }),
|
||||
timeThresholds: () => ({
|
||||
guest: 5,
|
||||
node: 5,
|
||||
storage: 5,
|
||||
pbs: 5,
|
||||
agent: 5,
|
||||
'k8s-cluster': 5,
|
||||
'k8s-node': 5,
|
||||
'k8s-deployment': 5,
|
||||
'k8s-namespace': 5,
|
||||
pod: 5,
|
||||
'truenas-system': 5,
|
||||
'truenas-pool': 5,
|
||||
'truenas-dataset': 5,
|
||||
'truenas-disk': 5,
|
||||
'vmware-host': 5,
|
||||
'vmware-vm': 5,
|
||||
'vmware-datastore': 5,
|
||||
'vmware-network': 5,
|
||||
}),
|
||||
metricTimeThresholds: () => ({}),
|
||||
setMetricTimeThresholds: vi.fn(),
|
||||
snapshotDefaults: () => ({
|
||||
|
|
@ -151,6 +240,12 @@ const buildProps = (): ThresholdsTabProps =>
|
|||
setDisableAllDockerServices: vi.fn(),
|
||||
disableAllDockerContainers: () => false,
|
||||
setDisableAllDockerContainers: vi.fn(),
|
||||
disableAllKubernetes: () => false,
|
||||
setDisableAllKubernetes: vi.fn(),
|
||||
disableAllTrueNAS: () => false,
|
||||
setDisableAllTrueNAS: vi.fn(),
|
||||
disableAllVMware: () => false,
|
||||
setDisableAllVMware: vi.fn(),
|
||||
disableAllNodesOffline: () => false,
|
||||
setDisableAllNodesOffline: vi.fn(),
|
||||
disableAllGuestsOffline: () => false,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ describe('alertsConfigurationModel', () => {
|
|||
);
|
||||
expect(snapshot.storageDefault).toBe(FACTORY_STORAGE_DEFAULT);
|
||||
expect(snapshot.timeThresholds.guest).toBe(5);
|
||||
expect(snapshot.timeThresholds['vmware-host']).toBe(5);
|
||||
expect(snapshot.vmwareDefaults.usage).toBe(85);
|
||||
expect(snapshot.scheduleCooldown.enabled).toBe(true);
|
||||
expect(snapshot.backupDefaults.ignoreVMIDs).toEqual([]);
|
||||
});
|
||||
|
|
@ -49,6 +51,10 @@ describe('alertsConfigurationModel', () => {
|
|||
criticalDays: 10,
|
||||
ignoreVMIDs: [' 101 ', '101', ''],
|
||||
},
|
||||
vmwareDefaults: {
|
||||
cpu: { trigger: 83, clear: 78 },
|
||||
usage: { trigger: 91, clear: 86 },
|
||||
},
|
||||
snapshotDefaults: {
|
||||
enabled: true,
|
||||
warningDays: 25,
|
||||
|
|
@ -74,6 +80,8 @@ describe('alertsConfigurationModel', () => {
|
|||
expect(snapshot.dockerDefaults.serviceWarnGapPercent).toBe(40);
|
||||
expect(snapshot.dockerDefaults.serviceCriticalGapPercent).toBe(40);
|
||||
expect(snapshot.backupDefaults.warningDays).toBe(10);
|
||||
expect(snapshot.vmwareDefaults.cpu).toBe(83);
|
||||
expect(snapshot.vmwareDefaults.usage).toBe(91);
|
||||
expect(snapshot.backupDefaults.criticalDays).toBe(10);
|
||||
expect(snapshot.backupDefaults.ignoreVMIDs).toEqual(['101']);
|
||||
expect(snapshot.snapshotDefaults.warningDays).toBe(5);
|
||||
|
|
@ -133,6 +141,8 @@ describe('alertsConfigurationModel', () => {
|
|||
expect(result.dockerValidationError).toBeUndefined();
|
||||
expect(result.alertConfig).toBeDefined();
|
||||
expect(result.alertConfig?.guestDefaults.cpu).toEqual({ trigger: 92, clear: 87 });
|
||||
expect(result.alertConfig?.vmwareDefaults?.cpu).toEqual({ trigger: 80, clear: 75 });
|
||||
expect(result.alertConfig?.vmwareDefaults?.usage).toEqual({ trigger: 85, clear: 80 });
|
||||
expect(result.alertConfig?.dockerIgnoredContainerPrefixes).toEqual(['web']);
|
||||
expect(result.alertConfig?.guestTagWhitelist).toEqual(['prod']);
|
||||
expect(result.alertConfig?.metricTimeThresholds).toEqual({ guest: { cpu: 17 } });
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ interface BuildProjectedOverridesArgs {
|
|||
containerRuntimeResources: Resource[];
|
||||
getChildren: (resourceId: string) => Resource[];
|
||||
pbsInstanceById: Map<string, PBSInstance>;
|
||||
allResources?: Resource[];
|
||||
}
|
||||
|
||||
export const buildProjectedOverrides = ({
|
||||
|
|
@ -225,6 +226,7 @@ export const buildProjectedOverrides = ({
|
|||
containerRuntimeResources,
|
||||
getChildren,
|
||||
pbsInstanceById,
|
||||
allResources = [],
|
||||
}: BuildProjectedOverridesArgs): Override[] => {
|
||||
const overridesList: Override[] = [];
|
||||
const overrideIndexByID = new Map<string, number>();
|
||||
|
|
@ -236,6 +238,10 @@ export const buildProjectedOverrides = ({
|
|||
const agentMap = new Map<string, Resource>();
|
||||
const guestMap = new Map<string, Resource>();
|
||||
const storageMap = new Map<string, Resource>();
|
||||
const alertPlatformMap = new Map<
|
||||
string,
|
||||
{ resource: Resource; type: Override['type']; resourceType: string }
|
||||
>();
|
||||
|
||||
const upsertProjectedOverride = (override: Override) => {
|
||||
const existingIndex = overrideIndexByID.get(override.id);
|
||||
|
|
@ -247,6 +253,96 @@ export const buildProjectedOverrides = ({
|
|||
overridesList.push(override);
|
||||
};
|
||||
|
||||
const alertResourceIdCandidates = (resource: Resource): string[] =>
|
||||
uniqueIds(
|
||||
resource.id,
|
||||
resource.metricsTarget?.resourceId,
|
||||
resource.discoveryTarget?.resourceId,
|
||||
resource.platformId,
|
||||
);
|
||||
|
||||
const isTrueNASResource = (resource: Resource): boolean =>
|
||||
isTrueNASSystemResource(resource) ||
|
||||
resource.platformType === 'truenas' ||
|
||||
resource.sources?.includes('truenas') ||
|
||||
resource.storage?.platform === 'truenas';
|
||||
|
||||
const isVMwareResource = (resource: Resource): boolean =>
|
||||
resource.platformType === 'vmware-vsphere' ||
|
||||
resource.sources?.includes('vmware') ||
|
||||
Boolean(resource.vmware) ||
|
||||
resource.storage?.platform === 'vmware-vsphere';
|
||||
|
||||
allResources.forEach((resource) => {
|
||||
let type: Override['type'] | undefined;
|
||||
let resourceType = '';
|
||||
switch (resource.type) {
|
||||
case 'k8s-cluster':
|
||||
type = 'kubernetesCluster';
|
||||
resourceType = 'Kubernetes Cluster';
|
||||
break;
|
||||
case 'k8s-node':
|
||||
type = 'kubernetesNode';
|
||||
resourceType = 'Kubernetes Node';
|
||||
break;
|
||||
case 'k8s-namespace':
|
||||
type = 'kubernetesNamespace';
|
||||
resourceType = 'Kubernetes Namespace';
|
||||
break;
|
||||
case 'k8s-deployment':
|
||||
type = 'kubernetesDeployment';
|
||||
resourceType = 'Kubernetes Deployment';
|
||||
break;
|
||||
case 'pod':
|
||||
type = 'kubernetesPod';
|
||||
resourceType = 'Kubernetes Pod';
|
||||
break;
|
||||
case 'agent':
|
||||
if (isTrueNASResource(resource)) {
|
||||
type = 'truenasSystem';
|
||||
resourceType = 'TrueNAS System';
|
||||
} else if (isVMwareResource(resource)) {
|
||||
type = 'vmwareHost';
|
||||
resourceType = 'vSphere Host';
|
||||
}
|
||||
break;
|
||||
case 'vm':
|
||||
if (isVMwareResource(resource)) {
|
||||
type = 'vmwareVm';
|
||||
resourceType = 'vSphere VM';
|
||||
}
|
||||
break;
|
||||
case 'physical_disk':
|
||||
if (isTrueNASResource(resource)) {
|
||||
type = 'truenasDisk';
|
||||
resourceType = 'TrueNAS Disk';
|
||||
}
|
||||
break;
|
||||
case 'storage':
|
||||
case 'pool':
|
||||
case 'dataset':
|
||||
if (isTrueNASResource(resource)) {
|
||||
const topology = resource.storage?.topology || resource.type;
|
||||
type = topology === 'dataset' ? 'truenasDataset' : 'truenasPool';
|
||||
resourceType = topology === 'dataset' ? 'TrueNAS Dataset' : 'TrueNAS Pool';
|
||||
} else if (isVMwareResource(resource)) {
|
||||
type = 'vmwareDatastore';
|
||||
resourceType = 'vSphere Datastore';
|
||||
}
|
||||
break;
|
||||
case 'network':
|
||||
if (isVMwareResource(resource)) {
|
||||
type = 'vmwareNetwork';
|
||||
resourceType = 'vSphere Network';
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!type) return;
|
||||
alertResourceIdCandidates(resource).forEach((id) => {
|
||||
alertPlatformMap.set(id, { resource, type, resourceType });
|
||||
});
|
||||
});
|
||||
|
||||
const storageCoords = (resource: Resource): { node: string; instance: string } => {
|
||||
const data = platformData(resource);
|
||||
if (resource.type === 'datastore') {
|
||||
|
|
@ -310,6 +406,35 @@ export const buildProjectedOverrides = ({
|
|||
});
|
||||
|
||||
Object.entries(rawConfig).forEach(([key, thresholds]) => {
|
||||
const alertPlatformResource = alertPlatformMap.get(key);
|
||||
if (alertPlatformResource) {
|
||||
const { resource, type, resourceType } = alertPlatformResource;
|
||||
upsertProjectedOverride({
|
||||
id: key,
|
||||
name: getAlertResourceDisplayLabel(resource),
|
||||
type,
|
||||
resourceType,
|
||||
node:
|
||||
resource.parentName ||
|
||||
resource.kubernetes?.nodeName ||
|
||||
resource.truenas?.hostname ||
|
||||
resource.vmware?.runtimeHostName ||
|
||||
resource.vmware?.clusterName ||
|
||||
resource.vmware?.datacenterName ||
|
||||
resource.vmware?.vcenterHost,
|
||||
instance:
|
||||
resource.kubernetes?.clusterName ||
|
||||
resource.kubernetes?.namespace ||
|
||||
(resource.truenas ? 'TrueNAS' : undefined) ||
|
||||
resource.vmware?.connectionName ||
|
||||
resource.vmware?.vcenterHost ||
|
||||
(resource.vmware ? 'vSphere' : undefined),
|
||||
disabled: thresholds.disabled || false,
|
||||
thresholds: extractTriggerValues(thresholds),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const dockerHost = dockerHostMap.get(key);
|
||||
if (dockerHost) {
|
||||
upsertProjectedOverride({
|
||||
|
|
|
|||
|
|
@ -11,10 +11,14 @@ import {
|
|||
FACTORY_DOCKER_STATE_DISABLE_CONNECTIVITY,
|
||||
FACTORY_DOCKER_STATE_SEVERITY,
|
||||
FACTORY_GUEST_DEFAULTS,
|
||||
FACTORY_KUBERNETES_DEFAULTS,
|
||||
FACTORY_NODE_DEFAULTS,
|
||||
FACTORY_PBS_DEFAULTS,
|
||||
FACTORY_SNAPSHOT_DEFAULTS,
|
||||
FACTORY_STORAGE_DEFAULT,
|
||||
FACTORY_TRUENAS_DEFAULTS,
|
||||
FACTORY_TRUENAS_DISK_DEFAULTS,
|
||||
FACTORY_VMWARE_DEFAULTS,
|
||||
} from '@/utils/alertThresholdDefaults';
|
||||
|
||||
import {
|
||||
|
|
@ -45,10 +49,14 @@ export {
|
|||
FACTORY_DOCKER_STATE_DISABLE_CONNECTIVITY,
|
||||
FACTORY_DOCKER_STATE_SEVERITY,
|
||||
FACTORY_GUEST_DEFAULTS,
|
||||
FACTORY_KUBERNETES_DEFAULTS,
|
||||
FACTORY_NODE_DEFAULTS,
|
||||
FACTORY_PBS_DEFAULTS,
|
||||
FACTORY_SNAPSHOT_DEFAULTS,
|
||||
FACTORY_STORAGE_DEFAULT,
|
||||
FACTORY_TRUENAS_DEFAULTS,
|
||||
FACTORY_TRUENAS_DISK_DEFAULTS,
|
||||
FACTORY_VMWARE_DEFAULTS,
|
||||
} from '@/utils/alertThresholdDefaults';
|
||||
|
||||
export interface AlertsConfigurationSnapshot {
|
||||
|
|
@ -62,6 +70,10 @@ export interface AlertsConfigurationSnapshot {
|
|||
guestPoweredOffSeverity: 'warning' | 'critical';
|
||||
nodeDefaults: Record<string, number | undefined>;
|
||||
pbsDefaults: Record<string, number | undefined>;
|
||||
kubernetesDefaults: Record<string, number | undefined>;
|
||||
trueNASDefaults: Record<string, number | undefined>;
|
||||
trueNASDiskDefaults: Record<string, number | undefined>;
|
||||
vmwareDefaults: Record<string, number | undefined>;
|
||||
agentDefaults: Record<string, number | undefined>;
|
||||
dockerDefaults: typeof FACTORY_DOCKER_DEFAULTS;
|
||||
dockerDisableConnectivity: boolean;
|
||||
|
|
@ -78,6 +90,19 @@ export interface AlertsConfigurationSnapshot {
|
|||
storage: number;
|
||||
pbs: number;
|
||||
agent: number;
|
||||
'k8s-cluster': number;
|
||||
'k8s-node': number;
|
||||
'k8s-deployment': number;
|
||||
'k8s-namespace': number;
|
||||
pod: number;
|
||||
'truenas-system': number;
|
||||
'truenas-pool': number;
|
||||
'truenas-dataset': number;
|
||||
'truenas-disk': number;
|
||||
'vmware-host': number;
|
||||
'vmware-vm': number;
|
||||
'vmware-datastore': number;
|
||||
'vmware-network': number;
|
||||
};
|
||||
metricTimeThresholds: Record<string, Record<string, number>>;
|
||||
snapshotDefaults: SnapshotAlertConfig;
|
||||
|
|
@ -108,6 +133,9 @@ export interface AlertsConfigurationSnapshot {
|
|||
disableAllDockerHosts: boolean;
|
||||
disableAllDockerServices: boolean;
|
||||
disableAllDockerContainers: boolean;
|
||||
disableAllKubernetes: boolean;
|
||||
disableAllTrueNAS: boolean;
|
||||
disableAllVMware: boolean;
|
||||
disableAllNodesOffline: boolean;
|
||||
disableAllGuestsOffline: boolean;
|
||||
disableAllAgentsOffline: boolean;
|
||||
|
|
@ -183,6 +211,10 @@ export function createDefaultAlertsConfigurationSnapshot(): AlertsConfigurationS
|
|||
guestPoweredOffSeverity: 'warning',
|
||||
nodeDefaults: { ...FACTORY_NODE_DEFAULTS },
|
||||
pbsDefaults: { ...FACTORY_PBS_DEFAULTS },
|
||||
kubernetesDefaults: { ...FACTORY_KUBERNETES_DEFAULTS },
|
||||
trueNASDefaults: { ...FACTORY_TRUENAS_DEFAULTS },
|
||||
trueNASDiskDefaults: { ...FACTORY_TRUENAS_DISK_DEFAULTS },
|
||||
vmwareDefaults: { ...FACTORY_VMWARE_DEFAULTS },
|
||||
agentDefaults: { ...FACTORY_AGENT_DEFAULTS },
|
||||
dockerDefaults: { ...FACTORY_DOCKER_DEFAULTS },
|
||||
dockerDisableConnectivity: FACTORY_DOCKER_STATE_DISABLE_CONNECTIVITY,
|
||||
|
|
@ -199,6 +231,19 @@ export function createDefaultAlertsConfigurationSnapshot(): AlertsConfigurationS
|
|||
storage: DEFAULT_DELAY_SECONDS,
|
||||
pbs: DEFAULT_DELAY_SECONDS,
|
||||
agent: DEFAULT_DELAY_SECONDS,
|
||||
'k8s-cluster': DEFAULT_DELAY_SECONDS,
|
||||
'k8s-node': DEFAULT_DELAY_SECONDS,
|
||||
'k8s-deployment': DEFAULT_DELAY_SECONDS,
|
||||
'k8s-namespace': DEFAULT_DELAY_SECONDS,
|
||||
pod: DEFAULT_DELAY_SECONDS,
|
||||
'truenas-system': DEFAULT_DELAY_SECONDS,
|
||||
'truenas-pool': DEFAULT_DELAY_SECONDS,
|
||||
'truenas-dataset': DEFAULT_DELAY_SECONDS,
|
||||
'truenas-disk': DEFAULT_DELAY_SECONDS,
|
||||
'vmware-host': DEFAULT_DELAY_SECONDS,
|
||||
'vmware-vm': DEFAULT_DELAY_SECONDS,
|
||||
'vmware-datastore': DEFAULT_DELAY_SECONDS,
|
||||
'vmware-network': DEFAULT_DELAY_SECONDS,
|
||||
},
|
||||
metricTimeThresholds: {},
|
||||
snapshotDefaults: { ...FACTORY_SNAPSHOT_DEFAULTS },
|
||||
|
|
@ -229,6 +274,9 @@ export function createDefaultAlertsConfigurationSnapshot(): AlertsConfigurationS
|
|||
disableAllDockerHosts: false,
|
||||
disableAllDockerServices: false,
|
||||
disableAllDockerContainers: false,
|
||||
disableAllKubernetes: false,
|
||||
disableAllTrueNAS: false,
|
||||
disableAllVMware: false,
|
||||
disableAllNodesOffline: false,
|
||||
disableAllGuestsOffline: false,
|
||||
disableAllAgentsOffline: false,
|
||||
|
|
@ -276,6 +324,69 @@ export function readAlertsConfigurationSnapshot(config: AlertConfig): AlertsConf
|
|||
};
|
||||
}
|
||||
|
||||
if (config.kubernetesDefaults) {
|
||||
snapshot.kubernetesDefaults = {
|
||||
cpu: getTriggerValue(config.kubernetesDefaults.cpu) ?? FACTORY_KUBERNETES_DEFAULTS.cpu,
|
||||
memory:
|
||||
getTriggerValue(config.kubernetesDefaults.memory) ?? FACTORY_KUBERNETES_DEFAULTS.memory,
|
||||
disk: getTriggerValue(config.kubernetesDefaults.disk) ?? FACTORY_KUBERNETES_DEFAULTS.disk,
|
||||
diskRead:
|
||||
getTriggerValue(config.kubernetesDefaults.diskRead) ?? FACTORY_KUBERNETES_DEFAULTS.diskRead,
|
||||
diskWrite:
|
||||
getTriggerValue(config.kubernetesDefaults.diskWrite) ??
|
||||
FACTORY_KUBERNETES_DEFAULTS.diskWrite,
|
||||
networkIn:
|
||||
getTriggerValue(config.kubernetesDefaults.networkIn) ??
|
||||
FACTORY_KUBERNETES_DEFAULTS.networkIn,
|
||||
networkOut:
|
||||
getTriggerValue(config.kubernetesDefaults.networkOut) ??
|
||||
FACTORY_KUBERNETES_DEFAULTS.networkOut,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.truenasDefaults) {
|
||||
snapshot.trueNASDefaults = {
|
||||
cpu: getTriggerValue(config.truenasDefaults.cpu) ?? FACTORY_TRUENAS_DEFAULTS.cpu,
|
||||
memory: getTriggerValue(config.truenasDefaults.memory) ?? FACTORY_TRUENAS_DEFAULTS.memory,
|
||||
disk: getTriggerValue(config.truenasDefaults.disk) ?? FACTORY_TRUENAS_DEFAULTS.disk,
|
||||
usage: getTriggerValue(config.truenasDefaults.usage) ?? FACTORY_TRUENAS_DEFAULTS.usage,
|
||||
temperature:
|
||||
getTriggerValue(config.truenasDefaults.temperature) ?? FACTORY_TRUENAS_DEFAULTS.temperature,
|
||||
diskRead:
|
||||
getTriggerValue(config.truenasDefaults.diskRead) ?? FACTORY_TRUENAS_DEFAULTS.diskRead,
|
||||
diskWrite:
|
||||
getTriggerValue(config.truenasDefaults.diskWrite) ?? FACTORY_TRUENAS_DEFAULTS.diskWrite,
|
||||
networkIn:
|
||||
getTriggerValue(config.truenasDefaults.networkIn) ?? FACTORY_TRUENAS_DEFAULTS.networkIn,
|
||||
networkOut:
|
||||
getTriggerValue(config.truenasDefaults.networkOut) ?? FACTORY_TRUENAS_DEFAULTS.networkOut,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.truenasDiskDefaults) {
|
||||
snapshot.trueNASDiskDefaults = {
|
||||
temperature:
|
||||
getTriggerValue(config.truenasDiskDefaults.temperature) ??
|
||||
FACTORY_TRUENAS_DISK_DEFAULTS.temperature,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.vmwareDefaults) {
|
||||
snapshot.vmwareDefaults = {
|
||||
cpu: getTriggerValue(config.vmwareDefaults.cpu) ?? FACTORY_VMWARE_DEFAULTS.cpu,
|
||||
memory: getTriggerValue(config.vmwareDefaults.memory) ?? FACTORY_VMWARE_DEFAULTS.memory,
|
||||
disk: getTriggerValue(config.vmwareDefaults.disk) ?? FACTORY_VMWARE_DEFAULTS.disk,
|
||||
usage: getTriggerValue(config.vmwareDefaults.usage) ?? FACTORY_VMWARE_DEFAULTS.usage,
|
||||
diskRead: getTriggerValue(config.vmwareDefaults.diskRead) ?? FACTORY_VMWARE_DEFAULTS.diskRead,
|
||||
diskWrite:
|
||||
getTriggerValue(config.vmwareDefaults.diskWrite) ?? FACTORY_VMWARE_DEFAULTS.diskWrite,
|
||||
networkIn:
|
||||
getTriggerValue(config.vmwareDefaults.networkIn) ?? FACTORY_VMWARE_DEFAULTS.networkIn,
|
||||
networkOut:
|
||||
getTriggerValue(config.vmwareDefaults.networkOut) ?? FACTORY_VMWARE_DEFAULTS.networkOut,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.agentDefaults) {
|
||||
snapshot.agentDefaults = {
|
||||
cpu: getTriggerValue(config.agentDefaults.cpu) ?? FACTORY_AGENT_DEFAULTS.cpu,
|
||||
|
|
@ -332,6 +443,19 @@ export function readAlertsConfigurationSnapshot(config: AlertConfig): AlertsConf
|
|||
storage: config.timeThresholds.storage ?? DEFAULT_DELAY_SECONDS,
|
||||
pbs: config.timeThresholds.pbs ?? DEFAULT_DELAY_SECONDS,
|
||||
agent: config.timeThresholds.agent ?? DEFAULT_DELAY_SECONDS,
|
||||
'k8s-cluster': config.timeThresholds['k8s-cluster'] ?? DEFAULT_DELAY_SECONDS,
|
||||
'k8s-node': config.timeThresholds['k8s-node'] ?? DEFAULT_DELAY_SECONDS,
|
||||
'k8s-deployment': config.timeThresholds['k8s-deployment'] ?? DEFAULT_DELAY_SECONDS,
|
||||
'k8s-namespace': config.timeThresholds['k8s-namespace'] ?? DEFAULT_DELAY_SECONDS,
|
||||
pod: config.timeThresholds.pod ?? DEFAULT_DELAY_SECONDS,
|
||||
'truenas-system': config.timeThresholds['truenas-system'] ?? DEFAULT_DELAY_SECONDS,
|
||||
'truenas-pool': config.timeThresholds['truenas-pool'] ?? DEFAULT_DELAY_SECONDS,
|
||||
'truenas-dataset': config.timeThresholds['truenas-dataset'] ?? DEFAULT_DELAY_SECONDS,
|
||||
'truenas-disk': config.timeThresholds['truenas-disk'] ?? DEFAULT_DELAY_SECONDS,
|
||||
'vmware-host': config.timeThresholds['vmware-host'] ?? DEFAULT_DELAY_SECONDS,
|
||||
'vmware-vm': config.timeThresholds['vmware-vm'] ?? DEFAULT_DELAY_SECONDS,
|
||||
'vmware-datastore': config.timeThresholds['vmware-datastore'] ?? DEFAULT_DELAY_SECONDS,
|
||||
'vmware-network': config.timeThresholds['vmware-network'] ?? DEFAULT_DELAY_SECONDS,
|
||||
};
|
||||
}
|
||||
if (config.metricTimeThresholds) {
|
||||
|
|
@ -403,6 +527,9 @@ export function readAlertsConfigurationSnapshot(config: AlertConfig): AlertsConf
|
|||
snapshot.disableAllDockerHosts = config.disableAllDockerHosts ?? false;
|
||||
snapshot.disableAllDockerServices = config.disableAllDockerServices ?? false;
|
||||
snapshot.disableAllDockerContainers = config.disableAllDockerContainers ?? false;
|
||||
snapshot.disableAllKubernetes = config.disableAllKubernetes ?? false;
|
||||
snapshot.disableAllTrueNAS = config.disableAllTrueNAS ?? false;
|
||||
snapshot.disableAllVMware = config.disableAllVMware ?? false;
|
||||
snapshot.disableAllNodesOffline = config.disableAllNodesOffline ?? false;
|
||||
snapshot.disableAllGuestsOffline = config.disableAllGuestsOffline ?? false;
|
||||
snapshot.disableAllAgentsOffline = config.disableAllAgentsOffline ?? false;
|
||||
|
|
@ -536,6 +663,9 @@ export function buildAlertsConfigurationPayload({
|
|||
disableAllDockerHosts: snapshot.disableAllDockerHosts,
|
||||
disableAllDockerContainers: snapshot.disableAllDockerContainers,
|
||||
disableAllDockerServices: snapshot.disableAllDockerServices,
|
||||
disableAllKubernetes: snapshot.disableAllKubernetes,
|
||||
disableAllTrueNAS: snapshot.disableAllTrueNAS,
|
||||
disableAllVMware: snapshot.disableAllVMware,
|
||||
disableAllNodesOffline: snapshot.disableAllNodesOffline,
|
||||
disableAllGuestsOffline: snapshot.disableAllGuestsOffline,
|
||||
disableAllPBSOffline: snapshot.disableAllPBSOffline,
|
||||
|
|
@ -569,6 +699,39 @@ export function buildAlertsConfigurationPayload({
|
|||
cpu: createHysteresisThreshold(snapshot.pbsDefaults.cpu),
|
||||
memory: createHysteresisThreshold(snapshot.pbsDefaults.memory),
|
||||
},
|
||||
kubernetesDefaults: {
|
||||
cpu: createHysteresisThreshold(snapshot.kubernetesDefaults.cpu),
|
||||
memory: createHysteresisThreshold(snapshot.kubernetesDefaults.memory),
|
||||
disk: createHysteresisThreshold(snapshot.kubernetesDefaults.disk),
|
||||
diskRead: createHysteresisThreshold(snapshot.kubernetesDefaults.diskRead),
|
||||
diskWrite: createHysteresisThreshold(snapshot.kubernetesDefaults.diskWrite),
|
||||
networkIn: createHysteresisThreshold(snapshot.kubernetesDefaults.networkIn),
|
||||
networkOut: createHysteresisThreshold(snapshot.kubernetesDefaults.networkOut),
|
||||
},
|
||||
truenasDefaults: {
|
||||
cpu: createHysteresisThreshold(snapshot.trueNASDefaults.cpu),
|
||||
memory: createHysteresisThreshold(snapshot.trueNASDefaults.memory),
|
||||
disk: createHysteresisThreshold(snapshot.trueNASDefaults.disk),
|
||||
usage: createHysteresisThreshold(snapshot.trueNASDefaults.usage),
|
||||
temperature: createHysteresisThreshold(snapshot.trueNASDefaults.temperature),
|
||||
diskRead: createHysteresisThreshold(snapshot.trueNASDefaults.diskRead),
|
||||
diskWrite: createHysteresisThreshold(snapshot.trueNASDefaults.diskWrite),
|
||||
networkIn: createHysteresisThreshold(snapshot.trueNASDefaults.networkIn),
|
||||
networkOut: createHysteresisThreshold(snapshot.trueNASDefaults.networkOut),
|
||||
},
|
||||
truenasDiskDefaults: {
|
||||
temperature: createHysteresisThreshold(snapshot.trueNASDiskDefaults.temperature),
|
||||
},
|
||||
vmwareDefaults: {
|
||||
cpu: createHysteresisThreshold(snapshot.vmwareDefaults.cpu),
|
||||
memory: createHysteresisThreshold(snapshot.vmwareDefaults.memory),
|
||||
disk: createHysteresisThreshold(snapshot.vmwareDefaults.disk),
|
||||
usage: createHysteresisThreshold(snapshot.vmwareDefaults.usage),
|
||||
diskRead: createHysteresisThreshold(snapshot.vmwareDefaults.diskRead),
|
||||
diskWrite: createHysteresisThreshold(snapshot.vmwareDefaults.diskWrite),
|
||||
networkIn: createHysteresisThreshold(snapshot.vmwareDefaults.networkIn),
|
||||
networkOut: createHysteresisThreshold(snapshot.vmwareDefaults.networkOut),
|
||||
},
|
||||
dockerDefaults: {
|
||||
cpu: createHysteresisThreshold(snapshot.dockerDefaults.cpu),
|
||||
memory: createHysteresisThreshold(snapshot.dockerDefaults.memory),
|
||||
|
|
|
|||
|
|
@ -28,9 +28,17 @@ export function ThresholdsTab(props: ThresholdsTabProps) {
|
|||
setGuestPoweredOffSeverity={props.setGuestPoweredOffSeverity}
|
||||
nodeDefaults={props.nodeDefaults()}
|
||||
pbsDefaults={props.pbsDefaults()}
|
||||
kubernetesDefaults={props.kubernetesDefaults()}
|
||||
trueNASDefaults={props.trueNASDefaults()}
|
||||
trueNASDiskDefaults={props.trueNASDiskDefaults()}
|
||||
vmwareDefaults={props.vmwareDefaults()}
|
||||
agentDefaults={props.agentDefaults()}
|
||||
setNodeDefaults={props.setNodeDefaults}
|
||||
setPBSDefaults={props.setPBSDefaults}
|
||||
setKubernetesDefaults={props.setKubernetesDefaults}
|
||||
setTrueNASDefaults={props.setTrueNASDefaults}
|
||||
setTrueNASDiskDefaults={props.setTrueNASDiskDefaults}
|
||||
setVMwareDefaults={props.setVMwareDefaults}
|
||||
setAgentDefaults={props.setAgentDefaults}
|
||||
dockerDefaults={props.dockerDefaults()}
|
||||
dockerDisableConnectivity={props.dockerDisableConnectivity}
|
||||
|
|
@ -51,6 +59,10 @@ export function ThresholdsTab(props: ThresholdsTabProps) {
|
|||
resetGuestDefaults={props.resetGuestDefaults}
|
||||
resetNodeDefaults={props.resetNodeDefaults}
|
||||
resetPBSDefaults={props.resetPBSDefaults}
|
||||
resetKubernetesDefaults={props.resetKubernetesDefaults}
|
||||
resetTrueNASDefaults={props.resetTrueNASDefaults}
|
||||
resetTrueNASDiskDefaults={props.resetTrueNASDiskDefaults}
|
||||
resetVMwareDefaults={props.resetVMwareDefaults}
|
||||
resetAgentDefaults={props.resetAgentDefaults}
|
||||
resetDockerDefaults={props.resetDockerDefaults}
|
||||
resetDockerIgnoredPrefixes={props.resetDockerIgnoredPrefixes}
|
||||
|
|
@ -58,6 +70,10 @@ export function ThresholdsTab(props: ThresholdsTabProps) {
|
|||
factoryGuestDefaults={props.factoryGuestDefaults}
|
||||
factoryNodeDefaults={props.factoryNodeDefaults}
|
||||
factoryPBSDefaults={props.factoryPBSDefaults}
|
||||
factoryKubernetesDefaults={props.factoryKubernetesDefaults}
|
||||
factoryTrueNASDefaults={props.factoryTrueNASDefaults}
|
||||
factoryTrueNASDiskDefaults={props.factoryTrueNASDiskDefaults}
|
||||
factoryVMwareDefaults={props.factoryVMwareDefaults}
|
||||
factoryAgentDefaults={props.factoryAgentDefaults}
|
||||
factoryDockerDefaults={props.factoryDockerDefaults}
|
||||
factoryStorageDefault={props.factoryStorageDefault}
|
||||
|
|
@ -93,6 +109,12 @@ export function ThresholdsTab(props: ThresholdsTabProps) {
|
|||
setDisableAllDockerServices={props.setDisableAllDockerServices}
|
||||
disableAllDockerContainers={props.disableAllDockerContainers}
|
||||
setDisableAllDockerContainers={props.setDisableAllDockerContainers}
|
||||
disableAllKubernetes={props.disableAllKubernetes}
|
||||
setDisableAllKubernetes={props.setDisableAllKubernetes}
|
||||
disableAllTrueNAS={props.disableAllTrueNAS}
|
||||
setDisableAllTrueNAS={props.setDisableAllTrueNAS}
|
||||
disableAllVMware={props.disableAllVMware}
|
||||
setDisableAllVMware={props.setDisableAllVMware}
|
||||
disableAllNodesOffline={props.disableAllNodesOffline}
|
||||
setDisableAllNodesOffline={props.setDisableAllNodesOffline}
|
||||
disableAllGuestsOffline={props.disableAllGuestsOffline}
|
||||
|
|
|
|||
|
|
@ -192,6 +192,9 @@ describe('useThresholdsTableState', () => {
|
|||
expect(localStorage.getItem('pulse-thresholds-help-dismissed')).toBe('true');
|
||||
|
||||
captured!.handleTabClick('docker');
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/alerts/thresholds/containers');
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/alerts/thresholds/docker');
|
||||
|
||||
captured!.handleTabClick('vmware');
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/alerts/thresholds/vmware');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,19 @@ interface ThresholdsAvailabilityMutationResources {
|
|||
dockerContainersFlat: () => TableResource[];
|
||||
pbsServersWithOverrides: () => TableResource[];
|
||||
storageWithOverrides: () => TableResource[];
|
||||
kubernetesClustersWithOverrides?: () => TableResource[];
|
||||
kubernetesNodesWithOverrides?: () => TableResource[];
|
||||
kubernetesNamespacesWithOverrides?: () => TableResource[];
|
||||
kubernetesDeploymentsWithOverrides?: () => TableResource[];
|
||||
kubernetesPodsWithOverrides?: () => TableResource[];
|
||||
trueNASSystemsWithOverrides?: () => TableResource[];
|
||||
trueNASPoolsWithOverrides?: () => TableResource[];
|
||||
trueNASDatasetsWithOverrides?: () => TableResource[];
|
||||
trueNASDisksWithOverrides?: () => TableResource[];
|
||||
vmwareHostsWithOverrides?: () => TableResource[];
|
||||
vmwareVMsWithOverrides?: () => TableResource[];
|
||||
vmwareDatastoresWithOverrides?: () => TableResource[];
|
||||
vmwareNetworksWithOverrides?: () => TableResource[];
|
||||
}
|
||||
|
||||
interface ThresholdsAvailabilityMutationProps {
|
||||
|
|
@ -42,6 +55,8 @@ export function useThresholdsAvailabilityMutations({
|
|||
removeOverride,
|
||||
}: ThresholdsAvailabilityMutationProps) {
|
||||
const guestLikeResources = () => [...resources.guestsFlat(), ...resources.dockerContainersFlat()];
|
||||
const optionalResources = (accessor?: () => TableResource[]): TableResource[] =>
|
||||
accessor?.() ?? [];
|
||||
|
||||
const clearDockerHostConnectivityAlerts = (resourceId: string) => {
|
||||
if (!props.removeAlerts) return;
|
||||
|
|
@ -60,6 +75,19 @@ export function useThresholdsAvailabilityMutations({
|
|||
...resources.pbsServersWithOverrides(),
|
||||
...resources.agentsWithOverrides(),
|
||||
...resources.agentDisksWithOverrides(),
|
||||
...optionalResources(resources.kubernetesClustersWithOverrides),
|
||||
...optionalResources(resources.kubernetesNodesWithOverrides),
|
||||
...optionalResources(resources.kubernetesNamespacesWithOverrides),
|
||||
...optionalResources(resources.kubernetesDeploymentsWithOverrides),
|
||||
...optionalResources(resources.kubernetesPodsWithOverrides),
|
||||
...optionalResources(resources.trueNASSystemsWithOverrides),
|
||||
...optionalResources(resources.trueNASPoolsWithOverrides),
|
||||
...optionalResources(resources.trueNASDatasetsWithOverrides),
|
||||
...optionalResources(resources.trueNASDisksWithOverrides),
|
||||
...optionalResources(resources.vmwareHostsWithOverrides),
|
||||
...optionalResources(resources.vmwareVMsWithOverrides),
|
||||
...optionalResources(resources.vmwareDatastoresWithOverrides),
|
||||
...optionalResources(resources.vmwareNetworksWithOverrides),
|
||||
].find((entry) => entry.id === resourceId);
|
||||
|
||||
if (
|
||||
|
|
@ -69,7 +97,20 @@ export function useThresholdsAvailabilityMutations({
|
|||
resource.type !== 'pbs' &&
|
||||
resource.type !== 'dockerContainer' &&
|
||||
resource.type !== 'agent' &&
|
||||
resource.type !== 'agentDisk')
|
||||
resource.type !== 'agentDisk' &&
|
||||
resource.type !== 'kubernetesCluster' &&
|
||||
resource.type !== 'kubernetesNode' &&
|
||||
resource.type !== 'kubernetesNamespace' &&
|
||||
resource.type !== 'kubernetesDeployment' &&
|
||||
resource.type !== 'kubernetesPod' &&
|
||||
resource.type !== 'truenasSystem' &&
|
||||
resource.type !== 'truenasPool' &&
|
||||
resource.type !== 'truenasDataset' &&
|
||||
resource.type !== 'truenasDisk' &&
|
||||
resource.type !== 'vmwareHost' &&
|
||||
resource.type !== 'vmwareVm' &&
|
||||
resource.type !== 'vmwareDatastore' &&
|
||||
resource.type !== 'vmwareNetwork')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useThresholdsDockerData } from './useThresholdsDockerData';
|
|||
import { useThresholdsGuestData } from './useThresholdsGuestData';
|
||||
import { useThresholdsHostData } from './useThresholdsHostData';
|
||||
import { useThresholdsInfrastructureData } from './useThresholdsInfrastructureData';
|
||||
import { useThresholdsPlatformData } from './useThresholdsPlatformData';
|
||||
|
||||
export function useThresholdsData(
|
||||
props: ThresholdsTableProps,
|
||||
|
|
@ -14,11 +15,13 @@ export function useThresholdsData(
|
|||
const dockerData = useThresholdsDockerData(inputs);
|
||||
const guestData = useThresholdsGuestData(inputs);
|
||||
const infrastructureData = useThresholdsInfrastructureData(inputs);
|
||||
const platformData = useThresholdsPlatformData(inputs);
|
||||
|
||||
return {
|
||||
...hostData,
|
||||
...dockerData,
|
||||
...guestData,
|
||||
...infrastructureData,
|
||||
...platformData,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,19 @@ interface ThresholdsOverrideMutationResources {
|
|||
pbsServersWithOverrides: Accessor<TableResource[]>;
|
||||
pmgServersWithOverrides: Accessor<TableResource[]>;
|
||||
storageWithOverrides: Accessor<TableResource[]>;
|
||||
kubernetesClustersWithOverrides?: Accessor<TableResource[]>;
|
||||
kubernetesNodesWithOverrides?: Accessor<TableResource[]>;
|
||||
kubernetesNamespacesWithOverrides?: Accessor<TableResource[]>;
|
||||
kubernetesDeploymentsWithOverrides?: Accessor<TableResource[]>;
|
||||
kubernetesPodsWithOverrides?: Accessor<TableResource[]>;
|
||||
trueNASSystemsWithOverrides?: Accessor<TableResource[]>;
|
||||
trueNASPoolsWithOverrides?: Accessor<TableResource[]>;
|
||||
trueNASDatasetsWithOverrides?: Accessor<TableResource[]>;
|
||||
trueNASDisksWithOverrides?: Accessor<TableResource[]>;
|
||||
vmwareHostsWithOverrides?: Accessor<TableResource[]>;
|
||||
vmwareVMsWithOverrides?: Accessor<TableResource[]>;
|
||||
vmwareDatastoresWithOverrides?: Accessor<TableResource[]>;
|
||||
vmwareNetworksWithOverrides?: Accessor<TableResource[]>;
|
||||
}
|
||||
|
||||
interface ThresholdsOverrideMutationProps {
|
||||
|
|
@ -64,6 +77,9 @@ export function useThresholdsOverrideMutations({
|
|||
updateBackupDefaults,
|
||||
updateSnapshotDefaults,
|
||||
}: ThresholdsOverrideMutationProps) {
|
||||
const optionalResources = (accessor?: Accessor<TableResource[]>): TableResource[] =>
|
||||
accessor?.() ?? [];
|
||||
|
||||
const proxmoxResources = () => [
|
||||
...resources.nodesWithOverrides(),
|
||||
...resources.agentsWithOverrides(),
|
||||
|
|
@ -72,6 +88,19 @@ export function useThresholdsOverrideMutations({
|
|||
...resources.pbsServersWithOverrides(),
|
||||
...resources.pmgServersWithOverrides(),
|
||||
...resources.storageWithOverrides(),
|
||||
...optionalResources(resources.kubernetesClustersWithOverrides),
|
||||
...optionalResources(resources.kubernetesNodesWithOverrides),
|
||||
...optionalResources(resources.kubernetesNamespacesWithOverrides),
|
||||
...optionalResources(resources.kubernetesDeploymentsWithOverrides),
|
||||
...optionalResources(resources.kubernetesPodsWithOverrides),
|
||||
...optionalResources(resources.trueNASSystemsWithOverrides),
|
||||
...optionalResources(resources.trueNASPoolsWithOverrides),
|
||||
...optionalResources(resources.trueNASDatasetsWithOverrides),
|
||||
...optionalResources(resources.trueNASDisksWithOverrides),
|
||||
...optionalResources(resources.vmwareHostsWithOverrides),
|
||||
...optionalResources(resources.vmwareVMsWithOverrides),
|
||||
...optionalResources(resources.vmwareDatastoresWithOverrides),
|
||||
...optionalResources(resources.vmwareNetworksWithOverrides),
|
||||
];
|
||||
|
||||
const guestLikeResources = () => [...resources.guestsFlat(), ...resources.dockerContainersFlat()];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,409 @@
|
|||
import { createMemo } from 'solid-js';
|
||||
|
||||
import { getAlertResourceDisplayLabel } from '@/features/alerts/helpers';
|
||||
import { buildKubernetesPageModel } from '@/features/kubernetes/kubernetesPageModel';
|
||||
import { buildTrueNASPageModel } from '@/features/truenas/truenasPageModel';
|
||||
import { buildVmwarePageModel } from '@/features/vmware/vmwarePageModel';
|
||||
import type { Resource } from '@/types/resource';
|
||||
|
||||
import type { Resource as TableResource } from '../tableTypes';
|
||||
import type { Override } from '../types';
|
||||
import type { ThresholdsDataInputs } from '../thresholdsResourceModel';
|
||||
import {
|
||||
createOverridesMap,
|
||||
findOverrideByCandidates,
|
||||
hasThresholdDiff,
|
||||
readString,
|
||||
uniqueIds,
|
||||
} from '../thresholdsResourceModel';
|
||||
|
||||
type AlertPlatformResourceType =
|
||||
| 'kubernetesCluster'
|
||||
| 'kubernetesNode'
|
||||
| 'kubernetesNamespace'
|
||||
| 'kubernetesDeployment'
|
||||
| 'kubernetesPod'
|
||||
| 'truenasSystem'
|
||||
| 'truenasPool'
|
||||
| 'truenasDataset'
|
||||
| 'truenasDisk'
|
||||
| 'vmwareHost'
|
||||
| 'vmwareVm'
|
||||
| 'vmwareDatastore'
|
||||
| 'vmwareNetwork';
|
||||
|
||||
const resourceAlertIdCandidates = (resource: Resource): string[] =>
|
||||
uniqueIds(
|
||||
resource.id,
|
||||
resource.metricsTarget?.resourceId,
|
||||
resource.discoveryTarget?.resourceId,
|
||||
resource.platformId,
|
||||
);
|
||||
|
||||
const resourceAlertActionId = (resource: Resource): string =>
|
||||
resourceAlertIdCandidates(resource)[0] || resource.id;
|
||||
|
||||
const searchResource = (resource: TableResource, search: string): boolean => {
|
||||
if (!search) return true;
|
||||
return [
|
||||
resource.name,
|
||||
resource.displayName,
|
||||
resource.rawName,
|
||||
resource.node,
|
||||
resource.instance,
|
||||
resource.resourceType,
|
||||
].some((value) => typeof value === 'string' && value.toLowerCase().includes(search));
|
||||
};
|
||||
|
||||
const metricPercent = (
|
||||
resource: Resource,
|
||||
metric: 'cpu' | 'memory' | 'disk',
|
||||
): number | undefined => {
|
||||
const value = resource[metric]?.current;
|
||||
return typeof value === 'number' ? value : undefined;
|
||||
};
|
||||
|
||||
const kubernetesClusterName = (resource: Resource): string =>
|
||||
readString(resource.kubernetes?.clusterName) ||
|
||||
readString(resource.kubernetes?.context) ||
|
||||
readString(resource.kubernetes?.clusterId) ||
|
||||
readString(resource.clusterId) ||
|
||||
'';
|
||||
|
||||
const kubernetesNamespace = (resource: Resource): string =>
|
||||
readString(resource.kubernetes?.namespace) || '';
|
||||
|
||||
const trueNASSystemName = (resource: Resource): string =>
|
||||
readString(resource.truenas?.hostname) || readString(resource.parentName) || '';
|
||||
|
||||
const vmwareConnectionName = (resource: Resource): string =>
|
||||
readString(resource.vmware?.connectionName) ||
|
||||
readString(resource.vmware?.vcenterHost) ||
|
||||
'vSphere';
|
||||
|
||||
const vmwareNodeName = (resource: Resource): string =>
|
||||
readString(resource.vmware?.runtimeHostName) ||
|
||||
readString(resource.parentName) ||
|
||||
readString(resource.vmware?.clusterName) ||
|
||||
readString(resource.vmware?.datacenterName) ||
|
||||
readString(resource.vmware?.vcenterHost) ||
|
||||
'';
|
||||
|
||||
const toTableResource = ({
|
||||
resource,
|
||||
type,
|
||||
resourceType,
|
||||
defaults,
|
||||
candidates = resourceAlertIdCandidates(resource),
|
||||
overridesMap,
|
||||
node,
|
||||
instance,
|
||||
}: {
|
||||
resource: Resource;
|
||||
type: AlertPlatformResourceType;
|
||||
resourceType: string;
|
||||
defaults: Record<string, number | undefined>;
|
||||
candidates?: string[];
|
||||
overridesMap: Map<string, Override>;
|
||||
node?: string;
|
||||
instance?: string;
|
||||
}): TableResource => {
|
||||
const override = findOverrideByCandidates(overridesMap, candidates);
|
||||
const hasCustomThresholds = hasThresholdDiff(override, defaults);
|
||||
const note = typeof override?.note === 'string' ? override.note : undefined;
|
||||
|
||||
return {
|
||||
id: override?.id || candidates[0] || resource.id,
|
||||
name: getAlertResourceDisplayLabel(resource),
|
||||
displayName: getAlertResourceDisplayLabel(resource),
|
||||
rawName: resource.name,
|
||||
type,
|
||||
resourceType,
|
||||
node,
|
||||
instance,
|
||||
status: resource.status,
|
||||
cpu: metricPercent(resource, 'cpu'),
|
||||
memory: metricPercent(resource, 'memory'),
|
||||
hasOverride: hasCustomThresholds || Boolean(override?.disabled) || Boolean(note && note.trim()),
|
||||
disabled: override?.disabled || false,
|
||||
thresholds: override?.thresholds || {},
|
||||
defaults,
|
||||
clusterName: kubernetesClusterName(resource) || undefined,
|
||||
note,
|
||||
};
|
||||
};
|
||||
|
||||
export function useThresholdsPlatformData(inputs: ThresholdsDataInputs) {
|
||||
const { props, editingId, searchTerm } = inputs;
|
||||
|
||||
const allResources = () => props.allResources ?? [];
|
||||
const kubernetesDefaults = () => props.kubernetesDefaults ?? {};
|
||||
const trueNASDefaults = () => props.trueNASDefaults ?? {};
|
||||
const trueNASDiskDefaults = () => props.trueNASDiskDefaults ?? {};
|
||||
const vmwareDefaults = () => props.vmwareDefaults ?? {};
|
||||
|
||||
const kubernetesClustersWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildKubernetesPageModel(allResources())
|
||||
.clusters.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'kubernetesCluster',
|
||||
resourceType: 'Kubernetes Cluster',
|
||||
defaults: kubernetesDefaults(),
|
||||
overridesMap,
|
||||
node: kubernetesClusterName(resource),
|
||||
instance: kubernetesClusterName(resource),
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
const kubernetesNodesWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildKubernetesPageModel(allResources())
|
||||
.nodes.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'kubernetesNode',
|
||||
resourceType: 'Kubernetes Node',
|
||||
defaults: kubernetesDefaults(),
|
||||
overridesMap,
|
||||
node: readString(resource.kubernetes?.nodeName) || resource.name,
|
||||
instance: kubernetesClusterName(resource),
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
const kubernetesNamespacesWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildKubernetesPageModel(allResources())
|
||||
.namespaces.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'kubernetesNamespace',
|
||||
resourceType: 'Kubernetes Namespace',
|
||||
defaults: {},
|
||||
overridesMap,
|
||||
node: kubernetesClusterName(resource),
|
||||
instance: kubernetesClusterName(resource),
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
const kubernetesDeploymentsWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildKubernetesPageModel(allResources())
|
||||
.deployments.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'kubernetesDeployment',
|
||||
resourceType: 'Kubernetes Deployment',
|
||||
defaults: kubernetesDefaults(),
|
||||
overridesMap,
|
||||
node: kubernetesNamespace(resource),
|
||||
instance: kubernetesClusterName(resource),
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
const kubernetesPodsWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildKubernetesPageModel(allResources())
|
||||
.pods.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'kubernetesPod',
|
||||
resourceType: 'Kubernetes Pod',
|
||||
defaults: kubernetesDefaults(),
|
||||
overridesMap,
|
||||
node: readString(resource.kubernetes?.nodeName) || kubernetesNamespace(resource),
|
||||
instance: kubernetesClusterName(resource),
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
const trueNASSystemsWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildTrueNASPageModel(allResources())
|
||||
.systems.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'truenasSystem',
|
||||
resourceType: 'TrueNAS System',
|
||||
defaults: trueNASDefaults(),
|
||||
overridesMap,
|
||||
node: trueNASSystemName(resource),
|
||||
instance: 'TrueNAS',
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
const trueNASPoolsWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildTrueNASPageModel(allResources())
|
||||
.pools.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'truenasPool',
|
||||
resourceType: 'TrueNAS Pool',
|
||||
defaults: { usage: trueNASDefaults().usage ?? 85 },
|
||||
overridesMap,
|
||||
node: trueNASSystemName(resource),
|
||||
instance: 'TrueNAS',
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
const trueNASDatasetsWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildTrueNASPageModel(allResources())
|
||||
.datasets.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'truenasDataset',
|
||||
resourceType: 'TrueNAS Dataset',
|
||||
defaults: { usage: trueNASDefaults().usage ?? 85 },
|
||||
overridesMap,
|
||||
node: trueNASSystemName(resource),
|
||||
instance: 'TrueNAS',
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
const trueNASDisksWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildTrueNASPageModel(allResources())
|
||||
.disks.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'truenasDisk',
|
||||
resourceType: 'TrueNAS Disk',
|
||||
defaults: trueNASDiskDefaults(),
|
||||
overridesMap,
|
||||
node: trueNASSystemName(resource),
|
||||
instance: readString(resource.physicalDisk?.diskType) || 'TrueNAS',
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
const vmwareHostsWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildVmwarePageModel(allResources())
|
||||
.hosts.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'vmwareHost',
|
||||
resourceType: 'vSphere Host',
|
||||
defaults: vmwareDefaults(),
|
||||
overridesMap,
|
||||
node: vmwareNodeName(resource),
|
||||
instance: vmwareConnectionName(resource),
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
const vmwareVMsWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildVmwarePageModel(allResources())
|
||||
.vms.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'vmwareVm',
|
||||
resourceType: 'vSphere VM',
|
||||
defaults: vmwareDefaults(),
|
||||
overridesMap,
|
||||
node: vmwareNodeName(resource),
|
||||
instance: vmwareConnectionName(resource),
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
const vmwareDatastoresWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildVmwarePageModel(allResources())
|
||||
.datastores.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'vmwareDatastore',
|
||||
resourceType: 'vSphere Datastore',
|
||||
defaults: { usage: vmwareDefaults().usage ?? 85 },
|
||||
overridesMap,
|
||||
node: vmwareNodeName(resource),
|
||||
instance: vmwareConnectionName(resource),
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
const vmwareNetworksWithOverrides = createMemo<TableResource[]>((prev = []) => {
|
||||
if (editingId()) return prev;
|
||||
const search = searchTerm().toLowerCase();
|
||||
const overridesMap = createOverridesMap(props.overrides());
|
||||
return buildVmwarePageModel(allResources())
|
||||
.networks.map((resource) =>
|
||||
toTableResource({
|
||||
resource,
|
||||
type: 'vmwareNetwork',
|
||||
resourceType: 'vSphere Network',
|
||||
defaults: {},
|
||||
overridesMap,
|
||||
node: vmwareNodeName(resource),
|
||||
instance: vmwareConnectionName(resource),
|
||||
}),
|
||||
)
|
||||
.filter((resource) => searchResource(resource, search));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
kubernetesClustersWithOverrides,
|
||||
kubernetesNodesWithOverrides,
|
||||
kubernetesNamespacesWithOverrides,
|
||||
kubernetesDeploymentsWithOverrides,
|
||||
kubernetesPodsWithOverrides,
|
||||
trueNASSystemsWithOverrides,
|
||||
trueNASPoolsWithOverrides,
|
||||
trueNASDatasetsWithOverrides,
|
||||
trueNASDisksWithOverrides,
|
||||
vmwareHostsWithOverrides,
|
||||
vmwareVMsWithOverrides,
|
||||
vmwareDatastoresWithOverrides,
|
||||
vmwareNetworksWithOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
export { resourceAlertIdCandidates, resourceAlertActionId };
|
||||
|
|
@ -54,11 +54,24 @@ interface ThresholdsSummaryItem {
|
|||
| 'dockerContainers'
|
||||
| 'dockerHosts'
|
||||
| 'guests'
|
||||
| 'kubernetesClusters'
|
||||
| 'kubernetesDeployments'
|
||||
| 'kubernetesNamespaces'
|
||||
| 'kubernetesNodes'
|
||||
| 'kubernetesPods'
|
||||
| 'nodes'
|
||||
| 'pbs'
|
||||
| 'pmg'
|
||||
| 'snapshots'
|
||||
| 'storage';
|
||||
| 'storage'
|
||||
| 'trueNASDatasets'
|
||||
| 'trueNASDisks'
|
||||
| 'trueNASPools'
|
||||
| 'trueNASSystems'
|
||||
| 'vmwareDatastores'
|
||||
| 'vmwareHosts'
|
||||
| 'vmwareNetworks'
|
||||
| 'vmwareVMs';
|
||||
label: string;
|
||||
overrides: number;
|
||||
tab: ThresholdsActiveTab;
|
||||
|
|
@ -90,6 +103,7 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
};
|
||||
|
||||
const [searchTerm, setSearchTerm] = createSignal('');
|
||||
const [overrideFilter, setOverrideFilter] = createSignal('all');
|
||||
const [editingId, setEditingId] = createSignal<string | null>(null);
|
||||
const [editingThresholds, setEditingThresholds] = createSignal<
|
||||
Record<string, number | undefined>
|
||||
|
|
@ -98,7 +112,7 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
const [bulkEditIds, setBulkEditIds] = createSignal<string[]>([]);
|
||||
const [bulkEditColumns, setBulkEditColumns] = createSignal<string[]>([]);
|
||||
const [isBulkEditDialogOpen, setIsBulkEditDialogOpen] = createSignal(false);
|
||||
const [activeTab, setActiveTab] = createSignal<ThresholdsActiveTab>('infrastructure');
|
||||
const [activeTab, setActiveTab] = createSignal<ThresholdsActiveTab>('proxmox');
|
||||
const [dockerIgnoredInput, setDockerIgnoredInput] = createSignal(
|
||||
props.dockerIgnoredPrefixes().join('\n'),
|
||||
);
|
||||
|
|
@ -132,10 +146,17 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
|
||||
const getActiveTabFromRoute = (): ThresholdsActiveTab => {
|
||||
const path = location.pathname;
|
||||
if (path.includes('/thresholds/containers')) return 'docker';
|
||||
if (path.includes('/thresholds/systems') || path.includes('/thresholds/agents')) return 'systems';
|
||||
if (path.includes('/thresholds/mail-gateway')) return 'pmg';
|
||||
return 'infrastructure';
|
||||
if (path.includes('/thresholds/containers') || path.includes('/thresholds/docker'))
|
||||
return 'docker';
|
||||
if (path.includes('/thresholds/kubernetes')) return 'kubernetes';
|
||||
if (path.includes('/thresholds/truenas')) return 'truenas';
|
||||
if (path.includes('/thresholds/vmware') || path.includes('/thresholds/vsphere'))
|
||||
return 'vmware';
|
||||
if (path.includes('/thresholds/pbs')) return 'pbs';
|
||||
if (path.includes('/thresholds/systems') || path.includes('/thresholds/agents'))
|
||||
return 'systems';
|
||||
if (path.includes('/thresholds/mail-gateway') || path.includes('/thresholds/pmg')) return 'pmg';
|
||||
return 'proxmox';
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
|
|
@ -147,12 +168,22 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
|
||||
createEffect(() => {
|
||||
if (location.pathname === '/alerts/thresholds') {
|
||||
navigate('/alerts/thresholds/infrastructure', { replace: true });
|
||||
navigate('/alerts/thresholds/proxmox', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.pathname === '/alerts/thresholds/proxmox') {
|
||||
navigate('/alerts/thresholds/infrastructure', { replace: true });
|
||||
if (location.pathname === '/alerts/thresholds/infrastructure') {
|
||||
navigate('/alerts/thresholds/proxmox', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.pathname === '/alerts/thresholds/containers') {
|
||||
navigate('/alerts/thresholds/docker', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.pathname === '/alerts/thresholds/mail-gateway') {
|
||||
navigate('/alerts/thresholds/pmg', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -163,9 +194,13 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
|
||||
const handleTabClick = (tab: ThresholdsActiveTab) => {
|
||||
const tabRoutes: Record<ThresholdsActiveTab, string> = {
|
||||
infrastructure: '/alerts/thresholds/infrastructure',
|
||||
docker: '/alerts/thresholds/containers',
|
||||
pmg: '/alerts/thresholds/mail-gateway',
|
||||
proxmox: '/alerts/thresholds/proxmox',
|
||||
docker: '/alerts/thresholds/docker',
|
||||
kubernetes: '/alerts/thresholds/kubernetes',
|
||||
truenas: '/alerts/thresholds/truenas',
|
||||
vmware: '/alerts/thresholds/vmware',
|
||||
pbs: '/alerts/thresholds/pbs',
|
||||
pmg: '/alerts/thresholds/pmg',
|
||||
systems: '/alerts/thresholds/systems',
|
||||
};
|
||||
navigate(tabRoutes[tab]);
|
||||
|
|
@ -194,26 +229,121 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
};
|
||||
|
||||
const {
|
||||
nodesWithOverrides,
|
||||
agentsWithOverrides,
|
||||
agentDisksWithOverrides,
|
||||
agentDisksGroupedByAgent,
|
||||
nodesWithOverrides: rawNodesWithOverrides,
|
||||
agentsWithOverrides: rawAgentsWithOverrides,
|
||||
agentDisksWithOverrides: rawAgentDisksWithOverrides,
|
||||
agentDisksGroupedByAgent: rawAgentDisksGroupedByAgent,
|
||||
agentGroupHeaderMeta,
|
||||
dockerHostsWithOverrides,
|
||||
dockerContainersGroupedByHost,
|
||||
dockerContainersFlat,
|
||||
dockerHostsWithOverrides: rawDockerHostsWithOverrides,
|
||||
dockerContainersGroupedByHost: rawDockerContainersGroupedByHost,
|
||||
dockerContainersFlat: rawDockerContainersFlat,
|
||||
totalDockerContainers,
|
||||
dockerHostGroupMeta,
|
||||
guestsGroupedByNode,
|
||||
guestsFlat,
|
||||
guestsGroupedByNode: rawGuestsGroupedByNode,
|
||||
guestsFlat: rawGuestsFlat,
|
||||
guestGroupHeaderMeta,
|
||||
pbsServersWithOverrides,
|
||||
kubernetesClustersWithOverrides: rawKubernetesClustersWithOverrides = () => [],
|
||||
kubernetesDeploymentsWithOverrides: rawKubernetesDeploymentsWithOverrides = () => [],
|
||||
kubernetesNamespacesWithOverrides: rawKubernetesNamespacesWithOverrides = () => [],
|
||||
kubernetesNodesWithOverrides: rawKubernetesNodesWithOverrides = () => [],
|
||||
kubernetesPodsWithOverrides: rawKubernetesPodsWithOverrides = () => [],
|
||||
pbsServersWithOverrides: rawPbsServersWithOverrides,
|
||||
pmgGlobalDefaults,
|
||||
pmgServersWithOverrides,
|
||||
storageWithOverrides,
|
||||
storageGroupedByNode,
|
||||
pmgServersWithOverrides: rawPmgServersWithOverrides,
|
||||
storageWithOverrides: rawStorageWithOverrides,
|
||||
storageGroupedByNode: rawStorageGroupedByNode,
|
||||
trueNASDatasetsWithOverrides: rawTrueNASDatasetsWithOverrides = () => [],
|
||||
trueNASDisksWithOverrides: rawTrueNASDisksWithOverrides = () => [],
|
||||
trueNASPoolsWithOverrides: rawTrueNASPoolsWithOverrides = () => [],
|
||||
trueNASSystemsWithOverrides: rawTrueNASSystemsWithOverrides = () => [],
|
||||
vmwareDatastoresWithOverrides: rawVmwareDatastoresWithOverrides = () => [],
|
||||
vmwareHostsWithOverrides: rawVmwareHostsWithOverrides = () => [],
|
||||
vmwareNetworksWithOverrides: rawVmwareNetworksWithOverrides = () => [],
|
||||
vmwareVMsWithOverrides: rawVmwareVMsWithOverrides = () => [],
|
||||
} = useThresholdsData(props, editingId, searchTerm);
|
||||
|
||||
const resourceMatchesOverrideFilter = (resource: TableResource): boolean => {
|
||||
const filter = overrideFilter();
|
||||
if (filter === 'custom') {
|
||||
return Boolean(
|
||||
resource.hasOverride || resource.disableConnectivity || resource.poweredOffSeverity,
|
||||
);
|
||||
}
|
||||
if (filter === 'disabled') {
|
||||
return Boolean(resource.disabled || resource.disableConnectivity);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const filterResources = (resources: TableResource[]): TableResource[] =>
|
||||
resources.filter(resourceMatchesOverrideFilter);
|
||||
|
||||
const filterGroupedResources = (
|
||||
groups: Record<string, TableResource[]>,
|
||||
): Record<string, TableResource[]> => {
|
||||
const filtered: Record<string, TableResource[]> = {};
|
||||
Object.entries(groups).forEach(([groupKey, resources]) => {
|
||||
const visibleResources = filterResources(resources);
|
||||
if (visibleResources.length > 0) {
|
||||
filtered[groupKey] = visibleResources;
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const nodesWithOverrides = createMemo(() => filterResources(rawNodesWithOverrides()));
|
||||
const agentsWithOverrides = createMemo(() => filterResources(rawAgentsWithOverrides()));
|
||||
const agentDisksWithOverrides = createMemo(() => filterResources(rawAgentDisksWithOverrides()));
|
||||
const agentDisksGroupedByAgent = createMemo(() =>
|
||||
filterGroupedResources(rawAgentDisksGroupedByAgent()),
|
||||
);
|
||||
const dockerHostsWithOverrides = createMemo(() => filterResources(rawDockerHostsWithOverrides()));
|
||||
const dockerContainersGroupedByHost = createMemo(() =>
|
||||
filterGroupedResources(rawDockerContainersGroupedByHost()),
|
||||
);
|
||||
const dockerContainersFlat = createMemo(() => filterResources(rawDockerContainersFlat()));
|
||||
const guestsGroupedByNode = createMemo(() => filterGroupedResources(rawGuestsGroupedByNode()));
|
||||
const guestsFlat = createMemo(() => filterResources(rawGuestsFlat()));
|
||||
const kubernetesClustersWithOverrides = createMemo(() =>
|
||||
filterResources(rawKubernetesClustersWithOverrides()),
|
||||
);
|
||||
const kubernetesDeploymentsWithOverrides = createMemo(() =>
|
||||
filterResources(rawKubernetesDeploymentsWithOverrides()),
|
||||
);
|
||||
const kubernetesNamespacesWithOverrides = createMemo(() =>
|
||||
filterResources(rawKubernetesNamespacesWithOverrides()),
|
||||
);
|
||||
const kubernetesNodesWithOverrides = createMemo(() =>
|
||||
filterResources(rawKubernetesNodesWithOverrides()),
|
||||
);
|
||||
const kubernetesPodsWithOverrides = createMemo(() =>
|
||||
filterResources(rawKubernetesPodsWithOverrides()),
|
||||
);
|
||||
const pbsServersWithOverrides = createMemo(() => filterResources(rawPbsServersWithOverrides()));
|
||||
const pmgServersWithOverrides = createMemo(() => filterResources(rawPmgServersWithOverrides()));
|
||||
const storageWithOverrides = createMemo(() => filterResources(rawStorageWithOverrides()));
|
||||
const storageGroupedByNode = createMemo(() => filterGroupedResources(rawStorageGroupedByNode()));
|
||||
const trueNASDatasetsWithOverrides = createMemo(() =>
|
||||
filterResources(rawTrueNASDatasetsWithOverrides()),
|
||||
);
|
||||
const trueNASDisksWithOverrides = createMemo(() =>
|
||||
filterResources(rawTrueNASDisksWithOverrides()),
|
||||
);
|
||||
const trueNASPoolsWithOverrides = createMemo(() =>
|
||||
filterResources(rawTrueNASPoolsWithOverrides()),
|
||||
);
|
||||
const trueNASSystemsWithOverrides = createMemo(() =>
|
||||
filterResources(rawTrueNASSystemsWithOverrides()),
|
||||
);
|
||||
const vmwareDatastoresWithOverrides = createMemo(() =>
|
||||
filterResources(rawVmwareDatastoresWithOverrides()),
|
||||
);
|
||||
const vmwareHostsWithOverrides = createMemo(() => filterResources(rawVmwareHostsWithOverrides()));
|
||||
const vmwareNetworksWithOverrides = createMemo(() =>
|
||||
filterResources(rawVmwareNetworksWithOverrides()),
|
||||
);
|
||||
const vmwareVMsWithOverrides = createMemo(() => filterResources(rawVmwareVMsWithOverrides()));
|
||||
|
||||
const {
|
||||
backupDefaultsRecord,
|
||||
backupFactoryConfig,
|
||||
|
|
@ -299,7 +429,7 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
key: 'nodes',
|
||||
label: 'Virtualization Hosts',
|
||||
overrides: countOverrides(nodesWithOverrides()),
|
||||
tab: 'infrastructure',
|
||||
tab: 'proxmox',
|
||||
total: props.nodes?.length ?? 0,
|
||||
},
|
||||
{
|
||||
|
|
@ -327,28 +457,28 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
key: 'storage',
|
||||
label: 'Storage',
|
||||
overrides: countOverrides(storageWithOverrides()),
|
||||
tab: 'infrastructure',
|
||||
tab: 'proxmox',
|
||||
total: props.storage?.length ?? 0,
|
||||
},
|
||||
{
|
||||
key: 'backups',
|
||||
label: 'Recovery',
|
||||
overrides: backupOverridesCount(),
|
||||
tab: 'infrastructure',
|
||||
tab: 'proxmox',
|
||||
total: 1,
|
||||
},
|
||||
{
|
||||
key: 'snapshots',
|
||||
label: 'Snapshot Age',
|
||||
overrides: snapshotOverridesCount(),
|
||||
tab: 'infrastructure',
|
||||
tab: 'proxmox',
|
||||
total: 1,
|
||||
},
|
||||
{
|
||||
key: 'pbs',
|
||||
label: 'PBS Servers',
|
||||
overrides: countOverrides(pbsServersWithOverrides()),
|
||||
tab: 'infrastructure',
|
||||
tab: 'pbs',
|
||||
total: props.pbsInstances?.length ?? 0,
|
||||
},
|
||||
{
|
||||
|
|
@ -369,9 +499,100 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
key: 'guests',
|
||||
label: 'VMs & Containers',
|
||||
overrides: countOverrides(guestsFlat()),
|
||||
tab: 'infrastructure',
|
||||
tab: 'proxmox',
|
||||
total: props.allGuests?.()?.length ?? 0,
|
||||
},
|
||||
{
|
||||
key: 'kubernetesClusters',
|
||||
label: 'Kubernetes Clusters',
|
||||
overrides: countOverrides(kubernetesClustersWithOverrides()),
|
||||
tab: 'kubernetes',
|
||||
total: kubernetesClustersWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'kubernetesNodes',
|
||||
label: 'Kubernetes Nodes',
|
||||
overrides: countOverrides(kubernetesNodesWithOverrides()),
|
||||
tab: 'kubernetes',
|
||||
total: kubernetesNodesWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'kubernetesNamespaces',
|
||||
label: 'Namespaces',
|
||||
overrides: countOverrides(kubernetesNamespacesWithOverrides()),
|
||||
tab: 'kubernetes',
|
||||
total: kubernetesNamespacesWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'kubernetesDeployments',
|
||||
label: 'Deployments',
|
||||
overrides: countOverrides(kubernetesDeploymentsWithOverrides()),
|
||||
tab: 'kubernetes',
|
||||
total: kubernetesDeploymentsWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'kubernetesPods',
|
||||
label: 'Pods',
|
||||
overrides: countOverrides(kubernetesPodsWithOverrides()),
|
||||
tab: 'kubernetes',
|
||||
total: kubernetesPodsWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'trueNASSystems',
|
||||
label: 'TrueNAS Systems',
|
||||
overrides: countOverrides(trueNASSystemsWithOverrides()),
|
||||
tab: 'truenas',
|
||||
total: trueNASSystemsWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'trueNASPools',
|
||||
label: 'Pools',
|
||||
overrides: countOverrides(trueNASPoolsWithOverrides()),
|
||||
tab: 'truenas',
|
||||
total: trueNASPoolsWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'trueNASDatasets',
|
||||
label: 'Datasets',
|
||||
overrides: countOverrides(trueNASDatasetsWithOverrides()),
|
||||
tab: 'truenas',
|
||||
total: trueNASDatasetsWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'trueNASDisks',
|
||||
label: 'Disks',
|
||||
overrides: countOverrides(trueNASDisksWithOverrides()),
|
||||
tab: 'truenas',
|
||||
total: trueNASDisksWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'vmwareHosts',
|
||||
label: 'vSphere Hosts',
|
||||
overrides: countOverrides(vmwareHostsWithOverrides()),
|
||||
tab: 'vmware',
|
||||
total: vmwareHostsWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'vmwareVMs',
|
||||
label: 'vSphere VMs',
|
||||
overrides: countOverrides(vmwareVMsWithOverrides()),
|
||||
tab: 'vmware',
|
||||
total: vmwareVMsWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'vmwareDatastores',
|
||||
label: 'Datastores',
|
||||
overrides: countOverrides(vmwareDatastoresWithOverrides()),
|
||||
tab: 'vmware',
|
||||
total: vmwareDatastoresWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'vmwareNetworks',
|
||||
label: 'Networks',
|
||||
overrides: countOverrides(vmwareNetworksWithOverrides()),
|
||||
tab: 'vmware',
|
||||
total: vmwareNetworksWithOverrides().length,
|
||||
},
|
||||
];
|
||||
|
||||
return items
|
||||
|
|
@ -420,6 +641,19 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
pbsServersWithOverrides,
|
||||
pmgServersWithOverrides,
|
||||
storageWithOverrides,
|
||||
kubernetesClustersWithOverrides,
|
||||
kubernetesNodesWithOverrides,
|
||||
kubernetesNamespacesWithOverrides,
|
||||
kubernetesDeploymentsWithOverrides,
|
||||
kubernetesPodsWithOverrides,
|
||||
trueNASSystemsWithOverrides,
|
||||
trueNASPoolsWithOverrides,
|
||||
trueNASDatasetsWithOverrides,
|
||||
trueNASDisksWithOverrides,
|
||||
vmwareHostsWithOverrides,
|
||||
vmwareVMsWithOverrides,
|
||||
vmwareDatastoresWithOverrides,
|
||||
vmwareNetworksWithOverrides,
|
||||
},
|
||||
editingThresholds,
|
||||
editingNote,
|
||||
|
|
@ -441,6 +675,19 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
dockerContainersFlat,
|
||||
pbsServersWithOverrides,
|
||||
storageWithOverrides,
|
||||
kubernetesClustersWithOverrides,
|
||||
kubernetesNodesWithOverrides,
|
||||
kubernetesNamespacesWithOverrides,
|
||||
kubernetesDeploymentsWithOverrides,
|
||||
kubernetesPodsWithOverrides,
|
||||
trueNASSystemsWithOverrides,
|
||||
trueNASPoolsWithOverrides,
|
||||
trueNASDatasetsWithOverrides,
|
||||
trueNASDisksWithOverrides,
|
||||
vmwareHostsWithOverrides,
|
||||
vmwareVMsWithOverrides,
|
||||
vmwareDatastoresWithOverrides,
|
||||
vmwareNetworksWithOverrides,
|
||||
},
|
||||
removeOverride,
|
||||
});
|
||||
|
|
@ -459,7 +706,25 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
};
|
||||
|
||||
const updateMetricDelay = (
|
||||
typeKey: 'guest' | 'node' | 'storage' | 'pbs' | 'agent',
|
||||
typeKey:
|
||||
| 'guest'
|
||||
| 'node'
|
||||
| 'storage'
|
||||
| 'pbs'
|
||||
| 'agent'
|
||||
| 'k8s-cluster'
|
||||
| 'k8s-node'
|
||||
| 'k8s-deployment'
|
||||
| 'k8s-namespace'
|
||||
| 'pod'
|
||||
| 'truenas-system'
|
||||
| 'truenas-pool'
|
||||
| 'truenas-dataset'
|
||||
| 'truenas-disk'
|
||||
| 'vmware-host'
|
||||
| 'vmware-vm'
|
||||
| 'vmware-datastore'
|
||||
| 'vmware-network',
|
||||
metricKey: string,
|
||||
value: number | null,
|
||||
) => {
|
||||
|
|
@ -554,6 +819,11 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
hasDockerSpecificControls,
|
||||
isBulkEditDialogOpen,
|
||||
isCollapsed,
|
||||
kubernetesClustersWithOverrides,
|
||||
kubernetesDeploymentsWithOverrides,
|
||||
kubernetesNamespacesWithOverrides,
|
||||
kubernetesNodesWithOverrides,
|
||||
kubernetesPodsWithOverrides,
|
||||
nodesWithOverrides,
|
||||
NODE_THRESHOLDS_FILTER_EMPTY_STATE,
|
||||
pbsServersWithOverrides,
|
||||
|
|
@ -565,6 +835,7 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
pmgServersWithOverrides,
|
||||
registerSection,
|
||||
removeOverride,
|
||||
overrideFilter,
|
||||
saveEdit,
|
||||
searchTerm,
|
||||
sectionTitles,
|
||||
|
|
@ -580,6 +851,7 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
setEditingThresholds,
|
||||
setHasUnsavedChanges: props.setHasUnsavedChanges,
|
||||
setIsBulkEditDialogOpen,
|
||||
setOverrideFilter,
|
||||
setOfflineState,
|
||||
setPMGGlobalDefaults,
|
||||
SNAPSHOT_THRESHOLDS_EMPTY_STATE,
|
||||
|
|
@ -593,6 +865,14 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
STORAGE_THRESHOLDS_EMPTY_STATE,
|
||||
STORAGE_THRESHOLDS_FILTER_EMPTY_STATE,
|
||||
summaryItems,
|
||||
trueNASDatasetsWithOverrides,
|
||||
trueNASDisksWithOverrides,
|
||||
trueNASPoolsWithOverrides,
|
||||
trueNASSystemsWithOverrides,
|
||||
vmwareDatastoresWithOverrides,
|
||||
vmwareHostsWithOverrides,
|
||||
vmwareNetworksWithOverrides,
|
||||
vmwareVMsWithOverrides,
|
||||
toggleBackup,
|
||||
toggleDisabled,
|
||||
toggleNodeConnectivity,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,15 @@ import type { ResourcePolicy } from '@/types/resource';
|
|||
import type { BackupAlertConfig, SnapshotAlertConfig } from '@/types/alerts';
|
||||
import type { AlertResourceThresholdMap } from '@/components/Alerts/alertResourceTableModel';
|
||||
|
||||
export type ThresholdsActiveTab = 'infrastructure' | 'pmg' | 'systems' | 'docker';
|
||||
export type ThresholdsActiveTab =
|
||||
| 'proxmox'
|
||||
| 'docker'
|
||||
| 'kubernetes'
|
||||
| 'truenas'
|
||||
| 'vmware'
|
||||
| 'pbs'
|
||||
| 'pmg'
|
||||
| 'systems';
|
||||
|
||||
export interface Resource {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,25 @@ import type { Accessor } from 'solid-js';
|
|||
|
||||
import type { ThresholdsTableProps } from './types';
|
||||
|
||||
export interface ThresholdsTabProps
|
||||
extends Omit<
|
||||
ThresholdsTableProps,
|
||||
'guestDefaults' | 'nodeDefaults' | 'pbsDefaults' | 'agentDefaults' | 'dockerDefaults'
|
||||
> {
|
||||
export interface ThresholdsTabProps extends Omit<
|
||||
ThresholdsTableProps,
|
||||
| 'guestDefaults'
|
||||
| 'nodeDefaults'
|
||||
| 'pbsDefaults'
|
||||
| 'kubernetesDefaults'
|
||||
| 'trueNASDefaults'
|
||||
| 'trueNASDiskDefaults'
|
||||
| 'vmwareDefaults'
|
||||
| 'agentDefaults'
|
||||
| 'dockerDefaults'
|
||||
> {
|
||||
guestDefaults: Accessor<ThresholdsTableProps['guestDefaults']>;
|
||||
nodeDefaults: Accessor<ThresholdsTableProps['nodeDefaults']>;
|
||||
pbsDefaults: Accessor<NonNullable<ThresholdsTableProps['pbsDefaults']>>;
|
||||
kubernetesDefaults: Accessor<NonNullable<ThresholdsTableProps['kubernetesDefaults']>>;
|
||||
trueNASDefaults: Accessor<NonNullable<ThresholdsTableProps['trueNASDefaults']>>;
|
||||
trueNASDiskDefaults: Accessor<NonNullable<ThresholdsTableProps['trueNASDiskDefaults']>>;
|
||||
vmwareDefaults: Accessor<NonNullable<ThresholdsTableProps['vmwareDefaults']>>;
|
||||
agentDefaults: Accessor<ThresholdsTableProps['agentDefaults']>;
|
||||
dockerDefaults: Accessor<ThresholdsTableProps['dockerDefaults']>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,19 @@ export type OverrideType =
|
|||
| 'storage'
|
||||
| 'pbs'
|
||||
| 'pmg'
|
||||
| 'kubernetesCluster'
|
||||
| 'kubernetesNode'
|
||||
| 'kubernetesNamespace'
|
||||
| 'kubernetesDeployment'
|
||||
| 'kubernetesPod'
|
||||
| 'truenasSystem'
|
||||
| 'truenasPool'
|
||||
| 'truenasDataset'
|
||||
| 'truenasDisk'
|
||||
| 'vmwareHost'
|
||||
| 'vmwareVm'
|
||||
| 'vmwareDatastore'
|
||||
| 'vmwareNetwork'
|
||||
| 'dockerHost'
|
||||
| 'dockerContainer';
|
||||
|
||||
|
|
@ -89,6 +102,10 @@ export interface ThresholdsTableProps {
|
|||
setGuestPoweredOffSeverity: (value: 'warning' | 'critical') => void;
|
||||
nodeDefaults: SimpleThresholds;
|
||||
pbsDefaults?: SimpleThresholds;
|
||||
kubernetesDefaults?: SimpleThresholds;
|
||||
trueNASDefaults?: SimpleThresholds;
|
||||
trueNASDiskDefaults?: SimpleThresholds;
|
||||
vmwareDefaults?: SimpleThresholds;
|
||||
agentDefaults: SimpleThresholds;
|
||||
setNodeDefaults: (
|
||||
value:
|
||||
|
|
@ -100,6 +117,26 @@ export interface ThresholdsTableProps {
|
|||
| Record<string, number | undefined>
|
||||
| ((prev: Record<string, number | undefined>) => Record<string, number | undefined>),
|
||||
) => void;
|
||||
setKubernetesDefaults?: (
|
||||
value:
|
||||
| Record<string, number | undefined>
|
||||
| ((prev: Record<string, number | undefined>) => Record<string, number | undefined>),
|
||||
) => void;
|
||||
setTrueNASDefaults?: (
|
||||
value:
|
||||
| Record<string, number | undefined>
|
||||
| ((prev: Record<string, number | undefined>) => Record<string, number | undefined>),
|
||||
) => void;
|
||||
setTrueNASDiskDefaults?: (
|
||||
value:
|
||||
| Record<string, number | undefined>
|
||||
| ((prev: Record<string, number | undefined>) => Record<string, number | undefined>),
|
||||
) => void;
|
||||
setVMwareDefaults?: (
|
||||
value:
|
||||
| Record<string, number | undefined>
|
||||
| ((prev: Record<string, number | undefined>) => Record<string, number | undefined>),
|
||||
) => void;
|
||||
setAgentDefaults: (
|
||||
value:
|
||||
| Record<string, number | undefined>
|
||||
|
|
@ -168,6 +205,10 @@ export interface ThresholdsTableProps {
|
|||
resetGuestDefaults?: () => void;
|
||||
resetNodeDefaults?: () => void;
|
||||
resetPBSDefaults?: () => void;
|
||||
resetKubernetesDefaults?: () => void;
|
||||
resetTrueNASDefaults?: () => void;
|
||||
resetTrueNASDiskDefaults?: () => void;
|
||||
resetVMwareDefaults?: () => void;
|
||||
resetAgentDefaults?: () => void;
|
||||
resetDockerDefaults?: () => void;
|
||||
resetDockerIgnoredPrefixes?: () => void;
|
||||
|
|
@ -175,6 +216,10 @@ export interface ThresholdsTableProps {
|
|||
factoryGuestDefaults?: Record<string, number | undefined>;
|
||||
factoryNodeDefaults?: Record<string, number | undefined>;
|
||||
factoryPBSDefaults?: Record<string, number | undefined>;
|
||||
factoryKubernetesDefaults?: Record<string, number | undefined>;
|
||||
factoryTrueNASDefaults?: Record<string, number | undefined>;
|
||||
factoryTrueNASDiskDefaults?: Record<string, number | undefined>;
|
||||
factoryVMwareDefaults?: Record<string, number | undefined>;
|
||||
factoryAgentDefaults?: Record<string, number | undefined>;
|
||||
factoryDockerDefaults?: Record<string, number | undefined>;
|
||||
factoryStorageDefault?: number;
|
||||
|
|
@ -184,6 +229,19 @@ export interface ThresholdsTableProps {
|
|||
storage: number;
|
||||
pbs: number;
|
||||
agent: number;
|
||||
'k8s-cluster': number;
|
||||
'k8s-node': number;
|
||||
'k8s-deployment': number;
|
||||
'k8s-namespace': number;
|
||||
pod: number;
|
||||
'truenas-system': number;
|
||||
'truenas-pool': number;
|
||||
'truenas-dataset': number;
|
||||
'truenas-disk': number;
|
||||
'vmware-host': number;
|
||||
'vmware-vm': number;
|
||||
'vmware-datastore': number;
|
||||
'vmware-network': number;
|
||||
};
|
||||
metricTimeThresholds: () => Record<string, Record<string, number>>;
|
||||
setMetricTimeThresholds: (
|
||||
|
|
@ -225,6 +283,12 @@ export interface ThresholdsTableProps {
|
|||
setDisableAllDockerServices: (value: boolean) => void;
|
||||
disableAllDockerContainers: () => boolean;
|
||||
setDisableAllDockerContainers: (value: boolean) => void;
|
||||
disableAllKubernetes: () => boolean;
|
||||
setDisableAllKubernetes: (value: boolean) => void;
|
||||
disableAllTrueNAS: () => boolean;
|
||||
setDisableAllTrueNAS: (value: boolean) => void;
|
||||
disableAllVMware: () => boolean;
|
||||
setDisableAllVMware: (value: boolean) => void;
|
||||
// Global disable offline alerts flags
|
||||
disableAllNodesOffline: () => boolean;
|
||||
setDisableAllNodesOffline: (value: boolean) => void;
|
||||
|
|
|
|||
|
|
@ -90,6 +90,19 @@ export type OverrideType =
|
|||
| 'storage'
|
||||
| 'pbs'
|
||||
| 'pmg'
|
||||
| 'kubernetesCluster'
|
||||
| 'kubernetesNode'
|
||||
| 'kubernetesNamespace'
|
||||
| 'kubernetesDeployment'
|
||||
| 'kubernetesPod'
|
||||
| 'truenasSystem'
|
||||
| 'truenasPool'
|
||||
| 'truenasDataset'
|
||||
| 'truenasDisk'
|
||||
| 'vmwareHost'
|
||||
| 'vmwareVm'
|
||||
| 'vmwareDatastore'
|
||||
| 'vmwareNetwork'
|
||||
| 'dockerHost'
|
||||
| 'dockerContainer';
|
||||
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ export function useAlertOverridesState(props: AlertOverridesStateProps) {
|
|||
containerRuntimeResources: containerRuntimeResources(),
|
||||
getChildren: props.children,
|
||||
pbsInstanceById: pbsInstanceById(),
|
||||
allResources: props.allResources(),
|
||||
});
|
||||
|
||||
const currentOverrides = overrides();
|
||||
|
|
@ -128,8 +129,7 @@ export function useAlertOverridesState(props: AlertOverridesStateProps) {
|
|||
Boolean(newOverride.disabled) !== Boolean(existing.disabled) ||
|
||||
(newOverride.poweredOffSeverity ?? null) !== (existing.poweredOffSeverity ?? null) ||
|
||||
JSON.stringify(newOverride.backup ?? null) !== JSON.stringify(existing.backup ?? null) ||
|
||||
JSON.stringify(newOverride.snapshot ?? null) !==
|
||||
JSON.stringify(existing.snapshot ?? null)
|
||||
JSON.stringify(newOverride.snapshot ?? null) !== JSON.stringify(existing.snapshot ?? null)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,18 +10,17 @@ import {
|
|||
FACTORY_DOCKER_STATE_DISABLE_CONNECTIVITY,
|
||||
FACTORY_DOCKER_STATE_SEVERITY,
|
||||
FACTORY_GUEST_DEFAULTS,
|
||||
FACTORY_KUBERNETES_DEFAULTS,
|
||||
FACTORY_NODE_DEFAULTS,
|
||||
FACTORY_PBS_DEFAULTS,
|
||||
FACTORY_SNAPSHOT_DEFAULTS,
|
||||
FACTORY_STORAGE_DEFAULT,
|
||||
FACTORY_TRUENAS_DEFAULTS,
|
||||
FACTORY_TRUENAS_DISK_DEFAULTS,
|
||||
FACTORY_VMWARE_DEFAULTS,
|
||||
type AlertsConfigurationSnapshot,
|
||||
} from './alertsConfigurationModel';
|
||||
import type {
|
||||
CooldownConfig,
|
||||
EscalationConfig,
|
||||
GroupingConfig,
|
||||
QuietHoursConfig,
|
||||
} from './types';
|
||||
import type { CooldownConfig, EscalationConfig, GroupingConfig, QuietHoursConfig } from './types';
|
||||
|
||||
interface UseAlertsConfigurationSnapshotStateProps {
|
||||
setHasUnsavedChanges: (value: boolean) => void;
|
||||
|
|
@ -61,6 +60,18 @@ export function useAlertsConfigurationSnapshotState(
|
|||
const [pbsDefaults, setPBSDefaults] = createSignal<Record<string, number | undefined>>(
|
||||
defaultSnapshot.pbsDefaults,
|
||||
);
|
||||
const [kubernetesDefaults, setKubernetesDefaults] = createSignal<
|
||||
Record<string, number | undefined>
|
||||
>(defaultSnapshot.kubernetesDefaults);
|
||||
const [trueNASDefaults, setTrueNASDefaults] = createSignal<Record<string, number | undefined>>(
|
||||
defaultSnapshot.trueNASDefaults,
|
||||
);
|
||||
const [trueNASDiskDefaults, setTrueNASDiskDefaults] = createSignal<
|
||||
Record<string, number | undefined>
|
||||
>(defaultSnapshot.trueNASDiskDefaults);
|
||||
const [vmwareDefaults, setVMwareDefaults] = createSignal<Record<string, number | undefined>>(
|
||||
defaultSnapshot.vmwareDefaults,
|
||||
);
|
||||
const [agentDefaults, setAgentDefaults] = createSignal<Record<string, number | undefined>>(
|
||||
defaultSnapshot.agentDefaults,
|
||||
);
|
||||
|
|
@ -110,6 +121,11 @@ export function useAlertsConfigurationSnapshotState(
|
|||
const [disableAllDockerContainers, setDisableAllDockerContainers] = createSignal(
|
||||
defaultSnapshot.disableAllDockerContainers,
|
||||
);
|
||||
const [disableAllKubernetes, setDisableAllKubernetes] = createSignal(
|
||||
defaultSnapshot.disableAllKubernetes,
|
||||
);
|
||||
const [disableAllTrueNAS, setDisableAllTrueNAS] = createSignal(defaultSnapshot.disableAllTrueNAS);
|
||||
const [disableAllVMware, setDisableAllVMware] = createSignal(defaultSnapshot.disableAllVMware);
|
||||
const [disableAllNodesOffline, setDisableAllNodesOffline] = createSignal(
|
||||
defaultSnapshot.disableAllNodesOffline,
|
||||
);
|
||||
|
|
@ -147,6 +163,10 @@ export function useAlertsConfigurationSnapshotState(
|
|||
setGuestPoweredOffSeverity(snapshot.guestPoweredOffSeverity);
|
||||
setNodeDefaults({ ...snapshot.nodeDefaults });
|
||||
setPBSDefaults({ ...snapshot.pbsDefaults });
|
||||
setKubernetesDefaults({ ...snapshot.kubernetesDefaults });
|
||||
setTrueNASDefaults({ ...snapshot.trueNASDefaults });
|
||||
setTrueNASDiskDefaults({ ...snapshot.trueNASDiskDefaults });
|
||||
setVMwareDefaults({ ...snapshot.vmwareDefaults });
|
||||
setAgentDefaults({ ...snapshot.agentDefaults });
|
||||
setDockerDefaults({ ...snapshot.dockerDefaults });
|
||||
setDockerDisableConnectivity(snapshot.dockerDisableConnectivity);
|
||||
|
|
@ -173,6 +193,9 @@ export function useAlertsConfigurationSnapshotState(
|
|||
setDisableAllDockerHosts(snapshot.disableAllDockerHosts);
|
||||
setDisableAllDockerServices(snapshot.disableAllDockerServices);
|
||||
setDisableAllDockerContainers(snapshot.disableAllDockerContainers);
|
||||
setDisableAllKubernetes(snapshot.disableAllKubernetes);
|
||||
setDisableAllTrueNAS(snapshot.disableAllTrueNAS);
|
||||
setDisableAllVMware(snapshot.disableAllVMware);
|
||||
setDisableAllNodesOffline(snapshot.disableAllNodesOffline);
|
||||
setDisableAllGuestsOffline(snapshot.disableAllGuestsOffline);
|
||||
setDisableAllAgentsOffline(snapshot.disableAllAgentsOffline);
|
||||
|
|
@ -199,6 +222,10 @@ export function useAlertsConfigurationSnapshotState(
|
|||
guestPoweredOffSeverity: guestPoweredOffSeverity(),
|
||||
nodeDefaults: { ...nodeDefaults() },
|
||||
pbsDefaults: { ...pbsDefaults() },
|
||||
kubernetesDefaults: { ...kubernetesDefaults() },
|
||||
trueNASDefaults: { ...trueNASDefaults() },
|
||||
trueNASDiskDefaults: { ...trueNASDiskDefaults() },
|
||||
vmwareDefaults: { ...vmwareDefaults() },
|
||||
agentDefaults: { ...agentDefaults() },
|
||||
dockerDefaults: { ...dockerDefaults() },
|
||||
dockerDisableConnectivity: dockerDisableConnectivity(),
|
||||
|
|
@ -225,6 +252,9 @@ export function useAlertsConfigurationSnapshotState(
|
|||
disableAllDockerHosts: disableAllDockerHosts(),
|
||||
disableAllDockerServices: disableAllDockerServices(),
|
||||
disableAllDockerContainers: disableAllDockerContainers(),
|
||||
disableAllKubernetes: disableAllKubernetes(),
|
||||
disableAllTrueNAS: disableAllTrueNAS(),
|
||||
disableAllVMware: disableAllVMware(),
|
||||
disableAllNodesOffline: disableAllNodesOffline(),
|
||||
disableAllGuestsOffline: disableAllGuestsOffline(),
|
||||
disableAllAgentsOffline: disableAllAgentsOffline(),
|
||||
|
|
@ -249,6 +279,22 @@ export function useAlertsConfigurationSnapshotState(
|
|||
setPBSDefaults({ ...FACTORY_PBS_DEFAULTS });
|
||||
markUnsaved();
|
||||
};
|
||||
const resetKubernetesDefaults = () => {
|
||||
setKubernetesDefaults({ ...FACTORY_KUBERNETES_DEFAULTS });
|
||||
markUnsaved();
|
||||
};
|
||||
const resetTrueNASDefaults = () => {
|
||||
setTrueNASDefaults({ ...FACTORY_TRUENAS_DEFAULTS });
|
||||
markUnsaved();
|
||||
};
|
||||
const resetTrueNASDiskDefaults = () => {
|
||||
setTrueNASDiskDefaults({ ...FACTORY_TRUENAS_DISK_DEFAULTS });
|
||||
markUnsaved();
|
||||
};
|
||||
const resetVMwareDefaults = () => {
|
||||
setVMwareDefaults({ ...FACTORY_VMWARE_DEFAULTS });
|
||||
markUnsaved();
|
||||
};
|
||||
const resetAgentDefaults = () => {
|
||||
setAgentDefaults({ ...FACTORY_AGENT_DEFAULTS });
|
||||
markUnsaved();
|
||||
|
|
@ -297,6 +343,14 @@ export function useAlertsConfigurationSnapshotState(
|
|||
setNodeDefaults,
|
||||
pbsDefaults,
|
||||
setPBSDefaults,
|
||||
kubernetesDefaults,
|
||||
setKubernetesDefaults,
|
||||
trueNASDefaults,
|
||||
setTrueNASDefaults,
|
||||
trueNASDiskDefaults,
|
||||
setTrueNASDiskDefaults,
|
||||
vmwareDefaults,
|
||||
setVMwareDefaults,
|
||||
agentDefaults,
|
||||
setAgentDefaults,
|
||||
dockerDefaults,
|
||||
|
|
@ -343,6 +397,12 @@ export function useAlertsConfigurationSnapshotState(
|
|||
setDisableAllDockerServices,
|
||||
disableAllDockerContainers,
|
||||
setDisableAllDockerContainers,
|
||||
disableAllKubernetes,
|
||||
setDisableAllKubernetes,
|
||||
disableAllTrueNAS,
|
||||
setDisableAllTrueNAS,
|
||||
disableAllVMware,
|
||||
setDisableAllVMware,
|
||||
disableAllNodesOffline,
|
||||
setDisableAllNodesOffline,
|
||||
disableAllGuestsOffline,
|
||||
|
|
@ -360,6 +420,10 @@ export function useAlertsConfigurationSnapshotState(
|
|||
resetGuestDefaults,
|
||||
resetNodeDefaults,
|
||||
resetPBSDefaults,
|
||||
resetKubernetesDefaults,
|
||||
resetTrueNASDefaults,
|
||||
resetTrueNASDiskDefaults,
|
||||
resetVMwareDefaults,
|
||||
resetAgentDefaults,
|
||||
resetDockerDefaults,
|
||||
resetDockerIgnoredPrefixes,
|
||||
|
|
@ -369,6 +433,10 @@ export function useAlertsConfigurationSnapshotState(
|
|||
factoryGuestDefaults: FACTORY_GUEST_DEFAULTS,
|
||||
factoryNodeDefaults: FACTORY_NODE_DEFAULTS,
|
||||
factoryPBSDefaults: FACTORY_PBS_DEFAULTS,
|
||||
factoryKubernetesDefaults: FACTORY_KUBERNETES_DEFAULTS,
|
||||
factoryTrueNASDefaults: FACTORY_TRUENAS_DEFAULTS,
|
||||
factoryTrueNASDiskDefaults: FACTORY_TRUENAS_DISK_DEFAULTS,
|
||||
factoryVMwareDefaults: FACTORY_VMWARE_DEFAULTS,
|
||||
factoryAgentDefaults: FACTORY_AGENT_DEFAULTS,
|
||||
factoryDockerDefaults: FACTORY_DOCKER_DEFAULTS,
|
||||
factoryStorageDefault: FACTORY_STORAGE_DEFAULT,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface AlertThresholds {
|
|||
networkOut?: HysteresisThreshold;
|
||||
temperature?: HysteresisThreshold;
|
||||
diskTemperature?: HysteresisThreshold;
|
||||
usage?: HysteresisThreshold;
|
||||
disableConnectivity?: boolean; // Disable connectivity/powered-off alerts
|
||||
poweredOffSeverity?: 'warning' | 'critical';
|
||||
// Allow indexing with string
|
||||
|
|
@ -131,6 +132,10 @@ export interface AlertConfig {
|
|||
guestTagBlacklist?: string[];
|
||||
pmgDefaults?: PMGThresholdDefaults;
|
||||
pbsDefaults?: AlertThresholds;
|
||||
kubernetesDefaults?: AlertThresholds;
|
||||
truenasDefaults?: AlertThresholds;
|
||||
truenasDiskDefaults?: AlertThresholds;
|
||||
vmwareDefaults?: AlertThresholds;
|
||||
snapshotDefaults?: SnapshotAlertConfig;
|
||||
backupDefaults?: BackupAlertConfig;
|
||||
customRules?: CustomAlertRule[];
|
||||
|
|
@ -144,6 +149,19 @@ export interface AlertConfig {
|
|||
storage?: number;
|
||||
pbs?: number;
|
||||
agent?: number;
|
||||
'k8s-cluster'?: number;
|
||||
'k8s-node'?: number;
|
||||
'k8s-deployment'?: number;
|
||||
'k8s-namespace'?: number;
|
||||
pod?: number;
|
||||
'truenas-system'?: number;
|
||||
'truenas-pool'?: number;
|
||||
'truenas-dataset'?: number;
|
||||
'truenas-disk'?: number;
|
||||
'vmware-host'?: number;
|
||||
'vmware-vm'?: number;
|
||||
'vmware-datastore'?: number;
|
||||
'vmware-network'?: number;
|
||||
};
|
||||
metricTimeThresholds?: Record<string, Record<string, number>>;
|
||||
aggregation?: {
|
||||
|
|
@ -217,6 +235,9 @@ export interface AlertConfig {
|
|||
disableAllDockerHosts?: boolean;
|
||||
disableAllDockerContainers?: boolean;
|
||||
disableAllDockerServices?: boolean;
|
||||
disableAllKubernetes?: boolean;
|
||||
disableAllTrueNAS?: boolean;
|
||||
disableAllVMware?: boolean;
|
||||
disableAllNodesOffline?: boolean;
|
||||
disableAllGuestsOffline?: boolean;
|
||||
disableAllPBSOffline?: boolean;
|
||||
|
|
|
|||
|
|
@ -10,13 +10,16 @@ describe('alertTargetTypes', () => {
|
|||
it('canonicalizes supported aliases used for alert investigation targets', () => {
|
||||
expect(canonicalizeAlertTargetType('host')).toBe('agent');
|
||||
expect(canonicalizeAlertTargetType('docker-service')).toBe('app-container');
|
||||
expect(canonicalizeAlertTargetType('kubernetes')).toBe('k8s-cluster');
|
||||
expect(canonicalizeAlertTargetType('Kubernetes Namespace')).toBe('k8s-namespace');
|
||||
expect(canonicalizeAlertTargetType('TrueNAS Dataset')).toBe('truenas-dataset');
|
||||
expect(canonicalizeAlertTargetType('TrueNAS Disk')).toBe('physical_disk');
|
||||
expect(canonicalizeAlertTargetType('vSphere Host')).toBe('vmware-host');
|
||||
expect(canonicalizeAlertTargetType('VMware Virtual Machine')).toBe('vmware-vm');
|
||||
expect(canonicalizeAlertTargetType('vSphere Datastore')).toBe('vmware-datastore');
|
||||
expect(canonicalizeAlertTargetType('VMware Network')).toBe('vmware-network');
|
||||
expect(canonicalizeAlertTargetType('lxc')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('intentionally leaves k8s generic aliases unresolved at this layer', () => {
|
||||
expect(canonicalizeAlertTargetType('k8s')).toBeUndefined();
|
||||
expect(canonicalizeAlertTargetType('kubernetes')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('inferAlertTargetTypeFromResourceId', () => {
|
||||
|
|
@ -26,6 +29,16 @@ describe('alertTargetTypes', () => {
|
|||
expect(inferAlertTargetTypeFromResourceId('docker:abc')).toBe('app-container');
|
||||
expect(inferAlertTargetTypeFromResourceId('node:host1')).toBe('agent');
|
||||
expect(inferAlertTargetTypeFromResourceId('pod:ns/name')).toBe('pod');
|
||||
expect(inferAlertTargetTypeFromResourceId('k8s:cluster-1:deployment:api')).toBe(
|
||||
'k8s-deployment',
|
||||
);
|
||||
expect(inferAlertTargetTypeFromResourceId('truenas-dataset:tank/media')).toBe(
|
||||
'truenas-dataset',
|
||||
);
|
||||
expect(inferAlertTargetTypeFromResourceId('vmware:vc-1:host:host-101')).toBe('vmware-host');
|
||||
expect(inferAlertTargetTypeFromResourceId('vmware:vc-1:datastore:datastore-301')).toBe(
|
||||
'vmware-datastore',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -62,6 +75,22 @@ describe('alertTargetTypes', () => {
|
|||
}),
|
||||
).toBe('pbs');
|
||||
|
||||
expect(
|
||||
resolveAlertTargetType({
|
||||
alertType: 'custom_alert',
|
||||
metadataResourceType: 'TrueNAS Disk',
|
||||
resourceId: 'disk:sdb',
|
||||
}),
|
||||
).toBe('physical_disk');
|
||||
|
||||
expect(
|
||||
resolveAlertTargetType({
|
||||
alertType: 'custom_alert',
|
||||
metadataResourceType: 'vSphere VM',
|
||||
resourceId: 'vmware:vc-1:vm:vm-201',
|
||||
}),
|
||||
).toBe('vmware-vm');
|
||||
|
||||
expect(resolveAlertTargetType({ alertType: 'custom_alert' })).toBe('agent');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ import {
|
|||
METRIC_THRESHOLDS,
|
||||
type MetricType,
|
||||
} from '@/utils/metricThresholds';
|
||||
import {
|
||||
FACTORY_KUBERNETES_DEFAULTS,
|
||||
FACTORY_TRUENAS_DEFAULTS,
|
||||
FACTORY_TRUENAS_DISK_DEFAULTS,
|
||||
FACTORY_VMWARE_DEFAULTS,
|
||||
} from '@/utils/alertThresholdDefaults';
|
||||
import type { AlertConfig } from '@/types/alerts';
|
||||
|
||||
describe('metricThresholds', () => {
|
||||
|
|
@ -237,5 +243,35 @@ describe('metricThresholds', () => {
|
|||
critical: 92,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps Kubernetes, TrueNAS, and vSphere factory defaults explicit for alert configuration', () => {
|
||||
expect(FACTORY_KUBERNETES_DEFAULTS).toEqual({
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 90,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
});
|
||||
expect(FACTORY_TRUENAS_DEFAULTS).toMatchObject({
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 85,
|
||||
usage: 85,
|
||||
temperature: 80,
|
||||
});
|
||||
expect(FACTORY_TRUENAS_DISK_DEFAULTS).toEqual({ temperature: 55 });
|
||||
expect(FACTORY_VMWARE_DEFAULTS).toEqual({
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 90,
|
||||
usage: 85,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,47 @@
|
|||
import { canonicalizeFrontendResourceType } from '@/utils/resourceTypeCompat';
|
||||
|
||||
const normalize = (value: string | null | undefined): string => (value || '').trim().toLowerCase();
|
||||
const canonicalToken = (value: string): string => value.replace(/[\s_]+/g, '-');
|
||||
|
||||
const ALERT_TARGET_TYPE_ALIASES: Record<string, string> = {
|
||||
k8s: 'k8s-cluster',
|
||||
kubernetes: 'k8s-cluster',
|
||||
'k8s-cluster': 'k8s-cluster',
|
||||
'kubernetes-cluster': 'k8s-cluster',
|
||||
'k8s-node': 'k8s-node',
|
||||
'kubernetes-node': 'k8s-node',
|
||||
'k8s-namespace': 'k8s-namespace',
|
||||
'kubernetes-namespace': 'k8s-namespace',
|
||||
'k8s-deployment': 'k8s-deployment',
|
||||
'kubernetes-deployment': 'k8s-deployment',
|
||||
'k8s-pod': 'pod',
|
||||
'kubernetes-pod': 'pod',
|
||||
truenas: 'truenas',
|
||||
'truenas-system': 'truenas',
|
||||
'truenas-pool': 'pool',
|
||||
'truenas-dataset': 'truenas-dataset',
|
||||
'truenas-disk': 'physical_disk',
|
||||
vmware: 'vmware-vsphere',
|
||||
vsphere: 'vmware-vsphere',
|
||||
vmsphere: 'vmware-vsphere',
|
||||
'vmware-host': 'vmware-host',
|
||||
'vsphere-host': 'vmware-host',
|
||||
'vmware-vm': 'vmware-vm',
|
||||
'vsphere-vm': 'vmware-vm',
|
||||
'vmware-virtual-machine': 'vmware-vm',
|
||||
'vsphere-virtual-machine': 'vmware-vm',
|
||||
'vmware-datastore': 'vmware-datastore',
|
||||
'vsphere-datastore': 'vmware-datastore',
|
||||
'vmware-network': 'vmware-network',
|
||||
'vsphere-network': 'vmware-network',
|
||||
};
|
||||
|
||||
export const canonicalizeAlertTargetType = (raw?: string): string | undefined => {
|
||||
const normalized = normalize(raw);
|
||||
if (!normalized) return undefined;
|
||||
if (normalized === 'k8s' || normalized === 'kubernetes') {
|
||||
return undefined;
|
||||
}
|
||||
const alias =
|
||||
ALERT_TARGET_TYPE_ALIASES[normalized] ?? ALERT_TARGET_TYPE_ALIASES[canonicalToken(normalized)];
|
||||
if (alias) return alias;
|
||||
if (normalized === 'docker-container' || normalized === 'docker-service') {
|
||||
return 'app-container';
|
||||
}
|
||||
|
|
@ -57,12 +91,50 @@ export const inferAlertTargetTypeFromResourceId = (resourceID?: string): string
|
|||
return 'pmg';
|
||||
}
|
||||
if (
|
||||
normalized.startsWith('k8s-') ||
|
||||
normalized.startsWith('k8s-pod:') ||
|
||||
normalized.startsWith('k8s-pod-') ||
|
||||
normalized.startsWith('pod:') ||
|
||||
normalized.includes(':pod:')
|
||||
) {
|
||||
return 'pod';
|
||||
}
|
||||
if (normalized.startsWith('k8s:')) {
|
||||
if (normalized.includes(':deployment:')) return 'k8s-deployment';
|
||||
if (normalized.includes(':namespace:')) return 'k8s-namespace';
|
||||
if (normalized.includes(':node:')) return 'k8s-node';
|
||||
return 'k8s-cluster';
|
||||
}
|
||||
if (normalized.startsWith('truenas-system:') || normalized.startsWith('system:truenas')) {
|
||||
return 'truenas';
|
||||
}
|
||||
if (normalized.startsWith('truenas-pool:')) {
|
||||
return 'pool';
|
||||
}
|
||||
if (normalized.startsWith('truenas-dataset:')) {
|
||||
return 'truenas-dataset';
|
||||
}
|
||||
if (normalized.startsWith('truenas-disk:') || normalized.startsWith('physical-disk:')) {
|
||||
return 'physical_disk';
|
||||
}
|
||||
if (normalized.startsWith('vmware-host:') || normalized.startsWith('vsphere-host:')) {
|
||||
return 'vmware-host';
|
||||
}
|
||||
if (normalized.startsWith('vmware-vm:') || normalized.startsWith('vsphere-vm:')) {
|
||||
return 'vmware-vm';
|
||||
}
|
||||
if (normalized.startsWith('vmware-datastore:') || normalized.startsWith('vsphere-datastore:')) {
|
||||
return 'vmware-datastore';
|
||||
}
|
||||
if (normalized.startsWith('vmware-network:') || normalized.startsWith('vsphere-network:')) {
|
||||
return 'vmware-network';
|
||||
}
|
||||
if (normalized.startsWith('vmware:') || normalized.startsWith('vsphere:')) {
|
||||
if (normalized.includes(':host:')) return 'vmware-host';
|
||||
if (normalized.includes(':vm:')) return 'vmware-vm';
|
||||
if (normalized.includes(':datastore:')) return 'vmware-datastore';
|
||||
if (normalized.includes(':network:')) return 'vmware-network';
|
||||
return 'vmware-vsphere';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,6 +22,43 @@ export const FACTORY_PBS_DEFAULTS = {
|
|||
memory: 85,
|
||||
};
|
||||
|
||||
export const FACTORY_KUBERNETES_DEFAULTS = {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 90,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
};
|
||||
|
||||
export const FACTORY_TRUENAS_DEFAULTS = {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 85,
|
||||
usage: 85,
|
||||
temperature: 80,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
};
|
||||
|
||||
export const FACTORY_TRUENAS_DISK_DEFAULTS = {
|
||||
temperature: 55,
|
||||
};
|
||||
|
||||
export const FACTORY_VMWARE_DEFAULTS = {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
disk: 90,
|
||||
usage: 85,
|
||||
diskRead: -1,
|
||||
diskWrite: -1,
|
||||
networkIn: -1,
|
||||
networkOut: -1,
|
||||
};
|
||||
|
||||
export const FACTORY_AGENT_DEFAULTS = {
|
||||
cpu: 80,
|
||||
memory: 85,
|
||||
|
|
|
|||
|
|
@ -65,13 +65,76 @@ func CanonicalResourceTypeKeys(resourceType string) []string {
|
|||
keys = addUnique(keys, "node")
|
||||
case "k8s-cluster":
|
||||
keys = addUnique(keys, "k8s-cluster")
|
||||
keys = addUnique(keys, "guest")
|
||||
keys = addUnique(keys, "node")
|
||||
case "kubernetes cluster":
|
||||
keys = addUnique(keys, "k8s-cluster")
|
||||
keys = addUnique(keys, "node")
|
||||
case "k8s-node":
|
||||
keys = addUnique(keys, "k8s-node")
|
||||
keys = addUnique(keys, "node")
|
||||
case "kubernetes node":
|
||||
keys = addUnique(keys, "k8s-node")
|
||||
keys = addUnique(keys, "node")
|
||||
case "k8s-deployment":
|
||||
keys = addUnique(keys, "k8s-deployment")
|
||||
keys = addUnique(keys, "guest")
|
||||
case "kubernetes deployment":
|
||||
keys = addUnique(keys, "k8s-deployment")
|
||||
keys = addUnique(keys, "guest")
|
||||
case "k8s-namespace":
|
||||
keys = addUnique(keys, "k8s-namespace")
|
||||
case "kubernetes namespace":
|
||||
keys = addUnique(keys, "k8s-namespace")
|
||||
case "pod":
|
||||
keys = addUnique(keys, "pod")
|
||||
keys = addUnique(keys, "guest")
|
||||
case "kubernetes pod":
|
||||
keys = addUnique(keys, "pod")
|
||||
keys = addUnique(keys, "guest")
|
||||
case "truenas-system":
|
||||
keys = addUnique(keys, "truenas-system")
|
||||
keys = addUnique(keys, "agent")
|
||||
keys = addUnique(keys, "node")
|
||||
case "truenas system":
|
||||
keys = addUnique(keys, "truenas-system")
|
||||
keys = addUnique(keys, "agent")
|
||||
keys = addUnique(keys, "node")
|
||||
case "truenas-pool":
|
||||
keys = addUnique(keys, "truenas-pool")
|
||||
keys = addUnique(keys, "storage")
|
||||
case "truenas pool":
|
||||
keys = addUnique(keys, "truenas-pool")
|
||||
keys = addUnique(keys, "storage")
|
||||
case "truenas-dataset":
|
||||
keys = addUnique(keys, "truenas-dataset")
|
||||
keys = addUnique(keys, "storage")
|
||||
case "truenas dataset":
|
||||
keys = addUnique(keys, "truenas-dataset")
|
||||
keys = addUnique(keys, "storage")
|
||||
case "truenas-disk":
|
||||
keys = addUnique(keys, "truenas-disk")
|
||||
keys = addUnique(keys, "physical_disk")
|
||||
keys = addUnique(keys, "disk")
|
||||
keys = addUnique(keys, "storage")
|
||||
case "truenas disk":
|
||||
keys = addUnique(keys, "truenas-disk")
|
||||
keys = addUnique(keys, "physical_disk")
|
||||
keys = addUnique(keys, "disk")
|
||||
keys = addUnique(keys, "storage")
|
||||
case "vmware-host":
|
||||
keys = addUnique(keys, "vmware-host")
|
||||
keys = addUnique(keys, "agent")
|
||||
keys = addUnique(keys, "node")
|
||||
case "vmware-vm":
|
||||
keys = addUnique(keys, "vmware-vm")
|
||||
keys = addUnique(keys, "vm")
|
||||
keys = addUnique(keys, "guest")
|
||||
case "vmware-datastore":
|
||||
keys = addUnique(keys, "vmware-datastore")
|
||||
keys = addUnique(keys, "storage")
|
||||
case "vmware-network":
|
||||
keys = addUnique(keys, "vmware-network")
|
||||
keys = addUnique(keys, "network")
|
||||
case "storage":
|
||||
keys = addUnique(keys, "storage")
|
||||
case "disk":
|
||||
|
|
@ -112,5 +175,35 @@ func isUnsupportedLegacyAlertResourceType(typeKey string) bool {
|
|||
}
|
||||
|
||||
func CanonicalAlertResourceType(resourceType string) string {
|
||||
return strings.ToLower(strings.TrimSpace(resourceType))
|
||||
typeKey := strings.ToLower(strings.TrimSpace(resourceType))
|
||||
switch typeKey {
|
||||
case "kubernetes cluster":
|
||||
return "k8s-cluster"
|
||||
case "kubernetes node":
|
||||
return "k8s-node"
|
||||
case "kubernetes deployment":
|
||||
return "k8s-deployment"
|
||||
case "kubernetes namespace":
|
||||
return "k8s-namespace"
|
||||
case "kubernetes pod":
|
||||
return "pod"
|
||||
case "truenas system":
|
||||
return "truenas-system"
|
||||
case "truenas pool":
|
||||
return "truenas-pool"
|
||||
case "truenas dataset":
|
||||
return "truenas-dataset"
|
||||
case "truenas disk":
|
||||
return "truenas-disk"
|
||||
case "vmware host", "vsphere host":
|
||||
return "vmware-host"
|
||||
case "vmware vm", "vsphere vm", "vmware virtual machine", "vsphere virtual machine":
|
||||
return "vmware-vm"
|
||||
case "vmware datastore", "vsphere datastore":
|
||||
return "vmware-datastore"
|
||||
case "vmware network", "vsphere network":
|
||||
return "vmware-network"
|
||||
default:
|
||||
return typeKey
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,6 +287,66 @@ func NormalizeAgentDefaults(config *AlertConfig) {
|
|||
NormalizeDiskTempByType(config)
|
||||
}
|
||||
|
||||
func normalizeThresholdPointer(
|
||||
current *HysteresisThreshold,
|
||||
defaultTrigger float64,
|
||||
defaultClear float64,
|
||||
metricName string,
|
||||
) *HysteresisThreshold {
|
||||
if current == nil || current.Trigger < 0 {
|
||||
return &HysteresisThreshold{Trigger: defaultTrigger, Clear: defaultClear}
|
||||
}
|
||||
normalized := *current
|
||||
if normalized.Trigger == 0 {
|
||||
normalized.Clear = 0
|
||||
return &normalized
|
||||
}
|
||||
if normalized.Clear <= 0 {
|
||||
normalized.Clear = normalized.Trigger - 5
|
||||
if normalized.Clear < 0 {
|
||||
normalized.Clear = 0
|
||||
}
|
||||
}
|
||||
EnsureValidHysteresis(&normalized, metricName)
|
||||
return &normalized
|
||||
}
|
||||
|
||||
func NormalizeKubernetesDefaults(config *AlertConfig) {
|
||||
config.KubernetesDefaults.CPU = normalizeThresholdPointer(config.KubernetesDefaults.CPU, 80, 75, "kubernetes.cpu")
|
||||
config.KubernetesDefaults.Memory = normalizeThresholdPointer(config.KubernetesDefaults.Memory, 85, 80, "kubernetes.memory")
|
||||
config.KubernetesDefaults.Disk = normalizeThresholdPointer(config.KubernetesDefaults.Disk, 90, 85, "kubernetes.disk")
|
||||
|
||||
config.KubernetesDefaults.DiskRead = normalizeThresholdPointer(config.KubernetesDefaults.DiskRead, 0, 0, "kubernetes.diskRead")
|
||||
config.KubernetesDefaults.DiskWrite = normalizeThresholdPointer(config.KubernetesDefaults.DiskWrite, 0, 0, "kubernetes.diskWrite")
|
||||
config.KubernetesDefaults.NetworkIn = normalizeThresholdPointer(config.KubernetesDefaults.NetworkIn, 0, 0, "kubernetes.networkIn")
|
||||
config.KubernetesDefaults.NetworkOut = normalizeThresholdPointer(config.KubernetesDefaults.NetworkOut, 0, 0, "kubernetes.networkOut")
|
||||
}
|
||||
|
||||
func NormalizeTrueNASDefaults(config *AlertConfig) {
|
||||
config.TrueNASDefaults.CPU = normalizeThresholdPointer(config.TrueNASDefaults.CPU, 80, 75, "truenas.cpu")
|
||||
config.TrueNASDefaults.Memory = normalizeThresholdPointer(config.TrueNASDefaults.Memory, 85, 80, "truenas.memory")
|
||||
config.TrueNASDefaults.Disk = normalizeThresholdPointer(config.TrueNASDefaults.Disk, 85, 80, "truenas.disk")
|
||||
config.TrueNASDefaults.Usage = normalizeThresholdPointer(config.TrueNASDefaults.Usage, 85, 80, "truenas.usage")
|
||||
config.TrueNASDefaults.Temperature = normalizeThresholdPointer(config.TrueNASDefaults.Temperature, 80, 75, "truenas.temperature")
|
||||
config.TrueNASDefaults.DiskRead = normalizeThresholdPointer(config.TrueNASDefaults.DiskRead, 0, 0, "truenas.diskRead")
|
||||
config.TrueNASDefaults.DiskWrite = normalizeThresholdPointer(config.TrueNASDefaults.DiskWrite, 0, 0, "truenas.diskWrite")
|
||||
config.TrueNASDefaults.NetworkIn = normalizeThresholdPointer(config.TrueNASDefaults.NetworkIn, 0, 0, "truenas.networkIn")
|
||||
config.TrueNASDefaults.NetworkOut = normalizeThresholdPointer(config.TrueNASDefaults.NetworkOut, 0, 0, "truenas.networkOut")
|
||||
|
||||
config.TrueNASDiskDefaults.Temperature = normalizeThresholdPointer(config.TrueNASDiskDefaults.Temperature, 55, 50, "truenas.disk.temperature")
|
||||
}
|
||||
|
||||
func NormalizeVMwareDefaults(config *AlertConfig) {
|
||||
config.VMwareDefaults.CPU = normalizeThresholdPointer(config.VMwareDefaults.CPU, 80, 75, "vmware.cpu")
|
||||
config.VMwareDefaults.Memory = normalizeThresholdPointer(config.VMwareDefaults.Memory, 85, 80, "vmware.memory")
|
||||
config.VMwareDefaults.Disk = normalizeThresholdPointer(config.VMwareDefaults.Disk, 90, 85, "vmware.disk")
|
||||
config.VMwareDefaults.Usage = normalizeThresholdPointer(config.VMwareDefaults.Usage, 85, 80, "vmware.usage")
|
||||
config.VMwareDefaults.DiskRead = normalizeThresholdPointer(config.VMwareDefaults.DiskRead, 0, 0, "vmware.diskRead")
|
||||
config.VMwareDefaults.DiskWrite = normalizeThresholdPointer(config.VMwareDefaults.DiskWrite, 0, 0, "vmware.diskWrite")
|
||||
config.VMwareDefaults.NetworkIn = normalizeThresholdPointer(config.VMwareDefaults.NetworkIn, 0, 0, "vmware.networkIn")
|
||||
config.VMwareDefaults.NetworkOut = normalizeThresholdPointer(config.VMwareDefaults.NetworkOut, 0, 0, "vmware.networkOut")
|
||||
}
|
||||
|
||||
// diskFillByTypeDefaults returns the canonical per-type fill-% defaults.
|
||||
// Keys are lowercase hardware type strings.
|
||||
func diskFillByTypeDefaults() map[string]HysteresisThreshold {
|
||||
|
|
@ -434,6 +494,19 @@ func NormalizeTimeThresholds(config *AlertConfig) {
|
|||
ensureDelay("storage")
|
||||
ensureDelay("pbs")
|
||||
ensureDelay("agent")
|
||||
ensureDelay("k8s-cluster")
|
||||
ensureDelay("k8s-node")
|
||||
ensureDelay("k8s-deployment")
|
||||
ensureDelay("k8s-namespace")
|
||||
ensureDelay("pod")
|
||||
ensureDelay("truenas-system")
|
||||
ensureDelay("truenas-pool")
|
||||
ensureDelay("truenas-dataset")
|
||||
ensureDelay("truenas-disk")
|
||||
ensureDelay("vmware-host")
|
||||
ensureDelay("vmware-vm")
|
||||
ensureDelay("vmware-datastore")
|
||||
ensureDelay("vmware-network")
|
||||
if delay, ok := config.TimeThresholds["all"]; ok && delay < 0 {
|
||||
config.TimeThresholds["all"] = defaultDelaySeconds
|
||||
}
|
||||
|
|
@ -447,6 +520,19 @@ func ValidateHysteresisThresholds(config *AlertConfig) {
|
|||
EnsureValidHysteresis(config.NodeDefaults.Memory, "node.memory")
|
||||
EnsureValidHysteresis(config.NodeDefaults.Temperature, "node.temperature")
|
||||
EnsureValidHysteresis(&config.StorageDefault, "storage")
|
||||
EnsureValidHysteresis(config.KubernetesDefaults.CPU, "kubernetes.cpu")
|
||||
EnsureValidHysteresis(config.KubernetesDefaults.Memory, "kubernetes.memory")
|
||||
EnsureValidHysteresis(config.KubernetesDefaults.Disk, "kubernetes.disk")
|
||||
EnsureValidHysteresis(config.TrueNASDefaults.CPU, "truenas.cpu")
|
||||
EnsureValidHysteresis(config.TrueNASDefaults.Memory, "truenas.memory")
|
||||
EnsureValidHysteresis(config.TrueNASDefaults.Disk, "truenas.disk")
|
||||
EnsureValidHysteresis(config.TrueNASDefaults.Usage, "truenas.usage")
|
||||
EnsureValidHysteresis(config.TrueNASDefaults.Temperature, "truenas.temperature")
|
||||
EnsureValidHysteresis(config.TrueNASDiskDefaults.Temperature, "truenas.disk.temperature")
|
||||
EnsureValidHysteresis(config.VMwareDefaults.CPU, "vmware.cpu")
|
||||
EnsureValidHysteresis(config.VMwareDefaults.Memory, "vmware.memory")
|
||||
EnsureValidHysteresis(config.VMwareDefaults.Disk, "vmware.disk")
|
||||
EnsureValidHysteresis(config.VMwareDefaults.Usage, "vmware.usage")
|
||||
}
|
||||
|
||||
func ValidateQuietHoursTimezone(config *AlertConfig) {
|
||||
|
|
|
|||
|
|
@ -230,6 +230,10 @@ type AlertConfig struct {
|
|||
GuestTagBlacklist []string `json:"guestTagBlacklist,omitempty"`
|
||||
PMGDefaults PMGThresholdConfig `json:"pmgDefaults"`
|
||||
PBSDefaults ThresholdConfig `json:"pbsDefaults"`
|
||||
KubernetesDefaults ThresholdConfig `json:"kubernetesDefaults"`
|
||||
TrueNASDefaults ThresholdConfig `json:"truenasDefaults"`
|
||||
TrueNASDiskDefaults ThresholdConfig `json:"truenasDiskDefaults"`
|
||||
VMwareDefaults ThresholdConfig `json:"vmwareDefaults"`
|
||||
SnapshotDefaults SnapshotAlertConfig `json:"snapshotDefaults"`
|
||||
BackupDefaults BackupAlertConfig `json:"backupDefaults"`
|
||||
Overrides map[string]ThresholdConfig `json:"overrides"` // keyed by resource ID
|
||||
|
|
@ -244,6 +248,9 @@ type AlertConfig struct {
|
|||
DisableAllDockerHosts bool `json:"disableAllDockerHosts"` // Disable all alerts for Docker hosts
|
||||
DisableAllDockerContainers bool `json:"disableAllDockerContainers"` // Disable all alerts for Docker containers
|
||||
DisableAllDockerServices bool `json:"disableAllDockerServices"` // Disable all alerts for Docker services
|
||||
DisableAllKubernetes bool `json:"disableAllKubernetes"` // Disable all alerts for Kubernetes resources
|
||||
DisableAllTrueNAS bool `json:"disableAllTrueNAS"` // Disable all alerts for TrueNAS resources
|
||||
DisableAllVMware bool `json:"disableAllVMware"` // Disable all alerts for VMware vSphere resources
|
||||
DisableAllNodesOffline bool `json:"disableAllNodesOffline"` // Disable node offline/connectivity alerts globally
|
||||
DisableAllGuestsOffline bool `json:"disableAllGuestsOffline"` // Disable guest powered-off alerts globally
|
||||
DisableAllAgentsOffline bool `json:"disableAllAgentsOffline"` // Disable agent offline alerts globally
|
||||
|
|
|
|||
|
|
@ -78,8 +78,13 @@ func normalizeSnapshotDefaults(config *AlertConfig) { alertconfig.NormalizeSnaps
|
|||
func normalizeBackupDefaults(config *AlertConfig) { alertconfig.NormalizeBackupDefaults(config) }
|
||||
func normalizeNodeDefaults(config *AlertConfig) { alertconfig.NormalizeNodeDefaults(config) }
|
||||
func normalizeAgentDefaults(config *AlertConfig) { alertconfig.NormalizeAgentDefaults(config) }
|
||||
func normalizeGeneralSettings(config *AlertConfig) { alertconfig.NormalizeGeneralSettings(config) }
|
||||
func normalizeTimeThresholds(config *AlertConfig) { alertconfig.NormalizeTimeThresholds(config) }
|
||||
func normalizeKubernetesDefaults(config *AlertConfig) {
|
||||
alertconfig.NormalizeKubernetesDefaults(config)
|
||||
}
|
||||
func normalizeTrueNASDefaults(config *AlertConfig) { alertconfig.NormalizeTrueNASDefaults(config) }
|
||||
func normalizeVMwareDefaults(config *AlertConfig) { alertconfig.NormalizeVMwareDefaults(config) }
|
||||
func normalizeGeneralSettings(config *AlertConfig) { alertconfig.NormalizeGeneralSettings(config) }
|
||||
func normalizeTimeThresholds(config *AlertConfig) { alertconfig.NormalizeTimeThresholds(config) }
|
||||
|
||||
func validateHysteresisThresholds(config *AlertConfig) {
|
||||
alertconfig.ValidateHysteresisThresholds(config)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ func (m *Manager) UpdateConfig(config AlertConfig) {
|
|||
alertconfig.NormalizeBackupDefaults(&config)
|
||||
alertconfig.NormalizeNodeDefaults(&config)
|
||||
alertconfig.NormalizeAgentDefaults(&config)
|
||||
alertconfig.NormalizeKubernetesDefaults(&config)
|
||||
alertconfig.NormalizeTrueNASDefaults(&config)
|
||||
alertconfig.NormalizeVMwareDefaults(&config)
|
||||
alertconfig.NormalizeGeneralSettings(&config)
|
||||
alertconfig.NormalizeTimeThresholds(&config)
|
||||
|
||||
|
|
@ -229,6 +232,42 @@ func (m *Manager) applyGlobalOfflineSettingsLocked() {
|
|||
m.clearAlertNoLock(alertID)
|
||||
}
|
||||
}
|
||||
|
||||
if m.config.DisableAllKubernetes || m.config.DisableAllTrueNAS || m.config.DisableAllVMware {
|
||||
var platformAlerts []string
|
||||
for storageKey, alert := range m.activeAlerts {
|
||||
primaryType := alertPrimaryResourceType(alert)
|
||||
if primaryType == "" {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case m.config.DisableAllKubernetes && isUnifiedKubernetesAlertType(primaryType):
|
||||
platformAlerts = append(platformAlerts, effectiveAlertID(alert, storageKey))
|
||||
case m.config.DisableAllTrueNAS && isUnifiedTrueNASAlertType(primaryType):
|
||||
platformAlerts = append(platformAlerts, effectiveAlertID(alert, storageKey))
|
||||
case m.config.DisableAllVMware && isUnifiedVMwareAlertType(primaryType):
|
||||
platformAlerts = append(platformAlerts, effectiveAlertID(alert, storageKey))
|
||||
}
|
||||
}
|
||||
for _, alertID := range platformAlerts {
|
||||
m.clearAlertNoLock(alertID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func alertPrimaryResourceType(alert *Alert) string {
|
||||
if alert == nil || alert.Metadata == nil {
|
||||
return ""
|
||||
}
|
||||
metaType, ok := alert.Metadata["resourceType"].(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
keys := CanonicalResourceTypeKeys(metaType)
|
||||
if len(keys) == 0 {
|
||||
return ""
|
||||
}
|
||||
return keys[0]
|
||||
}
|
||||
|
||||
// reevaluateActiveAlertsLocked re-evaluates all active alerts against the current configuration.
|
||||
|
|
@ -269,6 +308,11 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
resourceTypeMeta = alertconfig.CanonicalAlertResourceType(metaType)
|
||||
}
|
||||
}
|
||||
resourceTypeKeys := CanonicalResourceTypeKeys(resourceTypeMeta)
|
||||
primaryResourceType := ""
|
||||
if len(resourceTypeKeys) > 0 {
|
||||
primaryResourceType = resourceTypeKeys[0]
|
||||
}
|
||||
|
||||
if alert.Type == "queue-depth" || alert.Type == "queue-deferred" || alert.Type == "queue-hold" || alert.Type == "message-age" {
|
||||
if m.config.DisableAllPMG {
|
||||
|
|
@ -277,14 +321,29 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
}
|
||||
}
|
||||
|
||||
handledModernPlatformType := false
|
||||
if isUnifiedModernPlatformAlertType(primaryResourceType) {
|
||||
handledModernPlatformType = true
|
||||
if m.unifiedPlatformAlertsDisabledNoLock(primaryResourceType) {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
}
|
||||
thresholds := m.resolveResourceThresholds(primaryResourceType, resourceID)
|
||||
if thresholds.Disabled {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
}
|
||||
threshold = getThresholdForMetric(thresholds, metricType)
|
||||
}
|
||||
|
||||
isAgentResource := false
|
||||
for _, key := range CanonicalResourceTypeKeys(resourceTypeMeta) {
|
||||
for _, key := range resourceTypeKeys {
|
||||
if key == "agent" {
|
||||
isAgentResource = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isAgentResource {
|
||||
if !handledModernPlatformType && isAgentResource {
|
||||
if m.config.DisableAllAgents {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
|
|
@ -347,7 +406,7 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
threshold = getThresholdForMetric(thresholds, metricType)
|
||||
}
|
||||
|
||||
if threshold == nil && !strings.Contains(resourceID, ":") && (alert.Instance == "Node" || alert.Instance == alert.Node) {
|
||||
if threshold == nil && !handledModernPlatformType && !strings.Contains(resourceID, ":") && (alert.Instance == "Node" || alert.Instance == alert.Node) {
|
||||
if m.config.DisableAllNodes {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
|
|
@ -358,7 +417,7 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
continue
|
||||
}
|
||||
threshold = getThresholdForMetric(thresholds, metricType)
|
||||
} else if threshold == nil && (alert.Instance == "Storage" || strings.Contains(alert.ResourceID, ":storage/")) {
|
||||
} else if threshold == nil && !handledModernPlatformType && (alert.Instance == "Storage" || strings.Contains(alert.ResourceID, ":storage/")) {
|
||||
if m.config.DisableAllStorage {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
|
|
@ -369,7 +428,7 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
continue
|
||||
}
|
||||
threshold = getThresholdForMetric(thresholds, metricType)
|
||||
} else if threshold == nil && (resourceTypeMeta == "pbs" || alert.Instance == "PBS") {
|
||||
} else if threshold == nil && !handledModernPlatformType && (resourceTypeMeta == "pbs" || alert.Instance == "PBS") {
|
||||
if m.config.DisableAllPBS {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
|
|
@ -382,7 +441,7 @@ func (m *Manager) reevaluateActiveAlertsLocked() {
|
|||
threshold = getThresholdForMetric(thresholds, metricType)
|
||||
}
|
||||
|
||||
if threshold == nil {
|
||||
if threshold == nil && !handledModernPlatformType {
|
||||
if m.config.DisableAllGuests {
|
||||
alertsToResolve = append(alertsToResolve, alertID)
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -76,6 +76,39 @@ func defaultAlertConfig() AlertConfig {
|
|||
CPU: &HysteresisThreshold{Trigger: 80, Clear: 75},
|
||||
Memory: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
},
|
||||
KubernetesDefaults: ThresholdConfig{
|
||||
CPU: &HysteresisThreshold{Trigger: 80, Clear: 75},
|
||||
Memory: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
Disk: &HysteresisThreshold{Trigger: 90, Clear: 85},
|
||||
DiskRead: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
DiskWrite: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
NetworkIn: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
NetworkOut: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
},
|
||||
TrueNASDefaults: ThresholdConfig{
|
||||
CPU: &HysteresisThreshold{Trigger: 80, Clear: 75},
|
||||
Memory: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
Disk: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
Usage: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
Temperature: &HysteresisThreshold{Trigger: 80, Clear: 75},
|
||||
DiskRead: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
DiskWrite: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
NetworkIn: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
NetworkOut: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
},
|
||||
TrueNASDiskDefaults: ThresholdConfig{
|
||||
Temperature: &HysteresisThreshold{Trigger: 55, Clear: 50},
|
||||
},
|
||||
VMwareDefaults: ThresholdConfig{
|
||||
CPU: &HysteresisThreshold{Trigger: 80, Clear: 75},
|
||||
Memory: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
Disk: &HysteresisThreshold{Trigger: 90, Clear: 85},
|
||||
Usage: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
DiskRead: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
DiskWrite: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
NetworkIn: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
NetworkOut: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
},
|
||||
StorageDefault: HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
DiskFillByType: map[string]HysteresisThreshold{
|
||||
"nvme": {Trigger: 92, Clear: 87},
|
||||
|
|
@ -91,11 +124,24 @@ func defaultAlertConfig() AlertConfig {
|
|||
SuppressionWindow: 5, // 5 minutes
|
||||
HysteresisMargin: 5.0, // 5% default margin
|
||||
TimeThresholds: map[string]int{
|
||||
"guest": 5,
|
||||
"node": 5,
|
||||
"agent": 5,
|
||||
"storage": 5,
|
||||
"pbs": 5,
|
||||
"guest": 5,
|
||||
"node": 5,
|
||||
"agent": 5,
|
||||
"storage": 5,
|
||||
"pbs": 5,
|
||||
"k8s-cluster": 5,
|
||||
"k8s-node": 5,
|
||||
"k8s-deployment": 5,
|
||||
"k8s-namespace": 5,
|
||||
"pod": 5,
|
||||
"truenas-system": 5,
|
||||
"truenas-pool": 5,
|
||||
"truenas-dataset": 5,
|
||||
"truenas-disk": 5,
|
||||
"vmware-host": 5,
|
||||
"vmware-vm": 5,
|
||||
"vmware-datastore": 5,
|
||||
"vmware-network": 5,
|
||||
},
|
||||
Overrides: make(map[string]ThresholdConfig),
|
||||
Schedule: ScheduleConfig{
|
||||
|
|
|
|||
|
|
@ -1108,7 +1108,9 @@ func isKnownResourceType(rt unifiedresources.ResourceType) bool {
|
|||
unifiedresources.ResourceTypeK8sNode,
|
||||
unifiedresources.ResourceTypePod,
|
||||
unifiedresources.ResourceTypeK8sDeployment,
|
||||
unifiedresources.ResourceTypeK8sNamespace,
|
||||
unifiedresources.ResourceTypeStorage,
|
||||
unifiedresources.ResourceTypeNetwork,
|
||||
unifiedresources.ResourceTypePBS,
|
||||
unifiedresources.ResourceTypePMG,
|
||||
unifiedresources.ResourceTypeCeph,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,64 @@ func TestResourceAlertSpecValidateAcceptsSupportedKinds(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "kubernetes pod metric threshold",
|
||||
spec: ResourceAlertSpec{
|
||||
ID: "k8s-prod-default-api-cpu-high",
|
||||
ResourceID: "k8s:prod/ns:default/pod:api-7d9f",
|
||||
ResourceType: unifiedresources.ResourceTypePod,
|
||||
Kind: AlertSpecKindMetricThreshold,
|
||||
Severity: AlertSeverityWarning,
|
||||
MetricThreshold: &MetricThresholdSpec{
|
||||
Metric: "cpu",
|
||||
Direction: ThresholdDirectionAbove,
|
||||
Trigger: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "kubernetes namespace provider incident",
|
||||
spec: ResourceAlertSpec{
|
||||
ID: "k8s-prod-default-incident",
|
||||
ResourceID: "k8s:prod/ns:default",
|
||||
ResourceType: unifiedresources.ResourceTypeK8sNamespace,
|
||||
Kind: AlertSpecKindProviderIncident,
|
||||
Severity: AlertSeverityWarning,
|
||||
ProviderIncident: &ProviderIncidentSpec{
|
||||
Provider: "kubernetes",
|
||||
Codes: []string{"namespace_quota_pressure"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "truenas physical disk metric threshold",
|
||||
spec: ResourceAlertSpec{
|
||||
ID: "truenas-main-ada0-temperature-high",
|
||||
ResourceID: "physical-disk:truenas-main/ada0",
|
||||
ResourceType: unifiedresources.ResourceTypePhysicalDisk,
|
||||
Kind: AlertSpecKindMetricThreshold,
|
||||
Severity: AlertSeverityWarning,
|
||||
MetricThreshold: &MetricThresholdSpec{
|
||||
Metric: "temperature",
|
||||
Direction: ThresholdDirectionAbove,
|
||||
Trigger: 55,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "vmware network provider incident",
|
||||
spec: ResourceAlertSpec{
|
||||
ID: "vmware-vc1-network-alarm",
|
||||
ResourceID: "vmware:vc-1:network:network-401",
|
||||
ResourceType: unifiedresources.ResourceTypeNetwork,
|
||||
Kind: AlertSpecKindProviderIncident,
|
||||
Severity: AlertSeverityWarning,
|
||||
ProviderIncident: &ProviderIncidentSpec{
|
||||
Provider: "vmware",
|
||||
Codes: []string{"vmware_alarm_state"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "severity threshold",
|
||||
spec: ResourceAlertSpec{
|
||||
|
|
|
|||
|
|
@ -705,3 +705,98 @@ func TestReevaluateActiveAlertsUsesPBSResourceTypeMetadata(t *testing.T) {
|
|||
t.Fatalf("expected PBS metric alert to resolve when PBS memory threshold is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReevaluateActiveAlertsUsesKubernetesResourceTypeMetadata(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
resourceID := "k8s:prod/ns:default/pod:api-7d9f"
|
||||
m.mu.Lock()
|
||||
m.config.Enabled = true
|
||||
m.config.KubernetesDefaults = ThresholdConfig{
|
||||
CPU: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
}
|
||||
state, alert := testNewCanonicalAlert(resourceID, canonicalMetricSpecID(resourceID, "cpu"), string(alertspecs.AlertSpecKindMetricThreshold), "cpu")
|
||||
alert.Value = 88
|
||||
alert.Threshold = 80
|
||||
alert.ResourceName = "api-7d9f"
|
||||
alert.Node = "worker-1"
|
||||
alert.Instance = "prod"
|
||||
alert.Metadata = map[string]interface{}{
|
||||
"resourceType": "Kubernetes Pod",
|
||||
}
|
||||
m.setActiveAlertNoLock(state, alert)
|
||||
m.reevaluateActiveAlertsLocked()
|
||||
m.mu.Unlock()
|
||||
|
||||
m.mu.RLock()
|
||||
_, exists := m.activeAlerts[state]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if exists {
|
||||
t.Fatalf("expected Kubernetes metric alert to resolve when Kubernetes CPU threshold is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReevaluateActiveAlertsUsesTrueNASResourceTypeMetadata(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
resourceID := "storage:truenas-main/pool:tank"
|
||||
m.mu.Lock()
|
||||
m.config.Enabled = true
|
||||
m.config.StorageDefault = HysteresisThreshold{Trigger: 95, Clear: 90}
|
||||
m.config.TrueNASDefaults = ThresholdConfig{
|
||||
Usage: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
}
|
||||
state, alert := testNewCanonicalAlert(resourceID, canonicalMetricSpecID(resourceID, "usage"), string(alertspecs.AlertSpecKindMetricThreshold), "usage")
|
||||
alert.Value = 90
|
||||
alert.Threshold = 85
|
||||
alert.ResourceName = "tank"
|
||||
alert.Node = "truenas-main"
|
||||
alert.Instance = "TrueNAS"
|
||||
alert.Metadata = map[string]interface{}{
|
||||
"resourceType": "TrueNAS Pool",
|
||||
}
|
||||
m.setActiveAlertNoLock(state, alert)
|
||||
m.reevaluateActiveAlertsLocked()
|
||||
m.mu.Unlock()
|
||||
|
||||
m.mu.RLock()
|
||||
_, exists := m.activeAlerts[state]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if exists {
|
||||
t.Fatalf("expected TrueNAS metric alert to resolve when TrueNAS usage threshold is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReevaluateActiveAlertsUsesVMwareResourceTypeMetadata(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
resourceID := "vmware:vc-1:datastore:datastore-301"
|
||||
m.mu.Lock()
|
||||
m.config.Enabled = true
|
||||
m.config.StorageDefault = HysteresisThreshold{Trigger: 95, Clear: 90}
|
||||
m.config.VMwareDefaults = ThresholdConfig{
|
||||
Usage: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
}
|
||||
state, alert := testNewCanonicalAlert(resourceID, canonicalMetricSpecID(resourceID, "usage"), string(alertspecs.AlertSpecKindMetricThreshold), "usage")
|
||||
alert.Value = 90
|
||||
alert.Threshold = 85
|
||||
alert.ResourceName = "nvme-primary"
|
||||
alert.Node = "Lab Datacenter"
|
||||
alert.Instance = "Lab vCenter"
|
||||
alert.Metadata = map[string]interface{}{
|
||||
"resourceType": "vSphere Datastore",
|
||||
}
|
||||
m.setActiveAlertNoLock(state, alert)
|
||||
m.reevaluateActiveAlertsLocked()
|
||||
m.mu.Unlock()
|
||||
|
||||
m.mu.RLock()
|
||||
_, exists := m.activeAlerts[state]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if exists {
|
||||
t.Fatalf("expected vSphere metric alert to resolve when vSphere usage threshold is disabled")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package alerts
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
alertspecs "github.com/rcourtman/pulse-go-rewrite/internal/alerts/specs"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -19,6 +21,11 @@ import (
|
|||
// PBS: pbs.ID e.g. "pbs-1"
|
||||
// PMG: pmg.ID e.g. "pmg-1"
|
||||
// Docker Container: "docker:{hostID}/{containerID}"
|
||||
// Kubernetes: canonical unified resource ID for cluster/node/namespace/deployment/pod
|
||||
// TrueNAS System: canonical unified agent resource ID
|
||||
// TrueNAS Storage: canonical unified storage resource ID for pool/dataset
|
||||
// TrueNAS Disk: canonical unified physical_disk resource ID
|
||||
// VMware vSphere: canonical unified resource ID for host/vm/datastore/network
|
||||
//
|
||||
// For guests, CheckUnifiedResource expects the same canonical resource ID the
|
||||
// monitoring pipeline already uses. It does not translate IDs.
|
||||
|
|
@ -31,18 +38,19 @@ type UnifiedResourceMetric struct {
|
|||
// UnifiedResourceInput is the data needed to evaluate alerts for a unified resource.
|
||||
// This avoids importing unifiedresources (which would cause an import cycle).
|
||||
type UnifiedResourceInput struct {
|
||||
ID string
|
||||
Type string // lowercase: "vm", "system-container", "app-container", "agent", "pbs", "storage", "pmg"
|
||||
Name string
|
||||
Node string
|
||||
Instance string
|
||||
CPU *UnifiedResourceMetric
|
||||
Memory *UnifiedResourceMetric
|
||||
Disk *UnifiedResourceMetric
|
||||
DiskRead *UnifiedResourceMetric
|
||||
DiskWrite *UnifiedResourceMetric
|
||||
NetworkIn *UnifiedResourceMetric
|
||||
NetworkOut *UnifiedResourceMetric
|
||||
ID string
|
||||
Type string // lowercase: "vm", "system-container", "app-container", "agent", "pbs", "storage", "pmg"
|
||||
Name string
|
||||
Node string
|
||||
Instance string
|
||||
CPU *UnifiedResourceMetric
|
||||
Memory *UnifiedResourceMetric
|
||||
Disk *UnifiedResourceMetric
|
||||
DiskRead *UnifiedResourceMetric
|
||||
DiskWrite *UnifiedResourceMetric
|
||||
NetworkIn *UnifiedResourceMetric
|
||||
NetworkOut *UnifiedResourceMetric
|
||||
Temperature *UnifiedResourceMetric
|
||||
}
|
||||
|
||||
type unifiedMetricCandidate struct {
|
||||
|
|
@ -70,6 +78,32 @@ func unifiedAlertType(typeKey string) string {
|
|||
return "Storage"
|
||||
case "pmg":
|
||||
return "PMG"
|
||||
case "k8s-cluster":
|
||||
return "Kubernetes Cluster"
|
||||
case "k8s-node":
|
||||
return "Kubernetes Node"
|
||||
case "k8s-deployment":
|
||||
return "Kubernetes Deployment"
|
||||
case "k8s-namespace":
|
||||
return "Kubernetes Namespace"
|
||||
case "pod":
|
||||
return "Kubernetes Pod"
|
||||
case "truenas-system":
|
||||
return "TrueNAS System"
|
||||
case "truenas-pool":
|
||||
return "TrueNAS Pool"
|
||||
case "truenas-dataset":
|
||||
return "TrueNAS Dataset"
|
||||
case "truenas-disk":
|
||||
return "TrueNAS Disk"
|
||||
case "vmware-host":
|
||||
return "vSphere Host"
|
||||
case "vmware-vm":
|
||||
return "vSphere VM"
|
||||
case "vmware-datastore":
|
||||
return "vSphere Datastore"
|
||||
case "vmware-network":
|
||||
return "vSphere Network"
|
||||
default:
|
||||
return typeKey
|
||||
}
|
||||
|
|
@ -85,6 +119,59 @@ func isUnifiedGuestType(typeKey string) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func supportsUnifiedIOMetrics(typeKey string) bool {
|
||||
switch typeKey {
|
||||
case "vm", "system-container", "app-container", "k8s-cluster", "k8s-deployment", "pod", "truenas-system", "vmware-host", "vmware-vm":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isUnifiedKubernetesAlertType(typeKey string) bool {
|
||||
switch typeKey {
|
||||
case "k8s-cluster", "k8s-node", "k8s-deployment", "k8s-namespace", "pod":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isUnifiedTrueNASAlertType(typeKey string) bool {
|
||||
switch typeKey {
|
||||
case "truenas-system", "truenas-pool", "truenas-dataset", "truenas-disk":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isUnifiedVMwareAlertType(typeKey string) bool {
|
||||
switch typeKey {
|
||||
case "vmware-host", "vmware-vm", "vmware-datastore", "vmware-network":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isUnifiedModernPlatformAlertType(typeKey string) bool {
|
||||
return isUnifiedKubernetesAlertType(typeKey) || isUnifiedTrueNASAlertType(typeKey) || isUnifiedVMwareAlertType(typeKey)
|
||||
}
|
||||
|
||||
func (m *Manager) unifiedPlatformAlertsDisabledNoLock(typeKey string) bool {
|
||||
switch {
|
||||
case isUnifiedKubernetesAlertType(typeKey):
|
||||
return m.config.DisableAllKubernetes
|
||||
case isUnifiedTrueNASAlertType(typeKey):
|
||||
return m.config.DisableAllTrueNAS
|
||||
case isUnifiedVMwareAlertType(typeKey):
|
||||
return m.config.DisableAllVMware
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// defaultThresholdsForResourceType returns the default ThresholdConfig for a resource type key.
|
||||
func (m *Manager) defaultThresholdsForResourceType(typeKey string) ThresholdConfig {
|
||||
switch typeKey {
|
||||
|
|
@ -98,6 +185,24 @@ func (m *Manager) defaultThresholdsForResourceType(typeKey string) ThresholdConf
|
|||
return cloneThresholdConfig(m.config.PBSDefaults)
|
||||
case "storage":
|
||||
return ThresholdConfig{Usage: cloneThreshold(&m.config.StorageDefault)}
|
||||
case "k8s-cluster", "k8s-node", "k8s-deployment", "pod":
|
||||
return cloneThresholdConfig(m.config.KubernetesDefaults)
|
||||
case "k8s-namespace":
|
||||
return ThresholdConfig{}
|
||||
case "truenas-system":
|
||||
return cloneThresholdConfig(m.config.TrueNASDefaults)
|
||||
case "truenas-pool", "truenas-dataset":
|
||||
defaults := cloneThresholdConfig(m.config.TrueNASDefaults)
|
||||
return ThresholdConfig{Usage: cloneThreshold(defaults.Usage)}
|
||||
case "truenas-disk":
|
||||
return cloneThresholdConfig(m.config.TrueNASDiskDefaults)
|
||||
case "vmware-host", "vmware-vm":
|
||||
return cloneThresholdConfig(m.config.VMwareDefaults)
|
||||
case "vmware-datastore":
|
||||
defaults := cloneThresholdConfig(m.config.VMwareDefaults)
|
||||
return ThresholdConfig{Usage: cloneThreshold(defaults.Usage)}
|
||||
case "vmware-network":
|
||||
return ThresholdConfig{}
|
||||
default:
|
||||
return ThresholdConfig{}
|
||||
}
|
||||
|
|
@ -181,20 +286,31 @@ func buildUnifiedMetricCandidates(input *UnifiedResourceInput, thresholds Thresh
|
|||
appendCandidate("memory", input.Memory, input.MemoryValue(), thresholds.Memory)
|
||||
appendCandidate("disk", input.Disk, input.DiskValue(), thresholds.Disk)
|
||||
|
||||
if isUnifiedGuestType(input.Type) {
|
||||
if supportsUnifiedIOMetrics(input.Type) {
|
||||
appendCandidate("diskRead", input.DiskRead, input.DiskReadValue(), thresholds.DiskRead)
|
||||
appendCandidate("diskWrite", input.DiskWrite, input.DiskWriteValue(), thresholds.DiskWrite)
|
||||
appendCandidate("networkIn", input.NetworkIn, input.NetworkInValue(), thresholds.NetworkIn)
|
||||
appendCandidate("networkOut", input.NetworkOut, input.NetworkOutValue(), thresholds.NetworkOut)
|
||||
}
|
||||
|
||||
if input.Type == "storage" && input.Disk != nil {
|
||||
if unifiedStorageUsageResourceType(input.Type) && input.Disk != nil {
|
||||
appendCandidate("usage", input.Disk, input.DiskValue(), thresholds.Usage)
|
||||
}
|
||||
|
||||
appendCandidate("temperature", input.Temperature, input.TemperatureValue(), thresholds.Temperature)
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
func unifiedStorageUsageResourceType(typeKey string) bool {
|
||||
switch typeKey {
|
||||
case "storage", "truenas-pool", "truenas-dataset", "vmware-datastore":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func buildCanonicalMetricSpec(resourceID, title string, resourceType unifiedresources.ResourceType, metricType string, threshold *HysteresisThreshold) (alertspecs.ResourceAlertSpec, error) {
|
||||
spec := alertspecs.ResourceAlertSpec{
|
||||
ID: canonicalMetricSpecID(resourceID, metricType),
|
||||
|
|
@ -249,11 +365,238 @@ func unifiedMetricResourceType(typeKey string) (unifiedresources.ResourceType, b
|
|||
return unifiedresources.ResourceTypeStorage, true
|
||||
case "pmg":
|
||||
return unifiedresources.ResourceTypePMG, true
|
||||
case "k8s-cluster":
|
||||
return unifiedresources.ResourceTypeK8sCluster, true
|
||||
case "k8s-node":
|
||||
return unifiedresources.ResourceTypeK8sNode, true
|
||||
case "k8s-deployment":
|
||||
return unifiedresources.ResourceTypeK8sDeployment, true
|
||||
case "k8s-namespace":
|
||||
return unifiedresources.ResourceTypeK8sNamespace, true
|
||||
case "pod":
|
||||
return unifiedresources.ResourceTypePod, true
|
||||
case "truenas-system":
|
||||
return unifiedresources.ResourceTypeAgent, true
|
||||
case "truenas-pool", "truenas-dataset":
|
||||
return unifiedresources.ResourceTypeStorage, true
|
||||
case "truenas-disk":
|
||||
return unifiedresources.ResourceTypePhysicalDisk, true
|
||||
case "vmware-host":
|
||||
return unifiedresources.ResourceTypeAgent, true
|
||||
case "vmware-vm":
|
||||
return unifiedresources.ResourceTypeVM, true
|
||||
case "vmware-datastore":
|
||||
return unifiedresources.ResourceTypeStorage, true
|
||||
case "vmware-network":
|
||||
return unifiedresources.ResourceTypeNetwork, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) CheckUnifiedResourceMetrics(resources []unifiedresources.Resource) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
for _, resource := range resources {
|
||||
input, ok := UnifiedResourceInputFromResource(resource)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
m.CheckUnifiedResource(input)
|
||||
}
|
||||
}
|
||||
|
||||
func UnifiedResourceInputFromResource(resource unifiedresources.Resource) (*UnifiedResourceInput, bool) {
|
||||
typeKey, ok := unifiedAlertResourceType(resource)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
resourceID := unifiedresources.CanonicalResourceID(resource.ID)
|
||||
if strings.TrimSpace(resourceID) == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
input := &UnifiedResourceInput{
|
||||
ID: resourceID,
|
||||
Type: typeKey,
|
||||
Name: unifiedResourceAlertName(resource),
|
||||
Node: unifiedResourceAlertNode(resource),
|
||||
Instance: unifiedResourceAlertInstance(resource, typeKey),
|
||||
}
|
||||
|
||||
if metrics := resource.Metrics; metrics != nil {
|
||||
input.CPU = unifiedResourceMetric(metrics.CPU)
|
||||
input.Memory = unifiedResourceMetric(metrics.Memory)
|
||||
input.Disk = unifiedResourceMetric(metrics.Disk)
|
||||
input.DiskRead = unifiedResourceMetric(metrics.DiskRead)
|
||||
input.DiskWrite = unifiedResourceMetric(metrics.DiskWrite)
|
||||
input.NetworkIn = unifiedResourceMetric(metrics.NetIn)
|
||||
input.NetworkOut = unifiedResourceMetric(metrics.NetOut)
|
||||
}
|
||||
if resource.Temperature != nil {
|
||||
input.Temperature = &UnifiedResourceMetric{Value: *resource.Temperature, Percent: *resource.Temperature}
|
||||
} else if resource.PhysicalDisk != nil && resource.PhysicalDisk.Temperature > 0 {
|
||||
value := float64(resource.PhysicalDisk.Temperature)
|
||||
input.Temperature = &UnifiedResourceMetric{Value: value, Percent: value}
|
||||
}
|
||||
|
||||
return input, true
|
||||
}
|
||||
|
||||
func unifiedAlertResourceType(resource unifiedresources.Resource) (string, bool) {
|
||||
resourceType := unifiedresources.CanonicalResourceType(resource.Type)
|
||||
switch resourceType {
|
||||
case unifiedresources.ResourceTypeK8sCluster,
|
||||
unifiedresources.ResourceTypeK8sNode,
|
||||
unifiedresources.ResourceTypeK8sDeployment,
|
||||
unifiedresources.ResourceTypeK8sNamespace,
|
||||
unifiedresources.ResourceTypePod:
|
||||
return string(resourceType), true
|
||||
case unifiedresources.ResourceTypeAgent:
|
||||
if resource.TrueNAS != nil || resourceHasSource(resource, unifiedresources.SourceTrueNAS) {
|
||||
return "truenas-system", true
|
||||
}
|
||||
if resource.VMware != nil || resourceHasSource(resource, unifiedresources.SourceVMware) {
|
||||
return "vmware-host", true
|
||||
}
|
||||
case unifiedresources.ResourceTypeVM:
|
||||
if resource.VMware != nil || resourceHasSource(resource, unifiedresources.SourceVMware) {
|
||||
return "vmware-vm", true
|
||||
}
|
||||
case unifiedresources.ResourceTypeStorage:
|
||||
if isTrueNASStorageResource(resource) {
|
||||
switch strings.ToLower(strings.TrimSpace(resource.Storage.Topology)) {
|
||||
case "dataset":
|
||||
return "truenas-dataset", true
|
||||
default:
|
||||
return "truenas-pool", true
|
||||
}
|
||||
}
|
||||
if isVMwareDatastoreResource(resource) {
|
||||
return "vmware-datastore", true
|
||||
}
|
||||
case unifiedresources.ResourceTypePhysicalDisk:
|
||||
if resourceHasSource(resource, unifiedresources.SourceTrueNAS) || resource.TrueNAS != nil {
|
||||
return "truenas-disk", true
|
||||
}
|
||||
case unifiedresources.ResourceTypeNetwork:
|
||||
if resource.VMware != nil || resourceHasSource(resource, unifiedresources.SourceVMware) {
|
||||
return "vmware-network", true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func isTrueNASStorageResource(resource unifiedresources.Resource) bool {
|
||||
if resource.TrueNAS != nil || resourceHasSource(resource, unifiedresources.SourceTrueNAS) {
|
||||
return true
|
||||
}
|
||||
if resource.Storage == nil {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(resource.Storage.Platform), string(unifiedresources.SourceTrueNAS))
|
||||
}
|
||||
|
||||
func isVMwareDatastoreResource(resource unifiedresources.Resource) bool {
|
||||
if resource.VMware != nil || resourceHasSource(resource, unifiedresources.SourceVMware) {
|
||||
return true
|
||||
}
|
||||
if resource.Storage == nil {
|
||||
return false
|
||||
}
|
||||
platform := strings.TrimSpace(resource.Storage.Platform)
|
||||
return strings.EqualFold(platform, "vmware-vsphere") || strings.EqualFold(platform, string(unifiedresources.SourceVMware))
|
||||
}
|
||||
|
||||
func resourceHasSource(resource unifiedresources.Resource, source unifiedresources.DataSource) bool {
|
||||
for _, candidate := range resource.Sources {
|
||||
if candidate == source {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if _, ok := resource.SourceStatus[source]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func unifiedResourceMetric(metric *unifiedresources.MetricValue) *UnifiedResourceMetric {
|
||||
if metric == nil {
|
||||
return nil
|
||||
}
|
||||
return &UnifiedResourceMetric{Value: metric.Value, Percent: metric.Percent}
|
||||
}
|
||||
|
||||
func unifiedResourceAlertName(resource unifiedresources.Resource) string {
|
||||
if resource.Canonical != nil && strings.TrimSpace(resource.Canonical.DisplayName) != "" {
|
||||
return strings.TrimSpace(resource.Canonical.DisplayName)
|
||||
}
|
||||
if strings.TrimSpace(resource.Name) != "" {
|
||||
return strings.TrimSpace(resource.Name)
|
||||
}
|
||||
return strings.TrimSpace(resource.ID)
|
||||
}
|
||||
|
||||
func unifiedResourceAlertNode(resource unifiedresources.Resource) string {
|
||||
if strings.TrimSpace(resource.ParentName) != "" {
|
||||
return strings.TrimSpace(resource.ParentName)
|
||||
}
|
||||
if resource.Canonical != nil && strings.TrimSpace(resource.Canonical.Hostname) != "" {
|
||||
return strings.TrimSpace(resource.Canonical.Hostname)
|
||||
}
|
||||
if resource.Kubernetes != nil {
|
||||
if strings.TrimSpace(resource.Kubernetes.NodeName) != "" {
|
||||
return strings.TrimSpace(resource.Kubernetes.NodeName)
|
||||
}
|
||||
if strings.TrimSpace(resource.Kubernetes.ClusterName) != "" {
|
||||
return strings.TrimSpace(resource.Kubernetes.ClusterName)
|
||||
}
|
||||
}
|
||||
if resource.TrueNAS != nil && strings.TrimSpace(resource.TrueNAS.Hostname) != "" {
|
||||
return strings.TrimSpace(resource.TrueNAS.Hostname)
|
||||
}
|
||||
if resource.VMware != nil {
|
||||
if strings.TrimSpace(resource.VMware.RuntimeHostName) != "" {
|
||||
return strings.TrimSpace(resource.VMware.RuntimeHostName)
|
||||
}
|
||||
if strings.TrimSpace(resource.VMware.ClusterName) != "" {
|
||||
return strings.TrimSpace(resource.VMware.ClusterName)
|
||||
}
|
||||
if strings.TrimSpace(resource.VMware.DatacenterName) != "" {
|
||||
return strings.TrimSpace(resource.VMware.DatacenterName)
|
||||
}
|
||||
if strings.TrimSpace(resource.VMware.VCenterHost) != "" {
|
||||
return strings.TrimSpace(resource.VMware.VCenterHost)
|
||||
}
|
||||
}
|
||||
return unifiedResourceAlertName(resource)
|
||||
}
|
||||
|
||||
func unifiedResourceAlertInstance(resource unifiedresources.Resource, typeKey string) string {
|
||||
if resource.Kubernetes != nil {
|
||||
if strings.TrimSpace(resource.Kubernetes.ClusterName) != "" {
|
||||
return strings.TrimSpace(resource.Kubernetes.ClusterName)
|
||||
}
|
||||
return "Kubernetes"
|
||||
}
|
||||
if strings.HasPrefix(typeKey, "truenas-") {
|
||||
return "TrueNAS"
|
||||
}
|
||||
if strings.HasPrefix(typeKey, "vmware-") {
|
||||
if resource.VMware != nil {
|
||||
if strings.TrimSpace(resource.VMware.ConnectionName) != "" {
|
||||
return strings.TrimSpace(resource.VMware.ConnectionName)
|
||||
}
|
||||
if strings.TrimSpace(resource.VMware.VCenterHost) != "" {
|
||||
return strings.TrimSpace(resource.VMware.VCenterHost)
|
||||
}
|
||||
}
|
||||
return "vSphere"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func mergeMetricOptions(base *metricOptions, extra map[string]interface{}) *metricOptions {
|
||||
if len(extra) == 0 {
|
||||
return base
|
||||
|
|
@ -329,6 +672,13 @@ func (i *UnifiedResourceInput) NetworkOutValue() float64 {
|
|||
return i.NetworkOut.Value
|
||||
}
|
||||
|
||||
func (i *UnifiedResourceInput) TemperatureValue() float64 {
|
||||
if i == nil || i.Temperature == nil {
|
||||
return 0
|
||||
}
|
||||
return i.Temperature.Value
|
||||
}
|
||||
|
||||
// CheckUnifiedResource evaluates threshold-based metric alerts for a unified resource.
|
||||
// It resolves thresholds (defaults + overrides) and calls checkMetric for each
|
||||
// available metric. Discrete event alerts (offline, RAID, backup age, etc.)
|
||||
|
|
@ -343,6 +693,10 @@ func (m *Manager) CheckUnifiedResource(input *UnifiedResourceInput) {
|
|||
m.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
if m.unifiedPlatformAlertsDisabledNoLock(input.Type) {
|
||||
m.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
thresholds := m.resolveResourceThresholds(input.Type, input.ID)
|
||||
m.mu.RUnlock()
|
||||
|
|
|
|||
|
|
@ -33,6 +33,39 @@ func unifiedEvalBaseConfig() AlertConfig {
|
|||
CPU: &HysteresisThreshold{Trigger: 80, Clear: 75},
|
||||
Memory: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
},
|
||||
KubernetesDefaults: ThresholdConfig{
|
||||
CPU: &HysteresisThreshold{Trigger: 80, Clear: 75},
|
||||
Memory: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
Disk: &HysteresisThreshold{Trigger: 90, Clear: 85},
|
||||
DiskRead: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
DiskWrite: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
NetworkIn: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
NetworkOut: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
},
|
||||
TrueNASDefaults: ThresholdConfig{
|
||||
CPU: &HysteresisThreshold{Trigger: 80, Clear: 75},
|
||||
Memory: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
Disk: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
Usage: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
Temperature: &HysteresisThreshold{Trigger: 80, Clear: 75},
|
||||
DiskRead: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
DiskWrite: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
NetworkIn: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
NetworkOut: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
},
|
||||
TrueNASDiskDefaults: ThresholdConfig{
|
||||
Temperature: &HysteresisThreshold{Trigger: 55, Clear: 50},
|
||||
},
|
||||
VMwareDefaults: ThresholdConfig{
|
||||
CPU: &HysteresisThreshold{Trigger: 80, Clear: 75},
|
||||
Memory: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
Disk: &HysteresisThreshold{Trigger: 90, Clear: 85},
|
||||
Usage: &HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
DiskRead: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
DiskWrite: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
NetworkIn: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
NetworkOut: &HysteresisThreshold{Trigger: 0, Clear: 0},
|
||||
},
|
||||
StorageDefault: HysteresisThreshold{Trigger: 85, Clear: 80},
|
||||
Overrides: map[string]ThresholdConfig{},
|
||||
|
||||
|
|
@ -173,6 +206,411 @@ func TestCheckUnifiedResourceMajorFamilies(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCheckUnifiedResourceSupportsKubernetesTrueNASAndVMwareMetricTargets(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
configureUnifiedEvalManager(t, m, unifiedEvalBaseConfig())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
alertID string
|
||||
input *UnifiedResourceInput
|
||||
wantResourceType string
|
||||
}{
|
||||
{
|
||||
name: "Kubernetes cluster CPU",
|
||||
alertID: canonicalMetricStateID("k8s:prod", "cpu"),
|
||||
input: &UnifiedResourceInput{
|
||||
ID: "k8s:prod",
|
||||
Type: "k8s-cluster",
|
||||
Name: "prod",
|
||||
CPU: &UnifiedResourceMetric{Percent: 88},
|
||||
},
|
||||
wantResourceType: "Kubernetes Cluster",
|
||||
},
|
||||
{
|
||||
name: "Kubernetes node memory",
|
||||
alertID: canonicalMetricStateID("k8s:prod/node:worker-1", "memory"),
|
||||
input: &UnifiedResourceInput{
|
||||
ID: "k8s:prod/node:worker-1",
|
||||
Type: "k8s-node",
|
||||
Name: "worker-1",
|
||||
Node: "worker-1",
|
||||
Memory: &UnifiedResourceMetric{Percent: 90},
|
||||
},
|
||||
wantResourceType: "Kubernetes Node",
|
||||
},
|
||||
{
|
||||
name: "Kubernetes deployment CPU",
|
||||
alertID: canonicalMetricStateID("k8s:prod/ns:default/deployment:api", "cpu"),
|
||||
input: &UnifiedResourceInput{
|
||||
ID: "k8s:prod/ns:default/deployment:api",
|
||||
Type: "k8s-deployment",
|
||||
Name: "api",
|
||||
Node: "prod",
|
||||
Instance: "prod",
|
||||
CPU: &UnifiedResourceMetric{Percent: 83},
|
||||
},
|
||||
wantResourceType: "Kubernetes Deployment",
|
||||
},
|
||||
{
|
||||
name: "Kubernetes pod disk",
|
||||
alertID: canonicalMetricStateID("k8s:prod/ns:default/pod:api-7d9f", "disk"),
|
||||
input: &UnifiedResourceInput{
|
||||
ID: "k8s:prod/ns:default/pod:api-7d9f",
|
||||
Type: "pod",
|
||||
Name: "api-7d9f",
|
||||
Node: "worker-1",
|
||||
Instance: "prod",
|
||||
Disk: &UnifiedResourceMetric{Percent: 93},
|
||||
},
|
||||
wantResourceType: "Kubernetes Pod",
|
||||
},
|
||||
{
|
||||
name: "TrueNAS system temperature",
|
||||
alertID: canonicalMetricStateID("agent:truenas-main", "temperature"),
|
||||
input: &UnifiedResourceInput{
|
||||
ID: "agent:truenas-main",
|
||||
Type: "truenas-system",
|
||||
Name: "truenas-main",
|
||||
Node: "truenas-main",
|
||||
Temperature: &UnifiedResourceMetric{Value: 82, Percent: 82},
|
||||
},
|
||||
wantResourceType: "TrueNAS System",
|
||||
},
|
||||
{
|
||||
name: "TrueNAS pool usage",
|
||||
alertID: canonicalMetricStateID("storage:truenas-main/pool:tank", "usage"),
|
||||
input: &UnifiedResourceInput{
|
||||
ID: "storage:truenas-main/pool:tank",
|
||||
Type: "truenas-pool",
|
||||
Name: "tank",
|
||||
Node: "truenas-main",
|
||||
Instance: "TrueNAS",
|
||||
Disk: &UnifiedResourceMetric{Percent: 89},
|
||||
},
|
||||
wantResourceType: "TrueNAS Pool",
|
||||
},
|
||||
{
|
||||
name: "TrueNAS dataset usage",
|
||||
alertID: canonicalMetricStateID("storage:truenas-main/dataset:tank/apps", "usage"),
|
||||
input: &UnifiedResourceInput{
|
||||
ID: "storage:truenas-main/dataset:tank/apps",
|
||||
Type: "truenas-dataset",
|
||||
Name: "tank/apps",
|
||||
Node: "truenas-main",
|
||||
Instance: "TrueNAS",
|
||||
Disk: &UnifiedResourceMetric{Percent: 91},
|
||||
},
|
||||
wantResourceType: "TrueNAS Dataset",
|
||||
},
|
||||
{
|
||||
name: "TrueNAS disk temperature",
|
||||
alertID: canonicalMetricStateID("physical-disk:truenas-main/ada0", "temperature"),
|
||||
input: &UnifiedResourceInput{
|
||||
ID: "physical-disk:truenas-main/ada0",
|
||||
Type: "truenas-disk",
|
||||
Name: "ada0",
|
||||
Node: "truenas-main",
|
||||
Instance: "TrueNAS",
|
||||
Temperature: &UnifiedResourceMetric{Value: 58, Percent: 58},
|
||||
},
|
||||
wantResourceType: "TrueNAS Disk",
|
||||
},
|
||||
{
|
||||
name: "vSphere host CPU",
|
||||
alertID: canonicalMetricStateID("vmware:vc-1:host:host-101", "cpu"),
|
||||
input: &UnifiedResourceInput{
|
||||
ID: "vmware:vc-1:host:host-101",
|
||||
Type: "vmware-host",
|
||||
Name: "esxi-01.lab.local",
|
||||
Node: "Prod Compute",
|
||||
Instance: "Lab vCenter",
|
||||
CPU: &UnifiedResourceMetric{Percent: 88},
|
||||
},
|
||||
wantResourceType: "vSphere Host",
|
||||
},
|
||||
{
|
||||
name: "vSphere VM disk",
|
||||
alertID: canonicalMetricStateID("vmware:vc-1:vm:vm-201", "disk"),
|
||||
input: &UnifiedResourceInput{
|
||||
ID: "vmware:vc-1:vm:vm-201",
|
||||
Type: "vmware-vm",
|
||||
Name: "app-01",
|
||||
Node: "esxi-01.lab.local",
|
||||
Instance: "Lab vCenter",
|
||||
Disk: &UnifiedResourceMetric{Percent: 94},
|
||||
},
|
||||
wantResourceType: "vSphere VM",
|
||||
},
|
||||
{
|
||||
name: "vSphere datastore usage",
|
||||
alertID: canonicalMetricStateID("vmware:vc-1:datastore:datastore-301", "usage"),
|
||||
input: &UnifiedResourceInput{
|
||||
ID: "vmware:vc-1:datastore:datastore-301",
|
||||
Type: "vmware-datastore",
|
||||
Name: "nvme-primary",
|
||||
Node: "Lab Datacenter",
|
||||
Instance: "Lab vCenter",
|
||||
Disk: &UnifiedResourceMetric{Percent: 89},
|
||||
},
|
||||
wantResourceType: "vSphere Datastore",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m.ClearActiveAlerts()
|
||||
m.CheckUnifiedResource(tt.input)
|
||||
|
||||
alert := activeAlert(t, m, tt.alertID)
|
||||
if got := alert.Metadata["resourceType"]; got != tt.wantResourceType {
|
||||
t.Fatalf("resourceType metadata = %v, want %s", got, tt.wantResourceType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckUnifiedResourceUsesKubernetesTrueNASAndVMwareOverrides(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
cfg := unifiedEvalBaseConfig()
|
||||
cfg.KubernetesDefaults.CPU = &HysteresisThreshold{Trigger: 95, Clear: 90}
|
||||
cfg.TrueNASDefaults.Usage = &HysteresisThreshold{Trigger: 95, Clear: 90}
|
||||
cfg.VMwareDefaults.CPU = &HysteresisThreshold{Trigger: 95, Clear: 90}
|
||||
cfg.Overrides["k8s:prod/ns:default/pod:api-7d9f"] = ThresholdConfig{
|
||||
CPU: &HysteresisThreshold{Trigger: 60, Clear: 55},
|
||||
}
|
||||
cfg.Overrides["storage:truenas-main/pool:tank"] = ThresholdConfig{
|
||||
Usage: &HysteresisThreshold{Trigger: 60, Clear: 55},
|
||||
}
|
||||
cfg.Overrides["vmware:vc-1:host:host-101"] = ThresholdConfig{
|
||||
CPU: &HysteresisThreshold{Trigger: 60, Clear: 55},
|
||||
}
|
||||
configureUnifiedEvalManager(t, m, cfg)
|
||||
|
||||
m.CheckUnifiedResource(&UnifiedResourceInput{
|
||||
ID: "k8s:prod/ns:default/pod:api-7d9f",
|
||||
Type: "pod",
|
||||
Name: "api-7d9f",
|
||||
CPU: &UnifiedResourceMetric{Percent: 65},
|
||||
})
|
||||
m.CheckUnifiedResource(&UnifiedResourceInput{
|
||||
ID: "storage:truenas-main/pool:tank",
|
||||
Type: "truenas-pool",
|
||||
Name: "tank",
|
||||
Disk: &UnifiedResourceMetric{Percent: 65},
|
||||
})
|
||||
m.CheckUnifiedResource(&UnifiedResourceInput{
|
||||
ID: "vmware:vc-1:host:host-101",
|
||||
Type: "vmware-host",
|
||||
Name: "esxi-01.lab.local",
|
||||
CPU: &UnifiedResourceMetric{Percent: 65},
|
||||
})
|
||||
|
||||
k8sAlert := activeAlert(t, m, canonicalMetricStateID("k8s:prod/ns:default/pod:api-7d9f", "cpu"))
|
||||
if k8sAlert.Threshold != 60 {
|
||||
t.Fatalf("kubernetes override threshold = %v, want 60", k8sAlert.Threshold)
|
||||
}
|
||||
truenasAlert := activeAlert(t, m, canonicalMetricStateID("storage:truenas-main/pool:tank", "usage"))
|
||||
if truenasAlert.Threshold != 60 {
|
||||
t.Fatalf("truenas override threshold = %v, want 60", truenasAlert.Threshold)
|
||||
}
|
||||
vmwareAlert := activeAlert(t, m, canonicalMetricStateID("vmware:vc-1:host:host-101", "cpu"))
|
||||
if vmwareAlert.Threshold != 60 {
|
||||
t.Fatalf("vmware override threshold = %v, want 60", vmwareAlert.Threshold)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnifiedResourceInputFromKubernetesTrueNASAndVMwareResources(t *testing.T) {
|
||||
pod := unifiedresources.Resource{
|
||||
ID: " k8s:prod/ns:default/pod:api-7d9f ",
|
||||
Type: unifiedresources.ResourceTypePod,
|
||||
Name: "api-7d9f",
|
||||
ParentName: "worker-1",
|
||||
Sources: []unifiedresources.DataSource{unifiedresources.SourceK8s},
|
||||
Kubernetes: &unifiedresources.K8sData{
|
||||
ClusterName: "prod",
|
||||
NodeName: "worker-1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Metrics: &unifiedresources.ResourceMetrics{
|
||||
CPU: &unifiedresources.MetricValue{Percent: 82},
|
||||
Memory: &unifiedresources.MetricValue{Percent: 64},
|
||||
},
|
||||
}
|
||||
podInput, ok := UnifiedResourceInputFromResource(pod)
|
||||
if !ok {
|
||||
t.Fatalf("expected Kubernetes pod resource to become unified alert input")
|
||||
}
|
||||
if podInput.Type != "pod" || podInput.ID != "k8s:prod/ns:default/pod:api-7d9f" || podInput.Node != "worker-1" || podInput.Instance != "prod" {
|
||||
t.Fatalf("unexpected pod input: %+v", podInput)
|
||||
}
|
||||
if podInput.CPU == nil || podInput.CPU.Percent != 82 {
|
||||
t.Fatalf("unexpected pod CPU metric: %+v", podInput.CPU)
|
||||
}
|
||||
|
||||
pool := unifiedresources.Resource{
|
||||
ID: "storage:truenas-main/pool:tank",
|
||||
Type: unifiedresources.ResourceTypeStorage,
|
||||
Name: "tank",
|
||||
ParentName: "truenas-main",
|
||||
Sources: []unifiedresources.DataSource{unifiedresources.SourceTrueNAS},
|
||||
Storage: &unifiedresources.StorageMeta{
|
||||
Platform: string(unifiedresources.SourceTrueNAS),
|
||||
Topology: "pool",
|
||||
},
|
||||
Metrics: &unifiedresources.ResourceMetrics{
|
||||
Disk: &unifiedresources.MetricValue{Percent: 89},
|
||||
},
|
||||
}
|
||||
poolInput, ok := UnifiedResourceInputFromResource(pool)
|
||||
if !ok {
|
||||
t.Fatalf("expected TrueNAS pool resource to become unified alert input")
|
||||
}
|
||||
if poolInput.Type != "truenas-pool" || poolInput.ID != "storage:truenas-main/pool:tank" || poolInput.Node != "truenas-main" || poolInput.Instance != "TrueNAS" {
|
||||
t.Fatalf("unexpected pool input: %+v", poolInput)
|
||||
}
|
||||
if poolInput.Disk == nil || poolInput.Disk.Percent != 89 {
|
||||
t.Fatalf("unexpected pool disk metric: %+v", poolInput.Disk)
|
||||
}
|
||||
|
||||
temp := 58.0
|
||||
disk := unifiedresources.Resource{
|
||||
ID: "physical-disk:truenas-main/ada0",
|
||||
Type: unifiedresources.ResourceTypePhysicalDisk,
|
||||
Name: "ada0",
|
||||
ParentName: "truenas-main",
|
||||
Sources: []unifiedresources.DataSource{unifiedresources.SourceTrueNAS},
|
||||
Temperature: &temp,
|
||||
}
|
||||
diskInput, ok := UnifiedResourceInputFromResource(disk)
|
||||
if !ok {
|
||||
t.Fatalf("expected TrueNAS disk resource to become unified alert input")
|
||||
}
|
||||
if diskInput.Type != "truenas-disk" || diskInput.Temperature == nil || diskInput.Temperature.Value != 58 {
|
||||
t.Fatalf("unexpected disk input: %+v", diskInput)
|
||||
}
|
||||
|
||||
vmwareHost := unifiedresources.Resource{
|
||||
ID: " vmware:vc-1:host:host-101 ",
|
||||
Type: unifiedresources.ResourceTypeAgent,
|
||||
Name: "esxi-01.lab.local",
|
||||
Sources: []unifiedresources.DataSource{unifiedresources.SourceVMware},
|
||||
VMware: &unifiedresources.VMwareData{
|
||||
ConnectionName: "Lab vCenter",
|
||||
ClusterName: "Prod Compute",
|
||||
VCenterHost: "vcenter.lab.local",
|
||||
},
|
||||
Metrics: &unifiedresources.ResourceMetrics{
|
||||
CPU: &unifiedresources.MetricValue{Percent: 82},
|
||||
},
|
||||
}
|
||||
hostInput, ok := UnifiedResourceInputFromResource(vmwareHost)
|
||||
if !ok {
|
||||
t.Fatalf("expected VMware host resource to become unified alert input")
|
||||
}
|
||||
if hostInput.Type != "vmware-host" || hostInput.ID != "vmware:vc-1:host:host-101" || hostInput.Node != "Prod Compute" || hostInput.Instance != "Lab vCenter" {
|
||||
t.Fatalf("unexpected VMware host input: %+v", hostInput)
|
||||
}
|
||||
|
||||
vmwareVM := unifiedresources.Resource{
|
||||
ID: "vmware:vc-1:vm:vm-201",
|
||||
Type: unifiedresources.ResourceTypeVM,
|
||||
Name: "app-01",
|
||||
ParentName: "esxi-01.lab.local",
|
||||
Sources: []unifiedresources.DataSource{unifiedresources.SourceVMware},
|
||||
VMware: &unifiedresources.VMwareData{
|
||||
ConnectionName: "Lab vCenter",
|
||||
RuntimeHostName: "esxi-01.lab.local",
|
||||
},
|
||||
Metrics: &unifiedresources.ResourceMetrics{
|
||||
Disk: &unifiedresources.MetricValue{Percent: 94},
|
||||
},
|
||||
}
|
||||
vmInput, ok := UnifiedResourceInputFromResource(vmwareVM)
|
||||
if !ok {
|
||||
t.Fatalf("expected VMware VM resource to become unified alert input")
|
||||
}
|
||||
if vmInput.Type != "vmware-vm" || vmInput.Node != "esxi-01.lab.local" || vmInput.Instance != "Lab vCenter" || vmInput.Disk == nil || vmInput.Disk.Percent != 94 {
|
||||
t.Fatalf("unexpected VMware VM input: %+v", vmInput)
|
||||
}
|
||||
|
||||
vmwareDatastore := unifiedresources.Resource{
|
||||
ID: "vmware:vc-1:datastore:datastore-301",
|
||||
Type: unifiedresources.ResourceTypeStorage,
|
||||
Name: "nvme-primary",
|
||||
Sources: []unifiedresources.DataSource{unifiedresources.SourceVMware},
|
||||
Storage: &unifiedresources.StorageMeta{
|
||||
Platform: "vmware-vsphere",
|
||||
Topology: "datastore",
|
||||
},
|
||||
VMware: &unifiedresources.VMwareData{
|
||||
ConnectionName: "Lab vCenter",
|
||||
DatacenterName: "Lab Datacenter",
|
||||
},
|
||||
Metrics: &unifiedresources.ResourceMetrics{
|
||||
Disk: &unifiedresources.MetricValue{Percent: 89},
|
||||
},
|
||||
}
|
||||
datastoreInput, ok := UnifiedResourceInputFromResource(vmwareDatastore)
|
||||
if !ok {
|
||||
t.Fatalf("expected VMware datastore resource to become unified alert input")
|
||||
}
|
||||
if datastoreInput.Type != "vmware-datastore" || datastoreInput.Node != "Lab Datacenter" || datastoreInput.Instance != "Lab vCenter" {
|
||||
t.Fatalf("unexpected VMware datastore input: %+v", datastoreInput)
|
||||
}
|
||||
|
||||
vmwareNetwork := unifiedresources.Resource{
|
||||
ID: "vmware:vc-1:network:network-401",
|
||||
Type: unifiedresources.ResourceTypeNetwork,
|
||||
Name: "VM Network",
|
||||
Sources: []unifiedresources.DataSource{unifiedresources.SourceVMware},
|
||||
VMware: &unifiedresources.VMwareData{
|
||||
ConnectionName: "Lab vCenter",
|
||||
DatacenterName: "Lab Datacenter",
|
||||
EntityType: "network",
|
||||
},
|
||||
}
|
||||
networkInput, ok := UnifiedResourceInputFromResource(vmwareNetwork)
|
||||
if !ok {
|
||||
t.Fatalf("expected VMware network resource to become unified alert input")
|
||||
}
|
||||
if networkInput.Type != "vmware-network" || networkInput.Node != "Lab Datacenter" || networkInput.Instance != "Lab vCenter" {
|
||||
t.Fatalf("unexpected VMware network input: %+v", networkInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckUnifiedResourceHonorsKubernetesTrueNASAndVMwareGlobalDisables(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
cfg := unifiedEvalBaseConfig()
|
||||
cfg.DisableAllKubernetes = true
|
||||
cfg.DisableAllTrueNAS = true
|
||||
cfg.DisableAllVMware = true
|
||||
configureUnifiedEvalManager(t, m, cfg)
|
||||
|
||||
m.CheckUnifiedResource(&UnifiedResourceInput{
|
||||
ID: "k8s:prod",
|
||||
Type: "k8s-cluster",
|
||||
Name: "prod",
|
||||
CPU: &UnifiedResourceMetric{Percent: 90},
|
||||
})
|
||||
m.CheckUnifiedResource(&UnifiedResourceInput{
|
||||
ID: "storage:truenas-main/pool:tank",
|
||||
Type: "truenas-pool",
|
||||
Name: "tank",
|
||||
Disk: &UnifiedResourceMetric{Percent: 90},
|
||||
})
|
||||
m.CheckUnifiedResource(&UnifiedResourceInput{
|
||||
ID: "vmware:vc-1:host:host-101",
|
||||
Type: "vmware-host",
|
||||
Name: "esxi-01.lab.local",
|
||||
CPU: &UnifiedResourceMetric{Percent: 90},
|
||||
})
|
||||
|
||||
assertAlertMissing(t, m, canonicalMetricStateID("k8s:prod", "cpu"))
|
||||
assertAlertMissing(t, m, canonicalMetricStateID("storage:truenas-main/pool:tank", "usage"))
|
||||
assertAlertMissing(t, m, canonicalMetricStateID("vmware:vc-1:host:host-101", "cpu"))
|
||||
}
|
||||
|
||||
func TestCheckUnifiedResourceRejectsLegacyGuestTypeAlias(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
configureUnifiedEvalManager(t, m, unifiedEvalBaseConfig())
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ func (m *Manager) SyncUnifiedResourceIncidents(resources []unifiedresources.Reso
|
|||
m.mu.RLock()
|
||||
enabled := m.config.Enabled
|
||||
disableAllStorage := m.config.DisableAllStorage
|
||||
disableAllKubernetes := m.config.DisableAllKubernetes
|
||||
disableAllTrueNAS := m.config.DisableAllTrueNAS
|
||||
disableAllVMware := m.config.DisableAllVMware
|
||||
overrides := m.config.Overrides
|
||||
m.mu.RUnlock()
|
||||
|
||||
|
|
@ -65,7 +68,18 @@ func (m *Manager) SyncUnifiedResourceIncidents(resources []unifiedresources.Reso
|
|||
if !resourceSupportsUnifiedIncidentAlerts(resource) {
|
||||
continue
|
||||
}
|
||||
if disableAllStorage {
|
||||
if alertType, ok := unifiedAlertResourceType(resource); ok {
|
||||
if disableAllKubernetes && isUnifiedKubernetesAlertType(alertType) {
|
||||
continue
|
||||
}
|
||||
if disableAllTrueNAS && isUnifiedTrueNASAlertType(alertType) {
|
||||
continue
|
||||
}
|
||||
if disableAllVMware && isUnifiedVMwareAlertType(alertType) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if disableAllStorage && (resource.Type == unifiedresources.ResourceTypeStorage || resource.Type == unifiedresources.ResourceTypePhysicalDisk) {
|
||||
continue
|
||||
}
|
||||
if override, exists := overrides[spec.ResourceID]; exists && override.Disabled {
|
||||
|
|
@ -232,6 +246,14 @@ func resourceSupportsUnifiedIncidentAlerts(resource unifiedresources.Resource) b
|
|||
return len(resource.Incidents) > 0
|
||||
case unifiedresources.ResourceTypePBS:
|
||||
return len(resource.Incidents) > 0
|
||||
case unifiedresources.ResourceTypeNetwork:
|
||||
return len(resource.Incidents) > 0
|
||||
case unifiedresources.ResourceTypeK8sCluster,
|
||||
unifiedresources.ResourceTypeK8sNode,
|
||||
unifiedresources.ResourceTypeK8sNamespace,
|
||||
unifiedresources.ResourceTypeK8sDeployment,
|
||||
unifiedresources.ResourceTypePod:
|
||||
return len(resource.Incidents) > 0
|
||||
case unifiedresources.ResourceTypeNetworkEndpoint:
|
||||
return len(resource.Incidents) > 0
|
||||
default:
|
||||
|
|
@ -312,6 +334,20 @@ func unifiedIncidentNode(resource unifiedresources.Resource) string {
|
|||
if resource.Canonical != nil && strings.TrimSpace(resource.Canonical.Hostname) != "" {
|
||||
return strings.TrimSpace(resource.Canonical.Hostname)
|
||||
}
|
||||
if resource.VMware != nil {
|
||||
if strings.TrimSpace(resource.VMware.RuntimeHostName) != "" {
|
||||
return strings.TrimSpace(resource.VMware.RuntimeHostName)
|
||||
}
|
||||
if strings.TrimSpace(resource.VMware.ClusterName) != "" {
|
||||
return strings.TrimSpace(resource.VMware.ClusterName)
|
||||
}
|
||||
if strings.TrimSpace(resource.VMware.DatacenterName) != "" {
|
||||
return strings.TrimSpace(resource.VMware.DatacenterName)
|
||||
}
|
||||
if strings.TrimSpace(resource.VMware.VCenterHost) != "" {
|
||||
return strings.TrimSpace(resource.VMware.VCenterHost)
|
||||
}
|
||||
}
|
||||
return unifiedIncidentResourceName(resource)
|
||||
}
|
||||
|
||||
|
|
@ -325,7 +361,13 @@ func unifiedIncidentInstance(resource unifiedresources.Resource) string {
|
|||
case resource.PBS != nil:
|
||||
return "PBS"
|
||||
case resource.VMware != nil:
|
||||
return "VMware"
|
||||
if strings.TrimSpace(resource.VMware.ConnectionName) != "" {
|
||||
return strings.TrimSpace(resource.VMware.ConnectionName)
|
||||
}
|
||||
if strings.TrimSpace(resource.VMware.VCenterHost) != "" {
|
||||
return strings.TrimSpace(resource.VMware.VCenterHost)
|
||||
}
|
||||
return "vSphere"
|
||||
case resource.Availability != nil:
|
||||
return "Availability"
|
||||
case resource.Storage != nil && strings.TrimSpace(resource.Storage.Platform) != "":
|
||||
|
|
|
|||
|
|
@ -909,6 +909,48 @@ func TestSyncUnifiedResourceIncidentsRespectsDisableAllStorage(t *testing.T) {
|
|||
assertAlertMissing(t, m, alertID)
|
||||
}
|
||||
|
||||
func TestSyncUnifiedResourceIncidentsSupportsVMwareNetworkIncidentsAndGlobalDisable(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
cfg := unifiedEvalBaseConfig()
|
||||
configureUnifiedEvalManager(t, m, cfg)
|
||||
|
||||
resource := unifiedresources.Resource{
|
||||
ID: "vmware:vc-1:network:network-401",
|
||||
Type: unifiedresources.ResourceTypeNetwork,
|
||||
Name: "VM Network",
|
||||
Sources: []unifiedresources.DataSource{unifiedresources.SourceVMware},
|
||||
VMware: &unifiedresources.VMwareData{
|
||||
ConnectionName: "Lab vCenter",
|
||||
DatacenterName: "Lab Datacenter",
|
||||
EntityType: "network",
|
||||
},
|
||||
Incidents: []unifiedresources.ResourceIncident{{
|
||||
Provider: "vmware",
|
||||
NativeID: "alarm-401",
|
||||
Code: "vmware_alarm_state",
|
||||
Severity: storagehealth.RiskWarning,
|
||||
Summary: "vSphere network has an active alarm",
|
||||
}},
|
||||
}
|
||||
|
||||
m.SyncUnifiedResourceIncidents([]unifiedresources.Resource{resource})
|
||||
|
||||
alertID := "unified-incident-vmware-vc-1-network-network-401-vmware-alarm-401-vmware-alarm-state"
|
||||
assertAlertPresent(t, m, alertID)
|
||||
|
||||
cfg.DisableAllVMware = true
|
||||
m.UpdateConfig(cfg)
|
||||
m.mu.Lock()
|
||||
m.config.TimeThresholds = map[string]int{}
|
||||
m.config.MetricTimeThresholds = nil
|
||||
m.config.SuppressionWindow = 0
|
||||
m.config.MinimumDelta = 0
|
||||
m.mu.Unlock()
|
||||
m.SyncUnifiedResourceIncidents([]unifiedresources.Resource{resource})
|
||||
|
||||
assertAlertMissing(t, m, alertID)
|
||||
}
|
||||
|
||||
func TestDeriveCanonicalIdentityDoesNotMutateLegacyAlert(t *testing.T) {
|
||||
alert := &Alert{
|
||||
ID: "node-offline-pve-a",
|
||||
|
|
|
|||
|
|
@ -1453,16 +1453,71 @@ func TestCanonicalResourceTypeKeys(t *testing.T) {
|
|||
{name: "pbs canonical + node fallback", resourceType: "pbs", want: []string{"pbs", "node"}},
|
||||
{name: "pmg canonical + node fallback", resourceType: "pmg", want: []string{"pmg", "node"}},
|
||||
{
|
||||
name: "k8s cluster canonical + guest fallback",
|
||||
name: "k8s cluster canonical + node fallback",
|
||||
resourceType: "k8s-cluster",
|
||||
want: []string{"k8s-cluster", "guest"},
|
||||
want: []string{"k8s-cluster", "node"},
|
||||
},
|
||||
{
|
||||
name: "k8s node canonical + node fallback",
|
||||
resourceType: "k8s-node",
|
||||
want: []string{"k8s-node", "node"},
|
||||
},
|
||||
{
|
||||
name: "k8s deployment canonical + guest fallback",
|
||||
resourceType: "k8s-deployment",
|
||||
want: []string{"k8s-deployment", "guest"},
|
||||
},
|
||||
{
|
||||
name: "k8s namespace canonical",
|
||||
resourceType: "k8s-namespace",
|
||||
want: []string{"k8s-namespace"},
|
||||
},
|
||||
{
|
||||
name: "kubernetes display alias maps to pod",
|
||||
resourceType: "Kubernetes Pod",
|
||||
want: []string{"pod", "guest"},
|
||||
},
|
||||
{name: "pod canonical + guest fallback", resourceType: "pod", want: []string{"pod", "guest"}},
|
||||
{
|
||||
name: "truenas system canonical + agent fallback",
|
||||
resourceType: "truenas-system",
|
||||
want: []string{"truenas-system", "agent", "node"},
|
||||
},
|
||||
{
|
||||
name: "truenas pool canonical + storage fallback",
|
||||
resourceType: "truenas-pool",
|
||||
want: []string{"truenas-pool", "storage"},
|
||||
},
|
||||
{
|
||||
name: "truenas dataset display alias",
|
||||
resourceType: "TrueNAS Dataset",
|
||||
want: []string{"truenas-dataset", "storage"},
|
||||
},
|
||||
{
|
||||
name: "truenas disk canonical + disk/storage fallback",
|
||||
resourceType: "truenas-disk",
|
||||
want: []string{"truenas-disk", "physical_disk", "disk", "storage"},
|
||||
},
|
||||
{
|
||||
name: "vSphere host canonical + agent fallback",
|
||||
resourceType: "vSphere Host",
|
||||
want: []string{"vmware-host", "agent", "node"},
|
||||
},
|
||||
{
|
||||
name: "VMware VM canonical + guest fallback",
|
||||
resourceType: "VMware VM",
|
||||
want: []string{"vmware-vm", "vm", "guest"},
|
||||
},
|
||||
{
|
||||
name: "vSphere datastore canonical + storage fallback",
|
||||
resourceType: "vSphere Datastore",
|
||||
want: []string{"vmware-datastore", "storage"},
|
||||
},
|
||||
{
|
||||
name: "VMware network canonical + network fallback",
|
||||
resourceType: "VMware Network",
|
||||
want: []string{"vmware-network", "network"},
|
||||
},
|
||||
{name: "storage canonical", resourceType: "storage", want: []string{"storage"}},
|
||||
{name: "disk canonical + storage fallback", resourceType: "disk", want: []string{"disk", "storage"}},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -260,6 +260,26 @@ func TestAlertStateSyncDoesNotLogNormalResolvedStateAtInfo(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestUnifiedResourceAlertSyncEvaluatesMetricsBeforeIncidents(t *testing.T) {
|
||||
data, err := os.ReadFile("monitor_alert_sync.go")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read monitor_alert_sync.go: %v", err)
|
||||
}
|
||||
source := string(data)
|
||||
|
||||
metricIndex := strings.Index(source, "CheckUnifiedResourceMetrics(resources)")
|
||||
incidentIndex := strings.Index(source, "SyncUnifiedResourceIncidents(resources)")
|
||||
if metricIndex < 0 {
|
||||
t.Fatalf("monitor alert sync must run unified resource metric evaluation")
|
||||
}
|
||||
if incidentIndex < 0 {
|
||||
t.Fatalf("monitor alert sync must preserve unified resource incident sync")
|
||||
}
|
||||
if metricIndex > incidentIndex {
|
||||
t.Fatalf("unified resource metric evaluation must run before incident sync")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateBroadcastTreatsTypedNilHubAsAbsent(t *testing.T) {
|
||||
data, err := os.ReadFile("monitor.go")
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ func (m *Monitor) syncUnifiedResourceAlertsToState(resources []unifiedresources.
|
|||
return
|
||||
}
|
||||
|
||||
m.alertManager.CheckUnifiedResourceMetrics(resources)
|
||||
m.alertManager.SyncUnifiedResourceIncidents(resources)
|
||||
m.syncAlertsToState()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2040,6 +2040,7 @@ index 1111111..2222222 100644
|
|||
"frontend-modern/src/features/alerts/thresholds/hooks/__tests__/useThresholdsTableState.test.tsx",
|
||||
"frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts",
|
||||
"frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts",
|
||||
"frontend-modern/src/utils/__tests__/alertTargetTypes.test.ts",
|
||||
],
|
||||
}
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue