Skip to content

feat: Add update user roles action #1361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
May 10, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add missing tests
  • Loading branch information
BrunoQuaresma committed May 10, 2022
commit 9c0587ecf3f3721701ade54d7ba93c52d95b1846
24 changes: 24 additions & 0 deletions site/src/components/RoleSelect/RoleSelect.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentMeta, Story } from "@storybook/react"
import React from "react"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BrunoQuaresma Just looking at merged PRs to learn - no action necessary here. Curious if there's a reason we're importing React given we're on version 17 instead of silencing the 'React must be in scope' warnings.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I think it is automatically added by an eslint role, but I'm not 100% sure.

import { MockAdminRole, MockMemberRole, MockSiteRoles } from "../../testHelpers"
import { RoleSelect, RoleSelectProps } from "./RoleSelect"

export default {
title: "components/RoleSelect",
component: RoleSelect,
} as ComponentMeta<typeof RoleSelect>

const Template: Story<RoleSelectProps> = (args) => <RoleSelect {...args} />

export const Close = Template.bind({})
Close.args = {
roles: MockSiteRoles,
selectedRoles: [MockAdminRole, MockMemberRole],
}

export const Open = Template.bind({})
Open.args = {
open: true,
roles: MockSiteRoles,
selectedRoles: [MockAdminRole, MockMemberRole],
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { makeStyles, Theme } from "@material-ui/core/styles"
import React from "react"
import { Role } from "../../api/typesGenerated"

export const Language = {
label: "Roles",
}
export interface RoleSelectProps {
roles: Role[]
selectedRoles: Role[]
Expand All @@ -21,6 +24,7 @@ export const RoleSelect: React.FC<RoleSelectProps> = ({ roles, selectedRoles, lo

return (
<Select
aria-label={Language.label}
open={open}
multiple
value={value}
Expand Down
67 changes: 0 additions & 67 deletions site/src/components/UsersTable/RoleSelect.stories.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion site/src/components/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import React from "react"
import { UserResponse } from "../../api/types"
import * as TypesGen from "../../api/typesGenerated"
import { EmptyState } from "../EmptyState/EmptyState"
import { RoleSelect } from "../RoleSelect/RoleSelect"
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
import { TableRowMenu } from "../TableRowMenu/TableRowMenu"
import { TableTitle } from "../TableTitle/TableTitle"
import { UserCell } from "../UserCell/UserCell"
import { RoleSelect } from "./RoleSelect"

export const Language = {
pageTitle: "Users",
Expand Down
60 changes: 59 additions & 1 deletion site/src/pages/UsersPage/UsersPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { fireEvent, screen, waitFor, within } from "@testing-library/react"
import React from "react"
import * as API from "../../api"
import { Role } from "../../api/typesGenerated"
import { GlobalSnackbar } from "../../components/GlobalSnackbar/GlobalSnackbar"
import { Language as ResetPasswordDialogLanguage } from "../../components/ResetPasswordDialog/ResetPasswordDialog"
import { Language as RoleSelectLanguage } from "../../components/RoleSelect/RoleSelect"
import { Language as UsersTableLanguage } from "../../components/UsersTable/UsersTable"
import { MockUser, MockUser2, render } from "../../testHelpers"
import { MockAuditorRole, MockUser, MockUser2, render } from "../../testHelpers"
import { Language as usersXServiceLanguage } from "../../xServices/users/usersXService"
import { Language as UsersPageLanguage, UsersPage } from "./UsersPage"

Expand Down Expand Up @@ -62,6 +64,34 @@ const resetUserPassword = async (setupActionSpies: () => void) => {
fireEvent.click(confirmButton)
}

const updateUserRole = async (setupActionSpies: () => void, role: Role) => {
// Get the first user in the table
const users = await screen.findAllByText(/.*@coder.com/)
const firstUserRow = users[0].closest("tr")
if (!firstUserRow) {
throw new Error("Error on get the first user row")
}

// Click on the "roles" menu to display the role options
const rolesLabel = within(firstUserRow).getByLabelText(RoleSelectLanguage.label)
const rolesMenuTrigger = within(rolesLabel).getByRole("button")
// For v4, the Select was changed to open on mouseDown instead of click
// https://github.com/mui-org/material-ui/pull/17978
fireEvent.mouseDown(rolesMenuTrigger)

// Setup spies to check the actions after
setupActionSpies()

// Click on the role option
const listBox = screen.getByRole("listbox")
const auditorOption = within(listBox).getByRole("option", { name: role.display_name })
fireEvent.click(auditorOption)

return {
rolesMenuTrigger,
}
}

describe("Users Page", () => {
it("shows users", async () => {
render(<UsersPage />)
Expand Down Expand Up @@ -164,4 +194,32 @@ describe("Users Page", () => {
})
})
})

describe("Update user role", () => {
describe("when it is success", () => {
it("updates the roles", async () => {
render(
<>
<UsersPage />
<GlobalSnackbar />
</>,
)

const { rolesMenuTrigger } = await updateUserRole(() => {
jest.spyOn(API, "updateUserRoles").mockResolvedValueOnce({
...MockUser,
roles: [...MockUser.roles, MockAuditorRole],
})
}, MockAuditorRole)

// Check if the select text was updated with the Auditor role
await waitFor(() => expect(rolesMenuTrigger).toHaveTextContent("Admin, Member, Auditor"))

// Check if the API was called correctly
const currentRoles = MockUser.roles.map((r) => r.name)
expect(API.updateUserRoles).toBeCalledTimes(1)
expect(API.updateUserRoles).toBeCalledWith([...currentRoles, MockAuditorRole.name], MockUser.id)
})
})
})
})
11 changes: 1 addition & 10 deletions site/src/pages/UsersPage/UsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,14 @@ export const Language = {

const useRoles = () => {
const xServices = useContext(XServiceContext)
const [authState] = useActor(xServices.authXService)
const [rolesState, rolesSend] = useActor(xServices.siteRolesXService)
const { roles } = rolesState.context
const { me } = authState.context

useEffect(() => {
if (!me) {
throw new Error("User is not logged in")
}

const organizationId = me.organization_ids[0]

rolesSend({
type: "GET_ROLES",
organizationId,
})
}, [me, rolesSend])
}, [rolesSend])

return roles
}
Expand Down
23 changes: 20 additions & 3 deletions site/src/testHelpers/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
WorkspaceAutostartRequest,
WorkspaceResource,
} from "../api/types"
import { AuthMethods } from "../api/typesGenerated"
import { AuthMethods, Role } from "../api/typesGenerated"

export const MockSessionToken = { session_token: "my-session-token" }

Expand All @@ -21,14 +21,31 @@ export const MockBuildInfo: BuildInfoResponse = {
version: "v99.999.9999+c9cdf14",
}

export const MockAdminRole: Role = {
name: "admin",
display_name: "Admin",
}

export const MockMemberRole: Role = {
name: "member",
display_name: "Member",
}

export const MockAuditorRole: Role = {
name: "auditor",
display_name: "Auditor",
}

export const MockSiteRoles = [MockAdminRole, MockAuditorRole, MockMemberRole]

export const MockUser: UserResponse = {
id: "test-user",
username: "TestUser",
email: "test@coder.com",
created_at: "",
status: "active",
organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"],
roles: [],
roles: [MockAdminRole, MockMemberRole],
}

export const MockUser2: UserResponse = {
Expand All @@ -38,7 +55,7 @@ export const MockUser2: UserResponse = {
created_at: "",
status: "active",
organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"],
roles: [],
roles: [MockMemberRole],
}

export const MockOrganization: Organization = {
Expand Down
3 changes: 3 additions & 0 deletions site/src/testHelpers/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export const handlers = [
rest.get("/api/v2/users/authmethods", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockAuthMethods))
}),
rest.get("/api/v2/users/roles", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockSiteRoles))
}),

// workspaces
rest.get("/api/v2/organizations/:organizationId/workspaces/:userName/:workspaceName", (req, res, ctx) => {
Expand Down
1 change: 0 additions & 1 deletion site/src/xServices/roles/siteRolesXService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ type SiteRolesContext = {

type SiteRolesEvent = {
type: "GET_ROLES"
organizationId: string
}

export const siteRolesMachine = createMachine(
Expand Down