From e64ee98d99adf9c2a54b170e549fb4eb2092a614 Mon Sep 17 00:00:00 2001 From: ChristoferMendes Date: Fri, 26 Sep 2025 17:13:33 -0300 Subject: [PATCH] feat(notifications): implement custom notification form fields and connection testing for custom notifications --- .../notifications/handle-notifications.tsx | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index 4e4171bee..8f4bdc2d1 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -39,6 +39,7 @@ 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 { KeyValueInput } from "./key-value-input"; const notificationBaseSchema = z.object({ name: z.string().min(1, { @@ -110,6 +111,13 @@ export const notificationSchema = z.discriminatedUnion("type", [ priority: z.number().min(1).max(5).default(3), }) .merge(notificationBaseSchema), + z + .object({ + type: z.literal("custom"), + endpoint: z.string().min(1, { message: "Endpoint URL is required" }), + headers: z.string().optional(), + }) + .merge(notificationBaseSchema), ]); export const notificationsMap = { @@ -137,6 +145,10 @@ export const notificationsMap = { icon: , label: "ntfy", }, + custom: { + icon: , + label: "Custom", + }, }; export type NotificationSchema = z.infer; @@ -170,6 +182,8 @@ export const HandleNotifications = ({ notificationId }: Props) => { api.notification.testGotifyConnection.useMutation(); const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } = api.notification.testNtfyConnection.useMutation(); + const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } = + api.notification.testCustomConnection.useMutation(); const slackMutation = notificationId ? api.notification.updateSlack.useMutation() : api.notification.createSlack.useMutation(); @@ -188,6 +202,9 @@ export const HandleNotifications = ({ notificationId }: Props) => { const ntfyMutation = notificationId ? api.notification.updateNtfy.useMutation() : api.notification.createNtfy.useMutation(); + const customMutation = notificationId + ? api.notification.updateCustom.useMutation() + : api.notification.createCustom.useMutation(); const form = useForm({ defaultValues: { @@ -298,6 +315,19 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, }); + } else if (notification.notificationType === "custom") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + type: notification.notificationType, + endpoint: notification.custom?.endpoint || "", + headers: notification.custom?.headers || "", + name: notification.name, + dockerCleanup: notification.dockerCleanup, + serverThreshold: notification.serverThreshold, + }); } } else { form.reset(); @@ -311,6 +341,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { email: emailMutation, gotify: gotifyMutation, ntfy: ntfyMutation, + custom: customMutation, }; const onSubmit = async (data: NotificationSchema) => { @@ -414,6 +445,20 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", ntfyId: notification?.ntfyId || "", }); + } else if (data.type === "custom") { + promise = customMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + endpoint: data.endpoint, + headers: data.headers || "", + name: data.name, + dockerCleanup: dockerCleanup, + serverThreshold: serverThreshold, + notificationId: notificationId || "", + customId: notification?.customId || "", + }); } if (promise) { @@ -1000,6 +1045,49 @@ export const HandleNotifications = ({ notificationId }: Props) => { /> )} + + {type === "custom" && ( +
+ ( + + Webhook URL + + + + + The URL where POST requests will be sent with + notification data. + + + + )} + /> + + ( + + + + + + + )} + /> +
+ )}
@@ -1150,7 +1238,8 @@ export const HandleNotifications = ({ notificationId }: Props) => { isLoadingDiscord || isLoadingEmail || isLoadingGotify || - isLoadingNtfy + isLoadingNtfy || + isLoadingCustom } variant="secondary" onClick={async () => { @@ -1194,6 +1283,11 @@ export const HandleNotifications = ({ notificationId }: Props) => { accessToken: form.getValues("accessToken"), priority: form.getValues("priority"), }); + } else if (type === "custom") { + await testCustomConnection({ + endpoint: form.getValues("endpoint") as string, + headers: form.getValues("headers") as string | undefined, + }); } toast.success("Connection Success"); } catch {