feat: Add pagination to SSL table
- Implement pagination for the SSL dashboard table, includes adding a dropdown for selecting the number of records per page (10, 30, 50).
This commit is contained in:
@@ -33,11 +33,13 @@ import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
|
||||
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
|
||||
import { SSLCertificateActions } from "./SSLCertificateActions";
|
||||
import { SSLCertificateDetailDialog } from "./SSLCertificateDetailDialog";
|
||||
import { SSLPagination } from "./SSLPagination";
|
||||
import { fetchSSLCertificates, addSSLCertificate, deleteSSLCertificate } from "@/services/sslCertificateService";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { SSLCertificate } from "@/types/ssl.types";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useSSLPagination } from "@/hooks/useSSLPagination";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const SSLCertificatesTable = () => {
|
||||
@@ -56,6 +58,16 @@ export const SSLCertificatesTable = () => {
|
||||
queryFn: fetchSSLCertificates,
|
||||
});
|
||||
|
||||
const {
|
||||
paginatedCertificates,
|
||||
currentPage,
|
||||
totalPages,
|
||||
pageSize,
|
||||
totalItems,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
} = useSSLPagination({ certificates });
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (isError) return <div>Error loading certificates</div>;
|
||||
|
||||
@@ -67,7 +79,6 @@ export const SSLCertificatesTable = () => {
|
||||
setShowAddDialog(false);
|
||||
toast.success(t('certificateAdded'));
|
||||
} catch (error) {
|
||||
// console.error("Error adding certificate:", error);
|
||||
toast.error(t('failedToAddCertificate'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
@@ -89,7 +100,6 @@ export const SSLCertificatesTable = () => {
|
||||
setSelectedCertificate(null);
|
||||
toast.success(t('certificateUpdated'));
|
||||
} catch (error) {
|
||||
// console.error("Error updating certificate:", error);
|
||||
toast.error(t('failedToUpdateCertificate'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
@@ -107,7 +117,6 @@ export const SSLCertificatesTable = () => {
|
||||
setSelectedCertificate(null);
|
||||
toast.success(t('certificateDeleted'));
|
||||
} catch (error) {
|
||||
// console.error("Error deleting certificate:", error);
|
||||
toast.error(t('failedToDeleteCertificate'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
@@ -136,57 +145,68 @@ export const SSLCertificatesTable = () => {
|
||||
{t('noCertificatesFound')}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg border border-border shadow-sm`}>
|
||||
<Table>
|
||||
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}>
|
||||
<TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('domain')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('status')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('issuer')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('validUntil')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('daysLeft')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Check Interval</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right w-[50px]`}>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{certificates.map((certificate) => (
|
||||
<TableRow
|
||||
key={certificate.id}
|
||||
className="hover:bg-muted/50 cursor-pointer"
|
||||
onClick={() => openViewDialog(certificate)}
|
||||
>
|
||||
<TableCell className="font-medium">
|
||||
{certificate.domain}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SSLStatusBadge status={certificate.status} />
|
||||
</TableCell>
|
||||
<TableCell>{certificate.issuer_o || certificate.issuer_cn || 'Unknown'}</TableCell>
|
||||
<TableCell>
|
||||
{certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'}
|
||||
</TableCell>
|
||||
<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 className="text-right" onClick={(e) => e.stopPropagation()}>
|
||||
<SSLCertificateActions
|
||||
certificate={certificate}
|
||||
onView={openViewDialog}
|
||||
onEdit={openEditDialog}
|
||||
onDelete={openDeleteDialog}
|
||||
/>
|
||||
</TableCell>
|
||||
<>
|
||||
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg border border-border shadow-sm`}>
|
||||
<Table>
|
||||
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}>
|
||||
<TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('domain')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('status')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('issuer')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('validUntil')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('daysLeft')}</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Check Interval</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right w-[50px]`}>Actions</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedCertificates.map((certificate) => (
|
||||
<TableRow
|
||||
key={certificate.id}
|
||||
className="hover:bg-muted/50 cursor-pointer"
|
||||
onClick={() => openViewDialog(certificate)}
|
||||
>
|
||||
<TableCell className="font-medium">
|
||||
{certificate.domain}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SSLStatusBadge status={certificate.status} />
|
||||
</TableCell>
|
||||
<TableCell>{certificate.issuer_o || certificate.issuer_cn || 'Unknown'}</TableCell>
|
||||
<TableCell>
|
||||
{certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'}
|
||||
</TableCell>
|
||||
<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 className="text-right" onClick={(e) => e.stopPropagation()}>
|
||||
<SSLCertificateActions
|
||||
certificate={certificate}
|
||||
onView={openViewDialog}
|
||||
onEdit={openEditDialog}
|
||||
onDelete={openDeleteDialog}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<SSLPagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
pageSize={pageSize}
|
||||
totalItems={totalItems}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* View Certificate Dialog */}
|
||||
@@ -256,4 +276,4 @@ export const SSLCertificatesTable = () => {
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -30,12 +30,12 @@ export const SSLDomainContent = () => {
|
||||
queryKey: ['ssl-certificates'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
console.log("Fetching SSL certificates from SSLDomainContent...");
|
||||
// console.log("Fetching SSL certificates from SSLDomainContent...");
|
||||
const result = await fetchSSLCertificates();
|
||||
console.log("Received SSL certificates:", result);
|
||||
// console.log("Received SSL certificates:", result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Error fetching certificates:", error);
|
||||
// console.error("Error fetching certificates:", error);
|
||||
toast.error(t('failedToLoadCertificates'));
|
||||
throw error;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export const SSLDomainContent = () => {
|
||||
// Edit certificate mutation - Updated to ensure thresholds are properly updated
|
||||
const editMutation = useMutation({
|
||||
mutationFn: async (certificate: SSLCertificate) => {
|
||||
console.log("Updating certificate with data:", certificate);
|
||||
// console.log("Updating certificate with data:", certificate);
|
||||
|
||||
// Create the update data object
|
||||
const updateData = {
|
||||
@@ -70,12 +70,12 @@ export const SSLDomainContent = () => {
|
||||
notification_channel: certificate.notification_channel,
|
||||
};
|
||||
|
||||
console.log("Update data to be sent:", updateData);
|
||||
// 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);
|
||||
// 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
|
||||
@@ -90,7 +90,7 @@ export const SSLDomainContent = () => {
|
||||
toast.success(t('sslCertificateUpdated'));
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error updating SSL certificate:", error);
|
||||
// console.error("Error updating SSL certificate:", error);
|
||||
toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate'));
|
||||
}
|
||||
});
|
||||
@@ -103,7 +103,7 @@ export const SSLDomainContent = () => {
|
||||
toast.success(t('sslCertificateDeleted'));
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error deleting SSL certificate:", error);
|
||||
// console.error("Error deleting SSL certificate:", error);
|
||||
toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate'));
|
||||
}
|
||||
});
|
||||
@@ -117,7 +117,7 @@ export const SSLDomainContent = () => {
|
||||
// Removed individual success toast notification
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error refreshing SSL certificate:", error);
|
||||
// console.error("Error refreshing SSL certificate:", error);
|
||||
setRefreshingId(null);
|
||||
|
||||
// Still refresh the data to show any partial information that was saved
|
||||
@@ -142,7 +142,7 @@ export const SSLDomainContent = () => {
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error refreshing all certificates:", error);
|
||||
// console.error("Error refreshing all certificates:", error);
|
||||
toast.error(t('failedToCheckCertificate'));
|
||||
setIsRefreshingAll(false);
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious
|
||||
} from "@/components/ui/pagination";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { SSLPageSize } from "@/hooks/useSSLPagination";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface SSLPaginationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
pageSize: SSLPageSize;
|
||||
totalItems: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onPageSizeChange: (size: SSLPageSize) => void;
|
||||
}
|
||||
|
||||
export function SSLPagination({
|
||||
currentPage,
|
||||
totalPages,
|
||||
pageSize,
|
||||
totalItems,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}: SSLPaginationProps) {
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Generate page numbers to display
|
||||
const getPageNumbers = () => {
|
||||
const pages = [];
|
||||
const maxVisiblePages = 5;
|
||||
const halfVisible = Math.floor(maxVisiblePages / 2);
|
||||
|
||||
let startPage = Math.max(1, currentPage - halfVisible);
|
||||
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
||||
|
||||
// Adjust start page if we don't have enough pages at the end
|
||||
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
const startItem = Math.min((currentPage - 1) * pageSize + 1, totalItems);
|
||||
const endItem = Math.min(currentPage * pageSize, totalItems);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between py-4 px-4 border-t border-border">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("rowsPerPage") || "Rows per page"}:
|
||||
</span>
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
onValueChange={(value) => onPageSizeChange(parseInt(value) as SSLPageSize)}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="30">30</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{totalItems > 0
|
||||
? `${startItem}-${endItem} of ${totalItems} ${t("certificates") || "certificates"}`
|
||||
: `0 ${t("certificates") || "certificates"}`
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
|
||||
className={
|
||||
currentPage === 1
|
||||
? "pointer-events-none opacity-50"
|
||||
: "cursor-pointer"
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{getPageNumbers().map((page) => (
|
||||
<PaginationItem key={page}>
|
||||
<PaginationLink
|
||||
isActive={page === currentPage}
|
||||
onClick={() => onPageChange(page)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
|
||||
className={
|
||||
currentPage === totalPages
|
||||
? "pointer-events-none opacity-50"
|
||||
: "cursor-pointer"
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user