parent
3fa7356ff3
commit
88d0aeebdf
2 changed files with 106 additions and 10 deletions
|
|
@ -210,6 +210,45 @@ interface EscalationConfig {
|
|||
levels: EscalationLevel[];
|
||||
}
|
||||
|
||||
const COOLDOWN_MIN_MINUTES = 5;
|
||||
const COOLDOWN_MAX_MINUTES = 120;
|
||||
const COOLDOWN_DEFAULT_MINUTES = 30;
|
||||
const MAX_ALERTS_MIN = 1;
|
||||
const MAX_ALERTS_MAX = 10;
|
||||
const MAX_ALERTS_DEFAULT = 3;
|
||||
|
||||
export const clampCooldownMinutes = (value?: number): number => {
|
||||
const numericValue = typeof value === 'number' ? value : Number.NaN;
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
return COOLDOWN_MIN_MINUTES;
|
||||
}
|
||||
return Math.min(COOLDOWN_MAX_MINUTES, Math.max(COOLDOWN_MIN_MINUTES, numericValue));
|
||||
};
|
||||
|
||||
export const fallbackCooldownMinutes = (value?: number): number => {
|
||||
const numericValue = typeof value === 'number' ? value : Number.NaN;
|
||||
if (!Number.isFinite(numericValue) || numericValue <= 0) {
|
||||
return COOLDOWN_DEFAULT_MINUTES;
|
||||
}
|
||||
return clampCooldownMinutes(numericValue);
|
||||
};
|
||||
|
||||
export const clampMaxAlertsPerHour = (value?: number): number => {
|
||||
const numericValue = typeof value === 'number' ? value : Number.NaN;
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
return MAX_ALERTS_MIN;
|
||||
}
|
||||
return Math.min(MAX_ALERTS_MAX, Math.max(MAX_ALERTS_MIN, numericValue));
|
||||
};
|
||||
|
||||
export const fallbackMaxAlertsPerHour = (value?: number): number => {
|
||||
const numericValue = typeof value === 'number' ? value : Number.NaN;
|
||||
if (!Number.isFinite(numericValue) || numericValue <= 0) {
|
||||
return MAX_ALERTS_DEFAULT;
|
||||
}
|
||||
return clampMaxAlertsPerHour(numericValue);
|
||||
};
|
||||
|
||||
const getLocalTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
||||
|
||||
export const createDefaultQuietHours = (): QuietHoursConfig => ({
|
||||
|
|
@ -235,8 +274,8 @@ export const createDefaultQuietHours = (): QuietHoursConfig => ({
|
|||
|
||||
export const createDefaultCooldown = (): CooldownConfig => ({
|
||||
enabled: true,
|
||||
minutes: 30,
|
||||
maxAlerts: 3,
|
||||
minutes: COOLDOWN_DEFAULT_MINUTES,
|
||||
maxAlerts: MAX_ALERTS_DEFAULT,
|
||||
});
|
||||
|
||||
export const createDefaultGrouping = (): GroupingConfig => ({
|
||||
|
|
@ -1010,10 +1049,12 @@ const [appriseConfig, setAppriseConfig] = createSignal<UIAppriseConfig>(
|
|||
}
|
||||
|
||||
if (config.schedule.cooldown !== undefined) {
|
||||
const rawCooldown = config.schedule.cooldown;
|
||||
const cooldownEnabled = rawCooldown > 0;
|
||||
setScheduleCooldown({
|
||||
enabled: config.schedule.cooldown > 0,
|
||||
minutes: config.schedule.cooldown,
|
||||
maxAlerts: config.schedule.maxAlertsHour || 3,
|
||||
enabled: cooldownEnabled,
|
||||
minutes: cooldownEnabled ? clampCooldownMinutes(rawCooldown) : 0,
|
||||
maxAlerts: fallbackMaxAlertsPerHour(config.schedule.maxAlertsHour),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1486,6 +1527,11 @@ const [appriseConfig, setAppriseConfig] = createSignal<UIAppriseConfig>(
|
|||
return;
|
||||
}
|
||||
|
||||
const normalizedCooldownMinutes = scheduleCooldown().enabled
|
||||
? clampCooldownMinutes(scheduleCooldown().minutes)
|
||||
: 0;
|
||||
const normalizedMaxAlertsHour = clampMaxAlertsPerHour(scheduleCooldown().maxAlerts);
|
||||
|
||||
const alertConfig = {
|
||||
enabled: true,
|
||||
// Global disable flags per resource type
|
||||
|
|
@ -1565,12 +1611,12 @@ const [appriseConfig, setAppriseConfig] = createSignal<UIAppriseConfig>(
|
|||
overrides: rawOverridesConfig(),
|
||||
schedule: {
|
||||
quietHours: scheduleQuietHours(),
|
||||
cooldown: scheduleCooldown().enabled ? scheduleCooldown().minutes : 0,
|
||||
cooldown: normalizedCooldownMinutes,
|
||||
groupingWindow:
|
||||
scheduleGrouping().enabled && scheduleGrouping().window
|
||||
? scheduleGrouping().window * 60
|
||||
: 30, // Convert minutes to seconds
|
||||
maxAlertsHour: scheduleCooldown().maxAlerts || 10,
|
||||
maxAlertsHour: normalizedMaxAlertsHour,
|
||||
escalation: scheduleEscalation(),
|
||||
grouping: {
|
||||
enabled: scheduleGrouping().enabled,
|
||||
|
|
@ -3280,7 +3326,17 @@ function ScheduleTab(props: ScheduleTabProps) {
|
|||
<Toggle
|
||||
checked={cooldown().enabled}
|
||||
onChange={(e) => {
|
||||
setCooldown({ ...cooldown(), enabled: e.currentTarget.checked });
|
||||
const enabled = e.currentTarget.checked;
|
||||
const current = cooldown();
|
||||
const next: CooldownConfig = {
|
||||
...current,
|
||||
enabled,
|
||||
};
|
||||
if (enabled) {
|
||||
next.minutes = fallbackCooldownMinutes(current.minutes);
|
||||
next.maxAlerts = fallbackMaxAlertsPerHour(current.maxAlerts);
|
||||
}
|
||||
setCooldown(next);
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
containerClass="sm:self-start"
|
||||
|
|
@ -3307,7 +3363,11 @@ function ScheduleTab(props: ScheduleTabProps) {
|
|||
max="120"
|
||||
value={cooldown().minutes}
|
||||
onChange={(e) => {
|
||||
setCooldown({ ...cooldown(), minutes: parseInt(e.currentTarget.value) });
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
setCooldown({
|
||||
...cooldown(),
|
||||
minutes: Number.isNaN(value) ? cooldown().minutes : value,
|
||||
});
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class={controlClass('pr-16')}
|
||||
|
|
@ -3332,7 +3392,11 @@ function ScheduleTab(props: ScheduleTabProps) {
|
|||
max="10"
|
||||
value={cooldown().maxAlerts}
|
||||
onChange={(e) => {
|
||||
setCooldown({ ...cooldown(), maxAlerts: parseInt(e.currentTarget.value) });
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
setCooldown({
|
||||
...cooldown(),
|
||||
maxAlerts: Number.isNaN(value) ? cooldown().maxAlerts : value,
|
||||
});
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
class={controlClass('pr-16')}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@ import { describe, expect, it } from 'vitest';
|
|||
|
||||
import {
|
||||
ALERT_TAB_SEGMENTS,
|
||||
clampCooldownMinutes,
|
||||
clampMaxAlertsPerHour,
|
||||
createDefaultCooldown,
|
||||
createDefaultEscalation,
|
||||
createDefaultGrouping,
|
||||
createDefaultQuietHours,
|
||||
fallbackCooldownMinutes,
|
||||
fallbackMaxAlertsPerHour,
|
||||
extractTriggerValues,
|
||||
getTriggerValue,
|
||||
normalizeMetricDelayMap,
|
||||
|
|
@ -135,6 +139,34 @@ describe('default schedule helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('cooldown sanitizers', () => {
|
||||
it('clamps cooldown minutes into valid range', () => {
|
||||
expect(clampCooldownMinutes(2)).toBe(5);
|
||||
expect(clampCooldownMinutes(60)).toBe(60);
|
||||
expect(clampCooldownMinutes(999)).toBe(120);
|
||||
expect(clampCooldownMinutes(undefined)).toBe(5);
|
||||
});
|
||||
|
||||
it('provides sensible fallback when enabling cooldown', () => {
|
||||
expect(fallbackCooldownMinutes(0)).toBe(30);
|
||||
expect(fallbackCooldownMinutes(undefined)).toBe(30);
|
||||
expect(fallbackCooldownMinutes(2)).toBe(5);
|
||||
});
|
||||
|
||||
it('clamps max alerts per hour', () => {
|
||||
expect(clampMaxAlertsPerHour(0)).toBe(1);
|
||||
expect(clampMaxAlertsPerHour(7)).toBe(7);
|
||||
expect(clampMaxAlertsPerHour(40)).toBe(10);
|
||||
expect(clampMaxAlertsPerHour(undefined)).toBe(1);
|
||||
});
|
||||
|
||||
it('falls back to defaults for invalid max alerts values', () => {
|
||||
expect(fallbackMaxAlertsPerHour(undefined)).toBe(3);
|
||||
expect(fallbackMaxAlertsPerHour(0)).toBe(3);
|
||||
expect(fallbackMaxAlertsPerHour(50)).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('threshold helper utilities', () => {
|
||||
it('extracts trigger values and ignores non-threshold keys', () => {
|
||||
const result = extractTriggerValues({
|
||||
|
|
|
|||
Loading…
Reference in a new issue