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

F #5836: Add form to update VM configuration (#2112)

This commit is contained in:
Sergio Betanzos 2022-05-31 17:25:23 +02:00 committed by GitHub
parent 60c033b563
commit 67c626832f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1014 additions and 273 deletions

View File

@ -27,9 +27,7 @@ import {
DialogPropTypes,
} from 'client/components/Dialogs'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import SubmitButton, {
SubmitButtonPropTypes,
} from 'client/components/FormControl/SubmitButton'
import SubmitButton from 'client/components/FormControl/SubmitButton'
import FormStepper from 'client/components/FormStepper'
import { Translate } from 'client/components/HOC'
import { isDevelopment } from 'client/utils'
@ -130,6 +128,7 @@ const ButtonToTriggerForm = ({ buttonProps = {}, options = [] }) => {
resolver,
description,
fields,
ContentForm,
onSubmit,
}) =>
resolver && (
@ -145,6 +144,8 @@ const ButtonToTriggerForm = ({ buttonProps = {}, options = [] }) => {
schema={resolver}
onSubmit={onSubmit}
/>
) : ContentForm ? (
<ContentForm />
) : (
<>
{description}
@ -161,7 +162,7 @@ const ButtonToTriggerForm = ({ buttonProps = {}, options = [] }) => {
}
export const ButtonToTriggerFormPropTypes = {
buttonProps: PropTypes.shape(SubmitButtonPropTypes),
buttonProps: PropTypes.shape({ ...SubmitButton.propTypes }),
options: PropTypes.arrayOf(
PropTypes.shape({
cy: PropTypes.string,
@ -176,7 +177,6 @@ export const ButtonToTriggerFormPropTypes = {
}
ButtonToTriggerForm.propTypes = ButtonToTriggerFormPropTypes
ButtonToTriggerForm.displayName = 'ButtonToTriggerForm'
export default ButtonToTriggerForm

View File

@ -0,0 +1,42 @@
/* ------------------------------------------------------------------------- *
* 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, useMemo } from 'react'
import PropTypes from 'prop-types'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { SECTIONS } from 'client/components/Forms/Vm/UpdateConfigurationForm/booting/schema'
import { HYPERVISORS } from 'client/constants'
/**
* @param {object} props - Component props
* @param {HYPERVISORS} props.hypervisor - VM hypervisor
* @returns {ReactElement} OS section component
*/
const OsSection = ({ hypervisor }) => {
const sections = useMemo(() => SECTIONS(hypervisor), [hypervisor])
return (
<>
{sections.map(({ id, ...section }) => (
<FormWithSchema key={id} cy={id} {...section} />
))}
</>
)
}
OsSection.propTypes = { hypervisor: PropTypes.string }
export default OsSection

View File

@ -0,0 +1,79 @@
/* ------------------------------------------------------------------------- *
* 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 { ObjectSchema } from 'yup'
import * as bootingSchema from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting/schema'
import {
Field,
Section,
getObjectSchemaFromFields,
filterFieldsByHypervisor,
} from 'client/utils'
import { T, HYPERVISORS, ATTR_CONF_CAN_BE_UPDATED } from 'client/constants'
const getFields = (section) =>
section.map((attr) => bootingSchema[attr]).filter(Boolean)
// Supported fields
const OS_FIELDS = getFields(ATTR_CONF_CAN_BE_UPDATED.OS)
const FEATURES_FIELDS = getFields(ATTR_CONF_CAN_BE_UPDATED.FEATURES)
const RAW_FIELDS = getFields(ATTR_CONF_CAN_BE_UPDATED.RAW)
/**
* @param {object} [formProps] - Form props
* @param {HYPERVISORS} [formProps.hypervisor] - VM hypervisor
* @returns {Section[]} Sections
*/
const SECTIONS = ({ hypervisor }) => [
{
id: 'os-boot',
legend: T.Boot,
fields: filterFieldsByHypervisor(OS_FIELDS, hypervisor),
},
{
id: 'os-features',
legend: T.Features,
fields: filterFieldsByHypervisor(FEATURES_FIELDS, hypervisor),
},
{
id: 'os-raw',
legend: T.RawData,
legendTooltip: T.RawDataConcept,
fields: filterFieldsByHypervisor(RAW_FIELDS, hypervisor),
},
]
/**
* @param {object} [formProps] - Form props
* @param {HYPERVISORS} [formProps.hypervisor] - VM hypervisor
* @returns {Field[]} OS fields
*/
const FIELDS = ({ hypervisor }) => [
...SECTIONS({ hypervisor })
.map(({ fields }) => fields)
.flat(),
]
/**
* @param {object} [formProps] - Form props
* @param {HYPERVISORS} [formProps.hypervisor] - VM hypervisor
* @returns {ObjectSchema} Step schema
*/
const SCHEMA = ({ hypervisor }) =>
getObjectSchemaFromFields(FIELDS({ hypervisor }))
export { SECTIONS, FIELDS, SCHEMA }

View File

@ -0,0 +1,75 @@
/* ------------------------------------------------------------------------- *
* 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, useMemo } from 'react'
import PropTypes from 'prop-types'
import { useFormContext } from 'react-hook-form'
import {
SystemShut as OsIcon,
DataTransferBoth as IOIcon,
Folder as ContextIcon,
} from 'iconoir-react'
import InputOutput from 'client/components/Forms/Vm/UpdateConfigurationForm/inputOutput'
import Booting from 'client/components/Forms/Vm/UpdateConfigurationForm/booting'
import Context from 'client/components/Forms/Vm/UpdateConfigurationForm/context'
import Tabs from 'client/components/Tabs'
import { Translate } from 'client/components/HOC'
import { T, HYPERVISORS } from 'client/constants'
/**
* @param {object} props - Component props
* @param {HYPERVISORS} props.hypervisor - VM hypervisor
* @returns {ReactElement} Form content component
*/
const Content = ({ hypervisor }) => {
const {
formState: { errors },
} = useFormContext()
const tabs = useMemo(
() => [
{
id: 'booting',
icon: OsIcon,
label: <Translate word={T.OSAndCpu} />,
renderContent: () => <Booting hypervisor={hypervisor} />,
error: !!errors?.OS,
},
{
id: 'input_output',
icon: IOIcon,
label: <Translate word={T.InputOrOutput} />,
renderContent: () => <InputOutput hypervisor={hypervisor} />,
error: ['GRAPHICS', 'INPUT'].some((id) => errors?.[id]),
},
{
id: 'context',
icon: ContextIcon,
label: <Translate word={T.Context} />,
renderContent: () => <Context hypervisor={hypervisor} />,
error: !!errors?.CONTEXT,
},
],
[errors, hypervisor]
)
return <Tabs tabs={tabs} />
}
Content.propTypes = { hypervisor: PropTypes.string }
export default Content

View File

@ -0,0 +1,40 @@
/* ------------------------------------------------------------------------- *
* 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 } from 'react'
import PropTypes from 'prop-types'
import ConfigurationSection from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/configurationSection'
import FilesSection from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/filesSection'
import ContextVarsSection from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/contextVarsSection'
import { HYPERVISORS } from 'client/constants'
/**
* @param {object} props - Component props
* @param {HYPERVISORS} props.hypervisor - VM hypervisor
* @returns {ReactElement} Context section component
*/
const ContextSection = ({ hypervisor }) => (
<>
<ConfigurationSection hypervisor={hypervisor} />
<FilesSection hypervisor={hypervisor} />
<ContextVarsSection hypervisor={hypervisor} />
</>
)
ContextSection.propTypes = { hypervisor: PropTypes.string }
export default ContextSection

View File

@ -0,0 +1,28 @@
/* ------------------------------------------------------------------------- *
* 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 {
CONFIGURATION_SCHEMA,
FILES_SCHEMA,
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/schema'
/**
* @param {string} [hypervisor] - VM hypervisor
* @returns {ObjectSchema} Context schema
*/
export const SCHEMA = (hypervisor) =>
object().concat(CONFIGURATION_SCHEMA).concat(FILES_SCHEMA(hypervisor))

View File

@ -0,0 +1,51 @@
/* ------------------------------------------------------------------------- *
* 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 { reach } from 'yup'
import { SCHEMA } from 'client/components/Forms/Vm/UpdateConfigurationForm/schema'
import ContentForm from 'client/components/Forms/Vm/UpdateConfigurationForm/content'
import { ensureContextWithScript } from 'client/components/Forms/VmTemplate/CreateForm/Steps'
import { createForm, getUnknownAttributes } from 'client/utils'
const UpdateConfigurationForm = createForm(SCHEMA, undefined, {
ContentForm,
transformInitialValue: (vmTemplate, schema) => {
const template = vmTemplate?.TEMPLATE ?? {}
const context = template?.CONTEXT ?? {}
const knownTemplate = schema.cast(
{ ...vmTemplate, ...template },
{ stripUnknown: true, context: { ...template } }
)
// Get the custom vars from the context
const knownContext = reach(schema, 'CONTEXT').cast(context, {
stripUnknown: true,
context: { ...template },
})
// Merge known and unknown context custom vars
knownTemplate.CONTEXT = {
...knownContext,
...getUnknownAttributes(context, knownContext),
}
return knownTemplate
},
transformBeforeSubmit: (formData) => ensureContextWithScript(formData),
})
export default UpdateConfigurationForm

View File

@ -0,0 +1,49 @@
/* ------------------------------------------------------------------------- *
* 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, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { FormWithSchema } from 'client/components/Forms'
import InputsSection from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/inputsSection'
import { GRAPHICS_FIELDS } from 'client/components/Forms/Vm/UpdateConfigurationForm/inputOutput/schema'
import { T, HYPERVISORS } from 'client/constants'
/**
* @param {object} props - Component props
* @param {HYPERVISORS} props.hypervisor - VM hypervisor
* @returns {ReactElement} IO section component
*/
const InputOutput = ({ hypervisor }) => (
<Stack
display="grid"
gap="1em"
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
>
<FormWithSchema
cy={'io-graphics'}
fields={useMemo(() => GRAPHICS_FIELDS({ hypervisor }), [hypervisor])}
legend={T.Graphics}
/>
<InputsSection hypervisor={hypervisor} />
</Stack>
)
InputOutput.propTypes = { hypervisor: PropTypes.string }
InputOutput.displayName = 'InputOutput'
export default InputOutput

View File

@ -0,0 +1,57 @@
/* ------------------------------------------------------------------------- *
* 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 * as ioSchema from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/schema'
import {
Field,
filterFieldsByHypervisor,
getObjectSchemaFromFields,
} from 'client/utils'
import { HYPERVISORS, ATTR_CONF_CAN_BE_UPDATED } from 'client/constants'
const getFields = (section) =>
section.map((attr) => ioSchema[attr]).filter(Boolean)
/**
* @param {object} [formProps] - Form props
* @param {HYPERVISORS} [formProps.hypervisor] - VM hypervisor
* @returns {Field[]} List of Graphics editable fields
*/
export const GRAPHICS_FIELDS = ({ hypervisor }) =>
filterFieldsByHypervisor(
getFields(ATTR_CONF_CAN_BE_UPDATED.GRAPHICS),
hypervisor
)
/**
* @param {object} [formProps] - Form props
* @param {HYPERVISORS} [formProps.hypervisor] - VM hypervisor
* @returns {ObjectSchema} Graphics schema
*/
export const GRAPHICS_SCHEMA = ({ hypervisor }) =>
getObjectSchemaFromFields(GRAPHICS_FIELDS(hypervisor))
/**
* @param {object} [formProps] - Form props
* @param {HYPERVISORS} [formProps.hypervisor] - VM hypervisor
* @returns {ObjectSchema} I/O schema
*/
export const SCHEMA = ({ hypervisor }) =>
object()
.concat(ioSchema.INPUTS_SCHEMA)
.concat(GRAPHICS_SCHEMA({ hypervisor }))

View File

@ -0,0 +1,34 @@
/* ------------------------------------------------------------------------- *
* 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 { HYPERVISORS } from 'client/constants'
import { SCHEMA as OS_SCHEMA } from './booting/schema'
import { SCHEMA as IO_SCHEMA } from './inputOutput/schema'
import { SCHEMA as CONTEXT_SCHEMA } from './context/schema'
/**
* @param {object} [formProps] - Form props
* @param {HYPERVISORS} [formProps.hypervisor] - VM hypervisor
* @returns {ObjectSchema} Configuration schema
*/
export const SCHEMA = ({ hypervisor }) =>
object()
.concat(IO_SCHEMA({ hypervisor }))
.concat(OS_SCHEMA({ hypervisor }))
.concat(CONTEXT_SCHEMA({ hypervisor }))
export { IO_SCHEMA, OS_SCHEMA, CONTEXT_SCHEMA }

View File

@ -149,6 +149,13 @@ const CreateRelativeCharterForm = (configProps) =>
configProps
)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
*/
const UpdateConfigurationForm = (configProps) =>
AsyncLoadForm({ formPath: 'Vm/UpdateConfigurationForm' }, configProps)
export {
AttachNicForm,
AttachSecGroupForm,
@ -167,5 +174,6 @@ export {
ResizeDiskForm,
SaveAsDiskForm,
SaveAsTemplateForm,
UpdateConfigurationForm,
VolatileSteps,
}

View File

@ -163,13 +163,13 @@ export const FIRMWARE = {
label: T.Firmware,
tooltip: T.FirmwareConcept,
notOnHypervisors: [firecracker, lxc],
type: ([_, custom] = []) => (custom ? INPUT_TYPES.TEXT : INPUT_TYPES.SELECT),
type: ([, , custom] = []) => (custom ? INPUT_TYPES.TEXT : INPUT_TYPES.SELECT),
validation: string()
.trim()
.notRequired()
.default(() => undefined),
dependOf: ['$general.HYPERVISOR', FEATURE_CUSTOM_ENABLED.name],
values: ([hypervisor] = []) => {
dependOf: ['HYPERVISOR', '$general.HYPERVISOR', FEATURE_CUSTOM_ENABLED.name],
values: ([templateHyperv, hypervisor = templateHyperv] = []) => {
const types =
{
[vcenter]: VCENTER_FIRMWARE_TYPES,

View File

@ -90,3 +90,8 @@ const FIELDS = (hypervisor) => [
const SCHEMA = (hypervisor) => getObjectSchemaFromFields(FIELDS(hypervisor))
export { SECTIONS, FIELDS, BOOT_ORDER_FIELD, SCHEMA }
export * from './bootSchema'
export * from './kernelSchema'
export * from './ramdiskSchema'
export * from './featuresSchema'
export * from './rawSchema'

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string, boolean, ref, ObjectSchema } from 'yup'
import { string, boolean, lazy, ObjectSchema } from 'yup'
import { T, INPUT_TYPES } from 'client/constants'
import { Field, getObjectSchemaFromFields, decodeBase64 } from 'client/utils'
@ -64,9 +64,9 @@ 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')),
validation: lazy((_, { context }) =>
boolean().default(() => !!context?.CONTEXT?.START_SCRIPT_BASE64)
),
}
/** @type {Field} Start script field */
@ -76,13 +76,21 @@ export const START_SCRIPT = {
tooltip: T.StartScriptConcept,
type: INPUT_TYPES.TEXT,
multiline: true,
validation: string()
.trim()
.notRequired()
.ensure()
.when('$extra.CONTEXT.START_SCRIPT_BASE64', (scriptEncoded, schema) =>
scriptEncoded ? schema.default(() => decodeBase64(scriptEncoded)) : schema
),
validation: lazy((value, { context }) =>
string()
.trim()
.notRequired()
.ensure()
.default(() => {
try {
const base64 = context?.CONTEXT?.START_SCRIPT_BASE64
return value ?? decodeBase64(base64 ?? '')
} catch {
return value
}
})
),
grid: { md: 12 },
fieldProps: { rows: 4 },
}

View File

@ -13,34 +13,47 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo, JSXElementConstructor } from 'react'
import { ReactElement, useCallback, useMemo } from 'react'
import PropTypes from 'prop-types'
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'
const SSH_KEY_USER = '$USER[SSH_PUBLIC_KEY]'
/** @returns {JSXElementConstructor} - Configuration section */
const ConfigurationSection = () => {
/**
* Renders the configuration section to VM Template form.
*
* @param {object} props - Props passed to the component
* @param {string} [props.stepId] - ID of the step the section belongs to
* @returns {ReactElement} - Configuration section
*/
const ConfigurationSection = ({ stepId }) => {
const { setValue, getValues } = useFormContext()
const SSH_PUBLIC_KEY_PATH = useMemo(
() => `${EXTRA_ID}.${SSH_PUBLIC_KEY.name}`,
[]
() => [stepId, SSH_PUBLIC_KEY.name].filter(Boolean).join('.'),
[stepId]
)
const handleClearKey = () => setValue(SSH_PUBLIC_KEY_PATH)
const getCyPath = useCallback(
(cy) => [stepId, cy].filter(Boolean).join('-'),
[stepId]
)
const handleAddUserKey = () => {
const handleClearKey = useCallback(
() => setValue(SSH_PUBLIC_KEY_PATH),
[setValue, SSH_PUBLIC_KEY_PATH]
)
const handleAddUserKey = useCallback(() => {
let currentKey = getValues(SSH_PUBLIC_KEY_PATH)
currentKey &&= currentKey + '\n'
setValue(SSH_PUBLIC_KEY_PATH, `${currentKey ?? ''}${SSH_KEY_USER}`)
}
}, [getValues, setValue, SSH_PUBLIC_KEY_PATH])
return (
<FormControl component="fieldset" sx={{ width: '100%' }}>
@ -51,22 +64,22 @@ const ConfigurationSection = () => {
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
>
<FormWithSchema
cy={`${EXTRA_ID}-context-configuration-others`}
id={stepId}
cy={getCyPath('context-configuration-others')}
fields={OTHER_FIELDS}
id={EXTRA_ID}
/>
<div>
<section>
<FormWithSchema
cy={`${EXTRA_ID}-context-ssh-public-key`}
id={stepId}
cy={getCyPath('context-ssh-public-key')}
fields={[SSH_PUBLIC_KEY]}
id={EXTRA_ID}
/>
<Stack direction="row" gap="1em">
<Button
onClick={handleAddUserKey}
color="secondary"
variant="contained"
data-cy={`${EXTRA_ID}-add-context-ssh-public-key`}
data-cy={getCyPath('add-context-ssh-public-key')}
>
{T.AddUserSshPublicKey}
</Button>
@ -78,11 +91,11 @@ const ConfigurationSection = () => {
{T.Clear}
</Button>
</Stack>
</div>
</section>
<FormWithSchema
cy={`${EXTRA_ID}-context-script`}
id={stepId}
cy={getCyPath('context-script')}
fields={SCRIPT_FIELDS}
id={EXTRA_ID}
rootProps={{ sx: { width: '100%', gridColumn: '1 / -1' } }}
/>
</Stack>
@ -90,4 +103,9 @@ const ConfigurationSection = () => {
)
}
ConfigurationSection.propTypes = {
stepId: PropTypes.string,
hypervisor: PropTypes.string,
}
export default ConfigurationSection

View File

@ -19,9 +19,7 @@ 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'
@ -32,16 +30,19 @@ import { T } from 'client/constants'
export const SECTION_ID = 'CONTEXT'
/**
* Renders the context section of the extra configuration form.
* Renders the context section to VM Template form.
*
* @param {object} props - Props passed to the component
* @param {string} [props.stepId] - ID of the step the section belongs to
* @param {string} props.hypervisor - VM hypervisor
* @returns {ReactElement} - Context vars section
*/
const ContextVarsSection = ({ hypervisor }) => {
const ContextVarsSection = ({ stepId, hypervisor }) => {
const { enqueueError } = useGeneralApi()
const { setValue } = useFormContext()
const customVars = useWatch({ name: `${EXTRA_ID}.${SECTION_ID}` })
const customVars = useWatch({
name: [stepId, SECTION_ID].filter(Boolean).join('.'),
})
const unknownVars = useMemo(() => {
const knownVars = CONTEXT_SCHEMA(hypervisor).cast(
@ -57,7 +58,7 @@ const ContextVarsSection = ({ hypervisor }) => {
const handleChangeAttribute = useCallback(
(path, newValue) => {
const contextPath = `${SECTION_ID}.${path}`
const formPath = `${EXTRA_ID}.${contextPath}`
const formPath = [stepId, contextPath].filter(Boolean).join('.')
try {
// retrieve the schema for the given path
@ -100,10 +101,8 @@ const ContextVarsSection = ({ hypervisor }) => {
}
ContextVarsSection.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
stepId: PropTypes.string,
hypervisor: PropTypes.string,
control: PropTypes.object,
}
export default ContextVarsSection

View File

@ -13,12 +13,11 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { JSXElementConstructor } from 'react'
import { ReactElement, useMemo } from 'react'
import PropTypes from 'prop-types'
import { FormWithSchema } from 'client/components/Forms'
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
import { FILES_FIELDS } from './schema'
import { T } from 'client/constants'
@ -26,24 +25,26 @@ export const SECTION_ID = 'CONTEXT'
/**
* @param {object} props - Props
* @param {string} [props.stepId] - ID of the step the section belongs to
* @param {string} props.hypervisor - VM hypervisor
* @returns {JSXElementConstructor} - Files section
* @returns {ReactElement} - Files section
*/
const FilesSection = ({ hypervisor }) => (
const FilesSection = ({ stepId, hypervisor }) => (
<FormWithSchema
accordion
cy={`${EXTRA_ID}-context-files`}
legend={T.Files}
fields={() => FILES_FIELDS(hypervisor)}
id={EXTRA_ID}
id={stepId}
cy={useMemo(
() => [stepId, 'context-files'].filter(Boolean).join('-'),
[stepId]
)}
fields={useMemo(() => FILES_FIELDS(hypervisor), [hypervisor])}
/>
)
FilesSection.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
stepId: PropTypes.string,
hypervisor: PropTypes.string,
control: PropTypes.object,
}
export default FilesSection

View File

@ -16,7 +16,10 @@
import PropTypes from 'prop-types'
import { Folder as ContextIcon } from 'iconoir-react'
import { TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
import {
TabType,
STEP_ID as EXTRA_ID,
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
import UserInputsSection, {
SECTION_ID as USER_INPUTS_ID,
} from './userInputsSection'
@ -30,10 +33,10 @@ export const TAB_ID = ['CONTEXT', USER_INPUTS_ID]
const Context = (props) => (
<>
<ConfigurationSection />
<ConfigurationSection stepId={EXTRA_ID} />
<UserInputsSection />
<FilesSection {...props} />
<ContextVarsSection {...props} />
<FilesSection stepId={EXTRA_ID} {...props} />
<ContextVarsSection stepId={EXTRA_ID} {...props} />
</>
)

View File

@ -26,11 +26,11 @@ import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
const { vcenter, lxc, kvm } = HYPERVISORS
/** @type {Field} Type field */
const TYPE = {
export const TYPE = {
name: 'GRAPHICS.TYPE',
type: INPUT_TYPES.TOGGLE,
dependOf: '$general.HYPERVISOR',
values: (hypervisor = kvm) => {
dependOf: ['HYPERVISOR', '$general.HYPERVISOR'],
values: ([templateHyperv = kvm, hypervisor = templateHyperv] = []) => {
const types = {
[vcenter]: [T.Vmrc],
[lxc]: [T.Vnc],
@ -41,12 +41,13 @@ const TYPE = {
validation: string()
.trim()
.notRequired()
.uppercase()
.default(() => undefined),
grid: { md: 12 },
}
/** @type {Field} Listen field */
const LISTEN = {
export const LISTEN = {
name: 'GRAPHICS.LISTEN',
label: T.ListenOnIp,
type: INPUT_TYPES.TEXT,
@ -61,7 +62,7 @@ const LISTEN = {
}
/** @type {Field} Port field */
const PORT = {
export const PORT = {
name: 'GRAPHICS.PORT',
label: T.ServerPort,
tooltip: T.ServerPortConcept,
@ -75,7 +76,7 @@ const PORT = {
}
/** @type {Field} Keymap field */
const KEYMAP = {
export const KEYMAP = {
name: 'GRAPHICS.KEYMAP',
label: T.Keymap,
type: INPUT_TYPES.TEXT,
@ -89,7 +90,7 @@ const KEYMAP = {
}
/** @type {Field} Password random field */
const RANDOM_PASSWD = {
export const RANDOM_PASSWD = {
name: 'GRAPHICS.RANDOM_PASSWD',
label: T.GenerateRandomPassword,
type: INPUT_TYPES.CHECKBOX,
@ -100,7 +101,7 @@ const RANDOM_PASSWD = {
}
/** @type {Field} Password field */
const PASSWD = {
export const PASSWD = {
name: 'GRAPHICS.PASSWD',
label: T.Password,
type: INPUT_TYPES.PASSWORD,
@ -115,7 +116,7 @@ const PASSWD = {
}
/** @type {Field} Command field */
const COMMAND = {
export const COMMAND = {
name: 'GRAPHICS.COMMAND',
label: T.Command,
notOnHypervisors: [lxc],
@ -139,6 +140,6 @@ export const GRAPHICS_FIELDS = (hypervisor) =>
hypervisor
)
/** @type {ObjectSchema} Context Files schema */
/** @type {ObjectSchema} Graphics schema */
export const GRAPHICS_SCHEMA = (hypervisor) =>
getObjectSchemaFromFields(GRAPHICS_FIELDS(hypervisor))

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { DataTransferBoth as IOIcon } from 'iconoir-react'
@ -26,34 +25,27 @@ import {
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
import InputsSection, { SECTION_ID as INPUT_ID } from './inputsSection'
import PciDevicesSection, { SECTION_ID as PCI_ID } from './pciDevicesSection'
import { GRAPHICS_FIELDS, INPUTS_FIELDS, PCI_FIELDS } from './schema'
import { GRAPHICS_FIELDS } from './schema'
import { T } from 'client/constants'
export const TAB_ID = ['GRAPHICS', INPUT_ID, PCI_ID]
const InputOutput = ({ hypervisor }) => {
const inputsFields = useMemo(() => INPUTS_FIELDS(hypervisor), [hypervisor])
const pciDevicesFields = useMemo(() => PCI_FIELDS(hypervisor), [hypervisor])
return (
<Stack
display="grid"
gap="1em"
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
>
<FormWithSchema
cy={`${EXTRA_ID}-io-graphics`}
fields={GRAPHICS_FIELDS(hypervisor)}
legend={T.Graphics}
id={EXTRA_ID}
/>
{inputsFields.length > 0 && <InputsSection fields={inputsFields} />}
{pciDevicesFields.length > 0 && (
<PciDevicesSection fields={pciDevicesFields} />
)}
</Stack>
)
}
const InputOutput = ({ hypervisor }) => (
<Stack
display="grid"
gap="1em"
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
>
<FormWithSchema
cy={`${EXTRA_ID}-io-graphics`}
fields={GRAPHICS_FIELDS(hypervisor)}
legend={T.Graphics}
id={EXTRA_ID}
/>
<InputsSection stepId={EXTRA_ID} hypervisor={hypervisor} />
<PciDevicesSection stepId={EXTRA_ID} hypervisor={hypervisor} />
</Stack>
)
InputOutput.propTypes = {
data: PropTypes.any,

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { JSXElementConstructor } from 'react'
import { ReactElement, useCallback, memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Stack, FormControl, Divider, Button, IconButton } from '@mui/material'
import List from '@mui/material/List'
@ -26,109 +26,131 @@ import { yupResolver } from '@hookform/resolvers/yup'
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 { INPUT_SCHEMA, deviceTypeIcons, busTypeIcons } from './schema'
import { T } from 'client/constants'
import {
INPUTS_FIELDS,
INPUT_SCHEMA,
deviceTypeIcons,
busTypeIcons,
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/schema'
import { T, HYPERVISORS } from 'client/constants'
export const SECTION_ID = 'INPUT'
/**
* @param {object} props - Props
* @param {Array} props.fields - Fields
* @returns {JSXElementConstructor} - Inputs section
*/
const InputsSection = ({ fields }) => {
const {
fields: inputs,
append,
remove,
} = useFieldArray({
name: `${EXTRA_ID}.${SECTION_ID}`,
})
const InputsSection = memo(
/**
* @param {object} props - Props
* @param {string} [props.stepId] - ID of the step the section belongs to
* @param {HYPERVISORS} props.hypervisor - VM hypervisor
* @returns {ReactElement} - Inputs section
*/
({ stepId, hypervisor }) => {
const fields = useMemo(() => INPUTS_FIELDS(hypervisor), [hypervisor])
const methods = useForm({
defaultValues: INPUT_SCHEMA.default(),
resolver: yupResolver(INPUT_SCHEMA),
})
const {
fields: inputs,
append,
remove,
} = useFieldArray({
name: useMemo(
() => [stepId, SECTION_ID].filter(Boolean).join('.'),
[stepId]
),
})
const onSubmit = (newInput) => {
append(newInput)
methods.reset()
}
const getCyPath = useCallback(
(cy) => [stepId, cy].filter(Boolean).join('-'),
[stepId]
)
return (
<FormControl component="fieldset" sx={{ width: '100%' }}>
<Legend title={T.Inputs} />
<FormProvider {...methods}>
<Stack
direction="row"
alignItems="flex-start"
gap="0.5rem"
component="form"
onSubmit={methods.handleSubmit(onSubmit)}
>
<FormWithSchema
cy={`${EXTRA_ID}-io-inputs`}
fields={fields}
rootProps={{ sx: { m: 0 } }}
/>
<Button
variant="contained"
type="submit"
color="secondary"
startIcon={<AddCircledOutline />}
sx={{ mt: '1em' }}
data-cy={`${EXTRA_ID}-add-io-inputs`}
const methods = useForm({
defaultValues: INPUT_SCHEMA.default(),
resolver: yupResolver(INPUT_SCHEMA),
})
const onSubmit = (newInput) => {
append(newInput)
methods.reset()
}
if (fields.length === 0) {
return null
}
return (
<FormControl component="fieldset" sx={{ width: '100%' }}>
<Legend title={T.Inputs} />
<FormProvider {...methods}>
<Stack
direction="row"
alignItems="flex-start"
gap="0.5rem"
component="form"
onSubmit={methods.handleSubmit(onSubmit)}
>
<Translate word={T.Add} />
</Button>
</Stack>
</FormProvider>
<Divider />
<List>
{inputs?.map(({ id, TYPE, BUS }, index) => {
const deviceIcon = deviceTypeIcons[TYPE]
const deviceInfo = `${TYPE}`
const busIcon = busTypeIcons[BUS]
const busInfo = `${BUS}`
return (
<ListItem
key={id}
secondaryAction={
<IconButton onClick={() => remove(index)}>
<DeleteCircledOutline />
</IconButton>
}
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
<FormWithSchema
cy={getCyPath('io-inputs')}
fields={fields}
rootProps={{ sx: { m: 0 } }}
/>
<Button
variant="contained"
type="submit"
color="secondary"
startIcon={<AddCircledOutline />}
data-cy={getCyPath('add-io-inputs')}
sx={{ mt: '1em' }}
>
<ListItemText
primary={
<Stack
component="span"
direction="row"
spacing={2}
sx={{ '& > *': { width: 36 } }}
>
{deviceIcon}
<span>{deviceInfo}</span>
<Divider orientation="vertical" flexItem />
{busIcon}
<span>{busInfo}</span>
</Stack>
<Translate word={T.Add} />
</Button>
</Stack>
</FormProvider>
<Divider />
<List>
{inputs?.map(({ id, TYPE, BUS }, index) => {
const deviceIcon = deviceTypeIcons[TYPE]
const deviceInfo = `${TYPE}`
const busIcon = busTypeIcons[BUS]
const busInfo = `${BUS}`
return (
<ListItem
key={id}
secondaryAction={
<IconButton onClick={() => remove(index)}>
<DeleteCircledOutline />
</IconButton>
}
primaryTypographyProps={{ variant: 'body1' }}
/>
</ListItem>
)
})}
</List>
</FormControl>
)
}
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
>
<ListItemText
primary={
<Stack
component="span"
direction="row"
spacing={2}
sx={{ '& > *': { width: 36 } }}
>
{deviceIcon}
<span>{deviceInfo}</span>
<Divider orientation="vertical" flexItem />
{busIcon}
<span>{busInfo}</span>
</Stack>
}
primaryTypographyProps={{ variant: 'body1' }}
/>
</ListItem>
)
})}
</List>
</FormControl>
)
}
)
InputsSection.propTypes = {
fields: PropTypes.array,
stepId: PropTypes.string,
hypervisor: PropTypes.string,
}
InputsSection.displayName = 'InputsSection'

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { JSXElementConstructor, useMemo } from 'react'
import { ReactElement, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Stack, FormControl, Divider, Button, IconButton } from '@mui/material'
import List from '@mui/material/List'
@ -28,18 +28,23 @@ import { FormWithSchema, Legend } from 'client/components/Forms'
import { Translate } from 'client/components/HOC'
import { getPciDevices } from 'client/models/Host'
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
import { PCI_SCHEMA } from './schema'
import { T } from 'client/constants'
import {
PCI_FIELDS,
PCI_SCHEMA,
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/schema'
import { T, HYPERVISORS } from 'client/constants'
export const SECTION_ID = 'PCI'
/**
* @param {object} props - Props
* @param {Array} props.fields - Fields
* @returns {JSXElementConstructor} - Inputs section
* @param {string} [props.stepId] - ID of the step the section belongs to
* @param {HYPERVISORS} props.hypervisor - VM hypervisor
* @returns {ReactElement} - Inputs section
*/
const PciDevicesSection = ({ fields }) => {
const PciDevicesSection = ({ stepId, hypervisor }) => {
const fields = useMemo(() => PCI_FIELDS(hypervisor))
const { data: hosts = [] } = useGetHostsQuery()
const pciDevicesAvailable = useMemo(
() => hosts.map(getPciDevices).flat(),
@ -51,7 +56,7 @@ const PciDevicesSection = ({ fields }) => {
append,
remove,
} = useFieldArray({
name: `${EXTRA_ID}.${SECTION_ID}`,
name: [stepId, SECTION_ID].filter(Boolean).join('.'),
})
const methods = useForm({
@ -79,7 +84,7 @@ const PciDevicesSection = ({ fields }) => {
onSubmit={methods.handleSubmit(onSubmit)}
>
<FormWithSchema
cy={`${EXTRA_ID}-io-pci-devices`}
cy={[stepId, 'io-pci-devices'].filter(Boolean).join('.')}
fields={fields}
rootProps={{ sx: { m: 0 } }}
/>
@ -130,7 +135,8 @@ const PciDevicesSection = ({ fields }) => {
}
PciDevicesSection.propTypes = {
fields: PropTypes.array,
stepId: PropTypes.string,
hypervisor: PropTypes.string,
}
PciDevicesSection.displayName = 'PciDevicesSection'

View File

@ -33,6 +33,30 @@ import {
getUnknownAttributes,
} from 'client/utils'
/**
* Encodes the start script value to base64 if it is not already encoded.
*
* @param {object} template - VM template
* @returns {object} Context with the start script value encoded
*/
export const ensureContextWithScript = (template = {}) => {
template.CONTEXT = ((context = {}) => {
const { START_SCRIPT, ENCODE_START_SCRIPT, ...restOfContext } = context
if (!START_SCRIPT) return { ...restOfContext }
if (!ENCODE_START_SCRIPT) return { ...restOfContext, START_SCRIPT }
// encode the script if it is not already encoded
const encodedScript = isBase64(START_SCRIPT)
? START_SCRIPT
: encodeBase64(START_SCRIPT)
return { ...restOfContext, START_SCRIPT_BASE64: encodedScript }
})(template.CONTEXT)
return { ...template }
}
const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
transformInitialValue: (vmTemplate, schema) => {
const userInputs = userInputsToArray(vmTemplate?.TEMPLATE?.USER_INPUTS, {
@ -44,7 +68,10 @@ const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
[GENERAL_ID]: { ...vmTemplate, ...vmTemplate?.TEMPLATE },
[EXTRA_ID]: { ...vmTemplate?.TEMPLATE, USER_INPUTS: userInputs },
},
{ stripUnknown: true, context: { [EXTRA_ID]: vmTemplate.TEMPLATE } }
{
stripUnknown: true,
context: { ...vmTemplate, [EXTRA_ID]: vmTemplate.TEMPLATE },
}
)
const knownAttributes = {
@ -61,15 +88,18 @@ const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
// Get the custom vars from the context
const knownContext = reach(schema, `${EXTRA_ID}.CONTEXT`).cast(
vmTemplate?.TEMPLATE?.CONTEXT,
{ stripUnknown: true, context: { extra: vmTemplate.TEMPLATE } }
{
stripUnknown: true,
context: {
...vmTemplate,
[EXTRA_ID]: 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 } }
),
...knownContext,
...getUnknownAttributes(vmTemplate?.TEMPLATE?.CONTEXT, knownContext),
}
@ -77,38 +107,25 @@ const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
},
transformBeforeSubmit: (formData) => {
const {
[GENERAL_ID]: { MODIFICATION: _, ...general } = {},
[GENERAL_ID]: general = {},
[CUSTOM_ID]: customVariables = {},
[EXTRA_ID]: {
CONTEXT: { START_SCRIPT, ENCODE_START_SCRIPT, ...restOfContext },
TOPOLOGY: { ENABLE_NUMA, ...restOfTopology },
...extraTemplate
} = {},
[EXTRA_ID]: extraTemplate = {},
} = formData ?? {}
const context = {
...restOfContext,
// transform start script to base64 if needed
[ENCODE_START_SCRIPT ? 'START_SCRIPT_BASE64' : 'START_SCRIPT']:
ENCODE_START_SCRIPT && !isBase64(START_SCRIPT)
? encodeBase64(START_SCRIPT)
: START_SCRIPT,
}
const topology = ENABLE_NUMA ? { TOPOLOGY: restOfTopology } : {}
ensureContextWithScript(extraTemplate)
// add user inputs to context
Object.keys(extraTemplate?.USER_INPUTS ?? {}).forEach((name) => {
const isCapacity = ['MEMORY', 'CPU', 'VCPU'].includes(name)
const upperName = String(name).toUpperCase()
!isCapacity && (context[upperName] = `$${upperName}`)
!isCapacity && (extraTemplate.CONTEXT[upperName] = `$${upperName}`)
})
return jsonToXml({
...customVariables,
...extraTemplate,
...general,
...topology,
CONTEXT: context,
})
},
})

View File

@ -13,52 +13,147 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import { ReactElement, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Accordion, AccordionSummary, AccordionDetails } from '@mui/material'
import { Box, Stack } from '@mui/material'
import { useGetVmQuery } from 'client/features/OneApi/vm'
import { Translate } from 'client/components/HOC'
import { T } from 'client/constants'
import {
useGetVmQuery,
useUpdateConfigurationMutation,
} from 'client/features/OneApi/vm'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import { UpdateConfigurationForm } from 'client/components/Forms/Vm'
import { List } from 'client/components/Tabs/Common'
import { getHypervisor, isAvailableAction } from 'client/models/VirtualMachine'
import { getActionsAvailable, jsonToXml } from 'client/models/Helper'
import { T, VM_ACTIONS, ATTR_CONF_CAN_BE_UPDATED } from 'client/constants'
const { UPDATE_CONF } = VM_ACTIONS
/**
* Renders configuration tab.
*
* @param {object} props - Props
* @param {object|boolean} props.tabProps - Tab properties
* @param {object} [props.tabProps.actions] - Actions from tab view yaml
* @param {string} props.id - Virtual machine id
* @returns {ReactElement} Configuration tab
*/
const VmConfigurationTab = ({ id }) => {
const { data: vm = {} } = useGetVmQuery({ id })
const { TEMPLATE, USER_TEMPLATE } = vm
const VmConfigurationTab = ({ tabProps: { actions } = {}, id }) => {
const [updateConf] = useUpdateConfigurationMutation()
const { data: vm = {}, isFetching } = useGetVmQuery({ id })
const { TEMPLATE } = vm
const hypervisor = useMemo(() => getHypervisor(vm), [vm])
const isUpdateConfEnabled = useMemo(() => {
const actionsByHypervisor = getActionsAvailable(actions, hypervisor)
const actionsByState = actionsByHypervisor.filter((action) =>
isAvailableAction(action, vm)
)
return actionsByState.includes?.(UPDATE_CONF)
}, [vm])
const [
osAttributes,
featuresAttributes,
inputAttributes,
graphicsAttributes,
rawAttributes,
contextAttributes,
] = useMemo(() => {
const filterSection = (section) => {
const supported = ATTR_CONF_CAN_BE_UPDATED[section] || '*'
const attributes = TEMPLATE[section] || {}
const sectionAttributes = []
const getAttrFromEntry = (key, value, idx) => {
const isSupported = supported === '*' || supported.includes(key)
const hasValue = typeof value === 'string' && value !== ''
if (isSupported && hasValue) {
const name = idx ? `${idx}.${key}` : key
sectionAttributes.push({ name, value, dataCy: name })
}
}
const addAttrFromAttributes = (attrs, keyAsIndex) => {
for (const [key, value] of Object.entries(attrs)) {
typeof value === 'object'
? addAttrFromAttributes(value, key)
: getAttrFromEntry(key, value, keyAsIndex)
}
}
addAttrFromAttributes(attributes)
return sectionAttributes
}
return Object.keys(ATTR_CONF_CAN_BE_UPDATED).map(filterSection)
}, [TEMPLATE])
const handleUpdateConf = async (newConfiguration) => {
const xml = jsonToXml(newConfiguration)
await updateConf({ id, template: xml })
}
return (
<div>
<Accordion variant="outlined">
<AccordionSummary>
<Translate word={T.UserTemplate} />
</AccordionSummary>
<AccordionDetails>
<pre>
<code style={{ whiteSpace: 'break-spaces' }}>
{JSON.stringify(USER_TEMPLATE, null, 2)}
</code>
</pre>
</AccordionDetails>
</Accordion>
<Accordion variant="outlined">
<AccordionSummary>
<Translate word={T.Template} />
</AccordionSummary>
<AccordionDetails>
<pre>
<code style={{ whiteSpace: 'break-spaces' }}>
{JSON.stringify(TEMPLATE, null, 2)}
</code>
</pre>
</AccordionDetails>
</Accordion>
</div>
<Box>
{isUpdateConfEnabled && (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'update-conf',
label: T.UpdateVmConfiguration,
variant: 'outlined',
disabled: isFetching,
}}
options={[
{
dialogProps: {
title: T.UpdateVmConfiguration,
dataCy: 'modal-update-conf',
},
form: () =>
UpdateConfigurationForm({
stepProps: { hypervisor },
initialValues: vm,
}),
onSubmit: handleUpdateConf,
},
]}
/>
)}
<Stack
display="grid"
gap="1em"
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
marginTop="0.5em"
>
{osAttributes?.length > 0 && (
<List title={T.OSAndCpu} list={osAttributes} />
)}
{featuresAttributes?.length > 0 && (
<List title={T.Features} list={featuresAttributes} />
)}
{inputAttributes?.length > 0 && (
<List title={T.Input} list={inputAttributes} />
)}
{graphicsAttributes?.length > 0 && (
<List title={T.Graphics} list={graphicsAttributes} />
)}
{rawAttributes?.length > 0 && (
<List title={T.Raw} list={rawAttributes} />
)}
{contextAttributes?.length > 0 && (
<List title={T.Context} list={contextAttributes} />
)}
</Stack>
</Box>
)
}

View File

@ -0,0 +1,83 @@
/* ------------------------------------------------------------------------- *
* 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 } from 'react'
import PropTypes from 'prop-types'
import {
Box,
Accordion,
AccordionSummary,
AccordionDetails,
} from '@mui/material'
import { useGetVmQuery } from 'client/features/OneApi/vm'
import { Translate } from 'client/components/HOC'
import { T } from 'client/constants'
/**
* Renders template tab.
*
* @param {object} props - Props
* @param {string} props.id - Virtual machine id
* @returns {ReactElement} Template tab
*/
const TemplateTab = ({ id }) => {
const { data: vm = {} } = useGetVmQuery({ id })
const { TEMPLATE, USER_TEMPLATE } = vm
return (
<div>
<Accordion variant="outlined">
<AccordionSummary>
<Translate word={T.UserTemplate} />
</AccordionSummary>
<AccordionDetails>
<Box component="pre">
<Box
component="code"
sx={{ whiteSpace: 'break-spaces', wordBreak: 'break-all' }}
>
{JSON.stringify(USER_TEMPLATE, null, 2)}
</Box>
</Box>
</AccordionDetails>
</Accordion>
<Accordion variant="outlined">
<AccordionSummary>
<Translate word={T.Template} />
</AccordionSummary>
<AccordionDetails>
<Box component="pre">
<Box
component="code"
sx={{ whiteSpace: 'break-spaces', wordBreak: 'break-all' }}
>
{JSON.stringify(TEMPLATE, null, 2)}
</Box>
</Box>
</AccordionDetails>
</Accordion>
</div>
)
}
TemplateTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
TemplateTab.displayName = 'TemplateTab'
export default TemplateTab

View File

@ -23,23 +23,25 @@ import { getAvailableInfoTabs } from 'client/models/Helper'
import { RESOURCE_NAMES } from 'client/constants'
import Tabs from 'client/components/Tabs'
import Configuration from 'client/components/Tabs/Vm/Configuration'
import Info from 'client/components/Tabs/Vm/Info'
import Network from 'client/components/Tabs/Vm/Network'
import History from 'client/components/Tabs/Vm/History'
import SchedActions from 'client/components/Tabs/Vm/SchedActions'
import Snapshot from 'client/components/Tabs/Vm/Snapshot'
import Storage from 'client/components/Tabs/Vm/Storage'
import Configuration from 'client/components/Tabs/Vm/Configuration'
import Template from 'client/components/Tabs/Vm/Template'
const getTabComponent = (tabName) =>
({
configuration: Configuration,
info: Info,
network: Network,
history: History,
schedActions: SchedActions,
snapshot: Snapshot,
storage: Storage,
configuration: Configuration,
template: Template,
}[tabName])
const VmTabs = memo(({ id }) => {

View File

@ -99,16 +99,18 @@ const Tabs = ({
}}
{...tabsProps}
>
{tabs.map(({ id, value, name, label, error, icon: Icon }, idx) => (
<MTab
key={`tab-${name}`}
id={`tab-${name}`}
icon={error ? <WarningIcon /> : Icon && <Icon />}
value={value ?? idx}
label={label ?? name}
data-cy={`tab-${id}`}
/>
))}
{tabs.map(
({ value, name, id = name, label, error, icon: Icon }, idx) => (
<MTab
key={`tab-${id}`}
id={`tab-${id}`}
icon={error ? <WarningIcon /> : Icon && <Icon />}
value={value ?? idx}
label={label ?? id}
data-cy={`tab-${id}`}
/>
)
)}
</MTabs>
),
[tabs, tabSelected]

View File

@ -157,6 +157,7 @@ module.exports = {
UnReschedule: 'Un-Reschedule',
Unshare: 'Unshare',
Update: 'Update',
UpdateVmConfiguration: 'Update VM Configuration',
UpdateProvider: 'Update Provider',
UpdateScheduleAction: 'Update schedule action: %s',
UpdateVmTemplate: 'Update VM Template',
@ -663,6 +664,7 @@ module.exports = {
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`,
Raw: 'Raw',
RawData: 'Raw data',
RawDataConcept: 'Raw data to be passed directly to the hypervisor',
RawValidateConcept: `
@ -701,6 +703,7 @@ module.exports = {
ContextCustomVarErrorExists: 'Context Custom Variable already exists',
/* VM Template schema - Input/Output */
InputOrOutput: 'Input / Output',
Input: 'Input',
Inputs: 'Inputs',
PciDevices: 'PCI Devices',
DeviceName: 'Device name',

View File

@ -1151,3 +1151,22 @@ export const EXTERNAL_IP_ATTRS = [
'AZ_IPADDRESS',
'SL_PRIMARYIPADDRESS',
]
/** @enum {string[]} Supported configuration attributes in the VM */
export const ATTR_CONF_CAN_BE_UPDATED = {
OS: [
'ARCH',
'MACHINE',
'KERNEL',
'INITRD',
'BOOTLOADER',
'BOOT',
'SD_DISK_BUS',
'UUID',
],
FEATURES: ['ACPI', 'PAE', 'APIC', 'LOCALTIME', 'HYPERV', 'GUEST_AGENT'],
INPUT: ['TYPE', 'BUS'],
GRAPHICS: ['TYPE', 'LISTEN', 'PASSWD', 'KEYMAP'],
RAW: ['DATA', 'DATA_VMX', 'TYPE'],
CONTEXT: '*',
}

View File

@ -61,9 +61,7 @@ const DISABLE_ANIMATIONS_FIELD = {
name: 'DISABLE_ANIMATIONS',
label: T.DisableDashboardAnimations,
type: INPUT_TYPES.CHECKBOX,
validation: boolean()
.yesOrNo()
.default(() => false),
validation: boolean(),
grid: { md: 12 },
}

View File

@ -17,6 +17,7 @@
// eslint-disable-next-line no-unused-vars
import { ReactElement, SetStateAction } from 'react'
import {
// eslint-disable-next-line no-unused-vars
GridProps,
@ -174,6 +175,7 @@ import { stringToBoolean } from 'client/models/Helper'
* @typedef {object} ExtraParams
* @property {function(object):object} [transformBeforeSubmit] - Transform validated form data after submit
* @property {function(object, BaseSchema):object} [transformInitialValue] - Transform initial value after load form
* @property {ReactElement} [ContentForm] - Render content of form
*/
/**
@ -500,6 +502,7 @@ export const createForm =
const {
transformBeforeSubmit,
transformInitialValue = defaultTransformInitialValue,
ContentForm,
...restOfParams
} = extraParams
@ -519,6 +522,7 @@ export const createForm =
fields: () => fieldsCallback,
defaultValues,
transformBeforeSubmit,
ContentForm: ContentForm && (() => <ContentForm {...props} />),
...ensuredExtraParams,
}
}

View File

@ -561,7 +561,7 @@ module.exports = {
default: 0,
},
template: {
from: resource,
from: postBody,
default: '',
},
},