diff --git a/apps/dashboard/app/(main)/monitors/[id]/page.tsx b/apps/dashboard/app/(main)/monitors/[id]/page.tsx index 13404025c..5de3e1713 100644 --- a/apps/dashboard/app/(main)/monitors/[id]/page.tsx +++ b/apps/dashboard/app/(main)/monitors/[id]/page.tsx @@ -3,6 +3,7 @@ import { ArrowClockwiseIcon, ArrowLeftIcon, + BellIcon, GlobeIcon, HeartbeatIcon, PauseIcon, @@ -10,7 +11,7 @@ import { PlayIcon, TrashIcon, } from "@phosphor-icons/react"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; import { useMemo, useState } from "react"; @@ -35,8 +36,107 @@ import { orpc } from "@/lib/orpc"; import { fromNow, localDayjs } from "@/lib/time"; import { RecentActivity } from "../../websites/[id]/pulse/_components/recent-activity"; import { UptimeHeatmap } from "../../websites/[id]/pulse/_components/uptime-heatmap"; +import { Switch } from "@/components/ui/switch"; import { PageHeader } from "../_components/page-header"; +interface AlarmSummary { + id: string; + name: string; + enabled: boolean; + notificationChannels: string[]; + triggerType: string; +} + +function MonitorAlarms({ + organizationId, + websiteId, +}: { + organizationId: string; + websiteId: string | null; +}) { + const queryClient = useQueryClient(); + + const { data: alarmsList } = useQuery({ + ...orpc.alarms.list.queryOptions({ + input: { + organizationId, + websiteId: websiteId ?? undefined, + triggerType: "uptime", + }, + }), + }); + + const updateMutation = useMutation({ + ...orpc.alarms.update.mutationOptions(), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: orpc.alarms.list.key({ + input: { organizationId, websiteId: websiteId ?? undefined, triggerType: "uptime" }, + }), + }); + }, + }); + + const alarms = (alarmsList ?? []) as AlarmSummary[]; + + if (alarms.length === 0) { + return ( +
+
+ + + No uptime alarms configured.{" "} + + Create one in Settings + + +
+
+ ); + } + + return ( +
+
+ + Uptime Alarms +
+
+ {alarms.map((alarm) => ( +
+
+ {alarm.name} +
+ {alarm.notificationChannels.map((ch) => ( + + {ch} + + ))} +
+
+ + updateMutation.mutate({ + id: alarm.id, + enabled, + }) + } + /> +
+ ))} +
+
+ ); +} + const granularityLabels: Record = { minute: "Every minute", five_minutes: "Every 5 minutes", @@ -398,6 +498,11 @@ export default function MonitorDetailsPage() { + +
| null; + triggerConditions: Record | null; + websiteId: string | null; + organizationId: string | null; +} + +interface AlarmDialogProps { + alarm: Alarm | null; + isOpen: boolean; + onCloseAction: () => void; +} + +const TRIGGER_TYPES = [ + { value: "uptime", label: "Uptime" }, + { value: "traffic_spike", label: "Traffic Spike" }, + { value: "error_rate", label: "Error Rate" }, + { value: "goal", label: "Goal" }, + { value: "custom", label: "Custom" }, +] as const; + +const CHANNELS = [ + { value: "slack", label: "Slack" }, + { value: "discord", label: "Discord" }, + { value: "email", label: "Email" }, + { value: "webhook", label: "Webhook" }, +] as const; + +type TriggerType = (typeof TRIGGER_TYPES)[number]["value"]; +type Channel = (typeof CHANNELS)[number]["value"]; + +export function AlarmDialog({ alarm, isOpen, onCloseAction }: AlarmDialogProps) { + const queryClient = useQueryClient(); + const activeOrganization = useAtomValue(activeOrganizationAtom); + const isEditing = Boolean(alarm); + + const [name, setName] = useState(alarm?.name ?? ""); + const [description, setDescription] = useState(alarm?.description ?? ""); + const [enabled, setEnabled] = useState(alarm?.enabled ?? true); + const [triggerType, setTriggerType] = useState( + (alarm?.triggerType as TriggerType) ?? "uptime" + ); + const [channels, setChannels] = useState( + (alarm?.notificationChannels as Channel[]) ?? [] + ); + const [slackUrl, setSlackUrl] = useState(alarm?.slackWebhookUrl ?? ""); + const [discordUrl, setDiscordUrl] = useState(alarm?.discordWebhookUrl ?? ""); + const [emailAddresses, setEmailAddresses] = useState( + alarm?.emailAddresses?.join(", ") ?? "" + ); + const [webhookUrl, setWebhookUrl] = useState(alarm?.webhookUrl ?? ""); + + // Reset form state when alarm prop changes (fixes stale data when editing different alarms) + useEffect(() => { + setName(alarm?.name ?? ""); + setDescription(alarm?.description ?? ""); + setEnabled(alarm?.enabled ?? true); + setTriggerType((alarm?.triggerType as TriggerType) ?? "uptime"); + setChannels((alarm?.notificationChannels as Channel[]) ?? []); + setSlackUrl(alarm?.slackWebhookUrl ?? ""); + setDiscordUrl(alarm?.discordWebhookUrl ?? ""); + setEmailAddresses(alarm?.emailAddresses?.join(", ") ?? ""); + setWebhookUrl(alarm?.webhookUrl ?? ""); + }, [alarm]); + + const createMutation = useMutation({ + ...orpc.alarms.create.mutationOptions(), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: orpc.alarms.list.key({ input: {} }), + }); + toast.success("Alarm created"); + onCloseAction(); + }, + onError: () => { + toast.error("Failed to create alarm"); + }, + }); + + const updateMutation = useMutation({ + ...orpc.alarms.update.mutationOptions(), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: orpc.alarms.list.key({ input: {} }), + }); + toast.success("Alarm updated"); + onCloseAction(); + }, + onError: () => { + toast.error("Failed to update alarm"); + }, + }); + + const toggleChannel = (channel: Channel) => { + setChannels((prev) => + prev.includes(channel) + ? prev.filter((c) => c !== channel) + : [...prev, channel] + ); + }; + + const handleSubmit = () => { + if (!name.trim()) { + toast.error("Name is required"); + return; + } + + if (channels.length === 0) { + toast.error("Select at least one notification channel"); + return; + } + + if (!activeOrganization?.id && !isEditing) { + toast.error("No active workspace found"); + return; + } + + const emails = emailAddresses + .split(",") + .map((e) => e.trim()) + .filter(Boolean); + + if (isEditing && alarm) { + updateMutation.mutate({ + id: alarm.id, + name: name.trim(), + description: description.trim() || undefined, + enabled, + notificationChannels: channels, + triggerType, + slackWebhookUrl: slackUrl.trim() || null, + discordWebhookUrl: discordUrl.trim() || null, + emailAddresses: emails.length > 0 ? emails : null, + webhookUrl: webhookUrl.trim() || null, + }); + } else { + createMutation.mutate({ + organizationId: activeOrganization?.id ?? "", + name: name.trim(), + description: description.trim() || undefined, + enabled, + notificationChannels: channels, + triggerType, + slackWebhookUrl: slackUrl.trim() || undefined, + discordWebhookUrl: discordUrl.trim() || undefined, + emailAddresses: emails.length > 0 ? emails : undefined, + webhookUrl: webhookUrl.trim() || undefined, + }); + } + }; + + const isLoading = createMutation.isPending || updateMutation.isPending; + + return ( + !open && onCloseAction()} open={isOpen}> + + +
+
+ +
+
+ + {isEditing ? "Edit Alarm" : "Create Alarm"} + + + {isEditing + ? `Editing ${alarm?.name}` + : "Set up a notification alarm"} + +
+
+
+ +
+
+ + setName(e.target.value)} + placeholder="Site Down Alert" + value={name} + /> +
+ +
+ +