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

F #5422: Add context vars section (#2063)

This commit is contained in:
Sergio Betanzos 2022-05-19 13:08:28 +02:00 committed by GitHub
parent 77556f1b8f
commit d91510862e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 230 additions and 73 deletions

View File

@ -28,7 +28,7 @@ const ErrorTypo = styled(Typography)(({ theme }) => ({
}))
const ErrorHelper = memo(({ label, children, ...rest }) => {
const ensuredLabel = Array.isArray(label) ? label[0] : label
const ensuredLabel = Array.isArray(label) && label[0]?.word ? label[0] : label
const translateProps = ensuredLabel?.word
? { ...ensuredLabel }

View File

@ -36,7 +36,13 @@ const FIRST_STEP = 0
* @param {StepsForm} stepsForm - Steps form config
* @returns {ReactElement} Stepper form component
*/
const DefaultFormStepper = ({ onSubmit, steps, defaultValues, resolver }) => {
const DefaultFormStepper = ({
onSubmit,
steps,
defaultValues,
resolver,
initialValues,
}) => {
const methods = useForm({
mode: 'onSubmit',
defaultValues,
@ -44,7 +50,7 @@ const DefaultFormStepper = ({ onSubmit, steps, defaultValues, resolver }) => {
})
return (
<FormProvider {...methods}>
<FormProvider {...methods} initialValues={initialValues}>
<FormStepper steps={steps} schema={resolver} onSubmit={onSubmit} />
</FormProvider>
)
@ -54,6 +60,7 @@ DefaultFormStepper.propTypes = {
onSubmit: PropTypes.func,
steps: PropTypes.arrayOf(PropTypes.object),
defaultValues: PropTypes.object,
initialValues: PropTypes.object,
resolver: PropTypes.func,
}

View File

@ -13,22 +13,14 @@
* 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 { useCallback } from 'react'
import { object } from 'yup'
import { useFormContext, useWatch } from 'react-hook-form'
import { Box } from '@mui/material'
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,
]
import { T } from 'client/constants'
export const STEP_ID = 'custom-variables'
@ -36,37 +28,41 @@ const Content = () => {
const { setValue } = useFormContext()
const customVars = useWatch({ name: STEP_ID })
const handleChangeAttribute = (path, newValue) => {
const newCustomVars = cloneObject(customVars)
const handleChangeAttribute = useCallback(
(path, newValue) => {
const newCustomVars = cloneObject(customVars)
set(newCustomVars, path, newValue)
setValue(STEP_ID, cleanEmpty(newCustomVars))
}
set(newCustomVars, path, newValue)
setValue(STEP_ID, cleanEmpty(newCustomVars))
},
[customVars]
)
return (
<Box display="grid" gap="1em">
<AttributePanel
allActionsEnabled
handleAdd={handleChangeAttribute}
handleEdit={handleChangeAttribute}
handleDelete={handleChangeAttribute}
attributes={customVars}
actions={ALL_ACTIONS}
filtersSpecialAttributes={false}
/>
</Box>
)
}
/**
* Custom variables about VM Template.
*
* @returns {object} Custom configuration step
*/
const CustomVariables = () => ({
id: STEP_ID,
label: T.CustomVariables,
resolver: SCHEMA,
resolver: object(),
optionsValidate: { abortEarly: false },
content: Content,
})
Content.propTypes = {
data: PropTypes.any,
}
export default CustomVariables

View File

@ -1,24 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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

@ -0,0 +1,108 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 { ReactElement, useCallback, useMemo } from 'react'
import PropTypes from 'prop-types'
import { reach } from 'yup'
import { useFormContext, useWatch } from 'react-hook-form'
import { Accordion, AccordionSummary, Box } from '@mui/material'
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
import { SCHEMA as CONTEXT_SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/schema'
import { useGeneralApi } from 'client/features/General'
import { Legend } from 'client/components/Forms'
import { AttributePanel } from 'client/components/Tabs/Common'
import { getUnknownAttributes } from 'client/utils'
import { T } from 'client/constants'
export const SECTION_ID = 'CONTEXT'
/**
* Renders the context section of the extra configuration form.
*
* @param {object} props - Props passed to the component
* @param {string} props.hypervisor - VM hypervisor
* @returns {ReactElement} - Context vars section
*/
const ContextVarsSection = ({ hypervisor }) => {
const { enqueueError } = useGeneralApi()
const { setValue } = useFormContext()
const customVars = useWatch({ name: `${EXTRA_ID}.${SECTION_ID}` })
const unknownVars = useMemo(() => {
const knownVars = CONTEXT_SCHEMA(hypervisor).cast(
{ [SECTION_ID]: customVars },
{ stripUnknown: true }
)
const currentContext = knownVars?.[SECTION_ID] || {}
return getUnknownAttributes(customVars, currentContext)
}, [customVars])
const handleChangeAttribute = useCallback(
(path, newValue) => {
const contextPath = `${SECTION_ID}.${path}`
const formPath = `${EXTRA_ID}.${contextPath}`
try {
// retrieve the schema for the given path
reach(CONTEXT_SCHEMA(hypervisor), contextPath)
enqueueError(T.ContextCustomVarErrorExists)
} catch (e) {
// When the path is not found, it means that
// the attribute is correct and we can set it
setValue(formPath, newValue)
}
},
[hypervisor]
)
return (
<Box display="grid" gap="1em">
<Accordion
variant="transparent"
TransitionProps={{ unmountOnExit: false }}
>
<AccordionSummary>
<Legend
disableGutters
title={T.ContextCustomVariables}
tooltip={T.ContextCustomVariablesConcept}
/>
</AccordionSummary>
<AttributePanel
allActionsEnabled
handleAdd={handleChangeAttribute}
handleEdit={handleChangeAttribute}
handleDelete={handleChangeAttribute}
attributes={unknownVars}
filtersSpecialAttributes={false}
/>
</Accordion>
</Box>
)
}
ContextVarsSection.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
hypervisor: PropTypes.string,
control: PropTypes.object,
}
export default ContextVarsSection

View File

@ -22,6 +22,7 @@ import UserInputsSection, {
} from './userInputsSection'
import ConfigurationSection from './configurationSection'
import FilesSection from './filesSection'
import ContextVarsSection from './contextVarsSection'
import { T } from 'client/constants'
@ -32,6 +33,7 @@ const Context = (props) => (
<ConfigurationSection />
<UserInputsSection />
<FilesSection {...props} />
<ContextVarsSection {...props} />
</>
)

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
// eslint-disable-next-line no-unused-vars
import { useMemo, ReactElement } from 'react'
import PropTypes from 'prop-types'
@ -36,7 +35,7 @@ import Numa from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfi
import { STEP_ID as GENERAL_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General'
import { SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
import { T, RESOURCE_NAMES } from 'client/constants'
import { T, RESOURCE_NAMES, VmTemplate } from 'client/constants'
/**
* @typedef {object} TabType
@ -99,17 +98,27 @@ const Content = ({ data, setFormData }) => {
return <Tabs tabs={tabs} />
}
const ExtraConfiguration = () => ({
id: STEP_ID,
label: T.AdvancedOptions,
resolver: (formData) => {
const hypervisor = formData?.[GENERAL_ID]?.HYPERVISOR
/**
* Optional configuration about VM Template.
*
* @param {VmTemplate} vmTemplate - VM Template
* @returns {object} Optional configuration step
*/
const ExtraConfiguration = (vmTemplate) => {
const initialHypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
return SCHEMA(hypervisor)
},
optionsValidate: { abortEarly: false },
content: Content,
})
return {
id: STEP_ID,
label: T.AdvancedOptions,
resolver: (formData) => {
const hypervisor = formData?.[GENERAL_ID]?.HYPERVISOR ?? initialHypervisor
return SCHEMA(hypervisor)
},
optionsValidate: { abortEarly: false },
content: Content,
}
}
Content.propTypes = {
data: PropTypes.any,

View File

@ -20,6 +20,7 @@ export default makeStyles((theme) => ({
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: theme.spacing(1),
overflow: 'auto',
[theme.breakpoints.down('md')]: {
gridTemplateColumns: '1fr',
},

View File

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { reach } from 'yup'
import General, {
STEP_ID as GENERAL_ID,
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/General'
@ -22,8 +24,14 @@ import 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, encodeBase64 } from 'client/utils'
import {
createSteps,
isBase64,
encodeBase64,
getUnknownAttributes,
} from 'client/utils'
const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
transformInitialValue: (vmTemplate, schema) => {
@ -39,19 +47,33 @@ const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
{ stripUnknown: true, context: { [EXTRA_ID]: vmTemplate.TEMPLATE } }
)
const customVars = {}
const knownAttributes = Object.getOwnPropertyNames({
const knownAttributes = {
...knownTemplate[GENERAL_ID],
...knownTemplate[EXTRA_ID],
})
}
Object.entries(vmTemplate?.TEMPLATE).forEach(([key, value]) => {
if (!knownAttributes.includes(key) && value) {
customVars[key] = value
}
})
// Set the unknown attributes to the custom variables section
knownTemplate[CUSTOM_ID] = getUnknownAttributes(
vmTemplate?.TEMPLATE,
knownAttributes
)
return { ...knownTemplate, [CUSTOM_ID]: customVars }
// Get the custom vars from the context
const knownContext = reach(schema, `${EXTRA_ID}.CONTEXT`).cast(
vmTemplate?.TEMPLATE?.CONTEXT,
{ stripUnknown: true, context: { extra: vmTemplate.TEMPLATE } }
)
// Merge known and unknown context custom vars
knownTemplate[EXTRA_ID].CONTEXT = {
...reach(schema, `${EXTRA_ID}.CONTEXT`).cast(
vmTemplate?.TEMPLATE?.CONTEXT,
{ stripUnknown: true, context: { extra: vmTemplate.TEMPLATE } }
),
...getUnknownAttributes(vmTemplate?.TEMPLATE?.CONTEXT, knownContext),
}
return knownTemplate
},
transformBeforeSubmit: (formData) => {
const {

View File

@ -20,6 +20,7 @@ export default makeStyles((theme) => ({
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '2em',
overflow: 'auto',
[theme.breakpoints.down('lg')]: {
gridTemplateColumns: '1fr',
},

View File

@ -45,6 +45,8 @@ export default makeStyles((theme) => ({
paddingBottom: theme.spacing(2),
height: '100%',
overflow: 'auto',
display: 'flex',
flexDirection: 'column',
},
/* ROUTES TRANSITIONS */
appear: {},

View File

@ -42,6 +42,8 @@ const {
DELETE_ATTRIBUTE: DELETE,
} = ACTIONS
const ALL_ACTIONS = [COPY, ADD, EDIT, DELETE]
// This attributes has special restrictions
const SPECIAL_ATTRIBUTES = {
VCENTER_CCR_REF: {
@ -87,7 +89,8 @@ const AttributePanel = memo(
handleEdit,
handleDelete,
handleAdd,
actions = [],
allActionsEnabled = true,
actions = allActionsEnabled ? ALL_ACTIONS : [],
filtersSpecialAttributes = true,
collapse = false,
}) => {
@ -136,6 +139,7 @@ AttributePanel.propTypes = {
handleDelete: PropTypes.func,
title: PropTypes.string,
filtersSpecialAttributes: PropTypes.bool,
allActionsEnabled: PropTypes.bool,
collapse: PropTypes.bool,
}

View File

@ -691,6 +691,10 @@ module.exports = {
The contextualization package executes an init.sh file if it exists.
If more than one script file is added, this list contains the scripts
to run and their order`,
ContextCustomVariables: 'Context Custom Variables',
ContextCustomVariablesConcept:
'Context information will be send to the VM at boot time',
ContextCustomVarErrorExists: 'Context Custom Variable already exists',
/* VM Template schema - Input/Output */
InputOrOutput: 'Input / Output',
Inputs: 'Inputs',

View File

@ -478,3 +478,27 @@ export const intersperse = (arr, sep) => {
.slice(1)
.reduce((xs, x, i) => xs.concat([sep, x]), [ensuredArr[0]])
}
/**
* Returns the unknown properties of an object.
*
* @param {object} obj - Object
* @param {string[]|object} knownAttributes - Attributes to check
* @returns {object} Returns object with unknown properties
*/
export const getUnknownAttributes = (obj, knownAttributes) => {
const unknown = {}
const entries = Object.entries(obj)
const attributes = Array.isArray(knownAttributes)
? knownAttributes
: Object.getOwnPropertyNames({ ...knownAttributes })
for (const [key, value] of entries) {
if (!attributes.includes(key) && value !== undefined) {
unknown[key] = obj[key]
}
}
return unknown
}

View File

@ -475,6 +475,7 @@ export const createSteps =
steps: performedSteps,
defaultValues,
resolver: generateSchema,
initialValues,
...extraParams,
}
}