Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
refactor: Remove users redirect to active filter
  • Loading branch information
BrunoQuaresma committed Sep 14, 2022
commit 2d07d3e62fc7a1ccce76abed34cb71a1815e14fa
5 changes: 4 additions & 1 deletion site/src/components/NavbarView/NavbarView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ const NavItems: React.FC<
</NavLink>
</ListItem>
<ListItem button className={styles.item}>
<NavLink className={styles.link} to="/users">
<NavLink
className={styles.link}
to={`/users?filter=${encodeURIComponent("status:active")}`}
>
{Language.users}
</NavLink>
</ListItem>
Expand Down
43 changes: 19 additions & 24 deletions site/src/components/SearchBarWithFilter/SearchBarWithFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import OutlinedInput from "@material-ui/core/OutlinedInput"
import { makeStyles } from "@material-ui/core/styles"
import { Theme } from "@material-ui/core/styles/createMuiTheme"
import SearchIcon from "@material-ui/icons/Search"
import { FormikErrors, useFormik } from "formik"
import { FormikErrors } from "formik"
import debounce from "just-debounce-it"
import { useCallback, useEffect, useState } from "react"
import { useCallback, useRef, useState } from "react"
import { getValidationErrorMessage } from "../../api/errors"
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
import { Stack } from "../Stack/Stack"
Expand Down Expand Up @@ -43,16 +43,7 @@ export const SearchBarWithFilter: React.FC<React.PropsWithChildren<SearchBarWith
error,
}) => {
const styles = useStyles({ error: Boolean(error) })

const form = useFormik<FilterFormValues>({
enableReinitialize: true,
initialValues: {
query: filter ?? "",
},
onSubmit: ({ query }) => {
onFilter(query)
},
})
const searchInputRef = useRef<HTMLInputElement>(null)

// debounce query string entry by user
// we want the dependency array empty here
Expand All @@ -65,12 +56,6 @@ export const SearchBarWithFilter: React.FC<React.PropsWithChildren<SearchBarWith
[],
)

// update the query params while typing
useEffect(() => {
debouncedOnFilter(form.values.query)
return () => debouncedOnFilter.cancel()
}, [debouncedOnFilter, form.values.query])

const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
Expand All @@ -82,8 +67,15 @@ export const SearchBarWithFilter: React.FC<React.PropsWithChildren<SearchBarWith
}

const setPresetFilter = (query: string) => () => {
void form.setFieldValue("query", query)
void form.submitForm()
if (!searchInputRef.current) {
throw new Error("Search input not found.")
}

onFilter(query)
// Update this to the input directly instead of create a new state and
// re-render the component since the onFilter is already calling the
// filtering process
searchInputRef.current.value = query
handleClose()
}

Expand All @@ -103,21 +95,24 @@ export const SearchBarWithFilter: React.FC<React.PropsWithChildren<SearchBarWith
</Button>
)}

<form onSubmit={form.handleSubmit} className={styles.filterForm}>
<div role="form" className={styles.filterForm}>
<OutlinedInput
id="query"
name="query"
value={form.values.query}
defaultValue={filter}
error={Boolean(error)}
className={styles.inputStyles}
onChange={form.handleChange}
onChange={(event) => {
debouncedOnFilter(event.currentTarget.value)
}}
inputRef={searchInputRef}
startAdornment={
<InputAdornment position="start" className={styles.searchIcon}>
<SearchIcon fontSize="small" />
</InputAdornment>
}
/>
</form>
</div>

