mirror of https://github.com/coder/coder.git
refactor: Refactor login page (#5148)
This commit is contained in:
parent
71bc48dda4
commit
59355431d0
|
@ -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")')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"coder": "Coder",
|
||||
"workspaceStatus": {
|
||||
"loading": "Loading",
|
||||
"running": "Running",
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"signInTo": "Sign in to"
|
||||
}
|
|
@ -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 })
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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: {},
|
||||
}
|
|
@ -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),
|
||||
},
|
||||
}))
|
|
@ -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 |
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Loading…
Reference in New Issue