mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat(site): edit organization member roles (#13977)
This commit is contained in:
Generated
+3
@@ -10625,6 +10625,9 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"global_roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
Generated
+3
@@ -9567,6 +9567,9 @@
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"global_roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -4293,7 +4293,7 @@ func (q *sqlQuerier) InsertOrganizationMember(ctx context.Context, arg InsertOrg
|
||||
const organizationMembers = `-- name: OrganizationMembers :many
|
||||
SELECT
|
||||
organization_members.user_id, organization_members.organization_id, organization_members.created_at, organization_members.updated_at, organization_members.roles,
|
||||
users.username, users.avatar_url, users.name, users.rbac_roles as "global_roles"
|
||||
users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles"
|
||||
FROM
|
||||
organization_members
|
||||
INNER JOIN
|
||||
@@ -4323,6 +4323,7 @@ type OrganizationMembersRow struct {
|
||||
Username string `db:"username" json:"username"`
|
||||
AvatarURL string `db:"avatar_url" json:"avatar_url"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Email string `db:"email" json:"email"`
|
||||
GlobalRoles pq.StringArray `db:"global_roles" json:"global_roles"`
|
||||
}
|
||||
|
||||
@@ -4348,6 +4349,7 @@ func (q *sqlQuerier) OrganizationMembers(ctx context.Context, arg OrganizationMe
|
||||
&i.Username,
|
||||
&i.AvatarURL,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
&i.GlobalRoles,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
-- - Use both to get a specific org member row
|
||||
SELECT
|
||||
sqlc.embed(organization_members),
|
||||
users.username, users.avatar_url, users.name, users.rbac_roles as "global_roles"
|
||||
users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles"
|
||||
FROM
|
||||
organization_members
|
||||
INNER JOIN
|
||||
|
||||
@@ -319,6 +319,7 @@ func convertOrganizationMembersWithUserData(ctx context.Context, db database.Sto
|
||||
Username: rows[i].Username,
|
||||
AvatarURL: rows[i].AvatarURL,
|
||||
Name: rows[i].Name,
|
||||
Email: rows[i].Email,
|
||||
GlobalRoles: db2sdk.SlimRolesFromNames(rows[i].GlobalRoles),
|
||||
OrganizationMember: convertedMembers[i],
|
||||
})
|
||||
|
||||
@@ -74,6 +74,7 @@ type OrganizationMemberWithUserData struct {
|
||||
Username string `table:"username,default_sort" json:"username"`
|
||||
Name string `table:"name" json:"name"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Email string `json:"email"`
|
||||
GlobalRoles []SlimRole `json:"global_roles"`
|
||||
OrganizationMember `table:"m,recursive_inline"`
|
||||
}
|
||||
|
||||
Generated
+2
@@ -28,6 +28,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"email": "string",
|
||||
"global_roles": [
|
||||
{
|
||||
"display_name": "string",
|
||||
@@ -66,6 +67,7 @@ Status Code **200**
|
||||
| `[array item]` | array | false | | |
|
||||
| `» avatar_url` | string | false | | |
|
||||
| `» created_at` | string(date-time) | false | | |
|
||||
| `» email` | string | false | | |
|
||||
| `» global_roles` | array | false | | |
|
||||
| `»» display_name` | string | false | | |
|
||||
| `»» name` | string | false | | |
|
||||
|
||||
Generated
+2
@@ -3622,6 +3622,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
{
|
||||
"avatar_url": "string",
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"email": "string",
|
||||
"global_roles": [
|
||||
{
|
||||
"display_name": "string",
|
||||
@@ -3650,6 +3651,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
| ----------------- | ----------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `avatar_url` | string | false | | |
|
||||
| `created_at` | string | false | | |
|
||||
| `email` | string | false | | |
|
||||
| `global_roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
|
||||
@@ -549,6 +549,27 @@ class ApiMethods {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getOrganizationRoles = async (organizationId: string) => {
|
||||
const response = await this.axios.get<TypesGen.AssignableRoles[]>(
|
||||
`/api/v2/organizations/${organizationId}/members/roles`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
updateOrganizationMemberRoles = async (
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
roles: TypesGen.SlimRole["name"][],
|
||||
): Promise<TypesGen.User> => {
|
||||
const response = await this.axios.put<TypesGen.User>(
|
||||
`/api/v2/organizations/${organizationId}/members/${userId}/roles`,
|
||||
{ roles },
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
addOrganizationMember = async (organizationId: string, userId: string) => {
|
||||
const response = await this.axios.post<TypesGen.OrganizationMember>(
|
||||
`/api/v2/organizations/${organizationId}/members/${userId}`,
|
||||
|
||||
@@ -49,7 +49,7 @@ export const deleteOrganization = (queryClient: QueryClient) => {
|
||||
export const organizationMembers = (id: string) => {
|
||||
return {
|
||||
queryFn: () => API.getOrganizationMembers(id),
|
||||
key: ["organization", id, "members"],
|
||||
queryKey: ["organization", id, "members"],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -80,6 +80,25 @@ export const removeOrganizationMember = (
|
||||
};
|
||||
};
|
||||
|
||||
export const updateOrganizationMemberRoles = (
|
||||
queryClient: QueryClient,
|
||||
organizationId: string,
|
||||
) => {
|
||||
return {
|
||||
mutationFn: ({ userId, roles }: { userId: string; roles: string[] }) => {
|
||||
return API.updateOrganizationMemberRoles(organizationId, userId, roles);
|
||||
},
|
||||
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries([
|
||||
"organization",
|
||||
organizationId,
|
||||
"members",
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const organizationsKey = ["organizations"] as const;
|
||||
|
||||
export const organizations = () => {
|
||||
|
||||
@@ -6,3 +6,10 @@ export const roles = () => {
|
||||
queryFn: API.getRoles,
|
||||
};
|
||||
};
|
||||
|
||||
export const organizationRoles = (organizationId: string) => {
|
||||
return {
|
||||
queryKey: ["organization", organizationId, "roles"],
|
||||
queryFn: () => API.getOrganizationRoles(organizationId),
|
||||
};
|
||||
};
|
||||
|
||||
Generated
+1
@@ -874,6 +874,7 @@ export interface OrganizationMemberWithUserData extends OrganizationMember {
|
||||
readonly username: string;
|
||||
readonly name: string;
|
||||
readonly avatar_url: string;
|
||||
readonly email: string;
|
||||
readonly global_roles: readonly SlimRole[];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { fireEvent, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { HttpResponse, http } from "msw";
|
||||
import type { SlimRole } from "api/typesGenerated";
|
||||
import { MockUser, MockOrganizationAuditorRole } from "testHelpers/entities";
|
||||
import {
|
||||
renderWithTemplateSettingsLayout,
|
||||
waitForLoaderToBeRemoved,
|
||||
} from "testHelpers/renderHelpers";
|
||||
import { server } from "testHelpers/server";
|
||||
import OrganizationMembersPage from "./OrganizationMembersPage";
|
||||
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
beforeAll(() => {
|
||||
server.use(
|
||||
http.get("/api/v2/experiments", () => {
|
||||
return HttpResponse.json(["multi-organization"]);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const renderPage = async () => {
|
||||
renderWithTemplateSettingsLayout(<OrganizationMembersPage />, {
|
||||
route: `/organizations/my-organization/members`,
|
||||
path: `/organizations/:organization/members`,
|
||||
});
|
||||
await waitForLoaderToBeRemoved();
|
||||
};
|
||||
|
||||
const removeMember = async () => {
|
||||
const user = userEvent.setup();
|
||||
// Click on the "More options" button to display the "Remove" option
|
||||
const moreButtons = await screen.findAllByLabelText("More options");
|
||||
// get MockUser2
|
||||
const selectedMoreButton = moreButtons[0];
|
||||
|
||||
await user.click(selectedMoreButton);
|
||||
|
||||
const removeButton = screen.getByText(/Remove/);
|
||||
await user.click(removeButton);
|
||||
};
|
||||
|
||||
const updateUserRole = async (role: SlimRole) => {
|
||||
// Get the first user in the table
|
||||
const users = await screen.findAllByText(/.*@coder.com/);
|
||||
const userRow = users[0].closest("tr");
|
||||
if (!userRow) {
|
||||
throw new Error("Error on get the first user row");
|
||||
}
|
||||
|
||||
// Click on the "edit icon" to display the role options
|
||||
const editButton = within(userRow).getByTitle("Edit user roles");
|
||||
fireEvent.click(editButton);
|
||||
|
||||
// Click on the role option
|
||||
const fieldset = await screen.findByTitle("Available roles");
|
||||
const roleOption = within(fieldset).getByText(role.display_name);
|
||||
fireEvent.click(roleOption);
|
||||
|
||||
return {
|
||||
userRow,
|
||||
};
|
||||
};
|
||||
|
||||
describe("OrganizationMembersPage", () => {
|
||||
describe("remove member", () => {
|
||||
describe("when it is success", () => {
|
||||
it("shows a success message", async () => {
|
||||
await renderPage();
|
||||
await removeMember();
|
||||
await screen.findByText("Member removed.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Update user role", () => {
|
||||
describe("when it is success", () => {
|
||||
it("updates the roles", async () => {
|
||||
server.use(
|
||||
http.put(
|
||||
`/api/v2/organizations/:organizationId/members/${MockUser.id}/roles`,
|
||||
async () => {
|
||||
return HttpResponse.json({
|
||||
...MockUser,
|
||||
roles: [...MockUser.roles, MockOrganizationAuditorRole],
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await renderPage();
|
||||
await updateUserRole(MockOrganizationAuditorRole);
|
||||
await screen.findByText("Roles updated successfully.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when it fails", () => {
|
||||
it("shows an error message", async () => {
|
||||
server.use(
|
||||
http.put(
|
||||
`/api/v2/organizations/:organizationId/members/${MockUser.id}/roles`,
|
||||
() => {
|
||||
return HttpResponse.json(
|
||||
{ message: "Error on updating the user roles." },
|
||||
{ status: 400 },
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await renderPage();
|
||||
await updateUserRole(MockOrganizationAuditorRole);
|
||||
await screen.findByText("Error on updating the user roles.");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,6 @@ import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { type FC, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
@@ -16,11 +15,13 @@ import {
|
||||
addOrganizationMember,
|
||||
organizationMembers,
|
||||
removeOrganizationMember,
|
||||
updateOrganizationMemberRoles,
|
||||
} from "api/queries/organizations";
|
||||
import type { OrganizationMemberWithUserData, User } from "api/typesGenerated";
|
||||
import { organizationRoles } from "api/queries/roles";
|
||||
import type { User } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { AvatarData } from "components/AvatarData/AvatarData";
|
||||
import { displayError } from "components/GlobalSnackbar/utils";
|
||||
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
|
||||
import {
|
||||
MoreMenu,
|
||||
MoreMenuTrigger,
|
||||
@@ -29,11 +30,12 @@ import {
|
||||
ThreeDotsButton,
|
||||
} from "components/MoreMenu/MoreMenu";
|
||||
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||
import { UserAvatar } from "components/UserAvatar/UserAvatar";
|
||||
import { useAuthenticated } from "contexts/auth/RequireAuth";
|
||||
import { TableColumnHelpTooltip } from "./UserTable/TableColumnHelpTooltip";
|
||||
import { UserRoleCell } from "./UserTable/UserRoleCell";
|
||||
|
||||
const OrganizationMembersPage: FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -41,12 +43,17 @@ const OrganizationMembersPage: FC = () => {
|
||||
const { user: me } = useAuthenticated();
|
||||
|
||||
const membersQuery = useQuery(organizationMembers(organization));
|
||||
const organizationRolesQuery = useQuery(organizationRoles(organization));
|
||||
|
||||
const addMemberMutation = useMutation(
|
||||
addOrganizationMember(queryClient, organization),
|
||||
);
|
||||
const removeMemberMutation = useMutation(
|
||||
removeOrganizationMember(queryClient, organization),
|
||||
);
|
||||
const updateMemberRolesMutation = useMutation(
|
||||
updateOrganizationMemberRoles(queryClient, organization),
|
||||
);
|
||||
|
||||
const error =
|
||||
membersQuery.error ?? addMemberMutation.error ?? removeMemberMutation.error;
|
||||
@@ -61,7 +68,7 @@ const OrganizationMembersPage: FC = () => {
|
||||
<Stack>
|
||||
{Boolean(error) && <ErrorAlert error={error} />}
|
||||
|
||||
<AddGroupMember
|
||||
<AddOrganizationMember
|
||||
isLoading={addMemberMutation.isLoading}
|
||||
onSubmit={async (user) => {
|
||||
await addMemberMutation.mutateAsync(user.id);
|
||||
@@ -74,7 +81,12 @@ const OrganizationMembersPage: FC = () => {
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width="50%">User</TableCell>
|
||||
<TableCell width="49%">Roles</TableCell>
|
||||
<TableCell width="49%">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<span>Roles</span>
|
||||
<TableColumnHelpTooltip variant="roles" />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell width="1%"></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -89,26 +101,31 @@ const OrganizationMembersPage: FC = () => {
|
||||
avatarURL={member.avatar_url}
|
||||
/>
|
||||
}
|
||||
title={member.name}
|
||||
subtitle={member.username}
|
||||
title={member.name || member.username}
|
||||
subtitle={member.email}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getMemberRoles(member).map((role) => (
|
||||
<Pill
|
||||
key={role.name}
|
||||
css={role.global ? styles.globalRole : styles.role}
|
||||
>
|
||||
{role.global ? (
|
||||
<Tooltip title="This user has this role for all organizations.">
|
||||
<span>{role.name}*</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
role.name
|
||||
)}
|
||||
</Pill>
|
||||
))}
|
||||
</TableCell>
|
||||
<UserRoleCell
|
||||
inheritedRoles={member.global_roles}
|
||||
roles={member.roles}
|
||||
allAvailableRoles={organizationRolesQuery.data}
|
||||
oidcRoleSyncEnabled={false}
|
||||
isLoading={updateMemberRolesMutation.isLoading}
|
||||
canEditUsers
|
||||
onEditRoles={async (newRoleNames) => {
|
||||
try {
|
||||
await updateMemberRolesMutation.mutateAsync({
|
||||
userId: member.user_id,
|
||||
roles: newRoleNames,
|
||||
});
|
||||
displaySuccess("Roles updated successfully.");
|
||||
} catch (e) {
|
||||
displayError(
|
||||
getErrorMessage(e, "Failed to update roles."),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TableCell>
|
||||
{member.user_id !== me.id && (
|
||||
<MoreMenu>
|
||||
@@ -119,10 +136,20 @@ const OrganizationMembersPage: FC = () => {
|
||||
<MoreMenuItem
|
||||
danger
|
||||
onClick={async () => {
|
||||
await removeMemberMutation.mutateAsync(
|
||||
member.user_id,
|
||||
);
|
||||
void membersQuery.refetch();
|
||||
try {
|
||||
await removeMemberMutation.mutateAsync(
|
||||
member.user_id,
|
||||
);
|
||||
void membersQuery.refetch();
|
||||
displaySuccess("Member removed.");
|
||||
} catch (e) {
|
||||
displayError(
|
||||
getErrorMessage(
|
||||
e,
|
||||
"Failed to remove member.",
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
@@ -141,33 +168,17 @@ const OrganizationMembersPage: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
function getMemberRoles(member: OrganizationMemberWithUserData) {
|
||||
const roles = new Map<string, { name: string; global?: boolean }>();
|
||||
|
||||
for (const role of member.global_roles) {
|
||||
roles.set(role.name, {
|
||||
name: role.display_name || role.name,
|
||||
global: true,
|
||||
});
|
||||
}
|
||||
for (const role of member.roles) {
|
||||
if (roles.has(role.name)) {
|
||||
continue;
|
||||
}
|
||||
roles.set(role.name, { name: role.display_name || role.name });
|
||||
}
|
||||
|
||||
return [...roles.values()];
|
||||
}
|
||||
|
||||
export default OrganizationMembersPage;
|
||||
|
||||
interface AddGroupMemberProps {
|
||||
interface AddOrganizationMemberProps {
|
||||
isLoading: boolean;
|
||||
onSubmit: (user: User) => Promise<void>;
|
||||
}
|
||||
|
||||
const AddGroupMember: FC<AddGroupMemberProps> = ({ isLoading, onSubmit }) => {
|
||||
const AddOrganizationMember: FC<AddOrganizationMemberProps> = ({
|
||||
isLoading,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
|
||||
return (
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ export interface EditRolesButtonProps {
|
||||
selectedRoleNames: Set<string>;
|
||||
onChange: (roles: SlimRole["name"][]) => void;
|
||||
oidcRoleSync: boolean;
|
||||
userLoginType: string;
|
||||
userLoginType?: string;
|
||||
}
|
||||
|
||||
export const EditRolesButton: FC<EditRolesButtonProps> = ({
|
||||
+97
-36
@@ -13,11 +13,12 @@
|
||||
* went with a simpler design. If we decide we really do need to display the
|
||||
* users like that, though, know that it will be painful
|
||||
*/
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import type { FC } from "react";
|
||||
import type { SlimRole, User } from "api/typesGenerated";
|
||||
import type { LoginType, SlimRole } from "api/typesGenerated";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import {
|
||||
Popover,
|
||||
@@ -27,27 +28,34 @@ import {
|
||||
import { EditRolesButton } from "./EditRolesButton";
|
||||
|
||||
type UserRoleCellProps = {
|
||||
canEditUsers: boolean;
|
||||
allAvailableRoles: SlimRole[] | undefined;
|
||||
user: User;
|
||||
isLoading: boolean;
|
||||
canEditUsers: boolean;
|
||||
allAvailableRoles: readonly SlimRole[] | undefined;
|
||||
userLoginType?: LoginType;
|
||||
inheritedRoles?: readonly SlimRole[];
|
||||
roles: readonly SlimRole[];
|
||||
oidcRoleSyncEnabled: boolean;
|
||||
onUserRolesUpdate: (user: User, newRoleNames: string[]) => void;
|
||||
onEditRoles: (newRoleNames: string[]) => void;
|
||||
};
|
||||
|
||||
export const UserRoleCell: FC<UserRoleCellProps> = ({
|
||||
isLoading,
|
||||
canEditUsers,
|
||||
allAvailableRoles,
|
||||
user,
|
||||
isLoading,
|
||||
userLoginType,
|
||||
inheritedRoles,
|
||||
roles,
|
||||
oidcRoleSyncEnabled,
|
||||
onUserRolesUpdate,
|
||||
onEditRoles,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const mergedRoles = getTieredRoles(inheritedRoles ?? [], roles);
|
||||
const [mainDisplayRole = fallbackRole, ...extraRoles] =
|
||||
sortRolesByAccessLevel(user.roles ?? []);
|
||||
const hasOwnerRole = mainDisplayRole.name === "owner";
|
||||
sortRolesByAccessLevel(mergedRoles ?? []);
|
||||
const hasOwnerRole =
|
||||
mainDisplayRole.name === "owner" ||
|
||||
mainDisplayRole.name === "organization-admin";
|
||||
|
||||
const displayName = mainDisplayRole.display_name || mainDisplayRole.name;
|
||||
|
||||
return (
|
||||
<TableCell>
|
||||
@@ -55,9 +63,9 @@ export const UserRoleCell: FC<UserRoleCellProps> = ({
|
||||
{canEditUsers && (
|
||||
<EditRolesButton
|
||||
roles={sortRolesByAccessLevel(allAvailableRoles ?? [])}
|
||||
selectedRoleNames={getSelectedRoleNames(user.roles)}
|
||||
selectedRoleNames={getSelectedRoleNames(roles)}
|
||||
isLoading={isLoading}
|
||||
userLoginType={user.login_type}
|
||||
userLoginType={userLoginType}
|
||||
oidcRoleSync={oidcRoleSyncEnabled}
|
||||
onChange={(roles) => {
|
||||
// Remove the fallback role because it is only for the UI
|
||||
@@ -65,22 +73,27 @@ export const UserRoleCell: FC<UserRoleCellProps> = ({
|
||||
(role) => role !== fallbackRole.name,
|
||||
);
|
||||
|
||||
onUserRolesUpdate(user, rolesWithoutFallback);
|
||||
onEditRoles(rolesWithoutFallback);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Pill
|
||||
css={{
|
||||
backgroundColor: hasOwnerRole
|
||||
? theme.roles.info.background
|
||||
: theme.experimental.l2.background,
|
||||
borderColor: hasOwnerRole
|
||||
? theme.roles.info.outline
|
||||
: theme.experimental.l2.outline,
|
||||
}}
|
||||
css={
|
||||
hasOwnerRole
|
||||
? styles.ownerRoleBadge
|
||||
: mainDisplayRole.global
|
||||
? styles.globalRoleBadge
|
||||
: styles.roleBadge
|
||||
}
|
||||
>
|
||||
{mainDisplayRole.display_name}
|
||||
{mainDisplayRole.global ? (
|
||||
<Tooltip title="This user has this role for all organizations.">
|
||||
<span>{displayName}*</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
displayName
|
||||
)}
|
||||
</Pill>
|
||||
|
||||
{extraRoles.length > 0 && <OverflowRolePill roles={extraRoles} />}
|
||||
@@ -90,7 +103,7 @@ export const UserRoleCell: FC<UserRoleCellProps> = ({
|
||||
};
|
||||
|
||||
type OverflowRolePillProps = {
|
||||
roles: readonly SlimRole[];
|
||||
roles: readonly TieredSlimRole[];
|
||||
};
|
||||
|
||||
const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
|
||||
@@ -105,7 +118,7 @@ const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
|
||||
borderColor: theme.palette.divider,
|
||||
}}
|
||||
>
|
||||
{`+${roles.length} more`}
|
||||
+{roles.length} more
|
||||
</Pill>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -135,12 +148,15 @@ const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
|
||||
{roles.map((role) => (
|
||||
<Pill
|
||||
key={role.name}
|
||||
css={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderColor: theme.palette.divider,
|
||||
}}
|
||||
css={role.global ? styles.globalRoleBadge : styles.roleBadge}
|
||||
>
|
||||
{role.display_name || role.name}
|
||||
{role.global ? (
|
||||
<Tooltip title="This user has this role for all organizations.">
|
||||
<span>{role.display_name || role.name}*</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
role.display_name || role.name
|
||||
)}
|
||||
</Pill>
|
||||
))}
|
||||
</PopoverContent>
|
||||
@@ -148,21 +164,40 @@ const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const fallbackRole: SlimRole = {
|
||||
const styles = {
|
||||
globalRoleBadge: (theme) => ({
|
||||
backgroundColor: theme.roles.active.background,
|
||||
borderColor: theme.roles.active.outline,
|
||||
}),
|
||||
ownerRoleBadge: (theme) => ({
|
||||
backgroundColor: theme.roles.info.background,
|
||||
borderColor: theme.roles.info.outline,
|
||||
}),
|
||||
roleBadge: (theme) => ({
|
||||
backgroundColor: theme.experimental.l2.background,
|
||||
borderColor: theme.experimental.l2.outline,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
const fallbackRole: TieredSlimRole = {
|
||||
name: "member",
|
||||
display_name: "Member",
|
||||
} as const;
|
||||
|
||||
const roleNamesByAccessLevel: readonly string[] = [
|
||||
"owner",
|
||||
"organization-admin",
|
||||
"user-admin",
|
||||
"organization-user-admin",
|
||||
"template-admin",
|
||||
"organization-template-admin",
|
||||
"auditor",
|
||||
"organization-auditor",
|
||||
];
|
||||
|
||||
function sortRolesByAccessLevel(
|
||||
roles: readonly SlimRole[],
|
||||
): readonly SlimRole[] {
|
||||
function sortRolesByAccessLevel<T extends SlimRole>(
|
||||
roles: readonly T[],
|
||||
): readonly T[] {
|
||||
if (roles.length === 0) {
|
||||
return roles;
|
||||
}
|
||||
@@ -182,3 +217,29 @@ function getSelectedRoleNames(roles: readonly SlimRole[]) {
|
||||
|
||||
return roleNameSet;
|
||||
}
|
||||
|
||||
interface TieredSlimRole extends SlimRole {
|
||||
global?: boolean;
|
||||
}
|
||||
|
||||
function getTieredRoles(
|
||||
globalRoles: readonly SlimRole[],
|
||||
localRoles: readonly SlimRole[],
|
||||
) {
|
||||
const roles = new Map<string, TieredSlimRole>();
|
||||
|
||||
for (const role of globalRoles) {
|
||||
roles.set(role.name, {
|
||||
...role,
|
||||
global: true,
|
||||
});
|
||||
}
|
||||
for (const role of localRoles) {
|
||||
if (roles.has(role.name)) {
|
||||
continue;
|
||||
}
|
||||
roles.set(role.name, role);
|
||||
}
|
||||
|
||||
return [...roles.values()];
|
||||
}
|
||||
-3
@@ -4,7 +4,6 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { setGroupRole, setUserRole, templateACL } from "api/queries/templates";
|
||||
import { displaySuccess } from "components/GlobalSnackbar/utils";
|
||||
import { Paywall } from "components/Paywall/Paywall";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
|
||||
import { docs } from "utils/docs";
|
||||
import { pageTitle } from "utils/page";
|
||||
@@ -12,7 +11,6 @@ import { useTemplateSettings } from "../TemplateSettingsLayout";
|
||||
import { TemplatePermissionsPageView } from "./TemplatePermissionsPageView";
|
||||
|
||||
export const TemplatePermissionsPage: FC = () => {
|
||||
const { organizationId } = useDashboard();
|
||||
const { template, permissions } = useTemplateSettings();
|
||||
const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility();
|
||||
const templateACLQuery = useQuery(templateACL(template.id));
|
||||
@@ -39,7 +37,6 @@ export const TemplatePermissionsPage: FC = () => {
|
||||
/>
|
||||
) : (
|
||||
<TemplatePermissionsPageView
|
||||
organizationId={organizationId}
|
||||
templateID={template.id}
|
||||
templateACL={templateACLQuery.data}
|
||||
canUpdatePermissions={Boolean(permissions?.canUpdateTemplate)}
|
||||
|
||||
+1
-6
@@ -1,9 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import {
|
||||
MockOrganization,
|
||||
MockTemplateACL,
|
||||
MockTemplateACLEmpty,
|
||||
} from "testHelpers/entities";
|
||||
import { MockTemplateACL, MockTemplateACLEmpty } from "testHelpers/entities";
|
||||
import { TemplatePermissionsPageView } from "./TemplatePermissionsPageView";
|
||||
|
||||
const meta: Meta<typeof TemplatePermissionsPageView> = {
|
||||
@@ -32,6 +28,5 @@ export const WithUpdatePermissions: Story = {
|
||||
args: {
|
||||
templateACL: MockTemplateACL,
|
||||
canUpdatePermissions: true,
|
||||
organizationId: MockOrganization.id,
|
||||
},
|
||||
};
|
||||
|
||||
+1
-5
@@ -39,7 +39,6 @@ import {
|
||||
} from "./UserOrGroupAutocomplete";
|
||||
|
||||
type AddTemplateUserOrGroupProps = {
|
||||
organizationId: string;
|
||||
templateID: string;
|
||||
isLoading: boolean;
|
||||
templateACL: TemplateACL | undefined;
|
||||
@@ -56,9 +55,9 @@ type AddTemplateUserOrGroupProps = {
|
||||
|
||||
const AddTemplateUserOrGroup: FC<AddTemplateUserOrGroupProps> = ({
|
||||
isLoading,
|
||||
onSubmit,
|
||||
templateID,
|
||||
templateACL,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const [selectedOption, setSelectedOption] =
|
||||
useState<UserOrGroupAutocompleteValue>(null);
|
||||
@@ -161,7 +160,6 @@ const RoleSelect: FC<SelectProps> = (props) => {
|
||||
export interface TemplatePermissionsPageViewProps {
|
||||
templateACL: TemplateACL | undefined;
|
||||
templateID: string;
|
||||
organizationId: string;
|
||||
canUpdatePermissions: boolean;
|
||||
// User
|
||||
onAddUser: (
|
||||
@@ -190,7 +188,6 @@ export const TemplatePermissionsPageView: FC<
|
||||
> = ({
|
||||
templateACL,
|
||||
canUpdatePermissions,
|
||||
organizationId,
|
||||
templateID,
|
||||
// User
|
||||
onAddUser,
|
||||
@@ -222,7 +219,6 @@ export const TemplatePermissionsPageView: FC<
|
||||
<AddTemplateUserOrGroup
|
||||
templateACL={templateACL}
|
||||
templateID={templateID}
|
||||
organizationId={organizationId}
|
||||
isLoading={isAddingUser || isAddingGroup}
|
||||
onSubmit={(value, role, resetAutocomplete) =>
|
||||
"members" in value
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
import { renderWithAuth } from "testHelpers/renderHelpers";
|
||||
import { server } from "testHelpers/server";
|
||||
import { Language as ResetPasswordDialogLanguage } from "./ResetPasswordDialog";
|
||||
import { UsersPage } from "./UsersPage";
|
||||
import UsersPage from "./UsersPage";
|
||||
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
const renderPage = () => {
|
||||
return renderWithAuth(<UsersPage />);
|
||||
@@ -116,16 +118,14 @@ const updateUserRole = async (role: SlimRole) => {
|
||||
|
||||
// Click on the role option
|
||||
const fieldset = await screen.findByTitle("Available roles");
|
||||
const auditorOption = within(fieldset).getByText(role.display_name);
|
||||
fireEvent.click(auditorOption);
|
||||
const roleOption = within(fieldset).getByText(role.display_name);
|
||||
fireEvent.click(roleOption);
|
||||
|
||||
return {
|
||||
userRow,
|
||||
};
|
||||
};
|
||||
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
describe("UsersPage", () => {
|
||||
describe("suspend user", () => {
|
||||
describe("when it is success", () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ import { ResetPasswordDialog } from "./ResetPasswordDialog";
|
||||
import { useStatusFilterMenu } from "./UsersFilter";
|
||||
import { UsersPageView } from "./UsersPageView";
|
||||
|
||||
export const UsersPage: FC = () => {
|
||||
const UsersPage: FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -125,12 +125,9 @@ export const UsersPage: FC = () => {
|
||||
newPassword: generateRandomString(12),
|
||||
});
|
||||
}}
|
||||
onUpdateUserRoles={async (user, roles) => {
|
||||
onUpdateUserRoles={async (userId, roles) => {
|
||||
try {
|
||||
await updateRolesMutation.mutateAsync({
|
||||
userId: user.id,
|
||||
roles,
|
||||
});
|
||||
await updateRolesMutation.mutateAsync({ userId, roles });
|
||||
displaySuccess("Successfully updated the user roles.");
|
||||
} catch (e) {
|
||||
displayError(
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface UsersPageViewProps {
|
||||
onActivateUser: (user: TypesGen.User) => void;
|
||||
onResetUserPassword: (user: TypesGen.User) => void;
|
||||
onUpdateUserRoles: (
|
||||
user: TypesGen.User,
|
||||
userId: string,
|
||||
roles: TypesGen.SlimRole["name"][],
|
||||
) => void;
|
||||
filterProps: ComponentProps<typeof UsersFilter>;
|
||||
|
||||
@@ -3,6 +3,7 @@ import GroupIcon from "@mui/icons-material/Group";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import type { FC } from "react";
|
||||
import type { Group } from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { OverflowY } from "components/OverflowY/OverflowY";
|
||||
@@ -17,7 +18,7 @@ type GroupsCellProps = {
|
||||
userGroups: readonly Group[] | undefined;
|
||||
};
|
||||
|
||||
export function UserGroupsCell({ userGroups }: GroupsCellProps) {
|
||||
export const UserGroupsCell: FC<GroupsCellProps> = ({ userGroups }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
@@ -123,4 +124,4 @@ export function UserGroupsCell({ userGroups }: GroupsCellProps) {
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { FC } from "react";
|
||||
import type { GroupsByUserId } from "api/queries/groups";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { TableColumnHelpTooltip } from "./TableColumnHelpTooltip";
|
||||
import { TableColumnHelpTooltip } from "../../ManagementSettingsPage/UserTable/TableColumnHelpTooltip";
|
||||
import { UsersTableBody } from "./UsersTableBody";
|
||||
|
||||
export const Language = {
|
||||
@@ -35,7 +35,7 @@ export interface UsersTableProps {
|
||||
onViewActivity: (user: TypesGen.User) => void;
|
||||
onResetUserPassword: (user: TypesGen.User) => void;
|
||||
onUpdateUserRoles: (
|
||||
user: TypesGen.User,
|
||||
userId: string,
|
||||
roles: TypesGen.SlimRole["name"][],
|
||||
) => void;
|
||||
isNonInitialPage: boolean;
|
||||
|
||||
@@ -30,8 +30,8 @@ import {
|
||||
TableLoaderSkeleton,
|
||||
TableRowSkeleton,
|
||||
} from "components/TableLoader/TableLoader";
|
||||
import { UserRoleCell } from "../../ManagementSettingsPage/UserTable/UserRoleCell";
|
||||
import { UserGroupsCell } from "./UserGroupsCell";
|
||||
import { UserRoleCell } from "./UserRoleCell";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
@@ -51,7 +51,7 @@ interface UsersTableBodyProps {
|
||||
onActivateUser: (user: TypesGen.User) => void;
|
||||
onResetUserPassword: (user: TypesGen.User) => void;
|
||||
onUpdateUserRoles: (
|
||||
user: TypesGen.User,
|
||||
userId: string,
|
||||
roles: TypesGen.SlimRole["name"][],
|
||||
) => void;
|
||||
isNonInitialPage: boolean;
|
||||
@@ -156,10 +156,11 @@ export const UsersTableBody: FC<UsersTableBodyProps> = ({
|
||||
<UserRoleCell
|
||||
canEditUsers={canEditUsers}
|
||||
allAvailableRoles={roles}
|
||||
user={user}
|
||||
userLoginType={user.login_type}
|
||||
roles={user.roles}
|
||||
oidcRoleSyncEnabled={oidcRoleSyncEnabled}
|
||||
isLoading={Boolean(isUpdatingUserRoles)}
|
||||
onUserRolesUpdate={onUpdateUserRoles}
|
||||
onEditRoles={(roles) => onUpdateUserRoles(user.id, roles)}
|
||||
/>
|
||||
|
||||
<UserGroupsCell userGroups={groupsByUserId?.get(user.id)} />
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { FileTree } from "utils/filetree";
|
||||
import type { TemplateVersionFiles } from "utils/templateVersion";
|
||||
|
||||
export const MockOrganization: TypesGen.Organization = {
|
||||
id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0",
|
||||
id: "my-organization-id",
|
||||
name: "my-organization",
|
||||
display_name: "My Organization",
|
||||
description: "An organization that gets used for stuff.",
|
||||
@@ -27,6 +27,17 @@ export const MockDefaultOrganization: TypesGen.Organization = {
|
||||
is_default: true,
|
||||
};
|
||||
|
||||
export const MockOrganization2: TypesGen.Organization = {
|
||||
id: "my-organization-2-id",
|
||||
name: "my-organization-2",
|
||||
display_name: "My Organization 2",
|
||||
description: "Another organization that gets used for stuff.",
|
||||
icon: "/emojis/1f957.png",
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
is_default: false,
|
||||
};
|
||||
|
||||
export const MockTemplateDAUResponse: TypesGen.DAUsResponse = {
|
||||
tz_hour_offset: 0,
|
||||
entries: [
|
||||
@@ -265,11 +276,6 @@ export const MockTemplateAdminRole: TypesGen.Role = {
|
||||
organization_id: "",
|
||||
};
|
||||
|
||||
export const MockMemberRole: TypesGen.SlimRole = {
|
||||
name: "member",
|
||||
display_name: "Member",
|
||||
};
|
||||
|
||||
export const MockAuditorRole: TypesGen.Role = {
|
||||
name: "auditor",
|
||||
display_name: "Auditor",
|
||||
@@ -279,6 +285,47 @@ export const MockAuditorRole: TypesGen.Role = {
|
||||
organization_id: "",
|
||||
};
|
||||
|
||||
export const MockMemberRole: TypesGen.SlimRole = {
|
||||
name: "member",
|
||||
display_name: "Member",
|
||||
};
|
||||
|
||||
export const MockOrganizationAdminRole: TypesGen.Role = {
|
||||
name: "organization-admin",
|
||||
display_name: "Organization Admin",
|
||||
site_permissions: [],
|
||||
organization_permissions: [],
|
||||
user_permissions: [],
|
||||
organization_id: MockOrganization.id,
|
||||
};
|
||||
|
||||
export const MockOrganizationUserAdminRole: TypesGen.Role = {
|
||||
name: "organization-user-admin",
|
||||
display_name: "Organization User Admin",
|
||||
site_permissions: [],
|
||||
organization_permissions: [],
|
||||
user_permissions: [],
|
||||
organization_id: MockOrganization.id,
|
||||
};
|
||||
|
||||
export const MockOrganizationTemplateAdminRole: TypesGen.Role = {
|
||||
name: "organization-template-admin",
|
||||
display_name: "Organization Template Admin",
|
||||
site_permissions: [],
|
||||
organization_permissions: [],
|
||||
user_permissions: [],
|
||||
organization_id: MockOrganization.id,
|
||||
};
|
||||
|
||||
export const MockOrganizationAuditorRole: TypesGen.Role = {
|
||||
name: "organization-auditor",
|
||||
display_name: "Organization Auditor",
|
||||
site_permissions: [],
|
||||
organization_permissions: [],
|
||||
user_permissions: [],
|
||||
organization_id: MockOrganization.id,
|
||||
};
|
||||
|
||||
// assignableRole takes a role and a boolean. The boolean implies if the
|
||||
// actor can assign (add/remove) the role from other users.
|
||||
export function assignableRole(
|
||||
@@ -319,19 +366,8 @@ export const MockUser: TypesGen.User = {
|
||||
};
|
||||
|
||||
export const MockUserAdmin: TypesGen.User = {
|
||||
id: "test-user",
|
||||
username: "TestUser",
|
||||
email: "test@coder.com",
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
status: "active",
|
||||
organization_ids: [MockOrganization.id],
|
||||
...MockUser,
|
||||
roles: [MockUserAdminRole],
|
||||
avatar_url: "",
|
||||
last_seen_at: "",
|
||||
login_type: "password",
|
||||
theme_preference: "",
|
||||
name: "",
|
||||
};
|
||||
|
||||
export const MockUser2: TypesGen.User = {
|
||||
@@ -366,6 +402,33 @@ export const SuspendedMockUser: TypesGen.User = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
export const MockOrganizationMember: TypesGen.OrganizationMemberWithUserData = {
|
||||
organization_id: MockOrganization.id,
|
||||
user_id: MockUser.id,
|
||||
username: MockUser.username,
|
||||
email: MockUser.email,
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
name: MockUser.name,
|
||||
avatar_url: MockUser.avatar_url,
|
||||
global_roles: MockUser.roles,
|
||||
roles: [],
|
||||
};
|
||||
|
||||
export const MockOrganizationMember2: TypesGen.OrganizationMemberWithUserData =
|
||||
{
|
||||
organization_id: MockOrganization.id,
|
||||
user_id: MockUser2.id,
|
||||
username: MockUser2.username,
|
||||
email: MockUser2.email,
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
name: MockUser2.name,
|
||||
avatar_url: MockUser2.avatar_url,
|
||||
global_roles: MockUser2.roles,
|
||||
roles: [],
|
||||
};
|
||||
|
||||
export const MockProvisioner: TypesGen.ProvisionerDaemon = {
|
||||
created_at: "2022-05-17T17:39:01.382927298Z",
|
||||
id: "test-provisioner",
|
||||
|
||||
@@ -41,10 +41,13 @@ export const handlers = [
|
||||
}),
|
||||
|
||||
// organizations
|
||||
http.get("/api/v2/organizations", () => {
|
||||
return HttpResponse.json([M.MockDefaultOrganization, M.MockOrganization2]);
|
||||
}),
|
||||
http.get("/api/v2/organizations/:organizationId", () => {
|
||||
return HttpResponse.json(M.MockOrganization);
|
||||
}),
|
||||
http.get("api/v2/organizations/:organizationId/templates/examples", () => {
|
||||
http.get("/api/v2/organizations/:organizationId/templates/examples", () => {
|
||||
return HttpResponse.json([M.MockTemplateExample, M.MockTemplateExample2]);
|
||||
}),
|
||||
http.get(
|
||||
@@ -56,6 +59,26 @@ export const handlers = [
|
||||
http.get("/api/v2/organizations/:organizationId/templates", () => {
|
||||
return HttpResponse.json([M.MockTemplate]);
|
||||
}),
|
||||
http.get("/api/v2/organizations/:organizationId/members/roles", () => {
|
||||
return HttpResponse.json([
|
||||
M.MockOrganizationAdminRole,
|
||||
M.MockOrganizationUserAdminRole,
|
||||
M.MockOrganizationTemplateAdminRole,
|
||||
M.MockOrganizationAuditorRole,
|
||||
]);
|
||||
}),
|
||||
http.get("/api/v2/organizations/:organizationId/members", () => {
|
||||
return HttpResponse.json([
|
||||
M.MockOrganizationMember,
|
||||
M.MockOrganizationMember2,
|
||||
]);
|
||||
}),
|
||||
http.delete(
|
||||
`/api/v2/organizations/:organizationId/members/:userId`,
|
||||
async () => {
|
||||
return new HttpResponse(null, { status: 204 });
|
||||
},
|
||||
),
|
||||
|
||||
// templates
|
||||
http.get("/api/v2/templates/:templateId", () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { AppProviders } from "App";
|
||||
import { RequireAuth } from "contexts/auth/RequireAuth";
|
||||
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||
import { DashboardLayout } from "modules/dashboard/DashboardLayout";
|
||||
import { ManagementSettingsLayout } from "pages/ManagementSettingsPage/ManagementSettingsLayout";
|
||||
import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout";
|
||||
import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout";
|
||||
import { MockUser } from "./entities";
|
||||
@@ -186,6 +187,43 @@ export function renderWithWorkspaceSettingsLayout(
|
||||
};
|
||||
}
|
||||
|
||||
export function renderWithManagementSettingsLayout(
|
||||
element: JSX.Element,
|
||||
{
|
||||
path = "/",
|
||||
route = "/",
|
||||
extraRoutes = [],
|
||||
nonAuthenticatedRoutes = [],
|
||||
}: RenderWithAuthOptions = {},
|
||||
) {
|
||||
const routes: RouteObject[] = [
|
||||
{
|
||||
element: <RequireAuth />,
|
||||
children: [
|
||||
{
|
||||
element: <DashboardLayout />,
|
||||
children: [
|
||||
{
|
||||
element: <ManagementSettingsLayout />,
|
||||
children: [{ element, path }, ...extraRoutes],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
...nonAuthenticatedRoutes,
|
||||
];
|
||||
|
||||
const renderResult = renderWithRouter(
|
||||
createMemoryRouter(routes, { initialEntries: [route] }),
|
||||
);
|
||||
|
||||
return {
|
||||
user: MockUser,
|
||||
...renderResult,
|
||||
};
|
||||
}
|
||||
|
||||
export const waitForLoaderToBeRemoved = async (): Promise<void> => {
|
||||
return waitFor(
|
||||
() => {
|
||||
|
||||
Reference in New Issue
Block a user