From b7b5939c1648fa62b92085a4fa2e2e34a09c2a42 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Tue, 7 Oct 2025 22:59:48 +0530 Subject: [PATCH 1/7] 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} + /> )} Date: Wed, 8 Oct 2025 02:02:19 +0530 Subject: [PATCH 2/7] 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) }); From c91b803933f162fb7f222ced4143ad06379b4387 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Wed, 8 Oct 2025 23:10:22 +0530 Subject: [PATCH 3/7] feat: adds pagination in users table --- backend/src/server/routes/v1/admin-router.ts | 19 +++- .../super-admin/super-admin-service.ts | 11 +++ .../services/super-admin/super-admin-types.ts | 5 ++ backend/src/services/user/user-dal.ts | 30 +++++++ frontend/src/hooks/api/admin/queries.ts | 40 ++------- frontend/src/hooks/api/admin/types.ts | 18 +++- frontend/src/hooks/api/users/types.ts | 44 +++++----- .../components/AddServerAdminModal.tsx | 22 ++--- .../components/ServerAdminsTable.tsx | 86 +++++++++++-------- .../components/AddOrganizationModal.tsx | 21 ++--- .../components/UserIdentitiesTable.tsx | 86 +++++++++++-------- 11 files changed, 225 insertions(+), 157 deletions(-) diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 7b31326e4..eee4694e8 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -172,7 +172,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { email: true, id: true, superAdmin: true - }).array() + }).array(), + meta: z.object({ + total: z.number() + }) }) } }, @@ -186,8 +189,16 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { ...req.query }); + const count = await server.services.superAdmin.countUsers({ + searchTerm: req.query.searchTerm, + adminsOnly: req.query.adminsOnly + }); + return { - users + users, + meta: { + total: count + } }; } }); @@ -247,7 +258,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { ...req.query }); const count = await server.services.superAdmin.countOrganizations({ - ...req.query + searchTerm: req.query.searchTerm }); return { @@ -364,7 +375,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); const count = await server.services.superAdmin.countIdentities({ - ...req.query + searchTerm: req.query.searchTerm }); return { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index d056986e2..6f26b40e9 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -57,6 +57,7 @@ import { TAdminSignUpDTO, TCountIdentitiesDTO, TCountOrganizationsDTO, + TCountUsersDTO, TCreateOrganizationDTO, TGetOrganizationsDTO, TResendOrgInviteDTO @@ -670,6 +671,15 @@ export const superAdminServiceFactory = ({ }); }; + const countUsers = async ({ searchTerm, adminsOnly }: TCountUsersDTO) => { + const count = await userDAL.countUsersByFilter({ + searchTerm, + adminsOnly + }); + + return count; + }; + const deleteUser = async (userId: string) => { const superAdmins = await userDAL.find({ superAdmin: true @@ -1151,6 +1161,7 @@ export const superAdminServiceFactory = ({ adminSignUp, bootstrapInstance, getUsers, + countUsers, deleteUser, getIdentities, countIdentities, diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index ff606c1ed..467b6d808 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -42,6 +42,11 @@ export type TCountIdentitiesDTO = { searchTerm: string; }; +export type TCountUsersDTO = { + searchTerm: string; + adminsOnly: boolean; +}; + export type TCreateOrganizationDTO = { name: string; inviteAdminEmails: string[]; diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 4267d13ee..80f9fa510 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -70,6 +70,35 @@ export const userDALFactory = (db: TDbClient) => { } }; + const countUsersByFilter = async ({ searchTerm, adminsOnly }: { searchTerm: string; adminsOnly: boolean }) => { + interface CountResult { + count: string; + } + try { + const count = await db + .replicaNode()(TableName.Users) + .where("isGhost", "=", false) + .where((qb) => { + if (searchTerm) { + void qb + .whereILike("email", `%${searchTerm}%`) + .orWhereILike("firstName", `%${searchTerm}%`) + .orWhereILike("lastName", `%${searchTerm}%`) + .orWhereRaw('lower("username") like ?', `%${searchTerm}%`); + } + if (adminsOnly) { + void qb.where("superAdmin", true); + } + }) + .count("*") + .first(); + + return parseInt((count as unknown as CountResult).count || "0", 10); + } catch (error) { + throw new DatabaseError({ error, name: "Count Users by filter" }); + } + }; + // USER ENCRYPTION FUNCTIONS // ------------------------- const findUserEncKeyByUsername = async ({ username }: { username: string }) => { @@ -243,6 +272,7 @@ export const userDALFactory = (db: TDbClient) => { findOneUserAction, createUserAction, getUsersByFilter, + countUsersByFilter, findAllMyAccounts, findUserByEmail }; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index c16252fa3..e52931d73 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -1,15 +1,7 @@ -import { - DefaultError, - InfiniteData, - UndefinedInitialDataInfiniteOptions, - useInfiniteQuery, - useQuery, - UseQueryOptions -} from "@tanstack/react-query"; +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { User } from "../types"; import { AdminGetIdentitiesFilters, AdminGetOrganizationsFilters, @@ -20,6 +12,7 @@ import { TGetInvalidatingCacheStatus, TGetOrganizationsResponse, TGetServerRootKmsEncryptionDetails, + TGetUsersResponse, TServerConfig } from "./types"; @@ -87,37 +80,22 @@ export const useGetServerConfig = ({ enabled: options?.enabled ?? true }); -export const useAdminGetUsers = ( - filters: AdminGetUsersFilters, - options?: Partial< - UndefinedInitialDataInfiniteOptions< - User[], - DefaultError, - InfiniteData, - ReturnType, - number - > - > -) => { - return useInfiniteQuery({ - initialPageParam: 0, +export const useAdminGetUsers = (filters: AdminGetUsersFilters) => { + return useQuery({ queryKey: adminQueryKeys.getUsers(filters), - queryFn: async ({ pageParam }) => { - const { data } = await apiRequest.get<{ users: User[] }>( + queryFn: async () => { + const { data } = await apiRequest.get( "/api/v1/admin/user-management/users", { params: { - ...filters, - offset: pageParam + ...filters } } ); - return data.users; + return { users: data.users, totalCount: data.meta.total }; }, - getNextPageParam: (lastPage, pages) => - lastPage.length !== 0 ? pages.length * filters.limit : undefined, - ...options + placeholderData: (previousData) => previousData }); }; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index f8c8fbd2d..1a9a65994 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -1,7 +1,7 @@ import { Identity } from "@app/hooks/api/identities/types"; import { OrgMembershipStatus } from "@app/hooks/api/organization/types"; -import { Organization } from "../types"; +import { Organization, User } from "../types"; export enum LoginMethod { EMAIL = "email", @@ -49,6 +49,11 @@ export type TGetIdentitiesResponse = { meta: PaginatedDataMeta; }; +export type TGetUsersResponse = { + users: User[]; + meta: PaginatedDataMeta; +}; + export type TServerConfig = { initialized: boolean; allowSignUp: boolean; @@ -102,9 +107,10 @@ export type AdminGetOrganizationsFilters = { }; export type AdminGetUsersFilters = { - limit: number; - searchTerm: string; - adminsOnly: boolean; + limit?: number; + offset?: number; + searchTerm?: string; + adminsOnly?: boolean; }; export type AdminGetIdentitiesFilters = { @@ -187,3 +193,7 @@ export enum AdminOrganizationsOrderBy { export enum AdminIdentitiesOrderBy { Name = "name" } + +export enum AdminUsersOrderBy { + Username = "username" +} diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index a59965dfc..f442b4c1f 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -103,29 +103,29 @@ export type TWorkspaceUser = { organization: string; roles: ( | { - id: string; - role: "owner" | "admin" | "member" | "no-access" | "custom"; - customRoleId: string; - customRoleName: string; - customRoleSlug: string; - isTemporary: false; - temporaryRange: null; - temporaryMode: null; - temporaryAccessEndTime: null; - temporaryAccessStartTime: null; - } + id: string; + role: "owner" | "admin" | "member" | "no-access" | "custom"; + customRoleId: string; + customRoleName: string; + customRoleSlug: string; + isTemporary: false; + temporaryRange: null; + temporaryMode: null; + temporaryAccessEndTime: null; + temporaryAccessStartTime: null; + } | { - id: string; - role: "owner" | "admin" | "member" | "no-access" | "custom"; - customRoleId: string; - customRoleName: string; - customRoleSlug: string; - isTemporary: true; - temporaryRange: string; - temporaryMode: ProjectUserMembershipTemporaryMode; - temporaryAccessEndTime: string; - temporaryAccessStartTime: string; - } + id: string; + role: "owner" | "admin" | "member" | "no-access" | "custom"; + customRoleId: string; + customRoleName: string; + customRoleSlug: string; + isTemporary: true; + temporaryRange: string; + temporaryMode: ProjectUserMembershipTemporaryMode; + temporaryAccessEndTime: string; + temporaryAccessStartTime: string; + } )[]; status: "invited" | "accepted" | "verified" | "completed"; deniedPermissions: any[]; diff --git a/frontend/src/pages/admin/AccessManagementPage/components/AddServerAdminModal.tsx b/frontend/src/pages/admin/AccessManagementPage/components/AddServerAdminModal.tsx index aa02d40e8..3e7638627 100644 --- a/frontend/src/pages/admin/AccessManagementPage/components/AddServerAdminModal.tsx +++ b/frontend/src/pages/admin/AccessManagementPage/components/AddServerAdminModal.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -55,18 +55,14 @@ const Content = ({ onClose }: ContentProps) => { const [searchUserFilter, setSearchUserFilter] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500); - const { data, isFetching } = useAdminGetUsers( - { - limit: 20, - searchTerm: debouncedSearchTerm, - adminsOnly: false - }, - { - placeholderData: (prev) => prev - } - ); + const { data, isPending } = useAdminGetUsers({ + limit: 20, + searchTerm: debouncedSearchTerm, + adminsOnly: false + }); - const users = useMemo(() => data?.pages.flat().filter((user) => !user.superAdmin) ?? [], [data]); + const { users: usersData = [] } = data ?? {}; + const users = usersData.filter((user) => !user.superAdmin); const onSubmit = async ({ user }: FormData) => { try { @@ -91,7 +87,7 @@ const Content = ({ onClose }: ContentProps) => { render={({ field, fieldState: { error } }) => ( } {!isEmpty && ( - + setPage(newPage)} + onChangePerPage={handlePerPageChange} + /> )} @@ -278,18 +283,27 @@ export const ServerAdminsTable = () => { const [searchUserFilter, setSearchUserFilter] = useState(""); const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500); - const { - data: users, - isPending, - isFetchingNextPage, - hasNextPage, - fetchNextPage - } = useAdminGetUsers({ - limit: 20, + const { offset, limit, setPage, perPage, page, setPerPage } = usePagination( + AdminUsersOrderBy.Username, + { + initPerPage: getUserTablePreference("ServerAdminUsersTable", PreferenceKey.PerPage, 20) + } + ); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("ServerAdminUsersTable", PreferenceKey.PerPage, newPerPage); + }; + + const { data, isPending } = useAdminGetUsers({ + limit, + offset, searchTerm: debouncedSearchTerm, adminsOnly: true }); + const { users = [], totalCount = 0 } = data ?? {}; + const handleRemoveUser = async () => { const { id } = popUp?.removeUser?.data as { id: string; username: string }; @@ -389,9 +403,11 @@ export const ServerAdminsTable = () => { searchUserFilter={searchUserFilter} setSearchUserFilter={setSearchUserFilter} isPending={isPending} - isFetchingNextPage={isFetchingNextPage} - hasNextPage={hasNextPage} - fetchNextPage={fetchNextPage} + page={page} + perPage={perPage} + setPage={setPage} + handlePerPageChange={handlePerPageChange} + totalCount={totalCount} /> { const [searchUserFilter, setSearchUserFilter] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500); - const { data, isFetching } = useAdminGetUsers( - { - limit: 20, - searchTerm: debouncedSearchTerm, - adminsOnly: false - }, - { - placeholderData: (prev) => prev - } - ); + const { data, isPending } = useAdminGetUsers({ + limit: 20, + searchTerm: debouncedSearchTerm, + adminsOnly: false + }); - const users = useMemo(() => data?.pages.flat() ?? [], [data]); + const { users = [] } = data ?? {}; const onSubmit = async ({ name, invitees }: FormData) => { try { @@ -155,7 +150,7 @@ const Content = ({ onClose }: ContentProps) => { }) .includes(input) } - isLoading={searchUserFilter !== debouncedSearchTerm || isFetching} + isLoading={searchUserFilter !== debouncedSearchTerm || isPending} className="w-full" placeholder="Search users or invite new ones..." isMulti diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx index 9856ee497..7ce5598ac 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx @@ -13,7 +13,6 @@ import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { InfiniteData } from "@tanstack/react-query"; import { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; @@ -31,6 +30,7 @@ import { EmptyState, IconButton, Input, + Pagination, Table, TableContainer, TableSkeleton, @@ -42,7 +42,12 @@ import { Tr } from "@app/components/v2"; import { useSubscription, useUser } from "@app/context"; -import { useDebounce, usePopUp } from "@app/hooks"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; +import { useDebounce, usePagination, usePopUp } from "@app/hooks"; import { useAdminBulkDeleteUsers, useAdminDeleteUser, @@ -50,6 +55,7 @@ import { useAdminGrantServerAdminAccess, useRemoveUserServerAdminAccess } from "@app/hooks/api"; +import { AdminUsersOrderBy } from "@app/hooks/api/admin/types"; import { User } from "@app/hooks/api/users/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -58,17 +64,19 @@ const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions f const UserPanelTable = ({ handlePopUpOpen, - users: usersPages, + users, isPending, adminsOnly, searchUserFilter, setSearchUserFilter, setAdminsOnly, - isFetchingNextPage, - fetchNextPage, - hasNextPage, selectedUsers, - setSelectedUsers + setSelectedUsers, + totalCount, + page, + perPage, + setPage, + handlePerPageChange }: { handlePopUpOpen: ( popUpName: keyof UsePopUpState< @@ -81,22 +89,22 @@ const UserPanelTable = ({ } ) => void; isPending: boolean; - users: InfiniteData | undefined; + users: User[] | undefined; adminsOnly: boolean; setAdminsOnly: (adminsOnly: boolean) => void; searchUserFilter: string; setSearchUserFilter: (filter: string) => void; selectedUsers: User[]; setSelectedUsers: Dispatch>; - isFetchingNextPage: boolean; - fetchNextPage: () => void; - hasNextPage: boolean; + totalCount: number; + page: number; + perPage: number; + setPage: Dispatch>; + handlePerPageChange: (newPerPage: number) => void; }) => { const { subscription } = useSubscription(); - const users = usersPages?.pages.flat(); - - const isEmpty = !isPending && !users?.length; + const isEmpty = !isPending && totalCount === 0; const isTableFiltered = Boolean(adminsOnly); const selectedUserIds = selectedUsers.map((user) => user.id); @@ -302,16 +310,13 @@ const UserPanelTable = ({ {!isPending && isEmpty && } {!isEmpty && ( - + setPage(newPage)} + onChangePerPage={handlePerPageChange} + /> )} @@ -341,18 +346,27 @@ export const UserIdentitiesTable = () => { const [adminsOnly, setAdminsOnly] = useState(false); const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500); - const { - data: users, - isPending, - isFetchingNextPage, - hasNextPage, - fetchNextPage - } = useAdminGetUsers({ - limit: 20, + const { offset, limit, setPage, perPage, page, setPerPage } = usePagination( + AdminUsersOrderBy.Username, + { + initPerPage: getUserTablePreference("ResourceOverviewUsersTable", PreferenceKey.PerPage, 20) + } + ); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("ResourceOverviewUsersTable", PreferenceKey.PerPage, newPerPage); + }; + + const { data, isPending } = useAdminGetUsers({ + limit, + offset, searchTerm: debouncedSearchTerm, adminsOnly }); + const { users, totalCount = 0 } = data ?? {}; + const handleRemoveUser = async () => { const { id } = popUp?.removeUser?.data as { id: string; username: string }; @@ -479,9 +493,11 @@ export const UserIdentitiesTable = () => { isPending={isPending} adminsOnly={adminsOnly} setAdminsOnly={setAdminsOnly} - isFetchingNextPage={isFetchingNextPage} - hasNextPage={hasNextPage} - fetchNextPage={fetchNextPage} + page={page} + perPage={perPage} + setPage={setPage} + handlePerPageChange={handlePerPageChange} + totalCount={totalCount} /> Date: Wed, 8 Oct 2025 23:24:12 +0530 Subject: [PATCH 4/7] fix: greptile review comments --- backend/src/services/identity/identity-dal.ts | 2 +- .../components/MachineIdentitiesTable.tsx | 38 +++++++++---------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts index 596601703..c23e650d0 100644 --- a/backend/src/services/identity/identity-dal.ts +++ b/backend/src/services/identity/identity-dal.ts @@ -69,7 +69,7 @@ export const identityDALFactory = (db: TDbClient) => { .replicaNode()(TableName.Identity) .where((qb) => { if (searchTerm) { - void qb.whereILike(`${TableName.Identity}.name`, `%${searchTerm}%`); + void qb.whereILike(`name`, `%${searchTerm}%`); } }) .count("*") diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx index 8cfc697c0..5417636ea 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx @@ -132,26 +132,24 @@ const IdentityPanelTable = ({ - {isInstanceAdmin && ( - { - e.stopPropagation(); - handlePopUpOpen("removeServerAdmin", { name, id }); - }} - icon={ -
- - -
- } - > - Remove Server Admin -
- )} + { + e.stopPropagation(); + handlePopUpOpen("removeServerAdmin", { name, id }); + }} + icon={ +
+ + +
+ } + > + Remove Server Admin +
From a090a1745cd430bd2101e8799510823d5f74c449 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Thu, 9 Oct 2025 18:55:49 +0530 Subject: [PATCH 5/7] review changes --- backend/src/server/routes/v1/admin-router.ts | 51 ++--------- backend/src/services/identity/identity-dal.ts | 14 ++- backend/src/services/org/org-dal.ts | 91 ++++++++----------- .../super-admin/super-admin-service.ts | 45 ++------- .../services/super-admin/super-admin-types.ts | 13 --- backend/src/services/user/user-dal.ts | 41 ++------- frontend/src/hooks/api/admin/queries.ts | 6 +- frontend/src/hooks/api/admin/types.ts | 10 +- 8 files changed, 87 insertions(+), 184 deletions(-) diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index eee4694e8..03dd8ed94 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -173,9 +173,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { id: true, superAdmin: true }).array(), - meta: z.object({ - total: z.number() - }) + total: z.number() }) } }, @@ -185,21 +183,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); }, handler: async (req) => { - const users = await server.services.superAdmin.getUsers({ + const result = await server.services.superAdmin.getUsers({ ...req.query }); - const count = await server.services.superAdmin.countUsers({ - searchTerm: req.query.searchTerm, - adminsOnly: req.query.adminsOnly - }); - - return { - users, - meta: { - total: count - } - }; + return result; } }); @@ -242,9 +230,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }) .array() }).array(), - meta: z.object({ - total: z.number() - }) + total: z.number() }) } }, @@ -254,19 +240,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); }, handler: async (req) => { - const organizations = await server.services.superAdmin.getOrganizations({ + const result = await server.services.superAdmin.getOrganizations({ ...req.query }); - const count = await server.services.superAdmin.countOrganizations({ - searchTerm: req.query.searchTerm - }); - return { - organizations, - meta: { - total: count - } - }; + return result; } }); @@ -358,9 +336,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { isInstanceAdmin: z.boolean() }) .array(), - meta: z.object({ - total: z.number() - }) + total: z.number() }) } }, @@ -370,20 +346,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); }, handler: async (req) => { - const identities = await server.services.superAdmin.getIdentities({ + const result = await server.services.superAdmin.getIdentities({ ...req.query }); - const count = await server.services.superAdmin.countIdentities({ - searchTerm: req.query.searchTerm - }); - - return { - identities, - meta: { - total: count - } - }; + return result; } }); diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts index c23e650d0..56224bd57 100644 --- a/backend/src/services/identity/identity-dal.ts +++ b/backend/src/services/identity/identity-dal.ts @@ -50,11 +50,23 @@ export const identityDALFactory = (db: TDbClient) => { }); } + const countQuery = query.clone(); + if (sortBy) { query = query.orderBy(sortBy); } - return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Identity)); + const [identities, totalResult] = await Promise.all([ + query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Identity)), + countQuery.countDistinct(`${TableName.Identity}.id`, { as: "count" }).first() + ]); + + const total = Number(totalResult?.count || 0); + + return { + identities, + total + }; } catch (error) { throw new DatabaseError({ error, name: "Get identities by filter" }); } diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 6f91d2dc3..6ac363c76 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -43,8 +43,6 @@ export const orgDALFactory = (db: TDbClient) => { sortBy?: keyof TOrganizations; }) => { try { - const query = db.replicaNode()(TableName.Organization); - // Build the subquery for limited organization IDs const orgSubquery = db.replicaNode().select("id").from(TableName.Organization); @@ -54,37 +52,45 @@ export const orgDALFactory = (db: TDbClient) => { }); } + const countQuery = orgSubquery.clone(); + if (sortBy) { void orgSubquery.orderBy(sortBy); } - void orgSubquery.limit(limit).offset(offset); - // Main query with joins, limited to the subquery results - const docs = await query - .whereIn(`${TableName.Organization}.id`, orgSubquery) - .leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) - .leftJoin(TableName.OrgMembership, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) - .leftJoin(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`) - .where((qb) => { - void qb.where(`${TableName.Users}.isGhost`, false).orWhereNull(`${TableName.Users}.id`); - }) - .select(selectAllTableCols(TableName.Organization)) - .select(db.ref("name").withSchema(TableName.Project).as("projectName")) - .select(db.ref("id").withSchema(TableName.Project).as("projectId")) - .select(db.ref("slug").withSchema(TableName.Project).as("projectSlug")) - .select(db.ref("createdAt").withSchema(TableName.Project).as("projectCreatedAt")) - .select(db.ref("email").withSchema(TableName.Users).as("userEmail")) - .select(db.ref("username").withSchema(TableName.Users).as("username")) - .select(db.ref("firstName").withSchema(TableName.Users).as("firstName")) - .select(db.ref("lastName").withSchema(TableName.Users).as("lastName")) - .select(db.ref("id").withSchema(TableName.Users).as("userId")) - .select(db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId")) - .select(db.ref("role").withSchema(TableName.OrgMembership).as("orgMembershipRole")) - .select(db.ref("roleId").withSchema(TableName.OrgMembership).as("orgMembershipRoleId")) - .select(db.ref("status").withSchema(TableName.OrgMembership).as("orgMembershipStatus")) - .select(db.ref("name").withSchema(TableName.OrgRoles).as("orgMembershipRoleName")); + const buildBaseQuery = (orgIdSubquery: Knex.QueryBuilder) => { + return db + .replicaNode()(TableName.Organization) + .whereIn(`${TableName.Organization}.id`, orgIdSubquery) + .leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) + .leftJoin(TableName.OrgMembership, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) + .leftJoin(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`) + .where((qb) => { + void qb.where(`${TableName.Users}.isGhost`, false).orWhereNull(`${TableName.Users}.id`); + }); + }; + + const [docs, totalResult] = await Promise.all([ + buildBaseQuery(orgSubquery) + .select(selectAllTableCols(TableName.Organization)) + .select(db.ref("name").withSchema(TableName.Project).as("projectName")) + .select(db.ref("id").withSchema(TableName.Project).as("projectId")) + .select(db.ref("slug").withSchema(TableName.Project).as("projectSlug")) + .select(db.ref("createdAt").withSchema(TableName.Project).as("projectCreatedAt")) + .select(db.ref("email").withSchema(TableName.Users).as("userEmail")) + .select(db.ref("username").withSchema(TableName.Users).as("username")) + .select(db.ref("firstName").withSchema(TableName.Users).as("firstName")) + .select(db.ref("lastName").withSchema(TableName.Users).as("lastName")) + .select(db.ref("id").withSchema(TableName.Users).as("userId")) + .select(db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId")) + .select(db.ref("role").withSchema(TableName.OrgMembership).as("orgMembershipRole")) + .select(db.ref("roleId").withSchema(TableName.OrgMembership).as("orgMembershipRoleId")) + .select(db.ref("status").withSchema(TableName.OrgMembership).as("orgMembershipStatus")) + .select(db.ref("name").withSchema(TableName.OrgRoles).as("orgMembershipRoleName")), + buildBaseQuery(countQuery).countDistinct(`${TableName.Organization}.id`, { as: "count" }).first() + ]); const formattedDocs = sqlNestRelationships({ data: docs, @@ -132,33 +138,17 @@ export const orgDALFactory = (db: TDbClient) => { ] }); - return formattedDocs; + const total = Number(totalResult?.count || 0); + + return { + organizations: formattedDocs, + total + }; } catch (error) { throw new DatabaseError({ error, name: "Find organizations by filter" }); } }; - 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 @@ -691,7 +681,6 @@ 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 6f26b40e9..8897a1b3f 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -55,9 +55,6 @@ import { TAdminGetUsersDTO, TAdminIntegrationConfig, TAdminSignUpDTO, - TCountIdentitiesDTO, - TCountOrganizationsDTO, - TCountUsersDTO, TCreateOrganizationDTO, TGetOrganizationsDTO, TResendOrgInviteDTO @@ -671,15 +668,6 @@ export const superAdminServiceFactory = ({ }); }; - const countUsers = async ({ searchTerm, adminsOnly }: TCountUsersDTO) => { - const count = await userDAL.countUsersByFilter({ - searchTerm, - adminsOnly - }); - - return count; - }; - const deleteUser = async (userId: string) => { const superAdmins = await userDAL.find({ superAdmin: true @@ -753,20 +741,12 @@ export const superAdminServiceFactory = ({ }; const getOrganizations = async ({ offset, limit, searchTerm }: TGetOrganizationsDTO) => { - const organizations = await orgDAL.findOrganizationsByFilter({ + return orgDAL.findOrganizationsByFilter({ offset, searchTerm, sortBy: "name", limit }); - return organizations; - }; - - const countOrganizations = async ({ searchTerm }: TCountOrganizationsDTO) => { - const count = await orgDAL.countOrganizationsByFilter({ - searchTerm - }); - return count; }; const createOrganization = async ( @@ -1034,7 +1014,7 @@ export const superAdminServiceFactory = ({ }; const getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => { - const identities = await identityDAL.getIdentitiesByFilter({ + const result = await identityDAL.getIdentitiesByFilter({ limit, offset, searchTerm, @@ -1042,17 +1022,13 @@ export const superAdminServiceFactory = ({ }); const serverCfg = await getServerCfg(); - return identities.map((identity) => ({ - ...identity, - isInstanceAdmin: Boolean(serverCfg?.adminIdentityIds?.includes(identity.id)) - })); - }; - - const countIdentities = async ({ searchTerm }: TCountIdentitiesDTO) => { - const count = await identityDAL.countIdentitiesByFilter({ - searchTerm - }); - return count; + return { + identities: result.identities.map((identity) => ({ + ...identity, + isInstanceAdmin: Boolean(serverCfg?.adminIdentityIds?.includes(identity.id)) + })), + total: result.total + }; }; const grantServerAdminAccessToUser = async (userId: string) => { @@ -1161,10 +1137,8 @@ export const superAdminServiceFactory = ({ adminSignUp, bootstrapInstance, getUsers, - countUsers, deleteUser, getIdentities, - countIdentities, getAdminIntegrationsConfig, updateRootEncryptionStrategy, getConfiguredEncryptionStrategies, @@ -1174,7 +1148,6 @@ 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 467b6d808..6cfdd384d 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -34,19 +34,6 @@ export type TGetOrganizationsDTO = { searchTerm: string; }; -export type TCountOrganizationsDTO = { - searchTerm: string; -}; - -export type TCountIdentitiesDTO = { - searchTerm: string; -}; - -export type TCountUsersDTO = { - searchTerm: string; - adminsOnly: boolean; -}; - export type TCreateOrganizationDTO = { name: string; inviteAdminEmails: string[]; diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 80f9fa510..0da8a46a8 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -60,45 +60,25 @@ export const userDALFactory = (db: TDbClient) => { query = query.where("superAdmin", true); } + const countQuery = query.clone(); + if (sortBy) { query = query.orderBy(sortBy); } - return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Users)); + const [users, totalResult] = await Promise.all([ + query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Users)), + countQuery.count("*", { as: "count" }).first() + ]); + + const total = Number(totalResult?.count || 0); + + return { users, total }; } catch (error) { throw new DatabaseError({ error, name: "Get users by filter" }); } }; - const countUsersByFilter = async ({ searchTerm, adminsOnly }: { searchTerm: string; adminsOnly: boolean }) => { - interface CountResult { - count: string; - } - try { - const count = await db - .replicaNode()(TableName.Users) - .where("isGhost", "=", false) - .where((qb) => { - if (searchTerm) { - void qb - .whereILike("email", `%${searchTerm}%`) - .orWhereILike("firstName", `%${searchTerm}%`) - .orWhereILike("lastName", `%${searchTerm}%`) - .orWhereRaw('lower("username") like ?', `%${searchTerm}%`); - } - if (adminsOnly) { - void qb.where("superAdmin", true); - } - }) - .count("*") - .first(); - - return parseInt((count as unknown as CountResult).count || "0", 10); - } catch (error) { - throw new DatabaseError({ error, name: "Count Users by filter" }); - } - }; - // USER ENCRYPTION FUNCTIONS // ------------------------- const findUserEncKeyByUsername = async ({ username }: { username: string }) => { @@ -272,7 +252,6 @@ export const userDALFactory = (db: TDbClient) => { findOneUserAction, createUserAction, getUsersByFilter, - countUsersByFilter, findAllMyAccounts, findUserByEmail }; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index e52931d73..ea7226ad6 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -54,7 +54,7 @@ export const useAdminGetOrganizations = (filters: AdminGetOrganizationsFilters) } ); - return { organizations: data.organizations, totalCount: data.meta.total }; + return { organizations: data.organizations, totalCount: data.total }; }, placeholderData: (previousData) => previousData }); @@ -93,7 +93,7 @@ export const useAdminGetUsers = (filters: AdminGetUsersFilters) => { } ); - return { users: data.users, totalCount: data.meta.total }; + return { users: data.users, totalCount: data.total }; }, placeholderData: (previousData) => previousData }); @@ -112,7 +112,7 @@ export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => { } ); - return { identities: data.identities, totalCount: data.meta.total }; + return { identities: data.identities, totalCount: data.total }; }, placeholderData: (previousData) => previousData }); diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 1a9a65994..ca27c0f23 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -35,23 +35,19 @@ export type OrganizationWithProjects = Organization & { }[]; }; -type PaginatedDataMeta = { - total: number; -}; - export type TGetOrganizationsResponse = { organizations: OrganizationWithProjects[]; - meta: PaginatedDataMeta; + total: number; }; export type TGetIdentitiesResponse = { identities: Identity[]; - meta: PaginatedDataMeta; + total: number; }; export type TGetUsersResponse = { users: User[]; - meta: PaginatedDataMeta; + total: number; }; export type TServerConfig = { From 99bebcb97b9eecc659bb642fbe6b7afdcd98b16f Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Thu, 9 Oct 2025 19:03:44 +0530 Subject: [PATCH 6/7] chore: refactor --- backend/src/services/identity/identity-dal.ts | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts index 56224bd57..a2e4995ca 100644 --- a/backend/src/services/identity/identity-dal.ts +++ b/backend/src/services/identity/identity-dal.ts @@ -72,26 +72,5 @@ export const identityDALFactory = (db: TDbClient) => { } }; - 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(`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 }; + return { ...identityOrm, getTrustedIpsByAuthMethod, getIdentitiesByFilter }; }; From 3963a0bb6f5494a60e2a4fc1485d11a3fae8e557 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Thu, 9 Oct 2025 19:08:18 +0530 Subject: [PATCH 7/7] chore: removed orderBy in server admin tables --- frontend/src/hooks/api/admin/types.ts | 12 ------------ .../components/ServerAdminsTable.tsx | 10 +++------- .../components/MachineIdentitiesTable.tsx | 16 +++++++--------- .../components/OrganizationsTable.tsx | 9 ++++----- .../components/UserIdentitiesTable.tsx | 10 +++------- 5 files changed, 17 insertions(+), 40 deletions(-) diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index ca27c0f23..3b658cfa5 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -181,15 +181,3 @@ export type TResendOrgInviteDTO = { organizationId: string; membershipId: string; }; - -export enum AdminOrganizationsOrderBy { - Name = "name" -} - -export enum AdminIdentitiesOrderBy { - Name = "name" -} - -export enum AdminUsersOrderBy { - Username = "username" -} diff --git a/frontend/src/pages/admin/AccessManagementPage/components/ServerAdminsTable.tsx b/frontend/src/pages/admin/AccessManagementPage/components/ServerAdminsTable.tsx index 5b1110648..f50656a0e 100644 --- a/frontend/src/pages/admin/AccessManagementPage/components/ServerAdminsTable.tsx +++ b/frontend/src/pages/admin/AccessManagementPage/components/ServerAdminsTable.tsx @@ -51,7 +51,6 @@ import { useAdminGetUsers, useRemoveUserServerAdminAccess } from "@app/hooks/api"; -import { AdminUsersOrderBy } from "@app/hooks/api/admin/types"; import { User } from "@app/hooks/api/users/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { AddServerAdminModal } from "@app/pages/admin/AccessManagementPage/components/AddServerAdminModal"; @@ -283,12 +282,9 @@ export const ServerAdminsTable = () => { const [searchUserFilter, setSearchUserFilter] = useState(""); const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500); - const { offset, limit, setPage, perPage, page, setPerPage } = usePagination( - AdminUsersOrderBy.Username, - { - initPerPage: getUserTablePreference("ServerAdminUsersTable", PreferenceKey.PerPage, 20) - } - ); + const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", { + initPerPage: getUserTablePreference("ServerAdminUsersTable", PreferenceKey.PerPage, 20) + }); const handlePerPageChange = (newPerPage: number) => { setPerPage(newPerPage); diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx index 5417636ea..920c8b6c1 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx @@ -37,7 +37,6 @@ import { 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 = ({ @@ -54,14 +53,13 @@ const IdentityPanelTable = ({ const [searchIdentityFilter, setSearchIdentityFilter] = useState(""); const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500); - const { offset, limit, setPage, perPage, page, setPerPage } = - usePagination(AdminIdentitiesOrderBy.Name, { - initPerPage: getUserTablePreference( - "ResourceOverviewIdentitiesTable", - PreferenceKey.PerPage, - 10 - ) - }); + const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", { + initPerPage: getUserTablePreference( + "ResourceOverviewIdentitiesTable", + PreferenceKey.PerPage, + 10 + ) + }); const handlePerPageChange = (newPerPage: number) => { setPerPage(newPerPage); diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx index 43ff1a48d..ba1c4500a 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 { AdminOrganizationsOrderBy, OrganizationWithProjects } from "@app/hooks/api/admin/types"; +import { 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"; @@ -449,10 +449,9 @@ const OrganizationsPanelTable = ({ const navigate = useNavigate(); - const { offset, limit, setPage, perPage, page, setPerPage } = - usePagination(AdminOrganizationsOrderBy.Name, { - initPerPage: getUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, 10) - }); + const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", { + initPerPage: getUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, 10) + }); const handlePerPageChange = (newPerPage: number) => { setPerPage(newPerPage); diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx index 7ce5598ac..8c0046328 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/UserIdentitiesTable.tsx @@ -55,7 +55,6 @@ import { useAdminGrantServerAdminAccess, useRemoveUserServerAdminAccess } from "@app/hooks/api"; -import { AdminUsersOrderBy } from "@app/hooks/api/admin/types"; import { User } from "@app/hooks/api/users/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -346,12 +345,9 @@ export const UserIdentitiesTable = () => { const [adminsOnly, setAdminsOnly] = useState(false); const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500); - const { offset, limit, setPage, perPage, page, setPerPage } = usePagination( - AdminUsersOrderBy.Username, - { - initPerPage: getUserTablePreference("ResourceOverviewUsersTable", PreferenceKey.PerPage, 20) - } - ); + const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", { + initPerPage: getUserTablePreference("ResourceOverviewUsersTable", PreferenceKey.PerPage, 20) + }); const handlePerPageChange = (newPerPage: number) => { setPerPage(newPerPage);