From a6744fb8eb2bdd2257b7a4caa79caea9c6100938 Mon Sep 17 00:00:00 2001 From: Sergio Betanzos Date: Wed, 10 Nov 2021 10:23:41 +0100 Subject: [PATCH] F #5422: Add new sections to VM template form (#1566) --- .../components/FormControl/TextController.js | 11 +- .../client/components/Forms/FormWithSchema.js | 2 +- .../Steps/ExtraConfiguration/booting/index.js | 7 +- .../context/configurationSchema.js | 117 +++++++++++ .../context/configurationSection.js | 82 ++++++++ .../Steps/ExtraConfiguration/context/index.js | 174 +--------------- .../ExtraConfiguration/context/schema.js | 163 +-------------- .../context/userInputsSchema.js | 187 +++++++++++++++++ .../context/userInputsSection.js | 191 ++++++++++++++++++ .../inputOutput/inputsSection.js | 3 +- .../inputOutput/pciDevicesSchema.js | 6 +- .../inputOutput/pciDevicesSection.js | 11 +- .../Steps/ExtraConfiguration/schema.js | 4 +- .../VmTemplate/CreateForm/Steps/index.js | 15 +- .../src/client/components/Header/User.js | 2 +- .../components/Sidebar/SidebarCollapseItem.js | 43 ++-- .../client/components/Sidebar/SidebarLink.js | 57 ++---- .../src/client/components/Sidebar/index.js | 5 +- .../src/client/components/Sidebar/styles.js | 28 +-- .../Enhanced/Utils/GlobalSelectedRows.js | 2 +- .../client/components/Tabs/Vm/Network/Item.js | 15 +- .../client/components/Tabs/Vm/Network/List.js | 4 +- .../components/Typography/DevTypography.js | 23 +-- .../src/client/constants/translates.js | 22 +- .../src/client/models/VirtualMachine.js | 4 +- src/fireedge/src/client/utils/helpers.js | 4 +- src/fireedge/src/client/utils/schema.js | 27 +-- 27 files changed, 731 insertions(+), 478 deletions(-) create mode 100644 src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/configurationSchema.js create mode 100644 src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/configurationSection.js create mode 100644 src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/userInputsSchema.js create mode 100644 src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/userInputsSection.js diff --git a/src/fireedge/src/client/components/FormControl/TextController.js b/src/fireedge/src/client/components/FormControl/TextController.js index 9bf97c141b..c7a0e625ab 100644 --- a/src/fireedge/src/client/components/FormControl/TextController.js +++ b/src/fireedge/src/client/components/FormControl/TextController.js @@ -68,7 +68,7 @@ const TextController = memo( InputProps={{ endAdornment: tooltip && }} - inputProps={{ 'data-cy': cy, ...fieldProps }} + inputProps={{ 'data-cy': cy }} error={Boolean(error)} helperText={Boolean(error) && } FormHelperTextProps={{ 'data-cy': `${cy}-error` }} @@ -97,14 +97,7 @@ TextController.propTypes = { PropTypes.string, PropTypes.arrayOf(PropTypes.string) ]), - fieldProps: PropTypes.object, - formContext: PropTypes.shape({ - setValue: PropTypes.func, - setError: PropTypes.func, - clearErrors: PropTypes.func, - watch: PropTypes.func, - register: PropTypes.func - }) + fieldProps: PropTypes.object } TextController.displayName = 'TextController' diff --git a/src/fireedge/src/client/components/Forms/FormWithSchema.js b/src/fireedge/src/client/components/Forms/FormWithSchema.js index 132d528259..2dba641926 100644 --- a/src/fireedge/src/client/components/Forms/FormWithSchema.js +++ b/src/fireedge/src/client/components/Forms/FormWithSchema.js @@ -54,7 +54,7 @@ const FormWithSchema = ({ id, cy, fields, rootProps, className, legend, legendTo const getFields = useMemo(() => typeof fields === 'function' ? fields() : fields, []) - if (getFields.length === 0) return null + if (!getFields || getFields?.length === 0) return null const addIdToName = name => name.startsWith('$') ? name.slice(1) // removes character '$' and returns diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting/index.js index e9831ee63d..467848491f 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting/index.js @@ -15,7 +15,7 @@ * ------------------------------------------------------------------------- */ import { useMemo } from 'react' import PropTypes from 'prop-types' -import { Stack, Box } from '@mui/material' +import { Stack, FormControl } from '@mui/material' import { SystemShut as OsIcon } from 'iconoir-react' import FormWithSchema from 'client/components/Forms/FormWithSchema' @@ -44,10 +44,11 @@ const Booting = ({ hypervisor, ...props }) => { !!props.data?.[STORAGE_ID]?.length || !!props.data?.[NIC_ID]?.length ) && ( - + + - + )} {sections.map(({ id, ...section }) => ( { + if (typeof value === 'boolean') return value + + return String(value).toUpperCase() === 'YES' + }), + grid: { md: 12 } +} + +/** @type {Field} SSH public key field */ +export const SSH_PUBLIC_KEY = { + name: 'CONTEXT.SSH_PUBLIC_KEY', + label: T.SshPublicKey, + type: INPUT_TYPES.TEXT, + multiline: true, + validation: string() + .trim() + .notRequired(), + grid: { md: 12 }, + fieldProps: { rows: 4 } +} + +/** @type {Field} Network context field */ +const NETWORK = { + name: 'CONTEXT.NETWORK', + label: T.AddNetworkContextualization, + tooltip: T.AddNetworkContextualizationConcept, + ...switchField +} + +/** @type {Field} Token OneGate token field */ +const TOKEN = { + name: 'CONTEXT.TOKEN', + label: T.AddOneGateToken, + tooltip: T.AddOneGateTokenConcept, + ...switchField +} + +/** @type {Field} Report READY to OneGate field */ +const REPORT_READY = { + name: 'CONTEXT.REPORT_READY', + label: T.ReportReadyToOneGate, + tooltip: T.ReportReadyToOneGateConcept, + ...switchField +} + +/** @type {Field} Encode start script field */ +export const ENCODE_START_SCRIPT = { + name: 'CONTEXT.ENCODE_START_SCRIPT', + label: T.EncodeScriptInBase64, + ...switchField, + validation: boolean() + .transform(value => Boolean(value)) + .default(() => ref('$extra.CONTEXT.START_SCRIPT_BASE64')) +} + +/** @type {Field} Start script field */ +export const START_SCRIPT = { + name: 'CONTEXT.START_SCRIPT', + label: T.StartScript, + tooltip: T.StartScriptConcept, + type: INPUT_TYPES.TEXT, + multiline: true, + validation: string() + .trim() + .notRequired() + .when( + '$extra.CONTEXT.START_SCRIPT_BASE64', + (scriptEncoded, schema) => scriptEncoded + ? schema.default(() => decodeURIComponent(escape(atob(scriptEncoded)))) + : schema + ), + grid: { md: 12 }, + fieldProps: { rows: 4 } +} + +/** @type {Field} Start script in base64 field */ +export const START_SCRIPT_BASE64 = { + name: 'CONTEXT.START_SCRIPT_BASE64', + validation: string().strip() +} + +export const SCRIPT_FIELDS = [START_SCRIPT, ENCODE_START_SCRIPT] + +/** @type {Field[]} List of other fields */ +export const OTHER_FIELDS = [NETWORK, TOKEN, REPORT_READY] + +/** @type {ObjectSchema} User context configuration schema */ +export const CONFIGURATION_SCHEMA = getObjectSchemaFromFields([ + SSH_PUBLIC_KEY, + START_SCRIPT_BASE64, + ...SCRIPT_FIELDS, + ...OTHER_FIELDS +]) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/configurationSection.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/configurationSection.js new file mode 100644 index 0000000000..76e66c0b2c --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/configurationSection.js @@ -0,0 +1,82 @@ +/* ------------------------------------------------------------------------- * + * 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 { JSXElementConstructor } from 'react' +import { Stack, FormControl, Button } from '@mui/material' +import { useFormContext } from 'react-hook-form' + +import { FormWithSchema, Legend } from 'client/components/Forms' + +import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration' +import { SSH_PUBLIC_KEY, SCRIPT_FIELDS, OTHER_FIELDS } from './schema' +import { T } from 'client/constants' + +export const SECTION_ID = 'CONTEXT' + +const SSH_KEY_USER = '$USER[SSH_PUBLIC_KEY]' + +/** @returns {JSXElementConstructor} - Configuration section */ +const ConfigurationSection = () => { + const { setValue, getValues } = useFormContext() + const SSH_PUBLIC_KEY_PATH = `${EXTRA_ID}.${SSH_PUBLIC_KEY.name}` + + const handleClearKey = () => setValue(SSH_PUBLIC_KEY_PATH) + + const handleAddUserKey = () => { + const currentSshPublicKey = getValues(SSH_PUBLIC_KEY_PATH) ?? '' + + setValue(SSH_PUBLIC_KEY_PATH, currentSshPublicKey + `\n${SSH_KEY_USER}`) + } + + return ( + + + + +
+ + + + + +
+ +
+
+ ) +} + +export default ConfigurationSection diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/index.js index 6e4b8f963b..a032ebad49 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/index.js @@ -13,177 +13,23 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { forwardRef } from 'react' import PropTypes from 'prop-types' -import { - Folder as ContextIcon, - WarningCircledOutline as WarningIcon, - DeleteCircledOutline, - AddCircledOutline -} from 'iconoir-react' -import { - styled, - FormControl, - Stack, - IconButton, - Button, - Divider, - List, - ListItem, - ListItemIcon, - ListItemText -} from '@mui/material' +import { Folder as ContextIcon } from 'iconoir-react' -import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd' -import { useFieldArray, useForm, FormProvider, useFormContext, get } from 'react-hook-form' -import { yupResolver } from '@hookform/resolvers/yup' +import { TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration' +import ConfigurationSection, { SECTION_ID as CONFIGURATION_ID } from './configurationSection' +import UserInputsSection, { SECTION_ID as USER_INPUTS_ID } from './userInputsSection' -import { Tooltip } from 'client/components/FormControl' -import { FormWithSchema, Legend } from 'client/components/Forms' -import { Translate } from 'client/components/HOC' - -import { STEP_ID as EXTRA_ID, TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration' -import { FIELDS, USER_INPUT_SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/schema' -import { getUserInputString } from 'client/models/Helper' import { T } from 'client/constants' -export const TAB_ID = 'USER_INPUTS' - -const UserItemDraggable = styled(ListItem)(({ theme }) => ({ - '&:before': { - content: "''", - display: 'block', - width: 16, - height: 10, - background: `linear-gradient( - to bottom, - ${theme.palette.action.active} 4px, - transparent 4px, - transparent 6px, - ${theme.palette.action.active} 6px - )` - } -})) - -const UserInputItem = forwardRef(({ - removeAction, - error, - userInput: { name, ...ui } = {}, - ...props -}, ref) => ( - - - - } - sx={{ '&:hover': { bgcolor: 'action.hover' } }} - {...props} - > - {!!error && ( - - - - - - )} - - -)) - -UserInputItem.propTypes = { - removeAction: PropTypes.func, - error: PropTypes.object, - userInput: PropTypes.object -} - -UserInputItem.displayName = 'UserInputItem' +export const TAB_ID = [CONFIGURATION_ID, USER_INPUTS_ID] const Context = () => { - const { formState: { errors } } = useFormContext() - const { fields: userInputs, append, remove, move } = useFieldArray({ - name: `${EXTRA_ID}.${TAB_ID}` - }) - - const methods = useForm({ - defaultValues: USER_INPUT_SCHEMA.default(), - resolver: yupResolver(USER_INPUT_SCHEMA) - }) - - const onSubmit = newInput => { - append(newInput) - methods.reset() - } - - /** @param {DropResult} result - Drop result */ - const onDragEnd = result => { - const { destination, source } = result ?? {} - - if (destination && destination.index !== source.index) { - move(source.index, destination.index) - } - } - return ( - - - - - - - - - - - - {({ droppableProps, innerRef: outerRef, placeholder }) => ( - - {userInputs?.map(({ id, ...userInput }, index) => ( - - {({ draggableProps, dragHandleProps, innerRef }) => ( - remove(index)} - {...draggableProps} - {...dragHandleProps} - /> - )} - - ))} - {placeholder} - - )} - - - + <> + + + ) } @@ -200,7 +46,7 @@ const TAB = { name: T.Context, icon: ContextIcon, Content: Context, - getError: error => !!error?.[TAB_ID] + getError: error => TAB_ID.some(id => error?.[id]) } export default TAB diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/schema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/schema.js index 3a720280b4..129b20ac26 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/schema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/schema.js @@ -13,161 +13,14 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { array, string, boolean, number, ref } from 'yup' +import { object } from 'yup' -import { UserInputType, T, INPUT_TYPES, USER_INPUT_TYPES } from 'client/constants' -import { Field, arrayToOptions, sentenceCase, getObjectSchemaFromFields } from 'client/utils' +import { USER_INPUTS_SCHEMA } from './userInputsSchema' +import { CONFIGURATION_SCHEMA } from './configurationSchema' -const { - password: uiPassword, - list: uiList, - listMultiple: uiListMultiple, - number: uiNumber, - numberFloat: uiNumberFloat, - range: uiRange, - rangeFloat: uiRangeFloat, - boolean: uiBoolean -} = USER_INPUT_TYPES +export const SCHEMA = object() + .concat(CONFIGURATION_SCHEMA) + .concat(USER_INPUTS_SCHEMA) -const { array: _, ...userInputTypes } = USER_INPUT_TYPES - -/** @type {UserInputType[]} User inputs types */ -const valuesOfUITypes = Object.values(userInputTypes) - -/** @type {Field} Type field */ -export 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 */ -export const NAME = { - name: 'name', - label: T.Name, - type: INPUT_TYPES.TEXT, - validation: string() - .trim() - .required() - .default(() => undefined), - grid: { sm: 6, md: 4 } -} - -/** @type {Field} Description field */ -export const DESCRIPTION = { - name: 'description', - label: T.Description, - type: INPUT_TYPES.TEXT, - validation: string() - .trim() - .notRequired() - .default(() => undefined), - grid: { sm: 6, md: 4 } -} - -/** @type {Field} Options field */ -const OPTIONS = { - name: 'options', - label: T.Options, - tooltip: 'Press ENTER key to add a value', - dependOf: TYPE.name, - type: INPUT_TYPES.AUTOCOMPLETE, - multiple: true, - htmlType: type => - ![uiList, uiListMultiple].includes(type) && INPUT_TYPES.HIDDEN, - validation: array(string().trim()) - .default(() => []) - .when( - TYPE.name, - (type, schema) => [uiList, uiListMultiple].includes(type) - ? schema.required() - : schema.strip().notRequired() - ), - fieldProps: { - freeSolo: true, - placeholder: 'optA,optB,optC' - }, - grid: { md: 8 } -} - -/** @type {{ MIN: Field, MAX: Field }} Range fields */ -const { MIN, MAX } = (() => { - const validation = number() - .positive() - .default(() => undefined) - .when(TYPE.name, (type, schema) => [uiRange, uiRangeFloat].includes(type) - ? schema.required() - : schema.strip().notRequired() - ) - - const common = { - dependOf: TYPE.name, - type: INPUT_TYPES.TEXT, - htmlType: type => - [uiRange, uiRangeFloat].includes(type) ? 'number' : INPUT_TYPES.HIDDEN, - grid: { sm: 6, md: 4 }, - fieldProps: type => ({ step: type === uiRangeFloat ? 0.01 : 1 }) - } - - return { - MIN: { ...common, name: 'min', label: T.Min, validation: validation.lessThan(ref('max')) }, - MAX: { ...common, name: 'max', label: T.Max, validation: validation.moreThan(ref('min')) } - } -})() - -/** @type {Field} Default value field */ -const DEFAULT_VALUE = { - name: 'default', - label: T.DefaultValue, - dependOf: [TYPE.name, OPTIONS.name], - type: ([type] = []) => [uiBoolean, uiList, uiListMultiple].includes(type) - ? INPUT_TYPES.SELECT - : INPUT_TYPES.TEXT, - htmlType: ([type] = []) => ({ - [uiNumber]: 'number', - [uiNumberFloat]: 'number', - [uiPassword]: INPUT_TYPES.HIDDEN - }[type]), - multiple: ([type] = []) => type === uiListMultiple, - values: ([type, options = []] = []) => type === uiBoolean - ? arrayToOptions(['NO', 'YES']) - : arrayToOptions(options), - validation: string() - .trim() - .default(() => undefined) - .when([TYPE.name, OPTIONS.name], (type, options = [], schema) => { - return { - [uiList]: schema.oneOf(options).notRequired(), - [uiListMultiple]: schema.includesInOptions(options), - [uiRange]: number().min(ref(MIN.name)).max(ref(MAX.name)).integer(), - [uiRangeFloat]: number().min(ref(MIN.name)).max(ref(MAX.name)), - [uiPassword]: schema.strip().notRequired() - }[type] ?? schema - }), - grid: { sm: 6, md: 4 } -} - -/** @type {Field} Mandatory field */ -const MANDATORY = { - name: 'mandatory', - label: T.Mandatory, - type: INPUT_TYPES.SWITCH, - validation: boolean().default(() => false), - grid: { md: 12 } -} - -export const FIELDS = [TYPE, NAME, DESCRIPTION, DEFAULT_VALUE, OPTIONS, MIN, MAX, MANDATORY] - -export const USER_INPUT_SCHEMA = getObjectSchemaFromFields(FIELDS) - -export const SCHEMA = array(USER_INPUT_SCHEMA).ensure() +export * from './userInputsSchema' +export * from './configurationSchema' 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 new file mode 100644 index 0000000000..0a816df081 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/userInputsSchema.js @@ -0,0 +1,187 @@ +/* ------------------------------------------------------------------------- * + * 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 { object, array, string, boolean, number, ref, ObjectSchema } from 'yup' + +import { UserInputType, T, INPUT_TYPES, USER_INPUT_TYPES } from 'client/constants' +import { Field, arrayToOptions, sentenceCase, getObjectSchemaFromFields } from 'client/utils' + +const { + password: uiPassword, + list: uiList, + listMultiple: uiListMultiple, + number: uiNumber, + numberFloat: uiNumberFloat, + range: uiRange, + rangeFloat: uiRangeFloat, + boolean: uiBoolean +} = USER_INPUT_TYPES + +const { array: _, ...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', + label: T.Name, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .required() + .default(() => undefined), + grid: { sm: 6, md: 4 } +} + +/** @type {Field} Description field */ +const DESCRIPTION = { + name: 'description', + label: T.Description, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .notRequired() + .default(() => undefined), + grid: { sm: 6, md: 4 } +} + +/** @type {Field} Options field */ +const OPTIONS = { + name: 'options', + label: T.Options, + tooltip: 'Press ENTER key to add a value', + dependOf: TYPE.name, + type: INPUT_TYPES.AUTOCOMPLETE, + multiple: true, + htmlType: type => + ![uiList, uiListMultiple].includes(type) && INPUT_TYPES.HIDDEN, + validation: array(string().trim()) + .default(() => []) + .when( + TYPE.name, + (type, schema) => [uiList, uiListMultiple].includes(type) + ? schema.required() + : schema.strip().notRequired() + ), + fieldProps: { + freeSolo: true, + placeholder: 'optA,optB,optC' + }, + grid: { md: 8 } +} + +/** @type {{ MIN: Field, MAX: Field }} Range fields */ +const { MIN, MAX } = (() => { + const validation = number() + .positive() + .default(() => undefined) + .when(TYPE.name, (type, schema) => [uiRange, uiRangeFloat].includes(type) + ? schema.required() + : schema.strip().notRequired() + ) + + const common = { + dependOf: TYPE.name, + type: INPUT_TYPES.TEXT, + htmlType: type => + [uiRange, uiRangeFloat].includes(type) ? 'number' : INPUT_TYPES.HIDDEN, + grid: { sm: 6, md: 4 }, + fieldProps: type => ({ step: type === uiRangeFloat ? 0.01 : 1 }) + } + + return { + MIN: { ...common, name: 'min', label: T.Min, validation: validation.lessThan(ref('max')) }, + MAX: { ...common, name: 'max', label: T.Max, validation: validation.moreThan(ref('min')) } + } +})() + +/** @type {Field} Default value field */ +const DEFAULT_VALUE = { + name: 'default', + label: T.DefaultValue, + dependOf: [TYPE.name, OPTIONS.name], + type: ([type] = []) => [uiBoolean, uiList, uiListMultiple].includes(type) + ? INPUT_TYPES.SELECT + : INPUT_TYPES.TEXT, + htmlType: ([type] = []) => ({ + [uiNumber]: 'number', + [uiNumberFloat]: 'number', + [uiPassword]: INPUT_TYPES.HIDDEN + }[type]), + multiple: ([type] = []) => type === uiListMultiple, + values: ([type, options = []] = []) => type === uiBoolean + ? arrayToOptions(['NO', 'YES']) + : arrayToOptions(options), + validation: string() + .trim() + .default(() => undefined) + .when([TYPE.name, OPTIONS.name], (type, options = [], schema) => { + return { + [uiList]: schema.oneOf(options).notRequired(), + [uiListMultiple]: schema.includesInOptions(options), + [uiRange]: number().min(ref(MIN.name)).max(ref(MAX.name)).integer(), + [uiRangeFloat]: number().min(ref(MIN.name)).max(ref(MAX.name)), + [uiPassword]: schema.strip().notRequired() + }[type] ?? schema + }), + grid: { sm: 6, md: 4 } +} + +/** @type {Field} Mandatory field */ +const MANDATORY = { + name: 'mandatory', + label: T.Mandatory, + type: INPUT_TYPES.SWITCH, + validation: boolean().default(() => false), + grid: { md: 12 } +} + +/** @type {Field[]} List of User Inputs fields */ +export const USER_INPUT_FIELDS = [ + TYPE, + NAME, + DESCRIPTION, + DEFAULT_VALUE, + OPTIONS, + MIN, + MAX, + MANDATORY +] + +/** @type {ObjectSchema} User Input object schema */ +export const USER_INPUT_SCHEMA = getObjectSchemaFromFields(USER_INPUT_FIELDS) + +/** @type {ObjectSchema} User Inputs schema */ +export const USER_INPUTS_SCHEMA = object({ + USER_INPUTS: array(USER_INPUT_SCHEMA).ensure() +}) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/userInputsSection.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/userInputsSection.js new file mode 100644 index 0000000000..21249abb17 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/userInputsSection.js @@ -0,0 +1,191 @@ +/* ------------------------------------------------------------------------- * + * 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 { forwardRef, JSXElementConstructor } from 'react' +import PropTypes from 'prop-types' +import { + WarningCircledOutline as WarningIcon, + DeleteCircledOutline, + AddCircledOutline +} from 'iconoir-react' +import { + styled, + FormControl, + Stack, + IconButton, + Button, + Divider, + List, + ListItem, + ListItemIcon, + ListItemText +} from '@mui/material' + +import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd' +import { useFieldArray, useForm, FormProvider, useFormContext, get } from 'react-hook-form' +import { yupResolver } from '@hookform/resolvers/yup' + +import { Tooltip } from 'client/components/FormControl' +import { FormWithSchema, Legend } from 'client/components/Forms' +import { Translate } from 'client/components/HOC' + +import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration' +import { USER_INPUT_SCHEMA, USER_INPUT_FIELDS } from './schema' +import { getUserInputString } from 'client/models/Helper' +import { T } from 'client/constants' + +export const SECTION_ID = 'USER_INPUTS' + +const UserItemDraggable = styled(ListItem)(({ theme }) => ({ + '&:before': { + content: "''", + display: 'block', + width: 16, + height: 10, + background: `linear-gradient( + to bottom, + ${theme.palette.action.active} 4px, + transparent 4px, + transparent 6px, + ${theme.palette.action.active} 6px + )` + } +})) + +const UserInputItem = forwardRef(({ + removeAction, + error, + userInput: { name, ...ui } = {}, + ...props +}, ref) => ( + + + + } + sx={{ '&:hover': { bgcolor: 'action.hover' } }} + {...props} + > + {!!error && ( + + + + + + )} + + +)) + +UserInputItem.propTypes = { + removeAction: PropTypes.func, + error: PropTypes.object, + userInput: PropTypes.object +} + +UserInputItem.displayName = 'UserInputItem' + +/** @returns {JSXElementConstructor} - User Inputs section */ +const UserInputsSection = () => { + const { formState: { errors } } = useFormContext() + const { fields: userInputs, append, remove, move } = useFieldArray({ + name: `${EXTRA_ID}.${SECTION_ID}` + }) + + const methods = useForm({ + defaultValues: USER_INPUT_SCHEMA.default(), + resolver: yupResolver(USER_INPUT_SCHEMA) + }) + + const onSubmit = newInput => { + append(newInput) + methods.reset() + } + + /** @param {DropResult} result - Drop result */ + const onDragEnd = result => { + const { destination, source } = result ?? {} + + if (destination && destination.index !== source.index) { + move(source.index, destination.index) + } + } + + return ( + + + + + + + + + + + + {({ droppableProps, innerRef: outerRef, placeholder }) => ( + + {userInputs?.map(({ id, ...userInput }, index) => ( + + {({ draggableProps, dragHandleProps, innerRef }) => ( + remove(index)} + {...draggableProps} + {...dragHandleProps} + /> + )} + + ))} + {placeholder} + + )} + + + + ) +} + +export default UserInputsSection diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/inputsSection.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/inputsSection.js index feecdaf98e..d204ef0386 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/inputsSection.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/inputsSection.js @@ -67,8 +67,9 @@ const InputsSection = ({ fields }) => { rootProps={{ sx: { m: 0 } }} />