diff --git a/src/fireedge/src/client/components/Cards/VmTemplateCard.js b/src/fireedge/src/client/components/Cards/VmTemplateCard.js index 635960e91a..1f64ba0850 100644 --- a/src/fireedge/src/client/components/Cards/VmTemplateCard.js +++ b/src/fireedge/src/client/components/Cards/VmTemplateCard.js @@ -26,7 +26,7 @@ import Image from 'client/components/Image' import { timeFromMilliseconds } from 'client/models/Helper' import { isExternalURL } from 'client/utils' -import { VM, LOGO_IMAGES_URL } from 'client/constants' +import { VM, STATIC_FILES_URL } from 'client/constants' const VmTemplateCard = memo( /** @@ -48,13 +48,10 @@ const VmTemplateCard = memo( LOGO = '', } = template - 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 logoSource = useMemo( + () => (isExternalURL(LOGO) ? LOGO : `${STATIC_FILES_URL}/${LOGO}`), + [LOGO] + ) const time = timeFromMilliseconds(+REGTIME) diff --git a/src/fireedge/src/client/components/FormControl/AutocompleteController.js b/src/fireedge/src/client/components/FormControl/AutocompleteController.js index d9cbb947ee..64194c6140 100644 --- a/src/fireedge/src/client/components/FormControl/AutocompleteController.js +++ b/src/fireedge/src/client/components/FormControl/AutocompleteController.js @@ -33,6 +33,7 @@ const AutocompleteController = memo( multiple = false, values = [], fieldProps: { separators, ...fieldProps } = {}, + readOnly = false, }) => { const { field: { value: renderValue, onBlur, onChange }, @@ -78,6 +79,7 @@ const AutocompleteController = memo( @@ -115,6 +117,7 @@ AutocompleteController.propTypes = { multiple: PropTypes.bool, values: PropTypes.arrayOf(PropTypes.object), fieldProps: PropTypes.object, + readOnly: PropTypes.bool, } AutocompleteController.displayName = 'AutocompleteController' diff --git a/src/fireedge/src/client/components/FormControl/CheckboxController.js b/src/fireedge/src/client/components/FormControl/CheckboxController.js index f27ac05927..4e5d05cd62 100644 --- a/src/fireedge/src/client/components/FormControl/CheckboxController.js +++ b/src/fireedge/src/client/components/FormControl/CheckboxController.js @@ -43,6 +43,7 @@ const CheckboxController = memo( label = '', tooltip, fieldProps = {}, + readOnly = false, }) => { const { field: { value = false, onChange }, @@ -56,6 +57,7 @@ const CheckboxController = memo( onChange(e.target.checked)} name={name} + readOnly={readOnly} checked={Boolean(value)} color="secondary" inputProps={{ 'data-cy': cy }} @@ -87,6 +89,7 @@ CheckboxController.propTypes = { label: PropTypes.any, tooltip: PropTypes.any, fieldProps: PropTypes.object, + readOnly: PropTypes.bool, } CheckboxController.displayName = 'CheckboxController' diff --git a/src/fireedge/src/client/components/FormControl/ErrorHelper.js b/src/fireedge/src/client/components/FormControl/ErrorHelper.js index 70b7075ae5..cbb3ee5050 100644 --- a/src/fireedge/src/client/components/FormControl/ErrorHelper.js +++ b/src/fireedge/src/client/components/FormControl/ErrorHelper.js @@ -54,6 +54,7 @@ ErrorHelper.propTypes = { label: PropTypes.oneOfType([ PropTypes.string, PropTypes.node, + PropTypes.array, PropTypes.shape({ word: PropTypes.string, values: PropTypes.oneOfType([ diff --git a/src/fireedge/src/client/components/FormControl/FileController.js b/src/fireedge/src/client/components/FormControl/FileController.js index 79aa08c015..4471492e6a 100644 --- a/src/fireedge/src/client/components/FormControl/FileController.js +++ b/src/fireedge/src/client/components/FormControl/FileController.js @@ -49,6 +49,7 @@ const FileController = memo( validationBeforeTransform, transform, fieldProps = {}, + readOnly = false, formContext = {}, }) => { const { setValue, setError, clearErrors, watch } = formContext @@ -115,12 +116,14 @@ const FileController = memo( } return ( - + @@ -128,6 +131,7 @@ const FileController = memo( : } @@ -161,6 +165,7 @@ FileController.propTypes = { ), transform: PropTypes.func, fieldProps: PropTypes.object, + readOnly: PropTypes.bool, formContext: PropTypes.shape({ setValue: PropTypes.func, setError: PropTypes.func, diff --git a/src/fireedge/src/client/components/FormControl/SelectController.js b/src/fireedge/src/client/components/FormControl/SelectController.js index 81326ea245..38186639c3 100644 --- a/src/fireedge/src/client/components/FormControl/SelectController.js +++ b/src/fireedge/src/client/components/FormControl/SelectController.js @@ -34,6 +34,7 @@ const SelectController = memo( renderValue, tooltip, fieldProps = {}, + readOnly = false, }) => { const firstValue = values?.[0]?.value ?? '' const defaultValue = multiple ? [firstValue] : firstValue @@ -50,8 +51,15 @@ const SelectController = memo( ) useEffect(() => { - if (multiple && !Array.isArray(optionSelected)) { - onChange([optionSelected]) + if (!optionSelected && !optionSelected.length) return + + if (multiple) { + const exists = values.some((v) => optionSelected.includes(v.value)) + !exists && onChange([firstValue]) + } else { + const exists = values.some((v) => `${v.value}` === `${optionSelected}`) + + !exists && onChange(firstValue) } }, [multiple]) @@ -82,6 +90,7 @@ const SelectController = memo( label={labelCanBeTranslated(label) ? Tr(label) : label} InputLabelProps={{ shrink: needShrink }} InputProps={{ + readOnly, startAdornment: (optionSelected && renderValue?.(optionSelected)) || (tooltip && ), @@ -118,6 +127,7 @@ SelectController.propTypes = { values: PropTypes.arrayOf(PropTypes.object).isRequired, renderValue: PropTypes.func, fieldProps: PropTypes.object, + readOnly: PropTypes.bool, } SelectController.displayName = 'SelectController' diff --git a/src/fireedge/src/client/components/FormControl/SliderController.js b/src/fireedge/src/client/components/FormControl/SliderController.js index 027096c061..f2afbd3a40 100644 --- a/src/fireedge/src/client/components/FormControl/SliderController.js +++ b/src/fireedge/src/client/components/FormControl/SliderController.js @@ -31,6 +31,7 @@ const SliderController = memo( label = '', tooltip, fieldProps = {}, + readOnly = false, }) => { const { field: { value, onChange, ...inputProps }, @@ -48,6 +49,7 @@ const SliderController = memo( value={typeof value === 'number' ? value : 0} aria-labelledby={sliderId} valueLabelDisplay="auto" + disabled={readOnly} data-cy={sliderId} onChange={(_, val) => onChange(val)} {...fieldProps} @@ -60,6 +62,7 @@ const SliderController = memo( error={Boolean(error)} label={labelCanBeTranslated(label) ? Tr(label) : label} InputProps={{ + readOnly, endAdornment: tooltip && , }} inputProps={{ @@ -98,6 +101,7 @@ SliderController.propTypes = { label: PropTypes.any, tooltip: PropTypes.any, fieldProps: PropTypes.object, + readOnly: PropTypes.bool, } SliderController.displayName = 'SliderController' diff --git a/src/fireedge/src/client/components/FormControl/SwitchController.js b/src/fireedge/src/client/components/FormControl/SwitchController.js index d4b839978a..cdb9a72451 100644 --- a/src/fireedge/src/client/components/FormControl/SwitchController.js +++ b/src/fireedge/src/client/components/FormControl/SwitchController.js @@ -43,6 +43,7 @@ const SwitchController = memo( label = '', tooltip, fieldProps = {}, + readOnly = false, }) => { const { field: { value = false, onChange }, @@ -50,10 +51,11 @@ const SwitchController = memo( } = useController({ name, control }) return ( - + onChange(e.target.checked)} name={name} checked={Boolean(value)} @@ -87,6 +89,7 @@ SwitchController.propTypes = { label: PropTypes.any, tooltip: PropTypes.any, fieldProps: PropTypes.object, + readOnly: PropTypes.bool, } SwitchController.displayName = 'SwitchController' diff --git a/src/fireedge/src/client/components/FormControl/TableController.js b/src/fireedge/src/client/components/FormControl/TableController.js index 0bedd88993..8e6b8a8b01 100644 --- a/src/fireedge/src/client/components/FormControl/TableController.js +++ b/src/fireedge/src/client/components/FormControl/TableController.js @@ -43,6 +43,7 @@ const TableController = memo( singleSelect = true, getRowId = defaultGetRowId, formContext = {}, + readOnly = false, fieldProps: { initialState, ...fieldProps } = {}, }) => { const { clearErrors } = formContext @@ -74,7 +75,10 @@ const TableController = memo( onlyGlobalSelectedRows getRowId={getRowId} initialState={{ ...initialState, selectedRowIds: initialRows }} + disableRowSelect={readOnly} onSelectedRowsChange={(rows) => { + if (readOnly) return + const rowValues = rows?.map(({ original }) => getRowId(original)) onChange(singleSelect ? rowValues?.[0] : rowValues) @@ -102,6 +106,7 @@ TableController.propTypes = { label: PropTypes.any, tooltip: PropTypes.any, fieldProps: PropTypes.object, + readOnly: PropTypes.bool, formContext: PropTypes.shape({ setValue: PropTypes.func, setError: PropTypes.func, diff --git a/src/fireedge/src/client/components/FormControl/TextController.js b/src/fireedge/src/client/components/FormControl/TextController.js index fa8b4d88b3..6e618b602c 100644 --- a/src/fireedge/src/client/components/FormControl/TextController.js +++ b/src/fireedge/src/client/components/FormControl/TextController.js @@ -35,6 +35,7 @@ const TextController = memo( watcher, dependencies, fieldProps = {}, + readOnly = false, }) => { const watch = dependencies && @@ -68,9 +69,17 @@ const TextController = memo( type={type} label={labelCanBeTranslated(label) ? Tr(label) : label} InputProps={{ + readOnly, endAdornment: tooltip && , }} - inputProps={{ 'data-cy': cy }} + inputProps={{ + 'data-cy': cy, + ...(type === 'number' && { + min: fieldProps.min, + max: fieldProps.max, + step: fieldProps.step, + }), + }} error={Boolean(error)} helperText={Boolean(error) && } FormHelperTextProps={{ 'data-cy': `${cy}-error` }} @@ -99,6 +108,7 @@ TextController.propTypes = { PropTypes.arrayOf(PropTypes.string), ]), fieldProps: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), + readOnly: PropTypes.bool, } TextController.displayName = 'TextController' diff --git a/src/fireedge/src/client/components/FormControl/TimeController.js b/src/fireedge/src/client/components/FormControl/TimeController.js index 3cca046d20..96f2eb13af 100644 --- a/src/fireedge/src/client/components/FormControl/TimeController.js +++ b/src/fireedge/src/client/components/FormControl/TimeController.js @@ -33,7 +33,7 @@ const TimeController = memo( label = '', tooltip, fieldProps: { defaultValue, ...fieldProps } = {}, - ...props + readOnly = false, }) => { const { field: { value, ...controllerProps }, @@ -50,6 +50,7 @@ const TimeController = memo( clearText={} todayText={} InputProps={{ + readOnly, autoComplete: 'off', startAdornment: tooltip && , }} @@ -80,6 +81,7 @@ TimeController.propTypes = { label: PropTypes.any, tooltip: PropTypes.any, fieldProps: PropTypes.object, + readOnly: PropTypes.bool, } TimeController.displayName = 'TimeController' diff --git a/src/fireedge/src/client/components/FormControl/ToggleController.js b/src/fireedge/src/client/components/FormControl/ToggleController.js index f71e8415f6..26b5783101 100644 --- a/src/fireedge/src/client/components/FormControl/ToggleController.js +++ b/src/fireedge/src/client/components/FormControl/ToggleController.js @@ -48,6 +48,7 @@ const ToggleController = memo( values = [], tooltip, fieldProps = {}, + readOnly = false, }) => { const defaultValue = multiple ? [values?.[0]?.value] : values?.[0]?.value @@ -72,17 +73,17 @@ const ToggleController = memo( )} onChange(newValues)} + fullWidth ref={ref} id={cy} + onChange={(_, newValues) => !readOnly && onChange(newValues)} value={optionSelected} - fullWidth exclusive={!multiple} data-cy={cy} {...fieldProps} > {values?.map(({ text, value = '' }) => ( - + {text} ))} @@ -111,6 +112,7 @@ ToggleController.propTypes = { values: PropTypes.arrayOf(PropTypes.object).isRequired, renderValue: PropTypes.func, fieldProps: PropTypes.object, + readOnly: PropTypes.bool, } ToggleController.displayName = 'ToggleController' diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/userInputsSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/userInputsSchema.js index 1e44ba1c93..2fee6dfb6b 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/userInputsSchema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/userInputsSchema.js @@ -15,7 +15,7 @@ * ------------------------------------------------------------------------- */ import { object, array, string, boolean, number, ref, ObjectSchema } from 'yup' -import { userInputsToObject } from 'client/models/Helper' +import { userInputsToObject, userInputsToArray } from 'client/models/Helper' import { UserInputType, T, @@ -40,7 +40,7 @@ const { boolean: uiBoolean, } = USER_INPUT_TYPES -const { array: __, ...userInputTypes } = USER_INPUT_TYPES +const { array: _array, fixed: _fixed, ...userInputTypes } = USER_INPUT_TYPES /** @type {UserInputType[]} User inputs types */ const valuesOfUITypes = Object.values(userInputTypes) @@ -90,7 +90,7 @@ const DESCRIPTION = { const OPTIONS = { name: 'options', label: T.Options, - tooltip: 'Press ENTER key to add a value', + tooltip: [T.PressKeysToAddAValue, ['ENTER']], dependOf: TYPE.name, type: INPUT_TYPES.AUTOCOMPLETE, multiple: true, @@ -100,7 +100,7 @@ const OPTIONS = { .default(() => []) .when(TYPE.name, (type, schema) => [uiList, uiListMultiple].includes(type) - ? schema.required() + ? schema.required().min(1) : schema.strip().notRequired() ), fieldProps: { @@ -211,10 +211,22 @@ export const USER_INPUT_SCHEMA = getObjectSchemaFromFields(USER_INPUT_FIELDS) export const USER_INPUTS_SCHEMA = object({ USER_INPUTS: array(USER_INPUT_SCHEMA) .ensure() - .afterSubmit((userInputs) => userInputsToObject(userInputs)), + .afterSubmit((userInputs, { context }) => { + const capacityInputs = userInputsToArray(context?.general?.MODIFICATION, { + filterCapacityInputs: false, + }).map(({ name, ...userInput }) => ({ + name, + ...userInput, + // set default value from MEMORY, CPU and VCPU fields + default: context.general?.[name], + ...(['MEMORY', 'CPU'].includes(name) && { mandatory: true }), + })) + + return userInputsToObject([...capacityInputs, ...userInputs]) + }), INPUTS_ORDER: string() .trim() - .afterSubmit((_, { context }) => { + .afterSubmit((_inputsOrder_, { context }) => { const userInputs = context?.extra?.USER_INPUTS return userInputs?.map(({ name }) => String(name).toUpperCase()).join(',') 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 708b3cf481..f45ff4120f 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 @@ -13,105 +13,95 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { number, boolean } from 'yup' +import { number } from 'yup' -import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants' +import { + generateModificationInputs, + generateHotResizeInputs, + generateCapacityInput, +} from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/capacityUtils' import { Field } from 'client/utils' +import { T, HYPERVISORS } from 'client/constants' const commonValidation = number() .positive() .default(() => undefined) +// -------------------------------------------------------- +// MEMORY fields +// -------------------------------------------------------- + /** @type {Field} Memory field */ -export const MEMORY = { +export const MEMORY = generateCapacityInput({ name: 'MEMORY', label: T.Memory, tooltip: T.MemoryConcept, - type: INPUT_TYPES.TEXT, - htmlType: 'number', validation: commonValidation .required() .when('HYPERVISOR', (hypervisor, schema) => hypervisor === HYPERVISORS.vcenter ? schema.isDivisibleBy(4) : schema ), - grid: { md: 12 }, -} +}) -/** @type {Field} Hot reloading on memory field */ -export const ENABLE_HR_MEMORY = { - name: 'HOT_RESIZE.MEMORY_HOT_ADD_ENABLED', - label: T.EnableHotResize, - type: INPUT_TYPES.SWITCH, - validation: boolean().yesOrNo(), - grid: { xs: 4, md: 6 }, -} +/** @type {Field[]} Hot resize on memory field */ +export const HR_MEMORY_FIELDS = generateHotResizeInputs( + { fieldName: MEMORY.name }, + { + name: `${MEMORY.name}_MAX`, + label: T.MaxMemory, + tooltip: T.MaxMemoryConcept, + } +) -/** @type {Field} Maximum memory field */ -export const MEMORY_MAX = { - name: 'MEMORY_MAX', - label: T.MaxMemory, - dependOf: ENABLE_HR_MEMORY.name, - type: INPUT_TYPES.TEXT, - htmlType: (enabledHr) => (enabledHr ? 'number' : INPUT_TYPES.HIDDEN), - validation: commonValidation.when( - ENABLE_HR_MEMORY.name, - (enabledHr, schema) => - enabledHr ? schema.required() : schema.notRequired() - ), - grid: { xs: 8, md: 6 }, -} +/** @type {Field[]} Modification inputs on memory field */ +export const MOD_MEMORY_FIELDS = generateModificationInputs(MEMORY.name) + +/** @type {Field[]} List of memory fields */ +export const MEMORY_FIELDS = [MEMORY, ...HR_MEMORY_FIELDS, ...MOD_MEMORY_FIELDS] + +// -------------------------------------------------------- +// CPU fields +// -------------------------------------------------------- /** @type {Field} Physical CPU field */ -export const PHYSICAL_CPU = { +export const PHYSICAL_CPU = generateCapacityInput({ name: 'CPU', label: T.PhysicalCpu, tooltip: T.CpuConcept, - type: INPUT_TYPES.TEXT, - htmlType: 'number', validation: commonValidation.required(), - grid: { md: 12 }, -} +}) + +/** @type {Field[]} Modification inputs on CPU field */ +export const MOD_CPU_FIELDS = generateModificationInputs(PHYSICAL_CPU.name) + +/** @type {Field[]} List of CPU fields */ +export const CPU_FIELDS = [PHYSICAL_CPU, ...MOD_CPU_FIELDS] + +// -------------------------------------------------------- +// Virtual CPU fields +// -------------------------------------------------------- /** @type {Field} Virtual CPU field */ -export const VIRTUAL_CPU = { +export const VIRTUAL_CPU = generateCapacityInput({ name: 'VCPU', label: T.VirtualCpu, tooltip: T.VirtualCpuConcept, - type: INPUT_TYPES.TEXT, - htmlType: 'number', validation: commonValidation, - grid: { md: 12 }, -} + grid: { md: 3 }, +}) -/** @type {Field} Hot reloading on virtual CPU field */ -export const ENABLE_HR_VCPU = { - name: 'HOT_RESIZE.CPU_HOT_ADD_ENABLED', - label: T.EnableHotResize, - type: INPUT_TYPES.SWITCH, - validation: boolean().yesOrNo(), - grid: { xs: 4, md: 6 }, -} +/** @type {Field[]} Hot resize on memory field */ +export const HR_CPU_FIELDS = generateHotResizeInputs( + { name: PHYSICAL_CPU.name }, + { + name: `${VIRTUAL_CPU.name}_MAX`, + label: T.MaxVirtualCpu, + tooltip: T.MaxVirtualCpuConcept, + } +) -/** @type {Field} Maximum virtual CPU field */ -export const VCPU_MAX = { - name: 'VCPU_MAX', - label: T.MaxVirtualCpu, - dependOf: ENABLE_HR_VCPU.name, - type: INPUT_TYPES.TEXT, - htmlType: (enabledHr) => (enabledHr ? 'number' : INPUT_TYPES.HIDDEN), - validation: commonValidation.when(ENABLE_HR_VCPU.name, (enabledHr, schema) => - enabledHr ? schema.required() : schema.notRequired() - ), - grid: { xs: 8, md: 6 }, -} +/** @type {Field[]} Modification inputs on Virtual CPU field */ +export const MOD_VCPU_FIELDS = generateModificationInputs(VIRTUAL_CPU.name) -/** @type {Field[]} List of capacity fields */ -export const FIELDS = [ - MEMORY, - ENABLE_HR_MEMORY, - MEMORY_MAX, - PHYSICAL_CPU, - VIRTUAL_CPU, - ENABLE_HR_VCPU, - VCPU_MAX, -] +/** @type {Field[]} List of Virtual CPU fields */ +export const VCPU_FIELDS = [VIRTUAL_CPU, ...HR_CPU_FIELDS, ...MOD_VCPU_FIELDS] diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/capacityUtils.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/capacityUtils.js new file mode 100644 index 0000000000..77175eaf35 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/capacityUtils.js @@ -0,0 +1,266 @@ +/* ------------------------------------------------------------------------- * + * 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 { + boolean, + number, + lazy, + string, + array, + ref, + BaseSchema, + NumberSchema, +} from 'yup' + +import { getUserInputParams } from 'client/models/Helper' +import { Field, arrayToOptions, sentenceCase } from 'client/utils' +import { T, INPUT_TYPES, HYPERVISORS, USER_INPUT_TYPES } from 'client/constants' + +const { fixed, range, rangeFloat, list } = USER_INPUT_TYPES + +const MODIFICATION_TRANSLATES = { + MEMORY: { + label: T.MemoryModification, + tooltip: T.AllowUsersToModifyMemory, + }, + CPU: { + label: T.CpuModification, + tooltip: T.AllowUsersToModifyCpu, + }, + VCPU: { + label: T.VirtualCpuModification, + tooltip: T.AllowUsersToModifyVirtualCpu, + }, +} + +const isRangeType = (type) => [range, rangeFloat].includes(type) + +/** @typedef {'MEMORY'|'CPU'|'VCPU'} CapacityFieldName */ +/** @typedef {{ type: string, min: string, max: string, options: string }} ModificationIds */ + +/** + * @param {CapacityFieldName} fieldName - Capacity field name + * @returns {ModificationIds} - Modification ids + */ +const getModificationIdsByFieldName = (fieldName) => ({ + type: `MODIFICATION.${fieldName}.type`, + min: `MODIFICATION.${fieldName}.min`, + max: `MODIFICATION.${fieldName}.max`, + options: `MODIFICATION.${fieldName}.options`, +}) + +/** + * @param {CapacityFieldName} fieldName - Capacity field name + * @param {ModificationIds} [modificationIds] - Modification ids + * @returns {Field} - Type field for capacity modification + */ +const modificationTypeInput = (fieldName, { type: typeId }) => ({ + name: typeId, + ...MODIFICATION_TRANSLATES[fieldName], + type: INPUT_TYPES.SELECT, + values: arrayToOptions([fixed, range, list], { + addEmpty: 'Any value', + getValue: (type) => + // allow float numbers on CPU and VCPU + ['CPU', 'VCPU'].includes(fieldName) && type === range ? rangeFloat : type, + getText: (type) => sentenceCase(type), + }), + validation: lazy((_, { context }) => + string().default(() => { + const capacityUserInput = context.extra?.USER_INPUTS?.[fieldName] + const { type } = getUserInputParams(capacityUserInput) ?? {} + + return type + }) + ), + grid: { md: 3 }, +}) + +/** + * @param {CapacityFieldName} fieldName - Capacity field name + * @param {ModificationIds} [modificationIds] - Modification ids + * @returns {Field[]} - Range fields for capacity modification + */ +const modificationRangeInputs = (fieldName, { type, min, max }) => { + // Common attributes for range inputs (min, max) + const commonRangeAttributes = { + type: INPUT_TYPES.TEXT, + dependOf: type, + htmlType: (modificationType) => + isRangeType(modificationType) ? 'number' : INPUT_TYPES.HIDDEN, + grid: { md: 3 }, + } + + /** + * Generates validation for range inputs. + * + * @param {object} config - Validation config + * @param {NumberSchema} config.thenFn - Schema validation when the modification type is range + * @param {'min'|'max'} config.rangeParam - Range parameter: min or max + * @returns {BaseSchema} Schema validation for range inputs + */ + const commonRangeValidation = ({ thenFn, rangeParam }) => + lazy((_, { context }) => + number() + .positive() + .transform((value) => (!isNaN(value) ? value : null)) + .when('HYPERVISOR', (hypervisor, schema) => + hypervisor === HYPERVISORS.vcenter ? schema.isDivisibleBy(4) : schema + ) + .when(`$general.${type}`, { + is: isRangeType, + then: thenFn, + otherwise: (schema) => schema.optional().nullable(), + }) + .default(() => { + const capacityUserInput = context.extra?.USER_INPUTS?.[fieldName] + const params = getUserInputParams(capacityUserInput) + + return params[rangeParam] + }) + ) + + return [ + { + name: min, + label: T.Min, + ...commonRangeAttributes, + validation: commonRangeValidation({ + thenFn: (schema) => schema.required().lessThan(ref(`$general.${max}`)), + rangeParam: 'min', + }), + }, + { + name: max, + label: T.Max, + ...commonRangeAttributes, + validation: commonRangeValidation({ + thenFn: (schema) => schema.required().moreThan(ref(`$general.${min}`)), + rangeParam: 'max', + }), + }, + ] +} + +/** + * @param {CapacityFieldName} fieldName - Capacity field name + * @param {ModificationIds} [modificationIds] - Modification ids + * @returns {Field} - Options field for capacity modification + */ +const modificationOptionsInput = (fieldName, { type, options: optionsId }) => ({ + name: optionsId, + label: T.Options, + tooltip: [T.PressKeysToAddAValue, ['ENTER']], + type: INPUT_TYPES.AUTOCOMPLETE, + multiple: true, + dependOf: type, + htmlType: (modificationType) => + modificationType !== list && INPUT_TYPES.HIDDEN, + validation: lazy((_, { context }) => + array(number().positive()) + .when(`$general.${type}`, { + is: (modificationType) => modificationType === list, + then: (schema) => schema.required().min(1), + otherwise: (schema) => schema.nullable(), + }) + .default(() => { + const capacityUserInput = context.extra?.USER_INPUTS?.[fieldName] + const { options = [] } = getUserInputParams(capacityUserInput) ?? {} + + return options + }) + ), + fieldProps: { freeSolo: true }, + grid: fieldName === 'CPU' ? { md: 6 } : { md: 9 }, +}) + +/** + * @param {CapacityFieldName} fieldName - Capacity field name + * @returns {Field[]} - Fields for capacity modification + */ +export const generateModificationInputs = (fieldName) => { + const modificationIds = getModificationIdsByFieldName(fieldName) + + return [ + modificationTypeInput(fieldName, modificationIds), + modificationOptionsInput(fieldName, modificationIds), + ...modificationRangeInputs(fieldName, modificationIds), + ] +} + +/** + * @param {Field} hrField - Hot resize field + * @param {CapacityFieldName} hrField.fieldName - Capacity field name + * @param {Field} maxField - Max field + * @returns {Field[]} Hot resize fields + */ +export const generateHotResizeInputs = ( + { name: hrFieldName, ...hrField }, + maxField +) => [ + { + ...hrField, + name: `HOT_RESIZE.${hrFieldName}_HOT_ADD_ENABLED`, + dependOf: `HOT_RESIZE.${hrFieldName}_HOT_ADD_ENABLED`, + label: T.EnableHotResize, + type: INPUT_TYPES.SWITCH, + validation: boolean().yesOrNo(), + grid: (enabledHr) => + enabledHr ? { xs: 7, sm: 5, md: 3 } : { xs: 12, sm: 9, md: 9 }, + }, + { + ...maxField, + dependOf: `HOT_RESIZE.${hrFieldName}_HOT_ADD_ENABLED`, + type: INPUT_TYPES.TEXT, + htmlType: (enabledHr) => (enabledHr ? 'number' : INPUT_TYPES.HIDDEN), + validation: number() + .positive() + .default(() => undefined) + .when(`HOT_RESIZE.${hrFieldName}_HOT_ADD_ENABLED`, (enabledHr, schema) => + enabledHr ? schema.required() : schema.notRequired() + ), + grid: { xs: 5, sm: 7, md: 6 }, + }, +] + +/** + * @param {Field} config - Configuration + * @param {CapacityFieldName} config.name - Capacity field name + * @param {BaseSchema} config.validation - Validation schema + * @returns {Field} - Field with validation modification conditions + */ +export const generateCapacityInput = ({ validation, ...field }) => ({ + ...field, + type: INPUT_TYPES.TEXT, + htmlType: 'number', + validation: validation + .when(`$general.MODIFICATION.${field.name}.type`, { + is: (modificationType) => modificationType === range, + then: (schema) => + schema + .max(ref(`$general.MODIFICATION.${field.name}.max`)) + .min(ref(`$general.MODIFICATION.${field.name}.min`)), + otherwise: (schema) => schema, + }) + .when( + [ + `$general.MODIFICATION.${field.name}.type`, + `$general.MODIFICATION.${field.name}.options`, + ], + (modificationType, options = [], schema) => + modificationType === list ? schema.oneOf(options) : schema + ), + grid: { md: 3 }, +}) 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 04b82c9145..869cdd0946 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 @@ -13,7 +13,6 @@ * 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 { useWatch } from 'react-hook-form' @@ -22,13 +21,13 @@ import { useViews } from 'client/features/Auth' import FormWithSchema from 'client/components/Forms/FormWithSchema' import useStyles from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/styles' -import { HYPERVISOR_FIELD } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/informationSchema' import { SCHEMA, SECTIONS, } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/schema' import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper' -import { T, RESOURCE_NAMES } from 'client/constants' +import { generateKey } from 'client/utils' +import { T, RESOURCE_NAMES, VmTemplate } from 'client/constants' export const STEP_ID = 'general' @@ -42,22 +41,21 @@ const Content = ({ isUpdate }) => { const dialog = getResourceView(resource)?.dialogs?.create_dialog const sectionsAvailable = getSectionsAvailable(dialog, hypervisor) - return SECTIONS(hypervisor, isUpdate).filter( - ({ id, required }) => required || sectionsAvailable.includes(id) + return ( + SECTIONS(hypervisor, isUpdate) + .filter( + ({ id, required }) => required || sectionsAvailable.includes(id) + ) + // unique keys to avoid duplicates + .map((section) => ({ key: generateKey(), ...section })) ) }, [view, hypervisor]) return (
- - {sections.map(({ id, ...section }) => ( + {sections.map(({ key, id, ...section }) => ( { ) } -const General = (initialValues) => { - const isUpdate = initialValues?.NAME - const initialHypervisor = initialValues?.TEMPLATE?.HYPERVISOR +/** + * General configuration about VM Template. + * + * @param {VmTemplate} vmTemplate - VM Template + * @returns {object} General configuration step + */ +const General = (vmTemplate) => { + const isUpdate = vmTemplate?.NAME + const initialHypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR return { id: STEP_ID, diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/informationSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/informationSchema.js index 2da5a2cc2b..0e088bd604 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/informationSchema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/informationSchema.js @@ -16,7 +16,7 @@ import { string } from 'yup' import Image from 'client/components/Image' -import { T, LOGO_IMAGES_URL, INPUT_TYPES, HYPERVISORS } from 'client/constants' +import { T, STATIC_FILES_URL, INPUT_TYPES, HYPERVISORS } from 'client/constants' import { Field, arrayToOptions } from 'client/utils' /** @@ -31,7 +31,7 @@ export const NAME = (isUpdate) => ({ .trim() .required() .default(() => undefined), - grid: { sm: 6 }, + grid: { md: 12 }, ...(isUpdate && { fieldProps: { disabled: true } }), }) @@ -40,10 +40,12 @@ export const DESCRIPTION = { name: 'DESCRIPTION', label: T.Description, type: INPUT_TYPES.TEXT, + multiline: true, validation: string() .trim() .notRequired() .default(() => undefined), + grid: { md: 12 }, } /** @type {Field} Hypervisor field */ @@ -68,40 +70,38 @@ export const LOGO = { type: INPUT_TYPES.SELECT, values: [ { text: '-', value: '' }, - // client/assets/images/logos - { text: 'Alpine Linux', value: 'alpine.png' }, - { text: 'ALT', value: 'alt.png' }, - { text: 'Arch', value: 'arch.png' }, - { text: 'CentOS', value: 'centos.png' }, - { text: 'Debian', value: 'debian.png' }, - { text: 'Devuan', value: 'devuan.png' }, - { text: 'Fedora', value: 'fedora.png' }, - { text: 'FreeBSD', value: 'freebsd.png' }, - { text: 'HardenedBSD', value: 'hardenedbsd.png' }, - { text: 'Knoppix', value: 'knoppix.png' }, - { text: 'Linux', value: 'linux.png' }, - { text: 'Oracle', value: 'oracle.png' }, - { text: 'RedHat', value: 'redhat.png' }, - { text: 'Suse', value: 'suse.png' }, - { text: 'Ubuntu', value: 'ubuntu.png' }, - { text: 'Windows xp', value: 'windowsxp.png' }, - { text: 'Windows 10', value: 'windows8.png' }, + { text: 'Alpine Linux', value: 'images/logos/alpine.png' }, + { text: 'ALT', value: 'images/logos/alt.png' }, + { text: 'Arch', value: 'images/logos/arch.png' }, + { text: 'CentOS', value: 'images/logos/centos.png' }, + { text: 'Debian', value: 'images/logos/debian.png' }, + { text: 'Devuan', value: 'images/logos/devuan.png' }, + { text: 'Fedora', value: 'images/logos/fedora.png' }, + { text: 'FreeBSD', value: 'images/logos/freebsd.png' }, + { text: 'HardenedBSD', value: 'images/logos/hardenedbsd.png' }, + { text: 'Knoppix', value: 'images/logos/knoppix.png' }, + { text: 'Linux', value: 'images/logos/linux.png' }, + { text: 'Oracle', value: 'images/logos/oracle.png' }, + { text: 'RedHat', value: 'images/logos/redhat.png' }, + { text: 'Suse', value: 'images/logos/suse.png' }, + { text: 'Ubuntu', value: 'images/logos/ubuntu.png' }, + { text: 'Windows xp', value: 'images/logos/windowsxp.png' }, + { text: 'Windows 10', value: 'images/logos/windows8.png' }, ], renderValue: (value) => ( logo client/assets/images/logos/{value}.png + src={`${STATIC_FILES_URL}/${value}`} /> ), validation: string() .trim() .notRequired() .default(() => undefined), + grid: { md: 12 }, } /** diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/ownershipSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/ownershipSchema.js index 8407173b91..e805a707ef 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/ownershipSchema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/ownershipSchema.js @@ -29,6 +29,7 @@ export const UID_FIELD = { const { data: users = [] } = useGetUsersQuery() return arrayToOptions(users, { + addEmpty: false, getText: ({ ID, NAME }) => `#${ID} ${NAME}`, getValue: ({ ID }) => ID, sorter: OPTION_SORTERS.numeric, @@ -50,6 +51,7 @@ export const GID_FIELD = { const { data: groups = [] } = useGetGroupsQuery() return arrayToOptions(groups, { + addEmpty: false, getText: ({ ID, NAME }) => `#${ID} ${NAME}`, getValue: ({ ID }) => ID, sorter: OPTION_SORTERS.numeric, 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 604711831b..9b1ecf394d 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 @@ -19,7 +19,7 @@ import { FIELDS as INFORMATION_FIELDS, HYPERVISOR_FIELD, } from './informationSchema' -import { FIELDS as CAPACITY_FIELDS } from './capacitySchema' +import { MEMORY_FIELDS, CPU_FIELDS, VCPU_FIELDS } from './capacitySchema' import { FIELDS as VM_GROUP_FIELDS } from './vmGroupSchema' import { FIELDS as OWNERSHIP_FIELDS } from './ownershipSchema' import { FIELDS as VCENTER_FIELDS } from './vcenterSchema' @@ -41,12 +41,28 @@ const SECTIONS = (hypervisor, isUpdate) => [ id: 'information', legend: T.Information, required: true, - fields: filterFieldsByHypervisor(INFORMATION_FIELDS(isUpdate), hypervisor), + fields: INFORMATION_FIELDS(isUpdate), + }, + { + id: 'hypervisor', + legend: T.Hypervisor, + required: true, + fields: [HYPERVISOR_FIELD], }, { id: 'capacity', - legend: T.Capacity, - fields: filterFieldsByHypervisor(CAPACITY_FIELDS, hypervisor), + 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: 'ownership', @@ -70,11 +86,10 @@ const SECTIONS = (hypervisor, isUpdate) => [ * @returns {BaseSchema} Step schema */ const SCHEMA = (hypervisor) => - getObjectSchemaFromFields([ - HYPERVISOR_FIELD, - ...SECTIONS(hypervisor) + getObjectSchemaFromFields( + SECTIONS(hypervisor) .map(({ fields }) => fields) - .flat(), - ]) + .flat() + ) export { SECTIONS, SCHEMA } diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/styles.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/styles.js index 2646764c51..7677c6520d 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/styles.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/styles.js @@ -20,11 +20,11 @@ export default makeStyles((theme) => ({ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: theme.spacing(1), - [theme.breakpoints.down('lg')]: { + [theme.breakpoints.down('md')]: { gridTemplateColumns: '1fr', }, }, - information: { + capacity: { gridColumn: '1 / -1', }, })) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/vmGroupSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/vmGroupSchema.js index 25c8d24135..6e27b81ea1 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/vmGroupSchema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/General/vmGroupSchema.js @@ -28,6 +28,7 @@ export const VM_GROUP_FIELD = { const { data: vmGroups = [] } = useGetVMGroupsQuery() return arrayToOptions(vmGroups, { + addEmpty: false, getText: ({ ID, NAME }) => `#${ID} ${NAME}`, getValue: ({ ID }) => ID, sorter: OPTION_SORTERS.numeric, @@ -58,7 +59,7 @@ export const ROLE_FIELD = { ) ?.flat() - return arrayToOptions(roles) + return arrayToOptions(roles, { addEmpty: false }) }, grid: { md: 12 }, validation: string() diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/index.js index 07a43f903b..7819169304 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/index.js @@ -23,14 +23,13 @@ import CustomVariables, { STEP_ID as CUSTOM_ID, } from 'client/components/Forms/VmTemplate/CreateForm/Steps/CustomVariables' import { jsonToXml, userInputsToArray } from 'client/models/Helper' -import { createSteps, isBase64 } from 'client/utils' +import { createSteps, isBase64, encodeBase64 } from 'client/utils' const Steps = createSteps([General, ExtraConfiguration, CustomVariables], { transformInitialValue: (vmTemplate, schema) => { - const userInputs = userInputsToArray( - vmTemplate?.TEMPLATE?.USER_INPUTS, - vmTemplate?.TEMPLATE?.INPUTS_ORDER - ) + const userInputs = userInputsToArray(vmTemplate?.TEMPLATE?.USER_INPUTS, { + order: vmTemplate?.TEMPLATE?.INPUTS_ORDER, + }) const knownTemplate = schema.cast( { @@ -56,7 +55,7 @@ const Steps = createSteps([General, ExtraConfiguration, CustomVariables], { }, transformBeforeSubmit: (formData) => { const { - [GENERAL_ID]: general = {}, + [GENERAL_ID]: { MODIFICATION: _, ...general } = {}, [CUSTOM_ID]: customVariables = {}, [EXTRA_ID]: { CONTEXT: { START_SCRIPT, ENCODE_START_SCRIPT, ...restOfContext }, @@ -70,7 +69,7 @@ const Steps = createSteps([General, ExtraConfiguration, CustomVariables], { // transform start script to base64 if needed [ENCODE_START_SCRIPT ? 'START_SCRIPT_BASE64' : 'START_SCRIPT']: ENCODE_START_SCRIPT && !isBase64(START_SCRIPT) - ? btoa(unescape(encodeURIComponent(START_SCRIPT))) + ? encodeBase64(START_SCRIPT) : START_SCRIPT, } const topology = ENABLE_NUMA ? { TOPOLOGY: restOfTopology } : {} 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 5cc3af69ee..d6a17bb2bb 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 @@ -13,54 +13,73 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { number } from 'yup' +import { NumberSchema } from 'yup' +import { getUserInputParams } from 'client/models/Helper' +import { + Field, + schemaUserInput, + prettyBytes, + isDivisibleBy, +} from 'client/utils' +import { T, HYPERVISORS, VmTemplate } from 'client/constants' -import { Field } from 'client/utils' -import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants' - -const commonValidation = number() - .positive() - .default(() => undefined) - -/** @type {Field} Memory field */ -const MEMORY = (hypervisor) => { - let validation = commonValidation.required() - - if (hypervisor === HYPERVISORS.vcenter) { - validation = validation.isDivisibleBy(4) - } - - return { - name: 'MEMORY', - label: T.Memory, - tooltip: T.MemoryConcept, - type: INPUT_TYPES.TEXT, - htmlType: 'number', - validation, - grid: { md: 12 }, - } +const TRANSLATES = { + MEMORY: { name: 'MEMORY', label: T.Memory, tooltip: T.MemoryConcept }, + CPU: { name: 'CPU', label: T.PhysicalCpu, tooltip: T.CpuConcept }, + VCPU: { name: 'VCPU', label: T.VirtualCpu, tooltip: T.VirtualCpuConcept }, } -/** @type {Field} Physical CPU field */ -const PHYSICAL_CPU = { - name: 'CPU', - label: T.PhysicalCpu, - tooltip: T.PhysicalCpuConcept, - type: INPUT_TYPES.TEXT, - htmlType: 'number', - validation: commonValidation.required(), - grid: { md: 12 }, -} +const valueLabelFormat = (value) => prettyBytes(value, 'MB') -/** @type {Field} Virtual CPU field */ -const VIRTUAL_CPU = { - name: 'VCPU', - label: T.VirtualCpu, - tooltip: T.VirtualCpuConcept, - type: INPUT_TYPES.TEXT, - htmlType: 'number', - validation: commonValidation.notRequired(), - grid: { md: 12 }, -} +/** + * @param {VmTemplate} [vmTemplate] - VM Template + * @returns {Field[]} Basic configuration fields + */ +export const FIELDS = (vmTemplate) => { + const { + HYPERVISOR, + USER_INPUTS = {}, + MEMORY = '', + CPU = '', + VCPU = '', + } = vmTemplate?.TEMPLATE || {} -export const FIELDS = [MEMORY, PHYSICAL_CPU, VIRTUAL_CPU] + const { + MEMORY: memoryInput = `M|number|||${MEMORY}`, + CPU: cpuInput = `M|number-float|||${CPU}`, + VCPU: vcpuInput = `O|number|||${VCPU}`, + } = USER_INPUTS + + return [ + { name: 'MEMORY', ...getUserInputParams(memoryInput) }, + { name: 'CPU', ...getUserInputParams(cpuInput) }, + { name: 'VCPU', ...getUserInputParams(vcpuInput) }, + ].map(({ name, options, ...userInput }) => { + const isMemory = name === 'MEMORY' + const isVCenter = HYPERVISOR === HYPERVISORS.vcenter + const divisibleBy4 = isVCenter && isMemory + + const ensuredOptions = divisibleBy4 + ? options?.filter((value) => isDivisibleBy(+value, 4)) + : options + + const schemaUi = schemaUserInput({ options: ensuredOptions, ...userInput }) + const isNumber = schemaUi.validation instanceof NumberSchema + + if (isNumber) { + // add positive number validator + isNumber && (schemaUi.validation &&= schemaUi.validation.positive()) + + // add label format on pretty bytes + isMemory && + (schemaUi.fieldProps = { ...schemaUi.fieldProps, valueLabelFormat }) + + if (divisibleBy4) { + schemaUi.validation &&= schemaUi.validation.isDivisibleBy(4) + schemaUi.fieldProps = { ...schemaUi.fieldProps, step: 4 } + } + } + + 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 2b69d43f59..704bd33fb9 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 @@ -22,23 +22,26 @@ import useStyles from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ import { SCHEMA, - FIELDS, + SECTIONS, } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema' import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper' import { T, RESOURCE_NAMES, VmTemplate } from 'client/constants' export const STEP_ID = 'configuration' -const Content = ({ hypervisor }) => { +const Content = ({ vmTemplate }) => { const classes = useStyles() const { view, getResourceView } = useViews() const sections = useMemo(() => { + const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR const resource = RESOURCE_NAMES.VM_TEMPLATE const dialog = getResourceView(resource)?.dialogs?.instantiate_dialog const sectionsAvailable = getSectionsAvailable(dialog, hypervisor) - return FIELDS(hypervisor).filter(({ id }) => sectionsAvailable.includes(id)) + return SECTIONS(vmTemplate).filter(({ id }) => + sectionsAvailable.includes(id) + ) }, [view]) return ( @@ -57,26 +60,22 @@ const Content = ({ hypervisor }) => { ) } +Content.propTypes = { + vmTemplate: PropTypes.object, +} + /** * Basic configuration about VM Template. * * @param {VmTemplate} vmTemplate - VM Template * @returns {object} Basic configuration step */ -const BasicConfiguration = (vmTemplate) => { - const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR - - return { - id: STEP_ID, - label: T.Configuration, - resolver: () => SCHEMA(hypervisor), - optionsValidate: { abortEarly: false }, - content: (props) => Content({ ...props, hypervisor }), - } -} - -Content.propTypes = { - hypervisor: PropTypes.string, -} +const BasicConfiguration = (vmTemplate) => ({ + id: STEP_ID, + label: T.Configuration, + resolver: () => SCHEMA(vmTemplate), + optionsValidate: { abortEarly: false }, + content: (props) => Content({ ...props, vmTemplate }), +}) export default BasicConfiguration diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema.js index 5bce1cf3b7..dfa05fc644 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema.js @@ -28,50 +28,59 @@ import { filterFieldsByHypervisor, getObjectSchemaFromFields, Field, + Section, } from 'client/utils' -import { T, HYPERVISORS } from 'client/constants' +import { T, VmTemplate } from 'client/constants' /** - * @param {HYPERVISORS} [hypervisor] - Template hypervisor - * @returns {function(string):{ id: string, legend: string, fields: Field[] }[]} Fields + * @param {VmTemplate} [vmTemplate] - VM Template + * @returns {Section[]} Sections */ -const FIELDS = (hypervisor) => [ - { - id: 'information', - legend: T.Information, - fields: filterFieldsByHypervisor(INFORMATION_FIELDS, hypervisor), - }, - { - id: 'capacity', - legend: T.Capacity, - fields: filterFieldsByHypervisor(CAPACITY_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_FOLDER_FIELD], hypervisor), - }, -] +const SECTIONS = (vmTemplate) => { + const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR + + return [ + { + id: 'information', + legend: T.Information, + fields: filterFieldsByHypervisor(INFORMATION_FIELDS, hypervisor), + }, + { + id: 'capacity', + legend: T.Capacity, + fields: filterFieldsByHypervisor(CAPACITY_FIELDS(vmTemplate), 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_FOLDER_FIELD], hypervisor), + }, + ] +} /** - * @param {HYPERVISORS} [hypervisor] - Template hypervisor + * @param {VmTemplate} [vmTemplate] - VM Template + * @returns {Field[]} Basic configuration fields + */ +const FIELDS = (vmTemplate) => + SECTIONS(vmTemplate) + .map(({ fields }) => fields) + .flat() + +/** + * @param {VmTemplate} [vmTemplate] - VM Template * @returns {BaseSchema} Step schema */ -const SCHEMA = (hypervisor) => - getObjectSchemaFromFields( - FIELDS(hypervisor) - .map(({ fields }) => fields) - .flat() - ) +const SCHEMA = (vmTemplate) => getObjectSchemaFromFields(FIELDS(vmTemplate)) -export { FIELDS, SCHEMA } +export { SECTIONS, FIELDS, SCHEMA } diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/UserInputs/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/UserInputs/index.js index 34fd785ca1..1c2aca48ed 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/UserInputs/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/UserInputs/index.js @@ -20,8 +20,7 @@ import { FIELDS, SCHEMA, } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/UserInputs/schema' -import { userInputsToArray } from 'client/models/Helper' -import { T, VmTemplate } from 'client/constants' +import { T, UserInputObject } from 'client/constants' import { FormWithSchema } from 'client/components/Forms' export const STEP_ID = 'user_inputs' @@ -42,22 +41,15 @@ Content.propTypes = { /** * User inputs step. * - * @param {VmTemplate} vmTemplate - VM Template + * @param {UserInputObject[]} userInputs - User inputs * @returns {object} User inputs step */ -const UserInputsStep = (vmTemplate) => { - const userInputs = userInputsToArray( - vmTemplate?.TEMPLATE?.USER_INPUTS, - vmTemplate?.TEMPLATE?.INPUTS_ORDER - ) - - return { - id: STEP_ID, - label: T.UserInputs, - optionsValidate: { abortEarly: false }, - resolver: SCHEMA(userInputs), - content: (props) => Content({ ...props, userInputs }), - } -} +const UserInputsStep = (userInputs) => ({ + id: STEP_ID, + label: T.UserInputs, + optionsValidate: { abortEarly: false }, + resolver: SCHEMA(userInputs), + content: (props) => Content({ ...props, userInputs }), +}) export default UserInputsStep 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 24e77aa5a8..b3ff7b6864 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 @@ -22,16 +22,21 @@ import UserInputs, { import ExtraConfiguration, { STEP_ID as EXTRA_ID, } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration' -import { jsonToXml } from 'client/models/Helper' +import { jsonToXml, userInputsToArray } from 'client/models/Helper' import { createSteps } from 'client/utils' const Steps = createSteps( - (vmTemplate) => - [ + (vmTemplate) => { + const userInputs = userInputsToArray(vmTemplate?.TEMPLATE?.USER_INPUTS, { + order: vmTemplate?.TEMPLATE?.INPUTS_ORDER, + }) + + return [ BasicConfiguration, - vmTemplate?.TEMPLATE?.USER_INPUTS && UserInputs, + !!userInputs.length && (() => UserInputs(userInputs)), ExtraConfiguration, - ].filter(Boolean), + ].filter(Boolean) + }, { transformInitialValue: (vmTemplate, schema) => { const initialValue = schema.cast( diff --git a/src/fireedge/src/client/components/HOC/AsyncLoadForm.js b/src/fireedge/src/client/components/HOC/AsyncLoadForm.js index b121a77660..4cb38d9b5c 100644 --- a/src/fireedge/src/client/components/HOC/AsyncLoadForm.js +++ b/src/fireedge/src/client/components/HOC/AsyncLoadForm.js @@ -124,4 +124,4 @@ AsyncLoadForm.propTypes = { componentToLoad: PropTypes.string, } -export default AsyncLoadForm +export { AsyncLoadForm } diff --git a/src/fireedge/src/client/components/HOC/index.js b/src/fireedge/src/client/components/HOC/index.js index 43c910d38d..5b9446f4ae 100644 --- a/src/fireedge/src/client/components/HOC/index.js +++ b/src/fireedge/src/client/components/HOC/index.js @@ -16,9 +16,6 @@ export { default as AuthLayout } from 'client/components/HOC/AuthLayout' export { default as ConditionalWrap } from 'client/components/HOC/ConditionalWrap' export { default as InternalLayout } from 'client/components/HOC/InternalLayout' -export { - default as AsyncLoadForm, - ConfigurationProps, -} from 'client/components/HOC/AsyncLoadForm' +export * from 'client/components/HOC/AsyncLoadForm' export * from 'client/components/HOC/Translate' diff --git a/src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js b/src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js index 850488a2e7..cc0e2f5bbb 100644 --- a/src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js +++ b/src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js @@ -41,17 +41,17 @@ const VmTemplateInfoTab = ({ tabProps = {}, id }) => { ownership_panel: ownershipPanel, } = tabProps - const [changeOwnership] = useChangeTemplatePermissionsMutation() - const [changePermissions] = useChangeTemplateOwnershipMutation() + const [changePermissions] = useChangeTemplatePermissionsMutation() + const [changeOwnership] = useChangeTemplateOwnershipMutation() const { data: template = {} } = useGetTemplateQuery({ id }) - const { ID, UNAME, UID, GNAME, GID, PERMISSIONS } = template + const { UNAME, UID, GNAME, GID, PERMISSIONS } = template const handleChangeOwnership = async (newOwnership) => { - await changeOwnership({ id: ID, ...newOwnership }) + await changeOwnership({ id, ...newOwnership }) } const handleChangePermission = async (newPermission) => { - await changePermissions({ id: ID, ...newPermission }) + await changePermissions({ id, ...newPermission }) } const getActions = (actions) => Helper.getActionsAvailable(actions) @@ -72,6 +72,7 @@ const VmTemplateInfoTab = ({ tabProps = {}, id }) => { {permissionsPanel?.enabled && ( { otherUse={PERMISSIONS.OTHER_U} otherManage={PERMISSIONS.OTHER_M} otherAdmin={PERMISSIONS.OTHER_A} - handleEdit={handleChangePermission} /> )} {ownershipPanel?.enabled && ( )} diff --git a/src/fireedge/src/client/components/Tabs/VmTemplate/Info/information.js b/src/fireedge/src/client/components/Tabs/VmTemplate/Info/information.js index 1555cebea0..e40ff977ee 100644 --- a/src/fireedge/src/client/components/Tabs/VmTemplate/Info/information.js +++ b/src/fireedge/src/client/components/Tabs/VmTemplate/Info/information.js @@ -24,7 +24,7 @@ import { timeToString, levelLockToString } from 'client/models/Helper' import { T, VM_TEMPLATE_ACTIONS, - LOGO_IMAGES_URL, + STATIC_FILES_URL, VmTemplate, } from 'client/constants' @@ -67,7 +67,7 @@ const InformationPanel = ({ template = {}, actions }) => { }, LOGO && { name: T.Logo, - value: logo, + value: logo, dataCy: 'logo', }, ].filter(Boolean) diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index 9158983f42..02670010b8 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -484,6 +484,9 @@ module.exports = { DefaultNicFilter: 'Default network filtering rule for all NICs', /* VM Template schema - capacity */ MaxMemory: 'Max memory', + MaxMemoryConcept: ` + This value sets the maximum value of the MEMORY allowed to be modified + after instantiation, through the Capacity Resize options of instantiated VMs`, MemoryModification: 'Memory modification', AllowUsersToModifyMemory: "Allow users to modify this template's default memory on instantiate", @@ -492,11 +495,15 @@ module.exports = { Percentage of CPU divided by 100 required for the Virtual Machine. Half a processor is written 0.5`, MaxVirtualCpu: 'Max Virtual CPU', + MaxVirtualCpuConcept: ` + This value sets the maximum value of the VCPU allowed to be modified + after instantiation, through the Capacity Resize options of instantiated VMs`, + CpuModification: 'CPU modification', + AllowUsersToModifyCpu: + "Allow users to modify this template's default CPU on instantiate", VirtualCpuConcept: ` Number of virtual cpus. This value is optional, the default hypervisor behavior is used, usually one virtual CPU`, - AllowUsersToModifyCpu: - "Allow users to modify this template's default CPU on instantiate", VirtualCpuModification: 'Virtual CPU modification', AllowUsersToModifyVirtualCpu: "Allow users to modify this template's default Virtual CPU on instantiate", diff --git a/src/fireedge/src/client/constants/userInput.js b/src/fireedge/src/client/constants/userInput.js index a2a8d906a8..5f97ef99b9 100644 --- a/src/fireedge/src/client/constants/userInput.js +++ b/src/fireedge/src/client/constants/userInput.js @@ -16,6 +16,7 @@ /** * @typedef {( + * 'fixed' | * 'text' | * 'text64' | * 'password' | @@ -64,6 +65,7 @@ /** @enum {UserInputType} User input types */ export const USER_INPUT_TYPES = { + fixed: 'fixed', text: 'text', text64: 'text64', password: 'password', diff --git a/src/fireedge/src/client/features/OneApi/vmTemplate.js b/src/fireedge/src/client/features/OneApi/vmTemplate.js index 84e0d11139..fea81b4a18 100644 --- a/src/fireedge/src/client/features/OneApi/vmTemplate.js +++ b/src/fireedge/src/client/features/OneApi/vmTemplate.js @@ -215,6 +215,34 @@ const vmTemplateApi = oneApi.injectEndpoints({ return { params, command } }, invalidatesTags: (_, __, { id }) => [{ type: TEMPLATE, id }], + async onQueryStarted( + { id, ...permissions }, + { dispatch, queryFulfilled } + ) { + const patchResult = dispatch( + vmTemplateApi.util.updateQueryData('getTemplate', { id }, (draft) => { + Object.entries(permissions) + .filter(([_, value]) => value !== '-1') + .forEach(([name, value]) => { + const ensuredName = { + ownerUse: 'OWNER_U', + ownerManage: 'OWNER_M', + ownerAdmin: 'OWNER_A', + groupUse: 'GROUP_U', + groupManage: 'GROUP_M', + groupAdmin: 'GROUP_A', + otherUse: 'OTHER_U', + otherManage: 'OTHER_M', + otherAdmin: 'OTHER_A', + }[name] + + draft.PERMISSIONS[ensuredName] = value + }) + }) + ) + + queryFulfilled.catch(patchResult.undo) + }, }), changeTemplateOwnership: builder.mutation({ /** @@ -235,6 +263,19 @@ const vmTemplateApi = oneApi.injectEndpoints({ return { params, command } }, invalidatesTags: (_, __, { id }) => [{ type: TEMPLATE, id }], + async onQueryStarted( + { id, user, group }, + { dispatch, queryFulfilled, getState } + ) { + const patchResult = dispatch( + vmTemplateApi.util.updateQueryData('getTemplate', id, (draft) => { + user > 0 && (draft.UID = user) + group > 0 && (draft.GID = group) + }) + ) + + queryFulfilled.catch(patchResult.undo) + }, }), renameTemplate: builder.mutation({ /** diff --git a/src/fireedge/src/client/models/Helper.js b/src/fireedge/src/client/models/Helper.js index 4503da7c98..691911df2b 100644 --- a/src/fireedge/src/client/models/Helper.js +++ b/src/fireedge/src/client/models/Helper.js @@ -339,7 +339,7 @@ export const getUserInputString = (userInput) => { // mandatory|type|description|range/options/' '|defaultValue const uiString = [mandatoryString, type, description] - range?.length > 0 + ;[USER_INPUT_TYPES.range, USER_INPUT_TYPES.rangeFloat].includes(type) ? uiString.push(range) : options?.length > 0 ? uiString.push(options.join(LIST_SEPARATOR)) @@ -352,16 +352,19 @@ export const getUserInputString = (userInput) => { * Get list of user inputs defined in OpenNebula template. * * @param {object} userInputs - List of user inputs in string format - * @param {string} [inputsOrder] - List separated by comma of input names + * @param {object} [options] - Options to filter user inputs + * @param {boolean} [options.filterCapacityInputs] + * - If false, will not filter capacity inputs: MEMORY, CPU, VCPU. By default `true` + * @param {string} [options.order] - List separated by comma of input names * @example * const userInputs = { * "INPUT-1": "O|text|Description1| |text1", * "INPUT-2": "M|text|Description2| |text2" * } * - * const inputsOrder = "INPUT-2,INPUT-1" + * const order = "INPUT-2,INPUT-1" * - * => userInputsToArray(userInputs, inputsOrder) => [{ + * => userInputsToArray(userInputs, { order }) => [{ * name: 'INPUT-1', * mandatory: false, * type: 'text', @@ -377,17 +380,33 @@ export const getUserInputString = (userInput) => { * }] * @returns {UserInputObject[]} User input object */ -export const userInputsToArray = (userInputs = {}, inputsOrder) => { - const orderedList = inputsOrder?.split(',') ?? [] +export const userInputsToArray = ( + userInputs = {}, + { filterCapacityInputs = true, order } = {} +) => { + const orderedList = order?.split(',') ?? [] + const userInputsArray = Object.entries(userInputs) - return Object.entries(userInputs) - .map(([name, ui]) => ({ name, ...getUserInputParams(ui) })) - .sort((a, b) => { + let list = userInputsArray.map(([name, ui]) => ({ + name: `${name}`.toUpperCase(), + ...(typeof ui === 'string' ? getUserInputParams(ui) : ui), + })) + + if (filterCapacityInputs) { + const capacityInputs = ['MEMORY', 'CPU', 'VCPU'] + list = list.filter((ui) => !capacityInputs.includes(ui.name)) + } + + if (orderedList.length) { + list = list.sort((a, b) => { const upperAName = a.name?.toUpperCase?.() const upperBName = b.name?.toUpperCase?.() return orderedList.indexOf(upperAName) - orderedList.indexOf(upperBName) }) + } + + return list } /** diff --git a/src/fireedge/src/client/utils/helpers.js b/src/fireedge/src/client/utils/helpers.js index f6cee39718..16c28f01ed 100644 --- a/src/fireedge/src/client/utils/helpers.js +++ b/src/fireedge/src/client/utils/helpers.js @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ +import { v4 as uuidv4 } from 'uuid' import DOMPurify from 'dompurify' import { object, reach, ObjectSchema, BaseSchema } from 'yup' import { isMergeableObject } from 'client/utils/merge' @@ -41,8 +42,7 @@ export const isExternalURL = (url) => /^(http|https):/g.test(url) * * @returns {string} Random key */ -export const generateKey = () => - String(new Date().getTime() + Math.random()).replace('.', '') +export const generateKey = () => uuidv4() /** * Sanitizes HTML and prevents XSS attacks. @@ -443,15 +443,6 @@ export const isBase64 = (stringToValidate, options = {}) => { return regex.test(stringToValidate) } -/** - * Check if value is divisible by 4. - * - * @param {string|number} value - Number to check - * @returns {boolean} Returns `true` if string is divisible by 4 - */ -export const isDivisibleBy4 = (value) => - /[048]|\d*([02468][048]|[13579][26])/g.test(value) - /** * Check if value is divisible by another number. * diff --git a/src/fireedge/src/client/utils/schema.js b/src/fireedge/src/client/utils/schema.js index ef08e9532e..7a9ee6c233 100644 --- a/src/fireedge/src/client/utils/schema.js +++ b/src/fireedge/src/client/utils/schema.js @@ -115,6 +115,8 @@ import { stringToBoolean } from 'client/models/Helper' * - Filters the field when the driver is not include on list * @property {TextFieldProps|CheckboxProps|InputBaseComponentProps} [fieldProps] * - Extra properties to material-ui field + * @property {boolean|DependOfCallback} [readOnly] + * - If `true`, the field is read only * @property {function(string|number):any} [renderValue] * - Render the current selected value inside selector input * - **Only for select inputs.** @@ -218,12 +220,8 @@ const getRange = (options) => options?.split?.('..').map(parseFloat) const getValuesFromArray = (options, separator = SEMICOLON_CHAR) => options?.split(separator) -const getOptionsFromList = (options) => - options - ?.map((option) => - typeof option === 'string' ? { text: option, value: option } : option - ) - ?.filter(({ text, value } = {}) => text && value) +const getOptionsFromList = (options = []) => + arrayToOptions([...new Set(options)]) const parseUserInputValue = (value) => { if (value === true) { @@ -257,6 +255,22 @@ export const schemaUserInput = ({ default: defaultValue, }) => { switch (type) { + case USER_INPUT_TYPES.fixed: { + const isNumeric = !isNaN(defaultValue) + const ensuredValue = isNumeric ? parseFloat(defaultValue) : defaultValue + const validation = isNumeric ? number() : string().trim() + + return { + type: INPUT_TYPES.TEXT, + htmlType: isNaN(+defaultValue) ? 'text' : 'number', + validation: validation + .default(ensuredValue) + // ensures to send the value + .afterSubmit(() => defaultValue), + fieldProps: { disabled: true }, + readOnly: true, + } + } case USER_INPUT_TYPES.text: case USER_INPUT_TYPES.text64: case USER_INPUT_TYPES.password: @@ -311,7 +325,8 @@ export const schemaUserInput = ({ } case USER_INPUT_TYPES.list: { const values = getOptionsFromList(options) - const firstOption = values?.[0]?.value ?? undefined + const optionValues = values.map(({ value }) => value).filter(Boolean) + const firstOption = optionValues[0] ?? undefined return { values, @@ -319,7 +334,7 @@ export const schemaUserInput = ({ validation: string() .trim() .concat(requiredSchema(mandatory, string())) - .oneOf(values.map(({ value }) => value)) + .oneOf(optionValues) .default(() => defaultValue || firstOption), } }