From 7ccf752e0c68d696299384d5e253f45d393a59bf Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 12 Jun 2024 16:49:29 -0700 Subject: [PATCH] Add pagination for certificates table --- .../src/server/routes/v2/project-router.ts | 14 ++++-- .../services/certificate/certificate-dal.ts | 26 +++++++++- .../src/services/project/project-service.ts | 28 +++++++---- backend/src/services/project/project-types.ts | 6 +++ frontend/src/hooks/api/ca/mutations.tsx | 2 +- .../src/hooks/api/certificates/mutations.tsx | 4 +- frontend/src/hooks/api/workspace/queries.tsx | 47 +++++++++++++++---- .../components/CertificatesTable.tsx | 30 +++++++++--- 8 files changed, 126 insertions(+), 31 deletions(-) diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 1b36a689e..e1c7c2e69 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -356,15 +356,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { params: z.object({ slug: slugSchema.describe("The slug of the project to list certificates.") }), + querystring: z.object({ + offset: z.coerce.number().min(0).max(100).default(0), + limit: z.coerce.number().min(1).max(100).default(25) + }), response: { 200: z.object({ - certificates: z.array(CertificatesSchema) + certificates: z.array(CertificatesSchema), + totalCount: z.number() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const certificates = await server.services.project.listProjectCertificates({ + const { certificates, totalCount } = await server.services.project.listProjectCertificates({ filter: { slug: req.params.slug, orgId: req.permission.orgId, @@ -373,9 +378,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, - actor: req.permission.type + actor: req.permission.type, + ...req.query }); - return { certificates }; + return { certificates, totalCount }; } }); }; diff --git a/backend/src/services/certificate/certificate-dal.ts b/backend/src/services/certificate/certificate-dal.ts index 2f049fcc8..415bfabf9 100644 --- a/backend/src/services/certificate/certificate-dal.ts +++ b/backend/src/services/certificate/certificate-dal.ts @@ -1,10 +1,34 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; export type TCertificateDALFactory = ReturnType; export const certificateDALFactory = (db: TDbClient) => { const certificateOrm = ormify(db, TableName.Certificate); - return certificateOrm; + + const countCertificatesInProject = async (projectId: string) => { + try { + interface CountResult { + count: string; + } + + const count = await db(TableName.Certificate) + .join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`) + .join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`) + .where(`${TableName.Project}.id`, projectId) + .count("*") + .first(); + + return parseInt((count as unknown as CountResult).count || "0", 10); + } catch (error) { + throw new DatabaseError({ error, name: "Count all project certificates" }); + } + }; + + return { + ...certificateOrm, + countCertificatesInProject + }; }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 1cf2c2f71..1a8e65a41 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -39,6 +39,7 @@ import { TDeleteProjectDTO, TGetProjectDTO, TListProjectCasDTO, + TListProjectCertsDTO, TToggleProjectAutoCapitalizationDTO, TUpdateProjectDTO, TUpdateProjectNameDTO, @@ -68,7 +69,7 @@ type TProjectServiceFactoryDep = { projectUserMembershipRoleDAL: Pick; secretBlindIndexDAL: Pick; certificateAuthorityDAL: Pick; - certificateDAL: Pick; + certificateDAL: Pick; permissionService: TPermissionServiceFactory; orgService: Pick; licenseService: Pick; @@ -569,12 +570,14 @@ export const projectServiceFactory = ({ * Return list of certificates for project */ const listProjectCertificates = async ({ + offset, + limit, actorId, actorOrgId, actorAuthMethod, filter, actor - }: TListProjectCasDTO) => { + }: TListProjectCertsDTO) => { const project = await projectDAL.findProjectByFilter(filter); const { permission } = await permissionService.getProjectPermission( @@ -589,12 +592,21 @@ export const projectServiceFactory = ({ const cas = await certificateAuthorityDAL.find({ projectId: project.id }); - const certificates = await certificateDAL.find({ - $in: { - caId: cas.map((ca) => ca.id) - } - }); - return certificates; + const certificates = await certificateDAL.find( + { + $in: { + caId: cas.map((ca) => ca.id) + } + }, + { offset, limit, sort: [["updatedAt", "desc"]] } + ); + + const count = await certificateDAL.countCertificatesInProject(project.id); + + return { + certificates, + totalCount: count + }; }; return { diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 39e047748..e2d145d3d 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -86,3 +86,9 @@ export type TListProjectCasDTO = { status?: CaStatus; filter: Filter; } & Omit; + +export type TListProjectCertsDTO = { + filter: Filter; + offset: number; + limit: number; +} & Omit; diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx index 3a8f18e17..bb018f71a 100644 --- a/frontend/src/hooks/api/ca/mutations.tsx +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -102,7 +102,7 @@ export const useCreateCertificate = () => { return data; }, onSuccess: (_, { projectSlug }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificates(projectSlug)); + queryClient.invalidateQueries(workspaceKeys.forWorkspaceCertificates(projectSlug)); } }); }; diff --git a/frontend/src/hooks/api/certificates/mutations.tsx b/frontend/src/hooks/api/certificates/mutations.tsx index cb366e4e1..4c0f92339 100644 --- a/frontend/src/hooks/api/certificates/mutations.tsx +++ b/frontend/src/hooks/api/certificates/mutations.tsx @@ -17,7 +17,7 @@ export const useDeleteCert = () => { return certificate; }, onSuccess: (_, { projectSlug }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificates(projectSlug)); + queryClient.invalidateQueries(workspaceKeys.forWorkspaceCertificates(projectSlug)); } }); }; @@ -37,7 +37,7 @@ export const useRevokeCert = () => { return certificate; }, onSuccess: (_, { projectSlug }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificates(projectSlug)); + queryClient.invalidateQueries(workspaceKeys.forWorkspaceCertificates(projectSlug)); } }); }; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index d3d30e420..7f94bb498 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -48,8 +48,18 @@ export const workspaceKeys = { [{ projectSlug }, "workspace-cas"] as const, specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) => [...workspaceKeys.getWorkspaceCas({ projectSlug }), { status }] as const, - getWorkspaceCertificates: (projectSlug: string) => - [{ projectSlug }, "workspace-certificates"] as const + allWorkspaceCertificates: () => ["workspace-certificates"] as const, + forWorkspaceCertificates: (slug: string) => + [...workspaceKeys.allWorkspaceCertificates(), slug] as const, + specificWorkspaceCertificates: ({ + slug, + offset, + limit + }: { + slug: string; + offset: number; + limit: number; + }) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const }; const fetchWorkspaceById = async (workspaceId: string) => { @@ -526,16 +536,37 @@ export const useListWorkspaceCas = ({ }); }; -export const useListWorkspaceCertificates = (projectSlug: string) => { +export const useListWorkspaceCertificates = ({ + projectSlug, + offset, + limit +}: { + projectSlug: string; + offset: number; + limit: number; +}) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceCertificates(projectSlug), + queryKey: workspaceKeys.specificWorkspaceCertificates({ + slug: projectSlug, + offset, + limit + }), queryFn: async () => { + const params = new URLSearchParams({ + offset: String(offset), + limit: String(limit) + }); + const { - data: { certificates } - } = await apiRequest.get<{ certificates: TCertificate[] }>( - `/api/v2/workspace/${projectSlug}/certificates` + data: { certificates, totalCount } + } = await apiRequest.get<{ certificates: TCertificate[]; totalCount: number }>( + `/api/v2/workspace/${projectSlug}/certificates`, + { + params + } ); - return certificates; + + return { certificates, totalCount }; }, enabled: Boolean(projectSlug) }); diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx index 371c432a1..c83d47507 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { faBan, faCertificate, @@ -17,6 +18,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, EmptyState, + Pagination, Table, TableContainer, TableSkeleton, @@ -25,8 +27,7 @@ import { Th, THead, Tooltip, - Tr -} from "@app/components/v2"; + Tr} from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { useListWorkspaceCertificates } from "@app/hooks/api"; import { certStatusToNameMap } from "@app/hooks/api/certificates/constants"; @@ -45,8 +46,16 @@ type Props = { }; export const CertificatesTable = ({ handlePopUpOpen }: Props) => { + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(25); + const { currentWorkspace } = useWorkspace(); - const { data, isLoading } = useListWorkspaceCertificates(currentWorkspace?.slug ?? ""); + const { data, isLoading } = useListWorkspaceCertificates({ + projectSlug: currentWorkspace?.slug ?? "", + offset: (page - 1) * perPage, + limit: perPage + }); + return (
@@ -62,9 +71,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { {isLoading && } {!isLoading && - data && - data.length > 0 && - data.map((certificate) => { + data?.certificates.map((certificate) => { return ( {certificate.friendlyName} @@ -177,7 +184,16 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { })} - {!isLoading && data?.length === 0 && ( + {!isLoading && data?.totalCount !== undefined && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {!isLoading && !data?.certificates?.length && ( )}