feat: Add Server Threshold Tab

- Add a new tab for "Server Threshold" to the Alert Templates dashboard, to allow users to manage server threshold configurations.
This commit is contained in:
Tola Leng
2025-08-09 20:06:40 +07:00
parent 34dccb763d
commit 92e5f3f074
5 changed files with 235 additions and 37 deletions
@@ -64,10 +64,11 @@ export const AlertsTemplates = () => {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TemplateType)}> <Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TemplateType)}>
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="service">Service Uptime</TabsTrigger> <TabsTrigger value="service">Service Uptime</TabsTrigger>
<TabsTrigger value="server">Server Monitoring</TabsTrigger> <TabsTrigger value="server">Server Monitoring</TabsTrigger>
<TabsTrigger value="ssl">SSL Certificate</TabsTrigger> <TabsTrigger value="ssl">SSL Certificate</TabsTrigger>
<TabsTrigger value="server_threshold">Server Threshold</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="service" className="mt-4"> <TabsContent value="service" className="mt-4">
@@ -126,6 +127,25 @@ export const AlertsTemplates = () => {
/> />
)} )}
</TabsContent> </TabsContent>
<TabsContent value="server_threshold" className="mt-4">
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading server threshold templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={(id) => handleEditTemplate(id, 'server_threshold')}
refetchTemplates={refetch}
templateType="server_threshold"
/>
)}
</TabsContent>
</Tabs> </Tabs>
</CardContent> </CardContent>
@@ -10,6 +10,7 @@ import { useTemplateForm } from "./hooks/useTemplateForm";
import { ServerTemplateFields } from "./form/ServerTemplateFields"; import { ServerTemplateFields } from "./form/ServerTemplateFields";
import { ServiceTemplateFields } from "./form/ServiceTemplateFields"; import { ServiceTemplateFields } from "./form/ServiceTemplateFields";
import { SslTemplateFields } from "./form/SslTemplateFields"; import { SslTemplateFields } from "./form/SslTemplateFields";
import { ServerThresholdFields } from "./form/ServerThresholdFields";
import { Loader2, ChevronDown } from "lucide-react"; import { Loader2, ChevronDown } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { TemplateType, templateTypeConfigs } from "@/services/templateService"; import { TemplateType, templateTypeConfigs } from "@/services/templateService";
@@ -72,6 +73,8 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
return <ServiceTemplateFields control={form.control} />; return <ServiceTemplateFields control={form.control} />;
case 'ssl': case 'ssl':
return <SslTemplateFields control={form.control} />; return <SslTemplateFields control={form.control} />;
case 'server_threshold':
return <ServerThresholdFields control={form.control} />;
default: default:
return null; return null;
} }
@@ -158,6 +161,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
<SelectItem value="server">Server Monitoring</SelectItem> <SelectItem value="server">Server Monitoring</SelectItem>
<SelectItem value="service">Service Uptime</SelectItem> <SelectItem value="service">Service Uptime</SelectItem>
<SelectItem value="ssl">SSL Certificate</SelectItem> <SelectItem value="ssl">SSL Certificate</SelectItem>
<SelectItem value="server_threshold">Server Threshold</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</FormControl> </FormControl>
@@ -166,6 +170,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
)} )}
/> />
{selectedTemplateType !== 'server_threshold' && (
<FormField <FormField
control={form.control} control={form.control}
name="placeholder" name="placeholder"
@@ -182,11 +187,12 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
</FormItem> </FormItem>
)} )}
/> />
)}
</div> </div>
<Tabs defaultValue="messages"> <Tabs defaultValue="messages">
<TabsList className="grid w-full grid-cols-2"> <TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="messages">Messages</TabsTrigger> <TabsTrigger value="messages">{selectedTemplateType === 'server_threshold' ? 'Thresholds' : 'Messages'}</TabsTrigger>
<TabsTrigger value="placeholders">Placeholders</TabsTrigger> <TabsTrigger value="placeholders">Placeholders</TabsTrigger>
</TabsList> </TabsList>
@@ -0,0 +1,121 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Control } from "react-hook-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface ServerThresholdFieldsProps {
control: Control<any>;
}
export const ServerThresholdFields: React.FC<ServerThresholdFieldsProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Server Resource Thresholds</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="cpu_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Threshold (%)</FormLabel>
<FormControl>
<Input
type="number"
min="0"
max="100"
placeholder="85"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormDescription>
CPU usage percentage that triggers an alert
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="ram_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>RAM Threshold (%)</FormLabel>
<FormControl>
<Input
type="number"
min="0"
max="100"
placeholder="80"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormDescription>
Memory usage percentage that triggers an alert
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="disk_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>Disk Threshold (%)</FormLabel>
<FormControl>
<Input
type="number"
min="0"
max="100"
placeholder="90"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormDescription>
Disk usage percentage that triggers an alert
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="network_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>Network Threshold (%)</FormLabel>
<FormControl>
<Input
type="number"
min="0"
max="100"
placeholder="75"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormDescription>
Network usage percentage that triggers an alert
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -10,7 +10,7 @@ import { useEffect } from "react";
// Base schema // Base schema
const baseSchema = { const baseSchema = {
name: z.string().min(2, "Name is required and must be at least 2 characters"), name: z.string().min(2, "Name is required and must be at least 2 characters"),
templateType: z.enum(['server', 'service', 'ssl'] as const), templateType: z.enum(['server', 'service', 'ssl', 'server_threshold'] as const),
placeholder: z.string().optional(), placeholder: z.string().optional(),
}; };
@@ -52,11 +52,24 @@ const sslTemplateSchema = z.object({
warning: z.string().min(1, "Warning message is required"), warning: z.string().min(1, "Warning message is required"),
}); });
// Server threshold schema
const serverThresholdSchema = z.object({
...baseSchema,
templateType: z.literal('server_threshold'),
cpu_threshold: z.number().min(0).max(100, "CPU threshold must be between 0-100"),
ram_threshold: z.number().min(0).max(100, "RAM threshold must be between 0-100"),
disk_threshold: z.number().min(0).max(100, "Disk threshold must be between 0-100"),
network_threshold: z.number().min(0).max(100, "Network threshold must be between 0-100"),
notification_id: z.string().optional(),
server_template_id: z.string().optional(),
});
// Combined schema // Combined schema
export const templateFormSchema = z.discriminatedUnion("templateType", [ export const templateFormSchema = z.discriminatedUnion("templateType", [
serverTemplateSchema, serverTemplateSchema,
serviceTemplateSchema, serviceTemplateSchema,
sslTemplateSchema, sslTemplateSchema,
serverThresholdSchema,
]); ]);
export type TemplateFormData = z.infer<typeof templateFormSchema>; export type TemplateFormData = z.infer<typeof templateFormSchema>;
@@ -108,6 +121,18 @@ const getDefaultValues = (templateType: TemplateType): TemplateFormData => {
warning: "Warning: SSL certificate for ${domain} requires attention", warning: "Warning: SSL certificate for ${domain} requires attention",
}; };
case 'server_threshold':
return {
...base,
templateType: 'server_threshold' as const,
cpu_threshold: 85,
ram_threshold: 80,
disk_threshold: 90,
network_threshold: 75,
notification_id: "",
server_template_id: "",
};
default: default:
throw new Error(`Unknown template type: ${templateType}`); throw new Error(`Unknown template type: ${templateType}`);
} }
@@ -126,8 +151,6 @@ export const useTemplateForm = ({ templateId, templateType, open, onOpenChange,
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isEditMode = !!templateId; const isEditMode = !!templateId;
// console.log("Template form initialized with templateId:", templateId, "templateType:", templateType, "isEditMode:", isEditMode);
const form = useForm<TemplateFormData>({ const form = useForm<TemplateFormData>({
resolver: zodResolver(templateFormSchema), resolver: zodResolver(templateFormSchema),
defaultValues: getDefaultValues(templateType), defaultValues: getDefaultValues(templateType),
@@ -139,7 +162,6 @@ export const useTemplateForm = ({ templateId, templateType, open, onOpenChange,
queryKey: ['template', templateId, templateType], queryKey: ['template', templateId, templateType],
queryFn: () => { queryFn: () => {
if (!templateId) return null; if (!templateId) return null;
// console.log("Fetching template data for ID:", templateId, "type:", templateType);
return templateService.getTemplate(templateId, templateType); return templateService.getTemplate(templateId, templateType);
}, },
enabled: !!templateId && open, enabled: !!templateId && open,
@@ -148,7 +170,6 @@ export const useTemplateForm = ({ templateId, templateType, open, onOpenChange,
// Set form values when template data is loaded // Set form values when template data is loaded
useEffect(() => { useEffect(() => {
if (templateData && open) { if (templateData && open) {
// console.log("Setting form values with template data:", templateData);
const formData: any = { const formData: any = {
name: templateData.name || "", name: templateData.name || "",
@@ -156,10 +177,23 @@ export const useTemplateForm = ({ templateId, templateType, open, onOpenChange,
placeholder: (templateData as any).placeholder || "", placeholder: (templateData as any).placeholder || "",
}; };
// Add template-specific fields // Add template-specific fields with proper type conversion for server_threshold
Object.keys(templateData).forEach(key => { Object.keys(templateData).forEach(key => {
if (!['id', 'collectionId', 'collectionName', 'created', 'updated', 'name'].includes(key)) { if (!['id', 'collectionId', 'collectionName', 'created', 'updated', 'name'].includes(key)) {
formData[key] = (templateData as any)[key] || ""; let value = (templateData as any)[key];
// Convert string values to numbers for server threshold fields
if (templateType === 'server_threshold' &&
['cpu_threshold', 'ram_threshold', 'disk_threshold', 'network_threshold'].includes(key)) {
value = typeof value === 'string' ? Number(value) : value;
// Ensure valid number, fallback to default if invalid
if (isNaN(value)) {
const defaults = getDefaultValues(templateType) as any;
value = defaults[key] || 0;
}
}
formData[key] = value || "";
} }
}); });
@@ -179,7 +213,6 @@ export const useTemplateForm = ({ templateId, templateType, open, onOpenChange,
onSuccess(); onSuccess();
}, },
onError: (error) => { onError: (error) => {
// console.error("Error creating template:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to create template. Please check your inputs and try again.", description: "Failed to create template. Please check your inputs and try again.",
@@ -201,7 +234,6 @@ export const useTemplateForm = ({ templateId, templateType, open, onOpenChange,
onSuccess(); onSuccess();
}, },
onError: (error) => { onError: (error) => {
// console.error("Error updating template:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to update template. Please check your inputs and try again.", description: "Failed to update template. Please check your inputs and try again.",
@@ -214,17 +246,14 @@ export const useTemplateForm = ({ templateId, templateType, open, onOpenChange,
// Handle form submission // Handle form submission
const onSubmit = (formData: TemplateFormData) => { const onSubmit = (formData: TemplateFormData) => {
// console.log("Submitting form data:", formData);
// Remove templateType from the data before sending to API // Remove templateType from the data before sending to API
const { templateType: _, ...templateDataWithoutType } = formData; const { templateType: _, ...templateDataWithoutType } = formData;
const completeData = templateDataWithoutType as AnyTemplateData; const completeData = templateDataWithoutType as AnyTemplateData;
if (isEditMode && templateId) { if (isEditMode && templateId) {
// console.log("Updating template with ID:", templateId);
updateMutation.mutate({ id: templateId, data: completeData }); updateMutation.mutate({ id: templateId, data: completeData });
} else { } else {
// console.log("Creating new template");
createMutation.mutate(completeData); createMutation.mutate(completeData);
} }
}; };
@@ -232,7 +261,6 @@ export const useTemplateForm = ({ templateId, templateType, open, onOpenChange,
// Reset form when dialog closes or template type changes // Reset form when dialog closes or template type changes
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
// console.log("Dialog closed, resetting form");
form.reset(getDefaultValues(templateType)); form.reset(getDefaultValues(templateType));
} }
}, [open, form, templateType]); }, [open, form, templateType]);
+28 -5
View File
@@ -1,3 +1,4 @@
import { pb } from "@/lib/pocketbase"; import { pb } from "@/lib/pocketbase";
import { import {
serverNotificationTemplateService, serverNotificationTemplateService,
@@ -14,15 +15,20 @@ import {
SslNotificationTemplate, SslNotificationTemplate,
CreateUpdateSslNotificationTemplateData CreateUpdateSslNotificationTemplateData
} from "./sslNotificationTemplateService"; } from "./sslNotificationTemplateService";
import {
serverThresholdService,
ServerThreshold,
CreateUpdateServerThresholdData
} from "./serverThresholdService";
export type TemplateType = 'server' | 'service' | 'ssl'; export type TemplateType = 'server' | 'service' | 'ssl' | 'server_threshold';
export type AnyTemplate = ServerNotificationTemplate | ServiceNotificationTemplate | SslNotificationTemplate; export type AnyTemplate = ServerNotificationTemplate | ServiceNotificationTemplate | SslNotificationTemplate | ServerThreshold;
export type AnyTemplateData = CreateUpdateServerNotificationTemplateData | CreateUpdateServiceNotificationTemplateData | CreateUpdateSslNotificationTemplateData; export type AnyTemplateData = CreateUpdateServerNotificationTemplateData | CreateUpdateServiceNotificationTemplateData | CreateUpdateSslNotificationTemplateData | CreateUpdateServerThresholdData;
// Export individual template types // Export individual template types
export type { ServerNotificationTemplate, ServiceNotificationTemplate, SslNotificationTemplate }; export type { ServerNotificationTemplate, ServiceNotificationTemplate, SslNotificationTemplate, ServerThreshold };
export type { CreateUpdateServerNotificationTemplateData, CreateUpdateServiceNotificationTemplateData, CreateUpdateSslNotificationTemplateData }; export type { CreateUpdateServerNotificationTemplateData, CreateUpdateServiceNotificationTemplateData, CreateUpdateSslNotificationTemplateData, CreateUpdateServerThresholdData };
export const templateService = { export const templateService = {
async getTemplates(type: TemplateType): Promise<AnyTemplate[]> { async getTemplates(type: TemplateType): Promise<AnyTemplate[]> {
@@ -33,6 +39,8 @@ export const templateService = {
return serviceNotificationTemplateService.getTemplates(); return serviceNotificationTemplateService.getTemplates();
case 'ssl': case 'ssl':
return sslNotificationTemplateService.getTemplates(); return sslNotificationTemplateService.getTemplates();
case 'server_threshold':
return serverThresholdService.getServerThresholds();
default: default:
throw new Error(`Unknown template type: ${type}`); throw new Error(`Unknown template type: ${type}`);
} }
@@ -46,6 +54,8 @@ export const templateService = {
return serviceNotificationTemplateService.getTemplate(id); return serviceNotificationTemplateService.getTemplate(id);
case 'ssl': case 'ssl':
return sslNotificationTemplateService.getTemplate(id); return sslNotificationTemplateService.getTemplate(id);
case 'server_threshold':
return serverThresholdService.getServerThreshold(id);
default: default:
throw new Error(`Unknown template type: ${type}`); throw new Error(`Unknown template type: ${type}`);
} }
@@ -59,6 +69,8 @@ export const templateService = {
return serviceNotificationTemplateService.createTemplate(data as CreateUpdateServiceNotificationTemplateData); return serviceNotificationTemplateService.createTemplate(data as CreateUpdateServiceNotificationTemplateData);
case 'ssl': case 'ssl':
return sslNotificationTemplateService.createTemplate(data as CreateUpdateSslNotificationTemplateData); return sslNotificationTemplateService.createTemplate(data as CreateUpdateSslNotificationTemplateData);
case 'server_threshold':
return serverThresholdService.createServerThreshold(data as CreateUpdateServerThresholdData);
default: default:
throw new Error(`Unknown template type: ${type}`); throw new Error(`Unknown template type: ${type}`);
} }
@@ -72,6 +84,8 @@ export const templateService = {
return serviceNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateServiceNotificationTemplateData>); return serviceNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateServiceNotificationTemplateData>);
case 'ssl': case 'ssl':
return sslNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateSslNotificationTemplateData>); return sslNotificationTemplateService.updateTemplate(id, data as Partial<CreateUpdateSslNotificationTemplateData>);
case 'server_threshold':
return serverThresholdService.updateServerThreshold(id, data as Partial<CreateUpdateServerThresholdData>);
default: default:
throw new Error(`Unknown template type: ${type}`); throw new Error(`Unknown template type: ${type}`);
} }
@@ -85,6 +99,8 @@ export const templateService = {
return serviceNotificationTemplateService.deleteTemplate(id); return serviceNotificationTemplateService.deleteTemplate(id);
case 'ssl': case 'ssl':
return sslNotificationTemplateService.deleteTemplate(id); return sslNotificationTemplateService.deleteTemplate(id);
case 'server_threshold':
return serverThresholdService.deleteServerThreshold(id);
default: default:
throw new Error(`Unknown template type: ${type}`); throw new Error(`Unknown template type: ${type}`);
} }
@@ -116,5 +132,12 @@ export const templateTypeConfigs = {
'${domain}', '${certificate_name}', '${expiry_date}', '${days_left}', '${domain}', '${certificate_name}', '${expiry_date}', '${days_left}',
'${issuer}', '${serial_number}', '${time}' '${issuer}', '${serial_number}', '${time}'
] ]
},
server_threshold: {
label: 'Server Threshold',
description: 'Templates for server resource threshold configurations',
placeholders: [
'${name}', '${cpu_threshold}', '${ram_threshold}', '${disk_threshold}', '${network_threshold}'
]
} }
}; };