feat: Add check interval and check_at update

- Added a "Check Interval" field to the SSL certificate create/edit forms.
- Implemented functionality to update the `check_at` field in Pocketbase with the current time when the "Check" button is clicked.
This commit is contained in:
Tola Leng
2025-06-20 17:17:34 +07:00
parent 11408dcba0
commit 614c10188d
10 changed files with 390 additions and 357 deletions
@@ -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 = ({
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="warning_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>{t('warningThreshold')}</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormDescription>
{t('getNotifiedExpiration')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiry_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>{t('expiryThreshold')}</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormDescription>
{t('getNotifiedCritical')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="warning_threshold"
name="check_interval"
render={({ field }) => (
<FormItem>
<FormLabel>{t('warningThreshold')}</FormLabel>
<FormLabel>Check Interval (Days)</FormLabel>
<FormControl>
<Input type="number" {...field} />
<Input type="number" min="1" max="30" {...field} />
</FormControl>
<FormDescription>
{t('getNotifiedExpiration')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiry_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>{t('expiryThreshold')}</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormDescription>
{t('getNotifiedCritical')}
How often to check the SSL certificate (in days)
</FormDescription>
<FormMessage />
</FormItem>
@@ -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<typeof formSchema>;
@@ -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
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField
control={form.control}
name="warning_threshold"
@@ -157,6 +161,29 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
</FormItem>
)}
/>
<FormField
control={form.control}
name="check_interval"
render={({ field }) => (
<FormItem>
<FormLabel>Check Interval (Days)</FormLabel>
<FormControl>
<Input
{...field}
type="number"
min="1"
max="30"
placeholder="1"
/>
</FormControl>
<FormDescription>
How often to check
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
@@ -230,4 +257,4 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
</form>
</Form>
);
};
};
@@ -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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleCheck}>
<RefreshCw className="mr-2 h-4 w-4" />
Check
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onEdit(certificate)}>
<Edit className="mr-2 h-4 w-4" />
{t('edit')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onDelete(certificate)}
className="text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
{t('delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -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<SSLCertificate | null>(null);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [certToDelete, setCertToDelete] = useState<SSLCertificate | null>(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<SSLCertificate | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const { data: certificates = [], isLoading, isError } = useQuery({
queryKey: ['ssl-certificates'],
queryFn: fetchSSLCertificates,
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error loading certificates</div>;
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 (
<>
<div className="rounded-md border relative">
{refreshingId && (
<div className="absolute inset-0 bg-background/50 flex items-center justify-center z-10">
<div className="bg-background p-4 rounded-md shadow flex items-center gap-2">
<RefreshCw className="h-5 w-5 animate-spin text-primary" />
<span>{t('checkingSSLCertificate')}</span>
</div>
</div>
)}
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('domain')}</TableHead>
<TableHead>{t('issuer')}</TableHead>
<TableHead>{t('expirationDate')}</TableHead>
<TableHead>{t('daysLeft')}</TableHead>
<TableHead>{t('status')}</TableHead>
<TableHead>{t('lastNotified')}</TableHead>
<TableHead className="text-right">{t('actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{certificates.length === 0 ? (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle>{t('sslCertificates')}</CardTitle>
<Button onClick={() => setShowAddDialog(true)}>
<Plus className="h-4 w-4 mr-2" />
{t('addCertificate')}
</Button>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableCell colSpan={7} className="text-center py-8">
{t('noSSLCertificates')}
</TableCell>
<TableHead>{t('domain')}</TableHead>
<TableHead>{t('status')}</TableHead>
<TableHead>{t('issuer')}</TableHead>
<TableHead>{t('validUntil')}</TableHead>
<TableHead>{t('daysLeft')}</TableHead>
<TableHead>Check Interval</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
) : (
certificates.map((certificate) => (
</TableHeader>
<TableBody>
{certificates.map((certificate) => (
<TableRow key={certificate.id}>
<TableCell className="font-medium">{certificate.domain}</TableCell>
<TableCell>{certificate.issuer_o || t('unknown')}</TableCell>
<TableCell>
{formatDate(certificate.valid_till)}
</TableCell>
<TableCell>
{typeof certificate.days_left === 'number' ? certificate.days_left : t('unknown')}
<TableCell className="font-medium">
{certificate.domain}
</TableCell>
<TableCell>
<SSLStatusBadge status={certificate.status} />
</TableCell>
<TableCell>{certificate.issuer_o || certificate.issuer_cn || 'Unknown'}</TableCell>
<TableCell>
{certificate.last_notified
? formatDate(certificate.last_notified)
: t('never')}
{certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'}
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="bg-background border border-border">
<DropdownMenuItem
onClick={() => handleViewCertificate(certificate)}
className="cursor-pointer"
>
<Eye className="mr-2 h-4 w-4" /> {t('view')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
if (refreshingId === null) {
onRefresh(certificate.id);
}
}}
disabled={refreshingId !== null}
className="cursor-pointer"
>
<RefreshCw className={`mr-2 h-4 w-4 ${refreshingId === certificate.id ? 'animate-spin text-primary' : ''}`} />
{t('check')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleEditCertificate(certificate)}
className="cursor-pointer"
>
<Edit className="mr-2 h-4 w-4" /> {t('edit')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteCertificate(certificate)}
className="cursor-pointer text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" /> {t('delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<TableCell>
<span className={certificate.days_left <= 7 ? 'text-red-600 font-semibold' : certificate.days_left <= 30 ? 'text-yellow-600 font-semibold' : 'text-green-600'}>
{certificate.days_left} {t('days')}
</span>
</TableCell>
<TableCell>
{certificate.check_interval || 1} {t('days')}
</TableCell>
<TableCell>
<SSLCertificateActions
certificate={certificate}
onEdit={openEditDialog}
onDelete={openDeleteDialog}
/>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
))}
</TableBody>
</Table>
{/* SSL Certificate Details Dialog */}
<Dialog open={!!selectedCert} onOpenChange={(open) => !open && setSelectedCert(null)}>
<DialogContent className="max-w-3xl p-0 gap-0 overflow-hidden">
<DialogHeader className="p-6 pb-2">
<DialogTitle className="text-xl">
{t('sslCertificateDetails')}
</DialogTitle>
<p className="text-sm text-muted-foreground mt-1">
{t('detailedInfo')} {selectedCert?.domain}
</p>
</DialogHeader>
{selectedCert && (
<div className="p-6 pt-2 space-y-4 max-h-[70vh] overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Basic Information */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('basicInformation')}</h3>
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('domain')}:</span>
<span>{selectedCert.domain}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('status')}:</span>
<span><SSLStatusBadge status={selectedCert.status} /></span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('issuer')}:</span>
<span>{selectedCert.issued_to || t('unknown')}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">IP:</span>
<span>{selectedCert.resolved_ip || t('unknown')}</span>
</div>
</div>
</div>
{/* Validity */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('validity')}</h3>
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('validFrom')}:</span>
<span>{selectedCert.valid_from ? formatDate(selectedCert.valid_from) : t('unknown')}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('validUntil')}:</span>
<span>{formatDate(selectedCert.valid_till)}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('daysLeft')}:</span>
<span>{selectedCert.days_left}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('validityDays')}:</span>
<span>{selectedCert.validity_days || t('unknown')}</span>
</div>
</div>
</div>
{/* Issuer */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('issuerInfo')}</h3>
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('organization')}:</span>
<span>{selectedCert.issuer_o || t('unknown')}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('commonName')}:</span>
<span>{selectedCert.issuer_cn || t('unknown')}</span>
</div>
</div>
</div>
{/* Technical Details */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('technicalDetails')}</h3>
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('serialNumber')}:</span>
<span>{selectedCert.serial_number || t('unknown')}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('algorithm')}:</span>
<span>{selectedCert.cert_alg || t('unknown')}</span>
</div>
</div>
</div>
</div>
{/* Subject Alternative Names */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('subjectAltNames')}</h3>
<div>
{selectedCert.cert_sans ? (
<p className="break-words">{selectedCert.cert_sans}</p>
) : (
<p>{t('none')}</p>
)}
</div>
</div>
{/* Monitoring Configuration */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('monitoringConfig')}</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div className="space-y-1">
<div className="text-muted-foreground">{t('warningThreshold')}:</div>
<div>{selectedCert.warning_threshold} {t('daysLeft').toLowerCase()}</div>
</div>
<div className="space-y-1">
<div className="text-muted-foreground">{t('expiryThreshold')}:</div>
<div>{selectedCert.expiry_threshold} {t('daysLeft').toLowerCase()}</div>
</div>
<div className="space-y-1">
<div className="text-muted-foreground">{t('notificationChannel')}:</div>
<div>{selectedCert.notification_channel}</div>
</div>
</div>
</div>
{/* Timestamps */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('recordInfo')}</h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<div className="text-muted-foreground">{t('created')}:</div>
<div>{selectedCert.created ? formatDate(selectedCert.created) : t('unknown')}</div>
</div>
<div className="space-y-1">
<div className="text-muted-foreground">{t('lastUpdated')}:</div>
<div>{selectedCert.updated ? formatDate(selectedCert.updated) : t('unknown')}</div>
</div>
<div className="space-y-1">
<div className="text-muted-foreground">{t('lastNotification')}:</div>
<div>{selectedCert.last_notified ? formatDate(selectedCert.last_notified) : t('never')}</div>
</div>
<div className="space-y-1">
<div className="text-muted-foreground">{t('collectionId')}:</div>
<div>{selectedCert.collectionId || t('unknown')}</div>
</div>
</div>
</div>
{certificates.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
{t('noCertificatesFound')}
</div>
)}
<DialogFooter className="p-6 pt-0 border-t">
<Button
variant="default"
onClick={() => setSelectedCert(null)}
className="w-full sm:w-auto"
>
{t('close')}
</Button>
</DialogFooter>
</CardContent>
</Card>
{/* Add Certificate Dialog */}
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('addSSLCertificate')}</DialogTitle>
<DialogDescription>
{t('addCertificateDescription')}
</DialogDescription>
</DialogHeader>
<AddSSLCertificateForm
onSubmit={handleAddCertificate}
onCancel={() => setShowAddDialog(false)}
isPending={isSubmitting}
/>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
<DialogContent>
{/* Edit Certificate Dialog */}
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{t('deleteSSLCertificate')}</DialogTitle>
<DialogTitle>{t('editSSLCertificate')}</DialogTitle>
<DialogDescription>
{t('editCertificateDescription')}
</DialogDescription>
</DialogHeader>
<div className="py-4">
<p>{t('deleteConfirmation')} <strong>{certToDelete?.domain}</strong>?</p>
<p className="text-sm text-muted-foreground mt-2">{t('deleteWarning')}</p>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteConfirmOpen(false)}>{t('cancel')}</Button>
<Button variant="destructive" onClick={confirmDelete}>{t('delete')}</Button>
</DialogFooter>
{selectedCertificate && (
<EditSSLCertificateForm
certificate={selectedCertificate}
onSubmit={handleEditCertificate}
onCancel={() => setShowEditDialog(false)}
isPending={isSubmitting}
/>
)}
</DialogContent>
</Dialog>
{/* Delete Certificate Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteCertificate')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteCertificateConfirmation')} {selectedCertificate?.domain}?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteCertificate}
disabled={isSubmitting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isSubmitting ? t('deleting') : t('delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};
@@ -1,3 +1,4 @@
import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
@@ -242,13 +243,7 @@ export const SSLDomainContent = () => {
<SSLCertificateStatusCards certificates={certificates} />
<div className="mt-6 flex-1 flex flex-col pb-6">
<SSLCertificatesTable
certificates={certificates}
onRefresh={handleRefreshCertificate}
refreshingId={refreshingId}
onEdit={handleEditCertificate}
onDelete={handleDeleteCertificate}
/>
<SSLCertificatesTable />
</div>
</div>
+2 -1
View File
@@ -13,7 +13,8 @@ export {
addSSLCertificate,
checkAndUpdateCertificate,
deleteSSLCertificate,
refreshAllCertificates
refreshAllCertificates,
triggerImmediateCheck
} from './sslCertificateOperations';
// SSL-specific notification service
@@ -30,6 +30,7 @@ export const addSSLCertificate = async (
warning_threshold: Number(certificateData.warning_threshold) || 30,
expiry_threshold: Number(certificateData.expiry_threshold) || 7,
notification_channel: certificateData.notification_channel || "",
check_interval: Number(certificateData.check_interval) || 1, // New field
};
// Save to database
@@ -71,6 +72,27 @@ export const checkAndUpdateCertificate = async (
}
};
/**
* Trigger immediate SSL check by setting check_at to current time
*/
export const triggerImmediateCheck = async (certificateId: string): Promise<void> => {
try {
const currentTime = new Date().toISOString();
// Update the check_at field to current time to trigger immediate check by Go service
await pb.collection("ssl_certificates").update(certificateId, {
check_at: currentTime
});
console.log(`Triggered immediate check for certificate ${certificateId} at ${currentTime}`);
toast.success("SSL check scheduled - certificate will be checked shortly");
} catch (error) {
console.error("Error triggering immediate SSL check:", error);
toast.error("Failed to schedule SSL check");
throw error;
}
};
/**
* Delete an SSL certificate from monitoring
*/
+9 -1
View File
@@ -1,10 +1,10 @@
// SSL Certificate DTO for adding new certificates
export interface AddSSLCertificateDto {
domain: string;
warning_threshold: number;
expiry_threshold: number;
notification_channel: string;
check_interval?: number; // New field for check interval in days
}
// SSL Certificate model
@@ -28,6 +28,14 @@ export interface SSLCertificate {
last_notified?: string;
created?: string;
updated?: string;
// New fields
check_interval?: number; // Check interval in days
check_at?: string; // Next check time
// Existing fields based on the provided structure
collectionId?: string;
collectionName?: string;
resolved_ip?: string;
issuer_cn?: string;
}
// SSL specific notification types
@@ -3,7 +3,9 @@
import {
fetchSSLCertificates,
addSSLCertificate,
checkAndUpdateCertificate
checkAndUpdateCertificate,
triggerImmediateCheck,
deleteSSLCertificate
} from './ssl';
import { determineSSLStatus } from './ssl/sslStatusUtils';
@@ -20,6 +22,8 @@ export {
fetchSSLCertificates,
addSSLCertificate,
checkAndUpdateCertificate,
triggerImmediateCheck,
deleteSSLCertificate,
checkAllCertificatesAndNotify,
checkCertificateAndNotify,
shouldRunDailyCheck
+5 -2
View File
@@ -1,4 +1,3 @@
export interface SSLCertificate {
id: string;
domain: string;
@@ -19,7 +18,10 @@ export interface SSLCertificate {
last_notified?: string;
created?: string;
updated?: string;
// New fields based on the provided structure
// New fields
check_interval?: number; // Check interval in days
check_at?: string; // Next check time
// Existing fields based on the provided structure
collectionId?: string;
collectionName?: string;
resolved_ip?: string;
@@ -31,4 +33,5 @@ export interface AddSSLCertificateDto {
warning_threshold: number;
expiry_threshold: number;
notification_channel: string;
check_interval?: number; // New field for check interval in days
}