diff --git a/src/fireedge/src/client/assets/images/logos/alpine.png b/src/fireedge/src/client/assets/images/logos/alpine.png new file mode 100644 index 0000000000..66592c3660 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/alpine.png differ diff --git a/src/fireedge/src/client/assets/images/logos/alt.png b/src/fireedge/src/client/assets/images/logos/alt.png new file mode 100644 index 0000000000..647fe2f1c4 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/alt.png differ diff --git a/src/fireedge/src/client/assets/images/logos/arch.png b/src/fireedge/src/client/assets/images/logos/arch.png new file mode 100644 index 0000000000..11a3aa6430 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/arch.png differ diff --git a/src/fireedge/src/client/assets/images/logos/centos.png b/src/fireedge/src/client/assets/images/logos/centos.png new file mode 100644 index 0000000000..23e1696a11 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/centos.png differ diff --git a/src/fireedge/src/client/assets/images/logos/debian.png b/src/fireedge/src/client/assets/images/logos/debian.png new file mode 100644 index 0000000000..59d85ce0b7 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/debian.png differ diff --git a/src/fireedge/src/client/assets/images/logos/devuan.png b/src/fireedge/src/client/assets/images/logos/devuan.png new file mode 100644 index 0000000000..8df52556fd Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/devuan.png differ diff --git a/src/fireedge/src/client/assets/images/logos/fedora.png b/src/fireedge/src/client/assets/images/logos/fedora.png new file mode 100644 index 0000000000..476e2255d8 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/fedora.png differ diff --git a/src/fireedge/src/client/assets/images/logos/freebsd.png b/src/fireedge/src/client/assets/images/logos/freebsd.png new file mode 100644 index 0000000000..9767cecbf3 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/freebsd.png differ diff --git a/src/fireedge/src/client/assets/images/logos/hardenedbsd.png b/src/fireedge/src/client/assets/images/logos/hardenedbsd.png new file mode 100644 index 0000000000..23acdf82cd Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/hardenedbsd.png differ diff --git a/src/fireedge/src/client/assets/images/logos/knoppix.png b/src/fireedge/src/client/assets/images/logos/knoppix.png new file mode 100644 index 0000000000..ae62016ada Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/knoppix.png differ diff --git a/src/fireedge/src/client/assets/images/logos/linux.png b/src/fireedge/src/client/assets/images/logos/linux.png new file mode 100644 index 0000000000..45e219a929 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/linux.png differ diff --git a/src/fireedge/src/client/assets/images/logos/oracle.png b/src/fireedge/src/client/assets/images/logos/oracle.png new file mode 100644 index 0000000000..7b64b4d015 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/oracle.png differ diff --git a/src/fireedge/src/client/assets/images/logos/redhat.png b/src/fireedge/src/client/assets/images/logos/redhat.png new file mode 100644 index 0000000000..63d0bae718 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/redhat.png differ diff --git a/src/fireedge/src/client/assets/images/logos/suse.png b/src/fireedge/src/client/assets/images/logos/suse.png new file mode 100644 index 0000000000..8130fbe097 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/suse.png differ diff --git a/src/fireedge/src/client/assets/images/logos/ubuntu.png b/src/fireedge/src/client/assets/images/logos/ubuntu.png new file mode 100644 index 0000000000..a00dd5edb2 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/ubuntu.png differ diff --git a/src/fireedge/src/client/assets/images/logos/windows8.png b/src/fireedge/src/client/assets/images/logos/windows8.png new file mode 100644 index 0000000000..7acdd9889c Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/windows8.png differ diff --git a/src/fireedge/src/client/assets/images/logos/windowsxp.png b/src/fireedge/src/client/assets/images/logos/windowsxp.png new file mode 100644 index 0000000000..8503f76a00 Binary files /dev/null and b/src/fireedge/src/client/assets/images/logos/windowsxp.png differ diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/CommonFields.js b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/CommonFields.js index d8b3d3666e..268e2ea128 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/CommonFields.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/CommonFields.js @@ -39,7 +39,6 @@ export const READONLY = { notOnHypervisors: [vcenter], type: INPUT_TYPES.SELECT, values: [ - { text: '', value: '' }, { text: T.Yes, value: 'YES' }, { text: T.No, value: 'NO' } ], @@ -47,7 +46,7 @@ export const READONLY = { .string() .trim() .notRequired() - .default(undefined) + .default(() => 'NO') } export const DEV_PREFIX = { diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions/index.js index 6031663888..43b756ac61 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions/index.js @@ -14,33 +14,34 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useCallback } from 'react' +import PropTypes from 'prop-types' import FormWithSchema from 'client/components/Forms/FormWithSchema' - -import { - SCHEMA, - FIELDS -} from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions/schema' +import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions/schema' import { T } from 'client/constants' export const STEP_ID = 'advanced' +const Content = ({ hypervisor }) => ( + +) + const AdvancedOptions = ({ hypervisor } = {}) => ({ id: STEP_ID, label: T.AdvancedOptions, resolver: () => SCHEMA(hypervisor), optionsValidate: { abortEarly: false }, - content: useCallback( - () => ( - - ), - [hypervisor] - ) + content: () => Content({ hypervisor }) }) +Content.propTypes = { + hypervisor: PropTypes.any, + data: PropTypes.any, + setFormData: PropTypes.func +} + export default AdvancedOptions diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/index.js index a78626e2bb..c6d8976352 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/index.js @@ -14,55 +14,50 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useCallback } from 'react' +import PropTypes from 'prop-types' import { useListForm } from 'client/hooks' import { ImagesTable } from 'client/components/Tables' - -import { - SCHEMA -} from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/schema' +import { SCHEMA } from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/schema' import { T } from 'client/constants' export const STEP_ID = 'image' +const Content = ({ data, setFormData }) => { + const { ID } = data?.[0] ?? {} + + const { + handleSelect, + handleClear + } = useListForm({ key: STEP_ID, setList: setFormData }) + + const handleSelectedRows = rows => { + const { original = {} } = rows?.[0] ?? {} + + original.ID !== undefined ? handleSelect(original) : handleClear() + } + + return ( + + ) +} + const ImageStep = () => ({ id: STEP_ID, label: T.Image, - resolver: () => SCHEMA, - content: useCallback( - ({ data, setFormData }) => { - const selectedImage = data?.[0] - - const { - handleSelect, - handleClear - } = useListForm({ key: STEP_ID, setList: setFormData }) - - const handleSelectedRows = rows => { - const { original } = rows?.[0] ?? {} - const { ID, NAME, UID, UNAME } = original ?? {} - - const image = { - IMAGE_ID: ID, - IMAGE: NAME, - IMAGE_UID: UID, - IMAGE_UNAME: UNAME - } - - ID !== undefined ? handleSelect(image) : handleClear() - } - - return ( - - ) - }, []) + resolver: SCHEMA, + content: Content }) +Content.propTypes = { + data: PropTypes.any, + setFormData: PropTypes.func +} + export default ImageStep diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/schema.js b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/schema.js index 866b07ca12..f1024a0e2e 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/schema.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/schema.js @@ -20,4 +20,5 @@ export const SCHEMA = yup .min(1, 'Select image') .max(1, 'Max. one image selected') .required('Image field is required') - .default([]) + .ensure() + .default(() => []) diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/index.js index ad9a6ae2cd..10767ffafb 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/index.js @@ -13,10 +13,51 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import ImagesTable from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable' -import AdvancedOptions from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions' -import { createSteps } from 'client/utils' +import ImagesTable, { STEP_ID as STEP_IMAGE } from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable' +import AdvancedOptions, { STEP_ID as STEP_ADVANCED } from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions' +import { mapUserInputs, createSteps } from 'client/utils' -const Steps = createSteps([ImagesTable, AdvancedOptions]) +const Steps = createSteps( + [ImagesTable, AdvancedOptions], + { + transformInitialValue: initialValue => { + const { + IMAGE, + IMAGE_ID, + IMAGE_UID, + IMAGE_UNAME, + IMAGE_STATE, + ...diskProps + } = initialValue ?? {} + + return { + [STEP_IMAGE]: [{ + ...diskProps, + NAME: IMAGE, + ID: IMAGE_ID, + UID: IMAGE_UID, + UNAME: IMAGE_UNAME, + STATE: IMAGE_STATE + }], + [STEP_ADVANCED]: initialValue + } + }, + transformBeforeSubmit: formData => { + const { [STEP_IMAGE]: [image] = [], [STEP_ADVANCED]: advanced } = formData + const { ID, NAME, UID, UNAME, STATE, SIZE, ...imageProps } = image ?? {} + + return { + ...imageProps, + IMAGE: NAME, + IMAGE_ID: ID, + IMAGE_UID: UID, + IMAGE_UNAME: UNAME, + IMAGE_STATE: STATE, + ORIGINAL_SIZE: SIZE, + ...mapUserInputs(advanced) + } + } + } +) export default Steps diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions/index.js index bbf54c4813..db344cd5ba 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions/index.js @@ -14,31 +14,34 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useCallback } from 'react' +import PropTypes from 'prop-types' import FormWithSchema from 'client/components/Forms/FormWithSchema' - -import { - SCHEMA, - FIELDS -} from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions/schema' +import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions/schema' import { T } from 'client/constants' export const STEP_ID = 'advanced' +const Content = ({ hypervisor }) => ( + +) + const AdvancedOptions = ({ hypervisor } = {}) => ({ id: STEP_ID, label: T.AdvancedOptions, resolver: () => SCHEMA(hypervisor), optionsValidate: { abortEarly: false }, - content: useCallback( - () => , - [hypervisor] - ) + content: () => Content({ hypervisor }) }) +Content.propTypes = { + hypervisor: PropTypes.any, + data: PropTypes.any, + setFormData: PropTypes.func +} + export default AdvancedOptions diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration/index.js index c62bc31cc5..5069290aa4 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration/index.js @@ -14,31 +14,34 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useCallback } from 'react' +import PropTypes from 'prop-types' import FormWithSchema from 'client/components/Forms/FormWithSchema' - -import { - SCHEMA, - FIELDS -} from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration/schema' +import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration/schema' import { T } from 'client/constants' export const STEP_ID = 'configuration' +const Content = ({ hypervisor }) => ( + +) + const BasicConfiguration = ({ hypervisor } = {}) => ({ id: STEP_ID, label: T.Configuration, resolver: () => SCHEMA(hypervisor), optionsValidate: { abortEarly: false }, - content: useCallback( - () => , - [hypervisor] - ) + content: () => Content({ hypervisor }) }) +Content.propTypes = { + hypervisor: PropTypes.any, + data: PropTypes.any, + setFormData: PropTypes.func +} + export default BasicConfiguration diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/index.js index c8df7059ba..29fdbb810e 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/index.js @@ -13,10 +13,28 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import BasicConfiguration from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration' -import AdvancedOptions from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions' -import { createSteps } from 'client/utils' +import BasicConfiguration, { STEP_ID as BASIC_ID } from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration' +import AdvancedOptions, { STEP_ID as ADVANCED_ID } from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions' +import { mapUserInputs, createSteps } from 'client/utils' -const Steps = createSteps([BasicConfiguration, AdvancedOptions]) +const Steps = createSteps( + [BasicConfiguration, AdvancedOptions], + { + transformInitialValue: (disk = {}, schema) => ({ + ...schema.cast({ + [BASIC_ID]: disk, + [ADVANCED_ID]: disk + }, { stripUnknown: true }) + }), + transformBeforeSubmit: formData => { + const { + [BASIC_ID]: configuration = {}, + [ADVANCED_ID]: advanced = {} + } = formData ?? {} + + return { ...mapUserInputs(advanced), ...mapUserInputs(configuration) } + } + } +) export default Steps diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable/schema.js b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable/schema.js index 0a8c73a6c4..d1e3006421 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable/schema.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable/schema.js @@ -20,4 +20,5 @@ export const SCHEMA = yup .min(1, 'Select network') .max(1, 'Max. one network selected') .required('Network field is required') - .default([]) + .ensure() + .default(() => []) diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/index.js index 3a3e6f915c..4622c823bf 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/index.js @@ -13,16 +13,38 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import NetworksTable, { STEP_ID as NETWORK_STEP } from 'client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable' -import AdvancedOptions, { STEP_ID as ADVANCED_STEP } from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions' +import NetworksTable, { STEP_ID as NETWORK_ID } from 'client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable' +import AdvancedOptions, { STEP_ID as ADVANCED_ID } from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions' import { mapUserInputs, createSteps } from 'client/utils' const Steps = createSteps( [NetworksTable, AdvancedOptions], { + transformInitialValue: nic => { + const { + NETWORK, + NETWORK_ID: ID, + NETWORK_UID, + NETWORK_UNAME, + SECURITY_GROUPS, + ...rest + } = nic ?? {} + + return { + [NETWORK_ID]: [{ + ...nic, + ID, + NAME: NETWORK, + UID: NETWORK_UID, + UNAME: NETWORK_UNAME, + SECURITY_GROUPS + }], + [ADVANCED_ID]: rest + } + }, transformBeforeSubmit: formData => { - const { [NETWORK_STEP]: network, [ADVANCED_STEP]: advanced } = formData - const { ID, NAME, UID, UNAME, SECURITY_GROUPS } = network?.[0] + const { [NETWORK_ID]: [network] = [], [ADVANCED_ID]: advanced } = formData + const { ID, NAME, UID, UNAME, SECURITY_GROUPS } = network ?? {} return { NETWORK_ID: ID, @@ -32,19 +54,6 @@ const Steps = createSteps( SECURITY_GROUPS, ...mapUserInputs(advanced) } - }, - transformInitialValue: initialValue => { - const { NETWORK_ID, NETWORK, NETWORK_UID, NETWORK_UNAME, ...rest } = initialValue ?? {} - - return { - [NETWORK_STEP]: [{ - ID: NETWORK_ID, - NAME: NETWORK, - UID: NETWORK_UID, - UNAME: NETWORK_UNAME - }], - [ADVANCED_STEP]: rest - } } } ) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CloneForm/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/CloneForm/index.js new file mode 100644 index 0000000000..87ed298d18 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CloneForm/index.js @@ -0,0 +1,21 @@ +/* ------------------------------------------------------------------------- * + * 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/utils' +import { SCHEMA, FIELDS } from 'client/components/Forms/VmTemplate/CloneForm/schema' + +const CloneForm = createForm(SCHEMA, FIELDS) + +export default CloneForm diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CloneForm/schema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CloneForm/schema.js new file mode 100644 index 0000000000..3ecf1b3409 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CloneForm/schema.js @@ -0,0 +1,59 @@ +/* ------------------------------------------------------------------------- * + * 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 { string, boolean, object } from 'yup' +import { INPUT_TYPES } from 'client/constants' +import { getValidationFromFields } from 'client/utils' + +const PREFIX = { + name: 'prefix', + label: 'Prefix', + tooltip: ` + Several templates are selected, + please choose prefix to name the new copies.`, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .required('Prefix field is required') + .default(() => 'Copy of ') +} + +const NAME = { + name: 'name', + label: 'Name', + tooltip: 'Name for the new template.', + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .required('Name field is required') + .default(() => '') +} + +const IMAGES = { + name: 'image', + label: 'Clone with images', + tooltip: ` + You can also clone any Image referenced inside this Template. + They will be cloned to a new Image, and made persistent.`, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 } +} + +export const FIELDS = ({ isMultiple } = {}) => + [isMultiple ? PREFIX : NAME, IMAGES] + +export const SCHEMA = props => object(getValidationFromFields(FIELDS(props))) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/index.js index 6b10bd9a54..bb18176c70 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/index.js @@ -14,13 +14,9 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useMemo } from 'react' -import { useFormContext } from 'react-hook-form' - import FormWithSchema from 'client/components/Forms/FormWithSchema' import useStyles from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/styles' -import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable' import { SCHEMA, FIELDS } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema' import { Tr } from 'client/components/HOC' import { T } from 'client/constants' @@ -29,8 +25,6 @@ export const STEP_ID = 'configuration' const Content = () => { const classes = useStyles() - const { watch } = useFormContext() - const selectedVmTemplate = useMemo(() => watch(`${TEMPLATE_ID}[0]`), []) return (
@@ -47,12 +41,6 @@ const Content = () => { legend={Tr(T.Capacity)} id={STEP_ID} /> - DISK_FIELDS(vmTemplate), + // DISK: vmTemplate => DISK_FIELDS(vmTemplate), OWNERSHIP: OWNERSHIP_FIELDS, VM_GROUP: VM_GROUP_FIELDS, VCENTER: VCENTER_FIELDS @@ -35,7 +34,7 @@ export const FIELDS = { export const SCHEMA = object() .concat(INFORMATION_SCHEMA) .concat(CAPACITY_SCHEMA) - .concat(DISK_SCHEMA) + // .concat(DISK_SCHEMA) .concat(OWNERSHIP_SCHEMA) .concat(VM_GROUP_SCHEMA) .concat(VCENTER_SCHEMA) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting.js index 2cb05baf30..45f10bac8b 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting.js @@ -14,7 +14,7 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useMemo, SetStateAction } from 'react' +import { SetStateAction } from 'react' import PropTypes from 'prop-types' import { @@ -25,11 +25,10 @@ import { } from 'iconoir-react' import { Divider, makeStyles } from '@material-ui/core' import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd' -import { useFormContext } from 'react-hook-form' 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 STORAGE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/storage' import { TAB_ID as NIC_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking' import { set } from 'client/utils' import { T } from 'client/constants' @@ -54,10 +53,40 @@ const useStyles = makeStyles(theme => ({ })) /** - * @param {string[]} newBootOrder - New boot order - * @param {SetStateAction} setFormData - New boot order + * @param {string} id - Resource id: 'NIC' or 'DISK' + * @param {Array} list - List of resources + * @param {object} formData - Form data + * @param {SetStateAction} setFormData - React set state action */ -export const reorder = (newBootOrder, setFormData) => { +export const reorderBootAfterRemove = (id, list, formData, setFormData) => { + const type = String(id).toLowerCase().replace(/\d+/g, '') // nic | disk + const getIndexFromId = id => String(id).toLowerCase().replace(type, '') + const idxToRemove = getIndexFromId(id) + + const ids = list + .filter(resource => resource.NAME !== id) + .map(resource => String(resource.NAME).toLowerCase()) + + const newBootOrder = [...formData?.OS?.BOOT?.split(',').filter(Boolean)] + .filter(bootId => !bootId.startsWith(type) || ids.includes(bootId)) + .map(bootId => { + if (!bootId.startsWith(type)) return bootId + + const resourceId = getIndexFromId(bootId) + + return resourceId < idxToRemove + ? bootId + : `${type}${resourceId - 1}` + }) + + reorder(newBootOrder, setFormData) +} + +/** + * @param {string[]} newBootOrder - New boot order + * @param {SetStateAction} setFormData - React set state action + */ +const reorder = (newBootOrder, setFormData) => { setFormData(prev => { const newData = set({ ...prev }, 'extra.OS.BOOT', newBootOrder.join(',')) @@ -67,41 +96,32 @@ export const reorder = (newBootOrder, setFormData) => { const Booting = ({ data, setFormData }) => { const classes = useStyles() - const { watch } = useFormContext() const bootOrder = data?.OS?.BOOT?.split(',').filter(Boolean) ?? [] - const disks = useMemo(() => { - const templateSeleted = watch(`${TEMPLATE_ID}[0]`) - const listOfDisks = [templateSeleted?.TEMPLATE?.DISK ?? []].flat() - - return listOfDisks?.map(disk => { - const { DISK_ID, IMAGE, IMAGE_ID } = disk - const isVolatile = !IMAGE && !IMAGE_ID - - const name = isVolatile - ? <>`DISK ${DISK_ID}: ` - : `DISK ${DISK_ID}: ${IMAGE}` + const disks = data?.[STORAGE_ID] + ?.map((disk, idx) => { + const isVolatile = !disk?.IMAGE && !disk?.IMAGE_ID return { - ID: `disk${DISK_ID}`, + ID: `disk${idx}`, NAME: ( <> - {name} + {isVolatile + ? <>{`${disk?.NAME}: `} + : [disk?.NAME, disk?.IMAGE].filter(Boolean).join(': ')} ) } - }) - }, []) + }) ?? [] const nics = data?.[NIC_ID] - ?.map((nic, idx) => ({ ...nic, NAME: nic?.NAME ?? `NIC${idx}` })) ?.map((nic, idx) => ({ ID: `nic${idx}`, NAME: ( <> - {[nic?.NAME ?? `NIC${idx}`, nic.NETWORK].filter(Boolean).join(': ')} + {[nic?.NAME, nic.NETWORK].filter(Boolean).join(': ')} ) })) ?? [] diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js index dcd13b88cd..2e5cfaeb62 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js @@ -22,6 +22,7 @@ import { WarningCircledOutline as WarningIcon } from 'iconoir-react' import Tabs from 'client/components/Tabs' import { SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema' +import Storage from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/storage' import Networking from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking' import Placement from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/placement' import ScheduleAction from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/scheduleAction' @@ -35,6 +36,10 @@ const Content = ({ data, setFormData }) => { const { errors } = useFormContext() const tabs = [ + { + name: 'storage', + renderContent: Storage({ data, setFormData }) + }, { name: 'network', renderContent: Networking({ data, setFormData }) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking.js index fcdf2677ec..75ce48d1a5 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking.js @@ -25,8 +25,9 @@ import { AttachNicForm } from 'client/components/Forms/Vm' import { Tr, Translate } from 'client/components/HOC' 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 { SCHEMA as EXTRA_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema' +import { reorderBootAfterRemove } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting' +import { stringToBoolean } from 'client/models/Helper' import { T } from 'client/constants' const useStyles = makeStyles({ @@ -43,36 +44,21 @@ export const TAB_ID = 'NIC' const Networking = ({ data, setFormData }) => { const classes = useStyles() const nics = data?.[TAB_ID] - ?.map((nic, idx) => ({ ...nic, NAME: nic?.NAME ?? `NIC${idx}` })) - const { handleRemove, handleSave } = useListForm({ + const { handleSetList, handleRemove, handleSave } = useListForm({ parent: EXTRA_ID, key: TAB_ID, list: nics, setList: setFormData, - getItemId: (item) => item.NAME ?? `NIC${data.length + 1}`, - addItemId: (item, id) => ({ ...item, NAME: id }) + getItemId: item => item.NAME, + addItemId: (item, _, itemIndex) => ({ ...item, NAME: `${TAB_ID}${itemIndex}` }) }) - const reorderBootOrder = nicId => { - const getIndexFromNicId = id => String(id).toLowerCase().replace('nic', '') - const idxToRemove = getIndexFromNicId(nicId) + const reorderNics = () => { + const diskSchema = EXTRA_SCHEMA.pick([TAB_ID]) + const { [TAB_ID]: newList } = diskSchema.cast({ [TAB_ID]: data?.[TAB_ID] }) - 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) + handleSetList(newList) } return ( @@ -88,8 +74,7 @@ const Networking = ({ data, setFormData }) => { }} options={[{ form: () => AttachNicForm({ nics }), - onSubmit: formData => - handleSave(NIC_SCHEMA.cast(formData)) + onSubmit: handleSave }]} />
@@ -103,7 +88,12 @@ const Networking = ({ data, setFormData }) => { title={[NAME, NETWORK].filter(Boolean).join(' - ')} subheader={<> {Object - .entries({ RDP, SSH, ALIAS: PARENT, EXTERNAL }) + .entries({ + RDP: stringToBoolean(RDP), + SSH: stringToBoolean(SSH), + EXTERNAL: stringToBoolean(EXTERNAL), + ALIAS: PARENT + }) .map(([k, v]) => v ? `${k}` : '') .filter(Boolean) .join(' | ') @@ -116,7 +106,8 @@ const Networking = ({ data, setFormData }) => { data-cy={`remove-${NAME}`} handleClick={() => { handleRemove(NAME) - reorderBootOrder(NAME) + reorderNics() + reorderBootAfterRemove(NAME, nics, data, setFormData) }} icon={} /> diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js index 4840939958..4e8576a2f3 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js @@ -17,7 +17,6 @@ import { array, object, string, lazy } from 'yup' import { v4 as uuidv4 } from 'uuid' -import { SCHEMA as NETWORK_SCHEMA } from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/schema' import { SCHEMA as PUNCTUAL_SCHEMA } from 'client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/schema' import { SCHEMA as RELATIVE_SCHEMA } from 'client/components/Forms/Vm/CreateSchedActionForm/RelativeForm/schema' import { INPUT_TYPES } from 'client/constants' @@ -65,14 +64,6 @@ export const DS_RANK_FIELD = { validation: string().trim().notRequired() } -export const NIC_SCHEMA = object({ - NAME: string().trim(), - NETWORK_ID: string().trim(), - NETWORK: string().trim(), - NETWORK_UNAME: string().trim(), - SECURITY_GROUPS: string().trim() -}).concat(NETWORK_SCHEMA) - export const SCHED_ACTION_SCHEMA = lazy(({ TIME } = {}) => { const isRelative = String(TIME).includes('+') const schema = isRelative ? RELATIVE_SCHEMA : PUNCTUAL_SCHEMA @@ -81,7 +72,22 @@ export const SCHED_ACTION_SCHEMA = lazy(({ TIME } = {}) => { }) export const SCHEMA = object({ - NIC: array(NIC_SCHEMA).ensure(), + DISK: array() + .ensure() + .transform(disks => disks?.map((disk, idx) => ({ + ...disk, + NAME: disk?.NAME?.startsWith('DISK') || !disk?.NAME + ? `DISK${idx}` + : disk?.NAME + }))), + NIC: array() + .ensure() + .transform(nics => nics?.map((nic, idx) => ({ + ...nic, + NAME: nic?.NAME?.startsWith('NIC') || !nic?.NAME + ? `NIC${idx}` + : nic?.NAME + }))), SCHED_ACTION: array(SCHED_ACTION_SCHEMA).ensure(), OS: object({ BOOT: string().trim().notRequired() @@ -92,4 +98,4 @@ export const SCHEMA = object({ DS_REQ_FIELD, DS_RANK_FIELD ]) -}) +}).noUnknown(false) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/storage.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/storage.js new file mode 100644 index 0000000000..2303707f1a --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/storage.js @@ -0,0 +1,194 @@ +/* ------------------------------------------------------------------------- * + * 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 PropTypes from 'prop-types' +import { useFormContext } from 'react-hook-form' +import { makeStyles } from '@material-ui/core' +import { Edit, Trash } from 'iconoir-react' + +import { useListForm } from 'client/hooks' +import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm' +import SelectCard, { Action } from 'client/components/Cards/SelectCard' +import { ImageSteps, VolatileSteps } from 'client/components/Forms/Vm' +import { StatusCircle, StatusChip } from 'client/components/Status' +import { Tr, Translate } from 'client/components/HOC' + +import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable' +import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration' +import { SCHEMA as EXTRA_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema' +import { reorderBootAfterRemove } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting' +import { getState, getDiskType } from 'client/models/Image' +import { stringToBoolean } from 'client/models/Helper' +import { prettyBytes } from 'client/utils' +import { T } from 'client/constants' + +const useStyles = makeStyles({ + root: { + paddingBlock: '1em', + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(300px, auto))', + gap: '1em' + } +}) + +export const TAB_ID = 'DISK' + +const Storage = ({ data, setFormData }) => { + const classes = useStyles() + const { watch } = useFormContext() + const { HYPERVISOR } = useMemo(() => watch(`${TEMPLATE_ID}[0]`) ?? {}, []) + const disks = data?.[TAB_ID] + + const { handleSetList, handleRemove, handleSave } = useListForm({ + parent: EXTRA_ID, + key: TAB_ID, + list: disks, + setList: setFormData, + getItemId: item => item.NAME, + addItemId: (item, _, itemIndex) => ({ ...item, NAME: `${TAB_ID}${itemIndex}` }) + }) + + const reorderDisks = () => { + const diskSchema = EXTRA_SCHEMA.pick([TAB_ID]) + const { [TAB_ID]: newList } = diskSchema.cast({ [TAB_ID]: data?.[TAB_ID] }) + + handleSetList(newList) + } + + return ( + <> + ImageSteps({ hypervisor: HYPERVISOR }), + onSubmit: handleSave + }, + { + cy: 'attach-volatile-disk', + name: T.Volatile, + form: () => VolatileSteps({ hypervisor: HYPERVISOR }), + onSubmit: handleSave + } + ]} + /> +
+ {disks?.map(item => { + const { + NAME, + TYPE, + IMAGE, + IMAGE_ID, + IMAGE_STATE, + ORIGINAL_SIZE, + SIZE = ORIGINAL_SIZE, + READONLY, + DATASTORE, + PERSISTENT + } = item + + const isVolatile = !IMAGE && !IMAGE_ID + const isPersistent = stringToBoolean(PERSISTENT) + const state = !isVolatile && getState({ STATE: IMAGE_STATE }) + const type = isVolatile ? TYPE : getDiskType(item) + const originalSize = +ORIGINAL_SIZE ? prettyBytes(+ORIGINAL_SIZE, 'MB') : '-' + const size = prettyBytes(+SIZE, 'MB') + + return ( + + {`${NAME} - `} + + + ) : ( + + + {`${NAME}: ${IMAGE}`} + {isPersistent && } + + )} + subheader={<> + {Object + .entries({ + [DATASTORE]: DATASTORE, + READONLY: stringToBoolean(READONLY), + PERSISTENT: stringToBoolean(PERSISTENT), + [isVolatile || ORIGINAL_SIZE === SIZE ? size : `${originalSize}/${size}`]: true, + [type]: type + }) + .map(([k, v]) => v ? `${k}` : '') + .filter(Boolean) + .join(' | ') + } + } + action={ + <> + } + handleClick={() => { + handleRemove(NAME) + reorderDisks() + reorderBootAfterRemove(NAME, disks, data, setFormData) + }} + icon={} + /> + , + tooltip: + }} + dialogProps={{ + title: <>{`: ${NAME}`} + }} + options={[{ + form: () => isVolatile + ? VolatileSteps({ hypervisor: HYPERVISOR }, item) + : ImageSteps({ hypervisor: HYPERVISOR }, item), + onSubmit: newValues => handleSave(newValues, NAME) + }]} + /> + + } + /> + ) + })} +
+ + ) +} + +Storage.propTypes = { + data: PropTypes.any, + setFormData: PropTypes.func +} + +Storage.displayName = 'Storage' + +export default Storage diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable/index.js index 5f7501ffca..8fa507edfa 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable/index.js @@ -15,6 +15,7 @@ * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ import PropTypes from 'prop-types' +import { makeStyles } from '@material-ui/core' import { useListForm } from 'client/hooks' import { useVmTemplateApi } from 'client/features/One' @@ -29,7 +30,14 @@ import { T } from 'client/constants' export const STEP_ID = 'template' +const useStyles = makeStyles({ + body: { + gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))' + } +}) + const Content = ({ data, setFormData }) => { + const classes = useStyles() const selectedTemplate = data?.[0] const { getVmTemplate } = useVmTemplateApi() @@ -62,6 +70,7 @@ const Content = ({ data, setFormData }) => { singleSelect onlyGlobalSearch onlyGlobalSelectedRows + classes={classes} initialState={{ selectedRowIds: { [selectedTemplate?.ID]: true } }} onSelectedRowsChange={handleSelectedRows} /> diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/index.js index e5ca99d541..dfe51a8633 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/index.js @@ -17,7 +17,7 @@ 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, deepmerge } from 'client/utils' +import { createSteps } from 'client/utils' const Steps = createSteps( [VmTemplatesTable, BasicConfiguration, ExtraConfiguration], @@ -27,7 +27,7 @@ const Steps = createSteps( [TEMPLATE_ID]: [vmTemplate], [BASIC_ID]: vmTemplate?.TEMPLATE, [EXTRA_ID]: vmTemplate?.TEMPLATE - }, { stripUnknown: true }) + }, { stripUnknown: true, context: { [TEMPLATE_ID]: [vmTemplate] } }) }), transformBeforeSubmit: formData => { const { @@ -37,8 +37,7 @@ const Steps = createSteps( } = formData ?? {} // merge with template disks to get TYPE attribute - const DISK = deepmerge(templateSelected.TEMPLATE?.DISK, restOfConfig?.DISK) - const templateXML = jsonToXml({ ...extraTemplate, ...restOfConfig, DISK }) + const templateXML = jsonToXml({ ...extraTemplate, ...restOfConfig }) const data = { instances, hold, persistent, template: templateXML } const templates = [...new Array(instances)] diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/index.js index 43828f9b54..2b2c302b9c 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/index.js @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ +import CloneForm from 'client/components/Forms/VmTemplate/CloneForm' import InstantiateForm from 'client/components/Forms/VmTemplate/InstantiateForm' export { + CloneForm, InstantiateForm } diff --git a/src/fireedge/src/client/components/Icons/opennebula.js b/src/fireedge/src/client/components/Icons/opennebula.js index 4af4161754..9a5d8e57d8 100644 --- a/src/fireedge/src/client/components/Icons/opennebula.js +++ b/src/fireedge/src/client/components/Icons/opennebula.js @@ -60,26 +60,28 @@ const OpenNebulaLogo = memo(({ width, height, spinner, withText, viewBox, ...pro )} {/* --------------- CLOUD ------------------ */} - - - - - + + + + + + + {withText && ( diff --git a/src/fireedge/src/client/components/Image/index.js b/src/fireedge/src/client/components/Image/index.js index abfab0a17c..b43b061606 100644 --- a/src/fireedge/src/client/components/Image/index.js +++ b/src/fireedge/src/client/components/Image/index.js @@ -47,7 +47,12 @@ const Image = memo(({ src, imageInError, withSources, imgProps }) => { } if (error.fail) { - return + return } return ( diff --git a/src/fireedge/src/client/components/Tables/Enhanced/index.js b/src/fireedge/src/client/components/Tables/Enhanced/index.js index f864cc28fe..2b943a6190 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/index.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/index.js @@ -17,6 +17,7 @@ import { useMemo } from 'react' import PropTypes from 'prop-types' +import clsx from 'clsx' import { InfoEmpty } from 'iconoir-react' import { Box, LinearProgress } from '@material-ui/core' import { @@ -54,9 +55,10 @@ const EnhancedTable = ({ pageSize = 10, RowComponent, showPageCount, - singleSelect = false + singleSelect = false, + classes = {} }) => { - const classes = EnhancedTableStyles() + const styles = EnhancedTableStyles() const isFetching = isLoading && data === undefined @@ -127,8 +129,8 @@ const EnhancedTable = ({ } return ( - -
+ +
{/* TOOLBAR */} {!isFetching && ( +
{page?.length > 0 && ( {isLoading && ( - + )} -
+
{/* FILTERS */} {!isFetching && ( )} -
+
{/* NO DATA MESSAGE */} {!isFetching && page?.length === 0 && ( - + @@ -209,6 +211,10 @@ export const EnhancedTableProps = { fetchMore: PropTypes.func, getRowId: PropTypes.func, initialState: PropTypes.object, + classes: PropTypes.shape({ + root: PropTypes.string, + body: PropTypes.string + }), isLoading: PropTypes.bool, onlyGlobalSearch: PropTypes.bool, onlyGlobalSelectedRows: PropTypes.bool, diff --git a/src/fireedge/src/client/components/Tables/Enhanced/styles.js b/src/fireedge/src/client/components/Tables/Enhanced/styles.js index 7439bbecfd..bc6e057123 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/styles.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/styles.js @@ -58,6 +58,7 @@ export default makeStyles( display: 'grid', gap: '1em', gridTemplateColumns: 'minmax(0, 1fr)', + // gridTemplateRows: 'repeat(auto-fill, 10em)', gridAutoRows: 'max-content', paddingBlock: '0.8em', '& > [role=row]': { @@ -68,9 +69,9 @@ export default makeStyles( fontWeight: typography.fontWeightMedium, fontSize: '1em', border: `1px solid ${palette.divider}`, - borderRadius: 6, + borderRadius: '0.5em', display: 'flex', - gap: 8, + gap: '1em', '&:hover': { backgroundColor: palette.action.hover }, diff --git a/src/fireedge/src/client/components/Tables/VmTemplates/actions.js b/src/fireedge/src/client/components/Tables/VmTemplates/actions.js index d5b9b73cde..c7707f8469 100644 --- a/src/fireedge/src/client/components/Tables/VmTemplates/actions.js +++ b/src/fireedge/src/client/components/Tables/VmTemplates/actions.js @@ -33,6 +33,7 @@ import { import { useAuth } from 'client/features/Auth' import { useVmTemplateApi } from 'client/features/One' +import { CloneForm } from 'client/components/Forms/VmTemplate' 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' @@ -40,7 +41,14 @@ 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 { + getVmTemplate, + getVmTemplates, + lock, + unlock, + clone, + remove + } = useVmTemplateApi() const vmTemplateActions = useMemo(() => createActions({ filters: getResourceView('VM-TEMPLATE')?.actions, @@ -106,8 +114,42 @@ const Actions = () => { label: 'Clone', tooltip: 'Clone', selected: true, - disabled: true, - options: [{ onSubmit: () => undefined }] + dialogProps: { + title: rows => { + const isMultiple = rows?.length > 1 + const { ID, NAME } = rows?.[0]?.original + + return [ + isMultiple ? 'Clone several Templates' : 'Clone Template', + !isMultiple && `#${ID} ${NAME}` + ].filter(Boolean).join(' - ') + } + }, + options: [{ + form: rows => { + const vmTemplates = rows?.map(({ original }) => original) + const stepProps = { isMultiple: vmTemplates.length > 1 } + const initialValues = { name: `Copy of ${vmTemplates?.[0]?.NAME}` } + + return CloneForm(stepProps, initialValues) + }, + onSubmit: async (formData, rows) => { + try { + const { prefix } = formData + + const vmTemplates = rows?.map?.(({ original: { ID, NAME } = {} }) => { + // overwrite all names with prefix+NAME + const formatData = prefix ? { name: `${prefix} ${NAME}` } : {} + + return { id: ID, data: { ...formData, ...formatData } } + }) + + await Promise.all(vmTemplates.map(({ id, data }) => clone(id, data))) + } finally { + await getVmTemplates() + } + } + }] }, { tooltip: 'Change ownership', @@ -157,7 +199,7 @@ const Actions = () => { 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 => lock(id))) await Promise.all(templateIds.map(id => getVmTemplate(id))) } }] @@ -178,7 +220,7 @@ const Actions = () => { options: [{ isConfirmDialog: true, onSubmit: async (_, rows) => { - const templateIds = [...new Array(rows?.map?.(({ original }) => original?.ID))] + const templateIds = rows?.map?.(({ original }) => original?.ID) await Promise.all(templateIds.map(id => unlock(id))) await Promise.all(templateIds.map(id => getVmTemplate(id))) } @@ -199,7 +241,7 @@ const Actions = () => { options: [{ isConfirmDialog: true, onSubmit: async (_, rows) => { - const templateIds = [...new Array(rows?.map?.(({ original }) => original?.ID))] + const templateIds = rows?.map?.(({ original }) => original?.ID) await Promise.all(templateIds.map(id => remove(id))) await getVmTemplates() } diff --git a/src/fireedge/src/client/components/Tables/VmTemplates/columns.js b/src/fireedge/src/client/components/Tables/VmTemplates/columns.js index ce21cd4a1c..f5ca3471eb 100644 --- a/src/fireedge/src/client/components/Tables/VmTemplates/columns.js +++ b/src/fireedge/src/client/components/Tables/VmTemplates/columns.js @@ -16,6 +16,7 @@ /* eslint-disable jsdoc/require-jsdoc */ import { CategoryFilter } from 'client/components/Tables/Enhanced/Utils' import * as Helper from 'client/models/Helper' +import { } from 'client/constants' export default [ { Header: 'ID', accessor: 'ID', sortType: 'number' }, @@ -24,6 +25,11 @@ export default [ { Header: 'Group', accessor: 'GNAME' }, { Header: 'Start Time', accessor: 'REGTIME' }, { Header: 'Locked', accessor: 'LOCK' }, + { + Header: 'Logo', + id: 'LOGO', + accessor: row => row?.TEMPLATE?.LOGO + }, { Header: 'Virtual Router', id: 'VROUTER', diff --git a/src/fireedge/src/client/components/Tables/VmTemplates/row.js b/src/fireedge/src/client/components/Tables/VmTemplates/row.js index db62a56aa4..c61c075a7c 100644 --- a/src/fireedge/src/client/components/Tables/VmTemplates/row.js +++ b/src/fireedge/src/client/components/Tables/VmTemplates/row.js @@ -14,6 +14,7 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ +import { useMemo } from 'react' import PropTypes from 'prop-types' import { User, Group, Lock } from 'iconoir-react' @@ -21,18 +22,38 @@ import { Typography } from '@material-ui/core' import { StatusChip } from 'client/components/Status' import { rowStyles } from 'client/components/Tables/styles' +import Image from 'client/components/Image' import * as Helper from 'client/models/Helper' +import { isExternalURL } from 'client/utils' +import { LOGO_IMAGES_URL } from 'client/constants' const Row = ({ original, value, ...props }) => { const classes = rowStyles() - const { ID, NAME, UNAME, GNAME, REGTIME, LOCK, VROUTER } = value + const { ID, NAME, UNAME, GNAME, REGTIME, LOCK, VROUTER, LOGO = '' } = value + const [logoSource] = useMemo(() => { + const external = isExternalURL(LOGO) + const cleanLogoAttribute = String(LOGO).split('/').at(-1) + const src = external ? LOGO : `${LOGO_IMAGES_URL}/${cleanLogoAttribute}` + + return [src, external] + }, [LOGO]) + + const logo = String(LOGO).split('/').at(-1) const time = Helper.timeFromMilliseconds(+REGTIME) const timeAgo = `registered ${time.toRelative()}` return (
+ {logo && ( +
+ +
+ )}
@@ -44,7 +65,7 @@ const Row = ({ original, value, ...props }) => {
- + {`#${ID} ${timeAgo}`} diff --git a/src/fireedge/src/client/components/Tables/styles.js b/src/fireedge/src/client/components/Tables/styles.js index 107fcc0c09..e14675fce9 100644 --- a/src/fireedge/src/client/components/Tables/styles.js +++ b/src/fireedge/src/client/components/Tables/styles.js @@ -31,9 +31,25 @@ export const rowStyles = makeStyles( flexWrap: 'wrap' } }, + figure: { + flexBasis: '20%', + paddingTop: '12.5%', + overflow: 'hidden', + position: 'relative' + }, + image: { + top: 0, + left: 0, + width: '100%', + height: '100%', + objectFit: 'contain', + position: 'absolute', + userSelect: 'none' + }, main: { flex: 'auto', - overflow: 'hidden' + overflow: 'hidden', + alignSelf: 'center' }, title: { color: palette.text.primary, @@ -52,8 +68,12 @@ export const rowStyles = makeStyles( color: palette.text.secondary, marginTop: 4, display: 'flex', - gap: 8, - wordWrap: 'break-word' + gap: '0.5em', + flexWrap: 'wrap', + wordWrap: 'break-word', + '& > .full-width': { + flexBasis: '100%' + } }, secondary: { width: '25%', diff --git a/src/fireedge/src/client/components/Tabs/Vm/Storage/index.js b/src/fireedge/src/client/components/Tabs/Vm/Storage/index.js index 75a00b13ee..99e756b308 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Storage/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Storage/index.js @@ -38,11 +38,8 @@ const VmStorageTab = ({ tabProps: { actions } = {} }) => { const hypervisor = VirtualMachine.getHypervisor(vm) const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor) - const handleAttachDisk = async ({ image, advanced, configuration }) => { - const imageSelected = image?.[0] - const root = { ...imageSelected, ...advanced, ...configuration } - - const template = Helper.jsonToXml({ DISK: root }) + const handleAttachDisk = async formData => { + const template = Helper.jsonToXml({ DISK: formData }) await attachDisk(vm.ID, template) } diff --git a/src/fireedge/src/client/constants/image.js b/src/fireedge/src/client/constants/image.js index dc0c68980e..f0231e6c45 100644 --- a/src/fireedge/src/client/constants/image.js +++ b/src/fireedge/src/client/constants/image.js @@ -16,31 +16,51 @@ import * as STATES from 'client/constants/states' import COLOR from 'client/constants/color' -/** - * @enum {( - * 'OS'| - * 'CD ROM'| - * 'DATABLOCK'| - * 'KERNEL'| - * 'RAMDISK'| - * 'CONTEXT' - * )} Image type - */ +/** @enum {string} Image type */ +export const IMAGE_TYPES_STR = { + OS: 'OS', + CDROM: 'CDROM', + DATABLOCK: 'DATABLOCK', + KERNEL: 'KERNEL', + RAMDISK: 'RAMDISK', + CONTEXT: 'CONTEXT' +} + +/** @type {IMAGE_TYPES_STR[]} Return the string representation of an Image type */ export const IMAGE_TYPES = [ - 'OS', - 'CD ROM', - 'DATABLOCK', - 'KERNEL', - 'RAMDISK', - 'CONTEXT' + IMAGE_TYPES_STR.OS, + IMAGE_TYPES_STR.CDROM, + IMAGE_TYPES_STR.DATABLOCK, + IMAGE_TYPES_STR.KERNEL, + IMAGE_TYPES_STR.RAMDISK, + IMAGE_TYPES_STR.CONTEXT ] -/** @enum {('FILE'|'CD ROM'|'BLOCK'|'RBD')} Disk type */ +/** @enum {string} Disk type */ +export const DISK_TYPES_STR = { + FILE: 'FILE', + CDROM: 'CDROM', + BLOCK: 'BLOCK', + RBD: 'RBD', + RBD_CDROM: 'RBD CDROM', + GLUSTER: 'GLUSTER', + GLUSTER_CDROM: 'GLUSTER CDROM', + SHEEPDOG: 'SHEEPDOG', + SHEEPDOG_CDROM: 'SHEEPDOG CDROM', + ISCSI: 'ISCII' +} +/** @enum {DISK_TYPES_STR[]} Return the string representation of a Disk type */ export const DISK_TYPES = [ - 'FILE', - 'CD ROM', - 'BLOCK', - 'RBD' + DISK_TYPES_STR.FILE, + DISK_TYPES_STR.CDROM, + DISK_TYPES_STR.BLOCK, + DISK_TYPES_STR.RBD, + DISK_TYPES_STR.RBD_CDROM, + DISK_TYPES_STR.GLUSTER, + DISK_TYPES_STR.GLUSTER_CDROM, + DISK_TYPES_STR.SHEEPDOG, + DISK_TYPES_STR.SHEEPDOG_CDROM, + DISK_TYPES_STR.ISCSI ] /** @type {STATES.StateInfo[]} Image states */ diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js index bd6c3c7a2e..2ec55b8c17 100644 --- a/src/fireedge/src/client/constants/index.js +++ b/src/fireedge/src/client/constants/index.js @@ -31,6 +31,7 @@ export const WEBSOCKET_URL = `${APP_URL}/websockets` export const STATIC_FILES_URL = `${APP_URL}/client/assets` export const IMAGES_URL = `${STATIC_FILES_URL}/images` +export const LOGO_IMAGES_URL = `${IMAGES_URL}/logos` export const PROVIDER_IMAGES_URL = `${IMAGES_URL}/providers` export const PROVISION_IMAGES_URL = `${IMAGES_URL}/provisions` export const DEFAULT_IMAGE = `${IMAGES_URL}/default.webp` diff --git a/src/fireedge/src/client/features/One/utils.js b/src/fireedge/src/client/features/One/utils.js index d8b34bb417..5c878d6ae3 100644 --- a/src/fireedge/src/client/features/One/utils.js +++ b/src/fireedge/src/client/features/One/utils.js @@ -61,7 +61,7 @@ export const updateResourceList = (currentList, value) => { // update if exists in current list, if not add it to list const updatedList = currentItem ? currentList?.map(item => item?.ID === id ? value : item) - : [value, currentList] + : [value, ...currentList] return updatedList } diff --git a/src/fireedge/src/client/features/One/vmTemplate/actions.js b/src/fireedge/src/client/features/One/vmTemplate/actions.js index 333d153005..2813e4cf48 100644 --- a/src/fireedge/src/client/features/One/vmTemplate/actions.js +++ b/src/fireedge/src/client/features/One/vmTemplate/actions.js @@ -40,4 +40,4 @@ export const changePermissions = createAction(`${TEMPLATE}/chmod`, vmTemplateSer 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) +export const unlock = createAction(`${TEMPLATE}/unlock`, vmTemplateService.unlock) diff --git a/src/fireedge/src/client/hooks/useListForm.js b/src/fireedge/src/client/hooks/useListForm.js index 42973b503e..f09b78d6d3 100644 --- a/src/fireedge/src/client/hooks/useListForm.js +++ b/src/fireedge/src/client/hooks/useListForm.js @@ -61,7 +61,7 @@ import { set } from 'client/utils' * @property {SimpleCallback} handleSelect - Add an item to data form list * @property {SimpleCallback} handleClone - Clones an item and change two attributes * @property {SimpleCallback} handleEdit - Find the element by id and set value to editing state - * @property {function(newValues, id)} handleSave - Saves the data from editing state + * @property {SaveCallback} handleSave - Saves the data from editing state */ // ---------------------------------------------------------- @@ -182,7 +182,7 @@ const useListForm = ({ const index = EXISTS_INDEX(itemIndex) ? itemIndex : list.length const newList = Object.assign([], [...list], - { [index]: getItemId(values) ? values : addItemId(values, id, itemIndex) } + { [index]: getItemId(values) ? values : addItemId(values, id, index) } ) handleSetList(newList) diff --git a/src/fireedge/src/client/models/Image.js b/src/fireedge/src/client/models/Image.js index fa123f79ab..a20903cd8c 100644 --- a/src/fireedge/src/client/models/Image.js +++ b/src/fireedge/src/client/models/Image.js @@ -22,7 +22,8 @@ import { IMAGE_TYPES, DISK_TYPES, IMAGE_STATES, StateInfo } from 'client/constan * @param {number|string} image.TYPE - Type numeric code * @returns {IMAGE_TYPES} - Image type */ -export const getType = ({ TYPE } = {}) => IMAGE_TYPES[+TYPE] +export const getType = ({ TYPE } = {}) => + isNaN(+TYPE) ? TYPE : IMAGE_TYPES[+TYPE] /** * Returns the image state. @@ -40,4 +41,5 @@ export const getState = ({ STATE } = {}) => IMAGE_STATES[+STATE] * @param {number|string} image.DISK_TYPE - Disk type numeric code * @returns {DISK_TYPES} - Disk type */ -export const getDiskType = ({ DISK_TYPE } = {}) => DISK_TYPES[+DISK_TYPE] +export const getDiskType = ({ DISK_TYPE } = {}) => + isNaN(+DISK_TYPE) ? DISK_TYPE : DISK_TYPES[+DISK_TYPE] diff --git a/src/fireedge/src/client/utils/schema.js b/src/fireedge/src/client/utils/schema.js index 905e63c4da..faab65678f 100644 --- a/src/fireedge/src/client/utils/schema.js +++ b/src/fireedge/src/client/utils/schema.js @@ -111,7 +111,7 @@ import { INPUT_TYPES } from 'client/constants' /** * @typedef {object} StepsForm * @property {Step[]} steps - Steps - * @property {BaseSchema} resolver - Schema + * @property {BaseSchema|function():BaseSchema} resolver - Schema * @property {object} defaultValues - Default values */ @@ -347,8 +347,13 @@ export const createForm = (schema, fields, extraParams = {}) => const schemaCallback = typeof schema === 'function' ? schema(props) : schema const fieldsCallback = typeof fields === 'function' ? fields(props) : fields + const defaultTransformInitialValue = (values, schema) => + schema.cast(values, { stripUnknown: true }) + + const { transformInitialValue = defaultTransformInitialValue } = extraParams + const defaultValues = initialValues - ? schemaCallback.cast(initialValues, { stripUnknown: true }) + ? transformInitialValue(initialValues, schemaCallback) : schemaCallback.default() return {