diff --git a/application/src/components/settings/notification-settings/NotificationChannelDialog.tsx b/application/src/components/settings/notification-settings/NotificationChannelDialog.tsx index cddf07b..367521c 100644 --- a/application/src/components/settings/notification-settings/NotificationChannelDialog.tsx +++ b/application/src/components/settings/notification-settings/NotificationChannelDialog.tsx @@ -1,4 +1,3 @@ - import React, { useEffect } from "react"; import { Dialog, @@ -38,7 +37,7 @@ interface NotificationChannelDialogProps { const baseSchema = z.object({ notify_name: z.string().min(1, "Name is required"), - notification_type: z.enum(["telegram", "discord", "slack", "signal", "email"]), + notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "webhook"]), enabled: z.boolean().default(true), service_id: z.string().default("global"), template_id: z.string().optional(), @@ -65,6 +64,11 @@ const signalSchema = baseSchema.extend({ signal_number: z.string().min(1, "Signal number is required"), }); +const googleChatSchema = baseSchema.extend({ + notification_type: z.literal("google_chat"), + google_chat_webhook_url: z.string().url("Must be a valid URL"), +}); + const emailSchema = baseSchema.extend({ notification_type: z.literal("email"), email_address: z.string().email("Valid email is required"), @@ -90,6 +94,7 @@ const formSchema = z.discriminatedUnion("notification_type", [ discordSchema, slackSchema, signalSchema, + googleChatSchema, emailSchema, webhookSchema ]); @@ -101,6 +106,7 @@ const notificationTypeOptions = [ { 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: "google_chat", label: "Google Chat", description: "Send notifications to Google Chat webhook" }, { value: "email", label: "Email", description: "Send notifications via email" }, { value: "webhook", label: "Webhook", description: "Send notifications to custom webhook" }, ]; @@ -236,7 +242,7 @@ export const NotificationChannelDialog = ({ await webhookService.createWebhook(webhookData); } } else { - // Handle other notification types with existing service + // Handle all other notification types including Google Chat and Email const configData = { ...values, service_id: values.service_id || "global", @@ -248,7 +254,14 @@ export const NotificationChannelDialog = ({ await alertConfigService.createAlertConfiguration(configData as any); } } + onClose(true); // Close with refresh + } catch (error) { + toast({ + title: "Error", + description: "Failed to save notification channel", + variant: "destructive" + }); } finally { setIsSubmitting(false); } @@ -310,7 +323,6 @@ export const NotificationChannelDialog = ({ )} /> - {/* Show different fields based on notification type */} {notificationType === "telegram" && ( <> )} + {notificationType === "google_chat" && ( + ( + + Google Chat Webhook URL + + + + + Google Chat webhook URL from your Google Chat space + + + + )} + /> + )} + {notificationType === "email" && ( <> - {/* Payload Template Section */}
{ + isWebhook?: boolean; + url?: string; + method?: string; + description?: string; +} interface NotificationChannelListProps { - channels: AlertConfiguration[]; + channels: CombinedChannel[]; onEdit: (config: AlertConfiguration) => void; onDelete: (id: string) => void; } @@ -25,25 +33,63 @@ export const NotificationChannelList = ({ onEdit, onDelete }: NotificationChannelListProps) => { - const toggleEnabled = async (config: AlertConfiguration) => { + const toggleEnabled = async (config: CombinedChannel) => { if (!config.id) return; - await alertConfigService.updateAlertConfiguration(config.id, { - enabled: !config.enabled - }); - - // The parent component will refresh the list - onEdit(config); + if (config.isWebhook) { + // Handle webhook toggle + try { + const newEnabled = config.enabled ? "off" : "on"; + await pb.collection('webhook').update(config.id, { + enabled: newEnabled + }); + // Trigger refresh by calling onEdit with empty config + onEdit({} as AlertConfiguration); + } catch (error) { + console.error("Error updating webhook:", error); + } + } else { + // Handle alert config toggle + await alertConfigService.updateAlertConfiguration(config.id, { + enabled: !config.enabled + }); + + // The parent component will refresh the list + onEdit(config as AlertConfiguration); + } }; - const getChannelTypeLabel = (type: string) => { + const getChannelTypeLabel = (type: string | undefined) => { switch(type) { case "telegram": return "Telegram"; case "discord": return "Discord"; case "slack": return "Slack"; case "signal": return "Signal"; + case "google_chat": return "Google Chat"; case "email": return "Email"; - default: return type; + case "webhook": return "Webhook"; + default: return type || "Unknown"; + } + }; + + const getChannelDetails = (config: CombinedChannel) => { + if (config.isWebhook) { + return `${config.method || 'POST'} ${config.url || ''}`; + } + + switch(config.notification_type) { + case "telegram": + return config.telegram_chat_id || ''; + case "discord": + case "slack": + case "google_chat": + return config.discord_webhook_url || config.slack_webhook_url || config.google_chat_webhook_url || ''; + case "signal": + return config.signal_number || ''; + case "email": + return config.email_address || ''; + default: + return ''; } }; @@ -66,6 +112,7 @@ export const NotificationChannelList = ({ Name Type + Details Status Created Actions @@ -78,11 +125,14 @@ export const NotificationChannelList = ({ {getChannelTypeLabel(channel.notification_type)} + + {getChannelDetails(channel)} + toggleEnabled(channel)} @@ -96,7 +146,8 @@ export const NotificationChannelList = ({ @@ -119,4 +170,4 @@ export const NotificationChannelList = ({
); -}; +}; \ No newline at end of file diff --git a/application/src/components/settings/notification-settings/NotificationSettings.tsx b/application/src/components/settings/notification-settings/NotificationSettings.tsx index 8f27a0d..70958a6 100644 --- a/application/src/components/settings/notification-settings/NotificationSettings.tsx +++ b/application/src/components/settings/notification-settings/NotificationSettings.tsx @@ -5,28 +5,47 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Plus, Loader2 } from "lucide-react"; import { AlertConfiguration, alertConfigService } from "@/services/alertConfigService"; +import { WebhookConfiguration, webhookService } from "@/services/webhookService"; import { NotificationChannelDialog } from "./NotificationChannelDialog"; import { NotificationChannelList } from "./NotificationChannelList"; +import { pb } from "@/lib/pocketbase"; + +interface CombinedChannel extends Partial { + isWebhook?: boolean; + url?: string; + method?: string; + description?: string; +} const NotificationSettings = () => { const [isLoading, setIsLoading] = useState(true); const [alertConfigs, setAlertConfigs] = useState([]); + const [webhookConfigs, setWebhookConfigs] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); const [currentTab, setCurrentTab] = useState("all"); const [editingConfig, setEditingConfig] = useState(null); - const fetchAlertConfigurations = async () => { + const fetchNotificationChannels = async () => { setIsLoading(true); try { + // Fetch alert configurations const configs = await alertConfigService.getAlertConfigurations(); setAlertConfigs(configs); + + // Fetch webhooks + try { + const webhookResponse = await pb.collection('webhook').getList(1, 50); + setWebhookConfigs(webhookResponse.items as WebhookConfiguration[]); + } catch (webhookError) { + setWebhookConfigs([]); + } } finally { setIsLoading(false); } }; useEffect(() => { - fetchAlertConfigurations(); + fetchNotificationChannels(); }, []); const handleAddNew = () => { @@ -40,22 +59,66 @@ const NotificationSettings = () => { }; const handleDelete = async (id: string) => { - const success = await alertConfigService.deleteAlertConfiguration(id); - if (success) { - fetchAlertConfigurations(); + // Check if it's a webhook first + const isWebhook = webhookConfigs.find(w => w.id === id); + + if (isWebhook) { + // Handle webhook deletion + if (confirm("Are you sure you want to delete this webhook?")) { + try { + await pb.collection('webhook').delete(id); + fetchNotificationChannels(); + } catch (error) { + console.error("Error deleting webhook:", error); + } + } + } else { + // Handle alert config deletion + const success = await alertConfigService.deleteAlertConfiguration(id); + if (success) { + fetchNotificationChannels(); + } } }; const handleDialogClose = (refreshList: boolean) => { setDialogOpen(false); if (refreshList) { - fetchAlertConfigurations(); + fetchNotificationChannels(); } }; + const getCombinedChannels = (): CombinedChannel[] => { + const combined: CombinedChannel[] = []; + + // Add alert configurations + alertConfigs.forEach(config => { + combined.push(config); + }); + + // Add webhooks as notification channels + webhookConfigs.forEach(webhook => { + combined.push({ + id: webhook.id, + notify_name: webhook.name, + notification_type: "webhook" as const, + enabled: webhook.enabled === "on", + created: webhook.created, + updated: webhook.updated, + isWebhook: true, + url: webhook.url, + method: webhook.method, + description: webhook.description + }); + }); + + return combined; + }; + const getFilteredConfigs = () => { - if (currentTab === "all") return alertConfigs; - return alertConfigs.filter(config => config.notification_type === currentTab); + const combined = getCombinedChannels(); + if (currentTab === "all") return combined; + return combined.filter(config => config.notification_type === currentTab); }; return ( @@ -86,6 +149,7 @@ const NotificationSettings = () => { Discord Slack Signal + Google Chat Email Webhook diff --git a/application/src/services/alertConfigService.ts b/application/src/services/alertConfigService.ts index 98dda94..e845696 100644 --- a/application/src/services/alertConfigService.ts +++ b/application/src/services/alertConfigService.ts @@ -7,7 +7,7 @@ export interface AlertConfiguration { collectionId?: string; collectionName?: string; service_id: string; - notification_type: "telegram" | "discord" | "signal" | "slack" | "email"; + notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "webhook"; telegram_chat_id?: string; discord_webhook_url?: string; signal_number?: string; @@ -15,6 +15,7 @@ export interface AlertConfiguration { bot_token?: string; template_id?: string; slack_webhook_url?: string; + google_chat_webhook_url?: string; enabled: boolean; created?: string; updated?: string; @@ -29,13 +30,10 @@ export interface AlertConfiguration { export const alertConfigService = { async getAlertConfigurations(): Promise { - // console.info("Fetching alert configurations"); try { const response = await pb.collection('alert_configurations').getList(1, 50); - // console.info("Alert configurations response:", response); return response.items as AlertConfiguration[]; } catch (error) { - // console.error("Error fetching alert configurations:", error); toast({ title: "Error", description: "Failed to load notification settings", @@ -46,17 +44,50 @@ export const alertConfigService = { }, async createAlertConfiguration(config: Omit): Promise { - // console.info("Creating alert configuration:", config); + try { - const result = await pb.collection('alert_configurations').create(config); - // console.info("Alert configuration created:", result); + // Build the configuration object with proper field mapping + const cleanConfig: any = { + service_id: config.service_id || "global", + notification_type: config.notification_type, + notify_name: config.notify_name, + enabled: config.enabled, + template_id: config.template_id || "", + }; + + // Add type-specific fields based on notification type + if (config.notification_type === "telegram") { + cleanConfig.telegram_chat_id = config.telegram_chat_id || ""; + cleanConfig.bot_token = config.bot_token || ""; + } else if (config.notification_type === "discord") { + cleanConfig.discord_webhook_url = config.discord_webhook_url || ""; + } else if (config.notification_type === "slack") { + cleanConfig.slack_webhook_url = config.slack_webhook_url || ""; + } else if (config.notification_type === "signal") { + cleanConfig.signal_number = config.signal_number || ""; + } else if (config.notification_type === "google_chat") { + cleanConfig.google_chat_webhook_url = config.google_chat_webhook_url || ""; + } else if (config.notification_type === "email") { + + cleanConfig.email_address = config.email_address || ""; + cleanConfig.email_sender_name = config.email_sender_name || ""; + cleanConfig.smtp_server = config.smtp_server || ""; + cleanConfig.smtp_port = config.smtp_port || ""; + + } + + const result = await pb.collection('alert_configurations').create(cleanConfig); + toast({ title: "Success", description: "Notification channel created successfully", }); return result as AlertConfiguration; } catch (error) { - // console.error("Error creating alert configuration:", error); + // Try to get more details from the error + if (error && typeof error === 'object') { + } + toast({ title: "Error", description: "Failed to create notification channel", @@ -67,17 +98,14 @@ export const alertConfigService = { }, async updateAlertConfiguration(id: string, config: Partial): Promise { - // console.info(`Updating alert configuration ${id}:`, config); try { const result = await pb.collection('alert_configurations').update(id, config); - // console.info("Alert configuration updated:", result); toast({ title: "Success", description: "Notification channel updated successfully", }); return result as AlertConfiguration; } catch (error) { - // console.error("Error updating alert configuration:", error); toast({ title: "Error", description: "Failed to update notification channel", @@ -88,17 +116,14 @@ export const alertConfigService = { }, async deleteAlertConfiguration(id: string): Promise { - // console.info(`Deleting alert configuration ${id}`); try { await pb.collection('alert_configurations').delete(id); - // console.info("Alert configuration deleted"); toast({ title: "Success", description: "Notification channel removed", }); return true; } catch (error) { - // console.error("Error deleting alert configuration:", error); toast({ title: "Error", description: "Failed to remove notification channel",