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:
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -12,7 +13,7 @@ import { pb } from "@/lib/pocketbase";
|
|||||||
import { Server } from "@/types/server.types";
|
import { Server } from "@/types/server.types";
|
||||||
import { RefreshCw, X } from "lucide-react";
|
import { RefreshCw, X } from "lucide-react";
|
||||||
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
||||||
import { templateService, NotificationTemplate } from "@/services/templateService";
|
import { templateService, ServerNotificationTemplate } from "@/services/templateService";
|
||||||
import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService";
|
import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService";
|
||||||
|
|
||||||
interface EditServerDialogProps {
|
interface EditServerDialogProps {
|
||||||
@@ -66,9 +67,9 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
|||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
||||||
const [templates, setTemplates] = useState<NotificationTemplate[]>([]);
|
const [templates, setTemplates] = useState<ServerNotificationTemplate[]>([]);
|
||||||
const [thresholds, setThresholds] = useState<ServerThreshold[]>([]);
|
const [thresholds, setThresholds] = useState<ServerThreshold[]>([]);
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<NotificationTemplate | null>(null);
|
const [selectedTemplate, setSelectedTemplate] = useState<ServerNotificationTemplate | null>(null);
|
||||||
const [selectedThreshold, setSelectedThreshold] = useState<ServerThreshold | null>(null);
|
const [selectedThreshold, setSelectedThreshold] = useState<ServerThreshold | null>(null);
|
||||||
const [loadingAlertConfigs, setLoadingAlertConfigs] = useState(false);
|
const [loadingAlertConfigs, setLoadingAlertConfigs] = useState(false);
|
||||||
const [loadingTemplates, setLoadingTemplates] = useState(false);
|
const [loadingTemplates, setLoadingTemplates] = useState(false);
|
||||||
@@ -180,8 +181,9 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
|||||||
const loadTemplates = async () => {
|
const loadTemplates = async () => {
|
||||||
try {
|
try {
|
||||||
setLoadingTemplates(true);
|
setLoadingTemplates(true);
|
||||||
const templateList = await templateService.getTemplates();
|
const templateList = await templateService.getTemplates('server');
|
||||||
setTemplates(templateList);
|
// Cast to ServerNotificationTemplate[] since we know we're getting server templates
|
||||||
|
setTemplates(templateList as ServerNotificationTemplate[]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error('Error loading templates:', error);
|
// console.error('Error loading templates:', error);
|
||||||
toast({
|
toast({
|
||||||
@@ -621,27 +623,39 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
|||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
<div className="grid grid-cols-1 gap-3 text-sm">
|
<div className="grid grid-cols-1 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs font-medium text-muted-foreground">RAM Threshold Message</Label>
|
<Label className="text-xs font-medium text-muted-foreground">RAM Message</Label>
|
||||||
<p className="text-sm bg-background p-2 rounded border">
|
<p className="text-sm bg-background p-2 rounded border">
|
||||||
{selectedTemplate.up_message || "No RAM threshold message defined"}
|
{selectedTemplate.ram_message || "No RAM message defined"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs font-medium text-muted-foreground">CPU Threshold Message</Label>
|
<Label className="text-xs font-medium text-muted-foreground">CPU Message</Label>
|
||||||
<p className="text-sm bg-background p-2 rounded border">
|
<p className="text-sm bg-background p-2 rounded border">
|
||||||
{selectedTemplate.down_message || "No CPU threshold message defined"}
|
{selectedTemplate.cpu_message || "No CPU message defined"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs font-medium text-muted-foreground">Disk Threshold Message</Label>
|
<Label className="text-xs font-medium text-muted-foreground">Disk Message</Label>
|
||||||
<p className="text-sm bg-background p-2 rounded border">
|
<p className="text-sm bg-background p-2 rounded border">
|
||||||
{selectedTemplate.incident_message || "No disk threshold message defined"}
|
{selectedTemplate.disk_message || "No disk message defined"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs font-medium text-muted-foreground">Network Threshold Message</Label>
|
<Label className="text-xs font-medium text-muted-foreground">Network Message</Label>
|
||||||
<p className="text-sm bg-background p-2 rounded border">
|
<p className="text-sm bg-background p-2 rounded border">
|
||||||
{selectedTemplate.maintenance_message || "No network threshold message defined"}
|
{selectedTemplate.network_message || "No network message defined"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium text-muted-foreground">Up Message</Label>
|
||||||
|
<p className="text-sm bg-background p-2 rounded border">
|
||||||
|
{selectedTemplate.up_message || "No up message defined"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium text-muted-foreground">Down Message</Label>
|
||||||
|
<p className="text-sm bg-background p-2 rounded border">
|
||||||
|
{selectedTemplate.down_message || "No down message defined"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Play, Pause } from "lucide-react";
|
|||||||
import { Service } from "@/types/service.types";
|
import { Service } from "@/types/service.types";
|
||||||
import { serviceService } from "@/services/serviceService";
|
import { serviceService } from "@/services/serviceService";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { notificationService } from "@/services/notificationService";
|
|
||||||
|
|
||||||
interface ServiceMonitoringButtonProps {
|
interface ServiceMonitoringButtonProps {
|
||||||
service: Service;
|
service: Service;
|
||||||
@@ -34,16 +33,8 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni
|
|||||||
|
|
||||||
if (onStatusChange) onStatusChange("paused");
|
if (onStatusChange) onStatusChange("paused");
|
||||||
|
|
||||||
// Send notification for paused status (only here, not in pauseMonitoring.ts)
|
// Notification handling removed - will be handled by backend
|
||||||
if (service.alerts !== "muted") {
|
// console.log("Service paused - notifications will be handled by backend");
|
||||||
// console.log("Sending pause notification from UI component");
|
|
||||||
// IMPORTANT: Direct call to the notification service to ensure a message is sent
|
|
||||||
await notificationService.sendNotification({
|
|
||||||
service: service,
|
|
||||||
status: "paused",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Monitoring paused",
|
title: "Monitoring paused",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Plus, RefreshCcw } from "lucide-react";
|
import { Plus, RefreshCcw } from "lucide-react";
|
||||||
import { TemplateList } from "./TemplateList";
|
import { TemplateList } from "./TemplateList";
|
||||||
import { TemplateDialog } from "./TemplateDialog";
|
import { TemplateDialog } from "./TemplateDialog";
|
||||||
@@ -11,8 +12,10 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
|
|
||||||
export const AlertsTemplates = () => {
|
export const AlertsTemplates = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const [activeTab, setActiveTab] = useState<TemplateType>('service');
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [editingTemplate, setEditingTemplate] = useState<string | null>(null);
|
const [editingTemplate, setEditingTemplate] = useState<string | null>(null);
|
||||||
|
const [editingTemplateType, setEditingTemplateType] = useState<TemplateType | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: templates = [],
|
data: templates = [],
|
||||||
@@ -20,17 +23,19 @@ export const AlertsTemplates = () => {
|
|||||||
error,
|
error,
|
||||||
refetch
|
refetch
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ['notification_templates'],
|
queryKey: ['notification_templates', activeTab],
|
||||||
queryFn: templateService.getTemplates,
|
queryFn: () => templateService.getTemplates(activeTab),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAddTemplate = () => {
|
const handleAddTemplate = (templateType: TemplateType) => {
|
||||||
setEditingTemplate(null);
|
setEditingTemplate(null);
|
||||||
|
setEditingTemplateType(templateType);
|
||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditTemplate = (id: string) => {
|
const handleEditTemplate = (id: string, templateType: TemplateType) => {
|
||||||
setEditingTemplate(id);
|
setEditingTemplate(id);
|
||||||
|
setEditingTemplateType(templateType);
|
||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,16 +56,24 @@ export const AlertsTemplates = () => {
|
|||||||
<RefreshCcw className="h-4 w-4 mr-2" />
|
<RefreshCcw className="h-4 w-4 mr-2" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleAddTemplate}>
|
<Button onClick={() => handleAddTemplate(activeTab)}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Add Template
|
Add Template
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
<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 ? (
|
{error ? (
|
||||||
<div className="text-center p-6">
|
<div className="text-center p-6">
|
||||||
<p className="text-destructive mb-4">Error loading templates</p>
|
<p className="text-destructive mb-4">Error loading service templates</p>
|
||||||
<Button variant="outline" onClick={() => refetch()}>
|
<Button variant="outline" onClick={() => refetch()}>
|
||||||
Try Again
|
Try Again
|
||||||
</Button>
|
</Button>
|
||||||
@@ -69,15 +82,57 @@ export const AlertsTemplates = () => {
|
|||||||
<TemplateList
|
<TemplateList
|
||||||
templates={templates}
|
templates={templates}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onEdit={handleEditTemplate}
|
onEdit={(id) => handleEditTemplate(id, 'service')}
|
||||||
refetchTemplates={refetch}
|
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>
|
</CardContent>
|
||||||
|
|
||||||
<TemplateDialog
|
<TemplateDialog
|
||||||
open={isDialogOpen}
|
open={isDialogOpen}
|
||||||
templateId={editingTemplate}
|
templateId={editingTemplate}
|
||||||
|
templateType={editingTemplateType}
|
||||||
onOpenChange={setIsDialogOpen}
|
onOpenChange={setIsDialogOpen}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
refetch();
|
refetch();
|
||||||
|
|||||||
@@ -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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { useTemplateForm } from "./hooks/useTemplateForm";
|
import { useTemplateForm } from "./hooks/useTemplateForm";
|
||||||
import { BasicTemplateFields } from "./form/BasicTemplateFields";
|
import { ServerTemplateFields } from "./form/ServerTemplateFields";
|
||||||
import { MessagesTabContent } from "./form/MessagesTabContent";
|
import { ServiceTemplateFields } from "./form/ServiceTemplateFields";
|
||||||
import { PlaceholdersTabContent } from "./form/PlaceholdersTabContent";
|
import { SslTemplateFields } from "./form/SslTemplateFields";
|
||||||
import { Loader2, ChevronDown } from "lucide-react";
|
import { Loader2, ChevronDown } from "lucide-react";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
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 {
|
interface TemplateDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
templateId: string | null;
|
templateId: string | null;
|
||||||
|
templateType: TemplateType | null;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}
|
}
|
||||||
@@ -21,9 +27,12 @@ interface TemplateDialogProps {
|
|||||||
export const TemplateDialog: React.FC<TemplateDialogProps> = ({
|
export const TemplateDialog: React.FC<TemplateDialogProps> = ({
|
||||||
open,
|
open,
|
||||||
templateId,
|
templateId,
|
||||||
|
templateType: initialTemplateType,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [selectedTemplateType, setSelectedTemplateType] = useState<TemplateType>(initialTemplateType || 'service');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
form,
|
form,
|
||||||
isEditMode,
|
isEditMode,
|
||||||
@@ -32,28 +41,70 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
|
|||||||
onSubmit
|
onSubmit
|
||||||
} = useTemplateForm({
|
} = useTemplateForm({
|
||||||
templateId,
|
templateId,
|
||||||
|
templateType: selectedTemplateType,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onSuccess
|
onSuccess
|
||||||
});
|
});
|
||||||
|
|
||||||
// For debugging purposes
|
// Update template type when prop changes or dialog opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (initialTemplateType) {
|
||||||
// console.log("Template dialog opened. Edit mode:", isEditMode, "Template ID:", templateId);
|
setSelectedTemplateType(initialTemplateType);
|
||||||
|
} else if (open && !isEditMode) {
|
||||||
// Log form values when they change
|
setSelectedTemplateType('service');
|
||||||
const subscription = form.watch((value) => {
|
|
||||||
// console.log("Current form values:", value);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => subscription.unsubscribe();
|
|
||||||
}
|
}
|
||||||
}, [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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<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>
|
<DialogHeader>
|
||||||
<DialogTitle>{isEditMode ? "Edit Template" : "Add Template"}</DialogTitle>
|
<DialogTitle>{isEditMode ? "Edit Template" : "Add Template"}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -69,7 +120,69 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
|
|||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<ScrollArea className="pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}>
|
<ScrollArea className="pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}>
|
||||||
<div className="space-y-6 pb-6 pr-4">
|
<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">
|
<Tabs defaultValue="messages">
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
@@ -78,11 +191,11 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="messages" className="pt-4">
|
<TabsContent value="messages" className="pt-4">
|
||||||
<MessagesTabContent control={form.control} />
|
{renderTemplateFields()}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="placeholders" className="pt-4">
|
<TabsContent value="placeholders" className="pt-4">
|
||||||
<PlaceholdersTabContent control={form.control} />
|
{renderPlaceholderGuide()}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
|
|
||||||
import React, { useState } from "react";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Edit, Trash2 } from "lucide-react";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Trash2, Edit, MoreVertical } from "lucide-react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -22,13 +20,15 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
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 {
|
interface TemplateListProps {
|
||||||
templates: NotificationTemplate[];
|
templates: AnyTemplate[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onEdit: (id: string) => void;
|
onEdit: (id: string) => void;
|
||||||
refetchTemplates: () => void;
|
refetchTemplates: () => void;
|
||||||
|
templateType: TemplateType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateList: React.FC<TemplateListProps> = ({
|
export const TemplateList: React.FC<TemplateListProps> = ({
|
||||||
@@ -36,49 +36,59 @@ export const TemplateList: React.FC<TemplateListProps> = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
onEdit,
|
onEdit,
|
||||||
refetchTemplates,
|
refetchTemplates,
|
||||||
|
templateType
|
||||||
}) => {
|
}) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const queryClient = useQueryClient();
|
||||||
const [templateToDelete, setTemplateToDelete] = useState<NotificationTemplate | null>(null);
|
const [deleteTemplateId, setDeleteTemplateId] = useState<string | null>(null);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
|
||||||
|
|
||||||
const handleDeletePrompt = (template: NotificationTemplate) => {
|
// Delete mutation
|
||||||
setTemplateToDelete(template);
|
const deleteMutation = useMutation({
|
||||||
setDeleteDialogOpen(true);
|
mutationFn: (id: string) => templateService.deleteTemplate(id, templateType),
|
||||||
};
|
onSuccess: () => {
|
||||||
|
|
||||||
const handleDeleteTemplate = async () => {
|
|
||||||
if (!templateToDelete) return;
|
|
||||||
|
|
||||||
setIsDeleting(true);
|
|
||||||
try {
|
|
||||||
await templateService.deleteTemplate(templateToDelete.id);
|
|
||||||
toast({
|
toast({
|
||||||
title: "Template deleted",
|
title: "Template deleted",
|
||||||
description: `Template "${templateToDelete.name}" has been removed.`,
|
description: "The template has been deleted successfully.",
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] });
|
||||||
refetchTemplates();
|
refetchTemplates();
|
||||||
} catch (error) {
|
},
|
||||||
console.error("Error deleting template:", error);
|
onError: (error) => {
|
||||||
|
// console.error("Error deleting template:", error);
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to delete template. Please try again.",
|
description: "Failed to delete template. Please try again.",
|
||||||
variant: "destructive",
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="py-8 flex justify-center">
|
<div className="space-y-4">
|
||||||
<div className="animate-pulse flex flex-col items-center">
|
{[...Array(3)].map((_, i) => (
|
||||||
<div className="h-4 bg-gray-200 rounded w-32 mb-4"></div>
|
<div key={i} className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||||
<div className="h-4 bg-gray-200 rounded w-64"></div>
|
<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>
|
||||||
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -86,79 +96,82 @@ export const TemplateList: React.FC<TemplateListProps> = ({
|
|||||||
if (templates.length === 0) {
|
if (templates.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<p className="text-muted-foreground mb-2">No templates found</p>
|
<p className="text-muted-foreground">No templates found. Create your first template to get started.</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Create your first notification template to get started.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTemplateTypeLabel = (type: TemplateType) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'server': return 'Server';
|
||||||
|
case 'service': return 'Service';
|
||||||
|
case 'ssl': return 'SSL';
|
||||||
|
default: return 'Unknown';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="border rounded-md overflow-hidden">
|
<div className="space-y-4">
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Name</TableHead>
|
|
||||||
<TableHead>Type</TableHead>
|
|
||||||
<TableHead>Created</TableHead>
|
|
||||||
<TableHead className="w-24">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{templates.map((template) => (
|
{templates.map((template) => (
|
||||||
<TableRow key={template.id}>
|
<div key={template.id} className="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors">
|
||||||
<TableCell className="font-medium">{template.name}</TableCell>
|
<div className="space-y-1">
|
||||||
<TableCell>
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="outline">{template.type}</Badge>
|
<h3 className="font-medium">{template.name}</h3>
|
||||||
</TableCell>
|
<Badge variant="outline">{getTemplateTypeLabel(templateType)}</Badge>
|
||||||
<TableCell>
|
</div>
|
||||||
{new Date(template.created).toLocaleDateString()}
|
<p className="text-sm text-muted-foreground">
|
||||||
</TableCell>
|
Created: {new Date(template.created).toLocaleDateString()}
|
||||||
<TableCell>
|
{template.updated !== template.created &&
|
||||||
<div className="flex space-x-2">
|
` • Updated: ${new Date(template.updated).toLocaleDateString()}`
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
|
||||||
onClick={() => onEdit(template.id)}
|
onClick={() => onEdit(template.id)}
|
||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4 mr-1" />
|
||||||
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<DropdownMenu>
|
||||||
size="sm"
|
<DropdownMenuTrigger asChild>
|
||||||
variant="ghost"
|
<Button variant="outline" size="sm">
|
||||||
onClick={() => handleDeletePrompt(template)}
|
<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 text-destructive" />
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
</Button>
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<AlertDialog open={!!deleteTemplateId} onOpenChange={() => setDeleteTemplateId(null)}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Delete Template</AlertDialogTitle>
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<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>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={(e) => {
|
onClick={confirmDelete}
|
||||||
e.preventDefault();
|
|
||||||
handleDeleteTemplate();
|
|
||||||
}}
|
|
||||||
disabled={isDeleting}
|
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
>
|
>
|
||||||
{isDeleting ? "Deleting..." : "Delete"}
|
Delete
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,69 +4,143 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
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";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
// Template form schema
|
// Base schema
|
||||||
export const templateFormSchema = z.object({
|
const baseSchema = {
|
||||||
name: z.string().min(2, "Name is required and must be at least 2 characters"),
|
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"),
|
up_message: z.string().min(1, "Up message is required"),
|
||||||
down_message: z.string().min(1, "Down message is required"),
|
down_message: z.string().min(1, "Down message is required"),
|
||||||
maintenance_message: z.string().min(1, "Maintenance message is required"),
|
maintenance_message: z.string().min(1, "Maintenance message is required"),
|
||||||
incident_message: z.string().min(1, "Incident message is required"),
|
incident_message: z.string().min(1, "Incident message is required"),
|
||||||
resolved_message: z.string().min(1, "Resolved 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"),
|
warning_message: z.string().min(1, "Warning message 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"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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>;
|
export type TemplateFormData = z.infer<typeof templateFormSchema>;
|
||||||
|
|
||||||
// Default form values
|
// Default form values for each template type
|
||||||
const defaultFormValues: TemplateFormData = {
|
const getDefaultValues = (templateType: TemplateType): TemplateFormData => {
|
||||||
|
const base = {
|
||||||
name: "",
|
name: "",
|
||||||
type: "default",
|
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",
|
up_message: "Service ${service_name} is UP. Response time: ${response_time}ms",
|
||||||
down_message: "Service ${service_name} is DOWN. Status: ${status}",
|
down_message: "Service ${service_name} is DOWN. Status: ${status}",
|
||||||
maintenance_message: "Service ${service_name} is under maintenance",
|
maintenance_message: "Service ${service_name} is under maintenance",
|
||||||
incident_message: "Service ${service_name} has an incident",
|
incident_message: "Service ${service_name} has an incident",
|
||||||
resolved_message: "Service ${service_name} issue has been resolved",
|
resolved_message: "Issue with service ${service_name} has been resolved",
|
||||||
service_name_placeholder: "${service_name}",
|
warning_message: "Warning: Service ${service_name} response time is high",
|
||||||
response_time_placeholder: "${response_time}",
|
};
|
||||||
status_placeholder: "${status}",
|
|
||||||
threshold_placeholder: "${threshold}",
|
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 {
|
export interface UseTemplateFormProps {
|
||||||
templateId: string | null;
|
templateId: string | null;
|
||||||
|
templateType: TemplateType;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: UseTemplateFormProps) => {
|
export const useTemplateForm = ({ templateId, templateType, open, onOpenChange, onSuccess }: UseTemplateFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isEditMode = !!templateId;
|
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>({
|
const form = useForm<TemplateFormData>({
|
||||||
resolver: zodResolver(templateFormSchema),
|
resolver: zodResolver(templateFormSchema),
|
||||||
defaultValues: defaultFormValues,
|
defaultValues: getDefaultValues(templateType),
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Query to fetch template data for editing
|
// Query to fetch template data for editing
|
||||||
const { isLoading: isLoadingTemplate, data: templateData } = useQuery({
|
const { isLoading: isLoadingTemplate, data: templateData } = useQuery({
|
||||||
queryKey: ['template', templateId],
|
queryKey: ['template', templateId, templateType],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (!templateId) return null;
|
if (!templateId) return null;
|
||||||
console.log("Fetching template data for ID:", templateId);
|
// console.log("Fetching template data for ID:", templateId, "type:", templateType);
|
||||||
return templateService.getTemplate(templateId);
|
return templateService.getTemplate(templateId, templateType);
|
||||||
},
|
},
|
||||||
enabled: !!templateId && open,
|
enabled: !!templateId && open,
|
||||||
});
|
});
|
||||||
@@ -76,47 +150,32 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
|
|||||||
if (templateData && open) {
|
if (templateData && open) {
|
||||||
// console.log("Setting form values with template data:", templateData);
|
// console.log("Setting form values with template data:", templateData);
|
||||||
|
|
||||||
form.reset({
|
const formData: any = {
|
||||||
name: templateData.name || "",
|
name: templateData.name || "",
|
||||||
type: templateData.type || "default",
|
templateType: templateType,
|
||||||
up_message: templateData.up_message || "",
|
placeholder: (templateData as any).placeholder || "",
|
||||||
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]);
|
|
||||||
|
|
||||||
// Handle form errors
|
|
||||||
useEffect(() => {
|
|
||||||
const subscription = form.formState.errors.root?.message &&
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: form.formState.errors.root.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (subscription) {
|
|
||||||
// Clean up if needed
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [form.formState.errors.root, toast]);
|
|
||||||
|
// Add template-specific fields
|
||||||
|
Object.keys(templateData).forEach(key => {
|
||||||
|
if (!['id', 'collectionId', 'collectionName', 'created', 'updated', 'name'].includes(key)) {
|
||||||
|
formData[key] = (templateData as any)[key] || "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
form.reset(formData);
|
||||||
|
}
|
||||||
|
}, [templateData, open, form, templateType]);
|
||||||
|
|
||||||
// Create mutation
|
// Create mutation
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: CreateUpdateTemplateData) => templateService.createTemplate(data),
|
mutationFn: (data: AnyTemplateData) => templateService.createTemplate(data, templateType),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
title: "Template created",
|
title: "Template created",
|
||||||
description: "Your notification template has been created successfully.",
|
description: "Your notification template has been created successfully.",
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ['notification_templates'] });
|
queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] });
|
||||||
onSuccess();
|
onSuccess();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -131,14 +190,14 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
|
|||||||
|
|
||||||
// Update mutation
|
// Update mutation
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ id, data }: { id: string; data: CreateUpdateTemplateData }) =>
|
mutationFn: ({ id, data }: { id: string; data: AnyTemplateData }) =>
|
||||||
templateService.updateTemplate(id, data),
|
templateService.updateTemplate(id, data, templateType),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
title: "Template updated",
|
title: "Template updated",
|
||||||
description: "Your notification template has been updated successfully.",
|
description: "Your notification template has been updated successfully.",
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ['notification_templates'] });
|
queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] });
|
||||||
onSuccess();
|
onSuccess();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -157,20 +216,9 @@ export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: U
|
|||||||
const onSubmit = (formData: TemplateFormData) => {
|
const onSubmit = (formData: TemplateFormData) => {
|
||||||
// console.log("Submitting form data:", formData);
|
// console.log("Submitting form data:", formData);
|
||||||
|
|
||||||
// Ensure all required fields are present
|
// Remove templateType from the data before sending to API
|
||||||
const completeData: CreateUpdateTemplateData = {
|
const { templateType: _, ...templateDataWithoutType } = formData;
|
||||||
name: formData.name,
|
const completeData = templateDataWithoutType as AnyTemplateData;
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isEditMode && templateId) {
|
if (isEditMode && templateId) {
|
||||||
// console.log("Updating template with ID:", 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(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
// console.log("Dialog closed, resetting form");
|
// console.log("Dialog closed, resetting form");
|
||||||
form.reset(defaultFormValues);
|
form.reset(getDefaultValues(templateType));
|
||||||
}
|
}
|
||||||
}, [open, form]);
|
}, [open, form, templateType]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
form,
|
form,
|
||||||
|
|||||||
+236
-59
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -8,8 +7,9 @@ import {
|
|||||||
DialogFooter
|
DialogFooter
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { AlertConfiguration, alertConfigService } from "@/services/alertConfigService";
|
||||||
|
import { WebhookConfiguration, webhookService } from "@/services/webhookService";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -36,7 +36,7 @@ const baseSchema = z.object({
|
|||||||
notify_name: z.string().min(1, "Name is required"),
|
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", "email"]),
|
||||||
enabled: z.boolean().default(true),
|
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(),
|
template_id: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,7 +63,19 @@ const signalSchema = baseSchema.extend({
|
|||||||
|
|
||||||
const emailSchema = baseSchema.extend({
|
const emailSchema = baseSchema.extend({
|
||||||
notification_type: z.literal("email"),
|
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", [
|
const formSchema = z.discriminatedUnion("notification_type", [
|
||||||
@@ -71,11 +83,21 @@ const formSchema = z.discriminatedUnion("notification_type", [
|
|||||||
discordSchema,
|
discordSchema,
|
||||||
slackSchema,
|
slackSchema,
|
||||||
signalSchema,
|
signalSchema,
|
||||||
emailSchema
|
emailSchema,
|
||||||
|
webhookSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type FormValues = z.infer<typeof formSchema>;
|
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 = ({
|
export const NotificationChannelDialog = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -126,7 +148,26 @@ export const NotificationChannelDialog = ({
|
|||||||
const onSubmit = async (values: FormValues) => {
|
const onSubmit = async (values: FormValues) => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
// Ensure service_id is always present
|
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 {
|
||||||
|
// Handle other notification types with existing service
|
||||||
const configData = {
|
const configData = {
|
||||||
...values,
|
...values,
|
||||||
service_id: values.service_id || "global",
|
service_id: values.service_id || "global",
|
||||||
@@ -137,6 +178,7 @@ export const NotificationChannelDialog = ({
|
|||||||
} else {
|
} else {
|
||||||
await alertConfigService.createAlertConfiguration(configData as any);
|
await alertConfigService.createAlertConfiguration(configData as any);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
onClose(true); // Close with refresh
|
onClose(true); // Close with refresh
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
@@ -159,7 +201,7 @@ export const NotificationChannelDialog = ({
|
|||||||
name="notify_name"
|
name="notify_name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Channel Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="My Notification Channel" {...field} />
|
<Input placeholder="My Notification Channel" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -175,57 +217,25 @@ export const NotificationChannelDialog = ({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="notification_type"
|
name="notification_type"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="space-y-3">
|
<FormItem>
|
||||||
<FormLabel>Channel Type</FormLabel>
|
<FormLabel>Channel Type</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroup
|
<SelectTrigger>
|
||||||
onValueChange={field.onChange}
|
<SelectValue placeholder="Select notification type" />
|
||||||
defaultValue={field.value}
|
</SelectTrigger>
|
||||||
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>
|
</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 />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -243,6 +253,9 @@ export const NotificationChannelDialog = ({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Telegram Chat ID" {...field} />
|
<Input placeholder="Telegram Chat ID" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The Telegram chat ID to send notifications to
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -256,6 +269,9 @@ export const NotificationChannelDialog = ({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Telegram Bot Token" {...field} type="password" />
|
<Input placeholder="Telegram Bot Token" {...field} type="password" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Your Telegram bot token from @BotFather
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -271,8 +287,11 @@ export const NotificationChannelDialog = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Webhook URL</FormLabel>
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Discord Webhook URL" {...field} />
|
<Input placeholder="https://discord.com/api/webhooks/..." {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Discord webhook URL from your server settings
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -287,8 +306,11 @@ export const NotificationChannelDialog = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Webhook URL</FormLabel>
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Slack Webhook URL" {...field} />
|
<Input placeholder="https://hooks.slack.com/services/..." {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Slack incoming webhook URL
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -305,12 +327,167 @@ export const NotificationChannelDialog = ({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="+1234567890" {...field} />
|
<Input placeholder="+1234567890" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Signal phone number to send notifications to
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="enabled"
|
name="enabled"
|
||||||
@@ -319,7 +496,7 @@ export const NotificationChannelDialog = ({
|
|||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Enabled</FormLabel>
|
<FormLabel>Enabled</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Turn notifications on or off
|
Enable or disable this notification channel
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -338,7 +515,7 @@ export const NotificationChannelDialog = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
{isEditing ? "Update" : "Create"}
|
{isEditing ? "Update Channel" : "Create Channel"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ const NotificationSettings = () => {
|
|||||||
<TabsTrigger value="slack">Slack</TabsTrigger>
|
<TabsTrigger value="slack">Slack</TabsTrigger>
|
||||||
<TabsTrigger value="signal">Signal</TabsTrigger>
|
<TabsTrigger value="signal">Signal</TabsTrigger>
|
||||||
<TabsTrigger value="email">Email</TabsTrigger>
|
<TabsTrigger value="email">Email</TabsTrigger>
|
||||||
|
<TabsTrigger value="webhook">Webhook</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value={currentTab} className="mt-0">
|
<TabsContent value={currentTab} className="mt-0">
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { authService } from '@/services/authService';
|
import { authService } from '@/services/authService';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { toast } from '@/components/ui/use-toast';
|
import { toast } from '@/components/ui/use-toast';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Eye, EyeOff, Mail, LogIn, Settings, AlertCircle } from "lucide-react";
|
import { Eye, EyeOff, Mail, LogIn, AlertCircle, Github } from "lucide-react";
|
||||||
import { useLanguage } from '@/contexts/LanguageContext';
|
import { useLanguage } from '@/contexts/LanguageContext';
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme } from '@/contexts/ThemeContext';
|
||||||
import { API_ENDPOINTS, getCurrentEndpoint, setApiEndpoint } from '@/lib/pocketbase';
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { ForgotPasswordDialog } from '@/components/auth/ForgotPasswordDialog';
|
import { ForgotPasswordDialog } from '@/components/auth/ForgotPasswordDialog';
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
@@ -20,30 +16,13 @@ const Login = () => {
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [currentEndpoint, setCurrentEndpoint] = useState(getCurrentEndpoint());
|
|
||||||
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
||||||
const [loginError, setLoginError] = useState('');
|
const [loginError, setLoginError] = useState('');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
// Add responsiveness check
|
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkScreenSize = () => {
|
|
||||||
setIsMobile(window.innerWidth < 640);
|
|
||||||
};
|
|
||||||
|
|
||||||
checkScreenSize();
|
|
||||||
window.addEventListener('resize', checkScreenSize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', checkScreenSize);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setLoginError(''); // Clear previous errors
|
setLoginError(''); // Clear previous errors
|
||||||
@@ -69,7 +48,7 @@ const Login = () => {
|
|||||||
setLoginError(error.message);
|
setLoginError(error.message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setLoginError(`${t("authenticationFailed")}. Server: ${currentEndpoint}`);
|
setLoginError(t("authenticationFailed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@@ -82,60 +61,35 @@ const Login = () => {
|
|||||||
error.message.includes('invalid email or password'))
|
error.message.includes('invalid email or password'))
|
||||||
? t("invalidCredentials")
|
? t("invalidCredentials")
|
||||||
: error.message
|
: error.message
|
||||||
: `${t("authenticationFailed")}. Server: ${currentEndpoint}`,
|
: t("authenticationFailed"),
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [email, password, navigate, t]);
|
||||||
|
|
||||||
const handleEndpointChange = (value: string) => {
|
const togglePasswordVisibility = useCallback(() => {
|
||||||
setCurrentEndpoint(value);
|
setShowPassword(prev => !prev);
|
||||||
setApiEndpoint(value);
|
}, []);
|
||||||
};
|
|
||||||
|
|
||||||
const togglePasswordVisibility = () => {
|
const handleEmailChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setShowPassword(!showPassword);
|
setEmail(e.target.value);
|
||||||
};
|
setLoginError(''); // Clear error when user starts typing
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePasswordChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setPassword(e.target.value);
|
||||||
|
setLoginError(''); // Clear error when user starts typing
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openExternalLink = useCallback((url: string) => {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-background p-4">
|
<div className="flex flex-col items-center justify-center min-h-screen bg-background p-4">
|
||||||
<div className="w-full max-w-md p-4 sm:p-8 space-y-6 rounded-xl bg-card shadow-xl border border-border/20">
|
<div className="w-full max-w-md p-4 sm:p-8 space-y-6 rounded-xl bg-card shadow-xl border border-border/20">
|
||||||
<div className="text-center relative">
|
<div className="text-center">
|
||||||
{/* Commented out API Endpoint Settings button
|
|
||||||
<div className="absolute right-0 top-0">
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button variant="outline" size="icon" aria-label="API Settings">
|
|
||||||
<Settings className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-[260px] sm:w-80">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h4 className="font-medium">API Endpoint Settings</h4>
|
|
||||||
<RadioGroup
|
|
||||||
value={currentEndpoint}
|
|
||||||
onValueChange={handleEndpointChange}
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value={API_ENDPOINTS.LOCAL} id="local" />
|
|
||||||
<Label htmlFor="local" className="truncate text-sm">Local: {API_ENDPOINTS.LOCAL}</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value={API_ENDPOINTS.REMOTE} id="remote" />
|
|
||||||
<Label htmlFor="remote" className="truncate text-sm">Remote: {API_ENDPOINTS.REMOTE}</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
<div className="text-xs text-muted-foreground truncate">
|
|
||||||
Current endpoint: {currentEndpoint}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
*/}
|
|
||||||
|
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<img
|
<img
|
||||||
@@ -146,11 +100,8 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-xl sm:text-2xl font-bold tracking-tight text-foreground">{t("signInToYourAccount")}</h1>
|
<h1 className="text-xl sm:text-2xl font-bold tracking-tight text-foreground">{t("signInToYourAccount")}</h1>
|
||||||
{/* Removed "Don't have an account? Create one" text */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Removed Google Sign in button section */}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
|
||||||
{/* Error Alert */}
|
{/* Error Alert */}
|
||||||
{loginError && (
|
{loginError && (
|
||||||
@@ -171,10 +122,7 @@ const Login = () => {
|
|||||||
placeholder="your.email@provider.com"
|
placeholder="your.email@provider.com"
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => {
|
onChange={handleEmailChange}
|
||||||
setEmail(e.target.value);
|
|
||||||
setLoginError(''); // Clear error when user starts typing
|
|
||||||
}}
|
|
||||||
required
|
required
|
||||||
className="pl-10 text-sm sm:text-base h-9 sm:h-10"
|
className="pl-10 text-sm sm:text-base h-9 sm:h-10"
|
||||||
/>
|
/>
|
||||||
@@ -204,10 +152,7 @@ const Login = () => {
|
|||||||
placeholder="••••••••••••"
|
placeholder="••••••••••••"
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => {
|
onChange={handlePasswordChange}
|
||||||
setPassword(e.target.value);
|
|
||||||
setLoginError(''); // Clear error when user starts typing
|
|
||||||
}}
|
|
||||||
required
|
required
|
||||||
className="pl-10 text-sm sm:text-base h-9 sm:h-10"
|
className="pl-10 text-sm sm:text-base h-9 sm:h-10"
|
||||||
/>
|
/>
|
||||||
@@ -234,13 +179,38 @@ const Login = () => {
|
|||||||
{loading ? t("signingIn") : t("signIn")}
|
{loading ? t("signingIn") : t("signIn")}
|
||||||
{!loading && <LogIn className="ml-2 h-4 w-4" />}
|
{!loading && <LogIn className="ml-2 h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<p className="text-xxs sm:text-xs text-center text-muted-foreground">
|
|
||||||
{t("bySigningIn")} <a href="#" className="text-emerald-500">{t("termsAndConditions")}</a> {t("and")} <a href="#" className="text-emerald-500">{t("privacyPolicy")}</a>.
|
|
||||||
</p>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Footer with Social Media Icons */}
|
||||||
|
<div className="mt-8 flex items-center justify-center space-x-6">
|
||||||
|
<button
|
||||||
|
onClick={() => openExternalLink('https://github.com/operacle/checkcle')}
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
aria-label="GitHub"
|
||||||
|
>
|
||||||
|
<Github className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => openExternalLink('https://x.com/checkcle_oss')}
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
aria-label="X (Twitter)"
|
||||||
|
>
|
||||||
|
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => openExternalLink('https://discord.gg/xs9gbubGwX')}
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
aria-label="Discord"
|
||||||
|
>
|
||||||
|
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.010c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ForgotPasswordDialog
|
<ForgotPasswordDialog
|
||||||
open={showForgotPassword}
|
open={showForgotPassword}
|
||||||
onOpenChange={setShowForgotPassword}
|
onOpenChange={setShowForgotPassword}
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ export interface AlertConfiguration {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
created?: string;
|
created?: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
|
// Email specific fields
|
||||||
|
email_address?: string;
|
||||||
|
email_sender_name?: string;
|
||||||
|
smtp_server?: string;
|
||||||
|
smtp_port?: string;
|
||||||
|
webhook_id?: string;
|
||||||
|
channel_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const alertConfigService = {
|
export const alertConfigService = {
|
||||||
@@ -45,14 +52,14 @@ export const alertConfigService = {
|
|||||||
// console.info("Alert configuration created:", result);
|
// console.info("Alert configuration created:", result);
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Notification settings saved successfully",
|
description: "Notification channel created successfully",
|
||||||
});
|
});
|
||||||
return result as AlertConfiguration;
|
return result as AlertConfiguration;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("Error creating alert configuration:", error);
|
// console.error("Error creating alert configuration:", error);
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to save notification settings",
|
description: "Failed to create notification channel",
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
@@ -66,14 +73,14 @@ export const alertConfigService = {
|
|||||||
// console.info("Alert configuration updated:", result);
|
// console.info("Alert configuration updated:", result);
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Notification settings updated successfully",
|
description: "Notification channel updated successfully",
|
||||||
});
|
});
|
||||||
return result as AlertConfiguration;
|
return result as AlertConfiguration;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("Error updating alert configuration:", error);
|
// console.error("Error updating alert configuration:", error);
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to update notification settings",
|
description: "Failed to update notification channel",
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import { pb } from '@/lib/pocketbase';
|
import { pb } from '@/lib/pocketbase';
|
||||||
import { uptimeService } from '@/services/uptimeService';
|
import { uptimeService } from '@/services/uptimeService';
|
||||||
import { notificationService } from '@/services/notification'; // Import from the main notification service
|
|
||||||
import { prepareServiceForNotification } from '../utils/httpUtils';
|
import { prepareServiceForNotification } from '../utils/httpUtils';
|
||||||
import { UptimeData } from '@/types/service.types';
|
import { UptimeData } from '@/types/service.types';
|
||||||
|
|
||||||
@@ -9,7 +8,7 @@ import { UptimeData } from '@/types/service.types';
|
|||||||
* Handle a service that is determined to be UP
|
* Handle a service that is determined to be UP
|
||||||
*/
|
*/
|
||||||
export async function handleServiceUp(service: any, responseTime: number, formattedTime: string): Promise<void> {
|
export async function handleServiceUp(service: any, responseTime: number, formattedTime: string): Promise<void> {
|
||||||
console.log(`Service ${service.name} is UP! Response time: ${responseTime}ms`);
|
// console.log(`Service ${service.name} is UP! Response time: ${responseTime}ms`);
|
||||||
|
|
||||||
// Create a history record of this check with a more accurate timestamp
|
// Create a history record of this check with a more accurate timestamp
|
||||||
const uptimeData: UptimeData = {
|
const uptimeData: UptimeData = {
|
||||||
@@ -41,47 +40,18 @@ export async function handleServiceUp(service: any, responseTime: number, format
|
|||||||
try {
|
try {
|
||||||
await uptimeService.recordUptimeData(uptimeData);
|
await uptimeService.recordUptimeData(uptimeData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to record uptime data on first try, retrying...", error);
|
// console.error("Failed to record uptime data on first try, retrying...", error);
|
||||||
// Wait a short time and retry once
|
// Wait a short time and retry once
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
await uptimeService.recordUptimeData(uptimeData);
|
await uptimeService.recordUptimeData(uptimeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset notification count if service is recovered from DOWN status
|
// Status change logging (notification logic removed - will be handled by backend)
|
||||||
if (previousStatus === "down") {
|
|
||||||
console.log(`Service ${service.name} recovered from DOWN status - resetting notification count`);
|
|
||||||
notificationService.resetNotificationCount(service.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send notification if status changed from down to up
|
|
||||||
if (statusChanged) {
|
if (statusChanged) {
|
||||||
console.log(`Status changed from ${previousStatus} to UP - sending notification`);
|
// console.log(`Status changed from ${previousStatus} to UP - notification will be handled by backend`);
|
||||||
|
|
||||||
// Convert PocketBase record to Service type for notification
|
|
||||||
const serviceForNotification = prepareServiceForNotification(service, "up", responseTime);
|
|
||||||
|
|
||||||
// Check if alerts are muted - STRICT check for "muted" string value
|
|
||||||
if (service.alerts === "muted" || serviceForNotification.alerts === "muted") {
|
|
||||||
console.log(`Alerts are MUTED for service ${service.name}, SKIPPING UP notification`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Send notification through main notification service
|
|
||||||
await notificationService.sendNotification({
|
|
||||||
service: serviceForNotification,
|
|
||||||
status: "up",
|
|
||||||
responseTime: responseTime,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("UP notification sent successfully");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error sending UP notification:", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error handling service UP state:", error);
|
// console.error("Error handling service UP state:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +59,7 @@ export async function handleServiceUp(service: any, responseTime: number, format
|
|||||||
* Handle a service that is determined to be DOWN
|
* Handle a service that is determined to be DOWN
|
||||||
*/
|
*/
|
||||||
export async function handleServiceDown(service: any, formattedTime: string): Promise<void> {
|
export async function handleServiceDown(service: any, formattedTime: string): Promise<void> {
|
||||||
console.log(`Service ${service.name} is DOWN!`);
|
// console.log(`Service ${service.name} is DOWN!`);
|
||||||
|
|
||||||
// Create a history record of this check
|
// Create a history record of this check
|
||||||
const uptimeData: UptimeData = {
|
const uptimeData: UptimeData = {
|
||||||
@@ -106,7 +76,7 @@ export async function handleServiceDown(service: any, formattedTime: string): Pr
|
|||||||
const previousStatus = service.status;
|
const previousStatus = service.status;
|
||||||
const statusChanged = previousStatus !== "down";
|
const statusChanged = previousStatus !== "down";
|
||||||
|
|
||||||
console.log(`Service ${service.name} previous status: ${previousStatus}, statusChanged: ${statusChanged}`);
|
// console.log(`Service ${service.name} previous status: ${previousStatus}, statusChanged: ${statusChanged}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update service status
|
// Update service status
|
||||||
@@ -123,44 +93,15 @@ export async function handleServiceDown(service: any, formattedTime: string): Pr
|
|||||||
try {
|
try {
|
||||||
await uptimeService.recordUptimeData(uptimeData);
|
await uptimeService.recordUptimeData(uptimeData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to record uptime data on first try, retrying...", error);
|
// console.error("Failed to record uptime data on first try, retrying...", error);
|
||||||
// Wait a short time and retry once
|
// Wait a short time and retry once
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
await uptimeService.recordUptimeData(uptimeData);
|
await uptimeService.recordUptimeData(uptimeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert PocketBase record to Service type for notification
|
// Status change logging (notification logic removed - will be handled by backend)
|
||||||
const serviceForNotification = prepareServiceForNotification(service, "down");
|
// console.log("Service DOWN status recorded - notification will be handled by backend");
|
||||||
|
|
||||||
// Check if alerts are muted - STRICT check for "muted" string value
|
|
||||||
if (service.alerts === "muted" || serviceForNotification.alerts === "muted") {
|
|
||||||
console.log(`Alerts are MUTED for service ${service.name}, SKIPPING DOWN notification`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Attempting to send DOWN notification for service:", service.name);
|
|
||||||
console.log("Service notification data:", {
|
|
||||||
name: serviceForNotification.name,
|
|
||||||
notificationChannel: serviceForNotification.notificationChannel,
|
|
||||||
alertTemplate: serviceForNotification.alertTemplate,
|
|
||||||
retries: serviceForNotification.retries || 3,
|
|
||||||
alerts: serviceForNotification.alerts
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Use the main notification service - this handles retries internally
|
|
||||||
const result = await notificationService.sendNotification({
|
|
||||||
service: serviceForNotification,
|
|
||||||
status: "down",
|
|
||||||
responseTime: 0,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("DOWN notification sent result:", result);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending DOWN notification:", error);
|
// console.error("Error handling service DOWN state:", error);
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error handling service DOWN state:", error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import { pb } from '@/lib/pocketbase';
|
import { pb } from '@/lib/pocketbase';
|
||||||
import { monitoringIntervals } from '../monitoringIntervals';
|
import { monitoringIntervals } from '../monitoringIntervals';
|
||||||
import { notificationService } from '@/services/notificationService';
|
|
||||||
import { Service } from '@/types/service.types';
|
import { Service } from '@/types/service.types';
|
||||||
import { startMonitoringService } from './startMonitoring';
|
import { startMonitoringService } from './startMonitoring';
|
||||||
|
|
||||||
@@ -32,49 +31,13 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
|
|||||||
last_checked: now
|
last_checked: now
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert PocketBase record to Service type for notification
|
|
||||||
const serviceForNotification: Service = {
|
|
||||||
id: service.id,
|
|
||||||
name: service.name,
|
|
||||||
url: service.url || "",
|
|
||||||
host: service.host || "", // Include host property
|
|
||||||
type: service.service_type || service.type || "HTTP",
|
|
||||||
status: "up",
|
|
||||||
responseTime: service.response_time || 0,
|
|
||||||
uptime: service.uptime || 0,
|
|
||||||
lastChecked: now,
|
|
||||||
interval: service.heartbeat_interval || 60,
|
|
||||||
retries: service.max_retries || 3,
|
|
||||||
notificationChannel: service.notification_id,
|
|
||||||
alertTemplate: service.template_id,
|
|
||||||
muteAlerts: service.mute_alerts || false,
|
|
||||||
alerts: service.alerts || "unmuted",
|
|
||||||
muteChangedAt: service.mute_changed_at,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Double check if alerts are muted before sending notification
|
|
||||||
// Ensure we're using the correct field - alerts should be "muted" or "unmuted"
|
|
||||||
const alertsMuted = service.alerts === "muted" || serviceForNotification.alerts === "muted";
|
|
||||||
|
|
||||||
if (!alertsMuted) {
|
|
||||||
// console.log(`Alerts NOT muted for service ${service.name}, sending resume notification`);
|
|
||||||
// Send notification that service has been resumed
|
|
||||||
await notificationService.sendNotification({
|
|
||||||
service: serviceForNotification,
|
|
||||||
status: "up",
|
|
||||||
timestamp: now
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// console.log(`Alerts muted for service ${service.name}, skipping resume notification`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMPORTANT: Wait a brief moment to ensure the status update is processed
|
// IMPORTANT: Wait a brief moment to ensure the status update is processed
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
// Now start the service monitoring with a clean slate
|
// Now start the service monitoring with a clean slate
|
||||||
await startMonitoringService(serviceId);
|
await startMonitoringService(serviceId);
|
||||||
|
|
||||||
console.log(`Service ${service.name} resumed and ready for monitoring`);
|
// console.log(`Service ${service.name} resumed and ready for monitoring`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("Error resuming service:", error);
|
// console.error("Error resuming service:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,234 +1,6 @@
|
|||||||
import { pb } from "@/lib/pocketbase";
|
|
||||||
import { toast } from "@/hooks/use-toast";
|
|
||||||
import { AlertConfiguration } from "../alertConfigService";
|
|
||||||
import { NotificationTemplate } from "../templateService";
|
|
||||||
import { NotificationPayload } from "./types";
|
|
||||||
import { processTemplate, generateDefaultMessage } from "./templateProcessor";
|
|
||||||
import { sendTelegramNotification } from "./telegramService";
|
|
||||||
import { sendSignalNotification } from "./signalService";
|
|
||||||
|
|
||||||
// Track last notification times for services to implement cooldown
|
// Re-export template processor only
|
||||||
const lastNotifications: Record<string, {
|
export { processTemplate, generateDefaultMessage } from './templateProcessor';
|
||||||
timestamp: Date;
|
|
||||||
count: number;
|
|
||||||
lastMessageTime: Date; // Track when the last message was actually sent
|
|
||||||
}> = {};
|
|
||||||
|
|
||||||
// Cooldown period in milliseconds (5 minutes)
|
// Export types from template services
|
||||||
const NOTIFICATION_COOLDOWN = 5 * 60 * 1000;
|
export type { AnyTemplate } from '../templateService';
|
||||||
|
|
||||||
/**
|
|
||||||
* Notification service responsible for sending notifications based on service status changes
|
|
||||||
*/
|
|
||||||
export const notificationService = {
|
|
||||||
/**
|
|
||||||
* Send notification based on service status change
|
|
||||||
*/
|
|
||||||
async sendNotification(payload: NotificationPayload): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
console.log("=== NOTIFICATION SERVICE TRIGGER ===");
|
|
||||||
console.log("Sending notification for service:", payload.service.name, "Status:", payload.status);
|
|
||||||
console.log("Service type:", payload.service.type);
|
|
||||||
console.log("Service retries:", payload.service.retries || 3);
|
|
||||||
console.log("Alerts status:", payload.service.alerts);
|
|
||||||
|
|
||||||
// First check: Strictly check if alerts are muted for this specific service
|
|
||||||
if (payload.service.alerts === "muted") {
|
|
||||||
console.log(`BLOCKING NOTIFICATION: Alerts are explicitly muted for service ${payload.service.name} (ID: ${payload.service.id})`);
|
|
||||||
return true; // Return true as this is expected behavior, not a failure
|
|
||||||
}
|
|
||||||
|
|
||||||
// For DOWN status, implement the cooldown and max retries logic
|
|
||||||
if (payload.status === "down") {
|
|
||||||
const serviceId = payload.service.id;
|
|
||||||
// Get max retries from service config or default to 3
|
|
||||||
const maxRetries = typeof payload.service.retries === 'number' ? payload.service.retries : 3;
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
console.log(`DOWN notification for ${payload.service.name} - Max retries: ${maxRetries}`);
|
|
||||||
|
|
||||||
if (lastNotifications[serviceId]) {
|
|
||||||
const lastNotif = lastNotifications[serviceId];
|
|
||||||
const timeSinceLastNotif = now.getTime() - lastNotif.timestamp.getTime();
|
|
||||||
|
|
||||||
console.log(`Time since last notification tracking for ${payload.service.name}: ${Math.round(timeSinceLastNotif/1000)}s`);
|
|
||||||
console.log(`Current notification count: ${lastNotif.count}/${maxRetries}`);
|
|
||||||
|
|
||||||
// Check if we're within the cooldown period
|
|
||||||
if (timeSinceLastNotif < NOTIFICATION_COOLDOWN) {
|
|
||||||
// If max retries is set to 1, we should only send one notification during the cooldown period
|
|
||||||
if (maxRetries === 1) {
|
|
||||||
console.log(`Max retries is 1 - BLOCKING duplicate DOWN notification for ${payload.service.name}`);
|
|
||||||
return true; // Skip notification but return success
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we've reached max retries
|
|
||||||
if (lastNotif.count >= maxRetries) {
|
|
||||||
console.log(`DOWN notification for ${payload.service.name} skipped: Max retries (${maxRetries}) reached. Next notification after cooldown.`);
|
|
||||||
return true; // Skip notification but return success
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment count since we're sending another notification
|
|
||||||
console.log(`DOWN notification for ${payload.service.name}: ${lastNotif.count + 1}/${maxRetries}`);
|
|
||||||
lastNotifications[serviceId].count += 1;
|
|
||||||
lastNotifications[serviceId].lastMessageTime = now;
|
|
||||||
} else {
|
|
||||||
// Reset count after cooldown period
|
|
||||||
console.log(`Cooldown period elapsed for ${payload.service.name}. Resetting notification count.`);
|
|
||||||
lastNotifications[serviceId] = { timestamp: now, count: 1, lastMessageTime: now };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// First notification for this service
|
|
||||||
console.log(`First DOWN notification for ${payload.service.name}: 1/${maxRetries}`);
|
|
||||||
lastNotifications[serviceId] = { timestamp: now, count: 1, lastMessageTime: now };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if service has notification channel configured
|
|
||||||
const notificationChannel = payload.service.notificationChannel;
|
|
||||||
|
|
||||||
if (!notificationChannel || notificationChannel === "none") {
|
|
||||||
console.log("No notification channel configured for service", payload.service.name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Using notification channel ID:", notificationChannel);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get the alert configuration details
|
|
||||||
console.log("Fetching alert configuration with ID:", notificationChannel);
|
|
||||||
const alertConfig = await pb.collection('alert_configurations').getOne(notificationChannel);
|
|
||||||
console.log("Found alert configuration:", JSON.stringify({
|
|
||||||
...alertConfig,
|
|
||||||
bot_token: alertConfig.bot_token ? "[REDACTED]" : undefined
|
|
||||||
}, null, 2));
|
|
||||||
|
|
||||||
// Check enabled status - skip if explicitly disabled
|
|
||||||
if (alertConfig.enabled === false || alertConfig.enabled === "false") {
|
|
||||||
console.log("Alert configuration is disabled, skipping notification");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare message content
|
|
||||||
let messageContent = await this.prepareMessageContent(
|
|
||||||
payload.service,
|
|
||||||
payload.status,
|
|
||||||
payload.responseTime,
|
|
||||||
payload.message
|
|
||||||
);
|
|
||||||
|
|
||||||
// For DOWN status, add retry information to the message
|
|
||||||
if (payload.status === "down" && lastNotifications[payload.service.id]) {
|
|
||||||
const retryInfo = lastNotifications[payload.service.id];
|
|
||||||
const maxRetries = typeof payload.service.retries === 'number' ? payload.service.retries : 3;
|
|
||||||
if (maxRetries > 1) {
|
|
||||||
messageContent += `\n\nAlert ${retryInfo.count}/${maxRetries}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Prepared message for ${payload.status} notification:`, messageContent);
|
|
||||||
|
|
||||||
// Send notification based on type
|
|
||||||
if (alertConfig.notification_type === "telegram") {
|
|
||||||
console.log("Sending Telegram notification for service status change");
|
|
||||||
return await sendTelegramNotification(alertConfig as AlertConfiguration, messageContent);
|
|
||||||
} else if (alertConfig.notification_type === "signal") {
|
|
||||||
console.log("Sending Signal notification with message:", messageContent);
|
|
||||||
return await sendSignalNotification(alertConfig as AlertConfiguration, messageContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add other notification types here
|
|
||||||
console.log("Notification type not supported:", alertConfig.notification_type);
|
|
||||||
toast({
|
|
||||||
title: "Notification Error",
|
|
||||||
description: `Notification type '${alertConfig.notification_type}' not supported`,
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing notification channel:", error);
|
|
||||||
console.error("Error details:", error instanceof Error ? error.message : "Unknown error");
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Notification Error",
|
|
||||||
description: `Error processing notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error sending notification:", error);
|
|
||||||
toast({
|
|
||||||
title: "Notification Error",
|
|
||||||
description: `Failed to send notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare message content for notification
|
|
||||||
*/
|
|
||||||
async prepareMessageContent(
|
|
||||||
service: any,
|
|
||||||
status: string,
|
|
||||||
responseTime?: number,
|
|
||||||
defaultMessage?: string
|
|
||||||
): Promise<string> {
|
|
||||||
// Use provided message if available
|
|
||||||
if (defaultMessage) {
|
|
||||||
return defaultMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a template
|
|
||||||
let templateId = service.alertTemplate;
|
|
||||||
|
|
||||||
if (templateId && templateId !== "default") {
|
|
||||||
try {
|
|
||||||
console.log("Using specified template ID:", templateId);
|
|
||||||
const template = await pb.collection('notification_templates').getOne(templateId);
|
|
||||||
console.log("Template data:", template);
|
|
||||||
return processTemplate(
|
|
||||||
template as unknown as NotificationTemplate,
|
|
||||||
service,
|
|
||||||
status,
|
|
||||||
responseTime
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching template:", error);
|
|
||||||
// Fall back to default message format
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Using default message template");
|
|
||||||
return generateDefaultMessage(service.name, status, responseTime);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a test notification for a specific service status change
|
|
||||||
*/
|
|
||||||
async testServiceStatusNotification(serviceName: string, status: "up" | "down", responseTime?: number): Promise<boolean> {
|
|
||||||
const emoji = status === "up" ? "🟢" : "🔴";
|
|
||||||
const rtText = responseTime ? ` (Response time: ${responseTime}ms)` : "";
|
|
||||||
const message = `${emoji} Test notification: Service ${serviceName} is ${status.toUpperCase()}${rtText}`;
|
|
||||||
|
|
||||||
console.log("Test notification would be sent:", message);
|
|
||||||
// Instead of calling testTelegramNotification which no longer exists, just log and return success
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset notification count for a service
|
|
||||||
* This is called when a service recovers from DOWN to UP
|
|
||||||
*/
|
|
||||||
resetNotificationCount(serviceId: string): void {
|
|
||||||
if (lastNotifications[serviceId]) {
|
|
||||||
console.log(`Resetting notification count for service ${serviceId}`);
|
|
||||||
delete lastNotifications[serviceId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Re-export the types for easier imports
|
|
||||||
export * from "./types";
|
|
||||||
@@ -1,42 +1,42 @@
|
|||||||
|
|
||||||
import { NotificationTemplate } from "../templateService";
|
import { AnyTemplate } from "../templateService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a notification template with service data
|
* Process a notification template with service data
|
||||||
*/
|
*/
|
||||||
export function processTemplate(
|
export function processTemplate(
|
||||||
template: NotificationTemplate,
|
template: AnyTemplate,
|
||||||
service: any,
|
service: any,
|
||||||
status: string,
|
status: string,
|
||||||
responseTime?: number
|
responseTime?: number
|
||||||
): string {
|
): string {
|
||||||
try {
|
try {
|
||||||
console.log(`Processing template for status: ${status}`);
|
// console.log(`Processing template for status: ${status}`);
|
||||||
|
|
||||||
let templateText = "";
|
let templateText = "";
|
||||||
|
|
||||||
// Select the appropriate message template based on status
|
// Select the appropriate message template based on status
|
||||||
if (status === "up") {
|
if (status === "up") {
|
||||||
templateText = template.up_message || `Service ${service.name} is now UP`;
|
templateText = (template as any).up_message || `Service ${service.name} is now UP`;
|
||||||
} else if (status === "down") {
|
} else if (status === "down") {
|
||||||
templateText = template.down_message || `Service ${service.name} is DOWN`;
|
templateText = (template as any).down_message || `Service ${service.name} is DOWN`;
|
||||||
} else if (status === "warning") {
|
} else if (status === "warning") {
|
||||||
templateText = template.incident_message || `Warning: Service ${service.name} has an incident`;
|
templateText = (template as any).incident_message || `Warning: Service ${service.name} has an incident`;
|
||||||
} else if (status === "maintenance" || status === "paused") {
|
} else if (status === "maintenance" || status === "paused") {
|
||||||
templateText = template.maintenance_message || `Service ${service.name} is in maintenance mode`;
|
templateText = (template as any).maintenance_message || `Service ${service.name} is in maintenance mode`;
|
||||||
} else if (status === "resolved") {
|
} else if (status === "resolved") {
|
||||||
templateText = template.resolved_message || `Issue with service ${service.name} has been resolved`;
|
templateText = (template as any).resolved_message || `Issue with service ${service.name} has been resolved`;
|
||||||
} else {
|
} else {
|
||||||
templateText = `Service ${service.name} status changed to: ${status}`;
|
templateText = `Service ${service.name} status changed to: ${status}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip replacement if template is empty
|
// Skip replacement if template is empty
|
||||||
if (!templateText) {
|
if (!templateText) {
|
||||||
console.log("Empty template for status:", status);
|
// console.log("Empty template for status:", status);
|
||||||
return generateDefaultMessage(service.name, status, responseTime);
|
return generateDefaultMessage(service.name, status, responseTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Using template text:", templateText);
|
// console.log("Using template text:", templateText);
|
||||||
|
|
||||||
// Replace placeholders with actual values
|
// Replace placeholders with actual values
|
||||||
let message = templateText
|
let message = templateText
|
||||||
@@ -56,10 +56,10 @@ export function processTemplate(
|
|||||||
.replace(/\${url}/g, service.url || 'N/A')
|
.replace(/\${url}/g, service.url || 'N/A')
|
||||||
.replace(/\${time}/g, new Date().toLocaleString());
|
.replace(/\${time}/g, new Date().toLocaleString());
|
||||||
|
|
||||||
console.log("Processed template message:", message);
|
// console.log("Processed template message:", message);
|
||||||
return message;
|
return message;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing template:", error);
|
// console.error("Error processing template:", error);
|
||||||
return generateDefaultMessage(service.name, status, responseTime);
|
return generateDefaultMessage(service.name, status, responseTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
|
|
||||||
import { Service } from "@/types/service.types";
|
|
||||||
import { processTemplate, generateDefaultMessage } from "./notification/templateProcessor";
|
|
||||||
import { sendTelegramNotification, testSendTelegramMessage } from "./notification/telegramService";
|
|
||||||
import { pb } from "@/lib/pocketbase";
|
|
||||||
import { templateService } from "./templateService";
|
|
||||||
import { AlertConfiguration } from "./alertConfigService";
|
|
||||||
|
|
||||||
interface NotificationData {
|
|
||||||
service: Service;
|
|
||||||
status: string;
|
|
||||||
responseTime?: number;
|
|
||||||
timestamp: string;
|
|
||||||
_notificationSource?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track last notification times for services to implement cooldown
|
|
||||||
const lastNotifications: Record<string, {
|
|
||||||
timestamp: Date;
|
|
||||||
count: number;
|
|
||||||
}> = {};
|
|
||||||
|
|
||||||
// Cooldown period in milliseconds (5 minutes)
|
|
||||||
const NOTIFICATION_COOLDOWN = 5 * 60 * 1000;
|
|
||||||
|
|
||||||
export const notificationService = {
|
|
||||||
/**
|
|
||||||
* Send a notification for a service status change
|
|
||||||
*/
|
|
||||||
async sendNotification(data: NotificationData): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const { service, status, responseTime } = data;
|
|
||||||
|
|
||||||
// console.log(`Preparing to send notification for service: ${service.name}, status: ${status}`);
|
|
||||||
// console.log(`Service alerts status: ${service.alerts}`);
|
|
||||||
|
|
||||||
// First check if alerts are muted for this service
|
|
||||||
// STRICT equality check against "muted" string value
|
|
||||||
if (service.alerts === "muted") {
|
|
||||||
// console.log(`NOTIFICATION BLOCKED: Alerts are muted for service: ${service.name}`);
|
|
||||||
return true; // Return true as this is expected behavior
|
|
||||||
}
|
|
||||||
|
|
||||||
// For paused status, check if this is a duplicate notification from another source
|
|
||||||
// This helps prevent the double-notification issue
|
|
||||||
if (status === "paused" && data._notificationSource === "duplicate_check") {
|
|
||||||
// console.log("NOTIFICATION BLOCKED: Duplicate pause notification detected");
|
|
||||||
return true; // Return true as this is expected behavior
|
|
||||||
}
|
|
||||||
|
|
||||||
// For DOWN status, implement the cooldown and max retries logic
|
|
||||||
if (status === "down") {
|
|
||||||
const serviceId = service.id;
|
|
||||||
const maxRetries = service.retries || 3; // Default to 3 if not specified
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
if (lastNotifications[serviceId]) {
|
|
||||||
const lastNotif = lastNotifications[serviceId];
|
|
||||||
const timeSinceLastNotif = now.getTime() - lastNotif.timestamp.getTime();
|
|
||||||
|
|
||||||
// Check if we're within the cooldown period
|
|
||||||
if (timeSinceLastNotif < NOTIFICATION_COOLDOWN) {
|
|
||||||
// Increment count only if we haven't reached max retries
|
|
||||||
if (lastNotif.count < maxRetries) {
|
|
||||||
// console.log(`DOWN notification for ${service.name}: ${lastNotif.count + 1}/${maxRetries}`);
|
|
||||||
lastNotifications[serviceId].count += 1;
|
|
||||||
} else {
|
|
||||||
// console.log(`DOWN notification for ${service.name} skipped: Max retries (${maxRetries}) reached. Next notification after cooldown.`);
|
|
||||||
return true; // Skip notification but return success
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Reset count after cooldown period
|
|
||||||
// console.log(`Cooldown period elapsed for ${service.name}. Resetting notification count.`);
|
|
||||||
lastNotifications[serviceId] = { timestamp: now, count: 1 };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// First notification for this service
|
|
||||||
// console.log(`First DOWN notification for ${service.name}: 1/${maxRetries}`);
|
|
||||||
lastNotifications[serviceId] = { timestamp: now, count: 1 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update timestamp for the current notification
|
|
||||||
lastNotifications[serviceId].timestamp = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if notification channel is set
|
|
||||||
if (!service.notificationChannel) {
|
|
||||||
// console.log(`No notification channel set for service: ${service.name}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the notification configuration
|
|
||||||
const alertConfigRecord = await pb.collection('alert_configurations').getOne(service.notificationChannel);
|
|
||||||
|
|
||||||
if (!alertConfigRecord) {
|
|
||||||
// console.error(`Alert configuration not found for ID: ${service.notificationChannel}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!alertConfigRecord.enabled) {
|
|
||||||
// console.log(`Alert configuration is disabled for service: ${service.name}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert PocketBase record to AlertConfiguration
|
|
||||||
const alertConfig: AlertConfiguration = {
|
|
||||||
id: alertConfigRecord.id,
|
|
||||||
collectionId: alertConfigRecord.collectionId,
|
|
||||||
collectionName: alertConfigRecord.collectionName,
|
|
||||||
service_id: alertConfigRecord.service_id || "",
|
|
||||||
notification_type: alertConfigRecord.notification_type,
|
|
||||||
telegram_chat_id: alertConfigRecord.telegram_chat_id,
|
|
||||||
discord_webhook_url: alertConfigRecord.discord_webhook_url,
|
|
||||||
signal_number: alertConfigRecord.signal_number,
|
|
||||||
notify_name: alertConfigRecord.notify_name,
|
|
||||||
bot_token: alertConfigRecord.bot_token,
|
|
||||||
template_id: alertConfigRecord.template_id,
|
|
||||||
slack_webhook_url: alertConfigRecord.slack_webhook_url,
|
|
||||||
enabled: alertConfigRecord.enabled,
|
|
||||||
created: alertConfigRecord.created,
|
|
||||||
updated: alertConfigRecord.updated
|
|
||||||
};
|
|
||||||
|
|
||||||
// Select template if one is specified
|
|
||||||
let template;
|
|
||||||
if (service.alertTemplate) {
|
|
||||||
try {
|
|
||||||
template = await templateService.getTemplate(service.alertTemplate);
|
|
||||||
} catch (error) {
|
|
||||||
// console.error(`Error fetching template for ID: ${service.alertTemplate}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the notification message
|
|
||||||
let message;
|
|
||||||
if (template) {
|
|
||||||
message = processTemplate(template, service, status, responseTime);
|
|
||||||
} else {
|
|
||||||
message = generateDefaultMessage(service.name, status, responseTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For DOWN status, add retry information to the message
|
|
||||||
if (status === "down" && lastNotifications[service.id]) {
|
|
||||||
const retryInfo = lastNotifications[service.id];
|
|
||||||
const maxRetries = service.retries || 3;
|
|
||||||
message += `\n\nAlert ${retryInfo.count}/${maxRetries}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`Prepared notification message: ${message}`);
|
|
||||||
|
|
||||||
// Send notification based on notification type
|
|
||||||
const notificationType = alertConfig.notification_type;
|
|
||||||
|
|
||||||
if (notificationType === 'telegram') {
|
|
||||||
return await sendTelegramNotification(alertConfig, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other types like discord, slack, etc. (not implemented yet)
|
|
||||||
// console.log(`Notification type ${notificationType} not implemented yet`);
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
// console.error("Error sending notification:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a test notification for a service status change
|
|
||||||
*/
|
|
||||||
async testServiceStatusNotification(serviceName: string, status: string, responseTime?: number): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const message = status === 'up'
|
|
||||||
? `Service ${serviceName} is UP${responseTime ? ` (Response time: ${responseTime}ms)` : ''}`
|
|
||||||
: `Service ${serviceName} is DOWN`;
|
|
||||||
|
|
||||||
// console.log(`Test notification would have been sent: ${message}`);
|
|
||||||
return true; // Just log, don't actually send
|
|
||||||
} catch (error) {
|
|
||||||
// console.error("Error in test notification:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset notification count for a service
|
|
||||||
* This can be called when a service recovers from DOWN to UP
|
|
||||||
*/
|
|
||||||
resetNotificationCount(serviceId: string): void {
|
|
||||||
if (lastNotifications[serviceId]) {
|
|
||||||
// console.log(`Resetting notification count for service ${serviceId}`);
|
|
||||||
delete lastNotifications[serviceId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
|
||||||
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
|
||||||
|
export interface ServerNotificationTemplate {
|
||||||
|
id: string;
|
||||||
|
collectionId: string;
|
||||||
|
collectionName: string;
|
||||||
|
name: string;
|
||||||
|
ram_message: string;
|
||||||
|
cpu_message: string;
|
||||||
|
disk_message: string;
|
||||||
|
network_message: string;
|
||||||
|
up_message: string;
|
||||||
|
down_message: string;
|
||||||
|
notification_id: string;
|
||||||
|
warning_message: string;
|
||||||
|
paused_message: string;
|
||||||
|
cpu_temp_message: string;
|
||||||
|
disk_io_message: string;
|
||||||
|
placeholder: string;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateUpdateServerNotificationTemplateData {
|
||||||
|
name: string;
|
||||||
|
ram_message: string;
|
||||||
|
cpu_message: string;
|
||||||
|
disk_message: string;
|
||||||
|
network_message: string;
|
||||||
|
up_message: string;
|
||||||
|
down_message: string;
|
||||||
|
notification_id: string;
|
||||||
|
warning_message: string;
|
||||||
|
paused_message: string;
|
||||||
|
cpu_temp_message: string;
|
||||||
|
disk_io_message: string;
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const serverNotificationTemplateService = {
|
||||||
|
async getTemplates(): Promise<ServerNotificationTemplate[]> {
|
||||||
|
try {
|
||||||
|
// console.log("Fetching server notification templates");
|
||||||
|
const response = await pb.collection('server_notification_templates').getList(1, 50, {
|
||||||
|
sort: '-created',
|
||||||
|
});
|
||||||
|
// console.log("Server notification templates response:", response);
|
||||||
|
return response.items as unknown as ServerNotificationTemplate[];
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("Error fetching server notification templates:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTemplate(id: string): Promise<ServerNotificationTemplate> {
|
||||||
|
try {
|
||||||
|
// console.log(`Fetching server notification template with id: ${id}`);
|
||||||
|
const response = await pb.collection('server_notification_templates').getOne(id);
|
||||||
|
// console.log("Server notification template response:", response);
|
||||||
|
return response as unknown as ServerNotificationTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
/// console.error(`Error fetching server notification template with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTemplate(data: CreateUpdateServerNotificationTemplateData): Promise<ServerNotificationTemplate> {
|
||||||
|
try {
|
||||||
|
// console.log("Creating new server notification template with data:", data);
|
||||||
|
const response = await pb.collection('server_notification_templates').create(data);
|
||||||
|
// console.log("Create server notification template response:", response);
|
||||||
|
return response as unknown as ServerNotificationTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("Error creating server notification template:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTemplate(id: string, data: Partial<CreateUpdateServerNotificationTemplateData>): Promise<ServerNotificationTemplate> {
|
||||||
|
try {
|
||||||
|
// console.log(`Updating server notification template with id: ${id}`, data);
|
||||||
|
const response = await pb.collection('server_notification_templates').update(id, data);
|
||||||
|
// console.log("Update server notification template response:", response);
|
||||||
|
return response as unknown as ServerNotificationTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(`Error updating server notification template with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteTemplate(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// console.log(`Deleting server notification template with id: ${id}`);
|
||||||
|
await pb.collection('server_notification_templates').delete(id);
|
||||||
|
// console.log("Server notification template deleted successfully");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(`Error deleting server notification template with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
|
||||||
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
|
||||||
|
export interface ServiceNotificationTemplate {
|
||||||
|
id: string;
|
||||||
|
collectionId: string;
|
||||||
|
collectionName: string;
|
||||||
|
name: string;
|
||||||
|
up_message: string;
|
||||||
|
down_message: string;
|
||||||
|
maintenance_message: string;
|
||||||
|
incident_message: string;
|
||||||
|
resolved_message: string;
|
||||||
|
placeholder: string;
|
||||||
|
warning_message: string;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateUpdateServiceNotificationTemplateData {
|
||||||
|
name: string;
|
||||||
|
up_message: string;
|
||||||
|
down_message: string;
|
||||||
|
maintenance_message: string;
|
||||||
|
incident_message: string;
|
||||||
|
resolved_message: string;
|
||||||
|
placeholder: string;
|
||||||
|
warning_message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const serviceNotificationTemplateService = {
|
||||||
|
async getTemplates(): Promise<ServiceNotificationTemplate[]> {
|
||||||
|
try {
|
||||||
|
// console.log("Fetching service notification templates");
|
||||||
|
const response = await pb.collection('service_notification_templates').getList(1, 50, {
|
||||||
|
sort: '-created',
|
||||||
|
});
|
||||||
|
// console.log("Service notification templates response:", response);
|
||||||
|
return response.items as unknown as ServiceNotificationTemplate[];
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("Error fetching service notification templates:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTemplate(id: string): Promise<ServiceNotificationTemplate> {
|
||||||
|
try {
|
||||||
|
// console.log(`Fetching service notification template with id: ${id}`);
|
||||||
|
const response = await pb.collection('service_notification_templates').getOne(id);
|
||||||
|
// console.log("Service notification template response:", response);
|
||||||
|
return response as unknown as ServiceNotificationTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(`Error fetching service notification template with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTemplate(data: CreateUpdateServiceNotificationTemplateData): Promise<ServiceNotificationTemplate> {
|
||||||
|
try {
|
||||||
|
// console.log("Creating new service notification template with data:", data);
|
||||||
|
const response = await pb.collection('service_notification_templates').create(data);
|
||||||
|
// console.log("Create service notification template response:", response);
|
||||||
|
return response as unknown as ServiceNotificationTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("Error creating service notification template:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTemplate(id: string, data: Partial<CreateUpdateServiceNotificationTemplateData>): Promise<ServiceNotificationTemplate> {
|
||||||
|
try {
|
||||||
|
// console.log(`Updating service notification template with id: ${id}`, data);
|
||||||
|
const response = await pb.collection('service_notification_templates').update(id, data);
|
||||||
|
// console.log("Update service notification template response:", response);
|
||||||
|
return response as unknown as ServiceNotificationTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(`Error updating service notification template with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteTemplate(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// console.log(`Deleting service notification template with id: ${id}`);
|
||||||
|
await pb.collection('service_notification_templates').delete(id);
|
||||||
|
// console.log("Service notification template deleted successfully");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(`Error deleting service notification template with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
|
||||||
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
|
||||||
|
export interface SslNotificationTemplate {
|
||||||
|
id: string;
|
||||||
|
collectionId: string;
|
||||||
|
collectionName: string;
|
||||||
|
name: string;
|
||||||
|
expired: string;
|
||||||
|
exiring_soon: string;
|
||||||
|
warning: string;
|
||||||
|
placeholder: string;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateUpdateSslNotificationTemplateData {
|
||||||
|
name: string;
|
||||||
|
expired: string;
|
||||||
|
exiring_soon: string;
|
||||||
|
warning: string;
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sslNotificationTemplateService = {
|
||||||
|
async getTemplates(): Promise<SslNotificationTemplate[]> {
|
||||||
|
try {
|
||||||
|
// console.log("Fetching SSL notification templates");
|
||||||
|
const response = await pb.collection('ssl_notification_templates').getList(1, 50, {
|
||||||
|
sort: '-created',
|
||||||
|
});
|
||||||
|
// console.log("SSL notification templates response:", response);
|
||||||
|
return response.items as unknown as SslNotificationTemplate[];
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("Error fetching SSL notification templates:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTemplate(id: string): Promise<SslNotificationTemplate> {
|
||||||
|
try {
|
||||||
|
// console.log(`Fetching SSL notification template with id: ${id}`);
|
||||||
|
const response = await pb.collection('ssl_notification_templates').getOne(id);
|
||||||
|
// console.log("SSL notification template response:", response);
|
||||||
|
return response as unknown as SslNotificationTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(`Error fetching SSL notification template with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTemplate(data: CreateUpdateSslNotificationTemplateData): Promise<SslNotificationTemplate> {
|
||||||
|
try {
|
||||||
|
// console.log("Creating new SSL notification template with data:", data);
|
||||||
|
const response = await pb.collection('ssl_notification_templates').create(data);
|
||||||
|
// console.log("Create SSL notification template response:", response);
|
||||||
|
return response as unknown as SslNotificationTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("Error creating SSL notification template:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTemplate(id: string, data: Partial<CreateUpdateSslNotificationTemplateData>): Promise<SslNotificationTemplate> {
|
||||||
|
try {
|
||||||
|
// console.log(`Updating SSL notification template with id: ${id}`, data);
|
||||||
|
const response = await pb.collection('ssl_notification_templates').update(id, data);
|
||||||
|
// console.log("Update SSL notification template response:", response);
|
||||||
|
return response as unknown as SslNotificationTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(`Error updating SSL notification template with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteTemplate(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// console.log(`Deleting SSL notification template with id: ${id}`);
|
||||||
|
await pb.collection('ssl_notification_templates').delete(id);
|
||||||
|
// console.log("SSL notification template deleted successfully");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(`Error deleting SSL notification template with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,100 +1,120 @@
|
|||||||
|
|
||||||
|
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
import {
|
||||||
|
serverNotificationTemplateService,
|
||||||
|
ServerNotificationTemplate,
|
||||||
|
CreateUpdateServerNotificationTemplateData
|
||||||
|
} from "./serverNotificationTemplateService";
|
||||||
|
import {
|
||||||
|
serviceNotificationTemplateService,
|
||||||
|
ServiceNotificationTemplate,
|
||||||
|
CreateUpdateServiceNotificationTemplateData
|
||||||
|
} from "./serviceNotificationTemplateService";
|
||||||
|
import {
|
||||||
|
sslNotificationTemplateService,
|
||||||
|
SslNotificationTemplate,
|
||||||
|
CreateUpdateSslNotificationTemplateData
|
||||||
|
} from "./sslNotificationTemplateService";
|
||||||
|
|
||||||
export interface NotificationTemplate {
|
export type TemplateType = 'server' | 'service' | 'ssl';
|
||||||
id: string;
|
|
||||||
collectionId: string;
|
|
||||||
collectionName: string;
|
|
||||||
name: string;
|
|
||||||
up_message: string;
|
|
||||||
down_message: string;
|
|
||||||
service_name_placeholder: string;
|
|
||||||
response_time_placeholder: string;
|
|
||||||
status_placeholder: string;
|
|
||||||
threshold_placeholder: string;
|
|
||||||
type: string;
|
|
||||||
maintenance_message: string;
|
|
||||||
incident_message: string;
|
|
||||||
resolved_message: string;
|
|
||||||
created: string;
|
|
||||||
updated: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateUpdateTemplateData {
|
export type AnyTemplate = ServerNotificationTemplate | ServiceNotificationTemplate | SslNotificationTemplate;
|
||||||
name: string;
|
export type AnyTemplateData = CreateUpdateServerNotificationTemplateData | CreateUpdateServiceNotificationTemplateData | CreateUpdateSslNotificationTemplateData;
|
||||||
up_message: string;
|
|
||||||
down_message: string;
|
// Export individual template types
|
||||||
service_name_placeholder: string;
|
export type { ServerNotificationTemplate, ServiceNotificationTemplate, SslNotificationTemplate };
|
||||||
response_time_placeholder: string;
|
export type { CreateUpdateServerNotificationTemplateData, CreateUpdateServiceNotificationTemplateData, CreateUpdateSslNotificationTemplateData };
|
||||||
status_placeholder: string;
|
|
||||||
threshold_placeholder: string;
|
|
||||||
type: string;
|
|
||||||
maintenance_message: string;
|
|
||||||
incident_message: string;
|
|
||||||
resolved_message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const templateService = {
|
export const templateService = {
|
||||||
async getTemplates(): Promise<NotificationTemplate[]> {
|
async getTemplates(type: TemplateType): Promise<AnyTemplate[]> {
|
||||||
try {
|
switch (type) {
|
||||||
// console.log("Fetching server notification templates");
|
case 'server':
|
||||||
const response = await pb.collection('server_notification_templates').getList(1, 50, {
|
return serverNotificationTemplateService.getTemplates();
|
||||||
sort: '-created',
|
case 'service':
|
||||||
});
|
return serviceNotificationTemplateService.getTemplates();
|
||||||
// console.log("Server templates response:", response);
|
case 'ssl':
|
||||||
return response.items as unknown as NotificationTemplate[];
|
return sslNotificationTemplateService.getTemplates();
|
||||||
} catch (error) {
|
default:
|
||||||
// console.error("Error fetching server templates:", error);
|
throw new Error(`Unknown template type: ${type}`);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTemplate(id: string): Promise<NotificationTemplate> {
|
async getTemplate(id: string, type: TemplateType): Promise<AnyTemplate> {
|
||||||
try {
|
switch (type) {
|
||||||
// console.log(`Fetching server template with id: ${id}`);
|
case 'server':
|
||||||
const response = await pb.collection('server_notification_templates').getOne(id);
|
return serverNotificationTemplateService.getTemplate(id);
|
||||||
// console.log("Server template response:", response);
|
case 'service':
|
||||||
return response as unknown as NotificationTemplate;
|
return serviceNotificationTemplateService.getTemplate(id);
|
||||||
} catch (error) {
|
case 'ssl':
|
||||||
// console.error(`Error fetching server template with id ${id}:`, error);
|
return sslNotificationTemplateService.getTemplate(id);
|
||||||
throw error;
|
default:
|
||||||
|
throw new Error(`Unknown template type: ${type}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async createTemplate(data: CreateUpdateTemplateData): Promise<NotificationTemplate> {
|
async createTemplate(data: AnyTemplateData, type: TemplateType): Promise<AnyTemplate> {
|
||||||
try {
|
switch (type) {
|
||||||
// console.log("Creating new server template with data:", data);
|
case 'server':
|
||||||
const response = await pb.collection('server_notification_templates').create(data);
|
return serverNotificationTemplateService.createTemplate(data as CreateUpdateServerNotificationTemplateData);
|
||||||
// console.log("Create server template response:", response);
|
case 'service':
|
||||||
return response as unknown as NotificationTemplate;
|
return serviceNotificationTemplateService.createTemplate(data as CreateUpdateServiceNotificationTemplateData);
|
||||||
} catch (error) {
|
case 'ssl':
|
||||||
// console.error("Error creating server template:", error);
|
return sslNotificationTemplateService.createTemplate(data as CreateUpdateSslNotificationTemplateData);
|
||||||
throw error;
|
default:
|
||||||
|
throw new Error(`Unknown template type: ${type}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateTemplate(id: string, data: Partial<CreateUpdateTemplateData>): Promise<NotificationTemplate> {
|
async updateTemplate(id: string, data: Partial<AnyTemplateData>, type: TemplateType): Promise<AnyTemplate> {
|
||||||
try {
|
switch (type) {
|
||||||
// console.log(`Updating server template with id: ${id}`, data);
|
case 'server':
|
||||||
const response = await pb.collection('server_notification_templates').update(id, data);
|
return serverNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateServerNotificationTemplateData>);
|
||||||
// console.log("Update server template response:", response);
|
case 'service':
|
||||||
return response as unknown as NotificationTemplate;
|
return serviceNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateServiceNotificationTemplateData>);
|
||||||
} catch (error) {
|
case 'ssl':
|
||||||
// console.error(`Error updating server template with id ${id}:`, error);
|
return sslNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateSslNotificationTemplateData>);
|
||||||
throw error;
|
default:
|
||||||
|
throw new Error(`Unknown template type: ${type}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteTemplate(id: string): Promise<boolean> {
|
async deleteTemplate(id: string, type: TemplateType): Promise<boolean> {
|
||||||
try {
|
switch (type) {
|
||||||
// console.log(`Deleting server template with id: ${id}`);
|
case 'server':
|
||||||
await pb.collection('server_notification_templates').delete(id);
|
return serverNotificationTemplateService.deleteTemplate(id);
|
||||||
// console.log("Server template deleted successfully");
|
case 'service':
|
||||||
return true;
|
return serviceNotificationTemplateService.deleteTemplate(id);
|
||||||
} catch (error) {
|
case 'ssl':
|
||||||
// console.error(`Error deleting server template with id ${id}:`, error);
|
return sslNotificationTemplateService.deleteTemplate(id);
|
||||||
throw error;
|
default:
|
||||||
|
throw new Error(`Unknown template type: ${type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Template type configurations
|
||||||
|
export const templateTypeConfigs = {
|
||||||
|
server: {
|
||||||
|
label: 'Server Monitoring',
|
||||||
|
description: 'Templates for server resource monitoring alerts',
|
||||||
|
placeholders: [
|
||||||
|
'${server_name}', '${cpu_usage}', '${ram_usage}', '${disk_usage}',
|
||||||
|
'${network_usage}', '${cpu_temp}', '${disk_io}', '${threshold}', '${time}'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
service: {
|
||||||
|
label: 'Service Uptime',
|
||||||
|
description: 'Templates for service uptime monitoring alerts',
|
||||||
|
placeholders: [
|
||||||
|
'${service_name}', '${status}', '${response_time}', '${url}',
|
||||||
|
'${uptime}', '${downtime}', '${time}'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
ssl: {
|
||||||
|
label: 'SSL Certificate',
|
||||||
|
description: 'Templates for SSL certificate monitoring alerts',
|
||||||
|
placeholders: [
|
||||||
|
'${domain}', '${certificate_name}', '${expiry_date}', '${days_left}',
|
||||||
|
'${issuer}', '${serial_number}', '${time}'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
export interface WebhookConfiguration {
|
||||||
|
id?: string;
|
||||||
|
collectionId?: string;
|
||||||
|
collectionName?: string;
|
||||||
|
user_id?: string;
|
||||||
|
url: string;
|
||||||
|
enabled: string;
|
||||||
|
secret?: string;
|
||||||
|
headers?: string;
|
||||||
|
retry_count?: string;
|
||||||
|
trigger_events?: string;
|
||||||
|
description?: string;
|
||||||
|
name: string;
|
||||||
|
method?: string;
|
||||||
|
payload_template?: string;
|
||||||
|
created?: string;
|
||||||
|
updated?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const webhookService = {
|
||||||
|
async createWebhook(config: Omit<WebhookConfiguration, 'id' | 'collectionId' | 'collectionName' | 'created' | 'updated'>): Promise<WebhookConfiguration | null> {
|
||||||
|
// console.info("Creating webhook configuration:", config);
|
||||||
|
try {
|
||||||
|
const result = await pb.collection('webhook').create(config);
|
||||||
|
// console.info("Webhook configuration created:", result);
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Webhook created successfully",
|
||||||
|
});
|
||||||
|
return result as WebhookConfiguration;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("Error creating webhook configuration:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to create webhook",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateWebhook(id: string, config: Partial<WebhookConfiguration>): Promise<WebhookConfiguration | null> {
|
||||||
|
// console.info(`Updating webhook configuration ${id}:`, config);
|
||||||
|
try {
|
||||||
|
const result = await pb.collection('webhook').update(id, config);
|
||||||
|
// console.info("Webhook configuration updated:", result);
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Webhook updated successfully",
|
||||||
|
});
|
||||||
|
return result as WebhookConfiguration;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("Error updating webhook configuration:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to update webhook",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user