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

F #5628: Add step for custom vars (#1608)

This commit is contained in:
Sergio Betanzos 2021-11-24 13:43:04 +01:00 committed by GitHub
parent 84c00d29e3
commit 83df0dffee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 211 additions and 40 deletions

View File

@ -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 (
<Box display='grid' gap='1em'>
<AttributePanel
handleAdd={handleChangeAttribute}
handleEdit={handleChangeAttribute}
handleDelete={handleChangeAttribute}
attributes={customVars}
actions={ALL_ACTIONS}
filtersSpecialAttributes={false}
/>
</Box>
)
}
const CustomVariables = () => ({
id: STEP_ID,
label: T.CustomVariables,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: Content
})
Content.propTypes = {
data: PropTypes.any
}
export default CustomVariables

View File

@ -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 }

View File

@ -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

View File

@ -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 }
}

View File

@ -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
})
}
}

View File

@ -137,10 +137,10 @@ const Attribute = memo(({
}
</Typography>
<ActionWrapper {...(showActionsOnHover && { display: 'none' })}>
{canCopy && (
{value && canCopy && (
<Actions.Copy name={name} value={value} />
)}
{canEdit && (
{(value || numberOfParents > 0) && canEdit && (
<Actions.Edit name={name} handleClick={handleActiveEditForm} />
)}
{canDelete && (

View File

@ -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'

View File

@ -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)

View File

@ -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',

View File

@ -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.
*