mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-05 23:36:37 +00:00
Split alert schedule tab section owners
This commit is contained in:
parent
07ec62d597
commit
ac4b0806ee
11 changed files with 774 additions and 622 deletions
|
|
@ -271,6 +271,12 @@ provider-catalog loading and provider-default application, while
|
|||
`frontend-modern/src/components/Alerts/EmailProviderSelect.tsx` stays the
|
||||
render shell and consumes the canonical `UIEmailConfig` feature type instead of
|
||||
keeping a second local email-config interface.
|
||||
The alert scheduling surface now follows the same shell/section split:
|
||||
`frontend-modern/src/features/alerts/useAlertScheduleState.ts` owns schedule
|
||||
runtime and default/reset policy, while
|
||||
`frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx` stays the shell and
|
||||
composes the dedicated quiet-hours, cooldown, grouping, recovery, escalation,
|
||||
and summary section owners instead of carrying those panels inline.
|
||||
|
||||
Alert filter metadata and grouped header consumers must also preserve the
|
||||
canonical `agent` and `node` header boundary when reusing shared filter
|
||||
|
|
|
|||
|
|
@ -467,6 +467,11 @@ provider-catalog loading and provider-default application, while
|
|||
`frontend-modern/src/components/Alerts/EmailProviderSelect.tsx` stays the
|
||||
render shell and should not re-accumulate `NotificationsAPI.getEmailProviders`
|
||||
or a second local email-config contract inline.
|
||||
The alert scheduling surface now follows the same shell-versus-section split:
|
||||
`frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx` should compose the
|
||||
dedicated quiet-hours, cooldown, grouping, recovery, escalation, and summary
|
||||
section owners while `frontend-modern/src/features/alerts/useAlertScheduleState.ts`
|
||||
remains the canonical runtime owner.
|
||||
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
|
||||
|
|
|
|||
98
frontend-modern/src/features/alerts/AlertCooldownSection.tsx
Normal file
98
frontend-modern/src/features/alerts/AlertCooldownSection.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { Show } from 'solid-js';
|
||||
|
||||
import {
|
||||
controlClass,
|
||||
formHelpText,
|
||||
formField,
|
||||
labelClass,
|
||||
} from '@/components/shared/Form';
|
||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import {
|
||||
ALERT_CONFIG_COOLDOWN_DESCRIPTION,
|
||||
ALERT_CONFIG_COOLDOWN_MAX_ALERTS_HELP,
|
||||
ALERT_CONFIG_COOLDOWN_MAX_ALERTS_LABEL,
|
||||
ALERT_CONFIG_COOLDOWN_MAX_ALERTS_SUFFIX,
|
||||
ALERT_CONFIG_COOLDOWN_PERIOD_HELP,
|
||||
ALERT_CONFIG_COOLDOWN_PERIOD_LABEL,
|
||||
ALERT_CONFIG_COOLDOWN_PERIOD_SUFFIX,
|
||||
ALERT_CONFIG_COOLDOWN_TITLE,
|
||||
getAlertConfigToggleStatusLabel,
|
||||
} from '@/utils/alertConfigPresentation';
|
||||
|
||||
import type { CooldownConfig } from './types';
|
||||
|
||||
interface AlertCooldownSectionProps {
|
||||
cooldown: CooldownConfig;
|
||||
setCooldownEnabled: (value: boolean) => void;
|
||||
setCooldownMinutes: (value: string) => void;
|
||||
setCooldownMaxAlerts: (value: string) => void;
|
||||
}
|
||||
|
||||
export function AlertCooldownSection(props: AlertCooldownSectionProps) {
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={ALERT_CONFIG_COOLDOWN_TITLE}
|
||||
description={ALERT_CONFIG_COOLDOWN_DESCRIPTION}
|
||||
action={
|
||||
<Toggle
|
||||
checked={props.cooldown.enabled}
|
||||
onChange={(event) => props.setCooldownEnabled(event.currentTarget.checked)}
|
||||
containerClass="sm:self-start"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertConfigToggleStatusLabel(props.cooldown.enabled)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
class="space-y-4"
|
||||
>
|
||||
<Show when={props.cooldown.enabled}>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_CONFIG_COOLDOWN_PERIOD_LABEL}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="number"
|
||||
min="5"
|
||||
max="120"
|
||||
value={props.cooldown.minutes}
|
||||
onChange={(event) => props.setCooldownMinutes(event.currentTarget.value)}
|
||||
class={controlClass('pr-16')}
|
||||
/>
|
||||
<span class="pointer-events-none absolute inset-y-0 right-3 flex items-center text-sm text-muted">
|
||||
{ALERT_CONFIG_COOLDOWN_PERIOD_SUFFIX}
|
||||
</span>
|
||||
</div>
|
||||
<p class={`${formHelpText} mt-1`}>{ALERT_CONFIG_COOLDOWN_PERIOD_HELP}</p>
|
||||
</div>
|
||||
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_CONFIG_COOLDOWN_MAX_ALERTS_LABEL}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
value={props.cooldown.maxAlerts}
|
||||
onChange={(event) => props.setCooldownMaxAlerts(event.currentTarget.value)}
|
||||
class={controlClass('pr-16')}
|
||||
/>
|
||||
<span class="pointer-events-none absolute inset-y-0 right-3 flex items-center text-sm text-muted">
|
||||
{ALERT_CONFIG_COOLDOWN_MAX_ALERTS_SUFFIX}
|
||||
</span>
|
||||
</div>
|
||||
<p class={`${formHelpText} mt-1`}>{ALERT_CONFIG_COOLDOWN_MAX_ALERTS_HELP}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
132
frontend-modern/src/features/alerts/AlertEscalationSection.tsx
Normal file
132
frontend-modern/src/features/alerts/AlertEscalationSection.tsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { For, Show } from 'solid-js';
|
||||
|
||||
import { controlClass, formHelpText } from '@/components/shared/Form';
|
||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import {
|
||||
ALERT_CONFIG_ESCALATION_ADD_LABEL,
|
||||
ALERT_CONFIG_ESCALATION_AFTER_LABEL,
|
||||
ALERT_CONFIG_ESCALATION_DESCRIPTION,
|
||||
ALERT_CONFIG_ESCALATION_MINUTES_SUFFIX,
|
||||
ALERT_CONFIG_ESCALATION_NOTIFY_LABEL,
|
||||
ALERT_CONFIG_ESCALATION_REMOVE_TITLE,
|
||||
ALERT_CONFIG_ESCALATION_TITLE,
|
||||
getAlertConfigEscalationHelp,
|
||||
getAlertConfigEscalationNotifyLabel,
|
||||
getAlertConfigToggleStatusLabel,
|
||||
} from '@/utils/alertConfigPresentation';
|
||||
|
||||
import type { EscalationConfig, EscalationNotifyTarget } from './types';
|
||||
|
||||
interface AlertEscalationSectionProps {
|
||||
escalation: EscalationConfig;
|
||||
setEscalationEnabled: (value: boolean) => void;
|
||||
setEscalationAfter: (index: number, value: string) => void;
|
||||
setEscalationNotify: (index: number, value: EscalationNotifyTarget) => void;
|
||||
removeEscalationLevel: (index: number) => void;
|
||||
addEscalationLevel: () => void;
|
||||
}
|
||||
|
||||
export function AlertEscalationSection(props: AlertEscalationSectionProps) {
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={ALERT_CONFIG_ESCALATION_TITLE}
|
||||
description={ALERT_CONFIG_ESCALATION_DESCRIPTION}
|
||||
action={
|
||||
<Toggle
|
||||
checked={props.escalation.enabled}
|
||||
onChange={(event) => props.setEscalationEnabled(event.currentTarget.checked)}
|
||||
containerClass="sm:self-start"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertConfigToggleStatusLabel(props.escalation.enabled)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
class="space-y-4"
|
||||
>
|
||||
<Show when={props.escalation.enabled}>
|
||||
<div class="space-y-3">
|
||||
<p class={formHelpText}>{getAlertConfigEscalationHelp()}</p>
|
||||
<For each={props.escalation.levels}>
|
||||
{(level, index) => (
|
||||
<div class="flex items-center gap-3 rounded-md border border-border bg-surface-hover p-3">
|
||||
<div class="flex flex-1 flex-col gap-3 sm:grid sm:grid-cols-2 sm:items-center sm:gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{ALERT_CONFIG_ESCALATION_AFTER_LABEL}
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
min="5"
|
||||
max="180"
|
||||
value={level.after}
|
||||
onChange={(event) => props.setEscalationAfter(index(), event.currentTarget.value)}
|
||||
class={`${controlClass('px-2 py-1 text-sm')} w-20`}
|
||||
/>
|
||||
<span class="text-xs text-muted">
|
||||
{ALERT_CONFIG_ESCALATION_MINUTES_SUFFIX}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{ALERT_CONFIG_ESCALATION_NOTIFY_LABEL}
|
||||
</span>
|
||||
<select
|
||||
value={level.notify}
|
||||
onChange={(event) =>
|
||||
props.setEscalationNotify(
|
||||
index(),
|
||||
event.currentTarget.value as EscalationNotifyTarget,
|
||||
)
|
||||
}
|
||||
class={`${controlClass('px-2 py-1 text-sm')} flex-1`}
|
||||
>
|
||||
<option value="email">{getAlertConfigEscalationNotifyLabel('email')}</option>
|
||||
<option value="webhook">
|
||||
{getAlertConfigEscalationNotifyLabel('webhook')}
|
||||
</option>
|
||||
<option value="all">{getAlertConfigEscalationNotifyLabel('all')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.removeEscalationLevel(index())}
|
||||
class="rounded-md p-1.5 text-red-600 transition-colors hover:bg-red-100 dark:hover:bg-red-900"
|
||||
title={ALERT_CONFIG_ESCALATION_REMOVE_TITLE}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.addEscalationLevel}
|
||||
class="flex w-full items-center justify-center gap-2 rounded-md border-2 border-dashed border-border py-2 text-sm text-muted transition-all hover:border-slate-400 hover:bg-surface-hover"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/>
|
||||
</svg>
|
||||
{ALERT_CONFIG_ESCALATION_ADD_LABEL}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
136
frontend-modern/src/features/alerts/AlertGroupingSection.tsx
Normal file
136
frontend-modern/src/features/alerts/AlertGroupingSection.tsx
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { Show } from 'solid-js';
|
||||
|
||||
import {
|
||||
formHelpText,
|
||||
formField,
|
||||
labelClass,
|
||||
} from '@/components/shared/Form';
|
||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import {
|
||||
ALERT_CONFIG_GROUPING_BY_GUEST,
|
||||
ALERT_CONFIG_GROUPING_BY_NODE,
|
||||
ALERT_CONFIG_GROUPING_DESCRIPTION,
|
||||
ALERT_CONFIG_GROUPING_STRATEGY_LABEL,
|
||||
ALERT_CONFIG_GROUPING_TITLE,
|
||||
ALERT_CONFIG_GROUPING_WINDOW_HELP,
|
||||
ALERT_CONFIG_GROUPING_WINDOW_LABEL,
|
||||
getAlertConfigToggleStatusLabel,
|
||||
} from '@/utils/alertConfigPresentation';
|
||||
import {
|
||||
getAlertGroupingCardClass,
|
||||
getAlertGroupingCheckboxClass,
|
||||
} from '@/utils/alertGroupingPresentation';
|
||||
|
||||
import type { GroupingConfig } from './types';
|
||||
|
||||
interface AlertGroupingSectionProps {
|
||||
grouping: GroupingConfig;
|
||||
setGroupingEnabled: (value: boolean) => void;
|
||||
setGroupingWindow: (value: string) => void;
|
||||
setGroupingByNode: (value: boolean) => void;
|
||||
setGroupingByGuest: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function AlertGroupingSection(props: AlertGroupingSectionProps) {
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={ALERT_CONFIG_GROUPING_TITLE}
|
||||
description={ALERT_CONFIG_GROUPING_DESCRIPTION}
|
||||
action={
|
||||
<Toggle
|
||||
checked={props.grouping.enabled}
|
||||
onChange={(event) => props.setGroupingEnabled(event.currentTarget.checked)}
|
||||
containerClass="sm:self-start"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertConfigToggleStatusLabel(props.grouping.enabled)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
class="space-y-4"
|
||||
>
|
||||
<Show when={props.grouping.enabled}>
|
||||
<div class="space-y-4">
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_CONFIG_GROUPING_WINDOW_LABEL}
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="30"
|
||||
value={props.grouping.window}
|
||||
onChange={(event) => props.setGroupingWindow(event.currentTarget.value)}
|
||||
class="flex-1"
|
||||
/>
|
||||
<div class="w-16 rounded-md bg-surface-alt px-2 py-1 text-center text-sm text-base-content">
|
||||
{props.grouping.window} min
|
||||
</div>
|
||||
</div>
|
||||
<p class={`${formHelpText} mt-1`}>{ALERT_CONFIG_GROUPING_WINDOW_HELP}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class={`${labelClass('text-xs uppercase tracking-[0.08em]')} mb-2 block`}>
|
||||
{ALERT_CONFIG_GROUPING_STRATEGY_LABEL}
|
||||
</span>
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<label class={getAlertGroupingCardClass(props.grouping.byNode ?? false)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.grouping.byNode}
|
||||
onChange={(event) => props.setGroupingByNode(event.currentTarget.checked)}
|
||||
class="sr-only"
|
||||
/>
|
||||
<div class={getAlertGroupingCheckboxClass(props.grouping.byNode ?? false)}>
|
||||
<Show when={props.grouping.byNode}>
|
||||
<svg
|
||||
class="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</Show>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-base-content">
|
||||
{ALERT_CONFIG_GROUPING_BY_NODE}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class={getAlertGroupingCardClass(props.grouping.byGuest ?? false)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.grouping.byGuest}
|
||||
onChange={(event) => props.setGroupingByGuest(event.currentTarget.checked)}
|
||||
class="sr-only"
|
||||
/>
|
||||
<div class={getAlertGroupingCheckboxClass(props.grouping.byGuest ?? false)}>
|
||||
<Show when={props.grouping.byGuest}>
|
||||
<svg
|
||||
class="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</Show>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-base-content">
|
||||
{ALERT_CONFIG_GROUPING_BY_GUEST}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
182
frontend-modern/src/features/alerts/AlertQuietHoursSection.tsx
Normal file
182
frontend-modern/src/features/alerts/AlertQuietHoursSection.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { For, Show } from 'solid-js';
|
||||
|
||||
import { controlClass, formField, labelClass } from '@/components/shared/Form';
|
||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import {
|
||||
ALERT_CONFIG_QUIET_HOURS_DESCRIPTION,
|
||||
ALERT_CONFIG_QUIET_HOURS_END_TIME_LABEL,
|
||||
ALERT_CONFIG_QUIET_HOURS_START_TIME_LABEL,
|
||||
ALERT_CONFIG_QUIET_HOURS_TIMEZONE_LABEL,
|
||||
ALERT_CONFIG_QUIET_HOURS_TITLE,
|
||||
} from '@/utils/alertConfigPresentation';
|
||||
import {
|
||||
getAlertQuietDayButtonClass,
|
||||
getAlertQuietSuppressCardClass,
|
||||
getAlertQuietSuppressCheckboxClass,
|
||||
} from '@/utils/alertSchedulePresentation';
|
||||
|
||||
import { ALERT_SCHEDULE_DAYS, ALERT_SCHEDULE_TIMEZONES } from './useAlertScheduleState';
|
||||
import type { QuietHoursConfig } from './types';
|
||||
|
||||
interface QuietSuppressOption {
|
||||
key: keyof QuietHoursConfig['suppress'];
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface AlertQuietHoursSectionProps {
|
||||
quietHours: QuietHoursConfig;
|
||||
quietHourSuppressOptions: QuietSuppressOption[];
|
||||
weekdaysOnly: boolean;
|
||||
weekendsOnly: boolean;
|
||||
setQuietHoursEnabled: (value: boolean) => void;
|
||||
setQuietHoursStart: (value: string) => void;
|
||||
setQuietHoursEnd: (value: string) => void;
|
||||
setQuietHoursTimezone: (value: string) => void;
|
||||
toggleQuietDay: (day: keyof QuietHoursConfig['days']) => void;
|
||||
setQuietSuppressCategory: (category: keyof QuietHoursConfig['suppress'], value: boolean) => void;
|
||||
}
|
||||
|
||||
export function AlertQuietHoursSection(props: AlertQuietHoursSectionProps) {
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={ALERT_CONFIG_QUIET_HOURS_TITLE}
|
||||
description={ALERT_CONFIG_QUIET_HOURS_DESCRIPTION}
|
||||
action={
|
||||
<Toggle
|
||||
checked={props.quietHours.enabled}
|
||||
onChange={(event) => props.setQuietHoursEnabled(event.currentTarget.checked)}
|
||||
containerClass="sm:self-start"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{props.quietHours.enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
class="space-y-4"
|
||||
>
|
||||
<Show when={props.quietHours.enabled}>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_CONFIG_QUIET_HOURS_START_TIME_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={props.quietHours.start}
|
||||
onChange={(event) => props.setQuietHoursStart(event.currentTarget.value)}
|
||||
class={controlClass('font-mono')}
|
||||
/>
|
||||
</div>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_CONFIG_QUIET_HOURS_END_TIME_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={props.quietHours.end}
|
||||
onChange={(event) => props.setQuietHoursEnd(event.currentTarget.value)}
|
||||
class={controlClass('font-mono')}
|
||||
/>
|
||||
</div>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_CONFIG_QUIET_HOURS_TIMEZONE_LABEL}
|
||||
</label>
|
||||
<select
|
||||
value={props.quietHours.timezone}
|
||||
onChange={(event) => props.setQuietHoursTimezone(event.currentTarget.value)}
|
||||
class={controlClass('pr-8')}
|
||||
>
|
||||
<For each={ALERT_SCHEDULE_TIMEZONES}>
|
||||
{(timezone) => <option value={timezone}>{timezone}</option>}
|
||||
</For>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class={`${labelClass('text-xs uppercase tracking-[0.08em]')} mb-2 block`}>
|
||||
Quiet days
|
||||
</span>
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<For each={ALERT_SCHEDULE_DAYS}>
|
||||
{(day) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.toggleQuietDay(day.id)}
|
||||
title={day.fullLabel}
|
||||
class={getAlertQuietDayButtonClass(props.quietHours.days[day.id])}
|
||||
>
|
||||
{day.label}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-muted">
|
||||
<Show when={props.weekdaysOnly}>Weekdays only</Show>
|
||||
<Show when={props.weekendsOnly}>Weekends only</Show>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 border-t border-border pt-4">
|
||||
<span class={`${labelClass('text-xs uppercase tracking-[0.08em]')} block`}>
|
||||
Suppress categories
|
||||
</span>
|
||||
<p class="text-xs text-muted">
|
||||
Critical alerts in selected categories will stay silent during quiet hours.
|
||||
</p>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:gap-3">
|
||||
<For each={props.quietHourSuppressOptions}>
|
||||
{(option) => (
|
||||
<label
|
||||
class={getAlertQuietSuppressCardClass(
|
||||
props.quietHours.suppress[option.key],
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.quietHours.suppress[option.key]}
|
||||
onChange={(event) =>
|
||||
props.setQuietSuppressCategory(option.key, event.currentTarget.checked)
|
||||
}
|
||||
class="sr-only"
|
||||
/>
|
||||
<div
|
||||
class={getAlertQuietSuppressCheckboxClass(
|
||||
props.quietHours.suppress[option.key],
|
||||
)}
|
||||
>
|
||||
<Show when={props.quietHours.suppress[option.key]}>
|
||||
<svg
|
||||
class="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</Show>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-base-content">{option.label}</p>
|
||||
<p class="text-xs text-muted">{option.description}</p>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
38
frontend-modern/src/features/alerts/AlertRecoverySection.tsx
Normal file
38
frontend-modern/src/features/alerts/AlertRecoverySection.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import { formHelpText } from '@/components/shared/Form';
|
||||
import {
|
||||
ALERT_CONFIG_RECOVERY_DESCRIPTION,
|
||||
ALERT_CONFIG_RECOVERY_TITLE,
|
||||
getAlertConfigRecoveryHelp,
|
||||
getAlertConfigToggleStatusLabel,
|
||||
} from '@/utils/alertConfigPresentation';
|
||||
|
||||
interface AlertRecoverySectionProps {
|
||||
notifyOnResolve: boolean;
|
||||
setNotifyOnResolveEnabled: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function AlertRecoverySection(props: AlertRecoverySectionProps) {
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={ALERT_CONFIG_RECOVERY_TITLE}
|
||||
description={ALERT_CONFIG_RECOVERY_DESCRIPTION}
|
||||
action={
|
||||
<Toggle
|
||||
checked={props.notifyOnResolve}
|
||||
onChange={(event) => props.setNotifyOnResolveEnabled(event.currentTarget.checked)}
|
||||
containerClass="sm:self-start"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertConfigToggleStatusLabel(props.notifyOnResolve)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
class="space-y-3"
|
||||
>
|
||||
<p class={formHelpText}>{getAlertConfigRecoveryHelp()}</p>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import { Show } from 'solid-js';
|
||||
|
||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import {
|
||||
ALERT_CONFIG_SUMMARY_DESCRIPTION,
|
||||
ALERT_CONFIG_SUMMARY_TITLE,
|
||||
getAlertConfigSummaryAllDisabled,
|
||||
getAlertConfigSummaryCooldown,
|
||||
getAlertConfigSummaryEscalation,
|
||||
getAlertConfigSummaryGrouping,
|
||||
getAlertConfigSummaryQuietHours,
|
||||
getAlertConfigSummaryRecoveryEnabled,
|
||||
getAlertConfigSummarySuppressing,
|
||||
} from '@/utils/alertConfigPresentation';
|
||||
|
||||
import type {
|
||||
CooldownConfig,
|
||||
EscalationConfig,
|
||||
GroupingConfig,
|
||||
QuietHoursConfig,
|
||||
} from './types';
|
||||
|
||||
interface QuietSuppressOption {
|
||||
key: keyof QuietHoursConfig['suppress'];
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface AlertScheduleSummarySectionProps {
|
||||
quietHours: QuietHoursConfig;
|
||||
cooldown: CooldownConfig;
|
||||
grouping: GroupingConfig;
|
||||
notifyOnResolve: boolean;
|
||||
escalation: EscalationConfig;
|
||||
quietHourSuppressOptions: QuietSuppressOption[];
|
||||
}
|
||||
|
||||
export function AlertScheduleSummarySection(props: AlertScheduleSummarySectionProps) {
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={ALERT_CONFIG_SUMMARY_TITLE}
|
||||
description={ALERT_CONFIG_SUMMARY_DESCRIPTION}
|
||||
tone="muted"
|
||||
padding="lg"
|
||||
bodyClass="space-y-1 text-sm text-blue-800 dark:text-blue-300"
|
||||
class="lg:col-span-2"
|
||||
>
|
||||
<Show when={props.quietHours.enabled}>
|
||||
<p>
|
||||
{getAlertConfigSummaryQuietHours(
|
||||
props.quietHours.start,
|
||||
props.quietHours.end,
|
||||
props.quietHours.timezone,
|
||||
)}
|
||||
</p>
|
||||
</Show>
|
||||
<Show
|
||||
when={
|
||||
props.quietHours.enabled &&
|
||||
(props.quietHours.suppress.performance ||
|
||||
props.quietHours.suppress.storage ||
|
||||
props.quietHours.suppress.offline)
|
||||
}
|
||||
>
|
||||
<p>
|
||||
{getAlertConfigSummarySuppressing(
|
||||
props.quietHourSuppressOptions
|
||||
.filter((option) => props.quietHours.suppress[option.key])
|
||||
.map((option) => option.label),
|
||||
)}
|
||||
</p>
|
||||
</Show>
|
||||
<Show when={props.cooldown.enabled}>
|
||||
<p>{getAlertConfigSummaryCooldown(props.cooldown.minutes, props.cooldown.maxAlerts)}</p>
|
||||
</Show>
|
||||
<Show when={props.grouping.enabled}>
|
||||
<p>
|
||||
{getAlertConfigSummaryGrouping(
|
||||
props.grouping.window,
|
||||
props.grouping.byNode ?? false,
|
||||
props.grouping.byGuest ?? false,
|
||||
)}
|
||||
</p>
|
||||
</Show>
|
||||
<Show when={props.notifyOnResolve}>
|
||||
<p>{getAlertConfigSummaryRecoveryEnabled()}</p>
|
||||
</Show>
|
||||
<Show when={props.escalation.enabled && props.escalation.levels.length > 0}>
|
||||
<p>{getAlertConfigSummaryEscalation(props.escalation.levels.length)}</p>
|
||||
</Show>
|
||||
<Show
|
||||
when={
|
||||
!props.quietHours.enabled &&
|
||||
!props.cooldown.enabled &&
|
||||
!props.grouping.enabled &&
|
||||
!props.escalation.enabled
|
||||
}
|
||||
>
|
||||
<p>{getAlertConfigSummaryAllDisabled()}</p>
|
||||
</Show>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,84 +1,18 @@
|
|||
import { For, Show } from 'solid-js';
|
||||
|
||||
import {
|
||||
controlClass,
|
||||
formHelpText,
|
||||
formField,
|
||||
labelClass,
|
||||
} from '@/components/shared/Form';
|
||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import {
|
||||
ALERT_CONFIG_COOLDOWN_DESCRIPTION,
|
||||
ALERT_CONFIG_COOLDOWN_MAX_ALERTS_HELP,
|
||||
ALERT_CONFIG_COOLDOWN_MAX_ALERTS_LABEL,
|
||||
ALERT_CONFIG_COOLDOWN_MAX_ALERTS_SUFFIX,
|
||||
ALERT_CONFIG_COOLDOWN_PERIOD_HELP,
|
||||
ALERT_CONFIG_COOLDOWN_PERIOD_LABEL,
|
||||
ALERT_CONFIG_COOLDOWN_PERIOD_SUFFIX,
|
||||
ALERT_CONFIG_COOLDOWN_TITLE,
|
||||
ALERT_CONFIG_ESCALATION_ADD_LABEL,
|
||||
ALERT_CONFIG_ESCALATION_AFTER_LABEL,
|
||||
ALERT_CONFIG_ESCALATION_DESCRIPTION,
|
||||
ALERT_CONFIG_ESCALATION_MINUTES_SUFFIX,
|
||||
ALERT_CONFIG_ESCALATION_NOTIFY_LABEL,
|
||||
ALERT_CONFIG_ESCALATION_REMOVE_TITLE,
|
||||
ALERT_CONFIG_ESCALATION_TITLE,
|
||||
ALERT_CONFIG_GROUPING_BY_GUEST,
|
||||
ALERT_CONFIG_GROUPING_BY_NODE,
|
||||
ALERT_CONFIG_GROUPING_DESCRIPTION,
|
||||
ALERT_CONFIG_GROUPING_STRATEGY_LABEL,
|
||||
ALERT_CONFIG_GROUPING_TITLE,
|
||||
ALERT_CONFIG_GROUPING_WINDOW_HELP,
|
||||
ALERT_CONFIG_GROUPING_WINDOW_LABEL,
|
||||
ALERT_CONFIG_QUIET_HOURS_DESCRIPTION,
|
||||
ALERT_CONFIG_QUIET_HOURS_END_TIME_LABEL,
|
||||
ALERT_CONFIG_QUIET_HOURS_START_TIME_LABEL,
|
||||
ALERT_CONFIG_QUIET_HOURS_TIMEZONE_LABEL,
|
||||
ALERT_CONFIG_QUIET_HOURS_TITLE,
|
||||
ALERT_CONFIG_RECOVERY_DESCRIPTION,
|
||||
ALERT_CONFIG_RECOVERY_TITLE,
|
||||
ALERT_CONFIG_SCHEDULING_DESCRIPTION,
|
||||
ALERT_CONFIG_SCHEDULING_TITLE,
|
||||
ALERT_CONFIG_SUMMARY_DESCRIPTION,
|
||||
ALERT_CONFIG_SUMMARY_TITLE,
|
||||
getAlertConfigEscalationHelp,
|
||||
getAlertConfigEscalationNotifyLabel,
|
||||
getAlertConfigQuietHourSuppressOptions,
|
||||
getAlertConfigRecoveryHelp,
|
||||
getAlertConfigResetDefaultsLabel,
|
||||
getAlertConfigResetDefaultsTitle,
|
||||
getAlertConfigSummaryAllDisabled,
|
||||
getAlertConfigSummaryCooldown,
|
||||
getAlertConfigSummaryEscalation,
|
||||
getAlertConfigSummaryGrouping,
|
||||
getAlertConfigSummaryQuietHours,
|
||||
getAlertConfigSummaryRecoveryEnabled,
|
||||
getAlertConfigSummarySuppressing,
|
||||
getAlertConfigToggleStatusLabel,
|
||||
} from '@/utils/alertConfigPresentation';
|
||||
import {
|
||||
getAlertGroupingCardClass,
|
||||
getAlertGroupingCheckboxClass,
|
||||
} from '@/utils/alertGroupingPresentation';
|
||||
import {
|
||||
getAlertQuietDayButtonClass,
|
||||
getAlertQuietSuppressCardClass,
|
||||
getAlertQuietSuppressCheckboxClass,
|
||||
} from '@/utils/alertSchedulePresentation';
|
||||
|
||||
import {
|
||||
ALERT_SCHEDULE_DAYS,
|
||||
ALERT_SCHEDULE_TIMEZONES,
|
||||
useAlertScheduleState,
|
||||
} from '../useAlertScheduleState';
|
||||
import type {
|
||||
CooldownConfig,
|
||||
EscalationConfig,
|
||||
EscalationNotifyTarget,
|
||||
GroupingConfig,
|
||||
QuietHoursConfig,
|
||||
} from '../types';
|
||||
import { AlertCooldownSection } from '../AlertCooldownSection';
|
||||
import { AlertEscalationSection } from '../AlertEscalationSection';
|
||||
import { AlertGroupingSection } from '../AlertGroupingSection';
|
||||
import { AlertQuietHoursSection } from '../AlertQuietHoursSection';
|
||||
import { AlertRecoverySection } from '../AlertRecoverySection';
|
||||
import { AlertScheduleSummarySection } from '../AlertScheduleSummarySection';
|
||||
import { useAlertScheduleState } from '../useAlertScheduleState';
|
||||
import type { CooldownConfig, EscalationConfig, GroupingConfig, QuietHoursConfig } from '../types';
|
||||
|
||||
export interface ScheduleTabProps {
|
||||
setHasUnsavedChanges: (value: boolean) => void;
|
||||
|
|
@ -131,541 +65,56 @@ export function ScheduleTab(props: ScheduleTabProps) {
|
|||
</div>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-2">
|
||||
<SettingsPanel
|
||||
title={ALERT_CONFIG_QUIET_HOURS_TITLE}
|
||||
description={ALERT_CONFIG_QUIET_HOURS_DESCRIPTION}
|
||||
action={
|
||||
<Toggle
|
||||
checked={props.quietHours().enabled}
|
||||
onChange={(event) => {
|
||||
scheduleState.setQuietHoursEnabled(event.currentTarget.checked);
|
||||
}}
|
||||
containerClass="sm:self-start"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{props.quietHours().enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
class="space-y-4"
|
||||
>
|
||||
<Show when={props.quietHours().enabled}>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_CONFIG_QUIET_HOURS_START_TIME_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={props.quietHours().start}
|
||||
onChange={(event) => {
|
||||
scheduleState.setQuietHoursStart(event.currentTarget.value);
|
||||
}}
|
||||
class={controlClass('font-mono')}
|
||||
/>
|
||||
</div>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_CONFIG_QUIET_HOURS_END_TIME_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={props.quietHours().end}
|
||||
onChange={(event) => {
|
||||
scheduleState.setQuietHoursEnd(event.currentTarget.value);
|
||||
}}
|
||||
class={controlClass('font-mono')}
|
||||
/>
|
||||
</div>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_CONFIG_QUIET_HOURS_TIMEZONE_LABEL}
|
||||
</label>
|
||||
<select
|
||||
value={props.quietHours().timezone}
|
||||
onChange={(event) => {
|
||||
scheduleState.setQuietHoursTimezone(event.currentTarget.value);
|
||||
}}
|
||||
class={controlClass('pr-8')}
|
||||
>
|
||||
<For each={ALERT_SCHEDULE_TIMEZONES}>
|
||||
{(timezone) => <option value={timezone}>{timezone}</option>}
|
||||
</For>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<AlertQuietHoursSection
|
||||
quietHours={props.quietHours()}
|
||||
quietHourSuppressOptions={quietHourSuppressOptions}
|
||||
weekdaysOnly={scheduleState.weekdaysOnly()}
|
||||
weekendsOnly={scheduleState.weekendsOnly()}
|
||||
setQuietHoursEnabled={scheduleState.setQuietHoursEnabled}
|
||||
setQuietHoursStart={scheduleState.setQuietHoursStart}
|
||||
setQuietHoursEnd={scheduleState.setQuietHoursEnd}
|
||||
setQuietHoursTimezone={scheduleState.setQuietHoursTimezone}
|
||||
toggleQuietDay={scheduleState.toggleQuietDay}
|
||||
setQuietSuppressCategory={scheduleState.setQuietSuppressCategory}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<span class={`${labelClass('text-xs uppercase tracking-[0.08em]')} mb-2 block`}>
|
||||
Quiet days
|
||||
</span>
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<For each={ALERT_SCHEDULE_DAYS}>
|
||||
{(day) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
scheduleState.toggleQuietDay(day.id);
|
||||
}}
|
||||
title={day.fullLabel}
|
||||
class={getAlertQuietDayButtonClass(props.quietHours().days[day.id])}
|
||||
>
|
||||
{day.label}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-muted">
|
||||
<Show
|
||||
when={scheduleState.weekdaysOnly()}
|
||||
>
|
||||
Weekdays only
|
||||
</Show>
|
||||
<Show when={scheduleState.weekendsOnly()}>
|
||||
Weekends only
|
||||
</Show>
|
||||
</p>
|
||||
</div>
|
||||
<AlertCooldownSection
|
||||
cooldown={props.cooldown()}
|
||||
setCooldownEnabled={scheduleState.setCooldownEnabled}
|
||||
setCooldownMinutes={scheduleState.setCooldownMinutes}
|
||||
setCooldownMaxAlerts={scheduleState.setCooldownMaxAlerts}
|
||||
/>
|
||||
|
||||
<div class="space-y-3 border-t border-border pt-4">
|
||||
<span class={`${labelClass('text-xs uppercase tracking-[0.08em]')} block`}>
|
||||
Suppress categories
|
||||
</span>
|
||||
<p class="text-xs text-muted">
|
||||
Critical alerts in selected categories will stay silent during quiet hours.
|
||||
</p>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:gap-3">
|
||||
<For each={quietHourSuppressOptions}>
|
||||
{(option) => (
|
||||
<label
|
||||
class={getAlertQuietSuppressCardClass(
|
||||
props.quietHours().suppress[option.key],
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.quietHours().suppress[option.key]}
|
||||
onChange={(event) => {
|
||||
scheduleState.setQuietSuppressCategory(
|
||||
option.key,
|
||||
event.currentTarget.checked,
|
||||
);
|
||||
}}
|
||||
class="sr-only"
|
||||
/>
|
||||
<div
|
||||
class={getAlertQuietSuppressCheckboxClass(
|
||||
props.quietHours().suppress[option.key],
|
||||
)}
|
||||
>
|
||||
<Show when={props.quietHours().suppress[option.key]}>
|
||||
<svg
|
||||
class="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</Show>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-base-content">{option.label}</p>
|
||||
<p class="text-xs text-muted">{option.description}</p>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</SettingsPanel>
|
||||
<AlertGroupingSection
|
||||
grouping={props.grouping()}
|
||||
setGroupingEnabled={scheduleState.setGroupingEnabled}
|
||||
setGroupingWindow={scheduleState.setGroupingWindow}
|
||||
setGroupingByNode={scheduleState.setGroupingByNode}
|
||||
setGroupingByGuest={scheduleState.setGroupingByGuest}
|
||||
/>
|
||||
|
||||
<SettingsPanel
|
||||
title={ALERT_CONFIG_COOLDOWN_TITLE}
|
||||
description={ALERT_CONFIG_COOLDOWN_DESCRIPTION}
|
||||
action={
|
||||
<Toggle
|
||||
checked={props.cooldown().enabled}
|
||||
onChange={(event) => {
|
||||
scheduleState.setCooldownEnabled(event.currentTarget.checked);
|
||||
}}
|
||||
containerClass="sm:self-start"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertConfigToggleStatusLabel(props.cooldown().enabled)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
class="space-y-4"
|
||||
>
|
||||
<Show when={props.cooldown().enabled}>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_CONFIG_COOLDOWN_PERIOD_LABEL}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="number"
|
||||
min="5"
|
||||
max="120"
|
||||
value={props.cooldown().minutes}
|
||||
onChange={(event) => {
|
||||
scheduleState.setCooldownMinutes(event.currentTarget.value);
|
||||
}}
|
||||
class={controlClass('pr-16')}
|
||||
/>
|
||||
<span class="pointer-events-none absolute inset-y-0 right-3 flex items-center text-sm text-muted">
|
||||
{ALERT_CONFIG_COOLDOWN_PERIOD_SUFFIX}
|
||||
</span>
|
||||
</div>
|
||||
<p class={`${formHelpText} mt-1`}>{ALERT_CONFIG_COOLDOWN_PERIOD_HELP}</p>
|
||||
</div>
|
||||
<AlertRecoverySection
|
||||
notifyOnResolve={props.notifyOnResolve()}
|
||||
setNotifyOnResolveEnabled={scheduleState.setNotifyOnResolveEnabled}
|
||||
/>
|
||||
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_CONFIG_COOLDOWN_MAX_ALERTS_LABEL}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
value={props.cooldown().maxAlerts}
|
||||
onChange={(event) => {
|
||||
scheduleState.setCooldownMaxAlerts(event.currentTarget.value);
|
||||
}}
|
||||
class={controlClass('pr-16')}
|
||||
/>
|
||||
<span class="pointer-events-none absolute inset-y-0 right-3 flex items-center text-sm text-muted">
|
||||
{ALERT_CONFIG_COOLDOWN_MAX_ALERTS_SUFFIX}
|
||||
</span>
|
||||
</div>
|
||||
<p class={`${formHelpText} mt-1`}>{ALERT_CONFIG_COOLDOWN_MAX_ALERTS_HELP}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</SettingsPanel>
|
||||
<AlertEscalationSection
|
||||
escalation={props.escalation()}
|
||||
setEscalationEnabled={scheduleState.setEscalationEnabled}
|
||||
setEscalationAfter={scheduleState.setEscalationAfter}
|
||||
setEscalationNotify={scheduleState.setEscalationNotify}
|
||||
removeEscalationLevel={scheduleState.removeEscalationLevel}
|
||||
addEscalationLevel={scheduleState.addEscalationLevel}
|
||||
/>
|
||||
|
||||
<SettingsPanel
|
||||
title={ALERT_CONFIG_GROUPING_TITLE}
|
||||
description={ALERT_CONFIG_GROUPING_DESCRIPTION}
|
||||
action={
|
||||
<Toggle
|
||||
checked={props.grouping().enabled}
|
||||
onChange={(event) => {
|
||||
scheduleState.setGroupingEnabled(event.currentTarget.checked);
|
||||
}}
|
||||
containerClass="sm:self-start"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertConfigToggleStatusLabel(props.grouping().enabled)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
class="space-y-4"
|
||||
>
|
||||
<Show when={props.grouping().enabled}>
|
||||
<div class="space-y-4">
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_CONFIG_GROUPING_WINDOW_LABEL}
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="30"
|
||||
value={props.grouping().window}
|
||||
onChange={(event) => {
|
||||
scheduleState.setGroupingWindow(event.currentTarget.value);
|
||||
}}
|
||||
class="flex-1"
|
||||
/>
|
||||
<div class="w-16 rounded-md bg-surface-alt px-2 py-1 text-center text-sm text-base-content">
|
||||
{props.grouping().window} min
|
||||
</div>
|
||||
</div>
|
||||
<p class={`${formHelpText} mt-1`}>{ALERT_CONFIG_GROUPING_WINDOW_HELP}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class={`${labelClass('text-xs uppercase tracking-[0.08em]')} mb-2 block`}>
|
||||
{ALERT_CONFIG_GROUPING_STRATEGY_LABEL}
|
||||
</span>
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<label class={getAlertGroupingCardClass(props.grouping().byNode ?? false)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.grouping().byNode}
|
||||
onChange={(event) => {
|
||||
scheduleState.setGroupingByNode(event.currentTarget.checked);
|
||||
}}
|
||||
class="sr-only"
|
||||
/>
|
||||
<div class={getAlertGroupingCheckboxClass(props.grouping().byNode ?? false)}>
|
||||
<Show when={props.grouping().byNode}>
|
||||
<svg
|
||||
class="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</Show>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-base-content">
|
||||
{ALERT_CONFIG_GROUPING_BY_NODE}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class={getAlertGroupingCardClass(props.grouping().byGuest ?? false)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.grouping().byGuest}
|
||||
onChange={(event) => {
|
||||
scheduleState.setGroupingByGuest(event.currentTarget.checked);
|
||||
}}
|
||||
class="sr-only"
|
||||
/>
|
||||
<div class={getAlertGroupingCheckboxClass(props.grouping().byGuest ?? false)}>
|
||||
<Show when={props.grouping().byGuest}>
|
||||
<svg
|
||||
class="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</Show>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-base-content">
|
||||
{ALERT_CONFIG_GROUPING_BY_GUEST}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</SettingsPanel>
|
||||
|
||||
<SettingsPanel
|
||||
title={ALERT_CONFIG_RECOVERY_TITLE}
|
||||
description={ALERT_CONFIG_RECOVERY_DESCRIPTION}
|
||||
action={
|
||||
<Toggle
|
||||
checked={props.notifyOnResolve()}
|
||||
onChange={(event) => {
|
||||
scheduleState.setNotifyOnResolveEnabled(event.currentTarget.checked);
|
||||
}}
|
||||
containerClass="sm:self-start"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertConfigToggleStatusLabel(props.notifyOnResolve())}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
class="space-y-3"
|
||||
>
|
||||
<p class={formHelpText}>{getAlertConfigRecoveryHelp()}</p>
|
||||
</SettingsPanel>
|
||||
|
||||
<SettingsPanel
|
||||
title={ALERT_CONFIG_ESCALATION_TITLE}
|
||||
description={ALERT_CONFIG_ESCALATION_DESCRIPTION}
|
||||
action={
|
||||
<Toggle
|
||||
checked={props.escalation().enabled}
|
||||
onChange={(event) => {
|
||||
scheduleState.setEscalationEnabled(event.currentTarget.checked);
|
||||
}}
|
||||
containerClass="sm:self-start"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertConfigToggleStatusLabel(props.escalation().enabled)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
class="space-y-4"
|
||||
>
|
||||
<Show when={props.escalation().enabled}>
|
||||
<div class="space-y-3">
|
||||
<p class={formHelpText}>{getAlertConfigEscalationHelp()}</p>
|
||||
<For each={props.escalation().levels}>
|
||||
{(level, index) => (
|
||||
<div class="flex items-center gap-3 rounded-md border border-border bg-surface-hover p-3">
|
||||
<div class="flex flex-1 flex-col gap-3 sm:grid sm:grid-cols-2 sm:items-center sm:gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{ALERT_CONFIG_ESCALATION_AFTER_LABEL}
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
min="5"
|
||||
max="180"
|
||||
value={level.after}
|
||||
onChange={(event) => {
|
||||
scheduleState.setEscalationAfter(
|
||||
index(),
|
||||
event.currentTarget.value,
|
||||
);
|
||||
}}
|
||||
class={`${controlClass('px-2 py-1 text-sm')} w-20`}
|
||||
/>
|
||||
<span class="text-xs text-muted">
|
||||
{ALERT_CONFIG_ESCALATION_MINUTES_SUFFIX}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{ALERT_CONFIG_ESCALATION_NOTIFY_LABEL}
|
||||
</span>
|
||||
<select
|
||||
value={level.notify}
|
||||
onChange={(event) => {
|
||||
scheduleState.setEscalationNotify(
|
||||
index(),
|
||||
event.currentTarget.value as EscalationNotifyTarget,
|
||||
);
|
||||
}}
|
||||
class={`${controlClass('px-2 py-1 text-sm')} flex-1`}
|
||||
>
|
||||
<option value="email">{getAlertConfigEscalationNotifyLabel('email')}</option>
|
||||
<option value="webhook">
|
||||
{getAlertConfigEscalationNotifyLabel('webhook')}
|
||||
</option>
|
||||
<option value="all">{getAlertConfigEscalationNotifyLabel('all')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
scheduleState.removeEscalationLevel(index());
|
||||
}}
|
||||
class="rounded-md p-1.5 text-red-600 transition-colors hover:bg-red-100 dark:hover:bg-red-900"
|
||||
title={ALERT_CONFIG_ESCALATION_REMOVE_TITLE}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
scheduleState.addEscalationLevel();
|
||||
}}
|
||||
class="flex w-full items-center justify-center gap-2 rounded-md border-2 border-dashed border-border py-2 text-sm text-muted transition-all hover:border-slate-400 hover:bg-surface-hover"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/>
|
||||
</svg>
|
||||
{ALERT_CONFIG_ESCALATION_ADD_LABEL}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</SettingsPanel>
|
||||
|
||||
<SettingsPanel
|
||||
title={ALERT_CONFIG_SUMMARY_TITLE}
|
||||
description={ALERT_CONFIG_SUMMARY_DESCRIPTION}
|
||||
tone="muted"
|
||||
padding="lg"
|
||||
bodyClass="space-y-1 text-sm text-blue-800 dark:text-blue-300"
|
||||
class="lg:col-span-2"
|
||||
>
|
||||
<Show when={props.quietHours().enabled}>
|
||||
<p>
|
||||
{getAlertConfigSummaryQuietHours(
|
||||
props.quietHours().start,
|
||||
props.quietHours().end,
|
||||
props.quietHours().timezone,
|
||||
)}
|
||||
</p>
|
||||
</Show>
|
||||
<Show
|
||||
when={
|
||||
props.quietHours().enabled &&
|
||||
(props.quietHours().suppress.performance ||
|
||||
props.quietHours().suppress.storage ||
|
||||
props.quietHours().suppress.offline)
|
||||
}
|
||||
>
|
||||
<p>
|
||||
{getAlertConfigSummarySuppressing(
|
||||
quietHourSuppressOptions
|
||||
.filter((option) => props.quietHours().suppress[option.key])
|
||||
.map((option) => option.label),
|
||||
)}
|
||||
</p>
|
||||
</Show>
|
||||
<Show when={props.cooldown().enabled}>
|
||||
<p>
|
||||
{getAlertConfigSummaryCooldown(
|
||||
props.cooldown().minutes,
|
||||
props.cooldown().maxAlerts,
|
||||
)}
|
||||
</p>
|
||||
</Show>
|
||||
<Show when={props.grouping().enabled}>
|
||||
<p>
|
||||
{getAlertConfigSummaryGrouping(
|
||||
props.grouping().window,
|
||||
props.grouping().byNode ?? false,
|
||||
props.grouping().byGuest ?? false,
|
||||
)}
|
||||
</p>
|
||||
</Show>
|
||||
<Show when={props.notifyOnResolve()}>
|
||||
<p>{getAlertConfigSummaryRecoveryEnabled()}</p>
|
||||
</Show>
|
||||
<Show when={props.escalation().enabled && props.escalation().levels.length > 0}>
|
||||
<p>{getAlertConfigSummaryEscalation(props.escalation().levels.length)}</p>
|
||||
</Show>
|
||||
<Show
|
||||
when={
|
||||
!props.quietHours().enabled &&
|
||||
!props.cooldown().enabled &&
|
||||
!props.grouping().enabled &&
|
||||
!props.escalation().enabled
|
||||
}
|
||||
>
|
||||
<p>{getAlertConfigSummaryAllDisabled()}</p>
|
||||
</Show>
|
||||
</SettingsPanel>
|
||||
<AlertScheduleSummarySection
|
||||
quietHours={props.quietHours()}
|
||||
cooldown={props.cooldown()}
|
||||
grouping={props.grouping()}
|
||||
notifyOnResolve={props.notifyOnResolve()}
|
||||
escalation={props.escalation()}
|
||||
quietHourSuppressOptions={quietHourSuppressOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -309,6 +309,15 @@ describe('tab path helpers', () => {
|
|||
expect(alertScheduleStateSource).toContain('createDefaultGrouping');
|
||||
expect(alertScheduleStateSource).toContain('createDefaultEscalation');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigQuietHourSuppressOptions');
|
||||
expect(alertScheduleTabSource).toContain('AlertQuietHoursSection');
|
||||
expect(alertScheduleTabSource).toContain('AlertCooldownSection');
|
||||
expect(alertScheduleTabSource).toContain('AlertGroupingSection');
|
||||
expect(alertScheduleTabSource).toContain('AlertRecoverySection');
|
||||
expect(alertScheduleTabSource).toContain('AlertEscalationSection');
|
||||
expect(alertScheduleTabSource).toContain('AlertScheduleSummarySection');
|
||||
expect(alertScheduleTabSource).not.toContain('ALERT_CONFIG_COOLDOWN_TITLE');
|
||||
expect(alertScheduleTabSource).not.toContain('ALERT_CONFIG_QUIET_HOURS_TITLE');
|
||||
expect(alertScheduleTabSource).not.toContain('ALERT_CONFIG_ESCALATION_TITLE');
|
||||
expect(alertThresholdsTabSource).toContain('ThresholdsTable');
|
||||
expect(thresholdsTableSource).toContain(
|
||||
"import { useThresholdsTableState } from '@/features/alerts/thresholds/hooks/useThresholdsTableState';",
|
||||
|
|
|
|||
|
|
@ -2938,11 +2938,6 @@ describe('frontend resource type boundaries', () => {
|
|||
'export function useThresholdsOverrideMutations',
|
||||
);
|
||||
expect(thresholdsOverrideMutationsHookSource).toContain('matchesAlertIdentifier');
|
||||
expect(alertScheduleTabSource).toContain('getAlertGroupingCardClass');
|
||||
expect(alertScheduleTabSource).toContain('getAlertGroupingCheckboxClass');
|
||||
expect(alertScheduleTabSource).toContain('getAlertQuietDayButtonClass');
|
||||
expect(alertScheduleTabSource).toContain('getAlertQuietSuppressCardClass');
|
||||
expect(alertScheduleTabSource).toContain('getAlertQuietSuppressCheckboxClass');
|
||||
expect(alertsPageSource).not.toContain('getAlertConfigUnsavedChangesLabel');
|
||||
expect(alertsConfigurationSurfaceSource).toContain('getAlertConfigUnsavedChangesLabel');
|
||||
expect(alertsConfigurationSurfaceSource).toContain('getAlertConfigSaveChangesLabel');
|
||||
|
|
@ -2952,18 +2947,17 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(alertsPageSource).toContain('getAlertConfigLeaveConfirmation');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigResetDefaultsLabel');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigResetDefaultsTitle');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigToggleStatusLabel');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigSummaryQuietHours');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigSummarySuppressing');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigSummaryCooldown');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigSummaryGrouping');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigSummaryRecoveryEnabled');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigSummaryEscalation');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigQuietHourSuppressOptions');
|
||||
expect(alertScheduleTabSource).toContain('ALERT_CONFIG_COOLDOWN_PERIOD_LABEL');
|
||||
expect(alertScheduleTabSource).toContain('ALERT_CONFIG_COOLDOWN_MAX_ALERTS_LABEL');
|
||||
expect(alertScheduleTabSource).toContain('ALERT_CONFIG_GROUPING_WINDOW_LABEL');
|
||||
expect(alertScheduleTabSource).toContain('ALERT_CONFIG_GROUPING_STRATEGY_LABEL');
|
||||
expect(alertScheduleTabSource).toContain('AlertQuietHoursSection');
|
||||
expect(alertScheduleTabSource).toContain('AlertCooldownSection');
|
||||
expect(alertScheduleTabSource).toContain('AlertGroupingSection');
|
||||
expect(alertScheduleTabSource).toContain('AlertRecoverySection');
|
||||
expect(alertScheduleTabSource).toContain('AlertEscalationSection');
|
||||
expect(alertScheduleTabSource).toContain('AlertScheduleSummarySection');
|
||||
expect(alertScheduleTabSource).not.toContain('ALERT_CONFIG_COOLDOWN_PERIOD_LABEL');
|
||||
expect(alertScheduleTabSource).not.toContain('ALERT_CONFIG_COOLDOWN_MAX_ALERTS_LABEL');
|
||||
expect(alertScheduleTabSource).not.toContain('ALERT_CONFIG_GROUPING_WINDOW_LABEL');
|
||||
expect(alertScheduleTabSource).not.toContain('ALERT_CONFIG_GROUPING_STRATEGY_LABEL');
|
||||
expect(alertsPageSource).not.toContain('const statusClasses =');
|
||||
expect(alertsPageSource).not.toContain('const levelClasses =');
|
||||
expect(alertsPageSource).not.toContain("alert.source === 'ai' ? 'Patrol' : 'Alert'");
|
||||
|
|
@ -3896,8 +3890,8 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(alertDestinationsTabSource).toContain('AlertEmailDestinationsSection');
|
||||
expect(alertDestinationsTabSource).toContain('AlertWebhookDestinationsSection');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigQuietHourSuppressOptions');
|
||||
expect(alertScheduleTabSource).toContain('getAlertGroupingCardClass');
|
||||
expect(alertScheduleTabSource).toContain('getAlertQuietDayButtonClass');
|
||||
expect(alertScheduleTabSource).toContain('AlertGroupingSection');
|
||||
expect(alertScheduleTabSource).toContain('AlertQuietHoursSection');
|
||||
});
|
||||
|
||||
it('keeps alert resource table vocabulary in a shared presentation utility', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue