1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-21 14:50:08 +03:00

F #3951: Add stepper role form (#208)

This commit is contained in:
Sergio Betanzos 2020-09-11 14:32:56 +02:00 committed by GitHub
parent 1b57b97b26
commit 17ba0e63b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 854 additions and 599 deletions

View File

@ -5,5 +5,7 @@
"client/*": ["public/*"],
"server/*": ["*"]
}
}
},
"include": ["./src/*"],
"exclude": ["node_modules"]
}

View File

@ -8,7 +8,7 @@
"build-node": "webpack --mode=production --env.node --env.ssr",
"build-front": "npm run copy_static_assets && concurrently \"webpack --mode=production --env.front\"",
"dev": "npm run copy_static_assets && concurrently \"nodemon --inspect dist\" \"webpack --mode=development --env.node --env.front --env.ssr --env.hotreload --watch\"",
"dev-front": "npm run copy_static_assets && concurrently \"nodemon --inspect dist\" \"webpack --mode=development --env.node --env.hotreload\" \"webpack --mode=development --env.front --env.hotreload --watch\"" ,
"dev-front": "npm run copy_static_assets && concurrently \"nodemon --inspect dist\" \"webpack --mode=development --env.node --env.hotreload\" \"webpack --mode=development --env.front --env.hotreload --watch\"",
"start": "node dist/index",
"cypress:open": "cypress open",
"cypress:run": "cypress run --headless --browser chrome --spec \"cypress/integration/**/*.spec.js\"",
@ -58,6 +58,7 @@
"react": "^16.8.6",
"react-ace": "^9.1.1",
"react-dom": "^16.8.6",
"react-flow-renderer": "^5.7.1",
"react-hook-form": "^6.0.0",
"react-json-pretty": "^2.2.0",
"react-redux": "^7.2.0",

View File

