diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/CustomVariables/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/CustomVariables/index.js
new file mode 100644
index 0000000000..00fa8fe7e2
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/CustomVariables/index.js
@@ -0,0 +1,72 @@
+/* ------------------------------------------------------------------------- *
+ * 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. *
+ * ------------------------------------------------------------------------- */
+/* eslint-disable jsdoc/require-jsdoc */
+import PropTypes from 'prop-types'
+import { Box } from '@mui/material'
+import { useFormContext, useWatch } from 'react-hook-form'
+
+import { AttributePanel } from 'client/components/Tabs/Common'
+import { SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/CustomVariables/schema'
+import { cleanEmpty, cloneObject, set } from 'client/utils'
+import { T, ACTIONS } from 'client/constants'
+
+const ALL_ACTIONS = [
+ ACTIONS.COPY_ATTRIBUTE,
+ ACTIONS.ADD_ATTRIBUTE,
+ ACTIONS.EDIT_ATTRIBUTE,
+ ACTIONS.DELETE_ATTRIBUTE
+]
+
+export const STEP_ID = 'custom-variables'
+
+const Content = () => {
+ const { setValue } = useFormContext()
+ const customVars = useWatch({ name: STEP_ID })
+
+ const handleChangeAttribute = (path, newValue) => {
+ const newCustomVars = cloneObject(customVars)
+
+ set(newCustomVars, path, newValue)
+ setValue(STEP_ID, cleanEmpty(newCustomVars))
+ }
+
+ return (
+
+
+
+ )
+}
+
+const CustomVariables = () => ({
+ id: STEP_ID,
+ label: T.CustomVariables,
+ resolver: SCHEMA,
+ optionsValidate: { abortEarly: false },
+ content: Content
+})
+
+Content.propTypes = {
+ data: PropTypes.any
+}
+
+export default CustomVariables
diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/CustomVariables/schema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/CustomVariables/schema.js
new file mode 100644
index 0000000000..417d968dac
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/CustomVariables/schema.js
@@ -0,0 +1,24 @@
+/* ------------------------------------------------------------------------- *
+ * 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, ObjectSchema } from 'yup'
+
+import { } from 'client/utils'
+import { } from 'client/constants'
+
+/** @type {ObjectSchema} Step schema */
+const SCHEMA = object()
+
+export { SCHEMA }
diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/configurationSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/configurationSchema.js
index fda6d05fda..9c07c3f86c 100644
--- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/configurationSchema.js
+++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/configurationSchema.js
@@ -32,7 +32,8 @@ export const SSH_PUBLIC_KEY = {
multiline: true,
validation: string()
.trim()
- .notRequired(),
+ .notRequired()
+ .ensure(),
grid: { md: 12 },
fieldProps: { rows: 4 }
}
@@ -81,6 +82,7 @@ export const START_SCRIPT = {
validation: string()
.trim()
.notRequired()
+ .ensure()
.when(
'$extra.CONTEXT.START_SCRIPT_BASE64',
(scriptEncoded, schema) => scriptEncoded
diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/filesSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/filesSchema.js
index 0d2546f68b..5057fc18e0 100644
--- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/filesSchema.js
+++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/filesSchema.js
@@ -29,7 +29,8 @@ export const FILES_DS = {
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
- .notRequired(),
+ .notRequired()
+ .ensure(),
grid: { md: 12 }
}
@@ -41,7 +42,8 @@ export const INIT_SCRIPTS = {
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
- .notRequired(),
+ .notRequired()
+ .ensure(),
grid: { md: 12 }
}
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 606784bbcd..cecc24f8fb 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
@@ -15,38 +15,46 @@
* ------------------------------------------------------------------------- */
import General, { STEP_ID as GENERAL_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General'
import ExtraConfiguration, { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
+import CustomVariables, { STEP_ID as CUSTOM_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/CustomVariables'
import { jsonToXml, userInputsToArray } from 'client/models/Helper'
import { createSteps, isBase64 } from 'client/utils'
const Steps = createSteps(
- [General, ExtraConfiguration],
+ [General, ExtraConfiguration, CustomVariables],
{
transformInitialValue: (vmTemplate, schema) => {
- const generalStep = schema
- .pick([GENERAL_ID])
- .cast(
- { [GENERAL_ID]: { ...vmTemplate, ...vmTemplate?.TEMPLATE } },
- { stripUnknown: true }
- )
-
const inputsOrder = vmTemplate?.TEMPLATE?.INPUTS_ORDER?.split(',') ?? []
const userInputs = userInputsToArray(vmTemplate?.TEMPLATE?.USER_INPUTS)
.sort((a, b) => inputsOrder.indexOf(a.name) - inputsOrder.indexOf(b.name))
- const configurationStep = schema
- .pick([EXTRA_ID])
- .cast(
- { [EXTRA_ID]: { ...vmTemplate?.TEMPLATE, USER_INPUTS: userInputs } },
- { stripUnknown: true, context: { [EXTRA_ID]: vmTemplate.TEMPLATE } }
- )
+ const knownTemplate = schema.cast(
+ {
+ [GENERAL_ID]: { ...vmTemplate, ...vmTemplate?.TEMPLATE },
+ [EXTRA_ID]: { ...vmTemplate?.TEMPLATE, USER_INPUTS: userInputs }
+ },
+ { stripUnknown: true, context: { [EXTRA_ID]: vmTemplate.TEMPLATE } }
+ )
- return { ...generalStep, ...configurationStep }
+ const customVars = {}
+ const knownAttributes = Object.getOwnPropertyNames({
+ ...knownTemplate[GENERAL_ID],
+ ...knownTemplate[EXTRA_ID]
+ })
+
+ Object.entries(vmTemplate?.TEMPLATE)
+ .forEach(([key, value]) => {
+ if (!knownAttributes.includes(key) && value) {
+ customVars[key] = value
+ }
+ })
+
+ return { ...knownTemplate, [CUSTOM_ID]: customVars }
},
transformBeforeSubmit: formData => {
const {
[GENERAL_ID]: general = {},
+ [CUSTOM_ID]: customVariables = {},
[EXTRA_ID]: {
- USER_INPUTS,
CONTEXT: { START_SCRIPT, ENCODE_START_SCRIPT, ...restOfContext },
...extraTemplate
} = {}
@@ -62,16 +70,17 @@ const Steps = createSteps(
}
// add user inputs to context
- Object.keys(USER_INPUTS).forEach(name => {
- const upperName = String(name).toUpperCase()
- context[upperName] = `$${upperName}`
- })
+ Object.keys(extraTemplate?.USER_INPUTS ?? {})
+ .forEach(name => {
+ const upperName = String(name).toUpperCase()
+ context[upperName] = `$${upperName}`
+ })
return jsonToXml({
+ ...customVariables,
...extraTemplate,
...general,
- CONTEXT: context,
- USER_INPUTS: USER_INPUTS
+ CONTEXT: context
})
}
}
diff --git a/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js b/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js
index 1ef94d4a30..fad7eca4c6 100644
--- a/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js
+++ b/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js
@@ -137,10 +137,10 @@ const Attribute = memo(({
}
- {canCopy && (
+ {value && canCopy && (
)}
- {canEdit && (
+ {(value || numberOfParents > 0) && canEdit && (
)}
{canDelete && (
diff --git a/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js b/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js
index dc16ca049c..050662ef3c 100644
--- a/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js
+++ b/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js
@@ -28,7 +28,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
-import { memo } from 'react'
+import { memo, useCallback } from 'react'
import PropTypes from 'prop-types'
import makeStyles from '@mui/styles/makeStyles'
@@ -57,7 +57,6 @@ const SPECIAL_ATTRIBUTES = {
[DELETE]: false
},
VCENTER_PASSWORD: {
- [EDIT]: true,
[DELETE]: false
},
VCENTER_USER: {
@@ -87,21 +86,24 @@ const AttributePanel = memo(({
handleEdit,
handleDelete,
handleAdd,
- actions
+ actions = [],
+ filtersSpecialAttributes = true
}) => {
const classes = useStyles()
+ const canUseAction = useCallback((name, action) => (
+ actions?.includes?.(action) &&
+ (!filtersSpecialAttributes || SPECIAL_ATTRIBUTES[name]?.[action] === undefined)
+ ), [actions?.length])
+
const formatAttributes = Object.entries(attributes)
.map(([name, value]) => ({
name,
value,
showActionsOnHover: true,
- canCopy:
- actions?.includes?.(COPY) && !SPECIAL_ATTRIBUTES[name]?.[COPY],
- canEdit:
- actions?.includes?.(EDIT) && !SPECIAL_ATTRIBUTES[name]?.[EDIT],
- canDelete:
- actions?.includes?.(DELETE) && !SPECIAL_ATTRIBUTES[name]?.[DELETE],
+ canCopy: canUseAction(name, COPY),
+ canEdit: canUseAction(name, EDIT),
+ canDelete: canUseAction(name, DELETE),
handleEdit,
handleDelete
}))
@@ -124,7 +126,8 @@ AttributePanel.propTypes = {
handleAdd: PropTypes.func,
handleEdit: PropTypes.func,
handleDelete: PropTypes.func,
- title: PropTypes.string
+ title: PropTypes.string,
+ filtersSpecialAttributes: PropTypes.bool
}
AttributePanel.displayName = 'AttributePanel'
diff --git a/src/fireedge/src/client/components/Tabs/Common/List.js b/src/fireedge/src/client/components/Tabs/Common/List.js
index decc42a1c3..94d5d25c3f 100644
--- a/src/fireedge/src/client/components/Tabs/Common/List.js
+++ b/src/fireedge/src/client/components/Tabs/Common/List.js
@@ -36,7 +36,8 @@ const useStyles = makeStyles(theme => ({
gap: '1em',
'& > *': {
flex: '1 1 50%',
- overflow: 'hidden'
+ overflow: 'hidden',
+ minHeight: '100%'
},
'&:hover': {
backgroundColor: alpha(theme.palette.text.primary, 0.05)
diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js
index b922acf581..a60162eea8 100644
--- a/src/fireedge/src/client/constants/translates.js
+++ b/src/fireedge/src/client/constants/translates.js
@@ -328,7 +328,7 @@ module.exports = {
UserTemplate: 'User Template',
Template: 'Template',
WhereIsRunning:
- 'VM %1$s is currently running on Host %2$s and Datastore %3$s',
+ 'VM %1$s is currently running on Host %2$s and Datastore %3$s',
/* VM schema - capacity */
Capacity: 'Capacity',
PhysicalCpu: 'Physical CPU',
@@ -357,11 +357,12 @@ module.exports = {
ExternalConcept: 'The NIC will be attached as an external alias of the VM',
/* VM Template schema */
- /* VM schema - general */
+ /* VM Template schema - general */
Logo: 'Logo',
Hypervisor: 'Hypervisor',
TemplateName: 'Template name',
MakeNewImagePersistent: 'Make the new images persistent',
+ CustomVariables: 'Custom Variables',
/* VM schema - ownership */
InstantiateAsUser: 'Instantiate as different User',
InstantiateAsGroup: 'Instantiate as different Group',
diff --git a/src/fireedge/src/client/utils/helpers.js b/src/fireedge/src/client/utils/helpers.js
index 27f24de7fa..9b222eb0fd 100644
--- a/src/fireedge/src/client/utils/helpers.js
+++ b/src/fireedge/src/client/utils/helpers.js
@@ -15,6 +15,7 @@
* ------------------------------------------------------------------------- */
import DOMPurify from 'dompurify'
import { object, reach, ObjectSchema, BaseSchema } from 'yup'
+import { isMergeableObject } from 'client/utils/merge'
import { HYPERVISORS } from 'client/constants'
/**
@@ -266,6 +267,62 @@ export const groupBy = (list, key) =>
*/
export const cloneObject = obj => JSON.parse(JSON.stringify(obj))
+/**
+ * Removes undefined and null values from object.
+ *
+ * @param {object} obj - Object value
+ * @returns {object} - Cleaned object
+ */
+export const cleanEmptyObject = obj => {
+ const entries = Object.entries(obj)
+ .filter(([_, value]) =>
+ // filter object/array values without attributes
+ isMergeableObject(value)
+ ? Object.values(value).some(v => v != null)
+ : Array.isArray(value) ? value.length > 0 : true
+ )
+ .map(([key, value]) => {
+ let cleanedValue = value
+
+ if (isMergeableObject(value)) {
+ cleanedValue = cleanEmptyObject(value)
+ } else if (Array.isArray(value)) {
+ cleanedValue = cleanEmptyArray(value)
+ }
+
+ return [key, cleanedValue]
+ })
+
+ return entries?.length > 0
+ ? entries.reduce((cleanedObject, [key, value]) => {
+ // `value == null` checks against undefined and null
+ return value == null ? cleanedObject : { ...cleanedObject, [key]: value }
+ }, {})
+ : undefined
+}
+
+/**
+ * Removes undefined and null values from array.
+ *
+ * @param {Array} arr - Array value
+ * @returns {object} - Cleaned object
+ */
+export const cleanEmptyArray = arr => arr
+ .map(value => isMergeableObject(value) ? cleanEmpty(value) : value)
+ .filter(value =>
+ !(value == null) || // `value == null` checks against undefined and null
+ (Array.isArray(value) && value.length > 0)
+ )
+
+/**
+ * Removes undefined and null values from variable.
+ *
+ * @param {Array|object} variable - Variable
+ * @returns {Array|object} - Cleaned variable
+ */
+export const cleanEmpty = variable =>
+ Array.isArray(variable) ? cleanEmptyArray(variable) : cleanEmptyObject(variable)
+
/**
* Check if value is in base64.
*