diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx
index e5fee3a9d..97adb0f91 100644
--- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx
+++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx
@@ -6,6 +6,7 @@ import { toast } from "sonner";
import { z } from "zod";
import {
DiscordIcon,
+ MattermostIcon,
GotifyIcon,
LarkIcon,
NtfyIcon,
@@ -108,6 +109,14 @@ export const notificationSchema = z.discriminatedUnion("type", [
})
.merge(notificationBaseSchema),
z
+ .object({
+ type: z.literal("mattermost"),
+ webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
+ channel: z.string().optional(),
+ username: z.string().optional(),
+ })
+ .merge(notificationBaseSchema),
+ z
.object({
type: z.literal("lark"),
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
@@ -144,6 +153,10 @@ export const notificationsMap = {
icon: ,
label: "ntfy",
},
+ mattermost: {
+ icon: ,
+ label: "Mattermost",
+ },
};
export type NotificationSchema = z.infer;
@@ -177,6 +190,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
api.notification.testGotifyConnection.useMutation();
const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } =
api.notification.testNtfyConnection.useMutation();
+ const { mutateAsync: testMattermostConnection, isLoading: isLoadingMattermost } =
+ api.notification.testMattermostConnection.useMutation();
const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } =
api.notification.testLarkConnection.useMutation();
const slackMutation = notificationId
@@ -197,6 +212,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
const ntfyMutation = notificationId
? api.notification.updateNtfy.useMutation()
: api.notification.createNtfy.useMutation();
+ const mattermostMutation = notificationId
+ ? api.notification.updateMattermost.useMutation()
+ : api.notification.createMattermost.useMutation();
const larkMutation = notificationId
? api.notification.updateLark.useMutation()
: api.notification.createLark.useMutation();
@@ -323,6 +341,20 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
+ } else if (notification.notificationType === "mattermost") {
+ form.reset({
+ appBuildError: notification.appBuildError,
+ appDeploy: notification.appDeploy,
+ dokployRestart: notification.dokployRestart,
+ databaseBackup: notification.databaseBackup,
+ type: notification.notificationType,
+ webhookUrl: notification.mattermost?.webhookUrl,
+ channel: notification.mattermost?.channel || "",
+ username: notification.mattermost?.username || "",
+ name: notification.name,
+ dockerCleanup: notification.dockerCleanup,
+ serverThreshold: notification.serverThreshold,
+ });
}
} else {
form.reset();
@@ -336,6 +368,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
email: emailMutation,
gotify: gotifyMutation,
ntfy: ntfyMutation,
+ mattermost: mattermostMutation,
lark: larkMutation,
};
@@ -440,6 +473,21 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "",
ntfyId: notification?.ntfyId || "",
});
+ } else if (data.type === "mattermost") {
+ promise = mattermostMutation.mutateAsync({
+ appBuildError: appBuildError,
+ appDeploy: appDeploy,
+ dokployRestart: dokployRestart,
+ databaseBackup: databaseBackup,
+ webhookUrl: data.webhookUrl,
+ channel: data.channel || undefined,
+ username: data.username || undefined,
+ name: data.name,
+ dockerCleanup: dockerCleanup,
+ notificationId: notificationId || "",
+ mattermostId: notification?.mattermostId || "",
+ serverThreshold: serverThreshold,
+ });
} else if (data.type === "lark") {
promise = larkMutation.mutateAsync({
appBuildError: appBuildError,
@@ -451,6 +499,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
larkId: notification?.larkId || "",
+
+
serverThreshold: serverThreshold,
});
}
@@ -1040,6 +1090,60 @@ export const HandleNotifications = ({ notificationId }: Props) => {
>
)}
+ {type === "mattermost" && (
+ <>
+ (
+
+ Webhook URL
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ Channel
+
+
+
+
+ Optional. Channel to post to (without #).
+
+
+
+ )}
+ />
+
+ (
+
+ Username
+
+
+
+
+ Optional. Display name for the webhook.
+
+
+
+ )}
+ />
+ >
+ )}
{type === "lark" && (
<>
{
isLoadingEmail ||
isLoadingGotify ||
isLoadingNtfy ||
+ isLoadingMattermost ||
isLoadingLark
}
variant="secondary"
@@ -1255,6 +1360,12 @@ export const HandleNotifications = ({ notificationId }: Props) => {
accessToken: form.getValues("accessToken"),
priority: form.getValues("priority"),
});
+ } else if (type === "mattermost") {
+ await testMattermostConnection({
+ webhookUrl: form.getValues("webhookUrl"),
+ channel: form.getValues("channel") || undefined,
+ username: form.getValues("username") || undefined,
+ });
} else if (type === "lark") {
await testLarkConnection({
webhookUrl: form.getValues("webhookUrl"),
diff --git a/apps/dokploy/components/icons/notification-icons.tsx b/apps/dokploy/components/icons/notification-icons.tsx
index cc54327a8..d73186f2b 100644
--- a/apps/dokploy/components/icons/notification-icons.tsx
+++ b/apps/dokploy/components/icons/notification-icons.tsx
@@ -88,6 +88,20 @@ export const DiscordIcon = ({ className }: Props) => {
);
};
+
+export const MattermostIcon = ({ className }: Props) => {
+ return (
+
+ );
+};
export const LarkIcon = ({ className }: Props) => {
return (