From 993284eb97d61ff9e27f4c3646ed146258355590 Mon Sep 17 00:00:00 2001 From: Sergio Betanzos Date: Tue, 8 Sep 2020 19:05:54 +0200 Subject: [PATCH] F #3951: Add service config step (#192) --- .../public/components/Cards/ClusterCard.js | 131 ++++++++++++++++ .../src/public/components/Cards/RoleCard.js | 29 ++-- .../src/public/components/Cards/index.js | 7 +- .../src/public/components/Dialogs/index.js | 4 +- .../components/FormControl/ErrorHelper.js | 10 +- .../components/FormControl/SubmitButton.js | 14 +- .../components/FormStepper/FormDialog.js | 128 ++++++++++++++++ .../components/FormStepper/FormListSelect.js | 60 ++++++++ .../public/components/FormStepper/FormStep.js | 118 ++------------ .../public/components/FormStepper/index.js | 45 +++--- .../public/components/Forms/FormWithSchema.js | 69 +++++++++ .../src/public/components/Forms/index.js | 3 + .../public/components/LoadingScreen/index.js | 17 +-- .../containers/Application/Create/index.js | 42 ++--- .../containers/Application/Create/schema.js | 49 ++++++ .../containers/Application/Create/steps.js | 144 ++++++++++++------ .../public/containers/Application/Deploy.js | 25 +-- .../src/public/hooks/useOpennebula.js | 13 +- src/fireedge/src/public/services/pool.js | 18 ++- src/fireedge/src/public/utils/helpers.js | 17 +++ 20 files changed, 678 insertions(+), 265 deletions(-) create mode 100644 src/fireedge/src/public/components/Cards/ClusterCard.js create mode 100644 src/fireedge/src/public/components/FormStepper/FormDialog.js create mode 100644 src/fireedge/src/public/components/FormStepper/FormListSelect.js create mode 100644 src/fireedge/src/public/components/Forms/FormWithSchema.js create mode 100644 src/fireedge/src/public/components/Forms/index.js create mode 100644 src/fireedge/src/public/containers/Application/Create/schema.js diff --git a/src/fireedge/src/public/components/Cards/ClusterCard.js b/src/fireedge/src/public/components/Cards/ClusterCard.js new file mode 100644 index 0000000000..7d31f4c3c9 --- /dev/null +++ b/src/fireedge/src/public/components/Cards/ClusterCard.js @@ -0,0 +1,131 @@ +import React from 'react'; +import clsx from 'clsx'; + +import { + makeStyles, + Card, + CardHeader, + Fade, + CardActionArea, + CardContent, + Badge, + Box +} from '@material-ui/core'; +import StorageIcon from '@material-ui/icons/Storage'; +import VideogameAssetIcon from '@material-ui/icons/VideogameAsset'; +import AccountTreeIcon from '@material-ui/icons/AccountTree'; +import FolderOpenIcon from '@material-ui/icons/FolderOpen'; + +import { Tr } from 'client/components/HOC'; + +const useStyles = makeStyles(theme => ({ + root: { + height: '100%', + minHeight: 140, + display: 'flex', + flexDirection: 'column' + }, + selected: { + color: theme.palette.primary.contrastText, + backgroundColor: theme.palette.primary.main, + '& $badge': { + color: theme.palette.primary.main, + backgroundColor: theme.palette.common.white + } + }, + actionArea: { + height: '100%' + }, + header: { + overflowX: 'hidden', + flexGrow: 1 + }, + subheader: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'initial', + display: '-webkit-box', + lineClamp: 2, + boxOrient: 'vertical' + }, + badgesWrapper: { + display: 'flex', + gap: theme.typography.pxToRem(12) + }, + badge: {}, + icon: {} +})); + +const ClusterCard = React.memo( + ({ info, isSelected, handleSelect, handleUnselect }) => { + const classes = useStyles(); + const { ID, NAME, HOSTS, VNETS, DATASTORES } = info; + + const hosts = [HOSTS?.ID ?? []].flat(); + const vnets = [VNETS?.ID ?? []].flat(); + const datastores = [DATASTORES?.ID ?? []].flat(); + + const badgePosition = { vertical: 'top', horizontal: 'right' }; + + return ( + + + (isSelected ? handleUnselect(ID) : handleSelect(ID))} + > + } + className={classes.header} + classes={{ content: classes.headerContent }} + title={NAME} + titleTypographyProps={{ + variant: 'body2', + noWrap: true, + title: NAME + }} + /> + + + + + + + + + + + + + + + + + ); + } +); + +export default ClusterCard; diff --git a/src/fireedge/src/public/components/Cards/RoleCard.js b/src/fireedge/src/public/components/Cards/RoleCard.js index 6cb7445efd..d6209ea453 100644 --- a/src/fireedge/src/public/components/Cards/RoleCard.js +++ b/src/fireedge/src/public/components/Cards/RoleCard.js @@ -46,31 +46,21 @@ const RoleCard = React.memo( 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 }} @@ -92,6 +82,9 @@ const RoleCard = React.memo( + diff --git a/src/fireedge/src/public/components/Cards/index.js b/src/fireedge/src/public/components/Cards/index.js index 81d55ce04a..cc45f28981 100644 --- a/src/fireedge/src/public/components/Cards/index.js +++ b/src/fireedge/src/public/components/Cards/index.js @@ -1,4 +1,5 @@ -import NetworkCard from './NetworkCard'; -import RoleCard from './RoleCard'; +import ClusterCard from 'client/components/Cards/ClusterCard'; +import NetworkCard from 'client/components/Cards/NetworkCard'; +import RoleCard from 'client/components/Cards/RoleCard'; -export { NetworkCard, RoleCard }; +export { ClusterCard, NetworkCard, RoleCard }; diff --git a/src/fireedge/src/public/components/Dialogs/index.js b/src/fireedge/src/public/components/Dialogs/index.js index 537beed14d..25126df23d 100644 --- a/src/fireedge/src/public/components/Dialogs/index.js +++ b/src/fireedge/src/public/components/Dialogs/index.js @@ -1,4 +1,4 @@ -import NetworkDialog from './NetworkDialog'; -import RoleDialog from './RoleDialog'; +import NetworkDialog from 'client/components/Dialogs/NetworkDialog'; +import RoleDialog from 'client/components/Dialogs/RoleDialog'; export { NetworkDialog, RoleDialog }; diff --git a/src/fireedge/src/public/components/FormControl/ErrorHelper.js b/src/fireedge/src/public/components/FormControl/ErrorHelper.js index d2ff911474..c38dc9d3e3 100644 --- a/src/fireedge/src/public/components/FormControl/ErrorHelper.js +++ b/src/fireedge/src/public/components/FormControl/ErrorHelper.js @@ -10,8 +10,6 @@ import { } from '@material-ui/core'; import { Info as InfoIcon } from '@material-ui/icons'; -import { Translate } from 'client/components/HOC'; - const useStyles = makeStyles(theme => { const getColor = theme.palette.type === 'light' ? darken : lighten; const getBackgroundColor = theme.palette.type === 'light' ? lighten : darken; @@ -37,9 +35,13 @@ const ErrorHelper = ({ label, ...rest }) => { const classes = useStyles(); return ( - + - + {label} diff --git a/src/fireedge/src/public/components/FormControl/SubmitButton.js b/src/fireedge/src/public/components/FormControl/SubmitButton.js index 86c6d1a85b..ff60e11b9b 100644 --- a/src/fireedge/src/public/components/FormControl/SubmitButton.js +++ b/src/fireedge/src/public/components/FormControl/SubmitButton.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import { makeStyles, CircularProgress, Button } from '@material-ui/core'; +import { Submit } from 'client/constants/translates'; import { Tr } from 'client/components/HOC'; -import * as CONSTANT from 'client/constants'; const useStyles = makeStyles(theme => ({ button: { @@ -13,7 +13,7 @@ const useStyles = makeStyles(theme => ({ } })); -const ButtonSubmit = ({ isSubmitting, label, ...rest }) => { +const SubmitButton = ({ isSubmitting, label, ...rest }) => { const classes = useStyles(); return ( @@ -26,19 +26,19 @@ const ButtonSubmit = ({ isSubmitting, label, ...rest }) => { {...rest} > {isSubmitting && } - {!isSubmitting && Tr(label)} + {!isSubmitting && (label ?? Tr(Submit))} ); }; -ButtonSubmit.propTypes = { +SubmitButton.propTypes = { isSubmitting: PropTypes.bool, label: PropTypes.string }; -ButtonSubmit.defaultProps = { +SubmitButton.defaultProps = { isSubmitting: false, - label: CONSTANT.default.Submit + label: undefined }; -export default ButtonSubmit; +export default SubmitButton; diff --git a/src/fireedge/src/public/components/FormStepper/FormDialog.js b/src/fireedge/src/public/components/FormStepper/FormDialog.js new file mode 100644 index 0000000000..9cebd7a399 --- /dev/null +++ b/src/fireedge/src/public/components/FormStepper/FormDialog.js @@ -0,0 +1,128 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; + +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 FormDialog({ step, data, setFormData }) { + const classes = useStyles(); + const [dialogFormData, setDialogFormData] = useState({}); + const [showDialog, setShowDialog] = useState(false); + + const { + id, + addCardAction, + preRender, + InfoComponent, + DialogComponent, + DEFAULT_DATA + } = step; + + useEffect(() => { + preRender && preRender(); + }, []); + + const handleSubmit = values => { + setFormData(prevData => ({ + ...prevData, + [id]: Object.assign(prevData[id], { + [dialogFormData.index]: values + }) + })); + + setShowDialog(false); + }; + + const handleOpen = (index = data?.length) => { + const openData = data[index] ?? DEFAULT_DATA; + + setDialogFormData({ index, data: openData }); + setShowDialog(true); + }; + + const handleClone = index => { + const item = data[index]; + const cloneItem = { ...item, name: `${item?.name}_clone` }; + const cloneData = [...data]; + cloneData.splice(index + 1, 0, cloneItem); + + setFormData(prevData => ({ ...prevData, [id]: cloneData })); + }; + + const handleRemove = indexRemove => { + // TODO confirmation?? + setFormData(prevData => ({ + ...prevData, + [id]: prevData[id]?.filter((_, index) => index !== indexRemove) + })); + }; + + const handleClose = () => setShowDialog(false); + + return ( + + + {addCardAction && ( + + + handleOpen()}> + + + + + + + )} + {Array.isArray(data) && + data?.map((info, index) => ( + + handleOpen(index)} + handleClone={() => handleClone(index)} + handleRemove={() => handleRemove(index)} + /> + + ))} + + {showDialog && DialogComponent && ( + + )} + + ); +} + +FormDialog.propTypes = { + step: PropTypes.objectOf(PropTypes.any).isRequired, + data: PropTypes.arrayOf(PropTypes.object).isRequired, + setFormData: PropTypes.func.isRequired +}; + +FormDialog.defaultProps = { + step: {}, + data: [], + setFormData: data => data +}; + +export default FormDialog; diff --git a/src/fireedge/src/public/components/FormStepper/FormListSelect.js b/src/fireedge/src/public/components/FormStepper/FormListSelect.js new file mode 100644 index 0000000000..e9e433574c --- /dev/null +++ b/src/fireedge/src/public/components/FormStepper/FormListSelect.js @@ -0,0 +1,60 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; + +import { Box, Grid } from '@material-ui/core'; + +function FormListSelect({ step, data, setFormData }) { + const { id, onlyOneSelect, preRender, list, InfoComponent } = step; + + useEffect(() => { + preRender && preRender(); + }, []); + + const handleSelect = index => { + // select index => add select to data form + setFormData(prevData => ({ + ...prevData, + [id]: onlyOneSelect ? [index] : [...prevData[id], index] + })); + }; + + const handleUnselect = indexRemove => { + // unselect index => remove selected from data form + setFormData(prevData => ({ + ...prevData, + [id]: prevData[id]?.filter(index => index !== indexRemove) + })); + }; + + return ( + + + {Array.isArray(list) && + list?.map((info, index) => ( + + selected === info?.ID)} + handleSelect={handleSelect} + handleUnselect={handleUnselect} + /> + + ))} + + + ); +} + +FormListSelect.propTypes = { + step: PropTypes.objectOf(PropTypes.any).isRequired, + data: PropTypes.arrayOf(PropTypes.any).isRequired, + setFormData: PropTypes.func.isRequired +}; + +FormListSelect.defaultProps = { + step: {}, + data: [], + setFormData: data => data +}; + +export default FormListSelect; diff --git a/src/fireedge/src/public/components/FormStepper/FormStep.js b/src/fireedge/src/public/components/FormStepper/FormStep.js index 1aad95efca..9872bdd230 100644 --- a/src/fireedge/src/public/components/FormStepper/FormStep.js +++ b/src/fireedge/src/public/components/FormStepper/FormStep.js @@ -1,118 +1,32 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } 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' - } -})); +import { debounce } from '@material-ui/core'; 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; + const { reset, errors } = useFormContext(); + const { id, preRender, FormComponent } = step; useEffect(() => { - reset({ ...data }, { errors: true }); - }, [id, data]); + preRender && preRender(); + reset({ [id]: data }, { errors: true }); + }, []); - const handleSubmit = values => { - setFormData(prevData => ({ - ...prevData, - [id]: Object.assign(prevData[id], { - [dialogFormData.index]: values - }) - })); + useEffect(() => { + // setFormData(prev => ({ ...prev, [id]: watch() })); + console.log('errors', errors); + }, [errors]); - setShowDialog(false); - }; + const handleSubmit = dataForm => console.log(dataForm); - 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 && ( - - )} - - ); + return React.useMemo(() => , [id, errors]); } FormStep.propTypes = { - step: PropTypes.objectOf(PropTypes.object).isRequired, - data: PropTypes.objectOf(PropTypes.object).isRequired, - setFormData: PropTypes.func + step: PropTypes.objectOf(PropTypes.any).isRequired, + data: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, + setFormData: PropTypes.func.isRequired }; FormStep.defaultProps = { diff --git a/src/fireedge/src/public/components/FormStepper/index.js b/src/fireedge/src/public/components/FormStepper/index.js index 0c8dcfccf8..637fc0f0af 100644 --- a/src/fireedge/src/public/components/FormStepper/index.js +++ b/src/fireedge/src/public/components/FormStepper/index.js @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; import PropTypes from 'prop-types'; import { useFormContext } from 'react-hook-form'; @@ -12,7 +12,11 @@ const FIRST_STEP = 0; const FormStepper = ({ steps, initialValue, onSubmit }) => { const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs')); - const { watch } = useFormContext(); + const { + watch, + formState: { isValid } + } = useFormContext(); + const [activeStep, setActiveStep] = useState(FIRST_STEP); const [formData, setFormData] = useState(initialValue); @@ -20,19 +24,20 @@ const FormStepper = ({ steps, initialValue, onSubmit }) => { const lastStep = useMemo(() => totalSteps - 1, [totalSteps]); const disabledBack = useMemo(() => activeStep === FIRST_STEP, [activeStep]); - const handleNext = useCallback(() => { - setFormData(data => ({ ...data, ...watch() })); - + const handleNext = () => { + // TODO check if errors if (activeStep === lastStep) { onSubmit(formData); - } else setActiveStep(prevActiveStep => prevActiveStep + 1); - }, [activeStep]); + } else if (isValid) { + setFormData(prevData => ({ ...prevData, ...watch() })); + setActiveStep(prevActiveStep => prevActiveStep + 1); + } + }; const handleBack = useCallback(() => { if (activeStep <= FIRST_STEP) return; setActiveStep(prevActiveStep => prevActiveStep - 1); - setFormData(data => ({ ...data, ...watch() })); }, [activeStep]); return ( @@ -60,16 +65,19 @@ const FormStepper = ({ steps, initialValue, onSubmit }) => { {/* FORM CONTENT */} {React.useMemo(() => { - const { content: Content, ...rest } = steps[activeStep]; + const { id, content: Content } = steps[activeStep]; return ( - + Content && ( + + ) ); - }, [activeStep, formData, setFormData])} + }, [steps, formData, activeStep, setFormData])} ); }; @@ -77,13 +85,12 @@ const FormStepper = ({ steps, initialValue, onSubmit }) => { FormStepper.propTypes = { steps: PropTypes.arrayOf( PropTypes.shape({ - id: PropTypes.number.isRequired, + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, label: PropTypes.string.isRequired, - content: PropTypes.node.isRequired, - dependOf: PropTypes.bool + content: PropTypes.func.isRequired }) ), - initialValue: PropTypes.objectOf(PropTypes.object), + initialValue: PropTypes.objectOf(PropTypes.any), onSubmit: PropTypes.func }; diff --git a/src/fireedge/src/public/components/Forms/FormWithSchema.js b/src/fireedge/src/public/components/Forms/FormWithSchema.js new file mode 100644 index 0000000000..b2f4a384c8 --- /dev/null +++ b/src/fireedge/src/public/components/Forms/FormWithSchema.js @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + Grid, + TextField, + MenuItem, + FormControlLabel, + Checkbox +} from '@material-ui/core'; +import { useFormContext, Controller } from 'react-hook-form'; + +import { TYPE_INPUT } from 'client/constants'; +import ErrorHelper from 'client/components/FormControl/ErrorHelper'; + +const FormWithSchema = ({ id, cy, schema }) => { + const { register, control, errors } = useFormContext(); + + return ( + + {schema?.map(({ name, type, label, values }) => ( + + {(type === TYPE_INPUT.TEXT || type === TYPE_INPUT.SELECT) && ( + + ) + } + FormHelperTextProps={{ 'data-cy': `${cy}-${name}-error` }} + > + {type === TYPE_INPUT.SELECT && + values?.map(({ text, value }) => ( + + {text} + + ))} + + } + name={`${id}.${name}`} + control={control} + /> + )} + + ))} + + ); +}; + +FormWithSchema.propTypes = { + id: PropTypes.string, + cy: PropTypes.string, + schema: PropTypes.arrayOf(PropTypes.object) +}; + +FormWithSchema.defaultProps = { + id: '', + cy: 'form', + schema: [] +}; + +export default FormWithSchema; diff --git a/src/fireedge/src/public/components/Forms/index.js b/src/fireedge/src/public/components/Forms/index.js new file mode 100644 index 0000000000..002de10459 --- /dev/null +++ b/src/fireedge/src/public/components/Forms/index.js @@ -0,0 +1,3 @@ +import FormWithSchema from 'client/components/Forms/FormWithSchema'; + +export { FormWithSchema }; diff --git a/src/fireedge/src/public/components/LoadingScreen/index.js b/src/fireedge/src/public/components/LoadingScreen/index.js index 473ecb709b..e65954b362 100644 --- a/src/fireedge/src/public/components/LoadingScreen/index.js +++ b/src/fireedge/src/public/components/LoadingScreen/index.js @@ -1,9 +1,9 @@ import React from 'react'; -import { styled } from '@material-ui/core'; +import { styled, Box } from '@material-ui/core'; import Logo from 'client/icons/logo'; -const ScreenBox = styled('div')({ +const ScreenBox = styled(Box)({ width: '100%', height: '100vh', backgroundColor: '#ffffff', @@ -15,18 +15,7 @@ const ScreenBox = styled('div')({ }); const LoadingScreen = () => ( - + ); diff --git a/src/fireedge/src/public/containers/Application/Create/index.js b/src/fireedge/src/public/containers/Application/Create/index.js index 8c772cfe06..998ac536ff 100644 --- a/src/fireedge/src/public/containers/Application/Create/index.js +++ b/src/fireedge/src/public/containers/Application/Create/index.js @@ -1,43 +1,31 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useForm, FormProvider } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers'; 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: [] -}; +import { Container } from '@material-ui/core'; function ApplicationCreate() { - const { getVNetworks, getVNetworksTemplates, getTemplates } = useOpennebula(); + const { steps, defaultValues } = Steps(); const methods = useForm({ - mode: 'onBlur' + mode: 'onBlur', + defaultValues }); - 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 ( - - - + + + + + ); } diff --git a/src/fireedge/src/public/containers/Application/Create/schema.js b/src/fireedge/src/public/containers/Application/Create/schema.js new file mode 100644 index 0000000000..03d4767632 --- /dev/null +++ b/src/fireedge/src/public/containers/Application/Create/schema.js @@ -0,0 +1,49 @@ +import { TYPE_INPUT } from 'client/constants'; + +export const STRATEGIES_DEPLOY = [ + { text: 'None', value: 'none' }, + { text: 'Straight', value: 'straight' } +]; + +export const SHUTDOWN_ACTIONS = [ + { text: 'None', value: '' }, + { text: 'Terminate', value: 'shutdown' }, + { text: 'Terminate hard', value: 'shutdown-hard' } +]; + +export default [ + { + name: 'name', + label: 'Name', + type: TYPE_INPUT.TEXT, + initial: '' + }, + { + name: 'description', + label: 'Description', + type: TYPE_INPUT.TEXT, + multiline: true, + initial: '' + }, + { + name: 'deployment', + label: 'Select a strategy', + type: TYPE_INPUT.SELECT, + initial: STRATEGIES_DEPLOY[1].value, + values: STRATEGIES_DEPLOY + }, + { + name: 'shutdown_action', + label: 'Select a VM shutdown action', + type: TYPE_INPUT.SELECT, + initial: SHUTDOWN_ACTIONS[1].value, + values: SHUTDOWN_ACTIONS + }, + { + name: 'ready_status_gate', + label: + 'Wait for VMs to report that they are READY via OneGate to consider them running', + type: TYPE_INPUT.CHECKBOX, + initial: false + } +]; diff --git a/src/fireedge/src/public/containers/Application/Create/steps.js b/src/fireedge/src/public/containers/Application/Create/steps.js index f76b5bdcac..4ef54e998e 100644 --- a/src/fireedge/src/public/containers/Application/Create/steps.js +++ b/src/fireedge/src/public/containers/Application/Create/steps.js @@ -1,45 +1,101 @@ -import { NetworkDialog, RoleDialog } from 'client/components/Dialogs'; -import { NetworkCard, RoleCard } from 'client/components/Cards'; -import FormStep from 'client/components/FormStepper/FormStep'; +import React, { useMemo } from 'react'; -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 - } -]; +import useOpennebula from 'client/hooks/useOpennebula'; + +import { NetworkDialog, RoleDialog } from 'client/components/Dialogs'; +import { NetworkCard, RoleCard, ClusterCard } from 'client/components/Cards'; +import FormStep from 'client/components/FormStepper/FormStep'; +import FormDialog from 'client/components/FormStepper/FormDialog'; +import FormListSelect from 'client/components/FormStepper/FormListSelect'; + +import FormWithSchema from 'client/components/Forms/FormWithSchema'; +import Schema from 'client/containers/Application/Create/schema'; + +function Steps() { + const { + clusters, + getClusters, + getVNetworks, + getVNetworksTemplates, + getTemplates + } = useOpennebula(); + + const steps = useMemo( + () => [ + { + id: 'service', + label: 'Service configuration', + content: FormStep, + defaultValue: Schema?.reduce( + (val, { name, initial }) => ({ ...val, [name]: initial }), + {} + ), + FormComponent: props => ( + + ) + }, + { + id: 'networks', + label: 'Networks configuration', + content: FormDialog, + preRender: () => { + getVNetworks(); + getVNetworksTemplates(); + }, + defaultValue: [], + addCardAction: 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: FormDialog, + preRender: getTemplates, + defaultValue: [], + addCardAction: true, + DEFAULT_DATA: { + name: 'Master_dev', + cardinality: 2, + vm_template: 0, + elasticity_policies: [], + scheduled_policies: [] + }, + InfoComponent: RoleCard, + DialogComponent: RoleDialog + }, + { + id: 'clusters', + label: 'Where will it run?', + content: FormListSelect, + defaultValue: [], + onlyOneSelect: true, + preRender: getClusters, + list: clusters, + InfoComponent: ClusterCard + } + ], + [getVNetworks, getVNetworksTemplates, getTemplates, getClusters, clusters] + ); + + const defaultValues = useMemo( + () => + steps.reduce( + (values, { id, defaultValue }) => ({ ...values, [id]: defaultValue }), + {} + ), + [steps] + ); + + return { steps, defaultValues }; +} + +export default Steps; diff --git a/src/fireedge/src/public/containers/Application/Deploy.js b/src/fireedge/src/public/containers/Application/Deploy.js index 55089a96aa..51c9d3bb96 100644 --- a/src/fireedge/src/public/containers/Application/Deploy.js +++ b/src/fireedge/src/public/containers/Application/Deploy.js @@ -41,7 +41,7 @@ const useStyles = makeStyles({ function ApplicationDeploy() { const classes = useStyles(); const { isLoading } = useGeneral(); - const { users, groups, getUsers } = useOpennebula(); + const { groups, getUsers } = useOpennebula(); useEffect(() => { if (!isLoading) { @@ -54,28 +54,7 @@ function ApplicationDeploy() { return ( <> {isLoading && } - {users?.map(({ NAME, GROUPS }, index) => ( - - - - {NAME} - {[GROUPS?.ID ?? []].flat().map(ID => { - const group = getGroupById(ID); - return group ? ( - - ) : null; - })} - - - - ))} + Deploy ); } diff --git a/src/fireedge/src/public/hooks/useOpennebula.js b/src/fireedge/src/public/hooks/useOpennebula.js index 49356181b8..4d7140a0bb 100644 --- a/src/fireedge/src/public/hooks/useOpennebula.js +++ b/src/fireedge/src/public/hooks/useOpennebula.js @@ -16,6 +16,7 @@ export default function useOpennebula() { vNetworks, vNetworksTemplates, templates, + clusters, filterPool: filter } = useSelector( state => ({ @@ -65,6 +66,14 @@ export default function useOpennebula() { .catch(err => dispatch(failureOneRequest({ error: err }))); }, [dispatch, filter]); + const getClusters = useCallback(() => { + dispatch(startOneRequest()); + return servicePool + .getClusters({ filter }) + .then(data => dispatch(actions.setCluster(data))) + .catch(err => dispatch(failureOneRequest({ error: err }))); + }, [dispatch, filter]); + return { groups, getGroups, @@ -75,6 +84,8 @@ export default function useOpennebula() { vNetworksTemplates, getVNetworksTemplates, templates, - getTemplates + getTemplates, + clusters, + getClusters }; } diff --git a/src/fireedge/src/public/services/pool.js b/src/fireedge/src/public/services/pool.js index f6439c1e2b..89e94cadf4 100644 --- a/src/fireedge/src/public/services/pool.js +++ b/src/fireedge/src/public/services/pool.js @@ -3,6 +3,7 @@ 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 Cluster from 'server/utils/constants/commands/cluster'; import httpCodes from 'server/utils/constants/http-codes'; import { requestData, requestParams } from 'client/utils'; @@ -77,10 +78,25 @@ export const getTemplates = ({ filter }) => { }); }; +export const getClusters = ({ filter }) => { + const name = Cluster.Actions.CLUSTER_POOL_INFO; + const { url, options } = requestParams( + { filter }, + { name, ...Cluster.Commands[name] } + ); + + return requestData(url, options).then(res => { + if (!res?.id || res?.id !== httpCodes.ok.id) throw res; + + return [res?.data?.CLUSTER_POOL?.CLUSTER ?? []].flat(); + }); +}; + export default { getUsers, getGroups, getVNetworks, getVNetworksTemplates, - getTemplates + getTemplates, + getClusters }; diff --git a/src/fireedge/src/public/utils/helpers.js b/src/fireedge/src/public/utils/helpers.js index 94cab89185..a683912db1 100644 --- a/src/fireedge/src/public/utils/helpers.js +++ b/src/fireedge/src/public/utils/helpers.js @@ -1 +1,18 @@ export const fakeDelay = ms => new Promise(resolve => setTimeout(resolve, ms)); + +export const debounce = (func, delay, immediate) => { + let timerId; + return (...args) => { + const boundFunc = func.bind(this, ...args); + clearTimeout(timerId); + if (immediate && !timerId) { + boundFunc(); + } + const calleeFunc = immediate + ? () => { + timerId = null; + } + : boundFunc; + timerId = setTimeout(calleeFunc, delay); + }; +};