diff --git a/application/public/upload/os/arch.png b/application/public/upload/os/arch.png new file mode 100644 index 0000000..8a69099 Binary files /dev/null and b/application/public/upload/os/arch.png differ 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/AddServerAgentDialog.tsx b/application/src/components/servers/AddServerAgentDialog.tsx index 755bb3d..65c1645 100644 --- a/application/src/components/servers/AddServerAgentDialog.tsx +++ b/application/src/components/servers/AddServerAgentDialog.tsx @@ -14,6 +14,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { getCurrentEndpoint } from "@/lib/pocketbase"; import { ServerAgentConfigForm } from "./ServerAgentConfigForm"; import { OneClickInstallTab } from "./OneClickInstallTab"; +import { DockerOneClickTab } from "./DockerOneClickTab"; import { ManualInstallTab } from "./ManualInstallTab"; interface AddServerAgentDialogProps { @@ -123,9 +124,10 @@ export const AddServerAgentDialog: React.FC = ({ - + Configure Agent One-Click Install + Docker One-Click Manual Installation @@ -150,6 +152,16 @@ export const AddServerAgentDialog: React.FC = ({ onDialogClose={handleDialogClose} /> + + + + void; +} + +export const DockerOneClickTab: React.FC = ({ + serverToken, + currentPocketBaseUrl, + formData, + serverId, + onDialogClose, +}) => { + const getDockerOneClickCommand = () => { + const scriptUrl = "https://cdn.checkcle.io/scripts/server-docker-agent.sh"; + + return `curl -L -o server-docker-agent.sh "${scriptUrl}" +chmod +x server-docker-agent.sh +SERVER_TOKEN="${serverToken}" \\ +POCKETBASE_URL="${currentPocketBaseUrl}" \\ +SERVER_NAME="${formData.serverName}" \\ +AGENT_ID="${serverId}" \\ +sudo -E bash ./server-docker-agent.sh`; + }; + + const getDirectDockerCommand = () => { + return `docker run -d \\ + --name monitoring-agent \\ + --restart unless-stopped \\ + -p 8081:8081 \\ + --group-add 999 \\ + -e AGENT_ID="${serverId}" \\ + -e SERVER_NAME="${formData.serverName}" \\ + -e SERVER_TOKEN="${serverToken}" \\ + -e POCKETBASE_URL="${currentPocketBaseUrl}" \\ + -e POCKETBASE_ENABLED=true \\ + -v /proc:/host/proc:ro \\ + -v /etc:/host/etc:ro \\ + -v /sys:/host/sys:ro \\ + -v /:/host/root:ro \\ + -v /var/run:/host/var/run:ro \\ + -v /dev:/host/dev:ro \\ + -v /var/run/docker.sock:/var/run/docker.sock:ro \\ + -v monitoring_data:/var/lib/monitoring-agent \\ + -v monitoring_logs:/var/log/monitoring-agent \\ + operacle/checkcle-server-agent:latest`; + }; + + const handleCopyOneClickCommand = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + // console.log('Copy one-click command button clicked'); + const command = getDockerOneClickCommand(); + // 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'); + const command = getDirectDockerCommand(); + // console.log('Copying docker command:', command); + await copyToClipboard(command); + }; + + return ( +
+ {/* One-Click Docker Installation */} + + + + + Docker One-Click Install (Recommended) + + + Automated Docker container installation with system monitoring capabilities + + + +
+ +
+
+                {getDockerOneClickCommand()}
+              
+ +
+
+ +
+

This script will automatically:

+
    +
  1. Download and setup the Docker monitoring agent
  2. +
  3. Configure all required environment variables
  4. +
  5. Start the container with proper system access
  6. +
  7. Setup monitoring data persistence
  8. +
+
+
+
+ + {/* Direct Docker Run Command */} + + + + + Direct Docker Run Command + + + If you prefer to run the Docker container directly without the script + + + +
+ +
+
+                {getDirectDockerCommand()}
+              
+ +
+
+ +
+

Prerequisites for direct Docker run:

+
    +
  1. Docker must be installed and running
  2. +
  3. The operacle/checkcle-server-agent image must be available
  4. +
  5. Run as root or with sudo privileges
  6. +
+
+
+
+ +
+ +
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/servers/EditServerDialog.tsx b/application/src/components/servers/EditServerDialog.tsx index cce5755..3db001e 100644 --- a/application/src/components/servers/EditServerDialog.tsx +++ b/application/src/components/servers/EditServerDialog.tsx @@ -1,3 +1,4 @@ + import React, { useState, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; @@ -12,7 +13,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 { @@ -66,9 +67,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 +79,7 @@ export const EditServerDialog: React.FC = ({ // Initialize form data when server changes useEffect(() => { if (server) { - // console.log("Setting form data for server:", 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) @@ -180,8 +181,9 @@ 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({ @@ -621,27 +623,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"}

diff --git a/application/src/components/servers/ManualInstallTab.tsx b/application/src/components/servers/ManualInstallTab.tsx index 9d289a8..757e5a0 100644 --- a/application/src/components/servers/ManualInstallTab.tsx +++ b/application/src/components/servers/ManualInstallTab.tsx @@ -25,7 +25,7 @@ export const ManualInstallTab: React.FC = ({ onDialogClose, }) => { const getManualInstallSteps = () => { - const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh"; + const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh"; return [ { diff --git a/application/src/components/servers/OneClickInstallTab.tsx b/application/src/components/servers/OneClickInstallTab.tsx index 1067d20..24e5f6f 100644 --- a/application/src/components/servers/OneClickInstallTab.tsx +++ b/application/src/components/servers/OneClickInstallTab.tsx @@ -27,7 +27,7 @@ export const OneClickInstallTab: React.FC = ({ onDialogClose, }) => { const getOneClickInstallCommand = () => { - const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh"; + const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh"; return `curl -L -o server-agent.sh "${scriptUrl}" chmod +x server-agent.sh 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/settings/alerts-templates/AlertsTemplates.tsx b/application/src/components/settings/alerts-templates/AlertsTemplates.tsx index b8226d4..de52b33 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,83 @@ export const AlertsTemplates = () => { Refresh - - {error ? ( -
-

Error loading templates

- -
- ) : ( - - )} + setActiveTab(value as TemplateType)}> + + Service Uptime + Server Monitoring + SSL Certificate + + + + {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" + /> + )} +
+
{ refetch(); @@ -86,4 +141,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..a1afeb7 100644 --- a/application/src/components/settings/alerts-templates/TemplateDialog.tsx +++ b/application/src/components/settings/alerts-templates/TemplateDialog.tsx @@ -1,19 +1,25 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { 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 { 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 +27,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 +41,70 @@ 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 ; + 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,7 +120,69 @@ export const TemplateDialog: React.FC = ({
- + {/* Basic Fields */} +
+ ( + + Template Name + + + + + + )} + /> + + ( + + Template Type + + + + + + )} + /> + + ( + + Custom Placeholder + + + + + + )} + /> +
@@ -78,11 +191,11 @@ export const TemplateDialog: React.FC = ({ - + {renderTemplateFields()} - + {renderPlaceholderGuide()}
@@ -120,4 +233,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/ServerTemplateFields.tsx b/application/src/components/settings/alerts-templates/form/ServerTemplateFields.tsx new file mode 100644 index 0000000..259a2e4 --- /dev/null +++ b/application/src/components/settings/alerts-templates/form/ServerTemplateFields.tsx @@ -0,0 +1,211 @@ + +import React from "react"; +import { FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "@/components/ui/form"; +import { Textarea } from "@/components/ui/textarea"; +import { Control } from "react-hook-form"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +interface ServerTemplateFieldsProps { + control: Control; +} + +export const ServerTemplateFields: React.FC = ({ control }) => { + return ( +
+ + + System Resource Messages + + +
+ ( + + CPU Alert Message + +