From d69115283f817dadaaa6e9e54ae219b016d393b7 Mon Sep 17 00:00:00 2001 From: Jorge Miguel Lobo Escalona Date: Wed, 15 Nov 2023 16:26:28 +0100 Subject: [PATCH] B #6221: Add units selector MB..YB (#2819) --- .../FormControl/InformationUnitController.js | 186 ++++++++++++++++++ .../client/components/FormControl/index.js | 18 +- .../client/components/Forms/FormWithSchema.js | 1 + .../Forms/Vm/ResizeCapacityForm/schema.js | 4 +- .../Forms/Vm/ResizeDiskForm/schema.js | 9 +- .../Steps/General/capacitySchema.js | 2 +- .../BasicConfiguration/capacitySchema.js | 12 +- src/fireedge/src/client/constants/index.js | 3 +- .../src/client/constants/translates.js | 3 +- src/fireedge/src/client/utils/helpers.js | 15 +- 10 files changed, 228 insertions(+), 25 deletions(-) create mode 100644 src/fireedge/src/client/components/FormControl/InformationUnitController.js diff --git a/src/fireedge/src/client/components/FormControl/InformationUnitController.js b/src/fireedge/src/client/components/FormControl/InformationUnitController.js new file mode 100644 index 00000000000..3f141cbe08f --- /dev/null +++ b/src/fireedge/src/client/components/FormControl/InformationUnitController.js @@ -0,0 +1,186 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 PropTypes from 'prop-types' +import { memo, useCallback, useEffect, useState } from 'react' + +import { Grid, TextField } from '@mui/material' +import { useController, useWatch } from 'react-hook-form' + +import { ErrorHelper, Tooltip } from 'client/components/FormControl' +import { Tr, labelCanBeTranslated } from 'client/components/HOC' +import { T, UNITS } from 'client/constants' +import { generateKey, prettyBytes } from 'client/utils' + +const ARRAY_UNITS = Object.values(UNITS) +ARRAY_UNITS.splice(0, 1) // remove KB +const DEFAULT_UNIT = ARRAY_UNITS[0] + +const valueInMB = (value = 0, unit = DEFAULT_UNIT) => { + const idxUnit = ARRAY_UNITS.indexOf(unit) + const numberValue = +value + + return Math.round(numberValue * (idxUnit <= 0 ? 1 : 1024 ** idxUnit)) +} + +const InformationUnitController = memo( + ({ + control, + cy = `input-${generateKey()}`, + name = '', + label = '', + tooltip, + watcher, + dependencies, + fieldProps = {}, + readOnly = false, + onConditionChange, + }) => { + const watch = useWatch({ + name: dependencies, + disabled: dependencies == null, + defaultValue: Array.isArray(dependencies) ? [] : undefined, + }) + + const { + field: { ref, value = '', onChange, ...inputProps }, + fieldState: { error }, + } = useController({ name, control }) + + useEffect(() => { + if (!watcher || !dependencies || !watch) return + + const watcherValue = watcher(watch) + watcherValue !== undefined && onChange(watcherValue) + }, [watch, watcher, dependencies]) + + const [internalValue, setInternalValue] = useState(+value) + const [unit, setUnit] = useState(DEFAULT_UNIT) + + useEffect(() => { + const dataUnits = prettyBytes(value, DEFAULT_UNIT, 2, true) + setInternalValue(dataUnits.value) + setUnit(dataUnits.units) + }, [value]) + + const handleChange = useCallback( + (internalType, valueInput) => { + if (internalType === 'value') { + setInternalValue(valueInput) + } else { + setUnit(valueInput) + } + + const valueMB = + internalType === 'value' + ? valueInMB(valueInput, unit) + : valueInMB(internalValue, valueInput) + + onChange(valueMB) + if (typeof onConditionChange === 'function') { + onConditionChange(valueMB) + } + }, + [onChange, onConditionChange] + ) + + return ( +
+ + + handleChange('value', e.target.value)} + rows={3} + type="number" + label={labelCanBeTranslated(label) ? Tr(label) : label} + InputProps={{ + readOnly, + endAdornment: tooltip && , + }} + inputProps={{ + 'data-cy': cy, + ...{ + min: fieldProps.min, + max: fieldProps.max, + step: fieldProps.step, + }, + }} + error={Boolean(error)} + helperText={ + error ? ( + + ) : ( + fieldProps.helperText + ) + } + FormHelperTextProps={{ 'data-cy': `${cy}-error` }} + {...fieldProps} + /> + + + handleChange('unit', e.target.value)} + > + {ARRAY_UNITS.map((option, index) => ( + + ))} + + + +
+ ) + }, + (prevProps, nextProps) => + prevProps.type === nextProps.type && + prevProps.label === nextProps.label && + prevProps.tooltip === nextProps.tooltip && + prevProps.fieldProps?.value === nextProps.fieldProps?.value && + prevProps.fieldProps?.helperText === nextProps.fieldProps?.helperText && + prevProps.readOnly === nextProps.readOnly +) + +InformationUnitController.propTypes = { + control: PropTypes.object, + cy: PropTypes.string, + type: PropTypes.string, + multiline: PropTypes.bool, + name: PropTypes.string.isRequired, + label: PropTypes.any, + tooltip: PropTypes.any, + watcher: PropTypes.func, + dependencies: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string), + ]), + fieldProps: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), + readOnly: PropTypes.bool, + onConditionChange: PropTypes.func, +} + +InformationUnitController.displayName = 'InformationUnitController' + +export default InformationUnitController diff --git a/src/fireedge/src/client/components/FormControl/index.js b/src/fireedge/src/client/components/FormControl/index.js index 6443fa21c97..ed12418d42f 100644 --- a/src/fireedge/src/client/components/FormControl/index.js +++ b/src/fireedge/src/client/components/FormControl/index.js @@ -16,6 +16,7 @@ import AutocompleteController from 'client/components/FormControl/AutocompleteController' import CheckboxController from 'client/components/FormControl/CheckboxController' import FileController from 'client/components/FormControl/FileController' +import InformationUnitController from 'client/components/FormControl/InformationUnitController' import PasswordController from 'client/components/FormControl/PasswordController' import SelectController from 'client/components/FormControl/SelectController' import SliderController from 'client/components/FormControl/SliderController' @@ -25,30 +26,31 @@ import TextController from 'client/components/FormControl/TextController' import TimeController from 'client/components/FormControl/TimeController' import ToggleController from 'client/components/FormControl/ToggleController' +import DockerfileController from 'client/components/FormControl/DockerfileController' +import ErrorHelper from 'client/components/FormControl/ErrorHelper' +import InputCode from 'client/components/FormControl/InputCode' import SubmitButton, { SubmitButtonPropTypes, } from 'client/components/FormControl/SubmitButton' -import InputCode from 'client/components/FormControl/InputCode' -import DockerfileController from 'client/components/FormControl/DockerfileController' -import ErrorHelper from 'client/components/FormControl/ErrorHelper' import Tooltip from 'client/components/FormControl/Tooltip' export { AutocompleteController, CheckboxController, + DockerfileController, + ErrorHelper, FileController, + InformationUnitController, + InputCode, PasswordController, SelectController, SliderController, + SubmitButton, + SubmitButtonPropTypes, SwitchController, TableController, TextController, TimeController, ToggleController, - SubmitButton, - SubmitButtonPropTypes, - InputCode, - DockerfileController, - ErrorHelper, Tooltip, } diff --git a/src/fireedge/src/client/components/Forms/FormWithSchema.js b/src/fireedge/src/client/components/Forms/FormWithSchema.js index c501a7c2159..2f34426a2f2 100644 --- a/src/fireedge/src/client/components/Forms/FormWithSchema.js +++ b/src/fireedge/src/client/components/Forms/FormWithSchema.js @@ -56,6 +56,7 @@ const INPUT_CONTROLLER = { [INPUT_TYPES.TABLE]: FC.TableController, [INPUT_TYPES.TOGGLE]: FC.ToggleController, [INPUT_TYPES.DOCKERFILE]: FC.DockerfileController, + [INPUT_TYPES.UNITS]: FC.InformationUnitController, } /** diff --git a/src/fireedge/src/client/components/Forms/Vm/ResizeCapacityForm/schema.js b/src/fireedge/src/client/components/Forms/Vm/ResizeCapacityForm/schema.js index 26f8f16ef44..1014dbc8ad6 100644 --- a/src/fireedge/src/client/components/Forms/Vm/ResizeCapacityForm/schema.js +++ b/src/fireedge/src/client/components/Forms/Vm/ResizeCapacityForm/schema.js @@ -28,9 +28,9 @@ const ENFORCE = { const MEMORY = { name: 'MEMORY', - label: [T.MemoryWithUnit, '(MB)'], + label: T.Memory, tooltip: T.MemoryConcept, - type: INPUT_TYPES.TEXT, + type: INPUT_TYPES.UNITS, htmlType: 'number', validation: number() .required() diff --git a/src/fireedge/src/client/components/Forms/Vm/ResizeDiskForm/schema.js b/src/fireedge/src/client/components/Forms/Vm/ResizeDiskForm/schema.js index efd5e908a71..9f9ddccc1f1 100644 --- a/src/fireedge/src/client/components/Forms/Vm/ResizeDiskForm/schema.js +++ b/src/fireedge/src/client/components/Forms/Vm/ResizeDiskForm/schema.js @@ -13,15 +13,16 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { number, object } from 'yup' +import { INPUT_TYPES, T } from 'client/constants' import { getValidationFromFields } from 'client/utils' -import { T, INPUT_TYPES } from 'client/constants' +import { number, object } from 'yup' const SIZE = { name: 'SIZE', - label: [T.SizeOnUnits, 'MB'], - type: INPUT_TYPES.TEXT, + label: T.Size, + type: INPUT_TYPES.UNITS, htmlType: 'number', + grid: { md: 12 }, validation: number() .required() .positive() 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 748ea5c2572..38595ae22d1 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 @@ -47,7 +47,7 @@ const { vcenter, lxc, firecracker } = HYPERVISORS export const MEMORY = generateCapacityInput({ name: 'MEMORY', label: T.Memory, - tooltip: T.MemoryConceptWithoutUnit, + tooltip: T.MemoryConcept, validation: commonValidation .integer() .required() 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 24218e02320..16a05661a55 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 @@ -17,6 +17,7 @@ import { NumberSchema } from 'yup' import { HYPERVISORS, + INPUT_TYPES, T, USER_INPUT_TYPES, VmTemplate, @@ -36,7 +37,7 @@ const { number, numberFloat, range, rangeFloat } = USER_INPUT_TYPES const TRANSLATES = { MEMORY: { name: 'MEMORY', - label: [T.MemoryWithUnit, '(MB)'], + label: T.Memory, tooltip: T.MemoryConcept, }, CPU: { name: 'CPU', label: T.PhysicalCpuWithPercent, tooltip: T.CpuConcept }, @@ -98,9 +99,12 @@ export const FIELDS = ( // add positive number validator isNumber && (schemaUi.validation &&= schemaUi.validation.positive()) - if (isMemory && isRange) { - // add label format on pretty bytes - schemaUi.fieldProps = { ...schemaUi.fieldProps, valueLabelFormat } + if (isMemory) { + schemaUi.type = INPUT_TYPES.UNITS + if (isRange) { + // add label format on pretty bytes + schemaUi.fieldProps = { ...schemaUi.fieldProps, valueLabelFormat } + } } if (isNumber && divisibleBy4) { diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js index 9b68b42fa1f..1d6cb79d5ec 100644 --- a/src/fireedge/src/client/constants/index.js +++ b/src/fireedge/src/client/constants/index.js @@ -140,6 +140,7 @@ export const INPUT_TYPES = { TABLE: 'table', TOGGLE: 'toggle', DOCKERFILE: 'dockerfile', + UNITS: 'units', } export const DEBUG_LEVEL = { @@ -209,6 +210,6 @@ export * from 'client/constants/user' export * from 'client/constants/userInput' export * from 'client/constants/vdc' export * from 'client/constants/vm' -export * from 'client/constants/vmTemplate' export * from 'client/constants/vmGroup' +export * from 'client/constants/vmTemplate' export * from 'client/constants/zone' diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index e419cace98f..4225c078adf 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -852,8 +852,7 @@ module.exports = { MemoryModification: 'Memory modification', AllowUsersToModifyMemory: "Allow users to modify this template's default memory on instantiate", - MemoryConcept: 'Amount of RAM required for the VM, in Megabytes', - MemoryConceptWithoutUnit: 'Amount of RAM required for the VM', + MemoryConcept: 'Amount of RAM required for the VM', MemoryConceptUnit: 'Choose unit of memory', CpuConcept: ` Percentage of CPU divided by 100 required for the diff --git a/src/fireedge/src/client/utils/helpers.js b/src/fireedge/src/client/utils/helpers.js index 737321538ac..64ca7b52af6 100644 --- a/src/fireedge/src/client/utils/helpers.js +++ b/src/fireedge/src/client/utils/helpers.js @@ -123,13 +123,20 @@ export const downloadFile = (file) => { * @param {'KB'|'MB'|'GB'|'TB'|'PB'|'EB'|'ZB'|'YB'} unit - The unit of value. Defaults in KB * @param {number} fractionDigits * - Number of digits after the decimal point. Must be in the range 0 - 20, inclusive + * @param {boolean} json - return a json with data * @returns {string} Returns an string displaying sizes for humans. */ -export const prettyBytes = (value, unit = UNITS.KB, fractionDigits = 0) => { +export const prettyBytes = ( + value, + unit = UNITS.KB, + fractionDigits = 0, + json = false +) => { const units = Object.values(UNITS) let ensuredValue = +value - if (Math.abs(ensuredValue) === 0) return `${value} ${units[0]}` + if (Math.abs(ensuredValue) === 0) + return json ? { value, units: unit } : `${value} ${units[0]}` let idxUnit = units.indexOf(unit) @@ -140,7 +147,9 @@ export const prettyBytes = (value, unit = UNITS.KB, fractionDigits = 0) => { const decimals = fractionDigits && ensuredValue % 1 !== 0 ? fractionDigits : 0 - return `${ensuredValue.toFixed(decimals)} ${units[idxUnit]}` + return json + ? { value: ensuredValue.toFixed(decimals), units: units[idxUnit] } + : `${ensuredValue.toFixed(decimals)} ${units[idxUnit]}` } /**