1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-22 18:50:08 +03:00

B OpenNebula/one#6229: fix sorter user inputs

Signed-off-by: Jorge Lobo <jlobo@opennebula.io>
This commit is contained in:
Jorge Miguel Lobo Escalona 2024-02-02 12:02:18 +01:00 committed by GitHub
parent bf30d70aaf
commit 0ef438bb8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 180 additions and 89 deletions

View File

@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { INPUT_TYPES, T, FEDERATION_TYPE } from 'client/constants'
import { Field, getObjectSchemaFromFields, arrayToOptions } from 'client/utils'
import { string } from 'yup'
import { ZonesTable } from 'client/components/Tables'
import { FEDERATION_TYPE, INPUT_TYPES, T } from 'client/constants'
import { Field, arrayToOptions, getObjectSchemaFromFields } from 'client/utils'
import { string } from 'yup'
const ACL_TYPE_ZONE_TRANSLATIONS = {
ALL: { value: 'ALL', text: T.All },
@ -66,11 +66,7 @@ const ZONE = (oneConfig) => ({
* @param {object} oneConfig - . ONE config
* @returns {Array} - The list of fields
*/
const FIELDS = (oneConfig) => {
console.log(oneConfig)
return [TYPE(oneConfig), ZONE(oneConfig)]
}
const FIELDS = (oneConfig) => [TYPE(oneConfig), ZONE(oneConfig)]
/**
* Return the schema.
@ -78,10 +74,6 @@ const FIELDS = (oneConfig) => {
* @param {object} oneConfig - . ONE config
* @returns {object} - The schema
*/
const SCHEMA = (oneConfig) => {
console.log(oneConfig)
const SCHEMA = (oneConfig) => getObjectSchemaFromFields(FIELDS(oneConfig))
return getObjectSchemaFromFields(FIELDS(oneConfig))
}
export { SCHEMA, FIELDS }
export { FIELDS, SCHEMA }

View File

@ -160,7 +160,7 @@ const FieldComponent = memo(
const addIdToName = useCallback(
(n) => {
// removes character '$' and returns
if (n.startsWith('$')) return n.slice(1)
if (n?.startsWith('$')) return n.slice(1)
// concat form ID if exists
return id ? `${id}.${n}` : n

View File

@ -13,20 +13,20 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { object, array, string, boolean, number, ref, ObjectSchema } from 'yup'
import { ObjectSchema, array, boolean, number, object, ref, string } from 'yup'
import { userInputsToObject, userInputsToArray } from 'client/models/Helper'
import {
UserInputType,
T,
INPUT_TYPES,
T,
USER_INPUT_TYPES,
UserInputType,
} from 'client/constants'
import { userInputsToArray, userInputsToObject } from 'client/models/Helper'
import {
Field,
arrayToOptions,
sentenceCase,
getObjectSchemaFromFields,
sentenceCase,
} from 'client/utils'
const {
@ -45,23 +45,6 @@ const { array: _array, fixed: _fixed, ...userInputTypes } = USER_INPUT_TYPES
/** @type {UserInputType[]} User inputs types */
const valuesOfUITypes = Object.values(userInputTypes)
/** @type {Field} Type field */
const TYPE = {
name: 'type',
label: T.Type,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(valuesOfUITypes, {
addEmpty: false,
getText: (type) => sentenceCase(type),
}),
validation: string()
.trim()
.required()
.oneOf(valuesOfUITypes)
.default(() => valuesOfUITypes[0]),
grid: { sm: 6, md: 4 },
}
/** @type {Field} Name field */
const NAME = {
name: 'name',
@ -74,6 +57,54 @@ const NAME = {
grid: { sm: 6, md: 4 },
}
/** @type {Field} Type field */
const TYPE = {
name: 'type',
label: T.Type,
type: INPUT_TYPES.SELECT,
dependOf: NAME.name,
values: (name) => {
let defaultValues = valuesOfUITypes
const sanitizedName = name?.trim()?.toLowerCase()
switch (sanitizedName) {
case 'memory':
defaultValues = [
userInputTypes.text,
userInputTypes.text64,
userInputTypes.number,
userInputTypes.range,
userInputTypes.list,
]
break
case 'cpu':
case 'vcpu':
defaultValues = [
userInputTypes.text,
userInputTypes.text64,
userInputTypes.number,
userInputTypes.numberFloat,
userInputTypes.range,
userInputTypes.rangeFloat,
userInputTypes.list,
]
break
default:
break
}
return arrayToOptions(defaultValues, {
addEmpty: false,
getText: (type) => sentenceCase(type),
})
},
validation: string()
.trim()
.required()
.oneOf(valuesOfUITypes)
.default(() => valuesOfUITypes[0]),
grid: { sm: 6, md: 4 },
}
/** @type {Field} Description field */
const DESCRIPTION = {
name: 'description',
@ -194,8 +225,8 @@ const MANDATORY = {
/** @type {Field[]} List of User Inputs fields */
export const USER_INPUT_FIELDS = [
TYPE,
NAME,
TYPE,
DESCRIPTION,
DEFAULT_VALUE,
OPTIONS,

View File

@ -27,12 +27,14 @@ import { getUserInputParams } from 'client/models/Helper'
import { scaleVcpuByCpuFactor } from 'client/models/VirtualMachine'
import {
Field,
OPTION_SORTERS,
isDivisibleBy,
prettyBytes,
schemaUserInput,
} from 'client/utils'
const { number, numberFloat, range, rangeFloat } = USER_INPUT_TYPES
const { number, numberFloat, range, rangeFloat, text, text64, password } =
USER_INPUT_TYPES
const TRANSLATES = {
MEMORY: {
@ -88,20 +90,29 @@ export const FIELDS = (
// set default type to number
userInput.type ??= isCPU ? numberFloat : number
const ensuredOptions = divisibleBy4
? options?.filter((value) => isDivisibleBy(+value, 4))
: options
const schemaUi = schemaUserInput({ options: ensuredOptions, ...userInput })
const schemaUserInputConfig = { options: ensuredOptions, ...userInput }
userInput?.type === 'list' &&
(schemaUserInputConfig.sorter = OPTION_SORTERS.numeric)
const schemaUi = schemaUserInput(schemaUserInputConfig)
const isNumber = schemaUi.validation instanceof NumberSchema
// add positive number validator
isNumber && (schemaUi.validation &&= schemaUi.validation.positive())
if (isMemory) {
schemaUi.type = INPUT_TYPES.UNITS
;[text, number, numberFloat, text64, password].includes(
userInput?.type
) && (schemaUi.type = INPUT_TYPES.UNITS)
if (isRange) {
TRANSLATES[
name
].tooltip = `${T.MemoryConcept} ${T.MemoryConceptUserInput} `
// add label format on pretty bytes
schemaUi.fieldProps = { ...schemaUi.fieldProps, valueLabelFormat }
}

View File

@ -13,20 +13,20 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo, useEffect } from 'react'
import PropTypes from 'prop-types'
import { useEffect, useMemo } from 'react'
import { useViews } from 'client/features/Auth'
import { useFormContext } from 'react-hook-form'
import { scaleVcpuByCpuFactor } from 'client/models/VirtualMachine'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import useStyles from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/styles'
import {
SCHEMA,
SECTIONS,
} from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema'
import useStyles from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/styles'
import { RESOURCE_NAMES, T, VmTemplate } from 'client/constants'
import { useViews } from 'client/features/Auth'
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
import { T, RESOURCE_NAMES, VmTemplate } from 'client/constants'
import { scaleVcpuByCpuFactor } from 'client/models/VirtualMachine'
import { useFormContext } from 'react-hook-form'
let generalFeatures
@ -94,7 +94,7 @@ Content.propTypes = {
* @param {VmTemplate} vmTemplate - VM Template
* @returns {object} Basic configuration step
*/
const BasicConfiguration = ({ data: vmTemplate, oneConfig, adminGroup }) => ({
const BasicConfiguration = ({ vmTemplate, oneConfig, adminGroup }) => ({
id: STEP_ID,
label: T.Configuration,
resolver: () => SCHEMA(vmTemplate, generalFeatures),

View File

@ -15,8 +15,8 @@
* ------------------------------------------------------------------------- */
import { BaseSchema } from 'yup'
import { FIELDS as INFORMATION_FIELDS } from './informationSchema'
import { FIELDS as CAPACITY_FIELDS } from './capacitySchema'
import { FIELDS as INFORMATION_FIELDS } from './informationSchema'
// import { FIELDS as DISK_FIELDS, SCHEMA as DISK_SCHEMA } from './diskSchema'
// get schemas from VmTemplate/CreateForm
@ -24,14 +24,14 @@ import { FIELDS as OWNERSHIP_FIELDS } from 'client/components/Forms/VmTemplate/C
import { VCENTER_FOLDER_FIELD } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/vcenterSchema'
import { FIELDS as VM_GROUP_FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/vmGroupSchema'
import { T, VmTemplate, VmTemplateFeatures } from 'client/constants'
import {
filterFieldsByHypervisor,
getObjectSchemaFromFields,
Field,
Section,
disableFields,
filterFieldsByHypervisor,
getObjectSchemaFromFields,
} from 'client/utils'
import { T, VmTemplate, VmTemplateFeatures } from 'client/constants'
/**
* @param {VmTemplate} [vmTemplate] - VM Template
@ -118,4 +118,4 @@ const FIELDS = (vmTemplate, hideCpu) =>
const SCHEMA = (vmTemplate, hideCpu) =>
getObjectSchemaFromFields(FIELDS(vmTemplate, hideCpu))
export { SECTIONS, FIELDS, SCHEMA }
export { FIELDS, SCHEMA, SECTIONS }

View File

@ -22,35 +22,64 @@ import ExtraConfiguration, {
import UserInputs, {
STEP_ID as USER_INPUTS_ID,
} from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/UserInputs'
import { jsonToXml, userInputsToArray } from 'client/models/Helper'
import {
getUserInputParams,
jsonToXml,
parseRangeToArray,
userInputsToArray,
} from 'client/models/Helper'
import { createSteps } from 'client/utils'
const Steps = createSteps(
(stepProps) => {
({ dataTemplateExtended = {}, ...rest }) => {
const userInputs = userInputsToArray(
stepProps?.dataTemplateExtended?.TEMPLATE?.USER_INPUTS,
dataTemplateExtended?.TEMPLATE?.USER_INPUTS,
{
order: stepProps?.dataTemplateExtended?.TEMPLATE?.INPUTS_ORDER,
order: dataTemplateExtended?.TEMPLATE?.INPUTS_ORDER,
}
)
return [
BasicConfiguration,
() => BasicConfiguration({ vmTemplate: dataTemplateExtended, ...rest }),
!!userInputs.length && (() => UserInputs(userInputs)),
ExtraConfiguration,
].filter(Boolean)
},
{
transformInitialValue: (vmTemplate, schema) => {
const initialValue = schema.cast(
// this delete values that are representated in USER_INPUTS
if (vmTemplate?.TEMPLATE?.USER_INPUTS) {
;['MEMORY', 'CPU', 'VCPU'].forEach((element) => {
if (vmTemplate?.TEMPLATE?.USER_INPUTS?.[element]) {
const valuesOfUserInput = getUserInputParams(
vmTemplate.TEMPLATE.USER_INPUTS[element]
)
if (valuesOfUserInput?.default) {
let options = valuesOfUserInput?.options
valuesOfUserInput?.type === 'range' &&
(options = parseRangeToArray(options[0], options[1]))
if (!options.includes(valuesOfUserInput.default)) {
delete vmTemplate?.TEMPLATE?.USER_INPUTS?.[element]
} else {
vmTemplate?.TEMPLATE?.[element] &&
delete vmTemplate?.TEMPLATE?.[element]
}
} else {
vmTemplate?.TEMPLATE?.[element] &&
delete vmTemplate?.TEMPLATE?.[element]
}
}
})
}
return schema.cast(
{
[BASIC_ID]: vmTemplate?.TEMPLATE,
[EXTRA_ID]: vmTemplate?.TEMPLATE,
},
{ stripUnknown: true }
)
return initialValue
},
transformBeforeSubmit: (formData, vmTemplate, _, adminGroup, oneConfig) => {
const {

View File

@ -895,6 +895,7 @@ module.exports = {
"Allow users to modify this template's default memory on instantiate",
MemoryConcept: 'Amount of RAM required for the VM',
MemoryConceptUnit: 'Choose unit of memory',
MemoryConceptUserInput: '(This value is represented in MB)',
CpuConcept: `
Percentage of CPU divided by 100 required for the
Virtual Machine. Half a processor is written 0.5`,

View File

@ -14,30 +14,30 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import { useHistory, useLocation, Redirect } from 'react-router'
import { Redirect, useHistory, useLocation } from 'react-router'
import { useGeneralApi } from 'client/features/General'
import {
useInstantiateTemplateMutation,
useGetTemplateQuery,
} from 'client/features/OneApi/vmTemplate'
import { useGetUsersQuery } from 'client/features/OneApi/user'
import { useGetGroupsQuery } from 'client/features/OneApi/group'
import { useGetUsersQuery } from 'client/features/OneApi/user'
import {
useGetTemplateQuery,
useInstantiateTemplateMutation,
} from 'client/features/OneApi/vmTemplate'
import { PATH } from 'client/apps/sunstone/routesOne'
import {
DefaultFormStepper,
SkeletonStepsForm,
} from 'client/components/FormStepper'
import { InstantiateForm } from 'client/components/Forms/VmTemplate'
import { PATH } from 'client/apps/sunstone/routesOne'
import {
addTempInfo,
deleteTempInfo,
deleteRestrictedAttributes,
} from 'client/utils'
import { useSystemData } from 'client/features/Auth'
import { jsonToXml, xmlToJson } from 'client/models/Helper'
import {
addTempInfo,
deleteRestrictedAttributes,
deleteTempInfo,
} from 'client/utils'
const _ = require('lodash')

View File

@ -450,6 +450,26 @@ export const getUserInputString = (userInput) => {
return uiString.concat(defaultValue).join(PARAMS_SEPARATOR)
}
/**
* Transform range value to array.
*
* @param {number} start - start number.
* @param {number} end - end number.
* @returns {Array} range transformed into array
*/
export const parseRangeToArray = (start, end) => {
const startNumber = parseInt(start, 10)
const endNumber = parseInt(end, 10)
if (startNumber === endNumber) return [startNumber]
const ans = []
for (let i = startNumber; i <= endNumber; i++) {
ans.push(`${i}`)
}
return ans
}
/**
* Get list of user inputs defined in OpenNebula template.
*
@ -501,8 +521,10 @@ export const userInputsToArray = (
if (orderedList.length) {
list = list.sort((a, b) => {
const upperAName = a.name?.toUpperCase?.()
const upperBName = b.name?.toUpperCase?.()
const valueA = parseInt(a.name, 10)
const valueB = parseInt(b.name, 10)
const upperAName = isNaN(valueA) ? valueA : a.name?.toUpperCase?.()
const upperBName = isNaN(valueB) ? valueB : b.name?.toUpperCase?.()
return orderedList.indexOf(upperAName) - orderedList.indexOf(upperBName)
})

View File

@ -19,29 +19,29 @@
import { ReactElement, SetStateAction } from 'react'
import {
// eslint-disable-next-line no-unused-vars
GridProps,
// eslint-disable-next-line no-unused-vars
TextFieldProps,
// eslint-disable-next-line no-unused-vars
CheckboxProps,
// eslint-disable-next-line no-unused-vars
GridProps,
// eslint-disable-next-line no-unused-vars
InputBaseComponentProps,
// eslint-disable-next-line no-unused-vars
TextFieldProps,
} from '@mui/material'
import { string, number, boolean, array, object, BaseSchema } from 'yup'
import { BaseSchema, array, boolean, number, object, string } from 'yup'
// eslint-disable-next-line no-unused-vars
import { Row } from 'react-table'
import {
UserInputObject,
T,
// eslint-disable-next-line no-unused-vars
HYPERVISORS,
INPUT_TYPES,
RESTRICTED_ATTRIBUTES_TYPE,
T,
USER_INPUT_TYPES,
UserInputObject,
// eslint-disable-next-line no-unused-vars
VN_DRIVERS,
INPUT_TYPES,
USER_INPUT_TYPES,
RESTRICTED_ATTRIBUTES_TYPE,
} from 'client/constants'
import { stringToBoolean } from 'client/models/Helper'
@ -224,8 +224,12 @@ const getRange = (options) => options?.split?.('..').map(parseFloat)
const getValuesFromArray = (options, separator = SEMICOLON_CHAR) =>
options?.split(separator)
const getOptionsFromList = (options = []) =>
arrayToOptions([...new Set(options)], { addEmpty: false })
const getOptionsFromList = (options = [], sorter) => {
const config = { addEmpty: false }
sorter && (config.sorter = sorter)
return arrayToOptions([...new Set(options)], config)
}
const parseUserInputValue = (value) => {
if (value === true) {
@ -257,6 +261,7 @@ export const schemaUserInput = ({
max,
options,
default: defaultValue,
sorter,
}) => {
switch (type) {
case USER_INPUT_TYPES.fixed: {
@ -328,7 +333,7 @@ export const schemaUserInput = ({
.yesOrNo(),
}
case USER_INPUT_TYPES.list: {
const values = getOptionsFromList(options)
const values = getOptionsFromList(options, sorter)
const optionValues = values.map(({ value }) => value).filter(Boolean)
const firstOption = optionValues[0] ?? undefined