From db013f864c125913b18f89dbc6bfbb1d5b6f3b64 Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 22 Apr 2022 16:26:04 +0000 Subject: [PATCH 01/42] Add button and route --- site/src/AppRouter.tsx | 27 +++++++++++++------ .../CreateUserPage/CreateUserPage.tsx | 5 ++++ site/src/pages/UsersPage/UsersPage.tsx | 4 ++- site/src/pages/UsersPage/UsersPageView.tsx | 7 +++-- 4 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 3f302956a03e1..a253473b3bfb7 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -17,6 +17,7 @@ import { SettingsPage } from "./pages/SettingsPage/SettingsPage" import { CreateWorkspacePage } from "./pages/TemplatesPages/OrganizationPage/TemplatePage/CreateWorkspacePage" import { TemplatePage } from "./pages/TemplatesPages/OrganizationPage/TemplatePage/TemplatePage" import { TemplatesPage } from "./pages/TemplatesPages/TemplatesPage" +import { CreateUserPage } from "./pages/UsersPage/CreateUserPage/CreateUserPage" import { UsersPage } from "./pages/UsersPage/UsersPage" import { WorkspacePage } from "./pages/WorkspacesPage/WorkspacesPage" @@ -76,14 +77,24 @@ export const AppRouter: React.FC = () => ( /> - - - - } - /> + + + + + } + /> + + + + } + /> + { + return
"Coming soon!"
+} diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index fd4f073097bf0..d17c7f0f07128 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -1,5 +1,6 @@ import { useActor } from "@xstate/react" import React, { useContext } from "react" +import { useNavigate } from "react-router" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { XServiceContext } from "../../xServices/StateContext" import { UsersPageView } from "./UsersPageView" @@ -8,10 +9,11 @@ export const UsersPage: React.FC = () => { const xServices = useContext(XServiceContext) const [usersState] = useActor(xServices.usersXService) const { users, pager, getUsersError } = usersState.context + const navigate = useNavigate() if (usersState.matches("error")) { return } else { - return + return {navigate("/users/create")}} /> } } diff --git a/site/src/pages/UsersPage/UsersPageView.tsx b/site/src/pages/UsersPage/UsersPageView.tsx index 812cc4e318f1e..515ff4d353df5 100644 --- a/site/src/pages/UsersPage/UsersPageView.tsx +++ b/site/src/pages/UsersPage/UsersPageView.tsx @@ -7,18 +7,21 @@ import { UsersTable } from "../../components/UsersTable/UsersTable" export const Language = { pageTitle: "Users", pageSubtitle: (pager: Pager | undefined): string => (pager ? `${pager.total} total` : ""), + newUserButton: "New User" } export interface UsersPageViewProps { users: UserResponse[] pager?: Pager + openUserCreationDialog: () => void } -export const UsersPageView: React.FC = ({ users, pager }) => { +export const UsersPageView: React.FC = ({ users, pager, openUserCreationDialog }) => { const styles = useStyles() + return (
-
+
) From b73fb80a9b809ed010e0aa8f3ff4998cb9a9e2e5 Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 22 Apr 2022 16:57:24 +0000 Subject: [PATCH 02/42] Hook up api --- site/src/api/index.ts | 6 +++ site/src/components/GlobalSnackbar/utils.ts | 4 ++ site/src/xServices/users/usersXService.ts | 48 ++++++++++++++++++--- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/site/src/api/index.ts b/site/src/api/index.ts index 232a188a324ac..4e2c4c7df938b 100644 --- a/site/src/api/index.ts +++ b/site/src/api/index.ts @@ -2,6 +2,7 @@ import axios, { AxiosRequestHeaders } from "axios" import { mutate } from "swr" import { MockPager, MockUser, MockUser2 } from "../testHelpers/entities" import * as Types from "./types" +import * as GenTypes from "./typesGenerated" const CONTENT_TYPE_JSON: AxiosRequestHeaders = { "Content-Type": "application/json", @@ -79,6 +80,11 @@ export const getUsers = async (): Promise => { }) } +export const createUser = async (user: GenTypes.CreateUserRequest): Promise => { + const response = await axios.post("/api/v2/users/", user) + return response.data +} + export const getBuildInfo = async (): Promise => { const response = await axios.get("/api/v2/buildinfo") return response.data diff --git a/site/src/components/GlobalSnackbar/utils.ts b/site/src/components/GlobalSnackbar/utils.ts index 84fda4afa3a15..90e69e238f560 100644 --- a/site/src/components/GlobalSnackbar/utils.ts +++ b/site/src/components/GlobalSnackbar/utils.ts @@ -60,3 +60,7 @@ export const displayMsg = (msg: string, additionalMsg?: string): void => { export const displaySuccess = (msg: string, additionalMsg?: string): void => { dispatchNotificationEvent(MsgType.Success, msg, additionalMsg ? [additionalMsg] : undefined) } + +export const displayError = (msg: string, additionalMsg?: string): void => { + dispatchNotificationEvent(MsgType.Error, msg, additionalMsg ? [additionalMsg] : undefined) +} diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index d02c98b84bd35..9277c58b74549 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -1,14 +1,22 @@ import { assign, createMachine } from "xstate" import * as API from "../../api" import * as Types from "../../api/types" +import * as GenTypes from "../../api/typesGenerated" +import { displayError } from "../../components/GlobalSnackbar/utils" + +const Language = { + createUserError: "Unable to create user", + createUserSuccess: "Successfully created user" +} export interface UsersContext { users: Types.UserResponse[] pager?: Types.Pager getUsersError?: Error | unknown + createUserError?: Error | unknown } -export type UsersEvent = { type: "GET_USERS" } +export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE", user: GenTypes.CreateUserRequest } export const usersMachine = createMachine( { @@ -19,6 +27,9 @@ export const usersMachine = createMachine( services: {} as { getUsers: { data: Types.PagedUsers + }, + createUser: { + data: GenTypes.User } }, }, @@ -26,15 +37,21 @@ export const usersMachine = createMachine( context: { users: [], }, - initial: "gettingUsers", + initial: "idle", states: { + idle: { + on: { + GET_USERS: "gettingUsers", + CREATE: "creatingUser" + }, + }, gettingUsers: { invoke: { src: "getUsers", id: "getUsers", onDone: [ { - target: "#usersState.ready", + target: "#usersState.idle", actions: ["assignUsers", "clearGetUsersError"], }, ], @@ -47,10 +64,20 @@ export const usersMachine = createMachine( }, tags: "loading", }, - ready: { - on: { - GET_USERS: "gettingUsers", + creatingUser: { + invoke: { + src: "createUser", + id: "createUser", + onDone: { + target: "gettingUsers", + actions: "displayCreateUserSuccess" + }, + onError: { + target: "idle", + actions: "displayCreateUserError" + } }, + tags: "loading" }, error: { on: { @@ -62,6 +89,9 @@ export const usersMachine = createMachine( { services: { getUsers: API.getUsers, + createUser: (_, event) => ( + API.createUser(event.user) + ) }, actions: { assignUsers: assign({ @@ -75,6 +105,12 @@ export const usersMachine = createMachine( ...context, getUsersError: undefined, })), + displayCreateUserError: () => { + displayError(Language.createUserError) + }, + displayCreateUserSuccess: () => { + displayError(Language.createUserSuccess) + } }, }, ) From fc5ec160f99cd172a07a4800ed43d03f0e15de51 Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 22 Apr 2022 17:27:42 +0000 Subject: [PATCH 03/42] Lint --- site/src/AppRouter.tsx | 2 +- site/src/pages/UsersPage/UsersPage.tsx | 10 +++++++++- site/src/pages/UsersPage/UsersPageView.tsx | 8 ++++++-- site/src/xServices/users/usersXService.ts | 22 ++++++++++------------ 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index a253473b3bfb7..031939e44d768 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -77,7 +77,7 @@ export const AppRouter: React.FC = () => ( />
- + { if (usersState.matches("error")) { return } else { - return {navigate("/users/create")}} /> + return ( + { + navigate("/users/create") + }} + /> + ) } } diff --git a/site/src/pages/UsersPage/UsersPageView.tsx b/site/src/pages/UsersPage/UsersPageView.tsx index 515ff4d353df5..a919ac9f5f2f5 100644 --- a/site/src/pages/UsersPage/UsersPageView.tsx +++ b/site/src/pages/UsersPage/UsersPageView.tsx @@ -7,7 +7,7 @@ import { UsersTable } from "../../components/UsersTable/UsersTable" export const Language = { pageTitle: "Users", pageSubtitle: (pager: Pager | undefined): string => (pager ? `${pager.total} total` : ""), - newUserButton: "New User" + newUserButton: "New User", } export interface UsersPageViewProps { @@ -21,7 +21,11 @@ export const UsersPageView: React.FC = ({ users, pager, open return (
-
+
) diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 9277c58b74549..39cde6e9421d6 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -6,7 +6,7 @@ import { displayError } from "../../components/GlobalSnackbar/utils" const Language = { createUserError: "Unable to create user", - createUserSuccess: "Successfully created user" + createUserSuccess: "Successfully created user", } export interface UsersContext { @@ -16,7 +16,7 @@ export interface UsersContext { createUserError?: Error | unknown } -export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE", user: GenTypes.CreateUserRequest } +export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE"; user: GenTypes.CreateUserRequest } export const usersMachine = createMachine( { @@ -27,7 +27,7 @@ export const usersMachine = createMachine( services: {} as { getUsers: { data: Types.PagedUsers - }, + } createUser: { data: GenTypes.User } @@ -42,7 +42,7 @@ export const usersMachine = createMachine( idle: { on: { GET_USERS: "gettingUsers", - CREATE: "creatingUser" + CREATE: "creatingUser", }, }, gettingUsers: { @@ -70,14 +70,14 @@ export const usersMachine = createMachine( id: "createUser", onDone: { target: "gettingUsers", - actions: "displayCreateUserSuccess" + actions: "displayCreateUserSuccess", }, onError: { target: "idle", - actions: "displayCreateUserError" - } + actions: "displayCreateUserError", + }, }, - tags: "loading" + tags: "loading", }, error: { on: { @@ -89,9 +89,7 @@ export const usersMachine = createMachine( { services: { getUsers: API.getUsers, - createUser: (_, event) => ( - API.createUser(event.user) - ) + createUser: (_, event) => API.createUser(event.user), }, actions: { assignUsers: assign({ @@ -110,7 +108,7 @@ export const usersMachine = createMachine( }, displayCreateUserSuccess: () => { displayError(Language.createUserSuccess) - } + }, }, }, ) From 635eab9f4cb5c168e99a5243b36caeb3b5bc9d3b Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 22 Apr 2022 17:29:58 +0000 Subject: [PATCH 04/42] Add basic form --- .../CreateUserForm/CreateUserForm.stories.tsx | 22 +++++ .../CreateUserForm/CreateUserForm.tsx | 80 +++++++++++++++++++ .../CreateUserPage/CreateUserPage.tsx | 11 ++- 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 site/src/components/CreateUserForm/CreateUserForm.stories.tsx create mode 100644 site/src/components/CreateUserForm/CreateUserForm.tsx diff --git a/site/src/components/CreateUserForm/CreateUserForm.stories.tsx b/site/src/components/CreateUserForm/CreateUserForm.stories.tsx new file mode 100644 index 0000000000000..4d9b27980fe01 --- /dev/null +++ b/site/src/components/CreateUserForm/CreateUserForm.stories.tsx @@ -0,0 +1,22 @@ +import { Story } from "@storybook/react" +import React from "react" +import { CreateUserForm, CreateUserFormProps } from "./CreateUserForm" + +export default { + title: "components/CreateUserForm", + component: CreateUserForm, + argTypes: { + isLoading: "boolean", + authErrorMessage: "string", + onSubmit: { action: "Submit" }, + }, +} + +const Template: Story = (args: CreateUserFormProps) => + +export const Example = Template.bind({}) +Example.args = { + onSubmit: () => { + return Promise.resolve() + }, +} diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx new file mode 100644 index 0000000000000..9be5e2087be38 --- /dev/null +++ b/site/src/components/CreateUserForm/CreateUserForm.tsx @@ -0,0 +1,80 @@ +import Button from "@material-ui/core/Button" +import TextField from "@material-ui/core/TextField" +import { FormikContextType, useFormik } from "formik" +import React from "react" +import * as Yup from "yup" +import { CreateUserRequest } from "../../api/typesGenerated" +import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" +import { LoadingButton } from "../LoadingButton/LoadingButton" + +const Language = { + emailLabel: "Email", + passwordLabel: "Password", + usernameLabel: "Username", + emailInvalid: "Please enter a valid email address.", + emailRequired: "Please enter an email address.", + passwordRequired: "Please enter a password.", + usernameRequired: "Please enter a username.", + createUser: "Create", + cancel: "Cancel", +} + +export interface CreateUserFormProps { + onSubmit: (user: CreateUserRequest) => void +} + +const validationSchema = Yup.object({ + email: Yup.string().trim().email(Language.emailInvalid).required(Language.emailRequired), + password: Yup.string().required(), + username: Yup.string().required(), +}) + +export const CreateUserForm: React.FC = ({ onSubmit }) => { + const form: FormikContextType = useFormik({ + initialValues: { + email: "", + password: "", + username: "", + }, + validationSchema, + onSubmit, + }) + + return ( +
+ (form, "username")} + onChange={onChangeTrimmed(form)} + autoFocus + autoComplete="username" + fullWidth + label={Language.usernameLabel} + variant="outlined" + /> + (form, "email")} + onChange={onChangeTrimmed(form)} + autoFocus + autoComplete="email" + fullWidth + label={Language.emailLabel} + variant="outlined" + /> + (form, "password")} + autoComplete="current-password" + fullWidth + id="password" + label={Language.passwordLabel} + type="password" + variant="outlined" + /> +
+ + {Language.createUser} + + +
+ + ) +} diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index f21789b603af8..4d558706ac6a2 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -1,5 +1,12 @@ -import React from "react" +import { useActor } from "@xstate/react" +import React, { useContext } from "react" +import { CreateUserRequest } from "../../../api/typesGenerated" +import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" +import { XServiceContext } from "../../../xServices/StateContext" export const CreateUserPage = () => { - return
"Coming soon!"
+ const xServices = useContext(XServiceContext) + const [_, usersSend] = useActor(xServices.usersXService) + + return usersSend({ type: "CREATE", user })} /> } From c2de4d49d308835311a98102b3d2bb216143a29c Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 16:51:37 +0000 Subject: [PATCH 05/42] Get users on page mount --- site/src/pages/UsersPage/UsersPage.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 1c9e405ad3bc1..552ea3a835e50 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -1,5 +1,5 @@ import { useActor } from "@xstate/react" -import React, { useContext } from "react" +import React, { useContext, useEffect } from "react" import { useNavigate } from "react-router" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { XServiceContext } from "../../xServices/StateContext" @@ -7,10 +7,17 @@ import { UsersPageView } from "./UsersPageView" export const UsersPage: React.FC = () => { const xServices = useContext(XServiceContext) - const [usersState] = useActor(xServices.usersXService) + const [usersState, usersSend] = useActor(xServices.usersXService) const { users, pager, getUsersError } = usersState.context const navigate = useNavigate() + /** + * Fetch users on component mount + */ + useEffect(() => { + usersSend("GET_USERS") + }, []) + if (usersState.matches("error")) { return } else { From 028840ad3d0bf08f7935bba980720222d9b4e946 Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 16:52:17 +0000 Subject: [PATCH 06/42] Make cancel work --- site/src/components/CreateUserForm/CreateUserForm.tsx | 5 +++-- site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx index 9be5e2087be38..50d7fd9702653 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.tsx @@ -21,6 +21,7 @@ const Language = { export interface CreateUserFormProps { onSubmit: (user: CreateUserRequest) => void + onCancel: () => void } const validationSchema = Yup.object({ @@ -29,7 +30,7 @@ const validationSchema = Yup.object({ username: Yup.string().required(), }) -export const CreateUserForm: React.FC = ({ onSubmit }) => { +export const CreateUserForm: React.FC = ({ onSubmit, onCancel }) => { const form: FormikContextType = useFormik({ initialValues: { email: "", @@ -70,10 +71,10 @@ export const CreateUserForm: React.FC = ({ onSubmit }) => { variant="outlined" />
+ {Language.createUser} -
) diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index 4d558706ac6a2..57bdac412fbf4 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -1,5 +1,6 @@ import { useActor } from "@xstate/react" import React, { useContext } from "react" +import { useNavigate } from "react-router" import { CreateUserRequest } from "../../../api/typesGenerated" import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" import { XServiceContext } from "../../../xServices/StateContext" @@ -7,6 +8,7 @@ import { XServiceContext } from "../../../xServices/StateContext" export const CreateUserPage = () => { const xServices = useContext(XServiceContext) const [_, usersSend] = useActor(xServices.usersXService) + const navigate = useNavigate() - return usersSend({ type: "CREATE", user })} /> + return usersSend({ type: "CREATE", user })} onCancel={() => navigate("/users")} /> } From 8c31769f4bcf26e9debb9afaa6f17b30793c7521 Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 16:54:51 +0000 Subject: [PATCH 07/42] Creating -> idle bc users page refetches --- site/src/xServices/users/usersXService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 39cde6e9421d6..39a08dd4e869d 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -69,7 +69,7 @@ export const usersMachine = createMachine( src: "createUser", id: "createUser", onDone: { - target: "gettingUsers", + target: "idle", actions: "displayCreateUserSuccess", }, onError: { From 073d6941af4cefc7bd7f139c408244337039f055 Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 16:59:03 +0000 Subject: [PATCH 08/42] Import as TypesGen --- site/src/xServices/users/usersXService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 39a08dd4e869d..ef6e0a1f2bb81 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -1,7 +1,7 @@ import { assign, createMachine } from "xstate" import * as API from "../../api" import * as Types from "../../api/types" -import * as GenTypes from "../../api/typesGenerated" +import * as TypesGen from "../../api/typesGenerated" import { displayError } from "../../components/GlobalSnackbar/utils" const Language = { @@ -16,7 +16,7 @@ export interface UsersContext { createUserError?: Error | unknown } -export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE"; user: GenTypes.CreateUserRequest } +export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE"; user: TypesGen.CreateUserRequest } export const usersMachine = createMachine( { @@ -29,7 +29,7 @@ export const usersMachine = createMachine( data: Types.PagedUsers } createUser: { - data: GenTypes.User + data: TypesGen.User } }, }, From 4aa97e680f6dd869fe301dd1c67f728589d9391b Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 19:59:31 +0000 Subject: [PATCH 09/42] Handle api errors --- .../components/CreateUserForm/CreateUserForm.tsx | 12 +++++++----- .../UsersPage/CreateUserPage/CreateUserPage.tsx | 14 ++++++++++++-- site/src/xServices/users/usersXService.ts | 13 ++++++++++--- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx index 50d7fd9702653..5e7d422fe2483 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.tsx @@ -1,6 +1,6 @@ import Button from "@material-ui/core/Button" import TextField from "@material-ui/core/TextField" -import { FormikContextType, useFormik } from "formik" +import { FormikContextType, FormikErrors, useFormik } from "formik" import React from "react" import * as Yup from "yup" import { CreateUserRequest } from "../../api/typesGenerated" @@ -22,6 +22,7 @@ const Language = { export interface CreateUserFormProps { onSubmit: (user: CreateUserRequest) => void onCancel: () => void + formErrors?: FormikErrors } const validationSchema = Yup.object({ @@ -30,7 +31,7 @@ const validationSchema = Yup.object({ username: Yup.string().required(), }) -export const CreateUserForm: React.FC = ({ onSubmit, onCancel }) => { +export const CreateUserForm: React.FC = ({ onSubmit, onCancel, formErrors }) => { const form: FormikContextType = useFormik({ initialValues: { email: "", @@ -40,11 +41,12 @@ export const CreateUserForm: React.FC = ({ onSubmit, onCanc validationSchema, onSubmit, }) + const getFieldHelpers = getFormHelpers(form, formErrors) return (
(form, "username")} + {...getFieldHelpers("username")} onChange={onChangeTrimmed(form)} autoFocus autoComplete="username" @@ -53,7 +55,7 @@ export const CreateUserForm: React.FC = ({ onSubmit, onCanc variant="outlined" /> (form, "email")} + {...getFieldHelpers("email")} onChange={onChangeTrimmed(form)} autoFocus autoComplete="email" @@ -62,7 +64,7 @@ export const CreateUserForm: React.FC = ({ onSubmit, onCanc variant="outlined" /> (form, "password")} + {...getFieldHelpers("password")} autoComplete="current-password" fullWidth id="password" diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index 57bdac412fbf4..db376bb30d9ba 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -1,14 +1,24 @@ import { useActor } from "@xstate/react" import React, { useContext } from "react" import { useNavigate } from "react-router" +import { isApiError, mapApiErrorToFieldErrors } from "../../../api/errors" import { CreateUserRequest } from "../../../api/typesGenerated" import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" import { XServiceContext } from "../../../xServices/StateContext" export const CreateUserPage = () => { const xServices = useContext(XServiceContext) - const [_, usersSend] = useActor(xServices.usersXService) + const [usersState, usersSend] = useActor(xServices.usersXService) + const { createUserError } = usersState.context + const formErrors = + createUserError && isApiError(createUserError) ? mapApiErrorToFieldErrors(createUserError.response.data) : undefined const navigate = useNavigate() - return usersSend({ type: "CREATE", user })} onCancel={() => navigate("/users")} /> + return ( + usersSend({ type: "CREATE", user })} + onCancel={() => navigate("/users")} + /> + ) } diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index ef6e0a1f2bb81..ce03056545218 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -70,11 +70,11 @@ export const usersMachine = createMachine( id: "createUser", onDone: { target: "idle", - actions: "displayCreateUserSuccess", + actions: ["displayCreateUserSuccess", "clearCreateUserError"], }, onError: { target: "idle", - actions: "displayCreateUserError", + actions: ["assignCreateUserError", "displayCreateUserError"], }, }, tags: "loading", @@ -103,7 +103,14 @@ export const usersMachine = createMachine( ...context, getUsersError: undefined, })), - displayCreateUserError: () => { + assignCreateUserError: assign({ + createUserError: (_, event) => event.data + }), + clearCreateUserError: assign((context: UsersContext) => ({ + ...context, + createUserError: undefined + })), + displayCreateUserError: (_, event) => { displayError(Language.createUserError) }, displayCreateUserSuccess: () => { From 211ca6ff93e697aab6aca9c3e045011bf43b5a15 Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 20:00:45 +0000 Subject: [PATCH 10/42] Lint --- site/src/pages/UsersPage/UsersPage.tsx | 2 +- site/src/xServices/users/usersXService.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 552ea3a835e50..699a4322343ce 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -11,7 +11,7 @@ export const UsersPage: React.FC = () => { const { users, pager, getUsersError } = usersState.context const navigate = useNavigate() - /** + /** * Fetch users on component mount */ useEffect(() => { diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index ce03056545218..0be3f5968931e 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -104,11 +104,11 @@ export const usersMachine = createMachine( getUsersError: undefined, })), assignCreateUserError: assign({ - createUserError: (_, event) => event.data + createUserError: (_, event) => event.data, }), clearCreateUserError: assign((context: UsersContext) => ({ ...context, - createUserError: undefined + createUserError: undefined, })), displayCreateUserError: (_, event) => { displayError(Language.createUserError) From f9c29c898dda95f29e0f66471134404bcc34613a Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 20:14:46 +0000 Subject: [PATCH 11/42] Add handler --- site/src/api/index.ts | 2 +- site/src/testHelpers/handlers.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/site/src/api/index.ts b/site/src/api/index.ts index 318bcf9f84782..9ed9fa1852f79 100644 --- a/site/src/api/index.ts +++ b/site/src/api/index.ts @@ -86,7 +86,7 @@ export const getUsers = async (): Promise => { } export const createUser = async (user: TypesGen.CreateUserRequest): Promise => { - const response = await axios.post("/api/v2/users/", user) + const response = await axios.post("/api/v2/users", user) return response.data } diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 22e4818085a50..a6b53714b9283 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -24,6 +24,9 @@ export const handlers = [ rest.get("/api/v2/users", async (req, res, ctx) => { return res(ctx.status(200), ctx.json({ page: [M.MockUser, M.MockUser2], pager: M.MockPager })) }), + rest.post("/api/v2/users", async (req, res, ctx) => { + return res(ctx.status(200), ctx.json(M.MockUser)) + }), rest.post("/api/v2/users/me/workspaces", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockWorkspace)) }), From c501b6a7b3e4602a517b6648345d0346c5539208 Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 22:08:54 +0000 Subject: [PATCH 12/42] Add FormFooter --- .../FormFooter/FormFooter.stories.tsx | 29 +++++++++++++++ site/src/components/FormFooter/FormFooter.tsx | 37 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 site/src/components/FormFooter/FormFooter.stories.tsx create mode 100644 site/src/components/FormFooter/FormFooter.tsx diff --git a/site/src/components/FormFooter/FormFooter.stories.tsx b/site/src/components/FormFooter/FormFooter.stories.tsx new file mode 100644 index 0000000000000..9d860c0102529 --- /dev/null +++ b/site/src/components/FormFooter/FormFooter.stories.tsx @@ -0,0 +1,29 @@ +import { ComponentMeta, Story } from "@storybook/react" +import React from "react" +import { FormFooter, FormFooterProps } from "./FormFooter" + +export default { + title: "components/FormFooter", + component: FormFooter, + argTypes:{ + onCancel: { action: "cancel" } + } +} as ComponentMeta + +const Template: Story = (args) => + +export const Ready = Template.bind({}) +Ready.args = { + isLoading: false, +} + +export const Custom = Template.bind({}) +Custom.args = { + isLoading: false, + submitLabel: "Create", +} + +export const Loading = Template.bind({}) +Loading.args = { + isLoading: true, +} diff --git a/site/src/components/FormFooter/FormFooter.tsx b/site/src/components/FormFooter/FormFooter.tsx new file mode 100644 index 0000000000000..7107f7864b0d1 --- /dev/null +++ b/site/src/components/FormFooter/FormFooter.tsx @@ -0,0 +1,37 @@ +import Button from "@material-ui/core/Button" +import { makeStyles } from "@material-ui/core/styles" +import React from "react" +import { LoadingButton } from "../LoadingButton/LoadingButton" + +export interface FormFooterProps { + onCancel: () => void + isLoading: boolean + submitLabel?: string +} + +const useStyles = makeStyles(() => ({ + footer: { + display: "flex", + flex: "0", + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + }, + button: { + margin: "1em", + }, +})) + +export const FormFooter: React.FC = ({ onCancel, isLoading, submitLabel = "Submit" }) => { + const styles = useStyles() + return ( +
+ + + {submitLabel} + +
+ ) +} From 5d6d0c116a848a1f6bdf63fdfe037526ebe66834 Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 22:09:00 +0000 Subject: [PATCH 13/42] Add FullPageForm --- .../FullPageForm/FullPageForm.stories.tsx | 35 +++++++++++++++++++ .../components/FullPageForm/FullPageForm.tsx | 32 +++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 site/src/components/FullPageForm/FullPageForm.stories.tsx create mode 100644 site/src/components/FullPageForm/FullPageForm.tsx diff --git a/site/src/components/FullPageForm/FullPageForm.stories.tsx b/site/src/components/FullPageForm/FullPageForm.stories.tsx new file mode 100644 index 0000000000000..e34c90eb2055b --- /dev/null +++ b/site/src/components/FullPageForm/FullPageForm.stories.tsx @@ -0,0 +1,35 @@ +import TextField from "@material-ui/core/TextField" +import { action } from "@storybook/addon-actions" +import { ComponentMeta, Story } from "@storybook/react" +import React from "react" +import { FormFooter } from "../FormFooter/FormFooter" +import { Stack } from "../Stack/Stack" +import { FullPageForm, FullPageFormProps } from "./FullPageForm" + +export default { + title: "components/FullPageForm", + component: FullPageForm, +} as ComponentMeta + +const Template: Story = (args) => ( + + { + e.preventDefault() + }} + > + + + + + + + +) + +export const Example = Template.bind({}) +Example.args = { + title: "My Form", + detail: "Lorem ipsum dolor", + onCancel: action("cancel") +} diff --git a/site/src/components/FullPageForm/FullPageForm.tsx b/site/src/components/FullPageForm/FullPageForm.tsx new file mode 100644 index 0000000000000..dc45c2b923010 --- /dev/null +++ b/site/src/components/FullPageForm/FullPageForm.tsx @@ -0,0 +1,32 @@ +import { makeStyles } from "@material-ui/core/styles" +import React from "react" +import { FormCloseButton } from "../FormCloseButton/FormCloseButton" +import { FormTitle } from "../FormTitle/FormTitle" + +export interface FullPageFormProps { + title: string + detail: React.ReactNode + onCancel: () => void +} + +const useStyles = makeStyles(() => ({ + root: { + maxWidth: "1380px", + width: "100%", + display: "flex", + flexDirection: "column", + alignItems: "center", + }, +})) + +export const FullPageForm: React.FC = ({ title, detail, onCancel, children }) => { + const styles = useStyles() + return ( +
+ + + + {children} +
+ ) +} From 861969eead1339a5161b3cfd06eba9ba6af9e2fe Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 22:09:43 +0000 Subject: [PATCH 14/42] Lint --- site/src/components/FormFooter/FormFooter.stories.tsx | 6 +++--- site/src/components/FullPageForm/FullPageForm.stories.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/components/FormFooter/FormFooter.stories.tsx b/site/src/components/FormFooter/FormFooter.stories.tsx index 9d860c0102529..4590a263013d9 100644 --- a/site/src/components/FormFooter/FormFooter.stories.tsx +++ b/site/src/components/FormFooter/FormFooter.stories.tsx @@ -5,9 +5,9 @@ import { FormFooter, FormFooterProps } from "./FormFooter" export default { title: "components/FormFooter", component: FormFooter, - argTypes:{ - onCancel: { action: "cancel" } - } + argTypes: { + onCancel: { action: "cancel" }, + }, } as ComponentMeta const Template: Story = (args) => diff --git a/site/src/components/FullPageForm/FullPageForm.stories.tsx b/site/src/components/FullPageForm/FullPageForm.stories.tsx index e34c90eb2055b..3a92bc64cca1e 100644 --- a/site/src/components/FullPageForm/FullPageForm.stories.tsx +++ b/site/src/components/FullPageForm/FullPageForm.stories.tsx @@ -31,5 +31,5 @@ export const Example = Template.bind({}) Example.args = { title: "My Form", detail: "Lorem ipsum dolor", - onCancel: action("cancel") + onCancel: action("cancel"), } From 8b729766c8a59334e2dca9a50f7e5c5f18dac166 Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 22:50:12 +0000 Subject: [PATCH 15/42] Better form, error, stories bug in formErrors story --- .../CreateUserForm/CreateUserForm.stories.tsx | 41 +++++++--- .../CreateUserForm/CreateUserForm.tsx | 76 +++++++++---------- .../CreateUserPage/CreateUserPage.tsx | 11 ++- 3 files changed, 78 insertions(+), 50 deletions(-) diff --git a/site/src/components/CreateUserForm/CreateUserForm.stories.tsx b/site/src/components/CreateUserForm/CreateUserForm.stories.tsx index 4d9b27980fe01..271e9e7bc4687 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.stories.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.stories.tsx @@ -1,3 +1,4 @@ +import { action } from "@storybook/addon-actions" import { Story } from "@storybook/react" import React from "react" import { CreateUserForm, CreateUserFormProps } from "./CreateUserForm" @@ -5,18 +6,38 @@ import { CreateUserForm, CreateUserFormProps } from "./CreateUserForm" export default { title: "components/CreateUserForm", component: CreateUserForm, - argTypes: { - isLoading: "boolean", - authErrorMessage: "string", - onSubmit: { action: "Submit" }, - }, } const Template: Story = (args: CreateUserFormProps) => -export const Example = Template.bind({}) -Example.args = { - onSubmit: () => { - return Promise.resolve() - }, +export const Ready = Template.bind({}) +Ready.args = { + onCancel: action('cancel'), + onSubmit: action('submit'), + isLoading: false, +} + +export const UnknownError = Template.bind({}) +UnknownError.args = { + onCancel: action('cancel'), + onSubmit: action('submit'), + isLoading: false, + error: "Something went wrong" +} + +export const FormError = Template.bind({}) +FormError.args = { + onCancel: action('cancel'), + onSubmit: action('submit'), + isLoading: false, + formErrors: { + email: "Bad email address" + } +} + +export const Loading = Template.bind({}) +Loading.args = { + onCancel: action('cancel'), + onSubmit: action('submit'), + isLoading: true, } diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx index 5e7d422fe2483..10efd8114b31c 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.tsx @@ -1,11 +1,12 @@ -import Button from "@material-ui/core/Button" +import FormHelperText from "@material-ui/core/FormHelperText" import TextField from "@material-ui/core/TextField" import { FormikContextType, FormikErrors, useFormik } from "formik" import React from "react" import * as Yup from "yup" import { CreateUserRequest } from "../../api/typesGenerated" import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" -import { LoadingButton } from "../LoadingButton/LoadingButton" +import { FormFooter } from "../FormFooter/FormFooter" +import { FullPageForm } from "../FullPageForm/FullPageForm" const Language = { emailLabel: "Email", @@ -23,6 +24,8 @@ export interface CreateUserFormProps { onSubmit: (user: CreateUserRequest) => void onCancel: () => void formErrors?: FormikErrors + isLoading: boolean + error?: string } const validationSchema = Yup.object({ @@ -31,7 +34,7 @@ const validationSchema = Yup.object({ username: Yup.string().required(), }) -export const CreateUserForm: React.FC = ({ onSubmit, onCancel, formErrors }) => { +export const CreateUserForm: React.FC = ({ onSubmit, onCancel, formErrors, isLoading, error }) => { const form: FormikContextType = useFormik({ initialValues: { email: "", @@ -42,42 +45,39 @@ export const CreateUserForm: React.FC = ({ onSubmit, onCanc onSubmit, }) const getFieldHelpers = getFormHelpers(form, formErrors) + console.log(getFieldHelpers("email")) return ( -
- - - -
- - - {Language.createUser} - -
- + +
+ + + + {error && {error}} + + +
) } diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index db376bb30d9ba..0caa365b64ee4 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -6,12 +6,17 @@ import { CreateUserRequest } from "../../../api/typesGenerated" import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" import { XServiceContext } from "../../../xServices/StateContext" +const Language = { + unknownError: "Oops, an unknown error occurred.", +} + export const CreateUserPage = () => { const xServices = useContext(XServiceContext) const [usersState, usersSend] = useActor(xServices.usersXService) const { createUserError } = usersState.context - const formErrors = - createUserError && isApiError(createUserError) ? mapApiErrorToFieldErrors(createUserError.response.data) : undefined + const apiError = isApiError(createUserError) + const formErrors = apiError ? mapApiErrorToFieldErrors(createUserError.response.data) : undefined + const hasUnknownError = createUserError && !apiError const navigate = useNavigate() return ( @@ -19,6 +24,8 @@ export const CreateUserPage = () => { formErrors={formErrors} onSubmit={(user: CreateUserRequest) => usersSend({ type: "CREATE", user })} onCancel={() => navigate("/users")} + isLoading={usersState.hasTag("loading")} + error={hasUnknownError ? Language.unknownError : undefined} /> ) } From 369af6b04ad72222d058f1e7d837909103735850 Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 22:51:10 +0000 Subject: [PATCH 16/42] Make detail optional --- site/src/components/FullPageForm/FullPageForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/FullPageForm/FullPageForm.tsx b/site/src/components/FullPageForm/FullPageForm.tsx index dc45c2b923010..83f9c345da611 100644 --- a/site/src/components/FullPageForm/FullPageForm.tsx +++ b/site/src/components/FullPageForm/FullPageForm.tsx @@ -5,7 +5,7 @@ import { FormTitle } from "../FormTitle/FormTitle" export interface FullPageFormProps { title: string - detail: React.ReactNode + detail?: React.ReactNode onCancel: () => void } From 90283ed6c6de9d543f6c9e44c5874861740f6c17 Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 22:53:50 +0000 Subject: [PATCH 17/42] Use Language --- site/src/components/FormFooter/FormFooter.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/site/src/components/FormFooter/FormFooter.tsx b/site/src/components/FormFooter/FormFooter.tsx index 7107f7864b0d1..a7b5c48ba1717 100644 --- a/site/src/components/FormFooter/FormFooter.tsx +++ b/site/src/components/FormFooter/FormFooter.tsx @@ -3,6 +3,11 @@ import { makeStyles } from "@material-ui/core/styles" import React from "react" import { LoadingButton } from "../LoadingButton/LoadingButton" +const Language = { + cancelLabel: "Cancel", + defaultSubmitLabel: "Submit", +} + export interface FormFooterProps { onCancel: () => void isLoading: boolean @@ -22,12 +27,16 @@ const useStyles = makeStyles(() => ({ }, })) -export const FormFooter: React.FC = ({ onCancel, isLoading, submitLabel = "Submit" }) => { +export const FormFooter: React.FC = ({ + onCancel, + isLoading, + submitLabel = Language.defaultSubmitLabel, +}) => { const styles = useStyles() return (
{submitLabel} From 35f462a73bad655cf132af3dc379095b6fc51db5 Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 25 Apr 2022 22:55:56 +0000 Subject: [PATCH 18/42] Remove detail prop --- site/src/components/CreateUserForm/CreateUserForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx index 10efd8114b31c..392ad181ccb31 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.tsx @@ -48,7 +48,7 @@ export const CreateUserForm: React.FC = ({ onSubmit, onCanc console.log(getFieldHelpers("email")) return ( - +
Date: Tue, 26 Apr 2022 01:12:57 +0000 Subject: [PATCH 19/42] Add back autoFocus --- site/src/components/CreateUserForm/CreateUserForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx index 392ad181ccb31..9e00277e3f3c3 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.tsx @@ -54,6 +54,7 @@ export const CreateUserForm: React.FC = ({ onSubmit, onCanc {...getFieldHelpers("username")} onChange={onChangeTrimmed(form)} autoComplete="username" + autoFocus fullWidth label={Language.usernameLabel} variant="outlined" From c2f6cce93863e0b6eccc2ea4130d4ac68ac33fa1 Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 26 Apr 2022 02:19:47 +0000 Subject: [PATCH 20/42] Remove displayError, use displaySuccess --- site/src/components/GlobalSnackbar/utils.ts | 4 ---- site/src/xServices/users/usersXService.ts | 12 ++++-------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/site/src/components/GlobalSnackbar/utils.ts b/site/src/components/GlobalSnackbar/utils.ts index 90e69e238f560..84fda4afa3a15 100644 --- a/site/src/components/GlobalSnackbar/utils.ts +++ b/site/src/components/GlobalSnackbar/utils.ts @@ -60,7 +60,3 @@ export const displayMsg = (msg: string, additionalMsg?: string): void => { export const displaySuccess = (msg: string, additionalMsg?: string): void => { dispatchNotificationEvent(MsgType.Success, msg, additionalMsg ? [additionalMsg] : undefined) } - -export const displayError = (msg: string, additionalMsg?: string): void => { - dispatchNotificationEvent(MsgType.Error, msg, additionalMsg ? [additionalMsg] : undefined) -} diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 0be3f5968931e..e17619617ce47 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -2,10 +2,9 @@ import { assign, createMachine } from "xstate" import * as API from "../../api" import * as Types from "../../api/types" import * as TypesGen from "../../api/typesGenerated" -import { displayError } from "../../components/GlobalSnackbar/utils" +import { displaySuccess } from "../../components/GlobalSnackbar/utils" -const Language = { - createUserError: "Unable to create user", +export const Language = { createUserSuccess: "Successfully created user", } @@ -74,7 +73,7 @@ export const usersMachine = createMachine( }, onError: { target: "idle", - actions: ["assignCreateUserError", "displayCreateUserError"], + actions: ["assignCreateUserError"], }, }, tags: "loading", @@ -110,11 +109,8 @@ export const usersMachine = createMachine( ...context, createUserError: undefined, })), - displayCreateUserError: (_, event) => { - displayError(Language.createUserError) - }, displayCreateUserSuccess: () => { - displayError(Language.createUserSuccess) + displaySuccess(Language.createUserSuccess) }, }, }, From f46799e7379555b2bfcecace784d5b39b4340be7 Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 26 Apr 2022 02:20:33 +0000 Subject: [PATCH 21/42] Lint, export Language --- .../CreateUserForm/CreateUserForm.stories.tsx | 22 +++++++++---------- .../CreateUserForm/CreateUserForm.tsx | 3 +-- site/src/components/FormFooter/FormFooter.tsx | 2 +- .../CreateUserPage/CreateUserPage.tsx | 2 +- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/site/src/components/CreateUserForm/CreateUserForm.stories.tsx b/site/src/components/CreateUserForm/CreateUserForm.stories.tsx index 271e9e7bc4687..1f6d99c173fdc 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.stories.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.stories.tsx @@ -12,32 +12,32 @@ const Template: Story = (args: CreateUserFormProps) => = ({ onSubmit, onCanc onSubmit, }) const getFieldHelpers = getFormHelpers(form, formErrors) - console.log(getFieldHelpers("email")) return ( diff --git a/site/src/components/FormFooter/FormFooter.tsx b/site/src/components/FormFooter/FormFooter.tsx index a7b5c48ba1717..03233f42b720a 100644 --- a/site/src/components/FormFooter/FormFooter.tsx +++ b/site/src/components/FormFooter/FormFooter.tsx @@ -3,7 +3,7 @@ import { makeStyles } from "@material-ui/core/styles" import React from "react" import { LoadingButton } from "../LoadingButton/LoadingButton" -const Language = { +export const Language = { cancelLabel: "Cancel", defaultSubmitLabel: "Submit", } diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index 0caa365b64ee4..8b341aabdd48b 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -6,7 +6,7 @@ import { CreateUserRequest } from "../../../api/typesGenerated" import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" import { XServiceContext } from "../../../xServices/StateContext" -const Language = { +export const Language = { unknownError: "Oops, an unknown error occurred.", } From 7d6a03f43aeaa3613132358717f31ee36b5b45d5 Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 26 Apr 2022 02:21:25 +0000 Subject: [PATCH 22/42] Tests - wip --- .../CreateUserPage/CreateUserPage.test.tsx | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx new file mode 100644 index 0000000000000..24853b9ebccf5 --- /dev/null +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx @@ -0,0 +1,88 @@ +import { act, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { rest } from "msw" +import React from "react" +import { Language as FormLanguage } from "../../../components/CreateUserForm/CreateUserForm" +import { Language as FooterLanguage } from "../../../components/FormFooter/FormFooter" +import { Language as UserLanguage } from "../../../xServices/users/usersXService" +import { history, render } from "../../../testHelpers" +import { server } from "../../../testHelpers/server" +import { CreateUserPage, Language } from "./CreateUserPage" + +const fillForm = async ({ + username = "testuser", + email = "test@coder.com", + password = "password", +}: { + username?: string + email?: string + password?: string +}) => { + const usernameField = screen.getByLabelText(FormLanguage.usernameLabel) + const emailField = screen.getByLabelText(FormLanguage.emailLabel) + const passwordField = screen.getByLabelText(FormLanguage.passwordLabel) + await userEvent.type(usernameField, username) + await userEvent.type(emailField, email) + await userEvent.type(passwordField, password) + const submitButton = await screen.findByText(FooterLanguage.defaultSubmitLabel) + act(() => submitButton.click()) +} + +describe("Create User Page", () => { + beforeEach(() => { + history.replace("/users/create") + }) + + it("shows validation error message", async () => { + render() + await fillForm({ email: "test" }) + const errorMessage = await screen.findByText(FormLanguage.emailInvalid) + expect(errorMessage).toBeDefined() + }) + it("shows generic error message", async () => { + server.use( + rest.post("/api/v2/users", (req, res, ctx) => { + Promise.reject("something went wrong") + }), + ) + render() + await fillForm({}) + const errorMessage = await screen.findByText(Language.unknownError) + expect(errorMessage).toBeDefined() + }) + it("shows API error message", async () => { + const fieldErrorMessage = "username already in use" + server.use( + rest.post("/api/v2/users", (req, res, ctx) => { + return res(ctx.status(400), ctx.json({ + message: "invalid field", + errors: [{ + detail: fieldErrorMessage, + field: "username" + }] + })) + }), + ) + render() + await fillForm({}) + const errorMessage = await screen.findByText(fieldErrorMessage) + expect(errorMessage).toBeDefined() + }) + it("shows success notification and redirects to users page", async () => { + render() + await fillForm({}) + const successMessage = screen.findByText(UserLanguage.createUserSuccess) + expect(successMessage).toBeDefined() + expect(history.location.pathname).toEqual("/users") + }) + it("redirects to users page on cancel", () => { + render() + screen.findByText(FooterLanguage.cancelLabel) + expect(history.location.pathname).toEqual("/users") + }) + it("redirects to users page on close", () => { + render() + screen.findByText("ESC") + expect(history.location.pathname).toEqual("/users") + }) +}) From f5fe8e8219a7dfe4f8c7be703b3453bbfd288b28 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 27 Apr 2022 01:23:35 +0000 Subject: [PATCH 23/42] Fix cancel tests --- .../UsersPage/CreateUserPage/CreateUserPage.test.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx index 24853b9ebccf5..c03fb6915bfe8 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx @@ -75,14 +75,16 @@ describe("Create User Page", () => { expect(successMessage).toBeDefined() expect(history.location.pathname).toEqual("/users") }) - it("redirects to users page on cancel", () => { + it("redirects to users page on cancel", async () => { render() - screen.findByText(FooterLanguage.cancelLabel) + const cancelButton = await screen.findByText(FooterLanguage.cancelLabel) + act(() => cancelButton.click()) expect(history.location.pathname).toEqual("/users") }) - it("redirects to users page on close", () => { + it("redirects to users page on close", async () => { render() - screen.findByText("ESC") + const closeButton = await screen.findByText("ESC") + act(() => closeButton.click()) expect(history.location.pathname).toEqual("/users") }) }) From 470481c2b6a6b078c85a5ceb2a1665ff69c806f2 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 27 Apr 2022 01:29:51 +0000 Subject: [PATCH 24/42] Switch back to mock --- site/src/api/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/site/src/api/index.ts b/site/src/api/index.ts index 9ed9fa1852f79..b190d06726e18 100644 --- a/site/src/api/index.ts +++ b/site/src/api/index.ts @@ -86,8 +86,9 @@ export const getUsers = async (): Promise => { } export const createUser = async (user: TypesGen.CreateUserRequest): Promise => { - const response = await axios.post("/api/v2/users", user) - return response.data + // const response = await axios.post("/api/v2/users", user) + // return response.data + return Promise.resolve(MockUser) } export const getBuildInfo = async (): Promise => { From 6a490c2ee64f69a7d74befc5f4fc204a26f34ef5 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 27 Apr 2022 01:30:06 +0000 Subject: [PATCH 25/42] Add navigate to xservice Doesn't work in test --- site/src/xServices/StateContext.tsx | 4 +++- site/src/xServices/users/usersXService.ts | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index ecd16739224f8..4a9bf20991f1b 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -1,5 +1,6 @@ import { useInterpret } from "@xstate/react" import React, { createContext } from "react" +import { useNavigate } from "react-router" import { ActorRefFrom } from "xstate" import { authMachine } from "./auth/authXService" import { buildInfoMachine } from "./buildInfo/buildInfoXService" @@ -22,12 +23,13 @@ interface XServiceContextType { export const XServiceContext = createContext({} as XServiceContextType) export const XServiceProvider: React.FC = ({ children }) => { + const navigate = useNavigate() return ( {children} diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index e17619617ce47..71c5129aff665 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -1,3 +1,4 @@ +import { NavigateFunction } from "react-router" import { assign, createMachine } from "xstate" import * as API from "../../api" import * as Types from "../../api/types" @@ -13,6 +14,7 @@ export interface UsersContext { pager?: Types.Pager getUsersError?: Error | unknown createUserError?: Error | unknown + navigate?: NavigateFunction } export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE"; user: TypesGen.CreateUserRequest } @@ -69,7 +71,7 @@ export const usersMachine = createMachine( id: "createUser", onDone: { target: "idle", - actions: ["displayCreateUserSuccess", "clearCreateUserError"], + actions: ["displayCreateUserSuccess", "redirectToUsersPage", "clearCreateUserError"], }, onError: { target: "idle", @@ -112,6 +114,9 @@ export const usersMachine = createMachine( displayCreateUserSuccess: () => { displaySuccess(Language.createUserSuccess) }, + redirectToUsersPage: (context) => { + context.navigate && context.navigate("/users") + } }, }, ) From 7d98c131755286e6ec785a520d9765c811c48152 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 27 Apr 2022 02:43:33 +0000 Subject: [PATCH 26/42] Move error type predicate to xservice --- .../CreateUserPage/CreateUserPage.tsx | 12 ++++----- site/src/xServices/users/usersXService.ts | 26 +++++++++++++++---- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index 8b341aabdd48b..da4febd6df79e 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -1,7 +1,6 @@ import { useActor } from "@xstate/react" import React, { useContext } from "react" import { useNavigate } from "react-router" -import { isApiError, mapApiErrorToFieldErrors } from "../../../api/errors" import { CreateUserRequest } from "../../../api/typesGenerated" import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" import { XServiceContext } from "../../../xServices/StateContext" @@ -13,19 +12,18 @@ export const Language = { export const CreateUserPage = () => { const xServices = useContext(XServiceContext) const [usersState, usersSend] = useActor(xServices.usersXService) - const { createUserError } = usersState.context - const apiError = isApiError(createUserError) - const formErrors = apiError ? mapApiErrorToFieldErrors(createUserError.response.data) : undefined - const hasUnknownError = createUserError && !apiError + const { createUserError, createUserFormErrors } = usersState.context const navigate = useNavigate() + // There is no field for organization id in Community Edition, so handle its field error like a generic error + const genericError = (createUserError || createUserFormErrors?.organization_id) ? Language.unknownError : undefined return ( usersSend({ type: "CREATE", user })} onCancel={() => navigate("/users")} isLoading={usersState.hasTag("loading")} - error={hasUnknownError ? Language.unknownError : undefined} + error={genericError} /> ) } diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 71c5129aff665..7624250c19b44 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -1,6 +1,7 @@ import { NavigateFunction } from "react-router" import { assign, createMachine } from "xstate" import * as API from "../../api" +import { ApiError, FieldErrors, isApiError, mapApiErrorToFieldErrors } from "../../api/errors" import * as Types from "../../api/types" import * as TypesGen from "../../api/typesGenerated" import { displaySuccess } from "../../components/GlobalSnackbar/utils" @@ -14,6 +15,7 @@ export interface UsersContext { pager?: Types.Pager getUsersError?: Error | unknown createUserError?: Error | unknown + createUserFormErrors?: FieldErrors navigate?: NavigateFunction } @@ -73,10 +75,17 @@ export const usersMachine = createMachine( target: "idle", actions: ["displayCreateUserSuccess", "redirectToUsersPage", "clearCreateUserError"], }, - onError: { - target: "idle", - actions: ["assignCreateUserError"], - }, + onError: [ + { + target: "idle", + cond: "isFormError", + actions: ["assignCreateUserFormErrors"], + }, + { + target: "idle", + actions: ["assignCreateUserError"], + }, + ], }, tags: "loading", }, @@ -92,6 +101,9 @@ export const usersMachine = createMachine( getUsers: API.getUsers, createUser: (_, event) => API.createUser(event.user), }, + guards: { + isFormError: (_, event) => isApiError(event.data) + }, actions: { assignUsers: assign({ users: (_, event) => event.data.page, @@ -107,6 +119,10 @@ export const usersMachine = createMachine( assignCreateUserError: assign({ createUserError: (_, event) => event.data, }), + assignCreateUserFormErrors: assign({ + // the guard ensures it is ApiError + createUserFormErrors: (_, event) => mapApiErrorToFieldErrors((event.data as ApiError).response.data) + }), clearCreateUserError: assign((context: UsersContext) => ({ ...context, createUserError: undefined, @@ -116,7 +132,7 @@ export const usersMachine = createMachine( }, redirectToUsersPage: (context) => { context.navigate && context.navigate("/users") - } + }, }, }, ) From 30b8799bc237b97aa42461fc7a066e10127e2ae1 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 27 Apr 2022 02:44:02 +0000 Subject: [PATCH 27/42] Lint --- site/src/api/errors.ts | 2 +- .../CreateUserPage/CreateUserPage.test.tsx | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/site/src/api/errors.ts b/site/src/api/errors.ts index c05a0b2da5f22..80c93dfe6dada 100644 --- a/site/src/api/errors.ts +++ b/site/src/api/errors.ts @@ -11,7 +11,7 @@ interface FieldError { detail: string } -type FieldErrors = Record +export type FieldErrors = Record export interface ApiErrorResponse { message: string diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx index c03fb6915bfe8..d973932cf609f 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx @@ -4,9 +4,9 @@ import { rest } from "msw" import React from "react" import { Language as FormLanguage } from "../../../components/CreateUserForm/CreateUserForm" import { Language as FooterLanguage } from "../../../components/FormFooter/FormFooter" -import { Language as UserLanguage } from "../../../xServices/users/usersXService" import { history, render } from "../../../testHelpers" import { server } from "../../../testHelpers/server" +import { Language as UserLanguage } from "../../../xServices/users/usersXService" import { CreateUserPage, Language } from "./CreateUserPage" const fillForm = async ({ @@ -54,13 +54,18 @@ describe("Create User Page", () => { const fieldErrorMessage = "username already in use" server.use( rest.post("/api/v2/users", (req, res, ctx) => { - return res(ctx.status(400), ctx.json({ - message: "invalid field", - errors: [{ - detail: fieldErrorMessage, - field: "username" - }] - })) + return res( + ctx.status(400), + ctx.json({ + message: "invalid field", + errors: [ + { + detail: fieldErrorMessage, + field: "username", + }, + ], + }), + ) }), ) render() From cf8442fa4b150a3d61389a7e71c45e1072040a44 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 27 Apr 2022 18:18:07 +0000 Subject: [PATCH 28/42] Switch to using creation mode in XState still problems in tests --- .../CreateUserPage/CreateUserPage.tsx | 16 +++-- site/src/pages/UsersPage/UsersPage.tsx | 8 ++- site/src/testHelpers/index.tsx | 1 + site/src/xServices/StateContext.tsx | 4 +- site/src/xServices/users/usersXService.ts | 65 +++++++++++-------- 5 files changed, 54 insertions(+), 40 deletions(-) diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index da4febd6df79e..89e3f21e46942 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -1,6 +1,6 @@ import { useActor } from "@xstate/react" import React, { useContext } from "react" -import { useNavigate } from "react-router" +import { Navigate } from "react-router" import { CreateUserRequest } from "../../../api/typesGenerated" import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" import { XServiceContext } from "../../../xServices/StateContext" @@ -13,17 +13,19 @@ export const CreateUserPage = () => { const xServices = useContext(XServiceContext) const [usersState, usersSend] = useActor(xServices.usersXService) const { createUserError, createUserFormErrors } = usersState.context - const navigate = useNavigate() // There is no field for organization id in Community Edition, so handle its field error like a generic error - const genericError = (createUserError || createUserFormErrors?.organization_id) ? Language.unknownError : undefined + const genericError = createUserError || createUserFormErrors?.organization_id ? Language.unknownError : undefined - return ( - usersSend({ type: "CREATE", user })} - onCancel={() => navigate("/users")} + onCancel={() => {usersSend("EXIT_CREATION_MODE")}} isLoading={usersState.hasTag("loading")} error={genericError} /> - ) + } else { + // on cancel or success, redirect + return + } } diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 699a4322343ce..0efc19be1227e 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -1,6 +1,6 @@ import { useActor } from "@xstate/react" import React, { useContext, useEffect } from "react" -import { useNavigate } from "react-router" +import { Navigate } from "react-router" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { XServiceContext } from "../../xServices/StateContext" import { UsersPageView } from "./UsersPageView" @@ -9,7 +9,6 @@ export const UsersPage: React.FC = () => { const xServices = useContext(XServiceContext) const [usersState, usersSend] = useActor(xServices.usersXService) const { users, pager, getUsersError } = usersState.context - const navigate = useNavigate() /** * Fetch users on component mount @@ -20,13 +19,16 @@ export const UsersPage: React.FC = () => { if (usersState.matches("error")) { return + } else if (usersState.matches("creationMode")) { + console.log("loop") + return } else { return ( { - navigate("/users/create") + usersSend("ENTER_CREATION_MODE") }} /> ) diff --git a/site/src/testHelpers/index.tsx b/site/src/testHelpers/index.tsx index 873c9687dc2fb..527206f5a9ff0 100644 --- a/site/src/testHelpers/index.tsx +++ b/site/src/testHelpers/index.tsx @@ -3,6 +3,7 @@ import { render as wrappedRender, RenderResult } from "@testing-library/react" import { createMemoryHistory } from "history" import React from "react" import { MemoryRouter, Route, Routes, unstable_HistoryRouter as HistoryRouter } from "react-router-dom" +import { AppRouter } from "../AppRouter" import { RequireAuth } from "../components/RequireAuth/RequireAuth" import { dark } from "../theme" import { XServiceProvider } from "../xServices/StateContext" diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index 4a9bf20991f1b..aa0fad0dc07e9 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -1,6 +1,5 @@ import { useInterpret } from "@xstate/react" import React, { createContext } from "react" -import { useNavigate } from "react-router" import { ActorRefFrom } from "xstate" import { authMachine } from "./auth/authXService" import { buildInfoMachine } from "./buildInfo/buildInfoXService" @@ -23,13 +22,12 @@ interface XServiceContextType { export const XServiceContext = createContext({} as XServiceContextType) export const XServiceProvider: React.FC = ({ children }) => { - const navigate = useNavigate() return ( {children} diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 7624250c19b44..017d955a14fb1 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -1,4 +1,3 @@ -import { NavigateFunction } from "react-router" import { assign, createMachine } from "xstate" import * as API from "../../api" import { ApiError, FieldErrors, isApiError, mapApiErrorToFieldErrors } from "../../api/errors" @@ -16,10 +15,13 @@ export interface UsersContext { getUsersError?: Error | unknown createUserError?: Error | unknown createUserFormErrors?: FieldErrors - navigate?: NavigateFunction } -export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE"; user: TypesGen.CreateUserRequest } +export type UsersEvent = + | { type: "GET_USERS" } + | { type: "ENTER_CREATION_MODE" } + | { type: "EXIT_CREATION_MODE" } + | { type: "CREATE"; user: TypesGen.CreateUserRequest } export const usersMachine = createMachine( { @@ -45,7 +47,7 @@ export const usersMachine = createMachine( idle: { on: { GET_USERS: "gettingUsers", - CREATE: "creatingUser", + ENTER_CREATION_MODE: "creationMode", }, }, gettingUsers: { @@ -67,28 +69,40 @@ export const usersMachine = createMachine( }, tags: "loading", }, - creatingUser: { - invoke: { - src: "createUser", - id: "createUser", - onDone: { - target: "idle", - actions: ["displayCreateUserSuccess", "redirectToUsersPage", "clearCreateUserError"], - }, - onError: [ - { - target: "idle", - cond: "isFormError", - actions: ["assignCreateUserFormErrors"], + creationMode: { + initial: "idle", + states: { + idle: { + on: { + CREATE: "creatingUser", + EXIT_CREATION_MODE: "#usersState.idle" }, - { - target: "idle", - actions: ["assignCreateUserError"], + }, + creatingUser: { + invoke: { + src: "createUser", + id: "createUser", + onDone: { + target: "#usersState.idle", + actions: ["displayCreateUserSuccess", "clearCreateUserError"], + }, + onError: [ + { + target: "idle", + cond: "isFormError", + actions: ["assignCreateUserFormErrors"], + }, + { + target: "idle", + actions: ["assignCreateUserError"], + }, + ], }, - ], + tags: "loading", + }, }, - tags: "loading", }, + error: { on: { GET_USERS: "gettingUsers", @@ -102,7 +116,7 @@ export const usersMachine = createMachine( createUser: (_, event) => API.createUser(event.user), }, guards: { - isFormError: (_, event) => isApiError(event.data) + isFormError: (_, event) => isApiError(event.data), }, actions: { assignUsers: assign({ @@ -121,7 +135,7 @@ export const usersMachine = createMachine( }), assignCreateUserFormErrors: assign({ // the guard ensures it is ApiError - createUserFormErrors: (_, event) => mapApiErrorToFieldErrors((event.data as ApiError).response.data) + createUserFormErrors: (_, event) => mapApiErrorToFieldErrors((event.data as ApiError).response.data), }), clearCreateUserError: assign((context: UsersContext) => ({ ...context, @@ -130,9 +144,6 @@ export const usersMachine = createMachine( displayCreateUserSuccess: () => { displaySuccess(Language.createUserSuccess) }, - redirectToUsersPage: (context) => { - context.navigate && context.navigate("/users") - }, }, }, ) From 642ca2240b02b4ce6aceaeab3b29ce2d2190e371 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 27 Apr 2022 18:26:29 +0000 Subject: [PATCH 29/42] Lint --- .../CreateUserPage/CreateUserPage.tsx | 22 +++++++++++-------- site/src/pages/UsersPage/UsersPage.tsx | 1 - site/src/testHelpers/index.tsx | 1 - site/src/xServices/StateContext.tsx | 2 +- site/src/xServices/users/usersXService.ts | 2 +- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index 89e3f21e46942..e8c214df9beff 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -16,16 +16,20 @@ export const CreateUserPage = () => { // There is no field for organization id in Community Edition, so handle its field error like a generic error const genericError = createUserError || createUserFormErrors?.organization_id ? Language.unknownError : undefined - if (usersState.matches("creationMode")){ - return usersSend({ type: "CREATE", user })} - onCancel={() => {usersSend("EXIT_CREATION_MODE")}} - isLoading={usersState.hasTag("loading")} - error={genericError} - /> + if (usersState.matches("creationMode")) { + return ( + usersSend({ type: "CREATE", user })} + onCancel={() => { + usersSend("EXIT_CREATION_MODE") + }} + isLoading={usersState.hasTag("loading")} + error={genericError} + /> + ) } else { // on cancel or success, redirect - return + return } } diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 0efc19be1227e..324eaa54a9e9b 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -20,7 +20,6 @@ export const UsersPage: React.FC = () => { if (usersState.matches("error")) { return } else if (usersState.matches("creationMode")) { - console.log("loop") return } else { return ( diff --git a/site/src/testHelpers/index.tsx b/site/src/testHelpers/index.tsx index 527206f5a9ff0..873c9687dc2fb 100644 --- a/site/src/testHelpers/index.tsx +++ b/site/src/testHelpers/index.tsx @@ -3,7 +3,6 @@ import { render as wrappedRender, RenderResult } from "@testing-library/react" import { createMemoryHistory } from "history" import React from "react" import { MemoryRouter, Route, Routes, unstable_HistoryRouter as HistoryRouter } from "react-router-dom" -import { AppRouter } from "../AppRouter" import { RequireAuth } from "../components/RequireAuth/RequireAuth" import { dark } from "../theme" import { XServiceProvider } from "../xServices/StateContext" diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index aa0fad0dc07e9..ecd16739224f8 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -27,7 +27,7 @@ export const XServiceProvider: React.FC = ({ children }) => { value={{ authXService: useInterpret(authMachine), buildInfoXService: useInterpret(buildInfoMachine), - usersXService: useInterpret(usersMachine) + usersXService: useInterpret(usersMachine), }} > {children} diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 017d955a14fb1..af306eb116eaa 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -75,7 +75,7 @@ export const usersMachine = createMachine( idle: { on: { CREATE: "creatingUser", - EXIT_CREATION_MODE: "#usersState.idle" + EXIT_CREATION_MODE: "#usersState.idle", }, }, creatingUser: { From a0717f8eb3ccc22ac192498c80df70ed51602e3a Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 27 Apr 2022 22:41:08 +0000 Subject: [PATCH 30/42] Lint --- site/src/api/types.ts | 1 + site/src/pages/UsersPage/UsersPage.tsx | 2 +- site/src/testHelpers/entities.ts | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/site/src/api/types.ts b/site/src/api/types.ts index c303ffc9d4149..09d3eda948a7a 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -15,6 +15,7 @@ export interface UserResponse { readonly username: string readonly email: string readonly created_at: string + readonly status: "active" | "suspended" } /** diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 324eaa54a9e9b..3da172e165701 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -15,7 +15,7 @@ export const UsersPage: React.FC = () => { */ useEffect(() => { usersSend("GET_USERS") - }, []) + }, [usersSend]) if (usersState.matches("error")) { return diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index ffdd7418f2287..69b52300a8f14 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -25,6 +25,7 @@ export const MockUser: UserResponse = { username: "TestUser", email: "test@coder.com", created_at: "", + status: "active" } export const MockUser2: UserResponse = { @@ -32,6 +33,7 @@ export const MockUser2: UserResponse = { username: "TestUser2", email: "test2@coder.com", created_at: "", + status: "active" } export const MockPager: Pager = { From 9941c1cfdde3e435938c78c37f6773c8574cbf45 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 27 Apr 2022 22:44:07 +0000 Subject: [PATCH 31/42] Lint --- .../pages/PreferencesPages/AccountPage/AccountPage.test.tsx | 1 + .../pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx | 2 +- site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx | 2 +- site/src/testHelpers/entities.ts | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/site/src/pages/PreferencesPages/AccountPage/AccountPage.test.tsx b/site/src/pages/PreferencesPages/AccountPage/AccountPage.test.tsx index 038a12f7b3050..9038d54a1fd04 100644 --- a/site/src/pages/PreferencesPages/AccountPage/AccountPage.test.tsx +++ b/site/src/pages/PreferencesPages/AccountPage/AccountPage.test.tsx @@ -36,6 +36,7 @@ describe("AccountPage", () => { id: userId, ...data, created_at: new Date().toString(), + status: "active" }), ) const { user } = renderPage() diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx index d973932cf609f..d3cbcc2544ea5 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx @@ -41,7 +41,7 @@ describe("Create User Page", () => { }) it("shows generic error message", async () => { server.use( - rest.post("/api/v2/users", (req, res, ctx) => { + rest.post("/api/v2/users", () => { Promise.reject("something went wrong") }), ) diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index e8c214df9beff..31565d442de88 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -9,7 +9,7 @@ export const Language = { unknownError: "Oops, an unknown error occurred.", } -export const CreateUserPage = () => { +export const CreateUserPage: React.FC = () => { const xServices = useContext(XServiceContext) const [usersState, usersSend] = useActor(xServices.usersXService) const { createUserError, createUserFormErrors } = usersState.context diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 69b52300a8f14..279b4a665c96e 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -25,7 +25,7 @@ export const MockUser: UserResponse = { username: "TestUser", email: "test@coder.com", created_at: "", - status: "active" + status: "active", } export const MockUser2: UserResponse = { @@ -33,7 +33,7 @@ export const MockUser2: UserResponse = { username: "TestUser2", email: "test2@coder.com", created_at: "", - status: "active" + status: "active", } export const MockPager: Pager = { From 8ca5922c3050703b34dc062498b3b2f0feb1c7c9 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 27 Apr 2022 23:15:18 +0000 Subject: [PATCH 32/42] Revert "Switch to using creation mode in XState" This reverts commit cf8442fa4b150a3d61389a7e71c45e1072040a44. --- .../CreateUserPage/CreateUserPage.tsx | 30 ++++----- site/src/pages/UsersPage/UsersPage.tsx | 7 +- site/src/xServices/StateContext.tsx | 4 +- site/src/xServices/users/usersXService.ts | 65 ++++++++----------- 4 files changed, 45 insertions(+), 61 deletions(-) diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index 31565d442de88..7aebc60bd76d7 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -1,6 +1,6 @@ import { useActor } from "@xstate/react" import React, { useContext } from "react" -import { Navigate } from "react-router" +import { useNavigate } from "react-router" import { CreateUserRequest } from "../../../api/typesGenerated" import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" import { XServiceContext } from "../../../xServices/StateContext" @@ -13,23 +13,17 @@ export const CreateUserPage: React.FC = () => { const xServices = useContext(XServiceContext) const [usersState, usersSend] = useActor(xServices.usersXService) const { createUserError, createUserFormErrors } = usersState.context + const navigate = useNavigate() // There is no field for organization id in Community Edition, so handle its field error like a generic error - const genericError = createUserError || createUserFormErrors?.organization_id ? Language.unknownError : undefined + const genericError = (createUserError || createUserFormErrors?.organization_id) ? Language.unknownError : undefined - if (usersState.matches("creationMode")) { - return ( - usersSend({ type: "CREATE", user })} - onCancel={() => { - usersSend("EXIT_CREATION_MODE") - }} - isLoading={usersState.hasTag("loading")} - error={genericError} - /> - ) - } else { - // on cancel or success, redirect - return - } + return ( + usersSend({ type: "CREATE", user })} + onCancel={() => navigate("/users")} + isLoading={usersState.hasTag("loading")} + error={genericError} + /> + ) } diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 3da172e165701..b54422d5bfa1e 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -1,6 +1,6 @@ import { useActor } from "@xstate/react" import React, { useContext, useEffect } from "react" -import { Navigate } from "react-router" +import { useNavigate } from "react-router" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { XServiceContext } from "../../xServices/StateContext" import { UsersPageView } from "./UsersPageView" @@ -9,6 +9,7 @@ export const UsersPage: React.FC = () => { const xServices = useContext(XServiceContext) const [usersState, usersSend] = useActor(xServices.usersXService) const { users, pager, getUsersError } = usersState.context + const navigate = useNavigate() /** * Fetch users on component mount @@ -19,15 +20,13 @@ export const UsersPage: React.FC = () => { if (usersState.matches("error")) { return - } else if (usersState.matches("creationMode")) { - return } else { return ( { - usersSend("ENTER_CREATION_MODE") + navigate("/users/create") }} /> ) diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index ecd16739224f8..4a9bf20991f1b 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -1,5 +1,6 @@ import { useInterpret } from "@xstate/react" import React, { createContext } from "react" +import { useNavigate } from "react-router" import { ActorRefFrom } from "xstate" import { authMachine } from "./auth/authXService" import { buildInfoMachine } from "./buildInfo/buildInfoXService" @@ -22,12 +23,13 @@ interface XServiceContextType { export const XServiceContext = createContext({} as XServiceContextType) export const XServiceProvider: React.FC = ({ children }) => { + const navigate = useNavigate() return ( {children} diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index af306eb116eaa..7624250c19b44 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -1,3 +1,4 @@ +import { NavigateFunction } from "react-router" import { assign, createMachine } from "xstate" import * as API from "../../api" import { ApiError, FieldErrors, isApiError, mapApiErrorToFieldErrors } from "../../api/errors" @@ -15,13 +16,10 @@ export interface UsersContext { getUsersError?: Error | unknown createUserError?: Error | unknown createUserFormErrors?: FieldErrors + navigate?: NavigateFunction } -export type UsersEvent = - | { type: "GET_USERS" } - | { type: "ENTER_CREATION_MODE" } - | { type: "EXIT_CREATION_MODE" } - | { type: "CREATE"; user: TypesGen.CreateUserRequest } +export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE"; user: TypesGen.CreateUserRequest } export const usersMachine = createMachine( { @@ -47,7 +45,7 @@ export const usersMachine = createMachine( idle: { on: { GET_USERS: "gettingUsers", - ENTER_CREATION_MODE: "creationMode", + CREATE: "creatingUser", }, }, gettingUsers: { @@ -69,40 +67,28 @@ export const usersMachine = createMachine( }, tags: "loading", }, - creationMode: { - initial: "idle", - states: { - idle: { - on: { - CREATE: "creatingUser", - EXIT_CREATION_MODE: "#usersState.idle", - }, + creatingUser: { + invoke: { + src: "createUser", + id: "createUser", + onDone: { + target: "idle", + actions: ["displayCreateUserSuccess", "redirectToUsersPage", "clearCreateUserError"], }, - creatingUser: { - invoke: { - src: "createUser", - id: "createUser", - onDone: { - target: "#usersState.idle", - actions: ["displayCreateUserSuccess", "clearCreateUserError"], - }, - onError: [ - { - target: "idle", - cond: "isFormError", - actions: ["assignCreateUserFormErrors"], - }, - { - target: "idle", - actions: ["assignCreateUserError"], - }, - ], + onError: [ + { + target: "idle", + cond: "isFormError", + actions: ["assignCreateUserFormErrors"], }, - tags: "loading", - }, + { + target: "idle", + actions: ["assignCreateUserError"], + }, + ], }, + tags: "loading", }, - error: { on: { GET_USERS: "gettingUsers", @@ -116,7 +102,7 @@ export const usersMachine = createMachine( createUser: (_, event) => API.createUser(event.user), }, guards: { - isFormError: (_, event) => isApiError(event.data), + isFormError: (_, event) => isApiError(event.data) }, actions: { assignUsers: assign({ @@ -135,7 +121,7 @@ export const usersMachine = createMachine( }), assignCreateUserFormErrors: assign({ // the guard ensures it is ApiError - createUserFormErrors: (_, event) => mapApiErrorToFieldErrors((event.data as ApiError).response.data), + createUserFormErrors: (_, event) => mapApiErrorToFieldErrors((event.data as ApiError).response.data) }), clearCreateUserError: assign((context: UsersContext) => ({ ...context, @@ -144,6 +130,9 @@ export const usersMachine = createMachine( displayCreateUserSuccess: () => { displaySuccess(Language.createUserSuccess) }, + redirectToUsersPage: (context) => { + context.navigate && context.navigate("/users") + }, }, }, ) From 01522cc3c38f054dea5a8e9369d201a96c1a29e1 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 27 Apr 2022 23:15:42 +0000 Subject: [PATCH 33/42] Give XService a navigate action --- .../pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx | 1 - site/src/xServices/StateContext.tsx | 4 +++- site/src/xServices/users/usersXService.ts | 4 ---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx index d3cbcc2544ea5..5e992ea3e5e9a 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx @@ -78,7 +78,6 @@ describe("Create User Page", () => { await fillForm({}) const successMessage = screen.findByText(UserLanguage.createUserSuccess) expect(successMessage).toBeDefined() - expect(history.location.pathname).toEqual("/users") }) it("redirects to users page on cancel", async () => { render() diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index 4a9bf20991f1b..9881cd8fe5139 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -24,12 +24,14 @@ export const XServiceContext = createContext({} as XServiceContextType) export const XServiceProvider: React.FC = ({ children }) => { const navigate = useNavigate() + const redirectToUsersPage = () => { navigate("users") } + return ( {children} diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 7624250c19b44..130d0313a0a80 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -16,7 +16,6 @@ export interface UsersContext { getUsersError?: Error | unknown createUserError?: Error | unknown createUserFormErrors?: FieldErrors - navigate?: NavigateFunction } export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE"; user: TypesGen.CreateUserRequest } @@ -130,9 +129,6 @@ export const usersMachine = createMachine( displayCreateUserSuccess: () => { displaySuccess(Language.createUserSuccess) }, - redirectToUsersPage: (context) => { - context.navigate && context.navigate("/users") - }, }, }, ) From fa75015f54cbff03dc0459d48e1f1f500da7e133 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 28 Apr 2022 00:40:18 +0000 Subject: [PATCH 34/42] Add missing validation messages --- site/src/components/CreateUserForm/CreateUserForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx index 22ed15b9c392d..67af91aefa869 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.tsx @@ -30,8 +30,8 @@ export interface CreateUserFormProps { const validationSchema = Yup.object({ email: Yup.string().trim().email(Language.emailInvalid).required(Language.emailRequired), - password: Yup.string().required(), - username: Yup.string().required(), + password: Yup.string().required(Language.passwordRequired), + username: Yup.string().required(Language.usernameRequired), }) export const CreateUserForm: React.FC = ({ onSubmit, onCancel, formErrors, isLoading, error }) => { From c3bb6ffb890c532fabb5936be5c16680a1fba5d1 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 28 Apr 2022 00:41:04 +0000 Subject: [PATCH 35/42] Fix XState warning --- site/src/xServices/StateContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index 9881cd8fe5139..f0e11b3fade40 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -31,7 +31,7 @@ export const XServiceProvider: React.FC = ({ children }) => { value={{ authXService: useInterpret(authMachine), buildInfoXService: useInterpret(buildInfoMachine), - usersXService: useInterpret(usersMachine.withConfig({ actions: { redirectToUsersPage } })), + usersXService: useInterpret(() => usersMachine.withConfig({ actions: { redirectToUsersPage } })), }} > {children} From 62dad5e8984906c11775bf5c2ca5c94248000fa3 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 28 Apr 2022 00:52:27 +0000 Subject: [PATCH 36/42] Fix tests IRL is broken bc I need to send org id --- site/src/api/index.ts | 5 ++-- .../CreateUserForm/CreateUserForm.stories.tsx | 2 +- .../CreateUserPage/CreateUserPage.test.tsx | 23 +++++++++---------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/site/src/api/index.ts b/site/src/api/index.ts index b190d06726e18..9ed9fa1852f79 100644 --- a/site/src/api/index.ts +++ b/site/src/api/index.ts @@ -86,9 +86,8 @@ export const getUsers = async (): Promise => { } export const createUser = async (user: TypesGen.CreateUserRequest): Promise => { - // const response = await axios.post("/api/v2/users", user) - // return response.data - return Promise.resolve(MockUser) + const response = await axios.post("/api/v2/users", user) + return response.data } export const getBuildInfo = async (): Promise => { diff --git a/site/src/components/CreateUserForm/CreateUserForm.stories.tsx b/site/src/components/CreateUserForm/CreateUserForm.stories.tsx index 1f6d99c173fdc..1ae62559554e0 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.stories.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.stories.tsx @@ -31,7 +31,7 @@ FormError.args = { onSubmit: action("submit"), isLoading: false, formErrors: { - email: "Bad email address", + username: "Username taken", }, } diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx index 5e992ea3e5e9a..cb4fccbd54a17 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx @@ -1,4 +1,4 @@ -import { act, screen } from "@testing-library/react" +import { screen } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { rest } from "msw" import React from "react" @@ -8,10 +8,11 @@ import { history, render } from "../../../testHelpers" import { server } from "../../../testHelpers/server" import { Language as UserLanguage } from "../../../xServices/users/usersXService" import { CreateUserPage, Language } from "./CreateUserPage" +import * as API from "../../../api" const fillForm = async ({ - username = "testuser", - email = "test@coder.com", + username = "someuser", + email = "someone@coder.com", password = "password", }: { username?: string @@ -25,7 +26,7 @@ const fillForm = async ({ await userEvent.type(emailField, email) await userEvent.type(passwordField, password) const submitButton = await screen.findByText(FooterLanguage.defaultSubmitLabel) - act(() => submitButton.click()) + submitButton.click() } describe("Create User Page", () => { @@ -40,11 +41,9 @@ describe("Create User Page", () => { expect(errorMessage).toBeDefined() }) it("shows generic error message", async () => { - server.use( - rest.post("/api/v2/users", () => { - Promise.reject("something went wrong") - }), - ) + jest.spyOn(API, "createUser").mockRejectedValueOnce({ + data: "unknown error", + }) render() await fillForm({}) const errorMessage = await screen.findByText(Language.unknownError) @@ -53,7 +52,7 @@ describe("Create User Page", () => { it("shows API error message", async () => { const fieldErrorMessage = "username already in use" server.use( - rest.post("/api/v2/users", (req, res, ctx) => { + rest.post("/api/v2/users", async (req, res, ctx) => { return res( ctx.status(400), ctx.json({ @@ -82,13 +81,13 @@ describe("Create User Page", () => { it("redirects to users page on cancel", async () => { render() const cancelButton = await screen.findByText(FooterLanguage.cancelLabel) - act(() => cancelButton.click()) + cancelButton.click() expect(history.location.pathname).toEqual("/users") }) it("redirects to users page on close", async () => { render() const closeButton = await screen.findByText("ESC") - act(() => closeButton.click()) + closeButton.click() expect(history.location.pathname).toEqual("/users") }) }) From 797bb34adc0d15d8fef71d2d415f451d902b11a9 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 28 Apr 2022 01:19:05 +0000 Subject: [PATCH 37/42] Pretend user has org id and make it work --- site/src/api/index.ts | 2 +- site/src/api/types.ts | 8 ++++++++ site/src/components/CreateUserForm/CreateUserForm.tsx | 6 ++++-- .../PreferencesPages/AccountPage/AccountPage.test.tsx | 5 +++-- .../pages/UsersPage/CreateUserPage/CreateUserPage.tsx | 9 ++++++--- site/src/testHelpers/entities.ts | 2 ++ site/src/xServices/auth/authSelectors.ts | 6 ++++++ site/src/xServices/users/usersXService.ts | 3 +-- 8 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 site/src/xServices/auth/authSelectors.ts diff --git a/site/src/api/index.ts b/site/src/api/index.ts index 9ed9fa1852f79..aff8d5235c4be 100644 --- a/site/src/api/index.ts +++ b/site/src/api/index.ts @@ -85,7 +85,7 @@ export const getUsers = async (): Promise => { }) } -export const createUser = async (user: TypesGen.CreateUserRequest): Promise => { +export const createUser = async (user: Types.CreateUserRequest): Promise => { const response = await axios.post("/api/v2/users", user) return response.data } diff --git a/site/src/api/types.ts b/site/src/api/types.ts index 09d3eda948a7a..890433ea9d7f0 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -10,12 +10,20 @@ export interface LoginResponse { session_token: string } +export interface CreateUserRequest { + username: string + email: string + password: string + organization_id: string +} + export interface UserResponse { readonly id: string readonly username: string readonly email: string readonly created_at: string readonly status: "active" | "suspended" + readonly organization_id: string } /** diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx index 67af91aefa869..dd02a6af0ab3e 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.tsx @@ -3,7 +3,7 @@ import TextField from "@material-ui/core/TextField" import { FormikContextType, FormikErrors, useFormik } from "formik" import React from "react" import * as Yup from "yup" -import { CreateUserRequest } from "../../api/typesGenerated" +import { CreateUserRequest } from "../../api/types" import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" import { FormFooter } from "../FormFooter/FormFooter" import { FullPageForm } from "../FullPageForm/FullPageForm" @@ -26,6 +26,7 @@ export interface CreateUserFormProps { formErrors?: FormikErrors isLoading: boolean error?: string + myOrgId: string } const validationSchema = Yup.object({ @@ -34,12 +35,13 @@ const validationSchema = Yup.object({ username: Yup.string().required(Language.usernameRequired), }) -export const CreateUserForm: React.FC = ({ onSubmit, onCancel, formErrors, isLoading, error }) => { +export const CreateUserForm: React.FC = ({ onSubmit, onCancel, formErrors, isLoading, error, myOrgId }) => { const form: FormikContextType = useFormik({ initialValues: { email: "", password: "", username: "", + organization_id: myOrgId }, validationSchema, onSubmit, diff --git a/site/src/pages/PreferencesPages/AccountPage/AccountPage.test.tsx b/site/src/pages/PreferencesPages/AccountPage/AccountPage.test.tsx index 9038d54a1fd04..172e3efc95ec6 100644 --- a/site/src/pages/PreferencesPages/AccountPage/AccountPage.test.tsx +++ b/site/src/pages/PreferencesPages/AccountPage/AccountPage.test.tsx @@ -34,9 +34,10 @@ describe("AccountPage", () => { jest.spyOn(API, "updateProfile").mockImplementationOnce((userId, data) => Promise.resolve({ id: userId, - ...data, created_at: new Date().toString(), - status: "active" + status: "active", + organization_id: "123", + ...data, }), ) const { user } = renderPage() diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index 7aebc60bd76d7..c4e73f2262725 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -1,8 +1,9 @@ -import { useActor } from "@xstate/react" +import { useActor, useSelector } from "@xstate/react" import React, { useContext } from "react" import { useNavigate } from "react-router" -import { CreateUserRequest } from "../../../api/typesGenerated" +import { CreateUserRequest } from "../../../api/types" import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" +import { selectOrgId } from "../../../xServices/auth/authSelectors" import { XServiceContext } from "../../../xServices/StateContext" export const Language = { @@ -11,11 +12,12 @@ export const Language = { export const CreateUserPage: React.FC = () => { const xServices = useContext(XServiceContext) + const myOrgId = useSelector(xServices.authXService, selectOrgId) const [usersState, usersSend] = useActor(xServices.usersXService) const { createUserError, createUserFormErrors } = usersState.context const navigate = useNavigate() // There is no field for organization id in Community Edition, so handle its field error like a generic error - const genericError = (createUserError || createUserFormErrors?.organization_id) ? Language.unknownError : undefined + const genericError = (createUserError || createUserFormErrors?.organization_id || !myOrgId) ? Language.unknownError : undefined return ( { onCancel={() => navigate("/users")} isLoading={usersState.hasTag("loading")} error={genericError} + myOrgId={myOrgId ?? ""} /> ) } diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 279b4a665c96e..dc9d066b83411 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -26,6 +26,7 @@ export const MockUser: UserResponse = { email: "test@coder.com", created_at: "", status: "active", + organization_id: "123" } export const MockUser2: UserResponse = { @@ -34,6 +35,7 @@ export const MockUser2: UserResponse = { email: "test2@coder.com", created_at: "", status: "active", + organization_id: "123" } export const MockPager: Pager = { diff --git a/site/src/xServices/auth/authSelectors.ts b/site/src/xServices/auth/authSelectors.ts new file mode 100644 index 0000000000000..168e6ac6d2c2d --- /dev/null +++ b/site/src/xServices/auth/authSelectors.ts @@ -0,0 +1,6 @@ +import { State } from "xstate" +import { AuthContext, AuthEvent } from "./authXService" + +export const selectOrgId = (state: State) => { + return state.context.me?.organization_id +} diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 130d0313a0a80..eac293a7f1905 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -1,4 +1,3 @@ -import { NavigateFunction } from "react-router" import { assign, createMachine } from "xstate" import * as API from "../../api" import { ApiError, FieldErrors, isApiError, mapApiErrorToFieldErrors } from "../../api/errors" @@ -18,7 +17,7 @@ export interface UsersContext { createUserFormErrors?: FieldErrors } -export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE"; user: TypesGen.CreateUserRequest } +export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE"; user: Types.CreateUserRequest } export const usersMachine = createMachine( { From 6f6165b144688c2e408bf649f3c0b1513b92edfa Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 28 Apr 2022 01:19:28 +0000 Subject: [PATCH 38/42] Format --- site/src/components/CreateUserForm/CreateUserForm.tsx | 11 +++++++++-- .../UsersPage/CreateUserPage/CreateUserPage.test.tsx | 2 +- .../pages/UsersPage/CreateUserPage/CreateUserPage.tsx | 3 ++- site/src/testHelpers/entities.ts | 4 ++-- site/src/xServices/StateContext.tsx | 4 +++- site/src/xServices/users/usersXService.ts | 4 ++-- 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx index dd02a6af0ab3e..f95e7fd0c8757 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.tsx @@ -35,13 +35,20 @@ const validationSchema = Yup.object({ username: Yup.string().required(Language.usernameRequired), }) -export const CreateUserForm: React.FC = ({ onSubmit, onCancel, formErrors, isLoading, error, myOrgId }) => { +export const CreateUserForm: React.FC = ({ + onSubmit, + onCancel, + formErrors, + isLoading, + error, + myOrgId, +}) => { const form: FormikContextType = useFormik({ initialValues: { email: "", password: "", username: "", - organization_id: myOrgId + organization_id: myOrgId, }, validationSchema, onSubmit, diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx index cb4fccbd54a17..31b403d70eb5c 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx @@ -2,13 +2,13 @@ import { screen } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { rest } from "msw" import React from "react" +import * as API from "../../../api" import { Language as FormLanguage } from "../../../components/CreateUserForm/CreateUserForm" import { Language as FooterLanguage } from "../../../components/FormFooter/FormFooter" import { history, render } from "../../../testHelpers" import { server } from "../../../testHelpers/server" import { Language as UserLanguage } from "../../../xServices/users/usersXService" import { CreateUserPage, Language } from "./CreateUserPage" -import * as API from "../../../api" const fillForm = async ({ username = "someuser", diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx index c4e73f2262725..c8f36498c37a1 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx @@ -17,7 +17,8 @@ export const CreateUserPage: React.FC = () => { const { createUserError, createUserFormErrors } = usersState.context const navigate = useNavigate() // There is no field for organization id in Community Edition, so handle its field error like a generic error - const genericError = (createUserError || createUserFormErrors?.organization_id || !myOrgId) ? Language.unknownError : undefined + const genericError = + createUserError || createUserFormErrors?.organization_id || !myOrgId ? Language.unknownError : undefined return ( { const navigate = useNavigate() - const redirectToUsersPage = () => { navigate("users") } + const redirectToUsersPage = () => { + navigate("users") + } return ( API.createUser(event.user), }, guards: { - isFormError: (_, event) => isApiError(event.data) + isFormError: (_, event) => isApiError(event.data), }, actions: { assignUsers: assign({ @@ -119,7 +119,7 @@ export const usersMachine = createMachine( }), assignCreateUserFormErrors: assign({ // the guard ensures it is ApiError - createUserFormErrors: (_, event) => mapApiErrorToFieldErrors((event.data as ApiError).response.data) + createUserFormErrors: (_, event) => mapApiErrorToFieldErrors((event.data as ApiError).response.data), }), clearCreateUserError: assign((context: UsersContext) => ({ ...context, From 4c56afd688fba1ae7d45d6dc90709dad2024a942 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 28 Apr 2022 01:36:14 +0000 Subject: [PATCH 39/42] Lint --- site/src/xServices/auth/authSelectors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/xServices/auth/authSelectors.ts b/site/src/xServices/auth/authSelectors.ts index 168e6ac6d2c2d..2738472a65fee 100644 --- a/site/src/xServices/auth/authSelectors.ts +++ b/site/src/xServices/auth/authSelectors.ts @@ -1,6 +1,6 @@ import { State } from "xstate" import { AuthContext, AuthEvent } from "./authXService" -export const selectOrgId = (state: State) => { +export const selectOrgId = (state: State): string | undefined => { return state.context.me?.organization_id } From 9154a338290e67cefe365347f736ef0090eb2633 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 28 Apr 2022 15:36:19 +0000 Subject: [PATCH 40/42] Switch to org ids array --- site/src/api/types.ts | 2 +- .../pages/PreferencesPages/AccountPage/AccountPage.test.tsx | 2 +- site/src/testHelpers/entities.ts | 4 ++-- site/src/xServices/auth/authSelectors.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/site/src/api/types.ts b/site/src/api/types.ts index 890433ea9d7f0..24467756cb8c9 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -23,7 +23,7 @@ export interface UserResponse { readonly email: string readonly created_at: string readonly status: "active" | "suspended" - readonly organization_id: string + readonly organization_ids: string[] } /** diff --git a/site/src/pages/PreferencesPages/AccountPage/AccountPage.test.tsx b/site/src/pages/PreferencesPages/AccountPage/AccountPage.test.tsx index 172e3efc95ec6..180b0ac69df19 100644 --- a/site/src/pages/PreferencesPages/AccountPage/AccountPage.test.tsx +++ b/site/src/pages/PreferencesPages/AccountPage/AccountPage.test.tsx @@ -36,7 +36,7 @@ describe("AccountPage", () => { id: userId, created_at: new Date().toString(), status: "active", - organization_id: "123", + organization_ids: ["123"], ...data, }), ) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 60de05dd2682b..3163ed24c094f 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -26,7 +26,7 @@ export const MockUser: UserResponse = { email: "test@coder.com", created_at: "", status: "active", - organization_id: "123", + organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"], } export const MockUser2: UserResponse = { @@ -35,7 +35,7 @@ export const MockUser2: UserResponse = { email: "test2@coder.com", created_at: "", status: "active", - organization_id: "123", + organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"], } export const MockPager: Pager = { diff --git a/site/src/xServices/auth/authSelectors.ts b/site/src/xServices/auth/authSelectors.ts index 2738472a65fee..b57ac2ebfb522 100644 --- a/site/src/xServices/auth/authSelectors.ts +++ b/site/src/xServices/auth/authSelectors.ts @@ -2,5 +2,5 @@ import { State } from "xstate" import { AuthContext, AuthEvent } from "./authXService" export const selectOrgId = (state: State): string | undefined => { - return state.context.me?.organization_id + return state.context.me?.organization_ids[0] } From 983d7ff9a577fac3c5d72910e058b0dde314875f Mon Sep 17 00:00:00 2001 From: Presley Pizzo <1290996+presleyp@users.noreply.github.com> Date: Thu, 28 Apr 2022 11:56:05 -0400 Subject: [PATCH 41/42] Skip lines between tests Co-authored-by: G r e y --- .../pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx index 31b403d70eb5c..80c86367dd2e7 100644 --- a/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx +++ b/site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx @@ -40,6 +40,7 @@ describe("Create User Page", () => { const errorMessage = await screen.findByText(FormLanguage.emailInvalid) expect(errorMessage).toBeDefined() }) + it("shows generic error message", async () => { jest.spyOn(API, "createUser").mockRejectedValueOnce({ data: "unknown error", @@ -49,6 +50,7 @@ describe("Create User Page", () => { const errorMessage = await screen.findByText(Language.unknownError) expect(errorMessage).toBeDefined() }) + it("shows API error message", async () => { const fieldErrorMessage = "username already in use" server.use( @@ -72,18 +74,21 @@ describe("Create User Page", () => { const errorMessage = await screen.findByText(fieldErrorMessage) expect(errorMessage).toBeDefined() }) + it("shows success notification and redirects to users page", async () => { render() await fillForm({}) const successMessage = screen.findByText(UserLanguage.createUserSuccess) expect(successMessage).toBeDefined() }) + it("redirects to users page on cancel", async () => { render() const cancelButton = await screen.findByText(FooterLanguage.cancelLabel) cancelButton.click() expect(history.location.pathname).toEqual("/users") }) + it("redirects to users page on close", async () => { render() const closeButton = await screen.findByText("ESC") From 10c765c83d20bced4e01cb8f9ac28cdc8259f3ce Mon Sep 17 00:00:00 2001 From: Presley Pizzo <1290996+presleyp@users.noreply.github.com> Date: Thu, 28 Apr 2022 12:01:13 -0400 Subject: [PATCH 42/42] Punctuate notification messages --- site/src/xServices/users/usersXService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index ed56600b0b816..a2763ff76c670 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -6,7 +6,7 @@ import * as TypesGen from "../../api/typesGenerated" import { displaySuccess } from "../../components/GlobalSnackbar/utils" export const Language = { - createUserSuccess: "Successfully created user", + createUserSuccess: "Successfully created user.", } export interface UsersContext {