11import { zodResolver } from "@hookform/resolvers/zod" ;
2- import { AlertTriangle , Mail , PenBoxIcon , PlusIcon } from "lucide-react" ;
2+ import {
3+ AlertTriangle ,
4+ Mail ,
5+ PenBoxIcon ,
6+ PlusIcon ,
7+ Trash2 ,
8+ } from "lucide-react" ;
39import { useEffect , useState } from "react" ;
410import { useFieldArray , useForm } from "react-hook-form" ;
511import { toast } from "sonner" ;
@@ -108,6 +114,21 @@ export const notificationSchema = z.discriminatedUnion("type", [
108114 priority : z . number ( ) . min ( 1 ) . max ( 5 ) . default ( 3 ) ,
109115 } )
110116 . merge ( notificationBaseSchema ) ,
117+ z
118+ . object ( {
119+ type : z . literal ( "custom" ) ,
120+ endpoint : z . string ( ) . min ( 1 , { message : "Endpoint URL is required" } ) ,
121+ headers : z
122+ . array (
123+ z . object ( {
124+ key : z . string ( ) ,
125+ value : z . string ( ) ,
126+ } ) ,
127+ )
128+ . optional ( )
129+ . default ( [ ] ) ,
130+ } )
131+ . merge ( notificationBaseSchema ) ,
111132 z
112133 . object ( {
113134 type : z . literal ( "lark" ) ,
@@ -145,6 +166,10 @@ export const notificationsMap = {
145166 icon : < NtfyIcon /> ,
146167 label : "ntfy" ,
147168 } ,
169+ custom : {
170+ icon : < PenBoxIcon size = { 29 } className = "text-muted-foreground" /> ,
171+ label : "Custom" ,
172+ } ,
148173} ;
149174
150175export type NotificationSchema = z . infer < typeof notificationSchema > ;
@@ -180,6 +205,13 @@ export const HandleNotifications = ({ notificationId }: Props) => {
180205 api . notification . testNtfyConnection . useMutation ( ) ;
181206 const { mutateAsync : testLarkConnection , isLoading : isLoadingLark } =
182207 api . notification . testLarkConnection . useMutation ( ) ;
208+
209+ const { mutateAsync : testCustomConnection , isLoading : isLoadingCustom } =
210+ api . notification . testCustomConnection . useMutation ( ) ;
211+
212+ const customMutation = notificationId
213+ ? api . notification . updateCustom . useMutation ( )
214+ : api . notification . createCustom . useMutation ( ) ;
183215 const slackMutation = notificationId
184216 ? api . notification . updateSlack . useMutation ( )
185217 : api . notification . createSlack . useMutation ( ) ;
@@ -218,6 +250,15 @@ export const HandleNotifications = ({ notificationId }: Props) => {
218250 name : "toAddresses" as never ,
219251 } ) ;
220252
253+ const {
254+ fields : headerFields ,
255+ append : appendHeader ,
256+ remove : removeHeader ,
257+ } = useFieldArray ( {
258+ control : form . control ,
259+ name : "headers" as never ,
260+ } ) ;
261+
221262 useEffect ( ( ) => {
222263 if ( type === "email" && fields . length === 0 ) {
223264 append ( "" ) ;
@@ -330,6 +371,26 @@ export const HandleNotifications = ({ notificationId }: Props) => {
330371 dockerCleanup : notification . dockerCleanup ,
331372 serverThreshold : notification . serverThreshold ,
332373 } ) ;
374+ } else if ( notification . notificationType === "custom" ) {
375+ form . reset ( {
376+ appBuildError : notification . appBuildError ,
377+ appDeploy : notification . appDeploy ,
378+ dokployRestart : notification . dokployRestart ,
379+ databaseBackup : notification . databaseBackup ,
380+ type : notification . notificationType ,
381+ endpoint : notification . custom ?. endpoint || "" ,
382+ headers : notification . custom ?. headers
383+ ? Object . entries ( notification . custom . headers ) . map (
384+ ( [ key , value ] ) => ( {
385+ key,
386+ value,
387+ } ) ,
388+ )
389+ : [ ] ,
390+ name : notification . name ,
391+ dockerCleanup : notification . dockerCleanup ,
392+ serverThreshold : notification . serverThreshold ,
393+ } ) ;
333394 }
334395 } else {
335396 form . reset ( ) ;
@@ -344,6 +405,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
344405 gotify : gotifyMutation ,
345406 ntfy : ntfyMutation ,
346407 lark : larkMutation ,
408+ custom : customMutation ,
347409 } ;
348410
349411 const onSubmit = async ( data : NotificationSchema ) => {
@@ -467,6 +529,32 @@ export const HandleNotifications = ({ notificationId }: Props) => {
467529 larkId : notification ?. larkId || "" ,
468530 serverThreshold : serverThreshold ,
469531 } ) ;
532+ } else if ( data . type === "custom" ) {
533+ // Convert headers array to object
534+ const headersRecord =
535+ data . headers && data . headers . length > 0
536+ ? data . headers . reduce (
537+ ( acc , { key, value } ) => {
538+ if ( key . trim ( ) ) acc [ key ] = value ;
539+ return acc ;
540+ } ,
541+ { } as Record < string , string > ,
542+ )
543+ : undefined ;
544+
545+ promise = customMutation . mutateAsync ( {
546+ appBuildError : appBuildError ,
547+ appDeploy : appDeploy ,
548+ dokployRestart : dokployRestart ,
549+ databaseBackup : databaseBackup ,
550+ endpoint : data . endpoint ,
551+ headers : headersRecord ,
552+ name : data . name ,
553+ dockerCleanup : dockerCleanup ,
554+ serverThreshold : serverThreshold ,
555+ notificationId : notificationId || "" ,
556+ customId : notification ?. customId || "" ,
557+ } ) ;
470558 }
471559
472560 if ( promise ) {
@@ -1057,7 +1145,92 @@ export const HandleNotifications = ({ notificationId }: Props) => {
10571145 />
10581146 </ >
10591147 ) }
1148+ { type === "custom" && (
1149+ < div className = "space-y-4" >
1150+ < FormField
1151+ control = { form . control }
1152+ name = "endpoint"
1153+ render = { ( { field } ) => (
1154+ < FormItem >
1155+ < FormLabel > Webhook URL</ FormLabel >
1156+ < FormControl >
1157+ < Input
1158+ placeholder = "https://api.example.com/webhook"
1159+ { ...field }
1160+ />
1161+ </ FormControl >
1162+ < FormDescription >
1163+ The URL where POST requests will be sent with
1164+ notification data.
1165+ </ FormDescription >
1166+ < FormMessage />
1167+ </ FormItem >
1168+ ) }
1169+ />
1170+
1171+ < div className = "space-y-3" >
1172+ < div >
1173+ < FormLabel > Headers</ FormLabel >
1174+ < FormDescription >
1175+ Optional. Custom headers for your POST request (e.g.,
1176+ Authorization, Content-Type).
1177+ </ FormDescription >
1178+ </ div >
10601179
1180+ < div className = "space-y-2" >
1181+ { headerFields . map ( ( field , index ) => (
1182+ < div
1183+ key = { field . id }
1184+ className = "flex items-center gap-2 p-2 border rounded-md bg-muted/50"
1185+ >
1186+ < FormField
1187+ control = { form . control }
1188+ name = { `headers.${ index } .key` as never }
1189+ render = { ( { field } ) => (
1190+ < FormItem className = "flex-1" >
1191+ < FormControl >
1192+ < Input placeholder = "Key" { ...field } />
1193+ </ FormControl >
1194+ </ FormItem >
1195+ ) }
1196+ />
1197+ < FormField
1198+ control = { form . control }
1199+ name = { `headers.${ index } .value` as never }
1200+ render = { ( { field } ) => (
1201+ < FormItem className = "flex-[2]" >
1202+ < FormControl >
1203+ < Input placeholder = "Value" { ...field } />
1204+ </ FormControl >
1205+ </ FormItem >
1206+ ) }
1207+ />
1208+ < Button
1209+ type = "button"
1210+ variant = "ghost"
1211+ size = "sm"
1212+ onClick = { ( ) => removeHeader ( index ) }
1213+ className = "text-red-500 hover:text-red-700 hover:bg-red-50"
1214+ >
1215+ < Trash2 className = "h-4 w-4" />
1216+ </ Button >
1217+ </ div >
1218+ ) ) }
1219+ </ div >
1220+
1221+ < Button
1222+ type = "button"
1223+ variant = "outline"
1224+ size = "sm"
1225+ onClick = { ( ) => appendHeader ( { key : "" , value : "" } ) }
1226+ className = "w-full"
1227+ >
1228+ < PlusIcon className = "h-4 w-4 mr-2" />
1229+ Add header
1230+ </ Button >
1231+ </ div >
1232+ </ div >
1233+ ) }
10611234 { type === "lark" && (
10621235 < >
10631236 < FormField
@@ -1250,7 +1423,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
12501423 isLoadingEmail ||
12511424 isLoadingGotify ||
12521425 isLoadingNtfy ||
1253- isLoadingLark
1426+ isLoadingLark ||
1427+ isLoadingCustom
12541428 }
12551429 variant = "secondary"
12561430 type = "button"
@@ -1304,6 +1478,21 @@ export const HandleNotifications = ({ notificationId }: Props) => {
13041478 await testLarkConnection ( {
13051479 webhookUrl : data . webhookUrl ,
13061480 } ) ;
1481+ } else if ( data . type === "custom" ) {
1482+ const headersRecord =
1483+ data . headers && data . headers . length > 0
1484+ ? data . headers . reduce (
1485+ ( acc , { key, value } ) => {
1486+ if ( key . trim ( ) ) acc [ key ] = value ;
1487+ return acc ;
1488+ } ,
1489+ { } as Record < string , string > ,
1490+ )
1491+ : undefined ;
1492+ await testCustomConnection ( {
1493+ endpoint : data . endpoint ,
1494+ headers : headersRecord ,
1495+ } ) ;
13071496 }
13081497 toast . success ( "Connection Success" ) ;
13091498 } catch ( error ) {
0 commit comments