@ -42,9 +42,9 @@ module.exports = {
type: SUCCESS_ONE_REQUEST,
payload: { files }
}),
setMarketplaces: marketPlaces => ({
setMarketplaces: marketplaces => ({
type: SUCCESS_ONE_REQUEST,
payload: { marketPlaces }
payload: { marketplaces }
}),
setApps: apps => ({
type: SUCCESS_ONE_REQUEST,

View File

@ -57,9 +57,9 @@ const useStyles = makeStyles(theme => ({
}));
const ClusterCard = React.memo(
({ info, isSelected, handleSelect, handleUnselect }) => {
({ value, isSelected, handleSelect, handleUnselect }) => {
const classes = useStyles();
const { ID, NAME, HOSTS, VNETS, DATASTORES } = info;
const { ID, NAME, HOSTS, VNETS, DATASTORES } = value;
const hosts = [HOSTS?.ID ?? []].flat();
const vnets = [VNETS?.ID ?? []].flat();

View File

@ -36,9 +36,9 @@ const useStyles = makeStyles(theme => ({
}));
const NetworkCard = React.memo(
({ info, handleEdit, handleClone, handleRemove }) => {
({ value, handleEdit, handleClone, handleRemove }) => {
const classes = useStyles();
const { mandatory, name, description, type, id, extra } = info;
const { mandatory, name, description, type, id, extra } = value;
return (
<Fade in unmountOnExit={false}>

View File

@ -0,0 +1,83 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
useMediaQuery,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions
} from '@material-ui/core';
import { useForm, FormProvider } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers';
import { Tr } from 'client/components/HOC';
const DialogForm = React.memo(
({ open, title, values, resolver, onSubmit, onCancel, children }) => {
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'));
const { handleSubmit, ...methods } = useForm({
reValidateMode: 'onSubmit',
defaultValues: values,
resolver: yupResolver(resolver)
});
return (
<Dialog
fullScreen={isMobile}
open={open}
maxWidth="lg"
scroll="paper"
PaperProps={{ style: { height: '80%', minWidth: '80%' } }}
>
<DialogTitle>{title}</DialogTitle>
<DialogContent dividers>
<FormProvider {...methods}>{children}</FormProvider>
</DialogContent>
<DialogActions>
<Button onClick={onCancel} color="primary">
{Tr('Cancel')}
</Button>
<Button
type="submit"
variant="contained"
color="primary"
onClick={handleSubmit(onSubmit)}
>
{Tr('Save')}
</Button>
</DialogActions>
</Dialog>
);
}
);
DialogForm.propTypes = {
open: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
values: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.any),
PropTypes.objectOf(PropTypes.any)
]),
resolver: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
onSubmit: PropTypes.func,
onCancel: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
])
};
DialogForm.defaultProps = {
open: true,
title: 'Title dialog form',
values: {},
resolver: {},
onSubmit: () => undefined,
onCancel: () => undefined,
children: null
};
export default DialogForm;

View File

@ -1,234 +0,0 @@
import React 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 (
<Dialog fullScreen={isMobile} open={open} maxWidth="lg" scroll="paper">
<DialogTitle id={`${ID_CY}-title`}>
{network?.name ? 'Edit network' : 'New network'}
</DialogTitle>
<DialogContent dividers>
<Grid container spacing={3}>
<Grid item xs={12}>
<FormControlLabel
control={
<Checkbox
name="mandatory"
color="primary"
defaultChecked={network?.mandatory}
inputProps={{ 'data-cy': `${ID_CY}-mandatory` }}
inputRef={register}
/>
}
label={Tr('Mandatory')}
labelPlacement="end"
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
name="name"
label={Tr('Name')}
inputRef={register}
inputProps={{ 'data-cy': `${ID_CY}-name` }}
error={errors.name}
helperText={
errors.name && <ErrorHelper label={errors.name?.message} />
}
FormHelperTextProps={{ 'data-cy': `${ID_CY}-name-error` }}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
multiline
name="description"
label={Tr('Description')}
inputRef={register}
inputProps={{ 'data-cy': `${ID_CY}-description` }}
error={errors.description}
helperText={
errors.description && (
<ErrorHelper label={errors.description?.message} />
)
}
FormHelperTextProps={{
'data-cy': `${ID_CY}-description-error`
}}
/>
</Grid>
<Grid item xs={12} sm={6}>
<Controller
as={
<TextField
select
fullWidth
inputProps={{ 'data-cy': `${ID_CY}-type` }}
label={Tr('Select a type')}
error={errors.type}
helperText={
errors.type && (
<ErrorHelper label={errors.type?.message} />
)
}
FormHelperTextProps={{
'data-cy': `${ID_CY}-type-error`
}}
>
{Object.entries(TYPES_NETWORKS).map(([key, { text }]) => (
<MenuItem key={`type-${key}`} value={key}>
{text}
</MenuItem>
))}
</TextField>
}
name="type"
control={control}
/>
</Grid>
<Grid item xs={12} sm={6}>
<Controller
as={
<TextField
select
fullWidth
inputProps={{ 'data-cy': `${ID_CY}-id` }}
label={Tr('Select a') + SELECT[typeSelected]}
error={errors.id}
helperText={
errors.id && <ErrorHelper label={errors.id?.message} />
}
FormHelperTextProps={{
'data-cy': `${ID_CY}-id-error`
}}
>
{selectType?.map(({ ID, NAME }) => (
<MenuItem key={`${typeSelected}-${ID}`} value={ID}>
{NAME}
</MenuItem>
))}
</TextField>
}
name="id"
control={control}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
multiline
name="extra"
label={Tr('Extra template')}
inputRef={register}
inputProps={{ 'data-cy': `${ID_CY}-extra` }}
error={errors.extra}
helperText={
errors.extra && <ErrorHelper label={errors.extra?.message} />
}
FormHelperTextProps={{
'data-cy': `${ID_CY}-extra-error`
}}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={onCancel} color="primary">
{Tr('Cancel')}
</Button>
<Button
type="submit"
variant="contained"
color="primary"
onClick={handleSubmit(onSubmit)}
>
{Tr('Save')}
</Button>
</DialogActions>
</Dialog>
);
}
);
export default NetworkDialog;

View File

@ -1,96 +0,0 @@
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 (
<Dialog fullScreen={isMobile} open={open} maxWidth="lg" scroll="paper">
<DialogTitle id={`${ID_CY}-title`}>
{role?.name ? 'Edit role' : 'New role'}
</DialogTitle>
<DialogContent dividers>
<Grid container spacing={3}>
<Grid item xs={12}>
{'ROLE FORM'}
{/* <Controller
as={
<TextField
select
fullWidth
inputProps={{ 'data-cy': `${ID_CY}-id` }}
label={Tr('Select a vm')}
error={errors.template}
helperText={
errors.template && (
<ErrorHelper label={errors.template?.message} />
)
}
FormHelperTextProps={{
'data-cy': `${ID_CY}-id-error`
}}
>
{templates?.map(({ ID, NAME }) => (
<MenuItem key={`template-${ID}`} value={ID}>
{NAME}
</MenuItem>
))}
</TextField>
}
name="template"
control={control}
/> */}
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={onCancel} color="primary">
{Tr('Cancel')}
</Button>
<Button
type="submit"
variant="contained"
color="primary"
onClick={handleSubmit(() => onSubmit(role))}
>
{Tr('Save')}
</Button>
</DialogActions>
</Dialog>
);
});
export default NetworkDialog;

View File

@ -1,4 +1,3 @@
import NetworkDialog from 'client/components/Dialogs/NetworkDialog';
import RoleDialog from 'client/components/Dialogs/RoleDialog';
import DialogForm from 'client/components/Dialogs/DialogForm';
export { NetworkDialog, RoleDialog };
export { DialogForm };

View File

@ -0,0 +1,101 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Fab, Box, Card } from '@material-ui/core';
import AddIcon from '@material-ui/icons/Add';
import ReactFlow, {
removeElements,
addEdge,
MiniMap,
Background,
isNode
} from 'react-flow-renderer';
const initialElements = [];
const onNodeDragStart = (event, node) => console.log('drag start', node);
const onNodeDragStop = (event, node) => console.log('drag stop', node);
const onSelectionDrag = (event, nodes) => console.log('selection drag', nodes);
const onSelectionDragStart = (event, nodes) =>
console.log('selection drag start', nodes);
const onSelectionDragStop = (event, nodes) =>
console.log('selection drag stop', nodes);
const onElementClick = (event, element) =>
console.log(`${isNode(element) ? 'node' : 'edge'} click:`, element);
const onSelectionChange = elements => console.log('selection change', elements);
const onLoad = reactFlowInstance => {
console.log('flow loaded:', reactFlowInstance);
reactFlowInstance.fitView();
};
const onMoveEnd = transform => console.log('zoom/move end', transform);
const connectionLineStyle = { stroke: '#ddd' };
const snapGrid = [16, 16];
const CustomNode = React.memo(({ data }) => (
<Card style={{ height: 100 }}>
<div>Custom node</div>
</Card>
));
const FlowWithFAB = ({ handleClick }) => {
const [elements, setElements] = useState(initialElements);
const onElementsRemove = elementsToRemove =>
setElements(els => removeElements(elementsToRemove, els));
const onConnect = params => setElements(els => addEdge(params, els));
return (
<Box flexGrow={1} height={1}>
<ReactFlow
elements={elements}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
onConnect={onConnect}
onNodeDragStart={onNodeDragStart}
onNodeDragStop={onNodeDragStop}
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionChange={onSelectionChange}
onMoveEnd={onMoveEnd}
onLoad={onLoad}
connectionLineStyle={connectionLineStyle}
snapToGrid
snapGrid={snapGrid}
nodeTypes={{ custom: CustomNode }}
>
<MiniMap
nodeColor={n => {
if (n.style?.background) return n.style.background;
if (n.type === 'input') return '#9999ff';
if (n.type === 'output') return '#79c9b7';
if (n.type === 'default') return '#ff6060';
return '#eee';
}}
/>
<Fab
color="primary"
aria-label="add-role"
onClick={handleClick}
style={{ top: 10, left: 10, zIndex: 5 }}
>
<AddIcon />
</Fab>
<Background color="#aaa" gap={16} />
</ReactFlow>
</Box>
);
};
FlowWithFAB.propTypes = {
handleClick: PropTypes.func
};
FlowWithFAB.defaultProps = {
handleClick: evt => evt
};
export default FlowWithFAB;

