From 7a322f41f6304e10ff53e7ae1db6b21466b20a76 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Tue, 14 Jun 2022 20:38:30 +0000 Subject: [PATCH 1/5] add ability to activate users resolves #2254 --- site/src/components/UsersTable/UsersTable.tsx | 12 ++-- site/src/pages/UsersPage/UsersPage.tsx | 29 ++++++++- site/src/pages/UsersPage/UsersPageView.tsx | 3 + site/src/xServices/users/usersXService.ts | 63 ++++++++++++++++++- 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/site/src/components/UsersTable/UsersTable.tsx b/site/src/components/UsersTable/UsersTable.tsx index bd746c65c0265..14ab43418d26a 100644 --- a/site/src/components/UsersTable/UsersTable.tsx +++ b/site/src/components/UsersTable/UsersTable.tsx @@ -33,6 +33,7 @@ export interface UsersTableProps { canEditUsers?: boolean isLoading?: boolean onSuspendUser: (user: TypesGen.User) => void + onActivateUser: (user: TypesGen.User) => void onResetUserPassword: (user: TypesGen.User) => void onUpdateUserRoles: (user: TypesGen.User, roles: TypesGen.Role["name"][]) => void } @@ -41,6 +42,7 @@ export const UsersTable: FC = ({ users, roles, onSuspendUser, + onActivateUser, onResetUserPassword, onUpdateUserRoles, isUpdatingUserRoles, @@ -115,12 +117,10 @@ export const UsersTable: FC = ({ }, ] : [ - // TODO: Uncomment this and add activate user functionality. - // { - // label: Language.activateMenuItem, - // // eslint-disable-next-line @typescript-eslint/no-empty-function - // onClick: function () {}, - // }, + { + label: Language.activateMenuItem, + onClick: onActivateUser, + }, ] ).concat({ label: Language.resetPasswordMenuItem, diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 0aa095dc46e98..67ae7aa832b80 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -13,15 +13,20 @@ export const Language = { suspendDialogTitle: "Suspend user", suspendDialogAction: "Suspend", suspendDialogMessagePrefix: "Do you want to suspend the user", + activateDialogTitle: "Activate user", + activateDialogAction: "Activate", + activateDialogMessagePrefix: "Do you want to active the user", } export const UsersPage: React.FC = () => { const xServices = useContext(XServiceContext) const [usersState, usersSend] = useActor(xServices.usersXService) const [rolesState, rolesSend] = useActor(xServices.siteRolesXService) - const { users, getUsersError, userIdToSuspend, userIdToResetPassword, newUserPassword } = usersState.context + const { users, getUsersError, userIdToSuspend, userIdToActivate, userIdToResetPassword, newUserPassword } = + usersState.context const navigate = useNavigate() const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend) + const userToBeActivated = users?.find((u) => u.id === userIdToActivate) const userToResetPassword = users?.find((u) => u.id === userIdToResetPassword) const permissions = useSelector(xServices.authXService, selectPermissions) const canEditUsers = permissions && permissions.updateUsers @@ -62,6 +67,9 @@ export const UsersPage: React.FC = () => { onSuspendUser={(user) => { usersSend({ type: "SUSPEND_USER", userId: user.id }) }} + onActivateUser={(user) => { + usersSend({ type: "ACTIVATE_USER", userId: user.id }) + }} onResetUserPassword={(user) => { usersSend({ type: "RESET_USER_PASSWORD", userId: user.id }) }} @@ -99,6 +107,25 @@ export const UsersPage: React.FC = () => { } /> + { + usersSend("CONFIRM_USER_ACTIVATION") + }} + onClose={() => { + usersSend("CANCEL_USER_ACTIVATION") + }} + description={ + <> + {Language.activateDialogMessagePrefix} {userToBeActivated?.username}? + + } + /> + void onSuspendUser: (user: TypesGen.User) => void + onActivateUser: (user: TypesGen.User) => void onResetUserPassword: (user: TypesGen.User) => void onUpdateUserRoles: (user: TypesGen.User, roles: TypesGen.Role["name"][]) => void } @@ -31,6 +32,7 @@ export const UsersPageView: FC = ({ roles, openUserCreationDialog, onSuspendUser, + onActivateUser, onResetUserPassword, onUpdateUserRoles, error, @@ -60,6 +62,7 @@ export const UsersPageView: FC = ({ users={users} roles={roles} onSuspendUser={onSuspendUser} + onActivateUser={onActivateUser} onResetUserPassword={onResetUserPassword} onUpdateUserRoles={onUpdateUserRoles} isUpdatingUserRoles={isUpdatingUserRoles} diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 91317d584a488..8cf83559488c0 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -16,7 +16,9 @@ export const Language = { createUserSuccess: "Successfully created user.", createUserError: "Error on creating the user.", suspendUserSuccess: "Successfully suspended the user.", - suspendUserError: "Error on suspending the user.", + suspendUserError: "Error suspending user.", + activateUserSuccess: "Successfully activated the user", + activateUserError: "Error activating user", resetUserPasswordSuccess: "Successfully updated the user password.", resetUserPasswordError: "Error on resetting the user password.", updateUserRolesSuccess: "Successfully updated the user roles.", @@ -32,6 +34,9 @@ export interface UsersContext { // Suspend user userIdToSuspend?: TypesGen.User["id"] suspendUserError?: Error | unknown + // Activate user + userIdToActivate?: TypesGen.User["id"] + activateUserError?: Error | unknown // Reset user password userIdToResetPassword?: TypesGen.User["id"] resetUserPasswordError?: Error | unknown @@ -49,6 +54,10 @@ export type UsersEvent = | { type: "SUSPEND_USER"; userId: TypesGen.User["id"] } | { type: "CONFIRM_USER_SUSPENSION" } | { type: "CANCEL_USER_SUSPENSION" } + // Activate events + | { type: "ACTIVATE_USER"; userId: TypesGen.User["id"] } + | { type: "CONFIRM_USER_ACTIVATION" } + | { type: "CANCEL_USER_ACTIVATION" } // Reset password events | { type: "RESET_USER_PASSWORD"; userId: TypesGen.User["id"] } | { type: "CONFIRM_USER_PASSWORD_RESET" } @@ -72,6 +81,9 @@ export const usersMachine = createMachine( suspendUser: { data: TypesGen.User } + activateUser: { + data: TypesGen.User + } updateUserPassword: { data: undefined } @@ -92,6 +104,10 @@ export const usersMachine = createMachine( target: "confirmUserSuspension", actions: ["assignUserIdToSuspend"], }, + ACTIVATE_USER: { + target: "confirmUserActivation", + actions: ["assignUserIdToActivate"], + }, RESET_USER_PASSWORD: { target: "confirmUserPasswordReset", actions: ["assignUserIdToResetPassword", "generateRandomPassword"], @@ -150,6 +166,12 @@ export const usersMachine = createMachine( CANCEL_USER_SUSPENSION: "idle", }, }, + confirmUserActivation: { + on: { + CONFIRM_USER_ACTIVATION: "activatingUser", + CANCEL_USER_ACTIVATION: "idle", + }, + }, suspendingUser: { entry: "clearSuspendUserError", invoke: { @@ -166,6 +188,22 @@ export const usersMachine = createMachine( }, }, }, + activatingUser: { + entry: "clearActivateUserError", + invoke: { + src: "activateUser", + id: "activateUser", + onDone: { + // Update users list + target: "gettingUsers", + actions: ["displayActivateSuccess"], + }, + onError: { + target: "idle", + actions: ["assignActivateUserError", "displayActivatedErrorMessage"], + }, + }, + }, confirmUserPasswordReset: { on: { CONFIRM_USER_PASSWORD_RESET: "resettingUserPassword", @@ -223,6 +261,13 @@ export const usersMachine = createMachine( return API.suspendUser(context.userIdToSuspend) }, + activateUser: (context) => { + if (!context.userIdToActivate) { + throw new Error("userIdToActivate is undefined") + } + + return API.activateUser(context.userIdToActivate) + }, resetUserPassword: (context) => { if (!context.userIdToResetPassword) { throw new Error("userIdToResetPassword is undefined") @@ -258,6 +303,9 @@ export const usersMachine = createMachine( assignUserIdToSuspend: assign({ userIdToSuspend: (_, event) => event.userId, }), + assignUserIdToActivate: assign({ + userIdToActivate: (_, event) => event.userId, + }), assignUserIdToResetPassword: assign({ userIdToResetPassword: (_, event) => event.userId, }), @@ -278,6 +326,9 @@ export const usersMachine = createMachine( assignSuspendUserError: assign({ suspendUserError: (_, event) => event.data, }), + assignActivateUserError: assign({ + activateUserError: (_, event) => event.data, + }), assignResetUserPasswordError: assign({ resetUserPasswordError: (_, event) => event.data, }), @@ -292,6 +343,9 @@ export const usersMachine = createMachine( clearSuspendUserError: assign({ suspendUserError: (_) => undefined, }), + clearActivateUserError: assign({ + activateUserError: (_) => undefined, + }), clearResetUserPasswordError: assign({ resetUserPasswordError: (_) => undefined, }), @@ -308,6 +362,13 @@ export const usersMachine = createMachine( const message = getErrorMessage(context.suspendUserError, Language.suspendUserError) displayError(message) }, + displayActivateSuccess: () => { + displaySuccess(Language.activateUserSuccess) + }, + displayActivatedErrorMessage: (context) => { + const message = getErrorMessage(context.activateUserError, Language.activateUserError) + displayError(message) + }, displayResetPasswordSuccess: () => { displaySuccess(Language.resetUserPasswordSuccess) }, From 88643c67e0373a1a426206d03b78ac0f2a7f5c4b Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 15 Jun 2022 15:21:55 +0000 Subject: [PATCH 2/5] added test --- site/src/pages/UsersPage/UsersPage.test.tsx | 81 ++++++++++++++++++++- site/src/testHelpers/entities.ts | 10 +++ site/src/testHelpers/handlers.ts | 2 +- 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx index 85ca87b7aced2..3415c8430468f 100644 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -6,7 +6,7 @@ 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 { MockAuditorRole, MockUser, MockUser2, render } from "../../testHelpers/renderHelpers" +import { MockAuditorRole, MockUser, MockUser2, render, SuspendedMockUser } from "../../testHelpers/renderHelpers" import { server } from "../../testHelpers/server" import { permissionsToCheck } from "../../xServices/auth/authXService" import { Language as usersXServiceLanguage } from "../../xServices/users/usersXService" @@ -40,6 +40,35 @@ const suspendUser = async (setupActionSpies: () => void) => { fireEvent.click(confirmButton) } +const activateUser = async (setupActionSpies: () => void) => { + // Get the first user in the table + const users = await screen.findAllByText(/.*@coder.com/) + const firstUserRow = users[2].closest("tr") + if (!firstUserRow) { + throw new Error("Error on get the first user row") + } + + // Click on the "more" button to display the "Activate" option + const moreButton = within(firstUserRow).getByLabelText("more") + fireEvent.click(moreButton) + const menu = screen.getByRole("menu") + const activateButton = within(menu).getByText(UsersTableLanguage.activateMenuItem) + fireEvent.click(activateButton) + + // Check if the confirm message is displayed + const confirmDialog = screen.getByRole("dialog") + expect(confirmDialog).toHaveTextContent( + `${UsersPageLanguage.activateDialogMessagePrefix} ${SuspendedMockUser.username}?`, + ) + + // Setup spies to check the actions after + setupActionSpies() + + // Click on the "Confirm" button + const confirmButton = within(confirmDialog).getByText(UsersPageLanguage.activateDialogAction) + fireEvent.click(confirmButton) +} + const resetUserPassword = async (setupActionSpies: () => void) => { // Get the first user in the table const users = await screen.findAllByText(/.*@coder.com/) @@ -99,7 +128,7 @@ describe("Users Page", () => { it("shows users", async () => { render() const users = await screen.findAllByText(/.*@coder.com/) - expect(users.length).toEqual(2) + expect(users.length).toEqual(3) }) it("shows 'Create user' button to an authorized user", () => { @@ -178,6 +207,54 @@ describe("Users Page", () => { }) }) + describe("activate user", () => { + describe("when user is successfully activated", () => { + it("shows a success message and refreshes the page", async () => { + render( + <> + + + , + ) + + await activateUser(() => { + jest.spyOn(API, "activateUser").mockResolvedValueOnce(SuspendedMockUser) + jest + .spyOn(API, "getUsers") + .mockImplementationOnce(() => Promise.resolve([MockUser, MockUser2, SuspendedMockUser])) + }) + + // Check if the success message is displayed + await screen.findByText(usersXServiceLanguage.activateUserSuccess) + + // Check if the API was called correctly + expect(API.activateUser).toBeCalledTimes(1) + expect(API.activateUser).toBeCalledWith(SuspendedMockUser.id) + }) + }) + describe("when activation fails", () => { + it("shows an error message", async () => { + render( + <> + + + , + ) + + await activateUser(() => { + jest.spyOn(API, "activateUser").mockRejectedValueOnce({}) + }) + + // Check if the error message is displayed + await screen.findByText(usersXServiceLanguage.activateUserError) + + // Check if the API was called correctly + expect(API.activateUser).toBeCalledTimes(1) + expect(API.activateUser).toBeCalledWith(SuspendedMockUser.id) + }) + }) + }) + describe("reset user password", () => { describe("when it is success", () => { it("shows a success message", async () => { diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index b37e038529dbc..871369b7b3fde 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -51,6 +51,16 @@ export const MockUser2: TypesGen.User = { roles: [], } +export const SuspendedMockUser: TypesGen.User = { + id: "suspended-mock-user", + username: "SuspendedMockUser", + email: "iamsuspendedsad!@coder.com", + created_at: "", + status: "suspended", + organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"], + roles: [], +} + export const MockOrganization: TypesGen.Organization = { id: "test-org", name: "Test Organization", diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index bfb8e9be0f4ff..f9f130b86b261 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -37,7 +37,7 @@ export const handlers = [ // users rest.get("/api/v2/users", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json([M.MockUser, M.MockUser2])) + return res(ctx.status(200), ctx.json([M.MockUser, M.MockUser2, M.SuspendedMockUser])) }), rest.post("/api/v2/users", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockUser)) From 9a232a8fb8c4928877f62bd33f75053d9a8d0251 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 15 Jun 2022 17:11:37 +0000 Subject: [PATCH 3/5] PR feedback --- site/src/pages/UsersPage/UsersPage.tsx | 3 ++- site/src/xServices/users/usersXService.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 67ae7aa832b80..e379d03bbaa4d 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -15,7 +15,7 @@ export const Language = { suspendDialogMessagePrefix: "Do you want to suspend the user", activateDialogTitle: "Activate user", activateDialogAction: "Activate", - activateDialogMessagePrefix: "Do you want to active the user", + activateDialogMessagePrefix: "Do you want to activate the user", } export const UsersPage: React.FC = () => { @@ -108,6 +108,7 @@ export const UsersPage: React.FC = () => { /> Date: Wed, 15 Jun 2022 18:09:54 +0000 Subject: [PATCH 4/5] guarding against null validation_contains field --- .../ParameterInput/ParameterInput.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/site/src/components/ParameterInput/ParameterInput.tsx b/site/src/components/ParameterInput/ParameterInput.tsx index a12d3c07f012d..7d00bd9d8f16d 100644 --- a/site/src/components/ParameterInput/ParameterInput.tsx +++ b/site/src/components/ParameterInput/ParameterInput.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ import FormControlLabel from "@material-ui/core/FormControlLabel" import Radio from "@material-ui/core/Radio" import RadioGroup from "@material-ui/core/RadioGroup" @@ -29,7 +30,7 @@ export const ParameterInput: FC = ({ disabled, onChange, sc } const ParameterField: React.FC = ({ disabled, onChange, schema }) => { - if (schema.validation_contains.length > 0) { + if (schema.validation_contains && schema.validation_contains.length > 0) { return ( = ({ disabled, onChange, sch onChange(event.target.value) }} > - {schema.validation_contains.map((item) => ( - } - label={item} - /> - ))} + {schema.validation_contains && + schema.validation_contains.map((item) => ( + } + label={item} + /> + ))} ) } From f8dcb1b2ec3c8ba0e186b226e350b9fdca6cccd6 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 15 Jun 2022 18:43:24 +0000 Subject: [PATCH 5/5] fixing type for ParameterSchema resolves #2161 --- codersdk/parameters.go | 2 +- site/src/api/typesGenerated.ts | 2 +- .../ParameterInput/ParameterInput.tsx | 20 +++++++++---------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/codersdk/parameters.go b/codersdk/parameters.go index 073efafb63fbb..57df03d56077a 100644 --- a/codersdk/parameters.go +++ b/codersdk/parameters.go @@ -72,7 +72,7 @@ type ParameterSchema struct { // This is a special array of items provided if the validation condition // explicitly states the value must be one of a set. - ValidationContains []string `json:"validation_contains"` + ValidationContains []string `json:"validation_contains,omitempty"` } // CreateParameterRequest is used to create a new parameter value for a scope. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 61415137f3483..bb031235455b7 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -190,7 +190,7 @@ export interface ParameterSchema { readonly validation_condition: string readonly validation_type_system: string readonly validation_value_type: string - readonly validation_contains: string[] + readonly validation_contains?: string[] } // From codersdk/provisionerdaemons.go:33:6 diff --git a/site/src/components/ParameterInput/ParameterInput.tsx b/site/src/components/ParameterInput/ParameterInput.tsx index 7d00bd9d8f16d..638acdac7a35b 100644 --- a/site/src/components/ParameterInput/ParameterInput.tsx +++ b/site/src/components/ParameterInput/ParameterInput.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unnecessary-condition */ import FormControlLabel from "@material-ui/core/FormControlLabel" import Radio from "@material-ui/core/Radio" import RadioGroup from "@material-ui/core/RadioGroup" @@ -38,16 +37,15 @@ const ParameterField: React.FC = ({ disabled, onChange, sch onChange(event.target.value) }} > - {schema.validation_contains && - schema.validation_contains.map((item) => ( - } - label={item} - /> - ))} + {schema.validation_contains.map((item) => ( + } + label={item} + /> + ))} ) } 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