diff --git a/README.md b/README.md index 5640b43..78f3f05 100644 --- a/README.md +++ b/README.md @@ -126,18 +126,26 @@ docker run -d \ ### ๐Ÿฅˆ Silver Uptime Ally - - + + ### ๐Ÿงก Ping Supporter - - + + + + +### ๐Ÿค Ecosystem & Community Partner + + --- + ## ๐Ÿ‘ฅ Contributors Thank you for contributing and continuously making CheckCle better, you're awesome ๐Ÿซถ @@ -159,7 +167,7 @@ Here are some ways you can help improve CheckCle: ## ๐ŸŒ Stay Connected - Website: [checkcle.io](https://checkcle.io) -- Documentation: [docs.checkcle.io](https://docs.checkcle.io) +- Documentation: [docs.checkcle.io](https://docs.checkcle.io) | Big thanks to [GitBook](https://github.com/gitbookio) for sponsoring the OSS site plan for CheckCle! - Chat on Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX) - Follow us on X: [@checkcle_oss](https://x.com/checkcle_oss) diff --git a/application/public/upload/notification/discord.png b/application/public/upload/notification/discord.png new file mode 100644 index 0000000..cb1b9bd Binary files /dev/null and b/application/public/upload/notification/discord.png differ diff --git a/application/public/upload/notification/email.png b/application/public/upload/notification/email.png new file mode 100644 index 0000000..7b81629 Binary files /dev/null and b/application/public/upload/notification/email.png differ diff --git a/application/public/upload/notification/google.png b/application/public/upload/notification/google.png new file mode 100644 index 0000000..d4587ab Binary files /dev/null and b/application/public/upload/notification/google.png differ diff --git a/application/public/upload/notification/signal.png b/application/public/upload/notification/signal.png new file mode 100644 index 0000000..ce4e34c Binary files /dev/null and b/application/public/upload/notification/signal.png differ diff --git a/application/public/upload/notification/slack.png b/application/public/upload/notification/slack.png new file mode 100644 index 0000000..61f9b2e Binary files /dev/null and b/application/public/upload/notification/slack.png differ diff --git a/application/public/upload/notification/telegram.png b/application/public/upload/notification/telegram.png new file mode 100644 index 0000000..083d088 Binary files /dev/null and b/application/public/upload/notification/telegram.png differ diff --git a/application/public/upload/notification/webhook.png b/application/public/upload/notification/webhook.png new file mode 100644 index 0000000..b69edea Binary files /dev/null and b/application/public/upload/notification/webhook.png differ diff --git a/application/src/components/dashboard/Header.tsx b/application/src/components/dashboard/Header.tsx index c937d2e..9c8888e 100644 --- a/application/src/components/dashboard/Header.tsx +++ b/application/src/components/dashboard/Header.tsx @@ -1,15 +1,13 @@ - import { Button } from "@/components/ui/button"; import { AuthUser } from "@/services/authService"; import { useTheme } from "@/contexts/ThemeContext"; -import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut, Grid3x3 } from "lucide-react"; +import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut } from "lucide-react"; import { useLanguage } from "@/contexts/LanguageContext"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; import { useEffect, useState } from "react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { useNavigate } from "react-router-dom"; import { useSystemSettings } from "@/hooks/useSystemSettings"; -import QuickActionsDialog from "./QuickActionsDialog"; interface HeaderProps { currentUser: AuthUser | null; @@ -29,7 +27,6 @@ export const Header = ({ const [greeting, setGreeting] = useState(""); const { systemName } = useSystemSettings(); const navigate = useNavigate(); - const [quickActionsOpen, setQuickActionsOpen] = useState(false); // Set greeting based on time of day useEffect(() => { @@ -53,7 +50,7 @@ export const Header = ({ // Log avatar data for debugging useEffect(() => { if (currentUser) { - // console.log("Avatar URL in Header:", currentUser.avatar); + //console.log("Avatar URL in Header:", currentUser.avatar); } }, [currentUser]); @@ -66,7 +63,7 @@ export const Header = ({ } else { avatarUrl = currentUser.avatar; } - // console.log("Final avatar URL:", avatarUrl); + console.log("Final avatar URL:", avatarUrl); } return ( @@ -91,16 +88,6 @@ export const Header = ({ {sidebarCollapsed ? : } - {/* Quick Actions Button */} - -

{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} ๐Ÿ‘‹ โœจ

