Skip to content

Commit 4789180

Browse files
brettkolodnySasSwart
authored andcommitted
feat: add pagination to the organizaton members table (#16870)
Closes [coder/internal#344](coder/internal#344)
1 parent 8b3a2b1 commit 4789180

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
@@ -9611,7 +9611,7 @@ func (q *FakeQuerier) PaginatedOrganizationMembers(_ context.Context, arg databa
96119611
// All of the members in the organization
96129612
orgMembers := make([]database.OrganizationMember, 0)
96139613
for _, mem := range q.organizationMembers {
9614-
if arg.OrganizationID != uuid.Nil && mem.OrganizationID != arg.OrganizationID {
9614+
if mem.OrganizationID != arg.OrganizationID {
96159615
continue
96169616
}
96179617

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

96239623
skippedMembers := 0
9624-
for _, organizationMember := range q.organizationMembers {
9624+
for _, organizationMember := range orgMembers {
96259625
if skippedMembers < int(arg.OffsetOpt) {
96269626
skippedMembers++
96279627
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