From 23216d1aa88f257eaebb759636f8f70e96a01194 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 22 Jul 2024 19:24:14 +0000 Subject: [PATCH 01/11] oh boy --- coderd/apidoc/docs.go | 3 +++ coderd/apidoc/swagger.json | 3 +++ coderd/database/queries.sql.go | 4 +++- .../database/queries/organizationmembers.sql | 2 +- coderd/members.go | 1 + codersdk/organizations.go | 1 + docs/api/members.md | 2 ++ docs/api/schemas.md | 2 ++ site/chromatic.config.json | 6 +++++ site/src/api/typesGenerated.ts | 1 + .../OrganizationMembersPage.tsx | 23 ++++++++++++++----- .../TableColumnHelpTooltip.tsx | 0 .../TemplatePermissionsPage.tsx | 2 -- .../TemplatePermissionsPageView.stories.tsx | 7 +----- .../TemplatePermissionsPageView.tsx | 6 +---- .../UsersPage/UsersTable/UserGroupsCell.tsx | 5 ++-- .../pages/UsersPage/UsersTable/UsersTable.tsx | 2 +- 17 files changed, 46 insertions(+), 24 deletions(-) create mode 100644 site/chromatic.config.json rename site/src/pages/{UsersPage/UsersTable => ManagementSettingsPage}/TableColumnHelpTooltip.tsx (100%) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 1ebe7b806f3e4..0ea9a4c16738c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10600,6 +10600,9 @@ const docTemplate = `{ "type": "string", "format": "date-time" }, + "email": { + "type": "string" + }, "global_roles": { "type": "array", "items": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index b8c561568bf6f..e3aa5032e16f5 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9544,6 +9544,9 @@ "type": "string", "format": "date-time" }, + "email": { + "type": "string" + }, "global_roles": { "type": "array", "items": { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 511db6ae4dccf..c33a36ea272dc 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4283,7 +4283,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 @@ -4313,6 +4313,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"` } @@ -4338,6 +4339,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 diff --git a/coderd/database/queries/organizationmembers.sql b/coderd/database/queries/organizationmembers.sql index 8cf6a804e2682..71304c8883602 100644 --- a/coderd/database/queries/organizationmembers.sql +++ b/coderd/database/queries/organizationmembers.sql @@ -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 diff --git a/coderd/members.go b/coderd/members.go index e27f5f8840733..4c28d4b6434f6 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -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], }) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index b1b5933781386..3d9bfe39fa9e6 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -70,6 +70,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"` } diff --git a/docs/api/members.md b/docs/api/members.md index 6a06efdce7f77..1ecf490738f00 100644 --- a/docs/api/members.md +++ b/docs/api/members.md @@ -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 | | | diff --git a/docs/api/schemas.md b/docs/api/schemas.md index a1dd22f5be84e..ee1b40c01f89a 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -3589,6 +3589,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", @@ -3617,6 +3618,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 | | | diff --git a/site/chromatic.config.json b/site/chromatic.config.json new file mode 100644 index 0000000000000..22f9facca65b9 --- /dev/null +++ b/site/chromatic.config.json @@ -0,0 +1,6 @@ +{ + "onlyChanged": true, + "projectId": "Project:624de63c6aacee003aa84340", + "storybookBaseDir": "site", + "zip": true +} diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b6ffcf1c79874..2a1c11bd74232 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -869,6 +869,7 @@ export interface OrganizationMemberWithUserData extends OrganizationMember { readonly username: string; readonly name: string; readonly avatar_url: string; + readonly email: string; readonly global_roles: readonly SlimRole[]; } diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx index 467ee9cedaa10..e51ea9788b855 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx @@ -34,6 +34,8 @@ 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 "./TableColumnHelpTooltip"; +import { groupsByUserId } from "api/queries/groups"; const OrganizationMembersPage: FC = () => { const queryClient = useQueryClient(); @@ -41,6 +43,7 @@ const OrganizationMembersPage: FC = () => { const { user: me } = useAuthenticated(); const membersQuery = useQuery(organizationMembers(organization)); + // const groupsByUserIdQuery = useQuery(groupsByUserId(organization)); const addMemberMutation = useMutation( addOrganizationMember(queryClient, organization), ); @@ -61,7 +64,7 @@ const OrganizationMembersPage: FC = () => { {Boolean(error) && } - { await addMemberMutation.mutateAsync(user.id); @@ -74,7 +77,12 @@ const OrganizationMembersPage: FC = () => { User - Roles + + + Roles + + + @@ -89,8 +97,8 @@ const OrganizationMembersPage: FC = () => { avatarURL={member.avatar_url} /> } - title={member.name} - subtitle={member.username} + title={member.name || member.username} + subtitle={member.email} /> @@ -162,12 +170,15 @@ function getMemberRoles(member: OrganizationMemberWithUserData) { export default OrganizationMembersPage; -interface AddGroupMemberProps { +interface AddOrganizationMemberProps { isLoading: boolean; onSubmit: (user: User) => Promise; } -const AddGroupMember: FC = ({ isLoading, onSubmit }) => { +const AddOrganizationMember: FC = ({ + isLoading, + onSubmit, +}) => { const [selectedUser, setSelectedUser] = useState(null); return ( diff --git a/site/src/pages/UsersPage/UsersTable/TableColumnHelpTooltip.tsx b/site/src/pages/ManagementSettingsPage/TableColumnHelpTooltip.tsx similarity index 100% rename from site/src/pages/UsersPage/UsersTable/TableColumnHelpTooltip.tsx rename to site/src/pages/ManagementSettingsPage/TableColumnHelpTooltip.tsx diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx index 2e9aa072e699a..77086bfc991c4 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx @@ -12,7 +12,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 +38,6 @@ export const TemplatePermissionsPage: FC = () => { /> ) : ( = { @@ -32,6 +28,5 @@ export const WithUpdatePermissions: Story = { args: { templateACL: MockTemplateACL, canUpdatePermissions: true, - organizationId: MockOrganization.id, }, }; diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx index 6f75099bbfb2d..e7e169f80ae85 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx @@ -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 = ({ isLoading, - onSubmit, templateID, templateACL, + onSubmit, }) => { const [selectedOption, setSelectedOption] = useState(null); @@ -161,7 +160,6 @@ const RoleSelect: FC = (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< "members" in value diff --git a/site/src/pages/UsersPage/UsersTable/UserGroupsCell.tsx b/site/src/pages/UsersPage/UsersTable/UserGroupsCell.tsx index 071dc6c798b96..b348319355e7d 100644 --- a/site/src/pages/UsersPage/UsersTable/UserGroupsCell.tsx +++ b/site/src/pages/UsersPage/UsersTable/UserGroupsCell.tsx @@ -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 = ({ userGroups }) => { const theme = useTheme(); return ( @@ -123,4 +124,4 @@ export function UserGroupsCell({ userGroups }: GroupsCellProps) { )} ); -} +}; diff --git a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx index d3748f2d8ea95..82beeedc654da 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx @@ -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/TableColumnHelpTooltip"; import { UsersTableBody } from "./UsersTableBody"; export const Language = { From 86c86adfc3a6ede26348d07f94210563252899e3 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 22 Jul 2024 21:35:57 +0000 Subject: [PATCH 02/11] the stuff --- site/src/api/api.ts | 21 +++++ site/src/api/queries/organizations.ts | 21 ++++- site/src/api/queries/roles.ts | 7 ++ .../OrganizationMembersPage.tsx | 71 ++++++-------- .../UserTable}/EditRolesButton.stories.tsx | 0 .../UserTable}/EditRolesButton.tsx | 0 .../TableColumnHelpTooltip.tsx | 0 .../UserTable}/UserRoleCell.tsx | 93 +++++++++++++------ site/src/pages/UsersPage/UsersPage.tsx | 7 +- site/src/pages/UsersPage/UsersPageView.tsx | 2 +- .../pages/UsersPage/UsersTable/UsersTable.tsx | 4 +- .../UsersPage/UsersTable/UsersTableBody.tsx | 5 +- 12 files changed, 152 insertions(+), 79 deletions(-) rename site/src/pages/{UsersPage/UsersTable => ManagementSettingsPage/UserTable}/EditRolesButton.stories.tsx (100%) rename site/src/pages/{UsersPage/UsersTable => ManagementSettingsPage/UserTable}/EditRolesButton.tsx (100%) rename site/src/pages/ManagementSettingsPage/{ => UserTable}/TableColumnHelpTooltip.tsx (100%) rename site/src/pages/{UsersPage/UsersTable => ManagementSettingsPage/UserTable}/UserRoleCell.tsx (68%) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index b408e290e1273..07010543a63e5 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -549,6 +549,27 @@ class ApiMethods { return response.data; }; + getOrganizationRoles = async (organizationId: string) => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/members/roles`, + ); + + return response.data; + }; + + updateOrganizationMemberRoles = async ( + organizationId: string, + userId: string, + roles: TypesGen.SlimRole["name"][], + ): Promise => { + const response = await this.axios.put( + `/api/v2/organizations/${organizationId}/members/${userId}/roles`, + { roles }, + ); + + return response.data; + }; + addOrganizationMember = async (organizationId: string, userId: string) => { const response = await this.axios.post( `/api/v2/organizations/${organizationId}/members/${userId}`, diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 1dc44a2a1c9a3..98c3c9a61e66a 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -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 = () => { diff --git a/site/src/api/queries/roles.ts b/site/src/api/queries/roles.ts index 2a6c1700b53a7..3fc0486fd61f3 100644 --- a/site/src/api/queries/roles.ts +++ b/site/src/api/queries/roles.ts @@ -6,3 +6,10 @@ export const roles = () => { queryFn: API.getRoles, }; }; + +export const organizationRoles = (organizationId: string) => { + return { + queryKey: ["organizationRoles"], + queryFn: () => API.getOrganizationRoles(organizationId), + }; +}; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx index e51ea9788b855..1206cfd92a754 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx @@ -16,11 +16,12 @@ import { addOrganizationMember, organizationMembers, removeOrganizationMember, + updateOrganizationMemberRoles, } from "api/queries/organizations"; -import type { OrganizationMemberWithUserData, User } from "api/typesGenerated"; +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,13 +30,13 @@ 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 "./TableColumnHelpTooltip"; -import { groupsByUserId } from "api/queries/groups"; +import { TableColumnHelpTooltip } from "./UserTable/TableColumnHelpTooltip"; +import { organizationRoles } from "api/queries/roles"; +import { UserRoleCell } from "./UserTable/UserRoleCell"; const OrganizationMembersPage: FC = () => { const queryClient = useQueryClient(); @@ -43,13 +44,17 @@ const OrganizationMembersPage: FC = () => { const { user: me } = useAuthenticated(); const membersQuery = useQuery(organizationMembers(organization)); - // const groupsByUserIdQuery = useQuery(groupsByUserId(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; @@ -101,22 +106,25 @@ const OrganizationMembersPage: FC = () => { subtitle={member.email} /> - - {getMemberRoles(member).map((role) => ( - - {role.global ? ( - - {role.name}* - - ) : ( - role.name - )} - - ))} - + { + await updateMemberRolesMutation.mutateAsync({ + userId, + roles: newRoleNames, + }); + displaySuccess("Roles updated successfully."); + }} + /> {member.user_id !== me.id && ( @@ -149,25 +157,6 @@ const OrganizationMembersPage: FC = () => { ); }; -function getMemberRoles(member: OrganizationMemberWithUserData) { - const roles = new Map(); - - 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 AddOrganizationMemberProps { diff --git a/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx b/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.stories.tsx similarity index 100% rename from site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx rename to site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.stories.tsx diff --git a/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx b/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx similarity index 100% rename from site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx rename to site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx diff --git a/site/src/pages/ManagementSettingsPage/TableColumnHelpTooltip.tsx b/site/src/pages/ManagementSettingsPage/UserTable/TableColumnHelpTooltip.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/TableColumnHelpTooltip.tsx rename to site/src/pages/ManagementSettingsPage/UserTable/TableColumnHelpTooltip.tsx diff --git a/site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx b/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx similarity index 68% rename from site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx rename to site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx index 398354f94ee69..206659363877e 100644 --- a/site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx +++ b/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx @@ -13,9 +13,10 @@ * 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 { Interpolation, 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 { Pill } from "components/Pill/Pill"; @@ -27,26 +28,29 @@ import { import { EditRolesButton } from "./EditRolesButton"; type UserRoleCellProps = { - canEditUsers: boolean; - allAvailableRoles: SlimRole[] | undefined; - user: User; isLoading: boolean; + canEditUsers: boolean; + allAvailableRoles: readonly SlimRole[] | undefined; + user: Pick; + inheritedRoles?: readonly SlimRole[]; + roles: readonly SlimRole[]; oidcRoleSyncEnabled: boolean; - onUserRolesUpdate: (user: User, newRoleNames: string[]) => void; + onUserRolesUpdate: (userId: string, newRoleNames: string[]) => void; }; export const UserRoleCell: FC = ({ + isLoading, canEditUsers, allAvailableRoles, user, - isLoading, + inheritedRoles, + roles, oidcRoleSyncEnabled, onUserRolesUpdate, }) => { - const theme = useTheme(); - + const theRolesForReal = getMergedRoles(inheritedRoles ?? [], roles); const [mainDisplayRole = fallbackRole, ...extraRoles] = - sortRolesByAccessLevel(user.roles ?? []); + sortRolesByAccessLevel(theRolesForReal ?? []); const hasOwnerRole = mainDisplayRole.name === "owner"; return ( @@ -55,7 +59,7 @@ export const UserRoleCell: FC = ({ {canEditUsers && ( = ({ (role) => role !== fallbackRole.name, ); - onUserRolesUpdate(user, rolesWithoutFallback); + onUserRolesUpdate(user.id, rolesWithoutFallback); }} /> )} - - {mainDisplayRole.display_name} + + {mainDisplayRole.global ? ( + + {mainDisplayRole.display_name}* + + ) : ( + mainDisplayRole.display_name + )} {extraRoles.length > 0 && } @@ -105,7 +106,7 @@ const OverflowRolePill: FC = ({ roles }) => { borderColor: theme.palette.divider, }} > - {`+${roles.length} more`} + +{roles.length} more @@ -148,9 +149,21 @@ const OverflowRolePill: FC = ({ roles }) => { ); }; -const fallbackRole: SlimRole = { +const styles = { + 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>; + +const fallbackRole: MergedSlimRole = { name: "member", display_name: "Member", + global: false, } as const; const roleNamesByAccessLevel: readonly string[] = [ @@ -160,9 +173,9 @@ const roleNamesByAccessLevel: readonly string[] = [ "auditor", ]; -function sortRolesByAccessLevel( - roles: readonly SlimRole[], -): readonly SlimRole[] { +function sortRolesByAccessLevel( + roles: readonly T[], +): readonly T[] { if (roles.length === 0) { return roles; } @@ -182,3 +195,29 @@ function getSelectedRoleNames(roles: readonly SlimRole[]) { return roleNameSet; } + +interface MergedSlimRole extends SlimRole { + global?: boolean; +} + +function getMergedRoles( + globalRoles: readonly SlimRole[], + localRoles: readonly SlimRole[], +) { + const roles = new Map(); + + 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()]; +} diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 8ddc42e630aff..9537d4c2527c9 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -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( diff --git a/site/src/pages/UsersPage/UsersPageView.tsx b/site/src/pages/UsersPage/UsersPageView.tsx index be5f50b6ff9b8..41aa255ea9cd8 100644 --- a/site/src/pages/UsersPage/UsersPageView.tsx +++ b/site/src/pages/UsersPage/UsersPageView.tsx @@ -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; diff --git a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx index 82beeedc654da..c27de3e05588c 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx @@ -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 "../../ManagementSettingsPage/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; diff --git a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx index 03a99bd423bf9..264238da93f4c 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx @@ -31,7 +31,7 @@ import { TableRowSkeleton, } from "components/TableLoader/TableLoader"; import { UserGroupsCell } from "./UserGroupsCell"; -import { UserRoleCell } from "./UserRoleCell"; +import { UserRoleCell } from "../../ManagementSettingsPage/UserTable/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; @@ -157,6 +157,7 @@ export const UsersTableBody: FC = ({ canEditUsers={canEditUsers} allAvailableRoles={roles} user={user} + roles={user.roles} oidcRoleSyncEnabled={oidcRoleSyncEnabled} isLoading={Boolean(isUpdatingUserRoles)} onUserRolesUpdate={onUpdateUserRoles} From a7053bb8b3aaeb493c3b46e9f8ca98a23a688571 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 22 Jul 2024 21:45:10 +0000 Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/chromatic.config.json | 6 ------ .../ManagementSettingsPage/OrganizationMembersPage.tsx | 3 +-- .../pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx | 2 +- .../TemplatePermissionsPage/TemplatePermissionsPage.tsx | 1 - site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx | 2 +- 5 files changed, 3 insertions(+), 11 deletions(-) delete mode 100644 site/chromatic.config.json diff --git a/site/chromatic.config.json b/site/chromatic.config.json deleted file mode 100644 index 22f9facca65b9..0000000000000 --- a/site/chromatic.config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "onlyChanged": true, - "projectId": "Project:624de63c6aacee003aa84340", - "storybookBaseDir": "site", - "zip": true -} diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx index 1206cfd92a754..5167824c90959 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx @@ -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"; @@ -18,6 +17,7 @@ import { removeOrganizationMember, updateOrganizationMemberRoles, } from "api/queries/organizations"; +import { organizationRoles } from "api/queries/roles"; import type { User } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { AvatarData } from "components/AvatarData/AvatarData"; @@ -35,7 +35,6 @@ import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { TableColumnHelpTooltip } from "./UserTable/TableColumnHelpTooltip"; -import { organizationRoles } from "api/queries/roles"; import { UserRoleCell } from "./UserTable/UserRoleCell"; const OrganizationMembersPage: FC = () => { diff --git a/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx b/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx index 206659363877e..6e9fd208715de 100644 --- a/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx +++ b/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx @@ -13,7 +13,7 @@ * 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 { Interpolation, Theme, 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"; diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx index 77086bfc991c4..6af149bd20462 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx @@ -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"; diff --git a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx index 264238da93f4c..fac91ba477340 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx @@ -30,8 +30,8 @@ import { TableLoaderSkeleton, TableRowSkeleton, } from "components/TableLoader/TableLoader"; -import { UserGroupsCell } from "./UserGroupsCell"; import { UserRoleCell } from "../../ManagementSettingsPage/UserTable/UserRoleCell"; +import { UserGroupsCell } from "./UserGroupsCell"; dayjs.extend(relativeTime); From 42344efba15dff4e157fd5d5970ae030fde88e39 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 22 Jul 2024 22:29:38 +0000 Subject: [PATCH 04/11] =?UTF-8?q?=F0=9F=A7=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/api/queries/roles.ts | 2 +- .../OrganizationMembersPage.tsx | 8 ++------ .../UserTable/EditRolesButton.tsx | 2 +- .../UserTable/UserRoleCell.tsx | 19 +++++++++---------- .../UsersPage/UsersTable/UsersTableBody.tsx | 4 ++-- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/site/src/api/queries/roles.ts b/site/src/api/queries/roles.ts index 3fc0486fd61f3..e51805e72c527 100644 --- a/site/src/api/queries/roles.ts +++ b/site/src/api/queries/roles.ts @@ -9,7 +9,7 @@ export const roles = () => { export const organizationRoles = (organizationId: string) => { return { - queryKey: ["organizationRoles"], + queryKey: ["organization", organizationId, "roles"], queryFn: () => API.getOrganizationRoles(organizationId), }; }; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx index 5167824c90959..3cfe08883b75e 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx @@ -106,19 +106,15 @@ const OrganizationMembersPage: FC = () => { /> { + onEditRoles={async (newRoleNames) => { await updateMemberRolesMutation.mutateAsync({ - userId, + userId: member.user_id, roles: newRoleNames, }); displaySuccess("Roles updated successfully."); diff --git a/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx b/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx index 82cc574fb13a1..a3c9286fe8362 100644 --- a/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx +++ b/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx @@ -73,7 +73,7 @@ export interface EditRolesButtonProps { selectedRoleNames: Set; onChange: (roles: SlimRole["name"][]) => void; oidcRoleSync: boolean; - userLoginType: string; + userLoginType?: string; } export const EditRolesButton: FC = ({ diff --git a/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx b/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx index 6e9fd208715de..ddfa3a6d814c7 100644 --- a/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx +++ b/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx @@ -18,7 +18,7 @@ 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, @@ -31,26 +31,26 @@ type UserRoleCellProps = { isLoading: boolean; canEditUsers: boolean; allAvailableRoles: readonly SlimRole[] | undefined; - user: Pick; + userLoginType?: LoginType; inheritedRoles?: readonly SlimRole[]; roles: readonly SlimRole[]; oidcRoleSyncEnabled: boolean; - onUserRolesUpdate: (userId: string, newRoleNames: string[]) => void; + onEditRoles: (newRoleNames: string[]) => void; }; export const UserRoleCell: FC = ({ isLoading, canEditUsers, allAvailableRoles, - user, + userLoginType, inheritedRoles, roles, oidcRoleSyncEnabled, - onUserRolesUpdate, + onEditRoles, }) => { - const theRolesForReal = getMergedRoles(inheritedRoles ?? [], roles); + const mergedRoles = getMergedRoles(inheritedRoles ?? [], roles); const [mainDisplayRole = fallbackRole, ...extraRoles] = - sortRolesByAccessLevel(theRolesForReal ?? []); + sortRolesByAccessLevel(mergedRoles ?? []); const hasOwnerRole = mainDisplayRole.name === "owner"; return ( @@ -61,7 +61,7 @@ export const UserRoleCell: FC = ({ roles={sortRolesByAccessLevel(allAvailableRoles ?? [])} 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 @@ -69,7 +69,7 @@ export const UserRoleCell: FC = ({ (role) => role !== fallbackRole.name, ); - onUserRolesUpdate(user.id, rolesWithoutFallback); + onEditRoles(rolesWithoutFallback); }} /> )} @@ -163,7 +163,6 @@ const styles = { const fallbackRole: MergedSlimRole = { name: "member", display_name: "Member", - global: false, } as const; const roleNamesByAccessLevel: readonly string[] = [ diff --git a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx index fac91ba477340..fdcb88b447dbf 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx @@ -156,11 +156,11 @@ export const UsersTableBody: FC = ({ onUpdateUserRoles(user.id, roles)} /> From 840e073e7ff4d1ccf09fff95001ac511cf08bdd3 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 23 Jul 2024 21:13:57 +0000 Subject: [PATCH 05/11] fix error and loading states --- .../OrganizationMembersPage.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx index 3cfe08883b75e..05bfe80987b4a 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx @@ -110,14 +110,20 @@ const OrganizationMembersPage: FC = () => { roles={member.roles} allAvailableRoles={organizationRolesQuery.data} oidcRoleSyncEnabled={false} - isLoading={organizationRolesQuery.isLoading} + isLoading={updateMemberRolesMutation.isLoading} canEditUsers onEditRoles={async (newRoleNames) => { - await updateMemberRolesMutation.mutateAsync({ - userId: member.user_id, - roles: newRoleNames, - }); - displaySuccess("Roles updated successfully."); + try { + await updateMemberRolesMutation.mutateAsync({ + userId: member.user_id, + roles: newRoleNames, + }); + displaySuccess("Roles updated successfully."); + } catch (e) { + displayError( + getErrorMessage(e, "Failed to update roles."), + ); + } }} /> From 46a468fc784870b5b848425890b7aa56870447bf Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 24 Jul 2024 01:15:06 +0000 Subject: [PATCH 06/11] tests --- .../OrganizationMembersPage.test.tsx | 123 ++++++++++++++++++ .../OrganizationMembersPage.tsx | 18 ++- site/src/pages/UsersPage/UsersPage.test.tsx | 10 +- site/src/pages/UsersPage/UsersPage.tsx | 2 +- site/src/testHelpers/entities.ts | 99 +++++++++++--- site/src/testHelpers/handlers.ts | 20 ++- site/src/testHelpers/renderHelpers.tsx | 38 ++++++ 7 files changed, 281 insertions(+), 29 deletions(-) create mode 100644 site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx new file mode 100644 index 0000000000000..4ccc435b2dc25 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx @@ -0,0 +1,123 @@ +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, + MockUser2, + MockOrganizationAuditorRole, +} from "testHelpers/entities"; +import { + renderWithTemplateSettingsLayout, + waitForLoaderToBeRemoved, +} from "testHelpers/renderHelpers"; +import { server } from "testHelpers/server"; +import OrganizationMembersPage from "./OrganizationMembersPage"; + +jest.spyOn(console, "error").mockImplementation(() => {}); + +const renderPage = async () => { + renderWithTemplateSettingsLayout(, { + 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 () => { + server.use( + http.delete( + `/api/v2/organizations/:organizationId/members/${MockUser2.id}`, + async () => { + return new HttpResponse(null, { status: 204 }); + }, + ), + ); + + 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."); + }); + }); + }); +}); diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx index 05bfe80987b4a..e3ce5e7b20e7f 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx @@ -136,10 +136,20 @@ const OrganizationMembersPage: FC = () => { { - 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 diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx index edbc0118b09f2..f9d620ce509f2 100644 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -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(); @@ -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", () => { diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 9537d4c2527c9..bdc6b31bc5d6f 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -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(); diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 043cc405df7d3..78fe9d5e4c066 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -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,18 +276,54 @@ export const MockTemplateAdminRole: TypesGen.Role = { organization_id: "", }; +export const MockAuditorRole: TypesGen.Role = { + name: "auditor", + display_name: "Auditor", + site_permissions: [], + organization_permissions: [], + user_permissions: [], + organization_id: "", +}; + export const MockMemberRole: TypesGen.SlimRole = { name: "member", display_name: "Member", }; -export const MockAuditorRole: TypesGen.Role = { - name: "auditor", - display_name: "Auditor", +export const MockOrganizationAdminRole: TypesGen.Role = { + name: "organization-admin", + display_name: "Organization Admin", site_permissions: [], organization_permissions: [], user_permissions: [], - organization_id: "", + 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 @@ -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", @@ -2146,7 +2209,7 @@ export const MockEntitlementsWithUserLimit: TypesGen.Entitlements = { }), }; -export const MockExperiments: TypesGen.Experiment[] = []; +export const MockExperiments: TypesGen.Experiment[] = ["multi-organization"]; export const MockAuditLog: TypesGen.AuditLog = { id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 9f74737ce5e4c..1de174cdfd8f8 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -41,10 +41,14 @@ export const handlers = [ }), // organizations + http.get("/api/v2/organizations", () => { + console.log(" fuck and piss ================================== "); + 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 +60,20 @@ 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, + ]); + }), // templates http.get("/api/v2/templates/:templateId", () => { diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx index 3697e6136075a..50e1afe16baff 100644 --- a/site/src/testHelpers/renderHelpers.tsx +++ b/site/src/testHelpers/renderHelpers.tsx @@ -17,6 +17,7 @@ import { DashboardLayout } from "modules/dashboard/DashboardLayout"; import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout"; import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"; import { MockUser } from "./entities"; +import { ManagementSettingsLayout } from "pages/ManagementSettingsPage/ManagementSettingsLayout"; export function createTestQueryClient() { // Helps create one query client for each test case, to make sure that tests @@ -186,6 +187,43 @@ export function renderWithWorkspaceSettingsLayout( }; } +export function renderWithManagementSettingsLayout( + element: JSX.Element, + { + path = "/", + route = "/", + extraRoutes = [], + nonAuthenticatedRoutes = [], + }: RenderWithAuthOptions = {}, +) { + const routes: RouteObject[] = [ + { + element: , + children: [ + { + element: , + children: [ + { + element: , + children: [{ element, path }, ...extraRoutes], + }, + ], + }, + ], + }, + ...nonAuthenticatedRoutes, + ]; + + const renderResult = renderWithRouter( + createMemoryRouter(routes, { initialEntries: [route] }), + ); + + return { + user: MockUser, + ...renderResult, + }; +} + export const waitForLoaderToBeRemoved = async (): Promise => { return waitFor( () => { From 2144da8248e65a5817f6b17670c6ed43e7ce0030 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 24 Jul 2024 01:16:57 +0000 Subject: [PATCH 07/11] no --- site/src/testHelpers/handlers.ts | 1 - site/src/testHelpers/renderHelpers.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 1de174cdfd8f8..bf580f67a3bad 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -42,7 +42,6 @@ export const handlers = [ // organizations http.get("/api/v2/organizations", () => { - console.log(" fuck and piss ================================== "); return HttpResponse.json([M.MockDefaultOrganization, M.MockOrganization2]); }), http.get("/api/v2/organizations/:organizationId", () => { diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx index 50e1afe16baff..6abb5e93cff62 100644 --- a/site/src/testHelpers/renderHelpers.tsx +++ b/site/src/testHelpers/renderHelpers.tsx @@ -14,10 +14,10 @@ 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"; -import { ManagementSettingsLayout } from "pages/ManagementSettingsPage/ManagementSettingsLayout"; export function createTestQueryClient() { // Helps create one query client for each test case, to make sure that tests From 86973391cc123c4295d9401020aac38b42dfe2a9 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 24 Jul 2024 01:25:15 +0000 Subject: [PATCH 08/11] :) --- .../OrganizationMembersPage.test.tsx | 8 ++++++++ site/src/testHelpers/entities.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx index 4ccc435b2dc25..b9c8564cb8c79 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx @@ -59,6 +59,14 @@ const updateUserRole = async (role: SlimRole) => { }; }; +beforeAll(() => { + server.use( + http.get("/api/v2/experiments", () => { + return HttpResponse.json(["multi-organization"]); + }), + ); +}); + describe("OrganizationMembersPage", () => { describe("remove member", () => { describe("when it is success", () => { diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 78fe9d5e4c066..453f1455615ec 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2209,7 +2209,7 @@ export const MockEntitlementsWithUserLimit: TypesGen.Entitlements = { }), }; -export const MockExperiments: TypesGen.Experiment[] = ["multi-organization"]; +export const MockExperiments: TypesGen.Experiment[] = []; export const MockAuditLog: TypesGen.AuditLog = { id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", From 7a40e09543e41378d61b3dbfee67f1eaebb46f2e Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 24 Jul 2024 16:58:26 +0000 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OrganizationMembersPage.test.tsx | 25 +++------ .../UserTable/UserRoleCell.tsx | 53 +++++++++++++------ site/src/testHelpers/handlers.ts | 6 +++ 3 files changed, 52 insertions(+), 32 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx index b9c8564cb8c79..b9f1b76e8226b 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx @@ -16,6 +16,14 @@ 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(, { route: `/organizations/my-organization/members`, @@ -59,27 +67,10 @@ const updateUserRole = async (role: SlimRole) => { }; }; -beforeAll(() => { - server.use( - http.get("/api/v2/experiments", () => { - return HttpResponse.json(["multi-organization"]); - }), - ); -}); - describe("OrganizationMembersPage", () => { describe("remove member", () => { describe("when it is success", () => { it("shows a success message", async () => { - server.use( - http.delete( - `/api/v2/organizations/:organizationId/members/${MockUser2.id}`, - async () => { - return new HttpResponse(null, { status: 204 }); - }, - ), - ); - await renderPage(); await removeMember(); await screen.findByText("Member removed."); diff --git a/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx b/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx index ddfa3a6d814c7..8c6e1010579d3 100644 --- a/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx +++ b/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx @@ -48,10 +48,14 @@ export const UserRoleCell: FC = ({ oidcRoleSyncEnabled, onEditRoles, }) => { - const mergedRoles = getMergedRoles(inheritedRoles ?? [], roles); + const mergedRoles = getTieredRoles(inheritedRoles ?? [], roles); const [mainDisplayRole = fallbackRole, ...extraRoles] = sortRolesByAccessLevel(mergedRoles ?? []); - const hasOwnerRole = mainDisplayRole.name === "owner"; + const hasOwnerRole = + mainDisplayRole.name === "owner" || + mainDisplayRole.name === "organization-admin"; + + const displayName = mainDisplayRole.display_name || mainDisplayRole.name; return ( @@ -74,13 +78,21 @@ export const UserRoleCell: FC = ({ /> )} - + {mainDisplayRole.global ? ( - {mainDisplayRole.display_name}* + {displayName}* ) : ( - mainDisplayRole.display_name + displayName )} @@ -91,7 +103,7 @@ export const UserRoleCell: FC = ({ }; type OverflowRolePillProps = { - roles: readonly SlimRole[]; + roles: readonly TieredSlimRole[]; }; const OverflowRolePill: FC = ({ roles }) => { @@ -136,12 +148,15 @@ const OverflowRolePill: FC = ({ roles }) => { {roles.map((role) => ( - {role.display_name || role.name} + {role.global ? ( + + {role.display_name || role.name}* + + ) : ( + role.display_name || role.name + )} ))} @@ -150,6 +165,10 @@ const OverflowRolePill: FC = ({ roles }) => { }; const styles = { + globalRoleBadge: (theme) => ({ + backgroundColor: theme.roles.success.background, + borderColor: theme.roles.success.outline, + }), ownerRoleBadge: (theme) => ({ backgroundColor: theme.roles.info.background, borderColor: theme.roles.info.outline, @@ -160,16 +179,20 @@ const styles = { }), } satisfies Record>; -const fallbackRole: MergedSlimRole = { +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( @@ -195,15 +218,15 @@ function getSelectedRoleNames(roles: readonly SlimRole[]) { return roleNameSet; } -interface MergedSlimRole extends SlimRole { +interface TieredSlimRole extends SlimRole { global?: boolean; } -function getMergedRoles( +function getTieredRoles( globalRoles: readonly SlimRole[], localRoles: readonly SlimRole[], ) { - const roles = new Map(); + const roles = new Map(); for (const role of globalRoles) { roles.set(role.name, { diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index bf580f67a3bad..4624a58f0ce4e 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -73,6 +73,12 @@ export const handlers = [ M.MockOrganizationMember2, ]); }), + http.delete( + `/api/v2/organizations/:organizationId/members/:userId`, + async () => { + return new HttpResponse(null, { status: 204 }); + }, + ), // templates http.get("/api/v2/templates/:templateId", () => { From a3e7a8a50d149d0382d576d9237e0d30716729d7 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 24 Jul 2024 17:09:01 +0000 Subject: [PATCH 10/11] actually, use `active` --- .../pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx b/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx index 8c6e1010579d3..9b774d20a3f2e 100644 --- a/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx +++ b/site/src/pages/ManagementSettingsPage/UserTable/UserRoleCell.tsx @@ -166,8 +166,8 @@ const OverflowRolePill: FC = ({ roles }) => { const styles = { globalRoleBadge: (theme) => ({ - backgroundColor: theme.roles.success.background, - borderColor: theme.roles.success.outline, + backgroundColor: theme.roles.active.background, + borderColor: theme.roles.active.outline, }), ownerRoleBadge: (theme) => ({ backgroundColor: theme.roles.info.background, From 830979beb1bc78812f753e16f8f6f5d1c0359d6f Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 24 Jul 2024 17:13:11 +0000 Subject: [PATCH 11/11] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ManagementSettingsPage/OrganizationMembersPage.test.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx index b9f1b76e8226b..3aa7f4a606e29 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx @@ -2,11 +2,7 @@ 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, - MockUser2, - MockOrganizationAuditorRole, -} from "testHelpers/entities"; +import { MockUser, MockOrganizationAuditorRole } from "testHelpers/entities"; import { renderWithTemplateSettingsLayout, waitForLoaderToBeRemoved, 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