Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: Add setup page#3476

New issue

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

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

Already on GitHub?Sign in to your account

Merged
BrunoQuaresma merged 19 commits intomainfrombq/3225
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
19 commits
Select commitHold shift + click to select a range
2eccbf1
Check if has first user
BrunoQuaresmaAug 10, 2022
a3fabe3
Add missing handler
BrunoQuaresmaAug 10, 2022
5d3701b
Add setup
BrunoQuaresmaAug 10, 2022
87b55cc
Make user login after creation
BrunoQuaresmaAug 10, 2022
d6fe749
Authenticate user when setup is done
BrunoQuaresmaAug 10, 2022
f81942d
Fix setup flow
Aug 11, 2022
7b37e0e
Apply suggestions from code review
BrunoQuaresmaAug 11, 2022
4e4008f
Add comment into hasFirtUser
Aug 11, 2022
a0ef036
Move to language object
Aug 11, 2022
186a37c
Refactor tests to not use spy
Aug 11, 2022
e50648f
Merge
Aug 11, 2022
2cdebbd
Merge branch 'bq/3225' of github.com:coder/coder into bq/3225
Aug 11, 2022
d88d470
Merge branch 'main' of github.com:coder/coder into bq/3225
Aug 11, 2022
42fd362
Add back first user on dev script
Aug 11, 2022
5d988cd
Update site/src/pages/SetupPage/SetupPage.tsx
BrunoQuaresmaAug 11, 2022
7959e44
Apply suggestions from code review
BrunoQuaresmaAug 11, 2022
3895a0b
Better handle hasFirstUser error
Aug 11, 2022
b5e0a63
Fix formatting
Aug 11, 2022
4d13c28
Fix login machine
Aug 11, 2022
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
4 changes: 2 additions & 2 deletionssite/e2e/globalSetup.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
import axios from "axios"
import {postFirstUser } from "../src/api/api"
import {createFirstUser } from "../src/api/api"
import * as constants from "./constants"