View File

@ -1,42 +1,17 @@
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';
import { Box } from '@material-ui/core';
import { useFormContext } from 'react-hook-form';
import ErrorHelper from 'client/components/FormControl/ErrorHelper';
const useStyles = makeStyles(() => ({
cardPlus: {
height: '100%',
minHeight: 140,
display: 'flex',
textAlign: 'center'
}
}));
function FormDialog({ step, data, setFormData }) {
const classes = useStyles();
function FormList({ step, data, setFormData }) {
const { errors } = useFormContext();
const [dialogFormData, setDialogFormData] = useState({});
const [showDialog, setShowDialog] = useState(false);
const {
id,
addCardAction,
preRender,
InfoComponent,
DialogComponent,
DEFAULT_DATA
} = step;
const { id, preRender, ListComponent, DialogComponent, DEFAULT_DATA } = step;
useEffect(() => {
preRender && preRender();
@ -81,43 +56,22 @@ function FormDialog({ step, data, setFormData }) {
return (
<Box component="form">
<Grid container spacing={3}>
{typeof errors[id]?.message === 'string' && (
<Grid item xs={12}>
<ErrorHelper label={errors[id]?.message} />
</Grid>
)}
{addCardAction &&
React.useMemo(
() => (
<Grid item xs={12} sm={4} md={3} lg={2}>
<Card className={classes.cardPlus} raised>
<CardActionArea onClick={() => handleOpen()}>
<CardContent>
<Add />
</CardContent>
</CardActionArea>
</Card>
</Grid>
),
[handleOpen, classes]
)}
{Array.isArray(data) &&
data?.map((info, index) => (
<Grid key={`${id}-${index}`} item xs={12} sm={4} md={3} lg={2}>
<InfoComponent
info={info}
handleEdit={() => handleOpen(index)}
handleClone={() => handleClone(index)}
handleRemove={() => handleRemove(index)}
/>
</Grid>
))}
</Grid>
{typeof errors[id]?.message === 'string' && (
<ErrorHelper label={errors[id]?.message} />
)}
<ListComponent
list={data}
addCardClick={() => handleOpen()}
itemsProps={({ index }) => ({
handleEdit: () => handleOpen(index),
handleClone: () => handleClone(index),
handleRemove: () => handleRemove(index)
})}
/>
{showDialog && DialogComponent && (
<DialogComponent
open={showDialog}
info={dialogFormData?.data}
values={dialogFormData?.data}
onSubmit={handleSubmit}
onCancel={handleClose}
/>
@ -126,16 +80,16 @@ function FormDialog({ step, data, setFormData }) {
);
}
FormDialog.propTypes = {
FormList.propTypes = {
step: PropTypes.objectOf(PropTypes.any).isRequired,
data: PropTypes.arrayOf(PropTypes.object).isRequired,
setFormData: PropTypes.func.isRequired
};
FormDialog.defaultProps = {
FormList.defaultProps = {
step: {},
data: [],
setFormData: data => data
};
export default FormDialog;
export default FormList;

View File

@ -8,7 +8,7 @@ import ErrorHelper from '../FormControl/ErrorHelper';
function FormListSelect({ step, data, setFormData }) {
const { errors } = useFormContext();
const { id, onlyOneSelect, preRender, list, InfoComponent } = step;
const { id, onlyOneSelect, preRender, list, ItemComponent } = step;
useEffect(() => {
preRender && preRender();
@ -37,7 +37,7 @@ function FormListSelect({ step, data, setFormData }) {
{Array.isArray(list) &&
list?.map((info, index) => (
<Grid key={`${id}-${index}`} item xs={6} sm={4} md={3} lg={1}>
<InfoComponent
<ItemComponent
info={info}
isSelected={data?.some(selected => selected === info?.ID)}
handleSelect={handleSelect}

View File

@ -1,18 +1,44 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useFormContext } from 'react-hook-form';
import ErrorHelper from 'client/components/FormControl/ErrorHelper';
function FormStep({ step, data, setFormData }) {
const { reset, errors } = useFormContext();
const { id, preRender, FormComponent } = step;
const { errors } = useFormContext();
const [showDialog, setShowDialog] = useState(false);
const { id, preRender, FormComponent, DialogComponent } = step;
useEffect(() => {
preRender && preRender();
reset({ [id]: data }, { errors: true });
}, []);
return React.useMemo(() => <FormComponent id={id} />, [id]);
const handleOpen = () => setShowDialog(true);
const handleClose = () => setShowDialog(false);
const handleSubmit = d => console.log(d);
return (
<>
{typeof errors[id]?.message === 'string' && (
<ErrorHelper label={errors[id]?.message} />
)}
{React.useMemo(
() => (
<FormComponent id={id} handleClick={handleOpen} />
),
[id, handleOpen]
)}
{showDialog && DialogComponent && (
<DialogComponent
open={showDialog}
values={data}
onSubmit={handleSubmit}
onCancel={handleClose}
/>
)}
</>
);
}
FormStep.propTypes = {

View File

@ -1,11 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, MobileStepper } from '@material-ui/core';
import { styled, Button, MobileStepper } from '@material-ui/core';
import { KeyboardArrowLeft, KeyboardArrowRight } from '@material-ui/icons';
import { Tr } from 'client/components/HOC';
const StickyMobileStepper = styled(MobileStepper)({
position: 'sticky',
top: -15,
backdropFilter: 'blur(5px)',
background: '#fafafa9c',
zIndex: 1
});
const CustomMobileStepper = ({
totalSteps,
activeStep,
@ -14,12 +22,11 @@ const CustomMobileStepper = ({
handleNext,
handleBack
}) => (
<MobileStepper
<StickyMobileStepper
variant="progress"
position="static"
steps={totalSteps}
activeStep={activeStep}
style={{ flexGrow: 1 }}
backButton={
<Button size="small" onClick={handleBack} disabled={disabledBack}>
<KeyboardArrowLeft /> {Tr('Back')}

View File

@ -1,17 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Stepper, Step, StepLabel, Box } from '@material-ui/core';
import {
styled,
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 StickyStepper = styled(Stepper)({
position: 'sticky',
top: -15,
backdropFilter: 'blur(5px)',
background: '#fafafa9c',
zIndex: 1
});
const CustomStepper = ({
steps,
@ -22,13 +29,13 @@ const CustomStepper = ({
handleBack
}) => (
<>
<Stepper activeStep={activeStep}>
<StickyStepper activeStep={activeStep}>
{steps?.map(({ label }) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
</StickyStepper>
<Box marginY={2}>
<Button onClick={handleBack} disabled={disabledBack}>
{Tr('Back')}

View File

@ -15,12 +15,12 @@ const InputController = {
[TYPE_INPUT.CHECKBOX]: CheckboxController
};
const FormWithSchema = ({ id, cy, schema }) => {
const FormWithSchema = ({ id, cy, fields }) => {
const { control, errors } = useFormContext();
return (
<Grid container spacing={3}>
{schema?.map(({ name, type, label, values }) => {
<Grid container spacing={1}>
{fields?.map(({ name, type, label, values }) => {
const dataCy = `${cy}-${name}`;
const inputName = id ? `${id}.${name}` : name;
const formError = id ? errors[id] : errors;
@ -28,14 +28,15 @@ const FormWithSchema = ({ id, cy, schema }) => {
return (
<Grid key={`${cy}-${name}`} item xs={12} md={6}>
{React.createElement(InputController[type], {
control,
cy: dataCy,
name: inputName,
label,
values,
error: inputError
})}
{InputController[type] &&
React.createElement(InputController[type], {
control,
cy: dataCy,
name: inputName,
label,
values,
error: inputError
})}
</Grid>
);
})}
@ -46,13 +47,13 @@ const FormWithSchema = ({ id, cy, schema }) => {
FormWithSchema.propTypes = {
id: PropTypes.string,
cy: PropTypes.string,
schema: PropTypes.arrayOf(PropTypes.object)
fields: PropTypes.arrayOf(PropTypes.object)
};
FormWithSchema.defaultProps = {
id: '',
cy: 'form',
schema: []
fields: []
};
export default FormWithSchema;

View File

@ -0,0 +1,70 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
makeStyles,
CardActionArea,
CardContent,
Card,
Grid
} from '@material-ui/core';
import AddIcon from '@material-ui/icons/Add';
const useStyles = makeStyles(() => ({
cardPlus: {
height: '100%',
minHeight: 140,
display: 'flex',
textAlign: 'center'
}
}));
function ListCards({ addCardClick, list, CardComponent, cardsProps }) {
const classes = useStyles();
return (
<Grid container spacing={3}>
{addCardClick &&
React.useMemo(
() => (
<Grid item xs={12} sm={4} md={3} lg={2}>
<Card className={classes.cardPlus} raised>
<CardActionArea onClick={addCardClick}>
<CardContent>
<AddIcon />
</CardContent>
</CardActionArea>
</Card>
</Grid>
),
[addCardClick, classes]
)}
{Array.isArray(list) &&
list?.map((value, index) => (
<Grid key={`card-${index}`} item xs={12} sm={4} md={3} lg={2}>
<CardComponent value={value} {...cardsProps({ index })} />
</Grid>
))}
</Grid>
);
}
ListCards.propTypes = {
list: PropTypes.arrayOf(PropTypes.any).isRequired,
addCardClick: PropTypes.func,
CardComponent: PropTypes.oneOfType([
PropTypes.node,
PropTypes.object,
PropTypes.element
]),
cardsProps: PropTypes.func
};
ListCards.defaultProps = {
list: [],
addCardClick: [],
CardComponent: null,
cardsProps: () => undefined
};
export default ListCards;

View File

@ -0,0 +1,22 @@
import React from 'react';
import FormStep from 'client/components/FormStepper/FormStep';
import FormWithSchema from 'client/components/Forms/FormWithSchema';
import { FORM_FIELDS, STEP_FORM_SCHEMA } from './schema';
const BasicConfiguration = () => {
const STEP_ID = 'service';
return {
id: STEP_ID,
label: 'Service configuration',
content: FormStep,
resolver: STEP_FORM_SCHEMA,
FormComponent: () => (
<FormWithSchema cy="form-flow" fields={FORM_FIELDS} id={STEP_ID} />
)
};
};
export default BasicConfiguration;

View File

@ -1,4 +1,6 @@
import * as yup from 'yup';
import { TYPE_INPUT } from 'client/constants';
import { getValidationFromFields } from 'client/utils/helpers';
export const STRATEGIES_DEPLOY = [
{ text: 'None', value: 'none' },
@ -11,39 +13,58 @@ export const SHUTDOWN_ACTIONS = [
{ text: 'Terminate hard', value: 'shutdown-hard' }
];
export default [
export const FORM_FIELDS = [
{
name: 'name',
label: 'Name',
type: TYPE_INPUT.TEXT,
initial: ''
validation: yup
.string()
.min(5)
.trim()
.required('Name field is required')
.default('One_service')
},
{
name: 'description',
label: 'Description',
type: TYPE_INPUT.TEXT,
multiline: true,
initial: ''
validation: yup
.string()
.trim()
.default('OpenNebula is so cool!')
},
{
name: 'deployment',
label: 'Select a strategy',
type: TYPE_INPUT.SELECT,
initial: STRATEGIES_DEPLOY[1].value,
values: STRATEGIES_DEPLOY
values: STRATEGIES_DEPLOY,
validation: yup
.string()
.required()
.oneOf(STRATEGIES_DEPLOY.map(({ value }) => value))
.default(STRATEGIES_DEPLOY[0].value)
},
{
name: 'shutdown_action',
label: 'Select a VM shutdown action',
type: TYPE_INPUT.SELECT,
initial: SHUTDOWN_ACTIONS[0].value,
values: SHUTDOWN_ACTIONS
values: SHUTDOWN_ACTIONS,
validation: yup
.string()
.oneOf(SHUTDOWN_ACTIONS.map(({ value }) => value))
.default(SHUTDOWN_ACTIONS[0].value)
},
{
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
validation: yup.boolean().default(false)
}
];
export const STEP_FORM_SCHEMA = yup.object(
getValidationFromFields(FORM_FIELDS)
);

View File

@ -0,0 +1,29 @@
import { useMemo } from 'react';
import useOpennebula from 'client/hooks/useOpennebula';
import { ClusterCard } from 'client/components/Cards';
import FormListSelect from 'client/components/FormStepper/FormListSelect';
import { STEP_FORM_SCHEMA } from './schema';
const Clusters = () => {
const STEP_ID = 'clusters';
const { clusters, getClusters } = useOpennebula();
return useMemo(
() => ({
id: STEP_ID,
label: 'Where will it run?',
content: FormListSelect,
resolver: STEP_FORM_SCHEMA,
onlyOneSelect: true,
preRender: getClusters,
list: clusters,
ItemComponent: ClusterCard
}),
[getClusters, clusters]
);
};
export default Clusters;

View File

@ -0,0 +1,9 @@
import * as yup from 'yup';
export const STEP_FORM_SCHEMA = yup
.array()
.of(yup.string().trim())
.min(1)
.max(1)
.required()
.default([]);

View File

@ -0,0 +1,52 @@
import React, { useMemo } from 'react';
import useOpennebula from 'client/hooks/useOpennebula';
import { DialogForm } from 'client/components/Dialogs';
import { NetworkCard } from 'client/components/Cards';
import FormList from 'client/components/FormStepper/FormList';
import FormWithSchema from 'client/components/Forms/FormWithSchema';
import ListCards from 'client/components/List/ListCards';
import { FORM_FIELDS, NETWORK_FORM_SCHEMA, STEP_FORM_SCHEMA } from './schema';
const Networks = () => {
const STEP_ID = 'networks';
const { getVNetworks, getVNetworksTemplates } = useOpennebula();
return useMemo(
() => ({
id: STEP_ID,
label: 'Networks configuration',
content: FormList,
preRender: () => {
getVNetworks();
getVNetworksTemplates();
},
resolver: STEP_FORM_SCHEMA,
DEFAULT_DATA: NETWORK_FORM_SCHEMA.default(),
ListComponent: ({ list, addCardClick, itemsProps }) => (
<ListCards
list={list}
addCardClick={addCardClick}
CardComponent={NetworkCard}
cardsProps={itemsProps}
/>
),
ItemComponent: NetworkCard,
DialogComponent: props => (
<DialogForm
title={'Network form'}
resolver={NETWORK_FORM_SCHEMA}
{...props}
>
<FormWithSchema cy="form-dg-network" fields={FORM_FIELDS} />
</DialogForm>
)
}),
[getVNetworks, getVNetworksTemplates]
);
};
export default Networks;

View File

@ -0,0 +1,110 @@
import * as yup from 'yup';
import { TYPE_INPUT } from 'client/constants';
import { getValidationFromFields } from 'client/utils/helpers';
const DEFAULT_NETWORK = {
mandatory: true,
name: 'Public_dev',
description: 'Public network in development mode',
type: 'id',
id: '0',
extra: 'size=5'
};
export const SELECT = {
template: 'template',
network: 'network'
};
export const TYPES_NETWORKS = [
{ text: 'Create', value: 'template_id', select: SELECT.template },
{ text: 'Reserve', value: 'reserve_from', select: SELECT.network },
{ text: 'Existing', value: 'id', select: SELECT.network }
];
export const FORM_FIELDS = [
{
name: 'mandatory',
label: 'Mandatory',
type: TYPE_INPUT.CHECKBOX,
validation: yup
.boolean()
.required()
.default(false)
},
{
name: 'name',
label: 'Name',
type: TYPE_INPUT.TEXT,
validation: yup
.string()
.trim()
.required('Name is a required field')
.default('')
},
{
name: 'description',
label: 'Description',
type: TYPE_INPUT.TEXT,
multiline: true,
validation: yup
.string()
.trim()
.default('')
},
{
name: 'type',
label: 'Select a type',
type: TYPE_INPUT.SELECT,
values: TYPES_NETWORKS,
validation: yup
.string()
.oneOf(TYPES_NETWORKS.map(({ value }) => value))
.required('Type is required field')
.default(TYPES_NETWORKS[0].value)
},
{
name: 'id',
label: `Select a network`,
type: TYPE_INPUT.TEXT,
validation: yup
.string()
.when('type', {
is: type =>
TYPES_NETWORKS.some(
({ value, select }) => type === value && select === SELECT.network
),
then: yup
.string()
.trim()
.required('Network is required field'),
otherwise: yup
.string()
.trim()
.required('Network template is required field')
})
.required()
.default('')
},
{
name: 'extra',
label: 'Extra',
multiline: true,
type: TYPE_INPUT.TEXT,
validation: yup
.string()
.trim()
.default('')
}
];
export const NETWORK_FORM_SCHEMA = yup.object(
getValidationFromFields(FORM_FIELDS)
);
export const STEP_FORM_SCHEMA = yup
.array()
.of(NETWORK_FORM_SCHEMA)
.min(2)
.required()
.default([DEFAULT_NETWORK, DEFAULT_NETWORK]);

View File

@ -0,0 +1,22 @@
import React from 'react';
import FormStep from 'client/components/FormStepper/FormStep';
import FormWithSchema from 'client/components/Forms/FormWithSchema';
import { FORM_FIELDS, STEP_FORM_SCHEMA } from './schema';
const BasicConfiguration = () => {
const STEP_ID = 'role';
return {
id: STEP_ID,
label: 'Role configuration',
content: FormStep,
resolver: STEP_FORM_SCHEMA,
FormComponent: () => (
<FormWithSchema cy="form-flow" fields={FORM_FIELDS} id={STEP_ID} />
)
};
};
export default BasicConfiguration;

View File

@ -0,0 +1,31 @@
import * as yup from 'yup';
import { TYPE_INPUT } from 'client/constants';
import { getValidationFromFields } from 'client/utils/helpers';
export const FORM_FIELDS = [
{
name: 'name',
label: 'Name',
type: TYPE_INPUT.TEXT,
validation: yup
.string()
.min(1)
.trim()
.required('Name field is required')
.default('Main')
},
{
name: 'cardinality',
label: 'Cardinality',
type: TYPE_INPUT.TEXT,
validation: yup
.number()
.min(1)
.required()
.default(1)
}
];
export const STEP_FORM_SCHEMA = yup.object(
getValidationFromFields(FORM_FIELDS)
);

View File

@ -0,0 +1,19 @@
import React from 'react';
import FormStep from 'client/components/FormStepper/FormStep';
import { STEP_FORM_SCHEMA } from './schema';
const Template = () => {
const STEP_ID = 'template';
return {
id: STEP_ID,
label: 'Template VM',
content: FormStep,
resolver: STEP_FORM_SCHEMA,
FormComponent: () => <h1>Screen with options</h1>
};
};
export default Template;

View File

@ -0,0 +1,21 @@
import * as yup from 'yup';
import { TYPE_INPUT } from 'client/constants';
import { getValidationFromFields } from 'client/utils/helpers';
export const FORM_FIELDS = [
{
name: 'template',
label: 'Template VM',
type: TYPE_INPUT.TEXT,
validation: yup
.string()
.min(1)
.trim()
.required('Template field is required')
.default('0')
}
];
export const STEP_FORM_SCHEMA = yup.object(
getValidationFromFields(FORM_FIELDS)
);

View File

@ -0,0 +1,25 @@
import * as yup from 'yup';
import BasicConfiguration from './BasicConfiguration';
import Template from './Template';
// import Policies from './Policies';
const Steps = () => {
const basic = BasicConfiguration();
const template = Template();
// const policies = Policies();
const steps = [basic, template];
const resolvers = yup.object({
[basic.id]: basic.resolver,
[template.id]: template.resolver
// [policies.id]: policies.resolver
});
const defaultValues = resolvers.default();
return { steps, defaultValues, resolvers };
};
export default Steps;

View File

@ -0,0 +1,54 @@
import React, { useMemo } from 'react';
import * as yup from 'yup';
import { useForm, FormProvider } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers';
import FormStepper from 'client/components/FormStepper';
import { DialogForm } from 'client/components/Dialogs';
import FormStep from 'client/components/FormStepper/FormStep';
import FlowWithFAB from 'client/components/Flows/FlowWithFAB';
import Steps from './Steps';
const Roles = () => {
const STEP_ID = 'roles';
const { steps, defaultValues, resolvers } = Steps();
const methods = useForm({
mode: 'onBlur',
defaultValues,
resolver: yupResolver(resolvers)
});
const onSubmit = d => console.log('role data form', d);
return useMemo(
() => ({
id: STEP_ID,
label: 'Defining each role',
content: FormStep,
resolver: yup
.array()
.of(resolvers)
.min(1)
.required()
.default([]),
DialogComponent: props => (
<DialogForm title={'Role form'} resolver={resolvers} {...props}>
<FormProvider {...methods}>
<FormStepper
steps={steps}
initialValue={defaultValues}
onSubmit={onSubmit}
/>
</FormProvider>
</DialogForm>
),
FormComponent: FlowWithFAB
}),
[]
);
};
export default Roles;

View File

@ -0,0 +1,28 @@
import * as yup from 'yup';
import BasicConfiguration from './BasicConfiguration';
import Networks from './Networks';
import Roles from './Roles';
import Clusters from './Clusters';
const Steps = () => {
const basic = BasicConfiguration();
const networks = Networks();
const roles = Roles();
const clusters = Clusters();
const steps = [basic, networks, roles, clusters];
const resolvers = yup.object({
[basic.id]: basic.resolver,
[networks.id]: networks.resolver,
[roles.id]: roles.resolver,
[clusters.id]: clusters.resolver
});
const defaultValues = resolvers.default();
return { steps, defaultValues, resolvers };
};
export default Steps;

View File

@ -1,11 +1,11 @@
import React from 'react';
import { Container } from '@material-ui/core';
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 { Container } from '@material-ui/core';
import Steps from 'client/containers/Application/Create/Steps';
function ApplicationCreate() {
const { steps, defaultValues, resolvers } = Steps();
@ -19,7 +19,10 @@ function ApplicationCreate() {
const onSubmit = formData => console.log('submit', formData, methods.errors);
return (
<Container disableGutters>
<Container
disableGutters
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
>
<FormProvider {...methods}>
<FormStepper
steps={steps}

View File

@ -1,148 +0,0 @@
import React, { useMemo } from 'react';
import * as yup from 'yup';
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, {
SHUTDOWN_ACTIONS,
STRATEGIES_DEPLOY
} 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 }),
{}
),
resolver: yup.object().shape({
name: yup
.string()
.min(5)
.trim()
.required('is required'),
description: yup.string().trim(),
deployment: yup
.string()
.required()
.oneOf(STRATEGIES_DEPLOY.map(({ value }) => value)),
shutdown_action: yup
.string()
.oneOf(SHUTDOWN_ACTIONS.map(({ value }) => value)),
ready_status_gate: yup.boolean()
}),
FormComponent: props => (
<FormWithSchema cy="form-flow" schema={Schema} {...props} />
)
},
{
id: 'networks',
label: 'Networks configuration',
content: FormDialog,
preRender: () => {
getVNetworks();
getVNetworksTemplates();
},
defaultValue: [],
resolver: yup
.array()
.min(2)
.required(),
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: [],
resolver: yup
.array()
.min(1)
.required(),
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: [],
resolver: yup
.array()
.min(1)
.max(1)
.required(),
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]
);
const resolvers = useMemo(
() =>
yup
.object()
.shape(
steps.reduce(
(values, { id, resolver }) => ({ ...values, [id]: resolver }),
{}
)
)
.required(),
[steps]
);
return { steps, defaultValues, resolvers };
}
export default Steps;

View File

@ -17,6 +17,7 @@ export default function useOpennebula() {
vNetworksTemplates,
templates,
clusters,
apps,
filterPool: filter
} = useSelector(
state => ({
@ -66,6 +67,14 @@ export default function useOpennebula() {
.catch(err => dispatch(failureOneRequest({ error: err })));
}, [dispatch, filter]);
const getMarketApps = useCallback(() => {
dispatch(startOneRequest());
return servicePool
.getMarketApps({ filter })
.then(data => dispatch(actions.setApps(data)))
.catch(err => dispatch(failureOneRequest({ error: err })));
}, [dispatch, filter]);
const getClusters = useCallback(() => {
dispatch(startOneRequest());
return servicePool
@ -85,6 +94,8 @@ export default function useOpennebula() {
getVNetworksTemplates,
templates,
getTemplates,
apps,
getMarketApps,
clusters,
getClusters
};

View File

@ -25,7 +25,7 @@ const initial = {
vmGroups: [],
images: [],
files: [],
marketPlaces: [],
marketplaces: [],
apps: [],
vNetworks: [],
vNetworksTemplates: [],

View File

@ -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 MarketApp from 'server/utils/constants/commands/marketapp';
import Cluster from 'server/utils/constants/commands/cluster';
import httpCodes from 'server/utils/constants/http-codes';
@ -78,6 +79,20 @@ export const getTemplates = ({ filter }) => {
});
};
export const getMarketApps = ({ filter }) => {
const name = MarketApp.Actions.MARKETAPP_POOL_INFO;
const { url, options } = requestParams(
{ filter },
{ name, ...MarketApp.Commands[name] }
);
return requestData(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res;
return [res?.data?.MARKETPLACEAPP_POOL?.MARKETPLACEAPP ?? []].flat();
});
};
export const getClusters = ({ filter }) => {
const name = Cluster.Actions.CLUSTER_POOL_INFO;
const { url, options } = requestParams(
@ -98,5 +113,6 @@ export default {
getVNetworks,
getVNetworksTemplates,
getTemplates,
getMarketApps,
getClusters
};

View File

@ -1 +1,10 @@
export const fakeDelay = ms => new Promise(resolve => setTimeout(resolve, ms));
export const getValidationFromFields = schema =>
schema.reduce(
(validation, field) => ({
...validation,
[field?.name]: field?.validation
}),
{}
);