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",