const globalSetup = async (): Promise<void> => {
axios.defaults.baseURL = `http://localhost:${constants.basePort}`
awaitpostFirstUser({
awaitcreateFirstUser({
email: constants.email,
organization: constants.organization,
username: constants.username,
Expand Down
2 changes: 2 additions & 0 deletionssite/src/AppRouter.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import { useSelector } from "@xstate/react"
import { SetupPage } from "pages/SetupPage/SetupPage"
import { FC, lazy, Suspense, useContext } from "react"
import { Navigate, Route, Routes } from "react-router-dom"
import { selectPermissions } from "xServices/auth/authSelectors"
Expand DownExpand Up@@ -47,6 +48,7 @@ export const AppRouter: FC = () => {
/>

<Route path="login" element={<LoginPage />} />
<Route path="setup" element={<SetupPage />} />
<Route path="healthz" element={<HealthzPage />} />
<Route
path="cli-auth"
Expand Down
19 changes: 18 additions & 1 deletionsite/src/api/api.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -282,7 +282,24 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen
return response.data
}

export const postFirstUser = async (
// API definition:
// https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53
export const hasFirstUser = async (): Promise<boolean> => {
try {
// If it is success, it is true
await axios.get("/api/v2/users/first")
return true
} catch (error) {
// If it returns a 404, it is false
if (axios.isAxiosError(error) && error.response?.status === 404) {
return false
}

throw error
}
}

export const createFirstUser = async (
req: TypesGen.CreateFirstUserRequest,
): Promise<TypesGen.CreateFirstUserResponse> => {
const response = await axios.post(`/api/v2/users/first`, req)
Expand Down
35 changes: 35 additions & 0 deletionssite/src/components/SignInLayout/SignInLayout.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
import { makeStyles } from "@material-ui/core/styles"
import { FC } from "react"
import { Footer } from "../../components/Footer/Footer"

export const useStyles = makeStyles((theme) => ({
root: {
height: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
},
layout: {
display: "flex",
flexDirection: "column",
alignItems: "center",
},
container: {
marginTop: theme.spacing(-8),
minWidth: "320px",
maxWidth: "320px",
},
}))

export const SignInLayout: FC = ({ children }) => {
const styles = useStyles()

return (
<div className={styles.root}>
<div className={styles.layout}>
<div className={styles.container}>{children}</div>
<Footer />
</div>
</div>
)
}
12 changes: 10 additions & 2 deletionssite/src/components/Welcome/Welcome.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,15 @@ import Typography from "@material-ui/core/Typography"
import { FC } from "react"
import { CoderIcon } from "../Icons/CoderIcon"

export const Welcome: FC = () => {
const Language = {
defaultMessage: (
<>
Welcome to <strong>Coder</strong>
</>
),
}

export const Welcome: FC<{ message?: JSX.Element }> = ({ message = Language.defaultMessage }) => {
const styles = useStyles()

return (
Expand All@@ -12,7 +20,7 @@ export const Welcome: FC = () => {
<CoderIcon className={styles.logo} />
</div>
<Typography className={styles.title} variant="h1">
Welcome to <strong>Coder</strong>
{message}
</Typography>
</div>
)
Expand Down
17 changes: 16 additions & 1 deletionsite/src/pages/LoginPage/LoginPage.test.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { act, screen } from "@testing-library/react"
import { act, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { rest } from "msw"
import { Language } from "../../components/SignInForm/SignInForm"
Expand DownExpand Up@@ -89,4 +89,19 @@ describe("LoginPage", () => {
await screen.findByText(Language.passwordSignIn)
await screen.findByText(Language.githubSignIn)
})

it("redirects to the setup page if there is no first user", async () => {
// Given
server.use(
rest.get("/api/v2/users/first", async (req, res, ctx) => {
return res(ctx.status(404))
}),
)

// When
render(<LoginPage />)

// Then
await waitFor(() => expect(history.location.pathname).toEqual("/setup"))
})
})
62 changes: 18 additions & 44 deletionssite/src/pages/LoginPage/LoginPage.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,54 @@
import { makeStyles } from "@material-ui/core/styles"
import { useActor } from "@xstate/react"
import { SignInLayout } from "components/SignInLayout/SignInLayout"
import React, { useContext } from "react"
import { Helmet } from "react-helmet"
import { Navigate, useLocation } from "react-router-dom"
import { Footer } from "../../components/Footer/Footer"
import { LoginErrors, SignInForm } from "../../components/SignInForm/SignInForm"
import { pageTitle } from "../../util/page"
import { retrieveRedirect } from "../../util/redirect"
import { XServiceContext } from "../../xServices/StateContext"

export const useStyles = makeStyles((theme) => ({
root: {
height: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
},
layout: {
display: "flex",
flexDirection: "column",
alignItems: "center",
},
container: {
marginTop: theme.spacing(-8),
minWidth: "320px",
maxWidth: "320px",
},
}))

interface LocationState {
isRedirect: boolean
}

export const LoginPage: React.FC = () => {
const styles = useStyles()
const location = useLocation()
const xServices = useContext(XServiceContext)
const [authState, authSend] = useActor(xServices.authXService)
const isLoading = authState.hasTag("loading")
const redirectTo = retrieveRedirect(location.search)
const locationState = location.state ? (location.state as LocationState) : null
const isRedirected = locationState ? locationState.isRedirect : false
const { authError, getUserError, checkPermissionsError, getMethodsError } = authState.context

const onSubmit = async ({ email, password }: { email: string; password: string }) => {
authSend({ type: "SIGN_IN", email, password })
}

const { authError, getUserError, checkPermissionsError, getMethodsError } = authState.context

if (authState.matches("signedIn")) {
return <Navigate to={redirectTo} replace />
} else {
return (
<div className={styles.root}>
<>
<Helmet>
<title>{pageTitle("Login")}</title>
</Helmet>
<div className={styles.layout}>
<div className={styles.container}>
<SignInForm
authMethods={authState.context.methods}
redirectTo={redirectTo}
isLoading={isLoading}
loginErrors={{
[LoginErrors.AUTH_ERROR]: authError,
[LoginErrors.GET_USER_ERROR]: isRedirected ? getUserError : null,
[LoginErrors.CHECK_PERMISSIONS_ERROR]: checkPermissionsError,
[LoginErrors.GET_METHODS_ERROR]: getMethodsError,
}}
onSubmit={onSubmit}
/>
</div>

<Footer />
</div>
</div>
<SignInLayout>
<SignInForm
authMethods={authState.context.methods}
redirectTo={redirectTo}
isLoading={isLoading}
loginErrors={{
[LoginErrors.AUTH_ERROR]: authError,
[LoginErrors.GET_USER_ERROR]: isRedirected ? getUserError : null,
[LoginErrors.CHECK_PERMISSIONS_ERROR]: checkPermissionsError,
[LoginErrors.GET_METHODS_ERROR]: getMethodsError,
}}
onSubmit={onSubmit}
/>
</SignInLayout>
</>
)
}
}
99 changes: 99 additions & 0 deletionssite/src/pages/SetupPage/SetupPage.test.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
import { screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import * as API from "api/api"
import { rest } from "msw"
import { history, MockUser, render } from "testHelpers/renderHelpers"
import { server } from "testHelpers/server"
import { Language as SetupLanguage } from "xServices/setup/setupXService"
import { SetupPage } from "./SetupPage"
import { Language as PageViewLanguage } from "./SetupPageView"

const fillForm = async ({
username = "someuser",
email = "someone@coder.com",
password = "password",
organization = "Coder",
}: {
username?: string
email?: string
password?: string
organization?: string
} = {}) => {
const usernameField = screen.getByLabelText(PageViewLanguage.usernameLabel)
const emailField = screen.getByLabelText(PageViewLanguage.emailLabel)
const passwordField = screen.getByLabelText(PageViewLanguage.passwordLabel)
const organizationField = screen.getByLabelText(PageViewLanguage.organizationLabel)
await userEvent.type(organizationField, organization)
await userEvent.type(usernameField, username)
await userEvent.type(emailField, email)
await userEvent.type(passwordField, password)
const submitButton = screen.getByRole("button", { name: PageViewLanguage.create })
submitButton.click()
}

describe("Setup Page", () => {
beforeEach(() => {
history.replace("/setup")
// appear logged out
server.use(
rest.get("/api/v2/users/me", (req, res, ctx) => {
return res(ctx.status(401), ctx.json({ message: "no user here" }))
}),
)
})

it("shows validation error message", async () => {
render(<SetupPage />)
await fillForm({ email: "test" })
const errorMessage = await screen.findByText(PageViewLanguage.emailInvalid)
expect(errorMessage).toBeDefined()
})

it("shows generic error message", async () => {
jest.spyOn(API, "createFirstUser").mockRejectedValueOnce({
data: "unknown error",
})
render(<SetupPage />)
await fillForm()
const errorMessage = await screen.findByText(SetupLanguage.createFirstUserError)
expect(errorMessage).toBeDefined()
})

it("shows API error message", async () => {
const fieldErrorMessage = "invalid username"
server.use(
rest.post("/api/v2/users/first", async (req, res, ctx) => {
return res(
ctx.status(400),
ctx.json({
message: "invalid field",
validations: [
{
detail: fieldErrorMessage,
field: "username",
},
],
}),
)
}),
)
render(<SetupPage />)
await fillForm()
const errorMessage = await screen.findByText(fieldErrorMessage)
expect(errorMessage).toBeDefined()
})

it("redirects to workspaces page when success", async () => {
render(<SetupPage />)

// simulates the user will be authenticated
server.use(
rest.get("/api/v2/users/me", (req, res, ctx) => {
return res(ctx.status(200), ctx.json(MockUser))
}),
)

await fillForm()
await waitFor(() => expect(history.location.pathname).toEqual("/workspaces"))
})
})
47 changes: 47 additions & 0 deletionssite/src/pages/SetupPage/SetupPage.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
import { useActor, useMachine } from "@xstate/react"
import { FC, useContext, useEffect } from "react"
import { Helmet } from "react-helmet"
import { useNavigate } from "react-router-dom"
import { pageTitle } from "util/page"
import { setupMachine } from "xServices/setup/setupXService"
import { XServiceContext } from "xServices/StateContext"
import { SetupPageView } from "./SetupPageView"

export const SetupPage: FC = () => {
const navigate = useNavigate()
const xServices = useContext(XServiceContext)
const [authState, authSend] = useActor(xServices.authXService)
const [setupState, setupSend] = useMachine(setupMachine, {
actions: {
onCreateFirstUser: ({ firstUser }) => {
if (!firstUser) {
throw new Error("First user was not defined.")
}
authSend({ type: "SIGN_IN", email: firstUser.email, password: firstUser.password })
},
},
})
const { createFirstUserFormErrors, createFirstUserErrorMessage } = setupState.context

useEffect(() => {
if (authState.matches("signedIn")) {
return navigate("/workspaces")
}
}, [authState, navigate])

return (
<>
<Helmet>
<title>{pageTitle("Set up your account")}</title>
</Helmet>
<SetupPageView
isLoading={setupState.hasTag("loading")}
formErrors={createFirstUserFormErrors}
genericError={createFirstUserErrorMessage}
onSubmit={(firstUser) => {
setupSend({ type: "CREATE_FIRST_USER", firstUser })
}}
/>
</>
)
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp