Add pagination for certificates table

This commit is contained in:
Tuan Dang
2024-06-12 16:49:29 -07:00
parent 2d10265d0d
commit 7ccf752e0c
8 changed files with 126 additions and 31 deletions
+10 -4
View File
@@ -356,15 +356,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
params: z.object({ params: z.object({
slug: slugSchema.describe("The slug of the project to list certificates.") 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: { response: {
200: z.object({ 200: z.object({
certificates: z.array(CertificatesSchema) certificates: z.array(CertificatesSchema),
totalCount: z.number()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const certificates = await server.services.project.listProjectCertificates({ const { certificates, totalCount } = await server.services.project.listProjectCertificates({
filter: { filter: {
slug: req.params.slug, slug: req.params.slug,
orgId: req.permission.orgId, orgId: req.permission.orgId,
@@ -373,9 +378,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actor: req.permission.type actor: req.permission.type,
...req.query
}); });
return { certificates }; return { certificates, totalCount };
} }
}); });
}; };
@@ -1,10 +1,34 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>; export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
export const certificateDALFactory = (db: TDbClient) => { export const certificateDALFactory = (db: TDbClient) => {
const certificateOrm = ormify(db, TableName.Certificate); 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
};
}; };
@@ -39,6 +39,7 @@ import {
TDeleteProjectDTO, TDeleteProjectDTO,
TGetProjectDTO, TGetProjectDTO,
TListProjectCasDTO, TListProjectCasDTO,
TListProjectCertsDTO,
TToggleProjectAutoCapitalizationDTO, TToggleProjectAutoCapitalizationDTO,
TUpdateProjectDTO, TUpdateProjectDTO,
TUpdateProjectNameDTO, TUpdateProjectNameDTO,
@@ -68,7 +69,7 @@ type TProjectServiceFactoryDep = {
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">; secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: Pick<TCertificateDALFactory, "find">; certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">; orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -569,12 +570,14 @@ export const projectServiceFactory = ({
* Return list of certificates for project * Return list of certificates for project
*/ */
const listProjectCertificates = async ({ const listProjectCertificates = async ({
offset,
limit,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
filter, filter,
actor actor
}: TListProjectCasDTO) => { }: TListProjectCertsDTO) => {
const project = await projectDAL.findProjectByFilter(filter); const project = await projectDAL.findProjectByFilter(filter);
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -589,12 +592,21 @@ export const projectServiceFactory = ({
const cas = await certificateAuthorityDAL.find({ projectId: project.id }); const cas = await certificateAuthorityDAL.find({ projectId: project.id });
const certificates = await certificateDAL.find({ const certificates = await certificateDAL.find(
$in: { {
caId: cas.map((ca) => ca.id) $in: {
} caId: cas.map((ca) => ca.id)
}); }
return certificates; },
{ offset, limit, sort: [["updatedAt", "desc"]] }
);
const count = await certificateDAL.countCertificatesInProject(project.id);
return {
certificates,
totalCount: count
};
}; };
return { return {
@@ -86,3 +86,9 @@ export type TListProjectCasDTO = {
status?: CaStatus; status?: CaStatus;
filter: Filter; filter: Filter;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TListProjectCertsDTO = {
filter: Filter;
offset: number;
limit: number;
} & Omit<TProjectPermission, "projectId">;
+1 -1
View File
@@ -102,7 +102,7 @@ export const useCreateCertificate = () => {
return data; return data;
}, },
onSuccess: (_, { projectSlug }) => { onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificates(projectSlug)); queryClient.invalidateQueries(workspaceKeys.forWorkspaceCertificates(projectSlug));
} }
}); });
}; };
@@ -17,7 +17,7 @@ export const useDeleteCert = () => {
return certificate; return certificate;
}, },
onSuccess: (_, { projectSlug }) => { onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificates(projectSlug)); queryClient.invalidateQueries(workspaceKeys.forWorkspaceCertificates(projectSlug));
} }
}); });
}; };
@@ -37,7 +37,7 @@ export const useRevokeCert = () => {
return certificate; return certificate;
}, },
onSuccess: (_, { projectSlug }) => { onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificates(projectSlug)); queryClient.invalidateQueries(workspaceKeys.forWorkspaceCertificates(projectSlug));
} }
}); });
}; };
+39 -8
View File
@@ -48,8 +48,18 @@ export const workspaceKeys = {
[{ projectSlug }, "workspace-cas"] as const, [{ projectSlug }, "workspace-cas"] as const,
specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) => specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) =>
[...workspaceKeys.getWorkspaceCas({ projectSlug }), { status }] as const, [...workspaceKeys.getWorkspaceCas({ projectSlug }), { status }] as const,
getWorkspaceCertificates: (projectSlug: string) => allWorkspaceCertificates: () => ["workspace-certificates"] as const,
[{ projectSlug }, "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) => { 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({ return useQuery({
queryKey: workspaceKeys.getWorkspaceCertificates(projectSlug), queryKey: workspaceKeys.specificWorkspaceCertificates({
slug: projectSlug,
offset,
limit
}),
queryFn: async () => { queryFn: async () => {
const params = new URLSearchParams({
offset: String(offset),
limit: String(limit)
});
const { const {
data: { certificates } data: { certificates, totalCount }
} = await apiRequest.get<{ certificates: TCertificate[] }>( } = await apiRequest.get<{ certificates: TCertificate[]; totalCount: number }>(
`/api/v2/workspace/${projectSlug}/certificates` `/api/v2/workspace/${projectSlug}/certificates`,
{
params
}
); );
return certificates;
return { certificates, totalCount };
}, },
enabled: Boolean(projectSlug) enabled: Boolean(projectSlug)
}); });
@@ -1,3 +1,4 @@
import { useState } from "react";
import { import {
faBan, faBan,
faCertificate, faCertificate,
@@ -17,6 +18,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
EmptyState, EmptyState,
Pagination,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -25,8 +27,7 @@ import {
Th, Th,
THead, THead,
Tooltip, Tooltip,
Tr Tr} from "@app/components/v2";
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useListWorkspaceCertificates } from "@app/hooks/api"; import { useListWorkspaceCertificates } from "@app/hooks/api";
import { certStatusToNameMap } from "@app/hooks/api/certificates/constants"; import { certStatusToNameMap } from "@app/hooks/api/certificates/constants";
@@ -45,8 +46,16 @@ type Props = {
}; };
export const CertificatesTable = ({ handlePopUpOpen }: Props) => { export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(25);
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useListWorkspaceCertificates(currentWorkspace?.slug ?? ""); const { data, isLoading } = useListWorkspaceCertificates({
projectSlug: currentWorkspace?.slug ?? "",
offset: (page - 1) * perPage,
limit: perPage
});
return ( return (
<div> <div>
<TableContainer> <TableContainer>
@@ -62,9 +71,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
<TBody> <TBody>
{isLoading && <TableSkeleton columns={3} innerKey="project-cas" />} {isLoading && <TableSkeleton columns={3} innerKey="project-cas" />}
{!isLoading && {!isLoading &&
data && data?.certificates.map((certificate) => {
data.length > 0 &&
data.map((certificate) => {
return ( return (
<Tr className="h-10" key={`certificate-${certificate.id}`}> <Tr className="h-10" key={`certificate-${certificate.id}`}>
<Td>{certificate.friendlyName}</Td> <Td>{certificate.friendlyName}</Td>
@@ -177,7 +184,16 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
})} })}
</TBody> </TBody>
</Table> </Table>
{!isLoading && data?.length === 0 && ( {!isLoading && data?.totalCount !== undefined && (
<Pagination
count={data.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isLoading && !data?.certificates?.length && (
<EmptyState title="No certificates have been created" icon={faCertificate} /> <EmptyState title="No certificates have been created" icon={faCertificate} />
)} )}
</TableContainer> </TableContainer>