refactor(site): refactor login screen (#10768)

This commit is contained in:
Bruno Quaresma
2023-11-20 11:19:50 -03:00
committed by GitHub
parent 2895c108c2
commit fbec79f35d
11 changed files with 83 additions and 153 deletions

View File

@ -170,6 +170,7 @@
"wsconncache",
"wsjson",
"xerrors",
"xlarge",
"yamux"
],
"cSpell.ignorePaths": ["site/package.json", ".vscode/settings.json"],

View File

@ -31,6 +31,10 @@ declare module "@mui/material/Button" {
interface ButtonPropsColorOverrides {
neutral: true;
}
interface ButtonPropsSizeOverrides {
xlarge: true;
}
}
declare module "@mui/system" {

View File

@ -10,7 +10,6 @@ import {
} from "testHelpers/renderHelpers";
import { server } from "testHelpers/server";
import { LoginPage } from "./LoginPage";
import * as TypesGen from "api/typesGenerated";
describe("LoginPage", () => {
beforeEach(() => {
@ -76,32 +75,4 @@ describe("LoginPage", () => {
// Then
await screen.findByText("Setup");
});
it("hides password authentication if OIDC/GitHub is enabled and displays on click", async () => {
const authMethods: TypesGen.AuthMethods = {
password: { enabled: true },
github: { enabled: true },
oidc: { enabled: true, signInText: "", iconUrl: "" },
};
// Given
server.use(
rest.get("/api/v2/users/authmethods", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(authMethods));
}),
);
// When
render(<LoginPage />);
// Then
expect(screen.queryByText(Language.passwordSignIn)).not.toBeInTheDocument();
await screen.findByText(Language.githubSignIn);
const showPasswordAuthLink = screen.getByText("Email and password");
await userEvent.click(showPasswordAuthLink);
await screen.findByText(Language.passwordSignIn);
await screen.findByText(Language.githubSignIn);
});
});

View File

@ -75,7 +75,7 @@ const styles = {
container: {
width: "100%",
maxWidth: 385,
maxWidth: 320,
display: "flex",
flexDirection: "column",
alignItems: "center",

View File

@ -1,4 +1,3 @@
import Link from "@mui/material/Link";
import Button from "@mui/material/Button";
import GitHubIcon from "@mui/icons-material/GitHub";
import KeyIcon from "@mui/icons-material/VpnKey";
@ -26,49 +25,47 @@ export const OAuthSignInForm: FC<OAuthSignInFormProps> = ({
return (
<Box display="grid" gap="16px">
{authMethods?.github.enabled && (
<Link
<Button
component="a"
href={`/api/v2/users/oauth2/github/callback?redirect=${encodeURIComponent(
redirectTo,
)}`}
variant="contained"
startIcon={<GitHubIcon css={iconStyles} />}
disabled={isSigningIn}
fullWidth
type="submit"
size="xlarge"
>
<Button
startIcon={<GitHubIcon css={iconStyles} />}
disabled={isSigningIn}
fullWidth
type="submit"
size="large"
>
{Language.githubSignIn}
</Button>
</Link>
{Language.githubSignIn}
</Button>
)}
{authMethods?.oidc.enabled && (
<Link
<Button
component="a"
href={`/api/v2/users/oidc/callback?redirect=${encodeURIComponent(
redirectTo,
)}`}
variant="contained"
size="xlarge"
startIcon={
authMethods.oidc.iconUrl ? (
<img
alt="Open ID Connect icon"
src={authMethods.oidc.iconUrl}
css={iconStyles}
/>
) : (
<KeyIcon css={iconStyles} />
)
}
disabled={isSigningIn}
fullWidth
type="submit"
>
<Button
size="large"
startIcon={
authMethods.oidc.iconUrl ? (
<img
alt="Open ID Connect icon"
src={authMethods.oidc.iconUrl}
css={iconStyles}
/>
) : (
<KeyIcon css={iconStyles} />
)
}
disabled={isSigningIn}
fullWidth
type="submit"
>
{authMethods.oidc.signInText || Language.oidcSignIn}
</Button>
</Link>
{authMethods.oidc.signInText || Language.oidcSignIn}
</Button>
)}
</Box>
);

View File

@ -2,22 +2,21 @@ import { Stack } from "components/Stack/Stack";
import TextField from "@mui/material/TextField";
import { getFormHelpers, onChangeTrimmed } from "utils/formUtils";
import { Language } from "./SignInForm";
import { FormikContextType, FormikTouched, useFormik } from "formik";
import { useFormik } from "formik";
import * as Yup from "yup";
import { FC } from "react";
import { BuiltInAuthFormValues } from "./SignInForm.types";
import LoadingButton from "@mui/lab/LoadingButton";
type PasswordSignInFormProps = {
onSubmit: (credentials: { email: string; password: string }) => void;
initialTouched?: FormikTouched<BuiltInAuthFormValues>;
isSigningIn: boolean;
autoFocus: boolean;
};
export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
onSubmit,
initialTouched,
isSigningIn,
autoFocus,
}) => {
const validationSchema = Yup.object({
email: Yup.string()
@ -27,17 +26,16 @@ export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
password: Yup.string(),
});
const form: FormikContextType<BuiltInAuthFormValues> =
useFormik<BuiltInAuthFormValues>({
initialValues: {
email: "",
password: "",
},
validationSchema,
onSubmit,
initialTouched,
});
const getFieldHelpers = getFormHelpers<BuiltInAuthFormValues>(form);
const form = useFormik({
initialValues: {
email: "",
password: "",
},
validationSchema,
onSubmit,
validateOnBlur: false,
});
const getFieldHelpers = getFormHelpers(form);
return (
<form onSubmit={form.handleSubmit}>
@ -45,7 +43,7 @@ export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
<TextField
{...getFieldHelpers("email")}
onChange={onChangeTrimmed(form)}
autoFocus
autoFocus={autoFocus}
autoComplete="email"
fullWidth
label={Language.emailLabel}
@ -59,16 +57,14 @@ export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
label={Language.passwordLabel}
type="password"
/>
<div>
<LoadingButton
size="large"
loading={isSigningIn}
fullWidth
type="submit"
>
{Language.passwordSignIn}
</LoadingButton>
</div>
<LoadingButton
size="xlarge"
loading={isSigningIn}
fullWidth
type="submit"
>
{Language.passwordSignIn}
</LoadingButton>
</Stack>
</form>
);

View File

@ -37,9 +37,6 @@ export const WithError: Story = {
},
],
}),
initialTouched: {
password: true,
},
},
};

