diff --git a/src/fireedge/src/client/apps/sunstone/routes.js b/src/fireedge/src/client/apps/sunstone/routes.js index a395db8f80..9b7606ebdb 100644 --- a/src/fireedge/src/client/apps/sunstone/routes.js +++ b/src/fireedge/src/client/apps/sunstone/routes.js @@ -80,6 +80,7 @@ export const ENDPOINTS = [ * * @typedef {object} ResourceView - Resource view file selected in redux (auth-reducer) * @property {string} resource_name - Resource view name + * @property {object} features - Features about the resources * @property {object} actions - Bulk actions, including dialogs * Which buttons are visible to operate over the resources * @property {object} filters - List of criteria to filter the resources diff --git a/src/fireedge/src/client/components/FormControl/AutocompleteController.js b/src/fireedge/src/client/components/FormControl/AutocompleteController.js index a02049ab93..98060013e6 100644 --- a/src/fireedge/src/client/components/FormControl/AutocompleteController.js +++ b/src/fireedge/src/client/components/FormControl/AutocompleteController.js @@ -52,7 +52,9 @@ const AutocompleteController = memo( onChange={(_, newValue) => { const newValueToChange = multiple ? newValue?.map((value) => - typeof value === 'string' ? value : { text: value, value } + ['string', 'number'].includes(typeof value) + ? value + : { text: value, value } ) : newValue?.value diff --git a/src/fireedge/src/client/components/FormControl/FileController.js b/src/fireedge/src/client/components/FormControl/FileController.js index 71bb0a7721..a7c697e5c3 100644 --- a/src/fireedge/src/client/components/FormControl/FileController.js +++ b/src/fireedge/src/client/components/FormControl/FileController.js @@ -18,7 +18,7 @@ import PropTypes from 'prop-types' import { styled, FormControl, FormHelperText } from '@mui/material' import { Check as CheckIcon, Page as FileIcon } from 'iconoir-react' -import { useController } from 'react-hook-form' +import { useFormContext, useController } from 'react-hook-form' import { ErrorHelper, @@ -50,9 +50,8 @@ const FileController = memo( transform, fieldProps = {}, readOnly = false, - formContext = {}, }) => { - const { setValue, setError, clearErrors, watch } = formContext + const { setValue, setError, clearErrors, watch } = useFormContext() const { field: { ref, value, onChange, ...inputProps }, @@ -166,13 +165,6 @@ FileController.propTypes = { transform: PropTypes.func, fieldProps: PropTypes.object, readOnly: PropTypes.bool, - formContext: PropTypes.shape({ - setValue: PropTypes.func, - setError: PropTypes.func, - clearErrors: PropTypes.func, - watch: PropTypes.func, - register: PropTypes.func, - }), } FileController.displayName = 'FileController' diff --git a/src/fireedge/src/client/components/FormControl/SelectController.js b/src/fireedge/src/client/components/FormControl/SelectController.js index 71cf579773..890f68a9ef 100644 --- a/src/fireedge/src/client/components/FormControl/SelectController.js +++ b/src/fireedge/src/client/components/FormControl/SelectController.js @@ -17,11 +17,11 @@ import { memo, useMemo, useEffect } from 'react' import PropTypes from 'prop-types' import { TextField } from '@mui/material' -import { useController } from 'react-hook-form' +import { useController, useWatch } from 'react-hook-form' import { ErrorHelper, Tooltip } from 'client/components/FormControl' import { Tr, labelCanBeTranslated } from 'client/components/HOC' -import { generateKey } from 'client/utils' +import { generateKey, findClosestValue } from 'client/utils' const SelectController = memo( ({ @@ -33,9 +33,17 @@ const SelectController = memo( values = [], renderValue, tooltip, + watcher, + dependencies, fieldProps = {}, readOnly = false, }) => { + const watch = useWatch({ + name: dependencies, + disabled: dependencies == null, + defaultValue: Array.isArray(dependencies) ? [] : undefined, + }) + const firstValue = values?.[0]?.value ?? '' const defaultValue = multiple ? [firstValue] : firstValue @@ -46,7 +54,7 @@ const SelectController = memo( const needShrink = useMemo( () => - multiple || values.find((v) => v.value === optionSelected)?.text !== '', + multiple || values.find((o) => o.value === optionSelected)?.text !== '', [optionSelected] ) @@ -54,15 +62,29 @@ const SelectController = memo( if (!optionSelected && !optionSelected.length) return if (multiple) { - const exists = values.some((v) => optionSelected.includes(v.value)) + const exists = values.some((o) => optionSelected.includes(o.value)) !exists && onChange([firstValue]) } else { - const exists = values.some((v) => `${v.value}` === `${optionSelected}`) + const exists = values.some((o) => `${o.value}` === `${optionSelected}`) !exists && onChange(firstValue) } }, [multiple]) + useEffect(() => { + if (!watcher || !dependencies) return + if (!watch) return onChange(defaultValue) + + const watcherValue = watcher(watch) + const optionValues = values.map((o) => o.value) + + const ensuredWatcherValue = isNaN(watcherValue) + ? optionValues.find((o) => `${o}` === `${watcherValue}`) + : findClosestValue(watcherValue, optionValues) + + onChange(ensuredWatcherValue ?? defaultValue) + }, [watch, watcher, dependencies]) + return ( ), @@ -115,7 +137,8 @@ const SelectController = memo( prev.values.length === next.values.length && prev.label === next.label && prev.tooltip === next.tooltip && - prev.multiple === next.multiple + prev.multiple === next.multiple && + prev.readOnly === next.readOnly ) SelectController.propTypes = { @@ -127,6 +150,11 @@ SelectController.propTypes = { multiple: PropTypes.bool, values: PropTypes.arrayOf(PropTypes.object).isRequired, renderValue: PropTypes.func, + watcher: PropTypes.func, + dependencies: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string), + ]), fieldProps: PropTypes.object, readOnly: PropTypes.bool, } diff --git a/src/fireedge/src/client/components/FormControl/SliderController.js b/src/fireedge/src/client/components/FormControl/SliderController.js index 6c820ba035..1477729165 100644 --- a/src/fireedge/src/client/components/FormControl/SliderController.js +++ b/src/fireedge/src/client/components/FormControl/SliderController.js @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { memo } from 'react' +import { memo, useEffect, useCallback } from 'react' import PropTypes from 'prop-types' import { TextField, Slider, FormHelperText, Stack } from '@mui/material' -import { useController } from 'react-hook-form' +import { useController, useWatch } from 'react-hook-form' import { ErrorHelper, Tooltip } from 'client/components/FormControl' import { Tr, labelCanBeTranslated } from 'client/components/HOC' @@ -30,9 +30,17 @@ const SliderController = memo( name = '', label = '', tooltip, + watcher, + dependencies, fieldProps = {}, readOnly = false, }) => { + const watch = useWatch({ + name: dependencies, + disabled: dependencies == null, + defaultValue: Array.isArray(dependencies) ? [] : undefined, + }) + const { min, max, step } = fieldProps ?? {} const { @@ -40,12 +48,33 @@ const SliderController = memo( fieldState: { error }, } = useController({ name, control }) + const handleEnsuredChange = useCallback( + (newValue) => { + if (min && newValue < min) return onChange(min) + if (max && newValue > max) return onChange(max) + }, + [onChange, min, max] + ) + + useEffect(() => { + if (!watcher || !dependencies || !watch) return + + const watcherValue = watcher(watch) + watcherValue !== undefined && handleEnsuredChange(watcherValue) + }, [watch, watcher, dependencies]) + const sliderId = `${cy}-slider` const inputId = `${cy}-input` return ( <> - + - onChange(!evt.target.value ? '0' : Number(evt.target.value)) + handleEnsuredChange( + !evt.target.value ? '0' : Number(evt.target.value) + ) } - onBlur={() => { - if (min && value < min) { - onChange(min) - } else if (max && value > max) { - onChange(max) - } - }} + onBlur={() => handleEnsuredChange(value)} /> {Boolean(error) && ( @@ -102,6 +127,11 @@ SliderController.propTypes = { name: PropTypes.string.isRequired, label: PropTypes.any, tooltip: PropTypes.any, + watcher: PropTypes.func, + dependencies: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string), + ]), fieldProps: PropTypes.object, readOnly: PropTypes.bool, } diff --git a/src/fireedge/src/client/components/FormControl/TableController.js b/src/fireedge/src/client/components/FormControl/TableController.js index 20767d6733..8685d2f2bd 100644 --- a/src/fireedge/src/client/components/FormControl/TableController.js +++ b/src/fireedge/src/client/components/FormControl/TableController.js @@ -15,7 +15,7 @@ * ------------------------------------------------------------------------- */ import { memo, useEffect, useState } from 'react' import PropTypes from 'prop-types' -import { useController } from 'react-hook-form' +import { useFormContext, useController } from 'react-hook-form' import Legend from 'client/components/Forms/Legend' import { ErrorHelper } from 'client/components/FormControl' @@ -42,11 +42,10 @@ const TableController = memo( Table, singleSelect = true, getRowId = defaultGetRowId, - formContext = {}, readOnly = false, fieldProps: { initialState, ...fieldProps } = {}, }) => { - const { clearErrors } = formContext + const { clearErrors } = useFormContext() const { field: { value, onChange }, @@ -107,13 +106,6 @@ TableController.propTypes = { tooltip: PropTypes.any, fieldProps: PropTypes.object, readOnly: PropTypes.bool, - formContext: PropTypes.shape({ - setValue: PropTypes.func, - setError: PropTypes.func, - clearErrors: PropTypes.func, - watch: PropTypes.func, - register: PropTypes.func, - }), } TableController.displayName = 'TableController' diff --git a/src/fireedge/src/client/components/FormControl/TextController.js b/src/fireedge/src/client/components/FormControl/TextController.js index 640f9567a3..db2672ecd2 100644 --- a/src/fireedge/src/client/components/FormControl/TextController.js +++ b/src/fireedge/src/client/components/FormControl/TextController.js @@ -37,13 +37,11 @@ const TextController = memo( fieldProps = {}, readOnly = false, }) => { - const watch = - dependencies && - useWatch({ - control, - name: dependencies, - disabled: dependencies === null, - }) + const watch = useWatch({ + name: dependencies, + disabled: dependencies == null, + defaultValue: Array.isArray(dependencies) ? [] : undefined, + }) const { field: { ref, value = '', onChange, ...inputProps }, @@ -51,11 +49,11 @@ const TextController = memo( } = useController({ name, control }) useEffect(() => { - if (watch && watcher) { - const watcherValue = watcher(watch) - watcherValue && onChange(watcherValue) - } - }, [watch]) + if (!watcher || !dependencies || !watch) return + + const watcherValue = watcher(watch) + watcherValue !== undefined && onChange(watcherValue) + }, [watch, watcher, dependencies]) return ( { - const formContext = useFormContext() - const { control, watch } = formContext - const { sx: sxRoot, ...restOfRootProps } = rootProps ?? {} const RootWrapper = useMemo( @@ -88,25 +99,14 @@ const FormWithSchema = ({ const getFields = useMemo( () => (typeof fields === 'function' ? fields() : fields), - [fields?.length] + [fields] ) if (!getFields || getFields?.length === 0) return null - const addIdToName = useCallback( - (name) => - name.startsWith('$') - ? name.slice(1) // removes character '$' and returns - : id - ? `${id}.${name}` // concat form ID if exists - : name, - [id] - ) - return ( @@ -122,56 +122,9 @@ const FormWithSchema = ({ )} - {getFields?.map?.(({ dependOf, ...attributes }) => { - let valueOfDependField = null - let nameOfDependField = null - - if (dependOf) { - nameOfDependField = Array.isArray(dependOf) - ? dependOf.map(addIdToName) - : addIdToName(dependOf) - - valueOfDependField = watch(nameOfDependField) - } - - const { name, type, htmlType, grid, ...fieldProps } = - Object.entries(attributes).reduce((field, attribute) => { - const [key, value] = attribute - const isNotDependAttribute = NOT_DEPEND_ATTRIBUTES.includes(key) - - const finalValue = - typeof value === 'function' && - !isNotDependAttribute && - !isValidElement(value()) - ? value(valueOfDependField, formContext) - : value - - return { ...field, [key]: finalValue } - }, {}) - - const dataCy = `${cy}-${name}`.replaceAll('.', '-') - const inputName = addIdToName(name) - - const isHidden = htmlType === INPUT_TYPES.HIDDEN - - if (isHidden) return null - - return ( - INPUT_CONTROLLER[type] && ( - - {createElement(INPUT_CONTROLLER[type], { - control, - cy: dataCy, - formContext, - dependencies: nameOfDependField, - name: inputName, - type: htmlType === false ? undefined : htmlType, - ...fieldProps, - })} - - ) - ) - })} + {getFields?.map?.((field) => ( + + ))} @@ -189,7 +142,89 @@ FormWithSchema.propTypes = { legend: PropTypes.any, legendTooltip: PropTypes.string, rootProps: PropTypes.object, - className: PropTypes.string, } +const FieldComponent = memo(({ id, cy, dependOf, ...attributes }) => { + const formContext = useFormContext() + + const addIdToName = useCallback( + (n) => { + // removes character '$' and returns + if (n.startsWith('$')) return n.slice(1) + + // concat form ID if exists + return id ? `${id}.${n}` : n + }, + [id] + ) + + const nameOfDependField = useMemo(() => { + if (!dependOf) return null + + return Array.isArray(dependOf) + ? dependOf.map(addIdToName) + : addIdToName(dependOf) + }, [dependOf, addIdToName]) + + const valueOfDependField = useWatch({ + name: nameOfDependField, + disabled: dependOf === undefined, + defaultValue: Array.isArray(dependOf) ? [] : undefined, + }) + + /* const valueOfDependField = useMemo(() => { + if (!dependOf) return null + + return watch(nameOfDependField) + }, [dependOf, watch, nameOfDependField]) */ + + const { name, type, htmlType, grid, ...fieldProps } = Object.entries( + attributes + ).reduce((field, attribute) => { + const [key, value] = attribute + const isNotDependAttribute = NOT_DEPEND_ATTRIBUTES.includes(key) + + const finalValue = + typeof value === 'function' && + !isNotDependAttribute && + !isValidElement(value()) + ? value(valueOfDependField, formContext) + : value + + return { ...field, [key]: finalValue } + }, {}) + + const dataCy = useMemo(() => `${cy}-${name ?? ''}`.replaceAll('.', '-'), [cy]) + const inputName = useMemo(() => addIdToName(name), [addIdToName, name]) + const isHidden = useMemo(() => htmlType === INPUT_TYPES.HIDDEN, [htmlType]) + + if (isHidden) return null + + return ( + INPUT_CONTROLLER[type] && ( + + {createElement(INPUT_CONTROLLER[type], { + control: formContext.control, + cy: dataCy, + dependencies: nameOfDependField, + name: inputName, + type: htmlType === false ? undefined : htmlType, + ...fieldProps, + })} + + ) + ) +}) + +FieldComponent.propTypes = { + id: PropTypes.string, + cy: PropTypes.string, + dependOf: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string), + ]), +} + +FieldComponent.displayName = 'FieldComponent' + export default FormWithSchema diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/capacitySchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/capacitySchema.js index ca9708ffb1..7fce6e3eab 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/capacitySchema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/capacitySchema.js @@ -24,7 +24,7 @@ import { import { Translate } from 'client/components/HOC' import { formatNumberByCurrency } from 'client/models/Helper' import { Field } from 'client/utils' -import { T, HYPERVISORS } from 'client/constants' +import { T, HYPERVISORS, VmTemplateFeatures } from 'client/constants' const commonValidation = number() .positive() @@ -186,5 +186,9 @@ export const DISK_COST = generateCostCapacityInput({ }, }) -/** @type {Field[]} List of showback fields */ -export const SHOWBACK_FIELDS = [MEMORY_COST, CPU_COST, DISK_COST] +/** + * @param {VmTemplateFeatures} features - Features of the template + * @returns {Field[]} List of showback fields + */ +export const SHOWBACK_FIELDS = (features) => + [MEMORY_COST, !features?.hide_cpu && CPU_COST, DISK_COST].filter(Boolean) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/index.js index 8cf5818e87..5617516e71 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/index.js @@ -38,11 +38,12 @@ const Content = ({ isUpdate }) => { const sections = useMemo(() => { const resource = RESOURCE_NAMES.VM_TEMPLATE - const dialog = getResourceView(resource)?.dialogs?.create_dialog + const { features, dialogs } = getResourceView(resource) + const dialog = dialogs?.create_dialog const sectionsAvailable = getSectionsAvailable(dialog, hypervisor) return ( - SECTIONS(hypervisor, isUpdate) + SECTIONS(hypervisor, isUpdate, features) .filter( ({ id, required }) => required || sectionsAvailable.includes(id) ) @@ -57,8 +58,8 @@ const Content = ({ isUpdate }) => { ))} diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/schema.js index 1a4e7d0d9d..f5c5258962 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/schema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/schema.js @@ -35,62 +35,64 @@ import { filterFieldsByHypervisor, getObjectSchemaFromFields, } from 'client/utils' -import { T, HYPERVISORS } from 'client/constants' +import { T, HYPERVISORS, VmTemplateFeatures } from 'client/constants' /** * @param {HYPERVISORS} [hypervisor] - Template hypervisor * @param {boolean} [isUpdate] - If `true`, the form is being updated + * @param {VmTemplateFeatures} [features] - Features * @returns {Section[]} Fields */ -const SECTIONS = (hypervisor, isUpdate) => [ - { - id: 'information', - legend: T.Information, - required: true, - fields: INFORMATION_FIELDS(isUpdate), - }, - { - id: 'hypervisor', - legend: T.Hypervisor, - required: true, - fields: [HYPERVISOR_FIELD, VROUTER_FIELD], - }, - { - id: 'capacity', - legend: T.Memory, - fields: filterFieldsByHypervisor(MEMORY_FIELDS, hypervisor), - }, - { - id: 'capacity', - legend: T.PhysicalCpu, - fields: filterFieldsByHypervisor(CPU_FIELDS, hypervisor), - }, - { - id: 'capacity', - legend: T.VirtualCpu, - fields: filterFieldsByHypervisor(VCPU_FIELDS, hypervisor), - }, - { - id: 'showback', - legend: T.Cost, - fields: filterFieldsByHypervisor(SHOWBACK_FIELDS, hypervisor), - }, - { - id: 'ownership', - legend: T.Ownership, - fields: filterFieldsByHypervisor(OWNERSHIP_FIELDS, hypervisor), - }, - { - id: 'vm_group', - legend: T.VMGroup, - fields: filterFieldsByHypervisor(VM_GROUP_FIELDS, hypervisor), - }, - { - id: 'vcenter', - legend: T.vCenterDeployment, - fields: filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor), - }, -] +const SECTIONS = (hypervisor, isUpdate, features) => + [ + { + id: 'information', + legend: T.Information, + required: true, + fields: INFORMATION_FIELDS(isUpdate), + }, + { + id: 'hypervisor', + legend: T.Hypervisor, + required: true, + fields: [HYPERVISOR_FIELD, VROUTER_FIELD], + }, + { + id: 'capacity', + legend: T.Memory, + fields: filterFieldsByHypervisor(MEMORY_FIELDS, hypervisor), + }, + !features?.hide_cpu && { + id: 'capacity', + legend: T.PhysicalCpu, + fields: filterFieldsByHypervisor(CPU_FIELDS, hypervisor), + }, + { + id: 'capacity', + legend: T.VirtualCpu, + fields: filterFieldsByHypervisor(VCPU_FIELDS, hypervisor), + }, + { + id: 'showback', + legend: T.Cost, + fields: filterFieldsByHypervisor(SHOWBACK_FIELDS(features), hypervisor), + }, + { + id: 'ownership', + legend: T.Ownership, + fields: filterFieldsByHypervisor(OWNERSHIP_FIELDS, hypervisor), + }, + { + id: 'vm_group', + legend: T.VMGroup, + fields: filterFieldsByHypervisor(VM_GROUP_FIELDS, hypervisor), + }, + { + id: 'vcenter', + legend: T.vCenterDeployment, + fields: filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor), + }, + ].filter(Boolean) /** * @param {HYPERVISORS} [hypervisor] - Template hypervisor diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/capacitySchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/capacitySchema.js index 26b59c8a05..44bd86d6b9 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/capacitySchema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/capacitySchema.js @@ -14,6 +14,8 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ import { NumberSchema } from 'yup' + +import { scaleVcpuByCpuFactor } from 'client/models/VirtualMachine' import { getUserInputParams } from 'client/models/Helper' import { Field, @@ -21,7 +23,13 @@ import { prettyBytes, isDivisibleBy, } from 'client/utils' -import { T, HYPERVISORS, USER_INPUT_TYPES, VmTemplate } from 'client/constants' +import { + T, + HYPERVISORS, + USER_INPUT_TYPES, + VmTemplate, + VmTemplateFeatures, +} from 'client/constants' const { number, numberFloat, range, rangeFloat } = USER_INPUT_TYPES @@ -35,9 +43,13 @@ const valueLabelFormat = (value) => prettyBytes(value, 'MB') /** * @param {VmTemplate} [vmTemplate] - VM Template + * @param {VmTemplateFeatures} [features] - Features * @returns {Field[]} Basic configuration fields */ -export const FIELDS = (vmTemplate) => { +export const FIELDS = ( + vmTemplate, + { hide_cpu: hideCpu, cpu_factor: cpuFactor } = {} +) => { const { HYPERVISOR, USER_INPUTS = {}, @@ -52,18 +64,21 @@ export const FIELDS = (vmTemplate) => { VCPU: vcpuInput = `O|${number}|| |${VCPU}`, } = USER_INPUTS - return [ + const fields = [ { name: 'MEMORY', ...getUserInputParams(memoryInput) }, - { name: 'CPU', ...getUserInputParams(cpuInput) }, + !hideCpu && { name: 'CPU', ...getUserInputParams(cpuInput) }, { name: 'VCPU', ...getUserInputParams(vcpuInput) }, - ].map(({ name, options, ...userInput }) => { + ].filter(Boolean) + + return fields.map(({ name, options, ...userInput }) => { const isMemory = name === 'MEMORY' + const isCPU = name === 'CPU' const isVCenter = HYPERVISOR === HYPERVISORS.vcenter const divisibleBy4 = isVCenter && isMemory const isRange = [range, rangeFloat].includes(userInput.type) // set default type to number - userInput.type ??= name === 'CPU' ? numberFloat : number + userInput.type ??= isCPU ? numberFloat : number const ensuredOptions = divisibleBy4 ? options?.filter((value) => isDivisibleBy(+value, 4)) @@ -85,6 +100,12 @@ export const FIELDS = (vmTemplate) => { schemaUi.fieldProps = { ...schemaUi.fieldProps, step: 4 } } + if (cpuFactor && isCPU) { + schemaUi.readOnly = true + schemaUi.dependOf = 'VCPU' + schemaUi.watcher = (vcpu) => scaleVcpuByCpuFactor(vcpu, cpuFactor) + } + return { ...TRANSLATES[name], ...schemaUi, grid: { md: 12 } } }) } 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 087939d45e..9c7dc6c82e 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 @@ -36,11 +36,12 @@ const Content = ({ vmTemplate }) => { const sections = useMemo(() => { const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR const resource = RESOURCE_NAMES.VM_TEMPLATE - const dialog = getResourceView(resource)?.dialogs?.instantiate_dialog + const { features, dialogs } = getResourceView(resource) + const dialog = dialogs?.instantiate_dialog const sectionsAvailable = getSectionsAvailable(dialog, hypervisor) - return SECTIONS(vmTemplate).filter(({ id }) => - sectionsAvailable.includes(id) + return SECTIONS(vmTemplate, features).filter( + ({ id, required }) => required || sectionsAvailable.includes(id) ) }, [view]) @@ -49,8 +50,8 @@ const Content = ({ vmTemplate }) => { {sections.map(({ id, legend, fields }) => ( { +const SECTIONS = (vmTemplate, features) => { const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR return [ @@ -48,7 +49,10 @@ const SECTIONS = (vmTemplate) => { { id: 'capacity', legend: T.Capacity, - fields: filterFieldsByHypervisor(CAPACITY_FIELDS(vmTemplate), hypervisor), + fields: filterFieldsByHypervisor( + CAPACITY_FIELDS(vmTemplate, features), + hypervisor + ), }, { id: 'ownership', @@ -70,17 +74,20 @@ const SECTIONS = (vmTemplate) => { /** * @param {VmTemplate} [vmTemplate] - VM Template + * @param {boolean} [hideCpu] - If `true`, the CPU fields is hidden * @returns {Field[]} Basic configuration fields */ -const FIELDS = (vmTemplate) => - SECTIONS(vmTemplate) +const FIELDS = (vmTemplate, hideCpu) => + SECTIONS(vmTemplate, hideCpu) .map(({ fields }) => fields) .flat() /** * @param {VmTemplate} [vmTemplate] - VM Template + * @param {boolean} [hideCpu] - If `true`, the CPU fields is hidden * @returns {BaseSchema} Step schema */ -const SCHEMA = (vmTemplate) => getObjectSchemaFromFields(FIELDS(vmTemplate)) +const SCHEMA = (vmTemplate, hideCpu) => + getObjectSchemaFromFields(FIELDS(vmTemplate, hideCpu)) export { SECTIONS, FIELDS, SCHEMA } diff --git a/src/fireedge/src/client/constants/vmTemplate.js b/src/fireedge/src/client/constants/vmTemplate.js index e722ef71ce..b15cabb49f 100644 --- a/src/fireedge/src/client/constants/vmTemplate.js +++ b/src/fireedge/src/client/constants/vmTemplate.js @@ -18,7 +18,7 @@ import * as ACTIONS from 'client/constants/actions' import { Permissions, LockInfo } from 'client/constants/common' /** - * @typedef {object} VmTemplate + * @typedef VmTemplate * @property {string|number} ID - Id * @property {string} NAME - Name * @property {string|number} UID - User id @@ -35,6 +35,15 @@ import { Permissions, LockInfo } from 'client/constants/common' * @property {string} [TEMPLATE.VCENTER_TEMPLATE_REF] - vCenter information */ +/** + * @typedef VmTemplateFeatures + * @property {boolean} hide_cpu - If `true`, the CPU fields is hidden + * @property {false|number} cpu_factor - Scales CPU by VCPU + * - ``1``: Set it to 1 to tie CPU and vCPU + * - ``{number}``: CPU = cpu_factor * VCPU + * - ``{false}``: False to not scale the CPU + */ + export const VM_TEMPLATE_ACTIONS = { REFRESH: ACTIONS.REFRESH, CREATE_DIALOG: 'create_dialog', diff --git a/src/fireedge/src/client/models/VirtualMachine.js b/src/fireedge/src/client/models/VirtualMachine.js index ea39d61b41..29d46dfb46 100644 --- a/src/fireedge/src/client/models/VirtualMachine.js +++ b/src/fireedge/src/client/models/VirtualMachine.js @@ -330,3 +330,18 @@ export const nicsIncludesTheConnectionType = (vm, type) => { return getNics(vm).some((nic) => stringToBoolean(nic[ensuredConnection])) } + +/** + * Scales the VCPU value by CPU factor to get the real CPU value. + * + * @param {number} [vcpu] - VCPU value + * @param {number} cpuFactor - Factor CPU + * @returns {number|undefined} Real CPU value + */ +export const scaleVcpuByCpuFactor = (vcpu, cpuFactor) => { + if (!cpuFactor || isNaN(+vcpu) || +vcpu === 0) return + if (+cpuFactor === 1) return vcpu + + // round 2 decimals to avoid floating point errors + return Math.round(+vcpu * +cpuFactor * 100) / 100 +} diff --git a/src/fireedge/src/client/utils/helpers.js b/src/fireedge/src/client/utils/helpers.js index 97d7692129..f5c02594d9 100644 --- a/src/fireedge/src/client/utils/helpers.js +++ b/src/fireedge/src/client/utils/helpers.js @@ -420,47 +420,6 @@ export const cleanEmpty = (variable) => ? cleanEmptyArray(variable) : cleanEmptyObject(variable) -/** - * Check if value is in base64. - * - * @param {string} stringToValidate - String to check - * @param {object} options - Options - * @param {boolean} options.exact - Only match and exact string - * @returns {boolean} Returns `true` if string is a base64 - */ -export const isBase64 = (stringToValidate, options = {}) => { - if (stringToValidate === '') return false - - const { exact = true } = options - - const BASE64_REG = - /(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)/g - const EXACT_BASE64_REG = - /(?:^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$)/ - - const regex = exact ? EXACT_BASE64_REG : BASE64_REG - - return regex.test(stringToValidate) -} - -/** - * Check if value is divisible by another number. - * - * @param {string|number} number - Value to check - * @param {string|number} divisor - Divisor number - * @returns {boolean} Returns `true` if value is divisible by another - */ -export const isDivisibleBy = (number, divisor) => !(number % divisor) - -/** - * Returns factors of a number. - * - * @param {number} value - Number - * @returns {number[]} Returns list of numbers - */ -export const getFactorsOfNumber = (value) => - [...Array(+value + 1).keys()].filter((idx) => value % idx === 0) - /** * Returns an array with the separator interspersed between elements of the given array. * diff --git a/src/fireedge/src/client/utils/index.js b/src/fireedge/src/client/utils/index.js index cd7145e852..9c3a6eb358 100644 --- a/src/fireedge/src/client/utils/index.js +++ b/src/fireedge/src/client/utils/index.js @@ -22,4 +22,5 @@ export * from 'client/utils/rest' export * from 'client/utils/schema' export * from 'client/utils/storage' export * from 'client/utils/string' +export * from 'client/utils/number' export * from 'client/utils/translation' diff --git a/src/fireedge/src/client/utils/number.js b/src/fireedge/src/client/utils/number.js new file mode 100644 index 0000000000..a76c39068c --- /dev/null +++ b/src/fireedge/src/client/utils/number.js @@ -0,0 +1,75 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, 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. * + * ------------------------------------------------------------------------- */ + +/** + * Check if value is in base64. + * + * @param {string} stringToValidate - String to check + * @param {object} options - Options + * @param {boolean} options.exact - Only match and exact string + * @returns {boolean} Returns `true` if string is a base64 + */ +export const isBase64 = (stringToValidate, options = {}) => { + if (stringToValidate === '') return false + + const { exact = true } = options + + const BASE64_REG = + /(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)/g + const EXACT_BASE64_REG = + /(?:^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$)/ + + const regex = exact ? EXACT_BASE64_REG : BASE64_REG + + return regex.test(stringToValidate) +} + +/** + * Check if value is divisible by another number. + * + * @param {string|number} number - Value to check + * @param {string|number} divisor - Divisor number + * @returns {boolean} Returns `true` if value is divisible by another + */ +export const isDivisibleBy = (number, divisor) => !(number % divisor) + +/** + * Returns factors of a number. + * + * @param {number} value - Number + * @returns {number[]} Returns list of numbers + */ +export const getFactorsOfNumber = (value) => + [...Array(+value + 1).keys()].filter((idx) => value % idx === 0) + +/** + * Finds the closest number in list. + * + * @param {number} value - The value to compare + * @param {number} list - List of numbers + * @returns {number} Closest number + */ +export const findClosestValue = (value, list) => { + const closestValue = list.reduce((closest, current) => { + if (closest === null) return current + + return Math.abs(current - value) < Math.abs(closest - value) + ? current + : closest + }, null) + + return closestValue ?? value +} diff --git a/src/fireedge/src/client/utils/schema.js b/src/fireedge/src/client/utils/schema.js index cccf58241f..5b83df1dba 100644 --- a/src/fireedge/src/client/utils/schema.js +++ b/src/fireedge/src/client/utils/schema.js @@ -224,7 +224,7 @@ const getValuesFromArray = (options, separator = SEMICOLON_CHAR) => options?.split(separator) const getOptionsFromList = (options = []) => - arrayToOptions([...new Set(options)]) + arrayToOptions([...new Set(options)], { addEmpty: false }) const parseUserInputValue = (value) => { if (value === true) { diff --git a/src/fireedge/src/client/utils/translation.js b/src/fireedge/src/client/utils/translation.js index a2e6fee4ca..310325175a 100644 --- a/src/fireedge/src/client/utils/translation.js +++ b/src/fireedge/src/client/utils/translation.js @@ -26,7 +26,7 @@ import { } from 'yup' import { T } from 'client/constants' -import { isDivisibleBy, isBase64 } from 'client/utils/helpers' +import { isDivisibleBy, isBase64 } from 'client/utils/number' const buildMethods = () => { ;[number, string, boolean, object, array, date].forEach((schemaType) => {