import { DiscordIcon, SlackIcon, TelegramIcon, } from "@/components/icons/notification-icons"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { AlertTriangle, Mail } from "lucide-react"; import { useEffect, useState } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; const notificationBaseSchema = z.object({ name: z.string().min(1, { message: "Name is required", }), appDeploy: z.boolean().default(false), appBuildError: z.boolean().default(false), databaseBackup: z.boolean().default(false), dokployRestart: z.boolean().default(false), dockerCleanup: z.boolean().default(false), }); export const notificationSchema = z.discriminatedUnion("type", [ z .object({ type: z.literal("slack"), webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), channel: z.string(), }) .merge(notificationBaseSchema), z .object({ type: z.literal("telegram"), botToken: z.string().min(1, { message: "Bot Token is required" }), chatId: z.string().min(1, { message: "Chat ID is required" }), }) .merge(notificationBaseSchema), z .object({ type: z.literal("discord"), webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), }) .merge(notificationBaseSchema), z .object({ type: z.literal("email"), smtpServer: z.string().min(1, { message: "SMTP Server is required" }), smtpPort: z.number().min(1, { message: "SMTP Port is required" }), username: z.string().min(1, { message: "Username is required" }), password: z.string().min(1, { message: "Password is required" }), fromAddress: z.string().min(1, { message: "From Address is required" }), toAddresses: z .array( z.string().min(1, { message: "Email is required" }).email({ message: "Email is invalid", }), ) .min(1, { message: "At least one email is required" }), }) .merge(notificationBaseSchema), ]); export const notificationsMap = { slack: { icon: , label: "Slack", }, telegram: { icon: , label: "Telegram", }, discord: { icon: , label: "Discord", }, email: { icon: , label: "Email", }, }; export type NotificationSchema = z.infer; export const AddNotification = () => { const utils = api.useUtils(); const [visible, setVisible] = useState(false); const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } = api.notification.testSlackConnection.useMutation(); const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } = api.notification.testTelegramConnection.useMutation(); const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } = api.notification.testDiscordConnection.useMutation(); const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } = api.notification.testEmailConnection.useMutation(); const slackMutation = api.notification.createSlack.useMutation(); const telegramMutation = api.notification.createTelegram.useMutation(); const discordMutation = api.notification.createDiscord.useMutation(); const emailMutation = api.notification.createEmail.useMutation(); const form = useForm({ defaultValues: { type: "slack", webhookUrl: "", channel: "", name: "", }, resolver: zodResolver(notificationSchema), }); const type = form.watch("type"); const { fields, append, remove } = useFieldArray({ control: form.control, name: "toAddresses" as never, }); useEffect(() => { if (type === "email") { append(""); } }, [type, append]); useEffect(() => { form.reset(); }, [form, form.reset, form.formState.isSubmitSuccessful]); const activeMutation = { slack: slackMutation, telegram: telegramMutation, discord: discordMutation, email: emailMutation, }; const onSubmit = async (data: NotificationSchema) => { const { appBuildError, appDeploy, dokployRestart, databaseBackup, dockerCleanup, } = data; let promise: Promise | null = null; if (data.type === "slack") { promise = slackMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, webhookUrl: data.webhookUrl, channel: data.channel, name: data.name, dockerCleanup: dockerCleanup, }); } else if (data.type === "telegram") { promise = telegramMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, botToken: data.botToken, chatId: data.chatId, name: data.name, dockerCleanup: dockerCleanup, }); } else if (data.type === "discord") { promise = discordMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, webhookUrl: data.webhookUrl, name: data.name, dockerCleanup: dockerCleanup, }); } else if (data.type === "email") { promise = emailMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, smtpServer: data.smtpServer, smtpPort: data.smtpPort, username: data.username, password: data.password, fromAddress: data.fromAddress, toAddresses: data.toAddresses, name: data.name, dockerCleanup: dockerCleanup, }); } if (promise) { await promise .then(async () => { toast.success("Notification Created"); form.reset({ type: "slack", webhookUrl: "", }); setVisible(false); await utils.notification.all.invalidate(); }) .catch(() => { toast.error("Error to create a notification"); }); } }; return ( Add Notification Create new notifications providers for multiple
( Select a provider {Object.entries(notificationsMap).map(([key, value]) => (
))}
{activeMutation[field.value].isError && (
{activeMutation[field.value].error?.message}
)}
)} />
Fill the next fields.
( Name )} /> {type === "slack" && ( <> ( Webhook URL )} /> ( Channel )} /> )} {type === "telegram" && ( <> ( Bot Token )} /> ( Chat ID )} /> )} {type === "discord" && ( <> ( Webhook URL )} /> )} {type === "email" && ( <>
( SMTP Server )} /> ( SMTP Port { const value = e.target.value; if (value) { const port = Number.parseInt(value); if (port > 0 && port < 65536) { field.onChange(port); } } }} type="number" /> )} />
( Username )} /> ( Password )} />
( From Address )} />
To Addresses {fields.map((field, index) => (
( )} />
))} {type === "email" && "toAddresses" in form.formState.errors && (
{form.formState?.errors?.toAddresses?.root?.message}
)}
)}
Select the actions.
(
App Deploy Trigger the action when a app is deployed.
)} /> (
App Build Error Trigger the action when the build fails.
)} /> (
Database Backup Trigger the action when a database backup is created.
)} /> (
Docker Cleanup Trigger the action when the docker cleanup is performed.
)} /> (
Dokploy Restart Trigger the action when a dokploy is restarted.
)} />
); };