mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-06 12:52:19 +00:00
Split alert destinations tab section owners
This commit is contained in:
@@ -256,8 +256,12 @@ owns webhook load/mutate/test flow,
|
||||
destination test actions plus retry orchestration around that webhook runtime,
|
||||
while
|
||||
`frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx` stays the
|
||||
destinations render shell. Future config cleanup should extend the config
|
||||
transport hook, the config model, the override-projection hook, or the
|
||||
destinations render shell and composes
|
||||
`frontend-modern/src/features/alerts/AlertEmailDestinationsSection.tsx`,
|
||||
`frontend-modern/src/features/alerts/AlertAppriseDestinationsSection.tsx`,
|
||||
`frontend-modern/src/features/alerts/AlertWebhookDestinationsSection.tsx`, and
|
||||
the dedicated load/error wrappers. Future config cleanup should extend the
|
||||
config transport hook, the config model, the override-projection hook, or the
|
||||
destinations runtime hook based on which subsystem actually owns the behavior
|
||||
instead of letting the broader configuration hook absorb all four concerns
|
||||
again.
|
||||
|
||||
@@ -456,9 +456,11 @@ owns webhook runtime, and
|
||||
`frontend-modern/src/features/alerts/useAlertDestinationsTabState.ts` now owns
|
||||
destination test actions plus retry orchestration while
|
||||
`frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx` stays the
|
||||
render shell. Future cleanup should extend the transport hook, config model,
|
||||
override hook, or destinations runtime hook based on the true owner, not move
|
||||
config control flow back into the top-level page shell.
|
||||
render shell and should compose the dedicated email, Apprise, webhook, and
|
||||
load/error section owners instead of carrying those panels inline. Future
|
||||
cleanup should extend the transport hook, config model, override hook, or
|
||||
destinations runtime hook based on the true owner, not move config control
|
||||
flow back into the top-level page shell.
|
||||
The alert email provider picker now also follows the shell/runtime split:
|
||||
`frontend-modern/src/components/Alerts/useEmailProviderSelectState.ts` owns
|
||||
provider-catalog loading and provider-default application, while
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import { Show } from 'solid-js';
|
||||
|
||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import {
|
||||
formControl,
|
||||
formField,
|
||||
formHelpText,
|
||||
labelClass,
|
||||
} from '@/components/shared/Form';
|
||||
import type { UIAppriseConfig } from './types';
|
||||
import {
|
||||
ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_PLACEHOLDER,
|
||||
ALERT_DESTINATIONS_APPRISE_API_KEY_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_API_KEY_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_API_KEY_PLACEHOLDER,
|
||||
ALERT_DESTINATIONS_APPRISE_CLI_PATH_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_CLI_PATH_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_CLI_PATH_PLACEHOLDER,
|
||||
ALERT_DESTINATIONS_APPRISE_CONFIG_KEY_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_CONFIG_KEY_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_CONFIG_KEY_PLACEHOLDER,
|
||||
ALERT_DESTINATIONS_APPRISE_MODE_CLI_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_MODE_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_MODE_HTTP_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_MODE_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_PANEL_DESCRIPTION,
|
||||
ALERT_DESTINATIONS_APPRISE_PANEL_TITLE,
|
||||
ALERT_DESTINATIONS_APPRISE_SERVER_URL_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_SERVER_URL_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_SERVER_URL_PLACEHOLDER,
|
||||
ALERT_DESTINATIONS_APPRISE_TARGETS_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_TARGETS_PLACEHOLDER,
|
||||
ALERT_DESTINATIONS_APPRISE_TIMEOUT_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_TIMEOUT_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_TLS_CHECKBOX_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_TLS_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_TLS_LABEL,
|
||||
getAlertDestinationsAppriseTargetsHelp,
|
||||
getAlertDestinationsAppriseTestLabel,
|
||||
getAlertDestinationsStatusLabel,
|
||||
} from '@/utils/alertDestinationsPresentation';
|
||||
|
||||
interface AlertAppriseDestinationsSectionProps {
|
||||
config: UIAppriseConfig;
|
||||
updateApprise: (partial: Partial<UIAppriseConfig>) => void;
|
||||
setHasUnsavedChanges: (value: boolean) => void;
|
||||
onTest: () => void;
|
||||
testing: boolean;
|
||||
}
|
||||
|
||||
export function AlertAppriseDestinationsSection(props: AlertAppriseDestinationsSectionProps) {
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={ALERT_DESTINATIONS_APPRISE_PANEL_TITLE}
|
||||
description={ALERT_DESTINATIONS_APPRISE_PANEL_DESCRIPTION}
|
||||
action={
|
||||
<div class="flex items-center gap-3 sm:self-start">
|
||||
<Toggle
|
||||
checked={props.config.enabled}
|
||||
onChange={(event) => {
|
||||
props.updateApprise({ enabled: event.currentTarget.checked });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertDestinationsStatusLabel(props.config.enabled)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<button
|
||||
class="rounded-md border border-border px-3 py-2 text-sm font-medium text-base-content transition hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-60"
|
||||
disabled={!props.config.enabled || props.testing}
|
||||
onClick={props.onTest}
|
||||
>
|
||||
{getAlertDestinationsAppriseTestLabel(props.testing)}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
class="min-w-0"
|
||||
bodyClass="space-y-4"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_MODE_LABEL}
|
||||
</label>
|
||||
<select
|
||||
class={formControl}
|
||||
value={props.config.mode}
|
||||
onInput={(event) => {
|
||||
props.updateApprise({ mode: event.currentTarget.value as 'cli' | 'http' });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
>
|
||||
<option value="cli">{ALERT_DESTINATIONS_APPRISE_MODE_CLI_LABEL}</option>
|
||||
<option value="http">{ALERT_DESTINATIONS_APPRISE_MODE_HTTP_LABEL}</option>
|
||||
</select>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_MODE_HELP}</p>
|
||||
</div>
|
||||
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_TARGETS_LABEL}
|
||||
</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
class={`${formControl} min-h-[120px] font-mono`}
|
||||
value={props.config.targetsText}
|
||||
placeholder={ALERT_DESTINATIONS_APPRISE_TARGETS_PLACEHOLDER}
|
||||
onInput={(event) => {
|
||||
props.updateApprise({ targetsText: event.currentTarget.value });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{getAlertDestinationsAppriseTargetsHelp(props.config.mode)}</p>
|
||||
</div>
|
||||
|
||||
<Show when={props.config.mode === 'cli'}>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_CLI_PATH_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={props.config.cliPath}
|
||||
class={formControl}
|
||||
placeholder={ALERT_DESTINATIONS_APPRISE_CLI_PATH_PLACEHOLDER}
|
||||
onInput={(event) => {
|
||||
props.updateApprise({ cliPath: event.currentTarget.value });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_CLI_PATH_HELP}</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.config.mode === 'http'}>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class={`${formField} sm:col-span-2`}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_SERVER_URL_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={props.config.serverUrl}
|
||||
class={formControl}
|
||||
placeholder={ALERT_DESTINATIONS_APPRISE_SERVER_URL_PLACEHOLDER}
|
||||
onInput={(event) => {
|
||||
props.updateApprise({ serverUrl: event.currentTarget.value });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_SERVER_URL_HELP}</p>
|
||||
</div>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_CONFIG_KEY_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={props.config.configKey}
|
||||
class={formControl}
|
||||
placeholder={ALERT_DESTINATIONS_APPRISE_CONFIG_KEY_PLACEHOLDER}
|
||||
onInput={(event) => {
|
||||
props.updateApprise({ configKey: event.currentTarget.value });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_CONFIG_KEY_HELP}</p>
|
||||
</div>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_API_KEY_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={props.config.apiKey}
|
||||
class={formControl}
|
||||
placeholder={ALERT_DESTINATIONS_APPRISE_API_KEY_PLACEHOLDER}
|
||||
onInput={(event) => {
|
||||
props.updateApprise({ apiKey: event.currentTarget.value });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_API_KEY_HELP}</p>
|
||||
</div>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={props.config.apiKeyHeader}
|
||||
class={formControl}
|
||||
placeholder={ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_PLACEHOLDER}
|
||||
onInput={(event) => {
|
||||
props.updateApprise({ apiKeyHeader: event.currentTarget.value });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_HELP}</p>
|
||||
</div>
|
||||
<div class={`${formField} sm:col-span-2`}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_TLS_LABEL}
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border border-border"
|
||||
checked={props.config.skipTlsVerify}
|
||||
onChange={(event) => {
|
||||
props.updateApprise({ skipTlsVerify: event.currentTarget.checked });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<span class="text-sm text-muted">
|
||||
{ALERT_DESTINATIONS_APPRISE_TLS_CHECKBOX_LABEL}
|
||||
</span>
|
||||
</label>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_TLS_HELP}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_TIMEOUT_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="5"
|
||||
max="120"
|
||||
value={props.config.timeoutSeconds}
|
||||
class={formControl}
|
||||
onInput={(event) => {
|
||||
const raw = event.currentTarget.valueAsNumber;
|
||||
const safe = Number.isNaN(raw) ? 15 : Math.min(120, Math.max(5, Math.trunc(raw)));
|
||||
props.updateApprise({ timeoutSeconds: safe });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_TIMEOUT_HELP}</p>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import AlertTriangleIcon from 'lucide-solid/icons/alert-triangle';
|
||||
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import {
|
||||
getAlertDestinationsLoadErrorBanner,
|
||||
getAlertDestinationsRetryLabel,
|
||||
} from '@/utils/alertDestinationsPresentation';
|
||||
|
||||
interface AlertDestinationsLoadErrorCardProps {
|
||||
error: string;
|
||||
isRetrying: boolean;
|
||||
onRetry: () => void;
|
||||
}
|
||||
|
||||
export function AlertDestinationsLoadErrorCard(props: AlertDestinationsLoadErrorCardProps) {
|
||||
return (
|
||||
<Card tone="danger" padding="sm" class="border-red-200 dark:border-red-800 sm:p-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex items-center gap-2 text-red-800 dark:text-red-200">
|
||||
<AlertTriangleIcon class="h-4 w-4 flex-shrink-0" />
|
||||
<span class="text-sm font-medium">
|
||||
{getAlertDestinationsLoadErrorBanner(props.error)}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
class="flex-shrink-0 rounded-md border border-red-300 bg-transparent px-3 py-1.5 text-sm font-medium text-red-800 transition hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-red-700 dark:text-red-200 dark:hover:bg-red-900/30"
|
||||
disabled={props.isRetrying}
|
||||
onClick={props.onRetry}
|
||||
>
|
||||
{getAlertDestinationsRetryLabel(props.isRetrying)}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export function AlertDestinationsLoadingState() {
|
||||
return (
|
||||
<div class="flex w-full flex-col gap-6 animate-pulse pointer-events-none select-none md:gap-8">
|
||||
<div class="rounded-lg border border-border bg-surface p-6 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-2">
|
||||
<div class="h-5 w-40 rounded bg-surface-hover" />
|
||||
<div class="h-3 w-64 rounded bg-surface-hover" />
|
||||
</div>
|
||||
<div class="h-6 w-12 rounded-full bg-surface-hover" />
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="h-4 w-24 rounded bg-surface-hover" />
|
||||
<div class="h-10 w-full rounded bg-surface-hover" />
|
||||
<div class="h-4 w-32 rounded bg-surface-hover" />
|
||||
<div class="h-10 w-full rounded bg-surface-hover" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-border bg-surface p-6 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-2">
|
||||
<div class="h-5 w-44 rounded bg-surface-hover" />
|
||||
<div class="h-3 w-72 rounded bg-surface-hover" />
|
||||
</div>
|
||||
<div class="h-6 w-12 rounded-full bg-surface-hover" />
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="h-4 w-28 rounded bg-surface-hover" />
|
||||
<div class="h-10 w-full rounded bg-surface-hover" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-border bg-surface p-6 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-2">
|
||||
<div class="h-5 w-28 rounded bg-surface-hover" />
|
||||
<div class="h-3 w-56 rounded bg-surface-hover" />
|
||||
</div>
|
||||
<div class="h-4 w-20 rounded bg-surface-hover" />
|
||||
</div>
|
||||
<div class="h-10 w-full rounded bg-surface-hover" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { EmailProviderSelect } from '@/components/Alerts/EmailProviderSelect';
|
||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import type { UIEmailConfig } from './types';
|
||||
import {
|
||||
ALERT_DESTINATIONS_EMAIL_PANEL_DESCRIPTION,
|
||||
ALERT_DESTINATIONS_EMAIL_PANEL_TITLE,
|
||||
getAlertDestinationsStatusLabel,
|
||||
} from '@/utils/alertDestinationsPresentation';
|
||||
|
||||
interface AlertEmailDestinationsSectionProps {
|
||||
config: UIEmailConfig;
|
||||
setConfig: (config: UIEmailConfig) => void;
|
||||
setHasUnsavedChanges: (value: boolean) => void;
|
||||
onTest: () => void;
|
||||
testing: boolean;
|
||||
}
|
||||
|
||||
export function AlertEmailDestinationsSection(props: AlertEmailDestinationsSectionProps) {
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={ALERT_DESTINATIONS_EMAIL_PANEL_TITLE}
|
||||
description={ALERT_DESTINATIONS_EMAIL_PANEL_DESCRIPTION}
|
||||
action={
|
||||
<Toggle
|
||||
checked={props.config.enabled}
|
||||
onChange={(event) => {
|
||||
props.setConfig({
|
||||
...props.config,
|
||||
enabled: event.currentTarget.checked,
|
||||
});
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
containerClass="sm:self-start"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertDestinationsStatusLabel(props.config.enabled)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
class="min-w-0"
|
||||
bodyClass=""
|
||||
>
|
||||
<div
|
||||
class={`${!props.config.enabled ? 'pointer-events-none opacity-50 transition-opacity' : 'transition-opacity'}`}
|
||||
>
|
||||
<EmailProviderSelect
|
||||
config={props.config}
|
||||
onChange={(config) => {
|
||||
props.setConfig(config);
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
onTest={props.onTest}
|
||||
testing={props.testing}
|
||||
/>
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { WebhookConfig } from '@/components/Alerts/WebhookConfig';
|
||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import type { Webhook } from '@/api/notifications';
|
||||
import {
|
||||
getAlertWebhooksSectionDescription,
|
||||
getAlertWebhooksSectionTitle,
|
||||
} from '@/utils/alertWebhookPresentation';
|
||||
|
||||
interface AlertWebhookDestinationsSectionProps {
|
||||
webhooks: Webhook[];
|
||||
addWebhook: (webhook: Omit<Webhook, 'id'>) => void;
|
||||
updateWebhook: (webhook: Webhook) => void;
|
||||
deleteWebhook: (id: string) => void;
|
||||
testWebhook: (webhookId: string, webhookData?: Omit<Webhook, 'id'>) => void;
|
||||
testingWebhook: string | null;
|
||||
}
|
||||
|
||||
export function AlertWebhookDestinationsSection(props: AlertWebhookDestinationsSectionProps) {
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={getAlertWebhooksSectionTitle()}
|
||||
description={getAlertWebhooksSectionDescription()}
|
||||
action={<span class="whitespace-nowrap text-xs text-muted">{props.webhooks.length} configured</span>}
|
||||
class="min-w-0"
|
||||
bodyClass="space-y-4"
|
||||
>
|
||||
<WebhookConfig
|
||||
webhooks={props.webhooks}
|
||||
onAdd={props.addWebhook}
|
||||
onUpdate={props.updateWebhook}
|
||||
onDelete={props.deleteWebhook}
|
||||
onTest={props.testWebhook}
|
||||
testing={props.testingWebhook}
|
||||
/>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +1,9 @@
|
||||
import { Show } from 'solid-js';
|
||||
import AlertTriangleIcon from 'lucide-solid/icons/alert-triangle';
|
||||
|
||||
import { EmailProviderSelect } from '@/components/Alerts/EmailProviderSelect';
|
||||
import { WebhookConfig } from '@/components/Alerts/WebhookConfig';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import {
|
||||
formControl,
|
||||
formField,
|
||||
formHelpText,
|
||||
labelClass,
|
||||
} from '@/components/shared/Form';
|
||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import {
|
||||
ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_PLACEHOLDER,
|
||||
ALERT_DESTINATIONS_APPRISE_API_KEY_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_API_KEY_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_API_KEY_PLACEHOLDER,
|
||||
ALERT_DESTINATIONS_APPRISE_CLI_PATH_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_CLI_PATH_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_CLI_PATH_PLACEHOLDER,
|
||||
ALERT_DESTINATIONS_APPRISE_CONFIG_KEY_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_CONFIG_KEY_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_CONFIG_KEY_PLACEHOLDER,
|
||||
ALERT_DESTINATIONS_APPRISE_MODE_CLI_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_MODE_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_MODE_HTTP_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_MODE_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_PANEL_DESCRIPTION,
|
||||
ALERT_DESTINATIONS_APPRISE_PANEL_TITLE,
|
||||
ALERT_DESTINATIONS_APPRISE_SERVER_URL_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_SERVER_URL_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_SERVER_URL_PLACEHOLDER,
|
||||
ALERT_DESTINATIONS_APPRISE_TARGETS_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_TARGETS_PLACEHOLDER,
|
||||
ALERT_DESTINATIONS_APPRISE_TIMEOUT_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_TIMEOUT_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_TLS_CHECKBOX_LABEL,
|
||||
ALERT_DESTINATIONS_APPRISE_TLS_HELP,
|
||||
ALERT_DESTINATIONS_APPRISE_TLS_LABEL,
|
||||
ALERT_DESTINATIONS_EMAIL_PANEL_DESCRIPTION,
|
||||
ALERT_DESTINATIONS_EMAIL_PANEL_TITLE,
|
||||
getAlertDestinationsAppriseTargetsHelp,
|
||||
getAlertDestinationsAppriseTestLabel,
|
||||
getAlertDestinationsLoadErrorBanner,
|
||||
getAlertDestinationsRetryLabel,
|
||||
getAlertDestinationsStatusLabel,
|
||||
} from '@/utils/alertDestinationsPresentation';
|
||||
import {
|
||||
getAlertWebhooksSectionDescription,
|
||||
getAlertWebhooksSectionTitle,
|
||||
} from '@/utils/alertWebhookPresentation';
|
||||
import { AlertAppriseDestinationsSection } from '../AlertAppriseDestinationsSection';
|
||||
import { AlertDestinationsLoadErrorCard } from '../AlertDestinationsLoadErrorCard';
|
||||
import { AlertDestinationsLoadingState } from '../AlertDestinationsLoadingState';
|
||||
import { AlertEmailDestinationsSection } from '../AlertEmailDestinationsSection';
|
||||
import { AlertWebhookDestinationsSection } from '../AlertWebhookDestinationsSection';
|
||||
|
||||
import { useAlertDestinationsTabState, type AlertDestinationsTabStateProps } from '../useAlertDestinationsTabState';
|
||||
|
||||
@@ -68,325 +19,40 @@ export function DestinationsTab(props: DestinationsTabProps) {
|
||||
<div class="flex w-full max-w-full flex-col gap-6 md:gap-8">
|
||||
<Show
|
||||
when={!state.isLoading()}
|
||||
fallback={
|
||||
<div class="flex w-full flex-col gap-6 animate-pulse pointer-events-none select-none md:gap-8">
|
||||
<div class="rounded-lg border border-border bg-surface p-6 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-2">
|
||||
<div class="h-5 w-40 rounded bg-surface-hover" />
|
||||
<div class="h-3 w-64 rounded bg-surface-hover" />
|
||||
</div>
|
||||
<div class="h-6 w-12 rounded-full bg-surface-hover" />
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="h-4 w-24 rounded bg-surface-hover" />
|
||||
<div class="h-10 w-full rounded bg-surface-hover" />
|
||||
<div class="h-4 w-32 rounded bg-surface-hover" />
|
||||
<div class="h-10 w-full rounded bg-surface-hover" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-border bg-surface p-6 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-2">
|
||||
<div class="h-5 w-44 rounded bg-surface-hover" />
|
||||
<div class="h-3 w-72 rounded bg-surface-hover" />
|
||||
</div>
|
||||
<div class="h-6 w-12 rounded-full bg-surface-hover" />
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="h-4 w-28 rounded bg-surface-hover" />
|
||||
<div class="h-10 w-full rounded bg-surface-hover" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-border bg-surface p-6 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-2">
|
||||
<div class="h-5 w-28 rounded bg-surface-hover" />
|
||||
<div class="h-3 w-56 rounded bg-surface-hover" />
|
||||
</div>
|
||||
<div class="h-4 w-20 rounded bg-surface-hover" />
|
||||
</div>
|
||||
<div class="h-10 w-full rounded bg-surface-hover" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
fallback={<AlertDestinationsLoadingState />}
|
||||
>
|
||||
<Show when={state.hasLoadError()}>
|
||||
<Card tone="danger" padding="sm" class="border-red-200 dark:border-red-800 sm:p-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex items-center gap-2 text-red-800 dark:text-red-200">
|
||||
<AlertTriangleIcon class="h-4 w-4 flex-shrink-0" />
|
||||
<span class="text-sm font-medium">
|
||||
{getAlertDestinationsLoadErrorBanner(
|
||||
props.configLoadError() || state.webhookLoadError() || '',
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
class="flex-shrink-0 rounded-md border border-red-300 bg-transparent px-3 py-1.5 text-sm font-medium text-red-800 transition hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-red-700 dark:text-red-200 dark:hover:bg-red-900/30"
|
||||
disabled={props.isRetrying()}
|
||||
onClick={state.handleRetry}
|
||||
>
|
||||
{getAlertDestinationsRetryLabel(props.isRetrying())}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
<AlertDestinationsLoadErrorCard
|
||||
error={props.configLoadError() || state.webhookLoadError() || ''}
|
||||
isRetrying={props.isRetrying()}
|
||||
onRetry={state.handleRetry}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<SettingsPanel
|
||||
title={ALERT_DESTINATIONS_EMAIL_PANEL_TITLE}
|
||||
description={ALERT_DESTINATIONS_EMAIL_PANEL_DESCRIPTION}
|
||||
action={
|
||||
<Toggle
|
||||
checked={props.emailConfig().enabled}
|
||||
onChange={(event) => {
|
||||
props.setEmailConfig({
|
||||
...props.emailConfig(),
|
||||
enabled: event.currentTarget.checked,
|
||||
});
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
containerClass="sm:self-start"
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertDestinationsStatusLabel(props.emailConfig().enabled)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
class="min-w-0"
|
||||
bodyClass=""
|
||||
>
|
||||
<div
|
||||
class={`${!props.emailConfig().enabled ? 'pointer-events-none opacity-50 transition-opacity' : 'transition-opacity'}`}
|
||||
>
|
||||
<EmailProviderSelect
|
||||
config={props.emailConfig()}
|
||||
onChange={(config) => {
|
||||
props.setEmailConfig(config);
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
onTest={state.testEmailConfig}
|
||||
testing={state.testingEmail()}
|
||||
/>
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
<AlertEmailDestinationsSection
|
||||
config={props.emailConfig()}
|
||||
setConfig={props.setEmailConfig}
|
||||
setHasUnsavedChanges={props.setHasUnsavedChanges}
|
||||
onTest={state.testEmailConfig}
|
||||
testing={state.testingEmail()}
|
||||
/>
|
||||
|
||||
<SettingsPanel
|
||||
title={ALERT_DESTINATIONS_APPRISE_PANEL_TITLE}
|
||||
description={ALERT_DESTINATIONS_APPRISE_PANEL_DESCRIPTION}
|
||||
action={
|
||||
<div class="flex items-center gap-3 sm:self-start">
|
||||
<Toggle
|
||||
checked={state.appriseState().enabled}
|
||||
onChange={(event) => {
|
||||
state.updateApprise({ enabled: event.currentTarget.checked });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertDestinationsStatusLabel(state.appriseState().enabled)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<button
|
||||
class="rounded-md border border-border px-3 py-2 text-sm font-medium text-base-content transition hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-60"
|
||||
disabled={!state.appriseState().enabled || state.testingApprise()}
|
||||
onClick={state.testApprise}
|
||||
>
|
||||
{getAlertDestinationsAppriseTestLabel(state.testingApprise())}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
class="min-w-0"
|
||||
bodyClass="space-y-4"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_MODE_LABEL}
|
||||
</label>
|
||||
<select
|
||||
class={formControl}
|
||||
value={state.appriseState().mode}
|
||||
onInput={(event) => {
|
||||
state.updateApprise({ mode: event.currentTarget.value as 'cli' | 'http' });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
>
|
||||
<option value="cli">{ALERT_DESTINATIONS_APPRISE_MODE_CLI_LABEL}</option>
|
||||
<option value="http">{ALERT_DESTINATIONS_APPRISE_MODE_HTTP_LABEL}</option>
|
||||
</select>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_MODE_HELP}</p>
|
||||
</div>
|
||||
<AlertAppriseDestinationsSection
|
||||
config={state.appriseState()}
|
||||
updateApprise={state.updateApprise}
|
||||
setHasUnsavedChanges={props.setHasUnsavedChanges}
|
||||
onTest={state.testApprise}
|
||||
testing={state.testingApprise()}
|
||||
/>
|
||||
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_TARGETS_LABEL}
|
||||
</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
class={`${formControl} min-h-[120px] font-mono`}
|
||||
value={state.appriseState().targetsText}
|
||||
placeholder={ALERT_DESTINATIONS_APPRISE_TARGETS_PLACEHOLDER}
|
||||
onInput={(event) => {
|
||||
state.updateApprise({ targetsText: event.currentTarget.value });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>
|
||||
{getAlertDestinationsAppriseTargetsHelp(state.appriseState().mode)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Show when={state.appriseState().mode === 'cli'}>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_CLI_PATH_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.appriseState().cliPath}
|
||||
class={formControl}
|
||||
placeholder={ALERT_DESTINATIONS_APPRISE_CLI_PATH_PLACEHOLDER}
|
||||
onInput={(event) => {
|
||||
state.updateApprise({ cliPath: event.currentTarget.value });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_CLI_PATH_HELP}</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={state.appriseState().mode === 'http'}>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class={`${formField} sm:col-span-2`}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_SERVER_URL_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.appriseState().serverUrl}
|
||||
class={formControl}
|
||||
placeholder={ALERT_DESTINATIONS_APPRISE_SERVER_URL_PLACEHOLDER}
|
||||
onInput={(event) => {
|
||||
state.updateApprise({ serverUrl: event.currentTarget.value });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_SERVER_URL_HELP}</p>
|
||||
</div>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_CONFIG_KEY_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.appriseState().configKey}
|
||||
class={formControl}
|
||||
placeholder={ALERT_DESTINATIONS_APPRISE_CONFIG_KEY_PLACEHOLDER}
|
||||
onInput={(event) => {
|
||||
state.updateApprise({ configKey: event.currentTarget.value });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_CONFIG_KEY_HELP}</p>
|
||||
</div>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_API_KEY_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={state.appriseState().apiKey}
|
||||
class={formControl}
|
||||
placeholder={ALERT_DESTINATIONS_APPRISE_API_KEY_PLACEHOLDER}
|
||||
onInput={(event) => {
|
||||
state.updateApprise({ apiKey: event.currentTarget.value });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_API_KEY_HELP}</p>
|
||||
</div>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.appriseState().apiKeyHeader}
|
||||
class={formControl}
|
||||
placeholder={ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_PLACEHOLDER}
|
||||
onInput={(event) => {
|
||||
state.updateApprise({ apiKeyHeader: event.currentTarget.value });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_HELP}</p>
|
||||
</div>
|
||||
<div class={`${formField} sm:col-span-2`}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_TLS_LABEL}
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border border-border"
|
||||
checked={state.appriseState().skipTlsVerify}
|
||||
onChange={(event) => {
|
||||
state.updateApprise({ skipTlsVerify: event.currentTarget.checked });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<span class="text-sm text-muted">
|
||||
{ALERT_DESTINATIONS_APPRISE_TLS_CHECKBOX_LABEL}
|
||||
</span>
|
||||
</label>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_TLS_HELP}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class={formField}>
|
||||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_TIMEOUT_LABEL}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="5"
|
||||
max="120"
|
||||
value={state.appriseState().timeoutSeconds}
|
||||
class={formControl}
|
||||
onInput={(event) => {
|
||||
const raw = event.currentTarget.valueAsNumber;
|
||||
const safe = Number.isNaN(raw) ? 15 : Math.min(120, Math.max(5, Math.trunc(raw)));
|
||||
state.updateApprise({ timeoutSeconds: safe });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class={formHelpText}>{ALERT_DESTINATIONS_APPRISE_TIMEOUT_HELP}</p>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
|
||||
<SettingsPanel
|
||||
title={getAlertWebhooksSectionTitle()}
|
||||
description={getAlertWebhooksSectionDescription()}
|
||||
action={
|
||||
<span class="whitespace-nowrap text-xs text-muted">{state.webhooks().length} configured</span>
|
||||
}
|
||||
class="min-w-0"
|
||||
bodyClass="space-y-4"
|
||||
>
|
||||
<WebhookConfig
|
||||
webhooks={state.webhooks()}
|
||||
onAdd={state.addWebhook}
|
||||
onUpdate={state.updateWebhook}
|
||||
onDelete={state.deleteWebhook}
|
||||
onTest={state.testWebhook}
|
||||
testing={state.testingWebhook()}
|
||||
/>
|
||||
</SettingsPanel>
|
||||
<AlertWebhookDestinationsSection
|
||||
webhooks={state.webhooks()}
|
||||
addWebhook={state.addWebhook}
|
||||
updateWebhook={state.updateWebhook}
|
||||
deleteWebhook={state.deleteWebhook}
|
||||
testWebhook={state.testWebhook}
|
||||
testingWebhook={state.testingWebhook()}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -248,9 +248,17 @@ describe('tab path helpers', () => {
|
||||
expect(alertsPageSource).not.toContain('function HistoryTab(');
|
||||
expect(alertHistoryTabSource).toContain('useAlertHistoryState');
|
||||
expect(alertDestinationsTabSource).toContain('useAlertDestinationsTabState');
|
||||
expect(alertDestinationsTabSource).toContain('AlertDestinationsLoadingState');
|
||||
expect(alertDestinationsTabSource).toContain('AlertDestinationsLoadErrorCard');
|
||||
expect(alertDestinationsTabSource).toContain('AlertEmailDestinationsSection');
|
||||
expect(alertDestinationsTabSource).toContain('AlertAppriseDestinationsSection');
|
||||
expect(alertDestinationsTabSource).toContain('AlertWebhookDestinationsSection');
|
||||
expect(alertDestinationsTabSource).not.toContain('NotificationsAPI.getWebhooks');
|
||||
expect(alertDestinationsTabSource).not.toContain('NotificationsAPI.testNotification');
|
||||
expect(alertDestinationsTabSource).not.toContain('NotificationsAPI.createWebhook');
|
||||
expect(alertDestinationsTabSource).not.toContain('ALERT_DESTINATIONS_EMAIL_PANEL_TITLE');
|
||||
expect(alertDestinationsTabSource).not.toContain('ALERT_DESTINATIONS_APPRISE_PANEL_TITLE');
|
||||
expect(alertDestinationsTabSource).not.toContain('getAlertWebhooksSectionTitle');
|
||||
expect(alertHistoryTabSource).toContain('IncidentTimelinePanel');
|
||||
expect(alertHistoryTabSource).not.toContain('useAlertIncidentTimelineState');
|
||||
expect(alertHistoryTabSource).not.toContain('AlertsAPI.getHistory');
|
||||
|
||||
@@ -2823,15 +2823,18 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(alertWebhookDestinationsStateSource).toContain(
|
||||
'getAlertDestinationsWebhookLoadError',
|
||||
);
|
||||
expect(alertDestinationsTabSource).toContain('getAlertDestinationsLoadErrorBanner');
|
||||
expect(alertDestinationsTabSource).toContain('getAlertDestinationsAppriseTargetsHelp');
|
||||
expect(alertDestinationsTabSource).toContain('getAlertDestinationsAppriseTestLabel');
|
||||
expect(alertDestinationsTabSource).toContain('getAlertDestinationsRetryLabel');
|
||||
expect(alertDestinationsTabSource).toContain('getAlertDestinationsStatusLabel');
|
||||
expect(alertDestinationsTabSource).toContain('useAlertDestinationsTabState');
|
||||
expect(alertDestinationsTabSource).toContain('AlertDestinationsLoadingState');
|
||||
expect(alertDestinationsTabSource).toContain('AlertDestinationsLoadErrorCard');
|
||||
expect(alertDestinationsTabSource).toContain('AlertEmailDestinationsSection');
|
||||
expect(alertDestinationsTabSource).toContain('AlertAppriseDestinationsSection');
|
||||
expect(alertDestinationsTabSource).toContain('AlertWebhookDestinationsSection');
|
||||
expect(alertDestinationsTabSource).not.toContain('NotificationsAPI.getWebhooks');
|
||||
expect(alertDestinationsTabSource).not.toContain('NotificationsAPI.testNotification');
|
||||
expect(alertDestinationsTabSource).not.toContain('NotificationsAPI.createWebhook');
|
||||
expect(alertDestinationsTabSource).not.toContain('ALERT_DESTINATIONS_EMAIL_PANEL_TITLE');
|
||||
expect(alertDestinationsTabSource).not.toContain('ALERT_DESTINATIONS_APPRISE_PANEL_TITLE');
|
||||
expect(alertDestinationsTabSource).not.toContain('getAlertWebhooksSectionTitle');
|
||||
expect(alertsPageSource).toContain(
|
||||
"import { HistoryTab } from '@/features/alerts/tabs/HistoryTab';",
|
||||
);
|
||||
@@ -3890,8 +3893,8 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(alertsPageSource).not.toContain('function ScheduleTab(');
|
||||
expect(alertDestinationsTabSource).toContain('useAlertDestinationsTabState');
|
||||
expect(alertDestinationsTabSource).not.toContain('NotificationsAPI.getWebhooks');
|
||||
expect(alertDestinationsTabSource).toContain('EmailProviderSelect');
|
||||
expect(alertDestinationsTabSource).toContain('WebhookConfig');
|
||||
expect(alertDestinationsTabSource).toContain('AlertEmailDestinationsSection');
|
||||
expect(alertDestinationsTabSource).toContain('AlertWebhookDestinationsSection');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigQuietHourSuppressOptions');
|
||||
expect(alertScheduleTabSource).toContain('getAlertGroupingCardClass');
|
||||
expect(alertScheduleTabSource).toContain('getAlertQuietDayButtonClass');
|
||||
|
||||
Reference in New Issue
Block a user