Merge branch 'operacle:develop' into develop
@@ -126,18 +126,26 @@ docker run -d \
|
||||
|
||||
### 🥈 Silver Uptime Ally
|
||||
|
||||
<a href="https://github.com/sponsors/tolaleng">
|
||||
<img src="https://avatars.githubusercontent.com/u/135371107?v=4" width="75" height="75" style="border-radius: 50%" />
|
||||
<a href="https://github.com/michaelortnerit">
|
||||
<img src="https://avatars.githubusercontent.com/u/135371107?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
|
||||
</a>
|
||||
|
||||
### 🧡 Ping Supporter
|
||||
|
||||
<a href="https://github.com/sponsors/tolaleng">
|
||||
<img src="https://avatars.githubusercontent.com/u/4575656?v=4" width="75" height="75" style="border-radius: 50%" />
|
||||
<a href="https://github.com/samang-dauth">
|
||||
<img src="https://avatars.githubusercontent.com/u/4575656?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
|
||||
</a>
|
||||
|
||||
### 🤝 Ecosystem & Community Partner
|
||||
<a href="https://github.com/gitbookio">
|
||||
<img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4"
|
||||
width="75" height="75"
|
||||
style="border-radius: 50%; display: block;" />
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 👥 Contributors
|
||||
Thank you for contributing and continuously making CheckCle better, you're awesome 🫶
|
||||
|
||||
@@ -159,7 +167,7 @@ Here are some ways you can help improve CheckCle:
|
||||
|
||||
## 🌍 Stay Connected
|
||||
- Website: [checkcle.io](https://checkcle.io)
|
||||
- Documentation: [docs.checkcle.io](https://docs.checkcle.io)
|
||||
- Documentation: [docs.checkcle.io](https://docs.checkcle.io) | Big thanks to [GitBook](https://github.com/gitbookio) for sponsoring the OSS site plan for CheckCle!
|
||||
- Chat on Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
|
||||
- Follow us on X: [@checkcle_oss](https://x.com/checkcle_oss)
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -1,15 +1,13 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AuthUser } from "@/services/authService";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut, Grid3x3 } from "lucide-react";
|
||||
import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut } from "lucide-react";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSystemSettings } from "@/hooks/useSystemSettings";
|
||||
import QuickActionsDialog from "./QuickActionsDialog";
|
||||
|
||||
interface HeaderProps {
|
||||
currentUser: AuthUser | null;
|
||||
@@ -29,7 +27,6 @@ export const Header = ({
|
||||
const [greeting, setGreeting] = useState<string>("");
|
||||
const { systemName } = useSystemSettings();
|
||||
const navigate = useNavigate();
|
||||
const [quickActionsOpen, setQuickActionsOpen] = useState(false);
|
||||
|
||||
// Set greeting based on time of day
|
||||
useEffect(() => {
|
||||
@@ -53,7 +50,7 @@ export const Header = ({
|
||||
// Log avatar data for debugging
|
||||
useEffect(() => {
|
||||
if (currentUser) {
|
||||
// console.log("Avatar URL in Header:", currentUser.avatar);
|
||||
//console.log("Avatar URL in Header:", currentUser.avatar);
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
@@ -66,7 +63,7 @@ export const Header = ({
|
||||
} else {
|
||||
avatarUrl = currentUser.avatar;
|
||||
}
|
||||
// console.log("Final avatar URL:", avatarUrl);
|
||||
console.log("Final avatar URL:", avatarUrl);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -91,16 +88,6 @@ export const Header = ({
|
||||
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
|
||||
</Button>
|
||||
|
||||
{/* Quick Actions Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setQuickActionsOpen(true)}
|
||||
className="mr-2"
|
||||
>
|
||||
<Grid3x3 className="h-5 w-5 text-green-500" />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 ✨</h1>
|
||||
</div>
|
||||
@@ -162,7 +149,7 @@ export const Header = ({
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full w-8 h-8 border-border"
|
||||
onClick={() => window.open("https://x.com/checkcle_oss", "_blank")}
|
||||
onClick={() => window.open("https://x.com/checkcle_oss)", "_blank")}
|
||||
>
|
||||
<span className="sr-only">X (Twitter)</span>
|
||||
<Twitter className="w-4 h-4" />
|
||||
@@ -223,9 +210,6 @@ export const Header = ({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions Dialog */}
|
||||
<QuickActionsDialog isOpen={quickActionsOpen} setIsOpen={setQuickActionsOpen} />
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -1,145 +0,0 @@
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogClose
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { X } from "lucide-react";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface QuickActionsDialogProps {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const QuickActionsDialog: React.FC<QuickActionsDialogProps> = ({ isOpen, setIsOpen }) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent className="sm:max-w-[800px] bg-background max-h-[90vh] overflow-y-auto">
|
||||
<DialogClose className="absolute right-4 top-4">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold">{t('quickActions')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('quickActionsDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
|
||||
{/* Website Monitoring Card */}
|
||||
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||
<h3 className="font-semibold text-lg mb-2">{t('monitorWebsite')}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t('monitorWebsiteDesc')}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">{t('quickTips')}:</p>
|
||||
<ul className="list-disc pl-5 text-sm space-y-1">
|
||||
<li>{t('monitorMultipleEndpoints')}</li>
|
||||
<li>{t('trackResponseTimes')}</li>
|
||||
<li>{t('setCustomAlerts')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Server Monitoring Card */}
|
||||
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||
<h3 className="font-semibold text-lg mb-2">{t('monitorServer')}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t('monitorServerDesc')}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">{t('quickTips')}:</p>
|
||||
<ul className="list-disc pl-5 text-sm space-y-1">
|
||||
<li>{t('monitorCPURam')}</li>
|
||||
<li>{t('trackNetworkMetrics')}</li>
|
||||
<li>{t('viewProcesses')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SSL Certificate Card */}
|
||||
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||
<h3 className="font-semibold text-lg mb-2">{t('sslCertificate')}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t('sslCertificateDesc')}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">{t('quickTips')}:</p>
|
||||
<ul className="list-disc pl-5 text-sm space-y-1">
|
||||
<li>{t('trackSSLExpiration')}</li>
|
||||
<li>{t('getEarlyRenewal')}</li>
|
||||
<li>{t('monitorMultipleDomains')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Incidents Management Card */}
|
||||
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||
<h3 className="font-semibold text-lg mb-2">{t('incidentsManagement')}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t('incidentsManagementDesc')}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">{t('quickTips')}:</p>
|
||||
<ul className="list-disc pl-5 text-sm space-y-1">
|
||||
<li>{t('createTrackIncidents')}</li>
|
||||
<li>{t('assignIncidents')}</li>
|
||||
<li>{t('monitorResolution')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Operational Status Card */}
|
||||
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||
<h3 className="font-semibold text-lg mb-2">{t('operationalStatus')}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t('operationalStatusDesc')}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">{t('quickTips')}:</p>
|
||||
<ul className="list-disc pl-5 text-sm space-y-1">
|
||||
<li>{t('createCustomStatus')}</li>
|
||||
<li>{t('displayRealTime')}</li>
|
||||
<li>{t('shareIncidentUpdates')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reports & Analytics Card */}
|
||||
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||
<h3 className="font-semibold text-lg mb-2">{t('reportsAnalytics')}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t('reportsAnalyticsDesc')}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">{t('quickTips')}:</p>
|
||||
<ul className="list-disc pl-5 text-sm space-y-1">
|
||||
<li>{t('generateUptimeReports')}</li>
|
||||
<li>{t('analyzePerformance')}</li>
|
||||
<li>{t('exportMonitoringData')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-6">
|
||||
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
||||
{t('close')}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickActionsDialog;
|
||||
@@ -86,7 +86,7 @@ sudo -E bash ./server-docker-agent.sh`;
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
|
||||
<Container className="h-5 w-5" />
|
||||
Docker One-Click Install (Recommended)
|
||||
Docker One-Click Install
|
||||
</CardTitle>
|
||||
<CardDescription className="text-blue-600 dark:text-blue-300">
|
||||
Automated Docker container installation with system monitoring capabilities
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -79,7 +78,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
// Initialize form data when server changes
|
||||
useEffect(() => {
|
||||
if (server) {
|
||||
// console.log("Setting form data for server:", server);
|
||||
// Parse comma-separated notification_id into array
|
||||
const notificationChannels = server.notification_id
|
||||
? server.notification_id.split(',').map(id => id.trim()).filter(id => id)
|
||||
@@ -110,10 +108,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
// Load existing threshold data when thresholds are loaded and we have a server with threshold_id
|
||||
useEffect(() => {
|
||||
if (server && server.threshold_id && thresholds.length > 0) {
|
||||
// console.log("Loading existing threshold data for server:", server.threshold_id);
|
||||
const existingThreshold = thresholds.find(t => t.id === server.threshold_id);
|
||||
if (existingThreshold) {
|
||||
// console.log("Found existing threshold:", existingThreshold);
|
||||
setSelectedThreshold(existingThreshold);
|
||||
// Handle the API response format with proper field names and type conversion
|
||||
setThresholdFormData({
|
||||
@@ -167,7 +163,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
const configs = await alertConfigService.getAlertConfigurations();
|
||||
setAlertConfigs(configs);
|
||||
} catch (error) {
|
||||
// console.error('Error loading alert configurations:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
@@ -185,7 +180,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
// Cast to ServerNotificationTemplate[] since we know we're getting server templates
|
||||
setTemplates(templateList as ServerNotificationTemplate[]);
|
||||
} catch (error) {
|
||||
// console.error('Error loading templates:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
@@ -202,7 +196,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
const thresholdList = await serverThresholdService.getServerThresholds();
|
||||
setThresholds(thresholdList);
|
||||
} catch (error) {
|
||||
// console.error('Error loading server thresholds:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
@@ -213,47 +206,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleThresholdUpdate = async () => {
|
||||
if (!selectedThreshold) return;
|
||||
|
||||
try {
|
||||
// Use the correct field name for RAM threshold
|
||||
const updateData = {
|
||||
cpu_threshold: thresholdFormData.cpu_threshold,
|
||||
ram_threshold_message: thresholdFormData.ram_threshold, // Use the correct field name
|
||||
disk_threshold: thresholdFormData.disk_threshold,
|
||||
network_threshold: thresholdFormData.network_threshold,
|
||||
};
|
||||
|
||||
await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateData);
|
||||
|
||||
// Update local state
|
||||
setSelectedThreshold({
|
||||
...selectedThreshold,
|
||||
...thresholdFormData,
|
||||
});
|
||||
|
||||
// Update thresholds list
|
||||
setThresholds(prev => prev.map(t =>
|
||||
t.id === selectedThreshold.id
|
||||
? { ...t, ...thresholdFormData }
|
||||
: t
|
||||
));
|
||||
|
||||
toast({
|
||||
title: "Threshold updated",
|
||||
description: "Server threshold values have been updated successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
// console.error('Error updating threshold:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to update threshold values.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotificationChannelToggle = (channelId: string, checked: boolean) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
@@ -294,6 +246,40 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
|
||||
await pb.collection('servers').update(server.id, updateData);
|
||||
|
||||
// Update threshold if a threshold is selected and values have been modified
|
||||
if (selectedThreshold && formData.threshold_id !== "none") {
|
||||
try {
|
||||
const updateThresholdData = {
|
||||
cpu_threshold: thresholdFormData.cpu_threshold,
|
||||
ram_threshold_message: thresholdFormData.ram_threshold, // Use the correct field name
|
||||
disk_threshold: thresholdFormData.disk_threshold,
|
||||
network_threshold: thresholdFormData.network_threshold,
|
||||
};
|
||||
|
||||
await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateThresholdData);
|
||||
|
||||
// Update local state
|
||||
setSelectedThreshold({
|
||||
...selectedThreshold,
|
||||
...thresholdFormData,
|
||||
});
|
||||
|
||||
// Update thresholds list
|
||||
setThresholds(prev => prev.map(t =>
|
||||
t.id === selectedThreshold.id
|
||||
? { ...t, ...thresholdFormData }
|
||||
: t
|
||||
));
|
||||
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Warning",
|
||||
description: "Server updated but failed to update threshold values.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Server updated",
|
||||
description: `${formData.name} has been updated successfully.`,
|
||||
@@ -303,7 +289,6 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
onOpenChange(false);
|
||||
|
||||
} catch (error) {
|
||||
// console.error('Error updating server:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
@@ -518,16 +503,8 @@ export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
{/* Editable Threshold Details */}
|
||||
{selectedThreshold && (
|
||||
<Card className="bg-muted/50">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Threshold Details: {selectedThreshold.name}</CardTitle>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleThresholdUpdate}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
Update Thresholds
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
|
||||
@@ -55,7 +55,7 @@ sudo -E bash ./server-agent.sh`;
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-green-700 dark:text-green-400">
|
||||
<Download className="h-5 w-5" />
|
||||
One-Click Install
|
||||
One-Click Install (Recommended)
|
||||
</CardTitle>
|
||||
<CardDescription className="text-green-600 dark:text-green-300">
|
||||
Copy and paste this single command to install the monitoring agent instantly
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { FormControl, FormField, FormItem, FormLabel, FormDescription } from "@/components/ui/form";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
@@ -8,6 +7,7 @@ import { UseFormReturn } from "react-hook-form";
|
||||
import { ServiceFormData } from "./types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
||||
import { serviceNotificationTemplateService, ServiceNotificationTemplate } from "@/services/serviceNotificationTemplateService";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface ServiceNotificationFieldsProps {
|
||||
@@ -21,18 +21,18 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
||||
const notificationStatus = form.watch("notificationStatus");
|
||||
const notificationChannels = form.watch("notificationChannels") || [];
|
||||
const alertTemplate = form.watch("alertTemplate");
|
||||
|
||||
// console.log("Current notification values:", {
|
||||
// notificationStatus,
|
||||
// notificationChannels,
|
||||
// alertTemplate
|
||||
// });
|
||||
|
||||
|
||||
// Fetch alert configurations for notification channels
|
||||
const { data: alertConfigsData } = useQuery({
|
||||
queryKey: ['alertConfigs'],
|
||||
queryFn: () => alertConfigService.getAlertConfigurations(),
|
||||
});
|
||||
|
||||
// Fetch service notification templates
|
||||
const { data: serviceTemplates, isLoading: isLoadingTemplates } = useQuery({
|
||||
queryKey: ['serviceNotificationTemplates'],
|
||||
queryFn: () => serviceNotificationTemplateService.getTemplates(),
|
||||
});
|
||||
|
||||
// Update alert configs when data is loaded
|
||||
useEffect(() => {
|
||||
@@ -42,13 +42,19 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
||||
setAlertConfigs(enabledChannels);
|
||||
|
||||
// Debug log to check what alert configs are loaded
|
||||
// console.log("Loaded alert configurations:", enabledChannels);
|
||||
}
|
||||
}, [alertConfigsData]);
|
||||
|
||||
// Debug log for service templates
|
||||
useEffect(() => {
|
||||
if (serviceTemplates) {
|
||||
// console.log("Loaded service notification templates:", serviceTemplates);
|
||||
}
|
||||
}, [serviceTemplates]);
|
||||
|
||||
// Log when form values change to debug
|
||||
useEffect(() => {
|
||||
// console.log("Notification values changed:", {
|
||||
// console.log("Notification values changed:", {
|
||||
// notificationStatus: form.getValues("notificationStatus"),
|
||||
// notificationChannels: form.getValues("notificationChannels")
|
||||
// });
|
||||
@@ -159,9 +165,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
||||
control={form.control}
|
||||
name="alertTemplate"
|
||||
render={({ field }) => {
|
||||
// Don't convert existing values to "default"
|
||||
const displayValue = field.value || "default";
|
||||
// console.log("Rendering alert template field with value:", displayValue);
|
||||
// console.log("Rendering alert template field with value:", field.value);
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
@@ -170,17 +174,20 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
console.log("Alert template changed to:", value);
|
||||
field.onChange(value === "default" ? "" : value);
|
||||
field.onChange(value);
|
||||
}}
|
||||
value={displayValue}
|
||||
disabled={notificationStatus !== "enabled"}
|
||||
value={field.value || ""}
|
||||
disabled={notificationStatus !== "enabled" || isLoadingTemplates}
|
||||
>
|
||||
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
|
||||
<SelectValue placeholder="Select an alert template" />
|
||||
<SelectValue placeholder={isLoadingTemplates ? "Loading templates..." : "Select an alert template"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">Default</SelectItem>
|
||||
{/* Add templates here when available */}
|
||||
{serviceTemplates?.map((template) => (
|
||||
<SelectItem key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
@@ -195,4 +202,4 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -38,7 +37,7 @@ interface NotificationChannelDialogProps {
|
||||
|
||||
const baseSchema = z.object({
|
||||
notify_name: z.string().min(1, "Name is required"),
|
||||
notification_type: z.enum(["telegram", "discord", "slack", "signal", "email"]),
|
||||
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "webhook"]),
|
||||
enabled: z.boolean().default(true),
|
||||
service_id: z.string().default("global"),
|
||||
template_id: z.string().optional(),
|
||||
@@ -65,6 +64,11 @@ const signalSchema = baseSchema.extend({
|
||||
signal_number: z.string().min(1, "Signal number is required"),
|
||||
});
|
||||
|
||||
const googleChatSchema = baseSchema.extend({
|
||||
notification_type: z.literal("google_chat"),
|
||||
google_chat_webhook_url: z.string().url("Must be a valid URL"),
|
||||
});
|
||||
|
||||
const emailSchema = baseSchema.extend({
|
||||
notification_type: z.literal("email"),
|
||||
email_address: z.string().email("Valid email is required"),
|
||||
@@ -90,6 +94,7 @@ const formSchema = z.discriminatedUnion("notification_type", [
|
||||
discordSchema,
|
||||
slackSchema,
|
||||
signalSchema,
|
||||
googleChatSchema,
|
||||
emailSchema,
|
||||
webhookSchema
|
||||
]);
|
||||
@@ -97,12 +102,48 @@ const formSchema = z.discriminatedUnion("notification_type", [
|
||||
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" },
|
||||
{
|
||||
value: "telegram",
|
||||
label: "Telegram",
|
||||
description: "Send notifications via Telegram bot",
|
||||
icon: "/upload/notification/telegram.png"
|
||||
},
|
||||
{
|
||||
value: "discord",
|
||||
label: "Discord",
|
||||
description: "Send notifications to Discord webhook",
|
||||
icon: "/upload/notification/discord.png"
|
||||
},
|
||||
{
|
||||
value: "slack",
|
||||
label: "Slack",
|
||||
description: "Send notifications to Slack webhook",
|
||||
icon: "/upload/notification/slack.png"
|
||||
},
|
||||
{
|
||||
value: "signal",
|
||||
label: "Signal",
|
||||
description: "Send notifications via Signal",
|
||||
icon: "/upload/notification/signal.png"
|
||||
},
|
||||
{
|
||||
value: "google_chat",
|
||||
label: "Google Chat",
|
||||
description: "Send notifications to Google Chat webhook",
|
||||
icon: "/upload/notification/google.png"
|
||||
},
|
||||
{
|
||||
value: "email",
|
||||
label: "Email",
|
||||
description: "Send notifications via email",
|
||||
icon: "/upload/notification/email.png"
|
||||
},
|
||||
{
|
||||
value: "webhook",
|
||||
label: "Webhook",
|
||||
description: "Send notifications to custom webhook",
|
||||
icon: "/upload/notification/webhook.png"
|
||||
},
|
||||
];
|
||||
|
||||
const webhookPayloadTemplates = {
|
||||
@@ -236,7 +277,7 @@ export const NotificationChannelDialog = ({
|
||||
await webhookService.createWebhook(webhookData);
|
||||
}
|
||||
} else {
|
||||
// Handle other notification types with existing service
|
||||
// Handle all other notification types including Google Chat and Email
|
||||
const configData = {
|
||||
...values,
|
||||
service_id: values.service_id || "global",
|
||||
@@ -248,7 +289,14 @@ export const NotificationChannelDialog = ({
|
||||
await alertConfigService.createAlertConfiguration(configData as any);
|
||||
}
|
||||
}
|
||||
|
||||
onClose(true); // Close with refresh
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to save notification channel",
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -297,9 +345,16 @@ export const NotificationChannelDialog = ({
|
||||
<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 className="flex items-center space-x-3">
|
||||
<img
|
||||
src={option.icon}
|
||||
alt={`${option.label} icon`}
|
||||
className="w-5 h-5 object-contain"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{option.label}</span>
|
||||
<span className="text-xs text-muted-foreground">{option.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -310,7 +365,6 @@ export const NotificationChannelDialog = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Show different fields based on notification type */}
|
||||
{notificationType === "telegram" && (
|
||||
<>
|
||||
<FormField
|
||||
@@ -405,6 +459,25 @@ export const NotificationChannelDialog = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{notificationType === "google_chat" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="google_chat_webhook_url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Google Chat Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://chat.googleapis.com/v1/spaces/..." {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Google Chat webhook URL from your Google Chat space
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{notificationType === "email" && (
|
||||
<>
|
||||
<FormField
|
||||
@@ -597,7 +670,6 @@ export const NotificationChannelDialog = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Payload Template Section */}
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -13,9 +13,17 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { alertConfigService } from "@/services/alertConfigService";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
|
||||
interface CombinedChannel extends Partial<AlertConfiguration> {
|
||||
isWebhook?: boolean;
|
||||
url?: string;
|
||||
method?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface NotificationChannelListProps {
|
||||
channels: AlertConfiguration[];
|
||||
channels: CombinedChannel[];
|
||||
onEdit: (config: AlertConfiguration) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
@@ -25,25 +33,63 @@ export const NotificationChannelList = ({
|
||||
onEdit,
|
||||
onDelete
|
||||
}: NotificationChannelListProps) => {
|
||||
const toggleEnabled = async (config: AlertConfiguration) => {
|
||||
const toggleEnabled = async (config: CombinedChannel) => {
|
||||
if (!config.id) return;
|
||||
|
||||
await alertConfigService.updateAlertConfiguration(config.id, {
|
||||
enabled: !config.enabled
|
||||
});
|
||||
|
||||
// The parent component will refresh the list
|
||||
onEdit(config);
|
||||
if (config.isWebhook) {
|
||||
// Handle webhook toggle
|
||||
try {
|
||||
const newEnabled = config.enabled ? "off" : "on";
|
||||
await pb.collection('webhook').update(config.id, {
|
||||
enabled: newEnabled
|
||||
});
|
||||
// Trigger refresh by calling onEdit with empty config
|
||||
onEdit({} as AlertConfiguration);
|
||||
} catch (error) {
|
||||
console.error("Error updating webhook:", error);
|
||||
}
|
||||
} else {
|
||||
// Handle alert config toggle
|
||||
await alertConfigService.updateAlertConfiguration(config.id, {
|
||||
enabled: !config.enabled
|
||||
});
|
||||
|
||||
// The parent component will refresh the list
|
||||
onEdit(config as AlertConfiguration);
|
||||
}
|
||||
};
|
||||
|
||||
const getChannelTypeLabel = (type: string) => {
|
||||
const getChannelTypeLabel = (type: string | undefined) => {
|
||||
switch(type) {
|
||||
case "telegram": return "Telegram";
|
||||
case "discord": return "Discord";
|
||||
case "slack": return "Slack";
|
||||
case "signal": return "Signal";
|
||||
case "google_chat": return "Google Chat";
|
||||
case "email": return "Email";
|
||||
default: return type;
|
||||
case "webhook": return "Webhook";
|
||||
default: return type || "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const getChannelDetails = (config: CombinedChannel) => {
|
||||
if (config.isWebhook) {
|
||||
return `${config.method || 'POST'} ${config.url || ''}`;
|
||||
}
|
||||
|
||||
switch(config.notification_type) {
|
||||
case "telegram":
|
||||
return config.telegram_chat_id || '';
|
||||
case "discord":
|
||||
case "slack":
|
||||
case "google_chat":
|
||||
return config.discord_webhook_url || config.slack_webhook_url || config.google_chat_webhook_url || '';
|
||||
case "signal":
|
||||
return config.signal_number || '';
|
||||
case "email":
|
||||
return config.email_address || '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -66,6 +112,7 @@ export const NotificationChannelList = ({
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Details</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
@@ -78,11 +125,14 @@ export const NotificationChannelList = ({
|
||||
<TableCell>
|
||||
<Badge variant="outline">{getChannelTypeLabel(channel.notification_type)}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-xs truncate text-sm text-muted-foreground">
|
||||
{getChannelDetails(channel)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={
|
||||
typeof channel.enabled === 'string'
|
||||
? channel.enabled === "true"
|
||||
? channel.enabled === "true" || channel.enabled === "on"
|
||||
: !!channel.enabled
|
||||
}
|
||||
onCheckedChange={() => toggleEnabled(channel)}
|
||||
@@ -96,7 +146,8 @@ export const NotificationChannelList = ({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onEdit(channel)}
|
||||
onClick={() => onEdit(channel as AlertConfiguration)}
|
||||
disabled={channel.isWebhook} // Disable edit for webhooks for now
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -119,4 +170,4 @@ export const NotificationChannelList = ({
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -5,28 +5,47 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Loader2 } from "lucide-react";
|
||||
import { AlertConfiguration, alertConfigService } from "@/services/alertConfigService";
|
||||
import { WebhookConfiguration, webhookService } from "@/services/webhookService";
|
||||
import { NotificationChannelDialog } from "./NotificationChannelDialog";
|
||||
import { NotificationChannelList } from "./NotificationChannelList";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
|
||||
interface CombinedChannel extends Partial<AlertConfiguration> {
|
||||
isWebhook?: boolean;
|
||||
url?: string;
|
||||
method?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const NotificationSettings = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
||||
const [webhookConfigs, setWebhookConfigs] = useState<WebhookConfiguration[]>([]);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [currentTab, setCurrentTab] = useState<string>("all");
|
||||
const [editingConfig, setEditingConfig] = useState<AlertConfiguration | null>(null);
|
||||
|
||||
const fetchAlertConfigurations = async () => {
|
||||
const fetchNotificationChannels = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Fetch alert configurations
|
||||
const configs = await alertConfigService.getAlertConfigurations();
|
||||
setAlertConfigs(configs);
|
||||
|
||||
// Fetch webhooks
|
||||
try {
|
||||
const webhookResponse = await pb.collection('webhook').getList(1, 50);
|
||||
setWebhookConfigs(webhookResponse.items as WebhookConfiguration[]);
|
||||
} catch (webhookError) {
|
||||
setWebhookConfigs([]);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAlertConfigurations();
|
||||
fetchNotificationChannels();
|
||||
}, []);
|
||||
|
||||
const handleAddNew = () => {
|
||||
@@ -40,22 +59,66 @@ const NotificationSettings = () => {
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
const success = await alertConfigService.deleteAlertConfiguration(id);
|
||||
if (success) {
|
||||
fetchAlertConfigurations();
|
||||
// Check if it's a webhook first
|
||||
const isWebhook = webhookConfigs.find(w => w.id === id);
|
||||
|
||||
if (isWebhook) {
|
||||
// Handle webhook deletion
|
||||
if (confirm("Are you sure you want to delete this webhook?")) {
|
||||
try {
|
||||
await pb.collection('webhook').delete(id);
|
||||
fetchNotificationChannels();
|
||||
} catch (error) {
|
||||
console.error("Error deleting webhook:", error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle alert config deletion
|
||||
const success = await alertConfigService.deleteAlertConfiguration(id);
|
||||
if (success) {
|
||||
fetchNotificationChannels();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = (refreshList: boolean) => {
|
||||
setDialogOpen(false);
|
||||
if (refreshList) {
|
||||
fetchAlertConfigurations();
|
||||
fetchNotificationChannels();
|
||||
}
|
||||
};
|
||||
|
||||
const getCombinedChannels = (): CombinedChannel[] => {
|
||||
const combined: CombinedChannel[] = [];
|
||||
|
||||
// Add alert configurations
|
||||
alertConfigs.forEach(config => {
|
||||
combined.push(config);
|
||||
});
|
||||
|
||||
// Add webhooks as notification channels
|
||||
webhookConfigs.forEach(webhook => {
|
||||
combined.push({
|
||||
id: webhook.id,
|
||||
notify_name: webhook.name,
|
||||
notification_type: "webhook" as const,
|
||||
enabled: webhook.enabled === "on",
|
||||
created: webhook.created,
|
||||
updated: webhook.updated,
|
||||
isWebhook: true,
|
||||
url: webhook.url,
|
||||
method: webhook.method,
|
||||
description: webhook.description
|
||||
});
|
||||
});
|
||||
|
||||
return combined;
|
||||
};
|
||||
|
||||
const getFilteredConfigs = () => {
|
||||
if (currentTab === "all") return alertConfigs;
|
||||
return alertConfigs.filter(config => config.notification_type === currentTab);
|
||||
const combined = getCombinedChannels();
|
||||
if (currentTab === "all") return combined;
|
||||
return combined.filter(config => config.notification_type === currentTab);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -86,6 +149,7 @@ const NotificationSettings = () => {
|
||||
<TabsTrigger value="discord">Discord</TabsTrigger>
|
||||
<TabsTrigger value="slack">Slack</TabsTrigger>
|
||||
<TabsTrigger value="signal">Signal</TabsTrigger>
|
||||
<TabsTrigger value="google_chat">Google Chat</TabsTrigger>
|
||||
<TabsTrigger value="email">Email</TabsTrigger>
|
||||
<TabsTrigger value="webhook">Webhook</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import { toast } from "sonner";
|
||||
import { Bell } from "lucide-react";
|
||||
import { Bell, X } from "lucide-react";
|
||||
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { DialogFooter } from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { AddSSLCertificateDto } from "@/types/ssl.types";
|
||||
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
||||
import { sslNotificationTemplateService, SslNotificationTemplate } from "@/services/sslNotificationTemplateService";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
const formSchema = z.object({
|
||||
domain: z.string().min(1, "Domain is required"),
|
||||
warning_threshold: z.coerce.number().int().min(1).max(365),
|
||||
expiry_threshold: z.coerce.number().int().min(1).max(30),
|
||||
notification_channel: z.string().optional(), // Make it optional to allow empty string for "None"
|
||||
notification_channels: z.array(z.string()).optional(),
|
||||
alert_template: z.string().optional(),
|
||||
check_interval: z.coerce.number().int().min(1).max(30).optional()
|
||||
});
|
||||
|
||||
@@ -35,6 +39,7 @@ export const AddSSLCertificateForm = ({
|
||||
}: AddSSLCertificateFormProps) => {
|
||||
const { t } = useLanguage();
|
||||
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
||||
const [sslTemplates, setSslTemplates] = useState<SslNotificationTemplate[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
@@ -43,58 +48,78 @@ export const AddSSLCertificateForm = ({
|
||||
domain: "",
|
||||
warning_threshold: 30,
|
||||
expiry_threshold: 7,
|
||||
notification_channel: "none",
|
||||
notification_channels: [],
|
||||
alert_template: "none",
|
||||
check_interval: 1
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch notification channels when form loads
|
||||
// Fetch notification channels and SSL templates when form loads
|
||||
useEffect(() => {
|
||||
const fetchNotificationChannels = async () => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Fetch notification channels
|
||||
const configs = await alertConfigService.getAlertConfigurations();
|
||||
// console.log("Fetched notification channels:", configs);
|
||||
// Only include enabled channels
|
||||
const enabledConfigs = configs.filter(config => {
|
||||
// Handle the possibility of enabled being a string
|
||||
if (typeof config.enabled === 'string') {
|
||||
return config.enabled === "true";
|
||||
}
|
||||
// Otherwise treat as boolean
|
||||
return config.enabled === true;
|
||||
});
|
||||
setAlertConfigs(enabledConfigs);
|
||||
|
||||
// Fetch SSL notification templates
|
||||
const templates = await sslNotificationTemplateService.getTemplates();
|
||||
setSslTemplates(templates);
|
||||
} catch (error) {
|
||||
// console.error("Error fetching notification channels:", error);
|
||||
toast.error(t('failedToLoadCertificates'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchNotificationChannels();
|
||||
}, [form, t]);
|
||||
fetchData();
|
||||
}, [t]);
|
||||
|
||||
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
// Convert the form values to the required DTO format with required properties
|
||||
// Convert the form values to the required DTO format
|
||||
const certData: AddSSLCertificateDto = {
|
||||
domain: values.domain,
|
||||
warning_threshold: values.warning_threshold,
|
||||
expiry_threshold: values.expiry_threshold,
|
||||
notification_channel: values.notification_channel === "none" ? "" : (values.notification_channel || ""), // Convert "none" to empty string
|
||||
notification_channel: values.notification_channels && values.notification_channels.length > 0
|
||||
? values.notification_channels.join(',')
|
||||
: '',
|
||||
notification_id: values.notification_channels && values.notification_channels.length > 0
|
||||
? values.notification_channels.join(',')
|
||||
: '',
|
||||
template_id: values.alert_template && values.alert_template !== 'none' ? values.alert_template : '',
|
||||
check_interval: values.check_interval
|
||||
};
|
||||
|
||||
await onSubmit(certData);
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
// console.error("Error adding SSL certificate:", error);
|
||||
toast.error(t('failedToAddCertificate'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddNotificationChannel = (channelId: string) => {
|
||||
const currentChannels = form.getValues("notification_channels") || [];
|
||||
if (!currentChannels.includes(channelId)) {
|
||||
form.setValue("notification_channels", [...currentChannels, channelId]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveNotificationChannel = (channelId: string) => {
|
||||
const currentChannels = form.getValues("notification_channels") || [];
|
||||
form.setValue("notification_channels", currentChannels.filter(id => id !== channelId));
|
||||
};
|
||||
|
||||
const selectedChannels = form.watch("notification_channels") || [];
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
||||
@@ -167,31 +192,60 @@ export const AddSSLCertificateForm = ({
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notification_channel"
|
||||
name="notification_channels"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('notificationChannel')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value || "none"}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('chooseChannel')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">{t('none')}</SelectItem>
|
||||
{alertConfigs.length > 0 ? (
|
||||
alertConfigs.map((config) => (
|
||||
<SelectItem key={config.id} value={config.id || ""}>
|
||||
{config.notify_name} ({config.notification_type})
|
||||
</SelectItem>
|
||||
))
|
||||
) : isLoading ? (
|
||||
<SelectItem value="loading" disabled>{t('loadingChannels')}</SelectItem>
|
||||
) : (
|
||||
<SelectItem value="none-disabled" disabled>{t('noChannelsFound')}</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormLabel>{t('notificationChannel')} (Multi-select)</FormLabel>
|
||||
<div className="space-y-3">
|
||||
<Select
|
||||
onValueChange={handleAddNotificationChannel}
|
||||
value=""
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select channels to add..." />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{alertConfigs.length > 0 ? (
|
||||
alertConfigs
|
||||
.filter(config => config.id && !selectedChannels.includes(config.id))
|
||||
.map((config) => (
|
||||
<SelectItem key={config.id} value={config.id || "unknown"}>
|
||||
{config.notify_name} ({config.notification_type})
|
||||
</SelectItem>
|
||||
))
|
||||
) : isLoading ? (
|
||||
<SelectItem value="loading-placeholder" disabled>{t('loadingChannels')}</SelectItem>
|
||||
) : (
|
||||
<SelectItem value="no-channels-placeholder" disabled>{t('noChannelsFound')}</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Display selected channels */}
|
||||
{selectedChannels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedChannels.map((channelId) => {
|
||||
const channel = alertConfigs.find(config => config.id === channelId);
|
||||
return (
|
||||
<Badge key={channelId} variant="secondary" className="flex items-center gap-1">
|
||||
{channel ? `${channel.notify_name} (${channel.notification_type})` : channelId}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto p-0 ml-1"
|
||||
onClick={() => handleRemoveNotificationChannel(channelId)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FormDescription className="flex items-center gap-1">
|
||||
<Bell className="h-4 w-4" />
|
||||
{t('whereToSend')}
|
||||
@@ -200,6 +254,41 @@ export const AddSSLCertificateForm = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="alert_template"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Alert Template</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value || "none"}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose an alert template (optional)" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
{sslTemplates.length > 0 ? (
|
||||
sslTemplates.map((template) => (
|
||||
<SelectItem key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : isLoading ? (
|
||||
<SelectItem value="loading-templates" disabled>Loading templates...</SelectItem>
|
||||
) : (
|
||||
<SelectItem value="no-templates-found" disabled>No templates found</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Template for SSL certificate alert messages
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onCancel}>{t('cancel')}</Button>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
@@ -7,17 +6,20 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { SSLCertificate } from "@/types/ssl.types";
|
||||
import { Loader2, Bell } from "lucide-react";
|
||||
import { Loader2, Bell, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
||||
import { sslNotificationTemplateService, SslNotificationTemplate } from "@/services/sslNotificationTemplateService";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
const formSchema = z.object({
|
||||
domain: z.string().min(1, "Domain is required"),
|
||||
warning_threshold: z.coerce.number().min(1, "Warning threshold must be at least 1 day"),
|
||||
expiry_threshold: z.coerce.number().min(1, "Expiry threshold must be at least 1 day"),
|
||||
notification_channel: z.string().min(1, "Notification channel is required"),
|
||||
notification_channels: z.array(z.string()).default([]),
|
||||
alert_template: z.string().optional(),
|
||||
check_interval: z.coerce.number().int().min(1).max(30).optional(),
|
||||
});
|
||||
|
||||
@@ -33,6 +35,7 @@ interface EditSSLCertificateFormProps {
|
||||
export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPending }: EditSSLCertificateFormProps) => {
|
||||
const { t } = useLanguage();
|
||||
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
||||
const [sslTemplates, setSslTemplates] = useState<SslNotificationTemplate[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
@@ -41,58 +44,81 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
||||
domain: certificate.domain,
|
||||
warning_threshold: certificate.warning_threshold,
|
||||
expiry_threshold: certificate.expiry_threshold,
|
||||
notification_channel: certificate.notification_channel,
|
||||
notification_channels: certificate.notification_channel ? certificate.notification_channel.split(',') : [],
|
||||
alert_template: certificate.alert_template || "none",
|
||||
check_interval: certificate.check_interval || 1,
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch notification channels when form loads
|
||||
const notificationChannels = form.watch("notification_channels") || [];
|
||||
|
||||
// Fetch notification channels and SSL templates when form loads
|
||||
useEffect(() => {
|
||||
const fetchNotificationChannels = async () => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Fetch notification channels
|
||||
const configs = await alertConfigService.getAlertConfigurations();
|
||||
// console.log("Fetched notification channels:", configs);
|
||||
// Only include enabled channels
|
||||
const enabledConfigs = configs.filter(config => {
|
||||
// Handle the possibility of enabled being a string
|
||||
if (typeof config.enabled === 'string') {
|
||||
return config.enabled === "true";
|
||||
}
|
||||
// Otherwise treat as boolean
|
||||
return config.enabled === true;
|
||||
});
|
||||
setAlertConfigs(enabledConfigs);
|
||||
|
||||
// Fetch SSL notification templates
|
||||
const templates = await sslNotificationTemplateService.getTemplates();
|
||||
setSslTemplates(templates);
|
||||
} catch (error) {
|
||||
// console.error("Error fetching notification channels:", error);
|
||||
toast.error(t('failedToLoadCertificates'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchNotificationChannels();
|
||||
fetchData();
|
||||
}, [t]);
|
||||
|
||||
const handleChannelAdd = (channelId: string) => {
|
||||
const currentChannels = form.getValues("notification_channels") || [];
|
||||
if (!currentChannels.includes(channelId)) {
|
||||
form.setValue("notification_channels", [...currentChannels, channelId]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChannelRemove = (channelId: string) => {
|
||||
const currentChannels = form.getValues("notification_channels") || [];
|
||||
form.setValue("notification_channels", currentChannels.filter(id => id !== channelId));
|
||||
};
|
||||
|
||||
const getSelectedChannelNames = () => {
|
||||
return (notificationChannels || []).map(channelId => {
|
||||
const config = alertConfigs.find(c => c.id === channelId);
|
||||
return config ? `${config.notify_name} (${config.notification_type})` : channelId;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (data: FormValues) => {
|
||||
// Merge the updated values with the original certificate
|
||||
const updatedCertificate: SSLCertificate = {
|
||||
...certificate,
|
||||
...data,
|
||||
// Save notification channels as comma-separated string for notification_channel field
|
||||
notification_channel: data.notification_channels.length > 0 ? data.notification_channels.join(',') : '',
|
||||
// Save notification channels as comma-separated string for notification_id field (for PocketBase)
|
||||
notification_id: data.notification_channels.length > 0 ? data.notification_channels.join(',') : '',
|
||||
// Save alert template as template_id field (for PocketBase) - handle "none" value
|
||||
template_id: data.alert_template && data.alert_template !== 'none' ? data.alert_template : '',
|
||||
// Ensure values are correctly typed as numbers
|
||||
warning_threshold: Number(data.warning_threshold),
|
||||
expiry_threshold: Number(data.expiry_threshold),
|
||||
check_interval: data.check_interval ? Number(data.check_interval) : undefined
|
||||
};
|
||||
|
||||
console.log("Submitting updated certificate:", updatedCertificate);
|
||||
onSubmit(updatedCertificate);
|
||||
};
|
||||
|
||||
// For debugging
|
||||
console.log("Certificate data:", certificate);
|
||||
console.log("Form default values:", form.getValues());
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||
@@ -106,7 +132,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="example.com"
|
||||
disabled={true} // Domain shouldn't be editable
|
||||
disabled={true}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
@@ -188,41 +214,52 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notification_channel"
|
||||
name="notification_channels"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('notificationChannel')}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<FormControl>
|
||||
<FormDescription>
|
||||
Select multiple notification channels for this SSL certificate
|
||||
</FormDescription>
|
||||
|
||||
{/* Display selected channels as badges */}
|
||||
{notificationChannels && notificationChannels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{getSelectedChannelNames().map((channelName, index) => (
|
||||
<Badge key={notificationChannels[index]} variant="secondary" className="flex items-center gap-1">
|
||||
{channelName}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => handleChannelRemove(notificationChannels[index])}
|
||||
/>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={handleChannelAdd}
|
||||
disabled={isLoading}
|
||||
value=""
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('chooseChannel')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{alertConfigs.length > 0 ? (
|
||||
alertConfigs.map((config) => (
|
||||
<SelectItem key={config.id} value={config.id || ""}>
|
||||
{config.notify_name} ({config.notification_type})
|
||||
</SelectItem>
|
||||
))
|
||||
) : isLoading ? (
|
||||
<SelectItem value="loading" disabled>{t('loadingChannels')}</SelectItem>
|
||||
) : (
|
||||
<>
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
<SelectItem value="telegram">Telegram</SelectItem>
|
||||
<SelectItem value="slack">Slack</SelectItem>
|
||||
<SelectItem value="webhook">Webhook</SelectItem>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<SelectContent>
|
||||
{alertConfigs
|
||||
.filter(config => config.id && !notificationChannels?.includes(config.id))
|
||||
.map((config) => (
|
||||
<SelectItem key={config.id} value={config.id || "unknown"}>
|
||||
{config.notify_name} ({config.notification_type})
|
||||
</SelectItem>
|
||||
))}
|
||||
{alertConfigs.filter(config => config.id && !notificationChannels?.includes(config.id)).length === 0 && (
|
||||
<SelectItem value="no-available-channels" disabled>No available channels</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription className="flex items-center gap-1">
|
||||
<Bell className="h-4 w-4" />
|
||||
{t('whereToSend')}
|
||||
@@ -232,6 +269,45 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="alert_template"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Alert Template</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value || "none"}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={isLoading ? "Loading templates..." : "Select an alert template"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
{sslTemplates?.map((template) => (
|
||||
<SelectItem key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{sslTemplates?.length === 0 && !isLoading && (
|
||||
<SelectItem value="no-templates-available" disabled>No templates found</SelectItem>
|
||||
)}
|
||||
{isLoading && (
|
||||
<SelectItem value="loading-templates-placeholder" disabled>Loading templates...</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Choose a template for SSL certificate alert messages
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -30,12 +29,9 @@ export const SSLDomainContent = () => {
|
||||
queryKey: ['ssl-certificates'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
// console.log("Fetching SSL certificates from SSLDomainContent...");
|
||||
const result = await fetchSSLCertificates();
|
||||
// console.log("Received SSL certificates:", result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
// console.error("Error fetching certificates:", error);
|
||||
toast.error(t('failedToLoadCertificates'));
|
||||
throw error;
|
||||
}
|
||||
@@ -53,30 +49,28 @@ export const SSLDomainContent = () => {
|
||||
toast.success(t('sslCertificateAdded'));
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error adding SSL certificate:", error);
|
||||
toast.error(error instanceof Error ? error.message : t('failedToAddCertificate'));
|
||||
}
|
||||
});
|
||||
|
||||
// Edit certificate mutation - Updated to ensure thresholds are properly updated
|
||||
// Edit certificate mutation - Updated to include notification_id and template_id
|
||||
const editMutation = useMutation({
|
||||
mutationFn: async (certificate: SSLCertificate) => {
|
||||
// console.log("Updating certificate with data:", certificate);
|
||||
|
||||
// Create the update data object
|
||||
// Create the update data object with new fields
|
||||
const updateData = {
|
||||
warning_threshold: Number(certificate.warning_threshold),
|
||||
expiry_threshold: Number(certificate.expiry_threshold),
|
||||
notification_channel: certificate.notification_channel,
|
||||
notification_id: certificate.notification_id || '', // Multi notification channels
|
||||
template_id: certificate.template_id || '', // Alert template ID
|
||||
check_interval: certificate.check_interval,
|
||||
};
|
||||
|
||||
// console.log("Update data to be sent:", updateData);
|
||||
|
||||
// Update certificate in the database using PocketBase directly
|
||||
const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData);
|
||||
|
||||
// console.log("PocketBase update response:", updated);
|
||||
|
||||
// After updating the settings, refresh the certificate to ensure it's up to date
|
||||
// This will also check if notification needs to be sent based on updated thresholds
|
||||
const refreshedCert = await checkAndUpdateCertificate(certificate.id);
|
||||
@@ -90,7 +84,6 @@ export const SSLDomainContent = () => {
|
||||
toast.success(t('sslCertificateUpdated'));
|
||||
},
|
||||
onError: (error) => {
|
||||
// console.error("Error updating SSL certificate:", error);
|
||||
toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate'));
|
||||
}
|
||||
});
|
||||
@@ -103,7 +96,6 @@ export const SSLDomainContent = () => {
|
||||
toast.success(t('sslCertificateDeleted'));
|
||||
},
|
||||
onError: (error) => {
|
||||
// console.error("Error deleting SSL certificate:", error);
|
||||
toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate'));
|
||||
}
|
||||
});
|
||||
@@ -117,7 +109,6 @@ export const SSLDomainContent = () => {
|
||||
// Removed individual success toast notification
|
||||
},
|
||||
onError: (error) => {
|
||||
// console.error("Error refreshing SSL certificate:", error);
|
||||
setRefreshingId(null);
|
||||
|
||||
// Still refresh the data to show any partial information that was saved
|
||||
@@ -142,7 +133,6 @@ export const SSLDomainContent = () => {
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
// console.error("Error refreshing all certificates:", error);
|
||||
toast.error(t('failedToCheckCertificate'));
|
||||
setIsRefreshingAll(false);
|
||||
|
||||
@@ -167,7 +157,6 @@ export const SSLDomainContent = () => {
|
||||
};
|
||||
|
||||
const handleUpdateCertificate = (certificate: SSLCertificate) => {
|
||||
console.log("Handling certificate update with data:", certificate);
|
||||
editMutation.mutate(certificate);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface AlertConfiguration {
|
||||
collectionId?: string;
|
||||
collectionName?: string;
|
||||
service_id: string;
|
||||
notification_type: "telegram" | "discord" | "signal" | "slack" | "email";
|
||||
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "webhook";
|
||||
telegram_chat_id?: string;
|
||||
discord_webhook_url?: string;
|
||||
signal_number?: string;
|
||||
@@ -15,6 +15,7 @@ export interface AlertConfiguration {
|
||||
bot_token?: string;
|
||||
template_id?: string;
|
||||
slack_webhook_url?: string;
|
||||
google_chat_webhook_url?: string;
|
||||
enabled: boolean;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
@@ -29,13 +30,10 @@ export interface AlertConfiguration {
|
||||
|
||||
export const alertConfigService = {
|
||||
async getAlertConfigurations(): Promise<AlertConfiguration[]> {
|
||||
// console.info("Fetching alert configurations");
|
||||
try {
|
||||
const response = await pb.collection('alert_configurations').getList(1, 50);
|
||||
// console.info("Alert configurations response:", response);
|
||||
return response.items as AlertConfiguration[];
|
||||
} catch (error) {
|
||||
// console.error("Error fetching alert configurations:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to load notification settings",
|
||||
@@ -46,17 +44,50 @@ export const alertConfigService = {
|
||||
},
|
||||
|
||||
async createAlertConfiguration(config: Omit<AlertConfiguration, 'id' | 'collectionId' | 'collectionName' | 'created' | 'updated'>): Promise<AlertConfiguration | null> {
|
||||
// console.info("Creating alert configuration:", config);
|
||||
|
||||
try {
|
||||
const result = await pb.collection('alert_configurations').create(config);
|
||||
// console.info("Alert configuration created:", result);
|
||||
// Build the configuration object with proper field mapping
|
||||
const cleanConfig: any = {
|
||||
service_id: config.service_id || "global",
|
||||
notification_type: config.notification_type,
|
||||
notify_name: config.notify_name,
|
||||
enabled: config.enabled,
|
||||
template_id: config.template_id || "",
|
||||
};
|
||||
|
||||
// Add type-specific fields based on notification type
|
||||
if (config.notification_type === "telegram") {
|
||||
cleanConfig.telegram_chat_id = config.telegram_chat_id || "";
|
||||
cleanConfig.bot_token = config.bot_token || "";
|
||||
} else if (config.notification_type === "discord") {
|
||||
cleanConfig.discord_webhook_url = config.discord_webhook_url || "";
|
||||
} else if (config.notification_type === "slack") {
|
||||
cleanConfig.slack_webhook_url = config.slack_webhook_url || "";
|
||||
} else if (config.notification_type === "signal") {
|
||||
cleanConfig.signal_number = config.signal_number || "";
|
||||
} else if (config.notification_type === "google_chat") {
|
||||
cleanConfig.google_chat_webhook_url = config.google_chat_webhook_url || "";
|
||||
} else if (config.notification_type === "email") {
|
||||
|
||||
cleanConfig.email_address = config.email_address || "";
|
||||
cleanConfig.email_sender_name = config.email_sender_name || "";
|
||||
cleanConfig.smtp_server = config.smtp_server || "";
|
||||
cleanConfig.smtp_port = config.smtp_port || "";
|
||||
|
||||
}
|
||||
|
||||
const result = await pb.collection('alert_configurations').create(cleanConfig);
|
||||
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Notification channel created successfully",
|
||||
});
|
||||
return result as AlertConfiguration;
|
||||
} catch (error) {
|
||||
// console.error("Error creating alert configuration:", error);
|
||||
// Try to get more details from the error
|
||||
if (error && typeof error === 'object') {
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to create notification channel",
|
||||
@@ -67,17 +98,14 @@ export const alertConfigService = {
|
||||
},
|
||||
|
||||
async updateAlertConfiguration(id: string, config: Partial<AlertConfiguration>): Promise<AlertConfiguration | null> {
|
||||
// console.info(`Updating alert configuration ${id}:`, config);
|
||||
try {
|
||||
const result = await pb.collection('alert_configurations').update(id, config);
|
||||
// console.info("Alert configuration updated:", result);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Notification channel updated successfully",
|
||||
});
|
||||
return result as AlertConfiguration;
|
||||
} catch (error) {
|
||||
// console.error("Error updating alert configuration:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to update notification channel",
|
||||
@@ -88,17 +116,14 @@ export const alertConfigService = {
|
||||
},
|
||||
|
||||
async deleteAlertConfiguration(id: string): Promise<boolean> {
|
||||
// console.info(`Deleting alert configuration ${id}`);
|
||||
try {
|
||||
await pb.collection('alert_configurations').delete(id);
|
||||
// console.info("Alert configuration deleted");
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Notification channel removed",
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
// console.error("Error deleting alert configuration:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to remove notification channel",
|
||||
|
||||
@@ -32,6 +32,8 @@ export const addSSLCertificate = async (
|
||||
warning_threshold: Number(certificateData.warning_threshold) || 30,
|
||||
expiry_threshold: Number(certificateData.expiry_threshold) || 7,
|
||||
notification_channel: certificateData.notification_channel || "",
|
||||
notification_id: certificateData.notification_id || "", // Multi notification channels
|
||||
template_id: certificateData.template_id || "", // Alert template ID
|
||||
check_interval: Number(certificateData.check_interval) || 1, // New field
|
||||
check_at: currentTime, // Set to current time to trigger immediate check
|
||||
};
|
||||
@@ -41,7 +43,6 @@ export const addSSLCertificate = async (
|
||||
|
||||
return record as unknown as SSLCertificate;
|
||||
} catch (error) {
|
||||
console.error("Error adding SSL certificate:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -70,7 +71,6 @@ export const checkAndUpdateCertificate = async (
|
||||
// Return the current certificate data
|
||||
return typedCertificate;
|
||||
} catch (error) {
|
||||
console.error("Error updating SSL certificate:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -87,10 +87,8 @@ export const triggerImmediateCheck = async (certificateId: string): Promise<void
|
||||
check_at: currentTime
|
||||
});
|
||||
|
||||
console.log(`Triggered immediate check for certificate ${certificateId} at ${currentTime}`);
|
||||
toast.success("SSL check scheduled - certificate will be checked shortly");
|
||||
} catch (error) {
|
||||
console.error("Error triggering immediate SSL check:", error);
|
||||
toast.error("Failed to schedule SSL check");
|
||||
throw error;
|
||||
}
|
||||
@@ -104,7 +102,6 @@ export const deleteSSLCertificate = async (id: string): Promise<boolean> => {
|
||||
await pb.collection("ssl_certificates").delete(id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error deleting SSL certificate:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -118,8 +115,6 @@ export const refreshAllCertificates = async (): Promise<{ success: number; faile
|
||||
const response = await pb.collection("ssl_certificates").getList(1, 100);
|
||||
const certificates = response.items as unknown as SSLCertificate[];
|
||||
|
||||
console.log(`Refreshing ${certificates.length} certificates...`);
|
||||
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
|
||||
@@ -128,14 +123,12 @@ export const refreshAllCertificates = async (): Promise<{ success: number; faile
|
||||
await checkCertificateAndNotify(cert);
|
||||
success++;
|
||||
} catch (error) {
|
||||
console.error(`Failed to refresh certificate ${cert.domain}:`, error);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
return { success, failed };
|
||||
} catch (error) {
|
||||
console.error("Error refreshing certificates:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,9 +1,13 @@
|
||||
|
||||
// SSL Certificate DTO for adding new certificates
|
||||
export interface AddSSLCertificateDto {
|
||||
domain: string;
|
||||
warning_threshold: number;
|
||||
expiry_threshold: number;
|
||||
notification_channel: string;
|
||||
alert_template?: string; // New field for SSL alert template
|
||||
notification_id?: string; // Multi notification channels as comma-separated string
|
||||
template_id?: string; // Alert template ID for PocketBase
|
||||
check_interval?: number; // New field for check interval in days
|
||||
}
|
||||
|
||||
@@ -25,6 +29,10 @@ export interface SSLCertificate {
|
||||
warning_threshold: number;
|
||||
expiry_threshold: number;
|
||||
notification_channel: string;
|
||||
alert_template?: string; // New field for SSL alert template
|
||||
// PocketBase specific fields
|
||||
notification_id?: string; // Multi notification channels as comma-separated string
|
||||
template_id?: string; // Alert template ID for PocketBase
|
||||
last_notified?: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
export interface SSLCertificate {
|
||||
id: string;
|
||||
domain: string;
|
||||
@@ -15,6 +16,10 @@ export interface SSLCertificate {
|
||||
warning_threshold: number;
|
||||
expiry_threshold: number;
|
||||
notification_channel: string;
|
||||
alert_template?: string; // New field for SSL alert template
|
||||
// PocketBase specific fields
|
||||
notification_id?: string; // Multi notification channels as comma-separated string
|
||||
template_id?: string; // Alert template ID for PocketBase
|
||||
last_notified?: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
@@ -33,5 +38,8 @@ export interface AddSSLCertificateDto {
|
||||
warning_threshold: number;
|
||||
expiry_threshold: number;
|
||||
notification_channel: string;
|
||||
alert_template?: string; // New field for SSL alert template
|
||||
notification_id?: string; // Multi notification channels as comma-separated string
|
||||
template_id?: string; // Alert template ID for PocketBase
|
||||
check_interval?: number; // New field for check interval in days
|
||||
}
|
||||