feat: adds pagination in organizations table

This commit is contained in:
Piyush Gupta
2025-10-07 22:59:48 +05:30
parent 464a18ccb2
commit b7b5939c16
7 changed files with 198 additions and 126 deletions
+11 -2
View File
@@ -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
}
};
}
});
+22
View File
@@ -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,
@@ -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,
@@ -34,6 +34,10 @@ export type TGetOrganizationsDTO = {
searchTerm: string;
};
export type TCountOrganizationsDTO = {
searchTerm: string;
};
export type TCreateOrganizationDTO = {
name: string;
inviteAdminEmails: string[];
+7 -10
View File
@@ -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<TGetOrganizationsResponse>(
"/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
});
};
+14 -2
View File
@@ -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"
}
@@ -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,
const { offset, limit, setPage, perPage, page, setPerPage } =
usePagination<AdminOrganizationOrderBy>(AdminOrganizationOrderBy.Name, {
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 +523,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 +624,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