refactor: Refactor login page (#5148)

This commit is contained in:
Bruno Quaresma 2022-11-23 11:53:42 -03:00 committed by GitHub
parent 71bc48dda4
commit 59355431d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 244 additions and 65 deletions

View File

@ -12,6 +12,6 @@ export class SignInPage extends BasePom {
): Promise<void> {
await this.page.fill("text=Email", email)
await this.page.fill("text=Password", password)
await this.page.click("text=Sign In")
await this.page.click('button:has-text("Sign In")')
}
}

View File

@ -11,9 +11,9 @@ import { FC } from "react"
import * as Yup from "yup"
import { AuthMethods } from "../../api/typesGenerated"
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
import { Welcome } from "../Welcome/Welcome"
import { LoadingButton } from "./../LoadingButton/LoadingButton"
import { AlertBanner } from "components/AlertBanner/AlertBanner"
import { useTranslation } from "react-i18next"
/**
* BuiltInAuthFormValues describes a form using built-in (email/password)
@ -57,6 +57,27 @@ const validationSchema = Yup.object({
})
const useStyles = makeStyles((theme) => ({
wrapper: {
maxWidth: 385,
width: "100%",
[theme.breakpoints.down("sm")]: {
maxWidth: "none",
},
},
title: {
fontSize: theme.spacing(4),
fontWeight: 400,
margin: 0,
marginBottom: theme.spacing(4),
lineHeight: 1,
"& strong": {
fontWeight: 600,
},
},
buttonIcon: {
width: 14,
height: 14,
@ -87,13 +108,7 @@ export interface SignInFormProps {
redirectTo: string
loginErrors: Partial<Record<LoginErrors, Error | unknown>>
authMethods?: AuthMethods
onSubmit: ({
email,
password,
}: {
email: string
password: string
}) => Promise<void>
onSubmit: (credentials: { email: string; password: string }) => void
// initialTouched is only used for testing the error state of the form.
initialTouched?: FormikTouched<BuiltInAuthFormValues>
}
@ -107,7 +122,6 @@ export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
initialTouched,
}) => {
const styles = useStyles()
const form: FormikContextType<BuiltInAuthFormValues> =
useFormik<BuiltInAuthFormValues>({
initialValues: {
@ -127,10 +141,15 @@ export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
form,
loginErrors.authError,
)
const commonTranslation = useTranslation("common")
const loginPageTranslation = useTranslation("loginPage")
return (
<>
<Welcome />
<div className={styles.wrapper}>
<h1 className={styles.title}>
{loginPageTranslation.t("signInTo")}{" "}
<strong>{commonTranslation.t("coder")}</strong>
</h1>
<form onSubmit={form.handleSubmit}>
<Stack>
{Object.keys(loginErrors).map(
@ -176,7 +195,7 @@ export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
</Stack>
</form>
{(authMethods?.github || authMethods?.oidc) && (
<>
<div>
<div className={styles.divider}>
<div className={styles.dividerLine} />
<div className={styles.dividerLabel}>Or</div>
@ -222,8 +241,8 @@ export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
</Link>
)}
</Box>
</>
</div>
)}
</>
</div>
)
}

View File

@ -62,7 +62,7 @@ export const WorkspacesTableBody: FC<
}
image={
<div className={styles.emptyImage}>
<img src="/empty/workspaces.webp" alt="" />
<img src="/featured/workspaces.webp" alt="" />
</div>
}
/>

View File

@ -1,4 +1,5 @@
{
"coder": "Coder",
"workspaceStatus": {
"loading": "Loading",
"running": "Running",

View File

@ -9,6 +9,7 @@ import buildPage from "./buildPage.json"
import workspacesPage from "./workspacesPage.json"
import usersPage from "./usersPage.json"
import templateVersionPage from "./templateVersionPage.json"
import loginPage from "./loginPage.json"
export const en = {
common,
@ -22,4 +23,5 @@ export const en = {
workspacesPage,
usersPage,
templateVersionPage,
loginPage,
}

View File

@ -0,0 +1,3 @@
{
"signInTo": "Sign in to"
}

View File

@ -1,40 +1,19 @@
import { useActor } from "@xstate/react"
import { FullScreenLoader } from "components/Loader/FullScreenLoader"
import { SignInLayout } from "components/SignInLayout/SignInLayout"
import React, { useContext } from "react"
import { FC, useContext } from "react"
import { Helmet } from "react-helmet-async"
import { useTranslation } from "react-i18next"
import { Navigate, useLocation } from "react-router-dom"
import { LoginErrors, SignInForm } from "../../components/SignInForm/SignInForm"
import { pageTitle } from "../../util/page"
import { retrieveRedirect } from "../../util/redirect"
import { XServiceContext } from "../../xServices/StateContext"
import { LoginPageView } from "./LoginPageView"
interface LocationState {
isRedirect: boolean
}
export const LoginPage: React.FC = () => {
export const LoginPage: FC = () => {
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 commonTranslation = useTranslation("common")
const loginPageTranslation = useTranslation("loginPage")
if (authState.matches("signedIn")) {
return <Navigate to={redirectTo} replace />
@ -44,28 +23,17 @@ export const LoginPage: React.FC = () => {
return (
<>
<Helmet>
<title>{pageTitle("Login")}</title>
<title>
{loginPageTranslation.t("signInTo")} {commonTranslation.t("coder")}
</title>
</Helmet>
{authState.hasTag("loading") ? (
<FullScreenLoader />
) : (
<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>
)}
<LoginPageView
context={authState.context}
isLoading={authState.hasTag("loading")}
onSignIn={({ email, password }) => {
authSend({ type: "SIGN_IN", email, password })
}}
/>
</>
)
}

View File

@ -0,0 +1,19 @@
import { action } from "@storybook/addon-actions"
import { ComponentMeta, Story } from "@storybook/react"
import { LoginPageView, LoginPageViewProps } from "./LoginPageView"
export default {
title: "pages/LoginPageView",
component: LoginPageView,
} as ComponentMeta<typeof LoginPageView>
const Template: Story<LoginPageViewProps> = (args) => (
<LoginPageView {...args} />
)
export const Example = Template.bind({})
Example.args = {
isLoading: false,
onSignIn: action("onSignIn"),
context: {},
}

View File

@ -0,0 +1,167 @@
import { makeStyles } from "@material-ui/core/styles"
import { Logo } from "components/Icons/Logo"
import { FullScreenLoader } from "components/Loader/FullScreenLoader"
import { FC } from "react"
import { useLocation } from "react-router-dom"
import { AuthContext } from "xServices/auth/authXService"
import { LoginErrors, SignInForm } from "components/SignInForm/SignInForm"
import { retrieveRedirect } from "util/redirect"
interface LocationState {
isRedirect: boolean
}
export interface LoginPageViewProps {
context: AuthContext
isLoading: boolean
onSignIn: (credentials: { email: string; password: string }) => void
}
export const LoginPageView: FC<LoginPageViewProps> = ({
context,
isLoading,
onSignIn,
}) => {
const location = useLocation()
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 } =
context
const styles = useStyles()
return isLoading ? (
<FullScreenLoader />
) : (
<div className={styles.container}>
<div className={styles.left}>
<Logo fill="white" opacity={1} width={110} />
<div className={styles.formSection}>
<SignInForm
authMethods={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={onSignIn}
/>
</div>
<footer className={styles.footer}>
Copyright © 2022 Coder Technologies, Inc.
</footer>
</div>
<div className={styles.right}>
<div className={styles.tipWrapper}>
<div className={styles.tipContent}>
<h2 className={styles.tipTitle}>Scheduling</h2>
<p>
Coder automates your cloud cost control by ensuring developer
resources are only online while used.
</p>
<img
src="/featured/scheduling.webp"
alt=""
className={styles.tipImage}
/>
</div>
</div>
</div>
</div>
)
}
const useStyles = makeStyles((theme) => ({
container: {
padding: theme.spacing(5),
margin: "auto",
display: "flex",
height: "100vh",
[theme.breakpoints.down("md")]: {
height: "auto",
minHeight: "100vh",
},
[theme.breakpoints.down("sm")]: {
padding: theme.spacing(4),
},
},
left: {
flex: 1,
display: "flex",
flexDirection: "column",
gap: theme.spacing(4),
},
right: {
flex: 1,
[theme.breakpoints.down("md")]: {
display: "none",
},
},
formSection: {
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
},
footer: {
fontSize: 12,
color: theme.palette.text.secondary,
},
tipWrapper: {
width: "100%",
height: "100%",
borderRadius: theme.shape.borderRadius,
background: theme.palette.background.paper,
padding: theme.spacing(5),
display: "flex",
justifyContent: "center",
alignItems: "center",
},
tipContent: {
maxWidth: 570,
textAlign: "center",
fontSize: 16,
color: theme.palette.text.secondary,
lineHeight: "160%",
"& p": {
maxWidth: 440,
margin: "auto",
},
"& strong": {
color: theme.palette.text.primary,
},
},
tipTitle: {
fontWeight: 400,
fontSize: 24,
margin: 0,
lineHeight: 1,
marginBottom: theme.spacing(2),
color: theme.palette.text.primary,
},
tipImage: {
maxWidth: 570,
marginTop: theme.spacing(4),
},
}))

View File

@ -185,7 +185,7 @@ export const TemplatesPageView: FC<
cta={<CodeExample code="coder templates init" />}
image={
<div className={styles.emptyImage}>
<img src="/empty/templates.webp" alt="" />
<img src="/featured/templates.webp" alt="" />
</div>
}
/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB