From b7b5939c1648fa62b92085a4fa2e2e34a09c2a42 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Tue, 7 Oct 2025 22:59:48 +0530 Subject: [PATCH] feat: adds pagination in organizations table --- backend/src/server/routes/v1/admin-router.ts | 13 +- backend/src/services/org/org-dal.ts | 22 ++ .../super-admin/super-admin-service.ts | 9 + .../services/super-admin/super-admin-types.ts | 4 + frontend/src/hooks/api/admin/queries.ts | 17 +- frontend/src/hooks/api/admin/types.ts | 16 +- .../components/OrganizationsTable.tsx | 243 ++++++++++-------- 7 files changed, 198 insertions(+), 126 deletions(-) diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 7f5a2f374..145dd1c4f 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -230,7 +230,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { createdAt: z.date() }) .array() - }).array() + }).array(), + meta: z.object({ + total: z.number() + }) }) } }, @@ -243,9 +246,15 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { const organizations = await server.services.superAdmin.getOrganizations({ ...req.query }); + const count = await server.services.superAdmin.countOrganizations({ + ...req.query + }); return { - organizations + organizations, + meta: { + total: count + } }; } }); diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 925784bff..6f91d2dc3 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -138,6 +138,27 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const countOrganizationsByFilter = async ({ searchTerm }: { searchTerm: string }) => { + interface CountResult { + count: string; + } + try { + const count = await db + .replicaNode()(TableName.Organization) + .where((qb) => { + if (searchTerm) { + void qb.whereILike(`${TableName.Organization}.name`, `%${searchTerm}%`); + } + }) + .count("*") + .first(); + + return parseInt((count as unknown as CountResult).count || "0", 10); + } catch (error) { + throw new DatabaseError({ error, name: "Count organizations by filter" }); + } + }; + const findOrgById = async (orgId: string) => { try { const org = (await db @@ -670,6 +691,7 @@ export const orgDALFactory = (db: TDbClient) => { findOrgBySlug, findAllOrgsByUserId, findOrganizationsByFilter, + countOrganizationsByFilter, ghostUserExists, findOrgMembersByUsername, findOrgMembersByRole, diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index b43e4e0a2..f179c73b2 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -55,6 +55,7 @@ import { TAdminGetUsersDTO, TAdminIntegrationConfig, TAdminSignUpDTO, + TCountOrganizationsDTO, TCreateOrganizationDTO, TGetOrganizationsDTO, TResendOrgInviteDTO @@ -750,6 +751,13 @@ export const superAdminServiceFactory = ({ return organizations; }; + const countOrganizations = async ({ searchTerm }: TCountOrganizationsDTO) => { + const count = await orgDAL.countOrganizationsByFilter({ + searchTerm + }); + return count; + }; + const createOrganization = async ( { name, inviteAdminEmails: emails }: TCreateOrganizationDTO, actor: OrgServiceActor @@ -1146,6 +1154,7 @@ export const superAdminServiceFactory = ({ invalidateCache, checkIfInvalidatingCache, getOrganizations, + countOrganizations, deleteOrganization, deleteOrganizationMembership, initializeAdminIntegrationConfigSync, diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 6cfdd384d..10c8b7878 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -34,6 +34,10 @@ export type TGetOrganizationsDTO = { searchTerm: string; }; +export type TCountOrganizationsDTO = { + searchTerm: string; +}; + export type TCreateOrganizationDTO = { name: string; inviteAdminEmails: string[]; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 4b10ba3ce..1ee3bb5a9 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -16,9 +16,9 @@ import { AdminGetOrganizationsFilters, AdminGetUsersFilters, AdminIntegrationsConfig, - OrganizationWithProjects, TGetEnvOverrides, TGetInvalidatingCacheStatus, + TGetOrganizationsResponse, TGetServerRootKmsEncryptionDetails, TServerConfig } from "./types"; @@ -49,24 +49,21 @@ export const fetchServerConfig = async () => { }; export const useAdminGetOrganizations = (filters: AdminGetOrganizationsFilters) => { - return useInfiniteQuery({ - initialPageParam: 0, + return useQuery({ queryKey: adminQueryKeys.getOrganizations(filters), - queryFn: async ({ pageParam }) => { - const { data } = await apiRequest.get<{ organizations: OrganizationWithProjects[] }>( + queryFn: async () => { + const { data } = await apiRequest.get( "/api/v1/admin/organization-management/organizations", { params: { - ...filters, - offset: pageParam + ...filters } } ); - return data.organizations; + return { organizations: data.organizations, totalCount: data.meta.total }; }, - getNextPageParam: (lastPage, pages) => - lastPage.length !== 0 ? pages.length * filters.limit : undefined + placeholderData: (previousData) => previousData }); }; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index f8336fcac..c89f11631 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -34,6 +34,13 @@ export type OrganizationWithProjects = Organization & { }[]; }; +export type TGetOrganizationsResponse = { + organizations: OrganizationWithProjects[]; + meta: { + total: number; + }; +}; + export type TServerConfig = { initialized: boolean; allowSignUp: boolean; @@ -81,8 +88,9 @@ export type TCreateAdminUserDTO = { }; export type AdminGetOrganizationsFilters = { - limit: number; - searchTerm: string; + searchTerm?: string; + limit?: number; + offset?: number; }; export type AdminGetUsersFilters = { @@ -162,3 +170,7 @@ export type TResendOrgInviteDTO = { organizationId: string; membershipId: string; }; + +export enum AdminOrganizationOrderBy { + Name = "name" +} diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx index b69653180..be4abff04 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx @@ -48,6 +48,11 @@ import { } from "@app/components/v2"; import { useUser } from "@app/context"; import { OrgMembershipRole } from "@app/helpers/roles"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useAdminDeleteOrganization, @@ -57,7 +62,7 @@ import { useServerAdminAccessOrg, useServerAdminResendOrgInvite } from "@app/hooks/api"; -import { OrganizationWithProjects } from "@app/hooks/api/admin/types"; +import { AdminOrganizationOrderBy, OrganizationWithProjects } from "@app/hooks/api/admin/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrgMembershipStatus } from "@app/hooks/api/organization/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -443,13 +448,32 @@ const OrganizationsPanelTable = ({ const { user } = useUser(); const navigate = useNavigate(); - const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = - useAdminGetOrganizations({ - limit: 20, - searchTerm: debouncedSearchTerm + + const { offset, limit, setPage, perPage, page, setPerPage } = + usePagination(AdminOrganizationOrderBy.Name, { + initPerPage: getUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, 10) }); - const isEmpty = !isPending && !data?.pages?.[0].length; + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, newPerPage); + }; + + const { data, isPending } = useAdminGetOrganizations({ + limit, + offset, + searchTerm: debouncedSearchTerm + }); + + const { organizations, totalCount = 0 } = data ?? {}; + + const isEmpty = !isPending && !totalCount; + + useResetPageHelper({ + totalCount, + offset, + setPage + }); const { mutateAsync: accessOrganization } = useServerAdminAccessOrg(); @@ -499,125 +523,120 @@ const OrganizationsPanelTable = ({ {isPending && } {!isPending && - data?.pages?.map((orgs) => - orgs.map((org) => { - const isMember = org.members.find((member) => member.user.id === user.id); + organizations?.map((org) => { + const isMember = org.members.find((member) => member.user.id === user.id); - return ( - - -
- {org.name ? ( -

{org.name}

- ) : ( - Not Set - )} -
- - - - - - {org.projects.length} {org.projects.length === 1 ? "Project" : "Projects"} - - -
- {isMember && ( - + + + {org.projects.length} {org.projects.length === 1 ? "Project" : "Projects"} + + +
+ {isMember && ( + +
+ +
+
+ )} + + + -
- -
- - )} - - - - - - - - {!isMember && ( - { - e.stopPropagation(); - handleAccessOrg(org.id); - }} - icon={} - > - Join Organization - - )} + +
+
+ + {!isMember && ( { e.stopPropagation(); - handlePopUpOpen("deleteOrganization", { - orgId: org.id, - orgName: org.name - }); + handleAccessOrg(org.id); }} - icon={} + icon={} > - Delete Organization + Join Organization - -
-
- - - ); - }) - )} + )} + { + e.stopPropagation(); + handlePopUpOpen("deleteOrganization", { + orgId: org.id, + orgName: org.name + }); + }} + icon={} + > + Delete Organization + + + +
+ + + ); + })} {!isPending && isEmpty && } - {!isEmpty && ( - + {!isPending && totalCount > 0 && ( + setPage(newPage)} + onChangePerPage={handlePerPageChange} + /> )}