parent
3fa7356ff3
commit
88d0aeebdf
2 changed files with 106 additions and 10 deletions
|
|
@ -210,6 +210,45 @@ interface EscalationConfig {
|
||||||
levels: EscalationLevel[];
|
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';
|
const getLocalTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
||||||
|
|
||||||
export const createDefaultQuietHours = (): QuietHoursConfig => ({
|
export const createDefaultQuietHours = (): QuietHoursConfig => ({
|
||||||
|
|
@ -235,8 +274,8 @@ export const createDefaultQuietHours = (): QuietHoursConfig => ({
|
||||||
|
|
||||||
export const createDefaultCooldown = (): CooldownConfig => ({
|
export const createDefaultCooldown = (): CooldownConfig => ({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
minutes: 30,
|
minutes: COOLDOWN_DEFAULT_MINUTES,
|
||||||
maxAlerts: 3,
|
maxAlerts: MAX_ALERTS_DEFAULT,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createDefaultGrouping = (): GroupingConfig => ({
|
export const createDefaultGrouping = (): GroupingConfig => ({
|
||||||
|
|
@ -1010,10 +1049,12 @@ const [appriseConfig, setAppriseConfig] = createSignal<UIAppriseConfig>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.schedule.cooldown !== undefined) {
|
if (config.schedule.cooldown !== undefined) {
|
||||||
|
const rawCooldown = config.schedule.cooldown;
|
||||||
|
const cooldownEnabled = rawCooldown > 0;
|
||||||
setScheduleCooldown({
|
setScheduleCooldown({
|
||||||
enabled: config.schedule.cooldown > 0,
|
enabled: cooldownEnabled,
|
||||||
minutes: config.schedule.cooldown,
|
minutes: cooldownEnabled ? clampCooldownMinutes(rawCooldown) : 0,
|
||||||
maxAlerts: config.schedule.maxAlertsHour || 3,
|
maxAlerts: fallbackMaxAlertsPerHour(config.schedule.maxAlertsHour),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1486,6 +1527,11 @@ const [appriseConfig, setAppriseConfig] = createSignal<UIAppriseConfig>(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedCooldownMinutes = scheduleCooldown().enabled
|
||||||
|
? clampCooldownMinutes(scheduleCooldown().minutes)
|
||||||
|
: 0;
|
||||||
|
const normalizedMaxAlertsHour = clampMaxAlertsPerHour(scheduleCooldown().maxAlerts);
|
||||||
|
|
||||||
const alertConfig = {
|
const alertConfig = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
// Global disable flags per resource type
|
// Global disable flags per resource type
|
||||||
|
|
@ -1565,12 +1611,12 @@ const [appriseConfig, setAppriseConfig] = createSignal<UIAppriseConfig>(
|
||||||
overrides: rawOverridesConfig(),
|
overrides: rawOverridesConfig(),
|
||||||
schedule: {
|
schedule: {
|
||||||
quietHours: scheduleQuietHours(),
|
quietHours: scheduleQuietHours(),
|
||||||
cooldown: scheduleCooldown().enabled ? scheduleCooldown().minutes : 0,
|
cooldown: normalizedCooldownMinutes,
|
||||||
groupingWindow:
|
groupingWindow:
|
||||||
scheduleGrouping().enabled && scheduleGrouping().window
|
scheduleGrouping().enabled && scheduleGrouping().window
|
||||||
? scheduleGrouping().window * 60
|
? scheduleGrouping().window * 60
|
||||||
: 30, // Convert minutes to seconds
|
: 30, // Convert minutes to seconds
|
||||||
maxAlertsHour: scheduleCooldown().maxAlerts || 10,
|
maxAlertsHour: normalizedMaxAlertsHour,
|
||||||
escalation: scheduleEscalation(),
|
escalation: scheduleEscalation(),
|
||||||
grouping: {
|
grouping: {
|
||||||
enabled: scheduleGrouping().enabled,
|
enabled: scheduleGrouping().enabled,
|
||||||
|
|
@ -3280,7 +3326,17 @@ function ScheduleTab(props: ScheduleTabProps) {
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={cooldown().enabled}
|
checked={cooldown().enabled}
|
||||||
onChange={(e) => {
|
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);
|
props.setHasUnsavedChanges(true);
|
||||||
}}
|
}}
|
||||||
containerClass="sm:self-start"
|
containerClass="sm:self-start"
|
||||||
|
|
@ -3307,7 +3363,11 @@ function ScheduleTab(props: ScheduleTabProps) {
|
||||||
max="120"
|
max="120"
|
||||||
value={cooldown().minutes}
|
value={cooldown().minutes}
|
||||||
onChange={(e) => {
|
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);
|
props.setHasUnsavedChanges(true);
|
||||||
}}
|
}}
|
||||||
class={controlClass('pr-16')}
|
class={controlClass('pr-16')}
|
||||||
|
|
@ -3332,7 +3392,11 @@ function ScheduleTab(props: ScheduleTabProps) {
|
||||||
max="10"
|
max="10"
|
||||||
value={cooldown().maxAlerts}
|
value={cooldown().maxAlerts}
|
||||||
onChange={(e) => {
|
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);
|
props.setHasUnsavedChanges(true);
|
||||||
}}
|
}}
|
||||||
class={controlClass('pr-16')}
|
class={controlClass('pr-16')}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,14 @@ import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ALERT_TAB_SEGMENTS,
|
ALERT_TAB_SEGMENTS,
|
||||||
|
clampCooldownMinutes,
|
||||||
|
clampMaxAlertsPerHour,
|
||||||
createDefaultCooldown,
|
createDefaultCooldown,
|
||||||
createDefaultEscalation,
|
createDefaultEscalation,
|
||||||
createDefaultGrouping,
|
createDefaultGrouping,
|
||||||
createDefaultQuietHours,
|
createDefaultQuietHours,
|
||||||
|
fallbackCooldownMinutes,
|
||||||
|
fallbackMaxAlertsPerHour,
|
||||||
extractTriggerValues,
|
extractTriggerValues,
|
||||||
getTriggerValue,
|
getTriggerValue,
|
||||||
normalizeMetricDelayMap,
|
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', () => {
|
describe('threshold helper utilities', () => {
|
||||||
it('extracts trigger values and ignores non-threshold keys', () => {
|
it('extracts trigger values and ignores non-threshold keys', () => {
|
||||||
const result = extractTriggerValues({
|
const result = extractTriggerValues({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue