mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: add pagination to the organizaton members table (#16870)
Closes [coder/internal#344](https://github.com/coder/internal/issues/344)
This commit is contained in:
@ -9596,7 +9596,7 @@ func (q *FakeQuerier) PaginatedOrganizationMembers(_ context.Context, arg databa
|
|||||||
// All of the members in the organization
|
// All of the members in the organization
|
||||||
orgMembers := make([]database.OrganizationMember, 0)
|
orgMembers := make([]database.OrganizationMember, 0)
|
||||||
for _, mem := range q.organizationMembers {
|
for _, mem := range q.organizationMembers {
|
||||||
if arg.OrganizationID != uuid.Nil && mem.OrganizationID != arg.OrganizationID {
|
if mem.OrganizationID != arg.OrganizationID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9606,7 +9606,7 @@ func (q *FakeQuerier) PaginatedOrganizationMembers(_ context.Context, arg databa
|
|||||||
selectedMembers := make([]database.PaginatedOrganizationMembersRow, 0)
|
selectedMembers := make([]database.PaginatedOrganizationMembersRow, 0)
|
||||||
|
|
||||||
skippedMembers := 0
|
skippedMembers := 0
|
||||||
for _, organizationMember := range q.organizationMembers {
|
for _, organizationMember := range orgMembers {
|
||||||
if skippedMembers < int(arg.OffsetOpt) {
|
if skippedMembers < int(arg.OffsetOpt) {
|
||||||
skippedMembers++
|
skippedMembers++
|
||||||
continue
|
continue
|
||||||
|
@ -82,14 +82,13 @@ type OrganizationMemberWithUserData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PaginatedMembersRequest struct {
|
type PaginatedMembersRequest struct {
|
||||||
OrganizationID uuid.UUID `table:"organization id" json:"organization_id" format:"uuid"`
|
Limit int `json:"limit,omitempty"`
|
||||||
Limit int `json:"limit,omitempty"`
|
Offset int `json:"offset,omitempty"`
|
||||||
Offset int `json:"offset,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaginatedMembersResponse struct {
|
type PaginatedMembersResponse struct {
|
||||||
Members []OrganizationMemberWithUserData
|
Members []OrganizationMemberWithUserData `json:"members"`
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateOrganizationRequest struct {
|
type CreateOrganizationRequest struct {
|
||||||
|
@ -583,6 +583,24 @@ class ApiMethods {
|
|||||||
return response.data;
|
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<TypesGen.PaginatedMembersResponse>(url);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param organization Can be the organization's ID or name
|
* @param organization Can be the organization's ID or name
|
||||||
*/
|
*/
|
||||||
|
@ -2,9 +2,12 @@ import { API } from "api/api";
|
|||||||
import type {
|
import type {
|
||||||
CreateOrganizationRequest,
|
CreateOrganizationRequest,
|
||||||
GroupSyncSettings,
|
GroupSyncSettings,
|
||||||
|
PaginatedMembersRequest,
|
||||||
|
PaginatedMembersResponse,
|
||||||
RoleSyncSettings,
|
RoleSyncSettings,
|
||||||
UpdateOrganizationRequest,
|
UpdateOrganizationRequest,
|
||||||
} from "api/typesGenerated";
|
} from "api/typesGenerated";
|
||||||
|
import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery";
|
||||||
import {
|
import {
|
||||||
type OrganizationPermissionName,
|
type OrganizationPermissionName,
|
||||||
type OrganizationPermissions,
|
type OrganizationPermissions,
|
||||||
@ -59,13 +62,45 @@ export const organizationMembersKey = (id: string) => [
|
|||||||
"members",
|
"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) => {
|
export const organizationMembers = (id: string) => {
|
||||||
return {
|
return {
|
||||||
queryFn: () => API.getOrganizationMembers(id),
|
queryFn: () => API.getOrganizationPaginatedMembers(id, { limit: 0 }),
|
||||||
queryKey: organizationMembersKey(id),
|
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) => {
|
export const addOrganizationMember = (queryClient: QueryClient, id: string) => {
|
||||||
return {
|
return {
|
||||||
mutationFn: (userId: string) => {
|
mutationFn: (userId: string) => {
|
||||||
|
3
site/src/api/typesGenerated.ts
generated
3
site/src/api/typesGenerated.ts
generated
@ -1486,14 +1486,13 @@ export interface OrganizationSyncSettings {
|
|||||||
|
|
||||||
// From codersdk/organizations.go
|
// From codersdk/organizations.go
|
||||||
export interface PaginatedMembersRequest {
|
export interface PaginatedMembersRequest {
|
||||||
readonly organization_id: string;
|
|
||||||
readonly limit?: number;
|
readonly limit?: number;
|
||||||
readonly offset?: number;
|
readonly offset?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/organizations.go
|
// From codersdk/organizations.go
|
||||||
export interface PaginatedMembersResponse {
|
export interface PaginatedMembersResponse {
|
||||||
readonly Members: readonly OrganizationMemberWithUserData[];
|
readonly members: readonly OrganizationMemberWithUserData[];
|
||||||
readonly count: number;
|
readonly count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +69,6 @@ export const MemberAutocomplete: FC<MemberAutocompleteProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [filter, setFilter] = useState<string>();
|
const [filter, setFilter] = useState<string>();
|
||||||
|
|
||||||
// Currently this queries all members, as there is no pagination.
|
|
||||||
const membersQuery = useQuery({
|
const membersQuery = useQuery({
|
||||||
...organizationMembers(organizationId),
|
...organizationMembers(organizationId),
|
||||||
enabled: filter !== undefined,
|
enabled: filter !== undefined,
|
||||||
@ -80,7 +79,7 @@ export const MemberAutocomplete: FC<MemberAutocompleteProps> = ({
|
|||||||
error={membersQuery.error}
|
error={membersQuery.error}
|
||||||
isFetching={membersQuery.isFetching}
|
isFetching={membersQuery.isFetching}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
users={membersQuery.data}
|
users={membersQuery.data?.members}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -38,8 +38,8 @@ beforeEach(() => {
|
|||||||
|
|
||||||
const renderPage = async () => {
|
const renderPage = async () => {
|
||||||
renderWithOrganizationSettingsLayout(<OrganizationMembersPage />, {
|
renderWithOrganizationSettingsLayout(<OrganizationMembersPage />, {
|
||||||
route: `/organizations/${MockOrganization.name}/members`,
|
route: `/organizations/${MockOrganization.name}/paginated-members`,
|
||||||
path: "/organizations/:organization/members",
|
path: "/organizations/:organization/paginated-members",
|
||||||
});
|
});
|
||||||
await waitForLoaderToBeRemoved();
|
await waitForLoaderToBeRemoved();
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { getErrorMessage } from "api/errors";
|
|||||||
import { groupsByUserIdInOrganization } from "api/queries/groups";
|
import { groupsByUserIdInOrganization } from "api/queries/groups";
|
||||||
import {
|
import {
|
||||||
addOrganizationMember,
|
addOrganizationMember,
|
||||||
organizationMembers,
|
paginatedOrganizationMembers,
|
||||||
removeOrganizationMember,
|
removeOrganizationMember,
|
||||||
updateOrganizationMemberRoles,
|
updateOrganizationMemberRoles,
|
||||||
} from "api/queries/organizations";
|
} from "api/queries/organizations";
|
||||||
@ -14,12 +14,13 @@ import { EmptyState } from "components/EmptyState/EmptyState";
|
|||||||
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
|
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
|
||||||
import { Stack } from "components/Stack/Stack";
|
import { Stack } from "components/Stack/Stack";
|
||||||
import { useAuthenticated } from "contexts/auth/RequireAuth";
|
import { useAuthenticated } from "contexts/auth/RequireAuth";
|
||||||
|
import { usePaginatedQuery } from "hooks/usePaginatedQuery";
|
||||||
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
|
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
|
||||||
import { RequirePermission } from "modules/permissions/RequirePermission";
|
import { RequirePermission } from "modules/permissions/RequirePermission";
|
||||||
import { type FC, useState } from "react";
|
import { type FC, useState } from "react";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
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 { pageTitle } from "utils/page";
|
||||||
import { OrganizationMembersPageView } from "./OrganizationMembersPageView";
|
import { OrganizationMembersPageView } from "./OrganizationMembersPageView";
|
||||||
|
|
||||||
@ -30,17 +31,23 @@ const OrganizationMembersPage: FC = () => {
|
|||||||
organization: string;
|
organization: string;
|
||||||
};
|
};
|
||||||
const { organization, organizationPermissions } = useOrganizationSettings();
|
const { organization, organizationPermissions } = useOrganizationSettings();
|
||||||
|
const searchParamsResult = useSearchParams();
|
||||||
|
|
||||||
const membersQuery = useQuery(organizationMembers(organizationName));
|
|
||||||
const organizationRolesQuery = useQuery(organizationRoles(organizationName));
|
const organizationRolesQuery = useQuery(organizationRoles(organizationName));
|
||||||
const groupsByUserIdQuery = useQuery(
|
const groupsByUserIdQuery = useQuery(
|
||||||
groupsByUserIdInOrganization(organizationName),
|
groupsByUserIdInOrganization(organizationName),
|
||||||
);
|
);
|
||||||
|
|
||||||
const members = membersQuery.data?.map((member) => {
|
const membersQuery = usePaginatedQuery(
|
||||||
const groups = groupsByUserIdQuery.data?.get(member.user_id) ?? [];
|
paginatedOrganizationMembers(organizationName, searchParamsResult[0]),
|
||||||
return { ...member, groups };
|
);
|
||||||
});
|
|
||||||
|
const members = membersQuery.data?.members.map(
|
||||||
|
(member: OrganizationMemberWithUserData) => {
|
||||||
|
const groups = groupsByUserIdQuery.data?.get(member.user_id) ?? [];
|
||||||
|
return { ...member, groups };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const addMemberMutation = useMutation(
|
const addMemberMutation = useMutation(
|
||||||
addOrganizationMember(queryClient, organizationName),
|
addOrganizationMember(queryClient, organizationName),
|
||||||
@ -95,6 +102,7 @@ const OrganizationMembersPage: FC = () => {
|
|||||||
isUpdatingMemberRoles={updateMemberRolesMutation.isLoading}
|
isUpdatingMemberRoles={updateMemberRolesMutation.isLoading}
|
||||||
me={me}
|
me={me}
|
||||||
members={members}
|
members={members}
|
||||||
|
membersQuery={membersQuery}
|
||||||
addMember={async (user: User) => {
|
addMember={async (user: User) => {
|
||||||
await addMemberMutation.mutateAsync(user.id);
|
await addMemberMutation.mutateAsync(user.id);
|
||||||
void membersQuery.refetch();
|
void membersQuery.refetch();
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { mockSuccessResult } from "components/PaginationWidget/PaginationContainer.mocks";
|
||||||
|
import type { UsePaginatedQueryResult } from "hooks/usePaginatedQuery";
|
||||||
import {
|
import {
|
||||||
MockOrganizationMember,
|
MockOrganizationMember,
|
||||||
MockOrganizationMember2,
|
MockOrganizationMember2,
|
||||||
@ -14,11 +16,16 @@ const meta: Meta<typeof OrganizationMembersPageView> = {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
isAddingMember: false,
|
isAddingMember: false,
|
||||||
isUpdatingMemberRoles: false,
|
isUpdatingMemberRoles: false,
|
||||||
|
canViewMembers: true,
|
||||||
me: MockUser,
|
me: MockUser,
|
||||||
members: [
|
members: [
|
||||||
{ ...MockOrganizationMember, groups: [] },
|
{ ...MockOrganizationMember, groups: [] },
|
||||||
{ ...MockOrganizationMember2, groups: [] },
|
{ ...MockOrganizationMember2, groups: [] },
|
||||||
],
|
],
|
||||||
|
membersQuery: {
|
||||||
|
...mockSuccessResult,
|
||||||
|
totalRecords: 2,
|
||||||
|
} as UsePaginatedQueryResult,
|
||||||
addMember: () => Promise.resolve(),
|
addMember: () => Promise.resolve(),
|
||||||
removeMember: () => Promise.resolve(),
|
removeMember: () => Promise.resolve(),
|
||||||
updateMemberRoles: () => Promise.resolve(),
|
updateMemberRoles: () => Promise.resolve(),
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
MoreMenuTrigger,
|
MoreMenuTrigger,
|
||||||
ThreeDotsButton,
|
ThreeDotsButton,
|
||||||
} from "components/MoreMenu/MoreMenu";
|
} from "components/MoreMenu/MoreMenu";
|
||||||
|
import { PaginationContainer } from "components/PaginationWidget/PaginationContainer";
|
||||||
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
|
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
|
||||||
import { Stack } from "components/Stack/Stack";
|
import { Stack } from "components/Stack/Stack";
|
||||||
import {
|
import {
|
||||||
@ -29,6 +30,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "components/Table/Table";
|
} from "components/Table/Table";
|
||||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||||
|
import type { PaginationResultInfo } from "hooks/usePaginatedQuery";
|
||||||
import { TriangleAlert } from "lucide-react";
|
import { TriangleAlert } from "lucide-react";
|
||||||
import { UserGroupsCell } from "pages/UsersPage/UsersTable/UserGroupsCell";
|
import { UserGroupsCell } from "pages/UsersPage/UsersTable/UserGroupsCell";
|
||||||
import { type FC, useState } from "react";
|
import { type FC, useState } from "react";
|
||||||
@ -44,6 +46,9 @@ interface OrganizationMembersPageViewProps {
|
|||||||
isUpdatingMemberRoles: boolean;
|
isUpdatingMemberRoles: boolean;
|
||||||
me: User;
|
me: User;
|
||||||
members: Array<OrganizationMemberTableEntry> | undefined;
|
members: Array<OrganizationMemberTableEntry> | undefined;
|
||||||
|
membersQuery: PaginationResultInfo & {
|
||||||
|
isPreviousData: boolean;
|
||||||
|
};
|
||||||
addMember: (user: User) => Promise<void>;
|
addMember: (user: User) => Promise<void>;
|
||||||
removeMember: (member: OrganizationMemberWithUserData) => void;
|
removeMember: (member: OrganizationMemberWithUserData) => void;
|
||||||
updateMemberRoles: (
|
updateMemberRoles: (
|
||||||
@ -66,6 +71,7 @@ export const OrganizationMembersPageView: FC<
|
|||||||
isAddingMember,
|
isAddingMember,
|
||||||
isUpdatingMemberRoles,
|
isUpdatingMemberRoles,
|
||||||
me,
|
me,
|
||||||
|
membersQuery,
|
||||||
members,
|
members,
|
||||||
addMember,
|
addMember,
|
||||||
removeMember,
|
removeMember,
|
||||||
@ -92,81 +98,82 @@ export const OrganizationMembersPageView: FC<
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<PaginationContainer query={membersQuery} paginationUnitLabel="members">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-2/6">User</TableHead>
|
<TableHead className="w-2/6">User</TableHead>
|
||||||
<TableHead className="w-2/6">
|
<TableHead className="w-2/6">
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
<span>Roles</span>
|
<span>Roles</span>
|
||||||
<TableColumnHelpTooltip variant="roles" />
|
<TableColumnHelpTooltip variant="roles" />
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-2/6">
|
<TableHead className="w-2/6">
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
<span>Groups</span>
|
<span>Groups</span>
|
||||||
<TableColumnHelpTooltip variant="groups" />
|
<TableColumnHelpTooltip variant="groups" />
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-auto" />
|
<TableHead className="w-auto" />
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{members?.map((member) => (
|
|
||||||
<TableRow key={member.user_id} className="align-baseline">
|
|
||||||
<TableCell>
|
|
||||||
<AvatarData
|
|
||||||
avatar={
|
|
||||||
<Avatar
|
|
||||||
fallback={member.username}
|
|
||||||
src={member.avatar_url}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={member.name || member.username}
|
|
||||||
subtitle={member.email}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<UserRoleCell
|
|
||||||
inheritedRoles={member.global_roles}
|
|
||||||
roles={member.roles}
|
|
||||||
allAvailableRoles={allAvailableRoles}
|
|
||||||
oidcRoleSyncEnabled={false}
|
|
||||||
isLoading={isUpdatingMemberRoles}
|
|
||||||
canEditUsers={canEditMembers}
|
|
||||||
onEditRoles={async (roles) => {
|
|
||||||
try {
|
|
||||||
await updateMemberRoles(member, roles);
|
|
||||||
displaySuccess("Roles updated successfully.");
|
|
||||||
} catch (error) {
|
|
||||||
displayError(
|
|
||||||
getErrorMessage(error, "Failed to update roles."),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<UserGroupsCell userGroups={member.groups} />
|
|
||||||
<TableCell>
|
|
||||||
{member.user_id !== me.id && canEditMembers && (
|
|
||||||
<MoreMenu>
|
|
||||||
<MoreMenuTrigger>
|
|
||||||
<ThreeDotsButton />
|
|
||||||
</MoreMenuTrigger>
|
|
||||||
<MoreMenuContent>
|
|
||||||
<MoreMenuItem
|
|
||||||
danger
|
|
||||||
onClick={() => removeMember(member)}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</MoreMenuItem>
|
|
||||||
</MoreMenuContent>
|
|
||||||
</MoreMenu>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{members?.map((member) => (
|
||||||
|
<TableRow key={member.user_id} className="align-baseline">
|
||||||
|
<TableCell>
|
||||||
|
<AvatarData
|
||||||
|
avatar={
|
||||||
|
<Avatar
|
||||||
|
fallback={member.username}
|
||||||
|
src={member.avatar_url}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={member.name || member.username}
|
||||||
|
subtitle={member.email}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<UserRoleCell
|
||||||
|
inheritedRoles={member.global_roles}
|
||||||
|
roles={member.roles}
|
||||||
|
allAvailableRoles={allAvailableRoles}
|
||||||
|
oidcRoleSyncEnabled={false}
|
||||||
|
isLoading={isUpdatingMemberRoles}
|
||||||
|
canEditUsers={canEditMembers}
|
||||||
|
onEditRoles={async (roles) => {
|
||||||
|
try {
|
||||||
|
await updateMemberRoles(member, roles);
|
||||||
|
displaySuccess("Roles updated successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
displayError(
|
||||||
|
getErrorMessage(error, "Failed to update roles."),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<UserGroupsCell userGroups={member.groups} />
|
||||||
|
<TableCell>
|
||||||
|
{member.user_id !== me.id && canEditMembers && (
|
||||||
|
<MoreMenu>
|
||||||
|
<MoreMenuTrigger>
|
||||||
|
<ThreeDotsButton />
|
||||||
|
</MoreMenuTrigger>
|
||||||
|
<MoreMenuContent>
|
||||||
|
<MoreMenuItem
|
||||||
|
danger
|
||||||
|
onClick={() => removeMember(member)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</MoreMenuItem>
|
||||||
|
</MoreMenuContent>
|
||||||
|
</MoreMenu>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</PaginationContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -64,11 +64,11 @@ export const handlers = [
|
|||||||
M.MockOrganizationAuditorRole,
|
M.MockOrganizationAuditorRole,
|
||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
http.get("/api/v2/organizations/:organizationId/members", () => {
|
http.get("/api/v2/organizations/:organizationId/paginated-members", () => {
|
||||||
return HttpResponse.json([
|
return HttpResponse.json({
|
||||||
M.MockOrganizationMember,
|
members: [M.MockOrganizationMember, M.MockOrganizationMember2],
|
||||||
M.MockOrganizationMember2,
|
count: 2,
|
||||||
]);
|
});
|
||||||
}),
|
}),
|
||||||
http.delete(
|
http.delete(
|
||||||
"/api/v2/organizations/:organizationId/members/:userId",
|
"/api/v2/organizations/:organizationId/members/:userId",
|
||||||
|
Reference in New Issue
Block a user