1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-16 22:50:10 +03:00

F #5422: Add bulk actions to vm templates (#1469)

This commit is contained in:
Sergio Betanzos 2021-09-21 10:19:30 +02:00 committed by GitHub
parent e85f740f9c
commit 1d83bd2de6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 1759 additions and 660 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

@ -95,7 +95,9 @@ const CustomStepper = ({
}
}}
{...(Boolean(errors[id]?.message) && { error: true })}
>{Tr(label)}</StepLabel>
>
{typeof label === 'string' ? Tr(label) : label}
</StepLabel>
</StepButton>
</Step>
))}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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'
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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