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,9 +1,10 @@
import React, { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { templateService } from "@/services/templateService";
import { templateService, TemplateType } from "@/services/templateService";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Plus, RefreshCcw } from "lucide-react";
import { TemplateList } from "./TemplateList";
import { TemplateDialog } from "./TemplateDialog";
@@ -11,8 +12,10 @@ import { useToast } from "@/hooks/use-toast";
export const AlertsTemplates = () => {
const { toast } = useToast();
const [activeTab, setActiveTab] = useState<TemplateType>('service');
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<string | null>(null);
const [editingTemplateType, setEditingTemplateType] = useState<TemplateType | null>(null);
const {
data: templates = [],
@@ -20,17 +23,19 @@ export const AlertsTemplates = () => {
error,
refetch
} = useQuery({
queryKey: ['notification_templates'],
queryFn: templateService.getTemplates,
queryKey: ['notification_templates', activeTab],
queryFn: () => templateService.getTemplates(activeTab),
});
const handleAddTemplate = () => {
const handleAddTemplate = (templateType: TemplateType) => {
setEditingTemplate(null);
setEditingTemplateType(templateType);
setIsDialogOpen(true);
};
const handleEditTemplate = (id: string) => {
const handleEditTemplate = (id: string, templateType: TemplateType) => {
setEditingTemplate(id);
setEditingTemplateType(templateType);
setIsDialogOpen(true);
};
@@ -51,33 +56,83 @@ export const AlertsTemplates = () => {
<RefreshCcw className="h-4 w-4 mr-2" />
Refresh
</Button>
<Button onClick={handleAddTemplate}>
<Button onClick={() => handleAddTemplate(activeTab)}>
<Plus className="h-4 w-4 mr-2" />
Add Template
</Button>
</div>
</CardHeader>
<CardContent>
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={handleEditTemplate}
refetchTemplates={refetch}
/>
)}
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TemplateType)}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="service">Service Uptime</TabsTrigger>
<TabsTrigger value="server">Server Monitoring</TabsTrigger>
<TabsTrigger value="ssl">SSL Certificate</TabsTrigger>
</TabsList>
<TabsContent value="service" className="mt-4">
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading service templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={(id) => handleEditTemplate(id, 'service')}
refetchTemplates={refetch}
templateType="service"
/>
)}
</TabsContent>
<TabsContent value="server" className="mt-4">
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading server templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={(id) => handleEditTemplate(id, 'server')}
refetchTemplates={refetch}
templateType="server"
/>
)}
</TabsContent>
<TabsContent value="ssl" className="mt-4">
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading SSL templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={(id) => handleEditTemplate(id, 'ssl')}
refetchTemplates={refetch}
templateType="ssl"
/>
)}
</TabsContent>
</Tabs>
</CardContent>
<TemplateDialog
open={isDialogOpen}
templateId={editingTemplate}
templateType={editingTemplateType}
onOpenChange={setIsDialogOpen}
onSuccess={() => {
refetch();
@@ -86,4 +141,4 @@ export const AlertsTemplates = () => {
/>
</Card>
);
};
};
@@ -1,19 +1,25 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useTemplateForm } from "./hooks/useTemplateForm";
import { BasicTemplateFields } from "./form/BasicTemplateFields";
import { MessagesTabContent } from "./form/MessagesTabContent";
import { PlaceholdersTabContent } from "./form/PlaceholdersTabContent";
import { ServerTemplateFields } from "./form/ServerTemplateFields";
import { ServiceTemplateFields } from "./form/ServiceTemplateFields";
import { SslTemplateFields } from "./form/SslTemplateFields";
import { Loader2, ChevronDown } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { TemplateType, templateTypeConfigs } from "@/services/templateService";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
interface TemplateDialogProps {
open: boolean;
templateId: string | null;
templateType: TemplateType | null;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
@@ -21,9 +27,12 @@ interface TemplateDialogProps {
export const TemplateDialog: React.FC<TemplateDialogProps> = ({
open,
templateId,
templateType: initialTemplateType,
onOpenChange,
onSuccess,
}) => {
const [selectedTemplateType, setSelectedTemplateType] = useState<TemplateType>(initialTemplateType || 'service');
const {
form,
isEditMode,
@@ -32,28 +41,70 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
onSubmit
} = useTemplateForm({
templateId,
templateType: selectedTemplateType,
open,
onOpenChange,
onSuccess
});
// For debugging purposes
// Update template type when prop changes or dialog opens
useEffect(() => {
if (open) {
// console.log("Template dialog opened. Edit mode:", isEditMode, "Template ID:", templateId);
// Log form values when they change
const subscription = form.watch((value) => {
// console.log("Current form values:", value);
});
return () => subscription.unsubscribe();
if (initialTemplateType) {
setSelectedTemplateType(initialTemplateType);
} else if (open && !isEditMode) {
setSelectedTemplateType('service');
}
}, [open, isEditMode, templateId, form]);
}, [initialTemplateType, open, isEditMode]);
// Handle template type change
const handleTemplateTypeChange = (newType: TemplateType) => {
if (!isEditMode) {
setSelectedTemplateType(newType);
form.setValue('templateType', newType);
}
};
const renderTemplateFields = () => {
switch (selectedTemplateType) {
case 'server':
return <ServerTemplateFields control={form.control} />;
case 'service':
return <ServiceTemplateFields control={form.control} />;
case 'ssl':
return <SslTemplateFields control={form.control} />;
default:
return null;
}
};
const renderPlaceholderGuide = () => {
const config = templateTypeConfigs[selectedTemplateType];
if (!config) return null;
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Available Placeholders</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-3">
{config.description}. Use these placeholders in your messages:
</p>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-sm">
{config.placeholders.map((placeholder) => (
<div key={placeholder} className="bg-muted/30 p-2 rounded">
<code className="text-xs">{placeholder}</code>
</div>
))}
</div>
</CardContent>
</Card>
);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col overflow-hidden">
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col overflow-hidden">
<DialogHeader>
<DialogTitle>{isEditMode ? "Edit Template" : "Add Template"}</DialogTitle>
</DialogHeader>
@@ -69,7 +120,69 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
<div className="relative flex-1">
<ScrollArea className="pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}>
<div className="space-y-6 pb-6 pr-4">
<BasicTemplateFields control={form.control} />
{/* Basic Fields */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Template Name</FormLabel>
<FormControl>
<Input
placeholder="Enter template name"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="templateType"
render={({ field }) => (
<FormItem>
<FormLabel>Template Type</FormLabel>
<FormControl>
<Select
onValueChange={handleTemplateTypeChange}
value={selectedTemplateType}
disabled={isEditMode}
>
<SelectTrigger>
<SelectValue placeholder="Select template type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="server">Server Monitoring</SelectItem>
<SelectItem value="service">Service Uptime</SelectItem>
<SelectItem value="ssl">SSL Certificate</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Placeholder</FormLabel>
<FormControl>
<Input
placeholder="Optional custom placeholder"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Tabs defaultValue="messages">
<TabsList className="grid w-full grid-cols-2">
@@ -78,11 +191,11 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
</TabsList>
<TabsContent value="messages" className="pt-4">
<MessagesTabContent control={form.control} />
{renderTemplateFields()}
</TabsContent>
<TabsContent value="placeholders" className="pt-4">
<PlaceholdersTabContent control={form.control} />
{renderPlaceholderGuide()}
</TabsContent>
</Tabs>
</div>
@@ -120,4 +233,4 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
</DialogContent>
</Dialog>
);
};
};
@@ -1,17 +1,15 @@
import React, { useState } from "react";
import { NotificationTemplate, templateService } from "@/services/templateService";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Edit, Trash2 } from "lucide-react";
import {
import { Badge } from "@/components/ui/badge";
import { Trash2, Edit, MoreVertical } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
@@ -22,63 +20,75 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useToast } from "@/hooks/use-toast";
import { Badge } from "@/components/ui/badge";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { templateService, AnyTemplate, TemplateType } from "@/services/templateService";
interface TemplateListProps {
templates: NotificationTemplate[];
templates: AnyTemplate[];
isLoading: boolean;
onEdit: (id: string) => void;
refetchTemplates: () => void;
templateType: TemplateType;
}
export const TemplateList: React.FC<TemplateListProps> = ({
templates,
isLoading,
onEdit,
export const TemplateList: React.FC<TemplateListProps> = ({
templates,
isLoading,
onEdit,
refetchTemplates,
templateType
}) => {
const { toast } = useToast();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [templateToDelete, setTemplateToDelete] = useState<NotificationTemplate | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const queryClient = useQueryClient();
const [deleteTemplateId, setDeleteTemplateId] = useState<string | null>(null);
const handleDeletePrompt = (template: NotificationTemplate) => {
setTemplateToDelete(template);
setDeleteDialogOpen(true);
};
const handleDeleteTemplate = async () => {
if (!templateToDelete) return;
setIsDeleting(true);
try {
await templateService.deleteTemplate(templateToDelete.id);
// Delete mutation
const deleteMutation = useMutation({
mutationFn: (id: string) => templateService.deleteTemplate(id, templateType),
onSuccess: () => {
toast({
title: "Template deleted",
description: `Template "${templateToDelete.name}" has been removed.`,
description: "The template has been deleted successfully.",
});
queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] });
refetchTemplates();
} catch (error) {
console.error("Error deleting template:", error);
},
onError: (error) => {
// console.error("Error deleting template:", error);
toast({
title: "Error",
description: "Failed to delete template. Please try again.",
variant: "destructive",
});
} finally {
setIsDeleting(false);
setDeleteDialogOpen(false);
setTemplateToDelete(null);
},
});
const handleDelete = (id: string) => {
setDeleteTemplateId(id);
};
const confirmDelete = () => {
if (deleteTemplateId) {
deleteMutation.mutate(deleteTemplateId);
setDeleteTemplateId(null);
}
};
if (isLoading) {
return (
<div className="py-8 flex justify-center">
<div className="animate-pulse flex flex-col items-center">
<div className="h-4 bg-gray-200 rounded w-32 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-64"></div>
</div>
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="flex items-center justify-between p-4 border border-border rounded-lg">
<div className="space-y-2">
<div className="h-4 bg-muted animate-pulse rounded w-32"></div>
<div className="h-3 bg-muted animate-pulse rounded w-48"></div>
</div>
<div className="flex space-x-2">
<div className="h-8 w-8 bg-muted animate-pulse rounded"></div>
<div className="h-8 w-8 bg-muted animate-pulse rounded"></div>
</div>
</div>
))}
</div>
);
}
@@ -86,83 +96,86 @@ export const TemplateList: React.FC<TemplateListProps> = ({
if (templates.length === 0) {
return (
<div className="text-center py-8">
<p className="text-muted-foreground mb-2">No templates found</p>
<p className="text-sm text-muted-foreground">
Create your first notification template to get started.
</p>
<p className="text-muted-foreground">No templates found. Create your first template to get started.</p>
</div>
);
}
const getTemplateTypeLabel = (type: TemplateType) => {
switch (type) {
case 'server': return 'Server';
case 'service': return 'Service';
case 'ssl': return 'SSL';
default: return 'Unknown';
}
};
return (
<>
<div className="border rounded-md overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Created</TableHead>
<TableHead className="w-24">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{templates.map((template) => (
<TableRow key={template.id}>
<TableCell className="font-medium">{template.name}</TableCell>
<TableCell>
<Badge variant="outline">{template.type}</Badge>
</TableCell>
<TableCell>
{new Date(template.created).toLocaleDateString()}
</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button
size="sm"
variant="ghost"
onClick={() => onEdit(template.id)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDeletePrompt(template)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="space-y-4">
{templates.map((template) => (
<div key={template.id} className="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors">
<div className="space-y-1">
<div className="flex items-center gap-2">
<h3 className="font-medium">{template.name}</h3>
<Badge variant="outline">{getTemplateTypeLabel(templateType)}</Badge>
</div>
<p className="text-sm text-muted-foreground">
Created: {new Date(template.created).toLocaleDateString()}
{template.updated !== template.created &&
` • Updated: ${new Date(template.updated).toLocaleDateString()}`
}
</p>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => onEdit(template.id)}
>
<Edit className="h-4 w-4 mr-1" />
Edit
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => handleDelete(template.id)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
</div>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialog open={!!deleteTemplateId} onOpenChange={() => setDeleteTemplateId(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Template</AlertDialogTitle>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete the template "{templateToDelete?.name}"? This action cannot be undone.
This action cannot be undone. This will permanently delete the template.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
handleDeleteTemplate();
}}
disabled={isDeleting}
onClick={confirmDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? "Deleting..." : "Delete"}
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};
};
@@ -0,0 +1,211 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { Control } from "react-hook-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface ServerTemplateFieldsProps {
control: Control<any>;
}
export const ServerTemplateFields: React.FC<ServerTemplateFieldsProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">System Resource Messages</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="cpu_message"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Alert Message</FormLabel>
<FormControl>
<Textarea
placeholder="CPU usage on ${server_name} is ${cpu_usage}% (threshold: ${threshold}%)"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="ram_message"
render={({ field }) => (
<FormItem>
<FormLabel>RAM Alert Message</FormLabel>
<FormControl>
<Textarea
placeholder="Memory usage on ${server_name} is ${ram_usage}% (threshold: ${threshold}%)"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="disk_message"
render={({ field }) => (
<FormItem>
<FormLabel>Disk Alert Message</FormLabel>
<FormControl>
<Textarea
placeholder="Disk usage on ${server_name} is ${disk_usage}% (threshold: ${threshold}%)"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="network_message"
render={({ field }) => (
<FormItem>
<FormLabel>Network Alert Message</FormLabel>
<FormControl>
<Textarea
placeholder="Network usage on ${server_name} is ${network_usage}% (threshold: ${threshold}%)"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="up_message"
render={({ field }) => (
<FormItem>
<FormLabel>Server Up Message</FormLabel>
<FormControl>
<Textarea
placeholder="Server ${server_name} is UP and responding"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="down_message"
render={({ field }) => (
<FormItem>
<FormLabel>Server Down Message</FormLabel>
<FormControl>
<Textarea
placeholder="Server ${server_name} is DOWN"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="warning_message"
render={({ field }) => (
<FormItem>
<FormLabel>Warning Message</FormLabel>
<FormControl>
<Textarea
placeholder="Warning: Server ${server_name} requires attention"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="paused_message"
render={({ field }) => (
<FormItem>
<FormLabel>Paused Message</FormLabel>
<FormControl>
<Textarea
placeholder="Monitoring for server ${server_name} is paused"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="cpu_temp_message"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Temperature Message</FormLabel>
<FormControl>
<Textarea
placeholder="CPU temperature on ${server_name} is ${cpu_temp}°C"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="disk_io_message"
render={({ field }) => (
<FormItem>
<FormLabel>Disk I/O Message</FormLabel>
<FormControl>
<Textarea
placeholder="Disk I/O on ${server_name} is ${disk_io} MB/s"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,133 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { Control } from "react-hook-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface ServiceTemplateFieldsProps {
control: Control<any>;
}
export const ServiceTemplateFields: React.FC<ServiceTemplateFieldsProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Service Status Messages</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="up_message"
render={({ field }) => (
<FormItem>
<FormLabel>Service Up Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} is UP. Response time: ${response_time}ms"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="down_message"
render={({ field }) => (
<FormItem>
<FormLabel>Service Down Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} is DOWN. Status: ${status}"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="maintenance_message"
render={({ field }) => (
<FormItem>
<FormLabel>Maintenance Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} is under maintenance"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="incident_message"
render={({ field }) => (
<FormItem>
<FormLabel>Incident Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} has an incident"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="resolved_message"
render={({ field }) => (
<FormItem>
<FormLabel>Resolved Message</FormLabel>
<FormControl>
<Textarea
placeholder="Issue with service ${service_name} has been resolved"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="warning_message"
render={({ field }) => (
<FormItem>
<FormLabel>Warning Message</FormLabel>
<FormControl>
<Textarea
placeholder="Warning: Service ${service_name} response time is high"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,79 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { Control } from "react-hook-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface SslTemplateFieldsProps {
control: Control<any>;
}
export const SslTemplateFields: React.FC<SslTemplateFieldsProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">SSL Certificate Messages</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<FormField
control={control}
name="expired"
render={({ field }) => (
<FormItem>
<FormLabel>Certificate Expired Message</FormLabel>
<FormControl>
<Textarea
placeholder="SSL certificate for ${domain} has EXPIRED on ${expiry_date}"
className="min-h-24"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="exiring_soon"
render={({ field }) => (
<FormItem>
<FormLabel>Certificate Expiring Soon Message</FormLabel>
<FormControl>
<Textarea
placeholder="SSL certificate for ${domain} will expire in ${days_left} days on ${expiry_date}"
className="min-h-24"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="warning"
render={({ field }) => (
<FormItem>
<FormLabel>Certificate Warning Message</FormLabel>
<FormControl>
<Textarea
placeholder="Warning: SSL certificate for ${domain} requires attention"
className="min-h-24"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -1,2 +1,2 @@
export * from './useTemplateForm';
export * from './useTemplateForm';
@@ -4,69 +4,143 @@ import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { useToast } from "@/hooks/use-toast";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { templateService, CreateUpdateTemplateData } from "@/services/templateService";
import { templateService, TemplateType, AnyTemplateData } from "@/services/templateService";
import { useEffect } from "react";
// Template form schema
export const templateFormSchema = z.object({
// Base schema
const baseSchema = {
name: z.string().min(2, "Name is required and must be at least 2 characters"),
type: z.string().min(1, "Type is required"),
templateType: z.enum(['server', 'service', 'ssl'] as const),
placeholder: z.string().optional(),
};
// Server template schema
const serverTemplateSchema = z.object({
...baseSchema,
templateType: z.literal('server'),
ram_message: z.string().min(1, "RAM message is required"),
cpu_message: z.string().min(1, "CPU message is required"),
disk_message: z.string().min(1, "Disk message is required"),
network_message: z.string().min(1, "Network message is required"),
up_message: z.string().min(1, "Up message is required"),
down_message: z.string().min(1, "Down message is required"),
notification_id: z.string().optional(),
warning_message: z.string().min(1, "Warning message is required"),
paused_message: z.string().min(1, "Paused message is required"),
cpu_temp_message: z.string().min(1, "CPU temperature message is required"),
disk_io_message: z.string().min(1, "Disk I/O message is required"),
});
// Service template schema
const serviceTemplateSchema = z.object({
...baseSchema,
templateType: z.literal('service'),
up_message: z.string().min(1, "Up message is required"),
down_message: z.string().min(1, "Down message is required"),
maintenance_message: z.string().min(1, "Maintenance message is required"),
incident_message: z.string().min(1, "Incident message is required"),
resolved_message: z.string().min(1, "Resolved message is required"),
service_name_placeholder: z.string().min(1, "Service name placeholder is required"),
response_time_placeholder: z.string().min(1, "Response time placeholder is required"),
status_placeholder: z.string().min(1, "Status placeholder is required"),
threshold_placeholder: z.string().min(1, "Threshold placeholder is required"),
warning_message: z.string().min(1, "Warning message is required"),
});
// Define the form data type from the schema
// SSL template schema
const sslTemplateSchema = z.object({
...baseSchema,
templateType: z.literal('ssl'),
expired: z.string().min(1, "Expired message is required"),
exiring_soon: z.string().min(1, "Expiring soon message is required"),
warning: z.string().min(1, "Warning message is required"),
});
// Combined schema
export const templateFormSchema = z.discriminatedUnion("templateType", [
serverTemplateSchema,
serviceTemplateSchema,
sslTemplateSchema,
]);
export type TemplateFormData = z.infer<typeof templateFormSchema>;
// Default form values
const defaultFormValues: TemplateFormData = {
name: "",
type: "default",
up_message: "Service ${service_name} is UP. Response time: ${response_time}ms",
down_message: "Service ${service_name} is DOWN. Status: ${status}",
maintenance_message: "Service ${service_name} is under maintenance",
incident_message: "Service ${service_name} has an incident",
resolved_message: "Service ${service_name} issue has been resolved",
service_name_placeholder: "${service_name}",
response_time_placeholder: "${response_time}",
status_placeholder: "${status}",
threshold_placeholder: "${threshold}",
// Default form values for each template type
const getDefaultValues = (templateType: TemplateType): TemplateFormData => {
const base = {
name: "",
templateType,
placeholder: "",
};
switch (templateType) {
case 'server':
return {
...base,
templateType: 'server' as const,
ram_message: "Memory usage on ${server_name} is ${ram_usage}% (threshold: ${threshold}%)",
cpu_message: "CPU usage on ${server_name} is ${cpu_usage}% (threshold: ${threshold}%)",
disk_message: "Disk usage on ${server_name} is ${disk_usage}% (threshold: ${threshold}%)",
network_message: "Network usage on ${server_name} is ${network_usage}% (threshold: ${threshold}%)",
up_message: "Server ${server_name} is UP and responding",
down_message: "Server ${server_name} is DOWN",
notification_id: "",
warning_message: "Warning: Server ${server_name} requires attention",
paused_message: "Monitoring for server ${server_name} is paused",
cpu_temp_message: "CPU temperature on ${server_name} is ${cpu_temp}°C",
disk_io_message: "Disk I/O on ${server_name} is ${disk_io} MB/s",
};
case 'service':
return {
...base,
templateType: 'service' as const,
up_message: "Service ${service_name} is UP. Response time: ${response_time}ms",
down_message: "Service ${service_name} is DOWN. Status: ${status}",
maintenance_message: "Service ${service_name} is under maintenance",
incident_message: "Service ${service_name} has an incident",
resolved_message: "Issue with service ${service_name} has been resolved",
warning_message: "Warning: Service ${service_name} response time is high",
};
case 'ssl':
return {
...base,
templateType: 'ssl' as const,
expired: "SSL certificate for ${domain} has EXPIRED on ${expiry_date}",
exiring_soon: "SSL certificate for ${domain} will expire in ${days_left} days on ${expiry_date}",
warning: "Warning: SSL certificate for ${domain} requires attention",
};
default:
throw new Error(`Unknown template type: ${templateType}`);
}
};
export interface UseTemplateFormProps {
templateId: string | null;
templateType: TemplateType;
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: UseTemplateFormProps) => {
export const useTemplateForm = ({ templateId, templateType, open, onOpenChange, onSuccess }: UseTemplateFormProps) => {
const { toast } = useToast();
const queryClient = useQueryClient();
const isEditMode = !!templateId;
// console.log("Template form initialized with templateId:", templateId, "isEditMode:", isEditMode);
// console.log("Template form initialized with templateId:", templateId, "templateType:", templateType, "isEditMode:", isEditMode);
const form = useForm<TemplateFormData>({
resolver: zodResolver(templateFormSchema),
defaultValues: defaultFormValues,
defaultValues: getDefaultValues(templateType),
mode: "onChange"
});
// Query to fetch template data for editing
const { isLoading: isLoadingTemplate, data: templateData } = useQuery({
queryKey: ['template', templateId],
queryKey: ['template', templateId, templateType],
queryFn: () => {
if (!templateId) return null;
console.log("Fetching template data for ID:", templateId);
return templateService.getTemplate(templateId);
// console.log("Fetching template data for ID:", templateId, "type:", templateType);
return templateService.getTemplate(templateId, templateType);
},
enabled: !!templateId && open,
});
@@ -76,47 +150,32 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
if (templateData && open) {
// console.log("Setting form values with template data:", templateData);
form.reset({
const formData: any = {
name: templateData.name || "",
type: templateData.type || "default",
up_message: templateData.up_message || "",
down_message: templateData.down_message || "",
maintenance_message: templateData.maintenance_message || "",
incident_message: templateData.incident_message || "",
resolved_message: templateData.resolved_message || "",
service_name_placeholder: templateData.service_name_placeholder || "${service_name}",
response_time_placeholder: templateData.response_time_placeholder || "${response_time}",
status_placeholder: templateData.status_placeholder || "${status}",
threshold_placeholder: templateData.threshold_placeholder || "${threshold}",
});
}
}, [templateData, open, form]);
templateType: templateType,
placeholder: (templateData as any).placeholder || "",
};
// Handle form errors
useEffect(() => {
const subscription = form.formState.errors.root?.message &&
toast({
title: "Error",
description: form.formState.errors.root.message,
variant: "destructive",
// Add template-specific fields
Object.keys(templateData).forEach(key => {
if (!['id', 'collectionId', 'collectionName', 'created', 'updated', 'name'].includes(key)) {
formData[key] = (templateData as any)[key] || "";
}
});
return () => {
if (subscription) {
// Clean up if needed
}
};
}, [form.formState.errors.root, toast]);
form.reset(formData);
}
}, [templateData, open, form, templateType]);
// Create mutation
const createMutation = useMutation({
mutationFn: (data: CreateUpdateTemplateData) => templateService.createTemplate(data),
mutationFn: (data: AnyTemplateData) => templateService.createTemplate(data, templateType),
onSuccess: () => {
toast({
title: "Template created",
description: "Your notification template has been created successfully.",
});
queryClient.invalidateQueries({ queryKey: ['notification_templates'] });
queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] });
onSuccess();
},
onError: (error) => {
@@ -131,14 +190,14 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
// Update mutation
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: CreateUpdateTemplateData }) =>
templateService.updateTemplate(id, data),
mutationFn: ({ id, data }: { id: string; data: AnyTemplateData }) =>
templateService.updateTemplate(id, data, templateType),
onSuccess: () => {
toast({
title: "Template updated",
description: "Your notification template has been updated successfully.",
});
queryClient.invalidateQueries({ queryKey: ['notification_templates'] });
queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] });
onSuccess();
},
onError: (error) => {
@@ -157,20 +216,9 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
const onSubmit = (formData: TemplateFormData) => {
// console.log("Submitting form data:", formData);
// Ensure all required fields are present
const completeData: CreateUpdateTemplateData = {
name: formData.name,
type: formData.type,
up_message: formData.up_message,
down_message: formData.down_message,
maintenance_message: formData.maintenance_message,
incident_message: formData.incident_message,
resolved_message: formData.resolved_message,
service_name_placeholder: formData.service_name_placeholder,
response_time_placeholder: formData.response_time_placeholder,
status_placeholder: formData.status_placeholder,
threshold_placeholder: formData.threshold_placeholder,
};
// Remove templateType from the data before sending to API
const { templateType: _, ...templateDataWithoutType } = formData;
const completeData = templateDataWithoutType as AnyTemplateData;
if (isEditMode && templateId) {
// console.log("Updating template with ID:", templateId);
@@ -181,13 +229,13 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
}
};
// Reset form when dialog closes
// Reset form when dialog closes or template type changes
useEffect(() => {
if (!open) {
// console.log("Dialog closed, resetting form");
form.reset(defaultFormValues);
// console.log("Dialog closed, resetting form");
form.reset(getDefaultValues(templateType));
}
}, [open, form]);
}, [open, form, templateType]);
return {
form,
@@ -196,4 +244,4 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
isSubmitting,
onSubmit
};
};
};
@@ -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>
);
};
};
@@ -87,6 +87,7 @@ const NotificationSettings = () => {
<TabsTrigger value="slack">Slack</TabsTrigger>
<TabsTrigger value="signal">Signal</TabsTrigger>
<TabsTrigger value="email">Email</TabsTrigger>
<TabsTrigger value="webhook">Webhook</TabsTrigger>
</TabsList>
<TabsContent value={currentTab} className="mt-0">
@@ -114,4 +115,4 @@ const NotificationSettings = () => {
);
};
export default NotificationSettings;
export default NotificationSettings;