Skip to content

Commit fd94a14

Browse files
authored
Merge pull request #2674 from ischanx/feat-lark-webhook
feat(notifications): add lark webhook
2 parents d1130c4 + d7e0413 commit fd94a14

File tree

16 files changed

+7617
-18
lines changed

16 files changed

+7617
-18
lines changed

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

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
import { zodResolver } from "@hookform/resolvers/zod";
2-
import {
3-
AlertTriangle,
4-
Mail,
5-
MessageCircleMore,
6-
PenBoxIcon,
7-
PlusIcon,
8-
} from "lucide-react";
2+
import { AlertTriangle, Mail, PenBoxIcon, PlusIcon } from "lucide-react";
93
import { useEffect, useState } from "react";
104
import { useFieldArray, useForm } from "react-hook-form";
115
import { toast } from "sonner";
126
import { z } from "zod";
137
import {
148
DiscordIcon,
159
GotifyIcon,
10+
LarkIcon,
1611
NtfyIcon,
1712
SlackIcon,
1813
TelegramIcon,
@@ -112,6 +107,12 @@ export const notificationSchema = z.discriminatedUnion("type", [
112107
priority: z.number().min(1).max(5).default(3),
113108
})
114109
.merge(notificationBaseSchema),
110+
z
111+
.object({
112+
type: z.literal("lark"),
113+
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
114+
})
115+
.merge(notificationBaseSchema),
115116
]);
116117

117118
export const notificationsMap = {
@@ -127,6 +128,10 @@ export const notificationsMap = {
127128
icon: <DiscordIcon />,
128129
label: "Discord",
129130
},
131+
lark: {
132+
icon: <LarkIcon className="text-muted-foreground" />,
133+
label: "Lark",
134+
},
130135
email: {
131136
icon: <Mail size={29} className="text-muted-foreground" />,
132137
label: "Email",
@@ -172,6 +177,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
172177
api.notification.testGotifyConnection.useMutation();
173178
const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } =
174179
api.notification.testNtfyConnection.useMutation();
180+
const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } =
181+
api.notification.testLarkConnection.useMutation();
175182
const slackMutation = notificationId
176183
? api.notification.updateSlack.useMutation()
177184
: api.notification.createSlack.useMutation();
@@ -190,6 +197,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
190197
const ntfyMutation = notificationId
191198
? api.notification.updateNtfy.useMutation()
192199
: api.notification.createNtfy.useMutation();
200+
const larkMutation = notificationId
201+
? api.notification.updateLark.useMutation()
202+
: api.notification.createLark.useMutation();
193203

194204
const form = useForm<NotificationSchema>({
195205
defaultValues: {
@@ -299,6 +309,19 @@ export const HandleNotifications = ({ notificationId }: Props) => {
299309
serverUrl: notification.ntfy?.serverUrl,
300310
name: notification.name,
301311
dockerCleanup: notification.dockerCleanup,
312+
serverThreshold: notification.serverThreshold,
313+
});
314+
} else if (notification.notificationType === "lark") {
315+
form.reset({
316+
appBuildError: notification.appBuildError,
317+
appDeploy: notification.appDeploy,
318+
dokployRestart: notification.dokployRestart,
319+
databaseBackup: notification.databaseBackup,
320+
type: notification.notificationType,
321+
webhookUrl: notification.lark?.webhookUrl,
322+
name: notification.name,
323+
dockerCleanup: notification.dockerCleanup,
324+
serverThreshold: notification.serverThreshold,
302325
});
303326
}
304327
} else {
@@ -313,6 +336,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
313336
email: emailMutation,
314337
gotify: gotifyMutation,
315338
ntfy: ntfyMutation,
339+
lark: larkMutation,
316340
};
317341

318342
const onSubmit = async (data: NotificationSchema) => {
@@ -416,6 +440,19 @@ export const HandleNotifications = ({ notificationId }: Props) => {
416440
notificationId: notificationId || "",
417441
ntfyId: notification?.ntfyId || "",
418442
});
443+
} else if (data.type === "lark") {
444+
promise = larkMutation.mutateAsync({
445+
appBuildError: appBuildError,
446+
appDeploy: appDeploy,
447+
dokployRestart: dokployRestart,
448+
databaseBackup: databaseBackup,
449+
webhookUrl: data.webhookUrl,
450+
name: data.name,
451+
dockerCleanup: dockerCleanup,
452+
notificationId: notificationId || "",
453+
larkId: notification?.larkId || "",
454+
serverThreshold: serverThreshold,
455+
});
419456
}
420457

