mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-09 13:35:19 +00:00
feat(notifications): add Microsoft Teams integration for notifications
- Introduced support for Microsoft Teams notifications, including the ability to create, update, and test connections for Teams notifications. - Updated the notification schema to include Teams as a notification type. - Added Teams icon and UI components for handling Teams notifications in the dashboard. - Implemented backend logic for creating and updating Teams notifications, along with necessary database schema changes. - Enhanced existing notification functionalities to support Teams notifications across various events (e.g., build success, failure, database backups).
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
PushoverIcon,
|
||||
ResendIcon,
|
||||
SlackIcon,
|
||||
TeamsIcon,
|
||||
TelegramIcon,
|
||||
} from "@/components/icons/notification-icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -164,6 +165,12 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
||||
})
|
||||
.merge(notificationBaseSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("teams"),
|
||||
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
||||
})
|
||||
.merge(notificationBaseSchema),
|
||||
]);
|
||||
|
||||
export const notificationsMap = {
|
||||
@@ -183,6 +190,10 @@ export const notificationsMap = {
|
||||
icon: <LarkIcon className="text-muted-foreground" />,
|
||||
label: "Lark",
|
||||
},
|
||||
teams: {
|
||||
icon: <TeamsIcon className="text-muted-foreground" />,
|
||||
label: "Microsoft Teams",
|
||||
},
|
||||
email: {
|
||||
icon: <Mail size={29} className="text-muted-foreground" />,
|
||||
label: "Email",
|
||||
@@ -244,6 +255,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
api.notification.testNtfyConnection.useMutation();
|
||||
const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } =
|
||||
api.notification.testLarkConnection.useMutation();
|
||||
const { mutateAsync: testTeamsConnection, isLoading: isLoadingTeams } =
|
||||
api.notification.testTeamsConnection.useMutation();
|
||||
|
||||
const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } =
|
||||
api.notification.testCustomConnection.useMutation();
|
||||
@@ -278,6 +291,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
const larkMutation = notificationId
|
||||
? api.notification.updateLark.useMutation()
|
||||
: api.notification.createLark.useMutation();
|
||||
const teamsMutation = notificationId
|
||||
? api.notification.updateTeams.useMutation()
|
||||
: api.notification.createTeams.useMutation();
|
||||
const pushoverMutation = notificationId
|
||||
? api.notification.updatePushover.useMutation()
|
||||
: api.notification.createPushover.useMutation();
|
||||
@@ -435,6 +451,19 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
volumeBackup: notification.volumeBackup,
|
||||
serverThreshold: notification.serverThreshold,
|
||||
});
|
||||
} else if (notification.notificationType === "teams") {
|
||||
form.reset({
|
||||
appBuildError: notification.appBuildError,
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
type: notification.notificationType,
|
||||
webhookUrl: notification.teams?.webhookUrl,
|
||||
name: notification.name,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
serverThreshold: notification.serverThreshold,
|
||||
});
|
||||
} else if (notification.notificationType === "custom") {
|
||||
form.reset({
|
||||
appBuildError: notification.appBuildError,
|
||||
@@ -488,6 +517,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
gotify: gotifyMutation,
|
||||
ntfy: ntfyMutation,
|
||||
lark: larkMutation,
|
||||
teams: teamsMutation,
|
||||
custom: customMutation,
|
||||
pushover: pushoverMutation,
|
||||
};
|
||||
@@ -630,6 +660,20 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
larkId: notification?.larkId || "",
|
||||
serverThreshold: serverThreshold,
|
||||
});
|
||||
} else if (data.type === "teams") {
|
||||
promise = teamsMutation.mutateAsync({
|
||||
appBuildError: appBuildError,
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
webhookUrl: data.webhookUrl,
|
||||
name: data.name,
|
||||
dockerCleanup: dockerCleanup,
|
||||
notificationId: notificationId || "",
|
||||
teamsId: notification?.teamsId || "",
|
||||
serverThreshold: serverThreshold,
|
||||
});
|
||||
} else if (data.type === "custom") {
|
||||
// Convert headers array to object
|
||||
const headersRecord =
|
||||
@@ -1465,6 +1509,32 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "teams" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://xxx.webhook.office.com/webhookb2/..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Incoming Webhook URL from a Teams channel. Add an
|
||||
Incoming Webhook in your channel settings to get the
|
||||
URL.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === "pushover" && (
|
||||
<>
|
||||
<FormField
|
||||
@@ -1780,6 +1850,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
isLoadingGotify ||
|
||||
isLoadingNtfy ||
|
||||
isLoadingLark ||
|
||||
isLoadingTeams ||
|
||||
isLoadingCustom ||
|
||||
isLoadingPushover
|
||||
}
|
||||
@@ -1841,6 +1912,10 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
await testLarkConnection({
|
||||
webhookUrl: data.webhookUrl,
|
||||
});
|
||||
} else if (data.type === "teams") {
|
||||
await testTeamsConnection({
|
||||
webhookUrl: data.webhookUrl,
|
||||
});
|
||||
} else if (data.type === "custom") {
|
||||
const headersRecord =
|
||||
data.headers && data.headers.length > 0
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
NtfyIcon,
|
||||
ResendIcon,
|
||||
SlackIcon,
|
||||
TeamsIcon,
|
||||
TelegramIcon,
|
||||
} from "@/components/icons/notification-icons";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
@@ -37,7 +38,7 @@ export const ShowNotifications = () => {
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Add your providers to receive notifications, like Discord, Slack,
|
||||
Telegram, Email, Resend, Lark.
|
||||
Telegram, Teams, Email, Resend, Lark.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
@@ -112,6 +113,11 @@ export const ShowNotifications = () => {
|
||||
<LarkIcon className="size-7 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{notification.notificationType === "teams" && (
|
||||
<div className="flex items-center justify-center rounded-lg">
|
||||
<TeamsIcon className="size-7 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notification.name}
|
||||
</span>
|
||||
|
||||
@@ -88,6 +88,35 @@ export const DiscordIcon = ({ className }: Props) => {
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const TeamsIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="26"
|
||||
height="36"
|
||||
viewBox="0 0 512 476"
|
||||
className={cn("size-9", className)}
|
||||
>
|
||||
<g>
|
||||
<rect x="116" y="50" width="280" height="276" rx="64" fill="#6264A7" />
|
||||
<rect x="236" y="138" width="180" height="224" rx="60" fill="#5059C9" />
|
||||
<circle cx="122" cy="332" r="80" fill="#B2B4D3" />
|
||||
<circle cx="370" cy="364" r="64" fill="#A6A7DC" />
|
||||
<text
|
||||
x="180"
|
||||
y="270"
|
||||
fill="#fff"
|
||||
font-family="Segoe UI, Arial, sans-serif"
|
||||
font-size="110"
|
||||
font-weight="bold"
|
||||
>
|
||||
T
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const LarkIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TYPE "public"."notificationType" ADD VALUE 'teams';--> statement-breakpoint
|
||||
CREATE TABLE "teams" (
|
||||
"teamsId" text PRIMARY KEY NOT NULL,
|
||||
"webhookUrl" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD COLUMN "teamsId" text;--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD CONSTRAINT "notification_teamsId_teams_teamsId_fk" FOREIGN KEY ("teamsId") REFERENCES "public"."teams"("teamsId") ON DELETE cascade ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1009,6 +1009,13 @@
|
||||
"when": 1770961667210,
|
||||
"tag": "0143_brown_ultron",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 144,
|
||||
"version": "7",
|
||||
"when": 1771297084611,
|
||||
"tag": "0144_odd_gunslinger",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
createPushoverNotification,
|
||||
createResendNotification,
|
||||
createSlackNotification,
|
||||
createTeamsNotification,
|
||||
createTelegramNotification,
|
||||
findNotificationById,
|
||||
getWebServerSettings,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
sendResendNotification,
|
||||
sendServerThresholdNotifications,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
updateCustomNotification,
|
||||
updateDiscordNotification,
|
||||
@@ -33,6 +35,7 @@ import {
|
||||
updatePushoverNotification,
|
||||
updateResendNotification,
|
||||
updateSlackNotification,
|
||||
updateTeamsNotification,
|
||||
updateTelegramNotification,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -55,6 +58,7 @@ import {
|
||||
apiCreatePushover,
|
||||
apiCreateResend,
|
||||
apiCreateSlack,
|
||||
apiCreateTeams,
|
||||
apiCreateTelegram,
|
||||
apiFindOneNotification,
|
||||
apiTestCustomConnection,
|
||||
@@ -66,6 +70,7 @@ import {
|
||||
apiTestPushoverConnection,
|
||||
apiTestResendConnection,
|
||||
apiTestSlackConnection,
|
||||
apiTestTeamsConnection,
|
||||
apiTestTelegramConnection,
|
||||
apiUpdateCustom,
|
||||
apiUpdateDiscord,
|
||||
@@ -76,6 +81,7 @@ import {
|
||||
apiUpdatePushover,
|
||||
apiUpdateResend,
|
||||
apiUpdateSlack,
|
||||
apiUpdateTeams,
|
||||
apiUpdateTelegram,
|
||||
notifications,
|
||||
server,
|
||||
@@ -413,6 +419,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
orderBy: desc(notifications.createdAt),
|
||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||
@@ -705,6 +712,61 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createTeams: adminProcedure
|
||||
.input(apiCreateTeams)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createTeamsNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the notification",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateTeams: adminProcedure
|
||||
.input(apiUpdateTeams)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
if (
|
||||
IS_CLOUD &&
|
||||
notification.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateTeamsNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testTeamsConnection: adminProcedure
|
||||
.input(apiTestTeamsConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
await sendTeamsNotification(input, {
|
||||
title: "🤚 Test Notification",
|
||||
facts: [{ name: "Message", value: "Hi, From Dokploy 👋" }],
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
createPushover: adminProcedure
|
||||
.input(apiCreatePushover)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
Reference in New Issue
Block a user