feat: adds pagination in identities table

This commit is contained in:
Piyush Gupta
2025-10-08 02:02:19 +05:30
parent 80754fe0b3
commit e7990c493b
8 changed files with 170 additions and 94 deletions
+12 -2
View File
@@ -346,7 +346,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
.extend({ .extend({
isInstanceAdmin: z.boolean() isInstanceAdmin: z.boolean()
}) })
.array() .array(),
meta: z.object({
total: z.number()
})
}) })
} }
}, },
@@ -360,8 +363,15 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
...req.query ...req.query
}); });
const count = await server.services.superAdmin.countIdentities({
...req.query
});
return { return {
identities identities,
meta: {
total: count
}
}; };
} }
}); });
+22 -1
View File
@@ -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 };
}; };
@@ -55,6 +55,7 @@ import {
TAdminGetUsersDTO, TAdminGetUsersDTO,
TAdminIntegrationConfig, TAdminIntegrationConfig,
TAdminSignUpDTO, TAdminSignUpDTO,
TCountIdentitiesDTO,
TCountOrganizationsDTO, TCountOrganizationsDTO,
TCreateOrganizationDTO, TCreateOrganizationDTO,
TGetOrganizationsDTO, 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) => { const grantServerAdminAccessToUser = async (userId: string) => {
if (!licenseService.onPremFeatures?.instanceUserManagement) { if (!licenseService.onPremFeatures?.instanceUserManagement) {
throw new BadRequestError({ throw new BadRequestError({
@@ -1145,6 +1153,7 @@ export const superAdminServiceFactory = ({
getUsers, getUsers,
deleteUser, deleteUser,
getIdentities, getIdentities,
countIdentities,
getAdminIntegrationsConfig, getAdminIntegrationsConfig,
updateRootEncryptionStrategy, updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies, getConfiguredEncryptionStrategies,
@@ -38,6 +38,10 @@ export type TCountOrganizationsDTO = {
searchTerm: string; searchTerm: string;
}; };
export type TCountIdentitiesDTO = {
searchTerm: string;
};
export type TCreateOrganizationDTO = { export type TCreateOrganizationDTO = {
name: string; name: string;
inviteAdminEmails: string[]; inviteAdminEmails: string[];
+7 -10
View File
@@ -8,7 +8,6 @@ import {
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { Identity } from "@app/hooks/api/identities/types";
import { User } from "../types"; import { User } from "../types";
import { import {
@@ -17,6 +16,7 @@ import {
AdminGetUsersFilters, AdminGetUsersFilters,
AdminIntegrationsConfig, AdminIntegrationsConfig,
TGetEnvOverrides, TGetEnvOverrides,
TGetIdentitiesResponse,
TGetInvalidatingCacheStatus, TGetInvalidatingCacheStatus,
TGetOrganizationsResponse, TGetOrganizationsResponse,
TGetServerRootKmsEncryptionDetails, TGetServerRootKmsEncryptionDetails,
@@ -122,24 +122,21 @@ export const useAdminGetUsers = (
}; };
export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => { export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => {
return useInfiniteQuery({ return useQuery({
initialPageParam: 0,
queryKey: adminQueryKeys.getIdentities(filters), queryKey: adminQueryKeys.getIdentities(filters),
queryFn: async ({ pageParam }) => { queryFn: async () => {
const { data } = await apiRequest.get<{ identities: Identity[] }>( const { data } = await apiRequest.get<TGetIdentitiesResponse>(
"/api/v1/admin/identity-management/identities", "/api/v1/admin/identity-management/identities",
{ {
params: { params: {
...filters, ...filters
offset: pageParam
} }
} }
); );
return data.identities; return { identities: data.identities, totalCount: data.meta.total };
}, },
getNextPageParam: (lastPage, pages) => placeholderData: (previousData) => previousData
lastPage.length !== 0 ? pages.length * filters.limit : undefined
}); });
}; };
+19 -6
View File
@@ -1,3 +1,4 @@
import { Identity } from "@app/hooks/api/identities/types";
import { OrgMembershipStatus } from "@app/hooks/api/organization/types"; import { OrgMembershipStatus } from "@app/hooks/api/organization/types";
import { Organization } from "../types"; import { Organization } from "../types";
@@ -34,11 +35,18 @@ export type OrganizationWithProjects = Organization & {
}[]; }[];
}; };
type PaginatedDataMeta = {
total: number;
};
export type TGetOrganizationsResponse = { export type TGetOrganizationsResponse = {
organizations: OrganizationWithProjects[]; organizations: OrganizationWithProjects[];
meta: { meta: PaginatedDataMeta;
total: number; };
};
export type TGetIdentitiesResponse = {
identities: Identity[];
meta: PaginatedDataMeta;
}; };
export type TServerConfig = { export type TServerConfig = {
@@ -100,8 +108,9 @@ export type AdminGetUsersFilters = {
}; };
export type AdminGetIdentitiesFilters = { export type AdminGetIdentitiesFilters = {
limit: number; limit?: number;
searchTerm: string; offset?: number;
searchTerm?: string;
}; };
export type AdminIntegrationsConfig = { export type AdminIntegrationsConfig = {
@@ -171,6 +180,10 @@ export type TResendOrgInviteDTO = {
membershipId: string; membershipId: string;
}; };
export enum AdminOrganizationOrderBy { export enum AdminOrganizationsOrderBy {
Name = "name"
}
export enum AdminIdentitiesOrderBy {
Name = "name" Name = "name"
} }
@@ -11,7 +11,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Badge, Badge,
Button,
DeleteActionModal, DeleteActionModal,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -20,6 +19,7 @@ import {
EmptyState, EmptyState,
IconButton, IconButton,
Input, Input,
Pagination,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -29,9 +29,15 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } 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 { useAdminRemoveIdentitySuperAdminAccess } from "@app/hooks/api/admin";
import { useAdminGetIdentities } from "@app/hooks/api/admin/queries"; import { useAdminGetIdentities } from "@app/hooks/api/admin/queries";
import { AdminIdentitiesOrderBy } from "@app/hooks/api/admin/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const IdentityPanelTable = ({ const IdentityPanelTable = ({
@@ -48,14 +54,35 @@ const IdentityPanelTable = ({
const [searchIdentityFilter, setSearchIdentityFilter] = useState(""); const [searchIdentityFilter, setSearchIdentityFilter] = useState("");
const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500); const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500);
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetIdentities( const { offset, limit, setPage, perPage, page, setPerPage } =
{ usePagination<AdminIdentitiesOrderBy>(AdminIdentitiesOrderBy.Name, {
limit: 20, initPerPage: getUserTablePreference(
searchTerm: debouncedSearchTerm "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 ( return (
<> <>
@@ -80,75 +107,70 @@ const IdentityPanelTable = ({
<TBody> <TBody>
{isPending && <TableSkeleton columns={2} innerKey="identities" />} {isPending && <TableSkeleton columns={2} innerKey="identities" />}
{!isPending && {!isPending &&
data?.pages?.map((identities) => identities?.map(({ name, id, isInstanceAdmin }) => (
identities.map(({ name, id, isInstanceAdmin }) => ( <Tr key={`identity-${id}`} className="w-full">
<Tr key={`identity-${id}`} className="w-full"> <Td>
<Td> {name}
{name} {isInstanceAdmin && (
{isInstanceAdmin && ( <Badge variant="primary" className="ml-2">
<Badge variant="primary" className="ml-2"> Server Admin
Server Admin </Badge>
</Badge> )}
)} </Td>
</Td> <Td>
<Td> {isInstanceAdmin && (
{isInstanceAdmin && ( <div className="flex justify-end">
<div className="flex justify-end"> <DropdownMenu>
<DropdownMenu> <DropdownMenuTrigger asChild>
<DropdownMenuTrigger asChild> <IconButton
<IconButton ariaLabel="Options"
ariaLabel="Options" colorSchema="secondary"
colorSchema="secondary" className="w-6"
className="w-6" variant="plain"
variant="plain" >
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
{isInstanceAdmin && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeServerAdmin", { name, id });
}}
icon={
<div className="relative">
<FontAwesomeIcon icon={faShieldHalved} />
<FontAwesomeIcon
className="absolute -bottom-[0.01rem] -right-1"
size="2xs"
icon={faXmark}
/>
</div>
}
> >
<FontAwesomeIcon icon={faEllipsisV} /> Remove Server Admin
</IconButton> </DropdownMenuItem>
</DropdownMenuTrigger> )}
<DropdownMenuContent sideOffset={2} align="end"> </DropdownMenuContent>
{isInstanceAdmin && ( </DropdownMenu>
<DropdownMenuItem </div>
onClick={(e) => { )}
e.stopPropagation(); </Td>
handlePopUpOpen("removeServerAdmin", { name, id }); </Tr>
}} ))}
icon={
<div className="relative">
<FontAwesomeIcon icon={faShieldHalved} />
<FontAwesomeIcon
className="absolute -bottom-[0.01rem] -right-1"
size="2xs"
icon={faXmark}
/>
</div>
}
>
Remove Server Admin
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</Td>
</Tr>
))
)}
</TBody> </TBody>
</Table> </Table>
{!isPending && isEmpty && <EmptyState title="No identities found" icon={faWrench} />} {!isPending && isEmpty && <EmptyState title="No identities found" icon={faWrench} />}
</TableContainer> </TableContainer>
{!isEmpty && ( {!isPending && totalCount > 0 && (
<Button <Pagination
className="mt-4 py-3 text-sm" count={totalCount}
isFullWidth page={page}
variant="outline_bg" perPage={perPage}
isLoading={isFetchingNextPage} onChangePage={(newPage) => setPage(newPage)}
isDisabled={isFetchingNextPage || !hasNextPage} onChangePerPage={handlePerPageChange}
onClick={() => fetchNextPage()} />
>
{hasNextPage ? "Load More" : "End of List"}
</Button>
)} )}
</div> </div>
</> </>
@@ -62,7 +62,7 @@ import {
useServerAdminAccessOrg, useServerAdminAccessOrg,
useServerAdminResendOrgInvite useServerAdminResendOrgInvite
} from "@app/hooks/api"; } 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 { OrderByDirection } from "@app/hooks/api/generic/types";
import { OrgMembershipStatus } from "@app/hooks/api/organization/types"; import { OrgMembershipStatus } from "@app/hooks/api/organization/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -450,7 +450,7 @@ const OrganizationsPanelTable = ({
const navigate = useNavigate(); const navigate = useNavigate();
const { offset, limit, setPage, perPage, page, setPerPage } = const { offset, limit, setPage, perPage, page, setPerPage } =
usePagination<AdminOrganizationOrderBy>(AdminOrganizationOrderBy.Name, { usePagination<AdminOrganizationsOrderBy>(AdminOrganizationsOrderBy.Name, {
initPerPage: getUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, 10) initPerPage: getUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, 10)
}); });