feat: Support multiple notification channels

- Update the Notification Settings dashboard to allow users to select multiple notification channels for alerts.
- Add webhook to notification channels: Add webhook as a selectable option in the notification channel type dropdown.
- Implement template type selection in the Add Template dialog. Based on the selected type.
This commit is contained in:
Tola Leng
2025-08-02 16:53:15 +07:00
parent f7cf2fcc20
commit 914f1aba60
24 changed files with 1712 additions and 1049 deletions
@@ -1,4 +1,3 @@
import React, { useEffect } from "react";
import {
Dialog,
@@ -8,8 +7,9 @@ import {
DialogFooter
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { AlertConfiguration, alertConfigService } from "@/services/alertConfigService";
import { WebhookConfiguration, webhookService } from "@/services/webhookService";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -36,7 +36,7 @@ const baseSchema = z.object({
notify_name: z.string().min(1, "Name is required"),
notification_type: z.enum(["telegram", "discord", "slack", "signal", "email"]),
enabled: z.boolean().default(true),
service_id: z.string().default("global"), // Assuming global for now, could be linked to specific services
service_id: z.string().default("global"),
template_id: z.string().optional(),
});
@@ -63,7 +63,19 @@ const signalSchema = baseSchema.extend({
const emailSchema = baseSchema.extend({
notification_type: z.literal("email"),
// Email specific fields could be added here
email_address: z.string().email("Valid email is required"),
email_sender_name: z.string().min(1, "Sender name is required"),
smtp_server: z.string().min(1, "SMTP server is required"),
smtp_port: z.string().min(1, "SMTP port is required"),
});
const webhookSchema = baseSchema.extend({
notification_type: z.literal("webhook"),
webhook_url: z.string().url("Must be a valid URL"),
webhook_method: z.enum(["GET", "POST", "PUT", "PATCH"]).default("POST"),
webhook_secret: z.string().optional(),
webhook_headers: z.string().optional(),
webhook_description: z.string().optional(),
});
const formSchema = z.discriminatedUnion("notification_type", [
@@ -71,11 +83,21 @@ const formSchema = z.discriminatedUnion("notification_type", [
discordSchema,
slackSchema,
signalSchema,
emailSchema
emailSchema,
webhookSchema
]);
type FormValues = z.infer<typeof formSchema>;
const notificationTypeOptions = [
{ value: "telegram", label: "Telegram", description: "Send notifications via Telegram bot" },
{ value: "discord", label: "Discord", description: "Send notifications to Discord webhook" },
{ value: "slack", label: "Slack", description: "Send notifications to Slack webhook" },
{ value: "signal", label: "Signal", description: "Send notifications via Signal" },
{ value: "email", label: "Email", description: "Send notifications via email" },
{ value: "webhook", label: "Webhook", description: "Send notifications to custom webhook" },
];
export const NotificationChannelDialog = ({
open,
onClose,
@@ -126,16 +148,36 @@ export const NotificationChannelDialog = ({
const onSubmit = async (values: FormValues) => {
setIsSubmitting(true);
try {
// Ensure service_id is always present
const configData = {
...values,
service_id: values.service_id || "global",
};
if (isEditing && editingConfig?.id) {
await alertConfigService.updateAlertConfiguration(editingConfig.id, configData);
if (values.notification_type === "webhook") {
// Handle webhook creation/update separately
const webhookData: Omit<WebhookConfiguration, 'id' | 'collectionId' | 'collectionName' | 'created' | 'updated'> = {
name: values.notify_name,
url: values.webhook_url,
enabled: values.enabled ? "on" : "off",
method: values.webhook_method || "POST",
secret: values.webhook_secret || "",
headers: values.webhook_headers || "",
description: values.webhook_description || "",
user_id: "global", // or get current user id
};
if (isEditing && editingConfig?.id) {
await webhookService.updateWebhook(editingConfig.id, webhookData);
} else {
await webhookService.createWebhook(webhookData);
}
} else {
await alertConfigService.createAlertConfiguration(configData as any);
// Handle other notification types with existing service
const configData = {
...values,
service_id: values.service_id || "global",
};
if (isEditing && editingConfig?.id) {
await alertConfigService.updateAlertConfiguration(editingConfig.id, configData);
} else {
await alertConfigService.createAlertConfiguration(configData as any);
}
}
onClose(true); // Close with refresh
} finally {
@@ -159,7 +201,7 @@ export const NotificationChannelDialog = ({
name="notify_name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>Channel Name</FormLabel>
<FormControl>
<Input placeholder="My Notification Channel" {...field} />
</FormControl>
@@ -175,57 +217,25 @@ export const NotificationChannelDialog = ({
control={form.control}
name="notification_type"
render={({ field }) => (
<FormItem className="space-y-3">
<FormItem>
<FormLabel>Channel Type</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="telegram" />
</FormControl>
<FormLabel className="font-normal">
Telegram
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="discord" />
</FormControl>
<FormLabel className="font-normal">
Discord
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="slack" />
</FormControl>
<FormLabel className="font-normal">
Slack
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="signal" />
</FormControl>
<FormLabel className="font-normal">
Signal
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="email" />
</FormControl>
<FormLabel className="font-normal">
Email
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select notification type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{notificationTypeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex flex-col">
<span className="font-medium">{option.label}</span>
<span className="text-xs text-muted-foreground">{option.description}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
@@ -243,6 +253,9 @@ export const NotificationChannelDialog = ({
<FormControl>
<Input placeholder="Telegram Chat ID" {...field} />
</FormControl>
<FormDescription>
The Telegram chat ID to send notifications to
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -256,6 +269,9 @@ export const NotificationChannelDialog = ({
<FormControl>
<Input placeholder="Telegram Bot Token" {...field} type="password" />
</FormControl>
<FormDescription>
Your Telegram bot token from @BotFather
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -271,8 +287,11 @@ export const NotificationChannelDialog = ({
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input placeholder="Discord Webhook URL" {...field} />
<Input placeholder="https://discord.com/api/webhooks/..." {...field} />
</FormControl>
<FormDescription>
Discord webhook URL from your server settings
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -287,8 +306,11 @@ export const NotificationChannelDialog = ({
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input placeholder="Slack Webhook URL" {...field} />
<Input placeholder="https://hooks.slack.com/services/..." {...field} />
</FormControl>
<FormDescription>
Slack incoming webhook URL
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -305,11 +327,166 @@ export const NotificationChannelDialog = ({
<FormControl>
<Input placeholder="+1234567890" {...field} />
</FormControl>
<FormDescription>
Signal phone number to send notifications to
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{notificationType === "email" && (
<>
<FormField
control={form.control}
name="email_address"
render={({ field }) => (
<FormItem>
<FormLabel>Email Address</FormLabel>
<FormControl>
<Input placeholder="notifications@example.com" {...field} type="email" />
</FormControl>
<FormDescription>
Email address to send notifications to
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email_sender_name"
render={({ field }) => (
<FormItem>
<FormLabel>Sender Name</FormLabel>
<FormControl>
<Input placeholder="Alert System" {...field} />
</FormControl>
<FormDescription>
Display name for outgoing emails
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="smtp_server"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP Server</FormLabel>
<FormControl>
<Input placeholder="smtp.gmail.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtp_port"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP Port</FormLabel>
<FormControl>
<Input placeholder="587" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</>
)}
{notificationType === "webhook" && (
<>
<FormField
control={form.control}
name="webhook_url"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input placeholder="https://api.example.com/webhook" {...field} />
</FormControl>
<FormDescription>
The URL where webhook notifications will be sent
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="webhook_method"
render={({ field }) => (
<FormItem>
<FormLabel>HTTP Method</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select method" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="PATCH">PATCH</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="webhook_secret"
render={({ field }) => (
<FormItem>
<FormLabel>Secret (Optional)</FormLabel>
<FormControl>
<Input placeholder="webhook_secret_key" {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="webhook_headers"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Headers (Optional)</FormLabel>
<FormControl>
<Input placeholder='{"Authorization": "Bearer token"}' {...field} />
</FormControl>
<FormDescription>
JSON format for additional headers
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="webhook_description"
render={({ field }) => (
<FormItem>
<FormLabel>Description (Optional)</FormLabel>
<FormControl>
<Input placeholder="Description of this webhook" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
<FormField
control={form.control}
@@ -319,7 +496,7 @@ export const NotificationChannelDialog = ({
<div className="space-y-0.5">
<FormLabel>Enabled</FormLabel>
<FormDescription>
Turn notifications on or off
Enable or disable this notification channel
</FormDescription>
</div>
<FormControl>
@@ -338,7 +515,7 @@ export const NotificationChannelDialog = ({
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isEditing ? "Update" : "Create"}
{isEditing ? "Update Channel" : "Create Channel"}
</Button>
</DialogFooter>
</form>
@@ -346,4 +523,4 @@ export const NotificationChannelDialog = ({
</DialogContent>
</Dialog>
);
};
};