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