mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-21 14:50:08 +03:00
parent
75b90fb438
commit
993284eb97
131
src/fireedge/src/public/components/Cards/ClusterCard.js
Normal file
131
src/fireedge/src/public/components/Cards/ClusterCard.js
Normal file
@ -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 (
|
||||
<Fade in unmountOnExit={false}>
|
||||
<Card
|
||||
className={clsx(classes.root, { [classes.selected]: isSelected })}
|
||||
>
|
||||
<CardActionArea
|
||||
className={classes.actionArea}
|
||||
onClick={() => (isSelected ? handleUnselect(ID) : handleSelect(ID))}
|
||||
>
|
||||
<CardHeader
|
||||
avatar={<StorageIcon />}
|
||||
className={classes.header}
|
||||
classes={{ content: classes.headerContent }}
|
||||
title={NAME}
|
||||
titleTypographyProps={{
|
||||
variant: 'body2',
|
||||
noWrap: true,
|
||||
title: NAME
|
||||
}}
|
||||
/>
|
||||
<CardContent>
|
||||
<Box className={classes.badgesWrapper}>
|
||||
<Badge
|
||||
showZero
|
||||
title={Tr('Hosts')}
|
||||
classes={{ badge: classes.badge }}
|
||||
color="primary"
|
||||
badgeContent={hosts.length}
|
||||
anchorOrigin={badgePosition}
|
||||
>
|
||||
<VideogameAssetIcon />
|
||||
</Badge>
|
||||
<Badge
|
||||
showZero
|
||||
title={Tr('Virtual networks')}
|
||||
classes={{ badge: classes.badge }}
|
||||
color="primary"
|
||||
badgeContent={vnets.length}
|
||||
anchorOrigin={badgePosition}
|
||||
>
|
||||
<AccountTreeIcon />
|
||||
</Badge>
|
||||
<Badge
|
||||
showZero
|
||||
title={Tr('Datastores')}
|
||||
classes={{ badge: classes.badge }}
|
||||
color="primary"
|
||||
badgeContent={datastores.length}
|
||||
anchorOrigin={badgePosition}
|
||||
>
|
||||
<FolderOpenIcon />
|
||||
</Badge>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Fade>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ClusterCard;
|
@ -46,31 +46,21 @@ const RoleCard = React.memo(
|
||||
scheduled_policies
|
||||
} = info;
|
||||
|
||||
const ConditionalWrapper = ({ condition, wrapper, children }) =>
|
||||
condition ? wrapper(children) : children;
|
||||
console.log(info);
|
||||
return (
|
||||
<Fade in unmountOnExit={false}>
|
||||
<Card className={classes.root}>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<ConditionalWrapper
|
||||
condition={cardinality > 1}
|
||||
wrapper={children => (
|
||||
<Badge
|
||||
badgeContent={cardinality}
|
||||
color="primary"
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge
|
||||
badgeContent={cardinality}
|
||||
color="primary"
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
>
|
||||
<DesktopWindowsIcon />
|
||||
</ConditionalWrapper>
|
||||
</Badge>
|
||||
}
|
||||
className={classes.header}
|
||||
classes={{ content: classes.headerContent }}
|
||||
@ -92,6 +82,9 @@ const RoleCard = React.memo(
|
||||
<Button variant="contained" size="small" onClick={handleEdit}>
|
||||
{Tr('Edit')}
|
||||
</Button>
|
||||
<Button variant="contained" size="small" onClick={handleClone}>
|
||||
{Tr('Clone')}
|
||||
</Button>
|
||||
<Button size="small" onClick={handleRemove}>
|
||||
{Tr('Remove')}
|
||||
</Button>
|
||||
|
@ -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 };
|
||||
|
@ -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 };
|
||||
|
@ -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 (
|
||||
<Box className={classes.root} {...rest}>
|
||||
<Box component="span" className={classes.root} {...rest}>
|
||||
<InfoIcon className={classes.icon} />
|
||||
<Typography className={classes.text} data-cy="error-text">
|
||||
<Typography
|
||||
className={classes.text}
|
||||
component="span"
|
||||
data-cy="error-text"
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
@ -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 && <CircularProgress size={24} />}
|
||||
{!isSubmitting && Tr(label)}
|
||||
{!isSubmitting && (label ?? Tr(Submit))}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
128
src/fireedge/src/public/components/FormStepper/FormDialog.js
Normal file
128
src/fireedge/src/public/components/FormStepper/FormDialog.js
Normal file
@ -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 (
|
||||
<Box component="form">
|
||||
<Grid container spacing={3}>
|
||||
{addCardAction && (
|
||||
<Grid item xs={12} sm={4} md={3} lg={2}>
|
||||
<Card className={classes.cardPlus} raised>
|
||||
<CardActionArea onClick={() => handleOpen()}>
|
||||
<CardContent>
|
||||
<Add />
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
{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>
|
||||
{showDialog && DialogComponent && (
|
||||
<DialogComponent
|
||||
open={showDialog}
|
||||
info={dialogFormData?.data}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleClose}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
@ -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 (
|
||||
<Box component="form">
|
||||
<Grid container spacing={3}>
|
||||
{Array.isArray(list) &&
|
||||
list?.map((info, index) => (
|
||||
<Grid key={`${id}-${index}`} item xs={6} sm={4} md={3} lg={1}>
|
||||
<InfoComponent
|
||||
info={info}
|
||||
isSelected={data?.some(selected => selected === info?.ID)}
|
||||
handleSelect={handleSelect}
|
||||
handleUnselect={handleUnselect}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
@ -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 (
|
||||
<Box component="form">
|
||||
<Grid container spacing={3}>
|
||||
{addAction && (
|
||||
<Grid item xs={12} sm={4} md={3} lg={2}>
|
||||
<Card className={classes.cardPlus} raised>
|
||||
<CardActionArea onClick={() => handleOpen()}>
|
||||
<CardContent>
|
||||
<Add />
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
{Array.isArray(stepData) &&
|
||||
stepData?.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>
|
||||
{showDialog && (
|
||||
<DialogComponent
|
||||
open={showDialog}
|
||||
info={dialogFormData?.data}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleClose}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
return React.useMemo(() => <FormComponent id={id} />, [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 = {
|
||||
|
@ -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
|
||||
data={formData}
|
||||
setFormData={setFormData}
|
||||
step={{ ...rest }}
|
||||
/>
|
||||
Content && (
|
||||
<Content
|
||||
formData={formData}
|
||||
data={formData[id]}
|
||||
setFormData={setFormData}
|
||||
step={steps[activeStep]}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}, [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
|
||||
};
|
||||
|
||||
|
69
src/fireedge/src/public/components/Forms/FormWithSchema.js
Normal file
69
src/fireedge/src/public/components/Forms/FormWithSchema.js
Normal file
@ -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 (
|
||||
<Grid container spacing={3}>
|
||||
{schema?.map(({ name, type, label, values }) => (
|
||||
<Grid key={`${cy}-${name}`} item xs={12} md={6}>
|
||||
{(type === TYPE_INPUT.TEXT || type === TYPE_INPUT.SELECT) && (
|
||||
<Controller
|
||||
as={
|
||||
<TextField
|
||||
fullWidth
|
||||
select={type === TYPE_INPUT.SELECT}
|
||||
label={label}
|
||||
inputProps={{ 'data-cy': `${cy}-${name}` }}
|
||||
error={errors[name]}
|
||||
helperText={
|
||||
errors[name] && (
|
||||
<ErrorHelper label={errors[name]?.message} />
|
||||
)
|
||||
}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-${name}-error` }}
|
||||
>
|
||||
{type === TYPE_INPUT.SELECT &&
|
||||
values?.map(({ text, value }) => (
|
||||
<MenuItem key={`${name}-${value}`} value={`${value}`}>
|
||||
{text}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
}
|
||||
name={`${id}.${name}`}
|
||||
control={control}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
FormWithSchema.propTypes = {
|
||||
id: PropTypes.string,
|
||||
cy: PropTypes.string,
|
||||
schema: PropTypes.arrayOf(PropTypes.object)
|
||||
};
|
||||
|
||||
FormWithSchema.defaultProps = {
|
||||
id: '',
|
||||
cy: 'form',
|
||||
schema: []
|
||||
};
|
||||
|
||||
export default FormWithSchema;
|
3
src/fireedge/src/public/components/Forms/index.js
Normal file
3
src/fireedge/src/public/components/Forms/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema';
|
||||
|
||||
export { FormWithSchema };
|
@ -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 = () => (
|
||||
<ScreenBox
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100vh',
|
||||
backgroundColor: '#ffffff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'fixed',
|
||||
zIndex: 10000
|
||||
}}
|
||||
>
|
||||
<ScreenBox>
|
||||
<Logo width={360} height={360} spinner withText />
|
||||
</ScreenBox>
|
||||
);
|
||||
|
@ -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 (
|
||||
<FormProvider {...methods}>
|
||||
<FormStepper
|
||||
steps={Steps}
|
||||
initialValue={INITIAL_VALUE}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</FormProvider>
|
||||
<Container disableGutters>
|
||||
<FormProvider {...methods}>
|
||||
<FormStepper
|
||||
steps={steps}
|
||||
initialValue={defaultValues}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</FormProvider>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
];
|
@ -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 => (
|
||||
<FormWithSchema cy="form-flow" schema={Schema} {...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;
|
||||
|
@ -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 && <LinearProgress style={{ width: '100%' }} />}
|
||||
{users?.map(({ NAME, GROUPS }, index) => (
|
||||
<Card key={`user-${index}`} className={classes.card}>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Typography className={classes.title}>{NAME}</Typography>
|
||||
{[GROUPS?.ID ?? []].flat().map(ID => {
|
||||
const group = getGroupById(ID);
|
||||
return group ? (
|
||||
<Chip
|
||||
style={{ margin: '0 0.5em' }}
|
||||
key={`group-${index}-${ID}`}
|
||||
size="small"
|
||||
color="primary"
|
||||
clickable
|
||||
label={group.NAME}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
Deploy
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user