feat: Implement dropdown actions in SSL table

Action column in the SSL & Domain table to use a dropdown menu for View, Check, Edit, and Delete actions. And updated SSL Dashboard UI Component
This commit is contained in:
Tola Leng
2025-05-16 22:20:57 +08:00
parent 8ee933563e
commit 1ea81a23b2
4 changed files with 483 additions and 44 deletions
@@ -1,4 +1,3 @@
import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
@@ -9,20 +8,40 @@ 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 { fetchSSLCertificates, addSSLCertificate, checkAndUpdateCertificate, refreshAllCertificates, deleteSSLCertificate } from "@/services/ssl";
import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
import { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types";
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
import type { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types";
import { pb } from "@/lib/pocketbase";
export const SSLDomainContent = () => {
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [refreshingId, setRefreshingId] = useState<string | null>(null);
const [isRefreshingAll, setIsRefreshingAll] = useState(false);
const [selectedCertificate, setSelectedCertificate] = useState<SSLCertificate | null>(null);
const queryClient = useQueryClient();
// Fetch SSL certificates with explicit error handling
const { data: certificates = [], isLoading, error } = useQuery({
queryKey: ['ssl-certificates'],
queryFn: fetchSSLCertificates,
queryFn: async () => {
try {
console.log("Fetching SSL certificates from SSLDomainContent...");
const result = await fetchSSLCertificates();
console.log("Received SSL certificates:", result);
return result;
} catch (error) {
console.error("Error fetching certificates:", error);
toast.error("Failed to load SSL certificates");
throw error;
}
},
refetchOnWindowFocus: false,
refetchInterval: 300000, // Refresh every 5 minutes
});
// Add certificate mutation
const addMutation = useMutation({
mutationFn: addSSLCertificate,
onSuccess: () => {
@@ -32,21 +51,105 @@ export const SSLDomainContent = () => {
},
onError: (error) => {
console.error("Error adding SSL certificate:", error);
toast.error(error instanceof Error ? error.message : "Failed to add SSL certificate");
toast.error(error instanceof Error ? error.message : "Failed to add SSL certificate. Make sure the domain is valid and accessible.");
}
});
const refreshMutation = useMutation({
mutationFn: checkAndUpdateCertificate,
// Edit certificate mutation - Updated to ensure thresholds are properly updated
const editMutation = useMutation({
mutationFn: async (certificate: SSLCertificate) => {
console.log("Updating certificate with data:", certificate);
// Create the update data object
const updateData = {
warning_threshold: Number(certificate.warning_threshold),
expiry_threshold: Number(certificate.expiry_threshold),
notification_channel: certificate.notification_channel,
};
console.log("Update data to be sent:", updateData);
// Update certificate in the database using PocketBase directly
const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData);
console.log("PocketBase update response:", updated);
// After updating the settings, refresh the certificate to ensure it's up to date
// This will also check if notification needs to be sent based on updated thresholds
const refreshedCert = await checkAndUpdateCertificate(certificate.id);
return refreshedCert;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setIsEditDialogOpen(false);
setSelectedCertificate(null);
toast.success("SSL certificate updated successfully");
},
onError: (error) => {
console.error("Error updating SSL certificate:", error);
toast.error(error instanceof Error ? error.message : "Failed to update SSL certificate");
}
});
// Delete certificate mutation
const deleteMutation = useMutation({
mutationFn: deleteSSLCertificate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
toast.success("SSL certificate deleted successfully");
},
onError: (error) => {
console.error("Error deleting SSL certificate:", error);
toast.error(error instanceof Error ? error.message : "Failed to delete SSL certificate");
}
});
// Refresh certificate mutation - Updated to ensure notifications are sent
const refreshMutation = useMutation({
mutationFn: checkAndUpdateCertificate,
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setRefreshingId(null);
toast.success("SSL certificate checked and updated successfully");
toast.success(`SSL certificate for ${data.domain} updated successfully`);
},
onError: (error) => {
console.error("Error refreshing SSL certificate:", error);
toast.error(error instanceof Error ? error.message : "Failed to refresh SSL certificate");
let errorMessage = "Failed to check SSL certificate.";
if (error instanceof Error) {
errorMessage = error.message;
}
toast.error(errorMessage);
setRefreshingId(null);
// Still refresh the data to show any partial information that was saved
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
}
});
// Refresh all certificates mutation
const refreshAllMutation = useMutation({
mutationFn: refreshAllCertificates,
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setIsRefreshingAll(false);
if (result.failed === 0) {
toast.success(`Successfully refreshed all ${result.success} certificates`);
} else {
toast.info(`Refreshed ${result.success} certificates, ${result.failed} failed`);
}
},
onError: (error) => {
console.error("Error refreshing all certificates:", error);
toast.error("Failed to refresh all certificates");
setIsRefreshingAll(false);
// Still refresh the data to show any partial information
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
}
});
@@ -55,10 +158,36 @@ export const SSLDomainContent = () => {
};
const handleRefreshCertificate = (id: string) => {
if (refreshingId) return; // Prevent multiple refreshes
setRefreshingId(id);
refreshMutation.mutate(id);
};
const handleEditCertificate = (certificate: SSLCertificate) => {
setSelectedCertificate(certificate);
setIsEditDialogOpen(true);
};
const handleUpdateCertificate = (certificate: SSLCertificate) => {
console.log("Handling certificate update with data:", certificate);
editMutation.mutate(certificate);
};
const handleDeleteCertificate = (certificate: SSLCertificate) => {
deleteMutation.mutate(certificate.id);
};
const handleRefreshAll = async () => {
if (certificates.length === 0) {
toast.info("No certificates to refresh");
return;
}
setIsRefreshingAll(true);
toast.info(`Starting refresh of all ${certificates.length} certificates...`);
refreshAllMutation.mutate();
};
if (isLoading) {
return <LoadingState />;
}
@@ -67,7 +196,7 @@ export const SSLDomainContent = () => {
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>
<Button onClick={() => queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] })}>Retry</Button>
</div>
);
}
@@ -76,13 +205,32 @@ export const SSLDomainContent = () => {
<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>
<h2 className="text-2xl font-bold text-foreground">SSL & Domain Management</h2>
<p className="text-sm text-muted-foreground mt-1">Monitor SSL certificates and their expiration dates</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleRefreshAll}
disabled={isRefreshingAll || refreshingId !== null}
className="relative"
>
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshingAll ? 'animate-spin' : ''}`} />
Refresh All
{isRefreshingAll && (
<span className="absolute top-0 right-0 -mt-2 -mr-2 bg-primary text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
...
</span>
)}
</Button>
<Button
className="text-primary-foreground"
onClick={() => setIsAddDialogOpen(true)}
>
<Plus className="w-4 h-4 mr-2" /> Add Domain
</Button>
</div>
</div>
<SSLCertificateStatusCards certificates={certificates} />
@@ -92,6 +240,8 @@ export const SSLDomainContent = () => {
certificates={certificates}
onRefresh={handleRefreshCertificate}
refreshingId={refreshingId}
onEdit={handleEditCertificate}
onDelete={handleDeleteCertificate}
/>
</div>
</div>
@@ -108,6 +258,28 @@ export const SSLDomainContent = () => {
/>
</DialogContent>
</Dialog>
{selectedCertificate && (
<Dialog open={isEditDialogOpen} onOpenChange={(open) => {
setIsEditDialogOpen(open);
if (!open) setSelectedCertificate(null);
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit SSL Certificate</DialogTitle>
</DialogHeader>
<EditSSLCertificateForm
certificate={selectedCertificate}
onSubmit={handleUpdateCertificate}
onCancel={() => {
setIsEditDialogOpen(false);
setSelectedCertificate(null);
}}
isPending={editMutation.isPending}
/>
</DialogContent>
</Dialog>
)}
</main>
);
};