diff --git a/site/e2e/pom/SignInPage.ts b/site/e2e/pom/SignInPage.ts index d9c27bd16646f..362674588f6c0 100644 --- a/site/e2e/pom/SignInPage.ts +++ b/site/e2e/pom/SignInPage.ts @@ -12,6 +12,6 @@ export class SignInPage extends BasePom { ): Promise { 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")') } } diff --git a/site/src/components/SignInForm/SignInForm.tsx b/site/src/components/SignInForm/SignInForm.tsx index f55358375bef6..71e31e6c5e628 100644 --- a/site/src/components/SignInForm/SignInForm.tsx +++ b/site/src/components/SignInForm/SignInForm.tsx @@ -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> authMethods?: AuthMethods - onSubmit: ({ - email, - password, - }: { - email: string - password: string - }) => Promise + onSubmit: (credentials: { email: string; password: string }) => void // initialTouched is only used for testing the error state of the form. initialTouched?: FormikTouched } @@ -107,7 +122,6 @@ export const SignInForm: FC> = ({ initialTouched, }) => { const styles = useStyles() - const form: FormikContextType = useFormik({ initialValues: { @@ -127,10 +141,15 @@ export const SignInForm: FC> = ({ form, loginErrors.authError, ) + const commonTranslation = useTranslation("common") + const loginPageTranslation = useTranslation("loginPage") return ( - <> - +
+

+ {loginPageTranslation.t("signInTo")}{" "} + {commonTranslation.t("coder")} +

{Object.keys(loginErrors).map( @@ -176,7 +195,7 @@ export const SignInForm: FC> = ({
{(authMethods?.github || authMethods?.oidc) && ( - <> +
Or
@@ -222,8 +241,8 @@ export const SignInForm: FC> = ({ )} - +
)} - +
) } diff --git a/site/src/components/WorkspacesTable/WorkspacesTableBody.tsx b/site/src/components/WorkspacesTable/WorkspacesTableBody.tsx index 77a75907a0480..db41fb3bc078b 100644 --- a/site/src/components/WorkspacesTable/WorkspacesTableBody.tsx +++ b/site/src/components/WorkspacesTable/WorkspacesTableBody.tsx @@ -62,7 +62,7 @@ export const WorkspacesTableBody: FC< } image={
- +
} /> diff --git a/site/src/i18n/en/common.json b/site/src/i18n/en/common.json index 18afb7754453a..e2dc165b58f6f 100644 --- a/site/src/i18n/en/common.json +++ b/site/src/i18n/en/common.json @@ -1,4 +1,5 @@ { + "coder": "Coder", "workspaceStatus": { "loading": "Loading", "running": "Running", diff --git a/site/src/i18n/en/index.ts b/site/src/i18n/en/index.ts index 05836182b6e29..e37f8c6337760 100644 --- a/site/src/i18n/en/index.ts +++ b/site/src/i18n/en/index.ts @@ -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, } diff --git a/site/src/i18n/en/loginPage.json b/site/src/i18n/en/loginPage.json new file mode 100644 index 0000000000000..329dc925a2e8e --- /dev/null +++ b/site/src/i18n/en/loginPage.json @@ -0,0 +1,3 @@ +{ + "signInTo": "Sign in to" +} diff --git a/site/src/pages/LoginPage/LoginPage.tsx b/site/src/pages/LoginPage/LoginPage.tsx index 8ce55a45ef129..a131a39450a27 100644 --- a/site/src/pages/LoginPage/LoginPage.tsx +++ b/site/src/pages/LoginPage/LoginPage.tsx @@ -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 @@ -44,28 +23,17 @@ export const LoginPage: React.FC = () => { return ( <> - {pageTitle("Login")} + + {loginPageTranslation.t("signInTo")} {commonTranslation.t("coder")} + - {authState.hasTag("loading") ? ( - - ) : ( - - - - )} + { + authSend({ type: "SIGN_IN", email, password }) + }} + /> ) } diff --git a/site/src/pages/LoginPage/LoginPageView.stories.tsx b/site/src/pages/LoginPage/LoginPageView.stories.tsx new file mode 100644 index 0000000000000..9ecacde906619 --- /dev/null +++ b/site/src/pages/LoginPage/LoginPageView.stories.tsx @@ -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 + +const Template: Story = (args) => ( + +) + +export const Example = Template.bind({}) +Example.args = { + isLoading: false, + onSignIn: action("onSignIn"), + context: {}, +} diff --git a/site/src/pages/LoginPage/LoginPageView.tsx b/site/src/pages/LoginPage/LoginPageView.tsx new file mode 100644 index 0000000000000..bd15ed218c463 --- /dev/null +++ b/site/src/pages/LoginPage/LoginPageView.tsx @@ -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 = ({ + 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 ? ( + + ) : ( +
+
+ + +
+ +
+ +
+ Copyright © 2022 Coder Technologies, Inc. +
+
+ +
+
+
+

Scheduling

+

+ Coder automates your cloud cost control by ensuring developer + resources are only online while used. +

+ +
+
+
+
+ ) +} + +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), + }, +})) diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 17a94529319d0..64acc2f5f9fb0 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -185,7 +185,7 @@ export const TemplatesPageView: FC< cta={} image={
- +
} /> diff --git a/site/static/featured/scheduling.webp b/site/static/featured/scheduling.webp new file mode 100644 index 0000000000000..8883af09f7dd8 Binary files /dev/null and b/site/static/featured/scheduling.webp differ diff --git a/site/static/empty/templates.webp b/site/static/featured/templates.webp similarity index 100% rename from site/static/empty/templates.webp rename to site/static/featured/templates.webp diff --git a/site/static/empty/workspaces.webp b/site/static/featured/workspaces.webp similarity index 100% rename from site/static/empty/workspaces.webp rename to site/static/featured/workspaces.webp