"use client"; import { useState } from "react"; import { Bell, BellDot, Blocks, Loader2, Mail, Mailbox, MessageSquare, MessagesSquare, Pencil, Plus, Power, Radio, Send, ShieldAlert, Trash2, Webhook, X, type LucideIcon, } from "lucide-react"; import TablePagination from "@/components/TablePagination"; import { FieldLabel, Toggle, } from "@/app/settings/components/SettingsPrimitives"; import { getNotificationActionLabel, normalizeNotificationActionKeys, NOTIFICATION_ACTION_OPTIONS, } from "@/app/notifications/components/notification-actions"; import { createClientId } from "@/lib/random-id"; import type { EditableNotificationSettings, NotificationProviderItem, NotificationProviderKind, } from "@/app/notifications/components/notification-settings.types"; interface NotificationsSettingsPanelProps { settings: EditableNotificationSettings; actionLoading: string | null; savingProviderId?: string | null; testNotification: (providerId: string) => Promise; onUpsertProvider: (provider: NotificationProviderItem) => Promise; onToggleProvider: (providerId: string) => Promise; onDeleteProvider: (provider: NotificationProviderItem) => void; } const PROVIDER_OPTIONS: Array<{ type: NotificationProviderKind; label: string; description: string; icon: LucideIcon; color: string; }> = [ { type: "email", label: "Email SMTP", description: "SMTP relay, transactional mail, or mailbox internal.", icon: Mail, color: "#2563eb", }, { type: "telegram", label: "Telegram", description: "Bot token and chat ID for direct alerts to Telegram.", icon: Send, color: "#0ea5e9", }, { type: "discord", label: "Discord", description: "Discord webhook for incident or ops channel.", icon: MessagesSquare, color: "#5865f2", }, { type: "slack", label: "Slack", description: "Incoming webhook Slack for workspace notifications.", icon: MessageSquare, color: "#16a34a", }, { type: "lark", label: "Lark", description: "Lark/Feishu webhook for operations team.", icon: BellDot, color: "#f97316", }, { type: "teams", label: "Microsoft Teams", description: "Teams webhook for alerts to work channel.", icon: Blocks, color: "#4f46e5", }, { type: "resend", label: "Resend", description: "Provider email API with API key and sender identity.", icon: Mailbox, color: "#686f7e", }, { type: "gotify", label: "Gotify", description: "Push notification self-hosted via Gotify server.", icon: Radio, color: "#22c55e", }, { type: "ntfy", label: "ntfy", description: "Publish alert to ntfy topic, public or self-hosted.", icon: Bell, color: "#06b6d4", }, { type: "mattermost", label: "Mattermost", description: "Webhook for Mattermost incident channel.", icon: Webhook, color: "#2563eb", }, { type: "pushover", label: "Pushover", description: "Push alert to Pushover personal or team application.", icon: ShieldAlert, color: "#f59e0b", }, { type: "custom", label: "Custom Webhook", description: "Flexible webhook endpoint for internal systems.", icon: Webhook, color: "#64748b", }, ]; const TESTABLE_PROVIDER_TYPES: NotificationProviderKind[] = [ "email", "telegram", "discord", "custom", ]; const PAGE_SIZE = 6; function createProvider( type: NotificationProviderKind, ): NotificationProviderItem { return { id: createClientId("provider"), type, name: PROVIDER_OPTIONS.find((item) => item.type === type)?.label || "New Provider", enabled: true, actions: NOTIFICATION_ACTION_OPTIONS.map((action) => action.value), channel: "", webhookUrl: "", webhookConfigured: false, smtpHost: "", smtpPort: 587, smtpSecure: false, smtpUsername: "", smtpPassword: "", smtpPasswordConfigured: false, smtpFromEmail: "", smtpFromName: "", telegramChatId: "", telegramBotToken: "", telegramBotTokenConfigured: false, serverUrl: "", topic: "", userKey: "", apiKey: "", apiKeyConfigured: false, }; } function getProviderMeta(type: NotificationProviderKind) { return ( PROVIDER_OPTIONS.find((item) => item.type === type) || PROVIDER_OPTIONS[0] ); } function getProviderSummary(provider: NotificationProviderItem) { switch (provider.type) { case "email": return ( provider.smtpFromEmail || provider.smtpHost || "SMTP not configured" ); case "telegram": return provider.telegramChatId || "Chat ID not provided"; case "discord": case "slack": case "lark": case "teams": case "mattermost": case "custom": return provider.channel || provider.webhookUrl || "Webhook not provided"; case "resend": return provider.smtpFromEmail || "Sender email not provided"; case "gotify": return provider.serverUrl || "Server URL not provided"; case "ntfy": return provider.topic || provider.serverUrl || "Topic not provided"; case "pushover": return provider.userKey || "User key not provided"; default: return "Provider configuration not provided"; } } function formatActionSummary(actions: string[]) { const labels = normalizeNotificationActionKeys(actions).map( getNotificationActionLabel, ); if (labels.length === 0) { return "No active actions"; } if (labels.length <= 2) { return labels.join(", "); } return `${labels.slice(0, 2).join(", ")} +${labels.length - 2} others`; } function SecretHint({ configured, label, }: { configured: boolean; label: string; }) { if (!configured) return null; return (

{label} has been saved. Leave blank if you do not want to change it.

); } function StatusButton({ active, loading, onClick, }: { active: boolean; loading?: boolean; onClick: () => void; }) { return ( ); } export default function NotificationsSettingsPanel({ settings, actionLoading, savingProviderId, testNotification, onUpsertProvider, onToggleProvider, onDeleteProvider, }: NotificationsSettingsPanelProps) { const [draft, setDraft] = useState(null); const [editingId, setEditingId] = useState(null); const [currentPage, setCurrentPage] = useState(1); const providers = settings.notifications.providers; const totalItems = providers.length; const totalPages = Math.max(1, Math.ceil(totalItems / PAGE_SIZE)); const safePage = Math.min(currentPage, totalPages); const startIndex = (safePage - 1) * PAGE_SIZE; const pagedProviders = providers.slice(startIndex, startIndex + PAGE_SIZE); const startItem = totalItems === 0 ? 0 : startIndex + 1; const endItem = totalItems === 0 ? 0 : startIndex + pagedProviders.length; const openCreateModal = () => { setEditingId(null); setDraft(createProvider("email")); }; const openEditModal = (provider: NotificationProviderItem) => { setEditingId(provider.id); setDraft({ ...provider, actions: normalizeNotificationActionKeys(provider.actions), }); }; const closeModal = () => { setEditingId(null); setDraft(null); }; const saveDraft = () => { if (!draft) return; void onUpsertProvider({ ...draft, actions: normalizeNotificationActionKeys(draft.actions), }); closeModal(); }; return ( <>

Notification Providers

Manage notification providers and select which events will trigger notifications.

{totalItems === 0 ? (

Belum ada notification provider.

Tambahkan provider pertama untuk mulai mengirim alert otomatis.

) : ( <>
{pagedProviders.map((provider) => { const meta = getProviderMeta(provider.type); const Icon = meta.icon; const isSaving = savingProviderId === provider.id; const canTest = TESTABLE_PROVIDER_TYPES.includes( provider.type, ); return ( ); })}
Provider Destination Actions Actions

{provider.name}

{meta.label}

{getProviderSummary(provider)}

{meta.description}

{provider.actions.length} action activated

{formatActionSummary(provider.actions)}

setCurrentPage(Math.min(Math.max(page, 1), totalPages)) } /> )}
{draft ? (

{editingId ? "Update Notification" : "Add Notification"}

{editingId ? "Update your notification providers for multiple channels." : "Set up a new notification provider and configure which events will trigger notifications."}

Provider Type

{PROVIDER_OPTIONS.map((option) => { const Icon = option.icon; const selected = draft.type === option.type; return ( ); })}

Basic Information

Provider name setDraft((current) => current ? { ...current, name: event.target.value } : current, ) } placeholder="Contoh: Discord Infra, Telegram Ops, SMTP Finance" />
{draft.type === "email" ? ( <>
SMTP host setDraft((current) => current ? { ...current, smtpHost: event.target.value } : current, ) } placeholder="smtp.mailgun.org" />
SMTP port { const nextPort = Number.parseInt( event.target.value, 10, ); setDraft((current) => current ? { ...current, smtpPort: Number.isNaN(nextPort) ? 587 : nextPort, } : current, ); }} />

Secure SMTP connection

Activate if your SMTP server requires a secure connection (TLS/SSL).

setDraft((current) => current ? { ...current, smtpSecure: !current.smtpSecure, } : current, ) } />
SMTP username setDraft((current) => current ? { ...current, smtpUsername: event.target.value, } : current, ) } placeholder="apikey or mailbox username" />
SMTP password setDraft((current) => current ? { ...current, smtpPassword: event.target.value, smtpPasswordConfigured: current.smtpPasswordConfigured || Boolean(event.target.value.trim()), } : current, ) } />
From email setDraft((current) => current ? { ...current, smtpFromEmail: event.target.value, } : current, ) } placeholder="alerts@example.com" />
From name setDraft((current) => current ? { ...current, smtpFromName: event.target.value, } : current, ) } placeholder="Doktainer Alerts" />
) : null} {draft.type === "telegram" ? ( <>
Bot token setDraft((current) => current ? { ...current, telegramBotToken: event.target.value, telegramBotTokenConfigured: current.telegramBotTokenConfigured || Boolean(event.target.value.trim()), } : current, ) } />
Chat ID setDraft((current) => current ? { ...current, telegramChatId: event.target.value, } : current, ) } placeholder="-100xxxxxxxxxx" />
) : null} {[ "discord", "slack", "lark", "teams", "mattermost", "custom", ].includes(draft.type) ? ( <>
Webhook URL setDraft((current) => current ? { ...current, webhookUrl: event.target.value, webhookConfigured: current.webhookConfigured || Boolean(event.target.value.trim()), } : current, ) } placeholder="https://hooks.example.com/..." />
Channel / label setDraft((current) => current ? { ...current, channel: event.target.value } : current, ) } placeholder="#infra-alerts or Incident Room" />
) : null} {draft.type === "resend" ? ( <>
API key setDraft((current) => current ? { ...current, apiKey: event.target.value, apiKeyConfigured: current.apiKeyConfigured || Boolean(event.target.value.trim()), } : current, ) } />
From email setDraft((current) => current ? { ...current, smtpFromEmail: event.target.value, } : current, ) } />
From name setDraft((current) => current ? { ...current, smtpFromName: event.target.value, } : current, ) } />
) : null} {draft.type === "gotify" ? ( <>
Server URL setDraft((current) => current ? { ...current, serverUrl: event.target.value, } : current, ) } placeholder="https://gotify.example.com" />
API key setDraft((current) => current ? { ...current, apiKey: event.target.value, apiKeyConfigured: current.apiKeyConfigured || Boolean(event.target.value.trim()), } : current, ) } />
) : null} {draft.type === "ntfy" ? ( <>
Server URL setDraft((current) => current ? { ...current, serverUrl: event.target.value, } : current, ) } placeholder="https://ntfy.sh or internal endpoint" />
Topic setDraft((current) => current ? { ...current, topic: event.target.value } : current, ) } placeholder="dokta infra alerts" />
) : null} {draft.type === "pushover" ? ( <>
User key setDraft((current) => current ? { ...current, userKey: event.target.value } : current, ) } />
API token setDraft((current) => current ? { ...current, apiKey: event.target.value, apiKeyConfigured: current.apiKeyConfigured || Boolean(event.target.value.trim()), } : current, ) } />
) : null}

Alert Actions

Old actions remain available, and new actions can be enabled per provider.

setDraft((current) => current ? { ...current, enabled: !current.enabled } : current, ) } />
{NOTIFICATION_ACTION_OPTIONS.map((action) => { const checked = draft.actions.includes(action.value); return (

{action.label}

{action.description}

setDraft((current) => { if (!current) return current; return { ...current, actions: checked ? current.actions.filter( (item) => item !== action.value, ) : [...current.actions, action.value], }; }) } />
); })}
{editingId && TESTABLE_PROVIDER_TYPES.includes(draft.type) ? ( ) : null}
) : null} ); }