mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-16 22:50:10 +03:00
parent
e85f740f9c
commit
1d83bd2de6
@ -24,8 +24,19 @@ resource_name: "VM-TEMPLATE"
|
||||
# Actions - Which buttons are visible to operate over the resources
|
||||
|
||||
actions:
|
||||
refresh: true
|
||||
create_dialog: true
|
||||
import_dialog: true
|
||||
update_dialog: true
|
||||
instantiate_dialog: true
|
||||
clone: true
|
||||
delete: true
|
||||
chown: true
|
||||
chgrp: true
|
||||
lock: true
|
||||
unlock: true
|
||||
share: true
|
||||
unshare: true
|
||||
|
||||
# Filters - List of criteria to filter the resources
|
||||
|
||||
|
@ -25,10 +25,10 @@ import loadable from '@loadable/component'
|
||||
const Dashboard = loadable(() => import('client/containers/Dashboard/Provision'), { ssr: false })
|
||||
|
||||
const Providers = loadable(() => import('client/containers/Providers'), { ssr: false })
|
||||
const ProvidersFormCreate = loadable(() => import('client/containers/Providers/Form/Create'), { ssr: false })
|
||||
const CreateProvider = loadable(() => import('client/containers/Providers/Create'), { ssr: false })
|
||||
|
||||
const Provisions = loadable(() => import('client/containers/Provisions'), { ssr: false })
|
||||
const ProvisionsFormCreate = loadable(() => import('client/containers/Provisions/Form/Create'), { ssr: false })
|
||||
const CreateProvision = loadable(() => import('client/containers/Provisions/Create'), { ssr: false })
|
||||
|
||||
const Settings = loadable(() => import('client/containers/Settings'), { ssr: false })
|
||||
|
||||
@ -65,12 +65,12 @@ export const ENDPOINTS = [
|
||||
{
|
||||
label: 'Create Provider',
|
||||
path: PATH.PROVIDERS.CREATE,
|
||||
Component: ProvidersFormCreate
|
||||
Component: CreateProvider
|
||||
},
|
||||
{
|
||||
label: 'Edit Provider template',
|
||||
path: PATH.PROVIDERS.EDIT,
|
||||
Component: ProvidersFormCreate
|
||||
Component: CreateProvider
|
||||
},
|
||||
{
|
||||
label: 'Provisions',
|
||||
@ -82,12 +82,12 @@ export const ENDPOINTS = [
|
||||
{
|
||||
label: 'Create Provision',
|
||||
path: PATH.PROVISIONS.CREATE,
|
||||
Component: ProvisionsFormCreate
|
||||
Component: CreateProvision
|
||||
},
|
||||
{
|
||||
label: 'Edit Provision template',
|
||||
path: PATH.PROVISIONS.EDIT,
|
||||
Component: ProvisionsFormCreate
|
||||
Component: CreateProvision
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo } from 'react'
|
||||
import { memo, JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import clsx from 'clsx'
|
||||
|
||||
@ -33,6 +33,21 @@ import { Action } from 'client/components/Cards/SelectCard'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @typedef {object} DialogProps
|
||||
* @property {boolean} [open] - If `true`, the component is shown
|
||||
* @property {string|JSXElementConstructor} title - Title
|
||||
* @property {string|JSXElementConstructor} [subheader] - Subtitle
|
||||
* @property {object} [contentProps] - Content properties
|
||||
* @property {function():Promise} handleAccept - Accept action
|
||||
* @property {Function} handleCancel - Cancel action
|
||||
* @property {object} [acceptButtonProps] - Accept button properties
|
||||
* @property {object} [cancelButtonProps] - Cancel button properties
|
||||
* @property {boolean} [fixedWidth] - Fix minimum with to dialog
|
||||
* @property {boolean} [fixedHeight] - Fix minimum height to dialog
|
||||
* @property {JSXElementConstructor} [children] - Fix minimum height
|
||||
*/
|
||||
|
||||
const useStyles = makeStyles({
|
||||
title: {
|
||||
display: 'flex',
|
||||
@ -51,6 +66,10 @@ const useStyles = makeStyles({
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {DialogProps} props - Dialog properties
|
||||
* @returns {JSXElementConstructor} - Dialog with confirmation basic buttons
|
||||
*/
|
||||
const DialogConfirmation = memo(
|
||||
({
|
||||
open = true,
|
||||
|
@ -15,11 +15,11 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import DialogForm from 'client/components/Dialogs/DialogForm'
|
||||
import DialogRequest from 'client/components/Dialogs/DialogRequest'
|
||||
import DialogConfirmation, { DialogPropTypes } from 'client/components/Dialogs/DialogConfirmation'
|
||||
import DialogConfirmation from 'client/components/Dialogs/DialogConfirmation'
|
||||
export * from 'client/components/Dialogs/DialogConfirmation'
|
||||
|
||||
export {
|
||||
DialogForm,
|
||||
DialogRequest,
|
||||
DialogConfirmation,
|
||||
DialogPropTypes
|
||||
DialogForm,
|
||||
DialogRequest
|
||||
}
|
||||
|
@ -67,7 +67,11 @@ const TooltipComponent = ({ tooltip, tooltipProps, children }) => (
|
||||
placement='bottom'
|
||||
title={<Typography variant='subtitle2'>{tooltip}</Typography>}
|
||||
{...tooltipProps}
|
||||
>{wrapperChildren}</Tooltip>
|
||||
>
|
||||
<span>
|
||||
{wrapperChildren}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@ -77,6 +81,7 @@ const TooltipComponent = ({ tooltip, tooltipProps, children }) => (
|
||||
const SubmitButton = memo(
|
||||
({ isSubmitting, disabled, label, icon, className, ...props }) => {
|
||||
const classes = useStyles()
|
||||
const progressSize = icon?.props?.size ?? 24
|
||||
|
||||
return (
|
||||
<TooltipComponent {...props}>
|
||||
@ -91,7 +96,9 @@ const SubmitButton = memo(
|
||||
aria-label={label ?? T.Submit}
|
||||
{...props}
|
||||
>
|
||||
{isSubmitting && <CircularProgress color='secondary' size={24} />}
|
||||
{isSubmitting && (
|
||||
<CircularProgress color='secondary' size={progressSize} />
|
||||
)}
|
||||
{!isSubmitting && (icon ?? label ?? Tr(T.Submit))}
|
||||
</ButtonComponent>
|
||||
</TooltipComponent>
|
||||
|
@ -0,0 +1,126 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Flatpickr from 'react-flatpickr'
|
||||
import { TextField } from '@material-ui/core'
|
||||
import { Controller } from 'react-hook-form'
|
||||
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { ErrorHelper } from 'client/components/FormControl'
|
||||
|
||||
const WrapperToLoadLib = ({ children, id, lib }) => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const loadLib = async lib => {
|
||||
try {
|
||||
await import(lib)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadLib()
|
||||
|
||||
return () => {
|
||||
// remove all styles when component will be unmounted
|
||||
document
|
||||
.querySelectorAll(`[id^=${id}]`)
|
||||
.forEach(child => child.parentNode.removeChild(child))
|
||||
}
|
||||
}, [])
|
||||
|
||||
return loading ? null : children
|
||||
}
|
||||
|
||||
const TimeController = memo(
|
||||
({ control, cy, name, label, error, fieldProps }) => (
|
||||
<WrapperToLoadLib id='flatpicker' lib={'flatpickr/dist/themes/material_blue.css'}>
|
||||
<Controller
|
||||
render={({ value, onChange, onBlur }) => {
|
||||
const translated = typeof label === 'string' ? Tr(label) : label
|
||||
|
||||
return (
|
||||
<Flatpickr
|
||||
onblur={onBlur}
|
||||
onChange={onChange}
|
||||
// onCreate={function (flatpickr) { this.calendar = flatpickr }}
|
||||
onDestroy={() => { onChange(undefined) }}
|
||||
data-enable-time
|
||||
options={{ allowInput: true }}
|
||||
render={({ defaultValue, ...props }, ref) => (
|
||||
<TextField
|
||||
{...props}
|
||||
fullWidth
|
||||
color='secondary'
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
variant='outlined'
|
||||
margin='dense'
|
||||
label={translated}
|
||||
inputProps={{ 'data-cy': cy }}
|
||||
inputRef={ref}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
{...fieldProps}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
name={name}
|
||||
control={control}
|
||||
/>
|
||||
</WrapperToLoadLib>
|
||||
),
|
||||
(prevProps, nextProps) => prevProps.error === nextProps.error
|
||||
)
|
||||
|
||||
TimeController.propTypes = {
|
||||
control: PropTypes.object,
|
||||
cy: PropTypes.string,
|
||||
multiline: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.objectOf(PropTypes.any)
|
||||
]),
|
||||
fieldProps: PropTypes.object,
|
||||
formContext: PropTypes.shape({
|
||||
setValue: PropTypes.func,
|
||||
setError: PropTypes.func,
|
||||
clearErrors: PropTypes.func,
|
||||
watch: PropTypes.func,
|
||||
register: PropTypes.func
|
||||
})
|
||||
}
|
||||
|
||||
TimeController.defaultProps = {
|
||||
control: {},
|
||||
cy: 'cy',
|
||||
name: '',
|
||||
label: '',
|
||||
error: false,
|
||||
fieldProps: undefined
|
||||
}
|
||||
|
||||
TimeController.displayName = 'TimeController'
|
||||
|
||||
export default TimeController
|
@ -61,7 +61,9 @@ const CustomMobileStepper = ({
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Box minHeight={60}>
|
||||
<Typography className={classes.title}>{label}</Typography>
|
||||
<Typography className={classes.title}>
|
||||
{typeof label === 'string' ? Tr(label) : label}
|
||||
</Typography>
|
||||
{Boolean(errors[id]) && (
|
||||
<Typography className={classes.error} variant="caption" color="error">
|
||||
{errors[id]?.message}
|
||||
|
55
src/fireedge/src/client/components/FormStepper/Skeleton.js
Normal file
55
src/fireedge/src/client/components/FormStepper/Skeleton.js
Normal file
@ -0,0 +1,55 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, JSXElementConstructor } from 'react'
|
||||
|
||||
import { Skeleton } from '@material-ui/lab'
|
||||
import { useMediaQuery, styled } from '@material-ui/core'
|
||||
|
||||
const ControlWrapper = styled('div')(({ theme }) => ({
|
||||
marginBlock: '1em',
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
gap: '1em',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}
|
||||
}))
|
||||
|
||||
/**
|
||||
* Returns skeleton loader to stepper form.
|
||||
*
|
||||
* @returns {JSXElementConstructor} Skeleton loader component
|
||||
*/
|
||||
const SkeletonStepsForm = memo(() => {
|
||||
const isMobile = useMediaQuery(theme => theme.breakpoints.down('sm'))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Skeleton variant='rect' height={120} width='100%' />
|
||||
<ControlWrapper>
|
||||
<Skeleton variant='rect' height={35} width={95} />
|
||||
{isMobile && <Skeleton variant='rect' height={8} width='100%' />}
|
||||
<Skeleton variant='rect' height={35} width={95} />
|
||||
</ControlWrapper>
|
||||
<Skeleton variant='rect' height={200} width='100%' />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
SkeletonStepsForm.displayName = 'SkeletonStepsForm'
|
||||
|
||||
export default SkeletonStepsForm
|
@ -95,7 +95,9 @@ const CustomStepper = ({
|
||||
}
|
||||
}}
|
||||
{...(Boolean(errors[id]?.message) && { error: true })}
|
||||
>{Tr(label)}</StepLabel>
|
||||
>
|
||||
{typeof label === 'string' ? Tr(label) : label}
|
||||
</StepLabel>
|
||||
</StepButton>
|
||||
</Step>
|
||||
))}
|
||||
|
@ -16,13 +16,15 @@
|
||||
import { useState, useMemo, useCallback, useEffect, JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { BaseSchema } from 'yup'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
import { useMediaQuery } from '@material-ui/core'
|
||||
|
||||
import { useGeneral } from 'client/features/General'
|
||||
import CustomMobileStepper from 'client/components/FormStepper/MobileStepper'
|
||||
import CustomStepper from 'client/components/FormStepper/Stepper'
|
||||
import { groupBy, Step, ResolverCallback } from 'client/utils'
|
||||
import SkeletonStepsForm from 'client/components/FormStepper/Skeleton'
|
||||
import { groupBy, Step } from 'client/utils'
|
||||
|
||||
const FIRST_STEP = 0
|
||||
|
||||
@ -32,11 +34,11 @@ const FIRST_STEP = 0
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {Step[]} props.steps - Steps
|
||||
* @param {ResolverCallback} props.schema - Function to get form schema
|
||||
* @param {function():BaseSchema} props.schema - Function to get form schema
|
||||
* @param {Function} props.onSubmit - Submit function
|
||||
* @returns {JSXElementConstructor} Stepper form component
|
||||
*/
|
||||
const FormStepper = ({ steps, schema, onSubmit }) => {
|
||||
const FormStepper = ({ steps = [], schema, onSubmit }) => {
|
||||
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
|
||||
const { watch, reset, errors, setError } = useFormContext()
|
||||
const { isLoading } = useGeneral()
|
||||
@ -53,13 +55,13 @@ const FormStepper = ({ steps, schema, onSubmit }) => {
|
||||
}, [formData])
|
||||
|
||||
const validateSchema = async stepIdx => {
|
||||
const { id, resolver, optionsValidate, ...step } = steps[stepIdx]
|
||||
const { id, resolver, optionsValidate: options, ...step } = steps[stepIdx]
|
||||
const stepData = watch(id)
|
||||
|
||||
const allData = { ...formData, [id]: stepData }
|
||||
const stepSchema = typeof resolver === 'function' ? resolver(allData) : resolver
|
||||
|
||||
await stepSchema.validate(stepData, optionsValidate)
|
||||
await stepSchema.validate(stepData, options)
|
||||
|
||||
return { id, data: stepData, ...step }
|
||||
}
|
||||
@ -105,9 +107,9 @@ const FormStepper = ({ steps, schema, onSubmit }) => {
|
||||
const { id, data } = await validateSchema(activeStep)
|
||||
|
||||
if (activeStep === lastStep) {
|
||||
const submitData = schema().cast({ ...formData, [id]: data })
|
||||
|
||||
onSubmit(submitData)
|
||||
const submitData = { ...formData, [id]: data }
|
||||
const schemaData = schema().cast(submitData, { context: submitData })
|
||||
onSubmit(schemaData)
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, [id]: data }))
|
||||
setActiveStep(prevActiveStep => prevActiveStep + 1)
|
||||
@ -184,15 +186,13 @@ FormStepper.propTypes = {
|
||||
context: PropTypes.object
|
||||
})
|
||||
})
|
||||
),
|
||||
schema: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
|
||||
).isRequired,
|
||||
schema: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func
|
||||
}
|
||||
|
||||
FormStepper.defaultProps = {
|
||||
steps: [],
|
||||
schema: {},
|
||||
onSubmit: console.log
|
||||
export {
|
||||
SkeletonStepsForm
|
||||
}
|
||||
|
||||
export default FormStepper
|
||||
|
@ -23,7 +23,8 @@ import {
|
||||
Paper,
|
||||
Popper,
|
||||
MenuItem,
|
||||
MenuList
|
||||
MenuList,
|
||||
ListItemIcon
|
||||
} from '@material-ui/core'
|
||||
import { NavArrowDown } from 'iconoir-react'
|
||||
|
||||
@ -36,7 +37,6 @@ import { Translate } from 'client/components/HOC'
|
||||
|
||||
const ButtonToTriggerForm = ({
|
||||
buttonProps = {},
|
||||
isConfirmDialog = false,
|
||||
dialogProps = {},
|
||||
options = []
|
||||
}) => {
|
||||
@ -47,7 +47,7 @@ const ButtonToTriggerForm = ({
|
||||
const open = Boolean(anchorEl)
|
||||
|
||||
const { display, show, hide, values: Form } = useDialog()
|
||||
const { onSubmit: handleSubmit, form } = Form ?? {}
|
||||
const { onSubmit: handleSubmit, form, isConfirmDialog = true } = Form ?? {}
|
||||
|
||||
const formConfig = useMemo(() => form?.() ?? {}, [form])
|
||||
const { steps, defaultValues, resolver, fields, transformBeforeSubmit } = formConfig
|
||||
@ -89,18 +89,24 @@ const ButtonToTriggerForm = ({
|
||||
id={buttonId}
|
||||
open={open}
|
||||
transition
|
||||
style={{ zIndex: 2 }}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Grow {...TransitionProps}>
|
||||
<Grow {...TransitionProps} >
|
||||
<Paper variant='outlined'>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList disablePadding>
|
||||
{options.map(({ cy, name, ...option }) => (
|
||||
<MenuList variant='menu' disablePadding dense>
|
||||
{options.map(({ cy, icon: Icon, name, ...option }) => (
|
||||
<MenuItem
|
||||
key={name}
|
||||
data-cy={cy}
|
||||
onClick={() => openDialogForm(option)}
|
||||
>
|
||||
{Icon && (
|
||||
<ListItemIcon>
|
||||
<Icon size={18} />
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<Translate word={name} />
|
||||
</MenuItem>
|
||||
))}
|
||||
@ -142,20 +148,23 @@ const ButtonToTriggerForm = ({
|
||||
)
|
||||
}
|
||||
|
||||
ButtonToTriggerForm.propTypes = {
|
||||
export const ButtonToTriggerFormPropTypes = {
|
||||
buttonProps: PropTypes.shape(SubmitButtonPropTypes),
|
||||
dialogProps: PropTypes.shape(DialogPropTypes),
|
||||
isConfirmDialog: PropTypes.bool,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
cy: PropTypes.string,
|
||||
isConfirmDialog: PropTypes.bool,
|
||||
name: PropTypes.string,
|
||||
icon: PropTypes.any,
|
||||
form: PropTypes.func,
|
||||
handleSubmit: PropTypes.func
|
||||
onSubmit: PropTypes.func
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
ButtonToTriggerForm.propTypes = ButtonToTriggerFormPropTypes
|
||||
|
||||
ButtonToTriggerForm.displayName = 'ButtonToTriggerForm'
|
||||
|
||||
export default ButtonToTriggerForm
|
||||
|
@ -52,6 +52,8 @@ const FormWithSchema = ({ id, cy, fields, className, legend }) => {
|
||||
|
||||
const getFields = useMemo(() => typeof fields === 'function' ? fields() : fields, [])
|
||||
|
||||
if (getFields.length === 0) return null
|
||||
|
||||
return (
|
||||
<Fieldset className={className}>
|
||||
{legend && <Legend>{legend}</Legend>}
|
||||
|
@ -21,7 +21,7 @@ import { T } from 'client/constants'
|
||||
|
||||
import {
|
||||
FORM_FIELDS, STEP_FORM_SCHEMA
|
||||
} from 'client/containers/Providers/Form/ProviderForm/Steps/BasicConfiguration/schema'
|
||||
} from 'client/components/Forms/Provider/CreateForm/Steps/BasicConfiguration/schema'
|
||||
|
||||
export const STEP_ID = 'configuration'
|
||||
|
@ -27,11 +27,11 @@ import { T } from 'client/constants'
|
||||
|
||||
import {
|
||||
FORM_FIELDS, STEP_FORM_SCHEMA
|
||||
} from 'client/containers/Providers/Form/ProviderForm/Steps/Connection/schema'
|
||||
} from 'client/components/Forms/Provider/CreateForm/Steps/Connection/schema'
|
||||
|
||||
import {
|
||||
STEP_ID as TEMPLATE_ID
|
||||
} from 'client/containers/Providers/Form/ProviderForm/Steps/Template'
|
||||
} from 'client/components/Forms/Provider/CreateForm/Steps/Template'
|
||||
|
||||
export const STEP_ID = 'connection'
|
||||
|
||||
@ -76,5 +76,5 @@ const Connection = ({ isUpdate }) => ({
|
||||
}, [])
|
||||
})
|
||||
|
||||
export * from 'client/containers/Providers/Form/ProviderForm/Steps/Connection/schema'
|
||||
export * from 'client/components/Forms/Provider/CreateForm/Steps/Connection/schema'
|
||||
export default Connection
|
@ -28,10 +28,10 @@ import { sanitize } from 'client/utils'
|
||||
import { isValidProviderTemplate, getProvisionTypeFromTemplate } from 'client/models/ProviderTemplate'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
import { STEP_FORM_SCHEMA } from 'client/containers/Providers/Form/ProviderForm/Steps/Template/schema'
|
||||
import { STEP_FORM_SCHEMA } from 'client/components/Forms/Provider/CreateForm/Steps/Template/schema'
|
||||
|
||||
import { STEP_ID as CONFIGURATION_ID } from 'client/containers/Providers/Form/ProviderForm/Steps/BasicConfiguration'
|
||||
import { STEP_ID as CONNECTION_ID } from 'client/containers/Providers/Form/ProviderForm/Steps/Connection'
|
||||
import { STEP_ID as CONFIGURATION_ID } from 'client/components/Forms/Provider/CreateForm/Steps/BasicConfiguration'
|
||||
import { STEP_ID as CONNECTION_ID } from 'client/components/Forms/Provider/CreateForm/Steps/Connection'
|
||||
|
||||
export const STEP_ID = 'template'
|
||||
|
@ -14,32 +14,26 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import * as yup from 'yup'
|
||||
import Template from 'client/components/Forms/Provider/CreateForm/Steps/Template'
|
||||
import BasicConfiguration from 'client/components/Forms/Provider/CreateForm/Steps/BasicConfiguration'
|
||||
import Connection from 'client/components/Forms/Provider/CreateForm/Steps/Connection'
|
||||
import { createSteps, deepmerge } from 'client/utils'
|
||||
|
||||
import Template from './Template'
|
||||
import Provider from './Provider'
|
||||
import BasicConfiguration from './BasicConfiguration'
|
||||
import Inputs from './Inputs'
|
||||
const Steps = createSteps(stepProps => {
|
||||
const { isUpdate } = stepProps
|
||||
|
||||
const Steps = () => {
|
||||
const template = Template()
|
||||
const provider = Provider()
|
||||
const configuration = BasicConfiguration()
|
||||
const inputs = Inputs()
|
||||
return [
|
||||
!isUpdate && Template,
|
||||
BasicConfiguration,
|
||||
Connection
|
||||
].filter(Boolean)
|
||||
}, {
|
||||
transformBeforeSubmit: formData => {
|
||||
const { template, configuration, connection } = formData
|
||||
const templateSelected = template?.[0]
|
||||
|
||||
const steps = [template, provider, configuration, inputs]
|
||||
|
||||
const resolvers = () => yup
|
||||
.object({
|
||||
[template.id]: template.resolver(),
|
||||
[provider.id]: provider.resolver(),
|
||||
[configuration.id]: configuration.resolver(),
|
||||
[inputs.id]: inputs.resolver()
|
||||
})
|
||||
|
||||
const defaultValues = resolvers().default()
|
||||
|
||||
return { steps, defaultValues, resolvers }
|
||||
}
|
||||
return deepmerge(templateSelected, { ...configuration, connection })
|
||||
}
|
||||
})
|
||||
|
||||
export default Steps
|
@ -0,0 +1,58 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import FormStepper from 'client/components/FormStepper'
|
||||
import Steps from 'client/components/Forms/Provider/CreateForm/Steps'
|
||||
|
||||
const CreateForm = ({ stepProps, initialValues, onSubmit }) => {
|
||||
const {
|
||||
steps,
|
||||
defaultValues,
|
||||
resolver,
|
||||
transformBeforeSubmit
|
||||
} = Steps(stepProps, initialValues)
|
||||
|
||||
const methods = useForm({
|
||||
mode: 'onSubmit',
|
||||
defaultValues,
|
||||
resolver: yupResolver(resolver())
|
||||
})
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<FormStepper
|
||||
steps={steps}
|
||||
schema={resolver}
|
||||
onSubmit={data => onSubmit(transformBeforeSubmit?.(data) ?? data)}
|
||||
/>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
CreateForm.propTypes = {
|
||||
stepProps: PropTypes.shape({
|
||||
isUpdate: PropTypes.bool
|
||||
}),
|
||||
initialValues: PropTypes.object,
|
||||
onSubmit: PropTypes.func
|
||||
}
|
||||
|
||||
export default CreateForm
|
20
src/fireedge/src/client/components/Forms/Provider/index.js
Normal file
20
src/fireedge/src/client/components/Forms/Provider/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import CreateForm from 'client/components/Forms/Provider/CreateForm'
|
||||
|
||||
export {
|
||||
CreateForm
|
||||
}
|
@ -21,7 +21,7 @@ import { T } from 'client/constants'
|
||||
|
||||
import {
|
||||
FORM_FIELDS, STEP_FORM_SCHEMA
|
||||
} from 'client/containers/Provisions/Form/ProvisionForm/Steps/BasicConfiguration/schema'
|
||||
} from 'client/components/Forms/Provision/CreateForm/Steps/BasicConfiguration/schema'
|
||||
|
||||
export const STEP_ID = 'configuration'
|
||||
|
@ -26,11 +26,11 @@ import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import { EmptyCard } from 'client/components/Cards'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
import { STEP_ID as PROVIDER_ID } from 'client/containers/Provisions/Form/ProvisionForm/Steps/Provider'
|
||||
import { STEP_ID as TEMPLATE_ID } from 'client/containers/Provisions/Form/ProvisionForm/Steps/Template'
|
||||
import { STEP_ID as PROVIDER_ID } from 'client/components/Forms/Provision/CreateForm/Steps/Provider'
|
||||
import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/Provision/CreateForm/Steps/Template'
|
||||
import {
|
||||
FORM_FIELDS, STEP_FORM_SCHEMA
|
||||
} from 'client/containers/Provisions/Form/ProvisionForm/Steps/Inputs/schema'
|
||||
} from 'client/components/Forms/Provision/CreateForm/Steps/Inputs/schema'
|
||||
|
||||
export const STEP_ID = 'inputs'
|
||||
|
@ -25,9 +25,9 @@ import { EmptyCard, ProvisionCard } from 'client/components/Cards'
|
||||
import { getProvisionTypeFromTemplate } from 'client/models/ProvisionTemplate'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
import { STEP_ID as INPUTS_ID } from 'client/containers/Provisions/Form/ProvisionForm/Steps/Inputs'
|
||||
import { STEP_ID as TEMPLATE_ID } from 'client/containers/Provisions/Form/ProvisionForm/Steps/Template'
|
||||
import { STEP_FORM_SCHEMA } from 'client/containers/Provisions/Form/ProvisionForm/Steps/Provider/schema'
|
||||
import { STEP_ID as INPUTS_ID } from 'client/components/Forms/Provision/CreateForm/Steps/Inputs'
|
||||
import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/Provision/CreateForm/Steps/Template'
|
||||
import { STEP_FORM_SCHEMA } from 'client/components/Forms/Provision/CreateForm/Steps/Provider/schema'
|
||||
|
||||
export const STEP_ID = 'provider'
|
||||
|
@ -28,10 +28,10 @@ import { sanitize } from 'client/utils'
|
||||
import { isValidProvisionTemplate, getProvisionTypeFromTemplate } from 'client/models/ProvisionTemplate'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
import { STEP_ID as PROVIDER_ID } from 'client/containers/Provisions/Form/ProvisionForm/Steps/Provider'
|
||||
import { STEP_ID as CONFIGURATION_ID } from 'client/containers/Provisions/Form/ProvisionForm/Steps/BasicConfiguration'
|
||||
import { STEP_ID as INPUTS_ID } from 'client/containers/Provisions/Form/ProvisionForm/Steps/Inputs'
|
||||
import { STEP_FORM_SCHEMA } from 'client/containers/Provisions/Form/ProvisionForm/Steps/Template/schema'
|
||||
import { STEP_ID as PROVIDER_ID } from 'client/components/Forms/Provision/CreateForm/Steps/Provider'
|
||||
import { STEP_ID as CONFIGURATION_ID } from 'client/components/Forms/Provision/CreateForm/Steps/BasicConfiguration'
|
||||
import { STEP_ID as INPUTS_ID } from 'client/components/Forms/Provision/CreateForm/Steps/Inputs'
|
||||
import { STEP_FORM_SCHEMA } from 'client/components/Forms/Provision/CreateForm/Steps/Template/schema'
|
||||
|
||||
export const STEP_ID = 'template'
|
||||
|
@ -0,0 +1,52 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import Template from 'client/components/Forms/Provision/CreateForm/Steps/Template'
|
||||
import Provider from 'client/components/Forms/Provision/CreateForm/Steps/Provider'
|
||||
import BasicConfiguration from 'client/components/Forms/Provision/CreateForm/Steps/BasicConfiguration'
|
||||
import Inputs from 'client/components/Forms/Provision/CreateForm/Steps/Inputs'
|
||||
import { set, createSteps, cloneObject, mapUserInputs } from 'client/utils'
|
||||
|
||||
const Steps = createSteps(
|
||||
[Template, Provider, BasicConfiguration, Inputs],
|
||||
{
|
||||
transformBeforeSubmit: formData => {
|
||||
const { template, provider, configuration, inputs: dirtyInputs } = formData
|
||||
const { name, description } = configuration
|
||||
const providerName = provider?.[0]?.NAME
|
||||
|
||||
// clone object from redux store
|
||||
const provisionTemplateSelected = cloneObject(template?.[0] ?? {})
|
||||
|
||||
// update provider name if changed during form
|
||||
if (provisionTemplateSelected.defaults?.provision?.provider_name) {
|
||||
set(provisionTemplateSelected, 'defaults.provision.provider_name', providerName)
|
||||
} else if (provisionTemplateSelected.hosts?.length > 0) {
|
||||
provisionTemplateSelected.hosts.forEach(host => {
|
||||
set(host, 'provision.provider_name', providerName)
|
||||
})
|
||||
}
|
||||
|
||||
const parseInputs = mapUserInputs(dirtyInputs)
|
||||
const inputs = provisionTemplateSelected?.inputs
|
||||
?.map(input => ({ ...input, value: `${parseInputs[input?.name]}` }))
|
||||
|
||||
return { ...provisionTemplateSelected, name, description, inputs }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default Steps
|
@ -0,0 +1,50 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import FormStepper from 'client/components/FormStepper'
|
||||
import Steps from 'client/components/Forms/Provision/CreateForm/Steps'
|
||||
|
||||
const CreateForm = ({ onSubmit }) => {
|
||||
const { steps, defaultValues, resolver, transformBeforeSubmit } = Steps()
|
||||
|
||||
const methods = useForm({
|
||||
mode: 'onSubmit',
|
||||
defaultValues,
|
||||
resolver: yupResolver(resolver())
|
||||
})
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<FormStepper
|
||||
steps={steps}
|
||||
schema={resolver}
|
||||
onSubmit={data => onSubmit(transformBeforeSubmit?.(data) ?? data)}
|
||||
/>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
CreateForm.propTypes = {
|
||||
initialValues: PropTypes.object,
|
||||
onSubmit: PropTypes.func
|
||||
}
|
||||
|
||||
export default CreateForm
|
20
src/fireedge/src/client/components/Forms/Provision/index.js
Normal file
20
src/fireedge/src/client/components/Forms/Provision/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import CreateForm from 'client/components/Forms/Provision/CreateForm'
|
||||
|
||||
export {
|
||||
CreateForm
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import * as yup from 'yup'
|
||||
import { number, object } from 'yup'
|
||||
|
||||
import { getValidationFromFields } from 'client/utils'
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
@ -25,8 +25,8 @@ const MEMORY = {
|
||||
tooltip: 'Amount of RAM required for the VM.',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: yup
|
||||
.number()
|
||||
validation: number()
|
||||
.integer('Memory should be integer number')
|
||||
.positive('Memory should be positive number')
|
||||
.typeError('Memory must be a number')
|
||||
.required('Memory field is required')
|
||||
@ -42,8 +42,7 @@ const PHYSICAL_CPU = {
|
||||
the Virtual Machine. Half a processor is written 0.5.`,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: yup
|
||||
.number()
|
||||
validation: number()
|
||||
.positive('CPU should be positive number')
|
||||
.typeError('CPU must be a number')
|
||||
.required('CPU field is required')
|
||||
@ -59,8 +58,7 @@ const VIRTUAL_CPU = {
|
||||
hypervisor behavior is used, usually one virtual CPU`,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: yup
|
||||
.number()
|
||||
validation: number()
|
||||
.positive('Virtual CPU should be positive number')
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
@ -73,4 +71,4 @@ export const FIELDS = [
|
||||
VIRTUAL_CPU
|
||||
]
|
||||
|
||||
export const SCHEMA = yup.object(getValidationFromFields(FIELDS))
|
||||
export const SCHEMA = object(getValidationFromFields(FIELDS))
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import * as yup from 'yup'
|
||||
import { object, array, number } from 'yup'
|
||||
|
||||
import { StatusCircle, StatusChip } from 'client/components/Status'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
@ -61,8 +61,7 @@ const SIZE_FIELD = ({
|
||||
the datastore after the VM is terminated (ie goes into DONE state)`
|
||||
: `Non-persistent disk. The changes will be lost once
|
||||
the VM is terminated (ie goes into DONE state)`,
|
||||
validation: yup
|
||||
.number()
|
||||
validation: number()
|
||||
.positive()
|
||||
.min(0, 'Disk size field is required')
|
||||
.typeError('Disk must be a number')
|
||||
@ -79,13 +78,8 @@ export const FIELDS = vmTemplate => {
|
||||
return disks?.map(SIZE_FIELD).map(addParentToField)
|
||||
}
|
||||
|
||||
export const SCHEMA = yup
|
||||
.object({
|
||||
[PARENT]: yup.array(yup.object({
|
||||
[SIZE_FIELD().name]: SIZE_FIELD().validation
|
||||
}))
|
||||
})
|
||||
.transform(({ [PARENT]: disks, ...rest }) => ({
|
||||
...rest,
|
||||
[PARENT]: [disks].flat().filter(Boolean)
|
||||
}))
|
||||
export const SCHEMA = object({
|
||||
[PARENT]: array(object({
|
||||
[SIZE_FIELD().name]: SIZE_FIELD().validation
|
||||
})).ensure()
|
||||
})
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
@ -80,7 +80,7 @@ const BasicConfiguration = () => ({
|
||||
label: T.Configuration,
|
||||
resolver: SCHEMA,
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: useCallback(Content, [])
|
||||
content: Content
|
||||
})
|
||||
|
||||
export default BasicConfiguration
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import * as yup from 'yup'
|
||||
import { string, number, boolean, object } from 'yup'
|
||||
|
||||
import { getValidationFromFields } from 'client/utils'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
@ -27,10 +27,7 @@ const NAME = {
|
||||
When creating several VMs, the wildcard %idx will be
|
||||
replaced with a number starting from 0`,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: yup
|
||||
.string()
|
||||
.trim()
|
||||
.default(() => undefined)
|
||||
validation: string().trim().default(() => undefined)
|
||||
}
|
||||
|
||||
const INSTANCES = {
|
||||
@ -38,8 +35,7 @@ const INSTANCES = {
|
||||
label: 'Number of instances',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: yup
|
||||
.number()
|
||||
validation: number()
|
||||
.min(1, 'Instances minimum is 1')
|
||||
.integer('Instances should be an integer number')
|
||||
.required('Instances field is required')
|
||||
@ -54,9 +50,7 @@ const HOLD = {
|
||||
Sets the new VM to hold state, instead of pending.
|
||||
The scheduler will not deploy VMs in this state.
|
||||
It can be released later, or deployed manually.`,
|
||||
validation: yup
|
||||
.boolean()
|
||||
.default(() => false),
|
||||
validation: boolean().default(() => false),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
@ -67,9 +61,7 @@ const PERSISTENT = {
|
||||
tooltip: `
|
||||
Creates a private persistent copy of the template
|
||||
plus any image defined in DISK, and instantiates that copy.`,
|
||||
validation: yup
|
||||
.boolean()
|
||||
.default(() => false),
|
||||
validation: boolean().default(() => false),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
@ -80,4 +72,4 @@ export const FIELDS = [
|
||||
PERSISTENT
|
||||
]
|
||||
|
||||
export const SCHEMA = yup.object(getValidationFromFields(FIELDS))
|
||||
export const SCHEMA = object(getValidationFromFields(FIELDS))
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, SetStateAction } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {
|
||||
@ -27,7 +27,7 @@ import { Divider, makeStyles } from '@material-ui/core'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { Action } from 'client/components/Cards/SelectCard'
|
||||
import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable'
|
||||
import { TAB_ID as NIC_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking'
|
||||
@ -53,10 +53,22 @@ const useStyles = makeStyles(theme => ({
|
||||
}
|
||||
}))
|
||||
|
||||
/**
|
||||
* @param {string[]} newBootOrder - New boot order
|
||||
* @param {SetStateAction} setFormData - New boot order
|
||||
*/
|
||||
export const reorder = (newBootOrder, setFormData) => {
|
||||
setFormData(prev => {
|
||||
const newData = set({ ...prev }, 'extra.OS.BOOT', newBootOrder.join(','))
|
||||
|
||||
return { ...prev, extra: { ...prev.extra, OS: newData } }
|
||||
})
|
||||
}
|
||||
|
||||
const Booting = ({ data, setFormData }) => {
|
||||
const classes = useStyles()
|
||||
const { watch } = useFormContext()
|
||||
const bootOrder = data?.OS?.BOOT?.split(',')
|
||||
const bootOrder = data?.OS?.BOOT?.split(',').filter(Boolean)
|
||||
|
||||
const disks = useMemo(() => {
|
||||
const templateSeleted = watch(`${TEMPLATE_ID}[0]`)
|
||||
@ -67,7 +79,7 @@ const Booting = ({ data, setFormData }) => {
|
||||
const isVolatile = !IMAGE && !IMAGE_ID
|
||||
|
||||
const name = isVolatile
|
||||
? `DISK ${DISK_ID}: ${Tr(T.VolatileDisk)}`
|
||||
? <>`DISK ${DISK_ID}: `<Translate word={T.VolatileDisk} /></>
|
||||
: `DISK ${DISK_ID}: ${IMAGE}`
|
||||
|
||||
return {
|
||||
@ -91,7 +103,7 @@ const Booting = ({ data, setFormData }) => {
|
||||
{`NIC ${idx}: ${nic.NETWORK}`}
|
||||
</>
|
||||
)
|
||||
}))
|
||||
})) ?? []
|
||||
|
||||
const enabledItems = [...disks, ...nics]
|
||||
.filter(item => bootOrder.includes(item.ID))
|
||||
@ -100,15 +112,6 @@ const Booting = ({ data, setFormData }) => {
|
||||
const restOfItems = [...disks, ...nics]
|
||||
.filter(item => !bootOrder.includes(item.ID))
|
||||
|
||||
/** @param {string[]} newBootOrder - New boot order */
|
||||
const reorder = newBootOrder => {
|
||||
setFormData(prev => {
|
||||
const newData = set({ ...prev }, 'extra.OS.BOOT', newBootOrder.join(','))
|
||||
|
||||
return { ...prev, extra: { ...prev.extra, OS: newData } }
|
||||
})
|
||||
}
|
||||
|
||||
/** @param {DropResult} result - Drop result */
|
||||
const onDragEnd = result => {
|
||||
const { destination, source, draggableId } = result
|
||||
@ -122,7 +125,7 @@ const Booting = ({ data, setFormData }) => {
|
||||
newBootOrder.splice(source.index, 1) // remove current position
|
||||
newBootOrder.splice(destination.index, 0, draggableId) // set in new position
|
||||
|
||||
reorder(newBootOrder)
|
||||
reorder(newBootOrder, setFormData)
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,7 +137,7 @@ const Booting = ({ data, setFormData }) => {
|
||||
? newBootOrder.splice(itemIndex, 1)
|
||||
: newBootOrder.push(itemId)
|
||||
|
||||
reorder(newBootOrder)
|
||||
reorder(newBootOrder, setFormData)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -24,8 +24,9 @@ import SelectCard, { Action } from 'client/components/Cards/SelectCard'
|
||||
import { AttachNicForm } from 'client/components/Forms/Vm'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import { NIC_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
|
||||
import { reorder } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@ -45,7 +46,7 @@ const Networking = ({ data, setFormData }) => {
|
||||
?.map((nic, idx) => ({ ...nic, NAME: `NIC${idx}` }))
|
||||
|
||||
const { handleRemove, handleSave } = useListForm({
|
||||
parent: STEP_ID,
|
||||
parent: EXTRA_ID,
|
||||
key: TAB_ID,
|
||||
list: nics,
|
||||
setList: setFormData,
|
||||
@ -53,6 +54,27 @@ const Networking = ({ data, setFormData }) => {
|
||||
addItemId: (item, id) => ({ ...item, NAME: id })
|
||||
})
|
||||
|
||||
const reorderBootOrder = nicId => {
|
||||
const getIndexFromNicId = id => String(id).toLowerCase().replace('nic', '')
|
||||
const idxToRemove = getIndexFromNicId(nicId)
|
||||
|
||||
const nicIds = nics
|
||||
.filter(nic => nic.NAME !== nicId)
|
||||
.map(nic => String(nic.NAME).toLowerCase())
|
||||
|
||||
const newBootOrder = [...data?.OS?.BOOT?.split(',').filter(Boolean)]
|
||||
.filter(bootId => !bootId.startsWith('nic') || nicIds.includes(bootId))
|
||||
.map(bootId => {
|
||||
if (!bootId.startsWith('nic')) return bootId
|
||||
|
||||
const nicId = getIndexFromNicId(bootId)
|
||||
|
||||
return nicId < idxToRemove ? bootId : `nic${nicId - 1}`
|
||||
})
|
||||
|
||||
reorder(newBootOrder, setFormData)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonToTriggerForm
|
||||
@ -92,7 +114,10 @@ const Networking = ({ data, setFormData }) => {
|
||||
{!hasAlias &&
|
||||
<Action
|
||||
data-cy={`remove-${NAME}`}
|
||||
handleClick={() => handleRemove(NAME)}
|
||||
handleClick={() => {
|
||||
handleRemove(NAME)
|
||||
reorderBootOrder(NAME)
|
||||
}}
|
||||
icon={<Trash size={18} />}
|
||||
/>
|
||||
}
|
||||
|
@ -81,8 +81,8 @@ export const SCHED_ACTION_SCHEMA = lazy(({ TIME } = {}) => {
|
||||
})
|
||||
|
||||
export const SCHEMA = object({
|
||||
NIC: array(NIC_SCHEMA),
|
||||
SCHED_ACTION: array(SCHED_ACTION_SCHEMA),
|
||||
NIC: array(NIC_SCHEMA).ensure(),
|
||||
SCHED_ACTION: array(SCHED_ACTION_SCHEMA).ensure(),
|
||||
OS: object({
|
||||
BOOT: string().trim().notRequired()
|
||||
}),
|
||||
@ -93,8 +93,3 @@ export const SCHEMA = object({
|
||||
DS_RANK_FIELD
|
||||
])
|
||||
})
|
||||
.transform(({ SCHED_ACTION, NIC, ...rest }) => ({
|
||||
...rest,
|
||||
SCHED_ACTION: [SCHED_ACTION ?? []].flat(),
|
||||
NIC: [NIC ?? []].flat()
|
||||
}))
|
||||
|
@ -14,7 +14,6 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useListForm } from 'client/hooks'
|
||||
@ -76,9 +75,9 @@ Content.propTypes = {
|
||||
|
||||
const VmTemplateStep = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.VMTemplate,
|
||||
label: T.SelectVmTemplate,
|
||||
resolver: SCHEMA,
|
||||
content: useCallback(Content, [])
|
||||
content: Content
|
||||
})
|
||||
|
||||
export default VmTemplateStep
|
||||
|
@ -13,11 +13,19 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import * as yup from 'yup'
|
||||
import { array, object, string } from 'yup'
|
||||
|
||||
export const SCHEMA = yup
|
||||
.array(yup.object())
|
||||
const TEMPLATE_SCHEMA = object({
|
||||
ID: string(),
|
||||
NAME: string(),
|
||||
TEMPLATE: object({
|
||||
DISK: array().ensure(),
|
||||
NIC: array().ensure()
|
||||
})
|
||||
})
|
||||
|
||||
export const SCHEMA = array(TEMPLATE_SCHEMA)
|
||||
.min(1, 'Select VM Template')
|
||||
.max(1, 'Max. one template selected')
|
||||
.required('Template field is required')
|
||||
.default([])
|
||||
.default(undefined)
|
||||
|
@ -17,36 +17,36 @@ import VmTemplatesTable, { STEP_ID as TEMPLATE_ID } from 'client/components/Form
|
||||
import BasicConfiguration, { STEP_ID as BASIC_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration'
|
||||
import ExtraConfiguration, { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { createSteps } from 'client/utils'
|
||||
import { createSteps, deepmerge } from 'client/utils'
|
||||
|
||||
const Steps = createSteps(() => {
|
||||
// const { [STEP_ID]: initialTemplate } = initialValues ?? {}
|
||||
const Steps = createSteps(
|
||||
[VmTemplatesTable, BasicConfiguration, ExtraConfiguration],
|
||||
{
|
||||
transformInitialValue: (vmTemplate, schema) => ({
|
||||
...schema.cast({
|
||||
[TEMPLATE_ID]: [vmTemplate],
|
||||
[BASIC_ID]: vmTemplate?.TEMPLATE,
|
||||
[EXTRA_ID]: vmTemplate?.TEMPLATE
|
||||
}, { stripUnknown: true })
|
||||
}),
|
||||
transformBeforeSubmit: formData => {
|
||||
const {
|
||||
[TEMPLATE_ID]: [templateSelected] = [],
|
||||
[BASIC_ID]: { name, instances, hold, persistent, ...restOfConfig } = {},
|
||||
[EXTRA_ID]: extraTemplate = {}
|
||||
} = formData ?? {}
|
||||
|
||||
return [
|
||||
VmTemplatesTable,
|
||||
BasicConfiguration,
|
||||
ExtraConfiguration
|
||||
]
|
||||
}, {
|
||||
transformBeforeSubmit: formData => {
|
||||
const {
|
||||
[TEMPLATE_ID]: [templateSelected] = [],
|
||||
[BASIC_ID]: { name, instances, hold, persistent, ...restOfConfig } = {},
|
||||
[EXTRA_ID]: extraTemplate = {}
|
||||
} = formData ?? {}
|
||||
// merge with template disks to get TYPE attribute
|
||||
const DISK = deepmerge(templateSelected.TEMPLATE?.DISK, restOfConfig?.DISK)
|
||||
const templateXML = jsonToXml({ ...extraTemplate, ...restOfConfig, DISK })
|
||||
const data = { instances, hold, persistent, template: templateXML }
|
||||
|
||||
const templates = [...new Array(instances)]
|
||||
.map((_, idx) => {
|
||||
const replacedName = name?.replace(/%idx/gi, idx)
|
||||
const templates = [...new Array(instances)]
|
||||
.map((_, idx) => ({ name: name?.replace(/%idx/gi, idx), ...data }))
|
||||
|
||||
const template = jsonToXml({ TEMPLATE: { ...extraTemplate, ...restOfConfig } })
|
||||
const data = { name: replacedName, instances, hold, persistent, template }
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
return [templateSelected, templates]
|
||||
return [templateSelected, templates]
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
export default Steps
|
||||
|
@ -14,21 +14,25 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import FormStepper from 'client/components/FormStepper'
|
||||
import { useUserApi, useVmGroupApi, useVmTemplateApi } from 'client/features/One'
|
||||
import { useFetch } from 'client/hooks'
|
||||
import FormStepper, { SkeletonStepsForm } from 'client/components/FormStepper'
|
||||
import Steps from 'client/components/Forms/VmTemplate/InstantiateForm/Steps'
|
||||
|
||||
const InstantiateForm = ({ initialValues, onSubmit }) => {
|
||||
const { steps, defaultValues, resolver, transformBeforeSubmit } = Steps(initialValues)
|
||||
const InstantiateForm = ({ template, onSubmit }) => {
|
||||
const stepProps = useMemo(() => Steps(template, template), [])
|
||||
const { steps, defaultValues, resolver, transformBeforeSubmit } = stepProps
|
||||
|
||||
const methods = useForm({
|
||||
mode: 'onSubmit',
|
||||
defaultValues,
|
||||
resolver: yupResolver(resolver())
|
||||
resolver: yupResolver(resolver?.())
|
||||
})
|
||||
|
||||
return (
|
||||
@ -42,9 +46,33 @@ const InstantiateForm = ({ initialValues, onSubmit }) => {
|
||||
)
|
||||
}
|
||||
|
||||
InstantiateForm.propTypes = {
|
||||
initialValues: PropTypes.object,
|
||||
const PreFetchingForm = ({ templateId, ...props }) => {
|
||||
const { getUsers } = useUserApi()
|
||||
const { getVmGroups } = useVmGroupApi()
|
||||
const { getVmTemplate } = useVmTemplateApi()
|
||||
const { fetchRequest, data } = useFetch(
|
||||
() => getVmTemplate(templateId, { extended: true })
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
templateId && fetchRequest()
|
||||
getUsers()
|
||||
getVmGroups()
|
||||
}, [])
|
||||
|
||||
return (templateId && !data)
|
||||
? <SkeletonStepsForm />
|
||||
: <InstantiateForm {...props} template={data} />
|
||||
}
|
||||
|
||||
PreFetchingForm.propTypes = {
|
||||
templateId: PropTypes.string,
|
||||
onSubmit: PropTypes.func
|
||||
}
|
||||
|
||||
export default InstantiateForm
|
||||
InstantiateForm.propTypes = {
|
||||
template: PropTypes.object,
|
||||
onSubmit: PropTypes.func
|
||||
}
|
||||
|
||||
export default PreFetchingForm
|
||||
|
@ -13,10 +13,11 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
|
||||
import ButtonToTriggerForm, { ButtonToTriggerFormPropTypes } from 'client/components/Forms/ButtonToTriggerForm'
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
export {
|
||||
ButtonToTriggerForm,
|
||||
ButtonToTriggerFormPropTypes,
|
||||
FormWithSchema
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ const StatusBadge = memo(({ stateColor, children, customTransform, ...props }) =
|
||||
<Badge
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
classes={{ badge: classes.badge }}
|
||||
overlap='circle'
|
||||
overlap='circular'
|
||||
variant='dot'
|
||||
{...props}
|
||||
>
|
||||
|
@ -0,0 +1,137 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Row } from 'react-table'
|
||||
|
||||
import { Action } from 'client/components/Cards/SelectCard'
|
||||
import { ButtonToTriggerForm } from 'client/components/Forms'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { DialogPropTypes, DialogProps } from 'client/components/Dialogs'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { CreateStepsCallback, CreateFormCallback } from 'client/utils'
|
||||
|
||||
/**
|
||||
* @typedef {object} Option
|
||||
* @property {string} cy - Cypress selector
|
||||
* @property {string} name - Label of option
|
||||
* @property {JSXElementConstructor} [icon] - Icon
|
||||
* @property {boolean} isConfirmDialog
|
||||
* - If `true`, the form will be a dialog with confirmation buttons
|
||||
* @property {function(object, Row[])} onSubmit - Function to handle after finish the form
|
||||
* @property {function():CreateStepsCallback|CreateFormCallback} form - Form
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} GlobalAction
|
||||
* @property {string} accessor - Accessor action (id)
|
||||
* @property {string} [tooltip] - Tooltip
|
||||
* @property {string} [label] - Label
|
||||
* @property {string} [color] - Color
|
||||
* @property {string} [icon] - Icon
|
||||
* @property {DialogProps} [dialogProps] - Dialog properties
|
||||
* @property {Option[]} [options] - Group of actions
|
||||
* @property {function(Row[])} [action] - Singular action without form
|
||||
* @property {boolean|{min: number, max: number}} [selected] - Condition for selected rows
|
||||
* @property {boolean} [disabled] - If `true`, action will be disabled
|
||||
*/
|
||||
|
||||
/**
|
||||
* Render global action.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {GlobalAction[]} props.item - Item action
|
||||
* @param {Row[]} props.selectedRows - Selected rows
|
||||
* @returns {JSXElementConstructor} Component JSX
|
||||
*/
|
||||
const ActionItem = memo(({ item, selectedRows }) => {
|
||||
const {
|
||||
accessor,
|
||||
tooltip,
|
||||
label,
|
||||
color = 'secondary',
|
||||
icon: Icon,
|
||||
dialogProps: { title, children, ...dialogProps } = {},
|
||||
options,
|
||||
action,
|
||||
disabled
|
||||
} = item
|
||||
|
||||
const buttonProps = {
|
||||
color,
|
||||
'data-cy': accessor && `action.${accessor}`,
|
||||
disabled,
|
||||
icon: Icon && <Icon size={18} />,
|
||||
label: label && Tr(label),
|
||||
title: tooltip && Tr(tooltip)
|
||||
}
|
||||
|
||||
return action ? (
|
||||
<Action {...buttonProps} handleClick={() => action?.(selectedRows)} />
|
||||
) : (
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={buttonProps}
|
||||
dialogProps={{
|
||||
...dialogProps,
|
||||
title: typeof title === 'function' ? title(selectedRows) : title,
|
||||
children: typeof children === 'function' ? children(selectedRows) : children
|
||||
}}
|
||||
options={options?.map(({ form, onSubmit, ...option }) => ({
|
||||
form: form ? () => form(selectedRows) : undefined,
|
||||
onSubmit: data => onSubmit(data, selectedRows),
|
||||
...option
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
}, (prev, next) => prev.selectedRows?.length === next.selectedRows?.length)
|
||||
|
||||
export const ActionPropTypes = PropTypes.shape({
|
||||
accessor: PropTypes.string,
|
||||
color: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
tooltip: PropTypes.string,
|
||||
icon: PropTypes.any,
|
||||
disabled: PropTypes.bool,
|
||||
selected: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.shape({
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number
|
||||
})
|
||||
]),
|
||||
action: PropTypes.func,
|
||||
isConfirmDialog: PropTypes.bool,
|
||||
dialogProps: PropTypes.shape(DialogPropTypes),
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
cy: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
icon: PropTypes.any,
|
||||
form: PropTypes.func,
|
||||
onSubmit: PropTypes.func
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
ActionItem.propTypes = {
|
||||
item: ActionPropTypes,
|
||||
selectedRows: PropTypes.array
|
||||
}
|
||||
|
||||
ActionItem.displayName = 'ActionItem'
|
||||
|
||||
export default ActionItem
|
@ -0,0 +1,84 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { JSXElementConstructor, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Row } from 'react-table'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
|
||||
import Action, { ActionPropTypes, GlobalAction } from 'client/components/Tables/Enhanced/Utils/GlobalActions/Action'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
gap: '1em',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap'
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Render bulk actions.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {GlobalAction[]} props.globalActions - Possible bulk actions
|
||||
* @param {Row[]} props.selectedRows - Selected rows
|
||||
* @returns {JSXElementConstructor} Component JSX with all actions
|
||||
*/
|
||||
const GlobalActions = ({ globalActions, selectedRows }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const numberOfRowSelected = Object.keys(selectedRows)?.length
|
||||
|
||||
const [actionsSelected, actionsNoSelected] = useMemo(
|
||||
() => globalActions.reduce((memoResult, item) => {
|
||||
const { selected = false } = item
|
||||
|
||||
selected ? memoResult[0].push(item) : memoResult[1].push(item)
|
||||
|
||||
return memoResult
|
||||
}, [[], []]),
|
||||
[globalActions]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{actionsNoSelected?.map(item => (
|
||||
<Action key={item.accessor} item={item} />
|
||||
))}
|
||||
{numberOfRowSelected > 0 && (
|
||||
actionsSelected?.map(item => {
|
||||
const { min = 1, max = Number.MAX_SAFE_INTEGER } = item?.selected ?? {}
|
||||
const key = item.accessor ?? item.label
|
||||
|
||||
if (min < numberOfRowSelected && numberOfRowSelected > max) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Action key={key} item={item} selectedRows={selectedRows} />
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
GlobalActions.propTypes = {
|
||||
globalActions: PropTypes.arrayOf(ActionPropTypes),
|
||||
selectedRows: PropTypes.array
|
||||
}
|
||||
|
||||
export default GlobalActions
|
@ -14,6 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import CategoryFilter from 'client/components/Tables/Enhanced/Utils/CategoryFilter'
|
||||
import GlobalActions from 'client/components/Tables/Enhanced/Utils/GlobalActions'
|
||||
import GlobalFilter from 'client/components/Tables/Enhanced/Utils/GlobalFilter'
|
||||
import GlobalSelectedRows from 'client/components/Tables/Enhanced/Utils/GlobalSelectedRows'
|
||||
import GlobalSort from 'client/components/Tables/Enhanced/Utils/GlobalSort'
|
||||
@ -23,6 +24,7 @@ export * from 'client/components/Tables/Enhanced/Utils/utils'
|
||||
|
||||
export {
|
||||
CategoryFilter,
|
||||
GlobalActions,
|
||||
GlobalFilter,
|
||||
GlobalSelectedRows,
|
||||
GlobalSort,
|
||||
|
@ -20,8 +20,7 @@ import { CategoryFilter } from 'client/components/Tables/Enhanced/Utils'
|
||||
* Add filters defined in view yaml to columns.
|
||||
*
|
||||
* @param {object} config -
|
||||
* @param {object[]} config.filters
|
||||
* - List of criteria to filter the columns.
|
||||
* @param {object[]} config.filters - List of criteria to filter the columns.
|
||||
* @param {Column[]} config.columns - Columns
|
||||
* @returns {object} Column with filters
|
||||
*/
|
||||
@ -60,3 +59,36 @@ export const createCategoryFilter = title => ({
|
||||
}),
|
||||
filter: 'includesValue'
|
||||
})
|
||||
|
||||
/**
|
||||
* Add filters defined in view yaml to bulk actions.
|
||||
*
|
||||
* @param {object} params - Config parameters
|
||||
* @param {object[]} params.filters - Which buttons are visible to operate over the resources
|
||||
* @param {object[]} params.actions - Actions
|
||||
* @returns {object} Action with filters
|
||||
*/
|
||||
export const createActions = ({ filters = {}, actions = [] }) => {
|
||||
if (Object.keys(filters).length === 0) return actions
|
||||
|
||||
return actions
|
||||
.filter(({ accessor }) =>
|
||||
!accessor || filters[String(accessor.toLowerCase())] === true
|
||||
)
|
||||
.map(action => {
|
||||
const { accessor, options } = action
|
||||
|
||||
if (accessor) return action
|
||||
|
||||
const groupActions = options?.filter(({ cy }) => {
|
||||
const [, actionName] = cy?.split('.')
|
||||
|
||||
return filters[String(actionName.toLowerCase())] === true
|
||||
})
|
||||
|
||||
return groupActions?.length > 0
|
||||
? { ...action, options: groupActions }
|
||||
: undefined
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ import { T } from 'client/constants'
|
||||
const EnhancedTable = ({
|
||||
canFetchMore,
|
||||
columns,
|
||||
globalActions,
|
||||
data,
|
||||
fetchMore,
|
||||
getRowId,
|
||||
@ -131,6 +132,7 @@ const EnhancedTable = ({
|
||||
{/* TOOLBAR */}
|
||||
{!isFetching && (
|
||||
<Toolbar
|
||||
globalActions={globalActions}
|
||||
onlyGlobalSelectedRows={onlyGlobalSelectedRows}
|
||||
useTableProps={useTableProps}
|
||||
/>
|
||||
@ -201,6 +203,7 @@ const EnhancedTable = ({
|
||||
|
||||
export const EnhancedTableProps = {
|
||||
canFetchMore: PropTypes.bool,
|
||||
globalActions: PropTypes.array,
|
||||
columns: PropTypes.array,
|
||||
data: PropTypes.array,
|
||||
fetchMore: PropTypes.func,
|
||||
|
@ -16,7 +16,7 @@
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
|
||||
export default makeStyles(
|
||||
({ palette, typography, breakpoints, shadows }) => ({
|
||||
({ palette, typography, breakpoints }) => ({
|
||||
root: {
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
|
@ -13,12 +13,12 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { makeStyles, useMediaQuery } from '@material-ui/core'
|
||||
import { UseTableInstanceProps, UseRowSelectState, UseFiltersInstanceProps } from 'react-table'
|
||||
|
||||
import { GlobalSelectedRows, GlobalSort } from 'client/components/Tables/Enhanced/Utils'
|
||||
import { GlobalActions, GlobalSelectedRows, GlobalSort } from 'client/components/Tables/Enhanced/Utils'
|
||||
|
||||
const useToolbarStyles = makeStyles({
|
||||
root: {
|
||||
@ -29,9 +29,25 @@ const useToolbarStyles = makeStyles({
|
||||
}
|
||||
})
|
||||
|
||||
const Toolbar = ({ onlyGlobalSelectedRows = false, useTableProps = {} }) => {
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {object} props.globalActions - Global actions
|
||||
* @param {object} props.onlyGlobalSelectedRows - Show only the selected rows
|
||||
* @param {UseTableInstanceProps} props.useTableProps - Table props
|
||||
* @returns {JSXElementConstructor} Returns table toolbar
|
||||
*/
|
||||
const Toolbar = ({ globalActions, onlyGlobalSelectedRows, useTableProps }) => {
|
||||
const classes = useToolbarStyles()
|
||||
const isMobile = useMediaQuery(theme => theme.breakpoints.down('sm'))
|
||||
const isMobile = useMediaQuery(theme => theme.breakpoints.down('xs'))
|
||||
const isSmallDevice = useMediaQuery(theme => theme.breakpoints.down('sm'))
|
||||
|
||||
/** @type {UseRowSelectState} */
|
||||
const { selectedRowIds } = useTableProps?.state ?? {}
|
||||
|
||||
/** @type {UseFiltersInstanceProps} */
|
||||
const { preFilteredRows } = useTableProps ?? {}
|
||||
|
||||
const selectedRows = preFilteredRows.filter(row => !!selectedRowIds[row.id])
|
||||
|
||||
if (onlyGlobalSelectedRows) {
|
||||
return <GlobalSelectedRows useTableProps={useTableProps} />
|
||||
@ -39,12 +55,16 @@ const Toolbar = ({ onlyGlobalSelectedRows = false, useTableProps = {} }) => {
|
||||
|
||||
return isMobile ? null : (
|
||||
<div className={classes.root}>
|
||||
<GlobalSort useTableProps={useTableProps} />
|
||||
{globalActions?.length > 0 && (
|
||||
<GlobalActions globalActions={globalActions} selectedRows={selectedRows} />
|
||||
)}
|
||||
{!isSmallDevice && <GlobalSort useTableProps={useTableProps} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Toolbar.propTypes = {
|
||||
globalActions: PropTypes.array,
|
||||
onlyGlobalSelectedRows: PropTypes.bool,
|
||||
useTableProps: PropTypes.object
|
||||
}
|
||||
|
230
src/fireedge/src/client/components/Tables/VmTemplates/actions.js
Normal file
230
src/fireedge/src/client/components/Tables/VmTemplates/actions.js
Normal file
@ -0,0 +1,230 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useMemo } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import {
|
||||
RefreshDouble,
|
||||
AddSquare,
|
||||
Import,
|
||||
Trash,
|
||||
Lock,
|
||||
NoLock,
|
||||
UserSquareAlt,
|
||||
Group,
|
||||
ShareAndroid,
|
||||
Undo,
|
||||
Cart
|
||||
} from 'iconoir-react'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useVmTemplateApi } from 'client/features/One'
|
||||
|
||||
import { createActions } from 'client/components/Tables/Enhanced/Utils'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
import { VM_TEMPLATE_ACTIONS, MARKETPLACE_APP_ACTIONS } from 'client/constants'
|
||||
|
||||
const Actions = () => {
|
||||
const history = useHistory()
|
||||
const { view, getResourceView } = useAuth()
|
||||
const { getVmTemplate, getVmTemplates, lock, unlock, remove } = useVmTemplateApi()
|
||||
|
||||
const vmTemplateActions = useMemo(() => createActions({
|
||||
filters: getResourceView('VM-TEMPLATE')?.actions,
|
||||
actions: [
|
||||
{
|
||||
accessor: VM_TEMPLATE_ACTIONS.REFRESH,
|
||||
tooltip: 'Refresh',
|
||||
icon: RefreshDouble,
|
||||
action: async () => {
|
||||
await getVmTemplates()
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: VM_TEMPLATE_ACTIONS.CREATE_DIALOG,
|
||||
tooltip: 'Create',
|
||||
icon: AddSquare,
|
||||
disabled: true,
|
||||
action: rows => {
|
||||
// TODO: go to CREATE form
|
||||
// const { ID } = rows?.[0]?.original ?? {}
|
||||
// const path = generatePath(PATH.TEMPLATE.VMS.CREATE, { id: ID })
|
||||
|
||||
// history.push(path)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: VM_TEMPLATE_ACTIONS.IMPORT_DIALOG,
|
||||
tooltip: 'Import',
|
||||
icon: Import,
|
||||
selected: { max: 1 },
|
||||
disabled: true,
|
||||
action: rows => {
|
||||
// TODO: go to IMPORT form
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: VM_TEMPLATE_ACTIONS.UPDATE_DIALOG,
|
||||
label: 'Update',
|
||||
tooltip: 'Update',
|
||||
selected: { max: 1 },
|
||||
disabled: true,
|
||||
action: rows => {
|
||||
// const { ID } = rows?.[0]?.original ?? {}
|
||||
// const path = generatePath(PATH.TEMPLATE.VMS.CREATE, { id: ID })
|
||||
|
||||
// history.push(path)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: VM_TEMPLATE_ACTIONS.INSTANTIATE_DIALOG,
|
||||
label: 'Instantiate',
|
||||
tooltip: 'Instantiate',
|
||||
selected: { max: 1 },
|
||||
action: rows => {
|
||||
const template = rows?.[0]?.original ?? {}
|
||||
const path = PATH.TEMPLATE.VMS.INSTANTIATE
|
||||
|
||||
history.push(path, template)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: VM_TEMPLATE_ACTIONS.CLONE,
|
||||
label: 'Clone',
|
||||
tooltip: 'Clone',
|
||||
selected: true,
|
||||
disabled: true,
|
||||
options: [{ onSubmit: () => undefined }]
|
||||
},
|
||||
{
|
||||
tooltip: 'Change ownership',
|
||||
label: 'Ownership',
|
||||
selected: true,
|
||||
disabled: true,
|
||||
options: [{
|
||||
cy: `action.${VM_TEMPLATE_ACTIONS.CHANGE_OWNER}`,
|
||||
icon: UserSquareAlt,
|
||||
name: 'Change owner',
|
||||
isConfirmDialog: true,
|
||||
onSubmit: () => undefined
|
||||
}, {
|
||||
cy: `action.${VM_TEMPLATE_ACTIONS.CHANGE_GROUP}`,
|
||||
icon: Group,
|
||||
name: 'Change group',
|
||||
isConfirmDialog: true,
|
||||
onSubmit: () => undefined
|
||||
}, {
|
||||
cy: `action.${VM_TEMPLATE_ACTIONS.SHARE}`,
|
||||
icon: ShareAndroid,
|
||||
name: 'Share',
|
||||
isConfirmDialog: true,
|
||||
onSubmit: () => undefined
|
||||
}, {
|
||||
cy: `action.${VM_TEMPLATE_ACTIONS.UNSHARE}`,
|
||||
icon: Undo,
|
||||
name: 'Unshare',
|
||||
isConfirmDialog: true,
|
||||
onSubmit: () => undefined
|
||||
}]
|
||||
},
|
||||
{
|
||||
accessor: VM_TEMPLATE_ACTIONS.LOCK,
|
||||
tooltip: 'Lock',
|
||||
label: 'Lock',
|
||||
icon: Lock,
|
||||
selected: true,
|
||||
dialogProps: {
|
||||
title: 'Lock',
|
||||
children: rows => {
|
||||
const templates = rows?.map?.(({ original }) => original?.NAME)
|
||||
return 'Lock: ' + templates.join(', ')
|
||||
}
|
||||
},
|
||||
options: [{
|
||||
isConfirmDialog: true,
|
||||
onSubmit: async (_, rows) => {
|
||||
const templateIds = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all([...new Array(templateIds)].map(id => lock(id)))
|
||||
await Promise.all(templateIds.map(id => getVmTemplate(id)))
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
accessor: VM_TEMPLATE_ACTIONS.UNLOCK,
|
||||
tooltip: 'Unlock',
|
||||
label: 'Unlock',
|
||||
icon: NoLock,
|
||||
selected: true,
|
||||
dialogProps: {
|
||||
title: 'Unlock',
|
||||
children: rows => {
|
||||
const templates = rows?.map?.(({ original }) => original?.NAME)
|
||||
return 'Unlock: ' + templates.join(', ')
|
||||
}
|
||||
},
|
||||
options: [{
|
||||
isConfirmDialog: true,
|
||||
onSubmit: async (_, rows) => {
|
||||
const templateIds = [...new Array(rows?.map?.(({ original }) => original?.ID))]
|
||||
await Promise.all(templateIds.map(id => unlock(id)))
|
||||
await Promise.all(templateIds.map(id => getVmTemplate(id)))
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
accessor: VM_TEMPLATE_ACTIONS.DELETE,
|
||||
tooltip: 'Delete',
|
||||
icon: Trash,
|
||||
selected: true,
|
||||
dialogProps: {
|
||||
title: 'Delete',
|
||||
children: rows => {
|
||||
const templates = rows?.map?.(({ original }) => original?.NAME)
|
||||
return 'Delete: ' + templates.join(', ')
|
||||
}
|
||||
},
|
||||
options: [{
|
||||
isConfirmDialog: true,
|
||||
onSubmit: async (_, rows) => {
|
||||
const templateIds = [...new Array(rows?.map?.(({ original }) => original?.ID))]
|
||||
await Promise.all(templateIds.map(id => remove(id)))
|
||||
await getVmTemplates()
|
||||
}
|
||||
}]
|
||||
}
|
||||
]
|
||||
}), [view])
|
||||
|
||||
const marketplaceAppActions = useMemo(() => createActions({
|
||||
filters: getResourceView('MARKETPLACE-APP')?.actions,
|
||||
actions: [
|
||||
{
|
||||
accessor: MARKETPLACE_APP_ACTIONS.CREATE_DIALOG,
|
||||
tooltip: 'Create Marketplace App',
|
||||
icon: Cart,
|
||||
selected: { max: 1 },
|
||||
disabled: true,
|
||||
action: rows => {
|
||||
// TODO: go to Marketplace App CREATE form
|
||||
}
|
||||
}
|
||||
]
|
||||
}), [view])
|
||||
|
||||
return [...vmTemplateActions, ...marketplaceAppActions]
|
||||
}
|
||||
|
||||
export default Actions
|
@ -1,86 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@material-ui/core'
|
||||
|
||||
import Tabs from 'client/components/Tabs'
|
||||
|
||||
import { useFetch } from 'client/hooks'
|
||||
import { useVmTemplateApi } from 'client/features/One'
|
||||
|
||||
import * as Helper from 'client/models/Helper'
|
||||
|
||||
const VmTemplateDetail = ({ id }) => {
|
||||
const { getVmTemplate } = useVmTemplateApi()
|
||||
const { data, fetchRequest, loading, error } = useFetch(getVmTemplate)
|
||||
|
||||
useEffect(() => {
|
||||
fetchRequest(id)
|
||||
}, [id])
|
||||
|
||||
if ((!data && !error) || loading) {
|
||||
return <LinearProgress color='secondary' style={{ width: '100%' }} />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>{error}</div>
|
||||
}
|
||||
|
||||
const { ID, NAME, UNAME, GNAME, REGTIME, LOCK, TEMPLATE } = data
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: 'info',
|
||||
renderContent: (
|
||||
<div>
|
||||
<span>
|
||||
{`#${ID} - ${NAME}`}
|
||||
</span>
|
||||
<div>
|
||||
<p>Owner: {UNAME}</p>
|
||||
<p>Group: {GNAME}</p>
|
||||
<p>Locked: {Helper.levelLockToString(LOCK?.LOCKED)}</p>
|
||||
<p>Register time: {Helper.timeToString(REGTIME)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'template',
|
||||
renderContent: (
|
||||
<div>
|
||||
<pre>
|
||||
<code>
|
||||
{JSON.stringify(TEMPLATE, null, 2)}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Tabs tabs={tabs} />
|
||||
)
|
||||
}
|
||||
|
||||
VmTemplateDetail.propTypes = {
|
||||
id: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default VmTemplateDetail
|
@ -16,15 +16,22 @@
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useMemo, useEffect } from 'react'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useFetch } from 'client/hooks'
|
||||
import { useVmTemplate, useVmTemplateApi } from 'client/features/One'
|
||||
|
||||
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
|
||||
import { createColumns } from 'client/components/Tables/Enhanced/Utils'
|
||||
import VmTemplateColumns from 'client/components/Tables/VmTemplates/columns'
|
||||
import VmTemplateRow from 'client/components/Tables/VmTemplates/row'
|
||||
|
||||
const VmTemplatesTable = props => {
|
||||
const columns = useMemo(() => VmTemplateColumns, [])
|
||||
const { view, getResourceView, filterPool } = useAuth()
|
||||
|
||||
const columns = useMemo(() => createColumns({
|
||||
filters: getResourceView('VM-TEMPLATE')?.filters,
|
||||
columns: VmTemplateColumns
|
||||
}), [view])
|
||||
|
||||
const vmTemplates = useVmTemplate()
|
||||
const { getVmTemplates } = useVmTemplateApi()
|
||||
@ -32,7 +39,7 @@ const VmTemplatesTable = props => {
|
||||
const { status, fetchRequest, loading, reloading, STATUS } = useFetch(getVmTemplates)
|
||||
const { INIT, PENDING } = STATUS
|
||||
|
||||
useEffect(() => { fetchRequest() }, [])
|
||||
useEffect(() => { fetchRequest() }, [filterPool])
|
||||
|
||||
if (vmTemplates?.length === 0 && [INIT, PENDING].includes(status)) {
|
||||
return <SkeletonTable />
|
||||
|
@ -117,7 +117,6 @@ const DeleteSchedAction = memo(({ schedule, name }) => {
|
||||
|
||||
return (
|
||||
<ButtonToTriggerForm
|
||||
isConfirmDialog
|
||||
buttonProps={{
|
||||
'data-cy': `${VM_ACTIONS.SCHED_ACTION_DELETE}-${ID}`,
|
||||
icon: <Trash size={18} />,
|
||||
@ -127,7 +126,10 @@ const DeleteSchedAction = memo(({ schedule, name }) => {
|
||||
title: `${Tr(T.Delete)} ${Tr(T.ScheduledAction)}: ${name}`,
|
||||
children: <p>{Tr(T.DoYouWantProceed)}</p>
|
||||
}}
|
||||
options={[{ onSubmit: handleDelete }]}
|
||||
options={[{
|
||||
isConfirmDialog: true,
|
||||
onSubmit: handleDelete
|
||||
}]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@ -159,7 +161,6 @@ const CharterAction = memo(() => {
|
||||
|
||||
return (
|
||||
<ButtonToTriggerForm
|
||||
isConfirmDialog
|
||||
buttonProps={{
|
||||
'data-cy': 'create-charter',
|
||||
icon: <ClockOutline />,
|
||||
@ -194,7 +195,10 @@ const CharterAction = memo(() => {
|
||||
</>
|
||||
)
|
||||
}}
|
||||
options={[{ onSubmit: handleCreateCharter }]}
|
||||
options={[{
|
||||
isConfirmDialog: true,
|
||||
onSubmit: handleCreateCharter
|
||||
}]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
@ -35,7 +35,6 @@ const RevertAction = memo(({ snapshot }) => {
|
||||
|
||||
return (
|
||||
<ButtonToTriggerForm
|
||||
isConfirmDialog
|
||||
buttonProps={{
|
||||
'data-cy': `${VM_ACTIONS.SNAPSHOT_REVERT}-${SNAPSHOT_ID}`,
|
||||
icon: <UndoAction size={18} />
|
||||
@ -44,7 +43,10 @@ const RevertAction = memo(({ snapshot }) => {
|
||||
title: `${Tr(T.Revert)}: #${SNAPSHOT_ID} - ${NAME}`,
|
||||
children: <p>{Tr(T.DoYouWantProceed)}</p>
|
||||
}}
|
||||
options={[{ onSubmit: handleRevert }]}
|
||||
options={[{
|
||||
isConfirmDialog: true,
|
||||
onSubmit: handleRevert
|
||||
}]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@ -58,7 +60,6 @@ const DeleteAction = memo(({ snapshot }) => {
|
||||
|
||||
return (
|
||||
<ButtonToTriggerForm
|
||||
isConfirmDialog
|
||||
buttonProps={{
|
||||
'data-cy': `${VM_ACTIONS.SNAPSHOT_DELETE}-${SNAPSHOT_ID}`,
|
||||
icon: <Trash size={18} />
|
||||
@ -67,7 +68,10 @@ const DeleteAction = memo(({ snapshot }) => {
|
||||
title: `${Tr(T.Delete)}: #${SNAPSHOT_ID} - ${NAME}`,
|
||||
children: <p>{Tr(T.DoYouWantProceed)}</p>
|
||||
}}
|
||||
options={[{ onSubmit: handleDelete }]}
|
||||
options={[{
|
||||
isConfirmDialog: true,
|
||||
onSubmit: handleDelete
|
||||
}]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
@ -36,7 +36,6 @@ const DetachAction = memo(({ disk, name: imageName }) => {
|
||||
|
||||
return (
|
||||
<ButtonToTriggerForm
|
||||
isConfirmDialog
|
||||
buttonProps={{
|
||||
'data-cy': `${VM_ACTIONS.DETACH_DISK}-${DISK_ID}`,
|
||||
icon: <Trash size={18} />,
|
||||
@ -46,7 +45,10 @@ const DetachAction = memo(({ disk, name: imageName }) => {
|
||||
title: `${Tr(T.Detach)}: #${DISK_ID} - ${imageName}`,
|
||||
children: <p>{Tr(T.DoYouWantProceed)}</p>
|
||||
}}
|
||||
options={[{ onSubmit: handleDetach }]}
|
||||
options={[{
|
||||
isConfirmDialog: true,
|
||||
onSubmit: handleDetach
|
||||
}]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@ -185,7 +187,6 @@ const SnapshotRevertAction = memo(({ disk, snapshot }) => {
|
||||
|
||||
return (
|
||||
<ButtonToTriggerForm
|
||||
isConfirmDialog
|
||||
buttonProps={{
|
||||
'data-cy': `${VM_ACTIONS.SNAPSHOT_DISK_REVERT}-${DISK_ID}-${ID}`,
|
||||
icon: <UndoAction size={18} />,
|
||||
@ -195,7 +196,10 @@ const SnapshotRevertAction = memo(({ disk, snapshot }) => {
|
||||
title: `${Tr(T.Revert)}: #${ID} - ${NAME}`,
|
||||
children: <p>{Tr(T.DoYouWantProceed)}</p>
|
||||
}}
|
||||
options={[{ onSubmit: handleRevert }]}
|
||||
options={[{
|
||||
isConfirmDialog: true,
|
||||
onSubmit: handleRevert
|
||||
}]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@ -213,7 +217,6 @@ const SnapshotDeleteAction = memo(({ disk, snapshot }) => {
|
||||
|
||||
return (
|
||||
<ButtonToTriggerForm
|
||||
isConfirmDialog
|
||||
buttonProps={{
|
||||
'data-cy': `${VM_ACTIONS.SNAPSHOT_DISK_DELETE}-${DISK_ID}-${ID}`,
|
||||
icon: <Trash size={18} />,
|
||||
@ -223,7 +226,10 @@ const SnapshotDeleteAction = memo(({ disk, snapshot }) => {
|
||||
title: `${Tr(T.Delete)}: #${ID} - ${NAME}`,
|
||||
children: <p>{Tr(T.DoYouWantProceed)}</p>
|
||||
}}
|
||||
options={[{ onSubmit: handleDelete }]}
|
||||
options={[{
|
||||
isConfirmDialog: true,
|
||||
onSubmit: handleDelete
|
||||
}]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
@ -89,9 +89,11 @@ export * from 'client/constants/flow'
|
||||
export * from 'client/constants/provision'
|
||||
export * from 'client/constants/cluster'
|
||||
export * from 'client/constants/vm'
|
||||
export * from 'client/constants/vmTemplate'
|
||||
export * from 'client/constants/host'
|
||||
export * from 'client/constants/image'
|
||||
export * from 'client/constants/marketplace'
|
||||
export * from 'client/constants/marketplaceApp'
|
||||
export * from 'client/constants/datastore'
|
||||
export * from 'client/constants/securityGroup'
|
||||
export * from 'client/constants/zone'
|
||||
|
19
src/fireedge/src/client/constants/marketplaceApp.js
Normal file
19
src/fireedge/src/client/constants/marketplaceApp.js
Normal file
@ -0,0 +1,19 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
export const MARKETPLACE_APP_ACTIONS = {
|
||||
REFRESH: 'refresh',
|
||||
CREATE_DIALOG: 'create_dialog'
|
||||
}
|
@ -52,6 +52,7 @@ module.exports = {
|
||||
SaveAs: 'Save as',
|
||||
Search: 'Search',
|
||||
Select: 'Select',
|
||||
SelectVmTemplate: 'Select a VM Template',
|
||||
SelectGroup: 'Select a group',
|
||||
SelectRequest: 'Select request',
|
||||
Show: 'Show',
|
||||
|
@ -13,31 +13,22 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import * as yup from 'yup'
|
||||
import * as ACTIONS from 'client/constants/actions'
|
||||
|
||||
import Template from './Template'
|
||||
import BasicConfiguration from './BasicConfiguration'
|
||||
import Connection from './Connection'
|
||||
export const VM_TEMPLATE_ACTIONS = {
|
||||
REFRESH: 'refresh',
|
||||
CREATE_DIALOG: 'create_dialog',
|
||||
IMPORT_DIALOG: 'import_dialog',
|
||||
UPDATE_DIALOG: 'update_dialog',
|
||||
INSTANTIATE_DIALOG: 'instantiate_dialog',
|
||||
CLONE: 'clone',
|
||||
DELETE: 'delete',
|
||||
LOCK: 'lock',
|
||||
UNLOCK: 'unlock',
|
||||
SHARE: 'share',
|
||||
UNSHARE: 'unshare',
|
||||
|
||||
const Steps = ({ isUpdate }) => {
|
||||
const template = Template()
|
||||
const configuration = BasicConfiguration({ isUpdate })
|
||||
const connection = Connection({ isUpdate })
|
||||
|
||||
const steps = [configuration, connection]
|
||||
!isUpdate && steps.unshift(template)
|
||||
|
||||
const resolvers = () => yup
|
||||
.object({
|
||||
[template.id]: template.resolver(),
|
||||
[configuration.id]: configuration.resolver(),
|
||||
[connection.id]: connection.resolver()
|
||||
})
|
||||
|
||||
const defaultValues = resolvers().default()
|
||||
|
||||
return { steps, defaultValues, resolvers }
|
||||
RENAME: ACTIONS.RENAME,
|
||||
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
|
||||
CHANGE_GROUP: ACTIONS.CHANGE_GROUP
|
||||
}
|
||||
|
||||
export default Steps
|
@ -15,25 +15,58 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Redirect, useParams } from 'react-router'
|
||||
import { Redirect, useParams, useHistory } from 'react-router'
|
||||
|
||||
import { Container, LinearProgress } from '@material-ui/core'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useFetchAll } from 'client/hooks'
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { useProviderApi } from 'client/features/One'
|
||||
import ProviderForm from 'client/containers/Providers/Form/ProviderForm'
|
||||
import { getConnectionEditable } from 'client/models/ProviderTemplate'
|
||||
import { CreateForm } from 'client/components/Forms/Provider'
|
||||
import { isValidProviderTemplate, getConnectionEditable, getConnectionFixed } from 'client/models/ProviderTemplate'
|
||||
import { PATH } from 'client/apps/provision/routes'
|
||||
import { isDevelopment, deepmerge } from 'client/utils'
|
||||
|
||||
function ProviderCreateForm () {
|
||||
const { id } = useParams()
|
||||
const [initialValues, setInitialValues] = useState(null)
|
||||
const history = useHistory()
|
||||
const { id } = useParams()
|
||||
|
||||
const { providerConfig } = useAuth()
|
||||
const { getProvider, getProviderConnection } = useProviderApi()
|
||||
const { enqueueSuccess, enqueueError } = useGeneralApi()
|
||||
const { getProvider, getProviderConnection, createProvider, updateProvider } = useProviderApi()
|
||||
const { data: preloadedData, fetchRequestAll, loading, error } = useFetchAll()
|
||||
|
||||
const onSubmit = async formData => {
|
||||
try {
|
||||
if (id !== undefined) {
|
||||
const [provider = {}, connection = []] = preloadedData ?? []
|
||||
const providerId = provider?.ID
|
||||
|
||||
const formatData = deepmerge({ connection }, formData)
|
||||
|
||||
await updateProvider(id, formatData)
|
||||
enqueueSuccess(`Provider updated - ID: ${providerId}`)
|
||||
} else {
|
||||
if (!isValidProviderTemplate(formData, providerConfig)) {
|
||||
enqueueError('The template selected has a bad format. Ask your cloud administrator')
|
||||
history.push(PATH.PROVIDERS.LIST)
|
||||
}
|
||||
|
||||
const connectionFixed = getConnectionFixed(formData, providerConfig)
|
||||
const formatData = deepmerge(formData, { connection: connectionFixed })
|
||||
|
||||
const responseId = await createProvider(formatData)
|
||||
enqueueSuccess(`Provider created - ID: ${responseId}`)
|
||||
}
|
||||
|
||||
history.push(PATH.PROVIDERS.LIST)
|
||||
} catch (err) {
|
||||
isDevelopment() && console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const preloadFetchData = async () => {
|
||||
const data = await fetchRequestAll([
|
||||
@ -46,7 +79,8 @@ function ProviderCreateForm () {
|
||||
|
||||
const {
|
||||
PLAIN: { provider: plainProvider } = {},
|
||||
PROVISION_BODY: { description, ...currentBodyTemplate }
|
||||
// remove encrypted connection from body template
|
||||
PROVISION_BODY: { description, connection: _, ...currentBodyTemplate }
|
||||
} = provider?.TEMPLATE
|
||||
|
||||
const connectionEditable = getConnectionEditable(
|
||||
@ -73,7 +107,11 @@ function ProviderCreateForm () {
|
||||
<LinearProgress color='secondary' />
|
||||
) : (
|
||||
<Container style={{ display: 'flex', flexFlow: 'column' }} disableGutters>
|
||||
<ProviderForm {...{ id, preloadedData, initialValues }} />
|
||||
<CreateForm
|
||||
stepProps={{ isUpdate: id !== undefined }}
|
||||
onSubmit={onSubmit}
|
||||
initialValues={initialValues}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import PropTypes from 'prop-types'
|
||||
import { useHistory } from 'react-router'
|
||||
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import FormStepper from 'client/components/FormStepper'
|
||||
import Steps from 'client/containers/Providers/Form/ProviderForm/Steps'
|
||||
|
||||
import { useProviderApi } from 'client/features/One'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { isValidProviderTemplate, getConnectionFixed } from 'client/models/ProviderTemplate'
|
||||
import { PATH } from 'client/apps/provision/routes'
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
|
||||
const ProviderForm = ({ id, preloadedData, initialValues }) => {
|
||||
const history = useHistory()
|
||||
const isUpdate = id !== undefined
|
||||
|
||||
const { providerConfig } = useAuth()
|
||||
const { createProvider, updateProvider } = useProviderApi()
|
||||
const { enqueueError, enqueueSuccess, changeLoading } = useGeneralApi()
|
||||
|
||||
const { steps, defaultValues, resolvers } = Steps({ isUpdate })
|
||||
|
||||
const methods = useForm({
|
||||
mode: 'onSubmit',
|
||||
defaultValues: initialValues ?? defaultValues,
|
||||
resolver: yupResolver(resolvers())
|
||||
})
|
||||
|
||||
const redirectWithError = (message = 'Error') => {
|
||||
enqueueError(message)
|
||||
history.push(PATH.PROVIDERS.LIST)
|
||||
}
|
||||
|
||||
const callCreateProvider = async formData => {
|
||||
const { template, configuration, connection } = formData
|
||||
|
||||
const templateSelected = template?.[0]
|
||||
|
||||
const isValid = isValidProviderTemplate(templateSelected, providerConfig)
|
||||
|
||||
!isValid && redirectWithError(`
|
||||
The template selected has a bad format.
|
||||
Ask your cloud administrator`
|
||||
)
|
||||
|
||||
const { name, description } = configuration
|
||||
const connectionFixed = getConnectionFixed(templateSelected, providerConfig)
|
||||
|
||||
const formatData = {
|
||||
...templateSelected,
|
||||
connection: { ...connection, ...connectionFixed },
|
||||
description,
|
||||
name
|
||||
}
|
||||
|
||||
createProvider(formatData)
|
||||
.then(id => enqueueSuccess(`Provider created - ID: ${id}`))
|
||||
.then(() => history.push(PATH.PROVIDERS.LIST))
|
||||
}
|
||||
|
||||
const callUpdateProvider = formData => {
|
||||
const { configuration, connection: connectionEditable } = formData
|
||||
const { description } = configuration
|
||||
const [provider = {}, connection = []] = preloadedData
|
||||
|
||||
const { PROVISION_BODY: currentBodyTemplate } = provider?.TEMPLATE
|
||||
|
||||
const formatData = {
|
||||
...currentBodyTemplate,
|
||||
description,
|
||||
connection: { ...connection, ...connectionEditable }
|
||||
}
|
||||
|
||||
updateProvider(id, formatData)
|
||||
.then(() => enqueueSuccess(`Provider updated - ID: ${id}`))
|
||||
.then(() => history.push(PATH.PROVIDERS.LIST))
|
||||
}
|
||||
|
||||
const onSubmit = formData => {
|
||||
changeLoading(true)
|
||||
isUpdate ? callUpdateProvider(formData) : callCreateProvider(formData)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<FormStepper steps={steps} schema={resolvers} onSubmit={onSubmit} />
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
ProviderForm.propTypes = {
|
||||
id: PropTypes.string,
|
||||
preloadedData: PropTypes.object,
|
||||
initialValues: PropTypes.object
|
||||
}
|
||||
|
||||
export default ProviderForm
|
@ -21,11 +21,13 @@ import { NavArrowLeft as ArrowBackIcon } from 'iconoir-react'
|
||||
import { makeStyles, Container, LinearProgress, IconButton, Typography } from '@material-ui/core'
|
||||
|
||||
import { useFetch, useSocket } from 'client/hooks'
|
||||
import { useProviderApi } from 'client/features/One'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { useProviderApi, useProvisionApi } from 'client/features/One'
|
||||
import DebugLog from 'client/components/DebugLog'
|
||||
import ProvisionForm from 'client/containers/Provisions/Form/ProvisionForm'
|
||||
import { CreateForm } from 'client/components/Forms/Provision'
|
||||
import { PATH } from 'client/apps/provision/routes'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { isDevelopment } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@ -46,12 +48,25 @@ function ProvisionCreateForm () {
|
||||
const [uuid, setUuid] = useState(undefined)
|
||||
|
||||
const { getProvisionSocket: socket } = useSocket()
|
||||
const { enqueueInfo } = useGeneralApi()
|
||||
const { createProvision } = useProvisionApi()
|
||||
const { getProviders } = useProviderApi()
|
||||
const { data, fetchRequest, loading, error } = useFetch(getProviders)
|
||||
|
||||
const handleSetUuid = response => response && setUuid(response)
|
||||
const onSubmit = async formData => {
|
||||
try {
|
||||
const response = await createProvision(formData)
|
||||
enqueueInfo('Creating provision')
|
||||
|
||||
useEffect(() => { fetchRequest() }, [])
|
||||
response && setUuid(response)
|
||||
} catch (err) {
|
||||
isDevelopment() && console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchRequest()
|
||||
}, [])
|
||||
|
||||
if (uuid) {
|
||||
return <DebugLog {...{ uuid, socket, title: <Title /> }} />
|
||||
@ -65,7 +80,7 @@ function ProvisionCreateForm () {
|
||||
<LinearProgress color='secondary' />
|
||||
) : (
|
||||
<Container className={classes.container} disableGutters>
|
||||
<ProvisionForm handleAfterCreate={handleSetUuid} />
|
||||
<CreateForm onSubmit={onSubmit} />
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import FormStepper from 'client/components/FormStepper'
|
||||
import Steps from 'client/containers/Provisions/Form/ProvisionForm/Steps'
|
||||
|
||||
import { useProvisionApi } from 'client/features/One'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { set, cloneObject, mapUserInputs } from 'client/utils'
|
||||
|
||||
const ProvisionForm = ({ handleAfterCreate }) => {
|
||||
const { createProvision } = useProvisionApi()
|
||||
const { enqueueInfo } = useGeneralApi()
|
||||
|
||||
const { steps, defaultValues, resolvers } = Steps()
|
||||
|
||||
const methods = useForm({
|
||||
mode: 'onSubmit',
|
||||
defaultValues,
|
||||
resolver: yupResolver(resolvers())
|
||||
})
|
||||
|
||||
const onSubmit = async formData => {
|
||||
const { template, provider, configuration, inputs } = formData
|
||||
const { name, description } = configuration
|
||||
const providerName = provider?.[0]?.NAME
|
||||
|
||||
// clone object from redux store
|
||||
const provisionTemplateSelected = cloneObject(template?.[0] ?? {})
|
||||
|
||||
// update provider name if changed during form
|
||||
if (provisionTemplateSelected.defaults?.provision?.provider_name) {
|
||||
set(provisionTemplateSelected, 'defaults.provision.provider_name', providerName)
|
||||
} else if (provisionTemplateSelected.hosts?.length > 0) {
|
||||
provisionTemplateSelected.hosts.forEach(host => {
|
||||
set(host, 'provision.provider_name', providerName)
|
||||
})
|
||||
}
|
||||
|
||||
const parseInputs = mapUserInputs(inputs)
|
||||
|
||||
const formatData = {
|
||||
...provisionTemplateSelected,
|
||||
name,
|
||||
description,
|
||||
inputs: provisionTemplateSelected?.inputs
|
||||
?.map(input => ({ ...input, value: `${parseInputs[input?.name]}` }))
|
||||
}
|
||||
|
||||
const response = await createProvision(formatData)
|
||||
enqueueInfo('Creating provision')
|
||||
|
||||
handleAfterCreate?.(response)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<FormStepper steps={steps} schema={resolvers} onSubmit={onSubmit} />
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
ProvisionForm.propTypes = {
|
||||
handleAfterCreate: PropTypes.func
|
||||
}
|
||||
|
||||
export default ProvisionForm
|
@ -14,24 +14,21 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useEffect } from 'react'
|
||||
import { useHistory, useParams } from 'react-router'
|
||||
import { useHistory, useLocation } from 'react-router'
|
||||
import { Container } from '@material-ui/core'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { useVmTemplateApi, useUserApi, useVmGroupApi } from 'client/features/One'
|
||||
import { useVmTemplateApi } from 'client/features/One'
|
||||
import { InstantiateForm } from 'client/components/Forms/VmTemplate'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
import { isDevelopment } from 'client/utils'
|
||||
|
||||
function InstantiateVmTemplate () {
|
||||
const history = useHistory()
|
||||
const { templateId } = useParams()
|
||||
const initialValues = { template: { ID: templateId } }
|
||||
const { state } = useLocation()
|
||||
const { ID: templateId } = state ?? {}
|
||||
|
||||
const { enqueueInfo } = useGeneralApi()
|
||||
const { getUsers } = useUserApi()
|
||||
const { getVmGroups } = useVmGroupApi()
|
||||
const { instantiate } = useVmTemplateApi()
|
||||
|
||||
const onSubmit = async ([templateSelected, templates]) => {
|
||||
@ -47,14 +44,9 @@ function InstantiateVmTemplate () {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUsers()
|
||||
getVmGroups()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container style={{ display: 'flex', flexFlow: 'column' }} disableGutters>
|
||||
<InstantiateForm initialValues={initialValues} onSubmit={onSubmit} />
|
||||
<InstantiateForm templateId={templateId} onSubmit={onSubmit} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
@ -19,11 +19,13 @@ import { useState } from 'react'
|
||||
import { Container, Box } from '@material-ui/core'
|
||||
|
||||
import { VmTemplatesTable } from 'client/components/Tables'
|
||||
import VmTemplateActions from 'client/components/Tables/VmTemplates/actions'
|
||||
import VmTemplateTabs from 'client/components/Tabs/VmTemplate'
|
||||
import SplitPane from 'client/components/SplitPane'
|
||||
|
||||
function VmTemplates () {
|
||||
const [selectedRows, onSelectedRowsChange] = useState([])
|
||||
const actions = VmTemplateActions()
|
||||
|
||||
const getRowIds = () =>
|
||||
JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2)
|
||||
@ -38,7 +40,10 @@ function VmTemplates () {
|
||||
component={Container}
|
||||
>
|
||||
<SplitPane>
|
||||
<VmTemplatesTable onSelectedRowsChange={onSelectedRowsChange} />
|
||||
<VmTemplatesTable
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
globalActions={actions}
|
||||
/>
|
||||
|
||||
{selectedRows?.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'auto' }}>
|
||||
|
@ -17,10 +17,16 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { applicationService } from 'client/features/One/application/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const getApplication = createAction('cluster', applicationService.getApplication)
|
||||
/** @see {@link RESOURCES.document} */
|
||||
const SERVICE_APPLICATION = 'document[100]'
|
||||
|
||||
export const getApplication = createAction(
|
||||
`${SERVICE_APPLICATION}/detail`,
|
||||
applicationService.getApplication
|
||||
)
|
||||
|
||||
export const getApplications = createAction(
|
||||
'application/pool',
|
||||
`${SERVICE_APPLICATION}/pool`,
|
||||
applicationService.getApplications,
|
||||
response => ({ [RESOURCES.document[100]]: response })
|
||||
)
|
||||
|
@ -19,10 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/application/actions'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useApplication = () => (
|
||||
useSelector(state => state.one[RESOURCES.document[100]])
|
||||
useSelector(state => state[name]?.[RESOURCES.document[100]] ?? [])
|
||||
)
|
||||
|
||||
export const useApplicationApi = () => {
|
||||
|
@ -17,28 +17,31 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { applicationTemplateService } from 'client/features/One/applicationTemplate/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
/** @see {@link RESOURCES.document} */
|
||||
const SERVICE_TEMPLATE = 'document[101]'
|
||||
|
||||
export const getApplicationTemplate = createAction(
|
||||
'application-template',
|
||||
`${SERVICE_TEMPLATE}/detail`,
|
||||
applicationTemplateService.getApplicationTemplate
|
||||
)
|
||||
|
||||
export const getApplicationsTemplates = createAction(
|
||||
'application-template/pool',
|
||||
`${SERVICE_TEMPLATE}/pool`,
|
||||
applicationTemplateService.getApplicationsTemplates,
|
||||
response => ({ [RESOURCES.document[101]]: response })
|
||||
)
|
||||
|
||||
export const createApplicationTemplate = createAction(
|
||||
'application-template/create',
|
||||
`${SERVICE_TEMPLATE}/create`,
|
||||
applicationTemplateService.createApplicationTemplate
|
||||
)
|
||||
|
||||
export const updateApplicationTemplate = createAction(
|
||||
'application-template/update',
|
||||
`${SERVICE_TEMPLATE}/update`,
|
||||
applicationTemplateService.updateApplicationTemplate
|
||||
)
|
||||
|
||||
export const instantiateApplicationTemplate = createAction(
|
||||
'application-template/instantiate',
|
||||
`${SERVICE_TEMPLATE}/instantiate`,
|
||||
applicationTemplateService.instantiateApplicationTemplate
|
||||
)
|
||||
|
@ -19,10 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/applicationTemplate/actions'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useApplicationTemplate = () => (
|
||||
useSelector(state => state.one[RESOURCES.document[101]])
|
||||
useSelector(state => state[name]?.[RESOURCES.document[101]] ?? [])
|
||||
)
|
||||
|
||||
export const useApplicationTemplateApi = () => {
|
||||
|
@ -17,10 +17,16 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { clusterService } from 'client/features/One/cluster/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const getCluster = createAction('cluster', clusterService.getCluster)
|
||||
/** @see {@link RESOURCES.cluster} */
|
||||
const CLUSTER = 'cluster'
|
||||
|
||||
export const getCluster = createAction(
|
||||
`${CLUSTER}/detail`,
|
||||
clusterService.getCluster
|
||||
)
|
||||
|
||||
export const getClusters = createAction(
|
||||
'cluster/pool',
|
||||
`${CLUSTER}/pool`,
|
||||
clusterService.getClusters,
|
||||
response => ({ [RESOURCES.cluster]: response })
|
||||
)
|
||||
|
@ -19,10 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/cluster/actions'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useCluster = () => (
|
||||
useSelector(state => state.one[RESOURCES.cluster])
|
||||
useSelector(state => state[name]?.[RESOURCES.cluster] ?? [])
|
||||
)
|
||||
|
||||
export const useClusterApi = () => {
|
||||
|
@ -17,10 +17,16 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { datastoreService } from 'client/features/One/datastore/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const getDatastore = createAction('datastore', datastoreService.getDatastore)
|
||||
/** @see {@link RESOURCES.datastore} */
|
||||
const DATASTORE = 'datastore'
|
||||
|
||||
export const getDatastore = createAction(
|
||||
`${DATASTORE}/detail`,
|
||||
datastoreService.getDatastore
|
||||
)
|
||||
|
||||
export const getDatastores = createAction(
|
||||
'datastore/pool',
|
||||
`${DATASTORE}/pool`,
|
||||
datastoreService.getDatastores,
|
||||
response => ({ [RESOURCES.datastore]: response })
|
||||
)
|
||||
|
@ -19,10 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/datastore/actions'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useDatastore = () => (
|
||||
useSelector(state => state.one[RESOURCES.datastore])
|
||||
useSelector(state => state[name]?.[RESOURCES.datastore] ?? [])
|
||||
)
|
||||
|
||||
export const useDatastoreApi = () => {
|
||||
|
@ -17,10 +17,16 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { groupService } from 'client/features/One/group/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const getGroup = createAction('group', groupService.getGroup)
|
||||
/** @see {@link RESOURCES.group} */
|
||||
const GROUP = 'group'
|
||||
|
||||
export const getGroup = createAction(
|
||||
`${GROUP}/detail`,
|
||||
groupService.getGroup
|
||||
)
|
||||
|
||||
export const getGroups = createAction(
|
||||
'group/pool',
|
||||
`${GROUP}/pool`,
|
||||
groupService.getGroups,
|
||||
response => ({ [RESOURCES.group]: response })
|
||||
)
|
||||
|
@ -19,10 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/group/actions'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useGroup = () => (
|
||||
useSelector(state => state.one[RESOURCES.group])
|
||||
useSelector(state => state[name]?.[RESOURCES.group] ?? [])
|
||||
)
|
||||
|
||||
export const useGroupApi = () => {
|
||||
|
@ -17,10 +17,16 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { hostService } from 'client/features/One/host/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const getHost = createAction('host', hostService.getHost)
|
||||
/** @see {@link RESOURCES.host} */
|
||||
const HOST = 'host'
|
||||
|
||||
export const getHost = createAction(
|
||||
`${HOST}/detail`,
|
||||
hostService.getHost
|
||||
)
|
||||
|
||||
export const getHosts = createAction(
|
||||
'host/pool',
|
||||
`${HOST}/pool`,
|
||||
hostService.getHosts,
|
||||
response => ({ [RESOURCES.host]: response })
|
||||
)
|
||||
|
@ -19,9 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/host/actions'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useHost = () => (
|
||||
useSelector(state => state.one.hosts)
|
||||
useSelector(state => state[name]?.[RESOURCES.host])
|
||||
)
|
||||
|
||||
export const useHostApi = () => {
|
||||
|
@ -17,10 +17,16 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { imageService } from 'client/features/One/image/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const getImage = createAction('image', imageService.getImage)
|
||||
/** @see {@link RESOURCES.image} */
|
||||
const IMAGE = 'image'
|
||||
|
||||
export const getImage = createAction(
|
||||
`${IMAGE}/detail`,
|
||||
imageService.getImage
|
||||
)
|
||||
|
||||
export const getImages = createAction(
|
||||
'image/pool',
|
||||
`${IMAGE}/pool`,
|
||||
imageService.getImages,
|
||||
response => ({ [RESOURCES.image]: response })
|
||||
)
|
||||
|
@ -19,10 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/image/actions'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useImage = () => (
|
||||
useSelector(state => state.one[RESOURCES.image])
|
||||
useSelector(state => state[name]?.[RESOURCES.image] ?? [])
|
||||
)
|
||||
|
||||
export const useImageApi = () => {
|
||||
|
@ -17,10 +17,16 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { marketplaceService } from 'client/features/One/marketplace/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const getMarketplace = createAction('marketplace', marketplaceService.getMarketplace)
|
||||
/** @see {@link RESOURCES.marketplace} */
|
||||
const MARKETPLACE = 'marketplace'
|
||||
|
||||
export const getMarketplace = createAction(
|
||||
`${MARKETPLACE}/detail`,
|
||||
marketplaceService.getMarketplace
|
||||
)
|
||||
|
||||
export const getMarketplaces = createAction(
|
||||
'marketplace/pool',
|
||||
`${MARKETPLACE}/pool`,
|
||||
marketplaceService.getMarketplaces,
|
||||
response => ({ [RESOURCES.marketplace]: response })
|
||||
)
|
||||
|
@ -19,10 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/marketplace/actions'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useMarketplace = () => (
|
||||
useSelector(state => state.one[RESOURCES.marketplace])
|
||||
useSelector(state => state[name]?.[RESOURCES.marketplace] ?? [])
|
||||
)
|
||||
|
||||
export const useMarketplaceApi = () => {
|
||||
|
@ -17,13 +17,16 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { marketplaceAppService } from 'client/features/One/marketplaceApp/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
/** @see {@link RESOURCES.app} */
|
||||
const APP = 'app'
|
||||
|
||||
export const getMarketplaceApp = createAction(
|
||||
'app',
|
||||
`${APP}/detail`,
|
||||
marketplaceAppService.getMarketplaceApp
|
||||
)
|
||||
|
||||
export const getMarketplaceApps = createAction(
|
||||
'app/pool',
|
||||
`${APP}/pool`,
|
||||
marketplaceAppService.getMarketplaceApps,
|
||||
response => ({ [RESOURCES.app]: response })
|
||||
)
|
||||
|
@ -19,9 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/marketplaceApp/actions'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useMarketplaceApp = () => (
|
||||
useSelector(state => state.one.apps)
|
||||
useSelector(state => state[name]?.[RESOURCES.app])
|
||||
)
|
||||
|
||||
export const useMarketplaceAppApi = () => {
|
||||
|
@ -17,13 +17,19 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { userService } from 'client/features/One/user/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const changeGroup = createAction('user/change-group', userService.changeGroup)
|
||||
export const getUser = createAction('user', userService.getUser)
|
||||
/** @see {@link RESOURCES.user} */
|
||||
const USER = 'user'
|
||||
|
||||
export const getUser = createAction(
|
||||
`${USER}/detail`,
|
||||
userService.getUser
|
||||
)
|
||||
|
||||
export const getUsers = createAction(
|
||||
'user/pool',
|
||||
`${USER}/pool`,
|
||||
userService.getUsers,
|
||||
response => ({ [RESOURCES.user]: response })
|
||||
)
|
||||
|
||||
export const updateUser = createAction('user/update', userService.updateUser)
|
||||
export const updateUser = createAction(`${USER}/update`, userService.updateUser)
|
||||
export const changeGroup = createAction(`${USER}/change-group`, userService.changeGroup)
|
||||
|
@ -19,10 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/user/actions'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useUser = () => (
|
||||
useSelector(state => state.one[RESOURCES.user])
|
||||
useSelector(state => state[name]?.[RESOURCES.user] ?? [])
|
||||
)
|
||||
|
||||
export const useUserApi = () => {
|
||||
|
@ -16,21 +16,25 @@
|
||||
import { createAction } from 'client/features/One/utils'
|
||||
import { vmService } from 'client/features/One/vm/services'
|
||||
import { filterBy } from 'client/utils'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const getVm = createAction('vm/detail', vmService.getVm)
|
||||
/** @see {@link RESOURCES.vm} */
|
||||
const VM = 'vm'
|
||||
|
||||
export const getVm = createAction(`${VM}/detail`, vmService.getVm)
|
||||
|
||||
export const getVms = createAction(
|
||||
'vm/pool',
|
||||
`${VM}/pool`,
|
||||
vmService.getVms,
|
||||
(response, { vms: currentVms }) => {
|
||||
const vms = filterBy([...currentVms, ...response], 'ID')
|
||||
|
||||
return { vms }
|
||||
return { [RESOURCES.vm]: vms }
|
||||
}
|
||||
)
|
||||
|
||||
export const terminateVm = createAction(
|
||||
'vm/delete',
|
||||
`${VM}/delete`,
|
||||
payload => vmService.actionVm({
|
||||
...payload,
|
||||
action: {
|
||||
@ -40,24 +44,24 @@ export const terminateVm = createAction(
|
||||
})
|
||||
)
|
||||
|
||||
export const updateUserTemplate = createAction('vm/update', vmService.updateUserTemplate)
|
||||
export const rename = createAction('vm/rename', vmService.rename)
|
||||
export const resize = createAction('vm/resize', vmService.resize)
|
||||
export const changePermissions = createAction('vm/chmod', vmService.changePermissions)
|
||||
export const changeOwnership = createAction('vm/chown', vmService.changeOwnership)
|
||||
export const attachDisk = createAction('vm/attach/disk', vmService.attachDisk)
|
||||
export const detachDisk = createAction('vm/detach/disk', vmService.detachDisk)
|
||||
export const saveAsDisk = createAction('vm/saveas/disk', vmService.saveAsDisk)
|
||||
export const resizeDisk = createAction('vm/resize/disk', vmService.resizeDisk)
|
||||
export const createDiskSnapshot = createAction('vm/create/disk-snapshot', vmService.createDiskSnapshot)
|
||||
export const renameDiskSnapshot = createAction('vm/rename/disk-snapshot', vmService.renameDiskSnapshot)
|
||||
export const revertDiskSnapshot = createAction('vm/revert/disk-snapshot', vmService.revertDiskSnapshot)
|
||||
export const deleteDiskSnapshot = createAction('vm/delete/disk-snapshot', vmService.deleteDiskSnapshot)
|
||||
export const attachNic = createAction('vm/attach/nic', vmService.attachNic)
|
||||
export const detachNic = createAction('vm/detach/nic', vmService.detachNic)
|
||||
export const createSnapshot = createAction('vm/create/snapshot', vmService.createSnapshot)
|
||||
export const revertSnapshot = createAction('vm/revert/snapshot', vmService.revertSnapshot)
|
||||
export const deleteSnapshot = createAction('vm/delete/snapshot', vmService.deleteSnapshot)
|
||||
export const addScheduledAction = createAction('vm/add/scheduled-action', vmService.addScheduledAction)
|
||||
export const updateScheduledAction = createAction('vm/update/scheduled-action', vmService.updateScheduledAction)
|
||||
export const deleteScheduledAction = createAction('vm/delete/scheduled-action', vmService.deleteScheduledAction)
|
||||
export const updateUserTemplate = createAction(`${VM}/update`, vmService.updateUserTemplate)
|
||||
export const rename = createAction(`${VM}/rename`, vmService.rename)
|
||||
export const resize = createAction(`${VM}/resize`, vmService.resize)
|
||||
export const changePermissions = createAction(`${VM}/chmod`, vmService.changePermissions)
|
||||
export const changeOwnership = createAction(`${VM}/chown`, vmService.changeOwnership)
|
||||
export const attachDisk = createAction(`${VM}/attach/disk`, vmService.attachDisk)
|
||||
export const detachDisk = createAction(`${VM}/detach/disk`, vmService.detachDisk)
|
||||
export const saveAsDisk = createAction(`${VM}/saveas/disk`, vmService.saveAsDisk)
|
||||
export const resizeDisk = createAction(`${VM}/resize/disk`, vmService.resizeDisk)
|
||||
export const createDiskSnapshot = createAction(`${VM}/create/disk-snapshot`, vmService.createDiskSnapshot)
|
||||
export const renameDiskSnapshot = createAction(`${VM}/rename/disk-snapshot`, vmService.renameDiskSnapshot)
|
||||
export const revertDiskSnapshot = createAction(`${VM}/revert/disk-snapshot`, vmService.revertDiskSnapshot)
|
||||
export const deleteDiskSnapshot = createAction(`${VM}/delete/disk-snapshot`, vmService.deleteDiskSnapshot)
|
||||
export const attachNic = createAction(`${VM}/attach/nic`, vmService.attachNic)
|
||||
export const detachNic = createAction(`${VM}/detach/nic`, vmService.detachNic)
|
||||
export const createSnapshot = createAction(`${VM}/create/snapshot`, vmService.createSnapshot)
|
||||
export const revertSnapshot = createAction(`${VM}/revert/snapshot`, vmService.revertSnapshot)
|
||||
export const deleteSnapshot = createAction(`${VM}/delete/snapshot`, vmService.deleteSnapshot)
|
||||
export const addScheduledAction = createAction(`${VM}/add/scheduled-action`, vmService.addScheduledAction)
|
||||
export const updateScheduledAction = createAction(`${VM}/update/scheduled-action`, vmService.updateScheduledAction)
|
||||
export const deleteScheduledAction = createAction(`${VM}/delete/scheduled-action`, vmService.deleteScheduledAction)
|
||||
|
@ -19,9 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/vm/actions'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useVm = () => (
|
||||
useSelector(state => state.one.vms)
|
||||
useSelector(state => state[name]?.[RESOURCES.vm] ?? [])
|
||||
)
|
||||
|
||||
export const useVmApi = () => {
|
||||
|
@ -17,13 +17,16 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { vmGroupService } from 'client/features/One/vmGroup/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
/** @see {@link RESOURCES.vmgroup} */
|
||||
const VM_GROUP = 'vmgroup'
|
||||
|
||||
export const getVmGroup = createAction(
|
||||
'vmgroup/detail',
|
||||
`${VM_GROUP}/detail`,
|
||||
vmGroupService.getVmGroup
|
||||
)
|
||||
|
||||
export const getVmGroups = createAction(
|
||||
'vmgroup/pool',
|
||||
`${VM_GROUP}/pool`,
|
||||
vmGroupService.getVmGroups,
|
||||
response => ({ [RESOURCES.vmgroup]: response })
|
||||
)
|
||||
|
@ -19,10 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/vmGroup/actions'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useVmGroup = () => (
|
||||
useSelector(state => state.one[RESOURCES.vmgroup])
|
||||
useSelector(state => state[name]?.[RESOURCES.vmgroup])
|
||||
)
|
||||
|
||||
export const useVmGroupApi = () => {
|
||||
|
@ -17,12 +17,27 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { vmTemplateService } from 'client/features/One/vmTemplate/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const getVmTemplate = createAction('vm-template', vmTemplateService.getVmTemplate)
|
||||
/** @see {@link RESOURCES.template} */
|
||||
const TEMPLATE = 'template'
|
||||
|
||||
export const getVmTemplate = createAction(
|
||||
`${TEMPLATE}/detail`,
|
||||
vmTemplateService.getVmTemplate
|
||||
)
|
||||
|
||||
export const getVmTemplates = createAction(
|
||||
'vm-template/pool',
|
||||
`${TEMPLATE}/pool`,
|
||||
vmTemplateService.getVmTemplates,
|
||||
response => ({ [RESOURCES.template]: response })
|
||||
)
|
||||
|
||||
export const instantiate = createAction('vm-template/instantiate', vmTemplateService.instantiate)
|
||||
export const instantiate = createAction(`${TEMPLATE}/instantiate`, vmTemplateService.instantiate)
|
||||
export const allocate = createAction(`${TEMPLATE}/allocate`, vmTemplateService.allocate)
|
||||
export const clone = createAction(`${TEMPLATE}/clone`, vmTemplateService.clone)
|
||||
export const remove = createAction(`${TEMPLATE}/delete`, vmTemplateService.delete)
|
||||
export const update = createAction(`${TEMPLATE}/update`, vmTemplateService.update)
|
||||
export const changePermissions = createAction(`${TEMPLATE}/chmod`, vmTemplateService.changePermissions)
|
||||
export const changeOwnership = createAction(`${TEMPLATE}/chown`, vmTemplateService.changeOwnership)
|
||||
export const rename = createAction(`${TEMPLATE}/rename`, vmTemplateService.rename)
|
||||
export const lock = createAction(`${TEMPLATE}/lock`, vmTemplateService.lock)
|
||||
export const unlock = createAction(`${TEMPLATE}/unlock`, vmTemplateService.lock)
|
||||
|
@ -19,10 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/vmTemplate/actions'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useVmTemplate = () => (
|
||||
useSelector(state => state.one[RESOURCES.template])
|
||||
useSelector(state => state[name]?.[RESOURCES.template] ?? [])
|
||||
)
|
||||
|
||||
export const useVmTemplateApi = () => {
|
||||
@ -36,6 +36,18 @@ export const useVmTemplateApi = () => {
|
||||
return {
|
||||
getVmTemplate: (id, data) => unwrapDispatch(actions.getVmTemplate({ id, ...data })),
|
||||
getVmTemplates: () => unwrapDispatch(actions.getVmTemplates()),
|
||||
instantiate: (id, data) => unwrapDispatch(actions.instantiate({ id, ...data }))
|
||||
instantiate: (id, data) => unwrapDispatch(actions.instantiate({ id, ...data })),
|
||||
allocate: template => unwrapDispatch(actions.allocate(template)),
|
||||
clone: (id, data) => unwrapDispatch(actions.clone({ id, ...data })),
|
||||
remove: (id, image) => unwrapDispatch(actions.remove({ id, image })),
|
||||
update: (id, template, replace) =>
|
||||
unwrapDispatch(actions.update({ id, template, replace })),
|
||||
changePermissions: (id, data) =>
|
||||
unwrapDispatch(actions.changePermissions({ id, ...data })),
|
||||
changeOwnership: (id, ownership) =>
|
||||
unwrapDispatch(actions.changeOwnership({ id, ownership })),
|
||||
rename: (id, name) => unwrapDispatch(actions.rename({ id, name })),
|
||||
lock: (id, data) => unwrapDispatch(actions.lock({ id, ...data })),
|
||||
unlock: id => unwrapDispatch(actions.unlock({ id }))
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,219 @@ export const vmTemplateService = ({
|
||||
return [res?.data?.VMTEMPLATE_POOL?.VMTEMPLATE ?? []].flat()
|
||||
},
|
||||
|
||||
/**
|
||||
* Allocates a new template in OpenNebula.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @param {string} params.template - A string containing the template contents
|
||||
* @returns {number} Template id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
allocate: async params => {
|
||||
const name = Actions.TEMPLATE_ALLOCATE
|
||||
const command = { name, ...Commands[name] }
|
||||
const config = requestConfig(params, command)
|
||||
|
||||
const res = await RestClient.request(config)
|
||||
|
||||
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
|
||||
|
||||
return res?.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Clones an existing virtual machine template.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @param {number|string} params.id - The ID of the template to be cloned
|
||||
* @param {string} params.name - Name for the new template
|
||||
* @param {boolean} params.image
|
||||
* - `true` to clone the template plus any image defined in DISK.
|
||||
* The new IMAGE_ID is set into each DISK
|
||||
* @returns {number} Template id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
clone: async params => {
|
||||
const name = Actions.TEMPLATE_CLONE
|
||||
const command = { name, ...Commands[name] }
|
||||
const config = requestConfig(params, command)
|
||||
|
||||
const res = await RestClient.request(config)
|
||||
|
||||
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
|
||||
|
||||
return res?.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes the given template from the pool.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @param {number|string} params.id - Template id
|
||||
* @param {boolean} params.image
|
||||
* - `true` to delete the template plus any image defined in DISK
|
||||
* @returns {number} Template id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
delete: async params => {
|
||||
const name = Actions.TEMPLATE_DELETE
|
||||
const command = { name, ...Commands[name] }
|
||||
const config = requestConfig(params, command)
|
||||
|
||||
const res = await RestClient.request(config)
|
||||
|
||||
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
|
||||
|
||||
return res?.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces the template contents.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @param {number|string} params.id - Template id
|
||||
* @param {boolean} params.template - The new template contents
|
||||
* @param {0|1} params.replace
|
||||
* - Update type:
|
||||
* ``0``: Replace the whole template.
|
||||
* ``1``: Merge new template with the existing one.
|
||||
* @returns {number} Template id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
update: async params => {
|
||||
const name = Actions.TEMPLATE_UPDATE
|
||||
const command = { name, ...Commands[name] }
|
||||
const config = requestConfig(params, command)
|
||||
|
||||
const res = await RestClient.request(config)
|
||||
|
||||
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
|
||||
|
||||
return res?.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the permission bits of a template.
|
||||
*
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string|number} params.id - Template id
|
||||
* @param {{
|
||||
* ownerUse: number,
|
||||
* ownerManage: number,
|
||||
* ownerAdmin: number,
|
||||
* groupUse: number,
|
||||
* groupManage: number,
|
||||
* groupAdmin: number,
|
||||
* otherUse: number,
|
||||
* otherManage: number,
|
||||
* otherAdmin: number
|
||||
* }} params.permissions - Permissions data
|
||||
* @param {boolean} params.image
|
||||
* - `true` to chmod the template plus any image defined in DISK
|
||||
* @returns {number} Template id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
changePermissions: async ({ id, image, permissions }) => {
|
||||
const name = Actions.TEMPLATE_CHMOD
|
||||
const command = { name, ...Commands[name] }
|
||||
const config = requestConfig({ id, image, ...permissions }, command)
|
||||
|
||||
const res = await RestClient.request(config)
|
||||
|
||||
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
|
||||
|
||||
return res?.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the ownership of a template.
|
||||
*
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string|number} params.id - Template id
|
||||
* @param {{user: number, group: number}} params.ownership - Ownership data
|
||||
* @returns {number} Template id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
changeOwnership: async ({ id, ownership }) => {
|
||||
const name = Actions.TEMPLATE_CHOWN
|
||||
const command = { name, ...Commands[name] }
|
||||
const config = requestConfig({ id, ...ownership }, command)
|
||||
|
||||
const res = await RestClient.request(config)
|
||||
|
||||
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
|
||||
|
||||
return res?.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Renames a Template.
|
||||
*
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string|number} params.id - Template id
|
||||
* @param {string} params.name - New name
|
||||
* @returns {number} Template id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
rename: async params => {
|
||||
const name = Actions.TEMPLATE_RENAME
|
||||
const command = { name, ...Commands[name] }
|
||||
const config = requestConfig(params, command)
|
||||
|
||||
const res = await RestClient.request(config)
|
||||
|
||||
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
|
||||
|
||||
return res?.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Locks a Template.
|
||||
*
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string|number} params.id - Template id
|
||||
* @param {1|2|3|4} params.lock
|
||||
* - Lock level:
|
||||
* ``1``: Use
|
||||
* ``2``: Manage
|
||||
* ``3``: Admin
|
||||
* ``4``: All
|
||||
* @param {boolean} params.test - Check if the object is already locked to return an error
|
||||
* @returns {number} Template id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
lock: async params => {
|
||||
const name = Actions.TEMPLATE_LOCK
|
||||
const command = { name, ...Commands[name] }
|
||||
const config = requestConfig(params, command)
|
||||
|
||||
const res = await RestClient.request(config)
|
||||
|
||||
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
|
||||
|
||||
return res?.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Unlocks a Template.
|
||||
*
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string|number} params.id - Template id
|
||||
* @returns {number} Template id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
unlock: async params => {
|
||||
const name = Actions.TEMPLATE_UNLOCK
|
||||
const command = { name, ...Commands[name] }
|
||||
const config = requestConfig(params, command)
|
||||
|
||||
const res = await RestClient.request(config)
|
||||
|
||||
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
|
||||
|
||||
return res?.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Instantiates a new virtual machine from a template.
|
||||
*
|
||||
|
@ -17,10 +17,16 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { vNetworkService } from 'client/features/One/vnetwork/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const getVNetwork = createAction('vnet', vNetworkService.getVNetwork)
|
||||
/** @see {@link RESOURCES.vn} */
|
||||
const VNET = 'vn'
|
||||
|
||||
export const getVNetwork = createAction(
|
||||
`${VNET}/detail`,
|
||||
vNetworkService.getVNetwork
|
||||
)
|
||||
|
||||
export const getVNetworks = createAction(
|
||||
'vnet/pool',
|
||||
`${VNET}/pool`,
|
||||
vNetworkService.getVNetworks,
|
||||
response => ({ [RESOURCES.vn]: response })
|
||||
)
|
||||
|
@ -19,10 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/vnetwork/actions'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useVNetwork = () => (
|
||||
useSelector(state => state.one[RESOURCES.vn])
|
||||
useSelector(state => state[name]?.[RESOURCES.vn] ?? [])
|
||||
)
|
||||
|
||||
export const useVNetworkApi = () => {
|
||||
|
@ -17,13 +17,16 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { vNetworkTemplateService } from 'client/features/One/vnetworkTemplate/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
/** @see {@link RESOURCES.vntemplate} */
|
||||
const VNET_TEMPLATE = 'vntemplate'
|
||||
|
||||
export const getVNetworkTemplate = createAction(
|
||||
'vnet-template',
|
||||
`${VNET_TEMPLATE}/detail`,
|
||||
vNetworkTemplateService.getVNetworkTemplate
|
||||
)
|
||||
|
||||
export const getVNetworkTemplates = createAction(
|
||||
'vnet-template/pool',
|
||||
`${VNET_TEMPLATE}/pool`,
|
||||
vNetworkTemplateService.getVNetworkTemplates,
|
||||
response => ({ [RESOURCES.vntemplate]: response })
|
||||
)
|
||||
|
@ -19,10 +19,10 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { unwrapResult } from '@reduxjs/toolkit'
|
||||
|
||||
import * as actions from 'client/features/One/vnetworkTemplate/actions'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
import { name, RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
export const useVNetworkTemplate = () => (
|
||||
useSelector(state => state.one[RESOURCES.vntemplate])
|
||||
useSelector(state => state[name]?.[RESOURCES.vntemplate] ?? [])
|
||||
)
|
||||
|
||||
export const useVNetworkTemplateApi = () => {
|
||||
|
@ -17,13 +17,16 @@ import { createAction } from 'client/features/One/utils'
|
||||
import { vRouterService } from 'client/features/One/vrouter/services'
|
||||
import { RESOURCES } from 'client/features/One/slice'
|
||||
|
||||
/** @see {@link RESOURCES.vrouter} */
|
||||
const VROUTER = 'vrouter'
|
||||
|
||||
export const getVRouter = createAction(
|
||||
'vrouter/detail',
|
||||
`${VROUTER}/detail`,
|
||||
vRouterService.getVRouter
|
||||
)
|
||||
|
||||
export const getVRouters = createAction(
|
||||
'vrouter/pool',
|
||||
`${VROUTER}/pool`,
|
||||
vRouterService.getVRouters,
|
||||
response => ({ [RESOURCES.vrouter]: response })
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user