@@ -162,7 +149,7 @@ export const Header = ({ variant="outline" size="icon" className="rounded-full w-8 h-8 border-border" - onClick={() => window.open("https://x.com/checkcle_oss", "_blank")} + onClick={() => window.open("https://x.com/checkcle_oss)", "_blank")} > X (Twitter) @@ -223,9 +210,6 @@ export const Header = ({ - - {/* Quick Actions Dialog */} - ); }; \ No newline at end of file diff --git a/application/src/components/dashboard/QuickActionsDialog.tsx b/application/src/components/dashboard/QuickActionsDialog.tsx deleted file mode 100644 index 6bd6a90..0000000 --- a/application/src/components/dashboard/QuickActionsDialog.tsx +++ /dev/null @@ -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 = ({ isOpen, setIsOpen }) => { - const { t } = useLanguage(); - - return ( - - - - - Close - - - {t('quickActions')} - - {t('quickActionsDescription')} - - - -
- {/* Website Monitoring Card */} -
-

{t('monitorWebsite')}

-

- {t('monitorWebsiteDesc')} -

-
-

{t('quickTips')}:

-
    -
  • {t('monitorMultipleEndpoints')}
  • -
  • {t('trackResponseTimes')}
  • -
  • {t('setCustomAlerts')}
  • -
-
-
- - {/* Server Monitoring Card */} -
-

{t('monitorServer')}

-

- {t('monitorServerDesc')} -

-
-

{t('quickTips')}:

-
    -
  • {t('monitorCPURam')}
  • -
  • {t('trackNetworkMetrics')}
  • -
  • {t('viewProcesses')}
  • -
-
-
- - {/* SSL Certificate Card */} -
-

{t('sslCertificate')}

-

- {t('sslCertificateDesc')} -

-
-

{t('quickTips')}:

-
    -
  • {t('trackSSLExpiration')}
  • -
  • {t('getEarlyRenewal')}
  • -
  • {t('monitorMultipleDomains')}
  • -
-
-
- - {/* Incidents Management Card */} -
-

{t('incidentsManagement')}

-

- {t('incidentsManagementDesc')} -

-
-

{t('quickTips')}:

-
    -
  • {t('createTrackIncidents')}
  • -
  • {t('assignIncidents')}
  • -
  • {t('monitorResolution')}
  • -
-
-
- - {/* Operational Status Card */} -
-

{t('operationalStatus')}

-

- {t('operationalStatusDesc')} -

-
-

{t('quickTips')}:

-
    -
  • {t('createCustomStatus')}
  • -
  • {t('displayRealTime')}
  • -
  • {t('shareIncidentUpdates')}
  • -
-
-
- - {/* Reports & Analytics Card */} -
-

{t('reportsAnalytics')}

-

- {t('reportsAnalyticsDesc')} -

-
-

{t('quickTips')}:

-
    -
  • {t('generateUptimeReports')}
  • -
  • {t('analyzePerformance')}
  • -
  • {t('exportMonitoringData')}
  • -
-
-
-
- -
- -
-
-
- ); -}; - -export default QuickActionsDialog; \ No newline at end of file diff --git a/application/src/components/servers/DockerOneClickTab.tsx b/application/src/components/servers/DockerOneClickTab.tsx index 33719a9..2dd5256 100644 --- a/application/src/components/servers/DockerOneClickTab.tsx +++ b/application/src/components/servers/DockerOneClickTab.tsx @@ -86,7 +86,7 @@ sudo -E bash ./server-docker-agent.sh`; - Docker One-Click Install (Recommended) + Docker One-Click Install Automated Docker container installation with system monitoring capabilities diff --git a/application/src/components/servers/EditServerDialog.tsx b/application/src/components/servers/EditServerDialog.tsx index 3db001e..d7b4bcf 100644 --- a/application/src/components/servers/EditServerDialog.tsx +++ b/application/src/components/servers/EditServerDialog.tsx @@ -1,4 +1,3 @@ - import React, { useState, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; @@ -79,7 +78,6 @@ export const EditServerDialog: React.FC = ({ // Initialize form data when server changes useEffect(() => { if (server) { - // console.log("Setting form data for server:", server); // Parse comma-separated notification_id into array const notificationChannels = server.notification_id ? server.notification_id.split(',').map(id => id.trim()).filter(id => id) @@ -110,10 +108,8 @@ export const EditServerDialog: React.FC = ({ // Load existing threshold data when thresholds are loaded and we have a server with threshold_id useEffect(() => { if (server && server.threshold_id && thresholds.length > 0) { - // console.log("Loading existing threshold data for server:", server.threshold_id); const existingThreshold = thresholds.find(t => t.id === server.threshold_id); if (existingThreshold) { - // console.log("Found existing threshold:", existingThreshold); setSelectedThreshold(existingThreshold); // Handle the API response format with proper field names and type conversion setThresholdFormData({ @@ -167,7 +163,6 @@ export const EditServerDialog: React.FC = ({ const configs = await alertConfigService.getAlertConfigurations(); setAlertConfigs(configs); } catch (error) { - // console.error('Error loading alert configurations:', error); toast({ variant: "destructive", title: "Error", @@ -185,7 +180,6 @@ export const EditServerDialog: React.FC = ({ // Cast to ServerNotificationTemplate[] since we know we're getting server templates setTemplates(templateList as ServerNotificationTemplate[]); } catch (error) { - // console.error('Error loading templates:', error); toast({ variant: "destructive", title: "Error", @@ -202,7 +196,6 @@ export const EditServerDialog: React.FC = ({ const thresholdList = await serverThresholdService.getServerThresholds(); setThresholds(thresholdList); } catch (error) { - // console.error('Error loading server thresholds:', error); toast({ variant: "destructive", title: "Error", @@ -213,47 +206,6 @@ export const EditServerDialog: React.FC = ({ } }; - const handleThresholdUpdate = async () => { - if (!selectedThreshold) return; - - try { - // Use the correct field name for RAM threshold - const updateData = { - cpu_threshold: thresholdFormData.cpu_threshold, - ram_threshold_message: thresholdFormData.ram_threshold, // Use the correct field name - disk_threshold: thresholdFormData.disk_threshold, - network_threshold: thresholdFormData.network_threshold, - }; - - await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateData); - - // Update local state - setSelectedThreshold({ - ...selectedThreshold, - ...thresholdFormData, - }); - - // Update thresholds list - setThresholds(prev => prev.map(t => - t.id === selectedThreshold.id - ? { ...t, ...thresholdFormData } - : t - )); - - toast({ - title: "Threshold updated", - description: "Server threshold values have been updated successfully.", - }); - } catch (error) { - // console.error('Error updating threshold:', error); - toast({ - variant: "destructive", - title: "Error", - description: "Failed to update threshold values.", - }); - } - }; - const handleNotificationChannelToggle = (channelId: string, checked: boolean) => { setFormData(prev => ({ ...prev, @@ -294,6 +246,40 @@ export const EditServerDialog: React.FC = ({ await pb.collection('servers').update(server.id, updateData); + // Update threshold if a threshold is selected and values have been modified + if (selectedThreshold && formData.threshold_id !== "none") { + try { + const updateThresholdData = { + cpu_threshold: thresholdFormData.cpu_threshold, + ram_threshold_message: thresholdFormData.ram_threshold, // Use the correct field name + disk_threshold: thresholdFormData.disk_threshold, + network_threshold: thresholdFormData.network_threshold, + }; + + await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateThresholdData); + + // Update local state + setSelectedThreshold({ + ...selectedThreshold, + ...thresholdFormData, + }); + + // Update thresholds list + setThresholds(prev => prev.map(t => + t.id === selectedThreshold.id + ? { ...t, ...thresholdFormData } + : t + )); + + } catch (error) { + toast({ + variant: "destructive", + title: "Warning", + description: "Server updated but failed to update threshold values.", + }); + } + } + toast({ title: "Server updated", description: `${formData.name} has been updated successfully.`, @@ -303,7 +289,6 @@ export const EditServerDialog: React.FC = ({ onOpenChange(false); } catch (error) { - // console.error('Error updating server:', error); toast({ variant: "destructive", title: "Error", @@ -518,16 +503,8 @@ export const EditServerDialog: React.FC = ({ {/* Editable Threshold Details */} {selectedThreshold && ( - + Threshold Details: {selectedThreshold.name} -
diff --git a/application/src/components/servers/OneClickInstallTab.tsx b/application/src/components/servers/OneClickInstallTab.tsx index 24e5f6f..600bab6 100644 --- a/application/src/components/servers/OneClickInstallTab.tsx +++ b/application/src/components/servers/OneClickInstallTab.tsx @@ -55,7 +55,7 @@ sudo -E bash ./server-agent.sh`; - One-Click Install + One-Click Install (Recommended) Copy and paste this single command to install the monitoring agent instantly diff --git a/application/src/components/services/add-service/ServiceNotificationFields.tsx b/application/src/components/services/add-service/ServiceNotificationFields.tsx index a8824ff..d5234fe 100644 --- a/application/src/components/services/add-service/ServiceNotificationFields.tsx +++ b/application/src/components/services/add-service/ServiceNotificationFields.tsx @@ -1,4 +1,3 @@ - import { FormControl, FormField, FormItem, FormLabel, FormDescription } from "@/components/ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; @@ -8,6 +7,7 @@ import { UseFormReturn } from "react-hook-form"; import { ServiceFormData } from "./types"; import { useQuery } from "@tanstack/react-query"; import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; +import { serviceNotificationTemplateService, ServiceNotificationTemplate } from "@/services/serviceNotificationTemplateService"; import { useState, useEffect } from "react"; interface ServiceNotificationFieldsProps { @@ -21,18 +21,18 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro const notificationStatus = form.watch("notificationStatus"); const notificationChannels = form.watch("notificationChannels") || []; const alertTemplate = form.watch("alertTemplate"); - - // console.log("Current notification values:", { - // notificationStatus, - // notificationChannels, - // alertTemplate - // }); - + // Fetch alert configurations for notification channels const { data: alertConfigsData } = useQuery({ queryKey: ['alertConfigs'], queryFn: () => alertConfigService.getAlertConfigurations(), }); + + // Fetch service notification templates + const { data: serviceTemplates, isLoading: isLoadingTemplates } = useQuery({ + queryKey: ['serviceNotificationTemplates'], + queryFn: () => serviceNotificationTemplateService.getTemplates(), + }); // Update alert configs when data is loaded useEffect(() => { @@ -42,13 +42,19 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro setAlertConfigs(enabledChannels); // Debug log to check what alert configs are loaded - // console.log("Loaded alert configurations:", enabledChannels); } }, [alertConfigsData]); + // Debug log for service templates + useEffect(() => { + if (serviceTemplates) { + // console.log("Loaded service notification templates:", serviceTemplates); + } + }, [serviceTemplates]); + // Log when form values change to debug useEffect(() => { - // console.log("Notification values changed:", { + // console.log("Notification values changed:", { // notificationStatus: form.getValues("notificationStatus"), // notificationChannels: form.getValues("notificationChannels") // }); @@ -159,9 +165,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro control={form.control} name="alertTemplate" render={({ field }) => { - // Don't convert existing values to "default" - const displayValue = field.value || "default"; - // console.log("Rendering alert template field with value:", displayValue); + // console.log("Rendering alert template field with value:", field.value); return ( @@ -170,17 +174,20 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro @@ -195,4 +202,4 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro /> ); -} +} \ No newline at end of file diff --git a/application/src/components/settings/notification-settings/NotificationChannelDialog.tsx b/application/src/components/settings/notification-settings/NotificationChannelDialog.tsx index cddf07b..ef0469a 100644 --- a/application/src/components/settings/notification-settings/NotificationChannelDialog.tsx +++ b/application/src/components/settings/notification-settings/NotificationChannelDialog.tsx @@ -1,4 +1,3 @@ - import React, { useEffect } from "react"; import { Dialog, @@ -38,7 +37,7 @@ interface NotificationChannelDialogProps { const baseSchema = z.object({ notify_name: z.string().min(1, "Name is required"), - notification_type: z.enum(["telegram", "discord", "slack", "signal", "email"]), + notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "webhook"]), enabled: z.boolean().default(true), service_id: z.string().default("global"), template_id: z.string().optional(), @@ -65,6 +64,11 @@ const signalSchema = baseSchema.extend({ signal_number: z.string().min(1, "Signal number is required"), }); +const googleChatSchema = baseSchema.extend({ + notification_type: z.literal("google_chat"), + google_chat_webhook_url: z.string().url("Must be a valid URL"), +}); + const emailSchema = baseSchema.extend({ notification_type: z.literal("email"), email_address: z.string().email("Valid email is required"), @@ -90,6 +94,7 @@ const formSchema = z.discriminatedUnion("notification_type", [ discordSchema, slackSchema, signalSchema, + googleChatSchema, emailSchema, webhookSchema ]); @@ -97,12 +102,48 @@ const formSchema = z.discriminatedUnion("notification_type", [ type FormValues = z.infer; const notificationTypeOptions = [ - { value: "telegram", label: "Telegram", description: "Send notifications via Telegram bot" }, - { value: "discord", label: "Discord", description: "Send notifications to Discord webhook" }, - { value: "slack", label: "Slack", description: "Send notifications to Slack webhook" }, - { value: "signal", label: "Signal", description: "Send notifications via Signal" }, - { value: "email", label: "Email", description: "Send notifications via email" }, - { value: "webhook", label: "Webhook", description: "Send notifications to custom webhook" }, + { + value: "telegram", + label: "Telegram", + description: "Send notifications via Telegram bot", + icon: "/upload/notification/telegram.png" + }, + { + value: "discord", + label: "Discord", + description: "Send notifications to Discord webhook", + icon: "/upload/notification/discord.png" + }, + { + value: "slack", + label: "Slack", + description: "Send notifications to Slack webhook", + icon: "/upload/notification/slack.png" + }, + { + value: "signal", + label: "Signal", + description: "Send notifications via Signal", + icon: "/upload/notification/signal.png" + }, + { + value: "google_chat", + label: "Google Chat", + description: "Send notifications to Google Chat webhook", + icon: "/upload/notification/google.png" + }, + { + value: "email", + label: "Email", + description: "Send notifications via email", + icon: "/upload/notification/email.png" + }, + { + value: "webhook", + label: "Webhook", + description: "Send notifications to custom webhook", + icon: "/upload/notification/webhook.png" + }, ]; const webhookPayloadTemplates = { @@ -236,7 +277,7 @@ export const NotificationChannelDialog = ({ await webhookService.createWebhook(webhookData); } } else { - // Handle other notification types with existing service + // Handle all other notification types including Google Chat and Email const configData = { ...values, service_id: values.service_id || "global", @@ -248,7 +289,14 @@ export const NotificationChannelDialog = ({ await alertConfigService.createAlertConfiguration(configData as any); } } + onClose(true); // Close with refresh + } catch (error) { + toast({ + title: "Error", + description: "Failed to save notification channel", + variant: "destructive" + }); } finally { setIsSubmitting(false); } @@ -297,9 +345,16 @@ export const NotificationChannelDialog = ({ {notificationTypeOptions.map((option) => ( -
- {option.label} - {option.description} +
+ {`${option.label} +
+ {option.label} + {option.description} +
))} @@ -310,7 +365,6 @@ export const NotificationChannelDialog = ({ )} /> - {/* Show different fields based on notification type */} {notificationType === "telegram" && ( <> )} + {notificationType === "google_chat" && ( + ( + + Google Chat Webhook URL + + + + + Google Chat webhook URL from your Google Chat space + + + + )} + /> + )} + {notificationType === "email" && ( <> - {/* Payload Template Section */}
{ + isWebhook?: boolean; + url?: string; + method?: string; + description?: string; +} interface NotificationChannelListProps { - channels: AlertConfiguration[]; + channels: CombinedChannel[]; onEdit: (config: AlertConfiguration) => void; onDelete: (id: string) => void; } @@ -25,25 +33,63 @@ export const NotificationChannelList = ({ onEdit, onDelete }: NotificationChannelListProps) => { - const toggleEnabled = async (config: AlertConfiguration) => { + const toggleEnabled = async (config: CombinedChannel) => { if (!config.id) return; - await alertConfigService.updateAlertConfiguration(config.id, { - enabled: !config.enabled - }); - - // The parent component will refresh the list - onEdit(config); + if (config.isWebhook) { + // Handle webhook toggle + try { + const newEnabled = config.enabled ? "off" : "on"; + await pb.collection('webhook').update(config.id, { + enabled: newEnabled + }); + // Trigger refresh by calling onEdit with empty config + onEdit({} as AlertConfiguration); + } catch (error) { + console.error("Error updating webhook:", error); + } + } else { + // Handle alert config toggle + await alertConfigService.updateAlertConfiguration(config.id, { + enabled: !config.enabled + }); + + // The parent component will refresh the list + onEdit(config as AlertConfiguration); + } }; - const getChannelTypeLabel = (type: string) => { + const getChannelTypeLabel = (type: string | undefined) => { switch(type) { case "telegram": return "Telegram"; case "discord": return "Discord"; case "slack": return "Slack"; case "signal": return "Signal"; + case "google_chat": return "Google Chat"; case "email": return "Email"; - default: return type; + case "webhook": return "Webhook"; + default: return type || "Unknown"; + } + }; + + const getChannelDetails = (config: CombinedChannel) => { + if (config.isWebhook) { + return `${config.method || 'POST'} ${config.url || ''}`; + } + + switch(config.notification_type) { + case "telegram": + return config.telegram_chat_id || ''; + case "discord": + case "slack": + case "google_chat": + return config.discord_webhook_url || config.slack_webhook_url || config.google_chat_webhook_url || ''; + case "signal": + return config.signal_number || ''; + case "email": + return config.email_address || ''; + default: + return ''; } }; @@ -66,6 +112,7 @@ export const NotificationChannelList = ({ Name Type + Details Status Created Actions @@ -78,11 +125,14 @@ export const NotificationChannelList = ({ {getChannelTypeLabel(channel.notification_type)} + + {getChannelDetails(channel)} + toggleEnabled(channel)} @@ -96,7 +146,8 @@ export const NotificationChannelList = ({ @@ -119,4 +170,4 @@ export const NotificationChannelList = ({
); -}; +}; \ No newline at end of file diff --git a/application/src/components/settings/notification-settings/NotificationSettings.tsx b/application/src/components/settings/notification-settings/NotificationSettings.tsx index 8f27a0d..70958a6 100644 --- a/application/src/components/settings/notification-settings/NotificationSettings.tsx +++ b/application/src/components/settings/notification-settings/NotificationSettings.tsx @@ -5,28 +5,47 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Plus, Loader2 } from "lucide-react"; import { AlertConfiguration, alertConfigService } from "@/services/alertConfigService"; +import { WebhookConfiguration, webhookService } from "@/services/webhookService"; import { NotificationChannelDialog } from "./NotificationChannelDialog"; import { NotificationChannelList } from "./NotificationChannelList"; +import { pb } from "@/lib/pocketbase"; + +interface CombinedChannel extends Partial { + isWebhook?: boolean; + url?: string; + method?: string; + description?: string; +} const NotificationSettings = () => { const [isLoading, setIsLoading] = useState(true); const [alertConfigs, setAlertConfigs] = useState([]); + const [webhookConfigs, setWebhookConfigs] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); const [currentTab, setCurrentTab] = useState("all"); const [editingConfig, setEditingConfig] = useState(null); - const fetchAlertConfigurations = async () => { + const fetchNotificationChannels = async () => { setIsLoading(true); try { + // Fetch alert configurations const configs = await alertConfigService.getAlertConfigurations(); setAlertConfigs(configs); + + // Fetch webhooks + try { + const webhookResponse = await pb.collection('webhook').getList(1, 50); + setWebhookConfigs(webhookResponse.items as WebhookConfiguration[]); + } catch (webhookError) { + setWebhookConfigs([]); + } } finally { setIsLoading(false); } }; useEffect(() => { - fetchAlertConfigurations(); + fetchNotificationChannels(); }, []); const handleAddNew = () => { @@ -40,22 +59,66 @@ const NotificationSettings = () => { }; const handleDelete = async (id: string) => { - const success = await alertConfigService.deleteAlertConfiguration(id); - if (success) { - fetchAlertConfigurations(); + // Check if it's a webhook first + const isWebhook = webhookConfigs.find(w => w.id === id); + + if (isWebhook) { + // Handle webhook deletion + if (confirm("Are you sure you want to delete this webhook?")) { + try { + await pb.collection('webhook').delete(id); + fetchNotificationChannels(); + } catch (error) { + console.error("Error deleting webhook:", error); + } + } + } else { + // Handle alert config deletion + const success = await alertConfigService.deleteAlertConfiguration(id); + if (success) { + fetchNotificationChannels(); + } } }; const handleDialogClose = (refreshList: boolean) => { setDialogOpen(false); if (refreshList) { - fetchAlertConfigurations(); + fetchNotificationChannels(); } }; + const getCombinedChannels = (): CombinedChannel[] => { + const combined: CombinedChannel[] = []; + + // Add alert configurations + alertConfigs.forEach(config => { + combined.push(config); + }); + + // Add webhooks as notification channels + webhookConfigs.forEach(webhook => { + combined.push({ + id: webhook.id, + notify_name: webhook.name, + notification_type: "webhook" as const, + enabled: webhook.enabled === "on", + created: webhook.created, + updated: webhook.updated, + isWebhook: true, + url: webhook.url, + method: webhook.method, + description: webhook.description + }); + }); + + return combined; + }; + const getFilteredConfigs = () => { - if (currentTab === "all") return alertConfigs; - return alertConfigs.filter(config => config.notification_type === currentTab); + const combined = getCombinedChannels(); + if (currentTab === "all") return combined; + return combined.filter(config => config.notification_type === currentTab); }; return ( @@ -86,6 +149,7 @@ const NotificationSettings = () => { Discord Slack Signal + Google Chat Email Webhook diff --git a/application/src/components/ssl-domain/AddSSLCertificateForm.tsx b/application/src/components/ssl-domain/AddSSLCertificateForm.tsx index c78ff22..f13fd5d 100644 --- a/application/src/components/ssl-domain/AddSSLCertificateForm.tsx +++ b/application/src/components/ssl-domain/AddSSLCertificateForm.tsx @@ -1,24 +1,28 @@ + import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { toast } from "sonner"; -import { Bell } from "lucide-react"; +import { Bell, X } from "lucide-react"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { DialogFooter } from "@/components/ui/dialog"; +import { Badge } from "@/components/ui/badge"; import { AddSSLCertificateDto } from "@/types/ssl.types"; import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; +import { sslNotificationTemplateService, SslNotificationTemplate } from "@/services/sslNotificationTemplateService"; import { useLanguage } from "@/contexts/LanguageContext"; const formSchema = z.object({ domain: z.string().min(1, "Domain is required"), warning_threshold: z.coerce.number().int().min(1).max(365), expiry_threshold: z.coerce.number().int().min(1).max(30), - notification_channel: z.string().optional(), // Make it optional to allow empty string for "None" + notification_channels: z.array(z.string()).optional(), + alert_template: z.string().optional(), check_interval: z.coerce.number().int().min(1).max(30).optional() }); @@ -35,6 +39,7 @@ export const AddSSLCertificateForm = ({ }: AddSSLCertificateFormProps) => { const { t } = useLanguage(); const [alertConfigs, setAlertConfigs] = useState([]); + const [sslTemplates, setSslTemplates] = useState([]); const [isLoading, setIsLoading] = useState(false); const form = useForm>({ @@ -43,58 +48,78 @@ export const AddSSLCertificateForm = ({ domain: "", warning_threshold: 30, expiry_threshold: 7, - notification_channel: "none", + notification_channels: [], + alert_template: "none", check_interval: 1 } }); - // Fetch notification channels when form loads + // Fetch notification channels and SSL templates when form loads useEffect(() => { - const fetchNotificationChannels = async () => { + const fetchData = async () => { setIsLoading(true); try { + // Fetch notification channels const configs = await alertConfigService.getAlertConfigurations(); - // console.log("Fetched notification channels:", configs); - // Only include enabled channels const enabledConfigs = configs.filter(config => { - // Handle the possibility of enabled being a string if (typeof config.enabled === 'string') { return config.enabled === "true"; } - // Otherwise treat as boolean return config.enabled === true; }); setAlertConfigs(enabledConfigs); + + // Fetch SSL notification templates + const templates = await sslNotificationTemplateService.getTemplates(); + setSslTemplates(templates); } catch (error) { - // console.error("Error fetching notification channels:", error); toast.error(t('failedToLoadCertificates')); } finally { setIsLoading(false); } }; - fetchNotificationChannels(); - }, [form, t]); + fetchData(); + }, [t]); const handleSubmit = async (values: z.infer) => { try { - // Convert the form values to the required DTO format with required properties + // Convert the form values to the required DTO format const certData: AddSSLCertificateDto = { domain: values.domain, warning_threshold: values.warning_threshold, expiry_threshold: values.expiry_threshold, - notification_channel: values.notification_channel === "none" ? "" : (values.notification_channel || ""), // Convert "none" to empty string + notification_channel: values.notification_channels && values.notification_channels.length > 0 + ? values.notification_channels.join(',') + : '', + notification_id: values.notification_channels && values.notification_channels.length > 0 + ? values.notification_channels.join(',') + : '', + template_id: values.alert_template && values.alert_template !== 'none' ? values.alert_template : '', check_interval: values.check_interval }; await onSubmit(certData); form.reset(); } catch (error) { - // console.error("Error adding SSL certificate:", error); toast.error(t('failedToAddCertificate')); } }; + const handleAddNotificationChannel = (channelId: string) => { + const currentChannels = form.getValues("notification_channels") || []; + if (!currentChannels.includes(channelId)) { + form.setValue("notification_channels", [...currentChannels, channelId]); + } + }; + + const handleRemoveNotificationChannel = (channelId: string) => { + const currentChannels = form.getValues("notification_channels") || []; + form.setValue("notification_channels", currentChannels.filter(id => id !== channelId)); + }; + + const selectedChannels = form.watch("notification_channels") || []; + return (
@@ -167,31 +192,60 @@ export const AddSSLCertificateForm = ({ ( - {t('notificationChannel')} - + {t('notificationChannel')} (Multi-select) +
+ + + {/* Display selected channels */} + {selectedChannels.length > 0 && ( +
+ {selectedChannels.map((channelId) => { + const channel = alertConfigs.find(config => config.id === channelId); + return ( + + {channel ? `${channel.notify_name} (${channel.notification_type})` : channelId} + + + ); + })} +
+ )} +
{t('whereToSend')} @@ -200,6 +254,41 @@ export const AddSSLCertificateForm = ({
)} /> + + ( + + Alert Template + + + Template for SSL certificate alert messages + + + + )} + /> diff --git a/application/src/components/ssl-domain/EditSSLCertificateForm.tsx b/application/src/components/ssl-domain/EditSSLCertificateForm.tsx index a53a1a2..1888c07 100644 --- a/application/src/components/ssl-domain/EditSSLCertificateForm.tsx +++ b/application/src/components/ssl-domain/EditSSLCertificateForm.tsx @@ -1,4 +1,3 @@ - import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -7,17 +6,20 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Badge } from "@/components/ui/badge"; import { SSLCertificate } from "@/types/ssl.types"; -import { Loader2, Bell } from "lucide-react"; +import { Loader2, Bell, X } from "lucide-react"; import { toast } from "sonner"; import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; +import { sslNotificationTemplateService, SslNotificationTemplate } from "@/services/sslNotificationTemplateService"; import { useLanguage } from "@/contexts/LanguageContext"; const formSchema = z.object({ domain: z.string().min(1, "Domain is required"), warning_threshold: z.coerce.number().min(1, "Warning threshold must be at least 1 day"), expiry_threshold: z.coerce.number().min(1, "Expiry threshold must be at least 1 day"), - notification_channel: z.string().min(1, "Notification channel is required"), + notification_channels: z.array(z.string()).default([]), + alert_template: z.string().optional(), check_interval: z.coerce.number().int().min(1).max(30).optional(), }); @@ -33,6 +35,7 @@ interface EditSSLCertificateFormProps { export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPending }: EditSSLCertificateFormProps) => { const { t } = useLanguage(); const [alertConfigs, setAlertConfigs] = useState([]); + const [sslTemplates, setSslTemplates] = useState([]); const [isLoading, setIsLoading] = useState(false); const form = useForm({ @@ -41,58 +44,81 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend domain: certificate.domain, warning_threshold: certificate.warning_threshold, expiry_threshold: certificate.expiry_threshold, - notification_channel: certificate.notification_channel, + notification_channels: certificate.notification_channel ? certificate.notification_channel.split(',') : [], + alert_template: certificate.alert_template || "none", check_interval: certificate.check_interval || 1, }, }); - // Fetch notification channels when form loads + const notificationChannels = form.watch("notification_channels") || []; + + // Fetch notification channels and SSL templates when form loads useEffect(() => { - const fetchNotificationChannels = async () => { + const fetchData = async () => { setIsLoading(true); try { + // Fetch notification channels const configs = await alertConfigService.getAlertConfigurations(); - // console.log("Fetched notification channels:", configs); - // Only include enabled channels const enabledConfigs = configs.filter(config => { - // Handle the possibility of enabled being a string if (typeof config.enabled === 'string') { return config.enabled === "true"; } - // Otherwise treat as boolean return config.enabled === true; }); setAlertConfigs(enabledConfigs); + + // Fetch SSL notification templates + const templates = await sslNotificationTemplateService.getTemplates(); + setSslTemplates(templates); } catch (error) { - // console.error("Error fetching notification channels:", error); toast.error(t('failedToLoadCertificates')); } finally { setIsLoading(false); } }; - fetchNotificationChannels(); + fetchData(); }, [t]); + const handleChannelAdd = (channelId: string) => { + const currentChannels = form.getValues("notification_channels") || []; + if (!currentChannels.includes(channelId)) { + form.setValue("notification_channels", [...currentChannels, channelId]); + } + }; + + const handleChannelRemove = (channelId: string) => { + const currentChannels = form.getValues("notification_channels") || []; + form.setValue("notification_channels", currentChannels.filter(id => id !== channelId)); + }; + + const getSelectedChannelNames = () => { + return (notificationChannels || []).map(channelId => { + const config = alertConfigs.find(c => c.id === channelId); + return config ? `${config.notify_name} (${config.notification_type})` : channelId; + }); + }; + const handleSubmit = (data: FormValues) => { // Merge the updated values with the original certificate const updatedCertificate: SSLCertificate = { ...certificate, ...data, + // Save notification channels as comma-separated string for notification_channel field + notification_channel: data.notification_channels.length > 0 ? data.notification_channels.join(',') : '', + // Save notification channels as comma-separated string for notification_id field (for PocketBase) + notification_id: data.notification_channels.length > 0 ? data.notification_channels.join(',') : '', + // Save alert template as template_id field (for PocketBase) - handle "none" value + template_id: data.alert_template && data.alert_template !== 'none' ? data.alert_template : '', // Ensure values are correctly typed as numbers warning_threshold: Number(data.warning_threshold), expiry_threshold: Number(data.expiry_threshold), check_interval: data.check_interval ? Number(data.check_interval) : undefined }; - console.log("Submitting updated certificate:", updatedCertificate); onSubmit(updatedCertificate); }; - // For debugging - console.log("Certificate data:", certificate); - console.log("Form default values:", form.getValues()); - return ( @@ -106,7 +132,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend @@ -188,41 +214,52 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend ( {t('notificationChannel')} - - - - {alertConfigs.length > 0 ? ( - alertConfigs.map((config) => ( - - {config.notify_name} ({config.notification_type}) - - )) - ) : isLoading ? ( - {t('loadingChannels')} - ) : ( - <> - Email - Telegram - Slack - Webhook - None - - )} - - + + {alertConfigs + .filter(config => config.id && !notificationChannels?.includes(config.id)) + .map((config) => ( + + {config.notify_name} ({config.notification_type}) + + ))} + {alertConfigs.filter(config => config.id && !notificationChannels?.includes(config.id)).length === 0 && ( + No available channels + )} + + + {t('whereToSend')} @@ -232,6 +269,45 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend )} /> + ( + + Alert Template + + + + + Choose a template for SSL certificate alert messages + + + + )} + /> +