Initial Release

This commit is contained in:
Tola Leng
2025-05-09 21:28:07 +07:00
commit 0c6cd18e6f
244 changed files with 50633 additions and 0 deletions
@@ -0,0 +1,89 @@
import React, { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { templateService } from "@/services/templateService";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Plus, RefreshCcw } from "lucide-react";
import { TemplateList } from "./TemplateList";
import { TemplateDialog } from "./TemplateDialog";
import { useToast } from "@/hooks/use-toast";
export const AlertsTemplates = () => {
const { toast } = useToast();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<string | null>(null);
const {
data: templates = [],
isLoading,
error,
refetch
} = useQuery({
queryKey: ['notification_templates'],
queryFn: templateService.getTemplates,
});
const handleAddTemplate = () => {
setEditingTemplate(null);
setIsDialogOpen(true);
};
const handleEditTemplate = (id: string) => {
setEditingTemplate(id);
setIsDialogOpen(true);
};
const handleRefresh = () => {
refetch();
toast({
title: "Refreshing",
description: "Updating template list...",
});
};
return (
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Alert Templates</CardTitle>
<div className="flex space-x-2">
<Button variant="outline" onClick={handleRefresh} disabled={isLoading}>
<RefreshCcw className="h-4 w-4 mr-2" />
Refresh
</Button>
<Button onClick={handleAddTemplate}>
<Plus className="h-4 w-4 mr-2" />
Add Template
</Button>
</div>
</CardHeader>
<CardContent>
{error ? (
<div className="text-center p-6">
<p className="text-destructive mb-4">Error loading templates</p>
<Button variant="outline" onClick={() => refetch()}>
Try Again
</Button>
</div>
) : (
<TemplateList
templates={templates}
isLoading={isLoading}
onEdit={handleEditTemplate}
refetchTemplates={refetch}
/>
)}
</CardContent>
<TemplateDialog
open={isDialogOpen}
templateId={editingTemplate}
onOpenChange={setIsDialogOpen}
onSuccess={() => {
refetch();
setIsDialogOpen(false);
}}
/>
</Card>
);
};
@@ -0,0 +1,123 @@
import React, { useEffect } 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 { 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 { Loader2, ChevronDown } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
interface TemplateDialogProps {
open: boolean;
templateId: string | null;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
export const TemplateDialog: React.FC<TemplateDialogProps> = ({
open,
templateId,
onOpenChange,
onSuccess,
}) => {
const {
form,
isEditMode,
isLoadingTemplate,
isSubmitting,
onSubmit
} = useTemplateForm({
templateId,
open,
onOpenChange,
onSuccess
});
// For debugging purposes
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();
}
}, [open, isEditMode, templateId, form]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col overflow-hidden">
<DialogHeader>
<DialogTitle>{isEditMode ? "Edit Template" : "Add Template"}</DialogTitle>
</DialogHeader>
{isLoadingTemplate ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<span className="ml-2">Loading template data...</span>
</div>
) : (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 flex-1 flex flex-col">
<div className="relative flex-1">
<ScrollArea className="pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}>
<div className="space-y-6 pb-6 pr-4">
<BasicTemplateFields control={form.control} />
<Tabs defaultValue="messages">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="messages">Messages</TabsTrigger>
<TabsTrigger value="placeholders">Placeholders</TabsTrigger>
</TabsList>
<TabsContent value="messages" className="pt-4">
<MessagesTabContent control={form.control} />
</TabsContent>
<TabsContent value="placeholders" className="pt-4">
<PlaceholdersTabContent control={form.control} />
</TabsContent>
</Tabs>
</div>
</ScrollArea>
<div className="absolute bottom-2 right-4 text-muted-foreground opacity-60">
<ChevronDown className="h-4 w-4 animate-bounce" />
</div>
</div>
<DialogFooter className="mt-2 pt-2 border-t border-border">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting || isLoadingTemplate}
className="relative"
>
{isSubmitting && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
{isSubmitting
? (isEditMode ? "Updating..." : "Creating...")
: (isEditMode ? "Update Template" : "Create Template")}
</Button>
</DialogFooter>
</form>
</Form>
)}
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,168 @@
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 {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useToast } from "@/hooks/use-toast";
import { Badge } from "@/components/ui/badge";
interface TemplateListProps {
templates: NotificationTemplate[];
isLoading: boolean;
onEdit: (id: string) => void;
refetchTemplates: () => void;
}
export const TemplateList: React.FC<TemplateListProps> = ({
templates,
isLoading,
onEdit,
refetchTemplates,
}) => {
const { toast } = useToast();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [templateToDelete, setTemplateToDelete] = useState<NotificationTemplate | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const handleDeletePrompt = (template: NotificationTemplate) => {
setTemplateToDelete(template);
setDeleteDialogOpen(true);
};
const handleDeleteTemplate = async () => {
if (!templateToDelete) return;
setIsDeleting(true);
try {
await templateService.deleteTemplate(templateToDelete.id);
toast({
title: "Template deleted",
description: `Template "${templateToDelete.name}" has been removed.`,
});
refetchTemplates();
} catch (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);
}
};
if (isLoading) {
return (
<div className="py-8 flex justify-center">
<div className="animate-pulse flex flex-col items-center">
<div className="h-4 bg-gray-200 rounded w-32 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-64"></div>
</div>
</div>
);
}
if (templates.length === 0) {
return (
<div className="text-center py-8">
<p className="text-muted-foreground mb-2">No templates found</p>
<p className="text-sm text-muted-foreground">
Create your first notification template to get started.
</p>
</div>
);
}
return (
<>
<div className="border rounded-md overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Created</TableHead>
<TableHead className="w-24">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{templates.map((template) => (
<TableRow key={template.id}>
<TableCell className="font-medium">{template.name}</TableCell>
<TableCell>
<Badge variant="outline">{template.type}</Badge>
</TableCell>
<TableCell>
{new Date(template.created).toLocaleDateString()}
</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button
size="sm"
variant="ghost"
onClick={() => onEdit(template.id)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDeletePrompt(template)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Template</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete the template "{templateToDelete?.name}"? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
handleDeleteTemplate();
}}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? "Deleting..." : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};
@@ -0,0 +1,61 @@
import React from "react";
import { 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 { Control } from "react-hook-form";
interface BasicTemplateFieldsProps {
control: Control<any>;
}
export const BasicTemplateFields: React.FC<BasicTemplateFieldsProps> = ({ control }) => {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Template Name</FormLabel>
<FormControl>
<Input
placeholder="Enter template name"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Template Type</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
value={field.value}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select template type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="service">Service</SelectItem>
<SelectItem value="incident">Incident</SelectItem>
<SelectItem value="maintenance">Maintenance</SelectItem>
<SelectItem value="custom">Custom</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
);
};
@@ -0,0 +1,124 @@
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 } from "@/components/ui/card";
interface MessagesTabContentProps {
control: Control<any>;
}
export const MessagesTabContent: React.FC<MessagesTabContentProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="space-y-4">
<FormField
control={control}
name="up_message"
render={({ field }) => (
<FormItem>
<FormLabel>Up Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} is UP. Response time: ${response_time}ms"
className="min-h-24"
{...field}
/>
</FormControl>
<FormDescription>
Message sent when a service returns to UP status
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="down_message"
render={({ field }) => (
<FormItem>
<FormLabel>Down Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} is DOWN. Status: ${status}"
className="min-h-24"
{...field}
/>
</FormControl>
<FormDescription>
Message sent when a service goes DOWN
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField
control={control}
name="maintenance_message"
render={({ field }) => (
<FormItem>
<FormLabel>Maintenance Message</FormLabel>
<FormControl>
<Textarea
placeholder="Service ${service_name} is under maintenance"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="incident_message"
render={({ field }) => (
<FormItem>
<FormLabel>Incident Message</FormLabel>
<FormControl>
<Textarea
placeholder="Warning: Service ${service_name} has an incident"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="resolved_message"
render={({ field }) => (
<FormItem>
<FormLabel>Resolved Message</FormLabel>
<FormControl>
<Textarea
placeholder="Issue with service ${service_name} has been resolved"
className="min-h-20"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,132 @@
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 PlaceholdersTabContentProps {
control: Control<any>;
}
export const PlaceholdersTabContent: React.FC<PlaceholdersTabContentProps> = ({ control }) => {
return (
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Template Placeholders</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="service_name_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Service Name Placeholder</FormLabel>
<FormControl>
<Input placeholder="${service_name}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for service name in messages
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="response_time_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Response Time Placeholder</FormLabel>
<FormControl>
<Input placeholder="${response_time}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for response time in milliseconds
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="status_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Status Placeholder</FormLabel>
<FormControl>
<Input placeholder="${status}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for service status (UP, DOWN, etc.)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="threshold_placeholder"
render={({ field }) => (
<FormItem>
<FormLabel>Threshold Placeholder</FormLabel>
<FormControl>
<Input placeholder="${threshold}" {...field} />
</FormControl>
<FormDescription className="text-xs">
Used for threshold values in alerts
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Placeholder Usage Guide</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-3">
These placeholders will be replaced with actual values when notifications are sent:
</p>
<div className="space-y-2 text-sm">
<div className="grid grid-cols-2 gap-2">
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{service_name}"}</code>
<p className="text-xs text-muted-foreground mt-1">The name of the service</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{response_time}"}</code>
<p className="text-xs text-muted-foreground mt-1">Response time in milliseconds</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{status}"}</code>
<p className="text-xs text-muted-foreground mt-1">Service status (UP, DOWN)</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{threshold}"}</code>
<p className="text-xs text-muted-foreground mt-1">Service threshold value</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{url}"}</code>
<p className="text-xs text-muted-foreground mt-1">Service URL</p>
</div>
<div className="bg-muted/30 p-2 rounded">
<code className="text-xs">${"{time}"}</code>
<p className="text-xs text-muted-foreground mt-1">Current date and time</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,4 @@
export * from './BasicTemplateFields';
export * from './MessagesTabContent';
export * from './PlaceholdersTabContent';
@@ -0,0 +1,2 @@
export * from './useTemplateForm';
@@ -0,0 +1,199 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { useToast } from "@/hooks/use-toast";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { templateService, CreateUpdateTemplateData } from "@/services/templateService";
import { useEffect } from "react";
// Template form schema
export const templateFormSchema = z.object({
name: z.string().min(2, "Name is required and must be at least 2 characters"),
type: z.string().min(1, "Type is required"),
up_message: z.string().min(1, "Up message is required"),
down_message: z.string().min(1, "Down message is required"),
maintenance_message: z.string().min(1, "Maintenance message is required"),
incident_message: z.string().min(1, "Incident message is required"),
resolved_message: z.string().min(1, "Resolved message is required"),
service_name_placeholder: z.string().min(1, "Service name placeholder is required"),
response_time_placeholder: z.string().min(1, "Response time placeholder is required"),
status_placeholder: z.string().min(1, "Status placeholder is required"),
threshold_placeholder: z.string().min(1, "Threshold placeholder is required"),
});
// Define the form data type from the schema
export type TemplateFormData = z.infer<typeof templateFormSchema>;
// Default form values
const defaultFormValues: TemplateFormData = {
name: "",
type: "default",
up_message: "Service ${service_name} is UP. Response time: ${response_time}ms",
down_message: "Service ${service_name} is DOWN. Status: ${status}",
maintenance_message: "Service ${service_name} is under maintenance",
incident_message: "Service ${service_name} has an incident",
resolved_message: "Service ${service_name} issue has been resolved",
service_name_placeholder: "${service_name}",
response_time_placeholder: "${response_time}",
status_placeholder: "${status}",
threshold_placeholder: "${threshold}",
};
export interface UseTemplateFormProps {
templateId: string | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
export const useTemplateForm = ({ templateId, open, onOpenChange, onSuccess }: UseTemplateFormProps) => {
const { toast } = useToast();
const queryClient = useQueryClient();
const isEditMode = !!templateId;
console.log("Template form initialized with templateId:", templateId, "isEditMode:", isEditMode);
const form = useForm<TemplateFormData>({
resolver: zodResolver(templateFormSchema),
defaultValues: defaultFormValues,
mode: "onChange"
});
// Query to fetch template data for editing
const { isLoading: isLoadingTemplate, data: templateData } = useQuery({
queryKey: ['template', templateId],
queryFn: () => {
if (!templateId) return null;
console.log("Fetching template data for ID:", templateId);
return templateService.getTemplate(templateId);
},
enabled: !!templateId && open,
});
// Set form values when template data is loaded
useEffect(() => {
if (templateData && open) {
console.log("Setting form values with template data:", templateData);
form.reset({
name: templateData.name || "",
type: templateData.type || "default",
up_message: templateData.up_message || "",
down_message: templateData.down_message || "",
maintenance_message: templateData.maintenance_message || "",
incident_message: templateData.incident_message || "",
resolved_message: templateData.resolved_message || "",
service_name_placeholder: templateData.service_name_placeholder || "${service_name}",
response_time_placeholder: templateData.response_time_placeholder || "${response_time}",
status_placeholder: templateData.status_placeholder || "${status}",
threshold_placeholder: templateData.threshold_placeholder || "${threshold}",
});
}
}, [templateData, open, form]);
// Handle form errors
useEffect(() => {
const subscription = form.formState.errors.root?.message &&
toast({
title: "Error",
description: form.formState.errors.root.message,
variant: "destructive",
});
return () => {
if (subscription) {
// Clean up if needed
}
};
}, [form.formState.errors.root, toast]);
// Create mutation
const createMutation = useMutation({
mutationFn: (data: CreateUpdateTemplateData) => templateService.createTemplate(data),
onSuccess: () => {
toast({
title: "Template created",
description: "Your notification template has been created successfully.",
});
queryClient.invalidateQueries({ queryKey: ['notification_templates'] });
onSuccess();
},
onError: (error) => {
console.error("Error creating template:", error);
toast({
title: "Error",
description: "Failed to create template. Please check your inputs and try again.",
variant: "destructive",
});
},
});
// Update mutation
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: CreateUpdateTemplateData }) =>
templateService.updateTemplate(id, data),
onSuccess: () => {
toast({
title: "Template updated",
description: "Your notification template has been updated successfully.",
});
queryClient.invalidateQueries({ queryKey: ['notification_templates'] });
onSuccess();
},
onError: (error) => {
console.error("Error updating template:", error);
toast({
title: "Error",
description: "Failed to update template. Please check your inputs and try again.",
variant: "destructive",
});
},
});
const isSubmitting = createMutation.isPending || updateMutation.isPending;
// Handle form submission
const onSubmit = (formData: TemplateFormData) => {
console.log("Submitting form data:", formData);
// Ensure all required fields are present
const completeData: CreateUpdateTemplateData = {
name: formData.name,
type: formData.type,
up_message: formData.up_message,
down_message: formData.down_message,
maintenance_message: formData.maintenance_message,
incident_message: formData.incident_message,
resolved_message: formData.resolved_message,
service_name_placeholder: formData.service_name_placeholder,
response_time_placeholder: formData.response_time_placeholder,
status_placeholder: formData.status_placeholder,
threshold_placeholder: formData.threshold_placeholder,
};
if (isEditMode && templateId) {
console.log("Updating template with ID:", templateId);
updateMutation.mutate({ id: templateId, data: completeData });
} else {
console.log("Creating new template");
createMutation.mutate(completeData);
}
};
// Reset form when dialog closes
useEffect(() => {
if (!open) {
console.log("Dialog closed, resetting form");
form.reset(defaultFormValues);
}
}, [open, form]);
return {
form,
isEditMode,
isLoadingTemplate,
isSubmitting,
onSubmit
};
};
@@ -0,0 +1,2 @@
export { AlertsTemplates } from "./AlertsTemplates";