diff --git a/README.md b/README.md index 5640b43..6973d3f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,12 @@
Japanese + + + Chinese +
Chinese +
+ @@ -111,7 +117,7 @@ docker run -d \ ![Service Detail Page](https://cdn.checkcle.io/images/uptime/uptime-regional-detail.png) ![checkcle-server-instance](https://cdn.checkcle.io/images/server/server-list.png) ![SSL Monitoring](https://cdn.checkcle.io/images/ssl-domain/ssl-list.png) -![Schedule Maintenance](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-schedule-maintenance.png) +![Notification System](https://cdn.checkcle.io/general/powerfull_notification.png) ## 🌟 CheckCle for Communities? @@ -126,18 +132,32 @@ docker run -d \ ### 🥈 Silver Uptime Ally - - + + ### 🧡 Ping Supporter - - + + +### 🤝 Ecosystem & Community Partner + + + + + DigitalOcean Logo + + + --- + ## 👥 Contributors Thank you for contributing and continuously making CheckCle better, you're awesome 🫶 @@ -159,7 +179,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) @@ -168,5 +188,3 @@ Here are some ways you can help improve CheckCle: CheckCle is released under the MIT License. --- - - 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..ed5b542 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'} 👋 ✨

@@ -132,6 +119,9 @@ export const Header = ({ setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}> {t("japanese")} + setLanguage("zhcn")} className={language === "zhcn" ? "bg-accent" : ""}> + {t("simplifiedChinese")} + @@ -162,7 +152,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 +213,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/regional-monitoring/AddRegionalAgentDialog.tsx b/application/src/components/regional-monitoring/AddRegionalAgentDialog.tsx index c18cd18..ebde414 100644 --- a/application/src/components/regional-monitoring/AddRegionalAgentDialog.tsx +++ b/application/src/components/regional-monitoring/AddRegionalAgentDialog.tsx @@ -114,7 +114,7 @@ export const AddRegionalAgentDialog: React.FC = ({ textarea.setSelectionRange(0, 99999); // For mobile devices } } catch (selectError) { - console.error('Failed to select text:', selectError); + // console.error('Failed to select text:', selectError); } } }; diff --git a/application/src/components/servers/DockerOneClickTab.tsx b/application/src/components/servers/DockerOneClickTab.tsx index 9c23228..2dd5256 100644 --- a/application/src/components/servers/DockerOneClickTab.tsx +++ b/application/src/components/servers/DockerOneClickTab.tsx @@ -64,18 +64,18 @@ sudo -E bash ./server-docker-agent.sh`; const handleCopyOneClickCommand = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - console.log('Copy one-click command button clicked'); + // console.log('Copy one-click command button clicked'); const command = getDockerOneClickCommand(); - console.log('Copying one-click command:', command); + // console.log('Copying one-click command:', command); await copyToClipboard(command); }; const handleCopyDockerCommand = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - console.log('Copy docker command button clicked'); + // console.log('Copy docker command button clicked'); const command = getDirectDockerCommand(); - console.log('Copying docker command:', command); + // console.log('Copying docker command:', command); await copyToClipboard(command); }; @@ -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 cce5755..6f524f8 100644 --- a/application/src/components/servers/EditServerDialog.tsx +++ b/application/src/components/servers/EditServerDialog.tsx @@ -12,7 +12,7 @@ import { pb } from "@/lib/pocketbase"; import { Server } from "@/types/server.types"; import { RefreshCw, X } from "lucide-react"; import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; -import { templateService, NotificationTemplate } from "@/services/templateService"; +import { templateService, ServerNotificationTemplate } from "@/services/templateService"; import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService"; interface EditServerDialogProps { @@ -25,7 +25,7 @@ interface EditServerDialogProps { interface ServerFormData { name: string; check_interval: number; - retry_attempts: number; + max_retries: number; docker_monitoring: boolean; notification_enabled: boolean; notification_channels: string[]; // Changed to array for multiple selections @@ -49,7 +49,7 @@ export const EditServerDialog: React.FC = ({ const [formData, setFormData] = useState({ name: "", check_interval: 60, - retry_attempts: 3, + max_retries: 3, docker_monitoring: false, notification_enabled: false, notification_channels: [], // Changed to array @@ -66,9 +66,9 @@ export const EditServerDialog: React.FC = ({ const [isSubmitting, setIsSubmitting] = useState(false); const [alertConfigs, setAlertConfigs] = useState([]); - const [templates, setTemplates] = useState([]); + const [templates, setTemplates] = useState([]); const [thresholds, setThresholds] = useState([]); - const [selectedTemplate, setSelectedTemplate] = useState(null); + const [selectedTemplate, setSelectedTemplate] = useState(null); const [selectedThreshold, setSelectedThreshold] = useState(null); const [loadingAlertConfigs, setLoadingAlertConfigs] = useState(false); const [loadingTemplates, setLoadingTemplates] = useState(false); @@ -78,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) @@ -87,9 +86,9 @@ export const EditServerDialog: React.FC = ({ setFormData({ name: server.name || "", check_interval: server.check_interval || 60, - retry_attempts: 3, + max_retries: server.max_retries || 3, docker_monitoring: server.docker === "true", - notification_enabled: notificationChannels.length > 0, + notification_enabled: server.notification_status === true, notification_channels: notificationChannels, threshold_id: server.threshold_id || "none", template_id: server.template_id || "none", @@ -109,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({ @@ -166,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", @@ -180,10 +176,10 @@ export const EditServerDialog: React.FC = ({ const loadTemplates = async () => { try { setLoadingTemplates(true); - const templateList = await templateService.getTemplates(); - setTemplates(templateList); + const templateList = await templateService.getTemplates('server'); + // 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", @@ -200,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", @@ -211,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, @@ -276,22 +230,56 @@ export const EditServerDialog: React.FC = ({ setIsSubmitting(true); // Convert notification channels array to comma-separated string - const notificationChannelsString = formData.notification_enabled - ? formData.notification_channels.join(',') - : ""; + const notificationChannelsString = formData.notification_channels.join(','); const updateData = { name: formData.name, check_interval: formData.check_interval, + max_retries: formData.max_retries, docker: formData.docker_monitoring ? "true" : "false", + notification_status: formData.notification_enabled, notification_id: notificationChannelsString, - threshold_id: formData.notification_enabled && formData.threshold_id !== "none" ? formData.threshold_id : "", - template_id: formData.notification_enabled && formData.template_id !== "none" ? formData.template_id : "", + threshold_id: formData.threshold_id !== "none" ? formData.threshold_id : "", + template_id: formData.template_id !== "none" ? formData.template_id : "", updated: new Date().toISOString(), }; 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, + 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.`, @@ -301,7 +289,6 @@ export const EditServerDialog: React.FC = ({ onOpenChange(false); } catch (error) { - // console.error('Error updating server:', error); toast({ variant: "destructive", title: "Error", @@ -321,9 +308,9 @@ export const EditServerDialog: React.FC = ({ setFormData({ name: server.name || "", check_interval: server.check_interval || 60, - retry_attempts: 3, + max_retries: server.max_retries || 3, docker_monitoring: server.docker === "true", - notification_enabled: notificationChannels.length > 0, + notification_enabled: server.notification_status === true, notification_channels: notificationChannels, threshold_id: server.threshold_id || "none", template_id: server.template_id || "none", @@ -372,20 +359,20 @@ export const EditServerDialog: React.FC = ({
- +
@@ -416,10 +403,8 @@ export const EditServerDialog: React.FC = ({ checked={formData.notification_enabled} onCheckedChange={(checked) => setFormData(prev => ({ ...prev, - notification_enabled: checked, - notification_channels: checked ? prev.notification_channels : [], - threshold_id: checked ? prev.threshold_id : "none", - template_id: checked ? prev.template_id : "none" + notification_enabled: checked + // Remove the automatic clearing of notification_channels, threshold_id, and template_id }))} /> @@ -516,16 +501,8 @@ export const EditServerDialog: React.FC = ({ {/* Editable Threshold Details */} {selectedThreshold && ( - + Threshold Details: {selectedThreshold.name} -
@@ -621,27 +598,39 @@ export const EditServerDialog: React.FC = ({
- +

- {selectedTemplate.up_message || "No RAM threshold message defined"} + {selectedTemplate.ram_message || "No RAM message defined"}

- +

- {selectedTemplate.down_message || "No CPU threshold message defined"} + {selectedTemplate.cpu_message || "No CPU message defined"}

- +

- {selectedTemplate.incident_message || "No disk threshold message defined"} + {selectedTemplate.disk_message || "No disk message defined"}

- +

- {selectedTemplate.maintenance_message || "No network threshold message defined"} + {selectedTemplate.network_message || "No network message defined"} +

+
+
+ +

+ {selectedTemplate.up_message || "No up message defined"} +

+
+
+ +

+ {selectedTemplate.down_message || "No down message defined"}

@@ -677,4 +666,6 @@ export const EditServerDialog: React.FC = ({ ); -}; \ No newline at end of file +}; + +export default EditServerDialog; \ No newline at end of file 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/ServiceDetailContainer/hooks/useServiceData.tsx b/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx index fa42951..0fbcb6b 100644 --- a/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx +++ b/application/src/components/services/ServiceDetailContainer/hooks/useServiceData.tsx @@ -125,7 +125,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e } else { // Fetch regional agent specific data const [regionName, agentId] = currentAgent.split("|"); - console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`); + // console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`); history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId); // console.log(`Retrieved ${history.length} regional monitoring records`); } diff --git a/application/src/components/services/ServiceMonitoringButton.tsx b/application/src/components/services/ServiceMonitoringButton.tsx index 3e2d3fc..8a0aed0 100644 --- a/application/src/components/services/ServiceMonitoringButton.tsx +++ b/application/src/components/services/ServiceMonitoringButton.tsx @@ -5,7 +5,6 @@ import { Play, Pause } from "lucide-react"; import { Service } from "@/types/service.types"; import { serviceService } from "@/services/serviceService"; import { useToast } from "@/hooks/use-toast"; -import { notificationService } from "@/services/notificationService"; interface ServiceMonitoringButtonProps { service: Service; @@ -34,16 +33,8 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni if (onStatusChange) onStatusChange("paused"); - // Send notification for paused status (only here, not in pauseMonitoring.ts) - if (service.alerts !== "muted") { - // console.log("Sending pause notification from UI component"); - // IMPORTANT: Direct call to the notification service to ensure a message is sent - await notificationService.sendNotification({ - service: service, - status: "paused", - timestamp: new Date().toISOString(), - }); - } + // Notification handling removed - will be handled by backend + // console.log("Service paused - notifications will be handled by backend"); toast({ title: "Monitoring paused", @@ -51,7 +42,7 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni }); } else { // Start/resume monitoring - // console.log(`Starting monitoring for service ${service.id} (${service.name})`); + // console.log(`Starting monitoring for service ${service.id} (${service.name})`); // First ensure we update the status in the database to not be paused anymore await serviceService.resumeMonitoring(service.id); @@ -100,4 +91,4 @@ export function ServiceMonitoringButton({ service, onStatusChange }: ServiceMoni )} ); -} +} \ No newline at end of file diff --git a/application/src/components/services/add-service/ServiceNotificationFields.tsx b/application/src/components/services/add-service/ServiceNotificationFields.tsx index a8824ff..075b253 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 ( @@ -169,18 +173,20 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro @@ -195,4 +201,4 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro /> ); -} +} \ No newline at end of file diff --git a/application/src/components/services/add-service/serviceFormUtils.ts b/application/src/components/services/add-service/serviceFormUtils.ts index 3429c41..b606d84 100644 --- a/application/src/components/services/add-service/serviceFormUtils.ts +++ b/application/src/components/services/add-service/serviceFormUtils.ts @@ -74,7 +74,6 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => { notificationChannels.push(...parsedChannels); } } catch (error) { - // console.warn("Failed to parse notification_channel JSON:", error); // If parsing fails, treat as single channel ID notificationChannels.push(service.notification_channel); } @@ -91,16 +90,13 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => { } } -// console.log("Mapping service to form data:", { - // serviceName: service.name, - // notification_status: service.notification_status, - // notification_channel: service.notification_channel, - // notificationChannel: service.notificationChannel, - // mappedChannels: notificationChannels, - // regionalAgents: regionalAgents, - // region_name: service.region_name, - /// agent_id: service.agent_id - // }); + // Handle notification_status - it can be boolean or string + let notificationStatus: "enabled" | "disabled" = "disabled"; + if (typeof service.notification_status === "boolean") { + notificationStatus = service.notification_status ? "enabled" : "disabled"; + } else if (typeof service.notification_status === "string") { + notificationStatus = service.notification_status === "enabled" ? "enabled" : "disabled"; + } return { name: service.name || "", @@ -109,7 +105,7 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => { port: portValue, interval: String(service.interval || 60), retries: String(service.retries || 3), - notificationStatus: service.notification_status || "disabled", + notificationStatus: notificationStatus, notificationChannels: notificationChannels, alertTemplate: service.alertTemplate === "default" ? "" : service.alertTemplate || "", regionalMonitoringEnabled: isRegionalEnabled, @@ -152,7 +148,8 @@ export const mapFormDataToServiceData = (data: ServiceFormData) => { type: data.type, interval: parseInt(data.interval), retries: parseInt(data.retries), - notificationStatus: data.notificationStatus || "disabled", + // Convert string status to boolean for notification_status field + notificationStatus: data.notificationStatus === "enabled", notificationChannels: data.notificationChannels || [], alertTemplate: data.alertTemplate === "default" ? "" : data.alertTemplate, // Use regional_status field and store multiple agents as comma-separated values diff --git a/application/src/components/settings/alerts-templates/AlertsTemplates.tsx b/application/src/components/settings/alerts-templates/AlertsTemplates.tsx index b8226d4..ec14f0d 100644 --- a/application/src/components/settings/alerts-templates/AlertsTemplates.tsx +++ b/application/src/components/settings/alerts-templates/AlertsTemplates.tsx @@ -1,9 +1,10 @@ import React, { useState } from "react"; import { useQuery } from "@tanstack/react-query"; -import { templateService } from "@/services/templateService"; +import { templateService, TemplateType } from "@/services/templateService"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Plus, RefreshCcw } from "lucide-react"; import { TemplateList } from "./TemplateList"; import { TemplateDialog } from "./TemplateDialog"; @@ -11,8 +12,10 @@ import { useToast } from "@/hooks/use-toast"; export const AlertsTemplates = () => { const { toast } = useToast(); + const [activeTab, setActiveTab] = useState('service'); const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingTemplate, setEditingTemplate] = useState(null); + const [editingTemplateType, setEditingTemplateType] = useState(null); const { data: templates = [], @@ -20,17 +23,19 @@ export const AlertsTemplates = () => { error, refetch } = useQuery({ - queryKey: ['notification_templates'], - queryFn: templateService.getTemplates, + queryKey: ['notification_templates', activeTab], + queryFn: () => templateService.getTemplates(activeTab), }); - const handleAddTemplate = () => { + const handleAddTemplate = (templateType: TemplateType) => { setEditingTemplate(null); + setEditingTemplateType(templateType); setIsDialogOpen(true); }; - const handleEditTemplate = (id: string) => { + const handleEditTemplate = (id: string, templateType: TemplateType) => { setEditingTemplate(id); + setEditingTemplateType(templateType); setIsDialogOpen(true); }; @@ -51,33 +56,103 @@ export const AlertsTemplates = () => { Refresh -
- {error ? ( -
-

Error loading templates

- -
- ) : ( - - )} + setActiveTab(value as TemplateType)}> + + Service Uptime + Server Monitoring + SSL Certificate + Server Threshold + + + + {error ? ( +
+

Error loading service templates

+ +
+ ) : ( + handleEditTemplate(id, 'service')} + refetchTemplates={refetch} + templateType="service" + /> + )} +
+ + + {error ? ( +
+

Error loading server templates

+ +
+ ) : ( + handleEditTemplate(id, 'server')} + refetchTemplates={refetch} + templateType="server" + /> + )} +
+ + + {error ? ( +
+

Error loading SSL templates

+ +
+ ) : ( + handleEditTemplate(id, 'ssl')} + refetchTemplates={refetch} + templateType="ssl" + /> + )} +
+ + + {error ? ( +
+

Error loading server threshold templates

+ +
+ ) : ( + handleEditTemplate(id, 'server_threshold')} + refetchTemplates={refetch} + templateType="server_threshold" + /> + )} +
+
{ refetch(); @@ -86,4 +161,4 @@ export const AlertsTemplates = () => { />
); -}; +}; \ No newline at end of file diff --git a/application/src/components/settings/alerts-templates/TemplateDialog.tsx b/application/src/components/settings/alerts-templates/TemplateDialog.tsx index 4b9b13d..fb547d6 100644 --- a/application/src/components/settings/alerts-templates/TemplateDialog.tsx +++ b/application/src/components/settings/alerts-templates/TemplateDialog.tsx @@ -1,19 +1,26 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { Form } from "@/components/ui/form"; +import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useTemplateForm } from "./hooks/useTemplateForm"; -import { BasicTemplateFields } from "./form/BasicTemplateFields"; -import { MessagesTabContent } from "./form/MessagesTabContent"; -import { PlaceholdersTabContent } from "./form/PlaceholdersTabContent"; +import { ServerTemplateFields } from "./form/ServerTemplateFields"; +import { ServiceTemplateFields } from "./form/ServiceTemplateFields"; +import { SslTemplateFields } from "./form/SslTemplateFields"; +import { ServerThresholdFields } from "./form/ServerThresholdFields"; import { Loader2, ChevronDown } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { TemplateType, templateTypeConfigs } from "@/services/templateService"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Textarea } from "@/components/ui/textarea"; interface TemplateDialogProps { open: boolean; templateId: string | null; + templateType: TemplateType | null; onOpenChange: (open: boolean) => void; onSuccess: () => void; } @@ -21,9 +28,12 @@ interface TemplateDialogProps { export const TemplateDialog: React.FC = ({ open, templateId, + templateType: initialTemplateType, onOpenChange, onSuccess, }) => { + const [selectedTemplateType, setSelectedTemplateType] = useState(initialTemplateType || 'service'); + const { form, isEditMode, @@ -32,28 +42,72 @@ export const TemplateDialog: React.FC = ({ onSubmit } = useTemplateForm({ templateId, + templateType: selectedTemplateType, open, onOpenChange, onSuccess }); - // For debugging purposes + // Update template type when prop changes or dialog opens useEffect(() => { - if (open) { - // console.log("Template dialog opened. Edit mode:", isEditMode, "Template ID:", templateId); - - // Log form values when they change - const subscription = form.watch((value) => { - // console.log("Current form values:", value); - }); - - return () => subscription.unsubscribe(); + if (initialTemplateType) { + setSelectedTemplateType(initialTemplateType); + } else if (open && !isEditMode) { + setSelectedTemplateType('service'); } - }, [open, isEditMode, templateId, form]); + }, [initialTemplateType, open, isEditMode]); + + // Handle template type change + const handleTemplateTypeChange = (newType: TemplateType) => { + if (!isEditMode) { + setSelectedTemplateType(newType); + form.setValue('templateType', newType); + } + }; + + const renderTemplateFields = () => { + switch (selectedTemplateType) { + case 'server': + return ; + case 'service': + return ; + case 'ssl': + return ; + case 'server_threshold': + return ; + default: + return null; + } + }; + + const renderPlaceholderGuide = () => { + const config = templateTypeConfigs[selectedTemplateType]; + if (!config) return null; + + return ( + + + Available Placeholders + + +

+ {config.description}. Use these placeholders in your messages: +

+
+ {config.placeholders.map((placeholder) => ( +
+ {placeholder} +
+ ))} +
+
+
+ ); + }; return ( - + {isEditMode ? "Edit Template" : "Add Template"} @@ -69,20 +123,85 @@ export const TemplateDialog: React.FC = ({
- + {/* Basic Fields */} +
+ ( + + Template Name + + + + + + )} + /> + + ( + + Template Type + + + + + + )} + /> + + {selectedTemplateType !== 'server_threshold' && ( + ( + + Custom Placeholder + + + + + + )} + /> + )} +
- Messages + {selectedTemplateType === 'server_threshold' ? 'Thresholds' : 'Messages'} Placeholders - + {renderTemplateFields()} - + {renderPlaceholderGuide()}
@@ -120,4 +239,4 @@ export const TemplateDialog: React.FC = ({
); -}; +}; \ No newline at end of file diff --git a/application/src/components/settings/alerts-templates/TemplateList.tsx b/application/src/components/settings/alerts-templates/TemplateList.tsx index e3c40d2..d820e96 100644 --- a/application/src/components/settings/alerts-templates/TemplateList.tsx +++ b/application/src/components/settings/alerts-templates/TemplateList.tsx @@ -1,17 +1,15 @@ import React, { useState } from "react"; -import { NotificationTemplate, templateService } from "@/services/templateService"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; import { Button } from "@/components/ui/button"; -import { Edit, Trash2 } from "lucide-react"; -import { +import { Badge } from "@/components/ui/badge"; +import { Trash2, Edit, MoreVertical } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, @@ -22,63 +20,75 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { useToast } from "@/hooks/use-toast"; -import { Badge } from "@/components/ui/badge"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { templateService, AnyTemplate, TemplateType } from "@/services/templateService"; interface TemplateListProps { - templates: NotificationTemplate[]; + templates: AnyTemplate[]; isLoading: boolean; onEdit: (id: string) => void; refetchTemplates: () => void; + templateType: TemplateType; } -export const TemplateList: React.FC = ({ - templates, - isLoading, - onEdit, +export const TemplateList: React.FC = ({ + templates, + isLoading, + onEdit, refetchTemplates, + templateType }) => { const { toast } = useToast(); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [templateToDelete, setTemplateToDelete] = useState(null); - const [isDeleting, setIsDeleting] = useState(false); + const queryClient = useQueryClient(); + const [deleteTemplateId, setDeleteTemplateId] = useState(null); - const handleDeletePrompt = (template: NotificationTemplate) => { - setTemplateToDelete(template); - setDeleteDialogOpen(true); - }; - - const handleDeleteTemplate = async () => { - if (!templateToDelete) return; - - setIsDeleting(true); - try { - await templateService.deleteTemplate(templateToDelete.id); + // Delete mutation + const deleteMutation = useMutation({ + mutationFn: (id: string) => templateService.deleteTemplate(id, templateType), + onSuccess: () => { toast({ title: "Template deleted", - description: `Template "${templateToDelete.name}" has been removed.`, + description: "The template has been deleted successfully.", }); + queryClient.invalidateQueries({ queryKey: ['notification_templates', templateType] }); refetchTemplates(); - } catch (error) { - console.error("Error deleting template:", error); + }, + onError: (error) => { + // console.error("Error deleting template:", error); toast({ title: "Error", description: "Failed to delete template. Please try again.", variant: "destructive", }); - } finally { - setIsDeleting(false); - setDeleteDialogOpen(false); - setTemplateToDelete(null); + }, + }); + + const handleDelete = (id: string) => { + setDeleteTemplateId(id); + }; + + const confirmDelete = () => { + if (deleteTemplateId) { + deleteMutation.mutate(deleteTemplateId); + setDeleteTemplateId(null); } }; if (isLoading) { return ( -
-
-
-
-
+
+ {[...Array(3)].map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+ ))}
); } @@ -86,83 +96,86 @@ export const TemplateList: React.FC = ({ if (templates.length === 0) { return (
-

No templates found

-

- Create your first notification template to get started. -

+

No templates found. Create your first template to get started.

); } + const getTemplateTypeLabel = (type: TemplateType) => { + switch (type) { + case 'server': return 'Server'; + case 'service': return 'Service'; + case 'ssl': return 'SSL'; + default: return 'Unknown'; + } + }; + return ( <> -
- - - - Name - Type - Created - Actions - - - - {templates.map((template) => ( - - {template.name} - - {template.type} - - - {new Date(template.created).toLocaleDateString()} - - -
- - -
-
-
- ))} -
-
+
+ {templates.map((template) => ( +
+
+
+

{template.name}

+ {getTemplateTypeLabel(templateType)} +
+

+ Created: {new Date(template.created).toLocaleDateString()} + {template.updated !== template.created && + ` • Updated: ${new Date(template.updated).toLocaleDateString()}` + } +

+
+
+ + + + + + + handleDelete(template.id)} + className="text-destructive focus:text-destructive" + > + + Delete + + + +
+
+ ))}
- + setDeleteTemplateId(null)}> - Delete Template + Are you sure? - Are you sure you want to delete the template "{templateToDelete?.name}"? This action cannot be undone. + This action cannot be undone. This will permanently delete the template. - Cancel + Cancel { - e.preventDefault(); - handleDeleteTemplate(); - }} - disabled={isDeleting} + onClick={confirmDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > - {isDeleting ? "Deleting..." : "Delete"} + Delete ); -}; +}; \ No newline at end of file diff --git a/application/src/components/settings/alerts-templates/form/PlaceholdersTabContent.tsx b/application/src/components/settings/alerts-templates/form/PlaceholdersTabContent.tsx index f54461b..26bbec1 100644 --- a/application/src/components/settings/alerts-templates/form/PlaceholdersTabContent.tsx +++ b/application/src/components/settings/alerts-templates/form/PlaceholdersTabContent.tsx @@ -71,15 +71,168 @@ export const PlaceholdersTabContent: React.FC = ({ ( - Threshold Placeholder + URL Placeholder - + - Used for threshold values in alerts + Used for service URL + + + + )} + /> + + ( + + Host/IP Placeholder + + + + + Used for service host or IP address + + + + )} + /> + + ( + + Service Type Placeholder + + + + + Used for service type (HTTP, PING, TCP, DNS) + + + + )} + /> + + ( + + Port Placeholder + + + + + Used for service port number + + + + )} + /> + + ( + + Domain Placeholder + + + + + Used for domain name (DNS services) + + + + )} + /> + + ( + + Region Name Placeholder + + + + + Used for regional agent name + + + + )} + /> + + ( + + Agent ID Placeholder + + + + + Used for regional agent ID + + + + )} + /> + + ( + + Uptime Placeholder + + + + + Used for service uptime percentage + + + + )} + /> + + ( + + Time Placeholder + + + + + Used for current date and time + + + + )} + /> + + ( + + Error Message Placeholder + + + + + Used for error details when service is down @@ -111,22 +264,50 @@ export const PlaceholdersTabContent: React.FC = ({ ${"{status}"}

Service status (UP, DOWN)

-
- ${"{threshold}"} -

Service threshold value

-
${"{url}"}

Service URL

+
+ ${"{host}"} +

Service host or IP address

+
+
+ ${"{service_type}"} +

Service type (HTTP, PING, TCP, DNS)

+
+
+ ${"{port}"} +

Service port number

+
+
+ ${"{domain}"} +

Domain name (DNS services)

+
+
+ ${"{region_name}"} +

Regional agent name

+
+
+ ${"{agent_id}"} +

Regional agent ID

+
+
+ ${"{uptime}"} +

Service uptime percentage

+
${"{time}"}

Current date and time

+
+ ${"{error_message}"} +

Error details when service is down

+
); -}; +}; \ No newline at end of file diff --git a/application/src/components/settings/alerts-templates/form/ServerTemplateFields.tsx b/application/src/components/settings/alerts-templates/form/ServerTemplateFields.tsx new file mode 100644 index 0000000..92a90e9 --- /dev/null +++ b/application/src/components/settings/alerts-templates/form/ServerTemplateFields.tsx @@ -0,0 +1,328 @@ + +import React from "react"; +import { FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "@/components/ui/form"; +import { Textarea } from "@/components/ui/textarea"; +import { Control } from "react-hook-form"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +interface ServerTemplateFieldsProps { + control: Control; +} + +export const ServerTemplateFields: React.FC = ({ control }) => { + return ( +
+ + + System Resource Messages + + +
+ ( + + CPU Alert Message + +