From 4b5b2ad763788fd47147f7ebf4340bf7ac7d49de Mon Sep 17 00:00:00 2001 From: Sergio Betanzos Date: Mon, 7 Sep 2020 12:30:13 +0200 Subject: [PATCH] F #3951: Add form stepper component (#185) * Add new endpoints * Add pool actions * Add form stepper component * Add create application form --- src/fireedge/src/public/actions/pool.js | 8 +- .../src/public/assets/theme/defaults.js | 4 + .../public/components/Cards/NetworkCard.js | 81 ++++++ .../src/public/components/Cards/RoleCard.js | 105 ++++++++ .../src/public/components/Cards/index.js | 4 + .../components/Dialogs/NetworkDialog.js | 234 ++++++++++++++++++ .../public/components/Dialogs/RoleDialog.js | 96 +++++++ .../src/public/components/Dialogs/index.js | 4 + .../src/public/components/Footer/index.js | 20 +- .../components/FormControl/GroupSelect.js | 9 +- .../public/components/FormStepper/FormStep.js | 124 ++++++++++ .../components/FormStepper/MobileStepper.js | 55 ++++ .../public/components/FormStepper/Stepper.js | 66 +++++ .../public/components/FormStepper/index.js | 96 +++++++ .../components/HOC/InternalLayout/styles.js | 5 +- .../containers/Application/Create/index.js | 48 ++++ .../containers/Application/Create/steps.js | 45 ++++ .../containers/Application/Create/styles.js | 3 + .../Users.js => Application/Deploy.js} | 18 +- .../Groups.js => Application/Manage.js} | 19 +- .../public/containers/Application/index.js | 8 +- .../src/public/containers/Dashboard/index.js | 9 +- .../src/public/containers/Dashboard/styles.js | 8 + .../containers/Login/{ => Forms}/Form2fa.js | 0 .../containers/Login/{ => Forms}/FormGroup.js | 0 .../containers/Login/{ => Forms}/FormUser.js | 0 .../src/public/containers/Login/index.js | 6 +- .../src/public/containers/System/index.js | 4 - .../src/public/containers/TestApi/styles.js | 3 +- src/fireedge/src/public/hooks/useAuth.js | 4 +- .../src/public/hooks/useOpennebula.js | 69 ++++-- .../src/public/reducers/opennebula.js | 4 +- src/fireedge/src/public/router/endpoints.js | 118 +++------ src/fireedge/src/public/services/groups.js | 15 +- src/fireedge/src/public/services/pool.js | 86 +++++++ src/fireedge/src/public/services/users.js | 14 +- src/fireedge/src/public/utils/request.js | 4 +- 37 files changed, 1203 insertions(+), 193 deletions(-) create mode 100644 src/fireedge/src/public/components/Cards/NetworkCard.js create mode 100644 src/fireedge/src/public/components/Cards/RoleCard.js create mode 100644 src/fireedge/src/public/components/Cards/index.js create mode 100644 src/fireedge/src/public/components/Dialogs/NetworkDialog.js create mode 100644 src/fireedge/src/public/components/Dialogs/RoleDialog.js create mode 100644 src/fireedge/src/public/components/Dialogs/index.js create mode 100644 src/fireedge/src/public/components/FormStepper/FormStep.js create mode 100644 src/fireedge/src/public/components/FormStepper/MobileStepper.js create mode 100644 src/fireedge/src/public/components/FormStepper/Stepper.js create mode 100644 src/fireedge/src/public/components/FormStepper/index.js create mode 100644 src/fireedge/src/public/containers/Application/Create/index.js create mode 100644 src/fireedge/src/public/containers/Application/Create/steps.js create mode 100644 src/fireedge/src/public/containers/Application/Create/styles.js rename src/fireedge/src/public/containers/{System/Users.js => Application/Deploy.js} (85%) rename src/fireedge/src/public/containers/{System/Groups.js => Application/Manage.js} (86%) create mode 100644 src/fireedge/src/public/containers/Dashboard/styles.js rename src/fireedge/src/public/containers/Login/{ => Forms}/Form2fa.js (100%) rename src/fireedge/src/public/containers/Login/{ => Forms}/FormGroup.js (100%) rename src/fireedge/src/public/containers/Login/{ => Forms}/FormUser.js (100%) delete mode 100644 src/fireedge/src/public/containers/System/index.js create mode 100644 src/fireedge/src/public/services/pool.js diff --git a/src/fireedge/src/public/actions/pool.js b/src/fireedge/src/public/actions/pool.js index 002b50e493..b4bc73bd55 100644 --- a/src/fireedge/src/public/actions/pool.js +++ b/src/fireedge/src/public/actions/pool.js @@ -50,13 +50,13 @@ module.exports = { type: SUCCESS_ONE_REQUEST, payload: { apps } }), - setVNetworks: virtualNetworks => ({ + setVNetworks: vNetworks => ({ type: SUCCESS_ONE_REQUEST, - payload: { virtualNetworks } + payload: { vNetworks } }), - setNetworkTemplates: networkTemplates => ({ + setVNetworkTemplates: vNetworksTemplates => ({ type: SUCCESS_ONE_REQUEST, - payload: { networkTemplates } + payload: { vNetworksTemplates } }), setSecGroups: securityGroups => ({ type: SUCCESS_ONE_REQUEST, diff --git a/src/fireedge/src/public/assets/theme/defaults.js b/src/fireedge/src/public/assets/theme/defaults.js index 0aaa41fcb1..c54fd59b48 100644 --- a/src/fireedge/src/public/assets/theme/defaults.js +++ b/src/fireedge/src/public/assets/theme/defaults.js @@ -16,6 +16,10 @@ export const toolbar = { sm: 64 }; +export const footer = { + regular: 30 +}; + export const sidebar = { minified: 60, fixed: 240 diff --git a/src/fireedge/src/public/components/Cards/NetworkCard.js b/src/fireedge/src/public/components/Cards/NetworkCard.js new file mode 100644 index 0000000000..00e8c320ec --- /dev/null +++ b/src/fireedge/src/public/components/Cards/NetworkCard.js @@ -0,0 +1,81 @@ +import React from 'react'; + +import { + makeStyles, + Card, + Button, + CardHeader, + CardActions, + Fade +} from '@material-ui/core'; + +import { Tr } from 'client/components/HOC'; + +const useStyles = makeStyles(theme => ({ + root: { + height: '100%', + minHeight: 140, + display: 'flex', + flexDirection: 'column' + }, + header: { + overflowX: 'hidden', + flexGrow: 1 + }, + subheader: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'initial', + display: '-webkit-box', + lineClamp: 2, + boxOrient: 'vertical' + }, + remove: { + backgroundColor: theme.palette.error.dark + } +})); + +const NetworkCard = React.memo( + ({ info, handleEdit, handleClone, handleRemove }) => { + const classes = useStyles(); + const { mandatory, name, description, type, id, extra } = info; + + return ( + + + + + + + + + + + ); + } +); + +export default NetworkCard; diff --git a/src/fireedge/src/public/components/Cards/RoleCard.js b/src/fireedge/src/public/components/Cards/RoleCard.js new file mode 100644 index 0000000000..6cb7445efd --- /dev/null +++ b/src/fireedge/src/public/components/Cards/RoleCard.js @@ -0,0 +1,105 @@ +import React from 'react'; + +import { + makeStyles, + Card, + Button, + CardHeader, + CardActions, + Badge, + Fade +} from '@material-ui/core'; +import DesktopWindowsIcon from '@material-ui/icons/DesktopWindows'; + +import { Tr } from 'client/components/HOC'; + +const useStyles = makeStyles(theme => ({ + root: { + height: '100%', + minHeight: 140, + display: 'flex', + flexDirection: 'column' + }, + header: { + overflowX: 'hidden', + flexGrow: 1 + }, + headerContent: {}, + title: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'initial', + display: '-webkit-box', + lineClamp: 2, + boxOrient: 'vertical' + } +})); + +const RoleCard = React.memo( + ({ info, handleEdit, handleClone, handleRemove }) => { + const classes = useStyles(); + const { + name = 'Role name', + cardinality, + vm_template = 0, + elasticity_policies, + scheduled_policies + } = info; + + const ConditionalWrapper = ({ condition, wrapper, children }) => + condition ? wrapper(children) : children; + console.log(info); + return ( + + + 1} + wrapper={children => ( + + {children} + + )} + > + + + } + className={classes.header} + classes={{ content: classes.headerContent }} + title={name} + titleTypographyProps={{ + variant: 'body2', + noWrap: true, + className: classes.title, + title: name + }} + subheader={`Template id: ${vm_template}`} + subheaderTypographyProps={{ + variant: 'body2', + noWrap: true, + title: `Template id: ${vm_template}` + }} + /> + + + + + + + ); + } +); + +export default RoleCard; diff --git a/src/fireedge/src/public/components/Cards/index.js b/src/fireedge/src/public/components/Cards/index.js new file mode 100644 index 0000000000..81d55ce04a --- /dev/null +++ b/src/fireedge/src/public/components/Cards/index.js @@ -0,0 +1,4 @@ +import NetworkCard from './NetworkCard'; +import RoleCard from './RoleCard'; + +export { NetworkCard, RoleCard }; diff --git a/src/fireedge/src/public/components/Dialogs/NetworkDialog.js b/src/fireedge/src/public/components/Dialogs/NetworkDialog.js new file mode 100644 index 0000000000..6fe08c36ef --- /dev/null +++ b/src/fireedge/src/public/components/Dialogs/NetworkDialog.js @@ -0,0 +1,234 @@ +import React, { useMemo, useEffect } from 'react'; + +import { + makeStyles, + useMediaQuery, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Grid, + FormControlLabel, + Checkbox, + TextField, + MenuItem +} from '@material-ui/core'; +import { useForm, Controller } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers'; +import * as yup from 'yup'; + +import useOpennebula from 'client/hooks/useOpennebula'; +import ErrorHelper from 'client/components/FormControl/ErrorHelper'; +import { Tr } from 'client/components/HOC'; + +const useStyles = makeStyles(theme => ({})); + +const SELECT = { + template: 'template', + network: 'network' +}; + +const TYPES_NETWORKS = { + template_id: { text: 'Create', select: SELECT.template, extra: true }, + reserve_from: { text: 'Reserve', select: SELECT.network, extra: true }, + id: { text: 'Existing', select: SELECT.network, extra: false } +}; + +const ID_CY = 'form-network'; + +const NetworkDialog = React.memo( + ({ open, info: network, onSubmit, onCancel }) => { + const classes = useStyles(); + const { vNetworks, vNetworksTemplates } = useOpennebula(); + const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs')); + + const { register, handleSubmit, errors, control, watch } = useForm({ + reValidateMode: 'onSubmit', + defaultValues: { + type: Object.keys(TYPES_NETWORKS)[0], + ...network + }, + resolver: yupResolver( + yup.object().shape({ + mandatory: yup.boolean().required(), + name: yup.string().required('Name is a required field'), + description: yup.string(), + type: yup + .string() + .oneOf(Object.keys(TYPES_NETWORKS)) + .required('Type is required field'), + id: yup + .string() + .when('type', { + is: type => + Object.entries(TYPES_NETWORKS)?.some( + ([key, { select }]) => + type === key && select === SELECT.network + ), + then: yup.string().required('Network is required field'), + otherwise: yup + .string() + .required('Network template is required field') + }) + .required(), + extra: yup.string() + }) + ) + }); + + const { type } = watch(); + const typeSelected = TYPES_NETWORKS[type]?.select; + + const selectType = + typeSelected === SELECT.network ? vNetworks : vNetworksTemplates; + + return ( + + + {network?.name ? 'Edit network' : 'New network'} + + + + + + } + label={Tr('Mandatory')} + labelPlacement="end" + /> + + + + } + FormHelperTextProps={{ 'data-cy': `${ID_CY}-name-error` }} + /> + + + + ) + } + FormHelperTextProps={{ + 'data-cy': `${ID_CY}-description-error` + }} + /> + + + + ) + } + FormHelperTextProps={{ + 'data-cy': `${ID_CY}-type-error` + }} + > + {Object.entries(TYPES_NETWORKS).map(([key, { text }]) => ( + + {text} + + ))} + + } + name="type" + control={control} + /> + + + + } + FormHelperTextProps={{ + 'data-cy': `${ID_CY}-id-error` + }} + > + {selectType?.map(({ ID, NAME }) => ( + + {NAME} + + ))} + + } + name="id" + control={control} + /> + + + + } + FormHelperTextProps={{ + 'data-cy': `${ID_CY}-extra-error` + }} + /> + + + + + + + + + ); + } +); + +export default NetworkDialog; diff --git a/src/fireedge/src/public/components/Dialogs/RoleDialog.js b/src/fireedge/src/public/components/Dialogs/RoleDialog.js new file mode 100644 index 0000000000..68ffbaed45 --- /dev/null +++ b/src/fireedge/src/public/components/Dialogs/RoleDialog.js @@ -0,0 +1,96 @@ +import React, { useMemo, useEffect } from 'react'; + +import { + makeStyles, + useMediaQuery, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Grid, + FormControlLabel, + Checkbox, + TextField, + MenuItem +} from '@material-ui/core'; +import { useForm, Controller } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers'; +import * as yup from 'yup'; + +import useOpennebula from 'client/hooks/useOpennebula'; +import ErrorHelper from 'client/components/FormControl/ErrorHelper'; +import { Tr } from 'client/components/HOC'; + +const useStyles = makeStyles(theme => ({})); + +const ID_CY = 'form-role'; + +const NetworkDialog = React.memo(({ open, info: role, onSubmit, onCancel }) => { + // const classes = useStyles(); + const { templates } = useOpennebula(); + const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs')); + + const { register, handleSubmit, errors, control } = useForm({ + reValidateMode: 'onSubmit', + defaultValues: role + // resolver: yupResolver(yup.object().shape({})) + }); + + return ( + + + {role?.name ? 'Edit role' : 'New role'} + + + + + {'ROLE FORM'} + {/* + ) + } + FormHelperTextProps={{ + 'data-cy': `${ID_CY}-id-error` + }} + > + {templates?.map(({ ID, NAME }) => ( + + {NAME} + + ))} + + } + name="template" + control={control} + /> */} + + + + + + + + + ); +}); + +export default NetworkDialog; diff --git a/src/fireedge/src/public/components/Dialogs/index.js b/src/fireedge/src/public/components/Dialogs/index.js new file mode 100644 index 0000000000..537beed14d --- /dev/null +++ b/src/fireedge/src/public/components/Dialogs/index.js @@ -0,0 +1,4 @@ +import NetworkDialog from './NetworkDialog'; +import RoleDialog from './RoleDialog'; + +export { NetworkDialog, RoleDialog }; diff --git a/src/fireedge/src/public/components/Footer/index.js b/src/fireedge/src/public/components/Footer/index.js index 80ad160676..f2a0c74152 100644 --- a/src/fireedge/src/public/components/Footer/index.js +++ b/src/fireedge/src/public/components/Footer/index.js @@ -14,7 +14,7 @@ /* -------------------------------------------------------------------------- */ import React from 'react'; -import { Box, Link } from '@material-ui/core'; +import { Box, Link, Typography } from '@material-ui/core'; import footerStyles from 'client/components/Footer/styles'; import { by } from 'client/constants'; @@ -26,14 +26,16 @@ const Footer = React.memo(() => { return ( - {'Made with'} - - {'❤️'} - - {'by'} - - {text} - + + {'Made with'} + + {'❤️'} + + {'by'} + + {text} + + ); }); diff --git a/src/fireedge/src/public/components/FormControl/GroupSelect.js b/src/fireedge/src/public/components/FormControl/GroupSelect.js index 23400a297c..195e962cf5 100644 --- a/src/fireedge/src/public/components/FormControl/GroupSelect.js +++ b/src/fireedge/src/public/components/FormControl/GroupSelect.js @@ -42,7 +42,14 @@ const GroupSelect = props => { ?.map(({ ID, NAME }) => ( {`${ID} - ${String(NAME)}`} - {authUser?.GID === ID && } + {authUser?.GID === ID && ( + + )} )), [groups] diff --git a/src/fireedge/src/public/components/FormStepper/FormStep.js b/src/fireedge/src/public/components/FormStepper/FormStep.js new file mode 100644 index 0000000000..1aad95efca --- /dev/null +++ b/src/fireedge/src/public/components/FormStepper/FormStep.js @@ -0,0 +1,124 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; + +import { useFormContext } from 'react-hook-form'; +import { + makeStyles, + Box, + CardActionArea, + CardContent, + Card, + Grid +} from '@material-ui/core'; +import { Add } from '@material-ui/icons'; + +const useStyles = makeStyles(() => ({ + cardPlus: { + height: '100%', + minHeight: 140, + display: 'flex', + textAlign: 'center' + } +})); + +function FormStep({ step, data, setFormData }) { + const classes = useStyles(); + const [dialogFormData, setDialogFormData] = useState({}); + const [showDialog, setShowDialog] = useState(false); + const { reset } = useFormContext(); + + const { id, addAction, InfoComponent, DialogComponent, DEFAULT_DATA } = step; + const { [id]: stepData } = data; + + useEffect(() => { + reset({ ...data }, { errors: true }); + }, [id, data]); + + const handleSubmit = values => { + setFormData(prevData => ({ + ...prevData, + [id]: Object.assign(prevData[id], { + [dialogFormData.index]: values + }) + })); + + setShowDialog(false); + }; + + const handleOpen = (index = stepData?.length) => { + const openData = stepData[index] ?? DEFAULT_DATA; + + setDialogFormData({ index, data: openData }); + setShowDialog(true); + }; + + const handleClone = index => { + const cloneData = { ...stepData[index], name: 'clone' }; + + setFormData(prevData => { + prevData[id].splice(index + 1, 0, cloneData); + return prevData; + }); + }; + + const handleRemove = indexRemove => { + // TODO confirmation?? + setFormData(prevData => ({ + ...prevData, + [id]: prevData[id]?.filter((_, index) => index !== indexRemove) + })); + }; + + const handleClose = () => setShowDialog(false); + + return ( + + + {addAction && ( + + + handleOpen()}> + + + + + + + )} + {Array.isArray(stepData) && + stepData?.map((info, index) => ( + + handleOpen(index)} + handleClone={() => handleClone(index)} + handleRemove={() => handleRemove(index)} + /> + + ))} + + {showDialog && ( + + )} + + ); +} + +FormStep.propTypes = { + step: PropTypes.objectOf(PropTypes.object).isRequired, + data: PropTypes.objectOf(PropTypes.object).isRequired, + setFormData: PropTypes.func +}; + +FormStep.defaultProps = { + step: {}, + data: {}, + setFormData: data => data +}; + +export default FormStep; diff --git a/src/fireedge/src/public/components/FormStepper/MobileStepper.js b/src/fireedge/src/public/components/FormStepper/MobileStepper.js new file mode 100644 index 0000000000..f4a23fd197 --- /dev/null +++ b/src/fireedge/src/public/components/FormStepper/MobileStepper.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Button, MobileStepper } from '@material-ui/core'; +import { KeyboardArrowLeft, KeyboardArrowRight } from '@material-ui/icons'; + +import { Tr } from 'client/components/HOC'; + +const CustomMobileStepper = ({ + totalSteps, + activeStep, + lastStep, + disabledBack, + handleNext, + handleBack +}) => ( + + {Tr('Back')} + + } + nextButton={ + + } + /> +); + +CustomMobileStepper.propTypes = { + totalSteps: PropTypes.number, + activeStep: PropTypes.number, + lastStep: PropTypes.number, + disabledBack: PropTypes.bool, + handleNext: PropTypes.func, + handleBack: PropTypes.func +}; + +CustomMobileStepper.defaultProps = { + totalSteps: 0, + activeStep: 0, + lastStep: 0, + disabledBack: false, + handleNext: () => undefined, + handleBack: () => undefined +}; + +export default CustomMobileStepper; diff --git a/src/fireedge/src/public/components/FormStepper/Stepper.js b/src/fireedge/src/public/components/FormStepper/Stepper.js new file mode 100644 index 0000000000..a0d5a194a6 --- /dev/null +++ b/src/fireedge/src/public/components/FormStepper/Stepper.js @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Button, Stepper, Step, StepLabel, Box } from '@material-ui/core'; + +import { Tr } from 'client/components/HOC'; + +/* +position: sticky; +top: 0; +backdrop-filter: blur(5px); +background: #000000aa; +z-index: 1; +*/ + +const CustomStepper = ({ + steps, + activeStep, + lastStep, + disabledBack, + handleNext, + handleBack +}) => ( + <> + + {steps?.map(({ label }) => ( + + {label} + + ))} + + + + + + +); + +CustomStepper.propTypes = { + steps: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.oneOf([PropTypes.string, PropTypes.number]).isRequired, + label: PropTypes.string.isRequired + }) + ), + activeStep: PropTypes.number.isRequired, + lastStep: PropTypes.number.isRequired, + disabledBack: PropTypes.bool.isRequired, + handleNext: PropTypes.func, + handleBack: PropTypes.func +}; + +CustomStepper.defaultProps = { + steps: [], + activeStep: 0, + lastStep: 0, + disabledBack: false, + handleNext: () => undefined, + handleBack: () => undefined +}; + +export default CustomStepper; diff --git a/src/fireedge/src/public/components/FormStepper/index.js b/src/fireedge/src/public/components/FormStepper/index.js new file mode 100644 index 0000000000..0c8dcfccf8 --- /dev/null +++ b/src/fireedge/src/public/components/FormStepper/index.js @@ -0,0 +1,96 @@ +import React, { useState, useMemo, useCallback } from 'react'; +import PropTypes from 'prop-types'; + +import { useFormContext } from 'react-hook-form'; +import { useMediaQuery } from '@material-ui/core'; + +import CustomMobileStepper from 'client/components/FormStepper/MobileStepper'; +import CustomStepper from 'client/components/FormStepper/Stepper'; +import { console } from 'window-or-global'; + +const FIRST_STEP = 0; + +const FormStepper = ({ steps, initialValue, onSubmit }) => { + const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs')); + const { watch } = useFormContext(); + const [activeStep, setActiveStep] = useState(FIRST_STEP); + const [formData, setFormData] = useState(initialValue); + + const totalSteps = useMemo(() => steps?.length, [steps]); + const lastStep = useMemo(() => totalSteps - 1, [totalSteps]); + const disabledBack = useMemo(() => activeStep === FIRST_STEP, [activeStep]); + + const handleNext = useCallback(() => { + setFormData(data => ({ ...data, ...watch() })); + + if (activeStep === lastStep) { + onSubmit(formData); + } else setActiveStep(prevActiveStep => prevActiveStep + 1); + }, [activeStep]); + + const handleBack = useCallback(() => { + if (activeStep <= FIRST_STEP) return; + + setActiveStep(prevActiveStep => prevActiveStep - 1); + setFormData(data => ({ ...data, ...watch() })); + }, [activeStep]); + + return ( + <> + {/* STEPPER */} + {isMobile ? ( + + ) : ( + + )} + + {/* FORM CONTENT */} + {React.useMemo(() => { + const { content: Content, ...rest } = steps[activeStep]; + + return ( + + ); + }, [activeStep, formData, setFormData])} + + ); +}; + +FormStepper.propTypes = { + steps: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + label: PropTypes.string.isRequired, + content: PropTypes.node.isRequired, + dependOf: PropTypes.bool + }) + ), + initialValue: PropTypes.objectOf(PropTypes.object), + onSubmit: PropTypes.func +}; + +FormStepper.defaultProps = { + steps: [], + initialValue: {}, + onSubmit: dataForm => console.log(dataForm) +}; + +export default FormStepper; diff --git a/src/fireedge/src/public/components/HOC/InternalLayout/styles.js b/src/fireedge/src/public/components/HOC/InternalLayout/styles.js index 31515daf5d..355876b410 100644 --- a/src/fireedge/src/public/components/HOC/InternalLayout/styles.js +++ b/src/fireedge/src/public/components/HOC/InternalLayout/styles.js @@ -1,5 +1,5 @@ import { makeStyles } from '@material-ui/core'; -import { sidebar, toolbar } from 'client/assets/theme/defaults'; +import { sidebar, toolbar, footer } from 'client/assets/theme/defaults'; export default makeStyles(theme => ({ root: { @@ -23,10 +23,9 @@ export default makeStyles(theme => ({ } }, main: { - paddingBottom: 30, height: '100vh', width: '100%', - // paddingTop: 64 + paddingBottom: footer.regular, paddingTop: toolbar.regular, [`${theme.breakpoints.up('xs')} and (orientation: landscape)`]: { paddingTop: toolbar.xs diff --git a/src/fireedge/src/public/containers/Application/Create/index.js b/src/fireedge/src/public/containers/Application/Create/index.js new file mode 100644 index 0000000000..8c772cfe06 --- /dev/null +++ b/src/fireedge/src/public/containers/Application/Create/index.js @@ -0,0 +1,48 @@ +import React, { useEffect } from 'react'; + +import { useForm, FormProvider } from 'react-hook-form'; + +import FormStepper from 'client/components/FormStepper'; +import Steps from 'client/containers/Application/Create/steps'; +import useOpennebula from 'client/hooks/useOpennebula'; + +const INITIAL_VALUE = { + networks: [], + roles: [] +}; + +function ApplicationCreate() { + const { getVNetworks, getVNetworksTemplates, getTemplates } = useOpennebula(); + const methods = useForm({ + mode: 'onBlur' + }); + const { watch, errors } = methods; + + useEffect(() => { + getVNetworks(); + getVNetworksTemplates(); + getTemplates(); + }, []); + + useEffect(() => { + // console.log('FORM CONTEXT', watch(), errors); + }, [watch, errors]); + + const onSubmit = formData => console.log('submit', formData); + + return ( + + + + ); +} + +ApplicationCreate.propTypes = {}; + +ApplicationCreate.defaultProps = {}; + +export default ApplicationCreate; diff --git a/src/fireedge/src/public/containers/Application/Create/steps.js b/src/fireedge/src/public/containers/Application/Create/steps.js new file mode 100644 index 0000000000..f76b5bdcac --- /dev/null +++ b/src/fireedge/src/public/containers/Application/Create/steps.js @@ -0,0 +1,45 @@ +import { NetworkDialog, RoleDialog } from 'client/components/Dialogs'; +import { NetworkCard, RoleCard } from 'client/components/Cards'; +import FormStep from 'client/components/FormStepper/FormStep'; + +export default [ + { + id: 'networks', + label: 'Networks configuration', + content: FormStep, + preRender: () => undefined, + addAction: true, + DEFAULT_DATA: { + mandatory: true, + name: 'Public_dev', + description: 'Public network in development mode', + type: 'id', + id: '0', + extra: 'size=5' + }, + InfoComponent: NetworkCard, + DialogComponent: NetworkDialog + }, + { + id: 'roles', + label: 'Defining each role', + content: FormStep, + preRender: () => undefined, + addAction: true, + DEFAULT_DATA: { + name: 'Master_dev', + cardinality: 2, + vm_template: 0, + elasticity_policies: [], + scheduled_policies: [] + }, + InfoComponent: RoleCard, + DialogComponent: RoleDialog + }, + { + id: 'where', + label: 'Where will it run?', + content: FormStep, + preRender: () => undefined + } +]; diff --git a/src/fireedge/src/public/containers/Application/Create/styles.js b/src/fireedge/src/public/containers/Application/Create/styles.js new file mode 100644 index 0000000000..64648b6714 --- /dev/null +++ b/src/fireedge/src/public/containers/Application/Create/styles.js @@ -0,0 +1,3 @@ +import { makeStyles } from '@material-ui/core'; + +export default makeStyles(theme => ({})); diff --git a/src/fireedge/src/public/containers/System/Users.js b/src/fireedge/src/public/containers/Application/Deploy.js similarity index 85% rename from src/fireedge/src/public/containers/System/Users.js rename to src/fireedge/src/public/containers/Application/Deploy.js index f0e62dab77..55089a96aa 100644 --- a/src/fireedge/src/public/containers/System/Users.js +++ b/src/fireedge/src/public/containers/Application/Deploy.js @@ -38,7 +38,7 @@ const useStyles = makeStyles({ } }); -function Users() { +function ApplicationDeploy() { const classes = useStyles(); const { isLoading } = useGeneral(); const { users, groups, getUsers } = useOpennebula(); @@ -49,22 +49,22 @@ function Users() { } }, [getUsers]); - const getGroupById = id => groups?.find(({ ID }) => ID === id); + const getGroupById = findId => groups?.find(({ ID }) => ID === findId); return ( <> {isLoading && } - {users?.map(({ ID, NAME, GROUPS }, index) => ( + {users?.map(({ NAME, GROUPS }, index) => ( {NAME} - {[GROUPS?.ID ?? []].flat().map(id => { - const group = getGroupById(id); + {[GROUPS?.ID ?? []].flat().map(ID => { + const group = getGroupById(ID); return group ? ( - Groups - - ); +function ApplicationManage() { + return
Manage
; } -Groups.propTypes = { - name: PropTypes.string -}; +ApplicationManage.propTypes = {}; -Groups.defaultProps = { - name: '' -}; +ApplicationManage.defaultProps = {}; -export default Groups; +export default ApplicationManage; diff --git a/src/fireedge/src/public/containers/Application/index.js b/src/fireedge/src/public/containers/Application/index.js index cac823721a..8514305eb6 100644 --- a/src/fireedge/src/public/containers/Application/index.js +++ b/src/fireedge/src/public/containers/Application/index.js @@ -1,5 +1,5 @@ -import Create from 'client/containers/Application/Create'; -import Deploy from 'client/containers/Application/Deploy'; -import Manage from 'client/containers/Application/Manage'; +import ApplicationCreate from 'client/containers/Application/Create'; +import ApplicationDeploy from 'client/containers/Application/Deploy'; +import ApplicationManage from 'client/containers/Application/Manage'; -export { Create, Deploy, Manage }; +export { ApplicationCreate, ApplicationDeploy, ApplicationManage }; diff --git a/src/fireedge/src/public/containers/Dashboard/index.js b/src/fireedge/src/public/containers/Dashboard/index.js index 56983aee50..7a71aaff3d 100644 --- a/src/fireedge/src/public/containers/Dashboard/index.js +++ b/src/fireedge/src/public/containers/Dashboard/index.js @@ -15,14 +15,9 @@ import React from 'react'; -import { makeStyles, Box, Typography } from '@material-ui/core'; +import { Box, Typography } from '@material-ui/core'; -const dashboardStyles = makeStyles(theme => ({ - root: {}, - title: { - color: theme.palette.common.black - } -})); +import dashboardStyles from 'client/containers/Dashboard/styles'; function Dashboard() { const classes = dashboardStyles(); diff --git a/src/fireedge/src/public/containers/Dashboard/styles.js b/src/fireedge/src/public/containers/Dashboard/styles.js new file mode 100644 index 0000000000..e284b043c2 --- /dev/null +++ b/src/fireedge/src/public/containers/Dashboard/styles.js @@ -0,0 +1,8 @@ +import { makeStyles } from '@material-ui/core'; + +export default makeStyles(theme => ({ + root: {}, + title: { + color: theme.palette.common.black + } +})); diff --git a/src/fireedge/src/public/containers/Login/Form2fa.js b/src/fireedge/src/public/containers/Login/Forms/Form2fa.js similarity index 100% rename from src/fireedge/src/public/containers/Login/Form2fa.js rename to src/fireedge/src/public/containers/Login/Forms/Form2fa.js diff --git a/src/fireedge/src/public/containers/Login/FormGroup.js b/src/fireedge/src/public/containers/Login/Forms/FormGroup.js similarity index 100% rename from src/fireedge/src/public/containers/Login/FormGroup.js rename to src/fireedge/src/public/containers/Login/Forms/FormGroup.js diff --git a/src/fireedge/src/public/containers/Login/FormUser.js b/src/fireedge/src/public/containers/Login/Forms/FormUser.js similarity index 100% rename from src/fireedge/src/public/containers/Login/FormUser.js rename to src/fireedge/src/public/containers/Login/Forms/FormUser.js diff --git a/src/fireedge/src/public/containers/Login/index.js b/src/fireedge/src/public/containers/Login/index.js index 05be257a78..29230eb0ad 100644 --- a/src/fireedge/src/public/containers/Login/index.js +++ b/src/fireedge/src/public/containers/Login/index.js @@ -25,9 +25,9 @@ import { import useAuth from 'client/hooks/useAuth'; -import FormUser from 'client/containers/Login/FormUser'; -import Form2fa from 'client/containers/Login/Form2fa'; -import FormGroup from 'client/containers/Login/FormGroup'; +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'; diff --git a/src/fireedge/src/public/containers/System/index.js b/src/fireedge/src/public/containers/System/index.js deleted file mode 100644 index 2049efce7d..0000000000 --- a/src/fireedge/src/public/containers/System/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import Groups from './Groups'; -import Users from './Users'; - -export { Groups, Users }; diff --git a/src/fireedge/src/public/containers/TestApi/styles.js b/src/fireedge/src/public/containers/TestApi/styles.js index 9e8c2195b5..0084bdcbd3 100644 --- a/src/fireedge/src/public/containers/TestApi/styles.js +++ b/src/fireedge/src/public/containers/TestApi/styles.js @@ -2,6 +2,7 @@ import { makeStyles } from '@material-ui/core'; export default makeStyles(() => ({ root: { - minHeight: '100%' + minHeight: '100%', + width: '100%' } })); diff --git a/src/fireedge/src/public/hooks/useAuth.js b/src/fireedge/src/public/hooks/useAuth.js index 514fb9e242..3cf10eebb2 100644 --- a/src/fireedge/src/public/hooks/useAuth.js +++ b/src/fireedge/src/public/hooks/useAuth.js @@ -7,7 +7,7 @@ import { fakeDelay } from 'client/utils/helpers'; import * as serviceAuth from 'client/services/auth'; import * as serviceUsers from 'client/services/users'; -import * as serviceGroups from 'client/services/groups'; +import * as servicePool from 'client/services/pool'; import { startAuth, selectFilterGroup, @@ -80,7 +80,7 @@ export default function useAuth() { return serviceAuth .getUser() .then(user => dispatch(successAuth({ user }))) - .then(serviceGroups.getGroups) + .then(servicePool.getGroups) .then(groups => dispatch(setGroups(groups))) .catch(err => dispatch(failureAuth({ error: err }))); }, [dispatch, jwtName, authUser]); diff --git a/src/fireedge/src/public/hooks/useOpennebula.js b/src/fireedge/src/public/hooks/useOpennebula.js index 168e66c93d..49356181b8 100644 --- a/src/fireedge/src/public/hooks/useOpennebula.js +++ b/src/fireedge/src/public/hooks/useOpennebula.js @@ -1,43 +1,80 @@ import { useCallback } from 'react'; import { useSelector, useDispatch, shallowEqual } from 'react-redux'; -import { - setGroups, - setUsers, +import actions, { startOneRequest, failureOneRequest } from 'client/actions/pool'; -import * as servicesGroups from 'client/services/groups'; -import * as servicesUsers from 'client/services/users'; +import * as servicePool from 'client/services/pool'; export default function useOpennebula() { const dispatch = useDispatch(); - const { groups, users } = useSelector( - state => state?.Opennebula, + const { + groups, + users, + vNetworks, + vNetworksTemplates, + templates, + filterPool: filter + } = useSelector( + state => ({ + ...state?.Opennebula, + filterPool: state?.Authenticated?.filterPool + }), shallowEqual ); const getGroups = useCallback(() => { dispatch(startOneRequest()); - return servicesGroups - .getGroups() - .then(data => dispatch(setGroups(data))) + return servicePool + .getGroups({ filter }) + .then(data => dispatch(actions.setGroups(data))) .catch(err => dispatch(failureOneRequest({ error: err }))); - }, [dispatch]); + }, [dispatch, filter]); const getUsers = useCallback(() => { dispatch(startOneRequest()); - return servicesUsers - .getUsers() - .then(data => dispatch(setUsers(data))) + return servicePool + .getUsers({ filter }) + .then(data => dispatch(actions.setUsers(data))) .catch(err => dispatch(failureOneRequest({ error: err }))); - }, [dispatch]); + }, [dispatch, filter]); + + const getVNetworks = useCallback(() => { + dispatch(startOneRequest()); + return servicePool + .getVNetworks({ filter }) + .then(data => dispatch(actions.setVNetworks(data))) + .catch(err => dispatch(failureOneRequest({ error: err }))); + }, [dispatch, filter]); + + const getVNetworksTemplates = useCallback(() => { + dispatch(startOneRequest()); + return servicePool + .getVNetworksTemplates({ filter }) + .then(data => dispatch(actions.setVNetworkTemplates(data))) + .catch(err => dispatch(failureOneRequest({ error: err }))); + }, [dispatch, filter]); + + const getTemplates = useCallback(() => { + dispatch(startOneRequest()); + return servicePool + .getTemplates({ filter }) + .then(data => dispatch(actions.setTemplates(data))) + .catch(err => dispatch(failureOneRequest({ error: err }))); + }, [dispatch, filter]); return { groups, getGroups, users, - getUsers + getUsers, + vNetworks, + getVNetworks, + vNetworksTemplates, + getVNetworksTemplates, + templates, + getTemplates }; } diff --git a/src/fireedge/src/public/reducers/opennebula.js b/src/fireedge/src/public/reducers/opennebula.js index 1e09894597..9bf334ca9a 100644 --- a/src/fireedge/src/public/reducers/opennebula.js +++ b/src/fireedge/src/public/reducers/opennebula.js @@ -27,8 +27,8 @@ const initial = { files: [], marketPlaces: [], apps: [], - virtualNetworks: [], - networkTemplates: [], + vNetworks: [], + vNetworksTemplates: [], securityGroups: [], clusters: [], hosts: [], diff --git a/src/fireedge/src/public/router/endpoints.js b/src/fireedge/src/public/router/endpoints.js index e7f09042f0..9fe66c77e6 100644 --- a/src/fireedge/src/public/router/endpoints.js +++ b/src/fireedge/src/public/router/endpoints.js @@ -16,54 +16,50 @@ import { Dashboard as DashboardIcon, Settings as SettingsIcon, - Ballot as BallotIcon + Ballot as BallotIcon, + Palette as PaletteIcon, + Reddit as RedditIcon, + Build as BuildIcon } from '@material-ui/icons'; import Login from 'client/containers/Login'; -import { Clusters, Hosts, Zones } from 'client/containers/Infrastructure'; -import { Users, Groups } from 'client/containers/System'; +import Dashboard from 'client/containers/Dashboard'; import Settings from 'client/containers/Settings'; import TestApi from 'client/containers/TestApi'; -import Dashboard from 'client/containers/Dashboard'; +import { + ApplicationCreate, + ApplicationDeploy, + ApplicationManage +} from 'client/containers/Application'; export const PATH = { LOGIN: '/', DASHBOARD: '/dashboard', + APPLICATION: { + CREATE: '/application/create', + MANAGE: '/application/manage', + DEPLOY: '/application/deploy' + }, SETTINGS: '/settings', - TEST_API: '/test-api', - INFRASTRUCTURE: { - CLUSTERS: '/clusters', - HOSTS: '/hosts', - ZONES: '/zones' - }, - SYSTEM: { - USERS: '/users', - GROUPS: '/groups' - }, - NETWORKS: { - VNETS: '/vnets', - VNETS_TEMPLATES: '/vnets-templates', - VNETS_TOPOLOGY: '/vnets-topology', - SEC_GROUPS: '/secgroups' - } + TEST_API: '/test-api' }; const ENDPOINTS = [ { - label: 'login', + label: 'Login', path: PATH.LOGIN, authenticated: false, component: Login }, { - label: 'dashboard', + label: 'Dashboard', path: PATH.DASHBOARD, authenticated: true, icon: DashboardIcon, component: Dashboard }, { - label: 'settings', + label: 'Settings', path: PATH.SETTINGS, authenticated: true, header: true, @@ -71,7 +67,7 @@ const ENDPOINTS = [ component: Settings }, { - label: 'test api', + label: 'Test API', path: PATH.TEST_API, authenticated: true, devMode: true, @@ -79,75 +75,25 @@ const ENDPOINTS = [ component: TestApi }, { - label: 'infrastructure', + label: 'Create Application', + path: PATH.APPLICATION.CREATE, authenticated: true, - icon: BallotIcon, - routes: [ - { - label: 'clusters', - path: PATH.INFRASTRUCTURE.CLUSTERS, - authenticated: true, - component: Clusters - }, - { - label: 'hosts', - path: PATH.INFRASTRUCTURE.HOSTS, - authenticated: true, - component: Hosts - }, - { - label: 'zones', - path: PATH.INFRASTRUCTURE.ZONES, - authenticated: true, - component: Zones - } - ] + icon: PaletteIcon, + component: ApplicationCreate }, { - label: 'system', + label: 'Deploy Application', + path: PATH.APPLICATION.DEPLOY, authenticated: true, - icon: BallotIcon, - routes: [ - { - label: 'users', - path: PATH.SYSTEM.USERS, - authenticated: true, - component: Users - }, - { - label: 'groups', - path: PATH.SYSTEM.GROUPS, - authenticated: true, - component: Groups - } - ] + icon: RedditIcon, + component: ApplicationDeploy }, { - label: 'networks', + label: 'Manage Application', + path: PATH.APPLICATION.MANAGE, authenticated: true, - icon: BallotIcon, - routes: [ - { - label: 'vnets', - path: PATH.NETWORKS.VNETS, - authenticated: true - }, - { - label: 'vnets templates', - path: PATH.NETWORKS.VNETS_TEMPLATES, - authenticated: true - }, - { - label: 'vnets topology', - path: PATH.NETWORKS.VNETS_TOPOLOGY, - authenticated: true - }, - { - label: 'vnets secgroup', - path: PATH.NETWORKS.SEC_GROUPS, - authenticated: true - } - ] + icon: BuildIcon, + component: ApplicationManage } ]; diff --git a/src/fireedge/src/public/services/groups.js b/src/fireedge/src/public/services/groups.js index 8cfb8d8a2b..547b433c57 100644 --- a/src/fireedge/src/public/services/groups.js +++ b/src/fireedge/src/public/services/groups.js @@ -2,17 +2,4 @@ import { Actions, Commands } from 'server/utils/constants/commands/group'; import { requestData, requestParams } from 'client/utils'; import httpCodes from 'server/utils/constants/http-codes'; -export const getGroups = () => { - const name = Actions.GROUP_POOL_INFO; - const { url, options } = requestParams({}, { name, ...Commands[name] }); - - return requestData(url, options).then(res => { - if (!res?.id || res?.id !== httpCodes.ok.id) throw res; - - return [res?.data?.GROUP_POOL?.GROUP ?? []].flat(); - }); -}; - -export default { - getGroups -}; +export default {}; diff --git a/src/fireedge/src/public/services/pool.js b/src/fireedge/src/public/services/pool.js new file mode 100644 index 0000000000..f6439c1e2b --- /dev/null +++ b/src/fireedge/src/public/services/pool.js @@ -0,0 +1,86 @@ +import User from 'server/utils/constants/commands/user'; +import Group from 'server/utils/constants/commands/group'; +import VNet from 'server/utils/constants/commands/vn'; +import VNetTemplate from 'server/utils/constants/commands/vntemplate'; +import Template from 'server/utils/constants/commands/template'; + +import httpCodes from 'server/utils/constants/http-codes'; +import { requestData, requestParams } from 'client/utils'; + +export const getUsers = ({ filter }) => { + const name = User.Actions.USER_POOL_INFO; + const { url, options } = requestParams( + { filter }, + { name, ...User.Commands[name] } + ); + + return requestData(url, options).then(res => { + if (!res?.id || res?.id !== httpCodes.ok.id) throw res; + + return [res?.data?.USER_POOL?.USER ?? []].flat(); + }); +}; + +export const getGroups = ({ filter }) => { + const name = Group.Actions.GROUP_POOL_INFO; + const { url, options } = requestParams( + { filter }, + { name, ...Group.Commands[name] } + ); + + return requestData(url, options).then(res => { + if (!res?.id || res?.id !== httpCodes.ok.id) throw res; + + return [res?.data?.GROUP_POOL?.GROUP ?? []].flat(); + }); +}; + +export const getVNetworks = ({ filter }) => { + const name = VNet.Actions.VN_POOL_INFO; + const { url, options } = requestParams( + { filter }, + { name, ...VNet.Commands[name] } + ); + + return requestData(url, options).then(res => { + if (!res?.id || res?.id !== httpCodes.ok.id) throw res; + + return [res?.data?.VNET_POOL?.VNET ?? []].flat(); + }); +}; + +export const getVNetworksTemplates = ({ filter }) => { + const name = VNetTemplate.Actions.VNTEMPLATE_POOL_INFO; + const { url, options } = requestParams( + { filter }, + { name, ...VNetTemplate.Commands[name] } + ); + + return requestData(url, options).then(res => { + if (!res?.id || res?.id !== httpCodes.ok.id) throw res; + + return [res?.data?.VNTEMPLATE_POOL?.VNTEMPLATE ?? []].flat(); + }); +}; + +export const getTemplates = ({ filter }) => { + const name = Template.Actions.TEMPLATE_POOL_INFO; + const { url, options } = requestParams( + { filter }, + { name, ...Template.Commands[name] } + ); + + return requestData(url, options).then(res => { + if (!res?.id || res?.id !== httpCodes.ok.id) throw res; + + return [res?.data?.VMTEMPLATE_POOL?.VMTEMPLATE ?? []].flat(); + }); +}; + +export default { + getUsers, + getGroups, + getVNetworks, + getVNetworksTemplates, + getTemplates +}; diff --git a/src/fireedge/src/public/services/users.js b/src/fireedge/src/public/services/users.js index f054a01f6d..ca7f68f27f 100644 --- a/src/fireedge/src/public/services/users.js +++ b/src/fireedge/src/public/services/users.js @@ -13,18 +13,6 @@ export const changeGroup = values => { }); }; -export const getUsers = () => { - const name = Actions.USER_POOL_INFO; - const { url, options } = requestParams({}, { name, ...Commands[name] }); - - return requestData(url, options).then(res => { - if (!res?.id || res?.id !== httpCodes.ok.id) throw res; - - return [res?.data?.USER_POOL?.USER ?? []].flat(); - }); -}; - export default { - changeGroup, - getUsers + changeGroup }; diff --git a/src/fireedge/src/public/utils/request.js b/src/fireedge/src/public/utils/request.js index ab94c6fb60..1f54915034 100644 --- a/src/fireedge/src/public/utils/request.js +++ b/src/fireedge/src/public/utils/request.js @@ -4,7 +4,9 @@ import { from as resourceFrom } from 'server/utils/constants/defaults'; export const getQueries = params => Object.entries(params) - ?.filter(([, { from }]) => from === resourceFrom.query) + ?.filter(([, { from, value }]) => + Boolean(from === resourceFrom.query && value) + ) ?.map(([name, { value }]) => `${name}=${encodeURI(value)}`) ?.join('&');