diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go
index 63ee1d0bd95e7..1ece2571f4960 100644
--- a/coderd/database/dbmem/dbmem.go
+++ b/coderd/database/dbmem/dbmem.go
@@ -9596,7 +9596,7 @@ func (q *FakeQuerier) PaginatedOrganizationMembers(_ context.Context, arg databa
// All of the members in the organization
orgMembers := make([]database.OrganizationMember, 0)
for _, mem := range q.organizationMembers {
- if arg.OrganizationID != uuid.Nil && mem.OrganizationID != arg.OrganizationID {
+ if mem.OrganizationID != arg.OrganizationID {
continue
}
@@ -9606,7 +9606,7 @@ func (q *FakeQuerier) PaginatedOrganizationMembers(_ context.Context, arg databa
selectedMembers := make([]database.PaginatedOrganizationMembersRow, 0)
skippedMembers := 0
- for _, organizationMember := range q.organizationMembers {
+ for _, organizationMember := range orgMembers {
if skippedMembers < int(arg.OffsetOpt) {
skippedMembers++
continue
diff --git a/codersdk/organizations.go b/codersdk/organizations.go
index e093f6f85594a..8a028d46e098c 100644
--- a/codersdk/organizations.go
+++ b/codersdk/organizations.go
@@ -82,14 +82,13 @@ type OrganizationMemberWithUserData struct {
}
type PaginatedMembersRequest struct {
- OrganizationID uuid.UUID `table:"organization id" json:"organization_id" format:"uuid"`
- Limit int `json:"limit,omitempty"`
- Offset int `json:"offset,omitempty"`
+ Limit int `json:"limit,omitempty"`
+ Offset int `json:"offset,omitempty"`
}
type PaginatedMembersResponse struct {
- Members []OrganizationMemberWithUserData
- Count int `json:"count"`
+ Members []OrganizationMemberWithUserData `json:"members"`
+ Count int `json:"count"`
}
type CreateOrganizationRequest struct {
diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index 627ede80976c6..b6012335f93d8 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -583,6 +583,24 @@ class ApiMethods {
return response.data;
};
+ /**
+ * @param organization Can be the organization's ID or name
+ * @param options Pagination options
+ */
+ getOrganizationPaginatedMembers = async (
+ organization: string,
+ options?: TypesGen.Pagination,
+ ) => {
+ const url = getURLWithSearchParams(
+ `/api/v2/organizations/${organization}/paginated-members`,
+ options,
+ );
+ const response =
+ await this.axios.get(url);
+
+ return response.data;
+ };
+
/**
* @param organization Can be the organization's ID or name
*/
diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts
index bca0bc6a72fff..2dc0402d75484 100644
--- a/site/src/api/queries/organizations.ts
+++ b/site/src/api/queries/organizations.ts
@@ -2,9 +2,12 @@ import { API } from "api/api";
import type {
CreateOrganizationRequest,
GroupSyncSettings,
+ PaginatedMembersRequest,
+ PaginatedMembersResponse,
RoleSyncSettings,
UpdateOrganizationRequest,
} from "api/typesGenerated";
+import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery";
import {
type OrganizationPermissionName,
type OrganizationPermissions,
@@ -59,13 +62,45 @@ export const organizationMembersKey = (id: string) => [
"members",
];
+/**
+ * Creates a query configuration to fetch all members of an organization.
+ *
+ * Unlike the paginated version, this function sets the `limit` parameter to 0,
+ * which instructs the API to return all organization members in a single request
+ * without pagination.
+ *
+ * @param id - The unique identifier of the organization
+ * @returns A query configuration object for use with React Query
+ *
+ * @see paginatedOrganizationMembers - For fetching members with pagination support
+ */
export const organizationMembers = (id: string) => {
return {
- queryFn: () => API.getOrganizationMembers(id),
+ queryFn: () => API.getOrganizationPaginatedMembers(id, { limit: 0 }),
queryKey: organizationMembersKey(id),
};
};
+export const paginatedOrganizationMembers = (
+ id: string,
+ searchParams: URLSearchParams,
+): UsePaginatedQueryOptions<
+ PaginatedMembersResponse,
+ PaginatedMembersRequest
+> => {
+ return {
+ searchParams,
+ queryPayload: ({ limit, offset }) => {
+ return {
+ limit: limit,
+ offset: offset,
+ };
+ },
+ queryKey: ({ payload }) => [...organizationMembersKey(id), payload],
+ queryFn: ({ payload }) => API.getOrganizationPaginatedMembers(id, payload),
+ };
+};
+
export const addOrganizationMember = (queryClient: QueryClient, id: string) => {
return {
mutationFn: (userId: string) => {
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 6fdfb5ea9d9a1..cd993e61db94a 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -1486,14 +1486,13 @@ export interface OrganizationSyncSettings {
// From codersdk/organizations.go
export interface PaginatedMembersRequest {
- readonly organization_id: string;
readonly limit?: number;
readonly offset?: number;
}
// From codersdk/organizations.go
export interface PaginatedMembersResponse {
- readonly Members: readonly OrganizationMemberWithUserData[];
+ readonly members: readonly OrganizationMemberWithUserData[];
readonly count: number;
}
diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.tsx
index f5bfd109c4a5c..e375116cd2d22 100644
--- a/site/src/components/UserAutocomplete/UserAutocomplete.tsx
+++ b/site/src/components/UserAutocomplete/UserAutocomplete.tsx
@@ -69,7 +69,6 @@ export const MemberAutocomplete: FC = ({
}) => {
const [filter, setFilter] = useState();
- // Currently this queries all members, as there is no pagination.
const membersQuery = useQuery({
...organizationMembers(organizationId),
enabled: filter !== undefined,
@@ -80,7 +79,7 @@ export const MemberAutocomplete: FC = ({
error={membersQuery.error}
isFetching={membersQuery.isFetching}
setFilter={setFilter}
- users={membersQuery.data}
+ users={membersQuery.data?.members}
{...props}
/>
);
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx
index 1270f78484dc7..f828969238cec 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx
@@ -38,8 +38,8 @@ beforeEach(() => {
const renderPage = async () => {
renderWithOrganizationSettingsLayout(, {
- route: `/organizations/${MockOrganization.name}/members`,
- path: "/organizations/:organization/members",
+ route: `/organizations/${MockOrganization.name}/paginated-members`,
+ path: "/organizations/:organization/paginated-members",
});
await waitForLoaderToBeRemoved();
};
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx
index ffa7b08b83742..5b566efa914aa 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx
@@ -3,7 +3,7 @@ import { getErrorMessage } from "api/errors";
import { groupsByUserIdInOrganization } from "api/queries/groups";
import {
addOrganizationMember,
- organizationMembers,
+ paginatedOrganizationMembers,
removeOrganizationMember,
updateOrganizationMemberRoles,
} from "api/queries/organizations";
@@ -14,12 +14,13 @@ import { EmptyState } from "components/EmptyState/EmptyState";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Stack } from "components/Stack/Stack";
import { useAuthenticated } from "contexts/auth/RequireAuth";
+import { usePaginatedQuery } from "hooks/usePaginatedQuery";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import { RequirePermission } from "modules/permissions/RequirePermission";
import { type FC, useState } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQuery, useQueryClient } from "react-query";
-import { useParams } from "react-router-dom";
+import { useParams, useSearchParams } from "react-router-dom";
import { pageTitle } from "utils/page";
import { OrganizationMembersPageView } from "./OrganizationMembersPageView";
@@ -30,17 +31,23 @@ const OrganizationMembersPage: FC = () => {
organization: string;
};
const { organization, organizationPermissions } = useOrganizationSettings();
+ const searchParamsResult = useSearchParams();
- const membersQuery = useQuery(organizationMembers(organizationName));
const organizationRolesQuery = useQuery(organizationRoles(organizationName));
const groupsByUserIdQuery = useQuery(
groupsByUserIdInOrganization(organizationName),
);
- const members = membersQuery.data?.map((member) => {
- const groups = groupsByUserIdQuery.data?.get(member.user_id) ?? [];
- return { ...member, groups };
- });
+ const membersQuery = usePaginatedQuery(
+ paginatedOrganizationMembers(organizationName, searchParamsResult[0]),
+ );
+
+ const members = membersQuery.data?.members.map(
+ (member: OrganizationMemberWithUserData) => {
+ const groups = groupsByUserIdQuery.data?.get(member.user_id) ?? [];
+ return { ...member, groups };
+ },
+ );
const addMemberMutation = useMutation(
addOrganizationMember(queryClient, organizationName),
@@ -95,6 +102,7 @@ const OrganizationMembersPage: FC = () => {
isUpdatingMemberRoles={updateMemberRolesMutation.isLoading}
me={me}
members={members}
+ membersQuery={membersQuery}
addMember={async (user: User) => {
await addMemberMutation.mutateAsync(user.id);
void membersQuery.refetch();
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.stories.tsx
index f3427bd58775d..1c2f2c6e804a3 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.stories.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.stories.tsx
@@ -1,4 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
+import { mockSuccessResult } from "components/PaginationWidget/PaginationContainer.mocks";
+import type { UsePaginatedQueryResult } from "hooks/usePaginatedQuery";
import {
MockOrganizationMember,
MockOrganizationMember2,
@@ -14,11 +16,16 @@ const meta: Meta = {
error: undefined,
isAddingMember: false,
isUpdatingMemberRoles: false,
+ canViewMembers: true,
me: MockUser,
members: [
{ ...MockOrganizationMember, groups: [] },
{ ...MockOrganizationMember2, groups: [] },
],
+ membersQuery: {
+ ...mockSuccessResult,
+ totalRecords: 2,
+ } as UsePaginatedQueryResult,
addMember: () => Promise.resolve(),
removeMember: () => Promise.resolve(),
updateMemberRoles: () => Promise.resolve(),
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx
index 6c85f57dd538d..adf5e3e566ffc 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx
@@ -18,6 +18,7 @@ import {
MoreMenuTrigger,
ThreeDotsButton,
} from "components/MoreMenu/MoreMenu";
+import { PaginationContainer } from "components/PaginationWidget/PaginationContainer";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import {
@@ -29,6 +30,7 @@ import {
TableRow,
} from "components/Table/Table";
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
+import type { PaginationResultInfo } from "hooks/usePaginatedQuery";
import { TriangleAlert } from "lucide-react";
import { UserGroupsCell } from "pages/UsersPage/UsersTable/UserGroupsCell";
import { type FC, useState } from "react";
@@ -44,6 +46,9 @@ interface OrganizationMembersPageViewProps {
isUpdatingMemberRoles: boolean;
me: User;
members: Array | undefined;
+ membersQuery: PaginationResultInfo & {
+ isPreviousData: boolean;
+ };
addMember: (user: User) => Promise;
removeMember: (member: OrganizationMemberWithUserData) => void;
updateMemberRoles: (
@@ -66,6 +71,7 @@ export const OrganizationMembersPageView: FC<
isAddingMember,
isUpdatingMemberRoles,
me,
+ membersQuery,
members,
addMember,
removeMember,
@@ -92,81 +98,82 @@ export const OrganizationMembersPageView: FC<
)}
-
-
-
-
- User
-
-
- Roles
-
-
-
-
-
- Groups
-
-
-
-
-
-
-
- {members?.map((member) => (
-
-
-
- }
- title={member.name || member.username}
- subtitle={member.email}
- />
-
- {
- try {
- await updateMemberRoles(member, roles);
- displaySuccess("Roles updated successfully.");
- } catch (error) {
- displayError(
- getErrorMessage(error, "Failed to update roles."),
- );
- }
- }}
- />
-
-
- {member.user_id !== me.id && canEditMembers && (
-
-
-
-
-
- removeMember(member)}
- >
- Remove
-
-
-
- )}
-
+
+
+
+
+ User
+
+
+ Roles
+
+
+
+
+
+ Groups
+
+
+
+
- ))}
-
-
+
+
+ {members?.map((member) => (
+
+
+
+ }
+ title={member.name || member.username}
+ subtitle={member.email}
+ />
+
+ {
+ try {
+ await updateMemberRoles(member, roles);
+ displaySuccess("Roles updated successfully.");
+ } catch (error) {
+ displayError(
+ getErrorMessage(error, "Failed to update roles."),
+ );
+ }
+ }}
+ />
+
+
+ {member.user_id !== me.id && canEditMembers && (
+
+
+
+
+
+ removeMember(member)}
+ >
+ Remove
+
+
+
+ )}
+
+
+ ))}
+
+
+
);
diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts
index 7fbd14147af83..79bc116891bf9 100644
--- a/site/src/testHelpers/handlers.ts
+++ b/site/src/testHelpers/handlers.ts
@@ -64,11 +64,11 @@ export const handlers = [
M.MockOrganizationAuditorRole,
]);
}),
- http.get("/api/v2/organizations/:organizationId/members", () => {
- return HttpResponse.json([
- M.MockOrganizationMember,
- M.MockOrganizationMember2,
- ]);
+ http.get("/api/v2/organizations/:organizationId/paginated-members", () => {
+ return HttpResponse.json({
+ members: [M.MockOrganizationMember, M.MockOrganizationMember2],
+ count: 2,
+ });
}),
http.delete(
"/api/v2/organizations/:organizationId/members/:userId",
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