"use client"; import { ContactSalesBanner } from "@app/components/ContactSalesBanner"; import { StrategySelect } from "@app/components/StrategySelect"; import { TagInput, type Tag } from "@app/components/tags/tag-input"; import { Button } from "@app/components/ui/button"; import { Checkbox } from "@app/components/ui/checkbox"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@app/components/ui/command"; import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { Label } from "@app/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@app/components/ui/select"; import { type AlertRuleFormAction, type AlertRuleFormValues } from "@app/lib/alertRuleForm"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { orgQueries } from "@app/lib/queries"; import { useQuery } from "@tanstack/react-query"; import { Bell, ChevronsUpDown, Globe, Plus, Trash2 } from "lucide-react"; import { useTranslations } from "next-intl"; import { useEffect, useMemo, useRef, useState } from "react"; import type { Control, UseFormReturn } from "react-hook-form"; import { useFormContext, useWatch } from "react-hook-form"; import { useDebounce } from "use-debounce"; import { RolesSelector } from "../roles-selector"; import { UsersSelector } from "../users-selector"; export function AddActionPanel({ onAdd }: { onAdd: (type: AlertRuleFormAction["type"]) => void; }) { const t = useTranslations(); const EXTERNAL_INTEGRATIONS = [ { id: "pagerduty", name: "PagerDuty", logo: "/third-party/pgd.png", description: "Send alerts to PagerDuty for incident management", descriptionKey: t("alertingExternalPagerDutyDescription") }, { id: "opsgenie", name: "Opsgenie", logo: "/third-party/opsgenie.png", description: "Route alerts to Opsgenie for on-call management", descriptionKey: t("alertingExternalOpsgenieDescription") }, { id: "servicenow", name: "ServiceNow", logo: "/third-party/servicenow.png", description: "Create ServiceNow incidents from alert events", descriptionKey: t("alertingExternalServiceNowDescription") }, { id: "incidentio", name: "Incident.io", logo: "/third-party/incidentio.png", description: "Trigger Incident.io workflows from alert events", descriptionKey: t("alertingExternalIncidentIoDescription") } ] as const; const EXTERNAL_IDS = EXTERNAL_INTEGRATIONS.map((i) => i.id); const [selected, setSelected] = useState("notify"); const isPremiumSelected = selected !== null && EXTERNAL_IDS.includes(selected as any); const isBuiltInSelected = selected !== null && !isPremiumSelected; const actionTypeOptions = [ { id: "notify", title: t("alertingActionNotify"), description: t("alertingActionNotifyDescription"), icon: }, { id: "webhook", title: t("alertingActionWebhook"), description: t("alertingActionWebhookDescription"), icon: }, ...EXTERNAL_INTEGRATIONS.map((integration) => ({ id: integration.id, title: integration.name, description: integration.description, icon: ( {integration.name} ) })) ]; const handleAdd = () => { if (!isBuiltInSelected) return; onAdd(selected as AlertRuleFormAction["type"]); setSelected(null); }; return (
setSelected(v)} /> {isPremiumSelected && } {!isPremiumSelected && ( )}
); } function SiteMultiSelect({ orgId, value, onChange }: { orgId: string; value: number[]; onChange: (v: number[]) => void; }) { const t = useTranslations(); const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const [debounced] = useDebounce(q, 150); const { data: sites = [] } = useQuery( orgQueries.sites({ orgId, query: debounced, perPage: 500 }) ); const toggle = (id: number) => { if (value.includes(id)) { onChange(value.filter((x) => x !== id)); } else { onChange([...value, id]); } }; const summary = value.length === 0 ? t("alertingSelectSites") : t("alertingSitesSelected", { count: value.length }); return ( {t("siteNotFound")} {sites.map((s) => ( toggle(s.siteId)} className="cursor-pointer" > {s.name} ))} ); } function HealthCheckMultiSelect({ orgId, value, onChange }: { orgId: string; value: number[]; onChange: (v: number[]) => void; }) { const t = useTranslations(); const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const [debounced] = useDebounce(q, 150); const { data: healthChecks = [] } = useQuery( orgQueries.healthChecks({ orgId }) ); const shown = useMemo(() => { const query = debounced.trim().toLowerCase(); const base = query ? healthChecks.filter((hc) => hc.name.toLowerCase().includes(query)) : healthChecks; // Always keep already-selected items visible even if they fall outside the search if (query && value.length > 0) { const selectedNotInBase = healthChecks.filter( (hc) => value.includes(hc.targetHealthCheckId) && !base.some( (b) => b.targetHealthCheckId === hc.targetHealthCheckId ) ); return [...selectedNotInBase, ...base]; } return base; }, [healthChecks, debounced, value]); const toggle = (id: number) => { if (value.includes(id)) { onChange(value.filter((x) => x !== id)); } else { onChange([...value, id]); } }; const summary = value.length === 0 ? t("alertingSelectHealthChecks") : t("alertingHealthChecksSelected", { count: value.length }); return ( {t("alertingHealthChecksEmpty")} {shown.map((hc) => ( toggle(hc.targetHealthCheckId) } className="cursor-pointer" > {hc.name} ))} ); } function ResourceMultiSelect({ orgId, value, onChange }: { orgId: string; value: number[]; onChange: (v: number[]) => void; }) { const t = useTranslations(); const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const [debounced] = useDebounce(q, 150); const { data: resources = [] } = useQuery( orgQueries.resources({ orgId, query: debounced, perPage: 10 }) ); const shown = useMemo(() => { return resources; }, [resources]); const toggle = (id: number) => { if (value.includes(id)) { onChange(value.filter((x) => x !== id)); } else { onChange([...value, id]); } }; const summary = value.length === 0 ? t("alertingSelectResources") : t("alertingResourcesSelected", { count: value.length }); return ( {t("alertingResourcesEmpty")} {shown.map((r) => ( toggle(r.resourceId)} className="cursor-pointer" > {r.name} ))} ); } export function ActionBlock({ orgId, index, control, form, onRemove, onUpdate, canRemove }: { orgId: string; index: number; control: Control; form: UseFormReturn; onRemove: () => void; onUpdate: (val: AlertRuleFormAction) => void; canRemove: boolean; }) { const t = useTranslations(); const type = useWatch({ control, name: `actions.${index}.type` }); const typeHeader = type === "notify" ? (
{t("alertingActionNotify")}
) : (
{t("alertingActionWebhook")}
); return (
{canRemove && ( )} {typeHeader} {type === "notify" && ( )} {type === "webhook" && ( )}
); } function NotifyActionFields({ orgId, index, control, form }: { orgId: string; index: number; control: Control; form: UseFormReturn; }) { const t = useTranslations(); const [emailActiveIdx, setEmailActiveIdx] = useState(null); const { data: orgUsers = [], isLoading: isLoadingUsers } = useQuery( orgQueries.users({ orgId }) ); const { data: orgRoles = [], isLoading: isLoadingRoles } = useQuery( orgQueries.roles({ orgId }) ); const allUsers = useMemo( () => orgUsers.map((u) => ({ id: String(u.id), text: getUserDisplayName({ email: u.email, name: u.name, username: u.username }) })), [orgUsers] ); const allRoles = useMemo( () => orgRoles.map((r) => ({ id: String(r.roleId), text: r.name })), [orgRoles] ); const hasResolvedTagsRef = useRef(false); useEffect(() => { if (isLoadingUsers || isLoadingRoles) return; if (hasResolvedTagsRef.current) return; const currentUserTags = form.getValues( `actions.${index}.userTags` ) as Tag[]; const currentRoleTags = form.getValues( `actions.${index}.roleTags` ) as Tag[]; const resolvedUserTags = currentUserTags.map((tag) => { const match = allUsers.find((u) => u.id === tag.id); return match ? { id: tag.id, text: match.text } : tag; }); const resolvedRoleTags = currentRoleTags.map((tag) => { const match = allRoles.find((r) => r.id === tag.id); return match ? { id: tag.id, text: match.text } : tag; }); const userTagsNeedUpdate = resolvedUserTags.some( (t, i) => t.text !== currentUserTags[i]?.text ); const roleTagsNeedUpdate = resolvedRoleTags.some( (t, i) => t.text !== currentRoleTags[i]?.text ); if (userTagsNeedUpdate) { form.setValue(`actions.${index}.userTags`, resolvedUserTags, { shouldDirty: false }); } if (roleTagsNeedUpdate) { form.setValue(`actions.${index}.roleTags`, resolvedRoleTags, { shouldDirty: false }); } hasResolvedTagsRef.current = true; }, [isLoadingUsers, isLoadingRoles, allUsers, allRoles]); const emailTags = (useWatch({ control, name: `actions.${index}.emailTags` }) ?? []) as Tag[]; return (
( {t("alertingNotifyUsers")} { form.setValue( `actions.${index}.userTags`, newUsers as [Tag, ...Tag[]], { shouldDirty: true } ); }} /> )} /> ( {t("alertingNotifyRoles")} { form.setValue( `actions.${index}.roleTags`, newUsers as [Tag, ...Tag[]], { shouldDirty: true } ); }} /> )} /> ( {t("alertingNotifyEmails")} { const next = typeof updater === "function" ? updater(emailTags) : updater; form.setValue( `actions.${index}.emailTags`, next as Tag[], { shouldDirty: true } ); }} activeTagIndex={emailActiveIdx} setActiveTagIndex={setEmailActiveIdx} placeholder={t("alertingEmailPlaceholder")} size="sm" allowDuplicates={false} sortTags={true} validateTag={(tag) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(tag) } delimiterList={[",", "Enter"]} /> )} />
); } function WebhookActionFields({ index, control, form }: { index: number; control: Control; form: UseFormReturn; }) { const t = useTranslations(); return (
( {t("webhookUrlLabel")} )} /> ( {t("alertingWebhookMethod")} )} /> {/* Authentication */}

