diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index c4e301f19..4cfe955c4 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -149,12 +149,13 @@ assume discovery metadata is always present when deriving override IDs or toggle styling. The alerts page shell in `frontend-modern/src/pages/Alerts.tsx` must now keep -destinations, history, and schedule rendering feature-owned under +destinations, history, schedule, and thresholds rendering feature-owned under `frontend-modern/src/features/alerts/tabs/`. New alert tab surfaces should be extracted as feature modules instead of remaining page-local function blocks, so the page owns navigation/save orchestration while tab files own their runtime presentation, tab-local interaction logic, and any history-table -presentation that does not belong in a shared primitive. +presentation or thresholds-table adapter logic that does not belong in a shared +primitive. Alert filter metadata and grouped header consumers must also preserve the canonical `agent` and `node` header boundary when reusing shared filter diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 51d8a4be7..6f9eac37b 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -224,11 +224,13 @@ The alerts page shell now follows that same page-shell rule for feature tabs: orchestration, and cross-tab state, while feature-owned tab surfaces such as `frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx` and `frontend-modern/src/features/alerts/tabs/HistoryTab.tsx` plus -`frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx` own their tab-local -rendering and interaction logic. Future alert tab cleanup should continue by -extracting page-local tab blocks into feature modules rather than expanding the -top-level page file again, and history-table behavior should stay feature-owned -unless it graduates into a shared primitive used by more than one alert surface. +`frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx` and +`frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx` own their +tab-local rendering and interaction logic. Future alert tab cleanup should +continue by extracting page-local tab blocks into feature modules rather than +expanding the top-level page file again, and history-table behavior or +thresholds-table adapter logic should stay feature-owned unless it graduates +into a shared primitive used by more than one alert surface. Top-level settings surfaces must route through `Settings.tsx`, `SettingsPageShell.tsx`, and `frontend-modern/src/components/shared/SettingsPanel.tsx` instead of diff --git a/frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx b/frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx new file mode 100644 index 000000000..fb97a32d9 --- /dev/null +++ b/frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx @@ -0,0 +1,291 @@ +import type { Alert, PBSInstance, PMGInstance } from '@/types/api'; +import type { Resource } from '@/types/resource'; +import type { RawOverrideConfig, BackupAlertConfig, SnapshotAlertConfig, PMGThresholdDefaults } from '@/types/alerts'; +import { ThresholdsTable } from '@/components/Alerts/ThresholdsTable'; + +import type { Override } from '../types'; + +export interface ThresholdsTabProps { + allGuests: () => Resource[]; + pbsInstances: PBSInstance[]; + pmgInstances: PMGInstance[]; + nodes: Resource[]; + agents: Resource[]; + storage: Resource[]; + dockerHosts: Resource[]; + allResources: Resource[]; + guestDefaults: () => Record; + nodeDefaults: () => Record; + pbsDefaults: () => Record; + agentDefaults: () => Record; + dockerDefaults: () => { + cpu: number; + memory: number; + disk: number; + restartCount: number; + restartWindow: number; + memoryWarnPct: number; + memoryCriticalPct: number; + serviceWarnGapPercent: number; + serviceCriticalGapPercent: number; + }; + dockerDisableConnectivity: () => boolean; + dockerPoweredOffSeverity: () => 'warning' | 'critical'; + dockerIgnoredPrefixes: () => string[]; + ignoredGuestPrefixes: () => string[]; + guestTagWhitelist: () => string[]; + guestTagBlacklist: () => string[]; + storageDefault: () => number; + timeThresholds: () => { + guest: number; + node: number; + storage: number; + pbs: number; + agent: number; + }; + metricTimeThresholds: () => Record>; + overrides: () => Override[]; + rawOverridesConfig: () => Record; + pmgThresholds: () => PMGThresholdDefaults; + setPMGThresholds: ( + value: PMGThresholdDefaults | ((prev: PMGThresholdDefaults) => PMGThresholdDefaults), + ) => void; + setGuestDefaults: ( + value: + | Record + | ((prev: Record) => Record), + ) => void; + guestDisableConnectivity: () => boolean; + setGuestDisableConnectivity: (value: boolean) => void; + guestPoweredOffSeverity: () => 'warning' | 'critical'; + setGuestPoweredOffSeverity: (value: 'warning' | 'critical') => void; + setNodeDefaults: ( + value: + | Record + | ((prev: Record) => Record), + ) => void; + setAgentDefaults: ( + value: + | Record + | ((prev: Record) => Record), + ) => void; + setPBSDefaults: ( + value: + | Record + | ((prev: Record) => Record), + ) => void; + setDockerDefaults: ( + value: + | { + cpu: number; + memory: number; + disk: number; + restartCount: number; + restartWindow: number; + memoryWarnPct: number; + memoryCriticalPct: number; + serviceWarnGapPercent: number; + serviceCriticalGapPercent: number; + } + | ((prev: { + cpu: number; + memory: number; + disk: number; + restartCount: number; + restartWindow: number; + memoryWarnPct: number; + memoryCriticalPct: number; + serviceWarnGapPercent: number; + serviceCriticalGapPercent: number; + }) => { + cpu: number; + memory: number; + disk: number; + restartCount: number; + restartWindow: number; + memoryWarnPct: number; + memoryCriticalPct: number; + serviceWarnGapPercent: number; + serviceCriticalGapPercent: number; + }), + ) => void; + setDockerDisableConnectivity: (value: boolean) => void; + setDockerPoweredOffSeverity: (value: 'warning' | 'critical') => void; + setDockerIgnoredPrefixes: (value: string[] | ((prev: string[]) => string[])) => void; + setIgnoredGuestPrefixes: (value: string[] | ((prev: string[]) => string[])) => void; + setGuestTagWhitelist: (value: string[] | ((prev: string[]) => string[])) => void; + setGuestTagBlacklist: (value: string[] | ((prev: string[]) => string[])) => void; + setStorageDefault: (value: number) => void; + setMetricTimeThresholds: ( + value: + | Record> + | ((prev: Record>) => Record>), + ) => void; + snapshotDefaults: () => SnapshotAlertConfig; + setSnapshotDefaults: ( + value: SnapshotAlertConfig | ((prev: SnapshotAlertConfig) => SnapshotAlertConfig), + ) => void; + snapshotFactoryDefaults: SnapshotAlertConfig; + resetSnapshotDefaults: () => void; + backupDefaults: () => BackupAlertConfig; + setBackupDefaults: ( + value: BackupAlertConfig | ((prev: BackupAlertConfig) => BackupAlertConfig), + ) => void; + backupFactoryDefaults: BackupAlertConfig; + resetBackupDefaults: () => void; + setOverrides: (value: Override[]) => void; + setRawOverridesConfig: (value: Record) => void; + activeAlerts: Record; + setHasUnsavedChanges: (value: boolean) => void; + hasUnsavedChanges: () => boolean; + removeAlerts: (predicate: (alert: Alert) => boolean) => void; + disableAllNodes: () => boolean; + setDisableAllNodes: (value: boolean) => void; + disableAllGuests: () => boolean; + setDisableAllGuests: (value: boolean) => void; + disableAllAgents: () => boolean; + setDisableAllAgents: (value: boolean) => void; + disableAllStorage: () => boolean; + setDisableAllStorage: (value: boolean) => void; + disableAllPBS: () => boolean; + setDisableAllPBS: (value: boolean) => void; + disableAllPMG: () => boolean; + setDisableAllPMG: (value: boolean) => void; + disableAllDockerHosts: () => boolean; + setDisableAllDockerHosts: (value: boolean) => void; + disableAllDockerServices: () => boolean; + setDisableAllDockerServices: (value: boolean) => void; + disableAllDockerContainers: () => boolean; + setDisableAllDockerContainers: (value: boolean) => void; + disableAllNodesOffline: () => boolean; + setDisableAllNodesOffline: (value: boolean) => void; + disableAllGuestsOffline: () => boolean; + setDisableAllGuestsOffline: (value: boolean) => void; + disableAllAgentsOffline: () => boolean; + setDisableAllAgentsOffline: (value: boolean) => void; + disableAllPBSOffline: () => boolean; + setDisableAllPBSOffline: (value: boolean) => void; + disableAllPMGOffline: () => boolean; + setDisableAllPMGOffline: (value: boolean) => void; + disableAllDockerHostsOffline: () => boolean; + setDisableAllDockerHostsOffline: (value: boolean) => void; + resetGuestDefaults?: () => void; + resetNodeDefaults?: () => void; + resetPBSDefaults?: () => void; + resetAgentDefaults?: () => void; + resetDockerDefaults?: () => void; + resetDockerIgnoredPrefixes?: () => void; + resetStorageDefault?: () => void; + factoryGuestDefaults?: Record; + factoryNodeDefaults?: Record; + factoryPBSDefaults?: Record; + factoryAgentDefaults?: Record; + factoryDockerDefaults?: Record; + factoryStorageDefault?: number; +} + +export function ThresholdsTab(props: ThresholdsTabProps) { + return ( + + ); +} diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index 3b691aa86..7cde1d9dd 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -2,7 +2,6 @@ import { createSignal, Show, For, createMemo, createEffect, onMount, onCleanup } import { useBeforeLeave } from '@solidjs/router'; import type { JSX } from 'solid-js'; -import type { Alert } from '@/types/api'; import type { Resource } from '@/types/resource'; import type { RawOverrideConfig, BackupAlertConfig, SnapshotAlertConfig } from '@/types/alerts'; import { AlertsAPI } from '@/api/alerts'; @@ -22,8 +21,6 @@ import { eventBus } from '@/stores/events'; import { notificationStore } from '@/stores/notifications'; import Calendar from 'lucide-solid/icons/calendar'; -import { ThresholdsTable } from '@/components/Alerts/ThresholdsTable'; -import type { PMGThresholdDefaults } from '@/types/alerts'; import { useWebSocket } from '@/App'; import { useResources } from '@/hooks/useResources'; import { aiChatStore } from '@/stores/aiChat'; @@ -71,6 +68,7 @@ import { OverviewTab } from '@/features/alerts/OverviewTab'; import { DestinationsTab } from '@/features/alerts/tabs/DestinationsTab'; import { HistoryTab } from '@/features/alerts/tabs/HistoryTab'; import { ScheduleTab } from '@/features/alerts/tabs/ScheduleTab'; +import { ThresholdsTab } from '@/features/alerts/tabs/ThresholdsTab'; import { pathForTab, tabFromPath, @@ -2037,290 +2035,3 @@ export function Alerts() { // Overview Tab - Shows current alert status // Thresholds Tab - Improved design -interface ThresholdsTabProps { - allGuests: () => Resource[]; - pbsInstances: PBSInstance[]; - pmgInstances: PMGInstance[]; - nodes: Resource[]; - agents: Resource[]; - storage: Resource[]; - dockerHosts: Resource[]; - allResources: Resource[]; - guestDefaults: () => Record; - nodeDefaults: () => Record; - pbsDefaults: () => Record; - agentDefaults: () => Record; - dockerDefaults: () => { - cpu: number; - memory: number; - disk: number; - restartCount: number; - restartWindow: number; - memoryWarnPct: number; - memoryCriticalPct: number; - serviceWarnGapPercent: number; - serviceCriticalGapPercent: number; - }; - dockerDisableConnectivity: () => boolean; - dockerPoweredOffSeverity: () => 'warning' | 'critical'; - dockerIgnoredPrefixes: () => string[]; - ignoredGuestPrefixes: () => string[]; - guestTagWhitelist: () => string[]; - guestTagBlacklist: () => string[]; - storageDefault: () => number; - timeThresholds: () => { - guest: number; - node: number; - storage: number; - pbs: number; - agent: number; - }; - metricTimeThresholds: () => Record>; - overrides: () => Override[]; - rawOverridesConfig: () => Record; - pmgThresholds: () => PMGThresholdDefaults; - setPMGThresholds: ( - value: PMGThresholdDefaults | ((prev: PMGThresholdDefaults) => PMGThresholdDefaults), - ) => void; - setGuestDefaults: ( - value: - | Record - | ((prev: Record) => Record), - ) => void; - guestDisableConnectivity: () => boolean; - setGuestDisableConnectivity: (value: boolean) => void; - guestPoweredOffSeverity: () => 'warning' | 'critical'; - setGuestPoweredOffSeverity: (value: 'warning' | 'critical') => void; - setNodeDefaults: ( - value: - | Record - | ((prev: Record) => Record), - ) => void; - setAgentDefaults: ( - value: - | Record - | ((prev: Record) => Record), - ) => void; - setPBSDefaults: ( - value: - | Record - | ((prev: Record) => Record), - ) => void; - setDockerDefaults: ( - value: - | { - cpu: number; - memory: number; - disk: number; - restartCount: number; - restartWindow: number; - memoryWarnPct: number; - memoryCriticalPct: number; - serviceWarnGapPercent: number; - serviceCriticalGapPercent: number; - } - | ((prev: { - cpu: number; - memory: number; - disk: number; - restartCount: number; - restartWindow: number; - memoryWarnPct: number; - memoryCriticalPct: number; - serviceWarnGapPercent: number; - serviceCriticalGapPercent: number; - }) => { - cpu: number; - memory: number; - disk: number; - restartCount: number; - restartWindow: number; - memoryWarnPct: number; - memoryCriticalPct: number; - serviceWarnGapPercent: number; - serviceCriticalGapPercent: number; - }), - ) => void; - setDockerDisableConnectivity: (value: boolean) => void; - setDockerPoweredOffSeverity: (value: 'warning' | 'critical') => void; - setDockerIgnoredPrefixes: (value: string[] | ((prev: string[]) => string[])) => void; - setIgnoredGuestPrefixes: (value: string[] | ((prev: string[]) => string[])) => void; - setGuestTagWhitelist: (value: string[] | ((prev: string[]) => string[])) => void; - setGuestTagBlacklist: (value: string[] | ((prev: string[]) => string[])) => void; - setStorageDefault: (value: number) => void; - setMetricTimeThresholds: ( - value: - | Record> - | ((prev: Record>) => Record>), - ) => void; - snapshotDefaults: () => SnapshotAlertConfig; - setSnapshotDefaults: ( - value: SnapshotAlertConfig | ((prev: SnapshotAlertConfig) => SnapshotAlertConfig), - ) => void; - snapshotFactoryDefaults: SnapshotAlertConfig; - resetSnapshotDefaults: () => void; - backupDefaults: () => BackupAlertConfig; - setBackupDefaults: ( - value: BackupAlertConfig | ((prev: BackupAlertConfig) => BackupAlertConfig), - ) => void; - backupFactoryDefaults: BackupAlertConfig; - resetBackupDefaults: () => void; - setOverrides: (value: Override[]) => void; - setRawOverridesConfig: (value: Record) => void; - activeAlerts: Record; - setHasUnsavedChanges: (value: boolean) => void; - hasUnsavedChanges: () => boolean; - removeAlerts: (predicate: (alert: Alert) => boolean) => void; - // Global disable flags - disableAllNodes: () => boolean; - setDisableAllNodes: (value: boolean) => void; - disableAllGuests: () => boolean; - setDisableAllGuests: (value: boolean) => void; - disableAllAgents: () => boolean; - setDisableAllAgents: (value: boolean) => void; - disableAllStorage: () => boolean; - setDisableAllStorage: (value: boolean) => void; - disableAllPBS: () => boolean; - setDisableAllPBS: (value: boolean) => void; - disableAllPMG: () => boolean; - setDisableAllPMG: (value: boolean) => void; - disableAllDockerHosts: () => boolean; - setDisableAllDockerHosts: (value: boolean) => void; - disableAllDockerServices: () => boolean; - setDisableAllDockerServices: (value: boolean) => void; - disableAllDockerContainers: () => boolean; - setDisableAllDockerContainers: (value: boolean) => void; - // Global disable offline alerts flags - disableAllNodesOffline: () => boolean; - setDisableAllNodesOffline: (value: boolean) => void; - disableAllGuestsOffline: () => boolean; - setDisableAllGuestsOffline: (value: boolean) => void; - disableAllAgentsOffline: () => boolean; - setDisableAllAgentsOffline: (value: boolean) => void; - disableAllPBSOffline: () => boolean; - setDisableAllPBSOffline: (value: boolean) => void; - disableAllPMGOffline: () => boolean; - setDisableAllPMGOffline: (value: boolean) => void; - disableAllDockerHostsOffline: () => boolean; - setDisableAllDockerHostsOffline: (value: boolean) => void; - // Reset functions and factory defaults - resetGuestDefaults?: () => void; - resetNodeDefaults?: () => void; - resetPBSDefaults?: () => void; - resetAgentDefaults?: () => void; - resetDockerDefaults?: () => void; - resetDockerIgnoredPrefixes?: () => void; - resetStorageDefault?: () => void; - factoryGuestDefaults?: Record; - factoryNodeDefaults?: Record; - factoryPBSDefaults?: Record; - factoryAgentDefaults?: Record; - factoryDockerDefaults?: Record; - factoryStorageDefault?: number; -} - -function ThresholdsTab(props: ThresholdsTabProps) { - return ( - - ); -} diff --git a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts index dda7759d0..06b2db61c 100644 --- a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts +++ b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts @@ -3,6 +3,7 @@ import alertsPageSource from '@/pages/Alerts.tsx?raw'; import alertDestinationsTabSource from '@/features/alerts/tabs/DestinationsTab.tsx?raw'; import alertHistoryTabSource from '@/features/alerts/tabs/HistoryTab.tsx?raw'; import alertScheduleTabSource from '@/features/alerts/tabs/ScheduleTab.tsx?raw'; +import alertThresholdsTabSource from '@/features/alerts/tabs/ThresholdsTab.tsx?raw'; import { ALERT_TAB_SEGMENTS, @@ -153,7 +154,7 @@ describe('tab path helpers', () => { expect(tabFromPath('/alerts/summary', custom)).toBe('overview'); }); - it('keeps destinations, history, and schedule tabs feature-owned', () => { + it('keeps destinations, history, schedule, and thresholds tabs feature-owned', () => { expect(alertsPageSource).toContain( "import { DestinationsTab } from '@/features/alerts/tabs/DestinationsTab';", ); @@ -163,13 +164,18 @@ describe('tab path helpers', () => { expect(alertsPageSource).toContain( "import { ScheduleTab } from '@/features/alerts/tabs/ScheduleTab';", ); + expect(alertsPageSource).toContain( + "import { ThresholdsTab } from '@/features/alerts/tabs/ThresholdsTab';", + ); expect(alertsPageSource).not.toContain('function DestinationsTab('); expect(alertsPageSource).not.toContain('function HistoryTab('); expect(alertsPageSource).not.toContain('function ScheduleTab('); + expect(alertsPageSource).not.toContain('function ThresholdsTab('); expect(alertDestinationsTabSource).toContain('NotificationsAPI.getWebhooks'); expect(alertHistoryTabSource).toContain('AlertsAPI.getHistory'); expect(alertHistoryTabSource).toContain('IncidentTimelinePanel'); expect(alertScheduleTabSource).toContain('getAlertConfigQuietHourSuppressOptions'); + expect(alertThresholdsTabSource).toContain('ThresholdsTable'); }); }); diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index b9a7fcc9b..960c0bce0 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -226,6 +226,7 @@ import alertOverviewTabSource from '@/features/alerts/OverviewTab.tsx?raw'; import alertDestinationsTabSource from '@/features/alerts/tabs/DestinationsTab.tsx?raw'; import alertHistoryTabSource from '@/features/alerts/tabs/HistoryTab.tsx?raw'; import alertScheduleTabSource from '@/features/alerts/tabs/ScheduleTab.tsx?raw'; +import alertThresholdsTabSource from '@/features/alerts/tabs/ThresholdsTab.tsx?raw'; import alertIncidentPresentationSource from '@/utils/alertIncidentPresentation.ts?raw'; import alertHistoryPresentationSource from '@/utils/alertHistoryPresentation.ts?raw'; import bulkEditDialogSource from '@/components/Alerts/BulkEditDialog.tsx?raw'; @@ -2248,6 +2249,11 @@ describe('frontend resource type boundaries', () => { expect(alertsPageSource).toContain('getAlertsMobileTabClass'); expect(alertsPageSource).toContain('getAlertsTabTitle'); expect(alertsPageSource).toContain('getAlertsTabGroups'); + expect(alertsPageSource).toContain("import { ThresholdsTab } from '@/features/alerts/tabs/ThresholdsTab';"); + expect(alertsPageSource).not.toContain("import { ThresholdsTable } from '@/components/Alerts/ThresholdsTable';"); + expect(alertsPageSource).not.toContain('function ThresholdsTab('); + expect(alertThresholdsTabSource).toContain("import { ThresholdsTable } from '@/components/Alerts/ThresholdsTable';"); + expect(alertThresholdsTabSource).toContain('pmgThresholds={props.pmgThresholds}'); expect(alertScheduleTabSource).toContain('getAlertGroupingCardClass'); expect(alertScheduleTabSource).toContain('getAlertGroupingCheckboxClass'); expect(alertScheduleTabSource).toContain('getAlertQuietDayButtonClass');