Implement the SSL & Domain Features (Domain List, Real-time information for Expiration Date, Issuer, and Days Left.)

This commit is contained in:
Tola Leng
2025-05-11 20:51:38 +08:00
parent 981bc972f0
commit 580dbc424c
10 changed files with 569 additions and 4 deletions
@@ -0,0 +1,61 @@
import React from "react";
import { Card } from "@/components/ui/card";
import { SSLCertificate } from "@/types/ssl.types";
interface SSLCertificateStatusCardsProps {
certificates: SSLCertificate[];
}
export const SSLCertificateStatusCards = ({ certificates }: SSLCertificateStatusCardsProps) => {
// Count certificates by status
const validCount = certificates.filter(cert => cert.status === 'valid').length;
const expiringCount = certificates.filter(cert => cert.status === 'expiring_soon').length;
const expiredCount = certificates.filter(cert => cert.status === 'expired').length;
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="p-6 flex items-center space-x-4">
<div className="bg-green-100 dark:bg-green-900/20 p-3 rounded-full">
<div className="h-12 w-12 rounded-full bg-green-500/20 flex items-center justify-center">
<div className="h-8 w-8 rounded-full bg-green-500 flex items-center justify-center text-white">
</div>
</div>
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">Valid Certificates</p>
<p className="text-3xl font-bold">{validCount}</p>
</div>
</Card>
<Card className="p-6 flex items-center space-x-4">
<div className="bg-yellow-100 dark:bg-yellow-900/20 p-3 rounded-full">
<div className="h-12 w-12 rounded-full bg-yellow-500/20 flex items-center justify-center">
<div className="h-8 w-8 rounded-full bg-yellow-500 flex items-center justify-center text-white">
!
</div>
</div>
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">Expiring Soon</p>
<p className="text-3xl font-bold">{expiringCount}</p>
</div>
</Card>
<Card className="p-6 flex items-center space-x-4">
<div className="bg-red-100 dark:bg-red-900/20 p-3 rounded-full">
<div className="h-12 w-12 rounded-full bg-red-500/20 flex items-center justify-center">
<div className="h-8 w-8 rounded-full bg-red-500 flex items-center justify-center text-white">
</div>
</div>
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">Expired</p>
<p className="text-3xl font-bold">{expiredCount}</p>
</div>
</Card>
</div>
);
};
@@ -0,0 +1,102 @@
import React from "react";
import { format } from "date-fns";
import {
Table,
TableHeader,
TableRow,
TableHead,
TableBody,
TableCell
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { RefreshCw } from "lucide-react";
import { SSLCertificate } from "@/types/ssl.types";
import { SSLStatusBadge } from "./SSLStatusBadge";
interface SSLCertificatesTableProps {
certificates: SSLCertificate[];
onRefresh: (id: string) => void;
refreshingId: string | null;
}
export const SSLCertificatesTable = ({ certificates, onRefresh, refreshingId }: SSLCertificatesTableProps) => {
const calculateDaysLeft = (expirationDate: string) => {
try {
const expDate = new Date(expirationDate);
const today = new Date();
const diffTime = expDate.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays > 0 ? diffDays : 0;
} catch (error) {
console.error("Error calculating days left:", error);
return 0;
}
};
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Domain</TableHead>
<TableHead>Issuer</TableHead>
<TableHead>Expiration Date</TableHead>
<TableHead>Days Left</TableHead>
<TableHead>Status</TableHead>
<TableHead>Last Notified</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{certificates.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8">
No SSL certificates found
</TableCell>
</TableRow>
) : (
certificates.map((certificate) => (
<TableRow key={certificate.id}>
<TableCell className="font-medium">{certificate.domain}</TableCell>
<TableCell>{certificate.issuer || 'Unknown'}</TableCell>
<TableCell>
{certificate.expiration_date ?
format(new Date(certificate.expiration_date), "MMM dd, yyyy") :
'Unknown'}
</TableCell>
<TableCell>
{certificate.expiration_date ?
calculateDaysLeft(certificate.expiration_date) :
'Unknown'}
</TableCell>
<TableCell>
<SSLStatusBadge status={certificate.status} />
</TableCell>
<TableCell>
{certificate.last_notified
? format(new Date(certificate.last_notified), "MMM dd, yyyy")
: "Never"}
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => onRefresh(certificate.id)}
disabled={refreshingId === certificate.id}
>
<RefreshCw className={`h-4 w-4 mr-1 ${refreshingId === certificate.id ? 'animate-spin' : ''}`} />
Check
</Button>
<Button variant="outline" size="sm">View</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
);
};
@@ -0,0 +1,113 @@
import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Plus, RefreshCw } from "lucide-react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { toast } from "sonner";
import { SSLCertificateStatusCards } from "./SSLCertificateStatusCards";
import { SSLCertificatesTable } from "./SSLCertificatesTable";
import { LoadingState } from "@/components/services/LoadingState";
import { fetchSSLCertificates, addSSLCertificate, checkAndUpdateCertificate } from "@/services/sslCertificateService";
import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
import { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types";
export const SSLDomainContent = () => {
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [refreshingId, setRefreshingId] = useState<string | null>(null);
const queryClient = useQueryClient();
const { data: certificates = [], isLoading, error } = useQuery({
queryKey: ['ssl-certificates'],
queryFn: fetchSSLCertificates,
});
const addMutation = useMutation({
mutationFn: addSSLCertificate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setIsAddDialogOpen(false);
toast.success("SSL certificate added successfully");
},
onError: (error) => {
console.error("Error adding SSL certificate:", error);
toast.error(error instanceof Error ? error.message : "Failed to add SSL certificate");
}
});
const refreshMutation = useMutation({
mutationFn: checkAndUpdateCertificate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setRefreshingId(null);
toast.success("SSL certificate checked and updated successfully");
},
onError: (error) => {
console.error("Error refreshing SSL certificate:", error);
toast.error(error instanceof Error ? error.message : "Failed to refresh SSL certificate");
setRefreshingId(null);
}
});
const handleAddCertificate = async (data: AddSSLCertificateDto) => {
addMutation.mutate(data);
};
const handleRefreshCertificate = (id: string) => {
setRefreshingId(id);
refreshMutation.mutate(id);
};
if (isLoading) {
return <LoadingState />;
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
<p>Error loading SSL certificate data.</p>
<Button onClick={() => window.location.reload()}>Retry</Button>
</div>
);
}
return (
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
<div className="flex flex-col flex-1">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-foreground">SSL & Domain Management</h2>
<Button
className="text-primary-foreground"
onClick={() => setIsAddDialogOpen(true)}
>
<Plus className="w-4 h-4 mr-2" /> Add Domain
</Button>
</div>
<SSLCertificateStatusCards certificates={certificates} />
<div className="mt-6 flex-1 flex flex-col pb-6">
<SSLCertificatesTable
certificates={certificates}
onRefresh={handleRefreshCertificate}
refreshingId={refreshingId}
/>
</div>
</div>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add SSL Certificate</DialogTitle>
</DialogHeader>
<AddSSLCertificateForm
onSubmit={handleAddCertificate}
onCancel={() => setIsAddDialogOpen(false)}
isPending={addMutation.isPending}
/>
</DialogContent>
</Dialog>
</main>
);
};
@@ -0,0 +1,36 @@
import React from "react";
import { Badge } from "@/components/ui/badge";
interface SSLStatusBadgeProps {
status: string;
}
export const SSLStatusBadge: React.FC<SSLStatusBadgeProps> = ({ status }) => {
let variant = "";
let label = "";
switch (status) {
case "valid":
variant = "bg-green-500 hover:bg-green-600";
label = "Valid";
break;
case "expiring_soon":
variant = "bg-yellow-500 hover:bg-yellow-600";
label = "Expiring Soon";
break;
case "expired":
variant = "bg-red-500 hover:bg-red-600";
label = "Expired";
break;
default:
variant = "bg-gray-500 hover:bg-gray-600";
label = status.charAt(0).toUpperCase() + status.slice(1);
}
return (
<Badge className={`${variant} text-white`}>
{label}
</Badge>
);
};