Merge pull request #4629 from Infisical/feat/pagination-in-resource-overview-tables

feat: adds pagination to resource overview tables
This commit is contained in:
Piyush Gupta
2025-10-09 21:17:25 +05:30
committed by GitHub
14 changed files with 489 additions and 416 deletions
+12 -15
View File
@@ -172,7 +172,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
email: true,
id: true,
superAdmin: true
}).array()
}).array(),
total: z.number()
})
}
},
@@ -182,13 +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
});
return {
users
};
return result;
}
});
@@ -230,7 +229,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
createdAt: z.date()
})
.array()
}).array()
}).array(),
total: z.number()
})
}
},
@@ -240,13 +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
});
return {
organizations
};
return result;
}
});
@@ -337,7 +335,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
.extend({
isInstanceAdmin: z.boolean()
})
.array()
.array(),
total: z.number()
})
}
},
@@ -347,13 +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
});
return {
identities
};
return result;
}
});
+13 -1
View File
@@ -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" });
}
+20 -9
View File
@@ -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,22 +52,28 @@ 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)
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"))
@@ -84,7 +88,9 @@ export const orgDALFactory = (db: TDbClient) => {
.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"));
.select(db.ref("name").withSchema(TableName.OrgRoles).as("orgMembershipRoleName")),
buildBaseQuery(countQuery).countDistinct(`${TableName.Organization}.id`, { as: "count" }).first()
]);
const formattedDocs = sqlNestRelationships({
data: docs,
@@ -132,7 +138,12 @@ 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" });
}
@@ -741,13 +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 createOrganization = async (
@@ -1015,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,
@@ -1023,10 +1022,13 @@ export const superAdminServiceFactory = ({
});
const serverCfg = await getServerCfg();
return identities.map((identity) => ({
return {
identities: result.identities.map((identity) => ({
...identity,
isInstanceAdmin: Boolean(serverCfg?.adminIdentityIds?.includes(identity.id))
}));
})),
total: result.total
};
};
const grantServerAdminAccessToUser = async (userId: string) => {
+10 -1
View File
@@ -60,11 +60,20 @@ 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" });
}
+23 -51
View File
@@ -1,25 +1,18 @@
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 { Identity } from "@app/hooks/api/identities/types";
import { User } from "../types";
import {
AdminGetIdentitiesFilters,
AdminGetOrganizationsFilters,
AdminGetUsersFilters,
AdminIntegrationsConfig,
OrganizationWithProjects,
TGetEnvOverrides,
TGetIdentitiesResponse,
TGetInvalidatingCacheStatus,
TGetOrganizationsResponse,
TGetServerRootKmsEncryptionDetails,
TGetUsersResponse,
TServerConfig
} from "./types";
@@ -49,24 +42,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<TGetOrganizationsResponse>(
"/api/v1/admin/organization-management/organizations",
{
params: {
...filters,
offset: pageParam
...filters
}
}
);
return data.organizations;
return { organizations: data.organizations, totalCount: data.total };
},
getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * filters.limit : undefined
placeholderData: (previousData) => previousData
});
};
@@ -90,59 +80,41 @@ export const useGetServerConfig = ({
enabled: options?.enabled ?? true
});
export const useAdminGetUsers = (
filters: AdminGetUsersFilters,
options?: Partial<
UndefinedInitialDataInfiniteOptions<
User[],
DefaultError,
InfiniteData<User[]>,
ReturnType<typeof adminQueryKeys.getUsers>,
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<TGetUsersResponse>(
"/api/v1/admin/user-management/users",
{
params: {
...filters,
offset: pageParam
...filters
}
}
);
return data.users;
return { users: data.users, totalCount: data.total };
},
getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * filters.limit : undefined,
...options
placeholderData: (previousData) => previousData
});
};
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<TGetIdentitiesResponse>(
"/api/v1/admin/identity-management/identities",
{
params: {
...filters,
offset: pageParam
...filters
}
}
);
return data.identities;
return { identities: data.identities, totalCount: data.total };
},
getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * filters.limit : undefined
placeholderData: (previousData) => previousData
});
};
+27 -8
View File
@@ -1,6 +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",
@@ -34,6 +35,21 @@ export type OrganizationWithProjects = Organization & {
}[];
};
export type TGetOrganizationsResponse = {
organizations: OrganizationWithProjects[];
total: number;
};
export type TGetIdentitiesResponse = {
identities: Identity[];
total: number;
};
export type TGetUsersResponse = {
users: User[];
total: number;
};
export type TServerConfig = {
initialized: boolean;
allowSignUp: boolean;
@@ -81,19 +97,22 @@ export type TCreateAdminUserDTO = {
};
export type AdminGetOrganizationsFilters = {
limit: number;
searchTerm: string;
searchTerm?: string;
limit?: number;
offset?: number;
};
export type AdminGetUsersFilters = {
limit: number;
searchTerm: string;
adminsOnly: boolean;
limit?: number;
offset?: number;
searchTerm?: string;
adminsOnly?: boolean;
};
export type AdminGetIdentitiesFilters = {
limit: number;
searchTerm: string;
limit?: number;
offset?: number;
searchTerm?: string;
};
export type AdminIntegrationsConfig = {
@@ -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(
{
const { data, isPending } = useAdminGetUsers({
limit: 20,
searchTerm: debouncedSearchTerm,
adminsOnly: false
},
{
placeholderData: (prev) => prev
}
);
});
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 } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="User">
<FilterableSelect
isLoading={searchUserFilter !== debouncedSearchTerm || isFetching}
isLoading={searchUserFilter !== debouncedSearchTerm || isPending}
className="w-full"
placeholder="Search users..."
options={users}
@@ -11,7 +11,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";
@@ -28,6 +27,7 @@ import {
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TableSkeleton,
@@ -39,7 +39,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,
@@ -54,15 +59,17 @@ const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions f
const ServerAdminsPanelTable = ({
handlePopUpOpen,
users: usersPages,
users,
isPending,
searchUserFilter,
setSearchUserFilter,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
selectedUsers,
setSelectedUsers
setSelectedUsers,
totalCount,
page,
perPage,
setPage,
handlePerPageChange
}: {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
@@ -75,20 +82,20 @@ const ServerAdminsPanelTable = ({
}
) => void;
isPending: boolean;
users: InfiniteData<User[], unknown> | undefined;
users?: User[];
searchUserFilter: string;
setSearchUserFilter: (filter: string) => void;
selectedUsers: User[];
setSelectedUsers: Dispatch<SetStateAction<User[]>>;
isFetchingNextPage: boolean;
fetchNextPage: () => void;
hasNextPage: boolean;
totalCount: number;
page: number;
perPage: number;
setPage: Dispatch<SetStateAction<number>>;
handlePerPageChange: (newPerPage: number) => void;
}) => {
const { subscription } = useSubscription();
const users = usersPages?.pages.flat();
const isEmpty = !isPending && !users?.length;
const isEmpty = !isPending && totalCount === 0;
const selectedUserIds = selectedUsers.map((user) => user.id);
@@ -241,16 +248,13 @@ const ServerAdminsPanelTable = ({
{!isPending && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of List"}
</Button>
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
</>
@@ -278,18 +282,24 @@ 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("", {
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 +399,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}
/>
<DeleteActionModal
isOpen={popUp.removeUser.isOpen}
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -72,18 +72,13 @@ const Content = ({ onClose }: ContentProps) => {
const [searchUserFilter, setSearchUserFilter] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const { data, isFetching } = useAdminGetUsers(
{
const { data, isPending } = useAdminGetUsers({
limit: 20,
searchTerm: debouncedSearchTerm,
adminsOnly: false
},
{
placeholderData: (prev) => prev
}
);
});
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
@@ -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,7 +29,12 @@ 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 { UsePopUpState } from "@app/hooks/usePopUp";
@@ -48,14 +53,34 @@ 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("", {
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,8 +105,7 @@ const IdentityPanelTable = ({
<TBody>
{isPending && <TableSkeleton columns={2} innerKey="identities" />}
{!isPending &&
data?.pages?.map((identities) =>
identities.map(({ name, id, isInstanceAdmin }) => (
identities?.map(({ name, id, isInstanceAdmin }) => (
<Tr key={`identity-${id}`} className="w-full">
<Td>
{name}
@@ -106,7 +130,6 @@ const IdentityPanelTable = ({
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
{isInstanceAdmin && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
@@ -125,30 +148,25 @@ const IdentityPanelTable = ({
>
Remove Server Admin
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</Td>
</Tr>
))
)}
))}
</TBody>
</Table>
{!isPending && isEmpty && <EmptyState title="No identities found" icon={faWrench} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of List"}
</Button>
{!isPending && totalCount > 0 && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
</>
@@ -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,
@@ -443,13 +448,31 @@ const OrganizationsPanelTable = ({
const { user } = useUser();
const navigate = useNavigate();
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } =
useAdminGetOrganizations({
limit: 20,
const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", {
initPerPage: getUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, 10)
});
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, newPerPage);
};
const { data, isPending } = useAdminGetOrganizations({
limit,
offset,
searchTerm: debouncedSearchTerm
});
const isEmpty = !isPending && !data?.pages?.[0].length;
const { organizations, totalCount = 0 } = data ?? {};
const isEmpty = !isPending && !totalCount;
useResetPageHelper({
totalCount,
offset,
setPage
});
const { mutateAsync: accessOrganization } = useServerAdminAccessOrg();
@@ -499,8 +522,7 @@ const OrganizationsPanelTable = ({
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="organizations" />}
{!isPending &&
data?.pages?.map((orgs) =>
orgs.map((org) => {
organizations?.map((org) => {
const isMember = org.members.find((member) => member.user.id === user.id);
return (
@@ -601,23 +623,19 @@ const OrganizationsPanelTable = ({
</Td>
</Tr>
);
})
)}
})}
</TBody>
</Table>
{!isPending && isEmpty && <EmptyState title="No organizations found" icon={faBuilding} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of list"}
</Button>
{!isPending && totalCount > 0 && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
<ViewMembersModal
@@ -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,
@@ -58,17 +63,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 +88,22 @@ const UserPanelTable = ({
}
) => void;
isPending: boolean;
users: InfiniteData<User[], unknown> | undefined;
users: User[] | undefined;
adminsOnly: boolean;
setAdminsOnly: (adminsOnly: boolean) => void;
searchUserFilter: string;
setSearchUserFilter: (filter: string) => void;
selectedUsers: User[];
setSelectedUsers: Dispatch<SetStateAction<User[]>>;
isFetchingNextPage: boolean;
fetchNextPage: () => void;
hasNextPage: boolean;
totalCount: number;
page: number;
perPage: number;
setPage: Dispatch<SetStateAction<number>>;
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 +309,13 @@ const UserPanelTable = ({
{!isPending && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of List"}
</Button>
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
</>
@@ -341,18 +345,24 @@ 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("", {
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 +489,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}
/>
<DeleteActionModal
isOpen={popUp.removeUser.isOpen}