import { useState, useEffect, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { Bell, BellOff, Check, CheckCheck, Trash2, Plus, Settings2, } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/button"; import { getAlertFirings, acknowledgeAlertFiring, acknowledgeAllAlertFirings, getAlertRules, getNotificationChannels, deleteAlertRule, deleteNotificationChannel, testNotificationChannel, type AlertFiring, type AlertRule, type NotificationChannel, } from "@/api/alerts-api"; import { AlertRuleDialog } from "./AlertRuleDialog"; import { NotificationChannelDialog } from "./NotificationChannelDialog"; type PanelTab = "firings" | "rules" | "channels"; const SEVERITY_CLASS: Record = { info: "bg-blue-500/20 text-blue-400 border-blue-500/30", warning: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30", critical: "bg-red-500/20 text-red-400 border-red-500/30", }; function timeAgo(isoStr: string): string { const ms = Date.now() - new Date(isoStr).getTime(); if (!Number.isFinite(ms)) return ""; const sec = Math.floor(ms / 1000); if (sec < 60) return `${sec}s ago`; const min = Math.floor(sec / 60); if (min < 60) return `${min}m ago`; const hr = Math.floor(min / 60); if (hr < 24) return `${hr}h ago`; return `${Math.floor(hr / 24)}d ago`; } export function AlertsPanel() { const { t } = useTranslation(); const [tab, setTab] = useState("firings"); // Firings state const [firings, setFirings] = useState([]); const [firingsLoading, setFiringsLoading] = useState(true); const [showAcknowledged, setShowAcknowledged] = useState(false); // Rules state const [rules, setRules] = useState([]); const [rulesLoading, setRulesLoading] = useState(false); const [ruleDialogOpen, setRuleDialogOpen] = useState(false); const [editRule, setEditRule] = useState(null); // Channels state const [channels, setChannels] = useState([]); const [channelsLoading, setChannelsLoading] = useState(false); const [channelDialogOpen, setChannelDialogOpen] = useState(false); const [editChannel, setEditChannel] = useState( null, ); const loadFirings = useCallback(() => { setFiringsLoading(true); getAlertFirings({ limit: 100 }) .then(setFirings) .catch(() => {}) .finally(() => setFiringsLoading(false)); }, []); const loadRules = useCallback(() => { setRulesLoading(true); getAlertRules() .then(setRules) .catch(() => {}) .finally(() => setRulesLoading(false)); }, []); const loadChannels = useCallback(() => { setChannelsLoading(true); getNotificationChannels() .then(setChannels) .catch(() => {}) .finally(() => setChannelsLoading(false)); }, []); useEffect(() => { loadFirings(); loadChannels(); }, [loadFirings, loadChannels]); useEffect(() => { if (tab === "rules") loadRules(); if (tab === "channels") loadChannels(); }, [tab, loadRules, loadChannels]); const visibleFirings = firings.filter((f) => showAcknowledged ? true : !f.acknowledged, ); async function handleAck(id: number) { try { await acknowledgeAlertFiring(id); setFirings((prev) => prev.map((f) => (f.id === id ? { ...f, acknowledged: true } : f)), ); } catch { toast.error(t("alerts.ackFailed", "Failed to acknowledge alert")); } } async function handleAckAll() { try { await acknowledgeAllAlertFirings(); setFirings((prev) => prev.map((f) => ({ ...f, acknowledged: true }))); toast.success(t("alerts.allAcknowledged", "All alerts acknowledged")); } catch { toast.error(t("alerts.ackAllFailed", "Failed to acknowledge all alerts")); } } async function handleDeleteRule(id: number) { if (!confirm(t("common.confirmDelete", "Delete this rule?"))) return; try { await deleteAlertRule(id); setRules((prev) => prev.filter((r) => r.id !== id)); toast.success(t("common.deleted", "Deleted")); } catch { toast.error(t("common.deleteFailed", "Delete failed")); } } async function handleDeleteChannel(id: number) { if (!confirm(t("common.confirmDelete", "Delete this channel?"))) return; try { await deleteNotificationChannel(id); setChannels((prev) => prev.filter((c) => c.id !== id)); toast.success(t("common.deleted", "Deleted")); } catch { toast.error(t("common.deleteFailed", "Delete failed")); } } async function handleTestChannel(id: number) { try { await testNotificationChannel(id); toast.success(t("alerts.testSent", "Test notification sent")); } catch (err) { const detail = err instanceof Error ? err.message : undefined; toast.error( detail ? t( "alerts.testFailedWithReason", "Test notification failed: {{reason}}", { reason: detail }, ) : t("alerts.testFailed", "Test notification failed"), ); } } const unreadCount = firings.filter((f) => !f.acknowledged).length; return (
{/* Tab strip */}
{(["firings", "rules", "channels"] as PanelTab[]).map((t_) => ( ))}
{/* Firings tab */} {tab === "firings" && (
{unreadCount > 0 && ( )}
{firingsLoading && (
{t("common.loading", "Loading...")}
)} {!firingsLoading && visibleFirings.length === 0 && (
{t("alerts.noFirings", "No unacknowledged alerts")}
)} {!firingsLoading && visibleFirings.map((firing) => (
{firing.severity} {firing.hostName} {!firing.acknowledged && ( )}
{firing.message}
{firing.ruleName && {firing.ruleName}} {timeAgo(firing.firedAt)}
))}
)} {/* Rules tab */} {tab === "rules" && (
{t("alerts.rulesDesc", "Alert rules trigger notifications")}
{rulesLoading && (
{t("common.loading", "Loading...")}
)} {!rulesLoading && rules.length === 0 && (
{t("alerts.noRules", "No alert rules configured")}
)} {!rulesLoading && rules.map((rule) => (
{rule.name}
{(rule.triggerType || "").replace(/_/g, " ")}
))}
)} {/* Channels tab */} {tab === "channels" && (
{t("alerts.channelsDesc", "Where alert notifications are sent")}
{channelsLoading && (
{t("common.loading", "Loading...")}
)} {!channelsLoading && channels.length === 0 && (
{t( "alerts.noChannels", "No notification channels configured", )}
)} {!channelsLoading && channels.map((channel) => (
{channel.name}
{channel.type}
))}
)} { setRuleDialogOpen(false); loadRules(); }} /> { setChannelDialogOpen(false); loadChannels(); }} />
); }