Skip to content

Commit 7ae3d7d

Browse files
authored
Merge pull request #3512 from mhbdev/resend-provider-for-notifications
feat: add resend notification functionality
2 parents 37ea75b + 6877ebe commit 7ae3d7d

22 files changed

Lines changed: 7880 additions & 50 deletions

apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
LarkIcon,
1717
NtfyIcon,
1818
PushoverIcon,
19+
ResendIcon,
1920
SlackIcon,
2021
TelegramIcon,
2122
} from "@/components/icons/notification-icons";
@@ -97,6 +98,23 @@ export const notificationSchema = z.discriminatedUnion("type", [
9798
.min(1, { message: "At least one email is required" }),
9899
})
99100
.merge(notificationBaseSchema),
101+
z
102+
.object({
103+
type: z.literal("resend"),
104+
apiKey: z.string().min(1, { message: "API Key is required" }),
105+
fromAddress: z
106+
.string()
107+
.min(1, { message: "From Address is required" })
108+
.email({ message: "Email is invalid" }),
109+
toAddresses: z
110+
.array(
111+
z.string().min(1, { message: "Email is required" }).email({
112+
message: "Email is invalid",
113+
}),
114+
)
115+
.min(1, { message: "At least one email is required" }),
116+
})
117+
.merge(notificationBaseSchema),
100118
z
101119
.object({
102120
type: z.literal("gotify"),
@@ -169,6 +187,10 @@ export const notificationsMap = {
169187
icon: <Mail size={29} className="text-muted-foreground" />,
170188
label: "Email",
171189
},
190+
resend: {
191+
icon: <ResendIcon className="text-muted-foreground" />,
192+
label: "Resend",
193+
},
172194
gotify: {
173195
icon: <GotifyIcon />,
174196
label: "Gotify",
@@ -214,6 +236,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
214236
api.notification.testDiscordConnection.useMutation();
215237
const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } =
216238
api.notification.testEmailConnection.useMutation();
239+
const { mutateAsync: testResendConnection, isLoading: isLoadingResend } =
240+
api.notification.testResendConnection.useMutation();
217241
const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } =
218242
api.notification.testGotifyConnection.useMutation();
219243
const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } =
@@ -242,6 +266,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
242266
const emailMutation = notificationId
243267
? api.notification.updateEmail.useMutation()
244268
: api.notification.createEmail.useMutation();
269+
const resendMutation = notificationId
270+
? api.notification.updateResend.useMutation()
271+
: api.notification.createResend.useMutation();
245272
const gotifyMutation = notificationId
246273
? api.notification.updateGotify.useMutation()
247274
: api.notification.createGotify.useMutation();
@@ -281,7 +308,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
281308
});
282309

