mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 04:07:16 +00:00
fix(alerts): single global defaults editor for recovery and snapshots
The Recovery and Snapshot Age threshold sections rendered two Global Defaults surfaces at once, the live editor row plus a synthetic read-only resource row of the same values. Both looked editable but the synthetic row's pencil editor saved through findThresholdResource, which never finds the pseudo-resource, so its edits were dropped silently. Reported in #1680 by FabienD74. The synthetic rows are gone; the always-live editor row is the single surface in both table and card layouts. The dead editScope field and its unreachable saveEdit branches go with them. The size columns move from Recovery to Snapshot Age where they belong. BackupAlertConfig has no size fields, so on Recovery they always rendered an inert Off. SnapshotAlertConfig sizes are live in the alert engine (backup_snapshot.go) but were previously invisible and, worse, unpersistable. The records now carry the normalized warningSizeGiB and criticalSizeGiB metric keys the column editor reads and writes, and applyAlertsConfigToSnapshot plus buildAlertsConfigurationPayload carry the size fields through load and save instead of stripping them. Verified end to end against the mock dev stack, PUT then GET returns the saved sizes and a reload shows them. Guests in the VMs & Containers section now sort by display name with vmid as tiebreaker. The old vmid-first order looked random because the rows do not render the vmid, which is what the sorting complaint in #1680 was about. Dropping the phantom columns and the per-row bulk-edit checkbox also brings the Recovery table under the desktop width budget, so it renders as a table at 1280px and up instead of falling back to cards.
This commit is contained in:
@@ -459,6 +459,24 @@ templates, threshold ignored-prefix input, and incident timeline notes must
|
||||
compose `FormTextarea` for label/id/help wiring and textarea chrome instead of
|
||||
recreating raw labelled `<textarea>` shells locally. Alert resource row/mobile
|
||||
note editors now follow the same primitive contract.
|
||||
The Recovery and Snapshot Age threshold sections expose exactly one Global
|
||||
Defaults surface: the always-live editor row (table layout) or card (card
|
||||
layout) that the shared resource table renders when global defaults are
|
||||
supplied. They must not add a synthetic read-only resource row mirroring the
|
||||
same record; a second surface invites edits that no override mutation path
|
||||
persists, which is how #1680's silently-dropped edits happened. Snapshot size
|
||||
thresholds belong to the Snapshot Age section, not Recovery, because
|
||||
`BackupAlertConfig` has no size dimension. The recovery-defaults records carry
|
||||
the normalized `warningSizeGiB` and `criticalSizeGiB` metric keys that the
|
||||
column editor resolves for the size columns, and the configuration snapshot
|
||||
layer must round-trip those fields: `applyAlertsConfigToSnapshot` and
|
||||
`buildAlertsConfigurationPayload` carry them between the UI snapshot and
|
||||
`/api/alerts/config` rather than stripping them. Guests in the VMs &
|
||||
Containers section order by display name (numeric-aware, case-insensitive)
|
||||
with vmid as tiebreaker, because the rows render the name and an invisible
|
||||
sort key reads as an unsorted list. Regression ownership is
|
||||
`frontend-modern/src/utils/__tests__/metricThresholds.test.ts` and
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/__tests__/useThresholdsRecoveryDefaultsState.test.tsx`.
|
||||
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
|
||||
|
||||
@@ -795,6 +795,13 @@ 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.
|
||||
The shared alert resource table's global-defaults row and card are the single
|
||||
editing surface for a section's defaults. A section that supplies
|
||||
`globalDefaults` must not also inject a synthetic resource row that mirrors
|
||||
the same record; the table renders defaults once, in both desktop table and
|
||||
narrow card layouts, and per-section metric columns must resolve to metric
|
||||
keys the shared column normalizer produces so the defaults editor reads and
|
||||
writes the same record keys the section persists.
|
||||
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
|
||||
|
||||
@@ -1,86 +1,57 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "ff97e3943c866ef062d743e6f1da136c13109a26",
|
||||
"verified_at": "2026-08-06T09:05:33Z",
|
||||
"base_sha": "711eed6d695e9008d7c633fbdf985a86d1e6aea9",
|
||||
"verified_at": "2026-08-06T09:12:17Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/Workloads/guestRowModel.tsx",
|
||||
"frontend-modern/src/features/docker/dockerContainerTableModel.ts",
|
||||
"frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts"
|
||||
"frontend-modern/src/components/Alerts/ThresholdsTableProxmoxBackupsSection.tsx",
|
||||
"frontend-modern/src/components/Alerts/ThresholdsTableProxmoxSnapshotsSection.tsx",
|
||||
"frontend-modern/src/features/alerts/alertsConfigurationModel.ts",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsGuestData.ts",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts",
|
||||
"frontend-modern/src/features/alerts/thresholds/tableTypes.ts",
|
||||
"frontend-modern/src/utils/alertThresholdDefaults.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/Workloads/guestRowModel.tsx": "dc5b5c79a0ddeb71e38bf2f01fa386f1850dffcf7315d9f697b681ad729b64ca",
|
||||
"frontend-modern/src/features/docker/dockerContainerTableModel.ts": "85b9cac933ab3f5a7aa08f54d5fbcfa1b6e15b24b461eb8c3bea8efbc4e5364f",
|
||||
"frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts": "63078dbcaa4ac8b86c94612a3deeb116a72db90b0cdb01a9c997044be2e1750b"
|
||||
"frontend-modern/src/components/Alerts/ThresholdsTableProxmoxBackupsSection.tsx": "a5dfeb2bfcc356e23b9ff164ab88782fca74593d8b3e45efea7261d02153a109",
|
||||
"frontend-modern/src/components/Alerts/ThresholdsTableProxmoxSnapshotsSection.tsx": "222d2d08ad618fadbd34d5089287c3923a79b172c582203eac7469295b50a97e",
|
||||
"frontend-modern/src/features/alerts/alertsConfigurationModel.ts": "4db657570ac209a00c16652a4f07b3be442a406d245766371c07ca2d3d32a1cf",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsGuestData.ts": "d4584732f18e15316427e7b3a206978e2ca35d097028aee0e6ab2b11c34ee6f1",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts": "c285c93a1e3a12f8481e80a8e6778334722055dcd5b885291fb33bed7d4270b0",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts": "4a2deeb37740fe79b627df17abf25bba57f3048fc82a4bc34c7010b3c5058241",
|
||||
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts": "0178351e2848c42ad085fdbc24e9c84e5810a81d073805070e436d680277cc03",
|
||||
"frontend-modern/src/features/alerts/thresholds/tableTypes.ts": "90c1910781527b16bbdd509b25a946202956a2616cceac287edb0cafca895c54",
|
||||
"frontend-modern/src/utils/alertThresholdDefaults.ts": "f59d80d26290b0a4d021c4ca093f879659635048042bb374eb0173c0b8fa2691"
|
||||
},
|
||||
"routes": [
|
||||
"/proxmox/overview",
|
||||
"/proxmox/storage",
|
||||
"/proxmox/replication",
|
||||
"/proxmox/backups",
|
||||
"/proxmox/ceph",
|
||||
"/proxmox/mail",
|
||||
"/docker/overview",
|
||||
"/docker/images",
|
||||
"/docker/storage",
|
||||
"/docker/networks",
|
||||
"/docker/swarm",
|
||||
"/kubernetes/overview",
|
||||
"/kubernetes/nodes",
|
||||
"/kubernetes/workloads",
|
||||
"/kubernetes/services",
|
||||
"/kubernetes/storage",
|
||||
"/kubernetes/configuration",
|
||||
"/kubernetes/events",
|
||||
"/truenas/overview",
|
||||
"/truenas/storage",
|
||||
"/truenas/services",
|
||||
"/truenas/apps",
|
||||
"/truenas/vms",
|
||||
"/truenas/shares",
|
||||
"/truenas/protection",
|
||||
"/vmware/overview",
|
||||
"/vmware/storage",
|
||||
"/vmware/networks",
|
||||
"/vmware/health",
|
||||
"/vmware/activity",
|
||||
"/standalone/machines",
|
||||
"/standalone/availability"
|
||||
"/alerts/thresholds/proxmox"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 820
|
||||
"width": 1600,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"height": 820
|
||||
"width": 1000,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 820
|
||||
},
|
||||
{
|
||||
"width": 320,
|
||||
"height": 820
|
||||
"width": 375,
|
||||
"height": 812
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"populated mock inventory across every platform workflow",
|
||||
"phone workload layout with identity, availability, CPU, and memory",
|
||||
"similar Proxmox workload names with and without backup posture cues",
|
||||
"small-tablet workload layout with disk restored",
|
||||
"desktop workload and native platform tables with their full responsive column sets",
|
||||
"deep platform navigation tabs selected at the end of horizontally scrollable tab strips"
|
||||
"Recovery section expanded, desktop table layout at 1600px, single Global Defaults editor row",
|
||||
"Recovery and Snapshot Age sections expanded in card layout at 1000px and 375px, single Global Defaults card each",
|
||||
"Snapshot Age section with Warning/Critical Size (GiB) columns visible, factory Off state",
|
||||
"VMs & Containers section expanded, guest cards sorted by display name within node group"
|
||||
],
|
||||
"interactions": [
|
||||
"navigated all 32 platform workflow routes and waited for their loaded tables",
|
||||
"scrolled populated Proxmox workload groups at 320 and 390 pixels and visually compared similarly named rows",
|
||||
"changed from phone to 768 pixel tablet width and confirmed the disk column returned without table overflow",
|
||||
"verified Proxmox, Docker, and VMware tables after the final layout-mode change at 320, 390, 768, and 1280 pixels",
|
||||
"measured document, table, and scroll-container widths and found no page-level or table-level horizontal overflow",
|
||||
"confirmed Proxmox, Docker, Kubernetes, TrueNAS, VMware, and Machines share identical desktop content bounds",
|
||||
"opened deep Mail Gateway, Swarm, Events, Protection, Activity, and Availability tabs and confirmed the active tab was fully visible",
|
||||
"checked the affected desktop journeys after the final code change and found no browser console errors"
|
||||
"Enabled snapshot Warning Size via Off overlay, sanitizer mirrored value to Critical Size, Save Changes persisted warningSizeGiB and criticalSizeGiB via PUT /api/alerts/config and GET returned them",
|
||||
"Reloaded page and confirmed persisted snapshot size values load back into the editor",
|
||||
"Toggled snapshot size metrics back Off and saved, GET confirmed sizes cleared",
|
||||
"Expanded and collapsed Recovery, Snapshot Age, and VMs & Containers sections at all three viewports"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -30,37 +30,17 @@ export function ThresholdsTableProxmoxBackupsSection(props: ThresholdsTableSecti
|
||||
<div ref={state.registerSection('backups')} class="scroll-mt-24">
|
||||
<ResourceTable
|
||||
title=""
|
||||
resources={[
|
||||
{
|
||||
id: 'backups-defaults',
|
||||
name: 'Global Defaults',
|
||||
thresholds: state.backupDefaultsRecord(),
|
||||
defaults: state.backupDefaultsRecord(),
|
||||
editable: true,
|
||||
editScope: 'backup',
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
'Fresh Hours',
|
||||
'Stale Hours',
|
||||
'Warning Days',
|
||||
'Critical Days',
|
||||
'Warning Size (GiB)',
|
||||
'Critical Size (GiB)',
|
||||
]}
|
||||
columns={['Fresh Hours', 'Stale Hours', 'Warning Days', 'Critical Days']}
|
||||
activeAlerts={tableProps.activeAlerts}
|
||||
emptyMessage=""
|
||||
onEdit={state.startEditing}
|
||||
onSaveEdit={state.saveEdit}
|
||||
onCancelEdit={state.cancelEdit}
|
||||
onRemoveOverride={state.removeOverride}
|
||||
showOfflineAlertsColumn={true}
|
||||
editingId={state.editingId}
|
||||
editingThresholds={state.editingThresholds}
|
||||
setEditingThresholds={state.setEditingThresholds}
|
||||
editingNote={state.editingNote}
|
||||
setEditingNote={state.setEditingNote}
|
||||
onBulkEdit={(ids) => state.handleBulkEdit(ids, ['Usage %'])}
|
||||
formatMetricValue={formatMetricValue}
|
||||
hasActiveAlert={state.hasActiveAlert}
|
||||
globalDefaults={state.backupDefaultsRecord()}
|
||||
|
||||
@@ -23,30 +23,17 @@ export function ThresholdsTableProxmoxSnapshotsSection(props: ThresholdsTableSec
|
||||
<div ref={state.registerSection('snapshots')} class="scroll-mt-24">
|
||||
<ResourceTable
|
||||
title=""
|
||||
resources={[
|
||||
{
|
||||
id: 'snapshots-defaults',
|
||||
name: 'Global Defaults',
|
||||
thresholds: state.snapshotDefaultsRecord(),
|
||||
defaults: state.snapshotDefaultsRecord(),
|
||||
editable: true,
|
||||
editScope: 'snapshot',
|
||||
},
|
||||
]}
|
||||
columns={['Warning Days', 'Critical Days']}
|
||||
columns={['Warning Days', 'Critical Days', 'Warning Size (GiB)', 'Critical Size (GiB)']}
|
||||
activeAlerts={tableProps.activeAlerts}
|
||||
emptyMessage=""
|
||||
onEdit={state.startEditing}
|
||||
onSaveEdit={state.saveEdit}
|
||||
onCancelEdit={state.cancelEdit}
|
||||
onRemoveOverride={state.removeOverride}
|
||||
showOfflineAlertsColumn={true}
|
||||
editingId={state.editingId}
|
||||
editingThresholds={state.editingThresholds}
|
||||
setEditingThresholds={state.setEditingThresholds}
|
||||
editingNote={state.editingNote}
|
||||
setEditingNote={state.setEditingNote}
|
||||
onBulkEdit={(ids) => state.handleBulkEdit(ids, ['Usage %', 'Temperature °C'])}
|
||||
formatMetricValue={formatMetricValue}
|
||||
hasActiveAlert={state.hasActiveAlert}
|
||||
globalDefaults={state.snapshotDefaultsRecord()}
|
||||
@@ -55,8 +42,8 @@ export function ThresholdsTableProxmoxSnapshotsSection(props: ThresholdsTableSec
|
||||
const currentRecord = {
|
||||
'warning days': prev.warningDays ?? 0,
|
||||
'critical days': prev.criticalDays ?? 0,
|
||||
'warning size (gib)': prev.warningSizeGiB ?? 0,
|
||||
'critical size (gib)': prev.criticalSizeGiB ?? 0,
|
||||
warningSizeGiB: prev.warningSizeGiB ?? 0,
|
||||
criticalSizeGiB: prev.criticalSizeGiB ?? 0,
|
||||
};
|
||||
const nextRecord =
|
||||
typeof value === 'function'
|
||||
@@ -73,12 +60,12 @@ export function ThresholdsTableProxmoxSnapshotsSection(props: ThresholdsTableSec
|
||||
? nextRecord['critical days']
|
||||
: prev.criticalDays,
|
||||
warningSizeGiB:
|
||||
typeof nextRecord['warning size (gib)'] === 'number'
|
||||
? nextRecord['warning size (gib)']
|
||||
typeof nextRecord.warningSizeGiB === 'number'
|
||||
? nextRecord.warningSizeGiB
|
||||
: prev.warningSizeGiB,
|
||||
criticalSizeGiB:
|
||||
typeof nextRecord['critical size (gib)'] === 'number'
|
||||
? nextRecord['critical size (gib)']
|
||||
typeof nextRecord.criticalSizeGiB === 'number'
|
||||
? nextRecord.criticalSizeGiB
|
||||
: prev.criticalSizeGiB,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -549,10 +549,16 @@ export function readAlertsConfigurationSnapshot(config: AlertConfig): AlertsConf
|
||||
config.snapshotDefaults.warningDays,
|
||||
config.snapshotDefaults.criticalDays,
|
||||
);
|
||||
const normalizedSizePair = normalizeWarningCriticalPair(
|
||||
config.snapshotDefaults.warningSizeGiB,
|
||||
config.snapshotDefaults.criticalSizeGiB,
|
||||
);
|
||||
snapshot.snapshotDefaults = {
|
||||
enabled: Boolean(config.snapshotDefaults.enabled),
|
||||
warningDays: normalizedPair.warning,
|
||||
criticalDays: normalizedPair.critical,
|
||||
warningSizeGiB: normalizedSizePair.warning,
|
||||
criticalSizeGiB: normalizedSizePair.critical,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -695,6 +701,10 @@ export function buildAlertsConfigurationPayload({
|
||||
snapshot.snapshotDefaults.warningDays,
|
||||
snapshot.snapshotDefaults.criticalDays,
|
||||
);
|
||||
const normalizedSnapshotSizePair = normalizeWarningCriticalPair(
|
||||
snapshot.snapshotDefaults.warningSizeGiB,
|
||||
snapshot.snapshotDefaults.criticalSizeGiB,
|
||||
);
|
||||
const normalizedBackupPair = normalizeWarningCriticalPair(
|
||||
snapshot.backupDefaults.warningDays,
|
||||
snapshot.backupDefaults.criticalDays,
|
||||
@@ -830,6 +840,8 @@ export function buildAlertsConfigurationPayload({
|
||||
enabled: snapshot.snapshotDefaults.enabled,
|
||||
warningDays: normalizedSnapshotPair.warning,
|
||||
criticalDays: normalizedSnapshotPair.critical,
|
||||
warningSizeGiB: normalizedSnapshotSizePair.warning,
|
||||
criticalSizeGiB: normalizedSnapshotSizePair.critical,
|
||||
},
|
||||
backupDefaults: {
|
||||
enabled: snapshot.backupDefaults.enabled,
|
||||
|
||||
-4
@@ -156,8 +156,6 @@ describe('TrueNAS threshold persistence identity', () => {
|
||||
editingNote,
|
||||
bulkEditIds,
|
||||
cancelEdit,
|
||||
updateBackupDefaults: vi.fn(),
|
||||
updateSnapshotDefaults: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -289,8 +287,6 @@ describe('TrueNAS threshold persistence identity', () => {
|
||||
editingNote,
|
||||
bulkEditIds,
|
||||
cancelEdit,
|
||||
updateBackupDefaults: vi.fn(),
|
||||
updateSnapshotDefaults: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
-4
@@ -86,8 +86,6 @@ describe('useThresholdsOverrideMutations', () => {
|
||||
editingNote,
|
||||
bulkEditIds,
|
||||
cancelEdit,
|
||||
updateBackupDefaults: vi.fn(),
|
||||
updateSnapshotDefaults: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -150,8 +148,6 @@ describe('useThresholdsOverrideMutations', () => {
|
||||
editingNote: () => '',
|
||||
bulkEditIds: () => [filesystemResource.id],
|
||||
cancelEdit: vi.fn(),
|
||||
updateBackupDefaults: vi.fn(),
|
||||
updateSnapshotDefaults: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -106,9 +106,9 @@ describe('useThresholdsRecoveryDefaultsState', () => {
|
||||
expect(captured).toBeDefined();
|
||||
expect(captured!.snapshotDefaultsRecord()).toEqual({
|
||||
'critical days': 10,
|
||||
'critical size (gib)': 18,
|
||||
criticalSizeGiB: 18,
|
||||
'warning days': 6,
|
||||
'warning size (gib)': 12,
|
||||
warningSizeGiB: 12,
|
||||
});
|
||||
expect(captured!.backupFactoryDefaultsRecord()).toEqual({
|
||||
'critical days': 7,
|
||||
|
||||
@@ -87,10 +87,16 @@ export function useThresholdsGuestData(inputs: ThresholdsDataInputs) {
|
||||
grouped[groupKey].push(guest);
|
||||
});
|
||||
|
||||
// Sort by the name the row actually displays; vmid is not rendered here,
|
||||
// so a vmid-first order reads as unsorted (#1680). vmid only breaks ties.
|
||||
Object.keys(grouped).forEach((node) => {
|
||||
grouped[node].sort((a, b) => {
|
||||
if (a.vmid && b.vmid) return a.vmid - b.vmid;
|
||||
return a.name.localeCompare(b.name);
|
||||
const byName = a.name.localeCompare(b.name, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base',
|
||||
});
|
||||
if (byName !== 0) return byName;
|
||||
return (a.vmid ?? 0) - (b.vmid ?? 0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+1
-59
@@ -1,14 +1,6 @@
|
||||
import type { Accessor } from 'solid-js';
|
||||
|
||||
import type { RawOverrideConfig, BackupAlertConfig, SnapshotAlertConfig } from '@/types/alerts';
|
||||
import {
|
||||
DEFAULT_SNAPSHOT_WARNING,
|
||||
DEFAULT_SNAPSHOT_CRITICAL,
|
||||
DEFAULT_SNAPSHOT_WARNING_SIZE,
|
||||
DEFAULT_SNAPSHOT_CRITICAL_SIZE,
|
||||
DEFAULT_BACKUP_WARNING,
|
||||
DEFAULT_BACKUP_CRITICAL,
|
||||
} from '@/features/alerts/thresholds/constants';
|
||||
import type { RawOverrideConfig } from '@/types/alerts';
|
||||
import type {
|
||||
Override,
|
||||
OverrideType,
|
||||
@@ -60,12 +52,6 @@ interface ThresholdsOverrideMutationProps {
|
||||
editingNote: Accessor<string>;
|
||||
bulkEditIds: Accessor<string[]>;
|
||||
cancelEdit: () => void;
|
||||
updateBackupDefaults: (
|
||||
updater: BackupAlertConfig | ((prev: BackupAlertConfig) => BackupAlertConfig),
|
||||
) => void;
|
||||
updateSnapshotDefaults: (
|
||||
updater: SnapshotAlertConfig | ((prev: SnapshotAlertConfig) => SnapshotAlertConfig),
|
||||
) => void;
|
||||
}
|
||||
|
||||
export function useThresholdsOverrideMutations({
|
||||
@@ -75,8 +61,6 @@ export function useThresholdsOverrideMutations({
|
||||
editingNote,
|
||||
bulkEditIds,
|
||||
cancelEdit,
|
||||
updateBackupDefaults,
|
||||
updateSnapshotDefaults,
|
||||
}: ThresholdsOverrideMutationProps) {
|
||||
const optionalResources = (accessor?: Accessor<TableResource[]>): TableResource[] =>
|
||||
accessor?.() ?? [];
|
||||
@@ -126,48 +110,6 @@ export function useThresholdsOverrideMutations({
|
||||
const trimmedNote = editingNote().trim();
|
||||
const noteForOverride = trimmedNote.length > 0 ? trimmedNote : undefined;
|
||||
|
||||
if (resource.editScope === 'backup') {
|
||||
const currentBackupDefaults = props.backupDefaults();
|
||||
updateBackupDefaults({
|
||||
criticalDays:
|
||||
editedThresholdMap['critical days'] ??
|
||||
currentBackupDefaults.criticalDays ??
|
||||
DEFAULT_BACKUP_CRITICAL,
|
||||
enabled: currentBackupDefaults.enabled,
|
||||
warningDays:
|
||||
editedThresholdMap['warning days'] ??
|
||||
currentBackupDefaults.warningDays ??
|
||||
DEFAULT_BACKUP_WARNING,
|
||||
});
|
||||
cancelEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (resource.editScope === 'snapshot') {
|
||||
const currentSnapshotDefaults = props.snapshotDefaults();
|
||||
updateSnapshotDefaults({
|
||||
criticalDays:
|
||||
editedThresholdMap['critical days'] ??
|
||||
currentSnapshotDefaults.criticalDays ??
|
||||
DEFAULT_SNAPSHOT_CRITICAL,
|
||||
criticalSizeGiB:
|
||||
editedThresholdMap['critical size (gib)'] ??
|
||||
currentSnapshotDefaults.criticalSizeGiB ??
|
||||
DEFAULT_SNAPSHOT_CRITICAL_SIZE,
|
||||
enabled: currentSnapshotDefaults.enabled,
|
||||
warningDays:
|
||||
editedThresholdMap['warning days'] ??
|
||||
currentSnapshotDefaults.warningDays ??
|
||||
DEFAULT_SNAPSHOT_WARNING,
|
||||
warningSizeGiB:
|
||||
editedThresholdMap['warning size (gib)'] ??
|
||||
currentSnapshotDefaults.warningSizeGiB ??
|
||||
DEFAULT_SNAPSHOT_WARNING_SIZE,
|
||||
});
|
||||
cancelEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultThresholds = (resource.defaults ?? {}) as Record<string, number | undefined>;
|
||||
const overrideThresholds: Record<string, number> = {};
|
||||
|
||||
|
||||
+7
-4
@@ -106,13 +106,16 @@ export function useThresholdsRecoveryDefaultsState(props: ThresholdsTableProps)
|
||||
};
|
||||
};
|
||||
|
||||
// Size keys use the normalized metric key ('warningSizeGiB'), matching what
|
||||
// normalizeAlertResourceMetricKey resolves for the 'Warning Size (GiB)'
|
||||
// column — the global-defaults editor reads and writes through that key.
|
||||
const snapshotDefaultsRecord = createMemo(() => {
|
||||
const current = props.snapshotDefaults();
|
||||
return {
|
||||
'warning days': current.warningDays ?? 0,
|
||||
'critical days': current.criticalDays ?? 0,
|
||||
'warning size (gib)': current.warningSizeGiB ?? 0,
|
||||
'critical size (gib)': current.criticalSizeGiB ?? 0,
|
||||
warningSizeGiB: current.warningSizeGiB ?? 0,
|
||||
criticalSizeGiB: current.criticalSizeGiB ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -121,8 +124,8 @@ export function useThresholdsRecoveryDefaultsState(props: ThresholdsTableProps)
|
||||
return {
|
||||
'warning days': factory.warningDays ?? DEFAULT_SNAPSHOT_WARNING,
|
||||
'critical days': factory.criticalDays ?? DEFAULT_SNAPSHOT_CRITICAL,
|
||||
'warning size (gib)': factory.warningSizeGiB ?? DEFAULT_SNAPSHOT_WARNING_SIZE,
|
||||
'critical size (gib)': factory.criticalSizeGiB ?? DEFAULT_SNAPSHOT_CRITICAL_SIZE,
|
||||
warningSizeGiB: factory.warningSizeGiB ?? DEFAULT_SNAPSHOT_WARNING_SIZE,
|
||||
criticalSizeGiB: factory.criticalSizeGiB ?? DEFAULT_SNAPSHOT_CRITICAL_SIZE,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -738,8 +738,6 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
||||
editingNote,
|
||||
bulkEditIds,
|
||||
cancelEdit,
|
||||
updateBackupDefaults,
|
||||
updateSnapshotDefaults,
|
||||
});
|
||||
|
||||
const { setOfflineState, toggleDisabled, toggleNodeConnectivity } =
|
||||
|
||||
@@ -34,7 +34,6 @@ export interface Resource {
|
||||
clusterName?: string;
|
||||
isClusterMember?: boolean;
|
||||
delaySeconds?: number;
|
||||
editScope?: 'snapshot' | 'backup';
|
||||
isEnabled?: boolean;
|
||||
toggleEnabled?: () => void;
|
||||
toggleTitleEnabled?: string;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@/utils/metricThresholds';
|
||||
import {
|
||||
FACTORY_KUBERNETES_DEFAULTS,
|
||||
FACTORY_SNAPSHOT_DEFAULTS,
|
||||
FACTORY_TRUENAS_DEFAULTS,
|
||||
FACTORY_TRUENAS_DISK_DEFAULTS,
|
||||
FACTORY_VMWARE_DEFAULTS,
|
||||
@@ -408,6 +409,19 @@ describe('metricThresholds', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps snapshot factory defaults carrying size thresholds disabled at zero', () => {
|
||||
// The size pair must exist on the factory config so the thresholds
|
||||
// editor and the save payload round-trip them; 0 means size-based
|
||||
// snapshot alerts are off until an operator enables them.
|
||||
expect(FACTORY_SNAPSHOT_DEFAULTS).toEqual({
|
||||
enabled: false,
|
||||
warningDays: 30,
|
||||
criticalDays: 45,
|
||||
warningSizeGiB: 0,
|
||||
criticalSizeGiB: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps Kubernetes, TrueNAS, and vSphere factory defaults explicit for alert configuration', () => {
|
||||
expect(FACTORY_KUBERNETES_DEFAULTS).toEqual({
|
||||
cpu: 80,
|
||||
|
||||
@@ -95,6 +95,9 @@ export const FACTORY_SNAPSHOT_DEFAULTS: SnapshotAlertConfig = {
|
||||
enabled: false,
|
||||
warningDays: 30,
|
||||
criticalDays: 45,
|
||||
// 0 disables size-based snapshot alerts; the engine only fires on sizes > 0.
|
||||
warningSizeGiB: 0,
|
||||
criticalSizeGiB: 0,
|
||||
};
|
||||
|
||||
export const FACTORY_BACKUP_DEFAULTS: BackupAlertConfig = {
|
||||
|
||||
Reference in New Issue
Block a user