mirror of
https://github.com/OpenNebula/one.git
synced 2024-12-22 13:33:52 +03:00
B OpenNebula/one#6376: Fix for VM template create/update (#2962)
so it doesn't adds unnecessary parameters. Signed-off-by: Victor Hansson <vhansson@opennebula.io> Signed-off-by: David Carracedo <dcarracedo@opennebula.io> Co-authored-by: Victor Hansson <vhansson@opennebula.io> Co-authored-by: Tino Vázquez <cvazquez@opennebula.io>
This commit is contained in:
parent
0e1c0ecc49
commit
57f256606e
@ -46,7 +46,7 @@ const DialogForm = ({
|
||||
dialogProps.fixedHeight ??= true
|
||||
|
||||
const methods = useForm({
|
||||
mode: 'onBlur',
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: values,
|
||||
resolver: yupResolver(resolver()),
|
||||
|
@ -47,12 +47,13 @@ const CheckboxController = memo(
|
||||
onConditionChange,
|
||||
}) => {
|
||||
const {
|
||||
field: { value = false, onChange },
|
||||
field: { value = false, onChange, onBlur },
|
||||
fieldState: { error },
|
||||
} = useController({ name, control })
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e) => {
|
||||
onBlur()
|
||||
const condition = e.target.checked
|
||||
onChange(condition)
|
||||
if (typeof onConditionChange === 'function') {
|
||||
|
@ -55,7 +55,7 @@ const FileController = memo(
|
||||
const { setValue, setError, clearErrors, watch } = useFormContext()
|
||||
|
||||
const {
|
||||
field: { ref, value, onChange, ...inputProps },
|
||||
field: { ref, value, onChange, onBlur, ...inputProps },
|
||||
fieldState: { error },
|
||||
} = useController({ name, control })
|
||||
|
||||
@ -92,6 +92,7 @@ const FileController = memo(
|
||||
*/
|
||||
const handleChange = async (event) => {
|
||||
try {
|
||||
onBlur()
|
||||
const file = event.target.files?.[0]
|
||||
|
||||
if (!file) return
|
||||
|
@ -60,7 +60,7 @@ const SelectController = memo(
|
||||
: firstValue
|
||||
|
||||
const {
|
||||
field: { ref, value: optionSelected, onChange, ...inputProps },
|
||||
field: { ref, value: optionSelected, onChange, onBlur, ...inputProps },
|
||||
fieldState: { error },
|
||||
} = useController({ name, control, defaultValue })
|
||||
|
||||
@ -100,6 +100,7 @@ const SelectController = memo(
|
||||
|
||||
const handleChange = useCallback(
|
||||
(evt) => {
|
||||
onBlur()
|
||||
if (!multiple) {
|
||||
onChange(evt)
|
||||
if (typeof onConditionChange === 'function') {
|
||||
|
@ -45,7 +45,7 @@ const SliderController = memo(
|
||||
const { min, max, step } = fieldProps ?? {}
|
||||
|
||||
const {
|
||||
field: { value, onChange, ...inputProps },
|
||||
field: { value, onChange, onBlur, ...inputProps },
|
||||
fieldState: { error },
|
||||
} = useController({ name, control })
|
||||
|
||||
@ -71,6 +71,7 @@ const SliderController = memo(
|
||||
|
||||
const handleChange = useCallback(
|
||||
(_, newValue) => {
|
||||
onBlur()
|
||||
if (!readOnly) {
|
||||
onChange(newValue)
|
||||
if (typeof onConditionChange === 'function') {
|
||||
|
@ -49,12 +49,13 @@ const SwitchController = memo(
|
||||
dependencies,
|
||||
}) => {
|
||||
const {
|
||||
field: { value = false, onChange },
|
||||
field: { value = false, onChange, onBlur },
|
||||
fieldState: { error },
|
||||
} = useController({ name, control })
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e) => {
|
||||
onBlur()
|
||||
const condition = e.target.checked
|
||||
onChange(condition)
|
||||
if (typeof onConditionChange === 'function') {
|
||||
|
@ -45,7 +45,7 @@ const TextController = memo(
|
||||
})
|
||||
|
||||
const {
|
||||
field: { ref, value = '', onChange, ...inputProps },
|
||||
field: { ref, value = '', onChange, onBlur, ...inputProps },
|
||||
fieldState: { error },
|
||||
} = useController({ name, control })
|
||||
|
||||
@ -58,6 +58,7 @@ const TextController = memo(
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e) => {
|
||||
onBlur()
|
||||
const condition = e.target.value
|
||||
onChange(condition)
|
||||
if (typeof onConditionChange === 'function') {
|
||||
|
@ -53,7 +53,7 @@ const ToggleController = memo(
|
||||
onConditionChange,
|
||||
}) => {
|
||||
const {
|
||||
field: { ref, value: optionSelected, onChange },
|
||||
field: { ref, value: optionSelected, onChange, onBlur },
|
||||
fieldState: { error: { message } = {} },
|
||||
} = useController({ name, control })
|
||||
|
||||
@ -65,6 +65,7 @@ const ToggleController = memo(
|
||||
}, [])
|
||||
const handleChange = useCallback(
|
||||
(_, newValues) => {
|
||||
onBlur()
|
||||
if (!readOnly && (!notNull || newValues)) {
|
||||
onChange(newValues)
|
||||
if (typeof onConditionChange === 'function') {
|
||||
|
@ -116,6 +116,7 @@ const CustomStepper = ({
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
data-cy={`step-${id}`}
|
||||
>
|
||||
<StepLabel
|
||||
StepIconComponent={StepIconStyled}
|
||||
|
@ -22,10 +22,11 @@ import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
|
||||
import { Accordion, AccordionSummary, FormControl, Grid } from '@mui/material'
|
||||
import { useFormContext, useWatch } from 'react-hook-form'
|
||||
import { useFormContext, useWatch, useFormState } from 'react-hook-form'
|
||||
|
||||
import * as FC from 'client/components/FormControl'
|
||||
import { useDisableStep } from 'client/components/FormStepper'
|
||||
@ -33,8 +34,10 @@ import Legend from 'client/components/Forms/Legend'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
import { Field, deepStringify, simpleHash } from 'client/utils'
|
||||
|
||||
import get from 'lodash.get'
|
||||
import { get, set, merge, startsWith } from 'lodash'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { parseTouchedDirty, isDeeplyEmpty } from 'client/utils/parser'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
const NOT_DEPEND_ATTRIBUTES = [
|
||||
'watcher',
|
||||
@ -70,6 +73,9 @@ const INPUT_CONTROLLER = {
|
||||
* @param {object} props.rootProps - The props to be passed to the root element
|
||||
* @param {*} props.legend - The legend
|
||||
* @param {string} props.legendTooltip - The legend tooltip
|
||||
* @param {boolean} props.saveState - Save form state to redux
|
||||
* @param {string} props.fieldPath - Field path to set after touched or dirty fields change
|
||||
* @param {boolean} props.hiddenLegend - Hide the legend of the form
|
||||
* @returns {ReactElement} - The form component
|
||||
*/
|
||||
const FormWithSchema = ({
|
||||
@ -79,9 +85,135 @@ const FormWithSchema = ({
|
||||
fields,
|
||||
rootProps,
|
||||
legend,
|
||||
hiddenLegend = false,
|
||||
legendTooltip,
|
||||
saveState,
|
||||
fieldPath,
|
||||
}) => {
|
||||
const { setModifiedFields, setFieldPath } = useGeneralApi()
|
||||
const { sx: sxRoot, ...restOfRootProps } = rootProps ?? {}
|
||||
const formContext = useFormContext()
|
||||
const { touchedFields, dirtyFields } = useFormState({
|
||||
control: formContext.control,
|
||||
})
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (saveState) {
|
||||
// Fields to add to the modifiedFields
|
||||
let fieldsToMerge = {}
|
||||
|
||||
// Get the fields that are touched and dirty
|
||||
const touchedDirtyFields = parseTouchedDirty(touchedFields, dirtyFields)
|
||||
|
||||
// Add to the fieldsToMerge
|
||||
if (!isDeeplyEmpty(touchedDirtyFields)) {
|
||||
fieldsToMerge = touchedDirtyFields
|
||||
}
|
||||
|
||||
// Check hidden fields that have a dependOf that is a field touched or dirty so the hidden field has to be add to the modifiedFields
|
||||
const fieldsHiddenMerge = {}
|
||||
|
||||
// Fields that have a value on dependOf attribute (if depend is in a different schema, the name of the field will contain the step id and starts with $)
|
||||
const fieldWithDepend = fields.filter((item) =>
|
||||
item.dependOf && Array.isArray(item.dependOf)
|
||||
? item.dependOf.some((dependItem) =>
|
||||
get(
|
||||
id ? fieldsToMerge[id] : fieldsToMerge,
|
||||
startsWith(dependItem, '$' + id)
|
||||
? dependItem.substring(id.length + 2)
|
||||
: dependItem
|
||||
)
|
||||
)
|
||||
: get(
|
||||
id ? fieldsToMerge[id] : fieldsToMerge,
|
||||
startsWith(item.dependOf, '$' + id)
|
||||
? item.dependOf.substring(id.length + 2)
|
||||
: item.dependOf
|
||||
)
|
||||
)
|
||||
|
||||
// The fields that has a dependOf and has htmlType hidden has to be deleted
|
||||
fieldWithDepend
|
||||
.filter((field) => {
|
||||
const htmlTypeFunction = typeof field.htmlType === 'function'
|
||||
|
||||
const valueDependOf = Array.isArray(field.dependOf)
|
||||
? field.dependOf.map((depend) =>
|
||||
formContext?.getValues(
|
||||
`${id}.` +
|
||||
(startsWith(depend, '$' + id)
|
||||
? depend.substring(id.length + 2)
|
||||
: depend)
|
||||
)
|
||||
)
|
||||
: formContext?.getValues(
|
||||
`${id}.` +
|
||||
(startsWith(field.dependOf, '$' + id)
|
||||
? field.dependOf.substring(id.length + 2)
|
||||
: field.dependOf)
|
||||
)
|
||||
|
||||
const hidden =
|
||||
(htmlTypeFunction &&
|
||||
field.htmlType(valueDependOf, formContext) === 'hidden') ||
|
||||
(!htmlTypeFunction && field.htmlType === 'hidden')
|
||||
|
||||
return field.htmlType && hidden
|
||||
})
|
||||
.map((item) => item.name)
|
||||
.forEach((element) => {
|
||||
set(fieldsHiddenMerge, id ? `${id}.${element}` : `${element}`, {
|
||||
__delete__: true,
|
||||
})
|
||||
})
|
||||
|
||||
// The fields that has a dependOf and has htmlType different that hidden has to be added
|
||||
fieldWithDepend
|
||||
.filter((field) => {
|
||||
const htmlTypeFunction = typeof field.htmlType === 'function'
|
||||
|
||||
const valueDependOf = Array.isArray(field.dependOf)
|
||||
? field.dependOf.map((depend) =>
|
||||
formContext?.getValues(
|
||||
`${id}.` +
|
||||
(startsWith(depend, '$' + id)
|
||||
? depend.substring(id.length + 2)
|
||||
: depend)
|
||||
)
|
||||
)
|
||||
: formContext?.getValues(
|
||||
`${id}.` +
|
||||
(startsWith(field.dependOf, '$' + id)
|
||||
? field.dependOf.substring(id.length + 2)
|
||||
: field.dependOf)
|
||||
)
|
||||
|
||||
const notHidden =
|
||||
(htmlTypeFunction &&
|
||||
field.htmlType(valueDependOf, formContext) !== 'hidden') ||
|
||||
(!htmlTypeFunction && field.htmlType !== 'hidden')
|
||||
|
||||
// return field.htmlType && notHidden
|
||||
return notHidden
|
||||
})
|
||||
.map((item) => item.name)
|
||||
.forEach((element) => {
|
||||
set(fieldsHiddenMerge, id ? `${id}.${element}` : `${element}`, true)
|
||||
})
|
||||
|
||||
// Set modified fields
|
||||
const mix = merge({}, fieldsToMerge, fieldsHiddenMerge)
|
||||
setModifiedFields(mix)
|
||||
|
||||
// If fieldPath exists, set in the store
|
||||
if (fieldPath) {
|
||||
setFieldPath(fieldPath)
|
||||
}
|
||||
}
|
||||
},
|
||||
[touchedFields, dirtyFields]
|
||||
)
|
||||
|
||||
const RootWrapper = useMemo(
|
||||
() =>
|
||||
@ -95,12 +227,12 @@ const FormWithSchema = ({
|
||||
</Accordion>
|
||||
)
|
||||
: Fragment,
|
||||
[accordion, legend]
|
||||
[accordion, legend, hiddenLegend]
|
||||
)
|
||||
|
||||
const LegendWrapper = useMemo(
|
||||
() => (accordion && legend ? AccordionSummary : Fragment),
|
||||
[accordion, legend]
|
||||
[accordion, legend, hiddenLegend]
|
||||
)
|
||||
|
||||
const getFields = useMemo(
|
||||
@ -118,12 +250,13 @@ const FormWithSchema = ({
|
||||
>
|
||||
<RootWrapper>
|
||||
<LegendWrapper>
|
||||
{legend && (
|
||||
{legend && !hiddenLegend && (
|
||||
<Legend
|
||||
data-cy={`legend-${cy}`}
|
||||
title={legend}
|
||||
tooltip={legendTooltip}
|
||||
disableGutters={accordion}
|
||||
hiddenLegend={hiddenLegend}
|
||||
/>
|
||||
)}
|
||||
</LegendWrapper>
|
||||
@ -148,11 +281,15 @@ FormWithSchema.propTypes = {
|
||||
legend: PropTypes.any,
|
||||
legendTooltip: PropTypes.string,
|
||||
rootProps: PropTypes.object,
|
||||
saveState: PropTypes.bool,
|
||||
fieldPath: PropTypes.string,
|
||||
hiddenLegend: PropTypes.bool,
|
||||
}
|
||||
|
||||
const FieldComponent = memo(
|
||||
({ id, cy, dependOf, stepControl, ...attributes }) => {
|
||||
const formContext = useFormContext()
|
||||
|
||||
const disableSteps = useDisableStep()
|
||||
|
||||
const currentState = useSelector((state) => state)
|
||||
@ -179,7 +316,6 @@ const FieldComponent = memo(
|
||||
const valueOfDependField = useWatch({
|
||||
name: nameOfDependField,
|
||||
disabled: dependOf === undefined,
|
||||
defaultValue: Array.isArray(dependOf) ? [] : undefined,
|
||||
})
|
||||
|
||||
const handleConditionChange = useCallback(
|
||||
|
@ -47,6 +47,7 @@ const Content = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
fields={fields}
|
||||
legend={legend}
|
||||
id={STEP_ID}
|
||||
saveState={true}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
@ -29,6 +29,8 @@ const Content = ({ data, setFormData }) => {
|
||||
const { handleSelect, handleClear } = useListForm({
|
||||
key: STEP_ID,
|
||||
setList: setFormData,
|
||||
modifiedFields: ['IMAGE', 'IMAGE_UNAME'],
|
||||
fieldKey: 'general',
|
||||
})
|
||||
|
||||
const handleSelectedRows = (rows) => {
|
||||
@ -45,6 +47,7 @@ const Content = ({ data, setFormData }) => {
|
||||
<ImagesTable
|
||||
singleSelect
|
||||
disableGlobalSort
|
||||
displaySelectedRows
|
||||
pageSize={5}
|
||||
initialState={{ selectedRowIds: { [ID]: true } }}
|
||||
onSelectedRowsChange={handleSelectedRows}
|
||||
|
@ -20,6 +20,11 @@ import ImagesTable, {
|
||||
STEP_ID as STEP_IMAGE,
|
||||
} from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable'
|
||||
import { createSteps, mapUserInputs } from 'client/utils'
|
||||
import { store } from 'client/sunstone'
|
||||
import {
|
||||
setModifiedFields,
|
||||
setFieldPath,
|
||||
} from 'client/features/General/actions'
|
||||
|
||||
const Steps = createSteps([ImagesTable, AdvancedOptions], {
|
||||
transformInitialValue: (initialValue) => {
|
||||
@ -32,6 +37,8 @@ const Steps = createSteps([ImagesTable, AdvancedOptions], {
|
||||
...diskProps
|
||||
} = initialValue ?? {}
|
||||
|
||||
store.dispatch(setFieldPath(`extra.Storage.${diskProps?.DISK_ID}`))
|
||||
|
||||
return {
|
||||
[STEP_IMAGE]: [
|
||||
{
|
||||
@ -50,6 +57,9 @@ const Steps = createSteps([ImagesTable, AdvancedOptions], {
|
||||
const { [STEP_IMAGE]: [image] = [], [STEP_ADVANCED]: advanced } = formData
|
||||
const { ID, NAME, UID, UNAME, STATE, SIZE, ...imageProps } = image ?? {}
|
||||
|
||||
imageProps?.DATASTORE_ID &&
|
||||
store.dispatch(setModifiedFields({}, { batch: false }))
|
||||
|
||||
return {
|
||||
...imageProps,
|
||||
...mapUserInputs(advanced),
|
||||
|
@ -47,6 +47,7 @@ const Content = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
fields={fields}
|
||||
legend={legend}
|
||||
id={STEP_ID}
|
||||
saveState={true}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
@ -32,7 +32,14 @@ const Content = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
[]
|
||||
)
|
||||
|
||||
return <FormWithSchema cy="attach-disk" fields={memoFields} id={STEP_ID} />
|
||||
return (
|
||||
<FormWithSchema
|
||||
cy="attach-disk"
|
||||
fields={memoFields}
|
||||
id={STEP_ID}
|
||||
saveState={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,12 +74,16 @@ const TYPE = (hypervisor) => ({
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values:
|
||||
hypervisor === vcenter
|
||||
? [{ text: 'FS', value: 'fs' }]
|
||||
? [
|
||||
{ text: '-', value: undefined },
|
||||
{ text: 'FS', value: 'fs' },
|
||||
]
|
||||
: [
|
||||
{ text: '-', value: undefined },
|
||||
{ text: 'FS', value: 'fs' },
|
||||
{ text: 'Swap', value: 'swap' },
|
||||
],
|
||||
validation: string().trim().notRequired().default('fs'),
|
||||
validation: string().trim().required().default(undefined),
|
||||
})
|
||||
|
||||
/**
|
||||
@ -94,17 +98,22 @@ const FORMAT = (hypervisor) => ({
|
||||
htmlType: (type) => type === 'swap' && INPUT_TYPES.HIDDEN,
|
||||
values:
|
||||
hypervisor === vcenter
|
||||
? [{ text: 'Raw', value: 'raw' }]
|
||||
? [
|
||||
{ text: '-', value: undefined },
|
||||
{ text: 'Raw', value: 'raw' },
|
||||
]
|
||||
: [
|
||||
{ text: '-', value: undefined },
|
||||
{ text: 'Raw', value: 'raw' },
|
||||
{ text: 'qcow2', value: 'qcow2' },
|
||||
],
|
||||
validation: string()
|
||||
.trim()
|
||||
.when('TYPE', (type, schema) =>
|
||||
type === 'swap' ? schema.notRequired() : schema.required()
|
||||
)
|
||||
.default('raw'),
|
||||
type === 'swap'
|
||||
? schema.notRequired().default(undefined)
|
||||
: schema.required().default(undefined)
|
||||
),
|
||||
})
|
||||
|
||||
/** @type {Field} Filesystem field */
|
||||
|
@ -20,6 +20,11 @@ import AdvancedOptions, {
|
||||
STEP_ID as ADVANCED_ID,
|
||||
} from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions'
|
||||
import { mapUserInputs, createSteps, convertToMB } from 'client/utils'
|
||||
import { store } from 'client/sunstone'
|
||||
import {
|
||||
setModifiedFields,
|
||||
setFieldPath,
|
||||
} from 'client/features/General/actions'
|
||||
|
||||
const Steps = createSteps([BasicConfiguration, AdvancedOptions], {
|
||||
transformInitialValue: (disk = {}, schema) => {
|
||||
@ -31,8 +36,7 @@ const Steps = createSteps([BasicConfiguration, AdvancedOptions], {
|
||||
{ stripUnknown: true }
|
||||
)
|
||||
|
||||
// #6154: Add temp id to propagate it
|
||||
schemaCast[ADVANCED_ID].TEMP_ID = disk.TEMP_ID
|
||||
store.dispatch(setFieldPath(`extra.Storage.${disk?.DISK_ID}`))
|
||||
|
||||
return schemaCast
|
||||
},
|
||||
@ -40,6 +44,8 @@ const Steps = createSteps([BasicConfiguration, AdvancedOptions], {
|
||||
const { [BASIC_ID]: configuration = {}, [ADVANCED_ID]: advanced = {} } =
|
||||
formData ?? {}
|
||||
|
||||
store.dispatch(setModifiedFields({}, { batch: false }))
|
||||
|
||||
// ISSUE#6136: Convert size to MB (because XML API uses only MB) and delete sizeunit field (no needed on XML API)
|
||||
configuration.SIZE = convertToMB(configuration.SIZE, configuration.SIZEUNIT)
|
||||
delete configuration.SIZEUNIT
|
||||
|
@ -45,6 +45,7 @@ const Content = (props) => {
|
||||
},
|
||||
}}
|
||||
cy={id}
|
||||
saveState={true}
|
||||
fields={fields}
|
||||
legend={legend}
|
||||
id={STEP_ID}
|
||||
|
@ -33,6 +33,8 @@ import {
|
||||
NIC_HARDWARE,
|
||||
NIC_HARDWARE_STR,
|
||||
PCI_TYPES,
|
||||
IPV4_METHODS,
|
||||
IPV6_METHODS,
|
||||
} from 'client/constants'
|
||||
import { useGetHostsQuery } from 'client/features/OneApi/host'
|
||||
import { getPciDevices } from 'client/models/Host'
|
||||
@ -64,18 +66,22 @@ const fillPCIAtributes =
|
||||
/**
|
||||
* @param {object} [data] - VM or VM Template data
|
||||
* @param {Nic[]} [data.nics] - Current NICs
|
||||
* @param {boolean} [data.hasAlias] - If it's an alias
|
||||
* @param {boolean} [data.isPci] - If it's a PCI
|
||||
* @returns {Field[]} List of general fields
|
||||
*/
|
||||
const GENERAL_FIELDS = ({ nics = [] } = {}) =>
|
||||
const GENERAL_FIELDS = ({ nics = [], hasAlias = false, isPci = false } = {}) =>
|
||||
[
|
||||
!!nics?.length && {
|
||||
name: 'PARENT',
|
||||
label: T.AsAnAlias,
|
||||
dependOf: 'NAME',
|
||||
type: (name) => {
|
||||
const hasAlias = nics?.some((nic) => nic.PARENT === name)
|
||||
const hasAliasExisting = nics?.some((nic) => nic.PARENT === name)
|
||||
|
||||
return name && hasAlias ? INPUT_TYPES.HIDDEN : INPUT_TYPES.SELECT
|
||||
return name && hasAliasExisting
|
||||
? INPUT_TYPES.HIDDEN
|
||||
: INPUT_TYPES.SELECT
|
||||
},
|
||||
values: (name) => [
|
||||
{ text: '', value: '' },
|
||||
@ -91,6 +97,9 @@ const GENERAL_FIELDS = ({ nics = [] } = {}) =>
|
||||
return { text, value: NAME }
|
||||
}),
|
||||
],
|
||||
fieldProps: {
|
||||
disabled: hasAlias || isPci,
|
||||
},
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
@ -150,10 +159,11 @@ const GUACAMOLE_CONNECTIONS = [
|
||||
type: INPUT_TYPES.SELECT,
|
||||
dependOf: 'RDP',
|
||||
values: [
|
||||
{ text: '-', value: undefined },
|
||||
{ text: T.DisplayUpdate, value: 'display-update' },
|
||||
{ text: T.Reconnect, value: 'reconnect' },
|
||||
],
|
||||
validation: string().trim().notRequired().default('display-update'),
|
||||
validation: string().trim().notRequired().default(undefined),
|
||||
htmlType: (noneType) => !noneType && INPUT_TYPES.HIDDEN,
|
||||
grid: { sm: 6 },
|
||||
},
|
||||
@ -331,12 +341,12 @@ const OVERRIDE_IPV4_FIELDS = [
|
||||
label: T.NetworkMethod,
|
||||
tooltip: T.NetworkMethod4Concept,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: [
|
||||
{ text: 'static (Based on context)', value: 'static' },
|
||||
{ text: 'dhcp (DHCPv4)', value: 'dhcp' },
|
||||
{ text: 'skip (Do not configure IPv4)', value: 'skip' },
|
||||
],
|
||||
validation: string().trim().notRequired().default('static'),
|
||||
values: arrayToOptions(Object.keys(IPV4_METHODS), {
|
||||
getText: (key) => key,
|
||||
getValue: (key) => IPV4_METHODS[key],
|
||||
addEmpty: true,
|
||||
}),
|
||||
validation: string().trim().notRequired().default(undefined),
|
||||
},
|
||||
]
|
||||
|
||||
@ -367,19 +377,21 @@ const OVERRIDE_IPV6_FIELDS = [
|
||||
label: T.NetworkMethod,
|
||||
tooltip: T.NetworkMethod6Concept,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: [
|
||||
{ text: 'static (Based on context)', value: 'static' },
|
||||
{ text: 'auto (SLAAC)', value: 'auto' },
|
||||
{ text: 'dhcp (SLAAC and DHCPv6)', value: 'dhcp' },
|
||||
{ text: 'disable (Do not use IPv6)', value: 'disable' },
|
||||
{ text: 'skip (Do not configure IPv4)', value: 'skip' },
|
||||
],
|
||||
validation: string().trim().notRequired().default('static'),
|
||||
values: arrayToOptions(Object.keys(IPV6_METHODS), {
|
||||
getText: (key) => key,
|
||||
getValue: (key) => IPV6_METHODS[key],
|
||||
addEmpty: true,
|
||||
}),
|
||||
validation: string().trim().notRequired().default(undefined),
|
||||
},
|
||||
]
|
||||
|
||||
/** @type {Field[]} List of hardware fields */
|
||||
const HARDWARE_FIELDS = (defaultData = {}) => [
|
||||
const HARDWARE_FIELDS = (
|
||||
defaultData = {},
|
||||
hasAlias = false,
|
||||
isAlias = false
|
||||
) => [
|
||||
{
|
||||
name: PCI_TYPE_NAME,
|
||||
label: T.VirtualNicHardwareMode,
|
||||
@ -389,6 +401,9 @@ const HARDWARE_FIELDS = (defaultData = {}) => [
|
||||
getText: (key) => NIC_HARDWARE_STR[key],
|
||||
getValue: (type) => type,
|
||||
}),
|
||||
fieldProps: {
|
||||
disabled: hasAlias || isAlias,
|
||||
},
|
||||
validation: string()
|
||||
.trim()
|
||||
.default(() => {
|
||||
@ -411,6 +426,9 @@ const HARDWARE_FIELDS = (defaultData = {}) => [
|
||||
htmlType: (value) => value !== NIC_HARDWARE.EMULATED && INPUT_TYPES.HIDDEN,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
notOnHypervisors: [firecracker],
|
||||
fieldProps: {
|
||||
disabled: hasAlias || isAlias,
|
||||
},
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
@ -422,6 +440,9 @@ const HARDWARE_FIELDS = (defaultData = {}) => [
|
||||
tooltip: T.OnlySupportedForVirtioDriver,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
notOnHypervisors: [firecracker],
|
||||
fieldProps: {
|
||||
disabled: hasAlias || isAlias,
|
||||
},
|
||||
dependOf: PCI_TYPE_NAME,
|
||||
htmlType: (value) =>
|
||||
value !== NIC_HARDWARE.EMULATED ? INPUT_TYPES.HIDDEN : 'number',
|
||||
@ -492,13 +513,19 @@ const HARDWARE_FIELDS = (defaultData = {}) => [
|
||||
)
|
||||
},
|
||||
validation: string()
|
||||
.notRequired()
|
||||
.when('PCI_TYPE', (type, schema) =>
|
||||
type === NIC_HARDWARE.PCI_PASSTHROUGH_AUTOMATIC
|
||||
? schema.required()
|
||||
: schema.notRequired()
|
||||
)
|
||||
.default(() => undefined)
|
||||
.afterSubmit((value, { context }) =>
|
||||
context?.advanced?.PCI_TYPE === PCI_TYPES.AUTOMATIC ? value : undefined
|
||||
),
|
||||
grid: { md: 3 },
|
||||
readOnly: true,
|
||||
fieldProps: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: DEVICE,
|
||||
@ -516,13 +543,19 @@ const HARDWARE_FIELDS = (defaultData = {}) => [
|
||||
)
|
||||
},
|
||||
validation: string()
|
||||
.notRequired()
|
||||
.when('PCI_TYPE', (type, schema) =>
|
||||
type === NIC_HARDWARE.PCI_PASSTHROUGH_AUTOMATIC
|
||||
? schema.required()
|
||||
: schema.notRequired()
|
||||
)
|
||||
.default(() => undefined)
|
||||
.afterSubmit((value, { context }) =>
|
||||
context?.advanced?.PCI_TYPE === PCI_TYPES.AUTOMATIC ? value : undefined
|
||||
),
|
||||
grid: { md: 3 },
|
||||
readOnly: true,
|
||||
fieldProps: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: CLASS,
|
||||
@ -540,13 +573,19 @@ const HARDWARE_FIELDS = (defaultData = {}) => [
|
||||
)
|
||||
},
|
||||
validation: string()
|
||||
.notRequired()
|
||||
.when('PCI_TYPE', (type, schema) =>
|
||||
type === NIC_HARDWARE.PCI_PASSTHROUGH_AUTOMATIC
|
||||
? schema.required()
|
||||
: schema.notRequired()
|
||||
)
|
||||
.default(() => undefined)
|
||||
.afterSubmit((value, { context }) =>
|
||||
context?.advanced?.PCI_TYPE === PCI_TYPES.AUTOMATIC ? value : undefined
|
||||
),
|
||||
grid: { md: 3 },
|
||||
readOnly: true,
|
||||
fieldProps: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
// PCI Passthrough Manual mode fields
|
||||
{
|
||||
@ -558,7 +597,11 @@ const HARDWARE_FIELDS = (defaultData = {}) => [
|
||||
htmlType: (value) =>
|
||||
value !== NIC_HARDWARE.PCI_PASSTHROUGH_MANUAL && INPUT_TYPES.HIDDEN,
|
||||
validation: string()
|
||||
.notRequired()
|
||||
.when('PCI_TYPE', (type, schema) =>
|
||||
type === NIC_HARDWARE.PCI_PASSTHROUGH_MANUAL
|
||||
? schema.required()
|
||||
: schema.notRequired()
|
||||
)
|
||||
.default(() => defaultData?.SHORT_ADDRESS)
|
||||
.afterSubmit((value, { context }) =>
|
||||
context?.advanced?.PCI_TYPE === PCI_TYPES.MANUAL ? value : undefined
|
||||
@ -589,6 +632,9 @@ const GUEST_FIELDS = [
|
||||
* @param {object} data.defaultData - VM or VM Template data
|
||||
* @param {object} data.oneConfig - Config of oned.conf
|
||||
* @param {boolean} data.adminGroup - User is admin or not
|
||||
* @param {boolean} [data.hasAlias] - If has an alias
|
||||
* @param {boolean} [data.isPci] - If it's a PCI
|
||||
* @param {boolean} [data.isAlias] - If it's an alias
|
||||
* @returns {Section[]} Sections
|
||||
*/
|
||||
const SECTIONS = ({
|
||||
@ -598,6 +644,9 @@ const SECTIONS = ({
|
||||
defaultData,
|
||||
oneConfig,
|
||||
adminGroup,
|
||||
hasAlias,
|
||||
isPci,
|
||||
isAlias,
|
||||
} = {}) => {
|
||||
const filters = { driver, hypervisor }
|
||||
|
||||
@ -609,7 +658,10 @@ const SECTIONS = ({
|
||||
id: 'general',
|
||||
legend: T.General,
|
||||
fields: disableFields(
|
||||
filterByHypAndDriver(GENERAL_FIELDS({ nics }), filters),
|
||||
filterByHypAndDriver(
|
||||
GENERAL_FIELDS({ nics, hasAlias, isPci }),
|
||||
filters
|
||||
),
|
||||
'NIC',
|
||||
oneConfig,
|
||||
adminGroup
|
||||
@ -653,7 +705,10 @@ const SECTIONS = ({
|
||||
id: 'hardware',
|
||||
legend: T.Hardware,
|
||||
fields: disableFields(
|
||||
filterByHypAndDriver(HARDWARE_FIELDS(defaultData), filters),
|
||||
filterByHypAndDriver(
|
||||
HARDWARE_FIELDS(defaultData, hasAlias, isAlias),
|
||||
filters
|
||||
),
|
||||
'NIC',
|
||||
oneConfig,
|
||||
adminGroup
|
||||
|
@ -25,11 +25,13 @@ import { T } from 'client/constants'
|
||||
export const STEP_ID = 'network'
|
||||
|
||||
const Content = ({ data, setFormData }) => {
|
||||
const { NAME } = data?.[0] ?? {}
|
||||
const { ID } = data?.[0] ?? {}
|
||||
|
||||
const { handleSelect, handleClear } = useListForm({
|
||||
key: STEP_ID,
|
||||
setList: setFormData,
|
||||
modifiedFields: ['NETWORK', 'NETWORK_UNAME'],
|
||||
fieldKey: 'general',
|
||||
})
|
||||
|
||||
const handleSelectedRows = (rows) => {
|
||||
@ -44,8 +46,8 @@ const Content = ({ data, setFormData }) => {
|
||||
disableGlobalSort
|
||||
displaySelectedRows
|
||||
pageSize={5}
|
||||
getRowId={(row) => String(row.NAME)}
|
||||
initialState={{ selectedRowIds: { [NAME]: true } }}
|
||||
getRowId={(row) => String(row.ID)}
|
||||
initialState={{ selectedRowIds: { [ID]: true } }}
|
||||
onSelectedRowsChange={handleSelectedRows}
|
||||
/>
|
||||
)
|
||||
|
@ -44,6 +44,7 @@ const Content = (props) => {
|
||||
fields={fields}
|
||||
legend={legend}
|
||||
id={STEP_ID}
|
||||
saveState={true}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
@ -60,9 +60,6 @@ const Steps = createSteps([NetworksTable, AdvancedOptions, QOSOptions], {
|
||||
{ stripUnknown: true }
|
||||
)
|
||||
|
||||
// #6154: Add temp id to propagate it
|
||||
if (rest.TEMP_ID) castedValue[ADVANCED_ID].TEMP_ID = rest.TEMP_ID
|
||||
|
||||
return {
|
||||
[NETWORK_ID]: [
|
||||
{
|
||||
|
@ -37,7 +37,7 @@ const Backup = ({ oneConfig, adminGroup }) => (
|
||||
{SECTIONS(oneConfig, adminGroup).map(({ id, ...section }) => (
|
||||
<FormWithSchema
|
||||
key={id}
|
||||
cy="backups-conf"
|
||||
cy="backup-configuration"
|
||||
legend={T.Backup}
|
||||
{...section}
|
||||
/>
|
||||
|
@ -86,7 +86,7 @@ const Content = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
error: !!errors?.CONTEXT,
|
||||
},
|
||||
{
|
||||
id: 'backup_config',
|
||||
id: 'backup',
|
||||
icon: BackupIcon,
|
||||
label: <Translate word={T.Backup} />,
|
||||
renderContent: () => (
|
||||
|
@ -27,4 +27,4 @@ import { HYPERVISORS } from 'client/constants'
|
||||
* @returns {ObjectSchema} Context schema
|
||||
*/
|
||||
export const SCHEMA = ({ hypervisor }) =>
|
||||
object().concat(CONFIGURATION_SCHEMA).concat(FILES_SCHEMA(hypervisor))
|
||||
object().concat(CONFIGURATION_SCHEMA(true)).concat(FILES_SCHEMA(hypervisor))
|
||||
|
@ -15,6 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import PropTypes from 'prop-types'
|
||||
import { RefreshDouble as BackupIcon } from 'iconoir-react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
@ -27,19 +28,28 @@ import {
|
||||
FIELDS,
|
||||
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/backup/schema'
|
||||
import { T } from 'client/constants'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
const Backup = ({ oneConfig, adminGroup }) => (
|
||||
<>
|
||||
{SECTIONS(oneConfig, adminGroup).map(({ id, ...section }) => (
|
||||
<FormWithSchema
|
||||
key={id}
|
||||
id={EXTRA_ID}
|
||||
cy={`${EXTRA_ID}-${id}`}
|
||||
{...section}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
const Backup = ({ oneConfig, adminGroup }) => {
|
||||
const { setFieldPath } = useGeneralApi()
|
||||
useEffect(() => {
|
||||
setFieldPath(`extra.Backup`)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{SECTIONS(oneConfig, adminGroup).map(({ id, ...section }) => (
|
||||
<FormWithSchema
|
||||
key={id}
|
||||
id={EXTRA_ID}
|
||||
saveState={true}
|
||||
cy={`${EXTRA_ID}-${id}`}
|
||||
{...section}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Backup.propTypes = {
|
||||
data: PropTypes.any,
|
||||
|
@ -61,8 +61,7 @@ const KEEP_LAST_FIELD = {
|
||||
validation: number()
|
||||
.notRequired()
|
||||
.nullable(true)
|
||||
.default(() => undefined)
|
||||
.transform((_, val) => (val !== '' ? parseInt(val) : null)),
|
||||
.default(() => undefined),
|
||||
grid: { xs: 12, md: 6 },
|
||||
}
|
||||
|
||||
|
@ -152,8 +152,9 @@ export const FEATURE_CUSTOM_ENABLED = {
|
||||
notOnHypervisors: [vcenter, firecracker, lxc],
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean()
|
||||
.strip()
|
||||
.default(() => false),
|
||||
.yesOrNo()
|
||||
.default(() => false)
|
||||
.afterSubmit((value) => undefined),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
@ -163,7 +164,11 @@ export const FIRMWARE = {
|
||||
label: T.Firmware,
|
||||
tooltip: T.FirmwareConcept,
|
||||
notOnHypervisors: [firecracker, lxc],
|
||||
type: ([, , custom] = []) => (custom ? INPUT_TYPES.TEXT : INPUT_TYPES.SELECT),
|
||||
type: ([, , custom] = [], formContext) => {
|
||||
const enabled = formContext?.getValues('extra.OS.FEATURE_CUSTOM_ENABLED')
|
||||
|
||||
return custom || enabled ? INPUT_TYPES.TEXT : INPUT_TYPES.SELECT
|
||||
},
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useMemo } from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Stack, FormControl } from '@mui/material'
|
||||
import { SystemShut as OsIcon } from 'iconoir-react'
|
||||
@ -34,15 +34,27 @@ import { TAB_ID as NIC_ID } from 'client/components/Forms/VmTemplate/CreateForm/
|
||||
import { SECTIONS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting/schema'
|
||||
|
||||
import { T } from 'client/constants'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
import { useWatch } from 'react-hook-form'
|
||||
|
||||
export const TAB_ID = 'OS'
|
||||
|
||||
const Booting = ({ hypervisor, oneConfig, adminGroup, ...props }) => {
|
||||
const { setFieldPath } = useGeneralApi()
|
||||
useEffect(() => {
|
||||
setFieldPath(`extra.OsCpu`)
|
||||
}, [])
|
||||
const sections = useMemo(
|
||||
() => SECTIONS(hypervisor, oneConfig, adminGroup),
|
||||
[hypervisor]
|
||||
)
|
||||
|
||||
const kernelWatch = useWatch({ name: `${EXTRA_ID}.OS.KERNEL` })
|
||||
const kernelDsWatch = useWatch({ name: `${EXTRA_ID}.OS.KERNEL_DS` })
|
||||
|
||||
useEffect(() => {}, [kernelWatch, kernelDsWatch])
|
||||
|
||||
return (
|
||||
<Stack
|
||||
display="grid"
|
||||
@ -64,7 +76,9 @@ const Booting = ({ hypervisor, oneConfig, adminGroup, ...props }) => {
|
||||
<FormWithSchema
|
||||
key={id}
|
||||
id={EXTRA_ID}
|
||||
saveState={true}
|
||||
cy={`${EXTRA_ID}-${id}`}
|
||||
hiddenLegend={id === 'os-ramdisk' && !kernelWatch && !kernelDsWatch}
|
||||
{...section}
|
||||
/>
|
||||
))}
|
||||
|
@ -19,7 +19,11 @@ import { HYPERVISORS, IMAGE_TYPES_STR, INPUT_TYPES, T } from 'client/constants'
|
||||
import { useGetAllImagesQuery } from 'client/features/OneApi/image'
|
||||
import { getType } from 'client/models/Image'
|
||||
import { Field, clearNames } from 'client/utils'
|
||||
import { KERNEL_DS_NAME, KERNEL_NAME } from './kernelSchema'
|
||||
import {
|
||||
KERNEL_DS_NAME,
|
||||
KERNEL_NAME,
|
||||
KERNEL_PATH_ENABLED_NAME,
|
||||
} from './kernelSchema'
|
||||
|
||||
const { vcenter, lxc } = HYPERVISORS
|
||||
|
||||
@ -39,6 +43,7 @@ export const RAMDISK_PATH_ENABLED = {
|
||||
notOnHypervisors: [vcenter, lxc],
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
dependOf: [`$extra.${KERNEL_DS_NAME}`, `$extra.${KERNEL_NAME}`],
|
||||
htmlType: ([ds, path] = []) => !(ds || path) && INPUT_TYPES.HIDDEN,
|
||||
fieldProps: (_, form) => {
|
||||
const ds = form?.getValues(`extra.${KERNEL_DS_NAME}`)
|
||||
const path = form?.getValues(`extra.${KERNEL_NAME}`)
|
||||
@ -64,9 +69,11 @@ export const RAMDISK_DS = {
|
||||
dependOf: [
|
||||
RAMDISK_PATH_ENABLED.name,
|
||||
`$extra.${KERNEL_DS_NAME}`,
|
||||
`$extra${KERNEL_NAME}`,
|
||||
`$extra.${KERNEL_NAME}`,
|
||||
`$extra.${KERNEL_PATH_ENABLED_NAME}`,
|
||||
],
|
||||
htmlType: ([enabled = false] = []) => enabled && INPUT_TYPES.HIDDEN,
|
||||
htmlType: ([enabled = false, ds, path] = []) =>
|
||||
(enabled || !(ds || path)) && INPUT_TYPES.HIDDEN,
|
||||
fieldProps: (_, form) => {
|
||||
const ds = form?.getValues(`extra.${KERNEL_DS_NAME}`)
|
||||
const path = form?.getValues(`extra.${KERNEL_NAME}`)
|
||||
@ -117,7 +124,8 @@ export const RAMDISK = {
|
||||
dependOf: [
|
||||
RAMDISK_PATH_ENABLED.name,
|
||||
`$extra.${KERNEL_DS_NAME}`,
|
||||
`$extra${KERNEL_NAME}`,
|
||||
`$extra.${KERNEL_NAME}`,
|
||||
`$extra.${KERNEL_PATH_ENABLED_NAME}`,
|
||||
],
|
||||
fieldProps: (_, form) => {
|
||||
const ds = form?.getValues(`extra.${KERNEL_DS_NAME}`)
|
||||
@ -135,7 +143,8 @@ export const RAMDISK = {
|
||||
|
||||
return ds || path ? {} : { disabled: true }
|
||||
},
|
||||
htmlType: ([enabled = false] = []) => !enabled && INPUT_TYPES.HIDDEN,
|
||||
htmlType: ([enabled = false, ds, path] = []) =>
|
||||
(!enabled || !(ds || path)) && INPUT_TYPES.HIDDEN,
|
||||
validation: ramdiskValidation,
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,10 @@ const DATA = {
|
||||
type: INPUT_TYPES.TEXT,
|
||||
multiline: true,
|
||||
notOnHypervisors: [lxc, vcenter, firecracker],
|
||||
validation: string().trim().notRequired(),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.afterSubmit((value) => (value === '' ? undefined : value)),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
@ -55,7 +58,9 @@ const VALIDATE = {
|
||||
tooltip: T.RawValidateConcept,
|
||||
type: INPUT_TYPES.CHECKBOX,
|
||||
notOnHypervisors: [lxc, vcenter, firecracker],
|
||||
validation: boolean().yesOrNo(),
|
||||
dependOf: DATA.name,
|
||||
htmlType: (data) => !data && INPUT_TYPES.HIDDEN,
|
||||
validation: boolean().yesOrNo().default(false),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
|
@ -25,22 +25,30 @@ const switchField = {
|
||||
}
|
||||
|
||||
/** @type {Field} SSH public key field */
|
||||
export const SSH_PUBLIC_KEY = {
|
||||
export const SSH_PUBLIC_KEY = (isUpdate) => ({
|
||||
name: 'CONTEXT.SSH_PUBLIC_KEY',
|
||||
label: T.SshPublicKey,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
multiline: true,
|
||||
validation: string().trim().notRequired().ensure(),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.ensure()
|
||||
.default(() => (isUpdate ? undefined : '$USER[SSH_PUBLIC_KEY]')),
|
||||
grid: { md: 12 },
|
||||
fieldProps: { rows: 4 },
|
||||
}
|
||||
})
|
||||
|
||||
/** @type {Field} Network context field */
|
||||
const NETWORK = {
|
||||
name: 'CONTEXT.NETWORK',
|
||||
label: T.AddNetworkContextualization,
|
||||
tooltip: T.AddNetworkContextualizationConcept,
|
||||
...switchField,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean()
|
||||
.yesOrNo()
|
||||
.default(() => true),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
/** @type {Field} Token OneGate token field */
|
||||
@ -107,9 +115,10 @@ export const SCRIPT_FIELDS = [START_SCRIPT, ENCODE_START_SCRIPT]
|
||||
export const OTHER_FIELDS = [NETWORK, TOKEN, REPORT_READY]
|
||||
|
||||
/** @type {ObjectSchema} User context configuration schema */
|
||||
export const CONFIGURATION_SCHEMA = getObjectSchemaFromFields([
|
||||
SSH_PUBLIC_KEY,
|
||||
START_SCRIPT_BASE64,
|
||||
...SCRIPT_FIELDS,
|
||||
...OTHER_FIELDS,
|
||||
])
|
||||
export const CONFIGURATION_SCHEMA = (isUpdate) =>
|
||||
getObjectSchemaFromFields([
|
||||
SSH_PUBLIC_KEY(isUpdate),
|
||||
START_SCRIPT_BASE64,
|
||||
...SCRIPT_FIELDS,
|
||||
...OTHER_FIELDS,
|
||||
])
|
||||
|
@ -17,6 +17,7 @@ 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 { useGeneralApi } from 'client/features/General'
|
||||
|
||||
import { FormWithSchema, Legend } from 'client/components/Forms'
|
||||
import { SSH_PUBLIC_KEY, SCRIPT_FIELDS, OTHER_FIELDS } from './schema'
|
||||
@ -33,12 +34,14 @@ const SSH_KEY_USER = '$USER[SSH_PUBLIC_KEY]'
|
||||
* @param {string} [props.stepId] - ID of the step the section belongs to
|
||||
* @param {object} props.oneConfig - Config of oned.conf
|
||||
* @param {boolean} props.adminGroup - User is admin or not
|
||||
* @param {boolean} props.isUpdate - If it's an update of the form
|
||||
* @returns {ReactElement} - Configuration section
|
||||
*/
|
||||
const ConfigurationSection = ({ stepId, oneConfig, adminGroup }) => {
|
||||
const ConfigurationSection = ({ stepId, oneConfig, adminGroup, isUpdate }) => {
|
||||
const { setValue, getValues } = useFormContext()
|
||||
const { setModifiedFields, setFieldPath } = useGeneralApi()
|
||||
const SSH_PUBLIC_KEY_PATH = useMemo(
|
||||
() => [stepId, SSH_PUBLIC_KEY.name].filter(Boolean).join('.'),
|
||||
() => [stepId, SSH_PUBLIC_KEY(isUpdate).name].filter(Boolean).join('.'),
|
||||
[stepId]
|
||||
)
|
||||
|
||||
@ -47,16 +50,35 @@ const ConfigurationSection = ({ stepId, oneConfig, adminGroup }) => {
|
||||
[stepId]
|
||||
)
|
||||
|
||||
const handleClearKey = useCallback(
|
||||
() => setValue(SSH_PUBLIC_KEY_PATH),
|
||||
[setValue, SSH_PUBLIC_KEY_PATH]
|
||||
)
|
||||
const handleClearKey = useCallback(() => {
|
||||
setValue(SSH_PUBLIC_KEY_PATH, '')
|
||||
|
||||
// Set as delete
|
||||
setFieldPath('extra.Context')
|
||||
setModifiedFields({
|
||||
extra: {
|
||||
CONTEXT: {
|
||||
SSH_PUBLIC_KEY: { __delete__: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
}, [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}`)
|
||||
|
||||
// Set as update
|
||||
setFieldPath('extra.Context')
|
||||
setModifiedFields({
|
||||
extra: {
|
||||
CONTEXT: {
|
||||
SSH_PUBLIC_KEY: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}, [getValues, setValue, SSH_PUBLIC_KEY_PATH])
|
||||
|
||||
return (
|
||||
@ -69,15 +91,17 @@ const ConfigurationSection = ({ stepId, oneConfig, adminGroup }) => {
|
||||
>
|
||||
<FormWithSchema
|
||||
id={stepId}
|
||||
saveState={true}
|
||||
cy={getCyPath('context-configuration-others')}
|
||||
fields={disableFields(OTHER_FIELDS, 'CONTEXT', oneConfig, adminGroup)}
|
||||
/>
|
||||
<section>
|
||||
<FormWithSchema
|
||||
id={stepId}
|
||||
saveState={true}
|
||||
cy={getCyPath('context-ssh-public-key')}
|
||||
fields={disableFields(
|
||||
[SSH_PUBLIC_KEY],
|
||||
[SSH_PUBLIC_KEY(isUpdate)],
|
||||
'CONTEXT',
|
||||
oneConfig,
|
||||
adminGroup
|
||||
@ -96,6 +120,7 @@ const ConfigurationSection = ({ stepId, oneConfig, adminGroup }) => {
|
||||
onClick={handleClearKey}
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
data-cy={getCyPath('delete-context-ssh-public-key')}
|
||||
>
|
||||
{T.Clear}
|
||||
</Button>
|
||||
@ -103,6 +128,7 @@ const ConfigurationSection = ({ stepId, oneConfig, adminGroup }) => {
|
||||
</section>
|
||||
<FormWithSchema
|
||||
id={stepId}
|
||||
saveState={true}
|
||||
cy={getCyPath('context-script')}
|
||||
fields={disableFields(
|
||||
SCRIPT_FIELDS,
|
||||
@ -122,6 +148,7 @@ ConfigurationSection.propTypes = {
|
||||
hypervisor: PropTypes.string,
|
||||
oneConfig: PropTypes.object,
|
||||
adminGroup: PropTypes.bool,
|
||||
isUpdate: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default ConfigurationSection
|
||||
|
@ -38,8 +38,9 @@ export const SECTION_ID = 'CONTEXT'
|
||||
* @returns {ReactElement} - Context vars section
|
||||
*/
|
||||
const ContextVarsSection = ({ stepId, hypervisor }) => {
|
||||
const { enqueueError } = useGeneralApi()
|
||||
const { enqueueError, setModifiedFields, setFieldPath } = useGeneralApi()
|
||||
const { setValue } = useFormContext()
|
||||
|
||||
const customVars = useWatch({
|
||||
name: [stepId, SECTION_ID].filter(Boolean).join('.'),
|
||||
})
|
||||
@ -68,6 +69,17 @@ const ContextVarsSection = ({ stepId, hypervisor }) => {
|
||||
// When the path is not found, it means that
|
||||
// the attribute is correct and we can set it
|
||||
setValue(formPath, newValue)
|
||||
|
||||
// Set as update if the newValue is not undefined and delete if the newValue is undefined
|
||||
// Set as delete
|
||||
setFieldPath('extra.Context')
|
||||
setModifiedFields({
|
||||
extra: {
|
||||
CONTEXT: {
|
||||
[path]: newValue ? true : { __delete__: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
[hypervisor]
|
||||
|
@ -15,6 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import PropTypes from 'prop-types'
|
||||
import { Folder as ContextIcon } from 'iconoir-react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import {
|
||||
TabType,
|
||||
@ -28,17 +29,25 @@ import FilesSection from './filesSection'
|
||||
import ContextVarsSection from './contextVarsSection'
|
||||
|
||||
import { T } from 'client/constants'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
export const TAB_ID = ['CONTEXT', USER_INPUTS_ID]
|
||||
|
||||
const Context = (props) => (
|
||||
<>
|
||||
<ConfigurationSection stepId={EXTRA_ID} {...props} />
|
||||
<UserInputsSection {...props} />
|
||||
<FilesSection stepId={EXTRA_ID} {...props} />
|
||||
<ContextVarsSection stepId={EXTRA_ID} />
|
||||
</>
|
||||
)
|
||||
const Context = (props) => {
|
||||
const { setFieldPath } = useGeneralApi()
|
||||
useEffect(() => {
|
||||
setFieldPath(`extra.Context`)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigurationSection stepId={EXTRA_ID} {...props} />
|
||||
<UserInputsSection {...props} />
|
||||
<FilesSection stepId={EXTRA_ID} {...props} />
|
||||
<ContextVarsSection stepId={EXTRA_ID} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Context.propTypes = {
|
||||
data: PropTypes.any,
|
||||
|
@ -21,11 +21,12 @@ import { FILES_SCHEMA } from './filesSchema'
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @param {boolean} isUpdate - If it's an update of the form
|
||||
* @returns {ObjectSchema} Context schema
|
||||
*/
|
||||
export const SCHEMA = (hypervisor) =>
|
||||
export const SCHEMA = (hypervisor, isUpdate) =>
|
||||
object()
|
||||
.concat(CONFIGURATION_SCHEMA)
|
||||
.concat(CONFIGURATION_SCHEMA(isUpdate))
|
||||
.concat(USER_INPUTS_SCHEMA)
|
||||
.concat(FILES_SCHEMA(hypervisor))
|
||||
|
||||
|
@ -13,7 +13,16 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ObjectSchema, array, boolean, number, object, ref, string } from 'yup'
|
||||
import {
|
||||
object,
|
||||
array,
|
||||
string,
|
||||
boolean,
|
||||
number,
|
||||
ref,
|
||||
ObjectSchema,
|
||||
mixed,
|
||||
} from 'yup'
|
||||
|
||||
import {
|
||||
INPUT_TYPES,
|
||||
@ -197,15 +206,13 @@ const DEFAULT_VALUE = {
|
||||
type === uiBoolean
|
||||
? arrayToOptions(['NO', 'YES'])
|
||||
: arrayToOptions(options),
|
||||
validation: string()
|
||||
.trim()
|
||||
validation: mixed()
|
||||
.default(() => undefined)
|
||||
.when(
|
||||
[TYPE.name, OPTIONS.name],
|
||||
(type, options = [], schema) =>
|
||||
({
|
||||
[uiList]: schema.oneOf(options).notRequired(),
|
||||
[uiListMultiple]: schema.includesInOptions(options),
|
||||
[uiRange]: number().min(ref(MIN.name)).max(ref(MAX.name)).integer(),
|
||||
[uiRangeFloat]: number().min(ref(MIN.name)).max(ref(MAX.name)),
|
||||
[uiPassword]: schema.strip().notRequired(),
|
||||
|
@ -78,11 +78,17 @@ const UserItemDraggable = styled(ListItem)(({ theme }) => ({
|
||||
}))
|
||||
|
||||
const UserInputItem = forwardRef(
|
||||
({ removeAction, error, userInput: { name, ...ui } = {}, ...props }, ref) => (
|
||||
(
|
||||
{ removeAction, error, userInput: { name, ...ui } = {}, index, ...props },
|
||||
ref
|
||||
) => (
|
||||
<UserItemDraggable
|
||||
ref={ref}
|
||||
secondaryAction={
|
||||
<IconButton onClick={removeAction}>
|
||||
<IconButton
|
||||
onClick={removeAction}
|
||||
data-cy={`delete-userInput-${index}`}
|
||||
>
|
||||
<DeleteCircledOutline />
|
||||
</IconButton>
|
||||
}
|
||||
@ -110,6 +116,7 @@ UserInputItem.propTypes = {
|
||||
removeAction: PropTypes.func,
|
||||
error: PropTypes.object,
|
||||
userInput: PropTypes.object,
|
||||
index: PropTypes.string,
|
||||
}
|
||||
|
||||
UserInputItem.displayName = 'UserInputItem'
|
||||
@ -143,6 +150,10 @@ const UserInputsSection = ({ oneConfig, adminGroup }) => {
|
||||
methods.reset()
|
||||
}
|
||||
|
||||
const onDelete = (index) => {
|
||||
remove(index)
|
||||
}
|
||||
|
||||
/** @param {DropResult} result - Drop result */
|
||||
const onDragEnd = (result) => {
|
||||
const { destination, source } = result ?? {}
|
||||
@ -165,6 +176,7 @@ const UserInputsSection = ({ oneConfig, adminGroup }) => {
|
||||
>
|
||||
<FormWithSchema
|
||||
cy={`${EXTRA_ID}-context-user-input`}
|
||||
saveState={true}
|
||||
fields={disableFields(
|
||||
USER_INPUT_FIELDS,
|
||||
'USER_INPUTS',
|
||||
@ -172,6 +184,7 @@ const UserInputsSection = ({ oneConfig, adminGroup }) => {
|
||||
adminGroup
|
||||
)}
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
fieldPath={`${EXTRA_ID}.Context`}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
@ -199,10 +212,11 @@ const UserInputsSection = ({ oneConfig, adminGroup }) => {
|
||||
{({ draggableProps, dragHandleProps, innerRef }) => (
|
||||
<UserInputItem
|
||||
key={id}
|
||||
index={index}
|
||||
ref={innerRef}
|
||||
userInput={userInput}
|
||||
error={get(errors, `${EXTRA_ID}.${SECTION_ID}.${index}`)}
|
||||
removeAction={() => remove(index)}
|
||||
removeAction={() => onDelete(index)}
|
||||
{...draggableProps}
|
||||
{...dragHandleProps}
|
||||
/>
|
||||
|
@ -62,7 +62,7 @@ export const TABS = [
|
||||
Backup,
|
||||
]
|
||||
|
||||
const Content = ({ data, setFormData, oneConfig, adminGroup }) => {
|
||||
const Content = ({ data, setFormData, oneConfig, adminGroup, isUpdate }) => {
|
||||
const {
|
||||
watch,
|
||||
formState: { errors },
|
||||
@ -97,6 +97,7 @@ const Content = ({ data, setFormData, oneConfig, adminGroup }) => {
|
||||
control,
|
||||
oneConfig,
|
||||
adminGroup,
|
||||
isUpdate,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
@ -116,11 +117,12 @@ const Content = ({ data, setFormData, oneConfig, adminGroup }) => {
|
||||
* @returns {object} Optional configuration step
|
||||
*/
|
||||
const ExtraConfiguration = ({
|
||||
dataTemplateExtended: vmTemplate,
|
||||
apiTemplateDataExtended: vmTemplate,
|
||||
oneConfig,
|
||||
adminGroup,
|
||||
}) => {
|
||||
const initialHypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
|
||||
const isUpdate = vmTemplate?.NAME
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
@ -128,10 +130,10 @@ const ExtraConfiguration = ({
|
||||
resolver: (formData) => {
|
||||
const hypervisor = formData?.[GENERAL_ID]?.HYPERVISOR ?? initialHypervisor
|
||||
|
||||
return SCHEMA(hypervisor)
|
||||
return SCHEMA(hypervisor, isUpdate)
|
||||
},
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: (props) => Content({ ...props, oneConfig, adminGroup }),
|
||||
content: (props) => Content({ ...props, oneConfig, adminGroup, isUpdate }),
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,6 +142,7 @@ Content.propTypes = {
|
||||
setFormData: PropTypes.func,
|
||||
oneConfig: PropTypes.object,
|
||||
adminGroup: PropTypes.bool,
|
||||
isUpdate: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default ExtraConfiguration
|
||||
|
@ -18,6 +18,7 @@ import { Stack } from '@mui/material'
|
||||
import { DataTransferBoth as IOIcon } from 'iconoir-react'
|
||||
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import {
|
||||
STEP_ID as EXTRA_ID,
|
||||
@ -28,41 +29,50 @@ import PciDevicesSection, { SECTION_ID as PCI_ID } from './pciDevicesSection'
|
||||
import VideoSection, { SECTION_ID as VIDEO_ID } from './videoSection'
|
||||
import { GRAPHICS_FIELDS } from './schema'
|
||||
import { T } from 'client/constants'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
export const TAB_ID = ['GRAPHICS', INPUT_ID, PCI_ID, VIDEO_ID]
|
||||
|
||||
const InputOutput = ({ hypervisor, oneConfig, adminGroup }) => (
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
|
||||
>
|
||||
<FormWithSchema
|
||||
cy={`${EXTRA_ID}-io-graphics`}
|
||||
fields={GRAPHICS_FIELDS(hypervisor, oneConfig, adminGroup)}
|
||||
legend={T.Graphics}
|
||||
id={EXTRA_ID}
|
||||
/>
|
||||
<InputsSection
|
||||
stepId={EXTRA_ID}
|
||||
hypervisor={hypervisor}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
<PciDevicesSection
|
||||
stepId={EXTRA_ID}
|
||||
hypervisor={hypervisor}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
<VideoSection
|
||||
stepId={EXTRA_ID}
|
||||
hypervisor={hypervisor}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
const InputOutput = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
const { setFieldPath } = useGeneralApi()
|
||||
useEffect(() => {
|
||||
setFieldPath(`extra.InputOutput`)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
|
||||
>
|
||||
<FormWithSchema
|
||||
cy={`${EXTRA_ID}-io-graphics`}
|
||||
fields={GRAPHICS_FIELDS(hypervisor, oneConfig, adminGroup)}
|
||||
legend={T.Graphics}
|
||||
id={EXTRA_ID}
|
||||
saveState={true}
|
||||
/>
|
||||
<InputsSection
|
||||
stepId={EXTRA_ID}
|
||||
hypervisor={hypervisor}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
<PciDevicesSection
|
||||
stepId={EXTRA_ID}
|
||||
hypervisor={hypervisor}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
<VideoSection
|
||||
stepId={EXTRA_ID}
|
||||
hypervisor={hypervisor}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
InputOutput.propTypes = {
|
||||
data: PropTypes.any,
|
||||
|
@ -22,7 +22,6 @@ import ListItemText from '@mui/material/ListItemText'
|
||||
import { DeleteCircledOutline, AddCircledOutline } from 'iconoir-react'
|
||||
import { useFieldArray, useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import { FormWithSchema, Legend } from 'client/components/Forms'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
|
||||
@ -36,6 +35,7 @@ import { T, HYPERVISORS } from 'client/constants'
|
||||
import SubmitButton from 'client/components/FormControl/SubmitButton'
|
||||
|
||||
import { hasRestrictedAttributes } from 'client/utils'
|
||||
|
||||
export const SECTION_ID = 'INPUT'
|
||||
|
||||
const InputsSection = memo(
|
||||
@ -79,6 +79,10 @@ const InputsSection = memo(
|
||||
methods.reset()
|
||||
}
|
||||
|
||||
const onDelete = (index) => {
|
||||
remove(index)
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null
|
||||
}
|
||||
@ -98,6 +102,8 @@ const InputsSection = memo(
|
||||
cy={getCyPath('io-inputs')}
|
||||
fields={fields}
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
saveState={true}
|
||||
fieldPath={`${stepId}.InputOutput`}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
@ -119,7 +125,7 @@ const InputsSection = memo(
|
||||
const busIcon = busTypeIcons[BUS]
|
||||
const busInfo = `${BUS}`
|
||||
|
||||
// Disable action if the nic has a restricted attribute on the template
|
||||
// Disable action if the input has a restricted attribute on the template
|
||||
const disabledAction =
|
||||
!adminGroup &&
|
||||
hasRestrictedAttributes(
|
||||
@ -134,10 +140,11 @@ const InputsSection = memo(
|
||||
key={id}
|
||||
secondaryAction={
|
||||
<SubmitButton
|
||||
onClick={() => remove(index)}
|
||||
onClick={() => onDelete(index)}
|
||||
icon=<DeleteCircledOutline />
|
||||
disabled={disabledAction}
|
||||
tooltip={tooltip}
|
||||
data-cy={`input-delete-${index}`}
|
||||
/>
|
||||
}
|
||||
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
|
||||
|
@ -129,13 +129,28 @@ const commonHiddenFieldProps = (name) => ({
|
||||
})
|
||||
|
||||
/** @type {Field} PCI device field */
|
||||
const DEVICE_FIELD = { label: T.Device, ...commonFieldProps('DEVICE') }
|
||||
const DEVICE_FIELD = {
|
||||
label: T.Device,
|
||||
fieldProps: { disabled: true },
|
||||
tooltip: T.DeviceTooltip,
|
||||
...commonFieldProps('DEVICE'),
|
||||
}
|
||||
|
||||
/** @type {Field} PCI device field */
|
||||
const VENDOR_FIELD = { label: T.Vendor, ...commonFieldProps('VENDOR') }
|
||||
const VENDOR_FIELD = {
|
||||
label: T.Vendor,
|
||||
fieldProps: { disabled: true },
|
||||
tooltip: T.VendorTooltip,
|
||||
...commonFieldProps('VENDOR'),
|
||||
}
|
||||
|
||||
/** @type {Field} PCI device field */
|
||||
const CLASS_FIELD = { label: T.Class, ...commonFieldProps('CLASS') }
|
||||
const CLASS_FIELD = {
|
||||
label: T.Class,
|
||||
fieldProps: { disabled: true },
|
||||
tooltip: T.ClassTooltip,
|
||||
...commonFieldProps('CLASS'),
|
||||
}
|
||||
|
||||
/** @type {Field} PCI device field */
|
||||
const SHORT_ADDRESS_FIELD = { ...commonHiddenFieldProps('SHORT_ADDRESS') }
|
||||
@ -184,5 +199,5 @@ export const PCI_SCHEMA = getObjectSchemaFromFields([
|
||||
|
||||
/** @type {ObjectSchema} PCI devices schema */
|
||||
export const PCI_DEVICES_SCHEMA = object({
|
||||
PCI: array(PCI_SCHEMA).ensure(),
|
||||
PCI: array().ensure(),
|
||||
})
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useMemo } from 'react'
|
||||
import { ReactElement, useMemo, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Stack, FormControl, Divider, Button } from '@mui/material'
|
||||
import List from '@mui/material/List'
|
||||
@ -37,6 +37,8 @@ import { T, HYPERVISORS } from 'client/constants'
|
||||
import { hasRestrictedAttributes } from 'client/utils'
|
||||
import SubmitButton from 'client/components/FormControl/SubmitButton'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
export const SECTION_ID = 'PCI'
|
||||
|
||||
/**
|
||||
@ -64,6 +66,17 @@ const PciDevicesSection = ({ stepId, hypervisor, oneConfig, adminGroup }) => {
|
||||
name: [stepId, SECTION_ID].filter(Boolean).join('.'),
|
||||
})
|
||||
|
||||
const { setModifiedFields, setFieldPath, initModifiedFields } =
|
||||
useGeneralApi()
|
||||
|
||||
useEffect(() => {
|
||||
// Init pci devices modified fields
|
||||
setFieldPath(`extra.InputOutput.PCI`)
|
||||
initModifiedFields([
|
||||
...pciDevices.map((element, index) => ({ __aliasPci__: index })),
|
||||
])
|
||||
}, [])
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: PCI_SCHEMA.default(),
|
||||
resolver: yupResolver(PCI_SCHEMA),
|
||||
@ -71,10 +84,18 @@ const PciDevicesSection = ({ stepId, hypervisor, oneConfig, adminGroup }) => {
|
||||
|
||||
const onSubmit = (newInput) => {
|
||||
delete newInput.DEVICE_NAME
|
||||
|
||||
setFieldPath(`${stepId}.InputOutput.PCI.${pciDevices.length}`)
|
||||
append(newInput)
|
||||
methods.reset()
|
||||
}
|
||||
|
||||
const onDelete = (index) => {
|
||||
setFieldPath(`${stepId}.InputOutput.PCI.${index}`)
|
||||
setModifiedFields({ __delete__: true })
|
||||
remove(index)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
@ -93,6 +114,8 @@ const PciDevicesSection = ({ stepId, hypervisor, oneConfig, adminGroup }) => {
|
||||
cy={[stepId, 'io-pci-devices'].filter(Boolean).join('.')}
|
||||
fields={fields}
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
saveState={true}
|
||||
fieldPath={`${stepId}.InputOutput`}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
@ -100,6 +123,7 @@ const PciDevicesSection = ({ stepId, hypervisor, oneConfig, adminGroup }) => {
|
||||
color="secondary"
|
||||
startIcon={<AddCircledOutline />}
|
||||
sx={{ mt: '1em' }}
|
||||
data-cy={[stepId, 'add-io-pci-devices'].filter(Boolean).join('-')}
|
||||
>
|
||||
<Translate word={T.Add} />
|
||||
</Button>
|
||||
@ -141,10 +165,11 @@ const PciDevicesSection = ({ stepId, hypervisor, oneConfig, adminGroup }) => {
|
||||
key={id}
|
||||
secondaryAction={
|
||||
<SubmitButton
|
||||
onClick={() => remove(index)}
|
||||
onClick={() => onDelete(index)}
|
||||
icon=<DeleteCircledOutline />
|
||||
disabled={disabledAction}
|
||||
tooltip={tooltip}
|
||||
data-cy={`pci-delete-${index}`}
|
||||
/>
|
||||
}
|
||||
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
|
||||
|
@ -62,7 +62,7 @@ export const VIDEO_TYPE = {
|
||||
!context?.general?.HYPERVISOR
|
||||
|
||||
// Not send to the request the value if it's not a valid hypervisor
|
||||
return validHypervisor && value !== VIDEO_TYPES.auto ? value : undefined
|
||||
return validHypervisor && value !== VIDEO_TYPES.auto ? value : ''
|
||||
}),
|
||||
grid: { sm: 3, md: 3 },
|
||||
}
|
||||
|
@ -62,6 +62,7 @@ const VideoSection = ({ stepId, hypervisor, oneConfig, adminGroup }) => {
|
||||
fields={fields}
|
||||
legend={T.Video}
|
||||
rootProps={{ sx: { gridColumn: '1 / -1' } }}
|
||||
saveState={true}
|
||||
id={stepId}
|
||||
/>
|
||||
)
|
||||
|
@ -17,7 +17,7 @@ import PropTypes from 'prop-types'
|
||||
import { Stack } from '@mui/material'
|
||||
import { ServerConnection as NetworkIcon } from 'iconoir-react'
|
||||
import { useFormContext, useFieldArray } from 'react-hook-form'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import NicCard from 'client/components/Cards/NicCard'
|
||||
import {
|
||||
@ -36,6 +36,7 @@ import {
|
||||
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting'
|
||||
import { FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/networking/schema'
|
||||
import { T, PCI_TYPES } from 'client/constants'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
export const TAB_ID = ['NIC', 'NIC_ALIAS', 'PCI']
|
||||
|
||||
@ -44,6 +45,35 @@ const mapAliasNameFunction = mapNameByIndex(TAB_ID[1])
|
||||
const mapPCINameFunction = mapNameByIndex(TAB_ID[2])
|
||||
|
||||
const Networking = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
const {
|
||||
setModifiedFields,
|
||||
setFieldPath,
|
||||
initModifiedFields,
|
||||
changePositionModifiedFields,
|
||||
} = useGeneralApi()
|
||||
useEffect(() => {
|
||||
// Init nic modified fields
|
||||
setFieldPath(`extra.Network.NIC`)
|
||||
initModifiedFields([
|
||||
...nics.map((element, index) => ({ __nicIndex__: index })),
|
||||
])
|
||||
|
||||
// Init alias modified fields
|
||||
setFieldPath(`extra.Network.NIC_ALIAS`)
|
||||
initModifiedFields([
|
||||
...alias.map((element, index) => ({ __aliasIndex__: index })),
|
||||
])
|
||||
|
||||
// Init pci modified fields
|
||||
setFieldPath(`extra.InputOutput.PCI`)
|
||||
initModifiedFields([
|
||||
...pcis.map((element, index) => ({ __aliasPci__: index })),
|
||||
])
|
||||
|
||||
// Set field to network
|
||||
setFieldPath(`extra.Network`)
|
||||
}, [])
|
||||
|
||||
const { setValue, getValues } = useFormContext()
|
||||
|
||||
const {
|
||||
@ -73,7 +103,20 @@ const Networking = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
name: `${EXTRA_ID}.${TAB_ID[2]}`,
|
||||
})
|
||||
|
||||
const removeAndReorder = (nic) => {
|
||||
// Delay execution until next event loop tick to ensure state updates
|
||||
useEffect(() => {
|
||||
setFieldPath(`extra.Network`)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Remove a nic and reorder the array of nics, alias or pcis. Also, update boot order.
|
||||
*
|
||||
* @param {object} nic - Nic to delete
|
||||
* @param {string} idNic - If of the nic in the array
|
||||
* @param {object} updatedNic - Nic to update
|
||||
*/
|
||||
const removeAndReorder = (nic, idNic, updatedNic) => {
|
||||
// Get nic name and if it is alias or pci
|
||||
const nicName = nic?.NAME
|
||||
const isAlias = !!nic?.PARENT?.length
|
||||
const isPCI = nic?.TYPE === 'NIC'
|
||||
@ -81,21 +124,47 @@ const Networking = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
let list
|
||||
let mapFunction
|
||||
|
||||
if (isAlias) {
|
||||
list = alias
|
||||
mapFunction = mapAliasNameFunction
|
||||
} else if (isPCI) {
|
||||
if (isPCI) {
|
||||
// Select list and map name function with pci type
|
||||
list = pcis
|
||||
mapFunction = mapPCINameFunction
|
||||
|
||||
// Find the id of the nic in pci array
|
||||
const indexRemove = list.findIndex((nicPci) => nicPci.id === idNic)
|
||||
|
||||
// Set field path on the index of pci array to set delete flag in this element
|
||||
setFieldPath(`extra.InputOutput.PCI.${indexRemove}`)
|
||||
} else if (isAlias) {
|
||||
// Select list and map name function with alias type
|
||||
list = alias
|
||||
mapFunction = mapAliasNameFunction
|
||||
|
||||
// Find the id of the nic in alias array
|
||||
const indexRemove = list.findIndex((nicAlias) => nicAlias.id === idNic)
|
||||
|
||||
// Set field path on the index of alias array to set delete flag in this element
|
||||
setFieldPath(`extra.Network.NIC_ALIAS.${indexRemove}`)
|
||||
} else {
|
||||
// Select list and map name function with nic type
|
||||
list = nics
|
||||
mapFunction = mapNicNameFunction
|
||||
|
||||
// Find the id of the nic in nics array
|
||||
const indexRemove = list.findIndex(
|
||||
(nicNetwork) => nicNetwork.id === idNic
|
||||
)
|
||||
|
||||
// Set field path on the index of nics array to set delete flag in this element
|
||||
setFieldPath(`extra.Network.NIC.${indexRemove}`)
|
||||
}
|
||||
|
||||
const updatedList = list
|
||||
.filter(({ NAME }) => NAME !== nicName)
|
||||
.map(mapFunction)
|
||||
// Set delete flag in modified fields
|
||||
setModifiedFields({ __flag__: 'DELETE' })
|
||||
|
||||
// Update list selected (nics, alias or pcis) names. Names are based on index, so the names of the list elements are calculated and updated
|
||||
const updatedList = list.filter(({ id }) => idNic !== id).map(mapFunction)
|
||||
|
||||
// Update boot order of booting section (boot order has disks and nics, so if we delete a nic, we need to update it)
|
||||
const currentBootOrder = getValues(BOOT_ORDER_NAME())
|
||||
const updatedBootOrder = reorderBootAfterRemove(
|
||||
nicName,
|
||||
@ -103,6 +172,31 @@ const Networking = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
currentBootOrder
|
||||
)
|
||||
|
||||
// Set modifiedFields with boout order to update it
|
||||
setValue(BOOT_ORDER_NAME(), updatedBootOrder)
|
||||
|
||||
// Update refernces to NAME in alias items
|
||||
if (!isAlias) {
|
||||
alias.forEach((aliasItem) => {
|
||||
const oldParent = list.find((item) => item.NAME === aliasItem.PARENT)
|
||||
if (oldParent) {
|
||||
const newParent = updatedList.find((item) => item.id === oldParent.id)
|
||||
aliasItem.PARENT = newParent.NAME
|
||||
}
|
||||
})
|
||||
|
||||
if (updatedNic) {
|
||||
const oldParent = list.find((item) => item.NAME === updatedNic.PARENT)
|
||||
if (oldParent) {
|
||||
const newParent = updatedList.find((item) => item.id === oldParent.id)
|
||||
updatedNic.PARENT = newParent.NAME
|
||||
}
|
||||
}
|
||||
|
||||
replaceAlias(alias)
|
||||
}
|
||||
|
||||
// Replace the list (nics, alias or pcis) with the new values after delete the element
|
||||
if (isAlias) {
|
||||
replaceAlias(updatedList)
|
||||
} else if (isPCI) {
|
||||
@ -110,57 +204,237 @@ const Networking = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
} else {
|
||||
replaceNic(updatedList)
|
||||
}
|
||||
|
||||
setValue(BOOT_ORDER_NAME(), updatedBootOrder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a nic with the different cases that could be because nics, alias and pcis are on different array forms.
|
||||
*
|
||||
* @param {object} nic - The nic to update
|
||||
* @param {string} nic.NAME - Name of the nic
|
||||
* @param {string} id - The id of the nic in the array form
|
||||
* @param {object} nicForDelete - Nic to delete if user changes nic, alias or pci
|
||||
* @returns {void} - Void value
|
||||
*/
|
||||
const handleUpdate = ({ NAME: _, ...updatedNic }, id, nicForDelete) => {
|
||||
// Check if the nic is alias or pci
|
||||
const isAlias = !!updatedNic?.PARENT?.length
|
||||
const isPCI = Object.values(PCI_TYPES).includes(updatedNic?.PCI_TYPE)
|
||||
|
||||
if (isAlias) {
|
||||
const index = alias.findIndex((nic) => nic.id === id)
|
||||
if (index === -1) {
|
||||
removeAndReorder(nicForDelete)
|
||||
handleAppend(updatedNic)
|
||||
if (isPCI) {
|
||||
// Get the index of the pci in the pci array
|
||||
const indexPci = pcis.findIndex((nic) => nic.id === id)
|
||||
|
||||
// If the index is equal to -1, that's mean that it's an element that before is not a pci, but in this update, user change this element from nic or alias type to pci.
|
||||
// In this case, we need to delete the old element (that is on nics or alias array) and add to the pci arrays.
|
||||
if (indexPci === -1) {
|
||||
// Check if the old element is in nic or alias array and get the index
|
||||
const indexNic = nics.findIndex((nic) => nic.id === id)
|
||||
const indexAlias = alias.findIndex((nic) => nic.id === id)
|
||||
|
||||
// If the old element it's on nics array, we need to get the state (if it was deleted or updated) of the element from Network.NIC of modifiedFields and set on Network.PCI of modifiedFields
|
||||
if (indexNic !== -1) {
|
||||
changePositionModifiedFields({
|
||||
sourcePath: 'extra.Network.NIC',
|
||||
sourcePosition: indexNic,
|
||||
targetPath: 'extra.InputOutput.PCI',
|
||||
targetPosition: pcis.length,
|
||||
sourceDelete: false,
|
||||
emptyObjectContent: true,
|
||||
})
|
||||
}
|
||||
|
||||
// If the old element it's on alias array, we need to get the state (if it was deleted or updated) of the element from Network.NIC_ALIAS of moodifiedFields and set on Network.PCI of modifiedFields
|
||||
if (indexAlias !== -1) {
|
||||
changePositionModifiedFields({
|
||||
sourcePath: 'extra.Network.NIC_ALIAS',
|
||||
sourcePosition: indexAlias,
|
||||
targetPath: 'extra.InputOutput.PCI',
|
||||
targetPosition: pcis.length,
|
||||
sourceDelete: false,
|
||||
emptyObjectContent: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Remove the old element
|
||||
removeAndReorder(nicForDelete, id)
|
||||
|
||||
// Add the new element
|
||||
handleAppend(updatedNic, true)
|
||||
|
||||
return
|
||||
}
|
||||
updateAlias(index, mapAliasNameFunction(updatedNic, index))
|
||||
} else if (isPCI) {
|
||||
const index = pcis.findIndex((nic) => nic.id === id)
|
||||
if (index === -1) {
|
||||
removeAndReorder(nicForDelete)
|
||||
handleAppend(updatedNic)
|
||||
|
||||
return
|
||||
}
|
||||
// Update if the pci exists on pcis array
|
||||
updatedNic.TYPE = 'NIC'
|
||||
delete updatedNic.PCI_TYPE
|
||||
updatePCI(index, mapPCINameFunction(updatedNic, index))
|
||||
} else {
|
||||
const index = nics.findIndex((nic) => nic.id === id)
|
||||
if (index === -1) {
|
||||
removeAndReorder(nicForDelete)
|
||||
handleAppend(updatedNic)
|
||||
updatePCI(indexPci, mapPCINameFunction(updatedNic, indexPci))
|
||||
} else if (isAlias) {
|
||||
// Get the index of the alias in the alias array
|
||||
const indexAlias = alias.findIndex((nic) => nic.id === id)
|
||||
|
||||
// If the index is equal to -1, that's mean that it's an element that before is not an alias, but in this update, user change this element from nic or pci type to alias.
|
||||
// In this case, we need to delete the old element (that is on nics or pcis array) and add to the alias arrays.
|
||||
if (indexAlias === -1) {
|
||||
// Check if the old element is in nic or pcis array and get the index
|
||||
const indexNic = nics.findIndex((nic) => nic.id === id)
|
||||
const indexPci = pcis.findIndex((nic) => nic.id === id)
|
||||
|
||||
// If the old element it's on nics array, we need to get the state (if it was deleted or updated) of the element from Network.NIC of moodifiedFields and set on Network.NIC_ALIAS of modifiedFields
|
||||
if (indexNic !== -1) {
|
||||
changePositionModifiedFields({
|
||||
sourcePath: 'extra.Network.NIC',
|
||||
sourcePosition: indexNic,
|
||||
targetPath: 'extra.Network.NIC_ALIAS',
|
||||
targetPosition: alias.length,
|
||||
sourceDelete: false,
|
||||
emptyObjectContent: true,
|
||||
})
|
||||
}
|
||||
|
||||
// If the old element it's on pcis array, we need to get the state (if it was deleted or updated) of the element from Network.PCI of moodifiedFields and set on Network.NIC_ALIAS of modifiedFields
|
||||
if (indexPci !== -1) {
|
||||
changePositionModifiedFields({
|
||||
sourcePath: 'extra.InputOutput.PCI',
|
||||
sourcePosition: indexPci,
|
||||
targetPath: 'extra.Network.NIC_ALIAS',
|
||||
targetPosition: alias.length,
|
||||
sourceDelete: false,
|
||||
emptyObjectContent: true,
|
||||
})
|
||||
|
||||
// // If the element was pci, delete the pci fields
|
||||
setFieldPath(`extra.Network.NIC_ALIAS.${alias.length}.advanced`)
|
||||
setModifiedFields({
|
||||
advanced: { TYPE: { __delete__: true } },
|
||||
})
|
||||
}
|
||||
|
||||
// Remove the old element
|
||||
removeAndReorder(nicForDelete, id, updatedNic)
|
||||
|
||||
// Add the new element
|
||||
handleAppend(updatedNic, true)
|
||||
|
||||
return
|
||||
}
|
||||
updateNic(index, mapNicNameFunction(updatedNic, index))
|
||||
|
||||
// Update if the alias exists on alias array
|
||||
updateAlias(indexAlias, mapAliasNameFunction(updatedNic, indexAlias))
|
||||
} else {
|
||||
// Get the index of the nic in the nics array
|
||||
const indexNic = nics.findIndex((nic) => nic.id === id)
|
||||
|
||||
// If the index is equal to -1, that's mean that it's an element that before is not a nic, but in this update, user change this element from alias or pci type to nic.
|
||||
// In this case, we need to delete the old element (that is on nics or pcis array) and add to the alias arrays.
|
||||
if (indexNic === -1) {
|
||||
// Check if the old element is in alias or pcis array and get the index
|
||||
const indexAlias = alias.findIndex((nic) => nic.id === id)
|
||||
const indexPci = pcis.findIndex((nic) => nic.id === id)
|
||||
|
||||
// If the old element it's on alias array, we need to get the state (if it was deleted or updated) of the element from Network.NIC_ALIAS of moodifiedFields and set on Network.NIC of modifiedFields
|
||||
if (indexAlias !== -1) {
|
||||
changePositionModifiedFields({
|
||||
sourcePath: 'extra.Network.NIC_ALIAS',
|
||||
sourcePosition: indexAlias,
|
||||
targetPath: 'extra.Network.NIC',
|
||||
targetPosition: nics.length,
|
||||
sourceDelete: false,
|
||||
emptyObjectContent: true,
|
||||
})
|
||||
}
|
||||
|
||||
// If the old element it's on pcis array, we need to get the state (if it was deleted or updated) of the element from Network.PCI of moodifiedFields and set on Network.NIC_ALIAS of modifiedFields
|
||||
if (indexPci !== -1) {
|
||||
changePositionModifiedFields({
|
||||
sourcePath: 'extra.InputOutput.PCI',
|
||||
sourcePosition: indexPci,
|
||||
targetPath: 'extra.Network.NIC',
|
||||
targetPosition: nics.length,
|
||||
sourceDelete: false,
|
||||
emptyObjectContent: true,
|
||||
})
|
||||
|
||||
// If the element was pci, delete the pci fields
|
||||
setFieldPath(`extra.Network.NIC.${nics.length}`)
|
||||
setModifiedFields({
|
||||
advanced: { TYPE: { __delete__: true } },
|
||||
})
|
||||
}
|
||||
|
||||
// Remove the old element
|
||||
removeAndReorder(nicForDelete, id)
|
||||
|
||||
// Add the new element
|
||||
handleAppend(updatedNic, true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// In case that the element has not changed from nic to alias or pci, update on nics array
|
||||
updateNic(indexNic, mapNicNameFunction(updatedNic, indexNic))
|
||||
}
|
||||
|
||||
// Always set field path on the length of nics
|
||||
setFieldPath(`extra.Network`)
|
||||
}
|
||||
|
||||
const handleAppend = (newNic) => {
|
||||
/**
|
||||
* Append a nic to the corresponding array (nics, alias or pcis).
|
||||
*
|
||||
* @param {object} newNic - The nic to append
|
||||
* @param {boolean} update - If the append it's when user are updating a nic
|
||||
*/
|
||||
const handleAppend = (newNic, update) => {
|
||||
// Check if nic is alias or pci
|
||||
const isAlias = !!newNic?.PARENT?.length
|
||||
const isPCI = Object.values(PCI_TYPES).includes(newNic?.PCI_TYPE)
|
||||
|
||||
if (isAlias) {
|
||||
appendAlias(mapAliasNameFunction(newNic, alias.length))
|
||||
} else if (isPCI) {
|
||||
if (isPCI) {
|
||||
// Set pci type as pci attribute
|
||||
newNic.TYPE = 'NIC'
|
||||
delete newNic.PCI_TYPE
|
||||
|
||||
// Add the nic to the pci section in modified fields
|
||||
!update &&
|
||||
changePositionModifiedFields({
|
||||
sourcePath: 'extra.Network.NIC',
|
||||
sourcePosition: nics.length,
|
||||
targetPath: 'extra.InputOutput.PCI',
|
||||
targetPosition: pcis.length,
|
||||
sourceDelete: true,
|
||||
})
|
||||
setFieldPath(`extra.InputOutput.PCI.${pcis.length}`)
|
||||
setModifiedFields({
|
||||
advanced: { PCI_TYPE: { __delete__: true } },
|
||||
})
|
||||
|
||||
// Append to form array of pci
|
||||
appendPCI(mapPCINameFunction(newNic, pcis.length))
|
||||
} else if (isAlias) {
|
||||
// Add the nic to the alias section in modified fields
|
||||
!update &&
|
||||
changePositionModifiedFields({
|
||||
sourcePath: 'extra.Network.NIC',
|
||||
sourcePosition: nics.length,
|
||||
targetPath: 'extra.Network.NIC_ALIAS',
|
||||
targetPosition: alias.length,
|
||||
sourceDelete: true,
|
||||
})
|
||||
setFieldPath(`extra.Network.NIC_ALIAS.${alias.length}`)
|
||||
setModifiedFields({
|
||||
advanced: { PCI_TYPE: { __delete__: true } },
|
||||
})
|
||||
|
||||
// Append to form array of alias
|
||||
appendAlias(mapAliasNameFunction(newNic, alias.length))
|
||||
} else {
|
||||
// Set field path to last position
|
||||
setFieldPath(`extra.Network.NIC.${nics.length}`)
|
||||
setModifiedFields({
|
||||
advanced: { PCI_TYPE: { __delete__: true } },
|
||||
})
|
||||
|
||||
// Append to form array of nics
|
||||
appendNic(mapNicNameFunction(newNic, nics.length))
|
||||
}
|
||||
}
|
||||
@ -189,6 +463,8 @@ const Networking = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
{[...nics, ...alias, ...pcis.filter((pci) => pci?.TYPE === 'NIC')]?.map(
|
||||
({ id, ...item }, index) => {
|
||||
const hasAlias = alias?.some((nic) => nic.PARENT === item.NAME)
|
||||
const isPci = item.TYPE === 'NIC'
|
||||
const isAlias = Object.prototype.hasOwnProperty.call(item, 'PARENT')
|
||||
item.NIC_ID = index
|
||||
|
||||
return (
|
||||
@ -202,7 +478,7 @@ const Networking = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
{!hasAlias && (
|
||||
<DetachAction
|
||||
nic={item}
|
||||
onSubmit={() => removeAndReorder(item)}
|
||||
onSubmit={() => removeAndReorder(item, id)}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
@ -216,6 +492,12 @@ const Networking = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
onSubmit={(updatedNic) =>
|
||||
handleUpdate(updatedNic, id, item)
|
||||
}
|
||||
indexNic={nics.findIndex((nic) => nic.id === id)}
|
||||
indexAlias={alias.findIndex((nic) => nic.id === id)}
|
||||
indexPci={pcis.findIndex((nic) => nic.id === id)}
|
||||
hasAlias={hasAlias}
|
||||
isPci={isPci}
|
||||
isAlias={isAlias}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
@ -230,6 +512,7 @@ const Networking = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
legend={T.NetworkDefaults}
|
||||
legendTooltip={T.NetworkDefaultsConcept}
|
||||
id={EXTRA_ID}
|
||||
saveState={true}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import PropTypes from 'prop-types'
|
||||
import { ElectronicsChip as NumaIcon } from 'iconoir-react'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import { STEP_ID as GENERAL_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General'
|
||||
@ -24,18 +24,19 @@ import {
|
||||
TabType,
|
||||
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { VIRTUAL_CPU as VCPU_FIELD } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/capacitySchema'
|
||||
import {
|
||||
NUMA_FIELDS,
|
||||
ENABLE_NUMA,
|
||||
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/numa/schema'
|
||||
import { NUMA_FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/numa/schema'
|
||||
import { T } from 'client/constants'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
import { disableFields } from 'client/utils'
|
||||
|
||||
export const TAB_ID = 'NUMA'
|
||||
|
||||
const Numa = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
const enableNuma = useWatch({ name: `${EXTRA_ID}.${ENABLE_NUMA.name}` })
|
||||
const { setFieldPath } = useGeneralApi()
|
||||
useEffect(() => {
|
||||
setFieldPath(`extra.NUMA`)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -43,24 +44,19 @@ const Numa = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
cy={`${EXTRA_ID}-vcpu`}
|
||||
fields={disableFields([VCPU_FIELD], 'TOPOLOGY', oneConfig, adminGroup)}
|
||||
id={GENERAL_ID}
|
||||
saveState={true}
|
||||
/>
|
||||
<FormWithSchema
|
||||
cy={`${EXTRA_ID}-numa-enable`}
|
||||
fields={disableFields([ENABLE_NUMA], 'TOPOLOGY', oneConfig, adminGroup)}
|
||||
cy={`${EXTRA_ID}-numa`}
|
||||
fields={disableFields(
|
||||
NUMA_FIELDS(hypervisor),
|
||||
'TOPOLOGY',
|
||||
oneConfig,
|
||||
adminGroup
|
||||
)}
|
||||
saveState={true}
|
||||
id={EXTRA_ID}
|
||||
/>
|
||||
{enableNuma && (
|
||||
<FormWithSchema
|
||||
cy={`${EXTRA_ID}-numa`}
|
||||
fields={disableFields(
|
||||
NUMA_FIELDS(hypervisor),
|
||||
'TOPOLOGY',
|
||||
oneConfig,
|
||||
adminGroup
|
||||
)}
|
||||
id={EXTRA_ID}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -58,7 +58,9 @@ const PIN_POLICY = {
|
||||
addEmpty: false,
|
||||
getText: sentenceCase,
|
||||
}),
|
||||
dependOf: '$general.HYPERVISOR',
|
||||
dependOf: ENABLE_NUMA.name,
|
||||
htmlType: (enableNuma) => !enableNuma && INPUT_TYPES.HIDDEN,
|
||||
notOnHypervisors: [vcenter, firecracker],
|
||||
validation: lazy((_, { context }) =>
|
||||
string()
|
||||
.trim()
|
||||
@ -74,9 +76,6 @@ const PIN_POLICY = {
|
||||
: NUMA_PIN_POLICIES.NONE
|
||||
})
|
||||
),
|
||||
fieldProps: (hypervisor) => ({
|
||||
disabled: [vcenter, firecracker].includes(hypervisor),
|
||||
}),
|
||||
}
|
||||
|
||||
/** @type {Field} NODE_AFFINITY field */
|
||||
@ -84,17 +83,12 @@ const NODE_AFFINITY = {
|
||||
name: 'TOPOLOGY.NODE_AFFINITY',
|
||||
label: T.NodeAffinity,
|
||||
tooltip: T.NodeAffinityConcept,
|
||||
dependOf: ['$general.HYPERVISOR', PIN_POLICY.name],
|
||||
dependOf: [PIN_POLICY.name, ENABLE_NUMA.name],
|
||||
htmlType: ([pinPolicy, enableNuma] = []) =>
|
||||
(!enableNuma || pinPolicy !== NUMA_PIN_POLICIES.NODE_AFFINITY) &&
|
||||
INPUT_TYPES.HIDDEN,
|
||||
notOnHypervisors: [vcenter, firecracker],
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: (_, context) => {
|
||||
const values = context?.getValues() || {}
|
||||
const { general, extra } = values || {}
|
||||
|
||||
return ![vcenter, firecracker].includes(general?.HYPERVISOR) &&
|
||||
extra?.TOPOLOGY?.PIN_POLICY === NUMA_PIN_POLICIES.NODE_AFFINITY
|
||||
? 'number'
|
||||
: INPUT_TYPES.HIDDEN
|
||||
},
|
||||
validation: lazy((_, { context }) => {
|
||||
const { general, extra } = context || {}
|
||||
|
||||
@ -110,11 +104,15 @@ const CORES = {
|
||||
name: 'TOPOLOGY.CORES',
|
||||
label: T.Cores,
|
||||
tooltip: T.NumaCoresConcept,
|
||||
dependOf: ['$general.VCPU', '$general.HYPERVISOR'],
|
||||
dependOf: ['$general.VCPU', '$general.HYPERVISOR', ENABLE_NUMA.name],
|
||||
type: ([, hypervisor] = []) =>
|
||||
hypervisor === vcenter ? INPUT_TYPES.SELECT : INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
values: ([vcpu] = []) => arrayToOptions(getFactorsOfNumber(vcpu ?? 0)),
|
||||
htmlType: ([, , enableNuma] = []) =>
|
||||
!enableNuma ? INPUT_TYPES.HIDDEN : 'number',
|
||||
values: ([vcpu, hypervisor] = []) => {
|
||||
if (hypervisor === vcenter)
|
||||
return arrayToOptions(getFactorsOfNumber(vcpu ?? 0))
|
||||
},
|
||||
validation: number()
|
||||
.notRequired()
|
||||
.integer()
|
||||
@ -127,15 +125,19 @@ const SOCKETS = {
|
||||
label: T.Sockets,
|
||||
tooltip: T.NumaSocketsConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
dependOf: ['$general.HYPERVISOR', '$general.VCPU', 'TOPOLOGY.CORES'],
|
||||
dependOf: [
|
||||
'$general.HYPERVISOR',
|
||||
'$general.VCPU',
|
||||
'TOPOLOGY.CORES',
|
||||
ENABLE_NUMA.name,
|
||||
],
|
||||
htmlType: ([, , , enableNuma] = []) =>
|
||||
!enableNuma ? INPUT_TYPES.HIDDEN : 'number',
|
||||
notOnHypervisors: [vcenter, firecracker],
|
||||
validation: number()
|
||||
.notRequired()
|
||||
.integer()
|
||||
.default(() => 1),
|
||||
fieldProps: (hypervisor) => ({
|
||||
disabled: [vcenter, firecracker].includes(hypervisor),
|
||||
}),
|
||||
watcher: ([hypervisor, vcpu, cores] = []) => {
|
||||
if (hypervisor === vcenter) return
|
||||
|
||||
@ -158,9 +160,10 @@ const THREADS = {
|
||||
name: 'TOPOLOGY.THREADS',
|
||||
label: T.Threads,
|
||||
tooltip: T.ThreadsConcept,
|
||||
htmlType: 'number',
|
||||
dependOf: '$general.HYPERVISOR',
|
||||
type: (hypervisor) =>
|
||||
htmlType: ([, enableNuma] = []) =>
|
||||
!enableNuma ? INPUT_TYPES.HIDDEN : 'number',
|
||||
dependOf: ['$general.HYPERVISOR', ENABLE_NUMA.name],
|
||||
type: ([hypervisor] = []) =>
|
||||
[firecracker, vcenter].includes(hypervisor)
|
||||
? INPUT_TYPES.SELECT
|
||||
: INPUT_TYPES.TEXT,
|
||||
@ -184,6 +187,8 @@ const HUGEPAGES = {
|
||||
label: T.HugepagesSize,
|
||||
tooltip: T.HugepagesSizeConcept,
|
||||
notOnHypervisors: [vcenter, firecracker],
|
||||
dependOf: ENABLE_NUMA.name,
|
||||
htmlType: (enableNuma) => !enableNuma && INPUT_TYPES.HIDDEN,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: () => {
|
||||
const { data: hosts = [] } = useGetHostsQuery()
|
||||
@ -208,6 +213,8 @@ const MEMORY_ACCESS = {
|
||||
tooltip: [T.MemoryAccessConcept, NUMA_MEMORY_ACCESS.join(', ')],
|
||||
notOnHypervisors: [vcenter, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
dependOf: ENABLE_NUMA.name,
|
||||
htmlType: (enableNuma) => !enableNuma && INPUT_TYPES.HIDDEN,
|
||||
values: arrayToOptions(NUMA_MEMORY_ACCESS, { getText: sentenceCase }),
|
||||
validation: string()
|
||||
.trim()
|
||||
@ -222,6 +229,7 @@ const MEMORY_ACCESS = {
|
||||
const NUMA_FIELDS = (hypervisor) =>
|
||||
filterFieldsByHypervisor(
|
||||
[
|
||||
ENABLE_NUMA,
|
||||
PIN_POLICY,
|
||||
NODE_AFFINITY,
|
||||
CORES,
|
||||
@ -237,7 +245,7 @@ const NUMA_FIELDS = (hypervisor) =>
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of NUMA fields
|
||||
*/
|
||||
const SCHEMA_FIELDS = (hypervisor) => [ENABLE_NUMA, ...NUMA_FIELDS(hypervisor)]
|
||||
const SCHEMA_FIELDS = (hypervisor) => NUMA_FIELDS(hypervisor)
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
@ -266,9 +274,4 @@ const NUMA_SCHEMA = (hypervisor) =>
|
||||
}
|
||||
)
|
||||
|
||||
export {
|
||||
ENABLE_NUMA,
|
||||
SCHEMA_FIELDS as FIELDS,
|
||||
NUMA_FIELDS,
|
||||
NUMA_SCHEMA as SCHEMA,
|
||||
}
|
||||
export { SCHEMA_FIELDS as FIELDS, NUMA_FIELDS, NUMA_SCHEMA as SCHEMA }
|
||||
|
@ -15,6 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import PropTypes from 'prop-types'
|
||||
import { NetworkAlt as PlacementIcon } from 'iconoir-react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
@ -27,25 +28,34 @@ import {
|
||||
FIELDS,
|
||||
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/placement/schema'
|
||||
import { T } from 'client/constants'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
const Placement = ({ oneConfig, adminGroup }) => (
|
||||
// TODO - Host requirements: add button to select HOST in list => ID="<id>"
|
||||
// TODO - Host policy options: Packing|Stripping|Load-aware
|
||||
const Placement = ({ oneConfig, adminGroup }) => {
|
||||
const { setFieldPath } = useGeneralApi()
|
||||
useEffect(() => {
|
||||
setFieldPath(`extra.Placement`)
|
||||
}, [])
|
||||
|
||||
// TODO - DS requirements: add button to select DATASTORE in list => ID="<id>"
|
||||
// TODO - DS policy options: Packing|Stripping
|
||||
return (
|
||||
// TODO - Host requirements: add button to select HOST in list => ID="<id>"
|
||||
// TODO - Host policy options: Packing|Stripping|Load-aware
|
||||
|
||||
<>
|
||||
{SECTIONS(oneConfig, adminGroup).map(({ id, ...section }) => (
|
||||
<FormWithSchema
|
||||
key={id}
|
||||
id={EXTRA_ID}
|
||||
cy={`${EXTRA_ID}-${id}`}
|
||||
{...section}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
// TODO - DS requirements: add button to select DATASTORE in list => ID="<id>"
|
||||
// TODO - DS policy options: Packing|Stripping
|
||||
|
||||
<>
|
||||
{SECTIONS(oneConfig, adminGroup).map(({ id, ...section }) => (
|
||||
<FormWithSchema
|
||||
key={id}
|
||||
id={EXTRA_ID}
|
||||
cy={`${EXTRA_ID}-${id}`}
|
||||
saveState={true}
|
||||
{...section}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Placement.propTypes = {
|
||||
data: PropTypes.any,
|
||||
|
@ -38,7 +38,7 @@ const HOST_RANK_FIELD = {
|
||||
|
||||
/** @type {Field} Datastore requirement field */
|
||||
const DS_REQ_FIELD = {
|
||||
name: 'DS_SCHED_REQUIREMENTS',
|
||||
name: 'SCHED_DS_REQUIREMENTS',
|
||||
label: T.DatastoreReqExpression,
|
||||
tooltip: T.DatastoreReqExpressionConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
@ -47,7 +47,7 @@ const DS_REQ_FIELD = {
|
||||
|
||||
/** @type {Field} Datastore rank requirement field */
|
||||
const DS_RANK_FIELD = {
|
||||
name: 'DS_SCHED_RANK',
|
||||
name: 'SCHED_DS_RANK',
|
||||
label: T.DatastorePolicyExpression,
|
||||
tooltip: T.DatastorePolicyExpressionConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
|
@ -16,6 +16,7 @@
|
||||
import { Stack } from '@mui/material'
|
||||
import { Calendar as ActionIcon } from 'iconoir-react'
|
||||
import { useFieldArray } from 'react-hook-form'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
|
||||
import { ScheduleActionCard } from 'client/components/Cards'
|
||||
import {
|
||||
@ -32,6 +33,7 @@ import {
|
||||
import { mapNameByIndex } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export const TAB_ID = 'SCHED_ACTION'
|
||||
@ -39,6 +41,12 @@ export const TAB_ID = 'SCHED_ACTION'
|
||||
const mapNameFunction = mapNameByIndex('SCHED_ACTION')
|
||||
|
||||
const ScheduleAction = ({ oneConfig, adminGroup }) => {
|
||||
const { setModifiedFields, setFieldPath, initModifiedFields } =
|
||||
useGeneralApi()
|
||||
useEffect(() => {
|
||||
setFieldPath(`extra.ScheduleAction`)
|
||||
initModifiedFields([...scheduleActions.map(() => ({}))])
|
||||
}, [])
|
||||
const {
|
||||
fields: scheduleActions,
|
||||
remove,
|
||||
@ -49,8 +57,23 @@ const ScheduleAction = ({ oneConfig, adminGroup }) => {
|
||||
keyName: 'ID',
|
||||
})
|
||||
|
||||
const totalFieldsCount = useMemo(
|
||||
() => scheduleActions?.length,
|
||||
[scheduleActions]
|
||||
)
|
||||
|
||||
// Delay execution until next event loop tick to ensure state updates
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setFieldPath(`extra.ScheduleAction.${totalFieldsCount}`)
|
||||
}, 0)
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
}, [totalFieldsCount])
|
||||
|
||||
const handleCreateAction = (action) => {
|
||||
append(mapNameFunction(action, scheduleActions.length))
|
||||
setModifiedFields(action)
|
||||
}
|
||||
|
||||
const handleCreateCharter = (actions) => {
|
||||
@ -62,10 +85,12 @@ const ScheduleAction = ({ oneConfig, adminGroup }) => {
|
||||
}
|
||||
|
||||
const handleUpdate = (action, index) => {
|
||||
setModifiedFields(action)
|
||||
update(index, mapNameFunction(action, index))
|
||||
}
|
||||
|
||||
const handleRemove = (index) => {
|
||||
setModifiedFields({ __flag__: 'DELETE' })
|
||||
remove(index)
|
||||
}
|
||||
|
||||
|
@ -48,14 +48,15 @@ const SCHED_ACTION_SCHEMA = object({
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} hypervisor - VM hypervisor
|
||||
* @param {boolean} isUpdate - If it's an update of the form
|
||||
* @returns {ObjectSchema} Extra configuration schema
|
||||
*/
|
||||
export const SCHEMA = (hypervisor) =>
|
||||
export const SCHEMA = (hypervisor, isUpdate) =>
|
||||
object()
|
||||
.concat(SCHED_ACTION_SCHEMA)
|
||||
.concat(NETWORK_SCHEMA)
|
||||
.concat(STORAGE_SCHEMA)
|
||||
.concat(CONTEXT_SCHEMA(hypervisor))
|
||||
.concat(CONTEXT_SCHEMA(hypervisor, isUpdate))
|
||||
.concat(IO_SCHEMA(hypervisor))
|
||||
.concat(
|
||||
getObjectSchemaFromFields([...PLACEMENT_FIELDS, ...OS_FIELDS(hypervisor)])
|
||||
|
@ -17,6 +17,7 @@ import PropTypes from 'prop-types'
|
||||
import { Stack } from '@mui/material'
|
||||
import { Db as DatastoreIcon } from 'iconoir-react'
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import DiskCard from 'client/components/Cards/DiskCard'
|
||||
@ -37,15 +38,24 @@ import {
|
||||
import { FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/storage/schema'
|
||||
import { getDiskName } from 'client/models/Image'
|
||||
import { T } from 'client/constants'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
export const TAB_ID = 'DISK'
|
||||
|
||||
const mapNameFunction = mapNameByIndex('DISK')
|
||||
|
||||
const Storage = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
const { setModifiedFields, setFieldPath, initModifiedFields } =
|
||||
useGeneralApi()
|
||||
|
||||
useEffect(() => {
|
||||
setFieldPath(`extra.Storage`)
|
||||
initModifiedFields([...disks.map(() => ({}))])
|
||||
}, [])
|
||||
|
||||
const { getValues, setValue } = useFormContext()
|
||||
const {
|
||||
fields: disks,
|
||||
fields: disks = [],
|
||||
append,
|
||||
update,
|
||||
replace,
|
||||
@ -53,23 +63,40 @@ const Storage = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
name: `${EXTRA_ID}.${TAB_ID}`,
|
||||
})
|
||||
|
||||
const removeAndReorder = (diskName) => {
|
||||
const totalFieldsCount = useMemo(() => disks?.length, [disks])
|
||||
|
||||
// Delay execution until next event loop tick to ensure state updates
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setFieldPath(`extra.Storage.${totalFieldsCount}`)
|
||||
}, 0)
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
}, [totalFieldsCount])
|
||||
|
||||
const removeAndReorder = (disk) => {
|
||||
/** MARK FOR DELETION */
|
||||
setFieldPath(`extra.Storage.${disk?.DISK_ID}`)
|
||||
setModifiedFields({ __flag__: 'DELETE' })
|
||||
const updatedDisks = disks
|
||||
.filter(({ NAME }) => NAME !== diskName)
|
||||
.filter(({ NAME }) => NAME !== disk?.NAME)
|
||||
.map(mapNameFunction)
|
||||
const currentBootOrder = getValues(BOOT_ORDER_NAME())
|
||||
const updatedBootOrder = reorderBootAfterRemove(
|
||||
diskName,
|
||||
disk?.NAME,
|
||||
disks,
|
||||
currentBootOrder
|
||||
)
|
||||
|
||||
replace(updatedDisks)
|
||||
setValue(BOOT_ORDER_NAME(), updatedBootOrder)
|
||||
setFieldPath(`extra.OsCpu`)
|
||||
setModifiedFields({ OS: { BOOT: true } })
|
||||
}
|
||||
|
||||
const handleUpdate = (updatedDisk, index) => {
|
||||
update(index, mapNameFunction(updatedDisk, index))
|
||||
setFieldPath(`extra.Storage.${totalFieldsCount}`)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -93,7 +120,7 @@ const Storage = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
}}
|
||||
>
|
||||
{disks?.map(({ id, ...item }, index) => {
|
||||
item.DISK_ID ??= index
|
||||
item.DISK_ID = index
|
||||
|
||||
return (
|
||||
<DiskCard
|
||||
@ -106,7 +133,7 @@ const Storage = ({ hypervisor, oneConfig, adminGroup }) => {
|
||||
name={getDiskName(item)}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
onSubmit={() => removeAndReorder(item?.NAME)}
|
||||
onSubmit={() => removeAndReorder(item)}
|
||||
/>
|
||||
<AttachAction
|
||||
disk={item}
|
||||
|
@ -235,11 +235,11 @@ export const MEMORY_RESIZE_MODE_FIELD = {
|
||||
notOnHypervisors: [lxc, firecracker, vcenter],
|
||||
dependOf: ['HYPERVISOR', '$general.HYPERVISOR'],
|
||||
values: arrayToOptions(Object.keys(MEMORY_RESIZE_OPTIONS), {
|
||||
addEmpty: false,
|
||||
addEmpty: true,
|
||||
getText: (option) => option,
|
||||
getValue: (option) => MEMORY_RESIZE_OPTIONS[option],
|
||||
}),
|
||||
validation: string().default(() => MEMORY_RESIZE_OPTIONS[T.Ballooning]),
|
||||
validation: string().default(() => undefined),
|
||||
grid: { md: 6 },
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
|
||||
@ -28,6 +28,7 @@ import {
|
||||
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
|
||||
import { generateKey } from 'client/utils'
|
||||
import { T, RESOURCE_NAMES, VmTemplate } from 'client/constants'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
let generalFeatures
|
||||
|
||||
@ -38,6 +39,11 @@ const Content = ({ isUpdate, oneConfig, adminGroup }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const hypervisor = useWatch({ name: `${STEP_ID}.HYPERVISOR` })
|
||||
|
||||
const { setFieldPath } = useGeneralApi()
|
||||
useEffect(() => {
|
||||
setFieldPath(`general`)
|
||||
}, [])
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.VM_TEMPLATE
|
||||
const { features, dialogs } = getResourceView(resource)
|
||||
@ -64,6 +70,7 @@ const Content = ({ isUpdate, oneConfig, adminGroup }) => {
|
||||
id={STEP_ID}
|
||||
cy={`${STEP_ID}-${id}`}
|
||||
rootProps={{ className: classes[id] }}
|
||||
saveState={true}
|
||||
{...section}
|
||||
/>
|
||||
))}
|
||||
@ -78,11 +85,11 @@ const Content = ({ isUpdate, oneConfig, adminGroup }) => {
|
||||
* @returns {object} General configuration step
|
||||
*/
|
||||
const General = ({
|
||||
dataTemplateExtended: vmTemplate,
|
||||
apiTemplateDataExtended: vmTemplate,
|
||||
oneConfig,
|
||||
adminGroup,
|
||||
}) => {
|
||||
const isUpdate = vmTemplate?.NAME
|
||||
const isUpdate = !!vmTemplate?.NAME
|
||||
const initialHypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
|
||||
|
||||
return {
|
||||
|
@ -25,20 +25,16 @@ import General, {
|
||||
STEP_ID as GENERAL_ID,
|
||||
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/General'
|
||||
|
||||
import { MEMORY_RESIZE_OPTIONS, T } from 'client/constants'
|
||||
import { userInputsToArray } from 'client/models/Helper'
|
||||
import {
|
||||
jsonToXml,
|
||||
userInputsToArray,
|
||||
transformXmlString,
|
||||
} from 'client/models/Helper'
|
||||
import {
|
||||
convertToMB,
|
||||
createSteps,
|
||||
encodeBase64,
|
||||
getUnknownAttributes,
|
||||
isBase64,
|
||||
} from 'client/utils'
|
||||
|
||||
import { KVM_FIRMWARE_TYPES, VCENTER_FIRMWARE_TYPES } from 'client/constants'
|
||||
|
||||
/**
|
||||
* Encodes the start script value to base64 if it is not already encoded.
|
||||
*
|
||||
@ -85,6 +81,19 @@ const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
|
||||
}
|
||||
}
|
||||
|
||||
// cast FIRMWARE
|
||||
const firmware = vmTemplate?.TEMPLATE?.OS?.FIRMWARE
|
||||
if (firmware) {
|
||||
const firmwareOption =
|
||||
KVM_FIRMWARE_TYPES.includes(firmware) ||
|
||||
VCENTER_FIRMWARE_TYPES.includes(firmware)
|
||||
|
||||
objectSchema[EXTRA_ID].OS = {
|
||||
...vmTemplate?.TEMPLATE?.OS,
|
||||
FEATURE_CUSTOM_ENABLED: !firmwareOption ? 'YES' : 'NO',
|
||||
}
|
||||
}
|
||||
|
||||
const knownTemplate = schema.cast(objectSchema, {
|
||||
stripUnknown: true,
|
||||
context: { ...vmTemplate, [EXTRA_ID]: vmTemplate.TEMPLATE },
|
||||
@ -121,54 +130,9 @@ const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
|
||||
|
||||
return knownTemplate
|
||||
},
|
||||
transformBeforeSubmit: (formData) => {
|
||||
const {
|
||||
[GENERAL_ID]: general = {},
|
||||
[CUSTOM_ID]: customVariables = {},
|
||||
[EXTRA_ID]: extraTemplate = {},
|
||||
} = formData ?? {}
|
||||
|
||||
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 && (extraTemplate.CONTEXT[upperName] = `$${upperName}`)
|
||||
})
|
||||
|
||||
if (
|
||||
general?.MEMORY_RESIZE_MODE === MEMORY_RESIZE_OPTIONS[T.Ballooning] &&
|
||||
general?.MEMORY_SLOTS
|
||||
) {
|
||||
delete general.MEMORY_SLOTS
|
||||
}
|
||||
|
||||
// ISSUE#6136: Convert size to MB (because XML API uses only MB) and delete sizeunit field (no needed on XML API)
|
||||
general.MEMORY = convertToMB(general.MEMORY, general.MEMORYUNIT)
|
||||
delete general.MEMORYUNIT
|
||||
|
||||
// cast CPU_MODEL/FEATURES
|
||||
if (Array.isArray(extraTemplate?.CPU_MODEL?.FEATURES)) {
|
||||
extraTemplate.CPU_MODEL.FEATURES =
|
||||
extraTemplate.CPU_MODEL.FEATURES.join(', ')
|
||||
}
|
||||
|
||||
;['NIC', 'NIC_ALIAS'].forEach((nicKey) =>
|
||||
extraTemplate?.[nicKey]?.forEach((NIC) => delete NIC?.NAME)
|
||||
)
|
||||
|
||||
// ISSUE #6418: Raw data is in XML format, so it needs to be transform before sennding it to the API (otherwise the value of RAW.DATA will be treat as part of the XML template)
|
||||
extraTemplate?.RAW?.DATA &&
|
||||
(extraTemplate.RAW.DATA = transformXmlString(extraTemplate.RAW.DATA))
|
||||
|
||||
return jsonToXml({
|
||||
...customVariables,
|
||||
...extraTemplate,
|
||||
...general,
|
||||
})
|
||||
},
|
||||
transformBeforeSubmit: (formData) =>
|
||||
// All formatting and parsing is taken care of in the VmTemplate container
|
||||
formData,
|
||||
})
|
||||
|
||||
export default Steps
|
||||
|
@ -30,7 +30,7 @@ import { useFormContext } from 'react-hook-form'
|
||||
|
||||
let generalFeatures
|
||||
|
||||
export const STEP_ID = 'configuration'
|
||||
export const STEP_ID = 'general'
|
||||
|
||||
const Content = ({ vmTemplate, oneConfig, adminGroup }) => {
|
||||
const classes = useStyles()
|
||||
@ -57,7 +57,7 @@ const Content = ({ vmTemplate, oneConfig, adminGroup }) => {
|
||||
const oldValues = {
|
||||
...getValues(),
|
||||
}
|
||||
oldValues.configuration.CPU = `${scaleVcpuByCpuFactor(
|
||||
oldValues.general.CPU = `${scaleVcpuByCpuFactor(
|
||||
vmTemplate.TEMPLATE.VCPU,
|
||||
features.cpu_factor
|
||||
)}`
|
||||
@ -75,6 +75,7 @@ const Content = ({ vmTemplate, oneConfig, adminGroup }) => {
|
||||
rootProps={{ className: classes[id] }}
|
||||
fields={fields}
|
||||
legend={legend}
|
||||
saveState={true}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
))}
|
||||
|
@ -30,6 +30,7 @@ const Content = ({ userInputs }) => (
|
||||
cy="user-inputs"
|
||||
id={STEP_ID}
|
||||
fields={useMemo(() => FIELDS(userInputs), [])}
|
||||
saveState={true}
|
||||
/>
|
||||
)
|
||||
|
||||
|
@ -19,12 +19,9 @@ import BasicConfiguration, {
|
||||
import ExtraConfiguration, {
|
||||
STEP_ID as EXTRA_ID,
|
||||
} from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import UserInputs, {
|
||||
STEP_ID as USER_INPUTS_ID,
|
||||
} from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/UserInputs'
|
||||
import UserInputs from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/UserInputs'
|
||||
import {
|
||||
getUserInputParams,
|
||||
jsonToXml,
|
||||
parseRangeToArray,
|
||||
userInputsToArray,
|
||||
} from 'client/models/Helper'
|
||||
@ -81,36 +78,17 @@ const Steps = createSteps(
|
||||
{ stripUnknown: true }
|
||||
)
|
||||
},
|
||||
transformBeforeSubmit: (formData, vmTemplate, _, adminGroup, oneConfig) => {
|
||||
const {
|
||||
[BASIC_ID]: { name, instances, hold, persistent, ...restOfConfig } = {},
|
||||
[USER_INPUTS_ID]: userInputs,
|
||||
[EXTRA_ID]: extraTemplate = {},
|
||||
} = formData ?? {}
|
||||
|
||||
vmTemplate?.TEMPLATE?.OS &&
|
||||
extraTemplate?.OS &&
|
||||
(extraTemplate.OS = {
|
||||
...vmTemplate?.TEMPLATE?.OS,
|
||||
...extraTemplate?.OS,
|
||||
})
|
||||
;['NIC', 'NIC_ALIAS'].forEach((nicKey) =>
|
||||
extraTemplate?.[nicKey]?.forEach((NIC) => delete NIC?.NAME)
|
||||
)
|
||||
|
||||
// merge with template disks to get TYPE attribute
|
||||
const templateXML = jsonToXml({
|
||||
...userInputs,
|
||||
...extraTemplate,
|
||||
...restOfConfig,
|
||||
})
|
||||
|
||||
const data = { instances, hold, persistent, template: templateXML }
|
||||
transformBeforeSubmit: (formData, vmTemplate) => {
|
||||
const { [BASIC_ID]: { name, instances, hold, persistent } = {} } =
|
||||
formData ?? {}
|
||||
|
||||
const templates = [...new Array(instances)].map((__, idx) => ({
|
||||
id: vmTemplate.ID,
|
||||
name: name?.replace(/%idx/gi, idx),
|
||||
...data,
|
||||
instances: instances,
|
||||
hold: hold,
|
||||
persistent: persistent,
|
||||
...formData,
|
||||
}))
|
||||
|
||||
return templates
|
||||
|
@ -109,6 +109,14 @@ const EnhancedTable = ({
|
||||
|
||||
return updatedState
|
||||
}
|
||||
case 'toggleAllRowsSelected': {
|
||||
// If the action is to deselect all the rows, the selectRowIds has to be an empory objet
|
||||
if (singleSelect && !action.value) {
|
||||
newState.selectedRowIds = {}
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
||||
default:
|
||||
return newState
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import {
|
||||
} from 'client/components/Tabs/Common/Attribute'
|
||||
import AttributeCreateForm from 'client/components/Tabs/Common/AttributeCreateForm'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { camelCase } from 'client/utils'
|
||||
|
||||
const Title = styled(ListItem)(({ theme }) => ({
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
@ -92,6 +93,7 @@ const AttributeList = ({
|
||||
>
|
||||
<Attribute
|
||||
path={parentPath || name}
|
||||
dataCy={'attribute-' + camelCase(name)}
|
||||
{...attribute}
|
||||
{...(isParent && { canEdit: false, value: undefined })}
|
||||
/>
|
||||
|
@ -48,7 +48,15 @@ const AttachAction = memo(
|
||||
sx,
|
||||
oneConfig,
|
||||
adminGroup,
|
||||
indexNic,
|
||||
indexAlias,
|
||||
indexPci,
|
||||
hasAlias,
|
||||
isPci,
|
||||
isAlias,
|
||||
}) => {
|
||||
const { setFieldPath } = useGeneralApi()
|
||||
|
||||
const [attachNic] = useAttachNicMutation()
|
||||
|
||||
const handleAttachNic = async (formData) => {
|
||||
@ -56,7 +64,6 @@ const AttachAction = memo(
|
||||
return await onSubmit(formData)
|
||||
}
|
||||
|
||||
const isAlias = !!formData?.PARENT?.length
|
||||
const key = isAlias ? 'NIC_ALIAS' : 'NIC'
|
||||
const data = { [key]: formData }
|
||||
|
||||
@ -85,17 +92,34 @@ const AttachAction = memo(
|
||||
options={[
|
||||
{
|
||||
dialogProps: { title: T.AttachNic, dataCy: 'modal-attach-nic' },
|
||||
form: () =>
|
||||
AttachNicForm({
|
||||
form: () => {
|
||||
// Set field path
|
||||
if (nic) {
|
||||
if (isPci) {
|
||||
setFieldPath(`extra.InputOutput.PCI.${indexPci}`)
|
||||
} else if (isAlias) {
|
||||
setFieldPath(`extra.Network.NIC_ALIAS.${indexAlias}`)
|
||||
} else {
|
||||
setFieldPath(`extra.Network.NIC.${indexNic}`)
|
||||
}
|
||||
} else {
|
||||
setFieldPath(`extra.Network.NIC.${currentNics.length}`)
|
||||
}
|
||||
|
||||
return AttachNicForm({
|
||||
stepProps: {
|
||||
hypervisor,
|
||||
nics: currentNics,
|
||||
defaultData: nic,
|
||||
oneConfig,
|
||||
adminGroup,
|
||||
hasAlias,
|
||||
isPci,
|
||||
isAlias,
|
||||
},
|
||||
initialValues: nic,
|
||||
}),
|
||||
})
|
||||
},
|
||||
onSubmit: handleAttachNic,
|
||||
},
|
||||
]}
|
||||
@ -273,6 +297,12 @@ const ActionPropTypes = {
|
||||
sx: PropTypes.object,
|
||||
oneConfig: PropTypes.object,
|
||||
adminGroup: PropTypes.bool,
|
||||
indexNic: PropTypes.string,
|
||||
indexAlias: PropTypes.string,
|
||||
indexPci: PropTypes.string,
|
||||
hasAlias: PropTypes.bool,
|
||||
isPci: PropTypes.bool,
|
||||
isAlias: PropTypes.bool,
|
||||
}
|
||||
|
||||
AttachAction.propTypes = ActionPropTypes
|
||||
|
@ -46,8 +46,6 @@ import { jsonToXml } from 'client/models/Helper'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
import { T, VM_ACTIONS } from 'client/constants'
|
||||
|
||||
import { hasRestrictedAttributes } from 'client/utils'
|
||||
|
||||
const AttachAction = memo(
|
||||
({ vmId, disk, hypervisor, onSubmit, sx, oneConfig, adminGroup }) => {
|
||||
const [attachDisk] = useAttachDiskMutation()
|
||||
@ -91,6 +89,7 @@ const AttachAction = memo(
|
||||
title: (
|
||||
<Translate word={T.EditSomething} values={[disk?.NAME]} />
|
||||
),
|
||||
dataCy: 'modal-edit-disk',
|
||||
},
|
||||
form: () =>
|
||||
!disk?.IMAGE && !disk?.IMAGE_ID // is volatile
|
||||
@ -137,14 +136,7 @@ const DetachAction = memo(
|
||||
await handleDetachDisk({ id: vmId, disk: DISK_ID })
|
||||
}
|
||||
|
||||
// Disable action if the disk has a restricted attribute on the template
|
||||
const disabledAction =
|
||||
!adminGroup &&
|
||||
hasRestrictedAttributes(
|
||||
disk.ORIGINAL,
|
||||
'DISK',
|
||||
oneConfig?.VM_RESTRICTED_ATTR
|
||||
)
|
||||
const disabledAction = !adminGroup
|
||||
|
||||
return (
|
||||
<ButtonToTriggerForm
|
||||
|
@ -963,6 +963,14 @@ module.exports = {
|
||||
Final users may not be aware of this`,
|
||||
DefaultNicModel: 'Default hardware model to emulate for all NICs',
|
||||
DefaultNicFilter: 'Default network filtering rule for all NICs',
|
||||
Ipv4Static: 'Static (Based on context)',
|
||||
Ipv4Dhcp: 'DHCP (DHCPv4)',
|
||||
Ipv4Skip: 'Skip (Do not configure IPv4)',
|
||||
Ipv6Static: 'Static (Based on context)',
|
||||
Ipv6Auto: 'Auto (SLAAC)',
|
||||
Ipv6Dhcp: 'DHCP (SLAAC and DHCPv6)',
|
||||
Ipv6Disable: 'Disable (Do not use IPv6)',
|
||||
Ipv6Skip: 'Skip (Do not configure IPv6)',
|
||||
/* VM Template schema - capacity */
|
||||
MaxMemory: 'Max memory',
|
||||
MaxMemoryConcept: `
|
||||
@ -1157,8 +1165,14 @@ module.exports = {
|
||||
Profile: 'Profile',
|
||||
DeviceName: 'Device name',
|
||||
Device: 'Device',
|
||||
DeviceTooltip:
|
||||
'Select one device of the Device name list to complete this field please.',
|
||||
Vendor: 'Vendor',
|
||||
VendorTooltip:
|
||||
'Select one vendor of the Device name list to complete this field please.',
|
||||
Class: 'Class',
|
||||
ClassTooltip:
|
||||
'Select one class of the Device name list to complete this field please.',
|
||||
Video: 'Video',
|
||||
VideoType: 'Video device type',
|
||||
VideoTypeConcept:
|
||||
|
@ -133,7 +133,7 @@ export const TEMPLATE_LOGOS = {
|
||||
/** @enum {string} FS freeze options type */
|
||||
export const FS_FREEZE_OPTIONS = {
|
||||
[T.None]: 'NONE',
|
||||
[T.QEMUAgent]: 'QEMU-AGENT',
|
||||
[T.QEMUAgent]: 'AGENT',
|
||||
[T.Suspend]: 'SUSPEND',
|
||||
}
|
||||
|
||||
@ -174,3 +174,36 @@ export const MEMORY_RESIZE_OPTIONS = {
|
||||
[T.Ballooning]: 'BALLOONING',
|
||||
[T.Hotplug]: 'HOTPLUG',
|
||||
}
|
||||
|
||||
export const TAB_FORM_MAP = {
|
||||
Storage: ['DISK', 'TM_MAD_SYSTEM'],
|
||||
Network: ['NIC', 'NIC_ALIAS', 'PCI', 'NIC_DEFAULT'],
|
||||
OsCpu: ['OS', 'CPU_MODEL', 'FEATURES', 'RAW'],
|
||||
InputOutput: ['INPUT', 'GRAPHICS', 'VIDEO', 'PCI'],
|
||||
Context: ['CONTEXT', 'USER_INPUTS', 'INPUTS_ORDER'],
|
||||
ScheduleAction: ['SCHED_ACTION'],
|
||||
Placement: [
|
||||
'SCHED_DS_RANK',
|
||||
'SCHED_DS_REQUIREMENTS',
|
||||
'SCHED_RANK',
|
||||
'SCHED_REQUIREMENTS',
|
||||
],
|
||||
NUMA: ['TOPOLOGY'],
|
||||
Backup: ['BACKUP_CONFIG'],
|
||||
}
|
||||
|
||||
/** @enum {string} Methods on IP v4 options type */
|
||||
export const IPV4_METHODS = {
|
||||
[T.Ipv4Static]: 'static',
|
||||
[T.Ipv4Dhcp]: 'dhcp',
|
||||
[T.Ipv4Skip]: 'skip',
|
||||
}
|
||||
|
||||
/** @enum {string} Methods on IP v6 options type */
|
||||
export const IPV6_METHODS = {
|
||||
[T.Ipv4Static]: 'static',
|
||||
[T.Ipv6Auto]: 'auto',
|
||||
[T.Ipv4Dhcp]: 'dhcp',
|
||||
[T.Ipv6Disable]: 'disable',
|
||||
[T.Ipv4Skip]: 'skip',
|
||||
}
|
||||
|
@ -13,7 +13,8 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement } from 'react'
|
||||
import { ReactElement, useEffect } from 'react'
|
||||
import { useStore } from 'react-redux'
|
||||
import { useHistory, useLocation } from 'react-router'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
@ -35,16 +36,12 @@ import {
|
||||
import { CreateForm } from 'client/components/Forms/VmTemplate'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
|
||||
import { jsonToXml, xmlToJson } from 'client/models/Helper'
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { filterTemplateData, transformActionsCreate } from 'client/utils/parser'
|
||||
import { TAB_FORM_MAP } from 'client/constants'
|
||||
|
||||
import { useSystemData } from 'client/features/Auth'
|
||||
|
||||
import {
|
||||
addTempInfo,
|
||||
deleteTempInfo,
|
||||
deleteRestrictedAttributes,
|
||||
} from 'client/utils'
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
@ -53,10 +50,19 @@ const _ = require('lodash')
|
||||
* @returns {ReactElement} VM Template form
|
||||
*/
|
||||
function CreateVmTemplate() {
|
||||
// Reset modified fields + path on mount
|
||||
useEffect(() => {
|
||||
resetFieldPath()
|
||||
resetModifiedFields()
|
||||
}, [])
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const history = useHistory()
|
||||
const { state: { ID: templateId, NAME } = {} } = useLocation()
|
||||
|
||||
const { enqueueSuccess } = useGeneralApi()
|
||||
const { enqueueSuccess, resetFieldPath, resetModifiedFields } =
|
||||
useGeneralApi()
|
||||
const [update] = useUpdateTemplateMutation()
|
||||
const [allocate] = useAllocateTemplateMutation()
|
||||
|
||||
@ -72,70 +78,43 @@ function CreateVmTemplate() {
|
||||
{ skip: templateId === undefined }
|
||||
)
|
||||
|
||||
const dataTemplateExtended = _.cloneDeep(apiTemplateDataExtended)
|
||||
const dataTemplate = _.cloneDeep(apiTemplateData)
|
||||
|
||||
// #6154: Add an unique identifier to compare on submit items that exists at the beginning of the update
|
||||
if (!adminGroup) addTempInfo(dataTemplate, dataTemplateExtended)
|
||||
|
||||
useGetVMGroupsQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetHostsQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetImagesQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetUsersQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetDatastoresQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
|
||||
const onSubmit = async (xmlTemplate) => {
|
||||
const onSubmit = async (rawTemplate) => {
|
||||
try {
|
||||
const currentState = store.getState()
|
||||
const modifiedFields = currentState.general?.modifiedFields
|
||||
|
||||
const existingTemplate = {
|
||||
...apiTemplateData?.TEMPLATE,
|
||||
}
|
||||
const filteredTemplate = filterTemplateData(
|
||||
rawTemplate,
|
||||
modifiedFields,
|
||||
existingTemplate,
|
||||
TAB_FORM_MAP
|
||||
)
|
||||
|
||||
// Every action that is not an human action
|
||||
transformActionsCreate(filteredTemplate)
|
||||
|
||||
const xmlTemplate = jsonToXml(filteredTemplate)
|
||||
if (!templateId) {
|
||||
const newTemplateId = await allocate({ template: xmlTemplate }).unwrap()
|
||||
const newTemplateId = await allocate({
|
||||
template: xmlTemplate,
|
||||
}).unwrap()
|
||||
resetFieldPath()
|
||||
resetModifiedFields()
|
||||
history.push(PATH.TEMPLATE.VMS.LIST)
|
||||
enqueueSuccess(`VM Template created - #${newTemplateId}`)
|
||||
} else {
|
||||
/**
|
||||
* #6154: Consideration about resolving this issue:
|
||||
*
|
||||
* When the user is a non admin user, we have to delete the restricted attributes of the DISK to avoid errors.
|
||||
*
|
||||
* Core behaviour: The core will fail if in the template there is a restricted attribute that has a different value before modify it.
|
||||
* EXAMPLES:
|
||||
* - If you add a restricted attribute on the template
|
||||
* - If you remove a restricted attribute on the template
|
||||
* - If you modify the value of a restricted attribute on the template
|
||||
*
|
||||
* Core will not fail if you send a restricted attribute in the template without modify him.
|
||||
* EXAMPLES:
|
||||
* - If your template has a restricted attribute with value 1024 and you send the same attribute with 1024 value, core will not fail
|
||||
*
|
||||
* Fireedge Sunstone behaviour: The app has a different behaviour between the DISK attribute of a template and another attributes like NIC, CONTEXT, GRAPHICS,... The sequence when you're updating a template is the following:
|
||||
* 1. Get the info of the template from the core with extended value false. That returns only what the template has on the core.
|
||||
* 2. Get the info of the template from the core with extended value true. That returns the info of the template plus the info of the disk (only disk, not NIC or another attributes).
|
||||
* 3. When the template is update, DISK could have some attributes that are not in the template, so this could cause a failure.
|
||||
*
|
||||
* To resolve the issue we delete restricted attributes if there are not in template when the user is non admin . This can be done because the user can modify the restricted attributes (as part of this issue, the schemas has a read only attribute if the field is restricted)
|
||||
*
|
||||
* We delete this info onto onSubmit function becasue we need to get the original tempalte without modify. So there is need a hook that we can't do on tranformBeforeSubmit.
|
||||
* Also, the data that is an input parameter of the CreateForm is the data with extended values, so it's no possible to to that using initualValues on transformBeforeSubmit.
|
||||
*
|
||||
*/
|
||||
|
||||
// #6154: Delete restricted attributes (if there are not on the original template)
|
||||
const jsonFinal = adminGroup
|
||||
? xmlToJson(xmlTemplate)
|
||||
: deleteRestrictedAttributes(
|
||||
xmlToJson(xmlTemplate),
|
||||
dataTemplate?.TEMPLATE,
|
||||
oneConfig?.VM_RESTRICTED_ATTR
|
||||
)
|
||||
|
||||
// #6154: Delete unique identifier to compare on submit items that exists at the beginning of the update
|
||||
if (!adminGroup) {
|
||||
deleteTempInfo(jsonFinal)
|
||||
}
|
||||
|
||||
// Transform json to xml
|
||||
const xmlFinal = jsonToXml(jsonFinal)
|
||||
|
||||
await update({ id: templateId, template: xmlFinal }).unwrap()
|
||||
await update({ id: templateId, template: xmlTemplate }).unwrap()
|
||||
resetFieldPath()
|
||||
resetModifiedFields()
|
||||
history.push(PATH.TEMPLATE.VMS.LIST)
|
||||
enqueueSuccess(`VM Template updated - #${templateId} ${NAME}`)
|
||||
}
|
||||
@ -143,13 +122,13 @@ function CreateVmTemplate() {
|
||||
}
|
||||
|
||||
return templateId &&
|
||||
(!dataTemplateExtended || !dataTemplate || _.isEmpty(oneConfig)) ? (
|
||||
(!apiTemplateDataExtended || !apiTemplateData || _.isEmpty(oneConfig)) ? (
|
||||
<SkeletonStepsForm />
|
||||
) : (
|
||||
<CreateForm
|
||||
initialValues={dataTemplateExtended}
|
||||
initialValues={apiTemplateDataExtended}
|
||||
stepProps={{
|
||||
dataTemplateExtended,
|
||||
apiTemplateDataExtended,
|
||||
oneConfig,
|
||||
adminGroup,
|
||||
}}
|
||||
|
@ -14,6 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement } from 'react'
|
||||
import { useStore } from 'react-redux'
|
||||
import { Redirect, useHistory, useLocation } from 'react-router'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
@ -32,12 +33,12 @@ import {
|
||||
import { InstantiateForm } from 'client/components/Forms/VmTemplate'
|
||||
|
||||
import { useSystemData } from 'client/features/Auth'
|
||||
import { jsonToXml, xmlToJson } from 'client/models/Helper'
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import {
|
||||
addTempInfo,
|
||||
deleteRestrictedAttributes,
|
||||
deleteTempInfo,
|
||||
} from 'client/utils'
|
||||
filterTemplateData,
|
||||
transformActionsInstantiate,
|
||||
} from 'client/utils/parser'
|
||||
import { TAB_FORM_MAP } from 'client/constants'
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
@ -47,10 +48,11 @@ const _ = require('lodash')
|
||||
* @returns {ReactElement} Instantiation form
|
||||
*/
|
||||
function InstantiateVmTemplate() {
|
||||
const store = useStore()
|
||||
const history = useHistory()
|
||||
const { state: { ID: templateId, NAME: templateName } = {} } = useLocation()
|
||||
|
||||
const { enqueueInfo } = useGeneralApi()
|
||||
const { enqueueInfo, resetFieldPath, resetModifiedFields } = useGeneralApi()
|
||||
const [instantiate] = useInstantiateTemplateMutation()
|
||||
|
||||
const { adminGroup, oneConfig } = useSystemData()
|
||||
@ -66,44 +68,42 @@ function InstantiateVmTemplate() {
|
||||
)
|
||||
|
||||
const dataTemplateExtended = _.cloneDeep(apiTemplateDataExtended)
|
||||
const dataTemplate = _.cloneDeep(apiTemplateData)
|
||||
|
||||
// #6154: Add an unique identifier to compare on submit items that exists at the beginning of the update
|
||||
if (!adminGroup) addTempInfo(dataTemplate, dataTemplateExtended)
|
||||
|
||||
useGetUsersQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetGroupsQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
|
||||
const onSubmit = async (templates) => {
|
||||
try {
|
||||
const currentState = store.getState()
|
||||
const modifiedFields = currentState.general?.modifiedFields
|
||||
await Promise.all(
|
||||
templates.map((t) => {
|
||||
// #6154: Consideration about resolving this issue -> Read comment on src/client/containers/VmTemplates/Create.js
|
||||
|
||||
// #6154: Delete restricted attributes (if there are not on the original template)
|
||||
const jsonFinal = adminGroup
|
||||
? xmlToJson(t.template)
|
||||
: deleteRestrictedAttributes(
|
||||
xmlToJson(t?.template),
|
||||
dataTemplate?.TEMPLATE,
|
||||
oneConfig?.VM_RESTRICTED_ATTR
|
||||
)
|
||||
|
||||
// #6154: Delete unique identifier to compare on submit items that exists at the beginning of the update
|
||||
if (!adminGroup) {
|
||||
deleteTempInfo(jsonFinal)
|
||||
templates.map((rawTemplate) => {
|
||||
const existingTemplate = {
|
||||
...apiTemplateData?.TEMPLATE,
|
||||
}
|
||||
|
||||
// Transform json to xml
|
||||
const xmlFinal = jsonToXml(jsonFinal)
|
||||
const filteredTemplate = filterTemplateData(
|
||||
rawTemplate,
|
||||
modifiedFields,
|
||||
existingTemplate,
|
||||
TAB_FORM_MAP
|
||||
)
|
||||
|
||||
// Every action that is not an human action
|
||||
transformActionsInstantiate(filteredTemplate, apiTemplateData)
|
||||
|
||||
const xmlFinal = jsonToXml(filteredTemplate)
|
||||
|
||||
// Modify template
|
||||
t.template = xmlFinal
|
||||
rawTemplate.template = xmlFinal
|
||||
|
||||
return instantiate(t).unwrap()
|
||||
return instantiate(rawTemplate).unwrap()
|
||||
})
|
||||
)
|
||||
|
||||
resetFieldPath()
|
||||
resetModifiedFields()
|
||||
|
||||
history.push(PATH.INSTANCE.VMS.LIST)
|
||||
|
||||
const total = templates.length
|
||||
@ -116,7 +116,7 @@ function InstantiateVmTemplate() {
|
||||
return <Redirect to={PATH.TEMPLATE.VMS.LIST} />
|
||||
}
|
||||
|
||||
return !dataTemplateExtended || !dataTemplate || _.isEmpty(oneConfig) ? (
|
||||
return !dataTemplateExtended || !apiTemplateData || _.isEmpty(oneConfig) ? (
|
||||
<SkeletonStepsForm />
|
||||
) : (
|
||||
<InstantiateForm
|
||||
|
@ -26,6 +26,20 @@ export const updateDisabledSteps = createAction('Set disabled steps')
|
||||
export const dismissSnackbar = createAction('Dismiss snackbar')
|
||||
export const deleteSnackbar = createAction('Delete snackbar')
|
||||
export const setUploadSnackbar = createAction('Change upload snackbar')
|
||||
export const setFieldPath = createAction('Set dynamic field path')
|
||||
export const resetFieldPath = createAction('Reset field path')
|
||||
export const initModifiedFields = createAction('Init modified fields')
|
||||
export const changePositionModifiedFields = createAction(
|
||||
'Change position of two array elements in modified fields'
|
||||
)
|
||||
export const setModifiedFields = createAction(
|
||||
'Set modified fields',
|
||||
(fields, options = {}) => ({
|
||||
payload: fields,
|
||||
meta: { batch: options.batch || false },
|
||||
})
|
||||
)
|
||||
export const resetModifiedFields = createAction('Reset modified fields')
|
||||
|
||||
export const enqueueSnackbar = createAction(
|
||||
'Enqueue snackbar',
|
||||
|
@ -35,6 +35,18 @@ export const useGeneralApi = () => {
|
||||
setUpdateDialog: (updateDialog) =>
|
||||
dispatch(actions.setUpdateDialog(updateDialog)),
|
||||
|
||||
// modified fields
|
||||
setFieldPath: (path) => dispatch(actions.setFieldPath(path)),
|
||||
resetFieldPath: () => dispatch(actions.resetFieldPath()),
|
||||
initModifiedFields: (fields) =>
|
||||
dispatch(actions.initModifiedFields(fields)),
|
||||
changePositionModifiedFields: (fields) =>
|
||||
dispatch(actions.changePositionModifiedFields(fields)),
|
||||
setModifiedFields: (fields, options = {}) => {
|
||||
dispatch(actions.setModifiedFields(fields, options))
|
||||
},
|
||||
resetModifiedFields: () => dispatch(actions.resetModifiedFields()),
|
||||
|
||||
// dismiss all if no key has been defined
|
||||
dismissSnackbar: (key) =>
|
||||
dispatch(actions.dismissSnackbar({ key, dismissAll: !key })),
|
||||
|
@ -18,7 +18,9 @@ import { createSlice } from '@reduxjs/toolkit'
|
||||
import { APPS_IN_BETA, APPS_WITH_SWITCHER } from 'client/constants'
|
||||
import { logout } from 'client/features/Auth/slice'
|
||||
import * as actions from 'client/features/General/actions'
|
||||
import { generateKey } from 'client/utils'
|
||||
import { generateKey, calculateIndex } from 'client/utils'
|
||||
import { merge, cloneDeep, set, get, pullAt, pickBy } from 'lodash'
|
||||
import { parsePayload } from 'client/utils/parser'
|
||||
|
||||
const initial = {
|
||||
zone: 0,
|
||||
@ -32,6 +34,8 @@ const initial = {
|
||||
notifications: [],
|
||||
selectedIds: [],
|
||||
disabledSteps: [],
|
||||
fieldPath: '',
|
||||
modifiedFields: {},
|
||||
}
|
||||
|
||||
const slice = createSlice({
|
||||
@ -78,6 +82,144 @@ const slice = createSlice({
|
||||
state.isUpdateDialog = !!payload
|
||||
})
|
||||
|
||||
/* FIELD MODIFICATIONS */
|
||||
.addCase(actions.setFieldPath, (state, { payload }) => {
|
||||
state.fieldPath = payload
|
||||
})
|
||||
.addCase(actions.resetFieldPath, (state) => {
|
||||
state.fieldPath = ''
|
||||
})
|
||||
.addCase(actions.initModifiedFields, (state, { payload }) => {
|
||||
// Get field path and check if there is something in this path
|
||||
const fieldPath = state?.fieldPath || ''
|
||||
const exists = get(state.modifiedFields, fieldPath, false)
|
||||
|
||||
// If the path has content, do nothing
|
||||
if (!exists) {
|
||||
set(state.modifiedFields, fieldPath, payload)
|
||||
}
|
||||
})
|
||||
.addCase(actions.changePositionModifiedFields, (state, { payload }) => {
|
||||
// Get from payload the path to the arrays and the number of the element
|
||||
const sourcePath = payload?.sourcePath
|
||||
const sourcePosition = payload?.sourcePosition
|
||||
const targetPath = payload?.targetPath
|
||||
const sourceDelete = payload?.sourceDelete
|
||||
const emptyObjectContent = payload?.emptyObjectContent
|
||||
const targetPosition = payload?.targetPosition
|
||||
|
||||
// Get source element and change to target array and delete on source array
|
||||
const sourceArray = get(state.modifiedFields, sourcePath)
|
||||
const targetArray = get(state.modifiedFields, targetPath)
|
||||
|
||||
const sourceIndex = calculateIndex(sourceArray, sourcePosition)
|
||||
const targetIndex = calculateIndex(targetArray, targetPosition)
|
||||
|
||||
const sourceFinalPosition =
|
||||
sourceIndex === -1 ? sourceArray.length : sourceIndex
|
||||
const targetFinalPosition =
|
||||
targetIndex === -1 ? targetArray.length : targetIndex
|
||||
|
||||
targetArray[targetFinalPosition] = sourceArray[sourceFinalPosition]
|
||||
|
||||
sourceDelete && pullAt(sourceArray, sourceFinalPosition)
|
||||
emptyObjectContent &&
|
||||
(sourceArray[sourceFinalPosition] = pickBy(
|
||||
sourceArray[sourceFinalPosition],
|
||||
(value, key) => key.startsWith('__')
|
||||
))
|
||||
|
||||
set(state.modifiedFields, sourcePath, sourceArray)
|
||||
set(state.modifiedFields, targetPath, targetArray)
|
||||
})
|
||||
.addCase(actions.setModifiedFields, (state, { payload, meta }) => {
|
||||
// Get field path
|
||||
const fieldPath = state?.fieldPath || ''
|
||||
|
||||
// Removes references
|
||||
const mergedPayload = cloneDeep(payload)
|
||||
|
||||
if (fieldPath.length) {
|
||||
const pathSegments = fieldPath.split('.')
|
||||
const lastSegment = pathSegments.slice(-1)[0]
|
||||
const isArrayIndex = /^\d+$/.test(lastSegment)
|
||||
|
||||
if (isArrayIndex) {
|
||||
const pathWithoutIndex = pathSegments.slice(0, -1).join('.')
|
||||
|
||||
const arrayAtPath = get(state.modifiedFields, pathWithoutIndex, [])
|
||||
const index = parseInt(lastSegment, 10)
|
||||
while (arrayAtPath.length <= index) {
|
||||
arrayAtPath.push({})
|
||||
}
|
||||
if (payload?.__flag__ === 'DELETE') {
|
||||
// Filter the array to mark the correct position with delete
|
||||
arrayAtPath.filter((item) => !item.__delete__)[
|
||||
index
|
||||
].__delete__ = true
|
||||
} else {
|
||||
// In array cases, we need to calculate the correction index (could exists elements that were deleted)
|
||||
const arrayWithoutDeleteItems = arrayAtPath.filter(
|
||||
(item) => !item.__delete__
|
||||
)
|
||||
const arrayWithoutDeleteItemsLength =
|
||||
arrayWithoutDeleteItems.length
|
||||
|
||||
// Update original item
|
||||
if (index < arrayWithoutDeleteItemsLength) {
|
||||
arrayWithoutDeleteItems[index] = merge(
|
||||
arrayWithoutDeleteItems[index] || {},
|
||||
mergedPayload
|
||||
)
|
||||
} else {
|
||||
// Create new item
|
||||
arrayAtPath[arrayAtPath.length] = mergedPayload
|
||||
}
|
||||
}
|
||||
|
||||
set(state.modifiedFields, pathWithoutIndex, arrayAtPath)
|
||||
} else {
|
||||
const nested = pathSegments?.length > 2
|
||||
const nestedBasePath = nested
|
||||
? pathSegments.slice(0, -1).join('.')
|
||||
: pathSegments.join('.')
|
||||
const nestedKey = pathSegments.slice(-1)[0]
|
||||
|
||||
let existingNestedValue = get(
|
||||
state.modifiedFields,
|
||||
nestedBasePath,
|
||||
{}
|
||||
)
|
||||
|
||||
if (nested) {
|
||||
existingNestedValue[nestedKey] = merge(
|
||||
{},
|
||||
existingNestedValue || {},
|
||||
get(mergedPayload, pathSegments[0], {})
|
||||
)
|
||||
} else {
|
||||
existingNestedValue = merge(
|
||||
{},
|
||||
existingNestedValue || {},
|
||||
get(mergedPayload, pathSegments[0], {})
|
||||
)
|
||||
}
|
||||
|
||||
const parsedExisting = parsePayload(existingNestedValue, fieldPath)
|
||||
set(state.modifiedFields, nestedBasePath, parsedExisting)
|
||||
}
|
||||
} else {
|
||||
state.modifiedFields = merge({}, state.modifiedFields, mergedPayload)
|
||||
}
|
||||
|
||||
if (payload?.setPath) {
|
||||
state.fieldPath = payload.setPath
|
||||
}
|
||||
})
|
||||
.addCase(actions.resetModifiedFields, (state) => {
|
||||
state.modifiedFields = {}
|
||||
})
|
||||
|
||||
/* UPLOAD NOTIFICATION */
|
||||
.addCase(actions.setUploadSnackbar, (state, { payload }) => ({
|
||||
...state,
|
||||
|
@ -13,7 +13,8 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useCallback, useState, SetStateAction } from 'react'
|
||||
import { useCallback, useState, SetStateAction, useRef, useEffect } from 'react'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { set } from 'client/utils'
|
||||
|
||||
@ -94,6 +95,8 @@ const defaultAddItemId = (item, id) => ({ ...item, id })
|
||||
* @param {object} props.defaultValue - Default value of element
|
||||
* @param {function(object, number):string|number} props.getItemId - Function to change how detects unique item
|
||||
* @param {function(object, string|number, number):object} props.addItemId - Function to add ID
|
||||
* @param {object} props.modifiedFields - Array of field names to set as modified on select/unselect
|
||||
* @param {string} props.fieldKey - Key under which to save modified fields.
|
||||
* @example <caption>Example usage.</caption>
|
||||
* // const INITIAL_STATE = { listKey: [{ id: 'item1' }, { id: 'item2' }] }
|
||||
* // const [formData, setFormData] = useState(INITIAL_STATE)
|
||||
@ -115,7 +118,12 @@ const useListForm = ({
|
||||
defaultValue,
|
||||
getItemId = defaultGetItemId,
|
||||
addItemId = defaultAddItemId,
|
||||
modifiedFields,
|
||||
fieldKey,
|
||||
}) => {
|
||||
const { setModifiedFields } = useGeneralApi()
|
||||
const selectedRef = useRef(false)
|
||||
|
||||
const [editingData, setEditingData] = useState(() => undefined)
|
||||
|
||||
const getIndexById = useCallback(
|
||||
@ -135,15 +143,13 @@ const useListForm = ({
|
||||
[key, parent, setList]
|
||||
)
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(id) => {
|
||||
setList((prevList) => ({
|
||||
...prevList,
|
||||
[key]: multiple ? [...(prevList[key] ?? []), id] : [id],
|
||||
}))
|
||||
},
|
||||
[key, setList, multiple]
|
||||
)
|
||||
const handleSelect = useCallback((id) => {
|
||||
setList((prevList) => ({
|
||||
...prevList,
|
||||
[key]: multiple ? [...(prevList[key] ?? []), id] : [id],
|
||||
}))
|
||||
selectedRef.current = true
|
||||
})
|
||||
|
||||
const handleUnselect = useCallback(
|
||||
(id) => {
|
||||
@ -152,6 +158,7 @@ const useListForm = ({
|
||||
)
|
||||
|
||||
handleSetList(newList)
|
||||
selectedRef.current = false
|
||||
},
|
||||
[list, setList]
|
||||
)
|
||||
@ -199,6 +206,24 @@ const useListForm = ({
|
||||
[list, defaultValue]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (selectedRef?.current && !!modifiedFields?.length) {
|
||||
const mergedFields = modifiedFields.reduce((acc, field) => {
|
||||
if (fieldKey) {
|
||||
acc[fieldKey] = { ...acc[fieldKey], [field]: true }
|
||||
} else {
|
||||
acc[field] = true
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
setModifiedFields(mergedFields, { batch: true })
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return {
|
||||
list,
|
||||
editingData,
|
||||
|
@ -522,10 +522,8 @@ export const userInputsToArray = (
|
||||
|
||||
if (orderedList.length) {
|
||||
list = list.sort((a, b) => {
|
||||
const valueA = parseInt(a.name, 10)
|
||||
const valueB = parseInt(b.name, 10)
|
||||
const upperAName = isNaN(valueA) ? valueA : a.name?.toUpperCase?.()
|
||||
const upperBName = isNaN(valueB) ? valueB : b.name?.toUpperCase?.()
|
||||
const upperAName = a.name?.toUpperCase?.()
|
||||
const upperBName = b.name?.toUpperCase?.()
|
||||
|
||||
return orderedList.indexOf(upperAName) - orderedList.indexOf(upperBName)
|
||||
})
|
||||
|
@ -98,7 +98,9 @@ export const getAllocatedInfo = (host) => {
|
||||
*/
|
||||
export const getHugepageSizes = (host) => {
|
||||
const wrapHost = Array.isArray(host) ? host : [host]
|
||||
const numaNodes = [wrapHost?.HOST_SHARE?.NUMA_NODES?.NODE ?? []].flat()
|
||||
const numaNodes = wrapHost
|
||||
?.map((item) => item?.HOST_SHARE?.NUMA_NODES?.NODE)
|
||||
.flat()
|
||||
|
||||
return numaNodes
|
||||
.filter((node) => node?.NODE_ID && node?.HUGEPAGE)
|
||||
|
@ -18,7 +18,7 @@ import { hydrate, render } from 'react-dom'
|
||||
import { createStore } from 'client/store'
|
||||
import App from 'client/apps/sunstone'
|
||||
|
||||
const { store } = createStore({ initState: window.__PRELOADED_STATE__ })
|
||||
export const { store } = createStore({ initState: window.__PRELOADED_STATE__ })
|
||||
|
||||
delete window.__PRELOADED_STATE__
|
||||
|
||||
|
@ -591,3 +591,83 @@ export const generateDocLink = (version, path) => {
|
||||
// Return link
|
||||
return DOCS_BASE_PATH + '/' + versionDoc + '/' + path
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract TAB id from field path.
|
||||
*
|
||||
* @param {string} str - Field path
|
||||
* @returns {string} - Tab ID/NAME
|
||||
*/
|
||||
export const extractTab = (str) => {
|
||||
const parts = str?.split('.')
|
||||
|
||||
return /^\d+$/.test(parts[parts.length - 1])
|
||||
? parts[parts.length - 2]
|
||||
: parts.pop()
|
||||
}
|
||||
|
||||
export const findKeyWithPath = (() => {
|
||||
const cache = new Map()
|
||||
|
||||
/**
|
||||
* @param {object} root0 - Params
|
||||
* @param {object} root0.obj - Object to search
|
||||
* @param {string} root0.keyToFind - Key to find
|
||||
* @param {Array} root0.path - Path to key
|
||||
* @param {boolean} root0.findAll - Return first or all
|
||||
* @returns {object} - Found key(s) + path(s)
|
||||
*/
|
||||
const search = ({ obj, keyToFind, path = [], findAll = false }) => {
|
||||
const cacheKey = JSON.stringify({ obj, keyToFind, path, findAll })
|
||||
if (cache.has(cacheKey)) {
|
||||
return cache.get(cacheKey)
|
||||
}
|
||||
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return { found: false, paths: [] }
|
||||
}
|
||||
|
||||
if (keyToFind in obj) {
|
||||
const newPath = [...path, keyToFind]
|
||||
const searchResult = { found: true, paths: [newPath] }
|
||||
if (!findAll) {
|
||||
cache.set(cacheKey, searchResult)
|
||||
|
||||
return searchResult
|
||||
}
|
||||
|
||||
return searchResult
|
||||
}
|
||||
|
||||
let accumulatedResults = []
|
||||
|
||||
const entries = Array.isArray(obj) ? obj.entries() : Object.entries(obj)
|
||||
for (const [key, value] of entries) {
|
||||
if (typeof value === 'object') {
|
||||
const nextPath = Array.isArray(obj)
|
||||
? [...path, `[${key}]`]
|
||||
: [...path, key]
|
||||
const subSearchResult = search({
|
||||
obj: value,
|
||||
keyToFind,
|
||||
path: nextPath,
|
||||
findAll,
|
||||
})
|
||||
if (subSearchResult.found) {
|
||||
accumulatedResults = [...accumulatedResults, ...subSearchResult.paths]
|
||||
if (!findAll) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const found = accumulatedResults.length > 0
|
||||
const finalResult = { found, paths: accumulatedResults }
|
||||
cache.set(cacheKey, finalResult)
|
||||
|
||||
return finalResult
|
||||
}
|
||||
|
||||
return search
|
||||
})()
|
||||
|
@ -17,12 +17,20 @@ import templateToObject from 'client/utils/parser/templateToObject'
|
||||
import parseApplicationToForm from 'client/utils/parser/parseApplicationToForm'
|
||||
import parseFormToApplication from 'client/utils/parser/parseFormToApplication'
|
||||
import parseFormToDeployApplication from 'client/utils/parser/parseFormToDeployApplication'
|
||||
import { parseAcl } from 'client/utils/parser/parseACL'
|
||||
import {
|
||||
parseNetworkString,
|
||||
parseCustomInputString,
|
||||
} from 'client/utils/parser/parseServiceTemplate'
|
||||
import parseVmTemplateContents from 'client/utils/parser/parseVmTemplateContents'
|
||||
import { parseAcl } from 'client/utils/parser/parseACL'
|
||||
import parseTouchedDirty from 'client/utils/parser/parseTouchedDirty'
|
||||
import isDeeplyEmpty from 'client/utils/parser/isDeeplyEmpty'
|
||||
import {
|
||||
filterTemplateData,
|
||||
transformActionsCreate,
|
||||
transformActionsInstantiate,
|
||||
} from 'client/utils/parser/vmTemplateFilter'
|
||||
import parsePayload from 'client/utils/parser/parseTemplatePayload'
|
||||
|
||||
export {
|
||||
templateToObject,
|
||||
@ -33,4 +41,10 @@ export {
|
||||
parseNetworkString,
|
||||
parseCustomInputString,
|
||||
parseVmTemplateContents,
|
||||
parseTouchedDirty,
|
||||
isDeeplyEmpty,
|
||||
filterTemplateData,
|
||||
parsePayload,
|
||||
transformActionsCreate,
|
||||
transformActionsInstantiate,
|
||||
}
|
||||
|
34
src/fireedge/src/client/utils/parser/isDeeplyEmpty.js
Normal file
34
src/fireedge/src/client/utils/parser/isDeeplyEmpty.js
Normal file
@ -0,0 +1,34 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/**
|
||||
* @param {object} obj - Deeply nested object
|
||||
* @returns {boolean} - Empty
|
||||
*/
|
||||
const isDeeplyEmpty = (obj) => {
|
||||
if (obj == null || typeof obj !== 'object') return true
|
||||
|
||||
return Object.entries(obj).every(([_key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.every(isDeeplyEmpty)
|
||||
} else if (value !== null && typeof value === 'object') {
|
||||
return isDeeplyEmpty(value)
|
||||
}
|
||||
|
||||
return !value
|
||||
})
|
||||
}
|
||||
|
||||
export default isDeeplyEmpty
|
@ -25,7 +25,7 @@ import { ACL_ID, ACL_RESOURCES, ACL_RIGHTS } from 'client/constants'
|
||||
* @param {string} rule - The ACL rule
|
||||
* @returns {number} - The hex value for the four components of a rule (user, resources, rights and zone)
|
||||
*/
|
||||
export const parseAcl = (rule) => {
|
||||
const parseAcl = (rule) => {
|
||||
// Get each component
|
||||
const ruleComponents = rule.split(' ')
|
||||
|
||||
@ -140,3 +140,5 @@ const calculateIds = (id) => {
|
||||
// Return the integer id value
|
||||
return idValue
|
||||
}
|
||||
|
||||
export { parseAcl }
|
||||
|
54
src/fireedge/src/client/utils/parser/parseTemplatePayload.js
Normal file
54
src/fireedge/src/client/utils/parser/parseTemplatePayload.js
Normal file
@ -0,0 +1,54 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { get } from 'lodash'
|
||||
import { findKeyWithPath, extractTab } from 'client/utils'
|
||||
import { TAB_FORM_MAP } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @param {object} payload - Payload.
|
||||
* @param {string} fieldPath - Field path.
|
||||
* @returns {object} - Parsed payload.
|
||||
*/
|
||||
const parsePayload = (payload, fieldPath) => {
|
||||
const TAB = extractTab(fieldPath)
|
||||
|
||||
if (payload === undefined || !fieldPath?.includes('extra')) {
|
||||
return payload // only parses the extra step
|
||||
}
|
||||
const relevantFields = TAB_FORM_MAP[TAB]
|
||||
|
||||
if (!relevantFields) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return relevantFields.reduce((parsedPayload, key) => {
|
||||
const searchResult = findKeyWithPath({
|
||||
obj: payload,
|
||||
keyToFind: key,
|
||||
})
|
||||
|
||||
if (searchResult.found) {
|
||||
const value = get(payload, searchResult.paths[0].join('.'), {})
|
||||
if (value !== undefined) {
|
||||
parsedPayload[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return parsedPayload
|
||||
}, {})
|
||||
}
|
||||
|
||||
export default parsePayload
|
44
src/fireedge/src/client/utils/parser/parseTouchedDirty.js
Normal file
44
src/fireedge/src/client/utils/parser/parseTouchedDirty.js
Normal file
@ -0,0 +1,44 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @param {object} touchedFields - RHF touched fields object.
|
||||
* @param {object} dirtyFields - RHF dirty fields object.
|
||||
* @returns {object} - Touched dirty object.
|
||||
*/
|
||||
const parseTouchedDirty = (touchedFields, dirtyFields) => {
|
||||
const mergeRecursively = (touched, dirty) =>
|
||||
Object.keys(dirty).reduce((acc, key) => {
|
||||
if (touched?.[key]) {
|
||||
if (
|
||||
typeof dirty[key] === 'object' &&
|
||||
dirty[key] !== null &&
|
||||
typeof touched[key] === 'object' &&
|
||||
touched[key] !== null
|
||||
) {
|
||||
acc[key] = mergeRecursively(touched[key], dirty[key])
|
||||
} else {
|
||||
acc[key] = dirty[key]
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return mergeRecursively(touchedFields, dirtyFields)
|
||||
}
|
||||
|
||||
export default parseTouchedDirty
|
739
src/fireedge/src/client/utils/parser/vmTemplateFilter.js
Normal file
739
src/fireedge/src/client/utils/parser/vmTemplateFilter.js
Normal file
@ -0,0 +1,739 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 _, { cloneDeep, merge, get } from 'lodash'
|
||||
import { convertToMB } from 'client/utils'
|
||||
import { MEMORY_RESIZE_OPTIONS, T } from 'client/constants'
|
||||
import { transformXmlString } from 'client/models/Helper'
|
||||
|
||||
// Attributes that will be always modify with the value of the form (except Storage, Network and PCI sections)
|
||||
const alwaysIncludeAttributes = {
|
||||
general: {
|
||||
HYPERVISOR: true,
|
||||
},
|
||||
extra: {
|
||||
OsCpu: {
|
||||
OS: {
|
||||
BOOT: true,
|
||||
FIRMWARE: true,
|
||||
},
|
||||
},
|
||||
InputOutput: {
|
||||
INPUT: true,
|
||||
},
|
||||
Context: {
|
||||
INPUTS_ORDER: true,
|
||||
USER_INPUTS: true,
|
||||
CONTEXT: {
|
||||
SSH_PUBLIC_KEY: true,
|
||||
NETWORK: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Attributes that will be always modify with the value of the form in the Pci section
|
||||
const alwaysIncludePci = {
|
||||
TYPE: true,
|
||||
}
|
||||
|
||||
// Attributes that will be always modify with the value of the form in the Nic section
|
||||
const alwaysIncludeNic = {
|
||||
NAME: true,
|
||||
}
|
||||
|
||||
// Attributes that will be always modify with the value of the form in the Nic alias section
|
||||
const alwaysIncludeNicAlias = {
|
||||
PARENT: true,
|
||||
NAME: true,
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the data of the form data with the values that were modified by the user and not adding the ones that could be added by default. The goal is to create the most simplify template that we can.
|
||||
*
|
||||
* @param {object} formData - VM Template form data
|
||||
* @param {object} modifiedFields - Touched/Dirty fields object
|
||||
* @param {object} existingTemplate - Existing data
|
||||
* @param {object} tabFormMap - Maps formData fields to tabs
|
||||
* @returns {object} - Filtered template data
|
||||
*/
|
||||
const filterTemplateData = (
|
||||
formData,
|
||||
modifiedFields,
|
||||
existingTemplate,
|
||||
tabFormMap
|
||||
) => {
|
||||
// Generate a form from the original data
|
||||
const normalizedTemplate = normalizeTemplate(existingTemplate, tabFormMap)
|
||||
|
||||
// Filter data of formData.general
|
||||
const newGeneral = reduceGeneral(
|
||||
formData?.general,
|
||||
modifiedFields?.general,
|
||||
normalizedTemplate?.general,
|
||||
alwaysIncludeAttributes
|
||||
)
|
||||
|
||||
// Filter data of formData.extra
|
||||
const newExtra = reduceExtra(
|
||||
formData,
|
||||
modifiedFields,
|
||||
normalizedTemplate,
|
||||
tabFormMap,
|
||||
alwaysIncludeAttributes
|
||||
)
|
||||
|
||||
// Add custom variables
|
||||
const newCustomVariables = {
|
||||
...normalizedTemplate['custom-variables'],
|
||||
...formData['custom-variables'],
|
||||
}
|
||||
|
||||
const result = {
|
||||
...newGeneral,
|
||||
...newExtra,
|
||||
...newCustomVariables,
|
||||
}
|
||||
|
||||
// Instantiate form could have another step called user_inputs
|
||||
if (formData.user_inputs) {
|
||||
result.CONTEXT = {
|
||||
...result.CONTEXT,
|
||||
...formData.user_inputs,
|
||||
}
|
||||
}
|
||||
|
||||
// Return object with all sections
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a formData format object with the original data before the user modifies anything.
|
||||
*
|
||||
* @param {object} existingTemplate - Original data before user's modifications in the same format as formData
|
||||
* @param {string} tabFormMap - Map with the attributes (for the XML request) that will contain the section
|
||||
* @returns {object} - A form data object with the original data
|
||||
*/
|
||||
const normalizeTemplate = (existingTemplate, tabFormMap) => {
|
||||
const categoryLookup = Object.entries(tabFormMap).reduce(
|
||||
(acc, [category, keys]) => {
|
||||
keys.forEach((key) => (acc[key] = category))
|
||||
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const normalized = { general: {}, extra: {} }
|
||||
|
||||
Object.entries(existingTemplate).forEach(([key, value]) => {
|
||||
if (categoryLookup[key]) {
|
||||
normalized.extra[key] = value
|
||||
} else {
|
||||
normalized.general[key] = value
|
||||
}
|
||||
})
|
||||
|
||||
if ('custom-variables' in existingTemplate) {
|
||||
normalized['custom-variables'] = existingTemplate['custom-variables']
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the formData.general section to only add modifications of the user.
|
||||
*
|
||||
* @param {object} formData - The data from the form (the one that the user could be modify)
|
||||
* @param {object} itemModifications - Map with the fields that will be touched and modified by the user or deleted (in array case) by the user
|
||||
* @param {object} existingTemplate - Original data before user's modifications in the same format as formData
|
||||
* @param {object} alwaysInclude - Include this field always as modified
|
||||
* @returns {object} - The general section with the final data to submit
|
||||
*/
|
||||
const reduceGeneral = (
|
||||
formData,
|
||||
itemModifications,
|
||||
existingTemplate,
|
||||
alwaysInclude
|
||||
) => {
|
||||
const newGeneral = { ...existingTemplate }
|
||||
|
||||
// Add always include
|
||||
const correctionMap = merge({}, itemModifications, alwaysInclude?.general)
|
||||
|
||||
Object.entries(correctionMap || {}).forEach(([key, correction]) => {
|
||||
if (correction && typeof correction === 'object' && correction.__delete__) {
|
||||
delete newGeneral[key]
|
||||
} else if (
|
||||
correction &&
|
||||
key in formData &&
|
||||
(!_.isEmpty(formData[key]) || formData[key] !== null)
|
||||
) {
|
||||
// If the correction is boolean, means that the user changed this value that is a simple value
|
||||
// If the correction is an object with an attribute delete means that is a hidden field that was deleted because the user change the value of its parent
|
||||
// If the correction is an object without an attribute delete means that is a field with an object instead a simple value and the user changed its value
|
||||
if (typeof correction === 'boolean' && correction) {
|
||||
newGeneral[key] = formData[key]
|
||||
} else if (typeof correction === 'object') {
|
||||
// In object case, call the same function to iterate over each key of the object
|
||||
const newChildren = reduceGeneral(
|
||||
formData[key],
|
||||
correction,
|
||||
_.get(existingTemplate, key)
|
||||
)
|
||||
newGeneral[key] = newChildren
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return newGeneral
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the formData.extra section to only add modifications of the user.
|
||||
*
|
||||
* @param {object} formData - The data from the form (the one that the user could be modify)
|
||||
* @param {object} itemModifications - Map with the fields that will be touched and modified by the user or deleted (in array case) by the user
|
||||
* @param {object} existingTemplate - Original data before user's modifications in the same format as formData
|
||||
* @param {string} tabFormMap - Map with the attributes (for the XML request) that will contain the section
|
||||
* @param {object} alwaysInclude - Include this field always as modified
|
||||
* @returns {object} - The extra section with the final data to submit
|
||||
*/
|
||||
const reduceExtra = (
|
||||
formData,
|
||||
itemModifications,
|
||||
existingTemplate,
|
||||
tabFormMap,
|
||||
alwaysInclude
|
||||
) => {
|
||||
const newExtra = { ...existingTemplate.extra }
|
||||
|
||||
// Ensure NIC array
|
||||
if (newExtra?.NIC)
|
||||
newExtra.NIC = Array.isArray(newExtra.NIC) ? newExtra.NIC : [newExtra.NIC]
|
||||
|
||||
if (newExtra?.NIC_ALIAS)
|
||||
newExtra.NIC_ALIAS = Array.isArray(newExtra.NIC_ALIAS)
|
||||
? newExtra.NIC_ALIAS
|
||||
: [newExtra.NIC_ALIAS]
|
||||
|
||||
if (newExtra?.PCI)
|
||||
newExtra.PCI = Array.isArray(newExtra.PCI) ? newExtra.PCI : [newExtra.PCI]
|
||||
|
||||
const originalData = cloneDeep(newExtra)
|
||||
|
||||
// Add always include
|
||||
const correctionMap = merge({}, itemModifications, alwaysInclude)
|
||||
|
||||
Object.entries(correctionMap.extra || {}).forEach(
|
||||
([section, sectionData]) => {
|
||||
if (section === 'Network') {
|
||||
Object.entries(correctionMap.extra[section]).forEach(([subSection]) => {
|
||||
if (subSection === 'NIC') {
|
||||
handleNetwork(
|
||||
formData,
|
||||
correctionMap?.extra?.Network,
|
||||
newExtra,
|
||||
subSection,
|
||||
'NIC',
|
||||
originalData,
|
||||
alwaysIncludeNic
|
||||
)
|
||||
} else if (subSection === 'NIC_ALIAS') {
|
||||
handleNetwork(
|
||||
formData,
|
||||
correctionMap?.extra?.Network,
|
||||
newExtra,
|
||||
subSection,
|
||||
'NIC_ALIAS',
|
||||
originalData,
|
||||
alwaysIncludeNicAlias
|
||||
)
|
||||
} else if (subSection === 'NIC_DEFAULT') {
|
||||
filterSingleSection(
|
||||
formData,
|
||||
correctionMap,
|
||||
'Network',
|
||||
newExtra,
|
||||
'NIC_DEFAULT'
|
||||
)
|
||||
}
|
||||
})
|
||||
} else if (section === 'Storage') {
|
||||
handleStorage(formData, correctionMap, newExtra, section, 'DISK')
|
||||
} else {
|
||||
handleOtherSections(
|
||||
formData,
|
||||
correctionMap,
|
||||
section,
|
||||
newExtra,
|
||||
tabFormMap,
|
||||
originalData
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Omitting values that are empty
|
||||
return _.omitBy(newExtra, _.isEmpty)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the items that were deleted by the user in the original data.
|
||||
*
|
||||
* @param {object} existingData - The original data
|
||||
* @param {object} correctionMapSection - Map to get the delete items
|
||||
* @returns {object} - An array with the original data but without the deleted items
|
||||
*/
|
||||
const deleteItemsOnExistingData = (
|
||||
existingData = {},
|
||||
correctionMapSection = []
|
||||
) =>
|
||||
// The items will be deleted if the section exists on the correction map and the index exists and has the delete flag
|
||||
existingData.filter(
|
||||
(data, index) =>
|
||||
!correctionMapSection ||
|
||||
index >= correctionMapSection.length ||
|
||||
!correctionMapSection[index].__delete__
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle the network section to add modified fields.
|
||||
*
|
||||
* @param {object} formData - The data from the form (the one that the user could be modify)
|
||||
* @param {object} correctionMap - Map with the fields that will be touched and modified by the user
|
||||
* @param {object} newExtra - The extra section of the form.
|
||||
* @param {string} section - Section of the form (this function will have always Network)
|
||||
* @param {string} type - Section type inside network section
|
||||
* @param {object} originalData - Form data before the user makes any changes
|
||||
* @param {object} alwaysInclude - Include this field always as modified
|
||||
*/
|
||||
const handleNetwork = (
|
||||
formData,
|
||||
correctionMap,
|
||||
newExtra,
|
||||
section,
|
||||
type,
|
||||
originalData,
|
||||
alwaysInclude = {}
|
||||
) => {
|
||||
if (!formData.extra[type]) return
|
||||
|
||||
const existingData = _.cloneDeep(newExtra[type])
|
||||
|
||||
// Create an array if there is only one element and delete the items that were deleted by the user.
|
||||
const wrappedExistingData = existingData
|
||||
? deleteItemsOnExistingData(
|
||||
Array.isArray(existingData) ? existingData : [existingData],
|
||||
get(correctionMap, section, [])
|
||||
)
|
||||
: {}
|
||||
|
||||
// Delete the items that were deleted by the user to get the correct indexes.
|
||||
const sectionModifications = deleteItemsOnExistingData(
|
||||
get(correctionMap, section, []),
|
||||
get(correctionMap, section, [])
|
||||
)
|
||||
|
||||
if (type === 'NIC') {
|
||||
sectionModifications.forEach((value, index) => {
|
||||
const indexAlias = value.__aliasIndex__
|
||||
if (indexAlias !== undefined)
|
||||
wrappedExistingData[index] = { ...originalData.NIC_ALIAS[indexAlias] }
|
||||
|
||||
const indexPci = value.__aliasPci__
|
||||
if (indexPci !== undefined)
|
||||
wrappedExistingData[index] = { ...originalData.PCI[indexPci] }
|
||||
})
|
||||
} else if (type === 'NIC_ALIAS') {
|
||||
sectionModifications.forEach((value, index) => {
|
||||
const indexNic = value.__nicIndex__
|
||||
if (indexNic !== undefined)
|
||||
wrappedExistingData[index] = { ...originalData.NIC[indexNic] }
|
||||
const indexPci = value.__aliasPci__
|
||||
if (indexPci !== undefined)
|
||||
wrappedExistingData[index] = { ...originalData.PCI[indexPci] }
|
||||
})
|
||||
} else if (type === 'PCI') {
|
||||
sectionModifications.forEach((value, index) => {
|
||||
const indexNic = value.__nicIndex__
|
||||
if (indexNic !== undefined)
|
||||
wrappedExistingData[index] = { ...originalData.NIC[indexNic] }
|
||||
const indexAlias = value.__aliasIndex__
|
||||
if (indexAlias !== undefined)
|
||||
wrappedExistingData[index] = { ...originalData.NIC_ALIAS[indexAlias] }
|
||||
})
|
||||
}
|
||||
|
||||
// Iterate over the final data
|
||||
const modifiedData = formData.extra[type].map((item, index) => {
|
||||
// Check if the index of the item it's on the modifications map and has value
|
||||
if (
|
||||
index < sectionModifications.length &&
|
||||
sectionModifications[index] !== null
|
||||
) {
|
||||
let itemModifications = {}
|
||||
|
||||
// If the type is PCI and not has a TYPE attribute, the pci is from the inputOutput section. In other case, is from network section. So in the second case, we have to get the attribute from advanced step, and not in the first case.
|
||||
if (type === 'PCI' && !item?.TYPE) {
|
||||
// Get the fields where the modifications were done
|
||||
itemModifications = {
|
||||
...sectionModifications[index],
|
||||
...alwaysInclude,
|
||||
}
|
||||
} else {
|
||||
// Get the fields where the modifications were done
|
||||
itemModifications = Object.keys(sectionModifications[index])?.reduce(
|
||||
(acc, key) => ({
|
||||
...acc,
|
||||
...sectionModifications[index][key],
|
||||
...alwaysInclude,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
// Delete keys on existing data that are marked to delete and not exists on formData
|
||||
if (wrappedExistingData && wrappedExistingData[index])
|
||||
Object.entries(itemModifications)
|
||||
.filter(([key, value]) => value.__delete__ && !item[key])
|
||||
.forEach(
|
||||
([key, value]) =>
|
||||
wrappedExistingData[index][key] &&
|
||||
delete wrappedExistingData[index][key]
|
||||
)
|
||||
|
||||
// Iterate over each field of the item and, if it is one of the field that was modified, add the modification to the new data
|
||||
return Object.keys(item).reduce((acc, key) => {
|
||||
if (
|
||||
typeof itemModifications[key] === 'boolean' &&
|
||||
itemModifications[key]
|
||||
) {
|
||||
acc[key] = item[key]
|
||||
} else if (
|
||||
typeof itemModifications[key] === 'object' &&
|
||||
itemModifications[key].__delete__
|
||||
) {
|
||||
delete acc[key]
|
||||
}
|
||||
|
||||
return acc
|
||||
}, wrappedExistingData?.[index] || {})
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
newExtra[type] = modifiedData
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the storage section to add modified fields.
|
||||
*
|
||||
* @param {object} formData - The data from the form (the one that the user could be modify)
|
||||
* @param {object} correctionMap - Map with the fields that will be touched and modified by the user
|
||||
* @param {object} newExtra - The extra section of the form.
|
||||
* @param {string} section - Section of the form (this function will have always Storage)
|
||||
* @param {string} type - Section type inside network section
|
||||
*/
|
||||
const handleStorage = (formData, correctionMap, newExtra, section, type) => {
|
||||
if (!formData.extra[type]) return
|
||||
|
||||
// const sectionModifications = correctionMap.extra[section] || []
|
||||
const existingData = _.cloneDeep(newExtra[type])
|
||||
|
||||
// Delete the items that were deleted by the user to get the correct indexes.
|
||||
const wrappedExistingData = deleteItemsOnExistingData(
|
||||
Array.isArray(existingData) ? existingData : [existingData],
|
||||
correctionMap.extra[section]
|
||||
)
|
||||
|
||||
// Delete the items that were deleted by the user to get the correct indexes.
|
||||
const sectionModifications = deleteItemsOnExistingData(
|
||||
correctionMap.extra[section],
|
||||
correctionMap.extra[section]
|
||||
)
|
||||
|
||||
// Iterate over the final data
|
||||
const modifiedData = formData.extra[type].map((disk, index) => {
|
||||
// Check if the index of the item it's on the modifications map and has value
|
||||
if (
|
||||
index < sectionModifications.length &&
|
||||
sectionModifications[index] !== null
|
||||
) {
|
||||
// Get the fields where the modifications were done
|
||||
const diskModifications = Object.keys(
|
||||
sectionModifications[index]
|
||||
)?.reduce(
|
||||
(acc, key) => ({ ...acc, ...sectionModifications[index][key] }),
|
||||
{}
|
||||
)
|
||||
|
||||
// Iterate over each field of the item and, if it is one of the field that was modified, add the modification to the new data
|
||||
return Object.keys(disk).reduce((acc, key) => {
|
||||
if (
|
||||
typeof diskModifications[key] === 'boolean' &&
|
||||
diskModifications[key]
|
||||
) {
|
||||
acc[key] = disk[key]
|
||||
} else if (
|
||||
typeof diskModifications[key] === 'object' &&
|
||||
diskModifications[key].__delete__
|
||||
) {
|
||||
delete acc[key]
|
||||
} else if (key === 'SIZE' && diskModifications.SIZEUNIT) {
|
||||
acc[key] = disk[key]
|
||||
}
|
||||
|
||||
return acc
|
||||
}, wrappedExistingData?.[index] || {})
|
||||
}
|
||||
|
||||
return disk
|
||||
})
|
||||
|
||||
newExtra[type] = modifiedData
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle others section (that not are Storage or Network) to add modified fields.
|
||||
*
|
||||
* @param {object} formData - The data from the form (the one that the user could be modify)
|
||||
* @param {object} correctionMap - Map with the fields that will be touched and modified by the user
|
||||
* @param {string} section - Section of the form
|
||||
* @param {object} newExtra - The extra section of the form.
|
||||
* @param {string} tabFormMap - Map with the attributes (for the XML request) that will contain the section
|
||||
* @param {object} originalData - Form data before the user makes any changes
|
||||
*/
|
||||
const handleOtherSections = (
|
||||
formData,
|
||||
correctionMap,
|
||||
section,
|
||||
newExtra,
|
||||
tabFormMap,
|
||||
originalData
|
||||
) => {
|
||||
// Check each section of the templates form
|
||||
if (tabFormMap[section]) {
|
||||
tabFormMap[section].forEach((key) => {
|
||||
// Scheduled actions special case
|
||||
if (key === 'SCHED_ACTION') {
|
||||
newExtra[key] = formData?.extra[key]
|
||||
} else if (key === 'PCI') {
|
||||
handleNetwork(
|
||||
formData,
|
||||
correctionMap?.extra?.InputOutput,
|
||||
newExtra,
|
||||
'PCI',
|
||||
'PCI',
|
||||
originalData,
|
||||
alwaysIncludePci
|
||||
)
|
||||
} else {
|
||||
filterSingleSection(formData, correctionMap, section, newExtra, key)
|
||||
|
||||
// Special cases for Context.USER_INPUTS
|
||||
if (
|
||||
section === 'Context' &&
|
||||
key === 'USER_INPUTS' &&
|
||||
correctionMap.extra.Context.USER_INPUTS
|
||||
) {
|
||||
// Keep CONTEXT.INPUTS_ORDER because it's not a form by himself, depends on CONTEXT.USER_INPUTS
|
||||
newExtra.INPUTS_ORDER = formData.extra.INPUTS_ORDER
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter one section that is not Storage, Network or Schedule Actions.
|
||||
*
|
||||
* @param {object} formData - The data from the form (the one that the user could be modify)
|
||||
* @param {object} itemsModifications - Map with the fields that will be touched and modified by the user
|
||||
* @param {string} section - Section of the form
|
||||
* @param {object} newExtra - The extra section of the form.
|
||||
* @param {string} key - Section on the formData
|
||||
*/
|
||||
const filterSingleSection = (
|
||||
formData,
|
||||
itemsModifications,
|
||||
section,
|
||||
newExtra,
|
||||
key
|
||||
) => {
|
||||
// Check if attribute has changes on the correction map
|
||||
if (
|
||||
(key in formData.extra || key in itemsModifications.extra[section]) &&
|
||||
itemsModifications.extra[section]?.[key]
|
||||
) {
|
||||
// Get value and copy the section
|
||||
const value = formData.extra[key]
|
||||
const newOtherSection = { ...newExtra[key] }
|
||||
|
||||
// Arrays and single values replace the whole value
|
||||
if (
|
||||
Array.isArray(value) ||
|
||||
key === 'USER_INPUTS' ||
|
||||
!(typeof value === 'object')
|
||||
) {
|
||||
newExtra[key] = value
|
||||
} else {
|
||||
// Objects iterate over each key to check if the key was changed by the user
|
||||
Object.entries(itemsModifications.extra[section]?.[key] || {}).forEach(
|
||||
([childrenKey, correction]) => {
|
||||
// If the correction is boolean, means that the user changed this value that is a simple value
|
||||
// If the correction is an object with an attribute delete means that is a hidden field that was deleted because the user change the value of its parent
|
||||
// If the correction is an object without an attribute delete means that is a field with an object instead a simple value and the user changed its value
|
||||
if (
|
||||
correction &&
|
||||
typeof correction === 'object' &&
|
||||
correction.__delete__
|
||||
) {
|
||||
delete newOtherSection[childrenKey]
|
||||
} else if (
|
||||
correction &&
|
||||
typeof correction === 'boolean' &&
|
||||
childrenKey in formData.extra[key] &&
|
||||
(!_.isEmpty(formData.extra[key][childrenKey]) ||
|
||||
formData.extra[key][childrenKey] !== null)
|
||||
) {
|
||||
newOtherSection[childrenKey] = formData.extra[key][childrenKey]
|
||||
} else if (
|
||||
correction &&
|
||||
typeof correction === 'boolean' &&
|
||||
!(childrenKey in formData.extra[key])
|
||||
) {
|
||||
delete newOtherSection[childrenKey]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Add section with changes
|
||||
newExtra[key] = newOtherSection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute any needed action on create or update template after filter the template.
|
||||
*
|
||||
* @param {object} template - Template with data
|
||||
*/
|
||||
const transformActionsCreate = (template) => {
|
||||
transformActionsCommon(template)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute any needed action on instantiate template after filter the template.
|
||||
*
|
||||
* @param {object} template - Template with data
|
||||
* @param {object} original - Initial values of the template
|
||||
*/
|
||||
const transformActionsInstantiate = (template, original) => {
|
||||
transformActionsCommon(template)
|
||||
|
||||
original?.TEMPLATE?.OS &&
|
||||
template?.OS &&
|
||||
(template.OS = {
|
||||
...original?.TEMPLATE?.OS,
|
||||
...template?.OS,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute actions on the filtered template (common to instantiate and create/update).
|
||||
*
|
||||
* @param {object} template - Template with data
|
||||
*/
|
||||
const transformActionsCommon = (template) => {
|
||||
const newContext = template.CONTEXT ? { ...template.CONTEXT } : {}
|
||||
|
||||
// Reset user inputs in context object
|
||||
Object.keys(newContext).forEach((key) => {
|
||||
if (newContext[key] === '$' + key) {
|
||||
delete newContext[key]
|
||||
}
|
||||
})
|
||||
|
||||
// Add user inputs to context
|
||||
if (template?.USER_INPUTS) {
|
||||
Object.keys(template?.USER_INPUTS).forEach((name) => {
|
||||
const isCapacity = ['MEMORY', 'CPU', 'VCPU'].includes(name)
|
||||
const upperName = String(name).toUpperCase()
|
||||
|
||||
!isCapacity && (newContext[upperName] = `$${upperName}`)
|
||||
if (!template.CONTEXT) template.CONTEXT = {}
|
||||
})
|
||||
}
|
||||
|
||||
template.CONTEXT = newContext
|
||||
|
||||
// Delete MEMORY_SLOTS if the MEMORY_RESIZE_MODE is not Hotplug
|
||||
if (template?.MEMORY_RESIZE_MODE !== MEMORY_RESIZE_OPTIONS[T.Hotplug]) {
|
||||
delete template.MEMORY_SLOTS
|
||||
}
|
||||
|
||||
// ISSUE#6136: Convert size to MB (because XML API uses only MB) and delete sizeunit field (no needed on XML API)
|
||||
template.MEMORY = convertToMB(template.MEMORY, template.MEMORYUNIT)
|
||||
delete template.MEMORYUNIT
|
||||
|
||||
// cast CPU_MODEL/FEATURES
|
||||
if (Array.isArray(template?.CPU_MODEL?.FEATURES)) {
|
||||
template.CPU_MODEL.FEATURES = template.CPU_MODEL.FEATURES.join(', ')
|
||||
}
|
||||
|
||||
// Delete NIC name
|
||||
;['PCI', 'NIC_ALIAS'].forEach((nicKey) => {
|
||||
const section = template?.[nicKey]
|
||||
? Array.isArray(template?.[nicKey])
|
||||
? template?.[nicKey]
|
||||
: [template?.[nicKey]]
|
||||
: []
|
||||
|
||||
section.forEach((NIC) => delete NIC?.NAME)
|
||||
})
|
||||
|
||||
// Delete Schedule action NAME
|
||||
if (template.SCHED_ACTION) {
|
||||
const ensuredSched = template.SCHED_ACTION
|
||||
? Array.isArray(template.SCHED_ACTION)
|
||||
? template.SCHED_ACTION
|
||||
: [template.SCHED_ACTION]
|
||||
: []
|
||||
template.SCHED_ACTION = ensuredSched.map((action) => {
|
||||
delete action.NAME
|
||||
|
||||
return action
|
||||
})
|
||||
}
|
||||
|
||||
// If template has RAW attribute
|
||||
if (template.RAW) {
|
||||
// // Add type (hypervisor) on RAW data if exists data, if not, delete RAW section.
|
||||
if (template.RAW?.DATA) template.RAW.TYPE = template.HYPERVISOR
|
||||
else delete template.RAW
|
||||
|
||||
// ISSUE #6418: Raw data is in XML format, so it needs to be transform before sennding it to the API (otherwise the value of RAW.DATA will be treat as part of the XML template)
|
||||
template?.RAW?.DATA &&
|
||||
(template.RAW.DATA = transformXmlString(template.RAW.DATA))
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
filterTemplateData,
|
||||
transformActionsCreate,
|
||||
transformActionsInstantiate,
|
||||
}
|
@ -608,3 +608,27 @@ export const disableFields = (
|
||||
return field
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the index array without objects that have delete attribute.
|
||||
*
|
||||
* @param {Array} list - Array to filter
|
||||
* @param {number} position - Position to look for
|
||||
* @returns {number} - The index in the array
|
||||
*/
|
||||
export const calculateIndex = (list, position) => {
|
||||
let filteredIndex = -1
|
||||
let acc = 0
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (!list[i].__delete__) {
|
||||
if (acc === position) {
|
||||
filteredIndex = i
|
||||
break
|
||||
}
|
||||
acc++
|
||||
}
|
||||
}
|
||||
|
||||
return filteredIndex
|
||||
}
|
||||
|
@ -106,6 +106,10 @@ module.exports = {
|
||||
from: resource,
|
||||
default: 0,
|
||||
},
|
||||
force: {
|
||||
from: postBody,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
[IMAGE_ENABLE]: {
|
||||
|
Loading…
Reference in New Issue
Block a user