"use client"; import { useState, useMemo } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import Link from "next/link"; import { BellPlus, BellRing } from "lucide-react"; import { SettingsSection, SettingsSectionHeader, SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody } from "@app/components/Settings"; import UptimeBar from "@app/components/UptimeBar"; import { Button } from "@app/components/ui/button"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import { Input } from "@app/components/ui/input"; import { Label } from "@app/components/ui/label"; import { TagInput, type Tag } from "@app/components/tags/tag-input"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { orgQueries } from "@app/lib/queries"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { useTranslations } from "next-intl"; interface UptimeAlertSectionProps { orgId: string; siteId?: number; startingName?: string; resourceId?: number; days?: number; } export default function UptimeAlertSection({ orgId, siteId, startingName, resourceId, days = 90 }: UptimeAlertSectionProps) { const t = useTranslations(); const api = createApiClient(useEnvContext()); const queryClient = useQueryClient(); const { isPaidUser } = usePaidStatus(); const isPaid = isPaidUser(tierMatrix.alertingRules); const [open, setOpen] = useState(false); const [name, setName] = useState( `${siteId ? t("site") : t("resource")} ${startingName} ${t("alertLabel")}` ); const [userTags, setUserTags] = useState([]); const [roleTags, setRoleTags] = useState([]); const [emailTags, setEmailTags] = useState([]); const [activeUserTagIndex, setActiveUserTagIndex] = useState( null ); const [activeRoleTagIndex, setActiveRoleTagIndex] = useState( null ); const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< number | null >(null); const [loading, setLoading] = useState(false); const { data: alertRules, isLoading: alertRulesLoading } = useQuery({ ...orgQueries.alertRulesForSource({ orgId, siteId, resourceId }), enabled: isPaid }); const { data: orgUsers = [] } = useQuery(orgQueries.users({ orgId })); const { data: orgRoles = [] } = 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 hasRules = (alertRules?.length ?? 0) > 0; async function handleSubmit() { if ( userTags.length === 0 && roleTags.length === 0 && emailTags.length === 0 ) { toast({ variant: "destructive", title: t("uptimeAlertNoRecipients"), description: t("uptimeAlertNoRecipientsDescription") }); return; } setLoading(true); try { await api.put(`/org/${orgId}/alert-rule`, { name, eventType: siteId ? "site_toggle" : "resource_toggle", enabled: true, cooldownSeconds: 0, // default to 0 here because we dont want the extra confusion siteIds: siteId ? [siteId] : [], healthCheckIds: [], resourceIds: resourceId ? [resourceId] : [], userIds: userTags.map((tag) => tag.id), roleIds: roleTags.map((tag) => Number(tag.id)), emails: emailTags.map((tag) => tag.text), webhookActions: [] }); toast({ title: t("uptimeAlertCreated"), description: t("uptimeAlertCreatedDescription") }); setOpen(false); setName(t("uptimeSectionTitle")); setUserTags([]); setRoleTags([]); setEmailTags([]); queryClient.invalidateQueries({ queryKey: orgQueries.alertRulesForSource({ orgId, siteId, resourceId }).queryKey }); } catch (e) { toast({ variant: "destructive", title: t("uptimeAlertCreateFailed"), description: formatAxiosError(e, t("errorOccurred")) }); } setLoading(false); } const rulesListSearch = new URLSearchParams(); if (siteId != null) rulesListSearch.set("siteId", String(siteId)); if (resourceId != null) rulesListSearch.set("resourceId", String(resourceId)); const rulesListHref = `/${orgId}/settings/alerting/rules${ rulesListSearch.toString() ? `?${rulesListSearch}` : "" }`; const alertButton = alertRulesLoading ? ( ) : hasRules ? ( ) : ( ); return ( <>
{t("uptimeSectionTitle")} {t("uptimeSectionDescription", { days })}
{alertButton}
{t("uptimeCreateEmailAlert")} {siteId ? t("uptimeAlertDescriptionSite") : t("uptimeAlertDescriptionResource")}
setName(e.target.value)} placeholder={t("uptimeAlertNamePlaceholder")} />
{ const next = typeof newTags === "function" ? newTags(userTags) : newTags; setUserTags(next as Tag[]); }} enableAutocomplete autocompleteOptions={allUsers} restrictTagsToAutocompleteOptions allowDuplicates={false} sortTags />
{ const next = typeof newTags === "function" ? newTags(roleTags) : newTags; setRoleTags(next as Tag[]); }} enableAutocomplete autocompleteOptions={allRoles} restrictTagsToAutocompleteOptions allowDuplicates={false} sortTags />
{ const next = typeof newTags === "function" ? newTags(emailTags) : newTags; setEmailTags(next as Tag[]); }} allowDuplicates={false} sortTags validateTag={(tag) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(tag) } delimiterList={[",", "Enter"]} />
); }