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

F #3951: Add form stepper component (#185)

* Add new endpoints
* Add pool actions
* Add form stepper component
* Add create application form
This commit is contained in:
Sergio Betanzos 2020-09-07 12:30:13 +02:00 committed by GitHub
parent 22ff32e3d5
commit 4b5b2ad763
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1203 additions and 193 deletions

View File

@ -50,13 +50,13 @@ module.exports = {
type: SUCCESS_ONE_REQUEST,
payload: { apps }
}),
setVNetworks: virtualNetworks => ({
setVNetworks: vNetworks => ({
type: SUCCESS_ONE_REQUEST,
payload: { virtualNetworks }
payload: { vNetworks }
}),
setNetworkTemplates: networkTemplates => ({
setVNetworkTemplates: vNetworksTemplates => ({
type: SUCCESS_ONE_REQUEST,
payload: { networkTemplates }
payload: { vNetworksTemplates }
}),
setSecGroups: securityGroups => ({
type: SUCCESS_ONE_REQUEST,

View File

@ -16,6 +16,10 @@ export const toolbar = {
sm: 64
};
export const footer = {
regular: 30
};
export const sidebar = {
minified: 60,
fixed: 240

View File

@ -0,0 +1,81 @@
import React from 'react';
import {
makeStyles,
Card,
Button,
CardHeader,
CardActions,
Fade
} from '@material-ui/core';
import { Tr } from 'client/components/HOC';
const useStyles = makeStyles(theme => ({
root: {
height: '100%',
minHeight: 140,
display: 'flex',
flexDirection: 'column'
},
header: {
overflowX: 'hidden',
flexGrow: 1
},
subheader: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'initial',
display: '-webkit-box',
lineClamp: 2,
boxOrient: 'vertical'
},
remove: {
backgroundColor: theme.palette.error.dark
}
}));
const NetworkCard = React.memo(
({ info, handleEdit, handleClone, handleRemove }) => {
const classes = useStyles();
const { mandatory, name, description, type, id, extra } = info;
return (
<Fade in unmountOnExit={false}>
<Card className={classes.root}>
<CardHeader
avatar={mandatory ? 'M' : ''}
className={classes.header}
classes={{ content: classes.headerContent }}
title={name}
titleTypographyProps={{
variant: 'body2',
noWrap: true,
title: name
}}
subheader={description}
subheaderTypographyProps={{
variant: 'body2',
noWrap: true,
className: classes.subheader,
title: description
}}
/>
<CardActions>
<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>
</CardActions>
</Card>
</Fade>
);
}
);
export default NetworkCard;

View File

@ -0,0 +1,105 @@
import React from 'react';
import {
makeStyles,
Card,
Button,
CardHeader,
CardActions,
Badge,
Fade
} from '@material-ui/core';
import DesktopWindowsIcon from '@material-ui/icons/DesktopWindows';
import { Tr } from 'client/components/HOC';
const useStyles = makeStyles(theme => ({
root: {
height: '100%',
minHeight: 140,
display: 'flex',
flexDirection: 'column'
},
header: {
overflowX: 'hidden',
flexGrow: 1
},
headerContent: {},
title: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'initial',
display: '-webkit-box',
lineClamp: 2,
boxOrient: 'vertical'
}
}));
const RoleCard = React.memo(
({ info, handleEdit, handleClone, handleRemove }) => {
const classes = useStyles();
const {
name = 'Role name',
cardinality,
vm_template = 0,
elasticity_policies,
scheduled_policies
} = info;
const ConditionalWrapper = ({ condition, wrapper, children }) =>
condition ? wrapper(children) : children;
console.log(info);
return (
<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>
)}
>
<DesktopWindowsIcon />
</ConditionalWrapper>
}
className={classes.header}
classes={{ content: classes.headerContent }}
title={name}
titleTypographyProps={{
variant: 'body2',
noWrap: true,
className: classes.title,
title: name
}}
subheader={`Template id: ${vm_template}`}
subheaderTypographyProps={{
variant: 'body2',
noWrap: true,
title: `Template id: ${vm_template}`
}}
/>
<CardActions>
<Button variant="contained" size="small" onClick={handleEdit}>
{Tr('Edit')}
</Button>
<Button size="small" onClick={handleRemove}>
{Tr('Remove')}
</Button>
</CardActions>
</Card>
</Fade>
);
}
);
export default RoleCard;

View File

@ -0,0 +1,4 @@
import NetworkCard from './NetworkCard';
import RoleCard from './RoleCard';
export { NetworkCard, RoleCard };

View File

@ -0,0 +1,234 @@
import React, { useMemo, useEffect } from 'react';
import {
makeStyles,
useMediaQuery,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Grid,
FormControlLabel,
Checkbox,
TextField,
MenuItem
} from '@material-ui/core';
import { useForm, Controller } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers';
import * as yup from 'yup';
import useOpennebula from 'client/hooks/useOpennebula';
import ErrorHelper from 'client/components/FormControl/ErrorHelper';
import { Tr } from 'client/components/HOC';
const useStyles = makeStyles(theme => ({}));
const SELECT = {
template: 'template',
network: 'network'
};
const TYPES_NETWORKS = {
template_id: { text: 'Create', select: SELECT.template, extra: true },
reserve_from: { text: 'Reserve', select: SELECT.network, extra: true },
id: { text: 'Existing', select: SELECT.network, extra: false }
};
const ID_CY = 'form-network';
const NetworkDialog = React.memo(
({ open, info: network, onSubmit, onCancel }) => {
const classes = useStyles();
const { vNetworks, vNetworksTemplates } = useOpennebula();
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'));
const { register, handleSubmit, errors, control, watch } = useForm({
reValidateMode: 'onSubmit',
defaultValues: {
type: Object.keys(TYPES_NETWORKS)[0],
...network
},
resolver: yupResolver(
yup.object().shape({
mandatory: yup.boolean().required(),
name: yup.string().required('Name is a required field'),
description: yup.string(),
type: yup
.string()
.oneOf(Object.keys(TYPES_NETWORKS))
.required('Type is required field'),
id: yup
.string()
.when('type', {
is: type =>
Object.entries(TYPES_NETWORKS)?.some(
([key, { select }]) =>
type === key && select === SELECT.network
),
then: yup.string().required('Network is required field'),
otherwise: yup
.string()
.required('Network template is required field')
})
.required(),
extra: yup.string()
})
)
});
const { type } = watch();
const typeSelected = TYPES_NETWORKS[type]?.select;
const selectType =
typeSelected === SELECT.network ? vNetworks : vNetworksTemplates;
return (
<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

@ -0,0 +1,96 @@
import React, { useMemo, useEffect } from 'react';
import {
makeStyles,
useMediaQuery,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Grid,
FormControlLabel,
Checkbox,
TextField,
MenuItem
} from '@material-ui/core';
import { useForm, Controller } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers';
import * as yup from 'yup';
import useOpennebula from 'client/hooks/useOpennebula';
import ErrorHelper from 'client/components/FormControl/ErrorHelper';
import { Tr } from 'client/components/HOC';
const useStyles = makeStyles(theme => ({}));
const ID_CY = 'form-role';
const NetworkDialog = React.memo(({ open, info: role, onSubmit, onCancel }) => {
// const classes = useStyles();
const { templates } = useOpennebula();
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'));
const { register, handleSubmit, errors, control } = useForm({
reValidateMode: 'onSubmit',
defaultValues: role
// resolver: yupResolver(yup.object().shape({}))
});
return (
<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

@ -0,0 +1,4 @@
import NetworkDialog from './NetworkDialog';
import RoleDialog from './RoleDialog';
export { NetworkDialog, RoleDialog };

View File

@ -14,7 +14,7 @@
/* -------------------------------------------------------------------------- */
import React from 'react';
import { Box, Link } from '@material-ui/core';
import { Box, Link, Typography } from '@material-ui/core';
import footerStyles from 'client/components/Footer/styles';
import { by } from 'client/constants';
@ -26,14 +26,16 @@ const Footer = React.memo(() => {
return (
<Box className={classes.footer} component="footer">
{'Made with'}
<span className={classes.heartIcon} role="img" aria-label="heart-emoji">
{'❤️'}
</span>
{'by'}
<Link href={url} className={classes.link}>
{text}
</Link>
<Typography variant="body2">
{'Made with'}
<span className={classes.heartIcon} role="img" aria-label="heart-emoji">
{'❤️'}
</span>
{'by'}
<Link href={url} className={classes.link}>
{text}
</Link>
</Typography>
</Box>
);
});

View File

@ -42,7 +42,14 @@ const GroupSelect = props => {
?.map(({ ID, NAME }) => (
<MenuItem key={`selector-group-${ID}`} value={String(ID)}>
{`${ID} - ${String(NAME)}`}
{authUser?.GID === ID && <FilterVintage fontSize="small" />}
{authUser?.GID === ID && (
<FilterVintage
style={{
fontSize: '1rem',
marginLeft: 16
}}
/>
)}
</MenuItem>
)),
[groups]

View File

@ -0,0 +1,124 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useFormContext } from 'react-hook-form';
import {
makeStyles,
Box,
CardActionArea,
CardContent,
Card,
Grid
} from '@material-ui/core';
import { Add } from '@material-ui/icons';
const useStyles = makeStyles(() => ({
cardPlus: {
height: '100%',
minHeight: 140,
display: 'flex',
textAlign: 'center'
}
}));
function FormStep({ step, data, setFormData }) {
const classes = useStyles();
const [dialogFormData, setDialogFormData] = useState({});
const [showDialog, setShowDialog] = useState(false);
const { reset } = useFormContext();
const { id, addAction, InfoComponent, DialogComponent, DEFAULT_DATA } = step;
const { [id]: stepData } = data;
useEffect(() => {
reset({ ...data }, { errors: true });
}, [id, data]);
const handleSubmit = values => {
setFormData(prevData => ({
...prevData,
[id]: Object.assign(prevData[id], {
[dialogFormData.index]: values
})
}));
setShowDialog(false);
};
const handleOpen = (index = stepData?.length) => {
const openData = stepData[index] ?? DEFAULT_DATA;
setDialogFormData({ index, data: openData });
setShowDialog(true);
};
const handleClone = index => {
const cloneData = { ...stepData[index], name: 'clone' };
setFormData(prevData => {
prevData[id].splice(index + 1, 0, cloneData);
return prevData;
});
};
const handleRemove = indexRemove => {
// TODO confirmation??
setFormData(prevData => ({
...prevData,
[id]: prevData[id]?.filter((_, index) => index !== indexRemove)
}));
};
const handleClose = () => setShowDialog(false);
return (
<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>
);
}
FormStep.propTypes = {
step: PropTypes.objectOf(PropTypes.object).isRequired,
data: PropTypes.objectOf(PropTypes.object).isRequired,
setFormData: PropTypes.func
};
FormStep.defaultProps = {
step: {},
data: {},
setFormData: data => data
};
export default FormStep;

View File

@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, MobileStepper } from '@material-ui/core';
import { KeyboardArrowLeft, KeyboardArrowRight } from '@material-ui/icons';
import { Tr } from 'client/components/HOC';
const CustomMobileStepper = ({
totalSteps,
activeStep,
lastStep,
disabledBack,
handleNext,
handleBack
}) => (
<MobileStepper
variant="progress"
position="static"
steps={totalSteps}
activeStep={activeStep}
style={{ flexGrow: 1 }}
backButton={
<Button size="small" onClick={handleBack} disabled={disabledBack}>
<KeyboardArrowLeft /> {Tr('Back')}
</Button>
}
nextButton={
<Button size="small" onClick={handleNext}>
{Tr(activeStep === lastStep ? 'Finish' : 'Next')}
<KeyboardArrowRight />
</Button>
}
/>
);
CustomMobileStepper.propTypes = {
totalSteps: PropTypes.number,
activeStep: PropTypes.number,
lastStep: PropTypes.number,
disabledBack: PropTypes.bool,
handleNext: PropTypes.func,
handleBack: PropTypes.func
};
CustomMobileStepper.defaultProps = {
totalSteps: 0,
activeStep: 0,
lastStep: 0,
disabledBack: false,
handleNext: () => undefined,
handleBack: () => undefined
};
export default CustomMobileStepper;

View File

@ -0,0 +1,66 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Stepper, Step, StepLabel, Box } from '@material-ui/core';
import { Tr } from 'client/components/HOC';
/*
position: sticky;
top: 0;
backdrop-filter: blur(5px);
background: #000000aa;
z-index: 1;
*/
const CustomStepper = ({
steps,
activeStep,
lastStep,
disabledBack,
handleNext,
handleBack
}) => (
<>
<Stepper activeStep={activeStep}>
{steps?.map(({ label }) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
<Box marginY={2}>
<Button onClick={handleBack} disabled={disabledBack}>
{Tr('Back')}
</Button>
<Button variant="contained" color="primary" onClick={handleNext}>
{Tr(activeStep === lastStep ? 'Finish' : 'Next')}
</Button>
</Box>
</>
);
CustomStepper.propTypes = {
steps: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOf([PropTypes.string, PropTypes.number]).isRequired,
label: PropTypes.string.isRequired
})
),
activeStep: PropTypes.number.isRequired,
lastStep: PropTypes.number.isRequired,
disabledBack: PropTypes.bool.isRequired,
handleNext: PropTypes.func,
handleBack: PropTypes.func
};
CustomStepper.defaultProps = {
steps: [],
activeStep: 0,
lastStep: 0,
disabledBack: false,
handleNext: () => undefined,
handleBack: () => undefined
};
export default CustomStepper;

View File

@ -0,0 +1,96 @@
import React, { useState, useMemo, useCallback } from 'react';
import PropTypes from 'prop-types';
import { useFormContext } from 'react-hook-form';
import { useMediaQuery } from '@material-ui/core';
import CustomMobileStepper from 'client/components/FormStepper/MobileStepper';
import CustomStepper from 'client/components/FormStepper/Stepper';
import { console } from 'window-or-global';
const FIRST_STEP = 0;
const FormStepper = ({ steps, initialValue, onSubmit }) => {
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'));
const { watch } = useFormContext();
const [activeStep, setActiveStep] = useState(FIRST_STEP);
const [formData, setFormData] = useState(initialValue);
const totalSteps = useMemo(() => steps?.length, [steps]);
const lastStep = useMemo(() => totalSteps - 1, [totalSteps]);
const disabledBack = useMemo(() => activeStep === FIRST_STEP, [activeStep]);
const handleNext = useCallback(() => {
setFormData(data => ({ ...data, ...watch() }));
if (activeStep === lastStep) {
onSubmit(formData);
} else setActiveStep(prevActiveStep => prevActiveStep + 1);
}, [activeStep]);
const handleBack = useCallback(() => {
if (activeStep <= FIRST_STEP) return;
setActiveStep(prevActiveStep => prevActiveStep - 1);
setFormData(data => ({ ...data, ...watch() }));
}, [activeStep]);
return (
<>
{/* STEPPER */}
{isMobile ? (
<CustomMobileStepper
totalSteps={totalSteps}
activeStep={activeStep}
lastStep={lastStep}
disabledBack={disabledBack}
handleNext={handleNext}
handleBack={handleBack}
/>
) : (
<CustomStepper
steps={steps}
activeStep={activeStep}
lastStep={lastStep}
disabledBack={disabledBack}
handleNext={handleNext}
handleBack={handleBack}
/>
)}
{/* FORM CONTENT */}
{React.useMemo(() => {
const { content: Content, ...rest } = steps[activeStep];
return (
<Content
data={formData}
setFormData={setFormData}
step={{ ...rest }}
/>
);
}, [activeStep, formData, setFormData])}
</>
);
};
FormStepper.propTypes = {
steps: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
label: PropTypes.string.isRequired,
content: PropTypes.node.isRequired,
dependOf: PropTypes.bool
})
),
initialValue: PropTypes.objectOf(PropTypes.object),
onSubmit: PropTypes.func
};
FormStepper.defaultProps = {
steps: [],
initialValue: {},
onSubmit: dataForm => console.log(dataForm)
};
export default FormStepper;

View File

@ -1,5 +1,5 @@
import { makeStyles } from '@material-ui/core';
import { sidebar, toolbar } from 'client/assets/theme/defaults';
import { sidebar, toolbar, footer } from 'client/assets/theme/defaults';
export default makeStyles(theme => ({
root: {
@ -23,10 +23,9 @@ export default makeStyles(theme => ({
}
},
main: {
paddingBottom: 30,
height: '100vh',
width: '100%',
// paddingTop: 64
paddingBottom: footer.regular,
paddingTop: toolbar.regular,
[`${theme.breakpoints.up('xs')} and (orientation: landscape)`]: {
paddingTop: toolbar.xs

View File

@ -0,0 +1,48 @@
import React, { useEffect } from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import FormStepper from 'client/components/FormStepper';
import Steps from 'client/containers/Application/Create/steps';
import useOpennebula from 'client/hooks/useOpennebula';
const INITIAL_VALUE = {
networks: [],
roles: []
};
function ApplicationCreate() {
const { getVNetworks, getVNetworksTemplates, getTemplates } = useOpennebula();
const methods = useForm({
mode: 'onBlur'
});
const { watch, errors } = methods;
useEffect(() => {
getVNetworks();
getVNetworksTemplates();
getTemplates();
}, []);
useEffect(() => {
// console.log('FORM CONTEXT', watch(), errors);
}, [watch, errors]);
const onSubmit = formData => console.log('submit', formData);
return (
<FormProvider {...methods}>
<FormStepper
steps={Steps}
initialValue={INITIAL_VALUE}
onSubmit={onSubmit}
/>
</FormProvider>
);
}
ApplicationCreate.propTypes = {};
ApplicationCreate.defaultProps = {};
export default ApplicationCreate;

View File

@ -0,0 +1,45 @@
import { NetworkDialog, RoleDialog } from 'client/components/Dialogs';
import { NetworkCard, RoleCard } from 'client/components/Cards';
import FormStep from 'client/components/FormStepper/FormStep';
export default [
{
id: 'networks',
label: 'Networks configuration',
content: FormStep,
preRender: () => undefined,
addAction: true,
DEFAULT_DATA: {
mandatory: true,
name: 'Public_dev',
description: 'Public network in development mode',
type: 'id',
id: '0',
extra: 'size=5'
},
InfoComponent: NetworkCard,
DialogComponent: NetworkDialog
},
{
id: 'roles',
label: 'Defining each role',
content: FormStep,
preRender: () => undefined,
addAction: true,
DEFAULT_DATA: {
name: 'Master_dev',
cardinality: 2,
vm_template: 0,
elasticity_policies: [],
scheduled_policies: []
},
InfoComponent: RoleCard,
DialogComponent: RoleDialog
},
{
id: 'where',
label: 'Where will it run?',
content: FormStep,
preRender: () => undefined
}
];

View File

@ -0,0 +1,3 @@
import { makeStyles } from '@material-ui/core';
export default makeStyles(theme => ({}));

View File

@ -38,7 +38,7 @@ const useStyles = makeStyles({
}
});
function Users() {
function ApplicationDeploy() {
const classes = useStyles();
const { isLoading } = useGeneral();
const { users, groups, getUsers } = useOpennebula();
@ -49,22 +49,22 @@ function Users() {
}
}, [getUsers]);
const getGroupById = id => groups?.find(({ ID }) => ID === id);
const getGroupById = findId => groups?.find(({ ID }) => ID === findId);
return (
<>
{isLoading && <LinearProgress style={{ width: '100%' }} />}
{users?.map(({ ID, NAME, GROUPS }, index) => (
{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);
{[GROUPS?.ID ?? []].flat().map(ID => {
const group = getGroupById(ID);
return group ? (
<Chip
style={{ margin: '0 0.5em' }}
key={`group-${index}-${id}`}
key={`group-${index}-${ID}`}
size="small"
color="primary"
clickable
@ -80,8 +80,8 @@ function Users() {
);
}
Users.propTypes = {};
ApplicationDeploy.propTypes = {};
Users.defaultProps = {};
ApplicationDeploy.defaultProps = {};
export default Users;
export default ApplicationDeploy;

View File

@ -16,21 +16,12 @@
import React from 'react';
import PropTypes from 'prop-types';
function Groups() {
return (
<div>
Groups
</div>
);
function ApplicationManage() {
return <div>Manage</div>;
}
Groups.propTypes = {
name: PropTypes.string
};
ApplicationManage.propTypes = {};
Groups.defaultProps = {
name: ''
};
ApplicationManage.defaultProps = {};
export default Groups;
export default ApplicationManage;

View File

@ -1,5 +1,5 @@
import Create from 'client/containers/Application/Create';
import Deploy from 'client/containers/Application/Deploy';
import Manage from 'client/containers/Application/Manage';
import ApplicationCreate from 'client/containers/Application/Create';
import ApplicationDeploy from 'client/containers/Application/Deploy';
import ApplicationManage from 'client/containers/Application/Manage';
export { Create, Deploy, Manage };
export { ApplicationCreate, ApplicationDeploy, ApplicationManage };

View File

@ -15,14 +15,9 @@
import React from 'react';
import { makeStyles, Box, Typography } from '@material-ui/core';
import { Box, Typography } from '@material-ui/core';
const dashboardStyles = makeStyles(theme => ({
root: {},
title: {
color: theme.palette.common.black
}
}));
import dashboardStyles from 'client/containers/Dashboard/styles';
function Dashboard() {
const classes = dashboardStyles();

View File

@ -0,0 +1,8 @@
import { makeStyles } from '@material-ui/core';
export default makeStyles(theme => ({
root: {},
title: {
color: theme.palette.common.black
}
}));

View File

@ -25,9 +25,9 @@ import {
import useAuth from 'client/hooks/useAuth';
import FormUser from 'client/containers/Login/FormUser';
import Form2fa from 'client/containers/Login/Form2fa';
import FormGroup from 'client/containers/Login/FormGroup';
import FormUser from 'client/containers/Login/Forms/FormUser';
import Form2fa from 'client/containers/Login/Forms/Form2fa';
import FormGroup from 'client/containers/Login/Forms/FormGroup';
import loginStyles from 'client/containers/Login/styles';
import Logo from 'client/icons/logo';

View File

@ -1,4 +0,0 @@
import Groups from './Groups';
import Users from './Users';
export { Groups, Users };

View File

@ -2,6 +2,7 @@ import { makeStyles } from '@material-ui/core';
export default makeStyles(() => ({
root: {
minHeight: '100%'
minHeight: '100%',
width: '100%'
}
}));

View File

@ -7,7 +7,7 @@ import { fakeDelay } from 'client/utils/helpers';
import * as serviceAuth from 'client/services/auth';
import * as serviceUsers from 'client/services/users';
import * as serviceGroups from 'client/services/groups';
import * as servicePool from 'client/services/pool';
import {
startAuth,
selectFilterGroup,
@ -80,7 +80,7 @@ export default function useAuth() {
return serviceAuth
.getUser()
.then(user => dispatch(successAuth({ user })))
.then(serviceGroups.getGroups)
.then(servicePool.getGroups)
.then(groups => dispatch(setGroups(groups)))
.catch(err => dispatch(failureAuth({ error: err })));
}, [dispatch, jwtName, authUser]);

View File

@ -1,43 +1,80 @@
import { useCallback } from 'react';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import {
setGroups,
setUsers,
import actions, {
startOneRequest,
failureOneRequest
} from 'client/actions/pool';
import * as servicesGroups from 'client/services/groups';
import * as servicesUsers from 'client/services/users';
import * as servicePool from 'client/services/pool';
export default function useOpennebula() {
const dispatch = useDispatch();
const { groups, users } = useSelector(
state => state?.Opennebula,
const {
groups,
users,
vNetworks,
vNetworksTemplates,
templates,
filterPool: filter
} = useSelector(
state => ({
...state?.Opennebula,
filterPool: state?.Authenticated?.filterPool
}),
shallowEqual
);
const getGroups = useCallback(() => {
dispatch(startOneRequest());
return servicesGroups
.getGroups()
.then(data => dispatch(setGroups(data)))
return servicePool
.getGroups({ filter })
.then(data => dispatch(actions.setGroups(data)))
.catch(err => dispatch(failureOneRequest({ error: err })));
}, [dispatch]);
}, [dispatch, filter]);
const getUsers = useCallback(() => {
dispatch(startOneRequest());
return servicesUsers
.getUsers()
.then(data => dispatch(setUsers(data)))
return servicePool
.getUsers({ filter })
.then(data => dispatch(actions.setUsers(data)))
.catch(err => dispatch(failureOneRequest({ error: err })));
}, [dispatch]);
}, [dispatch, filter]);
const getVNetworks = useCallback(() => {
dispatch(startOneRequest());
return servicePool
.getVNetworks({ filter })
.then(data => dispatch(actions.setVNetworks(data)))
.catch(err => dispatch(failureOneRequest({ error: err })));
}, [dispatch, filter]);
const getVNetworksTemplates = useCallback(() => {
dispatch(startOneRequest());
return servicePool
.getVNetworksTemplates({ filter })
.then(data => dispatch(actions.setVNetworkTemplates(data)))
.catch(err => dispatch(failureOneRequest({ error: err })));
}, [dispatch, filter]);
const getTemplates = useCallback(() => {
dispatch(startOneRequest());
return servicePool
.getTemplates({ filter })
.then(data => dispatch(actions.setTemplates(data)))
.catch(err => dispatch(failureOneRequest({ error: err })));
}, [dispatch, filter]);
return {
groups,
getGroups,
users,
getUsers
getUsers,
vNetworks,
getVNetworks,
vNetworksTemplates,
getVNetworksTemplates,
templates,
getTemplates
};
}

View File

@ -27,8 +27,8 @@ const initial = {
files: [],
marketPlaces: [],
apps: [],
virtualNetworks: [],
networkTemplates: [],
vNetworks: [],
vNetworksTemplates: [],
securityGroups: [],
clusters: [],
hosts: [],

View File

@ -16,54 +16,50 @@
import {
Dashboard as DashboardIcon,
Settings as SettingsIcon,
Ballot as BallotIcon
Ballot as BallotIcon,
Palette as PaletteIcon,
Reddit as RedditIcon,
Build as BuildIcon
} from '@material-ui/icons';
import Login from 'client/containers/Login';
import { Clusters, Hosts, Zones } from 'client/containers/Infrastructure';
import { Users, Groups } from 'client/containers/System';
import Dashboard from 'client/containers/Dashboard';
import Settings from 'client/containers/Settings';
import TestApi from 'client/containers/TestApi';
import Dashboard from 'client/containers/Dashboard';
import {
ApplicationCreate,
ApplicationDeploy,
ApplicationManage
} from 'client/containers/Application';
export const PATH = {
LOGIN: '/',
DASHBOARD: '/dashboard',
APPLICATION: {
CREATE: '/application/create',
MANAGE: '/application/manage',
DEPLOY: '/application/deploy'
},
SETTINGS: '/settings',
TEST_API: '/test-api',
INFRASTRUCTURE: {
CLUSTERS: '/clusters',
HOSTS: '/hosts',
ZONES: '/zones'
},
SYSTEM: {
USERS: '/users',
GROUPS: '/groups'
},
NETWORKS: {
VNETS: '/vnets',
VNETS_TEMPLATES: '/vnets-templates',
VNETS_TOPOLOGY: '/vnets-topology',
SEC_GROUPS: '/secgroups'
}
TEST_API: '/test-api'
};
const ENDPOINTS = [
{
label: 'login',
label: 'Login',
path: PATH.LOGIN,
authenticated: false,
component: Login
},
{
label: 'dashboard',
label: 'Dashboard',
path: PATH.DASHBOARD,
authenticated: true,
icon: DashboardIcon,
component: Dashboard
},
{
label: 'settings',
label: 'Settings',
path: PATH.SETTINGS,
authenticated: true,
header: true,
@ -71,7 +67,7 @@ const ENDPOINTS = [
component: Settings
},
{
label: 'test api',
label: 'Test API',
path: PATH.TEST_API,
authenticated: true,
devMode: true,
@ -79,75 +75,25 @@ const ENDPOINTS = [
component: TestApi
},
{
label: 'infrastructure',
label: 'Create Application',
path: PATH.APPLICATION.CREATE,
authenticated: true,
icon: BallotIcon,
routes: [
{
label: 'clusters',
path: PATH.INFRASTRUCTURE.CLUSTERS,
authenticated: true,
component: Clusters
},
{
label: 'hosts',
path: PATH.INFRASTRUCTURE.HOSTS,
authenticated: true,
component: Hosts
},
{
label: 'zones',
path: PATH.INFRASTRUCTURE.ZONES,
authenticated: true,
component: Zones
}
]
icon: PaletteIcon,
component: ApplicationCreate
},
{
label: 'system',
label: 'Deploy Application',
path: PATH.APPLICATION.DEPLOY,
authenticated: true,
icon: BallotIcon,
routes: [
{
label: 'users',
path: PATH.SYSTEM.USERS,
authenticated: true,
component: Users
},
{
label: 'groups',
path: PATH.SYSTEM.GROUPS,
authenticated: true,
component: Groups
}
]
icon: RedditIcon,
component: ApplicationDeploy
},
{
label: 'networks',
label: 'Manage Application',
path: PATH.APPLICATION.MANAGE,
authenticated: true,
icon: BallotIcon,
routes: [
{
label: 'vnets',
path: PATH.NETWORKS.VNETS,
authenticated: true
},
{
label: 'vnets templates',
path: PATH.NETWORKS.VNETS_TEMPLATES,
authenticated: true
},
{
label: 'vnets topology',
path: PATH.NETWORKS.VNETS_TOPOLOGY,
authenticated: true
},
{
label: 'vnets secgroup',
path: PATH.NETWORKS.SEC_GROUPS,
authenticated: true
}
]
icon: BuildIcon,
component: ApplicationManage
}
];

View File

@ -2,17 +2,4 @@ import { Actions, Commands } from 'server/utils/constants/commands/group';
import { requestData, requestParams } from 'client/utils';
import httpCodes from 'server/utils/constants/http-codes';
export const getGroups = () => {
const name = Actions.GROUP_POOL_INFO;
const { url, options } = requestParams({}, { name, ...Commands[name] });
return requestData(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res;
return [res?.data?.GROUP_POOL?.GROUP ?? []].flat();
});
};
export default {
getGroups
};
export default {};

View File

@ -0,0 +1,86 @@
import User from 'server/utils/constants/commands/user';
import Group from 'server/utils/constants/commands/group';
import VNet from 'server/utils/constants/commands/vn';
import VNetTemplate from 'server/utils/constants/commands/vntemplate';
import Template from 'server/utils/constants/commands/template';
import httpCodes from 'server/utils/constants/http-codes';
import { requestData, requestParams } from 'client/utils';
export const getUsers = ({ filter }) => {
const name = User.Actions.USER_POOL_INFO;
const { url, options } = requestParams(
{ filter },
{ name, ...User.Commands[name] }
);
return requestData(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res;
return [res?.data?.USER_POOL?.USER ?? []].flat();
});
};
export const getGroups = ({ filter }) => {
const name = Group.Actions.GROUP_POOL_INFO;
const { url, options } = requestParams(
{ filter },
{ name, ...Group.Commands[name] }
);
return requestData(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res;
return [res?.data?.GROUP_POOL?.GROUP ?? []].flat();
});
};
export const getVNetworks = ({ filter }) => {
const name = VNet.Actions.VN_POOL_INFO;
const { url, options } = requestParams(
{ filter },
{ name, ...VNet.Commands[name] }
);
return requestData(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res;
return [res?.data?.VNET_POOL?.VNET ?? []].flat();
});
};
export const getVNetworksTemplates = ({ filter }) => {
const name = VNetTemplate.Actions.VNTEMPLATE_POOL_INFO;
const { url, options } = requestParams(
{ filter },
{ name, ...VNetTemplate.Commands[name] }
);
return requestData(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res;
return [res?.data?.VNTEMPLATE_POOL?.VNTEMPLATE ?? []].flat();
});
};
export const getTemplates = ({ filter }) => {
const name = Template.Actions.TEMPLATE_POOL_INFO;
const { url, options } = requestParams(
{ filter },
{ name, ...Template.Commands[name] }
);
return requestData(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res;
return [res?.data?.VMTEMPLATE_POOL?.VMTEMPLATE ?? []].flat();
});
};
export default {
getUsers,
getGroups,
getVNetworks,
getVNetworksTemplates,
getTemplates
};

View File

@ -13,18 +13,6 @@ export const changeGroup = values => {
});
};
export const getUsers = () => {
const name = Actions.USER_POOL_INFO;
const { url, options } = requestParams({}, { name, ...Commands[name] });
return requestData(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res;
return [res?.data?.USER_POOL?.USER ?? []].flat();
});
};
export default {
changeGroup,
getUsers
changeGroup
};

View File

@ -4,7 +4,9 @@ import { from as resourceFrom } from 'server/utils/constants/defaults';
export const getQueries = params =>
Object.entries(params)
?.filter(([, { from }]) => from === resourceFrom.query)
?.filter(([, { from, value }]) =>
Boolean(from === resourceFrom.query && value)
)
?.map(([name, { value }]) => `${name}=${encodeURI(value)}`)
?.join('&');