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

B OpenNebula/one#6154: Fix error updating template (#2733)

This commit is contained in:
David 2023-09-19 10:04:53 +02:00 committed by GitHub
parent d0c4f30c4b
commit f242d443ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1479 additions and 417 deletions

View File

@ -67,6 +67,7 @@
"jsonwebtoken": "8.5.1",
"jwt-simple": "0.5.6",
"lockfile": "1.0.4",
"lodash": "4.17.21",
"lodash.get": "4.4.2",
"luxon": "2.1.1",
"marked": "4.0.10",

View File

@ -101,6 +101,7 @@
"jsonwebtoken": "8.5.1",
"jwt-simple": "0.5.6",
"lockfile": "1.0.4",
"lodash": "4.17.21",
"lodash.get": "4.4.2",
"luxon": "2.1.1",
"marked": "4.0.10",

View File

@ -33,7 +33,7 @@ import {
VM_ACTIONS,
VM_ACTIONS_IN_CHARTER,
} from 'client/constants'
import { sentenceCase } from 'client/utils'
import { sentenceCase, hasRestrictedAttributes } from 'client/utils'
/**
* Returns a button to trigger form to create a scheduled action.
@ -44,30 +44,36 @@ import { sentenceCase } from 'client/utils'
* @param {function():Promise} props.onSubmit - Submit function
* @returns {ReactElement} Button
*/
const CreateSchedButton = memo(({ vm, relative, onSubmit }) => (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': VM_ACTIONS.SCHED_ACTION_CREATE,
label: T.AddAction,
variant: 'outlined',
}}
options={[
{
name: T.PunctualAction,
dialogProps: {
title: T.ScheduleAction,
dataCy: 'modal-sched-actions',
const CreateSchedButton = memo(
({ vm, relative, onSubmit, oneConfig, adminGroup }) => (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': VM_ACTIONS.SCHED_ACTION_CREATE,
label: T.AddAction,
variant: 'outlined',
}}
options={[
{
name: T.PunctualAction,
dialogProps: {
title: T.ScheduleAction,
dataCy: 'modal-sched-actions',
},
form: () =>
relative
? CreateRelativeSchedActionForm({
stepProps: { vm, oneConfig, adminGroup },
})
: CreateSchedActionForm({
stepProps: { vm, oneConfig, adminGroup },
}),
onSubmit,
},
form: () =>
relative
? CreateRelativeSchedActionForm({ stepProps: vm })
: CreateSchedActionForm({ stepProps: vm }),
onSubmit,
},
]}
/>
))
]}
/>
)
)
/**
* Returns a button to trigger form to update a scheduled action.
@ -79,36 +85,44 @@ const CreateSchedButton = memo(({ vm, relative, onSubmit }) => (
* @param {function():Promise} props.onSubmit - Submit function
* @returns {ReactElement} Button
*/
const UpdateSchedButton = memo(({ vm, schedule, relative, onSubmit }) => {
const { ID, ACTION } = schedule
const titleAction = `#${ID} ${sentenceCase(ACTION)}`
const formConfig = { stepProps: vm, initialValues: schedule }
const UpdateSchedButton = memo(
({ vm, schedule, relative, onSubmit, oneConfig, adminGroup }) => {
const { ID, ACTION } = schedule
const titleAction = `#${ID} ${sentenceCase(ACTION)}`
const formConfig = {
stepProps: { vm, oneConfig, adminGroup },
initialValues: schedule,
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VM_ACTIONS.SCHED_ACTION_UPDATE}-${ID}`,
icon: <Edit />,
tooltip: <Translate word={T.Edit} />,
}}
options={[
{
dialogProps: {
title: (
<Translate word={T.UpdateScheduleAction} values={[titleAction]} />
),
dataCy: 'modal-sched-actions',
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VM_ACTIONS.SCHED_ACTION_UPDATE}-${ID}`,
icon: <Edit />,
tooltip: <Translate word={T.Edit} />,
}}
options={[
{
dialogProps: {
title: (
<Translate
word={T.UpdateScheduleAction}
values={[titleAction]}
/>
),
dataCy: 'modal-sched-actions',
},
form: () =>
relative
? CreateRelativeSchedActionForm(formConfig)
: CreateSchedActionForm(formConfig),
onSubmit,
},
form: () =>
relative
? CreateRelativeSchedActionForm(formConfig)
: CreateSchedActionForm(formConfig),
onSubmit,
},
]}
/>
)
})
]}
/>
)
}
)
/**
* Returns a button to trigger modal to delete a scheduled action.
@ -118,32 +132,47 @@ const UpdateSchedButton = memo(({ vm, schedule, relative, onSubmit }) => {
* @param {function():Promise} props.onSubmit - Submit function
* @returns {ReactElement} Button
*/
const DeleteSchedButton = memo(({ onSubmit, schedule }) => {
const { ID, ACTION } = schedule
const titleAction = `#${ID} ${sentenceCase(ACTION)}`
const DeleteSchedButton = memo(
({ onSubmit, schedule, oneConfig, adminGroup }) => {
const { ID, ACTION } = schedule
const titleAction = `#${ID} ${sentenceCase(ACTION)}`
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VM_ACTIONS.SCHED_ACTION_DELETE}-${ID}`,
icon: <Trash />,
tooltip: <Translate word={T.Delete} />,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<Translate word={T.DeleteScheduleAction} values={[titleAction]} />
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
// Disable action if the nic has a restricted attribute on the template
const disabledAction =
!adminGroup &&
hasRestrictedAttributes(
schedule,
'SCHED_ACTION',
oneConfig?.VM_RESTRICTED_ATTR
)
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VM_ACTIONS.SCHED_ACTION_DELETE}-${ID}`,
icon: <Trash />,
tooltip: !disabledAction ? Tr(T.Delete) : Tr(T.DetachRestricted),
disabled: disabledAction,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<Translate
word={T.DeleteScheduleAction}
values={[titleAction]}
/>
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
},
onSubmit,
},
onSubmit,
},
]}
/>
)
})
]}
/>
)
}
)
/**
* Returns a button to trigger form to create a charter.
@ -195,6 +224,8 @@ const ButtonPropTypes = {
relative: PropTypes.bool,
onSubmit: PropTypes.func,
schedule: PropTypes.object,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
CreateSchedButton.propTypes = ButtonPropTypes

View File

@ -27,8 +27,11 @@ import { T, HYPERVISORS } from 'client/constants'
export const STEP_ID = 'advanced'
const Content = ({ hypervisor }) => {
const sections = useMemo(() => SECTIONS(hypervisor), [])
const Content = ({ hypervisor, oneConfig, adminGroup }) => {
const sections = useMemo(
() => SECTIONS(hypervisor, oneConfig, adminGroup),
[]
)
return (
<Box
@ -55,20 +58,24 @@ const Content = ({ hypervisor }) => {
*
* @param {object} props - Props
* @param {HYPERVISORS} props.hypervisor - Hypervisor
* @param {object} props.oneConfig - Config of oned.conf
* @param {boolean} props.adminGroup - User is admin or not
* @returns {Step} Advance options step
*/
const AdvancedOptions = ({ hypervisor } = {}) => ({
const AdvancedOptions = ({ hypervisor, oneConfig, adminGroup } = {}) => ({
id: STEP_ID,
label: T.AdvancedOptions,
resolver: SCHEMA(hypervisor),
resolver: SCHEMA(hypervisor, oneConfig, adminGroup),
optionsValidate: { abortEarly: false },
content: () => Content({ hypervisor }),
content: () => Content({ hypervisor, oneConfig, adminGroup }),
})
Content.propTypes = {
hypervisor: PropTypes.any,
data: PropTypes.any,
setFormData: PropTypes.func,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default AdvancedOptions

View File

@ -28,6 +28,7 @@ import {
Section,
getObjectSchemaFromFields,
filterFieldsByHypervisor,
disableFields,
} from 'client/utils'
const { vcenter } = HYPERVISORS
@ -47,33 +48,60 @@ const SIZE = {
/**
* @param {HYPERVISORS} hypervisor - Hypervisor
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - User is admin or not
* @returns {Section[]} Sections
*/
const SECTIONS = (hypervisor) => [
const SECTIONS = (hypervisor, oneConfig, adminGroup) => [
{
id: 'general',
legend: T.General,
fields: filterFieldsByHypervisor([SIZE, ...GENERAL_FIELDS], hypervisor),
fields: disableFields(
filterFieldsByHypervisor([SIZE, ...GENERAL_FIELDS], hypervisor),
'DISK',
oneConfig,
adminGroup
),
},
{
id: 'vcenter',
legend: 'vCenter',
fields: filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor),
'DISK',
oneConfig,
adminGroup
),
},
{
id: 'throttling-bytes',
legend: T.ThrottlingBytes,
fields: filterFieldsByHypervisor(THROTTLING_BYTES_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(THROTTLING_BYTES_FIELDS, hypervisor),
'DISK',
oneConfig,
adminGroup
),
},
{
id: 'throttling-iops',
legend: T.ThrottlingIOPS,
fields: filterFieldsByHypervisor(THROTTLING_IOPS_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(THROTTLING_IOPS_FIELDS, hypervisor),
'DISK',
oneConfig,
adminGroup
),
},
{
id: 'edge-cluster',
legend: T.EdgeCluster,
fields: filterFieldsByHypervisor(EDGE_CLUSTER_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(EDGE_CLUSTER_FIELDS, hypervisor),
'DISK',
oneConfig,
adminGroup
),
},
]

View File

@ -58,7 +58,6 @@ const Steps = createSteps([ImagesTable, AdvancedOptions], {
IMAGE_UID: UID,
IMAGE_UNAME: UNAME,
IMAGE_STATE: STATE,
ORIGINAL_SIZE: SIZE,
}
},
})

View File

@ -27,8 +27,11 @@ import { T, HYPERVISORS } from 'client/constants'
export const STEP_ID = 'advanced'
const Content = ({ hypervisor }) => {
const sections = useMemo(() => SECTIONS(hypervisor), [])
const Content = ({ hypervisor, oneConfig, adminGroup }) => {
const sections = useMemo(
() => SECTIONS(hypervisor, oneConfig, adminGroup),
[]
)
return (
<Box
@ -55,20 +58,24 @@ const Content = ({ hypervisor }) => {
*
* @param {object} props - Props
* @param {HYPERVISORS} props.hypervisor - Hypervisor
* @param {object} props.oneConfig - Config of oned.conf
* @param {boolean} props.adminGroup - User is admin or not
* @returns {Step} Advance options step
*/
const AdvancedOptions = ({ hypervisor } = {}) => ({
const AdvancedOptions = ({ hypervisor, oneConfig, adminGroup } = {}) => ({
id: STEP_ID,
label: T.AdvancedOptions,
resolver: SCHEMA(hypervisor),
resolver: SCHEMA(hypervisor, oneConfig, adminGroup),
optionsValidate: { abortEarly: false },
content: () => Content({ hypervisor }),
content: () => Content({ hypervisor, oneConfig, adminGroup }),
})
Content.propTypes = {
hypervisor: PropTypes.any,
data: PropTypes.any,
setFormData: PropTypes.func,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default AdvancedOptions

View File

@ -28,37 +28,65 @@ import {
Section,
getObjectSchemaFromFields,
filterFieldsByHypervisor,
disableFields,
} from 'client/utils'
/**
* @param {HYPERVISORS} hypervisor - Hypervisor
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - User is admin or not
* @returns {Section[]} Sections
*/
const SECTIONS = (hypervisor) => [
const SECTIONS = (hypervisor, oneConfig, adminGroup) => [
{
id: 'general',
legend: T.General,
fields: filterFieldsByHypervisor(GENERAL_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(GENERAL_FIELDS, hypervisor),
'DISK',
oneConfig,
adminGroup
),
},
{
id: 'vcenter',
legend: 'vCenter',
fields: filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor),
'DISK',
oneConfig,
adminGroup
),
},
{
id: 'throttling-bytes',
legend: T.ThrottlingBytes,
fields: filterFieldsByHypervisor(THROTTLING_BYTES_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(THROTTLING_BYTES_FIELDS, hypervisor),
'DISK',
oneConfig,
adminGroup
),
},
{
id: 'throttling-iops',
legend: T.ThrottlingIOPS,
fields: filterFieldsByHypervisor(THROTTLING_IOPS_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(THROTTLING_IOPS_FIELDS, hypervisor),
'DISK',
oneConfig,
adminGroup
),
},
{
id: 'edge-cluster',
legend: T.EdgeCluster,
fields: filterFieldsByHypervisor(EDGE_CLUSTER_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(EDGE_CLUSTER_FIELDS, hypervisor),
'DISK',
oneConfig,
adminGroup
),
},
]

View File

@ -26,8 +26,11 @@ import { T, HYPERVISORS } from 'client/constants'
export const STEP_ID = 'configuration'
const Content = ({ hypervisor }) => {
const memoFields = useMemo(() => FIELDS(hypervisor), [])
const Content = ({ hypervisor, oneConfig, adminGroup }) => {
const memoFields = useMemo(
() => FIELDS(hypervisor, oneConfig, adminGroup),
[]
)
return <FormWithSchema cy="attach-disk" fields={memoFields} id={STEP_ID} />
}
@ -37,20 +40,24 @@ const Content = ({ hypervisor }) => {
*
* @param {object} props - Props
* @param {HYPERVISORS} props.hypervisor - Hypervisor
* @param {object} props.oneConfig - Config of oned.conf
* @param {boolean} props.adminGroup - User is admin or not
* @returns {Step} Basic configuration step
*/
const BasicConfiguration = ({ hypervisor } = {}) => ({
const BasicConfiguration = ({ hypervisor, oneConfig, adminGroup } = {}) => ({
id: STEP_ID,
label: T.Configuration,
resolver: SCHEMA(hypervisor),
resolver: SCHEMA(hypervisor, oneConfig, adminGroup),
optionsValidate: { abortEarly: false },
content: () => Content({ hypervisor }),
content: () => Content({ hypervisor, oneConfig, adminGroup }),
})
Content.propTypes = {
hypervisor: PropTypes.any,
data: PropTypes.any,
setFormData: PropTypes.func,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default BasicConfiguration

View File

@ -20,6 +20,7 @@ import {
getValidationFromFields,
filterFieldsByHypervisor,
arrayToOptions,
disableFields,
} from 'client/utils'
import {
T,
@ -120,12 +121,19 @@ const FILESYSTEM = {
/**
* @param {HYPERVISORS} hypervisor - hypervisor
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - User is admin or not
* @returns {Field[]} List of fields
*/
export const FIELDS = (hypervisor) =>
filterFieldsByHypervisor(
[SIZE, SIZEUNIT, TYPE, FORMAT, FILESYSTEM],
hypervisor
export const FIELDS = (hypervisor, oneConfig, adminGroup) =>
disableFields(
filterFieldsByHypervisor(
[SIZE, SIZEUNIT, TYPE, FORMAT, FILESYSTEM],
hypervisor
),
'DISK',
oneConfig,
adminGroup
)
/**

View File

@ -22,15 +22,20 @@ import AdvancedOptions, {
import { mapUserInputs, createSteps, convertToMB } from 'client/utils'
const Steps = createSteps([BasicConfiguration, AdvancedOptions], {
transformInitialValue: (disk = {}, schema) => ({
...schema.cast(
transformInitialValue: (disk = {}, schema) => {
const schemaCast = schema.cast(
{
[BASIC_ID]: disk,
[ADVANCED_ID]: disk,
},
{ stripUnknown: true }
),
}),
)
// #6154: Add temp id to propagate it
schemaCast[ADVANCED_ID].TEMP_ID = disk.TEMP_ID
return schemaCast
},
transformBeforeSubmit: (formData) => {
const { [BASIC_ID]: configuration = {}, [ADVANCED_ID]: advanced = {} } =
formData ?? {}

View File

@ -22,6 +22,7 @@ import {
filterFieldsByDriver,
getObjectSchemaFromFields,
arrayToOptions,
disableFields,
} from 'client/utils'
import {
T,
@ -40,8 +41,6 @@ import {
transformPciToString,
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSchema'
import { useDisableInputByUserAndConfig } from 'client/features/Auth'
const { vcenter, firecracker, lxc } = HYPERVISORS
const PCI_TYPE_NAME = 'PCI_TYPE'
const DEVICE_LIST = 'DEVICE_LIST'
@ -283,7 +282,6 @@ const OVERRIDE_IPV4_FIELDS = [
label: T.MAC,
tooltip: T.MACConcept,
type: INPUT_TYPES.TEXT,
fieldProps: () => useDisableInputByUserAndConfig('NIC/MAC'),
validation: string()
.trim()
.notRequired()
@ -589,6 +587,8 @@ const GUEST_FIELDS = [
* @param {VN_DRIVERS} [data.driver] - Virtual network driver
* @param {HYPERVISORS} [data.hypervisor] - VM Hypervisor
* @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
* @returns {Section[]} Sections
*/
const SECTIONS = ({
@ -596,6 +596,8 @@ const SECTIONS = ({
driver,
hypervisor = HYPERVISORS.kvm,
defaultData,
oneConfig,
adminGroup,
} = {}) => {
const filters = { driver, hypervisor }
@ -606,7 +608,12 @@ const SECTIONS = ({
{
id: 'general',
legend: T.General,
fields: filterByHypAndDriver(GENERAL_FIELDS({ nics }), filters),
fields: disableFields(
filterByHypAndDriver(GENERAL_FIELDS({ nics }), filters),
'NIC',
oneConfig,
adminGroup
),
},
]
}
@ -615,27 +622,52 @@ const SECTIONS = ({
{
id: 'guacamole-connections',
legend: T.GuacamoleConnections,
fields: filterByHypAndDriver(GUACAMOLE_CONNECTIONS, filters),
fields: disableFields(
filterByHypAndDriver(GUACAMOLE_CONNECTIONS, filters),
'NIC',
oneConfig,
adminGroup
),
},
{
id: 'override-ipv4',
legend: T.OverrideNetworkValuesIPv4,
fields: filterByHypAndDriver(OVERRIDE_IPV4_FIELDS, filters),
fields: disableFields(
filterByHypAndDriver(OVERRIDE_IPV4_FIELDS, filters),
'NIC',
oneConfig,
adminGroup
),
},
{
id: 'override-ipv6',
legend: T.OverrideNetworkValuesIPv6,
fields: filterByHypAndDriver(OVERRIDE_IPV6_FIELDS, filters),
fields: disableFields(
filterByHypAndDriver(OVERRIDE_IPV6_FIELDS, filters),
'NIC',
oneConfig,
adminGroup
),
},
{
id: 'hardware',
legend: T.Hardware,
fields: filterByHypAndDriver(HARDWARE_FIELDS(defaultData), filters),
fields: disableFields(
filterByHypAndDriver(HARDWARE_FIELDS(defaultData), filters),
'NIC',
oneConfig,
adminGroup
),
},
{
id: 'guest',
legend: T.GuestOptions,
fields: filterByHypAndDriver(GUEST_FIELDS, filters),
fields: disableFields(
filterByHypAndDriver(GUEST_FIELDS, filters),
'NIC',
oneConfig,
adminGroup
),
},
])
}

View File

@ -21,6 +21,7 @@ import {
filterFieldsByHypervisor,
filterFieldsByDriver,
getObjectSchemaFromFields,
disableFields,
} from 'client/utils'
import { T, INPUT_TYPES, HYPERVISORS, VN_DRIVERS } from 'client/constants'
@ -112,21 +113,38 @@ const OVERRIDE_OUT_QOS_FIELDS = [
* @param {object} data - VM or VM Template data
* @param {VN_DRIVERS} [data.driver] - Virtual network driver
* @param {HYPERVISORS} [data.hypervisor] - VM Hypervisor
* @param {object} data.oneConfig - Config of oned.conf
* @param {boolean} data.adminGroup - User is admin or not
* @returns {Section[]} Sections
*/
const SECTIONS = ({ driver, hypervisor = HYPERVISORS.kvm } = {}) => {
const SECTIONS = ({
driver,
hypervisor = HYPERVISORS.kvm,
oneConfig,
adminGroup,
} = {}) => {
const filters = { driver, hypervisor }
return [
{
id: 'override-in-qos',
legend: T.OverrideNetworkInboundTrafficQos,
fields: filterByHypAndDriver(OVERRIDE_IN_QOS_FIELDS, filters),
fields: disableFields(
filterByHypAndDriver(OVERRIDE_IN_QOS_FIELDS, filters),
'NIC',
oneConfig,
adminGroup
),
},
{
id: 'override-out-qos',
legend: T.OverrideNetworkOutboundTrafficQos,
fields: filterByHypAndDriver(OVERRIDE_OUT_QOS_FIELDS, filters),
fields: disableFields(
filterByHypAndDriver(OVERRIDE_OUT_QOS_FIELDS, filters),
'NIC',
oneConfig,
adminGroup
),
},
]
}

View File

@ -60,6 +60,9 @@ 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]: [
{

View File

@ -49,10 +49,15 @@ const commonTransformInitialValue = (scheduledAction, schema, typeForm) => {
}
}
return schema.cast(dataToCast, {
const castSchema = schema.cast(dataToCast, {
context: scheduledAction,
stripUnknown: true,
})
// #6154: Add temportal id for restricted attributes
castSchema.TEMP_ID = scheduledAction.TEMP_ID
return castSchema
}
const commonTransformBeforeSubmit = (formData) => {

View File

@ -23,7 +23,7 @@ import {
} from 'client/components/Forms/Vm/CreateSchedActionForm/fields'
import { ARGS_TYPES } from 'client/constants'
import { getRequiredArgsByAction } from 'client/models/Scheduler'
import { Field, getObjectSchemaFromFields } from 'client/utils'
import { Field, getObjectSchemaFromFields, disableFields } from 'client/utils'
const ARG_SCHEMA = string()
.trim()
@ -66,16 +66,25 @@ const COMMON_SCHEMA = object({
})
/**
* @param {object} vm - Vm resource
* @param {object} props - Props
* @param {object} props.vm - Vm resource
* @param {object} props.oneConfig - Config of oned.conf
* @param {boolean} props.adminGroup - User is admin or not
* @returns {Field[]} Fields
*/
export const VM_SCHED_FIELDS = (vm) => [
PUNCTUAL_FIELDS.ACTION_FIELD(vm),
...COMMON_FIELDS(vm, true),
PUNCTUAL_FIELDS.TIME_FIELD,
PUNCTUAL_FIELDS.END_TYPE_FIELD,
PUNCTUAL_FIELDS.END_VALUE_FIELD,
]
export const VM_SCHED_FIELDS = ({ vm, oneConfig, adminGroup }) =>
disableFields(
[
PUNCTUAL_FIELDS.ACTION_FIELD(vm),
...COMMON_FIELDS(vm, true),
PUNCTUAL_FIELDS.TIME_FIELD,
PUNCTUAL_FIELDS.END_TYPE_FIELD,
PUNCTUAL_FIELDS.END_VALUE_FIELD,
],
'SCHED_ACTION',
oneConfig,
adminGroup
)
/** @type {ObjectSchema} Schema */
export const VM_SCHED_SCHEMA = COMMON_SCHEMA.concat(
@ -94,15 +103,21 @@ export const VM_SCHED_SCHEMA = COMMON_SCHEMA.concat(
)
/** @type {Field[]} Fields for relative actions */
export const TEMPLATE_SCHED_FIELDS = (vm) => [
PUNCTUAL_FIELDS.ACTION_FIELD(vm),
...COMMON_FIELDS(vm),
PUNCTUAL_FIELDS.TIME_FIELD,
RELATIVE_FIELDS.RELATIVE_TIME_FIELD,
RELATIVE_FIELDS.PERIOD_FIELD,
RELATIVE_FIELDS.END_TYPE_FIELD_WITHOUT_DATE,
PUNCTUAL_FIELDS.END_VALUE_FIELD,
]
export const TEMPLATE_SCHED_FIELDS = ({ vm, oneConfig, adminGroup }) =>
disableFields(
[
PUNCTUAL_FIELDS.ACTION_FIELD(vm),
...COMMON_FIELDS(vm),
PUNCTUAL_FIELDS.TIME_FIELD,
RELATIVE_FIELDS.RELATIVE_TIME_FIELD,
RELATIVE_FIELDS.PERIOD_FIELD,
RELATIVE_FIELDS.END_TYPE_FIELD_WITHOUT_DATE,
PUNCTUAL_FIELDS.END_VALUE_FIELD,
],
'SCHED_ACTION',
oneConfig,
adminGroup
)
/** @type {ObjectSchema} Relative Schema */
export const TEMPLATE_SCHED_SCHEMA = COMMON_SCHEMA.concat(

View File

@ -28,9 +28,9 @@ import {
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/backup/schema'
import { T } from 'client/constants'
const Backup = () => (
const Backup = ({ oneConfig, adminGroup }) => (
<>
{SECTIONS.map(({ id, ...section }) => (
{SECTIONS(oneConfig, adminGroup).map(({ id, ...section }) => (
<FormWithSchema
key={id}
id={EXTRA_ID}
@ -44,6 +44,8 @@ const Backup = () => (
Backup.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
Backup.displayName = 'Backup'

View File

@ -20,6 +20,7 @@ import {
Section,
arrayToOptions,
getObjectSchemaFromFields,
disableFields,
} from 'client/utils'
import {
T,
@ -80,15 +81,15 @@ const MODE_FIELD = {
}
/** @type {Section[]} Sections */
export const SECTIONS = [
export const SECTIONS = (oneConfig, adminGroup) => [
{
id: 'backup-configuration',
fields: [
BACKUP_VOLATILE_FIELD,
FS_FREEZE_FIELD,
KEEP_LAST_FIELD,
MODE_FIELD,
],
fields: disableFields(
[BACKUP_VOLATILE_FIELD, FS_FREEZE_FIELD, KEEP_LAST_FIELD, MODE_FIELD],
'BACKUP_CONFIG',
oneConfig,
adminGroup
),
},
]

View File

@ -37,8 +37,11 @@ import { T } from 'client/constants'
export const TAB_ID = 'OS'
const Booting = ({ hypervisor, ...props }) => {
const sections = useMemo(() => SECTIONS(hypervisor), [hypervisor])
const Booting = ({ hypervisor, oneConfig, adminGroup, ...props }) => {
const sections = useMemo(
() => SECTIONS(hypervisor, oneConfig, adminGroup),
[hypervisor]
)
return (
<Stack
@ -74,6 +77,8 @@ Booting.propTypes = {
setFormData: PropTypes.func,
hypervisor: PropTypes.string,
control: PropTypes.object,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */

View File

@ -27,39 +27,67 @@ import {
Section,
getObjectSchemaFromFields,
filterFieldsByHypervisor,
disableFields,
} from 'client/utils'
import { T, HYPERVISORS } from 'client/constants'
/**
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - User is admin or not
* @returns {Section[]} Sections
*/
const SECTIONS = (hypervisor) => [
const SECTIONS = (hypervisor, oneConfig, adminGroup) => [
{
id: 'os-boot',
legend: T.Boot,
fields: filterFieldsByHypervisor(BOOT_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(BOOT_FIELDS, hypervisor),
'OS',
oneConfig,
adminGroup
),
},
{
id: 'os-features',
legend: T.Features,
fields: filterFieldsByHypervisor(FEATURES_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(FEATURES_FIELDS, hypervisor),
'OS',
oneConfig,
adminGroup
),
},
{
id: 'os-kernel',
legend: T.Kernel,
fields: filterFieldsByHypervisor(KERNEL_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(KERNEL_FIELDS, hypervisor),
'OS',
oneConfig,
adminGroup
),
},
{
id: 'os-ramdisk',
legend: T.Ramdisk,
fields: filterFieldsByHypervisor(RAMDISK_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(RAMDISK_FIELDS, hypervisor),
'OS',
oneConfig,
adminGroup
),
},
{
id: 'os-raw',
legend: T.RawData,
legendTooltip: T.RawDataConcept,
fields: filterFieldsByHypervisor(RAW_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(RAW_FIELDS, hypervisor),
'OS',
oneConfig,
adminGroup
),
},
]

View File

@ -22,6 +22,8 @@ import { FormWithSchema, Legend } from 'client/components/Forms'
import { SSH_PUBLIC_KEY, SCRIPT_FIELDS, OTHER_FIELDS } from './schema'
import { T } from 'client/constants'
import { disableFields } from 'client/utils'
const SSH_KEY_USER = '$USER[SSH_PUBLIC_KEY]'
/**
@ -29,9 +31,11 @@ const SSH_KEY_USER = '$USER[SSH_PUBLIC_KEY]'
*
* @param {object} props - Props passed to the component
* @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
* @returns {ReactElement} - Configuration section
*/
const ConfigurationSection = ({ stepId }) => {
const ConfigurationSection = ({ stepId, oneConfig, adminGroup }) => {
const { setValue, getValues } = useFormContext()
const SSH_PUBLIC_KEY_PATH = useMemo(
() => [stepId, SSH_PUBLIC_KEY.name].filter(Boolean).join('.'),
@ -66,13 +70,18 @@ const ConfigurationSection = ({ stepId }) => {
<FormWithSchema
id={stepId}
cy={getCyPath('context-configuration-others')}
fields={OTHER_FIELDS}
fields={disableFields(OTHER_FIELDS, 'CONTEXT', oneConfig, adminGroup)}
/>
<section>
<FormWithSchema
id={stepId}
cy={getCyPath('context-ssh-public-key')}
fields={[SSH_PUBLIC_KEY]}
fields={disableFields(
[SSH_PUBLIC_KEY],
'CONTEXT',
oneConfig,
adminGroup
)}
/>
<Stack direction="row" gap="1em">
<Button
@ -95,7 +104,12 @@ const ConfigurationSection = ({ stepId }) => {
<FormWithSchema
id={stepId}
cy={getCyPath('context-script')}
fields={SCRIPT_FIELDS}
fields={disableFields(
SCRIPT_FIELDS,
'CONTEXT',
oneConfig,
adminGroup
)}
rootProps={{ sx: { width: '100%', gridColumn: '1 / -1' } }}
/>
</Stack>
@ -106,6 +120,8 @@ const ConfigurationSection = ({ stepId }) => {
ConfigurationSection.propTypes = {
stepId: PropTypes.string,
hypervisor: PropTypes.string,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default ConfigurationSection

View File

@ -21,15 +21,19 @@ import { FormWithSchema } from 'client/components/Forms'
import { FILES_FIELDS } from './schema'
import { T } from 'client/constants'
import { disableFields } from 'client/utils'
export const SECTION_ID = 'CONTEXT'
/**
* @param {object} props - Props
* @param {string} [props.stepId] - ID of the step the section belongs to
* @param {string} props.hypervisor - VM hypervisor
* @param {object} props.oneConfig - Config of oned.conf
* @param {boolean} props.adminGroup - User is admin or not
* @returns {ReactElement} - Files section
*/
const FilesSection = ({ stepId, hypervisor }) => (
const FilesSection = ({ stepId, hypervisor, oneConfig, adminGroup }) => (
<FormWithSchema
accordion
legend={T.Files}
@ -38,13 +42,24 @@ const FilesSection = ({ stepId, hypervisor }) => (
() => [stepId, 'context-files'].filter(Boolean).join('-'),
[stepId]
)}
fields={useMemo(() => FILES_FIELDS(hypervisor), [hypervisor])}
fields={useMemo(
() =>
disableFields(
FILES_FIELDS(hypervisor),
'CONTEXT',
oneConfig,
adminGroup
),
[hypervisor]
)}
/>
)
FilesSection.propTypes = {
stepId: PropTypes.string,
hypervisor: PropTypes.string,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default FilesSection

View File

@ -33,10 +33,10 @@ export const TAB_ID = ['CONTEXT', USER_INPUTS_ID]
const Context = (props) => (
<>
<ConfigurationSection stepId={EXTRA_ID} />
<UserInputsSection />
<ConfigurationSection stepId={EXTRA_ID} {...props} />
<UserInputsSection {...props} />
<FilesSection stepId={EXTRA_ID} {...props} />
<ContextVarsSection stepId={EXTRA_ID} {...props} />
<ContextVarsSection stepId={EXTRA_ID} />
</>
)

View File

@ -57,6 +57,8 @@ import { USER_INPUT_SCHEMA, USER_INPUT_FIELDS } from './schema'
import { getUserInputString } from 'client/models/Helper'
import { T } from 'client/constants'
import { disableFields } from 'client/utils'
export const SECTION_ID = 'USER_INPUTS'
const UserItemDraggable = styled(ListItem)(({ theme }) => ({
@ -112,8 +114,13 @@ UserInputItem.propTypes = {
UserInputItem.displayName = 'UserInputItem'
/** @returns {JSXElementConstructor} - User Inputs section */
const UserInputsSection = () => {
/**
* @param {object} props - Props
* @param {object} props.oneConfig - Config of oned.conf
* @param {boolean} props.adminGroup - User is admin or not
* @returns {JSXElementConstructor} - User Inputs section
*/
const UserInputsSection = ({ oneConfig, adminGroup }) => {
const {
formState: { errors },
} = useFormContext()
@ -158,7 +165,12 @@ const UserInputsSection = () => {
>
<FormWithSchema
cy={`${EXTRA_ID}-context-user-input`}
fields={USER_INPUT_FIELDS}
fields={disableFields(
USER_INPUT_FIELDS,
'USER_INPUTS',
oneConfig,
adminGroup
)}
rootProps={{ sx: { m: 0 } }}
/>
<Button
@ -206,4 +218,9 @@ const UserInputsSection = () => {
)
}
UserInputsSection.propTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default UserInputsSection

View File

@ -62,7 +62,7 @@ export const TABS = [
Backup,
]
const Content = ({ data, setFormData }) => {
const Content = ({ data, setFormData, oneConfig, adminGroup }) => {
const {
watch,
formState: { errors },
@ -89,12 +89,21 @@ const Content = ({ data, setFormData }) => {
name,
label: <Translate word={name} />,
renderContent: () => (
<TabContent {...{ data, setFormData, hypervisor, control }} />
<TabContent
{...{
data,
setFormData,
hypervisor,
control,
oneConfig,
adminGroup,
}}
/>
),
error: getError?.(errors[STEP_ID]),
})
),
[totalErrors, view, control]
[totalErrors, view, control, oneConfig, adminGroup]
)
return <Tabs tabs={tabs} />
@ -106,7 +115,7 @@ const Content = ({ data, setFormData }) => {
* @param {VmTemplate} vmTemplate - VM Template
* @returns {object} Optional configuration step
*/
const ExtraConfiguration = (vmTemplate) => {
const ExtraConfiguration = ({ data: vmTemplate, oneConfig, adminGroup }) => {
const initialHypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
return {
@ -118,13 +127,15 @@ const ExtraConfiguration = (vmTemplate) => {
return SCHEMA(hypervisor)
},
optionsValidate: { abortEarly: false },
content: Content,
content: (props) => Content({ ...props, oneConfig, adminGroup }),
}
}
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default ExtraConfiguration

View File

@ -20,6 +20,7 @@ import {
arrayToOptions,
filterFieldsByHypervisor,
getObjectSchemaFromFields,
disableFields,
} from 'client/utils'
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
@ -213,12 +214,28 @@ export const COMMAND = {
/**
* @param {string} [hypervisor] - VM hypervisor
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - User is admin or not
* @returns {Field[]} List of Graphics fields
*/
export const GRAPHICS_FIELDS = (hypervisor) =>
filterFieldsByHypervisor(
[TYPE, LISTEN, PORT, KEYMAP, CUSTOM_KEYMAP, PASSWD, RANDOM_PASSWD, COMMAND],
hypervisor
export const GRAPHICS_FIELDS = (hypervisor, oneConfig, adminGroup) =>
disableFields(
filterFieldsByHypervisor(
[
TYPE,
LISTEN,
PORT,
KEYMAP,
CUSTOM_KEYMAP,
PASSWD,
RANDOM_PASSWD,
COMMAND,
],
hypervisor
),
'GRAPHICS',
oneConfig,
adminGroup
)
/** @type {ObjectSchema} Graphics schema */

View File

@ -30,7 +30,7 @@ import { T } from 'client/constants'
export const TAB_ID = ['GRAPHICS', INPUT_ID, PCI_ID]
const InputOutput = ({ hypervisor }) => (
const InputOutput = ({ hypervisor, oneConfig, adminGroup }) => (
<Stack
display="grid"
gap="1em"
@ -38,12 +38,22 @@ const InputOutput = ({ hypervisor }) => (
>
<FormWithSchema
cy={`${EXTRA_ID}-io-graphics`}
fields={GRAPHICS_FIELDS(hypervisor)}
fields={GRAPHICS_FIELDS(hypervisor, oneConfig, adminGroup)}
legend={T.Graphics}
id={EXTRA_ID}
/>
<InputsSection stepId={EXTRA_ID} hypervisor={hypervisor} />
<PciDevicesSection stepId={EXTRA_ID} hypervisor={hypervisor} />
<InputsSection
stepId={EXTRA_ID}
hypervisor={hypervisor}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
<PciDevicesSection
stepId={EXTRA_ID}
hypervisor={hypervisor}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
</Stack>
)
@ -52,6 +62,8 @@ InputOutput.propTypes = {
setFormData: PropTypes.func,
hypervisor: PropTypes.string,
control: PropTypes.object,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
InputOutput.displayName = 'InputOutput'

View File

@ -21,6 +21,7 @@ import {
arrayToOptions,
filterFieldsByHypervisor,
getValidationFromFields,
disableFields,
} from 'client/utils'
import {
T,
@ -72,10 +73,17 @@ const BUS = {
/**
* @param {string} [hypervisor] - VM hypervisor
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - User is admin or not
* @returns {Field[]} List of Graphic inputs fields
*/
export const INPUTS_FIELDS = (hypervisor) =>
filterFieldsByHypervisor([TYPE, BUS], hypervisor)
export const INPUTS_FIELDS = (hypervisor, oneConfig, adminGroup) =>
disableFields(
filterFieldsByHypervisor([TYPE, BUS], hypervisor),
'INPUT',
oneConfig,
adminGroup
)
/** @type {ObjectSchema} Graphic input object schema */
export const INPUT_SCHEMA = object(getValidationFromFields([TYPE, BUS]))

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import { ReactElement, useCallback, memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Stack, FormControl, Divider, Button, IconButton } from '@mui/material'
import { Stack, FormControl, Divider, Button } from '@mui/material'
import List from '@mui/material/List'
import ListItem from '@mui/material/ListItem'
import ListItemText from '@mui/material/ListItemText'
@ -24,7 +24,7 @@ import { useFieldArray, useForm, FormProvider } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import { FormWithSchema, Legend } from 'client/components/Forms'
import { Translate } from 'client/components/HOC'
import { Tr, Translate } from 'client/components/HOC'
import {
INPUTS_FIELDS,
@ -33,7 +33,9 @@ import {
busTypeIcons,
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/schema'
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(
@ -41,10 +43,15 @@ const InputsSection = memo(
* @param {object} props - Props
* @param {string} [props.stepId] - ID of the step the section belongs to
* @param {HYPERVISORS} props.hypervisor - VM hypervisor
* @param {object} props.oneConfig - Config of oned.conf
* @param {boolean} props.adminGroup - User is admin or not
* @returns {ReactElement} - Inputs section
*/
({ stepId, hypervisor }) => {
const fields = useMemo(() => INPUTS_FIELDS(hypervisor), [hypervisor])
({ stepId, hypervisor, oneConfig, adminGroup }) => {
const fields = useMemo(
() => INPUTS_FIELDS(hypervisor, oneConfig, adminGroup),
[hypervisor]
)
const {
fields: inputs,
@ -112,13 +119,26 @@ const InputsSection = memo(
const busIcon = busTypeIcons[BUS]
const busInfo = `${BUS}`
// Disable action if the nic has a restricted attribute on the template
const disabledAction =
!adminGroup &&
hasRestrictedAttributes(
{ id, TYPE, BUS },
'INPUT',
oneConfig?.VM_RESTRICTED_ATTR
)
const tooltip = !disabledAction ? null : Tr(T.DetachRestricted)
return (
<ListItem
key={id}
secondaryAction={
<IconButton onClick={() => remove(index)}>
<DeleteCircledOutline />
</IconButton>
<SubmitButton
onClick={() => remove(index)}
icon=<DeleteCircledOutline />
disabled={disabledAction}
tooltip={tooltip}
/>
}
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
>
@ -151,6 +171,8 @@ const InputsSection = memo(
InputsSection.propTypes = {
stepId: PropTypes.string,
hypervisor: PropTypes.string,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
InputsSection.displayName = 'InputsSection'

View File

@ -22,6 +22,7 @@ import {
arrayToOptions,
filterFieldsByHypervisor,
getObjectSchemaFromFields,
disableFields,
} from 'client/utils'
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
@ -153,12 +154,19 @@ const TYPE_FIELD = { ...commonHiddenFieldProps('TYPE') }
/**
* @param {string} [hypervisor] - VM hypervisor
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - User is admin or not
* @returns {Field[]} List of Graphic inputs fields
*/
export const PCI_FIELDS = (hypervisor) =>
filterFieldsByHypervisor(
[NAME_FIELD, PROFILE_FIELD, DEVICE_FIELD, VENDOR_FIELD, CLASS_FIELD],
hypervisor
export const PCI_FIELDS = (hypervisor, oneConfig, adminGroup) =>
disableFields(
filterFieldsByHypervisor(
[NAME_FIELD, PROFILE_FIELD, DEVICE_FIELD, VENDOR_FIELD, CLASS_FIELD],
hypervisor
),
'PCI',
oneConfig,
adminGroup
)
/** @type {ObjectSchema} PCI devices object schema */

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import { ReactElement, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Stack, FormControl, Divider, Button, IconButton } from '@mui/material'
import { Stack, FormControl, Divider, Button } from '@mui/material'
import List from '@mui/material/List'
import ListItem from '@mui/material/ListItem'
import ListItemText from '@mui/material/ListItemText'
@ -25,7 +25,7 @@ import { yupResolver } from '@hookform/resolvers/yup'
import { useGetHostsQuery } from 'client/features/OneApi/host'
import { FormWithSchema, Legend } from 'client/components/Forms'
import { Translate } from 'client/components/HOC'
import { Tr, Translate } from 'client/components/HOC'
import { getPciDevices } from 'client/models/Host'
import {
@ -34,16 +34,21 @@ import {
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/schema'
import { T, HYPERVISORS } from 'client/constants'
import { hasRestrictedAttributes } from 'client/utils'
import SubmitButton from 'client/components/FormControl/SubmitButton'
export const SECTION_ID = 'PCI'
/**
* @param {object} props - Props
* @param {string} [props.stepId] - ID of the step the section belongs to
* @param {HYPERVISORS} props.hypervisor - VM hypervisor
* @param {object} props.oneConfig - Config of oned.conf
* @param {boolean} props.adminGroup - User is admin or not
* @returns {ReactElement} - Inputs section
*/
const PciDevicesSection = ({ stepId, hypervisor }) => {
const fields = useMemo(() => PCI_FIELDS(hypervisor))
const PciDevicesSection = ({ stepId, hypervisor, oneConfig, adminGroup }) => {
const fields = useMemo(() => PCI_FIELDS(hypervisor, oneConfig, adminGroup))
const { data: hosts = [] } = useGetHostsQuery()
const pciDevicesAvailable = useMemo(
@ -121,13 +126,26 @@ const PciDevicesSection = ({ stepId, hypervisor }) => {
secondaryFields.push(`${T.Profile}: ${PROFILE}`)
}
// Disable action if the nic has a restricted attribute on the template
const disabledAction =
!adminGroup &&
hasRestrictedAttributes(
{ id, DEVICE, VENDOR, CLASS, PROFILE, ...rest },
'PCI',
oneConfig?.VM_RESTRICTED_ATTR
)
const tooltip = !disabledAction ? null : Tr(T.DetachRestricted)
return (
<ListItem
key={id}
secondaryAction={
<IconButton onClick={() => remove(index)}>
<DeleteCircledOutline />
</IconButton>
<SubmitButton
onClick={() => remove(index)}
icon=<DeleteCircledOutline />
disabled={disabledAction}
tooltip={tooltip}
/>
}
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
>
@ -148,6 +166,8 @@ const PciDevicesSection = ({ stepId, hypervisor }) => {
PciDevicesSection.propTypes = {
stepId: PropTypes.string,
hypervisor: PropTypes.string,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
PciDevicesSection.displayName = 'PciDevicesSection'

View File

@ -43,7 +43,7 @@ const mapNicNameFunction = mapNameByIndex(TAB_ID[0])
const mapAliasNameFunction = mapNameByIndex(TAB_ID[1])
const mapPCINameFunction = mapNameByIndex(TAB_ID[2])
const Networking = ({ hypervisor }) => {
const Networking = ({ hypervisor, oneConfig, adminGroup }) => {
const { setValue, getValues } = useFormContext()
const {
@ -170,6 +170,8 @@ const Networking = ({ hypervisor }) => {
<AttachAction
currentNics={nics}
hypervisor={hypervisor}
oneConfig={oneConfig}
adminGroup={adminGroup}
onSubmit={handleAppend}
/>
<Stack
@ -201,11 +203,15 @@ const Networking = ({ hypervisor }) => {
<DetachAction
nic={item}
onSubmit={() => removeAndReorder(item)}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
)}
<AttachAction
nic={item}
hypervisor={hypervisor}
oneConfig={oneConfig}
adminGroup={adminGroup}
currentNics={nics}
onSubmit={(updatedNic) =>
handleUpdate(updatedNic, id, item)
@ -234,6 +240,8 @@ Networking.propTypes = {
setFormData: PropTypes.func,
hypervisor: PropTypes.string,
control: PropTypes.object,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */

View File

@ -30,27 +30,34 @@ import {
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/numa/schema'
import { T } from 'client/constants'
import { disableFields } from 'client/utils'
export const TAB_ID = 'NUMA'
const Numa = ({ hypervisor }) => {
const Numa = ({ hypervisor, oneConfig, adminGroup }) => {
const enableNuma = useWatch({ name: `${EXTRA_ID}.${ENABLE_NUMA.name}` })
return (
<>
<FormWithSchema
cy={`${EXTRA_ID}-vcpu`}
fields={[VCPU_FIELD]}
fields={disableFields([VCPU_FIELD], 'TOPOLOGY', oneConfig, adminGroup)}
id={GENERAL_ID}
/>
<FormWithSchema
cy={`${EXTRA_ID}-numa-enable`}
fields={[ENABLE_NUMA]}
fields={disableFields([ENABLE_NUMA], 'TOPOLOGY', oneConfig, adminGroup)}
id={EXTRA_ID}
/>
{enableNuma && (
<FormWithSchema
cy={`${EXTRA_ID}-numa`}
fields={NUMA_FIELDS(hypervisor)}
fields={disableFields(
NUMA_FIELDS(hypervisor),
'TOPOLOGY',
oneConfig,
adminGroup
)}
id={EXTRA_ID}
/>
)}
@ -63,6 +70,8 @@ Numa.propTypes = {
setFormData: PropTypes.func,
hypervisor: PropTypes.string,
control: PropTypes.object,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */

View File

@ -28,7 +28,7 @@ import {
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/placement/schema'
import { T } from 'client/constants'
const Placement = () => (
const Placement = ({ oneConfig, adminGroup }) => (
// TODO - Host requirements: add button to select HOST in list => ID="<id>"
// TODO - Host policy options: Packing|Stripping|Load-aware
@ -36,7 +36,7 @@ const Placement = () => (
// TODO - DS policy options: Packing|Stripping
<>
{SECTIONS.map(({ id, ...section }) => (
{SECTIONS(oneConfig, adminGroup).map(({ id, ...section }) => (
<FormWithSchema
key={id}
id={EXTRA_ID}
@ -50,6 +50,8 @@ const Placement = () => (
Placement.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
Placement.displayName = 'Placement'

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import { string } from 'yup'
import { Field, Section } from 'client/utils'
import { Field, Section, disableFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
/** @type {Field} Host requirement field */
@ -55,16 +55,26 @@ const DS_RANK_FIELD = {
}
/** @type {Section[]} Sections */
const SECTIONS = [
const SECTIONS = (oneConfig, adminGroup) => [
{
id: 'placement-host',
legend: T.Host,
fields: [HOST_REQ_FIELD, HOST_RANK_FIELD],
fields: disableFields(
[HOST_REQ_FIELD, HOST_RANK_FIELD],
'',
oneConfig,
adminGroup
),
},
{
id: 'placement-ds',
legend: T.Datastore,
fields: [DS_REQ_FIELD, DS_RANK_FIELD],
fields: disableFields(
[DS_REQ_FIELD, DS_RANK_FIELD],
'',
oneConfig,
adminGroup
),
},
]

View File

@ -32,11 +32,13 @@ import {
import { mapNameByIndex } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
import { T } from 'client/constants'
import PropTypes from 'prop-types'
export const TAB_ID = 'SCHED_ACTION'
const mapNameFunction = mapNameByIndex('SCHED_ACTION')
const ScheduleAction = () => {
const ScheduleAction = ({ oneConfig, adminGroup }) => {
const {
fields: scheduleActions,
remove,
@ -70,7 +72,12 @@ const ScheduleAction = () => {
return (
<>
<Stack flexDirection="row" gap="1em">
<CreateSchedButton relative onSubmit={handleCreateAction} />
<CreateSchedButton
relative
onSubmit={handleCreateAction}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
<CharterButton relative onSubmit={handleCreateCharter} />
</Stack>
@ -96,10 +103,14 @@ const ScheduleAction = () => {
vm={{}}
schedule={fakeValues}
onSubmit={(newAction) => handleUpdate(newAction, index)}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
<DeleteSchedButton
schedule={fakeValues}
onSubmit={() => handleRemove(index)}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
</>
}
@ -111,6 +122,13 @@ const ScheduleAction = () => {
)
}
ScheduleAction.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */
const TAB = {
id: 'sched_action',

View File

@ -42,7 +42,7 @@ export const TAB_ID = 'DISK'
const mapNameFunction = mapNameByIndex('DISK')
const Storage = ({ hypervisor }) => {
const Storage = ({ hypervisor, oneConfig, adminGroup }) => {
const { getValues, setValue } = useFormContext()
const {
fields: disks,
@ -76,6 +76,8 @@ const Storage = ({ hypervisor }) => {
<div>
<AttachAction
hypervisor={hypervisor}
oneConfig={oneConfig}
adminGroup={adminGroup}
onSubmit={(image) => append(mapNameFunction(image, disks.length))}
/>
<Stack
@ -102,11 +104,15 @@ const Storage = ({ hypervisor }) => {
<DetachAction
disk={item}
name={getDiskName(item)}
oneConfig={oneConfig}
adminGroup={adminGroup}
onSubmit={() => removeAndReorder(item?.NAME)}
/>
<AttachAction
disk={item}
hypervisor={hypervisor}
oneConfig={oneConfig}
adminGroup={adminGroup}
onSubmit={(updatedDisk) => handleUpdate(updatedDisk, index)}
/>
</>
@ -130,6 +136,8 @@ Storage.propTypes = {
setFormData: PropTypes.func,
hypervisor: PropTypes.string,
control: PropTypes.object,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */

View File

@ -33,7 +33,7 @@ let generalFeatures
export const STEP_ID = 'general'
const Content = ({ isUpdate }) => {
const Content = ({ isUpdate, oneConfig, adminGroup }) => {
const classes = useStyles()
const { view, getResourceView } = useViews()
const hypervisor = useWatch({ name: `${STEP_ID}.HYPERVISOR` })
@ -47,7 +47,7 @@ const Content = ({ isUpdate }) => {
generalFeatures = features
return (
SECTIONS(hypervisor, isUpdate, features)
SECTIONS(hypervisor, isUpdate, features, oneConfig, adminGroup)
.filter(
({ id, required }) => required || sectionsAvailable.includes(id)
)
@ -77,7 +77,7 @@ const Content = ({ isUpdate }) => {
* @param {VmTemplate} vmTemplate - VM Template
* @returns {object} General configuration step
*/
const General = (vmTemplate) => {
const General = ({ data: vmTemplate, oneConfig, adminGroup }) => {
const isUpdate = vmTemplate?.NAME
const initialHypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
@ -90,12 +90,14 @@ const General = (vmTemplate) => {
return SCHEMA(hypervisor, isUpdate, generalFeatures)
},
optionsValidate: { abortEarly: false },
content: () => Content({ isUpdate }),
content: () => Content({ isUpdate, oneConfig, adminGroup }),
}
}
Content.propTypes = {
isUpdate: PropTypes.bool,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default General

View File

@ -35,6 +35,7 @@ import {
Section,
filterFieldsByHypervisor,
getObjectSchemaFromFields,
disableFields,
} from 'client/utils'
import { T, HYPERVISORS, VmTemplateFeatures } from 'client/constants'
@ -42,60 +43,112 @@ import { T, HYPERVISORS, VmTemplateFeatures } from 'client/constants'
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
* @param {boolean} [isUpdate] - If `true`, the form is being updated
* @param {VmTemplateFeatures} [features] - Features
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - User is admin or not
* @returns {Section[]} Fields
*/
const SECTIONS = (hypervisor, isUpdate, features) =>
const SECTIONS = (hypervisor, isUpdate, features, oneConfig, adminGroup) =>
[
{
id: 'information',
legend: T.Information,
required: true,
fields: INFORMATION_FIELDS(isUpdate),
fields: disableFields(
INFORMATION_FIELDS(isUpdate),
'',
oneConfig,
adminGroup
),
},
{
id: 'hypervisor',
legend: T.Hypervisor,
required: true,
fields: [HYPERVISOR_FIELD, VROUTER_FIELD],
fields: disableFields(
[HYPERVISOR_FIELD, VROUTER_FIELD],
'',
oneConfig,
adminGroup
),
},
{
id: 'capacity',
legend: T.Memory,
fields: filterFieldsByHypervisor(MEMORY_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(MEMORY_FIELDS, hypervisor),
'',
oneConfig,
adminGroup
),
},
{
id: 'capacity',
fields: filterFieldsByHypervisor(MEMORY_RESIZE_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(MEMORY_RESIZE_FIELDS, hypervisor),
'',
oneConfig,
adminGroup
),
},
!features?.hide_cpu && {
id: 'capacity',
legend: T.PhysicalCpu,
fields: filterFieldsByHypervisor(CPU_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(CPU_FIELDS, hypervisor),
'',
oneConfig,
adminGroup
),
},
{
id: 'capacity',
legend: T.VirtualCpu,
fields: filterFieldsByHypervisor(VCPU_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(VCPU_FIELDS, hypervisor),
'',
oneConfig,
adminGroup
),
},
{
id: 'showback',
legend: T.Cost,
fields: filterFieldsByHypervisor(SHOWBACK_FIELDS(features), hypervisor),
fields: disableFields(
filterFieldsByHypervisor(SHOWBACK_FIELDS(features), hypervisor),
'',
oneConfig,
adminGroup
),
},
{
id: 'ownership',
legend: T.Ownership,
fields: filterFieldsByHypervisor(OWNERSHIP_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(OWNERSHIP_FIELDS, hypervisor),
'',
oneConfig,
adminGroup
),
},
{
id: 'vm_group',
legend: T.VMGroup,
fields: filterFieldsByHypervisor(VM_GROUP_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(VM_GROUP_FIELDS, hypervisor),
'',
oneConfig,
adminGroup
),
},
{
id: 'vcenter',
legend: T.vCenterDeployment,
fields: filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor),
'',
oneConfig,
adminGroup
),
},
].filter(Boolean)
@ -103,9 +156,11 @@ const SECTIONS = (hypervisor, isUpdate, features) =>
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
* @param {boolean} [isUpdate] - If `true`, the form is being updated
* @param {VmTemplateFeatures} [features] - Features
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - User is admin or not
* @returns {BaseSchema} Step schema
*/
const SCHEMA = (hypervisor, isUpdate, features) =>
const SCHEMA = (hypervisor, isUpdate, features, oneConfig, adminGroup) =>
getObjectSchemaFromFields(
SECTIONS(hypervisor, isUpdate, features)
.map(({ fields }) => fields)

View File

@ -32,7 +32,7 @@ let generalFeatures
export const STEP_ID = 'configuration'
const Content = ({ vmTemplate }) => {
const Content = ({ vmTemplate, oneConfig, adminGroup }) => {
const classes = useStyles()
const { view, getResourceView } = useViews()
const { getValues, setValue } = useFormContext()
@ -47,7 +47,7 @@ const Content = ({ vmTemplate }) => {
generalFeatures = features
return SECTIONS(vmTemplate, features).filter(
return SECTIONS(vmTemplate, features, oneConfig, adminGroup).filter(
({ id, required }) => required || sectionsAvailable.includes(id)
)
}, [view])
@ -84,6 +84,8 @@ const Content = ({ vmTemplate }) => {
Content.propTypes = {
vmTemplate: PropTypes.object,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/**
@ -92,12 +94,12 @@ Content.propTypes = {
* @param {VmTemplate} vmTemplate - VM Template
* @returns {object} Basic configuration step
*/
const BasicConfiguration = (vmTemplate) => ({
const BasicConfiguration = ({ data: vmTemplate, oneConfig, adminGroup }) => ({
id: STEP_ID,
label: T.Configuration,
resolver: () => SCHEMA(vmTemplate, generalFeatures),
optionsValidate: { abortEarly: false },
content: (props) => Content({ ...props, vmTemplate }),
content: (props) => Content({ ...props, vmTemplate, oneConfig, adminGroup }),
})
export default BasicConfiguration

View File

@ -29,45 +29,73 @@ import {
getObjectSchemaFromFields,
Field,
Section,
disableFields,
} from 'client/utils'
import { T, VmTemplate, VmTemplateFeatures } from 'client/constants'
/**
* @param {VmTemplate} [vmTemplate] - VM Template
* @param {VmTemplateFeatures} [features] - Features
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - User is admin or not
* @returns {Section[]} Sections
*/
const SECTIONS = (vmTemplate, features) => {
const SECTIONS = (vmTemplate, features, oneConfig, adminGroup) => {
const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
return [
{
id: 'information',
legend: T.Information,
fields: filterFieldsByHypervisor(INFORMATION_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(INFORMATION_FIELDS, hypervisor),
'',
oneConfig,
adminGroup
),
},
{
id: 'capacity',
legend: T.Capacity,
fields: filterFieldsByHypervisor(
CAPACITY_FIELDS(vmTemplate, features),
hypervisor
fields: disableFields(
filterFieldsByHypervisor(
CAPACITY_FIELDS(vmTemplate, features),
hypervisor
),
'',
oneConfig,
adminGroup
),
},
{
id: 'ownership',
legend: T.Ownership,
fields: filterFieldsByHypervisor(OWNERSHIP_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(OWNERSHIP_FIELDS, hypervisor),
'',
oneConfig,
adminGroup
),
},
{
id: 'vm_group',
legend: T.VMGroup,
fields: filterFieldsByHypervisor(VM_GROUP_FIELDS, hypervisor),
fields: disableFields(
filterFieldsByHypervisor(VM_GROUP_FIELDS, hypervisor),
'',
oneConfig,
adminGroup
),
},
{
id: 'vcenter',
legend: T.vCenterDeployment,
fields: filterFieldsByHypervisor([VCENTER_FOLDER_FIELD], hypervisor),
fields: disableFields(
filterFieldsByHypervisor([VCENTER_FOLDER_FIELD], hypervisor),
'',
oneConfig,
adminGroup
),
},
]
}

View File

@ -50,7 +50,7 @@ export const TABS = [
},
]
const Content = ({ data, setFormData, hypervisor }) => {
const Content = ({ data, setFormData, hypervisor, oneConfig, adminGroup }) => {
const {
formState: { errors },
control,
@ -74,7 +74,16 @@ const Content = ({ data, setFormData, hypervisor }) => {
name,
label: <Translate word={name} />,
renderContent: () => (
<TabContent {...{ data, setFormData, hypervisor, control }} />
<TabContent
{...{
data,
setFormData,
hypervisor,
control,
oneConfig,
adminGroup,
}}
/>
),
error: getError?.(errors[STEP_ID]),
})
@ -91,7 +100,7 @@ const Content = ({ data, setFormData, hypervisor }) => {
* @param {VmTemplate} vmTemplate - VM Template
* @returns {object} Optional configuration step
*/
const ExtraConfiguration = (vmTemplate) => {
const ExtraConfiguration = ({ data: vmTemplate, oneConfig, adminGroup }) => {
const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
return {
@ -99,7 +108,8 @@ const ExtraConfiguration = (vmTemplate) => {
label: T.AdvancedOptions,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: (props) => Content({ ...props, hypervisor }),
content: (props) =>
Content({ ...props, hypervisor, oneConfig, adminGroup }),
}
}
@ -107,6 +117,8 @@ Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
hypervisor: PropTypes.string,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default ExtraConfiguration

View File

@ -23,7 +23,7 @@ import UserInputs, {
STEP_ID as USER_INPUTS_ID,
} from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/UserInputs'
import { jsonToXml, userInputsToArray } from 'client/models/Helper'
import { createSteps, deleteObjectKeys } from 'client/utils'
import { createSteps } from 'client/utils'
const Steps = createSteps(
(vmTemplate) => {
@ -56,25 +56,6 @@ const Steps = createSteps(
[EXTRA_ID]: extraTemplate = {},
} = formData ?? {}
if (!adminGroup) {
const vmRestrictedAttributes = oneConfig?.VM_RESTRICTED_ATTR ?? []
vmRestrictedAttributes.forEach((restrictedAttr) => {
const splitedAttr = restrictedAttr.split('/')
/**
* For now, we will delete only the DISK attributes as we have to
* investigate the core behavior related to each of them (i.e.:
* Disk restricted attributes expect to be deleted, but NIC ones
* must be kept unchanged).
*
* TODO: Review each VM_RESTRICTED_ATTR behavior to implement
* the corresponding logic for them
*/
if (splitedAttr[0] !== 'DISK') return
deleteObjectKeys(splitedAttr, extraTemplate)
})
}
vmTemplate?.TEMPLATE?.OS &&
extraTemplate?.OS &&
(extraTemplate.OS = {

View File

@ -36,8 +36,19 @@ import { T } from 'client/constants'
import { useGeneralApi } from 'client/features/General'
import { jsonToXml } from 'client/models/Helper'
import { hasRestrictedAttributes } from 'client/utils'
const AttachAction = memo(
({ vmId, hypervisor, nic, currentNics, onSubmit, sx }) => {
({
vmId,
hypervisor,
nic,
currentNics,
onSubmit,
sx,
oneConfig,
adminGroup,
}) => {
const [attachNic] = useAttachNicMutation()
const handleAttachNic = async (formData) => {
@ -76,7 +87,13 @@ const AttachAction = memo(
dialogProps: { title: T.AttachNic, dataCy: 'modal-attach-nic' },
form: () =>
AttachNicForm({
stepProps: { hypervisor, nics: currentNics, defaultData: nic },
stepProps: {
hypervisor,
nics: currentNics,
defaultData: nic,
oneConfig,
adminGroup,
},
initialValues: nic,
}),
onSubmit: handleAttachNic,
@ -87,42 +104,50 @@ const AttachAction = memo(
}
)
const DetachAction = memo(({ vmId, nic, onSubmit, sx }) => {
const [detachNic] = useDetachNicMutation()
const { NIC_ID, PARENT } = nic
const isAlias = !!PARENT?.length
const DetachAction = memo(
({ vmId, nic, onSubmit, sx, oneConfig, adminGroup }) => {
const [detachNic] = useDetachNicMutation()
const { NIC_ID, PARENT } = nic
const isAlias = !!PARENT?.length
const handleDetach = async () => {
const handleDetachNic = onSubmit ?? detachNic
await handleDetachNic({ id: vmId, nic: NIC_ID })
}
const handleDetach = async () => {
const handleDetachNic = onSubmit ?? detachNic
await handleDetachNic({ id: vmId, nic: NIC_ID })
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `detach-nic-${NIC_ID}`,
icon: <Trash />,
tooltip: Tr(T.Detach),
sx,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<Translate
word={T.DetachSomething}
values={`${isAlias ? T.Alias : T.NIC} #${NIC_ID}`}
/>
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
// Disable action if the nic has a restricted attribute on the template
const disabledAction =
!adminGroup &&
hasRestrictedAttributes(nic, 'NIC', oneConfig?.VM_RESTRICTED_ATTR)
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `detach-nic-${NIC_ID}`,
icon: <Trash />,
tooltip: !disabledAction ? Tr(T.Detach) : Tr(T.DetachRestricted),
sx,
disabled: disabledAction,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<Translate
word={T.DetachSomething}
values={`${isAlias ? T.Alias : T.NIC} #${NIC_ID}`}
/>
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
},
onSubmit: handleDetach,
},
onSubmit: handleDetach,
},
]}
/>
)
})
]}
/>
)
}
)
const UpdateAction = memo(({ vmId, nic, sx }) => {
const { enqueueSuccess } = useGeneralApi()
@ -246,6 +271,8 @@ const ActionPropTypes = {
securityGroupId: PropTypes.string,
onSubmit: PropTypes.func,
sx: PropTypes.object,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
AttachAction.propTypes = ActionPropTypes

View File

@ -46,115 +46,134 @@ import { jsonToXml } from 'client/models/Helper'
import { Tr, Translate } from 'client/components/HOC'
import { T, VM_ACTIONS } from 'client/constants'
const AttachAction = memo(({ vmId, disk, hypervisor, onSubmit, sx }) => {
const [attachDisk] = useAttachDiskMutation()
const formConfig = { stepProps: { hypervisor }, initialValues: disk }
import { hasRestrictedAttributes } from 'client/utils'
const handleAttachDisk = async (formData) => {
if (onSubmit && typeof onSubmit === 'function') {
return await onSubmit(formData)
const AttachAction = memo(
({ vmId, disk, hypervisor, onSubmit, sx, oneConfig, adminGroup }) => {
const [attachDisk] = useAttachDiskMutation()
const formConfig = {
stepProps: { hypervisor, oneConfig, adminGroup },
initialValues: disk,
}
const template = jsonToXml({ DISK: formData })
await attachDisk({ id: vmId, template })
}
return (
<ButtonToTriggerForm
buttonProps={
disk
? {
'data-cy': `edit-${disk.DISK_ID}`,
icon: <Edit />,
tooltip: Tr(T.Edit),
sx,
}
: {
color: 'secondary',
'data-cy': 'add-disk',
label: T.AttachDisk,
variant: 'outlined',
sx,
}
const handleAttachDisk = async (formData) => {
if (onSubmit && typeof onSubmit === 'function') {
return await onSubmit(formData)
}
options={
disk
? [
{
dialogProps: {
title: (
<Translate word={T.EditSomething} values={[disk?.NAME]} />
),
},
form: () =>
!disk?.IMAGE && !disk?.IMAGE_ID // is volatile
? VolatileSteps(formConfig)
: ImageSteps(formConfig),
onSubmit: handleAttachDisk,
},
]
: [
{
cy: 'attach-image',
name: T.Image,
dialogProps: {
title: T.AttachImage,
dataCy: 'modal-attach-image',
},
form: () => ImageSteps(formConfig),
onSubmit: handleAttachDisk,
},
{
cy: 'attach-volatile',
name: T.Volatile,
dialogProps: {
title: T.AttachVolatile,
dataCy: 'modal-attach-volatile',
},
form: () => VolatileSteps(formConfig),
onSubmit: handleAttachDisk,
},
]
}
/>
)
})
const DetachAction = memo(({ vmId, disk, name: imageName, onSubmit, sx }) => {
const [detachDisk] = useDetachDiskMutation()
const { DISK_ID } = disk
const template = jsonToXml({ DISK: formData })
await attachDisk({ id: vmId, template })
}
const handleDetach = async () => {
const handleDetachDisk = onSubmit ?? detachDisk
await handleDetachDisk({ id: vmId, disk: DISK_ID })
return (
<ButtonToTriggerForm
buttonProps={
disk
? {
'data-cy': `edit-${disk.DISK_ID}`,
icon: <Edit />,
tooltip: Tr(T.Edit),
sx,
}
: {
color: 'secondary',
'data-cy': 'add-disk',
label: T.AttachDisk,
variant: 'outlined',
sx,
}
}
options={
disk
? [
{
dialogProps: {
title: (
<Translate word={T.EditSomething} values={[disk?.NAME]} />
),
},
form: () =>
!disk?.IMAGE && !disk?.IMAGE_ID // is volatile
? VolatileSteps(formConfig)
: ImageSteps(formConfig),
onSubmit: handleAttachDisk,
},
]
: [
{
cy: 'attach-image',
name: T.Image,
dialogProps: {
title: T.AttachImage,
dataCy: 'modal-attach-image',
},
form: () => ImageSteps(formConfig),
onSubmit: handleAttachDisk,
},
{
cy: 'attach-volatile',
name: T.Volatile,
dialogProps: {
title: T.AttachVolatile,
dataCy: 'modal-attach-volatile',
},
form: () => VolatileSteps(formConfig),
onSubmit: handleAttachDisk,
},
]
}
/>
)
}
)
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VM_ACTIONS.DETACH_DISK}-${DISK_ID}`,
icon: <Trash />,
tooltip: Tr(T.Detach),
sx,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<Translate
word={T.DetachSomething}
values={`#${DISK_ID} - ${imageName}`}
/>
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
const DetachAction = memo(
({ vmId, disk, name: imageName, onSubmit, sx, oneConfig, adminGroup }) => {
const [detachDisk] = useDetachDiskMutation()
const { DISK_ID } = disk
const handleDetach = async () => {
const handleDetachDisk = onSubmit ?? detachDisk
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
)
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VM_ACTIONS.DETACH_DISK}-${DISK_ID}`,
icon: <Trash />,
tooltip: !disabledAction ? Tr(T.Detach) : Tr(T.DetachRestricted),
sx,
disabled: disabledAction,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<Translate
word={T.DetachSomething}
values={`#${DISK_ID} - ${imageName}`}
/>
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
},
onSubmit: handleDetach,
},
onSubmit: handleDetach,
},
]}
/>
)
})
]}
/>
)
}
)
const SaveAsAction = memo(({ vmId, disk, snapshot, name: imageName, sx }) => {
const [saveAsDisk] = useSaveAsDiskMutation()
@ -382,6 +401,8 @@ const ActionPropTypes = {
name: PropTypes.string,
onSubmit: PropTypes.func,
sx: PropTypes.object,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
AttachAction.propTypes = ActionPropTypes

View File

@ -102,6 +102,8 @@ module.exports = {
Deploy: 'Deploy',
DeployServiceTemplate: 'Deploy Service Template',
Detach: 'Detach',
DetachRestricted:
'You cannot delete this resource because it has restricted attributes on this template. Please, contact with your administrator.',
DetachSomething: 'Detach: %s',
Disable: 'Disable',
Dismiss: 'Dismiss',

View File

@ -35,6 +35,18 @@ import {
import { CreateForm } from 'client/components/Forms/VmTemplate'
import { PATH } from 'client/apps/sunstone/routesOne'
import { jsonToXml, xmlToJson } from 'client/models/Helper'
import { useSystemData } from 'client/features/Auth'
import {
addTempInfo,
deleteTempInfo,
deleteRestrictedAttributes,
} from 'client/utils'
const _ = require('lodash')
/**
* Displays the creation or modification form to a VM Template.
*
@ -48,11 +60,24 @@ function CreateVmTemplate() {
const [update] = useUpdateTemplateMutation()
const [allocate] = useAllocateTemplateMutation()
const { data } = useGetTemplateQuery(
const { adminGroup, oneConfig } = useSystemData()
const { data: apiTemplateDataExtended } = useGetTemplateQuery(
{ id: templateId, extended: true },
{ skip: templateId === undefined }
)
const { data: apiTemplateData } = useGetTemplateQuery(
{ id: templateId, extended: false },
{ 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 })
@ -66,19 +91,67 @@ function CreateVmTemplate() {
history.push(PATH.TEMPLATE.VMS.LIST)
enqueueSuccess(`VM Template created - #${newTemplateId}`)
} else {
await update({ id: templateId, template: xmlTemplate }).unwrap()
/**
* #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()
history.push(PATH.TEMPLATE.VMS.LIST)
enqueueSuccess(`VM Template updated - #${templateId} ${NAME}`)
}
} catch {}
}
return templateId && !data ? (
return templateId && (!dataTemplateExtended || !dataTemplate) ? (
<SkeletonStepsForm />
) : (
<CreateForm
initialValues={data}
stepProps={data}
initialValues={dataTemplateExtended}
stepProps={{
dataTemplateExtended,
oneConfig,
adminGroup,
}}
onSubmit={onSubmit}
fallback={<SkeletonStepsForm />}
>

View File

@ -31,6 +31,16 @@ import {
import { InstantiateForm } from 'client/components/Forms/VmTemplate'
import { PATH } from 'client/apps/sunstone/routesOne'
import {
addTempInfo,
deleteTempInfo,
deleteRestrictedAttributes,
} from 'client/utils'
import { useSystemData } from 'client/features/Auth'
import { jsonToXml, xmlToJson } from 'client/models/Helper'
const _ = require('lodash')
/**
* Displays the instantiation form for a VM Template.
*
@ -43,17 +53,56 @@ function InstantiateVmTemplate() {
const { enqueueInfo } = useGeneralApi()
const [instantiate] = useInstantiateTemplateMutation()
const { data, isError } = useGetTemplateQuery(
const { adminGroup, oneConfig } = useSystemData()
const { data: apiTemplateDataExtended, isError } = useGetTemplateQuery(
{ id: templateId, extended: true },
{ skip: templateId === undefined, refetchOnMountOrArgChange: false }
{ skip: templateId === undefined }
)
const { data: apiTemplateData } = useGetTemplateQuery(
{ id: templateId, extended: false },
{ 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)
useGetUsersQuery(undefined, { refetchOnMountOrArgChange: false })
useGetGroupsQuery(undefined, { refetchOnMountOrArgChange: false })
const onSubmit = async (templates) => {
try {
await Promise.all(templates.map((t) => instantiate(t).unwrap()))
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)
}
// Transform json to xml
const xmlFinal = jsonToXml(jsonFinal)
// Modify template
t.template = xmlFinal
return instantiate(t).unwrap()
})
)
history.push(PATH.INSTANCE.VMS.LIST)
@ -67,12 +116,16 @@ function InstantiateVmTemplate() {
return <Redirect to={PATH.TEMPLATE.VMS.LIST} />
}
return !data ? (
return !dataTemplateExtended || !dataTemplate ? (
<SkeletonStepsForm />
) : (
<InstantiateForm
initialValues={data}
stepProps={data}
initialValues={dataTemplateExtended}
stepProps={{
dataTemplateExtended,
oneConfig,
adminGroup,
}}
onSubmit={onSubmit}
fallback={<SkeletonStepsForm />}
>

View File

@ -26,3 +26,4 @@ export * from 'client/utils/string'
export * from 'client/utils/number'
export * from 'client/utils/translation'
export * from 'client/utils/units'
export * from 'client/utils/restrictedAttributes'

View File

@ -0,0 +1,330 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
/**
* File to define functions that do something about restricted attributes
*/
import { v4 as uuidv4 } from 'uuid'
// Sections that are list of item
const listSections = [
'DISK',
'NIC',
'INPUT',
'PCI',
'SCHED_ACTION',
'NIC_ALIAS',
'NIC_DEFAULT',
'USER_INPUTS',
]
/**
* Add temporal info (temp ids and original data) to each item of some sections.
*
* @param {object} dataTemplate - Data of the template with extended=false
* @param {object} dataTemplateExtended - Data of the template with extended=true (basically add info about the images)
*/
export const addTempInfo = (dataTemplate, dataTemplateExtended) => {
addIds(dataTemplate?.TEMPLATE?.DISK, dataTemplateExtended?.TEMPLATE?.DISK)
// Disk is the only section that needs to add original data because the core with extended=true returns info about the images that are not on the template
addOriginalData(
dataTemplate?.TEMPLATE?.DISK,
dataTemplateExtended?.TEMPLATE?.DISK
)
addIds(dataTemplate?.TEMPLATE?.NIC, dataTemplateExtended?.TEMPLATE?.NIC)
addIds(dataTemplate?.TEMPLATE?.INPUT, dataTemplateExtended?.TEMPLATE?.INPUT)
addIds(dataTemplate?.TEMPLATE?.PCI, dataTemplateExtended?.TEMPLATE?.PCI)
addIds(
dataTemplate?.TEMPLATE?.SCHED_ACTION,
dataTemplateExtended?.TEMPLATE?.SCHED_ACTION
)
}
/**
* Delete all the temporal info of each section of a template.
*
* @param {object} dataTemplate - Template with data
*/
export const deleteTempInfo = (dataTemplate) => {
deleteTempId(dataTemplate?.DISK)
// Disk is the only section that needs to delete original data because the core with extended=true returns info about the images that are not on the template
deleteOriginalData(dataTemplate?.DISK)
deleteTempId(dataTemplate?.NIC)
deleteTempId(dataTemplate?.INPUT)
deleteTempId(dataTemplate?.PCI)
deleteTempId(dataTemplate?.SCHED_ACTION)
}
/**
* Add a temporal identifier to each item of a section.
*
* @param {object} section - Info about the template section with extended=false
* @param {object} sectionExtended - Info about the template section with extended=true (basically add info about the images)
*/
const addIds = (section, sectionExtended) => {
// Check if the section exists and it's an array or an attribute
if (section && Array.isArray(section)) {
// Iterate over each item
section.forEach((item, index) => {
// Create id
const tempId = uuidv4()
// Add id to the item of the section and the item of the extended section
item.TEMP_ID = tempId
if (sectionExtended) {
sectionExtended[index].TEMP_ID = tempId
}
})
} else if (section) {
// Create id
const tempId = uuidv4()
// Add id to the item of the section and the item of the extended section
if (sectionExtended) {
sectionExtended.TEMP_ID = tempId
}
section.TEMP_ID = tempId
}
}
/**
* Add to a section extended the data of the original section.
*
* @param {object} section - The section like is set on the template
* @param {object} sectionExtended - The section after use extended=true on the template
*/
const addOriginalData = (section, sectionExtended) => {
// Check if the section exists and it's an array or an attribute
if (section && Array.isArray(section)) {
// Iterate over each item
section.forEach((item, index) => {
// Add id to the item of the section and the item of the extended section
if (sectionExtended) {
sectionExtended[index].ORIGINAL = item
}
})
} else if (section) {
// Add id to the item of the section and the item of the extended section
if (sectionExtended) {
sectionExtended.ORIGINAL = section
}
}
}
/**
* Delete the temporal ide of each item of the section.
*
* @param {object} section - Section of a template
*/
const deleteTempId = (section) => {
// Check if the section exists and it's an array or an attribute
if (section && Array.isArray(section)) {
// Iterate and delete every temporal id
section.forEach((item) => {
delete item.TEMP_ID
})
} else if (section) {
// Delete the temporal id if it's only one object
delete section.TEMP_ID
}
}
/**
* Delete original data in a section.
*
* @param {object} section - The section where to delete the original data
*/
const deleteOriginalData = (section) => {
// Check if the section exists and it's an array or an attribute
if (section && Array.isArray(section)) {
// Iterate and delete every temporal id
section.forEach((item) => {
delete item.ORIGINAL
})
} else if (section) {
// Delete the temporal id if it's only one object
delete section.ORIGINAL
}
}
/**
* Delete the restricted attributes of a template if there are not on the original template before the user makes any modification.
*
* @param {object} data - Data modified by the user
* @param {object} originalData - Data before the user modifies it
* @param {Array} restrictedAttributes - List of restricted attributes of OpenNebula
* @returns {object}- Data without the restricted attributes that has to be removed
*/
export const deleteRestrictedAttributes = (
data,
originalData,
restrictedAttributes
) => {
// If there is no restricted attributes, do nothing
if (!restrictedAttributes) return data
// Create a map with the restricted attributes (using as key the parent, for example, DISK/SIZE will create DISK=["SIZE"])
const mapRestrictedAttributes =
mapRestrictedAttributesFunction(restrictedAttributes)
// Iterates over each key of the map of restricted attributes
Object.keys(mapRestrictedAttributes).forEach((key) => {
// Get all the restricted attributes for a key
const value = mapRestrictedAttributes[key]
// 1. If the attribute is a parent template attribute (like "NAME") delete it if it's not on the original template
// 2. If the attribute is one of the sections that could has lists (like DISK), iterate over each item and delete restricted attributes if there are not on the original template
// 3. If the attribute is a template attribute that is not a list (like "TOPOLOGY") delete it if it's not on the original template
if (key === 'PARENT') {
value
.filter((attribute) => !originalData[attribute])
.forEach((attribute) => delete data[attribute])
} else if (listSections.find((itemSection) => itemSection === key)) {
deleteRestrictedAttributesOnArraySection(
data,
originalData,
key,
restrictedAttributes
)
} else {
value
.filter(
(attribute) =>
(originalData[key] && data[key] && !originalData[key][attribute]) ||
(data[key] && !originalData[key])
)
.forEach((attribute) => delete data[key][attribute])
}
})
return data
}
/**
* Create a map of restricted attributes, where the key it's the left part if the attribute is splitted with "/" character and the child the righ part. For example, "DISK/SIZE" creates a key with "DISK" that has an array with one element "SIZE".
*
* @param {Array} restrictedAttributesArray - List of attributes
* @returns {object} - The map with the restricted attributes
*/
const mapRestrictedAttributesFunction = (restrictedAttributesArray) => {
// Creates the PARENT key
const restrictedAttributes = { PARENT: [] }
// Iterate over each attribute
restrictedAttributesArray.forEach((attribute) => {
// Get parent and child
const [parent, child] = attribute.split('/')
// Create the array if the key does not exist
if (child && !restrictedAttributes[parent]) {
restrictedAttributes[parent] = []
}
// Add to the array
if (child) {
restrictedAttributes[parent].push(child)
} else {
restrictedAttributes.PARENT.push(parent)
}
})
return restrictedAttributes
}
/**
* Delete restricted attributes for disks if there are not on the original data (#6154).
*
* @param {object} data - Data of the disks
* @param {object} originalData - Data of the disks before the user makes any changes
* @param {boolean} section - The section of the template
* @param {Array} restrictedAttributes - List of restricted attributes for DISK form
* @returns {object} - Data without disk restricted attributes that are not on the original data
*/
export const deleteRestrictedAttributesOnArraySection = (
data,
originalData,
section,
restrictedAttributes = []
) => {
// If there is no data of the section, return and exit the function
if (!data[section]) return data
// Check if the section it is an element on an array and create an array of one or more elements
const dataSection = Array.isArray(data[section])
? data[section]
: [data[section]]
// Check if the original section it is an element on an array and create an array of one or more elements
const originalDataSection =
originalData && originalData[section]
? Array.isArray(originalData[section])
? originalData[section]
: [originalData[section]]
: undefined
// Iterate over each item of the section
data[section] = dataSection.map((item) => {
// Find if the item it's on the original data
const originalItemSection = originalDataSection
? originalDataSection.find(
(originalItem) => originalItem.TEMP_ID === item.TEMP_ID
)
: undefined
// Iterate over each key of the item to check if it's a restricted attribute and delete it if it's not on the original data or it's a new item (an user cannot add a restricted attribute because it's read only field, so if there is an restricted attribute, or this attribute it's on the original data or we have to delete it because it wasn't set by the user)
Object.keys(item).forEach((key) => {
// Special case on ORIGINAL_SIZE attribute. It's no needed but in older templates could be, so if exists, copy with the original value (it's no used)
if (key === 'ORIGINAL_SIZE') {
item[key] = originalItemSection[key]
}
if (restrictedAttributes.find((attr) => attr === section + '/' + key)) {
if (!originalItemSection) delete item[key]
else if (originalItemSection && !originalItemSection[key])
delete item[key]
}
})
return item
})
return data
}
/**
* Find if a item has restricted attributes.
*
* @param {object} item - The item where to find the attribute
* @param {string} section - Section of the item
* @param {Array} restrictedAttributes - List of restricted attributes
* @returns {boolean} - True if any restricted attribute is found on the item
*/
export const hasRestrictedAttributes = (
item,
section,
restrictedAttributes = []
) => {
// Create map with restricted attributes
const mapRestrictedAttributes =
mapRestrictedAttributesFunction(restrictedAttributes)
// Find if there is a restricted attribute in the item
const restricteAttribute = mapRestrictedAttributes[section]?.find(
(attribute) => item && item[attribute]
)
return !!restricteAttribute
}

View File

@ -526,3 +526,46 @@ export const createForm =
...ensuredExtraParams,
}
}
/**
* Disable fields that are restricted attributes in oned.conf.
*
* @param {Array} fields - Fields of the form
* @param {string} nameParentAttribute - Parent name of the form
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - It he user is an admin
* @returns {Array} - New array of fields
*/
export const disableFields = (
fields = {},
nameParentAttribute,
oneConfig = {},
adminGroup = true
) => {
// Disable fields only if it is a non admin user
if (adminGroup) return fields
// Get restricted attributes
const restrictedAttributes = oneConfig?.VM_RESTRICTED_ATTR?.filter((item) =>
nameParentAttribute !== ''
? item.startsWith(nameParentAttribute)
: !item.includes('/')
).map((item) => item.split('/')[1] ?? item)
// Iterate over each field and add disabled attribute if it's a restricted attribute (almost all forms has attributes with name like "ATTR" but some of them like "PARENT.ATTR")
return fields.map((field) => {
if (
restrictedAttributes.some(
(item) =>
item === field.name || nameParentAttribute + '.' + item === field.name
)
) {
field.fieldProps = {
...field.fieldProps,
disabled: true,
}
}
return field
})
}