From 2bba7a7babe7a77ec94c16804d9ac9c8b0962be4 Mon Sep 17 00:00:00 2001 From: Sergio Betanzos <sbetanzos@opennebula.io> Date: Tue, 27 Oct 2020 13:29:13 +0100 Subject: [PATCH] F #3951: Refactor login component (#366) --- src/fireedge/src/client/app.js | 2 +- .../components/Cards/ApplicationCard.js | 1 - .../FormControl/CheckboxController.js | 9 +- .../components/FormControl/GroupSelect.js | 79 ----------- .../FormControl/SelectController.js | 12 +- .../components/FormControl/TextController.js | 9 +- .../client/components/Forms/FormWithSchema.js | 9 +- .../src/client/constants/translates.js | 5 +- .../DialogInfo/dialog.js | 6 +- .../ApplicationsInstances/DialogInfo/index.js | 7 +- .../src/client/containers/Login/Form.js | 91 +++++++++++++ .../client/containers/Login/Forms/Form2fa.js | 78 ----------- .../containers/Login/Forms/FormGroup.js | 49 ------- .../client/containers/Login/Forms/FormUser.js | 105 --------------- .../src/client/containers/Login/index.js | 122 +++++++++-------- .../src/client/containers/Login/schema.js | 123 ++++++++++++++++++ .../src/client/containers/Login/styles.js | 25 +++- src/fireedge/src/client/icons/logo.js | 8 +- 18 files changed, 338 insertions(+), 402 deletions(-) delete mode 100644 src/fireedge/src/client/components/FormControl/GroupSelect.js create mode 100644 src/fireedge/src/client/containers/Login/Form.js delete mode 100644 src/fireedge/src/client/containers/Login/Forms/Form2fa.js delete mode 100644 src/fireedge/src/client/containers/Login/Forms/FormGroup.js delete mode 100644 src/fireedge/src/client/containers/Login/Forms/FormUser.js create mode 100644 src/fireedge/src/client/containers/Login/schema.js diff --git a/src/fireedge/src/client/app.js b/src/fireedge/src/client/app.js index c49385e123..8b100c6a06 100644 --- a/src/fireedge/src/client/app.js +++ b/src/fireedge/src/client/app.js @@ -52,7 +52,7 @@ const App = ({ location, context, store, app }) => { {location && context ? ( // server build <StaticRouter location={location} context={context}> - {/* <Router app={appName} /> */} + <Router app={appName} /> </StaticRouter> ) : ( // browser build diff --git a/src/fireedge/src/client/components/Cards/ApplicationCard.js b/src/fireedge/src/client/components/Cards/ApplicationCard.js index bac36ad977..5fc50ce079 100644 --- a/src/fireedge/src/client/components/Cards/ApplicationCard.js +++ b/src/fireedge/src/client/components/Cards/ApplicationCard.js @@ -5,7 +5,6 @@ import { makeStyles, Box, Fade, - Badge, Button, Card, CardHeader, diff --git a/src/fireedge/src/client/components/FormControl/CheckboxController.js b/src/fireedge/src/client/components/FormControl/CheckboxController.js index 6f1ce146d3..7c024c5a1a 100644 --- a/src/fireedge/src/client/components/FormControl/CheckboxController.js +++ b/src/fireedge/src/client/components/FormControl/CheckboxController.js @@ -13,7 +13,7 @@ import ErrorHelper from 'client/components/FormControl/ErrorHelper' import { Tr } from 'client/components/HOC/Translate' const CheckboxController = memo( - ({ control, cy, name, label, tooltip, error }) => ( + ({ control, cy, name, label, tooltip, error, fieldProps }) => ( <Controller render={({ onChange, value }) => ( <Tooltip title={Tr(tooltip) ?? ''}> @@ -26,6 +26,7 @@ const CheckboxController = memo( checked={value} color="primary" inputProps={{ 'data-cy': cy }} + {...fieldProps} /> } label={Tr(label)} @@ -51,7 +52,8 @@ CheckboxController.propTypes = { error: PropTypes.oneOfType([ PropTypes.bool, PropTypes.objectOf(PropTypes.any) - ]) + ]), + fieldProps: PropTypes.object } CheckboxController.defaultProps = { @@ -61,7 +63,8 @@ CheckboxController.defaultProps = { label: '', tooltip: undefined, values: [], - error: false + error: false, + fieldProps: undefined } CheckboxController.displayName = 'CheckboxController' diff --git a/src/fireedge/src/client/components/FormControl/GroupSelect.js b/src/fireedge/src/client/components/FormControl/GroupSelect.js deleted file mode 100644 index 700c191561..0000000000 --- a/src/fireedge/src/client/components/FormControl/GroupSelect.js +++ /dev/null @@ -1,79 +0,0 @@ -/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */ -/* */ -/* Licensed under the Apache License, Version 2.0 (the "License"); you may */ -/* not use this file except in compliance with the License. You may obtain */ -/* a copy of the License at */ -/* */ -/* http://www.apache.org/licenses/LICENSE-2.0 */ -/* */ -/* Unless required by applicable law or agreed to in writing, software */ -/* distributed under the License is distributed on an "AS IS" BASIS, */ -/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ -/* See the License for the specific language governing permissions and */ -/* limitations under the License. */ -/* -------------------------------------------------------------------------- */ - -import React, { useMemo } from 'react' - -import { MenuItem, TextField } from '@material-ui/core' -import { FilterVintage } from '@material-ui/icons' - -import useAuth from 'client/hooks/useAuth' -import useOpennebula from 'client/hooks/useOpennebula' -import { Tr } from 'client/components/HOC' -import { FILTER_POOL } from 'client/constants' - -const GroupSelect = props => { - const { filterPool, authUser } = useAuth() - const { groups } = useOpennebula() - - const defaultValue = useMemo( - () => - filterPool === FILTER_POOL.ALL_RESOURCES - ? FILTER_POOL.ALL_RESOURCES - : authUser?.GID, - [filterPool] - ) - - const orderGroups = useMemo( - () => - groups - ?.sort((a, b) => a.ID - b.ID) - ?.map(({ ID, NAME }) => ( - <MenuItem key={`selector-group-${ID}`} value={String(ID)}> - {`${ID} - ${String(NAME)}`} - {authUser?.GID === ID && ( - <FilterVintage - style={{ - fontSize: '1rem', - marginLeft: 16 - }} - /> - )} - </MenuItem> - )), - [groups] - ) - - return ( - <TextField - select - fullWidth - defaultValue={defaultValue} - variant="outlined" - inputProps={{ 'data-cy': 'select-group' }} - label={Tr('Select a group')} - FormHelperTextProps={{ 'data-cy': 'select-group-error' }} - {...props} - > - <MenuItem value={FILTER_POOL.ALL_RESOURCES}>{Tr('Show all')}</MenuItem> - {orderGroups} - </TextField> - ) -} - -GroupSelect.propTypes = {} - -GroupSelect.defaultProps = {} - -export default GroupSelect diff --git a/src/fireedge/src/client/components/FormControl/SelectController.js b/src/fireedge/src/client/components/FormControl/SelectController.js index 52c0a6768d..29a5a0ab0c 100644 --- a/src/fireedge/src/client/components/FormControl/SelectController.js +++ b/src/fireedge/src/client/components/FormControl/SelectController.js @@ -8,18 +8,19 @@ import ErrorHelper from 'client/components/FormControl/ErrorHelper' import { Tr } from 'client/components/HOC/Translate' const SelectController = memo( - ({ control, cy, name, label, values, error }) => ( + ({ control, cy, name, label, values, error, fieldProps }) => ( <Controller as={ <TextField select - SelectProps={{ displayEmpty: true }} fullWidth + SelectProps={{ displayEmpty: true }} label={Tr(label)} inputProps={{ 'data-cy': cy }} error={Boolean(error)} helperText={Boolean(error) && <ErrorHelper label={error?.message} />} FormHelperTextProps={{ 'data-cy': `${cy}-error` }} + {...fieldProps} > {Array.isArray(values) && values?.map(({ text, value }) => ( @@ -31,6 +32,7 @@ const SelectController = memo( } name={name} control={control} + defaultValue={values[0]?.value} /> ), (prevProps, nextProps) => prevProps.error === nextProps.error @@ -45,7 +47,8 @@ SelectController.propTypes = { error: PropTypes.oneOfType([ PropTypes.bool, PropTypes.objectOf(PropTypes.any) - ]) + ]), + fieldProps: PropTypes.object } SelectController.defaultProps = { @@ -54,7 +57,8 @@ SelectController.defaultProps = { name: '', label: '', values: [], - error: false + error: false, + fieldProps: undefined } SelectController.displayName = 'SelectController' diff --git a/src/fireedge/src/client/components/FormControl/TextController.js b/src/fireedge/src/client/components/FormControl/TextController.js index 38d2651d26..e5f53b61b5 100644 --- a/src/fireedge/src/client/components/FormControl/TextController.js +++ b/src/fireedge/src/client/components/FormControl/TextController.js @@ -8,7 +8,7 @@ import { Tr } from 'client/components/HOC' import ErrorHelper from 'client/components/FormControl/ErrorHelper' const TextController = memo( - ({ control, cy, type, name, label, error }) => ( + ({ control, cy, type, name, label, error, fieldProps }) => ( <Controller render={({ value, ...props }) => <TextField @@ -21,6 +21,7 @@ const TextController = memo( helperText={Boolean(error) && <ErrorHelper label={error?.message} />} FormHelperTextProps={{ 'data-cy': `${cy}-error` }} {...props} + {...fieldProps} /> } name={name} @@ -40,7 +41,8 @@ TextController.propTypes = { error: PropTypes.oneOfType([ PropTypes.bool, PropTypes.objectOf(PropTypes.any) - ]) + ]), + fieldProps: PropTypes.object } TextController.defaultProps = { @@ -49,7 +51,8 @@ TextController.defaultProps = { type: 'text', name: '', label: '', - error: false + error: false, + fieldProps: undefined } TextController.displayName = 'TextController' diff --git a/src/fireedge/src/client/components/Forms/FormWithSchema.js b/src/fireedge/src/client/components/Forms/FormWithSchema.js index ef7be22c30..32b0a3633b 100644 --- a/src/fireedge/src/client/components/Forms/FormWithSchema.js +++ b/src/fireedge/src/client/components/Forms/FormWithSchema.js @@ -26,7 +26,7 @@ const FormWithSchema = ({ id, cy, fields }) => { return ( <Grid container spacing={1}> {fields?.map( - ({ name, type, htmlType, label, values, dependOf, tooltip, grid }) => { + ({ name, type, htmlType, label, values, dependOf, tooltip, grid, fieldProps }) => { const dataCy = `${cy}-${name}` const inputName = id ? `${id}.${name}` : name @@ -36,7 +36,7 @@ const FormWithSchema = ({ id, cy, fields }) => { ? useWatch({ control, name: id ? `${id}.${dependOf}` : dependOf }) : null - const htmlTypeValue = typeof htmlType === 'function' && dependOf + const htmlTypeValue = typeof htmlType === 'function' ? htmlType(dependValue) : htmlType @@ -53,10 +53,11 @@ const FormWithSchema = ({ id, cy, fields }) => { name: inputName, label, tooltip, - values: typeof values === 'function' && dependOf + values: typeof values === 'function' ? values(dependValue) : values, - error: inputError + error: inputError, + fieldProps })} </Grid> </HiddenInput> diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index 7fcae8bbac..e3d32988aa 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -1,12 +1,15 @@ module.exports = { SignIn: 'Sign In', + Back: 'Back', Next: 'Next', Username: 'Username', Password: 'Password', + Token2FA: '2FA Token', + SelectGroup: 'Select a group', + ShowAll: 'Show all', NotFound: 'Not found', Language: 'Language', KeepLoggedIn: 'Keep me logged in', - Token2FA: '2FA Token', SignOut: 'Sign Out', Submit: 'Submit', Response: 'Response', diff --git a/src/fireedge/src/client/containers/ApplicationsInstances/DialogInfo/dialog.js b/src/fireedge/src/client/containers/ApplicationsInstances/DialogInfo/dialog.js index c587be0b70..9974fb4b95 100644 --- a/src/fireedge/src/client/containers/ApplicationsInstances/DialogInfo/dialog.js +++ b/src/fireedge/src/client/containers/ApplicationsInstances/DialogInfo/dialog.js @@ -20,12 +20,12 @@ const CustomDialog = ({ title, handleClose, children }) => { fullScreen={isMobile} open onClose={handleClose} - maxWidth="lg" + maxWidth="xl" scroll="paper" PaperProps={{ style: { - height: isMobile ? '100%' : '80%', - width: isMobile ? '100%' : '80%' + height: isMobile ? '100%' : '90%', + width: isMobile ? '100%' : '90%' } }} > diff --git a/src/fireedge/src/client/containers/ApplicationsInstances/DialogInfo/index.js b/src/fireedge/src/client/containers/ApplicationsInstances/DialogInfo/index.js index e7937f8b33..2bd9c2803d 100644 --- a/src/fireedge/src/client/containers/ApplicationsInstances/DialogInfo/index.js +++ b/src/fireedge/src/client/containers/ApplicationsInstances/DialogInfo/index.js @@ -21,7 +21,12 @@ const DialogInfo = ({ info, handleClose }) => { const renderTabs = useMemo(() => ( <AppBar position="static"> - <Tabs value={tabSelected} onChange={(_, tab) => setTab(tab)}> + <Tabs + value={tabSelected} + variant="scrollable" + scrollButtons="auto" + onChange={(_, tab) => setTab(tab)} + > {TABS.map(({ name, icon: Icon }, idx) => <Tab key={`tab-${name}`} diff --git a/src/fireedge/src/client/containers/Login/Form.js b/src/fireedge/src/client/containers/Login/Form.js new file mode 100644 index 0000000000..c602bd1c18 --- /dev/null +++ b/src/fireedge/src/client/containers/Login/Form.js @@ -0,0 +1,91 @@ +import React, { useEffect } from 'react' +import PropTypes from 'prop-types' + +import clsx from 'clsx' +import { Box, Button, Slide } from '@material-ui/core' +import { useForm, FormProvider } from 'react-hook-form' +import { yupResolver } from '@hookform/resolvers' + +import { SignIn, Back, Next } from 'client/constants/translates' +import loginStyles from 'client/containers/Login/styles' + +import { Tr } from 'client/components/HOC' +import ButtonSubmit from 'client/components/FormControl/SubmitButton' +import FormWithSchema from 'client/components/Forms/FormWithSchema' + +const Form = ({ onBack, onSubmit, resolver, fields, error, isLoading, transitionProps }) => { + const defaultValues = resolver.default() + const classes = loginStyles() + + console.log(defaultValues) + + const { handleSubmit, setError, ...methods } = useForm({ + reValidateMode: 'onSubmit', + defaultValues, + resolver: yupResolver(resolver) + }) + + useEffect(() => { + error && setError(fields[0].name, { type: 'manual', message: error }) + }, [error]) + + return ( + <Slide + timeout={{ enter: 400 }} + mountOnEnter + unmountOnExit + {...transitionProps} + > + <Box + component="form" + onSubmit={handleSubmit(onSubmit)} + className={clsx(classes.form, { [classes.loading]: isLoading })} + > + <FormProvider {...methods}> + <FormWithSchema cy="login" fields={fields} /> + </FormProvider> + <Box> + {onBack && ( + <Button onClick={onBack} color="primary" disabled={isLoading}> + {Tr(Back)} + </Button> + )} + <ButtonSubmit + data-cy="login-button" + isSubmitting={isLoading} + label={onBack ? Tr(Next) : Tr(SignIn)} + className={classes.submit} + /> + </Box> + </Box> + </Slide> + ) +} + +Form.propTypes = { + onBack: PropTypes.func, + resolver: PropTypes.object, + fields: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string + }) + ), + onSubmit: PropTypes.func.isRequired, + error: PropTypes.string, + isLoading: PropTypes.bool, + transitionProps: PropTypes.shape({ + name: PropTypes.string + }) +} + +Form.defaultProps = { + onBack: undefined, + onSubmit: () => undefined, + resolver: {}, + fields: [], + error: undefined, + isLoading: false, + transitionProps: undefined +} + +export default Form diff --git a/src/fireedge/src/client/containers/Login/Forms/Form2fa.js b/src/fireedge/src/client/containers/Login/Forms/Form2fa.js deleted file mode 100644 index 2911e27a56..0000000000 --- a/src/fireedge/src/client/containers/Login/Forms/Form2fa.js +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react' -import { func, string } from 'prop-types' - -import { Box, Button, TextField } from '@material-ui/core' -import { useForm } from 'react-hook-form' -import { yupResolver } from '@hookform/resolvers' -import * as yup from 'yup' - -import { Token2FA, Next } from 'client/constants/translates' -import loginStyles from 'client/containers/Login/styles' - -import { Tr } from 'client/components/HOC' -import ButtonSubmit from 'client/components/FormControl/SubmitButton' -import ErrorHelper from 'client/components/FormControl/ErrorHelper' - -const Form2fa = ({ onBack, onSubmit, error }) => { - const classes = loginStyles() - - const { register, handleSubmit, errors } = useForm({ - reValidateMode: 'onSubmit', - resolver: yupResolver( - yup.object().shape({ - token2fa: yup.string().required('Authenticator is a required field') - }) - ) - }) - - const tokenError = Boolean(errors.token || error) - - return ( - <Box - component="form" - className={classes.form} - onSubmit={handleSubmit(onSubmit)} - > - <TextField - autoFocus - fullWidth - required - name="token2fa" - label={Tr(Token2FA)} - variant="outlined" - inputRef={register} - inputProps={{ 'data-cy': 'login-token' }} - error={tokenError} - helperText={ - tokenError && <ErrorHelper label={errors.token?.message || error} /> - } - FormHelperTextProps={{ 'data-cy': 'login-username-error' }} - /> - <Box> - <Button onClick={onBack} color="primary"> - Back - </Button> - <ButtonSubmit - data-cy="login-2fa-button" - isSubmitting={false} - label={Tr(Next)} - className={classes.submit} - /> - </Box> - </Box> - ) -} - -Form2fa.propTypes = { - onBack: func.isRequired, - onSubmit: func.isRequired, - error: string -} - -Form2fa.defaultProps = { - onBack: () => undefined, - onSubmit: () => undefined, - error: null -} - -export default Form2fa diff --git a/src/fireedge/src/client/containers/Login/Forms/FormGroup.js b/src/fireedge/src/client/containers/Login/Forms/FormGroup.js deleted file mode 100644 index 6277cfc8ac..0000000000 --- a/src/fireedge/src/client/containers/Login/Forms/FormGroup.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react' -import { func } from 'prop-types' - -import { Box, Button } from '@material-ui/core' -import { useForm, Controller } from 'react-hook-form' - -import GroupSelect from 'client/components/FormControl/GroupSelect' -import ButtonSubmit from 'client/components/FormControl/SubmitButton' -import { Tr } from 'client/components/HOC' -import loginStyles from 'client/containers/Login/styles' -import { Next } from 'client/constants/translates' - -function FormGroup ({ onBack, onSubmit }) { - const classes = loginStyles() - const { control, handleSubmit } = useForm() - - return ( - <Box - component="form" - className={classes.form} - onSubmit={handleSubmit(onSubmit)} - > - <Controller as={GroupSelect} name="group" control={control} /> - <Button onClick={onBack}>Logout</Button> - <ButtonSubmit - data-cy="login-group-button" - isSubmitting={false} - label={Tr(Next)} - className={classes.submit} - /> - </Box> - ) -} - -FormGroup.propTypes = { - onBack: func.isRequired, - onSubmit: func.isRequired -} - -FormGroup.defaultProps = { - onBack: () => undefined, - onSubmit: () => undefined -} - -FormGroup.propTypes = {} - -FormGroup.defaultProps = {} - -export default FormGroup diff --git a/src/fireedge/src/client/containers/Login/Forms/FormUser.js b/src/fireedge/src/client/containers/Login/Forms/FormUser.js deleted file mode 100644 index dc6f0f161e..0000000000 --- a/src/fireedge/src/client/containers/Login/Forms/FormUser.js +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react' -import { func, string } from 'prop-types' -import { Box, Checkbox, TextField, FormControlLabel } from '@material-ui/core' -import { useForm } from 'react-hook-form' -import { yupResolver } from '@hookform/resolvers' -import * as yup from 'yup' - -import { - SignIn, - Username, - Password, - KeepLoggedIn -} from 'client/constants/translates' -import { Tr } from 'client/components/HOC' -import ButtonSubmit from 'client/components/FormControl/SubmitButton' -import ErrorHelper from 'client/components/FormControl/ErrorHelper' -import loginStyles from 'client/containers/Login/styles' - -function FormUser ({ onSubmit, error }) { - const classes = loginStyles() - - const { register, handleSubmit, errors } = useForm({ - reValidateMode: 'onSubmit', - resolver: yupResolver( - yup.object().shape({ - user: yup.string().required('Username is a required field'), - token: yup.string().required('Password is a required field'), - remember: yup.boolean() - }) - ) - }) - - const userError = Boolean(errors.user || error) - const passError = Boolean(errors.pass) - - return ( - <Box - component="form" - className={classes.form} - onSubmit={handleSubmit(onSubmit)} - > - <TextField - autoFocus - fullWidth - required - name="user" - autoComplete="username" - label={Tr(Username)} - variant="outlined" - inputRef={register} - inputProps={{ 'data-cy': 'login-username' }} - error={userError} - helperText={ - userError && <ErrorHelper label={errors.user?.message || error} /> - } - FormHelperTextProps={{ 'data-cy': 'login-username-error' }} - /> - <TextField - fullWidth - required - name="token" - type="password" - autoComplete="current-password" - label={Tr(Password)} - variant="outlined" - inputRef={register} - inputProps={{ 'data-cy': 'login-password' }} - error={passError} - helperText={passError && <ErrorHelper label={errors.pass?.message} />} - FormHelperTextProps={{ 'data-cy': 'login-password-error' }} - /> - <FormControlLabel - control={ - <Checkbox - name="remember" - defaultValue={false} - color="primary" - inputRef={register} - inputProps={{ 'data-cy': 'login-remember' }} - /> - } - label={Tr(KeepLoggedIn)} - labelPlacement="end" - /> - <ButtonSubmit - data-cy="login-button" - isSubmitting={false} - label={Tr(SignIn)} - className={classes.submit} - /> - </Box> - ) -} - -FormUser.propTypes = { - onSubmit: func.isRequired, - error: string -} - -FormUser.defaultProps = { - onSubmit: () => undefined, - error: null -} - -export default FormUser diff --git a/src/fireedge/src/client/containers/Login/index.js b/src/fireedge/src/client/containers/Login/index.js index 65f3c8d85b..2c3ad76df1 100644 --- a/src/fireedge/src/client/containers/Login/index.js +++ b/src/fireedge/src/client/containers/Login/index.js @@ -1,23 +1,23 @@ -import React, { useState } from 'react' -import { - Paper, - Box, - Container, - Slide, - LinearProgress, - useMediaQuery -} from '@material-ui/core' +import React, { useMemo, useState } from 'react' + +import { Paper, Box, Container, LinearProgress, useMediaQuery } from '@material-ui/core' import useAuth from 'client/hooks/useAuth' - -import FormUser from 'client/containers/Login/Forms/FormUser' -import Form2fa from 'client/containers/Login/Forms/Form2fa' -import FormGroup from 'client/containers/Login/Forms/FormGroup' -import loginStyles from 'client/containers/Login/styles' import Logo from 'client/icons/logo' import { ONEADMIN_ID } from 'client/constants' -const STEP = { +import { + FORM_USER_FIELDS, + FORM_USER_SCHEMA, + FORM_2FA_FIELDS, + FORM_2FA_SCHEMA, + FORM_GROUP_FIELDS, + FORM_GROUP_SCHEMA +} from 'client/containers/Login/schema' +import Form from 'client/containers/Login/Form' +import loginStyles from 'client/containers/Login/styles' + +const STEPS = { USER_FORM: 0, FA2_FORM: 1, GROUP_FORM: 2 @@ -27,7 +27,7 @@ function Login () { const classes = loginStyles() const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs')) const [user, setUser] = useState(undefined) - const [step, setStep] = useState(STEP.USER_FORM) + const [step, setStep] = useState(STEPS.USER_FORM) const { isLoading, error, @@ -41,10 +41,10 @@ function Login () { login({ ...user, ...dataForm }).then(data => { if (data?.token) { getAuthInfo().then(() => { - data?.id !== ONEADMIN_ID && setStep(STEP.GROUP_FORM) + data?.id !== ONEADMIN_ID && setStep(STEPS.GROUP_FORM) }) } else { - setStep(data ? STEP.FA2_FORM : step) + setStep(data ? STEPS.FA2_FORM : step) setUser(data ? dataForm : user) } }) @@ -55,7 +55,7 @@ function Login () { const handleBack = () => { logout() setUser(undefined) - setStep(STEP.USER_FORM) + setStep(STEPS.USER_FORM) } return ( @@ -67,50 +67,46 @@ function Login () { > {isLoading && <LinearProgress className={classes.loading} />} <Paper variant="outlined" className={classes.paper}> - <Logo width="100%" height={100} withText viewBox="140 140 380 360" /> - <Box className={classes.wrapper}> - <Slide - direction="right" - timeout={{ enter: 400 }} - in={step === STEP.USER_FORM} - appear={false} - mountOnEnter - unmountOnExit - > - <Box style={{ opacity: isLoading ? 0.7 : 1 }}> - <FormUser onSubmit={handleSubmitUser} error={error} /> - </Box> - </Slide> - </Box> - <Box> - <Slide - direction="left" - timeout={{ enter: 400 }} - in={step === STEP.FA2_FORM} - mountOnEnter - unmountOnExit - > - <Box style={{ opacity: isLoading ? 0.7 : 1 }}> - <Form2fa - onBack={handleBack} - onSubmit={handleSubmitUser} - error={error} - /> - </Box> - </Slide> - </Box> - <Box className={classes.wrapper}> - <Slide - direction="left" - timeout={{ enter: 400 }} - in={step === STEP.GROUP_FORM} - mountOnEnter - unmountOnExit - > - <Box style={{ opacity: isLoading ? 0.7 : 1 }}> - <FormGroup onBack={handleBack} onSubmit={handleSubmitGroup} /> - </Box> - </Slide> + {useMemo(() => ( + <Logo width="100%" height={100} withText viewBox="140 140 380 360" /> + ), [])} + <Box className={classes.wrapperForm}> + {step === STEPS.USER_FORM && <Form + transitionProps={{ + direction: 'right', + in: step === STEPS.USER_FORM, + appear: false + }} + onSubmit={handleSubmitUser} + resolver={FORM_USER_SCHEMA} + fields={FORM_USER_FIELDS} + error={error} + isLoading={isLoading} + />} + {step === STEPS.FA2_FORM && <Form + transitionProps={{ + direction: 'left', + in: step === STEPS.FA2_FORM + }} + onBack={handleBack} + onSubmit={handleSubmitUser} + resolver={FORM_2FA_SCHEMA} + fields={FORM_2FA_FIELDS} + error={error} + isLoading={isLoading} + />} + {step === STEPS.GROUP_FORM && <Form + transitionProps={{ + direction: 'left', + in: step === STEPS.GROUP_FORM + }} + onBack={handleBack} + onSubmit={handleSubmitGroup} + resolver={FORM_GROUP_SCHEMA} + fields={FORM_GROUP_FIELDS} + error={error} + isLoading={isLoading} + />} </Box> </Paper> </Container> diff --git a/src/fireedge/src/client/containers/Login/schema.js b/src/fireedge/src/client/containers/Login/schema.js new file mode 100644 index 0000000000..3ce543bcd5 --- /dev/null +++ b/src/fireedge/src/client/containers/Login/schema.js @@ -0,0 +1,123 @@ +import React from 'react' + +import SelectedIcon from '@material-ui/icons/FilterVintage' +import * as yup from 'yup' + +import useAuth from 'client/hooks/useAuth' +import useOpennebula from 'client/hooks/useOpennebula' +import { INPUT_TYPES, FILTER_POOL } from 'client/constants' +import { getValidationFromFields } from 'client/utils/helpers' +import { + Username, + Password, + KeepLoggedIn, + Token2FA, + SelectGroup, + ShowAll +} from 'client/constants/translates' + +export const USERNAME = { + name: 'user', + label: Username, + type: INPUT_TYPES.TEXT, + validation: yup + .string() + .trim() + .required('Username is a required field') + .default(null), + grid: { md: 12 }, + fieldProps: { + autoFocus: true, + required: true, + autoComplete: 'username', + variant: 'outlined' + } +} + +export const PASSWORD = { + name: 'token', + label: Password, + type: INPUT_TYPES.TEXT, + htmlType: 'password', + validation: yup + .string() + .trim() + .required('Password is a required field') + .default(null), + grid: { md: 12 }, + fieldProps: { + required: true, + autoComplete: 'current-password', + variant: 'outlined' + } +} + +export const REMEMBER = { + name: 'remember', + label: KeepLoggedIn, + type: INPUT_TYPES.CHECKBOX, + validation: yup + .boolean() + .default(false), + grid: { md: 12 } +} + +export const TOKEN = { + name: 'token2fa', + label: Token2FA, + type: INPUT_TYPES.TEXT, + validation: yup + .string() + .trim() + .required('Authenticator is a required field') + .default(null), + grid: { md: 12 }, + fieldProps: { + autoFocus: true, + required: true, + variant: 'outlined' + } +} + +export const GROUP = { + name: 'group', + label: SelectGroup, + type: INPUT_TYPES.SELECT, + values: () => { + const { authUser } = useAuth() + const { groups } = useOpennebula() + + return [{ text: ShowAll, value: FILTER_POOL.ALL_RESOURCES }] + .concat(groups + .sort((a, b) => a.ID - b.ID) + .map(({ ID, NAME }) => ({ + text: ( + <> + {`${ID} - ${String(NAME)}`} + {authUser?.GID === ID && ( + <SelectedIcon style={{ fontSize: '1rem', marginLeft: 16 }} /> + )} + </> + ), + value: String(ID) + })) + ) + }, + validation: yup + .string() + .trim() + .nullable() + .default(FILTER_POOL.ALL_RESOURCES), + grid: { md: 12 }, + fieldProps: { + variant: 'outlined' + } +} + +export const FORM_USER_FIELDS = [USERNAME, PASSWORD, REMEMBER] +export const FORM_2FA_FIELDS = [TOKEN] +export const FORM_GROUP_FIELDS = [GROUP] + +export const FORM_USER_SCHEMA = yup.object(getValidationFromFields(FORM_USER_FIELDS)) +export const FORM_2FA_SCHEMA = yup.object(getValidationFromFields(FORM_2FA_FIELDS)) +export const FORM_GROUP_SCHEMA = yup.object(getValidationFromFields(FORM_GROUP_FIELDS)) diff --git a/src/fireedge/src/client/containers/Login/styles.js b/src/fireedge/src/client/containers/Login/styles.js index cc218d66e4..2e51d8328f 100644 --- a/src/fireedge/src/client/containers/Login/styles.js +++ b/src/fireedge/src/client/containers/Login/styles.js @@ -13,7 +13,7 @@ export default makeStyles(theme => justifyContent: 'center', height: '100vh' }, - loading: { + progress: { height: 4, width: '100%', [theme.breakpoints.only('xs')]: { @@ -23,18 +23,35 @@ export default makeStyles(theme => }, paper: { overflow: 'hidden', - padding: theme.spacing(3), - minHeight: 400, + padding: theme.spacing(2), + height: 440, + [theme.breakpoints.up('xs')]: { + display: 'flex', + flexDirection: 'column' + }, [theme.breakpoints.only('xs')]: { border: 'none', height: 'calc(100vh - 4px)', backgroundColor: 'transparent' } }, + wrapperForm: { + padding: theme.spacing(), + flexGrow: 1, + display: 'flex', + overflow: 'hidden' + }, form: { + width: '100%', + flexShrink: 0, display: 'flex', flexDirection: 'column', - justifyContent: 'center' + [theme.breakpoints.up('xs')]: { + justifyContent: 'center' + } + }, + loading: { + opacity: 0.7 }, helper: { animation: '1s ease-out 0s 1' diff --git a/src/fireedge/src/client/icons/logo.js b/src/fireedge/src/client/icons/logo.js index 5904923f40..67acbf10fa 100644 --- a/src/fireedge/src/client/icons/logo.js +++ b/src/fireedge/src/client/icons/logo.js @@ -1,7 +1,7 @@ -import React from 'react' +import React, { memo } from 'react' import { number, string, bool, oneOfType } from 'prop-types' -const Logo = ({ width, height, spinner, withText, viewBox, ...props }) => { +const Logo = memo(({ width, height, spinner, withText, viewBox, ...props }) => { const cloudColor = { child1: { from: '#0098c3', to: '#ffffff' }, child2: { from: '#0098c3', to: '#ffffff' }, @@ -83,7 +83,7 @@ const Logo = ({ width, height, spinner, withText, viewBox, ...props }) => { )} </svg> ) -} +}) Logo.propTypes = { width: oneOfType([number, string]).isRequired, @@ -101,4 +101,6 @@ Logo.defaultProps = { withText: false } +Logo.displayName = 'LogoOne' + export default Logo