diff --git a/application/src/components/ssl-domain/AddSSLCertificateForm.tsx b/application/src/components/ssl-domain/AddSSLCertificateForm.tsx index c78ff22..f13fd5d 100644 --- a/application/src/components/ssl-domain/AddSSLCertificateForm.tsx +++ b/application/src/components/ssl-domain/AddSSLCertificateForm.tsx @@ -1,24 +1,28 @@ + import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { toast } from "sonner"; -import { Bell } from "lucide-react"; +import { Bell, X } from "lucide-react"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { DialogFooter } from "@/components/ui/dialog"; +import { Badge } from "@/components/ui/badge"; import { AddSSLCertificateDto } from "@/types/ssl.types"; import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; +import { sslNotificationTemplateService, SslNotificationTemplate } from "@/services/sslNotificationTemplateService"; import { useLanguage } from "@/contexts/LanguageContext"; const formSchema = z.object({ domain: z.string().min(1, "Domain is required"), warning_threshold: z.coerce.number().int().min(1).max(365), expiry_threshold: z.coerce.number().int().min(1).max(30), - notification_channel: z.string().optional(), // Make it optional to allow empty string for "None" + notification_channels: z.array(z.string()).optional(), + alert_template: z.string().optional(), check_interval: z.coerce.number().int().min(1).max(30).optional() }); @@ -35,6 +39,7 @@ export const AddSSLCertificateForm = ({ }: AddSSLCertificateFormProps) => { const { t } = useLanguage(); const [alertConfigs, setAlertConfigs] = useState([]); + const [sslTemplates, setSslTemplates] = useState([]); const [isLoading, setIsLoading] = useState(false); const form = useForm>({ @@ -43,58 +48,78 @@ export const AddSSLCertificateForm = ({ domain: "", warning_threshold: 30, expiry_threshold: 7, - notification_channel: "none", + notification_channels: [], + alert_template: "none", check_interval: 1 } }); - // Fetch notification channels when form loads + // Fetch notification channels and SSL templates when form loads useEffect(() => { - const fetchNotificationChannels = async () => { + const fetchData = async () => { setIsLoading(true); try { + // Fetch notification channels const configs = await alertConfigService.getAlertConfigurations(); - // console.log("Fetched notification channels:", configs); - // Only include enabled channels const enabledConfigs = configs.filter(config => { - // Handle the possibility of enabled being a string if (typeof config.enabled === 'string') { return config.enabled === "true"; } - // Otherwise treat as boolean return config.enabled === true; }); setAlertConfigs(enabledConfigs); + + // Fetch SSL notification templates + const templates = await sslNotificationTemplateService.getTemplates(); + setSslTemplates(templates); } catch (error) { - // console.error("Error fetching notification channels:", error); toast.error(t('failedToLoadCertificates')); } finally { setIsLoading(false); } }; - fetchNotificationChannels(); - }, [form, t]); + fetchData(); + }, [t]); const handleSubmit = async (values: z.infer) => { try { - // Convert the form values to the required DTO format with required properties + // Convert the form values to the required DTO format const certData: AddSSLCertificateDto = { domain: values.domain, warning_threshold: values.warning_threshold, expiry_threshold: values.expiry_threshold, - notification_channel: values.notification_channel === "none" ? "" : (values.notification_channel || ""), // Convert "none" to empty string + notification_channel: values.notification_channels && values.notification_channels.length > 0 + ? values.notification_channels.join(',') + : '', + notification_id: values.notification_channels && values.notification_channels.length > 0 + ? values.notification_channels.join(',') + : '', + template_id: values.alert_template && values.alert_template !== 'none' ? values.alert_template : '', check_interval: values.check_interval }; await onSubmit(certData); form.reset(); } catch (error) { - // console.error("Error adding SSL certificate:", error); toast.error(t('failedToAddCertificate')); } }; + const handleAddNotificationChannel = (channelId: string) => { + const currentChannels = form.getValues("notification_channels") || []; + if (!currentChannels.includes(channelId)) { + form.setValue("notification_channels", [...currentChannels, channelId]); + } + }; + + const handleRemoveNotificationChannel = (channelId: string) => { + const currentChannels = form.getValues("notification_channels") || []; + form.setValue("notification_channels", currentChannels.filter(id => id !== channelId)); + }; + + const selectedChannels = form.watch("notification_channels") || []; + return (
@@ -167,31 +192,60 @@ export const AddSSLCertificateForm = ({ ( - {t('notificationChannel')} - + {t('notificationChannel')} (Multi-select) +
+ + + {/* Display selected channels */} + {selectedChannels.length > 0 && ( +
+ {selectedChannels.map((channelId) => { + const channel = alertConfigs.find(config => config.id === channelId); + return ( + + {channel ? `${channel.notify_name} (${channel.notification_type})` : channelId} + + + ); + })} +
+ )} +
{t('whereToSend')} @@ -200,6 +254,41 @@ export const AddSSLCertificateForm = ({
)} /> + + ( + + Alert Template + + + Template for SSL certificate alert messages + + + + )} + /> diff --git a/application/src/components/ssl-domain/EditSSLCertificateForm.tsx b/application/src/components/ssl-domain/EditSSLCertificateForm.tsx index a53a1a2..1888c07 100644 --- a/application/src/components/ssl-domain/EditSSLCertificateForm.tsx +++ b/application/src/components/ssl-domain/EditSSLCertificateForm.tsx @@ -1,4 +1,3 @@ - import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -7,17 +6,20 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Badge } from "@/components/ui/badge"; import { SSLCertificate } from "@/types/ssl.types"; -import { Loader2, Bell } from "lucide-react"; +import { Loader2, Bell, X } from "lucide-react"; import { toast } from "sonner"; import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; +import { sslNotificationTemplateService, SslNotificationTemplate } from "@/services/sslNotificationTemplateService"; import { useLanguage } from "@/contexts/LanguageContext"; const formSchema = z.object({ domain: z.string().min(1, "Domain is required"), warning_threshold: z.coerce.number().min(1, "Warning threshold must be at least 1 day"), expiry_threshold: z.coerce.number().min(1, "Expiry threshold must be at least 1 day"), - notification_channel: z.string().min(1, "Notification channel is required"), + notification_channels: z.array(z.string()).default([]), + alert_template: z.string().optional(), check_interval: z.coerce.number().int().min(1).max(30).optional(), }); @@ -33,6 +35,7 @@ interface EditSSLCertificateFormProps { export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPending }: EditSSLCertificateFormProps) => { const { t } = useLanguage(); const [alertConfigs, setAlertConfigs] = useState([]); + const [sslTemplates, setSslTemplates] = useState([]); const [isLoading, setIsLoading] = useState(false); const form = useForm({ @@ -41,58 +44,81 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend domain: certificate.domain, warning_threshold: certificate.warning_threshold, expiry_threshold: certificate.expiry_threshold, - notification_channel: certificate.notification_channel, + notification_channels: certificate.notification_channel ? certificate.notification_channel.split(',') : [], + alert_template: certificate.alert_template || "none", check_interval: certificate.check_interval || 1, }, }); - // Fetch notification channels when form loads + const notificationChannels = form.watch("notification_channels") || []; + + // Fetch notification channels and SSL templates when form loads useEffect(() => { - const fetchNotificationChannels = async () => { + const fetchData = async () => { setIsLoading(true); try { + // Fetch notification channels const configs = await alertConfigService.getAlertConfigurations(); - // console.log("Fetched notification channels:", configs); - // Only include enabled channels const enabledConfigs = configs.filter(config => { - // Handle the possibility of enabled being a string if (typeof config.enabled === 'string') { return config.enabled === "true"; } - // Otherwise treat as boolean return config.enabled === true; }); setAlertConfigs(enabledConfigs); + + // Fetch SSL notification templates + const templates = await sslNotificationTemplateService.getTemplates(); + setSslTemplates(templates); } catch (error) { - // console.error("Error fetching notification channels:", error); toast.error(t('failedToLoadCertificates')); } finally { setIsLoading(false); } }; - fetchNotificationChannels(); + fetchData(); }, [t]); + const handleChannelAdd = (channelId: string) => { + const currentChannels = form.getValues("notification_channels") || []; + if (!currentChannels.includes(channelId)) { + form.setValue("notification_channels", [...currentChannels, channelId]); + } + }; + + const handleChannelRemove = (channelId: string) => { + const currentChannels = form.getValues("notification_channels") || []; + form.setValue("notification_channels", currentChannels.filter(id => id !== channelId)); + }; + + const getSelectedChannelNames = () => { + return (notificationChannels || []).map(channelId => { + const config = alertConfigs.find(c => c.id === channelId); + return config ? `${config.notify_name} (${config.notification_type})` : channelId; + }); + }; + const handleSubmit = (data: FormValues) => { // Merge the updated values with the original certificate const updatedCertificate: SSLCertificate = { ...certificate, ...data, + // Save notification channels as comma-separated string for notification_channel field + notification_channel: data.notification_channels.length > 0 ? data.notification_channels.join(',') : '', + // Save notification channels as comma-separated string for notification_id field (for PocketBase) + notification_id: data.notification_channels.length > 0 ? data.notification_channels.join(',') : '', + // Save alert template as template_id field (for PocketBase) - handle "none" value + template_id: data.alert_template && data.alert_template !== 'none' ? data.alert_template : '', // Ensure values are correctly typed as numbers warning_threshold: Number(data.warning_threshold), expiry_threshold: Number(data.expiry_threshold), check_interval: data.check_interval ? Number(data.check_interval) : undefined }; - console.log("Submitting updated certificate:", updatedCertificate); onSubmit(updatedCertificate); }; - // For debugging - console.log("Certificate data:", certificate); - console.log("Form default values:", form.getValues()); - return ( @@ -106,7 +132,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend @@ -188,41 +214,52 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend ( {t('notificationChannel')} - - - - {alertConfigs.length > 0 ? ( - alertConfigs.map((config) => ( - - {config.notify_name} ({config.notification_type}) - - )) - ) : isLoading ? ( - {t('loadingChannels')} - ) : ( - <> - Email - Telegram - Slack - Webhook - None - - )} - - + + {alertConfigs + .filter(config => config.id && !notificationChannels?.includes(config.id)) + .map((config) => ( + + {config.notify_name} ({config.notification_type}) + + ))} + {alertConfigs.filter(config => config.id && !notificationChannels?.includes(config.id)).length === 0 && ( + No available channels + )} + + + {t('whereToSend')} @@ -232,6 +269,45 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend )} /> + ( + + Alert Template + + + + + Choose a template for SSL certificate alert messages + + + + )} + /> +