View File

@ -1,15 +1,10 @@
import { type Interpolation, type Theme } from "@emotion/react";
import { type FormikTouched } from "formik";
import { type FC, useState } from "react";
import { type FC } from "react";
import type { AuthMethods } from "api/typesGenerated";
import { PasswordSignInForm } from "./PasswordSignInForm";
import { OAuthSignInForm } from "./OAuthSignInForm";
import { type BuiltInAuthFormValues } from "./SignInForm.types";
import Button from "@mui/material/Button";
import EmailIcon from "@mui/icons-material/EmailOutlined";
import { Alert } from "components/Alert/Alert";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { getApplicationName } from "utils/appearance";
export const Language = {
emailLabel: "Email",
@ -71,8 +66,6 @@ export interface SignInFormProps {
info?: string;
authMethods?: AuthMethods;
onSubmit: (credentials: { email: string; password: string }) => void;
// initialTouched is only used for testing the error state of the form.
initialTouched?: FormikTouched<BuiltInAuthFormValues>;
}
export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
@ -82,21 +75,15 @@ export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
error,
info,
onSubmit,
initialTouched,
}) => {
const oAuthEnabled = Boolean(
authMethods?.github.enabled || authMethods?.oidc.enabled,
);
const passwordEnabled = authMethods?.password.enabled ?? true;
// Hide password auth by default if any OAuth method is enabled
const [showPasswordAuth, setShowPasswordAuth] = useState(!oAuthEnabled);
const applicationName = getApplicationName();
return (
<div css={styles.root}>
<h1 css={styles.title}>
Sign in to <strong>{applicationName}</strong>
</h1>
<h1 css={styles.title}>Sign in</h1>
{Boolean(error) && (
<div css={styles.alert}>
@ -110,22 +97,6 @@ export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
</div>
)}
{passwordEnabled && showPasswordAuth && (
<PasswordSignInForm
onSubmit={onSubmit}
initialTouched={initialTouched}
isSigningIn={isSigningIn}
/>
)}
{passwordEnabled && showPasswordAuth && oAuthEnabled && (
<div css={styles.divider}>
<div css={styles.dividerLine} />
<div css={styles.dividerLabel}>Or</div>
<div css={styles.dividerLine} />
</div>
)}
{oAuthEnabled && (
<OAuthSignInForm
isSigningIn={isSigningIn}
@ -134,27 +105,24 @@ export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
/>
)}
{!passwordEnabled && !oAuthEnabled && (
<Alert severity="error">No authentication methods configured!</Alert>
{passwordEnabled && oAuthEnabled && (
<div css={styles.divider}>
<div css={styles.dividerLine} />
<div css={styles.dividerLabel}>Or</div>
<div css={styles.dividerLine} />
</div>
)}
{passwordEnabled && !showPasswordAuth && (
<>
<div css={styles.divider}>
<div css={styles.dividerLine} />
<div css={styles.dividerLabel}>Or</div>
<div css={styles.dividerLine} />
</div>
{passwordEnabled && (
<PasswordSignInForm
onSubmit={onSubmit}
autoFocus={!oAuthEnabled}
isSigningIn={isSigningIn}
/>
)}
<Button
fullWidth
size="large"
onClick={() => setShowPasswordAuth(true)}
startIcon={<EmailIcon css={styles.icon} />}
>
Email and password
</Button>
</>
{!passwordEnabled && !oAuthEnabled && (
<Alert severity="error">No authentication methods configured!</Alert>
)}
</div>
);

View File

@ -1,9 +0,0 @@
/**
* BuiltInAuthFormValues describes a form using built-in (email/password)
* authentication. This form may not always be present depending on external
* auth providers available and administrative configurations
*/
export interface BuiltInAuthFormValues {
email: string;
password: string;
}

View File

@ -9,6 +9,7 @@ export const sidePadding = 24;
export const dashboardContentBottomPadding = 8 * 6;
// MUI does not have aligned heights for buttons and inputs so we have to "hack" it a little bit
export const BUTTON_XL_HEIGHT = 44;
export const BUTTON_LG_HEIGHT = 40;
export const BUTTON_MD_HEIGHT = 36;
export const BUTTON_SM_HEIGHT = 32;

View File

@ -6,6 +6,7 @@ import {
BUTTON_LG_HEIGHT,
BUTTON_MD_HEIGHT,
BUTTON_SM_HEIGHT,
BUTTON_XL_HEIGHT,
} from "./constants";
// eslint-disable-next-line no-restricted-imports -- We need MUI here
import { alertClasses } from "@mui/material/Alert";
@ -172,6 +173,9 @@ dark = createTheme(dark, {
sizeLarge: {
height: BUTTON_LG_HEIGHT,
},
sizeXlarge: {
height: BUTTON_XL_HEIGHT,
},
outlined: {
":hover": {
border: `1px solid ${colors.gray[11]}`,
@ -190,10 +194,10 @@ dark = createTheme(dark, {
},
},
containedNeutral: {
borderColor: colors.gray[12],
backgroundColor: colors.gray[13],
backgroundColor: colors.gray[14],
"&:hover": {
backgroundColor: colors.gray[12],
backgroundColor: colors.gray[13],
},
},
iconSizeMedium: {