{presetFilters && presetFilters.length && (
<Menu
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Language as FormLanguage } from "../../../components/CreateUserForm/Cre
import { Language as FooterLanguage } from "../../../components/FormFooter/FormFooter"
import { history, render } from "../../../testHelpers/renderHelpers"
import { server } from "../../../testHelpers/server"
import { Language as UserLanguage } from "../../../xServices/users/usersXService"
import { Language as CreateUserLanguage } from "../../../xServices/users/createUserXService"
import { CreateUserPage } from "./CreateUserPage"

const fillForm = async ({
Expand Down Expand Up @@ -46,7 +46,7 @@ describe("Create User Page", () => {
})
render(<CreateUserPage />)
await fillForm({})
const errorMessage = await screen.findByText(UserLanguage.createUserError)
const errorMessage = await screen.findByText(CreateUserLanguage.createUserError)
expect(errorMessage).toBeDefined()
})

Expand Down Expand Up @@ -77,7 +77,7 @@ describe("Create User Page", () => {
it("shows success notification and redirects to users page", async () => {
render(<CreateUserPage />)
await fillForm({})
const successMessage = screen.findByText(UserLanguage.createUserSuccess)
const successMessage = screen.findByText(CreateUserLanguage.createUserSuccess)
expect(successMessage).toBeDefined()
})

Expand Down
23 changes: 14 additions & 9 deletions site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import { useActor, useSelector } from "@xstate/react"
import React, { useContext } from "react"
import { useMachine } from "@xstate/react"
import { useOrganizationId } from "hooks/useOrganizationId"
import React from "react"
import { Helmet } from "react-helmet-async"
import { useNavigate } from "react-router"
import { createUserMachine } from "xServices/users/createUserXService"
import * as TypesGen from "../../../api/typesGenerated"
import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm"
import { Margins } from "../../../components/Margins/Margins"
import { pageTitle } from "../../../util/page"
import { selectOrgId } from "../../../xServices/auth/authSelectors"
import { XServiceContext } from "../../../xServices/StateContext"

export const Language = {
unknownError: "Oops, an unknown error occurred.",
}

export const CreateUserPage: React.FC = () => {
const xServices = useContext(XServiceContext)
const myOrgId = useSelector(xServices.authXService, selectOrgId)
const [usersState, usersSend] = useActor(xServices.usersXService)
const { createUserErrorMessage, createUserFormErrors } = usersState.context
const myOrgId = useOrganizationId()
const navigate = useNavigate()
const [usersState, usersSend] = useMachine(createUserMachine, {
actions: {
redirectToUsersPage: () => {
navigate("/users")
},
},
})
const { createUserErrorMessage, createUserFormErrors } = usersState.context
// There is no field for organization id in Community Edition, so handle its field error like a generic error
const genericError =
createUserErrorMessage ||
Expand All @@ -39,7 +44,7 @@ export const CreateUserPage: React.FC = () => {
}}
isLoading={usersState.hasTag("loading")}
error={genericError}
myOrgId={myOrgId ?? ""}
myOrgId={myOrgId}
/>
</Margins>
)
Expand Down
24 changes: 14 additions & 10 deletions site/src/pages/UsersPage/UsersPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useActor } from "@xstate/react"
import { useActor, useMachine } from "@xstate/react"
import { FC, ReactNode, useContext, useEffect } from "react"
import { Helmet } from "react-helmet-async"
import { useNavigate } from "react-router"
import { useSearchParams } from "react-router-dom"
import { usersMachine } from "xServices/users/usersXService"
import { ConfirmDialog } from "../../components/Dialogs/ConfirmDialog/ConfirmDialog"
import { ResetPasswordDialog } from "../../components/Dialogs/ResetPasswordDialog/ResetPasswordDialog"
import { userFilterQuery } from "../../util/filters"
import { pageTitle } from "../../util/page"
import { XServiceContext } from "../../xServices/StateContext"
import { UsersPageView } from "./UsersPageView"
Expand All @@ -24,7 +24,14 @@ export const Language = {

export const UsersPage: FC<{ children?: ReactNode }> = () => {
const xServices = useContext(XServiceContext)
const [usersState, usersSend] = useActor(xServices.usersXService)
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const filter = searchParams.get("filter") ?? undefined
const [usersState, usersSend] = useMachine(usersMachine, {
context: {
filter,
},
})
const {
users,
getUsersError,
Expand All @@ -34,8 +41,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
userIdToResetPassword,
newUserPassword,
} = usersState.context
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()

const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend)
const userToBeDeleted = users?.find((u) => u.id === userIdToDelete)
const userToBeActivated = users?.find((u) => u.id === userIdToActivate)
Expand All @@ -60,13 +66,11 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {

// Fetch users on component mount
useEffect(() => {
const filter = searchParams.get("filter")
const query = filter ?? userFilterQuery.active
usersSend({
type: "GET_USERS",
query,
query: filter,
})
}, [searchParams, usersSend])
}, [filter, usersSend])

// Fetch roles on component mount
useEffect(() => {
Expand Down Expand Up @@ -116,7 +120,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
isLoading={isLoading}
canEditUsers={canEditUsers}
canCreateUser={canCreateUser}
filter={usersState.context.filter}
filter={filter}
onFilter={(query) => {
searchParams.set("filter", query)
setSearchParams(searchParams)
Expand Down
8 changes: 0 additions & 8 deletions site/src/xServices/StateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ import { authMachine } from "./auth/authXService"
import { buildInfoMachine } from "./buildInfo/buildInfoXService"
import { entitlementsMachine } from "./entitlements/entitlementsXService"
import { siteRolesMachine } from "./roles/siteRolesXService"
import { usersMachine } from "./users/usersXService"

interface XServiceContextType {
authXService: ActorRefFrom<typeof authMachine>
buildInfoXService: ActorRefFrom<typeof buildInfoMachine>
entitlementsXService: ActorRefFrom<typeof entitlementsMachine>
usersXService: ActorRefFrom<typeof usersMachine>
siteRolesXService: ActorRefFrom<typeof siteRolesMachine>
}

Expand All @@ -28,9 +26,6 @@ export const XServiceContext = createContext({} as XServiceContextType)

export const XServiceProvider: FC<{ children: ReactNode }> = ({ children }) => {
const navigate = useNavigate()
const redirectToUsersPage = () => {
navigate("users")
}
const redirectToSetupPage = () => {
navigate("setup")
}
Expand All @@ -43,9 +38,6 @@ export const XServiceProvider: FC<{ children: ReactNode }> = ({ children }) => {
),
buildInfoXService: useInterpret(buildInfoMachine),
entitlementsXService: useInterpret(entitlementsMachine),
usersXService: useInterpret(() =>
usersMachine.withConfig({ actions: { redirectToUsersPage } }),
),
siteRolesXService: useInterpret(siteRolesMachine),
}}
>
Expand Down
101 changes: 101 additions & 0 deletions site/src/xServices/users/createUserXService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { assign, createMachine } from "xstate"
import * as API from "../../api/api"
import {
ApiError,
FieldErrors,
getErrorMessage,
hasApiFieldErrors,
isApiError,
mapApiErrorToFieldErrors,
} from "../../api/errors"
import * as TypesGen from "../../api/typesGenerated"
import { displaySuccess } from "../../components/GlobalSnackbar/utils"

export const Language = {
createUserSuccess: "Successfully created user.",
createUserError: "Error on creating the user.",
}

export interface CreateUserContext {
createUserErrorMessage?: string
createUserFormErrors?: FieldErrors
}

export type CreateUserEvent =
| { type: "CREATE"; user: TypesGen.CreateUserRequest }
| { type: "CANCEL_CREATE_USER" }

export const createUserMachine = createMachine(
{
id: "usersState",
predictableActionArguments: true,
tsTypes: {} as import("./createUserXService.typegen").Typegen0,
schema: {
context: {} as CreateUserContext,
events: {} as CreateUserEvent,
services: {} as {
createUser: {
data: TypesGen.User
}
},
},
initial: "idle",
states: {
idle: {
on: {
CREATE: "creatingUser",
CANCEL_CREATE_USER: { actions: ["clearCreateUserError"] },
},
},
creatingUser: {
entry: "clearCreateUserError",
invoke: {
src: "createUser",
id: "createUser",
onDone: {
target: "idle",
actions: ["displayCreateUserSuccess", "redirectToUsersPage"],
},
onError: [
{
target: "idle",
cond: "hasFieldErrors",
actions: ["assignCreateUserFormErrors"],
},
{
target: "idle",
actions: ["assignCreateUserError"],
},
],
},
tags: "loading",
},
},
},
{
services: {
createUser: (_, event) => API.createUser(event.user),
},
guards: {
hasFieldErrors: (_, event) => isApiError(event.data) && hasApiFieldErrors(event.data),
},
actions: {
assignCreateUserError: assign({
createUserErrorMessage: (_, event) => getErrorMessage(event.data, Language.createUserError),
}),
assignCreateUserFormErrors: assign({
// the guard ensures it is ApiError
createUserFormErrors: (_, event) =>
mapApiErrorToFieldErrors((event.data as ApiError).response.data),
}),
clearCreateUserError: assign((context: CreateUserContext) => ({
...context,
createUserErrorMessage: undefined,
createUserFormErrors: undefined,
})),
displayCreateUserSuccess: () => {
displaySuccess(Language.createUserSuccess)
},
},
},
)
Loading