{t("httpDestAuthDescription")}

( {/* None */}

{t( "httpDestAuthNoneDescription" )}

{/* Bearer */}

{t( "httpDestAuthBearerDescription" )}

{field.value === "bearer" && ( ( )} /> )}
{/* Basic */}

{t( "httpDestAuthBasicDescription" )}

{field.value === "basic" && ( ( )} /> )}
{/* Custom */}

{t( "httpDestAuthCustomDescription" )}

{field.value === "custom" && (
( )} /> ( )} />
)}
)} />
); } function WebhookHeadersField({ index, control, form }: { index: number; control: Control; form: UseFormReturn; }) { const t = useTranslations(); const headers = useWatch({ control, name: `actions.${index}.headers` as const }) ?? []; return (
{t("alertingWebhookHeaders")} {headers.map((_, hi) => (
( )} /> ( )} />
))}
); } export function AlertRuleSourceFields({ orgId, control }: { orgId: string; control: Control; }) { const t = useTranslations(); const { setValue, getValues } = useFormContext(); const sourceType = useWatch({ control, name: "sourceType" }); const allSites = useWatch({ control, name: "allSites" }); const allHealthChecks = useWatch({ control, name: "allHealthChecks" }); const allResources = useWatch({ control, name: "allResources" }); const siteStrategyOptions = useMemo( () => [ { id: "all" as const, title: t("alertingAllSites"), description: t("alertingAllSitesDescription") }, { id: "specific" as const, title: t("alertingSpecificSites"), description: t("alertingSpecificSitesDescription") } ], [t] ); const healthCheckStrategyOptions = useMemo( () => [ { id: "all" as const, title: t("alertingAllHealthChecks"), description: t("alertingAllHealthChecksDescription") }, { id: "specific" as const, title: t("alertingSpecificHealthChecks"), description: t("alertingSpecificHealthChecksDescription") } ], [t] ); const resourceStrategyOptions = useMemo( () => [ { id: "all" as const, title: t("alertingAllResources"), description: t("alertingAllResourcesDescription") }, { id: "specific" as const, title: t("alertingSpecificResources"), description: t("alertingSpecificResourcesDescription") } ], [t] ); return (
( {t("alertingSourceType")} )} /> {sourceType === "site" ? ( <> ( { field.onChange(v === "all"); if (v === "all") { setValue("siteIds", []); } }} cols={2} /> )} /> {!allSites && ( ( {t("alertingPickSites")} )} /> )} ) : sourceType === "resource" ? ( <> ( { field.onChange(v === "all"); if (v === "all") { setValue("resourceIds", []); } }} cols={2} /> )} /> {!allResources && ( ( {t("alertingPickResources")} )} /> )} ) : ( <> ( { field.onChange(v === "all"); if (v === "all") { setValue("healthCheckIds", []); } }} cols={2} /> )} /> {!allHealthChecks && ( ( {t("alertingPickHealthChecks")} )} /> )} )}
); } export function AlertRuleTriggerFields({ control }: { control: Control; }) { const t = useTranslations(); const sourceType = useWatch({ control, name: "sourceType" }); return ( ( {t("alertingTrigger")} )} /> ); }