Add Google Chat webhook to notification channels

- Added Google Chat  as a new channel type option in the Notification Channel
- Add Google Chat tab to notifications
- Fix: Ensure Google Chat and Email save record to PB
This commit is contained in:
Tola Leng
2025-08-05 21:35:56 +07:00
parent 96bf456338
commit 99d67deb10
4 changed files with 210 additions and 40 deletions
@@ -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" && (
<>
<FormField
@@ -405,6 +417,25 @@ export const NotificationChannelDialog = ({
/>
)}
{notificationType === "google_chat" && (
<FormField
control={form.control}
name="google_chat_webhook_url"
render={({ field }) => (
<FormItem>
<FormLabel>Google Chat Webhook URL</FormLabel>
<FormControl>
<Input placeholder="https://chat.googleapis.com/v1/spaces/..." {...field} />
</FormControl>
<FormDescription>
Google Chat webhook URL from your Google Chat space
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{notificationType === "email" && (
<>
<FormField
@@ -597,7 +628,6 @@ export const NotificationChannelDialog = ({
)}
/>
{/* Payload Template Section */}
<div className="space-y-4">
<FormField
control={form.control}
@@ -13,9 +13,17 @@ import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { alertConfigService } from "@/services/alertConfigService";
import { pb } from "@/lib/pocketbase";
interface CombinedChannel extends Partial<AlertConfiguration> {
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 = ({
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Details</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead className="text-right">Actions</TableHead>
@@ -78,11 +125,14 @@ export const NotificationChannelList = ({
<TableCell>
<Badge variant="outline">{getChannelTypeLabel(channel.notification_type)}</Badge>
</TableCell>
<TableCell className="max-w-xs truncate text-sm text-muted-foreground">
{getChannelDetails(channel)}
</TableCell>
<TableCell>
<Switch
checked={
typeof channel.enabled === 'string'
? channel.enabled === "true"
? channel.enabled === "true" || channel.enabled === "on"
: !!channel.enabled
}
onCheckedChange={() => toggleEnabled(channel)}
@@ -96,7 +146,8 @@ export const NotificationChannelList = ({
<Button
variant="outline"
size="sm"
onClick={() => onEdit(channel)}
onClick={() => onEdit(channel as AlertConfiguration)}
disabled={channel.isWebhook} // Disable edit for webhooks for now
>
<Edit className="h-4 w-4" />
</Button>
@@ -119,4 +170,4 @@ export const NotificationChannelList = ({
</Table>
</div>
);
};
};
@@ -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<AlertConfiguration> {
isWebhook?: boolean;
url?: string;
method?: string;
description?: string;
}
const NotificationSettings = () => {
const [isLoading, setIsLoading] = useState(true);
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [webhookConfigs, setWebhookConfigs] = useState<WebhookConfiguration[]>([]);
const [dialogOpen, setDialogOpen] = useState(false);
const [currentTab, setCurrentTab] = useState<string>("all");
const [editingConfig, setEditingConfig] = useState<AlertConfiguration | null>(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 = () => {
<TabsTrigger value="discord">Discord</TabsTrigger>
<TabsTrigger value="slack">Slack</TabsTrigger>
<TabsTrigger value="signal">Signal</TabsTrigger>
<TabsTrigger value="google_chat">Google Chat</TabsTrigger>
<TabsTrigger value="email">Email</TabsTrigger>
<TabsTrigger value="webhook">Webhook</TabsTrigger>
</TabsList>
+39 -14
View File
@@ -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<AlertConfiguration[]> {
// 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<AlertConfiguration, 'id' | 'collectionId' | 'collectionName' | 'created' | 'updated'>): Promise<AlertConfiguration | null> {
// 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<AlertConfiguration>): Promise<AlertConfiguration | null> {
// 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<boolean> {
// 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",