diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index e06267b59..ba2365e5a 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -132,6 +132,8 @@ so governed agent, guest, and storage rows do not leak raw names when the threshold editor saves or re-renders them. That threshold editor data shaping now lives under `frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts`, +backup and snapshot default sanitization plus factory-drift policy now live in +`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts`, while `frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts` owns threshold-table route sync, section collapse state, search/edit shell state, and bulk-edit dialog control. Override persistence, bulk threshold diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index fdc2478cf..226d0ef22 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -236,9 +236,12 @@ inline. The thresholds editor now follows that same split more tightly: `frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts` must stay the table-shell owner for route sync and local UI state, while +`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts` +owns backup/snapshot default sanitization and factory-drift policy, and `frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts` owns override save/bulk/toggle persistence and alert-removal side effects. The -table-shell hook should not re-accumulate raw override mutation logic inline. +table-shell hook should not re-accumulate raw override mutation logic or +recovery-threshold defaults policy inline. The updates settings surface now follows the same presentation-owner rule. `frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx` stays the @@ -430,6 +433,7 @@ is now explicitly a feature consumer rather than the data or controller owner. Canonical threshold row shaping, override-ID compatibility, grouped resource normalization, and thresholds-table controller state live in `frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts`, +`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts`, `frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts`, so future cleanup should extend those feature hooks instead of rebuilding resource normalization or thresholds-table runtime state inside the table diff --git a/frontend-modern/src/features/alerts/thresholds/hooks/__tests__/useThresholdsRecoveryDefaultsState.test.tsx b/frontend-modern/src/features/alerts/thresholds/hooks/__tests__/useThresholdsRecoveryDefaultsState.test.tsx new file mode 100644 index 000000000..16790f2de --- /dev/null +++ b/frontend-modern/src/features/alerts/thresholds/hooks/__tests__/useThresholdsRecoveryDefaultsState.test.tsx @@ -0,0 +1,122 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { cleanup, render } from '@solidjs/testing-library'; + +import type { ThresholdsTableProps } from '@/features/alerts/thresholds/types'; +import { useThresholdsRecoveryDefaultsState } from '../useThresholdsRecoveryDefaultsState'; + +const buildProps = (): ThresholdsTableProps => + ({ + backupDefaults: () => ({ + alertOrphaned: false, + criticalDays: 8, + enabled: true, + freshHours: 24, + ignoreVMIDs: ['100', '200'], + staleHours: 72, + warningDays: 4, + }), + backupFactoryDefaults: { + alertOrphaned: true, + criticalDays: 7, + enabled: false, + freshHours: 12, + ignoreVMIDs: ['100'], + staleHours: 48, + warningDays: 3, + }, + snapshotDefaults: () => ({ + criticalDays: 10, + criticalSizeGiB: 18, + enabled: true, + warningDays: 6, + warningSizeGiB: 12, + }), + snapshotFactoryDefaults: { + criticalDays: 7, + criticalSizeGiB: 16, + enabled: false, + warningDays: 3, + warningSizeGiB: 8, + }, + }) as ThresholdsTableProps; + +afterEach(() => { + cleanup(); +}); + +describe('useThresholdsRecoveryDefaultsState', () => { + it('owns backup and snapshot default-policy sanitization', () => { + let captured: ReturnType | undefined; + + const Harness = () => { + captured = useThresholdsRecoveryDefaultsState(buildProps()); + return null; + }; + + render(() => ); + + expect(captured).toBeDefined(); + expect( + captured!.sanitizeSnapshotConfig({ + criticalDays: 4, + criticalSizeGiB: 1.08, + enabled: true, + warningDays: 9, + warningSizeGiB: 3.16, + }), + ).toEqual({ + criticalDays: 4, + criticalSizeGiB: 1.1, + enabled: true, + warningDays: 4, + warningSizeGiB: 1.1, + }); + + expect( + captured!.sanitizeBackupConfig({ + alertOrphaned: false, + criticalDays: 2, + enabled: true, + freshHours: 9, + ignoreVMIDs: ['101', ' ', '101', '202'], + staleHours: 4, + warningDays: 7, + }), + ).toEqual({ + alertOrphaned: false, + criticalDays: 2, + enabled: true, + freshHours: 9, + ignoreVMIDs: ['101', '202'], + staleHours: 9, + warningDays: 2, + }); + }); + + it('tracks factory drift for backup and snapshot defaults', () => { + let captured: ReturnType | undefined; + + const Harness = () => { + captured = useThresholdsRecoveryDefaultsState(buildProps()); + return null; + }; + + render(() => ); + + expect(captured).toBeDefined(); + expect(captured!.snapshotDefaultsRecord()).toEqual({ + 'critical days': 10, + 'critical size (gib)': 18, + 'warning days': 6, + 'warning size (gib)': 12, + }); + expect(captured!.backupFactoryDefaultsRecord()).toEqual({ + 'critical days': 7, + 'fresh hours': 12, + 'stale hours': 48, + 'warning days': 3, + }); + expect(captured!.snapshotOverridesCount()).toBe(1); + expect(captured!.backupOverridesCount()).toBe(1); + }); +}); diff --git a/frontend-modern/src/features/alerts/thresholds/hooks/__tests__/useThresholdsTableState.test.tsx b/frontend-modern/src/features/alerts/thresholds/hooks/__tests__/useThresholdsTableState.test.tsx index 79ea5bfd4..7eb27e0ff 100644 --- a/frontend-modern/src/features/alerts/thresholds/hooks/__tests__/useThresholdsTableState.test.tsx +++ b/frontend-modern/src/features/alerts/thresholds/hooks/__tests__/useThresholdsTableState.test.tsx @@ -30,10 +30,6 @@ vi.mock('../useThresholdsData', () => ({ agentDisksGroupedByAgent: () => ({}), agentDisksWithOverrides: () => [], agentsWithOverrides: () => [], - backupDefaultsRecord: () => ({}), - backupFactoryConfig: () => ({ enabled: false }), - backupFactoryDefaultsRecord: () => ({}), - backupOverridesCount: () => 0, dockerContainersFlat: () => [], dockerContainersGroupedByHost: () => ({}), dockerHostGroupMeta: () => ({}), @@ -45,15 +41,24 @@ vi.mock('../useThresholdsData', () => ({ pbsServersWithOverrides: () => [], pmgGlobalDefaults: () => ({}), pmgServersWithOverrides: () => [], + storageGroupedByNode: () => ({}), + storageWithOverrides: () => [], + totalDockerContainers: () => 0, + }), +})); + +vi.mock('../useThresholdsRecoveryDefaultsState', () => ({ + useThresholdsRecoveryDefaultsState: () => ({ + backupDefaultsRecord: () => ({}), + backupFactoryConfig: () => ({ enabled: false }), + backupFactoryDefaultsRecord: () => ({}), + backupOverridesCount: () => 0, sanitizeBackupConfig: (value: T) => value, sanitizeSnapshotConfig: (value: T) => value, snapshotDefaultsRecord: () => ({}), snapshotFactoryConfig: () => ({ enabled: false }), snapshotFactoryDefaultsRecord: () => ({}), snapshotOverridesCount: () => 0, - storageGroupedByNode: () => ({}), - storageWithOverrides: () => [], - totalDockerContainers: () => 0, }), })); diff --git a/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts b/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts index 60724abbb..b12d8b28d 100644 --- a/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts +++ b/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts @@ -14,16 +14,8 @@ import { PMG_THRESHOLD_COLUMNS, PMG_KEY_TO_NORMALIZED, PMG_NORMALIZED_TO_KEY, - DEFAULT_SNAPSHOT_WARNING, - DEFAULT_SNAPSHOT_CRITICAL, - DEFAULT_SNAPSHOT_WARNING_SIZE, - DEFAULT_SNAPSHOT_CRITICAL_SIZE, - DEFAULT_BACKUP_WARNING, - DEFAULT_BACKUP_CRITICAL, - DEFAULT_BACKUP_FRESH_HOURS, - DEFAULT_BACKUP_STALE_HOURS, } from '../constants'; -import type { PMGThresholdDefaults, SnapshotAlertConfig, BackupAlertConfig } from '@/types/alerts'; +import type { PMGThresholdDefaults } from '@/types/alerts'; import type { ThresholdsTableProps, Override } from '../types'; import type { GroupHeaderMeta, Resource as TableResource } from '../tableTypes'; @@ -773,179 +765,6 @@ export function useThresholdsData( return meta; }); - const snapshotFactoryConfig = () => - props.snapshotFactoryDefaults ?? { - enabled: false, - warningDays: DEFAULT_SNAPSHOT_WARNING, - criticalDays: DEFAULT_SNAPSHOT_CRITICAL, - warningSizeGiB: DEFAULT_SNAPSHOT_WARNING_SIZE, - criticalSizeGiB: DEFAULT_SNAPSHOT_CRITICAL_SIZE, - }; - - const sanitizeSnapshotConfig = (config: SnapshotAlertConfig): SnapshotAlertConfig => { - let warning = Math.max(0, Math.round(config.warningDays ?? 0)); - let critical = Math.max(0, Math.round(config.criticalDays ?? 0)); - - if (critical > 0 && warning > critical) { - warning = critical; - } - if (critical === 0 && warning > 0) { - critical = warning; - } - - const rawWarningSize = Number.isFinite(config.warningSizeGiB) - ? Number(config.warningSizeGiB) - : DEFAULT_SNAPSHOT_WARNING_SIZE; - const rawCriticalSize = Number.isFinite(config.criticalSizeGiB) - ? Number(config.criticalSizeGiB) - : DEFAULT_SNAPSHOT_CRITICAL_SIZE; - - const roundSize = (value: number) => Math.round(Math.max(0, value) * 10) / 10; - - let warningSize = roundSize(rawWarningSize); - let criticalSize = roundSize(rawCriticalSize); - - if (criticalSize > 0 && warningSize > criticalSize) { - warningSize = criticalSize; - } - if (criticalSize === 0 && warningSize > 0) { - criticalSize = warningSize; - } - - return { - enabled: !!config.enabled, - warningDays: warning, - criticalDays: critical, - warningSizeGiB: warningSize, - criticalSizeGiB: criticalSize, - }; - }; - - const backupFactoryConfig = () => - props.backupFactoryDefaults ?? { - enabled: false, - warningDays: DEFAULT_BACKUP_WARNING, - criticalDays: DEFAULT_BACKUP_CRITICAL, - freshHours: DEFAULT_BACKUP_FRESH_HOURS, - staleHours: DEFAULT_BACKUP_STALE_HOURS, - alertOrphaned: true, - ignoreVMIDs: [], - }; - - const sanitizeBackupConfig = (config: BackupAlertConfig): BackupAlertConfig => { - let warning = Math.max(0, Math.round(config.warningDays ?? 0)); - let critical = Math.max(0, Math.round(config.criticalDays ?? 0)); - let fresh = Math.max(0, Math.round(config.freshHours ?? DEFAULT_BACKUP_FRESH_HOURS)); - let stale = Math.max(0, Math.round(config.staleHours ?? DEFAULT_BACKUP_STALE_HOURS)); - const alertOrphaned = config.alertOrphaned ?? true; - const ignoreVMIDs = Array.from( - new Set( - (config.ignoreVMIDs ?? []).map((value) => value.trim()).filter((value) => value.length > 0), - ), - ); - - if (critical > 0 && warning > critical) { - warning = critical; - } - if (critical === 0 && warning > 0) { - critical = warning; - } - - // Ensure stale is at least fresh - if (stale < fresh) { - stale = fresh; - } - - return { - enabled: !!config.enabled, - warningDays: warning, - criticalDays: critical, - freshHours: fresh, - staleHours: stale, - alertOrphaned, - ignoreVMIDs, - }; - }; - - 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, - }; - }); - - const snapshotFactoryDefaultsRecord = createMemo(() => { - const factory = snapshotFactoryConfig(); - 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, - }; - }); - - const backupDefaultsRecord = createMemo(() => { - const current = props.backupDefaults(); - return { - 'fresh hours': current.freshHours ?? DEFAULT_BACKUP_FRESH_HOURS, - 'stale hours': current.staleHours ?? DEFAULT_BACKUP_STALE_HOURS, - 'warning days': current.warningDays ?? 0, - 'critical days': current.criticalDays ?? 0, - }; - }); - - const backupFactoryDefaultsRecord = createMemo(() => { - const factory = backupFactoryConfig(); - return { - 'fresh hours': factory.freshHours ?? DEFAULT_BACKUP_FRESH_HOURS, - 'stale hours': factory.staleHours ?? DEFAULT_BACKUP_STALE_HOURS, - 'warning days': factory.warningDays ?? DEFAULT_BACKUP_WARNING, - 'critical days': factory.criticalDays ?? DEFAULT_BACKUP_CRITICAL, - }; - }); - - const snapshotOverridesCount = createMemo(() => { - const current = props.snapshotDefaults(); - const factory = snapshotFactoryConfig(); - const differs = - current.enabled !== factory.enabled || - (current.warningDays ?? DEFAULT_SNAPSHOT_WARNING) !== - (factory.warningDays ?? DEFAULT_SNAPSHOT_WARNING) || - (current.criticalDays ?? DEFAULT_SNAPSHOT_CRITICAL) !== - (factory.criticalDays ?? DEFAULT_SNAPSHOT_CRITICAL) || - (current.warningSizeGiB ?? DEFAULT_SNAPSHOT_WARNING_SIZE) !== - (factory.warningSizeGiB ?? DEFAULT_SNAPSHOT_WARNING_SIZE) || - (current.criticalSizeGiB ?? DEFAULT_SNAPSHOT_CRITICAL_SIZE) !== - (factory.criticalSizeGiB ?? DEFAULT_SNAPSHOT_CRITICAL_SIZE); - return differs ? 1 : 0; - }); - - const backupOverridesCount = createMemo(() => { - const backupCurrent = props.backupDefaults(); - const backupFactory = backupFactoryConfig(); - const currentIgnore = backupCurrent.ignoreVMIDs ?? []; - const factoryIgnore = backupFactory.ignoreVMIDs ?? []; - const ignoreDiff = - currentIgnore.length !== factoryIgnore.length || - currentIgnore.some((value, index) => value !== factoryIgnore[index]); - return backupCurrent.enabled !== backupFactory.enabled || - (backupCurrent.warningDays ?? DEFAULT_BACKUP_WARNING) !== - (backupFactory.warningDays ?? DEFAULT_BACKUP_WARNING) || - (backupCurrent.criticalDays ?? DEFAULT_BACKUP_CRITICAL) !== - (backupFactory.criticalDays ?? DEFAULT_BACKUP_CRITICAL) || - (backupCurrent.freshHours ?? DEFAULT_BACKUP_FRESH_HOURS) !== - (backupFactory.freshHours ?? DEFAULT_BACKUP_FRESH_HOURS) || - (backupCurrent.staleHours ?? DEFAULT_BACKUP_STALE_HOURS) !== - (backupFactory.staleHours ?? DEFAULT_BACKUP_STALE_HOURS) || - (backupCurrent.alertOrphaned ?? true) !== (backupFactory.alertOrphaned ?? true) || - ignoreDiff - ? 1 - : 0; - }); - // Process guests with their overrides and group by node const guestsGroupedByNode = createMemo>((prev = {}) => { // If we're currently editing, return the previous value to avoid re-renders @@ -1294,16 +1113,6 @@ export function useThresholdsData( dockerContainersFlat, totalDockerContainers, dockerHostGroupMeta, - snapshotFactoryConfig, - sanitizeSnapshotConfig, - backupFactoryConfig, - sanitizeBackupConfig, - snapshotDefaultsRecord, - snapshotFactoryDefaultsRecord, - backupDefaultsRecord, - backupFactoryDefaultsRecord, - snapshotOverridesCount, - backupOverridesCount, guestsGroupedByNode, guestsFlat, guestGroupHeaderMeta, diff --git a/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts b/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts new file mode 100644 index 000000000..065b708fb --- /dev/null +++ b/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts @@ -0,0 +1,201 @@ +import { createMemo } from 'solid-js'; + +import type { BackupAlertConfig, SnapshotAlertConfig } from '@/types/alerts'; +import type { ThresholdsTableProps } from '@/features/alerts/thresholds/types'; +import { + DEFAULT_BACKUP_CRITICAL, + DEFAULT_BACKUP_FRESH_HOURS, + DEFAULT_BACKUP_STALE_HOURS, + DEFAULT_BACKUP_WARNING, + DEFAULT_SNAPSHOT_CRITICAL, + DEFAULT_SNAPSHOT_CRITICAL_SIZE, + DEFAULT_SNAPSHOT_WARNING, + DEFAULT_SNAPSHOT_WARNING_SIZE, +} from '../constants'; + +export function useThresholdsRecoveryDefaultsState(props: ThresholdsTableProps) { + const snapshotFactoryConfig = () => + props.snapshotFactoryDefaults ?? { + enabled: false, + warningDays: DEFAULT_SNAPSHOT_WARNING, + criticalDays: DEFAULT_SNAPSHOT_CRITICAL, + warningSizeGiB: DEFAULT_SNAPSHOT_WARNING_SIZE, + criticalSizeGiB: DEFAULT_SNAPSHOT_CRITICAL_SIZE, + }; + + const sanitizeSnapshotConfig = (config: SnapshotAlertConfig): SnapshotAlertConfig => { + let warning = Math.max(0, Math.round(config.warningDays ?? 0)); + let critical = Math.max(0, Math.round(config.criticalDays ?? 0)); + + if (critical > 0 && warning > critical) { + warning = critical; + } + if (critical === 0 && warning > 0) { + critical = warning; + } + + const rawWarningSize = Number.isFinite(config.warningSizeGiB) + ? Number(config.warningSizeGiB) + : DEFAULT_SNAPSHOT_WARNING_SIZE; + const rawCriticalSize = Number.isFinite(config.criticalSizeGiB) + ? Number(config.criticalSizeGiB) + : DEFAULT_SNAPSHOT_CRITICAL_SIZE; + + const roundSize = (value: number) => Math.round(Math.max(0, value) * 10) / 10; + + let warningSize = roundSize(rawWarningSize); + let criticalSize = roundSize(rawCriticalSize); + + if (criticalSize > 0 && warningSize > criticalSize) { + warningSize = criticalSize; + } + if (criticalSize === 0 && warningSize > 0) { + criticalSize = warningSize; + } + + return { + enabled: !!config.enabled, + warningDays: warning, + criticalDays: critical, + warningSizeGiB: warningSize, + criticalSizeGiB: criticalSize, + }; + }; + + const backupFactoryConfig = () => + props.backupFactoryDefaults ?? { + enabled: false, + warningDays: DEFAULT_BACKUP_WARNING, + criticalDays: DEFAULT_BACKUP_CRITICAL, + freshHours: DEFAULT_BACKUP_FRESH_HOURS, + staleHours: DEFAULT_BACKUP_STALE_HOURS, + alertOrphaned: true, + ignoreVMIDs: [], + }; + + const sanitizeBackupConfig = (config: BackupAlertConfig): BackupAlertConfig => { + let warning = Math.max(0, Math.round(config.warningDays ?? 0)); + let critical = Math.max(0, Math.round(config.criticalDays ?? 0)); + let fresh = Math.max(0, Math.round(config.freshHours ?? DEFAULT_BACKUP_FRESH_HOURS)); + let stale = Math.max(0, Math.round(config.staleHours ?? DEFAULT_BACKUP_STALE_HOURS)); + const alertOrphaned = config.alertOrphaned ?? true; + const ignoreVMIDs = Array.from( + new Set( + (config.ignoreVMIDs ?? []).map((value) => value.trim()).filter((value) => value.length > 0), + ), + ); + + if (critical > 0 && warning > critical) { + warning = critical; + } + if (critical === 0 && warning > 0) { + critical = warning; + } + if (stale < fresh) { + stale = fresh; + } + + return { + enabled: !!config.enabled, + warningDays: warning, + criticalDays: critical, + freshHours: fresh, + staleHours: stale, + alertOrphaned, + ignoreVMIDs, + }; + }; + + 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, + }; + }); + + const snapshotFactoryDefaultsRecord = createMemo(() => { + const factory = snapshotFactoryConfig(); + 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, + }; + }); + + const backupDefaultsRecord = createMemo(() => { + const current = props.backupDefaults(); + return { + 'fresh hours': current.freshHours ?? DEFAULT_BACKUP_FRESH_HOURS, + 'stale hours': current.staleHours ?? DEFAULT_BACKUP_STALE_HOURS, + 'warning days': current.warningDays ?? 0, + 'critical days': current.criticalDays ?? 0, + }; + }); + + const backupFactoryDefaultsRecord = createMemo(() => { + const factory = backupFactoryConfig(); + return { + 'fresh hours': factory.freshHours ?? DEFAULT_BACKUP_FRESH_HOURS, + 'stale hours': factory.staleHours ?? DEFAULT_BACKUP_STALE_HOURS, + 'warning days': factory.warningDays ?? DEFAULT_BACKUP_WARNING, + 'critical days': factory.criticalDays ?? DEFAULT_BACKUP_CRITICAL, + }; + }); + + const snapshotOverridesCount = createMemo(() => { + const current = props.snapshotDefaults(); + const factory = snapshotFactoryConfig(); + const differs = + current.enabled !== factory.enabled || + (current.warningDays ?? DEFAULT_SNAPSHOT_WARNING) !== + (factory.warningDays ?? DEFAULT_SNAPSHOT_WARNING) || + (current.criticalDays ?? DEFAULT_SNAPSHOT_CRITICAL) !== + (factory.criticalDays ?? DEFAULT_SNAPSHOT_CRITICAL) || + (current.warningSizeGiB ?? DEFAULT_SNAPSHOT_WARNING_SIZE) !== + (factory.warningSizeGiB ?? DEFAULT_SNAPSHOT_WARNING_SIZE) || + (current.criticalSizeGiB ?? DEFAULT_SNAPSHOT_CRITICAL_SIZE) !== + (factory.criticalSizeGiB ?? DEFAULT_SNAPSHOT_CRITICAL_SIZE); + return differs ? 1 : 0; + }); + + const backupOverridesCount = createMemo(() => { + const current = props.backupDefaults(); + const factory = backupFactoryConfig(); + const currentIgnore = current.ignoreVMIDs ?? []; + const factoryIgnore = factory.ignoreVMIDs ?? []; + const ignoreDiff = + currentIgnore.length !== factoryIgnore.length || + currentIgnore.some((value, index) => value !== factoryIgnore[index]); + + return current.enabled !== factory.enabled || + (current.warningDays ?? DEFAULT_BACKUP_WARNING) !== + (factory.warningDays ?? DEFAULT_BACKUP_WARNING) || + (current.criticalDays ?? DEFAULT_BACKUP_CRITICAL) !== + (factory.criticalDays ?? DEFAULT_BACKUP_CRITICAL) || + (current.freshHours ?? DEFAULT_BACKUP_FRESH_HOURS) !== + (factory.freshHours ?? DEFAULT_BACKUP_FRESH_HOURS) || + (current.staleHours ?? DEFAULT_BACKUP_STALE_HOURS) !== + (factory.staleHours ?? DEFAULT_BACKUP_STALE_HOURS) || + (current.alertOrphaned ?? true) !== (factory.alertOrphaned ?? true) || + ignoreDiff + ? 1 + : 0; + }); + + return { + backupDefaultsRecord, + backupFactoryConfig, + backupFactoryDefaultsRecord, + backupOverridesCount, + sanitizeBackupConfig, + sanitizeSnapshotConfig, + snapshotDefaultsRecord, + snapshotFactoryConfig, + snapshotFactoryDefaultsRecord, + snapshotOverridesCount, + }; +} diff --git a/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts b/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts index 8d3dfe7b9..4dc559755 100644 --- a/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts +++ b/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts @@ -40,6 +40,7 @@ import type { } from '@/features/alerts/thresholds/tableTypes'; import { useCollapsedSections } from '@/components/Alerts/Thresholds/hooks/useCollapsedSections'; import { useThresholdsData } from './useThresholdsData'; +import { useThresholdsRecoveryDefaultsState } from './useThresholdsRecoveryDefaultsState'; import { useThresholdsOverrideMutations } from './useThresholdsOverrideMutations'; const HELP_BANNER_KEY = 'pulse-thresholds-help-dismissed'; @@ -189,16 +190,6 @@ export function useThresholdsTableState(props: ThresholdsTableProps) { dockerContainersFlat, totalDockerContainers, dockerHostGroupMeta, - snapshotFactoryConfig, - sanitizeSnapshotConfig, - backupFactoryConfig, - sanitizeBackupConfig, - snapshotDefaultsRecord, - snapshotFactoryDefaultsRecord, - backupDefaultsRecord, - backupFactoryDefaultsRecord, - snapshotOverridesCount, - backupOverridesCount, guestsGroupedByNode, guestsFlat, guestGroupHeaderMeta, @@ -209,6 +200,19 @@ export function useThresholdsTableState(props: ThresholdsTableProps) { storageGroupedByNode, } = useThresholdsData(props, editingId, searchTerm); + const { + backupDefaultsRecord, + backupFactoryConfig, + backupFactoryDefaultsRecord, + backupOverridesCount, + sanitizeBackupConfig, + sanitizeSnapshotConfig, + snapshotDefaultsRecord, + snapshotFactoryConfig, + snapshotFactoryDefaultsRecord, + snapshotOverridesCount, + } = useThresholdsRecoveryDefaultsState(props); + const countOverrides = (resources: TableResource[] | undefined) => resources?.filter( (resource) => resource.hasOverride || resource.disabled || resource.disableConnectivity, diff --git a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts index 2c877357d..ee77b5be3 100644 --- a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts +++ b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts @@ -17,6 +17,7 @@ import alertThresholdsTabSource from '@/features/alerts/tabs/ThresholdsTab.tsx?r import recentAlertsPanelSource from '@/components/Alerts/RecentAlertsPanel.tsx?raw'; import thresholdsTableSource from '@/components/Alerts/ThresholdsTable.tsx?raw'; import thresholdsDataHookSource from '@/features/alerts/thresholds/hooks/useThresholdsData.ts?raw'; +import thresholdsRecoveryDefaultsStateHookSource from '@/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts?raw'; import thresholdsTableStateHookSource from '@/features/alerts/thresholds/hooks/useThresholdsTableState.ts?raw'; import thresholdsOverrideMutationsHookSource from '@/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts?raw'; @@ -276,11 +277,19 @@ describe('tab path helpers', () => { expect(thresholdsDataHookSource).toContain('export function useThresholdsData'); expect(thresholdsTableStateHookSource).toContain('export function useThresholdsTableState'); expect(thresholdsTableStateHookSource).toContain('useThresholdsData(props, editingId, searchTerm)'); + expect(thresholdsTableStateHookSource).toContain('useThresholdsRecoveryDefaultsState(props)'); expect(thresholdsTableStateHookSource).toContain('useThresholdsOverrideMutations'); expect(thresholdsTableStateHookSource).not.toContain('const saveEdit = (resourceId: string) => {'); expect(thresholdsTableStateHookSource).not.toContain( 'const toggleNodeConnectivity = (resourceId: string, forceState?: boolean) => {', ); + expect(thresholdsDataHookSource).not.toContain('const sanitizeSnapshotConfig ='); + expect(thresholdsDataHookSource).not.toContain('const sanitizeBackupConfig ='); + expect(thresholdsRecoveryDefaultsStateHookSource).toContain( + 'export function useThresholdsRecoveryDefaultsState', + ); + expect(thresholdsRecoveryDefaultsStateHookSource).toContain('const sanitizeSnapshotConfig ='); + expect(thresholdsRecoveryDefaultsStateHookSource).toContain('const sanitizeBackupConfig ='); expect(thresholdsOverrideMutationsHookSource).toContain( 'export function useThresholdsOverrideMutations', ); diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index d6a1feb4e..6fca6e897 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -339,6 +339,7 @@ import alertScheduleTabSource from '@/features/alerts/tabs/ScheduleTab.tsx?raw'; import alertThresholdsTabSource from '@/features/alerts/tabs/ThresholdsTab.tsx?raw'; import thresholdsTableSource from '@/components/Alerts/ThresholdsTable.tsx?raw'; import thresholdsDataHookSource from '@/features/alerts/thresholds/hooks/useThresholdsData.ts?raw'; +import thresholdsRecoveryDefaultsStateHookSource from '@/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts?raw'; import thresholdsTableStateHookSource from '@/features/alerts/thresholds/hooks/useThresholdsTableState.ts?raw'; import thresholdsOverrideMutationsHookSource from '@/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts?raw'; import alertIncidentPresentationSource from '@/utils/alertIncidentPresentation.ts?raw'; @@ -2888,11 +2889,19 @@ describe('frontend resource type boundaries', () => { expect(thresholdsTableStateHookSource).toContain('export function useThresholdsTableState'); expect(thresholdsTableStateHookSource).toContain('useCollapsedSections()'); expect(thresholdsTableStateHookSource).toContain('useThresholdsData(props, editingId, searchTerm)'); + expect(thresholdsTableStateHookSource).toContain('useThresholdsRecoveryDefaultsState(props)'); expect(thresholdsTableStateHookSource).toContain('useThresholdsOverrideMutations'); expect(thresholdsTableStateHookSource).not.toContain('const saveEdit = (resourceId: string) => {'); expect(thresholdsTableStateHookSource).not.toContain( 'const toggleNodeConnectivity = (resourceId: string, forceState?: boolean) => {', ); + expect(thresholdsDataHookSource).not.toContain('const sanitizeSnapshotConfig ='); + expect(thresholdsDataHookSource).not.toContain('const sanitizeBackupConfig ='); + expect(thresholdsRecoveryDefaultsStateHookSource).toContain( + 'export function useThresholdsRecoveryDefaultsState', + ); + expect(thresholdsRecoveryDefaultsStateHookSource).toContain('const sanitizeSnapshotConfig ='); + expect(thresholdsRecoveryDefaultsStateHookSource).toContain('const sanitizeBackupConfig ='); expect(thresholdsOverrideMutationsHookSource).toContain( 'export function useThresholdsOverrideMutations', );