From e7990c493bbc35ab0e1443d0eeac85bc0a8b98f6 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Wed, 8 Oct 2025 02:02:19 +0530 Subject: [PATCH] feat: adds pagination in identities table --- backend/src/server/routes/v1/admin-router.ts | 14 +- backend/src/services/identity/identity-dal.ts | 23 ++- .../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 | 25 ++- .../components/MachineIdentitiesTable.tsx | 168 ++++++++++-------- .../components/OrganizationsTable.tsx | 4 +- 8 files changed, 170 insertions(+), 94 deletions(-) diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 145dd1c4f..7b31326e4 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -346,7 +346,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { .extend({ isInstanceAdmin: z.boolean() }) - .array() + .array(), + meta: z.object({ + total: z.number() + }) }) } }, @@ -360,8 +363,15 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { ...req.query }); + const count = await server.services.superAdmin.countIdentities({ + ...req.query + }); + return { - identities + identities, + meta: { + total: count + } }; } }); diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts index 7bc797600..596601703 100644 --- a/backend/src/services/identity/identity-dal.ts +++ b/backend/src/services/identity/identity-dal.ts @@ -60,5 +60,26 @@ export const identityDALFactory = (db: TDbClient) => { } }; - return { ...identityOrm, getTrustedIpsByAuthMethod, getIdentitiesByFilter }; + const countIdentitiesByFilter = async ({ searchTerm }: { searchTerm: string }) => { + interface CountResult { + count: string; + } + try { + const count = await db + .replicaNode()(TableName.Identity) + .where((qb) => { + if (searchTerm) { + void qb.whereILike(`${TableName.Identity}.name`, `%${searchTerm}%`); + } + }) + .count("*") + .first(); + + return parseInt((count as unknown as CountResult).count || "0", 10); + } catch (error) { + throw new DatabaseError({ error, name: "Count identities by filter" }); + } + }; + + return { ...identityOrm, getTrustedIpsByAuthMethod, getIdentitiesByFilter, countIdentitiesByFilter }; }; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index f179c73b2..d056986e2 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, + TCountIdentitiesDTO, TCountOrganizationsDTO, TCreateOrganizationDTO, TGetOrganizationsDTO, @@ -1037,6 +1038,13 @@ export const superAdminServiceFactory = ({ })); }; + const countIdentities = async ({ searchTerm }: TCountIdentitiesDTO) => { + const count = await identityDAL.countIdentitiesByFilter({ + searchTerm + }); + return count; + }; + const grantServerAdminAccessToUser = async (userId: string) => { if (!licenseService.onPremFeatures?.instanceUserManagement) { throw new BadRequestError({ @@ -1145,6 +1153,7 @@ export const superAdminServiceFactory = ({ getUsers, deleteUser, getIdentities, + countIdentities, getAdminIntegrationsConfig, updateRootEncryptionStrategy, getConfiguredEncryptionStrategies, diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 10c8b7878..ff606c1ed 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -38,6 +38,10 @@ export type TCountOrganizationsDTO = { searchTerm: string; }; +export type TCountIdentitiesDTO = { + 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 1ee3bb5a9..c16252fa3 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -8,7 +8,6 @@ import { } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { Identity } from "@app/hooks/api/identities/types"; import { User } from "../types"; import { @@ -17,6 +16,7 @@ import { AdminGetUsersFilters, AdminIntegrationsConfig, TGetEnvOverrides, + TGetIdentitiesResponse, TGetInvalidatingCacheStatus, TGetOrganizationsResponse, TGetServerRootKmsEncryptionDetails, @@ -122,24 +122,21 @@ export const useAdminGetUsers = ( }; export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => { - return useInfiniteQuery({ - initialPageParam: 0, + return useQuery({ queryKey: adminQueryKeys.getIdentities(filters), - queryFn: async ({ pageParam }) => { - const { data } = await apiRequest.get<{ identities: Identity[] }>( + queryFn: async () => { + const { data } = await apiRequest.get( "/api/v1/admin/identity-management/identities", { params: { - ...filters, - offset: pageParam + ...filters } } ); - return data.identities; + return { identities: data.identities, 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 c89f11631..f8c8fbd2d 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -1,3 +1,4 @@ +import { Identity } from "@app/hooks/api/identities/types"; import { OrgMembershipStatus } from "@app/hooks/api/organization/types"; import { Organization } from "../types"; @@ -34,11 +35,18 @@ export type OrganizationWithProjects = Organization & { }[]; }; +type PaginatedDataMeta = { + total: number; +}; + export type TGetOrganizationsResponse = { organizations: OrganizationWithProjects[]; - meta: { - total: number; - }; + meta: PaginatedDataMeta; +}; + +export type TGetIdentitiesResponse = { + identities: Identity[]; + meta: PaginatedDataMeta; }; export type TServerConfig = { @@ -100,8 +108,9 @@ export type AdminGetUsersFilters = { }; export type AdminGetIdentitiesFilters = { - limit: number; - searchTerm: string; + limit?: number; + offset?: number; + searchTerm?: string; }; export type AdminIntegrationsConfig = { @@ -171,6 +180,10 @@ export type TResendOrgInviteDTO = { membershipId: string; }; -export enum AdminOrganizationOrderBy { +export enum AdminOrganizationsOrderBy { + Name = "name" +} + +export enum AdminIdentitiesOrderBy { Name = "name" } diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx index 6370bab54..8cfc697c0 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx @@ -11,7 +11,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { Badge, - Button, DeleteActionModal, DropdownMenu, DropdownMenuContent, @@ -20,6 +19,7 @@ import { EmptyState, IconButton, Input, + Pagination, Table, TableContainer, TableSkeleton, @@ -29,9 +29,15 @@ import { THead, Tr } from "@app/components/v2"; -import { useDebounce, usePopUp } from "@app/hooks"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; +import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useAdminRemoveIdentitySuperAdminAccess } from "@app/hooks/api/admin"; import { useAdminGetIdentities } from "@app/hooks/api/admin/queries"; +import { AdminIdentitiesOrderBy } from "@app/hooks/api/admin/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; const IdentityPanelTable = ({ @@ -48,14 +54,35 @@ const IdentityPanelTable = ({ const [searchIdentityFilter, setSearchIdentityFilter] = useState(""); const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500); - const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetIdentities( - { - limit: 20, - searchTerm: debouncedSearchTerm - } - ); + const { offset, limit, setPage, perPage, page, setPerPage } = + usePagination(AdminIdentitiesOrderBy.Name, { + initPerPage: getUserTablePreference( + "ResourceOverviewIdentitiesTable", + PreferenceKey.PerPage, + 10 + ) + }); - const isEmpty = !isPending && !data?.pages?.[0].length; + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("ResourceOverviewIdentitiesTable", PreferenceKey.PerPage, newPerPage); + }; + + const { data, isPending } = useAdminGetIdentities({ + limit, + offset, + searchTerm: debouncedSearchTerm + }); + + const { identities, totalCount = 0 } = data ?? {}; + + const isEmpty = !isPending && !totalCount; + + useResetPageHelper({ + totalCount, + offset, + setPage + }); return ( <> @@ -80,75 +107,70 @@ const IdentityPanelTable = ({ {isPending && } {!isPending && - data?.pages?.map((identities) => - identities.map(({ name, id, isInstanceAdmin }) => ( - - - {name} - {isInstanceAdmin && ( - - Server Admin - - )} - - - {isInstanceAdmin && ( -
- - - ( + + + {name} + {isInstanceAdmin && ( + + Server Admin + + )} + + + {isInstanceAdmin && ( +
+ + + + + + + + {isInstanceAdmin && ( + { + e.stopPropagation(); + handlePopUpOpen("removeServerAdmin", { name, id }); + }} + icon={ +
+ + +
+ } > - - - - - {isInstanceAdmin && ( - { - e.stopPropagation(); - handlePopUpOpen("removeServerAdmin", { name, id }); - }} - icon={ -
- - -
- } - > - Remove Server Admin -
- )} -
-
-
- )} - - - )) - )} + Remove Server Admin + + )} + +
+
+ )} + + + ))} {!isPending && isEmpty && } - {!isEmpty && ( - + {!isPending && totalCount > 0 && ( + setPage(newPage)} + onChangePerPage={handlePerPageChange} + /> )} diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx index be4abff04..43ff1a48d 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx @@ -62,7 +62,7 @@ import { useServerAdminAccessOrg, useServerAdminResendOrgInvite } from "@app/hooks/api"; -import { AdminOrganizationOrderBy, OrganizationWithProjects } from "@app/hooks/api/admin/types"; +import { AdminOrganizationsOrderBy, 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"; @@ -450,7 +450,7 @@ const OrganizationsPanelTable = ({ const navigate = useNavigate(); const { offset, limit, setPage, perPage, page, setPerPage } = - usePagination(AdminOrganizationOrderBy.Name, { + usePagination(AdminOrganizationsOrderBy.Name, { initPerPage: getUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, 10) });