Skip to content

Commit 1ec39f4

Browse files
authored
feat: add pagination to the organizaton members table (coder#16870)
Closes [coder/internal#344](coder/internal#344)
1 parent 7ba4df1 commit 1ec39f4

File tree

11 files changed

+172
-100
lines changed

11 files changed

+172
-100
lines changed

coderd/database/dbmem/dbmem.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9596,7 +9596,7 @@ func (q *FakeQuerier) PaginatedOrganizationMembers(_ context.Context, arg databa
95969596
// All of the members in the organization
95979597
orgMembers := make([]database.OrganizationMember, 0)
95989598
for _, mem := range q.organizationMembers {
9599-
if arg.OrganizationID != uuid.Nil && mem.OrganizationID != arg.OrganizationID {
9599+
if mem.OrganizationID != arg.OrganizationID {
96009600
continue
96019601
}
96029602

@@ -9606,7 +9606,7 @@ func (q *FakeQuerier) PaginatedOrganizationMembers(_ context.Context, arg databa
96069606
selectedMembers := make([]database.PaginatedOrganizationMembersRow, 0)
96079607

96089608
skippedMembers := 0
9609-
for _, organizationMember := range q.organizationMembers {
9609+
for _, organizationMember := range orgMembers {
96109610
if skippedMembers < int(arg.OffsetOpt) {
96119611
skippedMembers++
96129612
continue

codersdk/organizations.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,13 @@ type OrganizationMemberWithUserData struct {
8282
}
8383

8484
type PaginatedMembersRequest struct {
85-
OrganizationID uuid.UUID `table:"organization id" json:"organization_id" format:"uuid"`
86-
Limit int `json:"limit,omitempty"`
87-
Offset int `json:"offset,omitempty"`
85+
Limit int `json:"limit,omitempty"`
86+
Offset int `json:"offset,omitempty"`
8887
}
8988

9089
type PaginatedMembersResponse struct {
91-
Members []OrganizationMemberWithUserData
92-
Count int `json:"count"`
90+
Members []OrganizationMemberWithUserData `json:"members"`
91+
Count int `json:"count"`
9392
}
9493

9594
type CreateOrganizationRequest struct {

site/src/api/api.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,24 @@ 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 =
599+
await this.axios.get<TypesGen.PaginatedMembersResponse>(url);
600+
601+
return response.data;
602+
};
603+
586604
/**
587605
* @param organization Can be the organization's ID or name
588606
*/

site/src/api/queries/organizations.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import { API } from "api/api";
22
import type {
33
CreateOrganizationRequest,
44
GroupSyncSettings,
5+
PaginatedMembersRequest,
6+
PaginatedMembersResponse,
57
RoleSyncSettings,
68
UpdateOrganizationRequest,
79
} from "api/typesGenerated";
10+
import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery";
811
import {
912
type OrganizationPermissionName,
1013
type OrganizationPermissions,
@@ -59,13 +62,45 @@ export const organizationMembersKey = (id: string) => [
5962
"members",
6063
];
6164

65+
/**
66+
* Creates a query configuration to fetch all members of an organization.
67+
*
68+
* Unlike the paginated version, this function sets the `limit` parameter to 0,
69+
* which instructs the API to return all organization members in a single request
70+
* without pagination.
71+
*
72+
* @param id - The unique identifier of the organization
73+
* @returns A query configuration object for use with React Query
74+
*
75+
* @see paginatedOrganizationMembers - For fetching members with pagination support
76+
*/
6277
export const organizationMembers = (id: string) => {
6378
return {
64-
queryFn: () => API.getOrganizationMembers(id),
79+
queryFn: () => API.getOrganizationPaginatedMembers(id, { limit: 0 }),
6580
queryKey: organizationMembersKey(id),
6681
};
6782
};
6883

84+
export const paginatedOrganizationMembers = (
85+
id: string,
86+
searchParams: URLSearchParams,
87+
): UsePaginatedQueryOptions<
88+
PaginatedMembersResponse,
89+
PaginatedMembersRequest
90+
> => {
91+
return {
92+
searchParams,
93+
queryPayload: ({ limit, offset }) => {
94+
return {
95+
limit: limit,
96+
offset: offset,
97+
};
98+
},
99+
queryKey: ({ payload }) => [...organizationMembersKey(id), payload],
100+
queryFn: ({ payload }) => API.getOrganizationPaginatedMembers(id, payload),
101+
};
102+
};
103+
69104
export const addOrganizationMember = (queryClient: QueryClient, id: string) => {
70105
return {
71106
mutationFn: (userId: string) => {

site/src/api/typesGenerated.ts

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/components/UserAutocomplete/UserAutocomplete.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ export const MemberAutocomplete: FC<MemberAutocompleteProps> = ({
6969
}) => {
7070
const [filter, setFilter] = useState<string>();
7171

72-
// Currently this queries all members, as there is no pagination.
7372
const membersQuery = useQuery({
7473
...organizationMembers(organizationId),
7574
enabled: filter !== undefined,
@@ -80,7 +79,7 @@ export const MemberAutocomplete: FC<MemberAutocompleteProps> = ({
8079
error={membersQuery.error}
8180
isFetching={membersQuery.isFetching}
8281
setFilter={setFilter}
83-
users={membersQuery.data}
82+
users={membersQuery.data?.members}
8483
{...props}
8584
/>
8685
);

site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ beforeEach(() => {
3838

3939
const renderPage = async () => {
4040
renderWithOrganizationSettingsLayout(<OrganizationMembersPage />, {
41-
route: `/organizations/${MockOrganization.name}/members`,
42-
path: "/organizations/:organization/members",
41+
route: `/organizations/${MockOrganization.name}/paginated-members`,
42+
path: "/organizations/:organization/paginated-members",
4343
});
4444
await waitForLoaderToBeRemoved();
4545
};

site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx

Lines changed: 15 additions & 7 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,17 +31,23 @@ 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 groups = groupsByUserIdQuery.data?.get(member.user_id) ?? [];
42-
return { ...member, groups };
43-
});
41+
const membersQuery = usePaginatedQuery(
42+
paginatedOrganizationMembers(organizationName, searchParamsResult[0]),
43+
);
44+
45+
const members = membersQuery.data?.members.map(
46+
(member: OrganizationMemberWithUserData) => {
47+
const groups = groupsByUserIdQuery.data?.get(member.user_id) ?? [];
48+
return { ...member, groups };
49+
},
50+
);
4451

4552
const addMemberMutation = useMutation(
4653
addOrganizationMember(queryClient, organizationName),
@@ -95,6 +102,7 @@ const OrganizationMembersPage: FC = () => {
95102
isUpdatingMemberRoles={updateMemberRolesMutation.isLoading}
96103
me={me}
97104
members={members}
105+
membersQuery={membersQuery}
98106
addMember={async (user: User) => {
99107
await addMemberMutation.mutateAsync(user.id);
100108
void membersQuery.refetch();

site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.stories.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2+
import { mockSuccessResult } from "components/PaginationWidget/PaginationContainer.mocks";
3+
import type { UsePaginatedQueryResult } from "hooks/usePaginatedQuery";
24
import {
35
MockOrganizationMember,
46
MockOrganizationMember2,
@@ -14,11 +16,16 @@ const meta: Meta<typeof OrganizationMembersPageView> = {
1416
error: undefined,
1517
isAddingMember: false,
1618
isUpdatingMemberRoles: false,
19+
canViewMembers: true,
1720
me: MockUser,
1821
members: [
1922
{ ...MockOrganizationMember, groups: [] },
2023
{ ...MockOrganizationMember2, groups: [] },
2124
],
25+
membersQuery: {
26+
...mockSuccessResult,
27+
totalRecords: 2,
28+
} as UsePaginatedQueryResult,
2229
addMember: () => Promise.resolve(),
2330
removeMember: () => Promise.resolve(),
2431
updateMemberRoles: () => Promise.resolve(),

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy