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

F OpenNebula#6332: Restricted attributes on images and vnets (#2832)

Co-authored-by: Tino Vázquez <cvazquez@opennebula.io>
This commit is contained in:
David 2023-11-23 14:19:53 +01:00 committed by GitHub
parent 23decf1d53
commit c9ed2fe5df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 618 additions and 262 deletions

View File

@ -29,123 +29,155 @@ import { AddRangeForm } from 'client/components/Forms/VNetwork'
import { jsonToXml } from 'client/models/Helper'
import { Tr, Translate } from 'client/components/HOC'
import { T, VN_ACTIONS } from 'client/constants'
import { T, VN_ACTIONS, RESTRICTED_ATTRIBUTES_TYPE } from 'client/constants'
const AddAddressRangeAction = memo(({ vnetId, onSubmit }) => {
const [addAR] = useAddRangeToVNetMutation()
import { hasRestrictedAttributes } from 'client/utils'
const handleAdd = async (formData) => {
if (onSubmit && typeof onSubmit === 'function') {
return await onSubmit(formData)
const AddAddressRangeAction = memo(
({ vnetId, onSubmit, oneConfig, adminGroup }) => {
const [addAR] = useAddRangeToVNetMutation()
const handleAdd = async (formData) => {
if (onSubmit && typeof onSubmit === 'function') {
return await onSubmit(formData)
}
const template = jsonToXml({ AR: formData })
await addAR({ id: vnetId, template }).unwrap()
}
const template = jsonToXml({ AR: formData })
await addAR({ id: vnetId, template }).unwrap()
}
return (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'add-ar',
startIcon: <AddIcon />,
label: T.AddressRange,
variant: 'outlined',
}}
options={[
{
dialogProps: {
title: T.AddressRange,
dataCy: 'modal-add-ar',
},
form: AddRangeForm,
onSubmit: handleAdd,
},
]}
/>
)
})
const UpdateAddressRangeAction = memo(({ vnetId, ar, onSubmit }) => {
const [updateAR] = useUpdateVNetRangeMutation()
const { AR_ID } = ar
const handleUpdate = async (formData) => {
if (onSubmit && typeof onSubmit === 'function') {
return await onSubmit(formData)
const formConfig = {
stepProps: {
vnetId,
oneConfig,
adminGroup,
restrictedAttributesType: RESTRICTED_ATTRIBUTES_TYPE.VNET,
nameParentAttribute: 'AR',
},
}
const template = jsonToXml({ AR: formData })
await updateAR({ id: vnetId, template })
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VN_ACTIONS.UPDATE_AR}-${AR_ID}`,
icon: <EditIcon />,
tooltip: T.Edit,
}}
options={[
{
dialogProps: {
title: `${Tr(T.AddressRange)}: #${AR_ID}`,
dataCy: 'modal-update-ar',
return (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'add-ar',
startIcon: <AddIcon />,
label: T.AddressRange,
variant: 'outlined',
}}
options={[
{
dialogProps: {
title: T.AddressRange,
dataCy: 'modal-add-ar',
},
form: () => AddRangeForm(formConfig),
onSubmit: handleAdd,
},
form: () =>
AddRangeForm({
initialValues: ar,
stepProps: { isUpdate: !onSubmit && AR_ID !== undefined },
}),
onSubmit: handleUpdate,
},
]}
/>
)
})
]}
/>
)
}
)
const DeleteAddressRangeAction = memo(({ vnetId, ar, onSubmit }) => {
const [removeAR] = useRemoveRangeFromVNetMutation()
const { AR_ID } = ar
const UpdateAddressRangeAction = memo(
({ vnetId, ar, onSubmit, oneConfig, adminGroup }) => {
const [updateAR] = useUpdateVNetRangeMutation()
const { AR_ID } = ar
const handleRemove = async () => {
if (onSubmit && typeof onSubmit === 'function') {
return await onSubmit(AR_ID)
const handleUpdate = async (formData) => {
if (onSubmit && typeof onSubmit === 'function') {
return await onSubmit(formData)
}
const template = jsonToXml({ AR: formData })
await updateAR({ id: vnetId, template })
}
await removeAR({ id: vnetId, address: AR_ID })
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VN_ACTIONS.DELETE_AR}-${AR_ID}`,
icon: <TrashIcon />,
tooltip: T.Delete,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<>
<Translate word={T.DeleteAddressRange} />
{`: #${AR_ID}`}
</>
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VN_ACTIONS.UPDATE_AR}-${AR_ID}`,
icon: <EditIcon />,
tooltip: T.Edit,
}}
options={[
{
dialogProps: {
title: `${Tr(T.AddressRange)}: #${AR_ID}`,
dataCy: 'modal-update-ar',
},
form: () =>
AddRangeForm({
initialValues: ar,
stepProps: {
isUpdate: !onSubmit && AR_ID !== undefined,
oneConfig,
adminGroup,
restrictedAttributesType: RESTRICTED_ATTRIBUTES_TYPE.VNET,
nameParentAttribute: 'AR',
},
}),
onSubmit: handleUpdate,
},
onSubmit: handleRemove,
},
]}
/>
)
})
]}
/>
)
}
)
const DeleteAddressRangeAction = memo(
({ vnetId, ar, onSubmit, oneConfig, adminGroup }) => {
const [removeAR] = useRemoveRangeFromVNetMutation()
const { AR_ID } = ar
const handleRemove = async () => {
if (onSubmit && typeof onSubmit === 'function') {
return await onSubmit(AR_ID)
}
await removeAR({ id: vnetId, address: AR_ID })
}
// Disable action if the disk has a restricted attribute on the template
const disabledAction =
!adminGroup &&
hasRestrictedAttributes(ar, 'AR', oneConfig?.VNET_RESTRICTED_ATTR)
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VN_ACTIONS.DELETE_AR}-${AR_ID}`,
icon: <TrashIcon />,
tooltip: !disabledAction ? Tr(T.Detach) : Tr(T.DetachRestricted),
disabled: disabledAction,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<>
<Translate word={T.DeleteAddressRange} />
{`: #${AR_ID}`}
</>
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
},
onSubmit: handleRemove,
},
]}
/>
)
}
)
const ActionPropTypes = {
vnetId: PropTypes.string,
ar: PropTypes.object,
onSubmit: PropTypes.func,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
AddAddressRangeAction.propTypes = ActionPropTypes

View File

@ -23,21 +23,28 @@ import { T } from 'client/constants'
export const STEP_ID = 'advanced'
const Content = () => (
<FormWithSchema id={STEP_ID} fields={FIELDS} cy={`${STEP_ID}`} />
const Content = (oneConfig, adminGroup) => (
<FormWithSchema
id={STEP_ID}
fields={FIELDS((oneConfig, adminGroup))}
cy={`${STEP_ID}`}
/>
)
/**
* Advanced options create image.
*
* @param {object} props - Step properties
* @param {object} props.oneConfig - Open Nebula configuration
* @param {boolean} props.adminGroup - If the user belongs to oneadmin group
* @returns {object} Advanced options configuration step
*/
const AdvancedOptions = () => ({
const AdvancedOptions = ({ oneConfig, adminGroup }) => ({
id: STEP_ID,
label: T.AdvancedOptions,
resolver: SCHEMA,
resolver: SCHEMA(oneConfig, adminGroup),
optionsValidate: { abortEarly: false },
content: Content,
content: () => Content(oneConfig, adminGroup),
})
export default AdvancedOptions

View File

@ -14,8 +14,13 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string, object, ObjectSchema } from 'yup'
import { Field, arrayToOptions, getValidationFromFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
import {
Field,
arrayToOptions,
getValidationFromFields,
disableFields,
} from 'client/utils'
import { T, INPUT_TYPES, RESTRICTED_ATTRIBUTES_TYPE } from 'client/constants'
import {
IMAGE_LOCATION_TYPES,
IMAGE_LOCATION_FIELD,
@ -156,18 +161,23 @@ export const FS = {
}
/**
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {Field[]} Fields
*/
export const FIELDS = [
DEV_PREFIX,
DEVICE,
CUSTOM_DEV_PREFIX,
FORMAT_FIELD,
FS,
CUSTOM_FORMAT,
]
export const FIELDS = (oneConfig, adminGroup) =>
disableFields(
[DEV_PREFIX, DEVICE, CUSTOM_DEV_PREFIX, FORMAT_FIELD, FS, CUSTOM_FORMAT],
'',
oneConfig,
adminGroup,
RESTRICTED_ATTRIBUTES_TYPE.IMAGE
)
/**
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = object(getValidationFromFields(FIELDS))
export const SCHEMA = (oneConfig, adminGroup) =>
object(getValidationFromFields(FIELDS(oneConfig, adminGroup)))

View File

@ -24,21 +24,28 @@ import { T } from 'client/constants'
export const STEP_ID = 'general'
const Content = () => (
<FormWithSchema id={STEP_ID} fields={FIELDS} cy={`${STEP_ID}`} />
const Content = (oneConfig, adminGroup) => (
<FormWithSchema
id={STEP_ID}
fields={FIELDS(oneConfig, adminGroup)}
cy={`${STEP_ID}`}
/>
)
/**
* General configuration about VM Template.
*
* @param {object} props - Step properties
* @param {object} props.oneConfig - Open Nebula configuration
* @param {boolean} props.adminGroup - If the user belongs to oneadmin group
* @returns {object} General configuration step
*/
const General = () => ({
const General = ({ oneConfig, adminGroup }) => ({
id: STEP_ID,
label: T.Configuration,
resolver: SCHEMA,
resolver: SCHEMA(oneConfig, adminGroup),
optionsValidate: { abortEarly: false },
content: Content,
content: () => Content(oneConfig, adminGroup),
})
export default General

View File

@ -19,6 +19,7 @@ import {
arrayToOptions,
getValidationFromFields,
upperCaseFirst,
disableFields,
} from 'client/utils'
import {
T,
@ -26,6 +27,7 @@ import {
IMAGE_TYPES_STR,
IMAGE_TYPES_FOR_IMAGES,
UNITS,
RESTRICTED_ATTRIBUTES_TYPE,
} from 'client/constants'
export const IMAGE_LOCATION_TYPES = {
@ -193,22 +195,33 @@ export const SIZEUNIT = {
}
/**
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {Field[]} Fields
*/
export const FIELDS = [
NAME,
DESCRIPTION,
TYPE,
PERSISTENT,
IMAGE_LOCATION_FIELD,
PATH_FIELD,
UPLOAD_FIELD,
SIZE,
SIZEUNIT,
]
export const FIELDS = (oneConfig, adminGroup) =>
disableFields(
[
NAME,
DESCRIPTION,
TYPE,
PERSISTENT,
IMAGE_LOCATION_FIELD,
PATH_FIELD,
UPLOAD_FIELD,
SIZE,
SIZEUNIT,
],
'',
oneConfig,
adminGroup,
RESTRICTED_ATTRIBUTES_TYPE.IMAGE
)
/**
* @param {object} [stepProps] - Step props
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = object(getValidationFromFields(FIELDS))
export const SCHEMA = (oneConfig, adminGroup) =>
object(getValidationFromFields(FIELDS(oneConfig, adminGroup)))

View File

@ -32,9 +32,11 @@ export const CUSTOM_ATTRS_ID = 'custom-attributes'
/**
* @param {object} props - Props
* @param {boolean} [props.isUpdate] - Is `true` the form will be filter immutable attributes
* @param {object} props.oneConfig - Open Nebula configuration
* @param {boolean} props.adminGroup - If the user belongs to oneadmin group
* @returns {ReactElement} Form content component
*/
const Content = ({ isUpdate }) => {
const Content = ({ isUpdate, oneConfig, adminGroup }) => {
const { setValue } = useFormContext()
const customAttrs = useWatch({ name: CUSTOM_ATTRS_ID }) || {}
@ -47,7 +49,13 @@ const Content = ({ isUpdate }) => {
return (
<Box display="grid" gap="1em">
<FormWithSchema fields={isUpdate ? MUTABLE_FIELDS : FIELDS} />
<FormWithSchema
fields={
isUpdate
? MUTABLE_FIELDS(oneConfig, adminGroup)
: FIELDS(oneConfig, adminGroup)
}
/>
<AttributePanel
collapse
askToDelete={false}
@ -63,6 +71,10 @@ const Content = ({ isUpdate }) => {
)
}
Content.propTypes = { isUpdate: PropTypes.bool }
Content.propTypes = {
isUpdate: PropTypes.bool,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default Content

View File

@ -23,8 +23,9 @@ import {
REG_V4,
REG_V6,
REG_MAC,
disableFields,
} from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
import { T, INPUT_TYPES, RESTRICTED_ATTRIBUTES_TYPE } from 'client/constants'
const AR_TYPES = {
IP4: 'IP4',
@ -183,25 +184,50 @@ const ULA_PREFIX_FIELD = {
}
/** @type {Field[]} Fields */
const FIELDS = [
TYPE_FIELD,
IP_FIELD,
MAC_FIELD,
IP6_FIELD,
SIZE_FIELD,
GLOBAL_PREFIX_FIELD,
PREFIX_LENGTH_FIELD,
ULA_PREFIX_FIELD,
]
const FIELDS = (oneConfig, adminGroup) =>
disableFields(
[
TYPE_FIELD,
IP_FIELD,
MAC_FIELD,
IP6_FIELD,
SIZE_FIELD,
GLOBAL_PREFIX_FIELD,
PREFIX_LENGTH_FIELD,
ULA_PREFIX_FIELD,
],
'AR',
oneConfig,
adminGroup,
RESTRICTED_ATTRIBUTES_TYPE.VNET
)
const MUTABLE_FIELDS = [SIZE_FIELD]
/**
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {Array} - Mutable fields
*/
const MUTABLE_FIELDS = (oneConfig, adminGroup) =>
disableFields(
[SIZE_FIELD],
'AR',
oneConfig,
adminGroup,
RESTRICTED_ATTRIBUTES_TYPE.VNET
)
/**
* @param {object} stepProps - Step props
* @param {boolean} stepProps.isUpdate - If true the form is to update the AR
* @param {object} stepProps.oneConfig - Open Nebula configuration
* @param {boolean} stepProps.adminGroup - If the user belongs to oneadmin group
* @returns {BaseSchema} Schema
*/
const SCHEMA = ({ isUpdate }) =>
getObjectSchemaFromFields([...(isUpdate ? MUTABLE_FIELDS : FIELDS)])
const SCHEMA = ({ isUpdate, oneConfig, adminGroup }) =>
getObjectSchemaFromFields([
...(isUpdate
? MUTABLE_FIELDS(oneConfig, adminGroup)
: FIELDS(oneConfig, adminGroup)),
])
export { FIELDS, MUTABLE_FIELDS, SCHEMA }

View File

@ -37,7 +37,7 @@ export const TAB_ID = 'AR'
const mapNameFunction = mapNameByIndex('AR')
const AddressesContent = () => {
const AddressesContent = ({ oneConfig, adminGroup }) => {
const {
fields: addresses,
remove,
@ -63,7 +63,11 @@ const AddressesContent = () => {
return (
<>
<Stack flexDirection="row" gap="1em">
<AddAddressRangeAction onSubmit={handleCreateAction} />
<AddAddressRangeAction
onSubmit={handleCreateAction}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
</Stack>
<Stack
@ -87,6 +91,8 @@ const AddressesContent = () => {
vm={{}}
ar={fakeValues}
onSubmit={(updatedAr) => handleUpdate(updatedAr, index)}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
<DeleteAddressRangeAction
ar={fakeValues}
@ -102,16 +108,25 @@ const AddressesContent = () => {
)
}
const Content = ({ isUpdate }) =>
AddressesContent.propTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
const Content = ({ isUpdate, oneConfig, adminGroup }) =>
isUpdate ? (
<Typography variant="subtitle2">
<Translate word={T.DisabledAddressRangeInForm} />
</Typography>
) : (
<AddressesContent />
<AddressesContent oneConfig={oneConfig} adminGroup={adminGroup} />
)
Content.propTypes = { isUpdate: PropTypes.bool }
Content.propTypes = {
isUpdate: PropTypes.bool,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */
const TAB = {

View File

@ -14,7 +14,7 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import ContextIcon from 'iconoir-react/dist/Folder'
import PropTypes from 'prop-types'
import {
TabType,
STEP_ID as EXTRA_ID,
@ -25,20 +25,29 @@ import CustomAttributes from 'client/components/Forms/VNetwork/CreateForm/Steps/
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { T } from 'client/constants'
const ContextContent = () => (
const ContextContent = ({ oneConfig, adminGroup }) => (
<>
<FormWithSchema id={EXTRA_ID} cy="context" fields={FIELDS} />
<FormWithSchema
id={EXTRA_ID}
cy="context"
fields={FIELDS(oneConfig, adminGroup)}
/>
<CustomAttributes />
</>
)
ContextContent.propTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */
const TAB = {
id: 'context',
name: T.Context,
icon: ContextIcon,
Content: ContextContent,
getError: (error) => FIELDS.some(({ name }) => error?.[name]),
getError: (error) => FIELDS().some(({ name }) => error?.[name]),
}
export default TAB

View File

@ -15,8 +15,19 @@
* ------------------------------------------------------------------------- */
import { string } from 'yup'
import { Field, arrayToOptions, getObjectSchemaFromFields } from 'client/utils'
import { T, INPUT_TYPES, VNET_METHODS, VNET_METHODS6 } from 'client/constants'
import {
Field,
arrayToOptions,
getObjectSchemaFromFields,
disableFields,
} from 'client/utils'
import {
T,
INPUT_TYPES,
VNET_METHODS,
VNET_METHODS6,
RESTRICTED_ATTRIBUTES_TYPE,
} from 'client/constants'
/** @type {Field} Network address field */
const NETWORK_ADDRESS_FIELD = {
@ -96,15 +107,33 @@ const IP6_METHOD_FIELD = {
validation: string().trim().notRequired(),
}
export const FIELDS = [
NETWORK_ADDRESS_FIELD,
NETWORK_MASK_FIELD,
GATEWAY_FIELD,
GATEWAY6_FIELD,
DNS_FIELD,
GUEST_MTU_FIELD,
METHOD_FIELD,
IP6_METHOD_FIELD,
]
/**
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {Array} Fields
*/
export const FIELDS = (oneConfig, adminGroup) =>
disableFields(
[
NETWORK_ADDRESS_FIELD,
NETWORK_MASK_FIELD,
GATEWAY_FIELD,
GATEWAY6_FIELD,
DNS_FIELD,
GUEST_MTU_FIELD,
METHOD_FIELD,
IP6_METHOD_FIELD,
],
'',
oneConfig,
adminGroup,
RESTRICTED_ATTRIBUTES_TYPE.VNET
)
export const SCHEMA = getObjectSchemaFromFields(FIELDS)
/**
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {object} Schema
*/
export const SCHEMA = (oneConfig, adminGroup) =>
getObjectSchemaFromFields(FIELDS(oneConfig, adminGroup))

View File

@ -45,7 +45,7 @@ export const STEP_ID = 'extra'
/** @type {TabType[]} */
export const TABS = [Addresses, Security, QoS, Context]
const Content = ({ isUpdate }) => {
const Content = ({ isUpdate, oneConfig, adminGroup }) => {
const {
watch,
formState: { errors },
@ -61,7 +61,14 @@ const Content = ({ isUpdate }) => {
...section,
name,
label: <Translate word={name} />,
renderContent: () => <TabContent isUpdate={isUpdate} driver={driver} />,
renderContent: () => (
<TabContent
isUpdate={isUpdate}
driver={driver}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
),
error: getError?.(errors[STEP_ID]),
})),
[totalErrors, driver]
@ -73,18 +80,19 @@ const Content = ({ isUpdate }) => {
/**
* Optional configuration about Virtual network.
*
* @param {VirtualNetwork} vnet - Virtual network
* @param {VirtualNetwork} data - Virtual network
* @returns {object} Optional configuration step
*/
const ExtraConfiguration = (vnet) => {
const isUpdate = vnet?.NAME !== undefined
const ExtraConfiguration = ({ data, oneConfig, adminGroup }) => {
const isUpdate = data?.NAME !== undefined
return {
id: STEP_ID,
label: T.AdvancedOptions,
resolver: SCHEMA(isUpdate),
resolver: SCHEMA(isUpdate, oneConfig, adminGroup),
optionsValidate: { abortEarly: false },
content: (formProps) => Content({ ...formProps, isUpdate }),
content: (formProps) =>
Content({ ...formProps, isUpdate, oneConfig, adminGroup }),
}
}
@ -92,6 +100,8 @@ Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
isUpdate: PropTypes.bool,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default ExtraConfiguration

View File

@ -14,6 +14,7 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import QoSIcon from 'iconoir-react/dist/DataTransferBoth'
import PropTypes from 'prop-types'
import {
STEP_ID as EXTRA_ID,
@ -27,9 +28,9 @@ import {
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { T } from 'client/constants'
const QoSContent = () => (
const QoSContent = ({ oneConfig, adminGroup }) => (
<>
{SECTIONS.map(({ id, ...section }) => (
{SECTIONS(oneConfig, adminGroup).map(({ id, ...section }) => (
<FormWithSchema
key={id}
id={EXTRA_ID}
@ -40,13 +41,18 @@ const QoSContent = () => (
</>
)
QoSContent.propTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */
const TAB = {
id: 'qos',
name: T.QoS,
icon: QoSIcon,
Content: QoSContent,
getError: (error) => FIELDS.some(({ name }) => error?.[name]),
getError: (error) => FIELDS().some(({ name }) => error?.[name]),
}
export default TAB

View File

@ -15,8 +15,13 @@
* ------------------------------------------------------------------------- */
import { ObjectSchema, string } from 'yup'
import { Field, Section, getObjectSchemaFromFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
import {
Field,
Section,
getObjectSchemaFromFields,
disableFields,
} from 'client/utils'
import { T, INPUT_TYPES, RESTRICTED_ATTRIBUTES_TYPE } from 'client/constants'
const commonFieldProps = {
type: INPUT_TYPES.TEXT,
@ -73,31 +78,39 @@ const OUTBOUND_PEAK_KB_FIELD = {
}
/** @type {Section[]} Sections */
const SECTIONS = [
const SECTIONS = (oneConfig, adminGroup) => [
{
id: 'qos-inbound',
legend: T.InboundTraffic,
fields: [
INBOUND_AVG_BW_FIELD,
INBOUND_PEAK_BW_FIELD,
INBOUND_PEAK_KB_FIELD,
],
fields: disableFields(
[INBOUND_AVG_BW_FIELD, INBOUND_PEAK_BW_FIELD, INBOUND_PEAK_KB_FIELD],
'',
oneConfig,
adminGroup,
RESTRICTED_ATTRIBUTES_TYPE.VNET
),
},
{
id: 'qos-outbound',
legend: T.OutboundTraffic,
fields: [
OUTBOUND_AVG_BW_FIELD,
OUTBOUND_PEAK_BW_FIELD,
OUTBOUND_PEAK_KB_FIELD,
],
fields: disableFields(
[OUTBOUND_AVG_BW_FIELD, OUTBOUND_PEAK_BW_FIELD, OUTBOUND_PEAK_KB_FIELD],
'',
oneConfig,
adminGroup,
RESTRICTED_ATTRIBUTES_TYPE.VNET
),
},
]
/** @type {Field[]} List of QoS fields */
const FIELDS = SECTIONS.map(({ fields }) => fields).flat()
const FIELDS = (oneConfig, adminGroup) =>
SECTIONS(oneConfig, adminGroup)
.map(({ fields }) => fields)
.flat()
/** @type {ObjectSchema} QoS schema */
const SCHEMA = getObjectSchemaFromFields(FIELDS)
const SCHEMA = (oneConfig, adminGroup) =>
getObjectSchemaFromFields(FIELDS(oneConfig, adminGroup))
export { SCHEMA, SECTIONS, FIELDS }

View File

@ -40,12 +40,14 @@ const AR_SCHEMA = object({
/**
* @param {boolean} isUpdate - If `true`, the form is being updated
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {ObjectSchema} Extra configuration schema
*/
export const SCHEMA = (isUpdate) => {
export const SCHEMA = (isUpdate, oneConfig, adminGroup) => {
const schema = object({ SECURITY_GROUPS: array().ensure() })
.concat(CONTEXT_SCHEMA)
.concat(QOS_SCHEMA)
.concat(CONTEXT_SCHEMA(oneConfig, adminGroup))
.concat(QOS_SCHEMA(oneConfig, adminGroup))
!isUpdate && schema.concat(AR_SCHEMA)

View File

@ -25,9 +25,13 @@ import {
} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration'
import { T } from 'client/constants'
import PropTypes from 'prop-types'
import { isRestrictedAttributes } from 'client/utils'
export const TAB_ID = 'SECURITY_GROUPS'
const SecurityContent = () => {
const SecurityContent = ({ oneConfig, adminGroup }) => {
const TAB_PATH = `${EXTRA_ID}.${TAB_ID}`
const { setValue } = useFormContext()
@ -43,6 +47,14 @@ const SecurityContent = () => {
setValue(TAB_PATH, newValue)
}
const readOnly =
!adminGroup &&
isRestrictedAttributes(
'SECURITY_GROUPS',
undefined,
oneConfig?.VNET_RESTRICTED_ATTR
)
return (
<SecurityGroupsTable
disableGlobalSort
@ -50,10 +62,16 @@ const SecurityContent = () => {
pageSize={5}
initialState={{ selectedRowIds }}
onSelectedRowsChange={handleSelectedRows}
readOnly={readOnly}
/>
)
}
SecurityContent.propTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */
const TAB = {
id: 'security',

View File

@ -34,17 +34,21 @@ const DRIVER_PATH = `${STEP_ID}.VN_MAD`
const IP_CONF_PATH = `${STEP_ID}.${IP_LINK_CONF_FIELD.name}`
/**
* @param {object} props - Props
* @param {boolean} [props.isUpdate] - Is `true` the form will be filter immutable attributes
* @param {boolean} isUpdate - True if it is an update operation
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {ReactElement} Form content component
*/
const Content = ({ isUpdate }) => {
const Content = (isUpdate, oneConfig, adminGroup) => {
const { setValue } = useFormContext()
const driver = useWatch({ name: DRIVER_PATH })
const ipConf = useWatch({ name: IP_CONF_PATH }) || {}
const sections = useMemo(() => SECTIONS(driver, isUpdate), [driver])
const sections = useMemo(
() => SECTIONS(driver, isUpdate, oneConfig, adminGroup),
[driver]
)
const handleChangeAttribute = (path, newValue) => {
const newConf = cloneObject(ipConf)
@ -89,12 +93,12 @@ const Content = ({ isUpdate }) => {
/**
* General configuration about Virtual network.
*
* @param {VirtualNetwork} vnet - Virtual network
* @param {VirtualNetwork} data - Virtual network
* @returns {object} General configuration step
*/
const General = (vnet) => {
const isUpdate = vnet?.NAME !== undefined
const initialDriver = vnet?.VN_MAD
const General = ({ data, oneConfig, adminGroup }) => {
const isUpdate = data?.NAME !== undefined
const initialDriver = data?.VN_MAD
return {
id: STEP_ID,
@ -102,7 +106,7 @@ const General = (vnet) => {
resolver: (formData) =>
SCHEMA(formData?.[STEP_ID]?.VN_MAD ?? initialDriver, isUpdate),
optionsValidate: { abortEarly: false },
content: () => Content({ isUpdate }),
content: () => Content(isUpdate, oneConfig, adminGroup),
}
}

View File

@ -22,26 +22,38 @@ import {
Section,
getObjectSchemaFromFields,
filterFieldsByHypervisor,
disableFields,
} from 'client/utils'
import { T, VN_DRIVERS } from 'client/constants'
import { T, VN_DRIVERS, RESTRICTED_ATTRIBUTES_TYPE } from 'client/constants'
/**
* @param {VN_DRIVERS} driver - Virtual network driver
* @param {boolean} [isUpdate] - If `true`, the form is being updated
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {Section[]} Fields
*/
const SECTIONS = (driver, isUpdate) => [
const SECTIONS = (driver, isUpdate, oneConfig, adminGroup) => [
{
id: 'information',
legend: T.Information,
fields: INFORMATION_FIELDS(isUpdate),
fields: disableFields(
INFORMATION_FIELDS(isUpdate),
'',
oneConfig,
adminGroup,
RESTRICTED_ATTRIBUTES_TYPE.VNET
),
},
{
id: 'configuration',
legend: T.Configuration,
fields: filterFieldsByHypervisor(
[DRIVER_FIELD, ...FIELDS_BY_DRIVER],
driver
fields: disableFields(
filterFieldsByHypervisor([DRIVER_FIELD, ...FIELDS_BY_DRIVER], driver),
'',
oneConfig,
adminGroup,
RESTRICTED_ATTRIBUTES_TYPE.VNET
),
},
]
@ -49,11 +61,13 @@ const SECTIONS = (driver, isUpdate) => [
/**
* @param {VN_DRIVERS} driver - Virtual network driver
* @param {boolean} [isUpdate] - If `true`, the form is being updated
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {BaseSchema} Step schema
*/
const SCHEMA = (driver, isUpdate) =>
const SCHEMA = (driver, isUpdate, oneConfig, adminGroup) =>
getObjectSchemaFromFields(
SECTIONS(driver, isUpdate)
SECTIONS(driver, isUpdate, oneConfig, adminGroup)
.map(({ schema, fields }) => schema ?? fields)
.flat()
).concat(object({ [IP_LINK_CONF_FIELD.name]: IP_LINK_CONF_FIELD.validation }))

View File

@ -75,8 +75,11 @@ const EnhancedTable = ({
noDataMessage,
messages = [],
dataDepend,
readOnly = false,
}) => {
const styles = EnhancedTableStyles()
const styles = EnhancedTableStyles({
readOnly: readOnly,
})
const isUninitialized = useMemo(
() => isLoading && data === undefined,
@ -258,7 +261,7 @@ const EnhancedTable = ({
refetch={refetch}
isLoading={isLoading}
singleSelect={singleSelect}
disableRowSelect={disableRowSelect}
disableRowSelect={disableRowSelect || readOnly}
globalActions={globalActions}
selectedRows={selectedRows}
onSelectedRowsChange={onSelectedRowsChange}
@ -294,7 +297,7 @@ const EnhancedTable = ({
{!disableGlobalSort && <GlobalSort {...useTableProps} />}
</div>
{/* SELECTED ROWS */}
{displaySelectedRows && (
{displaySelectedRows && !readOnly && (
<div>
<GlobalSelectedRows
useTableProps={useTableProps}
@ -369,7 +372,7 @@ const EnhancedTable = ({
onClick={(e) => {
typeof onRowClick === 'function' && onRowClick(original)
if (!disableRowSelect) {
if (!disableRowSelect && !readOnly) {
if (
singleSelect ||
(!singleSelect && !(e.ctrlKey || e.metaKey))
@ -427,6 +430,7 @@ EnhancedTable.propTypes = {
]),
messages: PropTypes.array,
dataDepend: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
readOnly: PropTypes.bool,
}
export * from 'client/components/Tables/Enhanced/Utils'

View File

@ -65,7 +65,12 @@ export default makeStyles(({ palette, typography, breakpoints }) => ({
padding: '0.8em',
cursor: 'pointer',
color: palette.text.primary,
backgroundColor: palette.background.paper,
/**
* @param {object} props - Properties of the styles
* @returns {object} - Background color
*/
backgroundColor: (props) =>
props.readOnly ? palette.action.hover : palette.background.paper,
fontWeight: typography.fontWeightRegular,
fontSize: '1em',
border: `1px solid ${palette.divider}`,

View File

@ -37,9 +37,16 @@ const { ADD_AR, UPDATE_AR, DELETE_AR } = VN_ACTIONS
* @param {object} props.tabProps - Tab information
* @param {string[]} props.tabProps.actions - Actions tab
* @param {string} props.id - Virtual Network id
* @param {object} props.oneConfig - Open Nebula configuration
* @param {boolean} props.adminGroup - If the user belongs to oneadmin group
* @returns {ReactElement} AR tab
*/
const AddressTab = ({ tabProps: { actions } = {}, id }) => {
const AddressTab = ({
tabProps: { actions } = {},
id,
oneConfig,
adminGroup,
}) => {
const { data: vnet } = useGetVNetworkQuery({ id })
/** @type {AddressRange[]} */
@ -47,7 +54,13 @@ const AddressTab = ({ tabProps: { actions } = {}, id }) => {
return (
<Box padding={{ sm: '0.8em' }}>
{actions[ADD_AR] === true && <AddAddressRangeAction vnetId={id} />}
{actions[ADD_AR] === true && (
<AddAddressRangeAction
vnetId={id}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
)}
<Stack gap="1em" py="0.8em">
{addressRanges.map((ar) => (
@ -58,10 +71,20 @@ const AddressTab = ({ tabProps: { actions } = {}, id }) => {
actions={
<>
{actions[UPDATE_AR] === true && (
<UpdateAddressRangeAction vnetId={id} ar={ar} />
<UpdateAddressRangeAction
vnetId={id}
ar={ar}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
)}
{actions[DELETE_AR] === true && (
<DeleteAddressRangeAction vnetId={id} ar={ar} />
<DeleteAddressRangeAction
vnetId={id}
ar={ar}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
)}
</>
}
@ -75,6 +98,8 @@ const AddressTab = ({ tabProps: { actions } = {}, id }) => {
AddressTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
AddressTab.displayName = 'AddressTab'

View File

@ -30,6 +30,8 @@ import { SecurityGroupsTable, GlobalAction } from 'client/components/Tables'
import { T, VN_ACTIONS, RESOURCE_NAMES } from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
import { isRestrictedAttributes } from 'client/utils'
const { SEC_GROUP } = RESOURCE_NAMES
const { ADD_SECGROUP } = VN_ACTIONS
@ -40,9 +42,16 @@ const { ADD_SECGROUP } = VN_ACTIONS
* @param {object} props.tabProps - Tab information
* @param {string[]} props.tabProps.actions - Actions tab
* @param {string} props.id - Virtual Network id
* @param {object} props.oneConfig - OpenNebula configuration
* @param {boolean} props.adminGroup - If the user belongs to the oneadmin group
* @returns {ReactElement} Security Groups tab
*/
const SecurityTab = ({ tabProps: { actions } = {}, id }) => {
const SecurityTab = ({
tabProps: { actions } = {},
id,
oneConfig,
adminGroup,
}) => {
const { push: redirectTo } = useHistory()
const { data: vnet } = useGetVNetworkQuery({ id })
@ -65,23 +74,31 @@ const SecurityTab = ({ tabProps: { actions } = {}, id }) => {
})
/** @type {GlobalAction[]} */
const globalActions = [
actions[ADD_SECGROUP] && {
accessor: VN_ACTIONS.ADD_SECGROUP,
dataCy: VN_ACTIONS.ADD_SECGROUP,
tooltip: T.SecurityGroup,
icon: AddIcon,
options: [
{
dialogProps: { title: T.SecurityGroup },
form: undefined,
onSubmit: () => async (formData) => {
console.log({ formData })
const globalActions =
adminGroup ||
!isRestrictedAttributes(
'SECURITY_GROUPS',
undefined,
oneConfig?.VNET_RESTRICTED_ATTR
)
? [
actions[ADD_SECGROUP] && {
accessor: VN_ACTIONS.ADD_SECGROUP,
dataCy: VN_ACTIONS.ADD_SECGROUP,
tooltip: T.SecurityGroup,
icon: AddIcon,
options: [
{
dialogProps: { title: T.SecurityGroup },
form: undefined,
onSubmit: () => async (formData) => {
console.log({ formData })
},
},
],
},
},
],
},
].filter(Boolean)
].filter(Boolean)
: undefined
return (
<Box padding={{ sm: '0.8em', overflow: 'auto' }}>
@ -100,6 +117,8 @@ const SecurityTab = ({ tabProps: { actions } = {}, id }) => {
SecurityTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
SecurityTab.displayName = 'SecurityTab'

View File

@ -18,7 +18,7 @@ import PropTypes from 'prop-types'
import { memo, useMemo } from 'react'
import { RESOURCE_NAMES } from 'client/constants'
import { useViews } from 'client/features/Auth'
import { useViews, useSystemData } from 'client/features/Auth'
import { useGetVNetworkQuery } from 'client/features/OneApi/network'
import { getAvailableInfoTabs } from 'client/models/Helper'
@ -46,11 +46,19 @@ const VNetworkTabs = memo(({ id }) => {
id,
})
const { adminGroup, oneConfig } = useSystemData()
const tabsAvailable = useMemo(() => {
const resource = RESOURCE_NAMES.VNET
const infoTabs = getResourceView(resource)?.['info-tabs'] ?? {}
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
return getAvailableInfoTabs(
infoTabs,
getTabComponent,
id,
oneConfig,
adminGroup
)
}, [view, id])
if (isError) {

View File

@ -15,3 +15,9 @@
* ------------------------------------------------------------------------- */
/** @enum {string} Base path for Open Nebula documentation */
export const DOCS_BASE_PATH = 'https://docs.opennebula.io'
export const RESTRICTED_ATTRIBUTES_TYPE = {
VM: 'VM_RESTRICTED_ATTR',
IMAGE: 'IMAGE_RESTRICTED_ATTR',
VNET: 'VNET_RESTRICTED_ATTR',
}

View File

@ -31,6 +31,10 @@ import {
import { CreateForm } from 'client/components/Forms/Image'
import { PATH } from 'client/apps/sunstone/routesOne'
import { useSystemData } from 'client/features/Auth'
const _ = require('lodash')
/**
* Displays the creation or modification form to a VM Template.
*
@ -41,6 +45,7 @@ function CreateImage() {
const [allocate] = useAllocateImageMutation()
const [upload] = useUploadImageMutation()
const { enqueueSuccess, uploadSnackbar } = useGeneralApi()
const { adminGroup, oneConfig } = useSystemData()
useGetDatastoresQuery(undefined, { refetchOnMountOrArgChange: false })
const onSubmit = async ({ template, datastore, file }) => {
@ -71,10 +76,19 @@ function CreateImage() {
} catch {}
}
return (
<CreateForm onSubmit={onSubmit} fallback={<SkeletonStepsForm />}>
return !_.isEmpty(oneConfig) ? (
<CreateForm
onSubmit={onSubmit}
stepProps={{
oneConfig,
adminGroup,
}}
fallback={<SkeletonStepsForm />}
>
{(config) => <DefaultFormStepper {...config} />}
</CreateForm>
) : (
<SkeletonStepsForm />
)
}

View File

@ -30,6 +30,10 @@ import {
import { CreateForm } from 'client/components/Forms/VNetwork'
import { PATH } from 'client/apps/sunstone/routesOne'
import { useSystemData } from 'client/features/Auth'
const _ = require('lodash')
/**
* Displays the creation or modification form to a Virtual Network.
*
@ -42,6 +46,7 @@ function CreateVirtualNetwork() {
const { enqueueSuccess } = useGeneralApi()
const [update] = useUpdateVNetMutation()
const [allocate] = useAllocateVnetMutation()
const { adminGroup, oneConfig } = useSystemData()
const { data } = useGetVNetworkQuery(
{ id: vnetId, extended: true },
@ -62,17 +67,21 @@ function CreateVirtualNetwork() {
} catch {}
}
return vnetId && !data ? (
<SkeletonStepsForm />
) : (
return !_.isEmpty(oneConfig) && ((vnetId && data) || !vnetId) ? (
<CreateForm
initialValues={data}
stepProps={data}
stepProps={{
data,
oneConfig,
adminGroup,
}}
onSubmit={onSubmit}
fallback={<SkeletonStepsForm />}
>
{(config) => <DefaultFormStepper {...config} />}
</CreateForm>
) : (
<SkeletonStepsForm />
)
}

View File

@ -328,3 +328,28 @@ export const hasRestrictedAttributes = (
return !!restricteAttribute
}
/**
* Find if an attribute is a restricted attribute.
*
* @param {object} attribute - The attribute
* @param {string} section - Section of the attribute
* @param {Array} restrictedAttributes - List of restricted attributes
* @returns {boolean} - True if it is restricted attribute
*/
export const isRestrictedAttributes = (
attribute,
section = 'PARENT',
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(
(restAttr) => restAttr === attribute
)
return !!restricteAttribute
}

View File

@ -41,6 +41,7 @@ import {
VN_DRIVERS,
INPUT_TYPES,
USER_INPUT_TYPES,
RESTRICTED_ATTRIBUTES_TYPE,
} from 'client/constants'
import { stringToBoolean } from 'client/models/Helper'
@ -505,13 +506,19 @@ export const createForm =
fields(props),
props.nameParentAttribute,
props.oneConfig,
props.adminGroup
props.adminGroup,
props && props.restrictedAttributesType
? props.restrictedAttributesType
: RESTRICTED_ATTRIBUTES_TYPE.VM
)
: disableFields(
fields,
props.nameParentAttribute,
props.oneConfig,
props.adminGroup
props.adminGroup,
props && props.restrictedAttributesType
? props.restrictedAttributesType
: RESTRICTED_ATTRIBUTES_TYPE.VM
)
: typeof fields === 'function'
? fields(props)
@ -555,23 +562,28 @@ export const createForm =
* @param {string} nameParentAttribute - Parent name of the form
* @param {object} oneConfig - Config of oned.conf
* @param {boolean} adminGroup - It he user is an admin
* @param {string} type - The type of restricted attributes use to filter
* @returns {Array} - New array of fields
*/
export const disableFields = (
fields = {},
fields = [],
nameParentAttribute,
oneConfig = {},
adminGroup = true
adminGroup = true,
type = RESTRICTED_ATTRIBUTES_TYPE.VM
) => {
// 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)
const listRestrictedAttributes = oneConfig[type]
const restrictedAttributes = listRestrictedAttributes
.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) => {

View File

@ -37,6 +37,8 @@ const ALLOWED_KEYS_ONED_CONF = [
'AUTH_MAD',
'FEDERATION',
'VM_RESTRICTED_ATTR',
'IMAGE_RESTRICTED_ATTR',
'VNET_RESTRICTED_ATTR',
]
/**