From e2f41fdef1d69d6f6e954386af00717ece1bbdb5 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Sun, 27 Jul 2025 20:07:59 +0700 Subject: [PATCH] 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). --- .../ssl-domain/SSLCertificatesTable.tsx | 128 +++++++++-------- .../ssl-domain/SSLDomainContent.tsx | 20 +-- .../components/ssl-domain/SSLPagination.tsx | 131 ++++++++++++++++++ application/src/hooks/useSSLPagination.ts | 52 +++++++ 4 files changed, 267 insertions(+), 64 deletions(-) create mode 100644 application/src/components/ssl-domain/SSLPagination.tsx create mode 100644 application/src/hooks/useSSLPagination.ts diff --git a/application/src/components/ssl-domain/SSLCertificatesTable.tsx b/application/src/components/ssl-domain/SSLCertificatesTable.tsx index dd28818..a1e5008 100644 --- a/application/src/components/ssl-domain/SSLCertificatesTable.tsx +++ b/application/src/components/ssl-domain/SSLCertificatesTable.tsx @@ -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
Loading...
; if (isError) return
Error loading certificates
; @@ -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')} ) : ( -
- - - - {t('domain')} - {t('status')} - {t('issuer')} - {t('validUntil')} - {t('daysLeft')} - Check Interval - Actions - - - - {certificates.map((certificate) => ( - openViewDialog(certificate)} - > - - {certificate.domain} - - - - - {certificate.issuer_o || certificate.issuer_cn || 'Unknown'} - - {certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'} - - - - {certificate.days_left} {t('days')} - - - - {certificate.check_interval || 1} {t('days')} - - e.stopPropagation()}> - - + <> +
+
+ + + {t('domain')} + {t('status')} + {t('issuer')} + {t('validUntil')} + {t('daysLeft')} + Check Interval + Actions - ))} - -
-
+ + + {paginatedCertificates.map((certificate) => ( + openViewDialog(certificate)} + > + + {certificate.domain} + + + + + {certificate.issuer_o || certificate.issuer_cn || 'Unknown'} + + {certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'} + + + + {certificate.days_left} {t('days')} + + + + {certificate.check_interval || 1} {t('days')} + + e.stopPropagation()}> + + + + ))} + + + + + + )} {/* View Certificate Dialog */} @@ -256,4 +276,4 @@ export const SSLCertificatesTable = () => { ); -}; +}; \ No newline at end of file diff --git a/application/src/components/ssl-domain/SSLDomainContent.tsx b/application/src/components/ssl-domain/SSLDomainContent.tsx index bb38980..6fe1b60 100644 --- a/application/src/components/ssl-domain/SSLDomainContent.tsx +++ b/application/src/components/ssl-domain/SSLDomainContent.tsx @@ -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); diff --git a/application/src/components/ssl-domain/SSLPagination.tsx b/application/src/components/ssl-domain/SSLPagination.tsx new file mode 100644 index 0000000..2ee6721 --- /dev/null +++ b/application/src/components/ssl-domain/SSLPagination.tsx @@ -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 ( +
+
+ + {t("rowsPerPage") || "Rows per page"}: + + + + {totalItems > 0 + ? `${startItem}-${endItem} of ${totalItems} ${t("certificates") || "certificates"}` + : `0 ${t("certificates") || "certificates"}` + } + +
+ + {totalPages > 1 && ( + + + + onPageChange(Math.max(1, currentPage - 1))} + className={ + currentPage === 1 + ? "pointer-events-none opacity-50" + : "cursor-pointer" + } + /> + + + {getPageNumbers().map((page) => ( + + onPageChange(page)} + className="cursor-pointer" + > + {page} + + + ))} + + + onPageChange(Math.min(totalPages, currentPage + 1))} + className={ + currentPage === totalPages + ? "pointer-events-none opacity-50" + : "cursor-pointer" + } + /> + + + + )} +
+ ); +} \ No newline at end of file diff --git a/application/src/hooks/useSSLPagination.ts b/application/src/hooks/useSSLPagination.ts new file mode 100644 index 0000000..25bc9e0 --- /dev/null +++ b/application/src/hooks/useSSLPagination.ts @@ -0,0 +1,52 @@ + +import { useState, useMemo } from 'react'; +import { SSLCertificate } from '@/types/ssl.types'; + +export type SSLPageSize = 10 | 30 | 50; + +interface UseSSLPaginationProps { + certificates: SSLCertificate[]; + initialPageSize?: SSLPageSize; +} + +export const useSSLPagination = ({ + certificates, + initialPageSize = 10 +}: UseSSLPaginationProps) => { + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(initialPageSize); + + const { paginatedCertificates, totalPages } = useMemo(() => { + const totalItems = certificates.length; + const pages = Math.ceil(totalItems / pageSize); + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + + return { + paginatedCertificates: certificates.slice(startIndex, endIndex), + totalPages: Math.max(1, pages) + }; + }, [certificates, currentPage, pageSize]); + + // Reset to first page when page size changes or certificates change + const handlePageSizeChange = (newPageSize: SSLPageSize) => { + setPageSize(newPageSize); + setCurrentPage(1); + }; + + // Reset to first page if current page exceeds total pages + const handlePageChange = (page: number) => { + const validPage = Math.min(Math.max(1, page), totalPages); + setCurrentPage(validPage); + }; + + return { + paginatedCertificates, + currentPage, + totalPages, + pageSize, + totalItems: certificates.length, + handlePageChange, + handlePageSizeChange, + }; +}; \ No newline at end of file