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 ( + + + + + + + + + {T.AddUserSshPublicKey} + + + {T.Clear} + + + + + + + ) +} + +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 ( - - - - - - } - sx={{ mt: '1em' }} - > - - - - - - - - {({ 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 ( + + + + + + } + sx={{ mt: '1em' }} + > + + + + + + + + {({ 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 } }} /> } sx={{ mt: '1em' }} > diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSchema.js index 301e53de14..76b55cdb1e 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSchema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSchema.js @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { string, array, object, ObjectSchema, ArraySchema } from 'yup' +import { string, array, ObjectSchema, ArraySchema } from 'yup' import { useHost } from 'client/features/One' import { getPciDevices } from 'client/models/Host' -import { Field, arrayToOptions, filterFieldsByHypervisor, getValidationFromFields } from 'client/utils' +import { Field, arrayToOptions, filterFieldsByHypervisor, getObjectSchemaFromFields } from 'client/utils' import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants' const { vcenter, lxc, firecracker } = HYPERVISORS @@ -86,7 +86,7 @@ export const PCI_FIELDS = (hypervisor) => filterFieldsByHypervisor([DEVICE_NAME, DEVICE, VENDOR, CLASS], hypervisor) /** @type {ObjectSchema} PCI devices object schema */ -export const PCI_SCHEMA = object(getValidationFromFields([DEVICE, VENDOR, CLASS])) +export const PCI_SCHEMA = getObjectSchemaFromFields([DEVICE, VENDOR, CLASS]) /** @type {ArraySchema} PCI devices schema */ export const PCI_DEVICES_SCHEMA = array(PCI_SCHEMA).ensure() diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSection.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSection.js index f4d339dc82..dcddf42613 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSection.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSection.js @@ -72,8 +72,9 @@ const PciDevicesSection = ({ fields }) => { rootProps={{ sx: { m: 0 } }} /> } sx={{ mt: '1em' }} > @@ -84,8 +85,8 @@ const PciDevicesSection = ({ fields }) => { {pciDevices?.map(({ id, DEVICE, VENDOR, CLASS }, index) => { - const deviceName = pciDevicesAvailable - .find(pciDevice => pciDevice?.DEVICE === DEVICE)?.DEVICE_NAME + const { DEVICE_NAME, VENDOR_NAME } = pciDevicesAvailable + .find(pciDevice => pciDevice?.DEVICE === DEVICE) ?? {} return ( { sx={{ '&:hover': { bgcolor: 'action.hover' } }} > diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema.js index 3789e6a987..3e9852fa2b 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema.js @@ -45,9 +45,9 @@ export const SCHED_ACTION_SCHEMA = array() export const SCHEMA = hypervisor => object({ DISK: DISK_SCHEMA, NIC: NIC_SCHEMA, - SCHED_ACTION: SCHED_ACTION_SCHEMA, - USER_INPUTS: CONTEXT_SCHEMA + SCHED_ACTION: SCHED_ACTION_SCHEMA }) + .concat(CONTEXT_SCHEMA) .concat(IO_SCHEMA(hypervisor)) .concat(getObjectSchemaFromFields([ ...PLACEMENT_FIELDS, 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 08b90f96f6..7b07f00e08 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 @@ -17,7 +17,7 @@ import General, { STEP_ID as GENERAL_ID } from 'client/components/Forms/VmTempla import ExtraConfiguration, { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration' // import { jsonToXml } from 'client/models/Helper' import { userInputsToArray, userInputsToObject } from 'client/models/Helper' -import { createSteps } from 'client/utils' +import { createSteps, isBase64 } from 'client/utils' const Steps = createSteps( [General, ExtraConfiguration], @@ -36,18 +36,29 @@ const Steps = createSteps( transformBeforeSubmit: formData => { const { [GENERAL_ID]: general = {}, - [EXTRA_ID]: { USER_INPUTS, ...extraTemplate } = {} + [EXTRA_ID]: { USER_INPUTS, CONTEXT, ...extraTemplate } = {} } = formData ?? {} // const templateXML = jsonToXml({ ...general, ...extraTemplate }) // return { template: templateXML } + const { START_SCRIPT, ENCODE_START_SCRIPT, ...restOfContext } = CONTEXT + + const context = { + ...restOfContext, + [ENCODE_START_SCRIPT ? 'START_SCRIPT_BASE64' : 'START_SCRIPT']: + ENCODE_START_SCRIPT && !isBase64(START_SCRIPT) + ? btoa(unescape(encodeURIComponent(START_SCRIPT))) + : START_SCRIPT + } + const userInputs = userInputsToObject(USER_INPUTS) const inputsOrder = USER_INPUTS.map(({ name }) => name).join(',') return { ...general, ...extraTemplate, + CONTEXT: context, USER_INPUTS: userInputs, INPUTS_ORDER: inputsOrder } diff --git a/src/fireedge/src/client/components/Header/User.js b/src/fireedge/src/client/components/Header/User.js index ad352c1d77..f77aae804a 100644 --- a/src/fireedge/src/client/components/Header/User.js +++ b/src/fireedge/src/client/components/Header/User.js @@ -55,7 +55,7 @@ const User = () => { color='secondary' href={`${APP_URL}/${appName}`} > - + {appName} )) diff --git a/src/fireedge/src/client/components/Sidebar/SidebarCollapseItem.js b/src/fireedge/src/client/components/Sidebar/SidebarCollapseItem.js index dd82ca94e4..4066056555 100644 --- a/src/fireedge/src/client/components/Sidebar/SidebarCollapseItem.js +++ b/src/fireedge/src/client/components/Sidebar/SidebarCollapseItem.js @@ -37,7 +37,7 @@ import { useGeneral } from 'client/features/General' import SidebarLink from 'client/components/Sidebar/SidebarLink' import sidebarStyles from 'client/components/Sidebar/styles' -const SidebarCollapseItem = ({ label, routes, icon: Icon }) => { +const SidebarCollapseItem = ({ label = '', routes = [], icon: Icon }) => { const classes = sidebarStyles() const { pathname } = useLocation() const { isFixMenu } = useGeneral() @@ -63,29 +63,26 @@ const SidebarCollapseItem = ({ label, routes, icon: Icon }) => { )} {expanded ? : } - {routes - ?.filter(({ sidebar = false, label }) => sidebar && typeof label === 'string') - ?.map((subItem, index) => ( - - - - - - )) - } + + + {routes + ?.filter(({ sidebar = false, label }) => sidebar && typeof label === 'string') + ?.map((subItem, index) => ( + + ))} + + > ) } @@ -107,10 +104,4 @@ SidebarCollapseItem.propTypes = { ) } -SidebarCollapseItem.defaultProps = { - label: '', - icon: null, - routes: [] -} - export default SidebarCollapseItem diff --git a/src/fireedge/src/client/components/Sidebar/SidebarLink.js b/src/fireedge/src/client/components/Sidebar/SidebarLink.js index ee6070d69e..dd90522e75 100644 --- a/src/fireedge/src/client/components/Sidebar/SidebarLink.js +++ b/src/fireedge/src/client/components/Sidebar/SidebarLink.js @@ -14,6 +14,7 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ +import { memo } from 'react' import PropTypes from 'prop-types' import { useHistory, useLocation } from 'react-router-dom' @@ -25,17 +26,19 @@ import { } from '@mui/material' import { useGeneralApi } from 'client/features/General' -import sidebarStyles from 'client/components/Sidebar/styles' import { DevTypography } from 'client/components/Typography' -const STATIC_LABEL_PROPS = { - 'data-cy': 'main-menu-item-text', - variant: 'body1' -} - -const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => { - const classes = sidebarStyles() - const isUpLg = useMediaQuery(theme => theme.breakpoints.up('lg'), { noSsr: true }) +const SidebarLink = memo(({ + label = '', + path = '/', + icon: Icon, + devMode = false, + isSubItem = false +}) => { + const isUpLg = useMediaQuery( + theme => theme.breakpoints.up('lg'), + { noSsr: true } + ) const history = useHistory() const { pathname } = useLocation() @@ -48,10 +51,10 @@ const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => { return ( {Icon && ( @@ -59,41 +62,25 @@ const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => { )} - ) : label - } + primary={label} + primaryTypographyProps={{ + ...(devMode && { component: DevTypography }), + 'data-cy': 'main-menu-item-text', + variant: 'body1' + }} /> ) -} +}) SidebarLink.propTypes = { label: PropTypes.string.isRequired, path: PropTypes.string.isRequired, - icon: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.node, - PropTypes.func, - PropTypes.string, - PropTypes.symbol, - PropTypes.object - ]), + icon: PropTypes.any, devMode: PropTypes.bool, isSubItem: PropTypes.bool } -SidebarLink.defaultProps = { - label: '', - path: '/', - icon: undefined, - devMode: false, - isSubItem: false -} - SidebarLink.displayName = 'SidebarLink' export default SidebarLink diff --git a/src/fireedge/src/client/components/Sidebar/index.js b/src/fireedge/src/client/components/Sidebar/index.js index 9bd1b66397..bbff7c6c2f 100644 --- a/src/fireedge/src/client/components/Sidebar/index.js +++ b/src/fireedge/src/client/components/Sidebar/index.js @@ -66,7 +66,6 @@ const Sidebar = ({ endpoints }) => { return ( { width='100%' height={50} withText - className={classes.svg} + className={classes.logo} disabledBetaText /> {!isUpLg || isFixMenu ? ( @@ -96,7 +95,7 @@ const Sidebar = ({ endpoints }) => { - + {SidebarEndpoints} diff --git a/src/fireedge/src/client/components/Sidebar/styles.js b/src/fireedge/src/client/components/Sidebar/styles.js index 11bba76325..40792101d0 100644 --- a/src/fireedge/src/client/components/Sidebar/styles.js +++ b/src/fireedge/src/client/components/Sidebar/styles.js @@ -54,9 +54,6 @@ export default makeStyles(theme => ({ }, '& $subItemWrapper': { display: 'none' - }, - '& $itemText::before': { - content: 'attr(data-min-label)' } } } @@ -78,9 +75,6 @@ export default makeStyles(theme => ({ }, '& $subItemWrapper': { display: 'block !important' - }, - '& $itemText::before': { - content: 'attr(data-max-label) !important' } } }, @@ -104,7 +98,7 @@ export default makeStyles(theme => ({ minHeight: toolbar.sm } }, - svg: { + logo: { minWidth: 100 }, // ------------------------------- @@ -114,11 +108,7 @@ export default makeStyles(theme => ({ overflowY: 'auto', overflowX: 'hidden', textTransform: 'capitalize', - color: 'transparent', transition: 'color 0.3s', - '&:hover': { - color: theme.palette.primary.light - }, '&::-webkit-scrollbar': { width: 14 }, @@ -130,19 +120,5 @@ export default makeStyles(theme => ({ color: theme.palette.secondary.light } }, - list: { - color: theme.palette.text.primary - }, - itemText: { - '&::before': { - ...theme.typography.body1, - display: 'block', - minWidth: 100, - content: 'attr(data-max-label)' - } - }, - subItemWrapper: {}, - subItem: { - paddingLeft: theme.spacing(4) - } + subItemWrapper: {} })) diff --git a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalSelectedRows.js b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalSelectedRows.js index 64a6a447c5..1ea36792e9 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalSelectedRows.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalSelectedRows.js @@ -50,7 +50,7 @@ const GlobalSelectedRows = ({ withAlert = false, useTableProps }) => { const allSelected = numberOfRowSelected === preFilteredRows.length return withAlert ? ( - + {'.'} diff --git a/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js b/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js index 3a3678a7e2..dda0c6c2bf 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js @@ -42,9 +42,7 @@ const Accordion = styled(props => ( '&:before': { display: 'none' } })) -const Summary = styled(props => ( - } {...props} /> -))(({ +const Summary = styled(AccordionSummary)(({ [`&.${accordionSummaryClasses.root}`]: { backgroundColor: 'rgba(0, 0, 0, .03)', flexDirection: 'row-reverse', @@ -90,7 +88,8 @@ const NetworkItem = ({ nic = {}, actions }) => { const { detachNic } = useVmApi() const { handleRefetch, data: vm } = useContext(TabContext) - const { NIC_ID, NETWORK = '-', IP, MAC, PCI_ID, ALIAS, SECURITY_GROUPS } = nic + const { NIC_ID, NETWORK = '-', IP, MAC, PCI_ID, ADDRESS, ALIAS, SECURITY_GROUPS } = nic + const isPciDevice = PCI_ID !== undefined const hasDetails = useMemo( () => !!ALIAS.length || !!SECURITY_GROUPS?.length, @@ -117,7 +116,7 @@ const NetworkItem = ({ nic = {}, actions }) => { return ( <> - + })}> {`${NIC_ID} | ${NETWORK}`} @@ -126,10 +125,10 @@ const NetworkItem = ({ nic = {}, actions }) => { - {!isMobile && detachAction(NIC_ID)} + {!isMobile && !isPciDevice && detachAction(NIC_ID)} {hasDetails && ( @@ -146,7 +145,7 @@ const NetworkItem = ({ nic = {}, actions }) => { tags={[IP, MAC, BRIDGE && `BRIDGE - ${BRIDGE}`].filter(Boolean)} /> - {!isMobile && detachAction(NIC_ID, true)} + {!isMobile && !isPciDevice && detachAction(NIC_ID, true)} ))} {!!SECURITY_GROUPS?.length && ( diff --git a/src/fireedge/src/client/components/Tabs/Vm/Network/List.js b/src/fireedge/src/client/components/Tabs/Vm/Network/List.js index a924f2ca68..774f362974 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Network/List.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Network/List.js @@ -26,8 +26,8 @@ const NetworkList = ({ nics, actions }) => ( paddingBlock: '0.8em' }}> {nics.map(nic => { - const { IP, MAC } = nic - const key = IP ?? MAC + const { IP, MAC, ADDRESS } = nic + const key = IP ?? MAC ?? ADDRESS // address only exists form PCI nics return })} diff --git a/src/fireedge/src/client/components/Typography/DevTypography.js b/src/fireedge/src/client/components/Typography/DevTypography.js index 6f9fcd9cc2..61c51dd112 100644 --- a/src/fireedge/src/client/components/Typography/DevTypography.js +++ b/src/fireedge/src/client/components/Typography/DevTypography.js @@ -18,7 +18,12 @@ import PropTypes from 'prop-types' import { Typography, Chip, Box } from '@mui/material' -const DevTypography = memo(({ label, labelProps, color, chipProps }) => ( +const DevTypography = memo(({ + labelProps = {}, + color = 'secondary', + chipProps = {}, + children = '' +}) => ( ( sx={{ textTransform: 'capitalize' }} {...labelProps} > - {label} + {children} ( DevTypography.propTypes = { chipProps: PropTypes.object, color: PropTypes.string, - label: PropTypes.oneOfType([ - PropTypes.object, - PropTypes.string.isRequired - ]), - labelProps: PropTypes.object -} - -DevTypography.defaultProps = { - chipProps: undefined, - color: 'secondary', - label: '', - labelProps: undefined + labelProps: PropTypes.object, + children: PropTypes.any } DevTypography.displayName = 'DevTypography' diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index 1a262e94b5..9fe1e1d763 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -459,8 +459,8 @@ module.exports = { `Number of vCPU queues to use in the virtio-scsi controller. Leave blank to use the default value`, IoThreads: 'Iothreads', - IoThreadsConcept: - `Number of iothreads for virtio disks. + IoThreadsConcept: ` + Number of iothreads for virtio disks. By default threads will be assign to disk by round robin algorithm. Disk thread id can be forced by disk IOTHREAD attribute`, RawData: 'Raw data', @@ -470,6 +470,24 @@ module.exports = { By default, the data will be checked against the libvirt schema`, /* VM Template schema - context */ Context: 'Context', + SshPublicKey: 'SSH public key', + AddUserSshPublicKey: 'Add user SSH public key', + AddNetworkContextualization: 'Add Network contextualization', + AddNetworkContextualizationConcept: ` + Add network contextualization parameters. For each NIC defined in + the NETWORK section, ETH$i_IP, ETH$i_NETWORK... parameters will be + included in the CONTEXT section and will be available in the Virtual Machine`, + AddOneGateToken: 'Add OneGate token', + AddOneGateTokenConcept: ` + Add a file (token.txt) to the context containing the token to push + custom metrics to the Virtual Machine through OneGate`, + ReportReadyToOneGate: 'Report Ready to OneGate', + ReportReadyToOneGateConcept: 'Sends READY=YES to OneGate, useful for OneFlow', + StartScript: 'Start script', + StartScriptConcept: ` + Text of the script executed when the machine starts up. It can contain + shebang in case it is not shell script`, + EncodeScriptInBase64: 'Encode script in Base64', /* VM Template schema - Input/Output */ InputOrOutput: 'Input / Output', Inputs: 'Inputs', diff --git a/src/fireedge/src/client/models/VirtualMachine.js b/src/fireedge/src/client/models/VirtualMachine.js index dc1248d1ea..3c4221769f 100644 --- a/src/fireedge/src/client/models/VirtualMachine.js +++ b/src/fireedge/src/client/models/VirtualMachine.js @@ -159,11 +159,13 @@ export const getNics = (vm, options = {}) => { const { NIC = [], NIC_ALIAS = [], PCI = [] } = TEMPLATE const { GUEST_IP, GUEST_IP_ADDRESSES = '' } = MONITORING + const pciNics = PCI.filter(({ NIC_ID } = {}) => NIC_ID !== undefined) + const extraIps = [GUEST_IP, ...GUEST_IP_ADDRESSES?.split(',')] .filter(Boolean) .map(ip => ({ NIC_ID: '-', IP: ip, NETWORK: 'Additional IP', BRIDGE: '-' })) - let nics = [NIC, NIC_ALIAS, PCI, extraIps].flat().filter(Boolean) + let nics = [NIC, NIC_ALIAS, pciNics, extraIps].flat().filter(Boolean) if (groupAlias) { nics = nics diff --git a/src/fireedge/src/client/utils/helpers.js b/src/fireedge/src/client/utils/helpers.js index e0f8677430..27f24de7fa 100644 --- a/src/fireedge/src/client/utils/helpers.js +++ b/src/fireedge/src/client/utils/helpers.js @@ -14,7 +14,7 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ import DOMPurify from 'dompurify' -import { object, reach, BaseSchema } from 'yup' +import { object, reach, ObjectSchema, BaseSchema } from 'yup' import { HYPERVISORS } from 'client/constants' /** @@ -107,7 +107,7 @@ export const getValidationFromFields = fields => * Returns fields in schema object. * * @param {{name: string, validation: BaseSchema}[]} fields - Fields - * @returns {BaseSchema} Object schema + * @returns {ObjectSchema} Object schema * @example * [{ name: 'VM.NAME', validation: string() }] * => object({ 'VM': object({ NAME: string() }) }) diff --git a/src/fireedge/src/client/utils/schema.js b/src/fireedge/src/client/utils/schema.js index 260d7596ca..043c4edf70 100644 --- a/src/fireedge/src/client/utils/schema.js +++ b/src/fireedge/src/client/utils/schema.js @@ -197,7 +197,10 @@ const parseUserInputValue = value => { return 'YES' } else if (value === false) { return 'NO' - } else if (Array.isArray(value)) { + } else if ( + Array.isArray(value) && + value.every(v => typeof v === 'string') + ) { return value.join(',') } else return value } @@ -215,9 +218,9 @@ const parseUserInputValue = value => { */ export const schemaUserInput = ({ mandatory, name, type, options, default: defaultValue }) => { switch (type) { - case [USER_INPUT_TYPES.text]: - case [USER_INPUT_TYPES.text64]: - case [USER_INPUT_TYPES.password]: return { + case USER_INPUT_TYPES.text: + case USER_INPUT_TYPES.text64: + case USER_INPUT_TYPES.password: return { type: INPUT_TYPES.TEXT, htmlType: type === 'password' ? 'password' : 'text', validation: string() @@ -225,8 +228,8 @@ export const schemaUserInput = ({ mandatory, name, type, options, default: defau .concat(requiredSchema(mandatory, name, string())) .default(defaultValue || undefined) } - case [USER_INPUT_TYPES.number]: - case [USER_INPUT_TYPES.numberFloat]: return { + case USER_INPUT_TYPES.number: + case USER_INPUT_TYPES.numberFloat: return { type: INPUT_TYPES.TEXT, htmlType: 'number', validation: number() @@ -235,8 +238,8 @@ export const schemaUserInput = ({ mandatory, name, type, options, default: defau .transform(value => !isNaN(value) ? value : null) .default(() => parseFloat(defaultValue) ?? undefined) } - case [USER_INPUT_TYPES.range]: - case [USER_INPUT_TYPES.rangeFloat]: { + case USER_INPUT_TYPES.range: + case USER_INPUT_TYPES.rangeFloat: { const [min, max] = getRange(options) return { @@ -251,13 +254,13 @@ export const schemaUserInput = ({ mandatory, name, type, options, default: defau fieldProps: { min, max, step: type === 'range-float' ? 0.01 : 1 } } } - case [USER_INPUT_TYPES.boolean]: return { + case USER_INPUT_TYPES.boolean: return { type: INPUT_TYPES.CHECKBOX, validation: boolean() .concat(requiredSchema(mandatory, name, boolean())) .default(defaultValue === 'YES' ?? false) } - case [USER_INPUT_TYPES.list]: { + case USER_INPUT_TYPES.list: { const values = getOptionsFromList(options) const firstOption = values?.[0]?.value ?? undefined @@ -271,7 +274,7 @@ export const schemaUserInput = ({ mandatory, name, type, options, default: defau .default(defaultValue ?? firstOption) } } - case [USER_INPUT_TYPES.array]: { + case USER_INPUT_TYPES.array: { const defaultValues = getValuesFromArray(defaultValue) return { @@ -283,7 +286,7 @@ export const schemaUserInput = ({ mandatory, name, type, options, default: defau fieldProps: { freeSolo: true } } } - case [USER_INPUT_TYPES.listMultiple]: { + case USER_INPUT_TYPES.listMultiple: { const values = getOptionsFromList(options) const defaultValues = defaultValue?.split(',') ?? undefined