mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-12 07:18:49 +00:00
Extract alert destinations state owner
This commit is contained in:
@@ -204,10 +204,13 @@ override normalization now route through
|
||||
living inline in `frontend-modern/src/pages/Alerts.tsx`. The page shell owns
|
||||
navigation, activation chrome, and cross-surface routing; the configuration
|
||||
surface is now a shell that composes the destinations, schedule, and thresholds
|
||||
tabs, while `frontend-modern/src/features/alerts/useAlertsConfigurationState.ts`
|
||||
owns alert config load/save behavior, notification-config reloads, defaults,
|
||||
override normalization, and derived resource mapping beneath that feature
|
||||
boundary.
|
||||
tabs. The canonical alert-policy runtime owner is now
|
||||
`frontend-modern/src/features/alerts/useAlertsConfigurationState.ts`, while
|
||||
notification destination reload and persistence now route through
|
||||
`frontend-modern/src/features/alerts/useAlertDestinationsState.ts`. Future
|
||||
config cleanup should extend the alert-policy hook or the destinations hook
|
||||
based on which subsystem actually owns the behavior instead of letting the
|
||||
broader configuration hook absorb both concerns again.
|
||||
|
||||
Alert filter metadata and grouped header consumers must also preserve the
|
||||
canonical `agent` and `node` header boundary when reusing shared filter
|
||||
|
||||
@@ -417,12 +417,11 @@ The alerts page now also applies the same shell-versus-feature rule to
|
||||
configuration orchestration. `frontend-modern/src/pages/Alerts.tsx` is the page
|
||||
shell, while `frontend-modern/src/features/alerts/AlertsConfigurationSurface.tsx`
|
||||
is the feature shell. The canonical runtime owner is now
|
||||
`frontend-modern/src/features/alerts/useAlertsConfigurationState.ts`, which
|
||||
owns alert config load/save behavior, notification-config reloads, defaults,
|
||||
derived resource mapping, and threshold-override normalization for the
|
||||
destinations, schedule, and thresholds tabs. Future cleanup should extend that
|
||||
feature hook or a narrower shared primitive, not move config control flow back
|
||||
into the top-level page shell.
|
||||
`frontend-modern/src/features/alerts/useAlertsConfigurationState.ts` for alert
|
||||
policy state, while `frontend-modern/src/features/alerts/useAlertDestinationsState.ts`
|
||||
owns notification destination reload and persistence. Future cleanup should
|
||||
extend the alert-policy hook or the destinations hook based on the true owner,
|
||||
not move config control flow back into the top-level page shell.
|
||||
The same rule now also covers cross-tab incident timelines: the shared runtime
|
||||
owner is `frontend-modern/src/features/alerts/useAlertIncidentTimelineState.ts`,
|
||||
while `frontend-modern/src/features/alerts/OverviewTab.tsx` and
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { renderHook } from '@solidjs/testing-library';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { NotificationsAPI } from '@/api/notifications';
|
||||
|
||||
import { useAlertDestinationsState } from '../useAlertDestinationsState';
|
||||
|
||||
vi.mock('@/api/notifications', () => ({
|
||||
NotificationsAPI: {
|
||||
getAppriseConfig: vi.fn(),
|
||||
getEmailConfig: vi.fn(),
|
||||
updateAppriseConfig: vi.fn(),
|
||||
updateEmailConfig: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useAlertDestinationsState', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(NotificationsAPI.getEmailConfig).mockReset();
|
||||
vi.mocked(NotificationsAPI.getAppriseConfig).mockReset();
|
||||
vi.mocked(NotificationsAPI.updateEmailConfig).mockReset();
|
||||
vi.mocked(NotificationsAPI.updateAppriseConfig).mockReset();
|
||||
});
|
||||
|
||||
it('owns alert destinations reload and save behavior separately from alert policy config', async () => {
|
||||
const [activeTab, setActiveTab] = createSignal<'overview' | 'destinations'>('overview');
|
||||
|
||||
vi.mocked(NotificationsAPI.getEmailConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
provider: 'smtp',
|
||||
server: 'smtp.example.com',
|
||||
port: 587,
|
||||
username: 'ops@example.com',
|
||||
password: '',
|
||||
from: 'pulse@example.com',
|
||||
to: ['alerts@example.com'],
|
||||
tls: true,
|
||||
startTLS: true,
|
||||
} as any);
|
||||
vi.mocked(NotificationsAPI.getAppriseConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
mode: 'cli',
|
||||
targets: ['mailto://ops@example.com'],
|
||||
cliPath: '/usr/local/bin/apprise',
|
||||
timeoutSeconds: 20,
|
||||
} as any);
|
||||
vi.mocked(NotificationsAPI.updateEmailConfig).mockResolvedValue(undefined as any);
|
||||
vi.mocked(NotificationsAPI.updateAppriseConfig).mockResolvedValue({
|
||||
enabled: true,
|
||||
mode: 'http',
|
||||
targets: ['https://notify.example.test'],
|
||||
serverUrl: 'https://apprise.example.test',
|
||||
configKey: 'prod',
|
||||
apiKey: 'masked',
|
||||
apiKeyHeader: 'X-API-KEY',
|
||||
timeoutSeconds: 30,
|
||||
skipTlsVerify: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useAlertDestinationsState({ activeTab }));
|
||||
|
||||
await result.loadDestinations();
|
||||
expect(NotificationsAPI.getEmailConfig).toHaveBeenCalledTimes(1);
|
||||
expect(NotificationsAPI.getAppriseConfig).toHaveBeenCalledTimes(1);
|
||||
expect(result.emailConfig().server).toBe('smtp.example.com');
|
||||
expect(result.appriseConfig().targetsText).toContain('mailto://ops@example.com');
|
||||
|
||||
setActiveTab('destinations');
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(NotificationsAPI.getEmailConfig).toHaveBeenCalledTimes(2);
|
||||
expect(NotificationsAPI.getAppriseConfig).toHaveBeenCalledTimes(2);
|
||||
|
||||
setActiveTab('overview');
|
||||
await Promise.resolve();
|
||||
|
||||
result.setEmailConfig({
|
||||
...result.emailConfig(),
|
||||
server: 'smtp.internal',
|
||||
});
|
||||
result.setAppriseConfig({
|
||||
...result.appriseConfig(),
|
||||
mode: 'http',
|
||||
serverUrl: 'https://apprise.internal',
|
||||
targetsText: 'https://notify.internal',
|
||||
});
|
||||
|
||||
await result.saveDestinations();
|
||||
|
||||
expect(NotificationsAPI.updateEmailConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ server: 'smtp.internal' }),
|
||||
);
|
||||
expect(NotificationsAPI.updateAppriseConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mode: 'http',
|
||||
serverUrl: 'https://apprise.internal',
|
||||
targets: ['https://notify.internal'],
|
||||
}),
|
||||
);
|
||||
expect(result.appriseConfig().mode).toBe('http');
|
||||
expect(result.appriseConfig().serverUrl).toBe('https://apprise.example.test');
|
||||
|
||||
result.resetDestinations();
|
||||
expect(result.destConfigLoadError()).toBeNull();
|
||||
expect(result.emailConfig().enabled).toBe(false);
|
||||
expect(result.appriseConfig().enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
import { createEffect, createSignal } from 'solid-js';
|
||||
import type { Accessor } from 'solid-js';
|
||||
|
||||
import type { AppriseConfig, EmailConfig } from '@/api/notifications';
|
||||
import { NotificationsAPI } from '@/api/notifications';
|
||||
import { getAlertDestinationsConfigLoadError } from '@/utils/alertDestinationsPresentation';
|
||||
import { logger } from '@/utils/logger';
|
||||
|
||||
import {
|
||||
createDefaultAppriseConfig,
|
||||
createDefaultEmailConfig,
|
||||
formatAppriseTargets,
|
||||
normalizeEmailConfigFromAPI,
|
||||
parseAppriseTargets,
|
||||
} from './helpers';
|
||||
import type { AlertTab, UIAppriseConfig, UIEmailConfig } from './types';
|
||||
|
||||
interface AlertDestinationsStateOptions {
|
||||
activeTab: Accessor<AlertTab>;
|
||||
}
|
||||
|
||||
function normalizeAppriseConfig(config: Partial<AppriseConfig> | null | undefined): UIAppriseConfig {
|
||||
return {
|
||||
enabled: config?.enabled ?? false,
|
||||
mode: config?.mode === 'http' ? 'http' : 'cli',
|
||||
targetsText: formatAppriseTargets(config?.targets),
|
||||
cliPath: config?.cliPath || 'apprise',
|
||||
timeoutSeconds:
|
||||
typeof config?.timeoutSeconds === 'number' && config.timeoutSeconds > 0
|
||||
? config.timeoutSeconds
|
||||
: 15,
|
||||
serverUrl: config?.serverUrl || '',
|
||||
configKey: config?.configKey || '',
|
||||
apiKey: config?.apiKey || '',
|
||||
apiKeyHeader: config?.apiKeyHeader || 'X-API-KEY',
|
||||
skipTlsVerify: Boolean(config?.skipTlsVerify),
|
||||
};
|
||||
}
|
||||
|
||||
export function useAlertDestinationsState(options: AlertDestinationsStateOptions) {
|
||||
const [isLoadingDestinations, setIsLoadingDestinations] = createSignal(false);
|
||||
const [destConfigLoadError, setDestConfigLoadError] = createSignal<string | null>(null);
|
||||
const [emailConfig, setEmailConfig] = createSignal<UIEmailConfig>(createDefaultEmailConfig());
|
||||
const [appriseConfig, setAppriseConfig] = createSignal<UIAppriseConfig>(
|
||||
createDefaultAppriseConfig(),
|
||||
);
|
||||
|
||||
let reloadVersion = 0;
|
||||
let lastActiveTab: AlertTab | null = null;
|
||||
|
||||
const resetDestinations = () => {
|
||||
setDestConfigLoadError(null);
|
||||
setEmailConfig(createDefaultEmailConfig());
|
||||
setAppriseConfig(createDefaultAppriseConfig());
|
||||
};
|
||||
|
||||
const loadDestinations = async (options: { indicateLoading?: boolean } = {}) => {
|
||||
const indicateLoading = options.indicateLoading ?? false;
|
||||
const thisVersion = ++reloadVersion;
|
||||
|
||||
if (indicateLoading) {
|
||||
setIsLoadingDestinations(true);
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
NotificationsAPI.getEmailConfig(),
|
||||
NotificationsAPI.getAppriseConfig(),
|
||||
]);
|
||||
|
||||
if (thisVersion !== reloadVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [emailResult, appriseResult] = results;
|
||||
|
||||
if (emailResult.status === 'fulfilled') {
|
||||
setEmailConfig(normalizeEmailConfigFromAPI(emailResult.value));
|
||||
}
|
||||
|
||||
if (appriseResult.status === 'fulfilled') {
|
||||
setAppriseConfig(normalizeAppriseConfig(appriseResult.value));
|
||||
}
|
||||
|
||||
const failures = results.filter(
|
||||
(result): result is PromiseRejectedResult => result.status === 'rejected',
|
||||
);
|
||||
|
||||
if (failures.length > 0) {
|
||||
failures.forEach((result) => {
|
||||
logger.error('Failed to load notification configuration:', result.reason);
|
||||
});
|
||||
setDestConfigLoadError(getAlertDestinationsConfigLoadError());
|
||||
} else {
|
||||
setDestConfigLoadError(null);
|
||||
}
|
||||
|
||||
if (indicateLoading) {
|
||||
setIsLoadingDestinations(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveDestinations = async () => {
|
||||
const currentEmailConfig = emailConfig();
|
||||
await NotificationsAPI.updateEmailConfig({
|
||||
enabled: currentEmailConfig.enabled,
|
||||
provider: currentEmailConfig.provider,
|
||||
server: currentEmailConfig.server,
|
||||
port: currentEmailConfig.port,
|
||||
username: currentEmailConfig.username,
|
||||
password: currentEmailConfig.password,
|
||||
from: currentEmailConfig.from,
|
||||
to: currentEmailConfig.to,
|
||||
tls: currentEmailConfig.tls,
|
||||
startTLS: currentEmailConfig.startTLS,
|
||||
} as EmailConfig);
|
||||
|
||||
const currentAppriseConfig = appriseConfig();
|
||||
const updatedApprise = await NotificationsAPI.updateAppriseConfig({
|
||||
enabled: currentAppriseConfig.enabled,
|
||||
mode: currentAppriseConfig.mode,
|
||||
targets: parseAppriseTargets(currentAppriseConfig.targetsText),
|
||||
cliPath: currentAppriseConfig.cliPath,
|
||||
timeoutSeconds: currentAppriseConfig.timeoutSeconds,
|
||||
serverUrl: currentAppriseConfig.serverUrl,
|
||||
configKey: currentAppriseConfig.configKey,
|
||||
apiKey: currentAppriseConfig.apiKey,
|
||||
apiKeyHeader: currentAppriseConfig.apiKeyHeader,
|
||||
skipTlsVerify: currentAppriseConfig.skipTlsVerify,
|
||||
} as AppriseConfig);
|
||||
|
||||
setAppriseConfig(normalizeAppriseConfig(updatedApprise));
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
const current = options.activeTab();
|
||||
const previous = lastActiveTab;
|
||||
lastActiveTab = current;
|
||||
|
||||
if (current !== 'destinations' || previous === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
void loadDestinations({ indicateLoading: true });
|
||||
});
|
||||
|
||||
return {
|
||||
isLoadingDestinations,
|
||||
destConfigLoadError,
|
||||
emailConfig,
|
||||
setEmailConfig,
|
||||
appriseConfig,
|
||||
setAppriseConfig,
|
||||
resetDestinations,
|
||||
loadDestinations,
|
||||
saveDestinations,
|
||||
};
|
||||
}
|
||||
@@ -2,11 +2,9 @@ import { createEffect, createMemo, createSignal, onCleanup, onMount } from 'soli
|
||||
import type { Accessor } from 'solid-js';
|
||||
|
||||
import { AlertsAPI } from '@/api/alerts';
|
||||
import { NotificationsAPI } from '@/api/notifications';
|
||||
import { eventBus } from '@/stores/events';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import type { Alert, PBSInstance, PMGInstance } from '@/types/api';
|
||||
import type { AppriseConfig, EmailConfig } from '@/api/notifications';
|
||||
import type {
|
||||
ActivationState,
|
||||
BackupAlertConfig,
|
||||
@@ -19,7 +17,6 @@ import {
|
||||
getAlertConfigReloadFailure,
|
||||
getAlertConfigSaveSuccess,
|
||||
} from '@/utils/alertConfigPresentation';
|
||||
import { getAlertDestinationsConfigLoadError } from '@/utils/alertDestinationsPresentation';
|
||||
import { getActionableAgentIdFromResource, hasAgentFacet } from '@/utils/agentResources';
|
||||
import { isAppContainerDiscoveryResourceType } from '@/utils/discoveryTarget';
|
||||
import { logger } from '@/utils/logger';
|
||||
@@ -27,9 +24,7 @@ import { pbsInstanceFromResource, pmgInstanceFromResource } from '@/utils/resour
|
||||
|
||||
import {
|
||||
clampMaxAlertsPerHour,
|
||||
createDefaultAppriseConfig,
|
||||
createDefaultCooldown,
|
||||
createDefaultEmailConfig,
|
||||
createDefaultEscalation,
|
||||
createDefaultGrouping,
|
||||
createDefaultQuietHours,
|
||||
@@ -37,15 +32,13 @@ import {
|
||||
DEFAULT_DELAY_SECONDS,
|
||||
extractTriggerValues,
|
||||
fallbackMaxAlertsPerHour,
|
||||
formatAppriseTargets,
|
||||
getAlertResourceDisplayLabel,
|
||||
getTriggerValue,
|
||||
guessNumericId,
|
||||
normalizeEmailConfigFromAPI,
|
||||
normalizeMetricDelayMap,
|
||||
parseAppriseTargets,
|
||||
platformData,
|
||||
} from './helpers';
|
||||
import { useAlertDestinationsState } from './useAlertDestinationsState';
|
||||
import type {
|
||||
AlertTab,
|
||||
CooldownConfig,
|
||||
@@ -54,8 +47,6 @@ import type {
|
||||
GroupingConfig,
|
||||
Override,
|
||||
QuietHoursConfig,
|
||||
UIAppriseConfig,
|
||||
UIEmailConfig,
|
||||
} from './types';
|
||||
import { GROUPING_WINDOW_DEFAULT_SECONDS, clampCooldownMinutes } from './types';
|
||||
|
||||
@@ -138,16 +129,10 @@ export const FACTORY_BACKUP_DEFAULTS: BackupAlertConfig = {
|
||||
|
||||
export function useAlertsConfigurationState(props: AlertsConfigurationSurfaceProps) {
|
||||
const [isReloadingConfig, setIsReloadingConfig] = createSignal(false);
|
||||
const [isLoadingDestinations, setIsLoadingDestinations] = createSignal(false);
|
||||
const [destConfigLoadError, setDestConfigLoadError] = createSignal<string | null>(null);
|
||||
const [overrides, setOverrides] = createSignal<Override[]>([]);
|
||||
const [rawOverridesConfig, setRawOverridesConfig] = createSignal<
|
||||
Record<string, RawOverrideConfig>
|
||||
>({});
|
||||
const [emailConfig, setEmailConfig] = createSignal<UIEmailConfig>(createDefaultEmailConfig());
|
||||
const [appriseConfig, setAppriseConfig] = createSignal<UIAppriseConfig>(
|
||||
createDefaultAppriseConfig(),
|
||||
);
|
||||
const [scheduleQuietHours, setScheduleQuietHours] =
|
||||
createSignal<QuietHoursConfig>(createDefaultQuietHours());
|
||||
const [scheduleCooldown, setScheduleCooldown] =
|
||||
@@ -236,6 +221,7 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro
|
||||
const [disableAllPBSOffline, setDisableAllPBSOffline] = createSignal(false);
|
||||
const [disableAllPMGOffline, setDisableAllPMGOffline] = createSignal(false);
|
||||
const [disableAllDockerHostsOffline, setDisableAllDockerHostsOffline] = createSignal(false);
|
||||
const destinationsState = useAlertDestinationsState({ activeTab: props.activeTab });
|
||||
|
||||
const pd = platformData;
|
||||
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
|
||||
@@ -645,7 +631,7 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro
|
||||
const loadAlertConfiguration = async (options: { notify?: boolean } = {}) => {
|
||||
setIsReloadingConfig(true);
|
||||
props.setHasUnsavedChanges(false);
|
||||
setDestConfigLoadError(null);
|
||||
destinationsState.resetDestinations();
|
||||
|
||||
setGuestDefaults({ ...FACTORY_GUEST_DEFAULTS });
|
||||
setGuestDisableConnectivity(false);
|
||||
@@ -674,8 +660,6 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro
|
||||
setScheduleGrouping(createDefaultGrouping());
|
||||
setScheduleEscalation(createDefaultEscalation());
|
||||
setNotifyOnResolve(createDefaultResolveNotifications());
|
||||
setEmailConfig(createDefaultEmailConfig());
|
||||
setAppriseConfig(createDefaultAppriseConfig());
|
||||
setBackupDefaults({ ...FACTORY_BACKUP_DEFAULTS });
|
||||
setSnapshotDefaults({ ...FACTORY_SNAPSHOT_DEFAULTS });
|
||||
|
||||
@@ -971,42 +955,13 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const emailConfigData = await NotificationsAPI.getEmailConfig();
|
||||
setEmailConfig(normalizeEmailConfigFromAPI(emailConfigData));
|
||||
} catch (emailError) {
|
||||
logger.error('Failed to load email configuration:', emailError);
|
||||
setDestConfigLoadError(getAlertDestinationsConfigLoadError());
|
||||
}
|
||||
|
||||
try {
|
||||
const appriseData = await NotificationsAPI.getAppriseConfig();
|
||||
setAppriseConfig({
|
||||
enabled: appriseData.enabled ?? false,
|
||||
mode: appriseData.mode === 'http' ? 'http' : 'cli',
|
||||
targetsText: formatAppriseTargets(appriseData.targets),
|
||||
cliPath: appriseData.cliPath || 'apprise',
|
||||
timeoutSeconds:
|
||||
typeof appriseData.timeoutSeconds === 'number' && appriseData.timeoutSeconds > 0
|
||||
? appriseData.timeoutSeconds
|
||||
: 15,
|
||||
serverUrl: appriseData.serverUrl || '',
|
||||
configKey: appriseData.configKey || '',
|
||||
apiKey: appriseData.apiKey || '',
|
||||
apiKeyHeader: appriseData.apiKeyHeader || 'X-API-KEY',
|
||||
skipTlsVerify: Boolean(appriseData.skipTlsVerify),
|
||||
});
|
||||
} catch (appriseError) {
|
||||
logger.error('Failed to load Apprise configuration:', appriseError);
|
||||
setDestConfigLoadError(getAlertDestinationsConfigLoadError());
|
||||
}
|
||||
await destinationsState.loadDestinations();
|
||||
|
||||
if (options.notify) {
|
||||
notificationStore.success(getAlertConfigDiscardedSuccess());
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to load alert configuration:', error);
|
||||
setDestConfigLoadError(getAlertDestinationsConfigLoadError());
|
||||
if (options.notify) {
|
||||
notificationStore.error(getAlertConfigReloadFailure());
|
||||
}
|
||||
@@ -1193,48 +1148,7 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro
|
||||
|
||||
await AlertsAPI.updateConfig(alertConfig);
|
||||
|
||||
const currentEmailConfig = emailConfig();
|
||||
await NotificationsAPI.updateEmailConfig({
|
||||
enabled: currentEmailConfig.enabled,
|
||||
provider: currentEmailConfig.provider,
|
||||
server: currentEmailConfig.server,
|
||||
port: currentEmailConfig.port,
|
||||
username: currentEmailConfig.username,
|
||||
password: currentEmailConfig.password,
|
||||
from: currentEmailConfig.from,
|
||||
to: currentEmailConfig.to,
|
||||
tls: currentEmailConfig.tls,
|
||||
startTLS: currentEmailConfig.startTLS,
|
||||
} as EmailConfig);
|
||||
|
||||
const currentAppriseConfig = appriseConfig();
|
||||
const updatedApprise = await NotificationsAPI.updateAppriseConfig({
|
||||
enabled: currentAppriseConfig.enabled,
|
||||
mode: currentAppriseConfig.mode,
|
||||
targets: parseAppriseTargets(currentAppriseConfig.targetsText),
|
||||
cliPath: currentAppriseConfig.cliPath,
|
||||
timeoutSeconds: currentAppriseConfig.timeoutSeconds,
|
||||
serverUrl: currentAppriseConfig.serverUrl,
|
||||
configKey: currentAppriseConfig.configKey,
|
||||
apiKey: currentAppriseConfig.apiKey,
|
||||
apiKeyHeader: currentAppriseConfig.apiKeyHeader,
|
||||
skipTlsVerify: currentAppriseConfig.skipTlsVerify,
|
||||
} as AppriseConfig);
|
||||
setAppriseConfig({
|
||||
enabled: updatedApprise.enabled ?? false,
|
||||
mode: updatedApprise.mode === 'http' ? 'http' : 'cli',
|
||||
targetsText: formatAppriseTargets(updatedApprise.targets),
|
||||
cliPath: updatedApprise.cliPath || 'apprise',
|
||||
timeoutSeconds:
|
||||
typeof updatedApprise.timeoutSeconds === 'number' && updatedApprise.timeoutSeconds > 0
|
||||
? updatedApprise.timeoutSeconds
|
||||
: 15,
|
||||
serverUrl: updatedApprise.serverUrl || '',
|
||||
configKey: updatedApprise.configKey || '',
|
||||
apiKey: updatedApprise.apiKey || '',
|
||||
apiKeyHeader: updatedApprise.apiKeyHeader || 'X-API-KEY',
|
||||
skipTlsVerify: Boolean(updatedApprise.skipTlsVerify),
|
||||
});
|
||||
await destinationsState.saveDestinations();
|
||||
props.setHasUnsavedChanges(false);
|
||||
notificationStore.success(getAlertConfigSaveSuccess());
|
||||
};
|
||||
@@ -1249,70 +1163,18 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro
|
||||
});
|
||||
});
|
||||
|
||||
let destReloadVersion = 0;
|
||||
createEffect(() => {
|
||||
if (props.activeTab() !== 'destinations') {
|
||||
return;
|
||||
}
|
||||
|
||||
const thisVersion = ++destReloadVersion;
|
||||
setIsLoadingDestinations(true);
|
||||
|
||||
const emailPromise = NotificationsAPI.getEmailConfig().then((emailConfigData) => {
|
||||
if (thisVersion === destReloadVersion) {
|
||||
setEmailConfig(normalizeEmailConfigFromAPI(emailConfigData));
|
||||
}
|
||||
});
|
||||
|
||||
const apprisePromise = NotificationsAPI.getAppriseConfig().then((appriseData) => {
|
||||
if (thisVersion === destReloadVersion) {
|
||||
setAppriseConfig({
|
||||
enabled: appriseData.enabled ?? false,
|
||||
mode: appriseData.mode === 'http' ? 'http' : 'cli',
|
||||
targetsText: formatAppriseTargets(appriseData.targets),
|
||||
cliPath: appriseData.cliPath || 'apprise',
|
||||
timeoutSeconds:
|
||||
typeof appriseData.timeoutSeconds === 'number' && appriseData.timeoutSeconds > 0
|
||||
? appriseData.timeoutSeconds
|
||||
: 15,
|
||||
serverUrl: appriseData.serverUrl || '',
|
||||
configKey: appriseData.configKey || '',
|
||||
apiKey: appriseData.apiKey || '',
|
||||
apiKeyHeader: appriseData.apiKeyHeader || 'X-API-KEY',
|
||||
skipTlsVerify: Boolean(appriseData.skipTlsVerify),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
void Promise.allSettled([emailPromise, apprisePromise]).then((results) => {
|
||||
if (thisVersion !== destReloadVersion) return;
|
||||
const failed = results.some((result) => result.status === 'rejected');
|
||||
if (failed) {
|
||||
results
|
||||
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
||||
.forEach((result) => {
|
||||
logger.error('Failed to reload notification configuration:', result.reason);
|
||||
});
|
||||
setDestConfigLoadError(getAlertDestinationsConfigLoadError());
|
||||
} else {
|
||||
setDestConfigLoadError(null);
|
||||
}
|
||||
setIsLoadingDestinations(false);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
isReloadingConfig,
|
||||
isLoadingDestinations,
|
||||
destConfigLoadError,
|
||||
isLoadingDestinations: destinationsState.isLoadingDestinations,
|
||||
destConfigLoadError: destinationsState.destConfigLoadError,
|
||||
overrides,
|
||||
setOverrides,
|
||||
rawOverridesConfig,
|
||||
setRawOverridesConfig,
|
||||
emailConfig,
|
||||
setEmailConfig,
|
||||
appriseConfig,
|
||||
setAppriseConfig,
|
||||
emailConfig: destinationsState.emailConfig,
|
||||
setEmailConfig: destinationsState.setEmailConfig,
|
||||
appriseConfig: destinationsState.appriseConfig,
|
||||
setAppriseConfig: destinationsState.setAppriseConfig,
|
||||
scheduleQuietHours,
|
||||
setScheduleQuietHours,
|
||||
scheduleCooldown,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
|
||||
import alertsPageSource from '@/pages/Alerts.tsx?raw';
|
||||
import alertsConfigurationSurfaceSource from '@/features/alerts/AlertsConfigurationSurface.tsx?raw';
|
||||
import alertsConfigurationStateSource from '@/features/alerts/useAlertsConfigurationState.ts?raw';
|
||||
import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw';
|
||||
import alertIncidentTimelineStateSource from '@/features/alerts/useAlertIncidentTimelineState.ts?raw';
|
||||
import alertDestinationsTabSource from '@/features/alerts/tabs/DestinationsTab.tsx?raw';
|
||||
import alertHistoryTabSource from '@/features/alerts/tabs/HistoryTab.tsx?raw';
|
||||
@@ -185,9 +186,13 @@ describe('tab path helpers', () => {
|
||||
expect(alertsConfigurationSurfaceSource).not.toContain("eventBus.on('org_switched'");
|
||||
expect(alertsConfigurationStateSource).toContain('export function useAlertsConfigurationState');
|
||||
expect(alertsConfigurationStateSource).toContain('AlertsAPI.getConfig');
|
||||
expect(alertsConfigurationStateSource).toContain('NotificationsAPI.getEmailConfig');
|
||||
expect(alertsConfigurationStateSource).toContain('NotificationsAPI.updateEmailConfig');
|
||||
expect(alertsConfigurationStateSource).toContain('useAlertDestinationsState');
|
||||
expect(alertsConfigurationStateSource).not.toContain('NotificationsAPI.getEmailConfig');
|
||||
expect(alertsConfigurationStateSource).not.toContain('NotificationsAPI.updateEmailConfig');
|
||||
expect(alertsConfigurationStateSource).toContain("eventBus.on('org_switched'");
|
||||
expect(alertDestinationsStateSource).toContain('export function useAlertDestinationsState');
|
||||
expect(alertDestinationsStateSource).toContain('NotificationsAPI.getEmailConfig');
|
||||
expect(alertDestinationsStateSource).toContain('NotificationsAPI.updateEmailConfig');
|
||||
expect(alertsPageSource).toContain(
|
||||
"import { HistoryTab } from '@/features/alerts/tabs/HistoryTab';",
|
||||
);
|
||||
|
||||
@@ -326,6 +326,7 @@ import alertWebhookPresentationSource from '@/utils/alertWebhookPresentation.ts?
|
||||
import alertOverviewTabSource from '@/features/alerts/OverviewTab.tsx?raw';
|
||||
import alertsConfigurationSurfaceSource from '@/features/alerts/AlertsConfigurationSurface.tsx?raw';
|
||||
import alertsConfigurationStateSource from '@/features/alerts/useAlertsConfigurationState.ts?raw';
|
||||
import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw';
|
||||
import alertIncidentTimelineStateSource from '@/features/alerts/useAlertIncidentTimelineState.ts?raw';
|
||||
import alertDestinationsTabSource from '@/features/alerts/tabs/DestinationsTab.tsx?raw';
|
||||
import alertHistoryTabSource from '@/features/alerts/tabs/HistoryTab.tsx?raw';
|
||||
@@ -2764,11 +2765,14 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(alertsPageSource).not.toContain('getAlertDestinationsConfigLoadError');
|
||||
expect(alertsConfigurationSurfaceSource).toContain('useAlertsConfigurationState');
|
||||
expect(alertsConfigurationSurfaceSource).not.toContain('AlertsAPI.getConfig');
|
||||
expect(alertsConfigurationStateSource).toContain('getAlertDestinationsConfigLoadError');
|
||||
expect(alertsConfigurationStateSource).toContain('AlertsAPI.getConfig');
|
||||
expect(alertsConfigurationStateSource).toContain('NotificationsAPI.getEmailConfig');
|
||||
expect(alertsConfigurationStateSource).toContain('NotificationsAPI.updateEmailConfig');
|
||||
expect(alertsConfigurationStateSource).toContain('useAlertDestinationsState');
|
||||
expect(alertsConfigurationStateSource).not.toContain('NotificationsAPI.getEmailConfig');
|
||||
expect(alertsConfigurationStateSource).not.toContain('NotificationsAPI.updateEmailConfig');
|
||||
expect(alertsConfigurationStateSource).toContain("eventBus.on('org_switched'");
|
||||
expect(alertDestinationsStateSource).toContain('getAlertDestinationsConfigLoadError');
|
||||
expect(alertDestinationsStateSource).toContain('NotificationsAPI.getEmailConfig');
|
||||
expect(alertDestinationsStateSource).toContain('NotificationsAPI.updateEmailConfig');
|
||||
expect(alertDestinationsTabSource).toContain('getAlertDestinationsWebhookLoadError');
|
||||
expect(alertDestinationsTabSource).toContain('getAlertDestinationsLoadErrorBanner');
|
||||
expect(alertDestinationsTabSource).toContain('getAlertDestinationsAppriseTargetsHelp');
|
||||
|
||||
Reference in New Issue
Block a user