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