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,349 @@
import React, { useEffect } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { AlertConfiguration, alertConfigService } from "@/services/alertConfigService";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@/components/ui/input";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { Loader2 } from "lucide-react";
interface NotificationChannelDialogProps {
open: boolean;
onClose: (refreshList: boolean) => void;
editingConfig: AlertConfiguration | null;
}
const baseSchema = z.object({
notify_name: z.string().min(1, "Name is required"),
notification_type: z.enum(["telegram", "discord", "slack", "signal", "email"]),
enabled: z.boolean().default(true),
service_id: z.string().default("global"), // Assuming global for now, could be linked to specific services
template_id: z.string().optional(),
});
const telegramSchema = baseSchema.extend({
notification_type: z.literal("telegram"),
telegram_chat_id: z.string().min(1, "Chat ID is required"),
bot_token: z.string().min(1, "Bot token is required"),
});
const discordSchema = baseSchema.extend({
notification_type: z.literal("discord"),
discord_webhook_url: z.string().url("Must be a valid URL"),
});
const slackSchema = baseSchema.extend({
notification_type: z.literal("slack"),
slack_webhook_url: z.string().url("Must be a valid URL"),
});
const signalSchema = baseSchema.extend({
notification_type: z.literal("signal"),
signal_number: z.string().min(1, "Signal number is required"),
});
const emailSchema = baseSchema.extend({
notification_type: z.literal("email"),
// Email specific fields could be added here
});
const formSchema = z.discriminatedUnion("notification_type", [
telegramSchema,
discordSchema,
slackSchema,
signalSchema,
emailSchema
]);
type FormValues = z.infer<typeof formSchema>;
export const NotificationChannelDialog = ({
open,
onClose,
editingConfig
}: NotificationChannelDialogProps) => {
const isEditing = !!editingConfig;
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
notify_name: "",
notification_type: "telegram" as const,
enabled: true,
service_id: "global",
template_id: "",
},
});
const { watch, reset } = form;
const notificationType = watch("notification_type");
const [isSubmitting, setIsSubmitting] = React.useState(false);
useEffect(() => {
if (editingConfig) {
// Handle string vs boolean for enabled field
const enabled = typeof editingConfig.enabled === 'string'
? editingConfig.enabled === "true"
: !!editingConfig.enabled;
reset({
...editingConfig,
enabled
});
} else if (open) {
reset({
notify_name: "",
notification_type: "telegram" as const,
enabled: true,
service_id: "global",
template_id: "",
});
}
}, [editingConfig, open, reset]);
const handleClose = () => {
onClose(false);
};
const onSubmit = async (values: FormValues) => {
setIsSubmitting(true);
try {
// Ensure service_id is always present
const configData = {
...values,
service_id: values.service_id || "global",
};
if (isEditing && editingConfig?.id) {
await alertConfigService.updateAlertConfiguration(editingConfig.id, configData);
} else {
await alertConfigService.createAlertConfiguration(configData as any);
}
onClose(true); // Close with refresh
} finally {
setIsSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={() => handleClose()}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>
{isEditing ? "Edit Notification Channel" : "Add Notification Channel"}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="notify_name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="My Notification Channel" {...field} />
</FormControl>
<FormDescription>
A name to identify this notification channel
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notification_type"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Channel Type</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="telegram" />
</FormControl>
<FormLabel className="font-normal">
Telegram
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="discord" />
</FormControl>
<FormLabel className="font-normal">
Discord
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="slack" />
</FormControl>
<FormLabel className="font-normal">
Slack
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="signal" />
</FormControl>
<FormLabel className="font-normal">
Signal
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="email" />
</FormControl>
<FormLabel className="font-normal">
Email
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Show different fields based on notification type */}
{notificationType === "telegram" && (
<>
<FormField
control={form.control}
name="telegram_chat_id"
render={({ field }) => (
<FormItem>
<FormLabel>Chat ID</FormLabel>
<FormControl>
<Input placeholder="Telegram Chat ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bot_token"
render={({ field }) => (
<FormItem>
<FormLabel>Bot Token</FormLabel>
<FormControl>
<Input placeholder="Telegram Bot Token" {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{notificationType === "discord" && (
<FormField
control={form.control}
name="discord_webhook_url"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input placeholder="Discord Webhook URL" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{notificationType === "slack" && (
<FormField
control={form.control}
name="slack_webhook_url"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input placeholder="Slack Webhook URL" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{notificationType === "signal" && (
<FormField
control={form.control}
name="signal_number"
render={({ field }) => (
<FormItem>
<FormLabel>Signal Number</FormLabel>
<FormControl>
<Input placeholder="+1234567890" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>Enabled</FormLabel>
<FormDescription>
Turn notifications on or off
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<DialogFooter>
<Button variant="outline" type="button" onClick={handleClose}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isEditing ? "Update" : "Create"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,122 @@
import { AlertConfiguration } from "@/services/alertConfigService";
import { Bell, Edit, Trash2 } from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { alertConfigService } from "@/services/alertConfigService";
interface NotificationChannelListProps {
channels: AlertConfiguration[];
onEdit: (config: AlertConfiguration) => void;
onDelete: (id: string) => void;
}
export const NotificationChannelList = ({
channels,
onEdit,
onDelete
}: NotificationChannelListProps) => {
const toggleEnabled = async (config: AlertConfiguration) => {
if (!config.id) return;
await alertConfigService.updateAlertConfiguration(config.id, {
enabled: !config.enabled
});
// The parent component will refresh the list
onEdit(config);
};
const getChannelTypeLabel = (type: string) => {
switch(type) {
case "telegram": return "Telegram";
case "discord": return "Discord";
case "slack": return "Slack";
case "signal": return "Signal";
case "email": return "Email";
default: return type;
}
};
if (channels.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-10 text-center">
<Bell className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium">No notification channels configured</h3>
<p className="text-muted-foreground mt-2">
Add a notification channel to get alerts when your services go down.
</p>
</div>
);
}
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{channels.map((channel) => (
<TableRow key={channel.id}>
<TableCell className="font-medium">{channel.notify_name}</TableCell>
<TableCell>
<Badge variant="outline">{getChannelTypeLabel(channel.notification_type)}</Badge>
</TableCell>
<TableCell>
<Switch
checked={
typeof channel.enabled === 'string'
? channel.enabled === "true"
: !!channel.enabled
}
onCheckedChange={() => toggleEnabled(channel)}
/>
</TableCell>
<TableCell>
{channel.created ? new Date(channel.created).toLocaleDateString() : "-"}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onEdit(channel)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
if (channel.id && confirm("Are you sure you want to delete this notification channel?")) {
onDelete(channel.id)
}
}}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
@@ -0,0 +1,117 @@
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Plus, Loader2 } from "lucide-react";
import { AlertConfiguration, alertConfigService } from "@/services/alertConfigService";
import { NotificationChannelDialog } from "./NotificationChannelDialog";
import { NotificationChannelList } from "./NotificationChannelList";
const NotificationSettings = () => {
const [isLoading, setIsLoading] = useState(true);
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
const [dialogOpen, setDialogOpen] = useState(false);
const [currentTab, setCurrentTab] = useState<string>("all");
const [editingConfig, setEditingConfig] = useState<AlertConfiguration | null>(null);
const fetchAlertConfigurations = async () => {
setIsLoading(true);
try {
const configs = await alertConfigService.getAlertConfigurations();
setAlertConfigs(configs);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchAlertConfigurations();
}, []);
const handleAddNew = () => {
setEditingConfig(null);
setDialogOpen(true);
};
const handleEdit = (config: AlertConfiguration) => {
setEditingConfig(config);
setDialogOpen(true);
};
const handleDelete = async (id: string) => {
const success = await alertConfigService.deleteAlertConfiguration(id);
if (success) {
fetchAlertConfigurations();
}
};
const handleDialogClose = (refreshList: boolean) => {
setDialogOpen(false);
if (refreshList) {
fetchAlertConfigurations();
}
};
const getFilteredConfigs = () => {
if (currentTab === "all") return alertConfigs;
return alertConfigs.filter(config => config.notification_type === currentTab);
};
return (
<Card className="w-full">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Notification Settings</CardTitle>
<CardDescription>
Configure notification channels for your services
</CardDescription>
</div>
<Button onClick={handleAddNew}>
<Plus className="mr-2 h-4 w-4" /> Add Channel
</Button>
</div>
</CardHeader>
<CardContent>
<Tabs
defaultValue="all"
value={currentTab}
onValueChange={setCurrentTab}
className="w-full"
>
<TabsList className="mb-4">
<TabsTrigger value="all">All Channels</TabsTrigger>
<TabsTrigger value="telegram">Telegram</TabsTrigger>
<TabsTrigger value="discord">Discord</TabsTrigger>
<TabsTrigger value="slack">Slack</TabsTrigger>
<TabsTrigger value="signal">Signal</TabsTrigger>
<TabsTrigger value="email">Email</TabsTrigger>
</TabsList>
<TabsContent value={currentTab} className="mt-0">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : (
<NotificationChannelList
channels={getFilteredConfigs()}
onEdit={handleEdit}
onDelete={handleDelete}
/>
)}
</TabsContent>
</Tabs>
</CardContent>
<NotificationChannelDialog
open={dialogOpen}
onClose={handleDialogClose}
editingConfig={editingConfig}
/>
</Card>
);
};
export default NotificationSettings;
@@ -0,0 +1,4 @@
import NotificationSettings from "./NotificationSettings";
export { NotificationSettings };