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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user