283310
useEffect(() => {
284-
if (type === "email" && fields.length === 0) {
311+
if ((type === "email" || type === "resend") && fields.length === 0) {
285312
append("");
286313
}
287314
}, [type, append, fields.length]);
@@ -349,6 +376,21 @@ export const HandleNotifications = ({ notificationId }: Props) => {
349376
dockerCleanup: notification.dockerCleanup,
350377
serverThreshold: notification.serverThreshold,
351378
});
379+
} else if (notification.notificationType === "resend") {
380+
form.reset({
381+
appBuildError: notification.appBuildError,
382+
appDeploy: notification.appDeploy,
383+
dokployRestart: notification.dokployRestart,
384+
databaseBackup: notification.databaseBackup,
385+
volumeBackup: notification.volumeBackup,
386+
type: notification.notificationType,
387+
apiKey: notification.resend?.apiKey,
388+
toAddresses: notification.resend?.toAddresses,
389+
fromAddress: notification.resend?.fromAddress,
390+
name: notification.name,
391+
dockerCleanup: notification.dockerCleanup,
392+
serverThreshold: notification.serverThreshold,
393+
});
352394
} else if (notification.notificationType === "gotify") {
353395
form.reset({
354396
appBuildError: notification.appBuildError,
@@ -442,6 +484,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
442484
telegram: telegramMutation,
443485
discord: discordMutation,
444486
email: emailMutation,
487+
resend: resendMutation,
445488
gotify: gotifyMutation,
446489
ntfy: ntfyMutation,
447490
lark: larkMutation,
@@ -525,6 +568,22 @@ export const HandleNotifications = ({ notificationId }: Props) => {
525568
emailId: notification?.emailId || "",
526569
serverThreshold: serverThreshold,
527570
});
571+
} else if (data.type === "resend") {
572+
promise = resendMutation.mutateAsync({
573+
appBuildError: appBuildError,
574+
appDeploy: appDeploy,
575+
dokployRestart: dokployRestart,
576+
databaseBackup: databaseBackup,
577+
volumeBackup: volumeBackup,
578+
apiKey: data.apiKey,
579+
fromAddress: data.fromAddress,
580+
toAddresses: data.toAddresses,
581+
name: data.name,
582+
dockerCleanup: dockerCleanup,
583+
notificationId: notificationId || "",
584+
resendId: notification?.resendId || "",
585+
serverThreshold: serverThreshold,
586+
});
528587
} else if (data.type === "gotify") {
529588
promise = gotifyMutation.mutateAsync({
530589
appBuildError: appBuildError,
@@ -1042,6 +1101,96 @@ export const HandleNotifications = ({ notificationId }: Props) => {
10421101
</>
10431102
)}
10441103

1104+
{type === "resend" && (
1105+
<>
1106+
<FormField
1107+
control={form.control}
1108+
name="apiKey"
1109+
render={({ field }) => (
1110+
<FormItem>
1111+
<FormLabel>API Key</FormLabel>
1112+
<FormControl>
1113+
<Input
1114+
type="password"
1115+
placeholder="re_********"
1116+
{...field}
1117+
/>
1118+
</FormControl>
1119+
<FormMessage />
1120+
</FormItem>
1121+
)}
1122+
/>
1123+
1124+
<FormField
1125+
control={form.control}
1126+
name="fromAddress"
1127+
render={({ field }) => (
1128+
<FormItem>
1129+
<FormLabel>From Address</FormLabel>
1130+
<FormControl>
1131+
<Input placeholder="from@example.com" {...field} />
1132+
</FormControl>
1133+
<FormMessage />
1134+
</FormItem>
1135+
)}
1136+
/>
1137+
1138+
<div className="flex flex-col gap-2 pt-2">
1139+
<FormLabel>To Addresses</FormLabel>
1140+
1141+
{fields.map((field, index) => (
1142+
<div
1143+
key={field.id}
1144+
className="flex flex-row gap-2 w-full"
1145+
>
1146+
<FormField
1147+
control={form.control}
1148+
name={`toAddresses.${index}`}
1149+
render={({ field }) => (
1150+
<FormItem className="w-full">
1151+
<FormControl>
1152+
<Input
1153+
placeholder="email@example.com"
1154+
className="w-full"
1155+
{...field}
1156+
/>
1157+
</FormControl>
1158+
1159+
<FormMessage />
1160+
</FormItem>
1161+
)}
1162+
/>
1163+
<Button
1164+
variant="outline"
1165+
type="button"
1166+
onClick={() => {
1167+
remove(index);
1168+
}}
1169+
>
1170+
Remove
1171+
</Button>
1172+
</div>
1173+
))}
1174+
{type === "resend" &&
1175+
"toAddresses" in form.formState.errors && (
1176+
<div className="text-sm font-medium text-destructive">
1177+
{form.formState?.errors?.toAddresses?.root?.message}
1178+
</div>
1179+
)}
1180+
</div>
1181+
1182+
<Button
1183+
variant="outline"
1184+
type="button"
1185+
onClick={() => {
1186+
append("");
1187+
}}
1188+
>
1189+
Add
1190+
</Button>
1191+
</>
1192+
)}
1193+
10451194
{type === "gotify" && (
10461195
<>
10471196
<FormField
@@ -1627,6 +1776,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
16271776
isLoadingTelegram ||
16281777
isLoadingDiscord ||
16291778
isLoadingEmail ||
1779+
isLoadingResend ||
16301780
isLoadingGotify ||
16311781
isLoadingNtfy ||
16321782
isLoadingLark ||
@@ -1667,6 +1817,12 @@ export const HandleNotifications = ({ notificationId }: Props) => {
16671817
fromAddress: data.fromAddress,
16681818
toAddresses: data.toAddresses,
16691819
});
1820+
} else if (data.type === "resend") {
1821+
await testResendConnection({
1822+
apiKey: data.apiKey,
1823+
fromAddress: data.fromAddress,
1824+
toAddresses: data.toAddresses,
1825+
});
16701826
} else if (data.type === "gotify") {
16711827
await testGotifyConnection({
16721828
serverUrl: data.serverUrl,

apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
GotifyIcon,
66
LarkIcon,
77
NtfyIcon,
8+
ResendIcon,
89
SlackIcon,
910
TelegramIcon,
1011
} from "@/components/icons/notification-icons";
@@ -36,7 +37,7 @@ export const ShowNotifications = () => {
3637
</CardTitle>
3738
<CardDescription>
3839
Add your providers to receive notifications, like Discord, Slack,
39-
Telegram, Email, Lark.
40+
Telegram, Email, Resend, Lark.
4041
</CardDescription>
4142
</CardHeader>
4243
<CardContent className="space-y-2 py-8 border-t">
@@ -86,6 +87,11 @@ export const ShowNotifications = () => {
8687
<Mail className="size-6 text-muted-foreground" />
8788
</div>
8889
)}
90+
{notification.notificationType === "resend" && (
91+
<div className="flex items-center justify-center rounded-lg ">
92+
<ResendIcon className="size-6 text-muted-foreground" />
93+
</div>
94+
)}
8995
{notification.notificationType === "gotify" && (
9096
<div className="flex items-center justify-center rounded-lg ">
9197
<GotifyIcon className="size-6" />

apps/dokploy/components/icons/notification-icons.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,23 @@ export const PushoverIcon = ({ className }: Props) => {
257257
</svg>
258258
);
259259
};
260+
261+
export const ResendIcon = ({ className }: Props) => {
262+
return (
263+
<svg
264+
viewBox="0 0 24 24"
265+
className={cn("size-8", className)}
266+
xmlns="http://www.w3.org/2000/svg"
267+
>
268+
<circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.12" />
269+
<path
270+
d="M8 17V7h6a3 3 0 0 1 0 6H8m6 0 2 4"
271+
stroke="currentColor"
272+
strokeWidth="1.6"
273+
strokeLinecap="round"
274+
strokeLinejoin="round"
275+
fill="none"
276+
/>
277+
</svg>
278+
);
279+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
ALTER TYPE "public"."notificationType" ADD VALUE 'resend' BEFORE 'gotify';--> statement-breakpoint
2+
CREATE TABLE "resend" (
3+
"resendId" text PRIMARY KEY NOT NULL,
4+
"apiKey" text NOT NULL,
5+
"fromAddress" text NOT NULL,
6+
"toAddress" text[] NOT NULL
7+
);
8+
--> statement-breakpoint
9+
ALTER TABLE "notification" ADD COLUMN "resendId" text;--> statement-breakpoint
10+
ALTER TABLE "notification" ADD CONSTRAINT "notification_resendId_resend_resendId_fk" FOREIGN KEY ("resendId") REFERENCES "public"."resend"("resendId") ON DELETE cascade ON UPDATE no action;

0 commit comments

Comments
 (0)