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