Skip to content

Commit 98b48d6

Browse files
committed
feat: implement pagination for org members table
1 parent 8c0350e commit 98b48d6

File tree

4 files changed

+146
-77
lines changed

4 files changed

+146
-77
lines changed

site/src/api/api.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,23 @@ class ApiMethods {
583583
return response.data;
584584
};
585585

586+
/**
587+
* @param organization Can be the organization's ID or name
588+
* @param options Pagination options
589+
*/
590+
getOrganizationPaginatedMembers = async (
591+
organization: string,
592+
options?: TypesGen.Pagination
593+
) => {
594+
const url = getURLWithSearchParams(
595+
`/api/v2/organizations/${organization}/paginated-members`,
596+
options
597+
);
598+
const response = await this.axios.get<TypesGen.PaginatedMembersResponse>(url);
599+
600+
return response.data;
601+
};
602+
586603
/**
587604
* @param organization Can be the organization's ID or name
588605
*/

site/src/api/queries/organizations.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { API } from "api/api";
22
import type {
33
CreateOrganizationRequest,
44
GroupSyncSettings,
5+
PaginatedMembersResponse,
56
RoleSyncSettings,
67
UpdateOrganizationRequest,
78
} from "api/typesGenerated";
9+
import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery";
810
import {
911
type OrganizationPermissionName,
1012
type OrganizationPermissions,
@@ -59,6 +61,26 @@ export const organizationMembersKey = (id: string) => [
5961
"members",
6062
];
6163

64+
export function paginatedOrganizationMembers(
65+
organizationName: string,
66+
searchParams: URLSearchParams
67+
): UsePaginatedQueryOptions<PaginatedMembersResponse, { limit: number; offset: number }> {
68+
return {
69+
searchParams,
70+
queryPayload: (params) => {
71+
return {
72+
limit: params.limit,
73+
offset: params.offset,
74+
};
75+
},
76+
queryKey: ({ payload }) => [
77+
...organizationMembersKey(organizationName),
78+
payload,
79+
],
80+
queryFn: ({ payload }) => API.getOrganizationPaginatedMembers(organizationName, payload),
81+
};
82+
}
83+
6284
export const organizationMembers = (id: string) => {
6385
return {
6486
queryFn: () => API.getOrganizationMembers(id),

site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getErrorMessage } from "api/errors";
33
import { groupsByUserIdInOrganization } from "api/queries/groups";
44
import {
55
addOrganizationMember,
6-
organizationMembers,
6+
paginatedOrganizationMembers,
77
removeOrganizationMember,
88
updateOrganizationMemberRoles,
99
} from "api/queries/organizations";
@@ -14,12 +14,13 @@ import { EmptyState } from "components/EmptyState/EmptyState";
1414
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
1515
import { Stack } from "components/Stack/Stack";
1616
import { useAuthenticated } from "contexts/auth/RequireAuth";
17+
import { usePaginatedQuery } from "hooks/usePaginatedQuery";
1718
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
1819
import { RequirePermission } from "modules/permissions/RequirePermission";
1920
import { type FC, useState } from "react";
2021
import { Helmet } from "react-helmet-async";
2122
import { useMutation, useQuery, useQueryClient } from "react-query";
22-
import { useParams } from "react-router-dom";
23+
import { useParams, useSearchParams } from "react-router-dom";
2324
import { pageTitle } from "utils/page";
2425
import { OrganizationMembersPageView } from "./OrganizationMembersPageView";
2526

@@ -30,14 +31,18 @@ const OrganizationMembersPage: FC = () => {
3031
organization: string;
3132
};
3233
const { organization, organizationPermissions } = useOrganizationSettings();
34+
const searchParamsResult = useSearchParams();
3335

34-
const membersQuery = useQuery(organizationMembers(organizationName));
3536
const organizationRolesQuery = useQuery(organizationRoles(organizationName));
3637
const groupsByUserIdQuery = useQuery(
3738
groupsByUserIdInOrganization(organizationName),
3839
);
3940

40-
const members = membersQuery.data?.map((member) => {
41+
const membersQuery = usePaginatedQuery(
42+
paginatedOrganizationMembers(organizationName, searchParamsResult[0])
43+
);
44+
45+
const members = membersQuery.data?.Members.map((member: OrganizationMemberWithUserData) => {
4146
const groups = groupsByUserIdQuery.data?.get(member.user_id) ?? [];
4247
return { ...member, groups };
4348
});
@@ -76,6 +81,11 @@ const OrganizationMembersPage: FC = () => {
7681
);
7782
}
7883

84+
const isLoading =
85+
membersQuery.isLoading ||
86+
organizationRolesQuery.isLoading ||
87+
groupsByUserIdQuery.isLoading;
88+
7989
return (
8090
<>
8191
{helmet}
@@ -95,6 +105,8 @@ const OrganizationMembersPage: FC = () => {
95105
isUpdatingMemberRoles={updateMemberRolesMutation.isLoading}
96106
me={me}
97107
members={members}
108+
isLoading={isLoading}
109+
membersQuery={membersQuery}
98110
addMember={async (user: User) => {
99111
await addMemberMutation.mutateAsync(user.id);
100112
void membersQuery.refetch();

site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx

Lines changed: 91 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import {
1818
MoreMenuTrigger,
1919
ThreeDotsButton,
2020
} from "components/MoreMenu/MoreMenu";
21+
import { PaginationContainer } from "components/PaginationWidget/PaginationContainer";
2122
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
23+
import { Loader } from "components/Loader/Loader";
2224
import { Stack } from "components/Stack/Stack";
2325
import {
2426
Table,
@@ -28,6 +30,7 @@ import {
2830
TableRow,
2931
} from "components/Table/Table";
3032
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
33+
import type { PaginationResultInfo } from "hooks/usePaginatedQuery";
3134
import { TriangleAlert } from "lucide-react";
3235
import { UserGroupsCell } from "pages/UsersPage/UsersTable/UserGroupsCell";
3336
import { type FC, useState } from "react";
@@ -41,8 +44,12 @@ interface OrganizationMembersPageViewProps {
4144
error: unknown;
4245
isAddingMember: boolean;
4346
isUpdatingMemberRoles: boolean;
47+
isLoading: boolean;
4448
me: User;
4549
members: Array<OrganizationMemberTableEntry> | undefined;
50+
membersQuery: PaginationResultInfo & {
51+
isPreviousData: boolean;
52+
};
4653
addMember: (user: User) => Promise<void>;
4754
removeMember: (member: OrganizationMemberWithUserData) => void;
4855
updateMemberRoles: (
@@ -64,8 +71,10 @@ export const OrganizationMembersPageView: FC<
6471
error,
6572
isAddingMember,
6673
isUpdatingMemberRoles,
74+
isLoading,
6775
me,
6876
members,
77+
membersQuery,
6978
addMember,
7079
removeMember,
7180
updateMemberRoles,
@@ -92,80 +101,89 @@ export const OrganizationMembersPageView: FC<
92101
</div>
93102
)}
94103

95-
<Table>
96-
<TableHeader>
97-
<TableRow>
98-
<TableCell width="33%">User</TableCell>
99-
<TableCell width="33%">
100-
<Stack direction="row" spacing={1} alignItems="center">
101-
<span>Roles</span>
102-
<TableColumnHelpTooltip variant="roles" />
103-
</Stack>
104-
</TableCell>
105-
<TableCell width="33%">
106-
<Stack direction="row" spacing={1} alignItems="center">
107-
<span>Groups</span>
108-
<TableColumnHelpTooltip variant="groups" />
109-
</Stack>
110-
</TableCell>
111-
<TableCell width="1%" />
112-
</TableRow>
113-
</TableHeader>
114-
<TableBody>
115-
{members?.map((member) => (
116-
<TableRow key={member.user_id} className="align-baseline">
117-
<TableCell>
118-
<AvatarData
119-
avatar={
120-
<Avatar
121-
fallback={member.username}
122-
src={member.avatar_url}
104+
{isLoading ? (
105+
<Loader />
106+
) : (
107+
<PaginationContainer
108+
query={membersQuery}
109+
paginationUnitLabel="members"
110+
>
111+
<Table>
112+
<TableHeader>
113+
<TableRow>
114+
<TableCell width="33%">User</TableCell>
115+
<TableCell width="33%">
116+
<Stack direction="row" spacing={1} alignItems="center">
117+
<span>Roles</span>
118+
<TableColumnHelpTooltip variant="roles" />
119+
</Stack>
120+
</TableCell>
121+
<TableCell width="33%">
122+
<Stack direction="row" spacing={1} alignItems="center">
123+
<span>Groups</span>
124+
<TableColumnHelpTooltip variant="groups" />
125+
</Stack>
126+
</TableCell>
127+
<TableCell width="1%" />
128+
</TableRow>
129+
</TableHeader>
130+
<TableBody>
131+
{members?.map((member) => (
132+
<TableRow key={member.user_id} className="align-baseline">
133+
<TableCell>
134+
<AvatarData
135+
avatar={
136+
<Avatar
137+
fallback={member.username}
138+
src={member.avatar_url}
139+
/>
140+
}
141+
title={member.name || member.username}
142+
subtitle={member.email}
123143
/>
124-
}
125-
title={member.name || member.username}
126-
subtitle={member.email}
127-
/>
128-
</TableCell>
129-
<UserRoleCell
130-
inheritedRoles={member.global_roles}
131-
roles={member.roles}
132-
allAvailableRoles={allAvailableRoles}
133-
oidcRoleSyncEnabled={false}
134-
isLoading={isUpdatingMemberRoles}
135-
canEditUsers={canEditMembers}
136-
onEditRoles={async (roles) => {
137-
try {
138-
await updateMemberRoles(member, roles);
139-
displaySuccess("Roles updated successfully.");
140-
} catch (error) {
141-
displayError(
142-
getErrorMessage(error, "Failed to update roles."),
143-
);
144-
}
145-
}}
146-
/>
147-
<UserGroupsCell userGroups={member.groups} />
148-
<TableCell>
149-
{member.user_id !== me.id && canEditMembers && (
150-
<MoreMenu>
151-
<MoreMenuTrigger>
152-
<ThreeDotsButton />
153-
</MoreMenuTrigger>
154-
<MoreMenuContent>
155-
<MoreMenuItem
156-
danger
157-
onClick={() => removeMember(member)}
158-
>
159-
Remove
160-
</MoreMenuItem>
161-
</MoreMenuContent>
162-
</MoreMenu>
163-
)}
164-
</TableCell>
165-
</TableRow>
166-
))}
167-
</TableBody>
168-
</Table>
144+
</TableCell>
145+
<UserRoleCell
146+
inheritedRoles={member.global_roles}
147+
roles={member.roles}
148+
allAvailableRoles={allAvailableRoles}
149+
oidcRoleSyncEnabled={false}
150+
isLoading={isUpdatingMemberRoles}
151+
canEditUsers={canEditMembers}
152+
onEditRoles={async (roles) => {
153+
try {
154+
await updateMemberRoles(member, roles);
155+
displaySuccess("Roles updated successfully.");
156+
} catch (error) {
157+
displayError(
158+
getErrorMessage(error, "Failed to update roles."),
159+
);
160+
}
161+
}}
162+
/>
163+
<UserGroupsCell userGroups={member.groups} />
164+
<TableCell>
165+
{member.user_id !== me.id && canEditMembers && (
166+
<MoreMenu>
167+
<MoreMenuTrigger>
168+
<ThreeDotsButton />
169+
</MoreMenuTrigger>
170+
<MoreMenuContent>
171+
<MoreMenuItem
172+
danger
173+
onClick={() => removeMember(member)}
174+
>
175+
Remove
176+
</MoreMenuItem>
177+
</MoreMenuContent>
178+
</MoreMenu>
179+
)}
180+
</TableCell>
181+
</TableRow>
182+
))}
183+
</TableBody>
184+
</Table>
185+
</PaginationContainer>
186+
)}
169187
</div>
170188
</div>
171189
);

0 commit comments

Comments
 (0)