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

F #3951: Add service config step (#192)

This commit is contained in:
Sergio Betanzos 2020-09-08 19:05:54 +02:00 committed by GitHub
parent 75b90fb438
commit 993284eb97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 678 additions and 265 deletions

View 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;

View File

@ -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>

View File

@ -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 };

View File

@ -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 };

View File

@ -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>

View File

@ -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;

View 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;

View File

@ -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;

View File

@ -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 = {

View File

@ -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
};

View 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;

View File

@ -0,0 +1,3 @@
import FormWithSchema from 'client/components/Forms/FormWithSchema';
export { FormWithSchema };

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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
}
];

View File

@ -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;

View File

@ -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
</>
);
}

View File

@ -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
};
}

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 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
};

View File

@ -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);
};
};