mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 18:27:19 +00:00
feat: adds pagination in identities table
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-73
@@ -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)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user