diff --git a/application/src/components/ssl-domain/AddSSLCertificateForm.tsx b/application/src/components/ssl-domain/AddSSLCertificateForm.tsx
index 8314d36..3cb0a5e 100644
--- a/application/src/components/ssl-domain/AddSSLCertificateForm.tsx
+++ b/application/src/components/ssl-domain/AddSSLCertificateForm.tsx
@@ -1,3 +1,4 @@
+
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -18,7 +19,8 @@ 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().min(1, "Notification channel is required")
+ notification_channel: z.string().min(1, "Notification channel is required"),
+ check_interval: z.coerce.number().int().min(1).max(30).optional()
});
interface AddSSLCertificateFormProps {
@@ -42,7 +44,8 @@ export const AddSSLCertificateForm = ({
domain: "",
warning_threshold: 30,
expiry_threshold: 7,
- notification_channel: ""
+ notification_channel: "",
+ check_interval: 1
}
});
@@ -89,7 +92,8 @@ export const AddSSLCertificateForm = ({
domain: values.domain,
warning_threshold: values.warning_threshold,
expiry_threshold: values.expiry_threshold,
- notification_channel: values.notification_channel
+ notification_channel: values.notification_channel,
+ check_interval: values.check_interval
};
await onSubmit(certData);
@@ -117,34 +121,53 @@ export const AddSSLCertificateForm = ({
)}
/>
+
+ (
+
+ {t('warningThreshold')}
+
+
+
+
+ {t('getNotifiedExpiration')}
+
+
+
+ )}
+ />
+
+ (
+
+ {t('expiryThreshold')}
+
+
+
+
+ {t('getNotifiedCritical')}
+
+
+
+ )}
+ />
+
+
(
- {t('warningThreshold')}
+ Check Interval (Days)
-
+
- {t('getNotifiedExpiration')}
-
-
-
- )}
- />
-
- (
-
- {t('expiryThreshold')}
-
-
-
-
- {t('getNotifiedCritical')}
+ How often to check the SSL certificate (in days)
diff --git a/application/src/components/ssl-domain/EditSSLCertificateForm.tsx b/application/src/components/ssl-domain/EditSSLCertificateForm.tsx
index 1512ce4..0e1cad1 100644
--- a/application/src/components/ssl-domain/EditSSLCertificateForm.tsx
+++ b/application/src/components/ssl-domain/EditSSLCertificateForm.tsx
@@ -1,3 +1,4 @@
+
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -17,6 +18,7 @@ const formSchema = z.object({
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"),
+ check_interval: z.coerce.number().int().min(1).max(30).optional(),
});
type FormValues = z.infer;
@@ -40,6 +42,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
warning_threshold: certificate.warning_threshold,
expiry_threshold: certificate.expiry_threshold,
notification_channel: certificate.notification_channel,
+ check_interval: certificate.check_interval || 1,
},
});
@@ -78,7 +81,8 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
...data,
// Ensure values are correctly typed as numbers
warning_threshold: Number(data.warning_threshold),
- expiry_threshold: Number(data.expiry_threshold)
+ expiry_threshold: Number(data.expiry_threshold),
+ check_interval: data.check_interval ? Number(data.check_interval) : undefined
};
console.log("Submitting updated certificate:", updatedCertificate);
@@ -113,7 +117,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
)}
/>
-
+
)}
/>
+
+ (
+
+ Check Interval (Days)
+
+
+
+
+ How often to check
+
+
+
+ )}
+ />
);
-};
+};
\ No newline at end of file
diff --git a/application/src/components/ssl-domain/SSLCertificateActions.tsx b/application/src/components/ssl-domain/SSLCertificateActions.tsx
new file mode 100644
index 0000000..9a86843
--- /dev/null
+++ b/application/src/components/ssl-domain/SSLCertificateActions.tsx
@@ -0,0 +1,64 @@
+
+import React from "react";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Button } from "@/components/ui/button";
+import { MoreHorizontal, RefreshCw, Edit, Trash2 } from "lucide-react";
+import { SSLCertificate } from "@/types/ssl.types";
+import { triggerImmediateCheck } from "@/services/sslCertificateService";
+import { toast } from "sonner";
+import { useLanguage } from "@/contexts/LanguageContext";
+
+interface SSLCertificateActionsProps {
+ certificate: SSLCertificate;
+ onEdit: (certificate: SSLCertificate) => void;
+ onDelete: (certificate: SSLCertificate) => void;
+}
+
+export const SSLCertificateActions = ({
+ certificate,
+ onEdit,
+ onDelete
+}: SSLCertificateActionsProps) => {
+ const { t } = useLanguage();
+
+ const handleCheck = async () => {
+ try {
+ await triggerImmediateCheck(certificate.id);
+ } catch (error) {
+ console.error("Error triggering SSL check:", error);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ Check
+
+ onEdit(certificate)}>
+
+ {t('edit')}
+
+ onDelete(certificate)}
+ className="text-destructive"
+ >
+
+ {t('delete')}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/application/src/components/ssl-domain/SSLCertificatesTable.tsx b/application/src/components/ssl-domain/SSLCertificatesTable.tsx
index 43ec130..64825d8 100644
--- a/application/src/components/ssl-domain/SSLCertificatesTable.tsx
+++ b/application/src/components/ssl-domain/SSLCertificatesTable.tsx
@@ -1,362 +1,248 @@
+
import React, { useState } from "react";
-import { format } from "date-fns";
-import {
- Table,
- TableHeader,
- TableRow,
- TableHead,
- TableBody,
- TableCell
-} from "@/components/ui/table";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
-import { RefreshCw, Eye, Edit, Trash2, MoreHorizontal } from "lucide-react";
-import { SSLCertificate } from "@/types/ssl.types";
-import { SSLStatusBadge } from "./SSLStatusBadge";
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { toast } from "sonner";
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
+import { SSLStatusBadge } from "./SSLStatusBadge";
+import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
+import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
+import { SSLCertificateActions } from "./SSLCertificateActions";
+import { fetchSSLCertificates, addSSLCertificate, deleteSSLCertificate } from "@/services/sslCertificateService";
+import { pb } from "@/lib/pocketbase";
+import { SSLCertificate } from "@/types/ssl.types";
import { useLanguage } from "@/contexts/LanguageContext";
+import { toast } from "sonner";
-interface SSLCertificatesTableProps {
- certificates: SSLCertificate[];
- onRefresh: (id: string) => void;
- refreshingId: string | null;
- onEdit?: (certificate: SSLCertificate) => void;
- onDelete?: (certificate: SSLCertificate) => void;
-}
-
-export const SSLCertificatesTable = ({
- certificates,
- onRefresh,
- refreshingId,
- onEdit,
- onDelete
-}: SSLCertificatesTableProps) => {
+export const SSLCertificatesTable = () => {
const { t } = useLanguage();
- const [selectedCert, setSelectedCert] = useState(null);
- const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
- const [certToDelete, setCertToDelete] = useState(null);
-
- const formatDate = (dateString: string | undefined) => {
- if (!dateString) return t('unknown');
-
+ const queryClient = useQueryClient();
+ const [showAddDialog, setShowAddDialog] = useState(false);
+ const [showEditDialog, setShowEditDialog] = useState(false);
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
+ const [selectedCertificate, setSelectedCertificate] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const { data: certificates = [], isLoading, isError } = useQuery({
+ queryKey: ['ssl-certificates'],
+ queryFn: fetchSSLCertificates,
+ });
+
+ if (isLoading) return Loading...
;
+ if (isError) return Error loading certificates
;
+
+ const handleAddCertificate = async (data: any) => {
+ setIsSubmitting(true);
try {
- const date = new Date(dateString);
- if (isNaN(date.getTime())) {
- console.warn("Invalid date for formatting:", dateString);
- return t('unknown');
- }
- return format(date, "MMM dd, yyyy");
+ await addSSLCertificate(data);
+ await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
+ setShowAddDialog(false);
+ toast.success(t('certificateAdded'));
} catch (error) {
- console.error("Error formatting date:", error);
- return t('unknown');
+ console.error("Error adding certificate:", error);
+ toast.error(t('failedToAddCertificate'));
+ } finally {
+ setIsSubmitting(false);
}
};
- const handleViewCertificate = (certificate: SSLCertificate) => {
- setSelectedCert(certificate);
- };
-
- const handleEditCertificate = (certificate: SSLCertificate) => {
- if (onEdit) {
- onEdit(certificate);
- } else {
- toast.error("Edit functionality not implemented yet");
+ const handleEditCertificate = async (updatedCertificate: SSLCertificate) => {
+ setIsSubmitting(true);
+ try {
+ await pb.collection('ssl_certificates').update(updatedCertificate.id, {
+ warning_threshold: updatedCertificate.warning_threshold,
+ expiry_threshold: updatedCertificate.expiry_threshold,
+ notification_channel: updatedCertificate.notification_channel,
+ check_interval: updatedCertificate.check_interval,
+ });
+
+ await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
+ setShowEditDialog(false);
+ setSelectedCertificate(null);
+ toast.success(t('certificateUpdated'));
+ } catch (error) {
+ console.error("Error updating certificate:", error);
+ toast.error(t('failedToUpdateCertificate'));
+ } finally {
+ setIsSubmitting(false);
}
};
- const handleDeleteCertificate = (certificate: SSLCertificate) => {
- setCertToDelete(certificate);
- setDeleteConfirmOpen(true);
+ const handleDeleteCertificate = async () => {
+ if (!selectedCertificate) return;
+
+ setIsSubmitting(true);
+ try {
+ await deleteSSLCertificate(selectedCertificate.id);
+ await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
+ setShowDeleteDialog(false);
+ setSelectedCertificate(null);
+ toast.success(t('certificateDeleted'));
+ } catch (error) {
+ console.error("Error deleting certificate:", error);
+ toast.error(t('failedToDeleteCertificate'));
+ } finally {
+ setIsSubmitting(false);
+ }
};
- const confirmDelete = () => {
- if (certToDelete && onDelete) {
- onDelete(certToDelete);
- setDeleteConfirmOpen(false);
- setCertToDelete(null);
- }
+ const openEditDialog = (certificate: SSLCertificate) => {
+ setSelectedCertificate(certificate);
+ setShowEditDialog(true);
+ };
+
+ const openDeleteDialog = (certificate: SSLCertificate) => {
+ setSelectedCertificate(certificate);
+ setShowDeleteDialog(true);
};
return (
<>
-
- {refreshingId && (
-
-
-
- {t('checkingSSLCertificate')}
-
-
- )}
-
-
-
- {t('domain')}
- {t('issuer')}
- {t('expirationDate')}
- {t('daysLeft')}
- {t('status')}
- {t('lastNotified')}
- {t('actions')}
-
-
-
- {certificates.length === 0 ? (
+
+
+ {t('sslCertificates')}
+
+
+
+
+
-
- {t('noSSLCertificates')}
-
+ {t('domain')}
+ {t('status')}
+ {t('issuer')}
+ {t('validUntil')}
+ {t('daysLeft')}
+ Check Interval
+
- ) : (
- certificates.map((certificate) => (
+
+
+ {certificates.map((certificate) => (
- {certificate.domain}
- {certificate.issuer_o || t('unknown')}
-
- {formatDate(certificate.valid_till)}
-
-
- {typeof certificate.days_left === 'number' ? certificate.days_left : t('unknown')}
+
+ {certificate.domain}
+ {certificate.issuer_o || certificate.issuer_cn || 'Unknown'}
- {certificate.last_notified
- ? formatDate(certificate.last_notified)
- : t('never')}
+ {certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'}
-
-
-
-
-
-
- handleViewCertificate(certificate)}
- className="cursor-pointer"
- >
- {t('view')}
-
- {
- if (refreshingId === null) {
- onRefresh(certificate.id);
- }
- }}
- disabled={refreshingId !== null}
- className="cursor-pointer"
- >
-
- {t('check')}
-
- handleEditCertificate(certificate)}
- className="cursor-pointer"
- >
- {t('edit')}
-
- handleDeleteCertificate(certificate)}
- className="cursor-pointer text-destructive focus:text-destructive"
- >
- {t('delete')}
-
-
-
+
+
+ {certificate.days_left} {t('days')}
+
+
+
+ {certificate.check_interval || 1} {t('days')}
+
+
+
- ))
- )}
-
-
-
+ ))}
+
+
- {/* SSL Certificate Details Dialog */}
-