import { getFormProps, getInputProps, useForm } from "@conform-to/react"; import { parseWithZod } from "@conform-to/zod"; import { EnvelopeIcon, GlobeAltIcon, HashtagIcon, LockClosedIcon, XMarkIcon, } from "@heroicons/react/20/solid"; import { BellAlertIcon } from "@heroicons/react/24/solid"; import { useFetcher, useNavigate } from "@remix-run/react"; import { SlackIcon } from "@trigger.dev/companyicons"; import { Fragment, useEffect, useRef, useState } from "react"; import { z } from "zod"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { InlineCode } from "~/components/code/InlineCode"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Callout, variantClasses } from "~/components/primitives/Callout"; import { Fieldset } from "~/components/primitives/Fieldset"; import { FormError } from "~/components/primitives/FormError"; import { Header2, Header3 } from "~/components/primitives/Headers"; import { Hint } from "~/components/primitives/Hint"; import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Select, SelectItem } from "~/components/primitives/Select"; import { TextLink } from "~/components/primitives/TextLink"; import { useToast } from "~/components/primitives/Toast"; import { UnorderedList } from "~/components/primitives/UnorderedList"; import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; import { useOrganization } from "~/hooks/useOrganizations"; import type { ErrorAlertChannelData } from "~/presenters/v3/ErrorAlertChannelPresenter.server"; import { cn } from "~/utils/cn"; import { organizationSlackIntegrationPath } from "~/utils/pathBuilder"; export const ErrorAlertsFormSchema = z.object({ emails: z.preprocess((i) => { if (typeof i === "string") return i === "" ? [] : [i]; if (Array.isArray(i)) return i.filter((v) => typeof v === "string" && v !== ""); return []; }, z.string().email().array()), slackChannel: z.string().optional(), slackIntegrationId: z.string().optional(), webhooks: z.preprocess((i) => { if (typeof i === "string") return i === "" ? [] : [i]; if (Array.isArray(i)) return i.filter((v) => typeof v === "string" && v !== ""); return []; }, z.string().url().array()), }); type ConfigureErrorAlertsProps = ErrorAlertChannelData & { connectToSlackHref?: string; formAction: string; }; export function ConfigureErrorAlerts({ emails: existingEmails, webhooks: existingWebhooks, slackChannel: existingSlackChannel, slack, emailAlertsEnabled, connectToSlackHref, formAction, }: ConfigureErrorAlertsProps) { const organization = useOrganization(); const fetcher = useFetcher<{ ok?: boolean }>(); const navigate = useNavigate(); const toast = useToast(); const location = useOptimisticLocation(); const isSubmitting = fetcher.state !== "idle"; const [selectedSlackChannelValue, setSelectedSlackChannelValue] = useState( existingSlackChannel ? `${existingSlackChannel.channelId}/${existingSlackChannel.channelName}` : undefined ); const selectedSlackChannel = slack.status === "READY" ? slack.channels?.find((s) => selectedSlackChannelValue === `${s.id}/${s.name}`) : undefined; const closeHref = (() => { const params = new URLSearchParams(location.search); params.delete("alerts"); const qs = params.toString(); return qs ? `?${qs}` : location.pathname; })(); const hasHandledSuccess = useRef(false); useEffect(() => { if (fetcher.state === "idle" && fetcher.data?.ok && !hasHandledSuccess.current) { hasHandledSuccess.current = true; toast.success("Alert settings saved"); navigate(closeHref, { replace: true }); } }, [fetcher.state, fetcher.data, closeHref, navigate, toast]); const emailFieldValues = useRef( existingEmails.length > 0 ? [...existingEmails.map((e) => e.email), ""] : [""] ); const webhookFieldValues = useRef( existingWebhooks.length > 0 ? [...existingWebhooks.map((w) => w.url), ""] : [""] ); const [form, fields] = useForm>({ id: "configure-error-alerts", onValidate({ formData }) { return parseWithZod(formData, { schema: ErrorAlertsFormSchema }); }, shouldRevalidate: "onSubmit", defaultValue: { emails: emailFieldValues.current, webhooks: webhookFieldValues.current, }, }); const { emails, webhooks, slackChannel, slackIntegrationId } = fields; const emailFields = emails.getFieldList(); const webhookFields = webhooks.getFieldList(); return (
Configure alerts
Receive alerts when
  • An error is seen for the first time
  • A resolved error re-occurs
  • An ignored error re-occurs based on settings you configured
  • {/* Email section */}
    Email {emailAlertsEnabled ? ( {emailFields.map((emailField, index) => ( { emailFieldValues.current[index] = e.target.value; if ( emailFields.length === emailFieldValues.current.length && emailFieldValues.current.every((v) => v !== "") ) { form.insert({ name: emails.name }); } }} /> {emailField.errors} ))} ) : ( Email integration is not available. Please contact your organization administrator. )}
    {/* Slack section */}
    Slack {slack.status === "READY" ? ( <> {selectedSlackChannel && selectedSlackChannel.is_private && ( To receive alerts in the{" "} {selectedSlackChannel.name}{" "} channel, you need to invite the @Trigger.dev Slack Bot. Go to the channel in Slack and type:{" "} /invite @Trigger.dev. )} Manage Slack connection ) : slack.status === "NOT_CONFIGURED" ? ( connectToSlackHref ? ( Connect to Slack ) : ( Slack is not connected. Connect Slack from the{" "} Alerts page to enable Slack notifications. ) ) : slack.status === "TOKEN_REVOKED" || slack.status === "TOKEN_EXPIRED" ? ( connectToSlackHref ? (
    The Slack integration in your workspace has been revoked or has expired. Please re-connect your Slack workspace. Connect to Slack
    ) : ( The Slack integration in your workspace has been revoked or expired. Please re-connect from the{" "} Alerts page. ) ) : slack.status === "FAILED_FETCHING_CHANNELS" ? ( Failed loading channels from Slack. Please try again later. ) : ( Slack integration is not available. Please contact your organization administrator. )}
    {/* Webhook section */}
    Webhook {webhookFields.map((webhookField, index) => ( { webhookFieldValues.current[index] = e.target.value; if ( webhookFields.length === webhookFieldValues.current.length && webhookFieldValues.current.every((v) => v !== "") ) { form.insert({ name: webhooks.name }); } }} /> {webhookField.errors} ))} We'll issue POST requests to these URLs with a JSON payload.
    {form.errors}
    Cancel
    ); } function SlackChannelTitle({ name, is_private }: { name?: string; is_private?: boolean }) { return (
    {is_private ? : } {name}
    ); }