mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(notifications): implement gotify provider
This commit is contained in:
@@ -28,7 +28,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, Mail, PenBoxIcon, PlusIcon } from "lucide-react";
|
import { AlertTriangle, Mail, PenBoxIcon, PlusIcon, MessageCircleMore } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -84,6 +84,15 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
|||||||
.min(1, { message: "At least one email is required" }),
|
.min(1, { message: "At least one email is required" }),
|
||||||
})
|
})
|
||||||
.merge(notificationBaseSchema),
|
.merge(notificationBaseSchema),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("gotify"),
|
||||||
|
serverUrl: z.string().min(1, { message: "Server URL is required" }),
|
||||||
|
appToken: z.string().min(1, { message: "App Token is required" }),
|
||||||
|
priority: z.number().min(1).max(10).default(5),
|
||||||
|
decoration: z.boolean().default(true),
|
||||||
|
})
|
||||||
|
.merge(notificationBaseSchema),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const notificationsMap = {
|
export const notificationsMap = {
|
||||||
@@ -103,6 +112,10 @@ export const notificationsMap = {
|
|||||||
icon: <Mail size={29} className="text-muted-foreground" />,
|
icon: <Mail size={29} className="text-muted-foreground" />,
|
||||||
label: "Email",
|
label: "Email",
|
||||||
},
|
},
|
||||||
|
gotify: {
|
||||||
|
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
|
||||||
|
label: "Gotify",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotificationSchema = z.infer<typeof notificationSchema>;
|
export type NotificationSchema = z.infer<typeof notificationSchema>;
|
||||||
@@ -126,13 +139,14 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
);
|
);
|
||||||
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
||||||
api.notification.testSlackConnection.useMutation();
|
api.notification.testSlackConnection.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } =
|
const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } =
|
||||||
api.notification.testTelegramConnection.useMutation();
|
api.notification.testTelegramConnection.useMutation();
|
||||||
const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } =
|
const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } =
|
||||||
api.notification.testDiscordConnection.useMutation();
|
api.notification.testDiscordConnection.useMutation();
|
||||||
const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } =
|
const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } =
|
||||||
api.notification.testEmailConnection.useMutation();
|
api.notification.testEmailConnection.useMutation();
|
||||||
|
const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } =
|
||||||
|
api.notification.testGotifyConnection.useMutation();
|
||||||
const slackMutation = notificationId
|
const slackMutation = notificationId
|
||||||
? api.notification.updateSlack.useMutation()
|
? api.notification.updateSlack.useMutation()
|
||||||
: api.notification.createSlack.useMutation();
|
: api.notification.createSlack.useMutation();
|
||||||
@@ -145,6 +159,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
const emailMutation = notificationId
|
const emailMutation = notificationId
|
||||||
? api.notification.updateEmail.useMutation()
|
? api.notification.updateEmail.useMutation()
|
||||||
: api.notification.createEmail.useMutation();
|
: api.notification.createEmail.useMutation();
|
||||||
|
const gotifyMutation = notificationId
|
||||||
|
? api.notification.updateGotify.useMutation()
|
||||||
|
: api.notification.createGotify.useMutation();
|
||||||
|
|
||||||
const form = useForm<NotificationSchema>({
|
const form = useForm<NotificationSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -233,6 +250,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
telegram: telegramMutation,
|
telegram: telegramMutation,
|
||||||
discord: discordMutation,
|
discord: discordMutation,
|
||||||
email: emailMutation,
|
email: emailMutation,
|
||||||
|
gotify: gotifyMutation,
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: NotificationSchema) => {
|
const onSubmit = async (data: NotificationSchema) => {
|
||||||
@@ -300,6 +318,21 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
notificationId: notificationId || "",
|
notificationId: notificationId || "",
|
||||||
emailId: notification?.emailId || "",
|
emailId: notification?.emailId || "",
|
||||||
});
|
});
|
||||||
|
} else if (data.type === "gotify") {
|
||||||
|
promise = gotifyMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
serverUrl: data.serverUrl,
|
||||||
|
appToken: data.appToken,
|
||||||
|
priority: data.priority,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
decoration: data.decoration,
|
||||||
|
notificationId: notificationId || "",
|
||||||
|
gotifyId: notification?.gotifyId || "",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (promise) {
|
if (promise) {
|
||||||
@@ -700,6 +733,94 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{type === "gotify" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serverUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://gotify.example.com"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>App Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="AzxcvbnmKjhgfdsa..."
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="priority"
|
||||||
|
defaultValue={5}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Priority</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="5"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value) {
|
||||||
|
const port = Number.parseInt(value);
|
||||||
|
if (port > 0 && port < 10) {
|
||||||
|
field.onChange(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Message priority (1-10, default: 5)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="decoration"
|
||||||
|
defaultValue={true}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Decoration</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Decorate the notification with emojis.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@@ -824,7 +945,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
isLoadingSlack ||
|
isLoadingSlack ||
|
||||||
isLoadingTelegram ||
|
isLoadingTelegram ||
|
||||||
isLoadingDiscord ||
|
isLoadingDiscord ||
|
||||||
isLoadingEmail
|
isLoadingEmail ||
|
||||||
|
isLoadingGotify
|
||||||
}
|
}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -853,6 +975,13 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
toAddresses: form.getValues("toAddresses"),
|
toAddresses: form.getValues("toAddresses"),
|
||||||
fromAddress: form.getValues("fromAddress"),
|
fromAddress: form.getValues("fromAddress"),
|
||||||
});
|
});
|
||||||
|
} else if (type === "gotify") {
|
||||||
|
await testGotifyConnection({
|
||||||
|
serverUrl: form.getValues("serverUrl"),
|
||||||
|
appToken: form.getValues("appToken"),
|
||||||
|
priority: form.getValues("priority"),
|
||||||
|
decoration: form.getValues("decoration"),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
toast.success("Connection Success");
|
toast.success("Connection Success");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -0,0 +1,864 @@
|
|||||||
|
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 { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Mail, Pen } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
type NotificationSchema,
|
||||||
|
notificationSchema,
|
||||||
|
} from "./add-notification";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
notificationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateNotification = ({ notificationId }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { data, refetch } = api.notification.one.useQuery(
|
||||||
|
{
|
||||||
|
notificationId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!notificationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
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 { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } =
|
||||||
|
api.notification.testGotifyConnection.useMutation();
|
||||||
|
const slackMutation = api.notification.updateSlack.useMutation();
|
||||||
|
const telegramMutation = api.notification.updateTelegram.useMutation();
|
||||||
|
const discordMutation = api.notification.updateDiscord.useMutation();
|
||||||
|
const emailMutation = api.notification.updateEmail.useMutation();
|
||||||
|
const gotifyMutation = api.notification.updateGotify.useMutation();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
const form = useForm<NotificationSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
type: "slack",
|
||||||
|
webhookUrl: "",
|
||||||
|
channel: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(notificationSchema),
|
||||||
|
});
|
||||||
|
const type = form.watch("type");
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "toAddresses" as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
if (data.notificationType === "slack") {
|
||||||
|
form.reset({
|
||||||
|
appBuildError: data.appBuildError,
|
||||||
|
appDeploy: data.appDeploy,
|
||||||
|
dokployRestart: data.dokployRestart,
|
||||||
|
databaseBackup: data.databaseBackup,
|
||||||
|
dockerCleanup: data.dockerCleanup,
|
||||||
|
webhookUrl: data.slack?.webhookUrl,
|
||||||
|
channel: data.slack?.channel || "",
|
||||||
|
name: data.name,
|
||||||
|
type: data.notificationType,
|
||||||
|
});
|
||||||
|
} else if (data.notificationType === "telegram") {
|
||||||
|
form.reset({
|
||||||
|
appBuildError: data.appBuildError,
|
||||||
|
appDeploy: data.appDeploy,
|
||||||
|
dokployRestart: data.dokployRestart,
|
||||||
|
databaseBackup: data.databaseBackup,
|
||||||
|
botToken: data.telegram?.botToken,
|
||||||
|
chatId: data.telegram?.chatId,
|
||||||
|
type: data.notificationType,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: data.dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (data.notificationType === "discord") {
|
||||||
|
form.reset({
|
||||||
|
appBuildError: data.appBuildError,
|
||||||
|
appDeploy: data.appDeploy,
|
||||||
|
dokployRestart: data.dokployRestart,
|
||||||
|
databaseBackup: data.databaseBackup,
|
||||||
|
type: data.notificationType,
|
||||||
|
webhookUrl: data.discord?.webhookUrl,
|
||||||
|
decoration: data.discord?.decoration || undefined,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: data.dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (data.notificationType === "email") {
|
||||||
|
form.reset({
|
||||||
|
appBuildError: data.appBuildError,
|
||||||
|
appDeploy: data.appDeploy,
|
||||||
|
dokployRestart: data.dokployRestart,
|
||||||
|
databaseBackup: data.databaseBackup,
|
||||||
|
type: data.notificationType,
|
||||||
|
smtpServer: data.email?.smtpServer,
|
||||||
|
smtpPort: data.email?.smtpPort,
|
||||||
|
username: data.email?.username,
|
||||||
|
password: data.email?.password,
|
||||||
|
toAddresses: data.email?.toAddresses,
|
||||||
|
fromAddress: data.email?.fromAddress,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: data.dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (data.notificationType === "gotify") {
|
||||||
|
form.reset({
|
||||||
|
appBuildError: data.appBuildError,
|
||||||
|
appDeploy: data.appDeploy,
|
||||||
|
dokployRestart: data.dokployRestart,
|
||||||
|
databaseBackup: data.databaseBackup,
|
||||||
|
type: data.notificationType,
|
||||||
|
serverUrl: data.gotify?.serverUrl,
|
||||||
|
appToken: data.gotify?.appToken,
|
||||||
|
priority: data.gotify?.priority || 5,
|
||||||
|
decoration: data.gotify?.decoration || undefined,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: data.dockerCleanup,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: NotificationSchema) => {
|
||||||
|
const {
|
||||||
|
appBuildError,
|
||||||
|
appDeploy,
|
||||||
|
dokployRestart,
|
||||||
|
databaseBackup,
|
||||||
|
dockerCleanup,
|
||||||
|
} = formData;
|
||||||
|
let promise: Promise<unknown> | null = null;
|
||||||
|
if (formData?.type === "slack" && data?.slackId) {
|
||||||
|
promise = slackMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
webhookUrl: formData.webhookUrl,
|
||||||
|
channel: formData.channel,
|
||||||
|
name: formData.name,
|
||||||
|
notificationId: notificationId,
|
||||||
|
slackId: data?.slackId,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (formData.type === "telegram" && data?.telegramId) {
|
||||||
|
promise = telegramMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
botToken: formData.botToken,
|
||||||
|
chatId: formData.chatId,
|
||||||
|
name: formData.name,
|
||||||
|
notificationId: notificationId,
|
||||||
|
telegramId: data?.telegramId,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (formData.type === "discord" && data?.discordId) {
|
||||||
|
promise = discordMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
webhookUrl: formData.webhookUrl,
|
||||||
|
decoration: formData.decoration,
|
||||||
|
name: formData.name,
|
||||||
|
notificationId: notificationId,
|
||||||
|
discordId: data?.discordId,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (formData.type === "email" && data?.emailId) {
|
||||||
|
promise = emailMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
smtpServer: formData.smtpServer,
|
||||||
|
smtpPort: formData.smtpPort,
|
||||||
|
username: formData.username,
|
||||||
|
password: formData.password,
|
||||||
|
fromAddress: formData.fromAddress,
|
||||||
|
toAddresses: formData.toAddresses,
|
||||||
|
name: formData.name,
|
||||||
|
notificationId: notificationId,
|
||||||
|
emailId: data?.emailId,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (formData.type === "gotify" && data?.gotifyId) {
|
||||||
|
promise = gotifyMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
serverUrl: formData.serverUrl,
|
||||||
|
appToken: formData.appToken,
|
||||||
|
priority: formData.priority,
|
||||||
|
decoration: formData.decoration,
|
||||||
|
name: formData.name,
|
||||||
|
notificationId: notificationId,
|
||||||
|
gotifyId: data?.gotifyId,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promise) {
|
||||||
|
await promise
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Notification Updated");
|
||||||
|
await utils.notification.all.invalidate();
|
||||||
|
refetch();
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error updating a notification");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 dark:hover:bg-zinc-900/80 hover:bg-gray-200/80"
|
||||||
|
>
|
||||||
|
<Pen className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update Notification</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Update the current notification config
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-8 "
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4 ">
|
||||||
|
<div className="flex flex-row gap-2 w-full items-center">
|
||||||
|
<div className="flex flex-row gap-2 items-center w-full ">
|
||||||
|
<FormLabel className="text-lg font-semibold leading-none tracking-tight flex">
|
||||||
|
{data?.notificationType === "slack"
|
||||||
|
? "Slack"
|
||||||
|
: data?.notificationType === "telegram"
|
||||||
|
? "Telegram"
|
||||||
|
: data?.notificationType === "discord"
|
||||||
|
? "Discord"
|
||||||
|
: data?.notificationType === "email"
|
||||||
|
? "Email"
|
||||||
|
: "Gotify"}
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
{data?.notificationType === "slack" && (
|
||||||
|
<SlackIcon className="text-muted-foreground size-6 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
{data?.notificationType === "telegram" && (
|
||||||
|
<TelegramIcon className="text-muted-foreground size-8 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
{data?.notificationType === "discord" && (
|
||||||
|
<DiscordIcon className="text-muted-foreground size-7 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
{data?.notificationType === "email" && (
|
||||||
|
<Mail
|
||||||
|
size={29}
|
||||||
|
className="text-muted-foreground size-6 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{data?.notificationType === "gotify" && (
|
||||||
|
<Mail
|
||||||
|
size={29}
|
||||||
|
className="text-muted-foreground size-6 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Name" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{type === "slack" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="webhookUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="channel"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Channel</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Channel" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "telegram" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="botToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Bot Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="6660491268:AAFMGmajZOVewpMNZCgJr5H7cpXpoZPgvXw"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="chatId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Chat ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="431231869" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "discord" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="webhookUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="decoration"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Decoration</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Decorate the notification with emojis.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{type === "email" && (
|
||||||
|
<>
|
||||||
|
<div className="flex md:flex-row flex-col gap-2 w-full">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="smtpServer"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>SMTP Server</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="smtp.gmail.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="smtpPort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>SMTP Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="587"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value) {
|
||||||
|
const port = Number.parseInt(value);
|
||||||
|
if (port > 0 && port < 65536) {
|
||||||
|
field.onChange(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex md:flex-row flex-col gap-2 w-full">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="username" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="******************"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fromAddress"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>From Address</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="from@example.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2 pt-2">
|
||||||
|
<FormLabel>To Addresses</FormLabel>
|
||||||
|
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div
|
||||||
|
key={field.id}
|
||||||
|
className="flex flex-row gap-2 w-full"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`toAddresses.${index}`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="email@example.com"
|
||||||
|
className="w-full"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
remove(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{type === "email" &&
|
||||||
|
"toAddresses" in form.formState.errors && (
|
||||||
|
<div className="text-sm font-medium text-destructive">
|
||||||
|
{form.formState?.errors?.toAddresses?.root?.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
append("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{type === "gotify" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serverUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://gotify.example.com"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>App Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="AzxF2.d9KzP..."
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="priority"
|
||||||
|
defaultValue={5}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Priority</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="5"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value) {
|
||||||
|
const priority = Number.parseInt(value);
|
||||||
|
if (priority > 0 && priority < 10) {
|
||||||
|
field.onChange(priority);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="decoration"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Decoration</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Decorate the notification with emojis.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<FormLabel className="text-lg font-semibold leading-none tracking-tight">
|
||||||
|
Select the actions.
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
defaultValue={form.control._defaultValues.appDeploy}
|
||||||
|
name="appDeploy"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>App Deploy</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when a app is deployed.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
defaultValue={form.control._defaultValues.appBuildError}
|
||||||
|
name="appBuildError"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>App Builder Error</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when the build fails.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="databaseBackup"
|
||||||
|
defaultValue={form.control._defaultValues.databaseBackup}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Database Backup</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when a database backup is created.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerCleanup"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Docker Cleanup</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when the docker cleanup is
|
||||||
|
performed.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{!isCloud && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
defaultValue={form.control._defaultValues.dokployRestart}
|
||||||
|
name="dokployRestart"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Dokploy Restart</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when a dokploy is restarted.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter className="flex flex-row gap-2 !justify-between w-full">
|
||||||
|
<Button
|
||||||
|
isLoading={
|
||||||
|
isLoadingSlack ||
|
||||||
|
isLoadingTelegram ||
|
||||||
|
isLoadingDiscord ||
|
||||||
|
isLoadingEmail ||
|
||||||
|
isLoadingGotify
|
||||||
|
}
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (type === "slack") {
|
||||||
|
await testSlackConnection({
|
||||||
|
webhookUrl: form.getValues("webhookUrl"),
|
||||||
|
channel: form.getValues("channel"),
|
||||||
|
});
|
||||||
|
} else if (type === "telegram") {
|
||||||
|
await testTelegramConnection({
|
||||||
|
botToken: form.getValues("botToken"),
|
||||||
|
chatId: form.getValues("chatId"),
|
||||||
|
});
|
||||||
|
} else if (type === "discord") {
|
||||||
|
await testDiscordConnection({
|
||||||
|
webhookUrl: form.getValues("webhookUrl"),
|
||||||
|
decoration: form.getValues("decoration"),
|
||||||
|
});
|
||||||
|
} else if (type === "email") {
|
||||||
|
await testEmailConnection({
|
||||||
|
smtpServer: form.getValues("smtpServer"),
|
||||||
|
smtpPort: form.getValues("smtpPort"),
|
||||||
|
username: form.getValues("username"),
|
||||||
|
password: form.getValues("password"),
|
||||||
|
toAddresses: form.getValues("toAddresses"),
|
||||||
|
fromAddress: form.getValues("fromAddress"),
|
||||||
|
});
|
||||||
|
} else if (type === "gotify") {
|
||||||
|
await testGotifyConnection({
|
||||||
|
priority: form.getValues("priority"),
|
||||||
|
serverUrl: form.getValues("serverUrl"),
|
||||||
|
appToken: form.getValues("appToken"),
|
||||||
|
decoration: form.getValues("decoration"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
toast.success("Connection Success");
|
||||||
|
} catch (err) {
|
||||||
|
toast.error("Error testing the provider");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test Notification
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
form="hook-form"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4250,4 +4250,4 @@
|
|||||||
"schemas": {},
|
"schemas": {},
|
||||||
"tables": {}
|
"tables": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,4 +395,4 @@
|
|||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,24 @@ import {
|
|||||||
adminProcedure,
|
adminProcedure,
|
||||||
createTRPCRouter,
|
createTRPCRouter,
|
||||||
protectedProcedure,
|
protectedProcedure,
|
||||||
|
publicProcedure,
|
||||||
} from "@/server/api/trpc";
|
} from "@/server/api/trpc";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
apiCreateDiscord,
|
apiCreateDiscord,
|
||||||
apiCreateEmail,
|
apiCreateEmail,
|
||||||
|
apiCreateGotify,
|
||||||
apiCreateSlack,
|
apiCreateSlack,
|
||||||
apiCreateTelegram,
|
apiCreateTelegram,
|
||||||
apiFindOneNotification,
|
apiFindOneNotification,
|
||||||
apiTestDiscordConnection,
|
apiTestDiscordConnection,
|
||||||
apiTestEmailConnection,
|
apiTestEmailConnection,
|
||||||
|
apiTestGotifyConnection,
|
||||||
apiTestSlackConnection,
|
apiTestSlackConnection,
|
||||||
apiTestTelegramConnection,
|
apiTestTelegramConnection,
|
||||||
apiUpdateDiscord,
|
apiUpdateDiscord,
|
||||||
apiUpdateEmail,
|
apiUpdateEmail,
|
||||||
|
apiUpdateGotify,
|
||||||
apiUpdateSlack,
|
apiUpdateSlack,
|
||||||
apiUpdateTelegram,
|
apiUpdateTelegram,
|
||||||
notifications,
|
notifications,
|
||||||
@@ -24,16 +28,19 @@ import {
|
|||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
createDiscordNotification,
|
createDiscordNotification,
|
||||||
createEmailNotification,
|
createEmailNotification,
|
||||||
|
createGotifyNotification,
|
||||||
createSlackNotification,
|
createSlackNotification,
|
||||||
createTelegramNotification,
|
createTelegramNotification,
|
||||||
findNotificationById,
|
findNotificationById,
|
||||||
removeNotificationById,
|
removeNotificationById,
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
|
sendGotifyNotification,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
sendTelegramNotification,
|
sendTelegramNotification,
|
||||||
updateDiscordNotification,
|
updateDiscordNotification,
|
||||||
updateEmailNotification,
|
updateEmailNotification,
|
||||||
|
updateGotifyNotification,
|
||||||
updateSlackNotification,
|
updateSlackNotification,
|
||||||
updateTelegramNotification,
|
updateTelegramNotification,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
@@ -300,10 +307,61 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
telegram: true,
|
telegram: true,
|
||||||
discord: true,
|
discord: true,
|
||||||
email: true,
|
email: true,
|
||||||
|
gotify: true,
|
||||||
},
|
},
|
||||||
orderBy: desc(notifications.createdAt),
|
orderBy: desc(notifications.createdAt),
|
||||||
...(IS_CLOUD && { where: eq(notifications.adminId, ctx.user.adminId) }),
|
...(IS_CLOUD && { where: eq(notifications.adminId, ctx.user.adminId) }),
|
||||||
// TODO: Remove this line when the cloud version is ready
|
// TODO: Remove this line when the cloud version is ready
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
createGotify: adminProcedure
|
||||||
|
.input(apiCreateGotify)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
return await createGotifyNotification(input, ctx.user.adminId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error creating the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
updateGotify: adminProcedure
|
||||||
|
.input(apiUpdateGotify)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const notification = await findNotificationById(input.notificationId);
|
||||||
|
if (IS_CLOUD && notification.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to update this notification",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await updateGotifyNotification({
|
||||||
|
...input,
|
||||||
|
adminId: ctx.user.adminId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
testGotifyConnection: adminProcedure
|
||||||
|
.input(apiTestGotifyConnection)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
await sendGotifyNotification(
|
||||||
|
input,
|
||||||
|
"Test Notification",
|
||||||
|
"Hi, From Dokploy 👋"
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error testing the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const notificationType = pgEnum("notificationType", [
|
|||||||
"telegram",
|
"telegram",
|
||||||
"discord",
|
"discord",
|
||||||
"email",
|
"email",
|
||||||
|
"gotify",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const notifications = pgTable("notification", {
|
export const notifications = pgTable("notification", {
|
||||||
@@ -39,6 +40,9 @@ export const notifications = pgTable("notification", {
|
|||||||
emailId: text("emailId").references(() => email.emailId, {
|
emailId: text("emailId").references(() => email.emailId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
|
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
adminId: text("adminId").references(() => admins.adminId, {
|
adminId: text("adminId").references(() => admins.adminId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
@@ -84,6 +88,17 @@ export const email = pgTable("email", {
|
|||||||
toAddresses: text("toAddress").array().notNull(),
|
toAddresses: text("toAddress").array().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const gotify = pgTable("gotify", {
|
||||||
|
gotifyId: text("gotifyId")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => nanoid()),
|
||||||
|
serverUrl: text("serverUrl").notNull(),
|
||||||
|
appToken: text("appToken").notNull(),
|
||||||
|
priority: integer("priority").notNull().default(5),
|
||||||
|
decoration: boolean("decoration"),
|
||||||
|
});
|
||||||
|
|
||||||
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||||
slack: one(slack, {
|
slack: one(slack, {
|
||||||
fields: [notifications.slackId],
|
fields: [notifications.slackId],
|
||||||
@@ -101,6 +116,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
|||||||
fields: [notifications.emailId],
|
fields: [notifications.emailId],
|
||||||
references: [email.emailId],
|
references: [email.emailId],
|
||||||
}),
|
}),
|
||||||
|
gotify: one(gotify, {
|
||||||
|
fields: [notifications.gotifyId],
|
||||||
|
references: [gotify.gotifyId],
|
||||||
|
}),
|
||||||
admin: one(admins, {
|
admin: one(admins, {
|
||||||
fields: [notifications.adminId],
|
fields: [notifications.adminId],
|
||||||
references: [admins.adminId],
|
references: [admins.adminId],
|
||||||
@@ -224,6 +243,37 @@ export const apiTestEmailConnection = apiCreateEmail.pick({
|
|||||||
fromAddress: true,
|
fromAddress: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const apiCreateGotify = notificationsSchema
|
||||||
|
.pick({
|
||||||
|
appBuildError: true,
|
||||||
|
databaseBackup: true,
|
||||||
|
dokployRestart: true,
|
||||||
|
name: true,
|
||||||
|
appDeploy: true,
|
||||||
|
dockerCleanup: true,
|
||||||
|
})
|
||||||
|
.extend({
|
||||||
|
serverUrl: z.string().min(1),
|
||||||
|
appToken: z.string().min(1),
|
||||||
|
priority: z.number().min(1),
|
||||||
|
decoration: z.boolean(),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export const apiUpdateGotify = apiCreateGotify.partial().extend({
|
||||||
|
notificationId: z.string().min(1),
|
||||||
|
gotifyId: z.string().min(1),
|
||||||
|
adminId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiTestGotifyConnection = apiCreateGotify.pick({
|
||||||
|
serverUrl: true,
|
||||||
|
appToken: true,
|
||||||
|
priority: true,
|
||||||
|
}).extend({
|
||||||
|
decoration: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const apiFindOneNotification = notificationsSchema
|
export const apiFindOneNotification = notificationsSchema
|
||||||
.pick({
|
.pick({
|
||||||
notificationId: true,
|
notificationId: true,
|
||||||
@@ -242,5 +292,8 @@ export const apiSendTest = notificationsSchema
|
|||||||
username: z.string(),
|
username: z.string(),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
toAddresses: z.array(z.string()),
|
toAddresses: z.array(z.string()),
|
||||||
|
serverUrl: z.string(),
|
||||||
|
appToken: z.string(),
|
||||||
|
priority: z.number(),
|
||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import { db } from "@dokploy/server/db";
|
|||||||
import {
|
import {
|
||||||
type apiCreateDiscord,
|
type apiCreateDiscord,
|
||||||
type apiCreateEmail,
|
type apiCreateEmail,
|
||||||
|
type apiCreateGotify,
|
||||||
type apiCreateSlack,
|
type apiCreateSlack,
|
||||||
type apiCreateTelegram,
|
type apiCreateTelegram,
|
||||||
type apiUpdateDiscord,
|
type apiUpdateDiscord,
|
||||||
type apiUpdateEmail,
|
type apiUpdateEmail,
|
||||||
|
type apiUpdateGotify,
|
||||||
type apiUpdateSlack,
|
type apiUpdateSlack,
|
||||||
type apiUpdateTelegram,
|
type apiUpdateTelegram,
|
||||||
discord,
|
discord,
|
||||||
email,
|
email,
|
||||||
|
gotify,
|
||||||
notifications,
|
notifications,
|
||||||
slack,
|
slack,
|
||||||
telegram,
|
telegram,
|
||||||
@@ -379,6 +382,96 @@ export const updateEmailNotification = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createGotifyNotification = async (
|
||||||
|
input: typeof apiCreateGotify._type,
|
||||||
|
adminId: string,
|
||||||
|
) => {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
const newGotify = await tx
|
||||||
|
.insert(gotify)
|
||||||
|
.values({
|
||||||
|
serverUrl: input.serverUrl,
|
||||||
|
appToken: input.appToken,
|
||||||
|
priority: input.priority,
|
||||||
|
decoration: input.decoration,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
|
if (!newGotify) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error input: Inserting gotify",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDestination = await tx
|
||||||
|
.insert(notifications)
|
||||||
|
.values({
|
||||||
|
gotifyId: newGotify.gotifyId,
|
||||||
|
name: input.name,
|
||||||
|
appDeploy: input.appDeploy,
|
||||||
|
appBuildError: input.appBuildError,
|
||||||
|
databaseBackup: input.databaseBackup,
|
||||||
|
dokployRestart: input.dokployRestart,
|
||||||
|
dockerCleanup: input.dockerCleanup,
|
||||||
|
notificationType: "gotify",
|
||||||
|
adminId: adminId,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
|
if (!newDestination) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error input: Inserting notification",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDestination;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateGotifyNotification = async (
|
||||||
|
input: typeof apiUpdateGotify._type,
|
||||||
|
) => {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
const newDestination = await tx
|
||||||
|
.update(notifications)
|
||||||
|
.set({
|
||||||
|
name: input.name,
|
||||||
|
appDeploy: input.appDeploy,
|
||||||
|
appBuildError: input.appBuildError,
|
||||||
|
databaseBackup: input.databaseBackup,
|
||||||
|
dokployRestart: input.dokployRestart,
|
||||||
|
dockerCleanup: input.dockerCleanup,
|
||||||
|
adminId: input.adminId,
|
||||||
|
})
|
||||||
|
.where(eq(notifications.notificationId, input.notificationId))
|
||||||
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
|
if (!newDestination) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error Updating notification",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.update(gotify)
|
||||||
|
.set({
|
||||||
|
serverUrl: input.serverUrl,
|
||||||
|
appToken: input.appToken,
|
||||||
|
priority: input.priority,
|
||||||
|
decoration: input.decoration,
|
||||||
|
})
|
||||||
|
.where(eq(gotify.gotifyId, input.gotifyId));
|
||||||
|
|
||||||
|
return newDestination;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const findNotificationById = async (notificationId: string) => {
|
export const findNotificationById = async (notificationId: string) => {
|
||||||
const notification = await db.query.notifications.findFirst({
|
const notification = await db.query.notifications.findFirst({
|
||||||
where: eq(notifications.notificationId, notificationId),
|
where: eq(notifications.notificationId, notificationId),
|
||||||
@@ -387,6 +480,7 @@ export const findNotificationById = async (notificationId: string) => {
|
|||||||
telegram: true,
|
telegram: true,
|
||||||
discord: true,
|
discord: true,
|
||||||
email: true,
|
email: true,
|
||||||
|
gotify: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!notification) {
|
if (!notification) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
email,
|
email,
|
||||||
slack,
|
slack,
|
||||||
telegram,
|
telegram,
|
||||||
|
gotify,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import nodemailer from "nodemailer";
|
import nodemailer from "nodemailer";
|
||||||
|
|
||||||
@@ -87,3 +88,31 @@ export const sendSlackNotification = async (
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendGotifyNotification = async (
|
||||||
|
connection: typeof gotify.$inferInsert,
|
||||||
|
title: string,
|
||||||
|
message: string,
|
||||||
|
) => {
|
||||||
|
const response = await fetch(`${connection.serverUrl}/message`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Gotify-Key": connection.appToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
priority: connection.priority,
|
||||||
|
extras: {
|
||||||
|
"client::display": {
|
||||||
|
"contentType": "text/plain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to send Gotify notification: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user