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:
+35
-5
@@ -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}
|
||||
|
||||
+64
-13
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user