421458
if (promise) {
@@ -504,7 +541,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
504541
/>
505542
<Label
506543
htmlFor={key}
507-
className="flex flex-col gap-2 items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
544+
className="h-24 flex flex-col gap-2 items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
508545
>
509546
{value.icon}
510547
{value.label}
@@ -1002,6 +1039,27 @@ export const HandleNotifications = ({ notificationId }: Props) => {
10021039
/>
10031040
</>
10041041
)}
1042+
1043+
{type === "lark" && (
1044+
<>
1045+
<FormField
1046+
control={form.control}
1047+
name="webhookUrl"
1048+
render={({ field }) => (
1049+
<FormItem>
1050+
<FormLabel>Webhook URL</FormLabel>
1051+
<FormControl>
1052+
<Input
1053+
placeholder="https://open.larksuite.com/open-apis/bot/v2/hook/xxxxxxxxxxxxxxxxxxxxxxxx"
1054+
{...field}
1055+
/>
1056+
</FormControl>
1057+
<FormMessage />
1058+
</FormItem>
1059+
)}
1060+
/>
1061+
</>
1062+
)}
10051063
</div>
10061064
</div>
10071065
<div className="flex flex-col gap-4">
@@ -1152,7 +1210,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
11521210
isLoadingDiscord ||
11531211
isLoadingEmail ||
11541212
isLoadingGotify ||
1155-
isLoadingNtfy
1213+
isLoadingNtfy ||
1214+
isLoadingLark
11561215
}
11571216
variant="secondary"
11581217
onClick={async () => {
@@ -1196,6 +1255,10 @@ export const HandleNotifications = ({ notificationId }: Props) => {
11961255
accessToken: form.getValues("accessToken"),
11971256
priority: form.getValues("priority"),
11981257
});
1258+
} else if (type === "lark") {
1259+
await testLarkConnection({
1260+
webhookUrl: form.getValues("webhookUrl"),
1261+
});
11991262
}
12001263
toast.success("Connection Success");
12011264
} catch {

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { Bell, Loader2, Mail, MessageCircleMore, Trash2 } from "lucide-react";
1+
import { Bell, Loader2, Mail, Trash2 } from "lucide-react";
22
import { toast } from "sonner";
33
import {
44
DiscordIcon,
55
GotifyIcon,
6+
LarkIcon,
67
NtfyIcon,
78
SlackIcon,
89
TelegramIcon,
@@ -35,7 +36,7 @@ export const ShowNotifications = () => {
3536
</CardTitle>
3637
<CardDescription>
3738
Add your providers to receive notifications, like Discord, Slack,
38-
Telegram, Email.
39+
Telegram, Email, Lark.
3940
</CardDescription>
4041
</CardHeader>
4142
<CardContent className="space-y-2 py-8 border-t">
@@ -95,6 +96,11 @@ export const ShowNotifications = () => {
9596
<NtfyIcon className="size-6" />
9697
</div>
9798
)}
99+
{notification.notificationType === "lark" && (
100+
<div className="flex items-center justify-center rounded-lg">
101+
<LarkIcon className="size-7 text-muted-foreground" />
102+
</div>
103+
)}
98104

99105
{notification.name}
100106
</span>

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,32 @@ export const DiscordIcon = ({ className }: Props) => {
8888
</svg>
8989
);
9090
};
91-
91+
export const LarkIcon = ({ className }: Props) => {
92+
return (
93+
<svg
94+
width="1em"
95+
height="1em"
96+
viewBox="0 0 24 24"
97+
fill="none"
98+
xmlns="http://www.w3.org/2000/svg"
99+
data-icon="LarkLogoColorful"
100+
className={cn("size-9", className)}
101+
>
102+
<path
103+
d="m12.924 12.803.056-.054c.038-.034.076-.072.11-.11l.077-.076.23-.227 1.334-1.319.335-.331c.063-.063.13-.123.195-.183a7.777 7.777 0 0 1 1.823-1.24 7.607 7.607 0 0 1 1.014-.4 13.177 13.177 0 0 0-2.5-5.013 1.203 1.203 0 0 0-.94-.448h-9.65c-.173 0-.246.224-.107.325a28.23 28.23 0 0 1 8 9.098c.007-.006.016-.013.023-.022Z"
104+
fill="#00D6B9"
105+
/>
106+
<path
107+
d="M9.097 21.299a13.258 13.258 0 0 0 11.82-7.247 5.576 5.576 0 0 1-.731 1.076 5.315 5.315 0 0 1-.745.7 5.117 5.117 0 0 1-.615.404 4.626 4.626 0 0 1-.726.331 5.312 5.312 0 0 1-1.883.312 5.892 5.892 0 0 1-.524-.031 6.509 6.509 0 0 1-.729-.126c-.06-.016-.12-.029-.18-.044-.166-.044-.33-.092-.494-.14-.082-.024-.164-.046-.246-.072-.123-.038-.247-.072-.366-.11l-.3-.095-.284-.094-.192-.067c-.08-.025-.155-.053-.234-.082a3.49 3.49 0 0 1-.167-.06c-.11-.04-.221-.079-.328-.12-.063-.025-.126-.047-.19-.072l-.252-.098c-.088-.035-.18-.07-.268-.107l-.174-.07c-.072-.028-.141-.06-.214-.088l-.164-.07c-.057-.024-.114-.05-.17-.075l-.149-.066-.135-.06-.14-.063a90.183 90.183 0 0 1-.141-.066 4.808 4.808 0 0 0-.18-.083c-.063-.028-.123-.06-.186-.088a5.697 5.697 0 0 1-.199-.098 27.762 27.762 0 0 1-8.067-5.969.18.18 0 0 0-.312.123l.006 9.21c0 .4.199.779.533 1a13.177 13.177 0 0 0 7.326 2.205Z"
108+
fill="#3370FF"
109+
/>
110+
<path
111+
d="M23.732 9.295a7.55 7.55 0 0 0-3.35-.776 7.521 7.521 0 0 0-2.284.35c-.054.016-.107.035-.158.05a8.297 8.297 0 0 0-.855.35 7.14 7.14 0 0 0-.552.297 6.716 6.716 0 0 0-.533.347c-.123.089-.243.18-.363.275-.13.104-.252.211-.375.321-.067.06-.13.123-.196.184l-.334.328-1.338 1.321-.23.228-.076.075c-.038.038-.076.073-.11.11l-.057.054a1.914 1.914 0 0 1-.085.08c-.032.028-.063.06-.095.088a13.286 13.286 0 0 1-2.748 1.946c.06.028.12.057.18.082l.142.066c.044.022.091.041.139.063l.135.06.149.067.17.075.164.07c.073.031.142.06.215.088.056.025.116.047.173.07.088.034.177.072.268.107.085.031.168.066.253.098l.189.072c.11.041.218.082.328.12.057.019.11.041.167.06.08.028.155.053.234.082l.192.066.284.095.3.095c.123.037.243.075.366.11l.246.072c.164.048.331.095.495.14.06.015.12.03.18.043.114.029.227.05.34.07.13.022.26.04.389.057a5.815 5.815 0 0 0 .994.019 5.172 5.172 0 0 0 1.413-.3 5.405 5.405 0 0 0 .726-.334c.06-.035.122-.07.182-.108a7.96 7.96 0 0 0 .432-.297 5.362 5.362 0 0 0 .577-.517 5.285 5.285 0 0 0 .37-.429 5.797 5.797 0 0 0 .527-.827l.13-.258 1.166-2.325-.003.006a7.391 7.391 0 0 1 1.527-2.186Z"
112+
fill="#133C9A"
113+
/>
114+
</svg>
115+
);
116+
};
92117
export const GotifyIcon = ({ className }: Props) => {
93118
return (
94119
<svg
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ALTER TYPE "public"."notificationType" ADD VALUE 'lark';--> statement-breakpoint
2+
CREATE TABLE "lark" (
3+
"larkId" text PRIMARY KEY NOT NULL,
4+
"webhookUrl" text NOT NULL
5+
);
6+
--> statement-breakpoint
7+
ALTER TABLE "notification" ADD COLUMN "larkId" text;--> statement-breakpoint
8+
ALTER TABLE "notification" ADD CONSTRAINT "notification_larkId_lark_larkId_fk" FOREIGN KEY ("larkId") REFERENCES "public"."lark"("larkId") ON DELETE cascade ON UPDATE no action;

0 commit comments

Comments
 (0)