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

F #6118: Add vnet template tab (#2894)

This commit is contained in:
Jorge Miguel Lobo Escalona 2024-01-11 18:00:12 +01:00 committed by GitHub
parent 2abb92c1be
commit 15a4825445
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 3536 additions and 123 deletions

View File

@ -19,7 +19,6 @@ import {
Server as ClusterIcon,
Db as DatastoreIcon,
Archive as FileIcon,
Folder as VmGroupIcon,
Group as GroupIcon,
HardDrive as HostIcon,
BoxIso as ImageIcon,
@ -40,6 +39,7 @@ import {
User as UserIcon,
List as VDCIcon,
Shuffle as VRoutersIcons,
Folder as VmGroupIcon,
ModernTv as VmsIcons,
MinusPinAlt as ZoneIcon,
KeyAlt as ACLIcon,
@ -198,6 +198,17 @@ const VNetworkTemplates = loadable(
() => import('client/containers/VNetworkTemplates'),
{ ssr: false }
)
const InstantiateVirtualNetwork = loadable(
() => import('client/containers/VNetworkTemplates/Instantiate'),
{ ssr: false }
)
const CreateVirtualNetworkTemplate = loadable(
() => import('client/containers/VNetworkTemplates/Create'),
{ ssr: false }
)
// const NetworkTopologies = loadable(() => import('client/containers/NetworkTopologies'), { ssr: false })
const Clusters = loadable(() => import('client/containers/Clusters'), {
@ -352,7 +363,10 @@ export const PATH = {
},
VN_TEMPLATES: {
LIST: `/${RESOURCE_NAMES.VN_TEMPLATE}`,
INSTANTIATE: `/${RESOURCE_NAMES.VN_TEMPLATE}/instantiate`,
DETAIL: `/${RESOURCE_NAMES.VN_TEMPLATE}/:id`,
CREATE: `/${RESOURCE_NAMES.VN_TEMPLATE}/create`,
UPDATE: `/${RESOURCE_NAMES.VN_TEMPLATE}/update`,
},
SEC_GROUPS: {
LIST: `/${RESOURCE_NAMES.SEC_GROUP}`,
@ -639,11 +653,19 @@ const ENDPOINTS = [
icon: NetworkIcon,
Component: VirtualNetworks,
},
// JORGE
{
title: T.InstantiateVnTemplate,
description: (_, state) =>
state?.ID !== undefined && `#${state.ID} ${state.NAME}`,
path: PATH.NETWORK.VN_TEMPLATES.INSTANTIATE,
Component: InstantiateVirtualNetwork,
},
{
title: (_, state) =>
state?.ID !== undefined
? T.UpdateVirtualNetwork
: T.CreateVirtualNetwork,
? T.UpdateVirtualNetworkTemplate
: T.CreateVirtualNetworkTemplate,
description: (_, state) =>
state?.ID !== undefined && `#${state.ID} ${state.NAME}`,
path: PATH.NETWORK.VNETS.CREATE,
@ -669,6 +691,16 @@ const ENDPOINTS = [
icon: NetworkTemplateIcon,
Component: VNetworkTemplates,
},
{
title: (_, state) =>
state?.ID !== undefined
? T.UpdateVirtualNetworkTemplate
: T.CreateVirtualNetworkTemplate,
description: (_, state) =>
state?.ID !== undefined && `#${state.ID} ${state.NAME}`,
path: PATH.NETWORK.VN_TEMPLATES.CREATE,
Component: CreateVirtualNetworkTemplate,
},
{
title: T.SecurityGroups,
path: PATH.NETWORK.SEC_GROUPS.LIST,

View File

@ -13,23 +13,23 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import PropTypes from 'prop-types'
import AddIcon from 'iconoir-react/dist/Plus'
import EditIcon from 'iconoir-react/dist/Edit'
import AddIcon from 'iconoir-react/dist/Plus'
import TrashIcon from 'iconoir-react/dist/Trash'
import PropTypes from 'prop-types'
import { memo } from 'react'
import {
useAddRangeToVNetMutation,
useUpdateVNetRangeMutation,
useRemoveRangeFromVNetMutation,
} from 'client/features/OneApi/network'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import { AddRangeForm } from 'client/components/Forms/VNetwork'
import {
useAddRangeToVNetMutation,
useRemoveRangeFromVNetMutation,
useUpdateVNetRangeMutation,
} from 'client/features/OneApi/network'
import { Tr } from 'client/components/HOC'
import { RESTRICTED_ATTRIBUTES_TYPE, T, VN_ACTIONS } from 'client/constants'
import { jsonToXml } from 'client/models/Helper'
import { Tr, Translate } from 'client/components/HOC'
import { T, VN_ACTIONS, RESTRICTED_ATTRIBUTES_TYPE } from 'client/constants'
import { hasRestrictedAttributes } from 'client/utils'
@ -104,7 +104,9 @@ const UpdateAddressRangeAction = memo(
options={[
{
dialogProps: {
title: `${Tr(T.AddressRange)}: #${AR_ID}`,
title: AR_ID
? `${Tr(T.AddressRange)}: #${AR_ID}`
: `${Tr(T.AddressRange)}`,
dataCy: 'modal-update-ar',
},
form: () =>
@ -156,12 +158,9 @@ const DeleteAddressRangeAction = memo(
{
isConfirmDialog: true,
dialogProps: {
title: (
<>
<Translate word={T.DeleteAddressRange} />
{`: #${AR_ID}`}
</>
),
title: AR_ID
? `${Tr(T.DeleteAddressRange)}: #${AR_ID}`
: `${Tr(T.DeleteAddressRange)}`,
children: <p>{Tr(T.DoYouWantProceed)}</p>,
},
onSubmit: handleRemove,
@ -189,6 +188,6 @@ DeleteAddressRangeAction.displayName = 'DeleteAddressRangeAction'
export {
AddAddressRangeAction,
UpdateAddressRangeAction,
DeleteAddressRangeAction,
UpdateAddressRangeAction,
}

View File

@ -13,27 +13,27 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, memo, useMemo } from 'react'
import { Box, Link, Typography } from '@mui/material'
import PropTypes from 'prop-types'
import { ReactElement, memo, useMemo } from 'react'
import { Link as RouterLink, generatePath } from 'react-router-dom'
import { Typography, Link, Box } from '@mui/material'
import { useViews } from 'client/features/Auth'
import { rowStyles } from 'client/components/Tables/styles'
import MultipleTags from 'client/components/MultipleTags'
import { LinearProgressWithLabel } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { getARLeasesInfo } from 'client/models/VirtualNetwork'
import { PATH } from 'client/apps/sunstone/routesOne'
import { Tr, Translate } from 'client/components/HOC'
import {
T,
VirtualNetwork,
AddressRange,
VNET_THRESHOLD,
RESOURCE_NAMES,
T,
VNET_THRESHOLD,
VirtualNetwork,
} from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
import { getARLeasesInfo } from 'client/models/VirtualNetwork'
const { VNET } = RESOURCE_NAMES
@ -108,7 +108,7 @@ const AddressRangeCard = memo(
<div className={classes.main}>
<div className={classes.title}>
<Typography noWrap component="span" data-cy="id">
{`#${AR_ID}`}
{`#${AR_ID || '-'}`}
</Typography>
<span className={classes.labels}>
<MultipleTags tags={labels} limitTags={labels.length} />

View File

@ -0,0 +1,140 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Stack, Typography } from '@mui/material'
import AddressesIcon from 'iconoir-react/dist/Menu'
import PropTypes from 'prop-types'
import { useFieldArray } from 'react-hook-form'
import {
AddAddressRangeAction,
DeleteAddressRangeAction,
UpdateAddressRangeAction,
} from 'client/components/Buttons'
import { AddressRangeCard } from 'client/components/Cards'
import { Translate } from 'client/components/HOC'
import {
STEP_ID as EXTRA_ID,
TabType,
} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration'
import { mapNameByIndex } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/schema'
import { T } from 'client/constants'
export const TAB_ID = 'AR'
const mapNameFunction = mapNameByIndex('AR')
const AddressesContent = ({ oneConfig, adminGroup }) => {
const {
fields: addresses,
remove,
update,
append,
} = useFieldArray({
name: `${EXTRA_ID}.${TAB_ID}`,
keyName: 'ID',
})
const handleCreateAction = (action) => {
append(mapNameFunction(action, addresses.length))
}
const handleUpdate = (action, index) => {
update(index, mapNameFunction(action, index))
}
const handleRemove = (index) => {
remove(index)
}
return (
<>
<Stack flexDirection="row" gap="1em">
<AddAddressRangeAction
onSubmit={handleCreateAction}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
</Stack>
<Stack
pb="1em"
display="grid"
gridTemplateColumns="repeat(auto-fit, minmax(300px, 0.5fr))"
gap="1em"
mt="1em"
>
{addresses?.map((ar, index) => {
const key = ar.ID ?? ar.NAME
const fakeValues = { ...ar, AR_ID: index }
return (
<AddressRangeCard
key={key}
ar={fakeValues}
actions={
<>
<UpdateAddressRangeAction
vm={{}}
ar={fakeValues}
onSubmit={(updatedAr) => handleUpdate(updatedAr, index)}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
<DeleteAddressRangeAction
ar={fakeValues}
onSubmit={() => handleRemove(index)}
/>
</>
}
/>
)
})}
</Stack>
</>
)
}
AddressesContent.propTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
const Content = ({ isUpdate, oneConfig, adminGroup }) =>
isUpdate ? (
<Typography variant="subtitle2">
<Translate word={T.DisabledAddressRangeInForm} />
</Typography>
) : (
<AddressesContent oneConfig={oneConfig} adminGroup={adminGroup} />
)
Content.propTypes = {
isUpdate: PropTypes.bool,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */
const TAB = {
id: 'addresses',
name: T.Addresses,
icon: AddressesIcon,
Content,
getError: (error) => !!error?.[TAB_ID],
}
export default TAB

View File

@ -0,0 +1,82 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
// import { Stack } from '@mui/material'
import ClusterIcon from 'iconoir-react/dist/Server'
import { useFormContext, useWatch } from 'react-hook-form'
import { ClustersTable } from 'client/components/Tables'
import {
STEP_ID as EXTRA_ID,
TabType,
} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration'
import { T } from 'client/constants'
import PropTypes from 'prop-types'
import { isRestrictedAttributes } from 'client/utils'
export const TAB_ID = 'CLUSTER'
const ClustersContent = ({ oneConfig, adminGroup }) => {
const TAB_PATH = `${EXTRA_ID}.${TAB_ID}`
const { setValue } = useFormContext()
const clusters = useWatch({ name: TAB_PATH })
const selectedRowIds =
clusters?.reduce((res, id) => ({ ...res, [id]: true }), {}) || {}
const handleSelectedRows = (rows) => {
const newValue = rows?.map((row) => row.original.ID) || []
setValue(TAB_PATH, newValue)
}
const readOnly =
!adminGroup &&
isRestrictedAttributes(
'CLUSTER',
undefined,
oneConfig?.VNET_RESTRICTED_ATTR
)
return (
<ClustersTable
disableGlobalSort
displaySelectedRows
pageSize={5}
initialState={{ selectedRowIds }}
onSelectedRowsChange={handleSelectedRows}
readOnly={readOnly}
/>
)
}
ClustersContent.propTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */
const TAB = {
id: 'clusters',
name: T.Clusters,
icon: ClusterIcon,
Content: ClustersContent,
getError: (error) => !!error?.[TAB_ID],
}
export default TAB

View File

@ -0,0 +1,52 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import ConfigurationsIcon from 'iconoir-react/dist/Settings'
import PropTypes from 'prop-types'
import {
STEP_ID as EXTRA_ID,
TabType,
} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration'
import { FIELDS } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/schema'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { T } from 'client/constants'
const ConfigurationContent = ({ oneConfig, adminGroup }) => (
<>
<FormWithSchema
id={EXTRA_ID}
cy="configuration"
fields={FIELDS(oneConfig, adminGroup)}
/>
</>
)
ConfigurationContent.propTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */
const TAB = {
id: 'configuration',
name: T.Configuration,
icon: ConfigurationsIcon,
Content: ConfigurationContent,
getError: (error) => FIELDS().some(({ name }) => error?.[name]),
}
export default TAB

View File

@ -0,0 +1,267 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { boolean, lazy, object, string } from 'yup'
import {
INPUT_TYPES,
RESTRICTED_ATTRIBUTES_TYPE,
T,
VN_DRIVERS,
VN_DRIVERS_STR,
} from 'client/constants'
import {
Field,
OPTION_SORTERS,
arrayToOptions,
disableFields,
getObjectSchemaFromFields,
} from 'client/utils'
const {
fw,
dot1Q,
vxlan,
ovswitch,
ovswitch_vxlan: openVSwitchVXLAN,
} = VN_DRIVERS
const drivers = Object.keys(VN_DRIVERS_STR)
/** @type {Field} Driver field */
const DRIVER_FIELD = {
name: 'VN_MAD',
type: INPUT_TYPES.TOGGLE,
values: () =>
arrayToOptions(drivers, {
addEmpty: false,
getText: (key) => VN_DRIVERS_STR[key],
sorter: OPTION_SORTERS.unsort,
}),
validation: string()
.trim()
.required()
.default(() => drivers[0]),
grid: { md: 12 },
notNull: true,
}
/** @type {Field} Bridge linux field */
const BRIDGE_FIELD = {
name: 'BRIDGE',
label: T.Bridge,
tooltip: T.BridgeConcept,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.notRequired()
.default(() => undefined),
}
/** @type {Field} Physical device field */
const PHYDEV_FIELD = {
name: 'PHYDEV',
label: T.PhysicalDevice,
tooltip: T.PhysicalDeviceConcept,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.default(() => undefined)
.when(DRIVER_FIELD.name, {
is: (driver) => [dot1Q, vxlan, openVSwitchVXLAN].includes(driver),
then: (schema) => schema.required(),
}),
}
/** @type {Field} Filter MAC spoofing field */
const FILTER_MAC_SPOOFING_FIELD = {
name: 'FILTER_MAC_SPOOFING',
label: T.MacSpoofingFilter,
type: INPUT_TYPES.SWITCH,
dependOf: DRIVER_FIELD.name,
htmlType: (driver) => {
const allowedDrivers = [fw, dot1Q, vxlan, ovswitch, openVSwitchVXLAN]
return !allowedDrivers.includes(driver) && INPUT_TYPES.HIDDEN
},
validation: boolean().yesOrNo(),
grid: { md: 12 },
}
/** @type {Field} Filter IP spoofing field */
const FILTER_IP_SPOOFING_FIELD = {
name: 'FILTER_IP_SPOOFING',
label: T.IpSpoofingFilter,
type: INPUT_TYPES.SWITCH,
dependOf: DRIVER_FIELD.name,
htmlType: (driver) => {
const allowedDrivers = [fw, dot1Q, vxlan, ovswitch, openVSwitchVXLAN]
return !allowedDrivers.includes(driver) && INPUT_TYPES.HIDDEN
},
validation: boolean().yesOrNo(),
grid: { md: 12 },
}
/** @type {Field} MTU field */
const MTU_FIELD = {
name: 'MTU',
label: T.MTU,
tooltip: T.MTUConcept,
dependOf: DRIVER_FIELD.name,
htmlType: (driver) => {
const allowedDrivers = [dot1Q, vxlan, ovswitch, openVSwitchVXLAN]
return !allowedDrivers.includes(driver) && INPUT_TYPES.HIDDEN
},
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.default(() => undefined),
}
/** @type {Field} Automatic VLAN field */
const AUTOMATIC_VLAN_FIELD = {
name: 'AUTOMATIC_VLAN_ID',
label: T.AutomaticVlanId,
type: INPUT_TYPES.SWITCH,
dependOf: DRIVER_FIELD.name,
htmlType: (driver) => {
const allowedDrivers = [dot1Q, vxlan, ovswitch, openVSwitchVXLAN]
return !allowedDrivers.includes(driver) && INPUT_TYPES.HIDDEN
},
validation: lazy((_, { context }) =>
boolean()
.yesOrNo()
.default(() => context?.AUTOMATIC_VLAN_ID === '1')
),
grid: (self) => (self ? { md: 12 } : { sm: 6 }),
}
/** @type {Field} VLAN ID field */
const VLAN_ID_FIELD = {
name: 'VLAN_ID',
label: T.VlanId,
type: INPUT_TYPES.TEXT,
dependOf: [DRIVER_FIELD.name, AUTOMATIC_VLAN_FIELD.name],
htmlType: ([driver, automatic] = []) => {
const allowedDrivers = [dot1Q, vxlan, ovswitch, openVSwitchVXLAN]
if (automatic) {
return INPUT_TYPES.HIDDEN
} else if (!allowedDrivers.includes(driver)) {
return INPUT_TYPES.HIDDEN
}
},
validation: string()
.trim()
.default(() => undefined)
.when(AUTOMATIC_VLAN_FIELD.name, {
is: (automatic) => !automatic,
then: (schema) => schema.required(),
}),
grid: { sm: 6 },
}
/** @type {Field} Automatic Outer VLAN field */
const AUTOMATIC_OUTER_VLAN_ID_FIELD = {
name: 'AUTOMATIC_OUTER_VLAN_ID',
label: T.AutomaticOuterVlanId,
type: INPUT_TYPES.SWITCH,
dependOf: DRIVER_FIELD.name,
htmlType: (driver) => {
const allowedDrivers = [openVSwitchVXLAN]
return !allowedDrivers.includes(driver) && INPUT_TYPES.HIDDEN
},
validation: lazy((_, { context }) =>
boolean()
.yesOrNo()
.default(() => context?.AUTOMATIC_OUTER_VLAN_ID === '1')
),
grid: (self) => (self ? { md: 12 } : { sm: 6 }),
}
/** @type {Field} Outer VLAN ID field */
const OUTER_VLAN_ID_FIELD = {
name: 'OUTER_VLAN_ID',
label: T.OuterVlanId,
type: INPUT_TYPES.TEXT,
dependOf: [DRIVER_FIELD.name, AUTOMATIC_OUTER_VLAN_ID_FIELD.name],
htmlType: ([driver, oAutomatic] = []) => {
const allowedDrivers = [openVSwitchVXLAN]
if (oAutomatic) {
return INPUT_TYPES.HIDDEN
} else if (!allowedDrivers.includes(driver)) {
return INPUT_TYPES.HIDDEN
}
},
validation: string()
.trim()
.default(() => undefined)
.when(AUTOMATIC_OUTER_VLAN_ID_FIELD.name, {
is: (oAutomatic) => !oAutomatic,
then: (schema) => schema.required(),
}),
grid: { sm: 6 },
}
const IP_LINK_CONF_FIELD = {
name: 'IP_LINK_CONF',
validation: object().afterSubmit((conf, { parent }) => {
if (vxlan !== parent[DRIVER_FIELD.name]) return
// => string result: IP_LINK_CONF="option1=value1,option2=,..."
return Object.entries(conf || {})
.map(([k, v]) => `${k}=${v}`)
.join(',')
}),
}
/**
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {Array} Fields
*/
const FIELDS = (oneConfig, adminGroup) =>
disableFields(
[
DRIVER_FIELD,
BRIDGE_FIELD,
PHYDEV_FIELD,
FILTER_MAC_SPOOFING_FIELD,
FILTER_IP_SPOOFING_FIELD,
AUTOMATIC_VLAN_FIELD,
VLAN_ID_FIELD,
AUTOMATIC_OUTER_VLAN_ID_FIELD,
OUTER_VLAN_ID_FIELD,
MTU_FIELD,
],
'',
oneConfig,
adminGroup,
RESTRICTED_ATTRIBUTES_TYPE.VNET
)
/**
* @param {object} oneConfig - Open Nebula configuration
* @param {boolean} adminGroup - If the user belongs to oneadmin group
* @returns {object} Schema
*/
const SCHEMA = (oneConfig, adminGroup) =>
getObjectSchemaFromFields(FIELDS(oneConfig, adminGroup))
export { FIELDS, IP_LINK_CONF_FIELD, SCHEMA }

View File

@ -0,0 +1,80 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useCallback, useMemo } from 'react'
import { useFormContext, useWatch } from 'react-hook-form'
import { reach } from 'yup'
import { getUnknownVars } from 'client/components/Forms/VNTemplate/CreateForm/Steps'
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration'
import { useGeneralApi } from 'client/features/General'
import { Legend } from 'client/components/Forms'
import { AttributePanel } from 'client/components/Tabs/Common'
import { T } from 'client/constants'
/**
* Renders the context attributes to Virtual Network form.
*
* @returns {ReactElement} - Context attributes section
*/
const ContextAttrsSection = () => {
const { enqueueError } = useGeneralApi()
const { setValue, getResolver } = useFormContext()
const extraStepVars = useWatch({ name: EXTRA_ID }) || {}
const unknownVars = useMemo(
() => getUnknownVars(extraStepVars, getResolver()),
[extraStepVars]
)
const handleChangeAttribute = useCallback(
(path, newValue) => {
try {
const extraSchema = reach(getResolver(), EXTRA_ID)
// retrieve the schema for the given path
reach(extraSchema, path)
enqueueError(T.InvalidAttribute)
} catch (e) {
// When the path is not found, it means that
// the attribute is correct and we can set it
setValue(`${EXTRA_ID}.${path}`, newValue)
}
},
[setValue]
)
return (
<AttributePanel
collapse
title={
<Legend
disableGutters
data-cy={'custom-attributes'}
title={T.CustomAttributes}
/>
}
allActionsEnabled
handleAdd={handleChangeAttribute}
handleEdit={handleChangeAttribute}
handleDelete={handleChangeAttribute}
attributes={unknownVars}
filtersSpecialAttributes={false}
/>
)
}
export default ContextAttrsSection

View File

@ -0,0 +1,53 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import {
STEP_ID as EXTRA_ID,
TabType,
} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration'
import CustomAttributes from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/customAttributes'
import { FIELDS } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/schema'
import ContextIcon from 'iconoir-react/dist/Folder'
import PropTypes from 'prop-types'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { T } from 'client/constants'
const ContextContent = ({ oneConfig, adminGroup }) => (
<>
<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]),
}
export default TAB

View File

@ -0,0 +1,139 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string } from 'yup'
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 = {
name: 'NETWORK_ADDRESS',
label: T.NetworkAddress,
type: INPUT_TYPES.TEXT,
validation: string().trim().notRequired(),
}
/** @type {Field} Network mask field */
const NETWORK_MASK_FIELD = {
name: 'NETWORK_MASK',
label: T.NetworkMask,
type: INPUT_TYPES.TEXT,
validation: string().trim().notRequired(),
}
/** @type {Field} Gateway field */
const GATEWAY_FIELD = {
name: 'GATEWAY',
label: T.Gateway,
tooltip: T.GatewayConcept,
type: INPUT_TYPES.TEXT,
validation: string().trim().notRequired(),
}
/** @type {Field} Gateway for IPv6 field */
const GATEWAY6_FIELD = {
name: 'GATEWAY6',
label: T.Gateway6,
tooltip: T.Gateway6Concept,
type: INPUT_TYPES.TEXT,
validation: string().trim().notRequired(),
}
/** @type {Field} DNS field */
const DNS_FIELD = {
name: 'DNS',
label: T.DNS,
tooltip: T.DNSConcept,
type: INPUT_TYPES.TEXT,
validation: string().trim().notRequired(),
}
/** @type {Field} Guest MTU field */
const GUEST_MTU_FIELD = {
name: 'GUEST_MTU',
label: T.GuestMTU,
tooltip: T.GuestMTUConcept,
type: INPUT_TYPES.TEXT,
validation: string().trim().notRequired(),
}
/** @type {Field} Method field */
const METHOD_FIELD = {
name: 'METHOD',
label: T.NetMethod,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.entries(VNET_METHODS), {
addEmpty: 'none (Use default)',
getText: ([, text]) => text,
getValue: ([value]) => value,
}),
validation: string().trim().notRequired(),
}
/** @type {Field} Method for IPv6 field */
const IP6_METHOD_FIELD = {
name: 'IP6_METHOD',
label: T.NetMethod6,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.entries(VNET_METHODS6), {
addEmpty: 'none (Use default)',
getText: ([, text]) => text,
getValue: ([value]) => value,
}),
validation: string().trim().notRequired(),
}
/**
* @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
)
/**
* @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

@ -0,0 +1,109 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
// eslint-disable-next-line no-unused-vars
import PropTypes from 'prop-types'
// eslint-disable-next-line no-unused-vars
import { ReactElement, useMemo } from 'react'
// eslint-disable-next-line no-unused-vars
import { FieldErrors, useFormContext } from 'react-hook-form'
import Addresses from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/addresses'
import Clusters from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/clusters'
import Configuration from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration'
import Context from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context'
import QoS from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos'
import { Translate } from 'client/components/HOC'
import Tabs from 'client/components/Tabs'
import { SCHEMA } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/schema'
import { STEP_ID as GENERAL_ID } from 'client/components/Forms/VNTemplate/CreateForm/Steps/General'
import { T, VirtualNetwork } from 'client/constants'
/**
* @typedef {object} TabType
* @property {string} id - Id will be to use in view yaml to hide/display the tab
* @property {string} name - Label of tab
* @property {ReactElement} Content - Content tab
* @property {object} [icon] - Icon of tab
* @property {boolean} [immutable] - If `true`, the section will not be displayed whe is updating
* @property {function(FieldErrors):boolean} [getError] - Returns `true` if the tab contains an error in form
*/
export const STEP_ID = 'extra'
/** @type {TabType[]} */
export const TABS = [Configuration, Clusters, Addresses, QoS, Context]
const Content = ({ isUpdate, oneConfig, adminGroup }) => {
const {
watch,
formState: { errors },
} = useFormContext()
const driver = useMemo(() => watch(`${GENERAL_ID}.VN_MAD`), [])
const totalErrors = Object.keys(errors[STEP_ID] ?? {}).length
const tabs = useMemo(
() =>
TABS.map(({ Content: TabContent, name, getError, ...section }) => ({
...section,
name,
label: <Translate word={name} />,
renderContent: () => (
<TabContent
isUpdate={isUpdate}
driver={driver}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
),
error: getError?.(errors[STEP_ID]),
})),
[totalErrors, driver]
)
return <Tabs tabs={tabs} />
}
/**
* Optional configuration about Virtual network.
*
* @param {VirtualNetwork} data - Virtual network
* @returns {object} Optional configuration step
*/
const ExtraConfiguration = ({ data, oneConfig, adminGroup }) => {
const isUpdate = data?.NAME !== undefined
return {
id: STEP_ID,
label: T.AdvancedOptions,
resolver: SCHEMA(isUpdate, oneConfig, adminGroup),
optionsValidate: { abortEarly: false },
content: (formProps) =>
Content({ ...formProps, isUpdate, oneConfig, adminGroup }),
}
}
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
isUpdate: PropTypes.bool,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default ExtraConfiguration

View File

@ -0,0 +1,62 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Alert } from '@mui/material'
import {
STEP_ID as EXTRA_ID,
TabType,
} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration'
import {
FIELDS,
SECTIONS,
} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/schema'
import { Translate } from 'client/components/HOC'
import QoSIcon from 'iconoir-react/dist/DataTransferBoth'
import PropTypes from 'prop-types'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { T } from 'client/constants'
const QoSContent = ({ oneConfig, adminGroup }) => (
<>
<Alert severity="info" variant="outlined">
<Translate word={T.MessageQos} />
</Alert>
{SECTIONS(oneConfig, adminGroup).map(({ id, ...section }) => (
<FormWithSchema
key={id}
id={EXTRA_ID}
cy={`${EXTRA_ID}-${id}`}
{...section}
/>
))}
</>
)
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]),
}
export default TAB

View File

@ -0,0 +1,116 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ObjectSchema, string } from 'yup'
import {
Field,
Section,
getObjectSchemaFromFields,
disableFields,
} from 'client/utils'
import { T, INPUT_TYPES, RESTRICTED_ATTRIBUTES_TYPE } from 'client/constants'
const commonFieldProps = {
type: INPUT_TYPES.TEXT,
htmlType: 'number',
validation: string().trim().notRequired(),
}
/** @type {Field} Inbound AVG Bandwidth field */
const INBOUND_AVG_BW_FIELD = {
...commonFieldProps,
name: 'INBOUND_AVG_BW',
label: T.AverageBandwidth,
tooltip: T.InboundAverageBandwidthConcept,
}
/** @type {Field} Inbound Peak Bandwidth field */
const INBOUND_PEAK_BW_FIELD = {
...commonFieldProps,
name: 'INBOUND_PEAK_BW',
label: T.PeakBandwidth,
tooltip: T.InboundPeakBandwidthConcept,
}
/** @type {Field} Inbound Peak Burst field */
const INBOUND_PEAK_KB_FIELD = {
...commonFieldProps,
name: 'INBOUND_PEAK_KB',
label: T.PeakBurst,
tooltip: T.PeakBurstConcept,
}
/** @type {Field} Outbound AVG Bandwidth field */
const OUTBOUND_AVG_BW_FIELD = {
...commonFieldProps,
name: 'OUTBOUND_AVG_BW',
label: T.AverageBandwidth,
tooltip: T.OutboundAverageBandwidthConcept,
}
/** @type {Field} Outbound Peak Bandwidth field */
const OUTBOUND_PEAK_BW_FIELD = {
...commonFieldProps,
name: 'OUTBOUND_PEAK_BW',
label: T.PeakBandwidth,
tooltip: T.OutboundPeakBandwidthConcept,
}
/** @type {Field} Outbound Peak Burst field */
const OUTBOUND_PEAK_KB_FIELD = {
...commonFieldProps,
name: 'OUTBOUND_PEAK_KB',
label: T.PeakBurst,
tooltip: T.PeakBurstConcept,
}
/** @type {Section[]} Sections */
const SECTIONS = (oneConfig, adminGroup) => [
{
id: 'qos-inbound',
legend: T.InboundTraffic,
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: 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 = (oneConfig, adminGroup) =>
SECTIONS(oneConfig, adminGroup)
.map(({ fields }) => fields)
.flat()
/** @type {ObjectSchema} QoS schema */
const SCHEMA = (oneConfig, adminGroup) =>
getObjectSchemaFromFields(FIELDS(oneConfig, adminGroup))
export { SCHEMA, SECTIONS, FIELDS }

View File

@ -0,0 +1,57 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { array, object, ObjectSchema } from 'yup'
import { SCHEMA as CONTEXT_SCHEMA } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/schema'
import { SCHEMA as QOS_SCHEMA } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/schema'
/**
* Map name attribute if not exists.
*
* @param {string} prefixName - Prefix to add in name
* @returns {object[]} Resource object
*/
const mapNameByIndex = (prefixName) => (resource, idx) => ({
...resource,
NAME:
resource?.NAME?.startsWith(prefixName) || !resource?.NAME
? `${prefixName}${idx}`
: resource?.NAME,
})
const AR_SCHEMA = object({
AR: array()
.ensure()
.transform((actions) => actions.map(mapNameByIndex('AR'))),
})
/**
* @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, oneConfig, adminGroup) => {
const schema = object({ SECURITY_GROUPS: array().ensure() })
.concat(CONTEXT_SCHEMA(oneConfig, adminGroup))
.concat(QOS_SCHEMA(oneConfig, adminGroup))
!isUpdate && schema.concat(AR_SCHEMA)
return schema
}
export { AR_SCHEMA, mapNameByIndex }

View File

@ -0,0 +1,75 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import PropTypes from 'prop-types'
import { ReactElement, useMemo } from 'react'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import {
SCHEMA,
SECTIONS,
} from 'client/components/Forms/VNTemplate/CreateForm/Steps/General/schema'
import { T, VirtualNetwork } from 'client/constants'
export const STEP_ID = 'general'
/**
* @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, oneConfig, adminGroup) => {
const sections = useMemo(() => SECTIONS(isUpdate, oneConfig, adminGroup))
return (
<>
{sections.map(({ id, ...section }) => (
<FormWithSchema
key={id}
id={STEP_ID}
cy={`${STEP_ID}-${id}`}
{...section}
/>
))}
</>
)
}
/**
* General configuration about Virtual network.
*
* @param {VirtualNetwork} data - Virtual network
* @returns {object} General configuration step
*/
const General = ({ data, oneConfig, adminGroup }) => {
const isUpdate = data?.NAME !== undefined
return {
id: STEP_ID,
label: T.General,
resolver: () => SCHEMA(isUpdate, oneConfig, adminGroup),
optionsValidate: { abortEarly: false },
content: () => Content(isUpdate, oneConfig, adminGroup),
}
}
Content.propTypes = {
isUpdate: PropTypes.bool,
}
export default General

View File

@ -0,0 +1,58 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string } from 'yup'
import { INPUT_TYPES, T } from 'client/constants'
import { Field } from 'client/utils'
/**
* @param {boolean} isUpdate - If `true`, the form is being updated
* @returns {Field} Name field
*/
export const NAME_FIELD = (isUpdate = false) => ({
name: 'NAME',
label: T.Name,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.required()
.default(() => undefined)
// if the form is updating then display the name but not change it
.afterSubmit((name) => (isUpdate ? undefined : name)),
grid: { md: 12 },
fieldProps: { disabled: isUpdate },
})
/** @type {Field} Description field */
export const DESCRIPTION_FIELD = {
name: 'DESCRIPTION',
label: T.Description,
type: INPUT_TYPES.TEXT,
multiline: true,
validation: string()
.trim()
.notRequired()
.default(() => undefined)
.afterSubmit((description) => description),
grid: { md: 12 },
}
/**
* @param {boolean} isUpdate - If `true`, the form is being updated
* @returns {Field[]} List of information fields
*/
export const FIELDS = (isUpdate) =>
[NAME_FIELD(isUpdate), DESCRIPTION_FIELD].filter(Boolean)

View File

@ -0,0 +1,56 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { BaseSchema } from 'yup'
import { FIELDS as INFORMATION_FIELDS } from './informationSchema'
import { RESTRICTED_ATTRIBUTES_TYPE, T } from 'client/constants'
import { Section, disableFields, getObjectSchemaFromFields } from 'client/utils'
/**
* @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 = (isUpdate, oneConfig, adminGroup) => [
{
id: 'information',
legend: T.Information,
fields: disableFields(
INFORMATION_FIELDS(isUpdate),
'',
oneConfig,
adminGroup,
RESTRICTED_ATTRIBUTES_TYPE.VNET
),
},
]
/**
* @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 = (isUpdate, oneConfig, adminGroup) =>
getObjectSchemaFromFields(
SECTIONS(isUpdate, oneConfig, adminGroup)
.map(({ schema, fields }) => schema ?? fields)
.flat()
)
export { SCHEMA, SECTIONS }

View File

@ -0,0 +1,85 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ObjectSchema, reach } from 'yup'
import ExtraConfiguration, {
STEP_ID as EXTRA_ID,
} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration'
import General, {
STEP_ID as GENERAL_ID,
} from 'client/components/Forms/VNTemplate/CreateForm/Steps/General'
import { jsonToXml } from 'client/models/Helper'
import { createSteps } from 'client/utils'
const existsOnSchema = (schema, key) => {
try {
return reach(schema, key) && true
} catch (e) {
return false
}
}
/**
* @param {object} fromAttributes - Attributes to check
* @param {ObjectSchema} schema - Current form schema
* @returns {object} List of unknown attributes
*/
export const getUnknownVars = (fromAttributes = {}, schema) => {
const unknown = {}
for (const [key, value] of Object.entries(fromAttributes)) {
if (
!!value &&
!existsOnSchema(schema, `${GENERAL_ID}.${key}`) &&
!existsOnSchema(schema, `${EXTRA_ID}.${key}`)
) {
// When the path is not found, it means that
// the attribute is correct and we can set it
unknown[key] = value
}
}
return unknown
}
const Steps = createSteps([General, ExtraConfiguration], {
transformInitialValue: ({ TEMPLATE, ...vnet } = {}, schema) => {
const { AR = {}, DESCRIPTION = '' } = TEMPLATE
const initialValue = schema.cast(
{
[GENERAL_ID]: { ...vnet, DESCRIPTION },
[EXTRA_ID]: { ...TEMPLATE, AR, ...vnet },
},
{ stripUnknown: true, context: vnet }
)
initialValue[EXTRA_ID] = {
...getUnknownVars(TEMPLATE, schema),
...initialValue[EXTRA_ID],
}
return initialValue
},
transformBeforeSubmit: (formData) => {
const { [GENERAL_ID]: general = {}, [EXTRA_ID]: extra = {} } =
formData ?? {}
return jsonToXml({ ...extra, ...general })
},
})
export default Steps

View File

@ -0,0 +1,16 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
export { default } from 'client/components/Forms/VNTemplate/CreateForm/Steps'

View File

@ -0,0 +1,140 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Stack, Typography } from '@mui/material'
import AddressesIcon from 'iconoir-react/dist/Menu'
import PropTypes from 'prop-types'
import { useFieldArray } from 'react-hook-form'
import {
AddAddressRangeAction,
DeleteAddressRangeAction,
UpdateAddressRangeAction,
} from 'client/components/Buttons'
import { AddressRangeCard } from 'client/components/Cards'
import { Translate } from 'client/components/HOC'
import {
STEP_ID as EXTRA_ID,
TabType,
} from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration'
import { mapNameByIndex } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
import { T } from 'client/constants'
export const TAB_ID = 'AR'
const mapNameFunction = mapNameByIndex('AR')
const AddressesContent = ({ oneConfig, adminGroup }) => {
const {
fields: addresses,
remove,
update,
append,
} = useFieldArray({
name: `${EXTRA_ID}.${TAB_ID}`,
keyName: 'ID',
})
const handleCreateAction = (action) => {
append(mapNameFunction(action, addresses.length))
}
const handleUpdate = (action, index) => {
update(index, mapNameFunction(action, index))
}
const handleRemove = (index) => {
remove(index)
}
return (
<>
<Stack flexDirection="row" gap="1em">
<AddAddressRangeAction
onSubmit={handleCreateAction}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
</Stack>
<Stack
pb="1em"
display="grid"
gridTemplateColumns="repeat(auto-fit, minmax(300px, 0.5fr))"
gap="1em"
mt="1em"
>
{addresses?.map((ar, index) => {
const key = ar.ID ?? ar.NAME
const fakeValues = { ...ar, AR_ID: index }
return (
<AddressRangeCard
key={key}
ar={fakeValues}
actions={
<>
<UpdateAddressRangeAction
vm={{}}
ar={fakeValues}
onSubmit={(updatedAr) => handleUpdate(updatedAr, index)}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
<DeleteAddressRangeAction
ar={fakeValues}
onSubmit={() => handleRemove(index)}
/>
</>
}
/>
)
})}
</Stack>
</>
)
}
AddressesContent.propTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
const Content = ({ isUpdate, oneConfig, adminGroup }) =>
isUpdate ? (
<Typography variant="subtitle2">
<Translate word={T.DisabledAddressRangeInForm} />
</Typography>
) : (
<AddressesContent oneConfig={oneConfig} adminGroup={adminGroup} />
)
Content.propTypes = {
isUpdate: PropTypes.bool,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */
const TAB = {
id: 'addresses',
name: T.Addresses,
icon: AddressesIcon,
Content,
getError: (error) => !!error?.[TAB_ID],
}
export default TAB

View File

@ -0,0 +1,80 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useCallback, useMemo } from 'react'
import { useFormContext, useWatch } from 'react-hook-form'
import { reach } from 'yup'
import { getUnknownVars } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps'
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration'
import { useGeneralApi } from 'client/features/General'
import { Legend } from 'client/components/Forms'
import { AttributePanel } from 'client/components/Tabs/Common'
import { T } from 'client/constants'
/**
* Renders the context attributes to Virtual Network form.
*
* @returns {ReactElement} - Context attributes section
*/
const ContextAttrsSection = () => {
const { enqueueError } = useGeneralApi()
const { setValue, getResolver } = useFormContext()
const extraStepVars = useWatch({ name: EXTRA_ID }) || {}
const unknownVars = useMemo(
() => getUnknownVars(extraStepVars, getResolver()),
[extraStepVars]
)
const handleChangeAttribute = useCallback(
(path, newValue) => {
try {
const extraSchema = reach(getResolver(), EXTRA_ID)
// retrieve the schema for the given path
reach(extraSchema, path)
enqueueError(T.InvalidAttribute)
} catch (e) {
// When the path is not found, it means that
// the attribute is correct and we can set it
setValue(`${EXTRA_ID}.${path}`, newValue)
}
},
[setValue]
)
return (
<AttributePanel
collapse
title={
<Legend
disableGutters
data-cy={'custom-attributes'}
title={T.CustomAttributes}
/>
}
allActionsEnabled
handleAdd={handleChangeAttribute}
handleEdit={handleChangeAttribute}
handleDelete={handleChangeAttribute}
attributes={unknownVars}
filtersSpecialAttributes={false}
/>
)
}
export default ContextAttrsSection

View File

@ -0,0 +1,53 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import {
STEP_ID as EXTRA_ID,
TabType,
} from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration'
import CustomAttributes from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/customAttributes'
import { FIELDS } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/schema'
import ContextIcon from 'iconoir-react/dist/Folder'
import PropTypes from 'prop-types'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { T } from 'client/constants'
const ContextContent = ({ oneConfig, adminGroup }) => (
<>
<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]),
}
export default TAB

View File

@ -0,0 +1,139 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string } from 'yup'
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 = {
name: 'NETWORK_ADDRESS',
label: T.NetworkAddress,
type: INPUT_TYPES.TEXT,
validation: string().trim().notRequired(),
}
/** @type {Field} Network mask field */
const NETWORK_MASK_FIELD = {
name: 'NETWORK_MASK',
label: T.NetworkMask,
type: INPUT_TYPES.TEXT,
validation: string().trim().notRequired(),
}
/** @type {Field} Gateway field */
const GATEWAY_FIELD = {
name: 'GATEWAY',
label: T.Gateway,
tooltip: T.GatewayConcept,
type: INPUT_TYPES.TEXT,
validation: string().trim().notRequired(),
}
/** @type {Field} Gateway for IPv6 field */
const GATEWAY6_FIELD = {
name: 'GATEWAY6',
label: T.Gateway6,
tooltip: T.Gateway6Concept,
type: INPUT_TYPES.TEXT,
validation: string().trim().notRequired(),
}
/** @type {Field} DNS field */
const DNS_FIELD = {
name: 'DNS',
label: T.DNS,
tooltip: T.DNSConcept,
type: INPUT_TYPES.TEXT,
validation: string().trim().notRequired(),
}
/** @type {Field} Guest MTU field */
const GUEST_MTU_FIELD = {
name: 'GUEST_MTU',
label: T.GuestMTU,
tooltip: T.GuestMTUConcept,
type: INPUT_TYPES.TEXT,
validation: string().trim().notRequired(),
}
/** @type {Field} Method field */
const METHOD_FIELD = {
name: 'METHOD',
label: T.NetMethod,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.entries(VNET_METHODS), {
addEmpty: 'none (Use default)',
getText: ([, text]) => text,
getValue: ([value]) => value,
}),
validation: string().trim().notRequired(),
}
/** @type {Field} Method for IPv6 field */
const IP6_METHOD_FIELD = {
name: 'IP6_METHOD',
label: T.NetMethod6,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.entries(VNET_METHODS6), {
addEmpty: 'none (Use default)',
getText: ([, text]) => text,
getValue: ([value]) => value,
}),
validation: string().trim().notRequired(),
}
/**
* @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
)
/**
* @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

@ -0,0 +1,102 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
// eslint-disable-next-line no-unused-vars
import PropTypes from 'prop-types'
// eslint-disable-next-line no-unused-vars
import { ReactElement, useMemo } from 'react'
// eslint-disable-next-line no-unused-vars
import { FieldErrors, useFormContext } from 'react-hook-form'
import Addresses from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/addresses'
import Context from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context'
import Security from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/security'
import { Translate } from 'client/components/HOC'
import Tabs from 'client/components/Tabs'
import { SCHEMA } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
import { T, VirtualNetwork } from 'client/constants'
/**
* @typedef {object} TabType
* @property {string} id - Id will be to use in view yaml to hide/display the tab
* @property {string} name - Label of tab
* @property {ReactElement} Content - Content tab
* @property {object} [icon] - Icon of tab
* @property {boolean} [immutable] - If `true`, the section will not be displayed whe is updating
* @property {function(FieldErrors):boolean} [getError] - Returns `true` if the tab contains an error in form
*/
export const STEP_ID = 'extra'
/** @type {TabType[]} */
export const TABS = [Addresses, Security, Context]
const Content = ({ isUpdate, oneConfig, adminGroup }) => {
const {
formState: { errors },
} = useFormContext()
const totalErrors = Object.keys(errors[STEP_ID] ?? {}).length
const tabs = useMemo(
() =>
TABS.map(({ Content: TabContent, name, getError, ...section }) => ({
...section,
name,
label: <Translate word={name} />,
renderContent: () => (
<TabContent
isUpdate={isUpdate}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
),
error: getError?.(errors[STEP_ID]),
})),
[totalErrors]
)
return <Tabs tabs={tabs} />
}
/**
* Optional configuration about Virtual network.
*
* @param {VirtualNetwork} data - Virtual network
* @returns {object} Optional configuration step
*/
const ExtraConfiguration = ({ data, oneConfig, adminGroup }) => {
const isUpdate = data?.NAME !== undefined
return {
id: STEP_ID,
label: T.AdvancedOptions,
resolver: SCHEMA(isUpdate, oneConfig, adminGroup),
optionsValidate: { abortEarly: false },
content: (formProps) =>
Content({ ...formProps, isUpdate, oneConfig, adminGroup }),
}
}
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
isUpdate: PropTypes.bool,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default ExtraConfiguration

View File

@ -0,0 +1,56 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { array, object, ObjectSchema } from 'yup'
import { SCHEMA as CONTEXT_SCHEMA } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/schema'
/**
* Map name attribute if not exists.
*
* @param {string} prefixName - Prefix to add in name
* @returns {object[]} Resource object
*/
const mapNameByIndex = (prefixName) => (resource, idx) => ({
...resource,
NAME:
resource?.NAME?.startsWith(prefixName) || !resource?.NAME
? `${prefixName}${idx}`
: resource?.NAME,
})
const AR_SCHEMA = object({
AR: array()
.ensure()
.transform((actions) => actions.map(mapNameByIndex('AR'))),
})
/**
* @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, oneConfig, adminGroup) => {
const schema = object({ SECURITY_GROUPS: array().ensure() }).concat(
CONTEXT_SCHEMA(oneConfig, adminGroup)
)
!isUpdate && schema.concat(AR_SCHEMA)
return schema
}
export { AR_SCHEMA, mapNameByIndex }

View File

@ -0,0 +1,86 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Alert } from '@mui/material'
import {
STEP_ID as EXTRA_ID,
TabType,
} from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration'
import { Translate } from 'client/components/HOC'
import { SecurityGroupsTable } from 'client/components/Tables'
import { T } from 'client/constants'
import { isRestrictedAttributes } from 'client/utils'
import SecurityIcon from 'iconoir-react/dist/HistoricShield'
import PropTypes from 'prop-types'
import { useFormContext, useWatch } from 'react-hook-form'
export const TAB_ID = 'SECURITY_GROUPS'
const SecurityContent = ({ oneConfig, adminGroup }) => {
const TAB_PATH = `${EXTRA_ID}.${TAB_ID}`
const { setValue } = useFormContext()
const secGroups = useWatch({ name: TAB_PATH })
const selectedRowIds = secGroups?.reduce(
(res, id) => ({ ...res, [id]: true }),
{}
)
const handleSelectedRows = (rows) => {
const newValue = rows?.map((row) => row.original.ID) || []
setValue(TAB_PATH, newValue)
}
const readOnly =
!adminGroup &&
isRestrictedAttributes(
'SECURITY_GROUPS',
undefined,
oneConfig?.VNET_RESTRICTED_ATTR
)
return (
<>
<Alert severity="warning" variant="outlined">
<Translate word={T.MessageAddSecGroupDefault} />
</Alert>
<SecurityGroupsTable
disableGlobalSort
displaySelectedRows
pageSize={5}
initialState={{ selectedRowIds }}
onSelectedRowsChange={handleSelectedRows}
readOnly={readOnly}
/>
</>
)
}
SecurityContent.propTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/** @type {TabType} */
const TAB = {
id: 'security',
name: T.Security,
icon: SecurityIcon,
Content: SecurityContent,
getError: (error) => !!error?.[TAB_ID],
}
export default TAB

View File

@ -0,0 +1,105 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import PropTypes from 'prop-types'
import { useEffect, useMemo } from 'react'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import {
SCHEMA,
SECTIONS,
} from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/General/schema'
import useStyles from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/General/styles'
import { RESOURCE_NAMES, T, VmTemplate } from 'client/constants'
import { useViews } from 'client/features/Auth'
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
import { scaleVcpuByCpuFactor } from 'client/models/VirtualMachine'
import { useFormContext } from 'react-hook-form'
let generalFeatures
export const STEP_ID = 'general'
const Content = ({ vmTemplate, oneConfig, adminGroup }) => {
const classes = useStyles()
const { view, getResourceView } = useViews()
const { getValues, setValue } = useFormContext()
const resource = RESOURCE_NAMES.VM_TEMPLATE
const { features, dialogs } = getResourceView(resource)
const sections = useMemo(() => {
const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
const dialog = dialogs?.instantiate_dialog
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
generalFeatures = features
return SECTIONS(vmTemplate, features, oneConfig, adminGroup).filter(
({ id, required }) => required || sectionsAvailable.includes(id)
)
}, [view])
useEffect(() => {
if (vmTemplate?.TEMPLATE?.VCPU && features?.cpu_factor) {
const oldValues = {
...getValues(),
}
oldValues.configuration.CPU = `${scaleVcpuByCpuFactor(
vmTemplate.TEMPLATE.VCPU,
features.cpu_factor
)}`
setValue(`${STEP_ID}`, oldValues)
}
}, [])
return (
<div className={classes.root}>
{sections.map(({ id, legend, fields }) => (
<FormWithSchema
key={id}
cy={id}
rootProps={{ className: classes[id] }}
fields={fields}
legend={legend}
id={STEP_ID}
/>
))}
</div>
)
}
Content.propTypes = {
vmTemplate: PropTypes.object,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
/**
* Basic configuration about VM Template.
*
* @param {VmTemplate} vmTemplate - VM Template
* @returns {object} Basic configuration step
*/
const BasicConfiguration = ({ data: vmTemplate, oneConfig, adminGroup }) => ({
id: STEP_ID,
label: T.Configuration,
resolver: () => SCHEMA(vmTemplate, generalFeatures),
optionsValidate: { abortEarly: false },
content: (props) => Content({ ...props, vmTemplate, oneConfig, adminGroup }),
})
export default BasicConfiguration

View File

@ -0,0 +1,30 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string } from 'yup'
import { INPUT_TYPES, T } from 'client/constants'
const NAME = {
name: 'name',
label: T.VNName,
tooltip: T.VnTemplateNameHelper,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.default(() => undefined),
}
export const FIELDS = [NAME]

View File

@ -0,0 +1,73 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { BaseSchema } from 'yup'
import { FIELDS as INFORMATION_FIELDS } from './informationSchema'
// get schemas from VmTemplate/CreateForm
import { T, VmTemplate, VmTemplateFeatures } from 'client/constants'
import {
Field,
Section,
disableFields,
filterFieldsByHypervisor,
getObjectSchemaFromFields,
} from 'client/utils'
/**
* @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, oneConfig, adminGroup) => {
const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
return [
{
id: 'information',
legend: T.Information,
fields: disableFields(
filterFieldsByHypervisor(INFORMATION_FIELDS, hypervisor),
'',
oneConfig,
adminGroup
),
},
]
}
/**
* @param {VmTemplate} [vmTemplate] - VM Template
* @param {boolean} [hideCpu] - If `true`, the CPU fields is hidden
* @returns {Field[]} Basic configuration fields
*/
const FIELDS = (vmTemplate, hideCpu) =>
SECTIONS(vmTemplate, hideCpu)
.map(({ fields }) => fields)
.flat()
/**
* @param {VmTemplate} [vmTemplate] - VM Template
* @param {boolean} [hideCpu] - If `true`, the CPU fields is hidden
* @returns {BaseSchema} Step schema
*/
const SCHEMA = (vmTemplate, hideCpu) =>
getObjectSchemaFromFields(FIELDS(vmTemplate, hideCpu))
export { FIELDS, SCHEMA, SECTIONS }

View File

@ -0,0 +1,31 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import makeStyles from '@mui/styles/makeStyles'
export default makeStyles((theme) => ({
root: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '2em',
overflow: 'auto',
[theme.breakpoints.down('lg')]: {
gridTemplateColumns: '1fr',
},
},
information: {
gridColumn: '1 / -1',
},
}))

View File

@ -0,0 +1,94 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import ExtraConfiguration, {
STEP_ID as EXTRA_ID,
} from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration'
import General, {
STEP_ID as GENERAL_ID,
} from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/General'
import { jsonToXml } from 'client/models/Helper'
import { createSteps } from 'client/utils'
import { ObjectSchema, reach } from 'yup'
const DYNAMIC_FIELDS = ['AR']
const existsOnSchema = (schema, key) => {
try {
return reach(schema, key) && true
} catch (e) {
return false
}
}
/**
* @param {object} fromAttributes - Attributes to check
* @param {ObjectSchema} schema - Current form schema
* @returns {object} List of unknown attributes
*/
export const getUnknownVars = (fromAttributes = {}, schema) => {
const unknown = {}
for (const [key, value] of Object.entries(fromAttributes)) {
if (
!!value &&
!DYNAMIC_FIELDS.includes(key) &&
!existsOnSchema(schema, `${GENERAL_ID}.${key}`) &&
!existsOnSchema(schema, `${EXTRA_ID}.${key}`)
) {
// When the path is not found, it means that
// the attribute is correct and we can set it
unknown[key] = value
}
}
return unknown
}
const Steps = createSteps(() => [General, ExtraConfiguration].filter(Boolean), {
transformInitialValue: (vmTemplate, schema) => {
const initialValue = schema.cast(
{
[GENERAL_ID]: vmTemplate?.TEMPLATE,
[EXTRA_ID]: vmTemplate?.TEMPLATE,
},
{ stripUnknown: true }
)
return initialValue
},
transformBeforeSubmit: (formData, vnTemplate) => {
const { [GENERAL_ID]: { name } = {}, [EXTRA_ID]: extraTemplate = {} } =
formData ?? {}
Array.isArray(extraTemplate?.SECURITY_GROUPS) &&
extraTemplate?.SECURITY_GROUPS?.length &&
(extraTemplate.SECURITY_GROUPS = extraTemplate.SECURITY_GROUPS.join(','))
const templateXML = jsonToXml({
...extraTemplate,
})
const templates = {
id: vnTemplate.ID,
name,
template: templateXML,
}
return templates
},
})
export default Steps

View File

@ -0,0 +1,16 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
export { default } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps'

View File

@ -0,0 +1,34 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC'
import { CreateStepsCallback } from 'client/utils/schema'
import { ReactElement } from 'react'
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
*/
const CreateForm = (configProps) =>
AsyncLoadForm({ formPath: 'VNTemplate/CreateForm' }, configProps)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
*/
const InstantiateForm = (configProps) =>
AsyncLoadForm({ formPath: 'VNTemplate/InstantiateForm' }, configProps)
export { CreateForm, InstantiateForm }

View File

@ -14,11 +14,11 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useCallback, useMemo } from 'react'
import { reach } from 'yup'
import { useFormContext, useWatch } from 'react-hook-form'
import { reach } from 'yup'
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration'
import { getUnknownVars } from 'client/components/Forms/VNetwork/CreateForm/Steps'
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration'
import { useGeneralApi } from 'client/features/General'
import { Legend } from 'client/components/Forms'
@ -46,8 +46,7 @@ const ContextAttrsSection = () => {
const extraSchema = reach(getResolver(), EXTRA_ID)
// retrieve the schema for the given path
const existsSchema = reach(extraSchema, path)
console.log(existsSchema)
reach(extraSchema, path)
enqueueError(T.InvalidAttribute)
} catch (e) {
// When the path is not found, it means that

View File

@ -0,0 +1,274 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Typography } from '@mui/material'
import {
AddCircledOutline,
Group,
Lock,
PlayOutline,
// Import,
Trash,
} from 'iconoir-react'
import { useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import { useViews } from 'client/features/Auth'
import { useAddNetworkToClusterMutation } from 'client/features/OneApi/cluster'
import {
useChangeVNTemplateOwnershipMutation,
useLockVNTemplateMutation,
// useRecoverVNetMutation,
useRemoveVNTemplateMutation,
// useReserveAddressMutation,
useUnlockVNTemplateMutation,
} from 'client/features/OneApi/networkTemplate'
import { ChangeClusterForm } from 'client/components/Forms/Cluster'
import { ChangeGroupForm, ChangeUserForm } from 'client/components/Forms/Vm'
import { Translate } from 'client/components/HOC'
import {
GlobalAction,
createActions,
} from 'client/components/Tables/Enhanced/Utils'
import { PATH } from 'client/apps/sunstone/routesOne'
import { RESOURCE_NAMES, T, VN_TEMPLATE_ACTIONS } from 'client/constants'
const ListNames = ({ rows = [] }) =>
rows?.map?.(({ id, original }) => {
const { ID, NAME } = original
return (
<Typography
key={`vnet-${id}`}
variant="inherit"
component="span"
display="block"
>
{`#${ID} ${NAME}`}
</Typography>
)
})
const SubHeader = (rows) => <ListNames rows={rows} />
const MessageToConfirmAction = (rows) => {
const names = rows?.map?.(({ original }) => original?.NAME)
return (
<>
<p>
<Translate word={T.VirtualNetworks} />
{`: ${names.join(', ')}`}
</p>
<p>
<Translate word={T.DoYouWantProceed} />
</p>
</>
)
}
MessageToConfirmAction.displayName = 'MessageToConfirmAction'
/**
* Generates the actions to operate resources on Virtual networks table.
*
* @returns {GlobalAction} - Actions
*/
const Actions = () => {
const history = useHistory()
const { view, getResourceView } = useViews()
// const [reserve] = useReserveAddressMutation()
// const [recover] = useRecoverVNetMutation()
const [changeCluster] = useAddNetworkToClusterMutation()
const [lock] = useLockVNTemplateMutation()
const [unlock] = useUnlockVNTemplateMutation()
const [changeOwnership] = useChangeVNTemplateOwnershipMutation()
const [remove] = useRemoveVNTemplateMutation()
const actions = useMemo(
() =>
createActions({
filters: getResourceView(RESOURCE_NAMES.VN_TEMPLATE)?.actions,
actions: [
{
accessor: VN_TEMPLATE_ACTIONS.CREATE_DIALOG,
dataCy: `vnettemplate-${VN_TEMPLATE_ACTIONS.CREATE_DIALOG}`,
tooltip: T.Create,
icon: AddCircledOutline,
action: () => history.push(PATH.NETWORK.VN_TEMPLATES.CREATE),
},
{
accessor: VN_TEMPLATE_ACTIONS.INSTANTIATE_DIALOG,
dataCy: `vnettemplate-${VN_TEMPLATE_ACTIONS.INSTANTIATE_DIALOG}`,
tooltip: T.Instantiate,
icon: PlayOutline,
selected: { max: 1 },
action: (rows) => {
const template = rows?.[0]?.original ?? {}
const path = PATH.NETWORK.VN_TEMPLATES.INSTANTIATE
history.push(path, template)
},
},
{
accessor: VN_TEMPLATE_ACTIONS.UPDATE_DIALOG,
dataCy: `vnettemplate-${VN_TEMPLATE_ACTIONS.UPDATE_DIALOG}`,
label: T.Update,
tooltip: T.Update,
selected: { max: 1 },
color: 'secondary',
action: (rows) => {
const vnet = rows?.[0]?.original ?? {}
const path = PATH.NETWORK.VN_TEMPLATES.CREATE
history.push(path, vnet)
},
},
{
accessor: VN_TEMPLATE_ACTIONS.CHANGE_CLUSTER,
color: 'secondary',
dataCy: `vnettemplate-${VN_TEMPLATE_ACTIONS.CHANGE_CLUSTER}`,
label: T.SelectCluster,
tooltip: T.SelectCluster,
selected: true,
options: [
{
dialogProps: {
title: T.SelectCluster,
dataCy: 'modal-select-cluster',
},
form: () => ChangeClusterForm(),
onSubmit: (rows) => async (formData) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(
ids.map((id) =>
changeCluster({ id: formData.cluster, vnet: id })
)
)
},
},
],
},
{
tooltip: T.Ownership,
icon: Group,
selected: true,
color: 'secondary',
dataCy: 'vnettemplate-ownership',
options: [
{
accessor: VN_TEMPLATE_ACTIONS.CHANGE_OWNER,
name: T.ChangeOwner,
dialogProps: {
title: T.ChangeOwner,
subheader: SubHeader,
dataCy: `modal-${VN_TEMPLATE_ACTIONS.CHANGE_OWNER}`,
},
form: ChangeUserForm,
onSubmit: (rows) => (newOwnership) => {
rows?.map?.(({ original }) =>
changeOwnership({ id: original?.ID, ...newOwnership })
)
},
},
{
accessor: VN_TEMPLATE_ACTIONS.CHANGE_GROUP,
name: T.ChangeGroup,
dialogProps: {
title: T.ChangeGroup,
subheader: SubHeader,
dataCy: `modal-${VN_TEMPLATE_ACTIONS.CHANGE_GROUP}`,
},
form: ChangeGroupForm,
onSubmit: (rows) => async (newOwnership) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(
ids.map((id) => changeOwnership({ id, ...newOwnership }))
)
},
},
],
},
{
tooltip: T.Lock,
icon: Lock,
selected: true,
color: 'secondary',
dataCy: 'vnettemplate-lock',
options: [
{
accessor: VN_TEMPLATE_ACTIONS.LOCK,
name: T.Lock,
isConfirmDialog: true,
dialogProps: {
title: T.Lock,
dataCy: `modal-${VN_TEMPLATE_ACTIONS.LOCK}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => lock({ id })))
},
},
{
accessor: VN_TEMPLATE_ACTIONS.UNLOCK,
name: T.Unlock,
isConfirmDialog: true,
dialogProps: {
title: T.Unlock,
dataCy: `modal-${VN_TEMPLATE_ACTIONS.UNLOCK}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => unlock({ id })))
},
},
],
},
{
accessor: VN_TEMPLATE_ACTIONS.DELETE,
dataCy: `vnettemplate-${VN_TEMPLATE_ACTIONS.DELETE}`,
tooltip: T.Delete,
icon: Trash,
selected: true,
color: 'error',
options: [
{
isConfirmDialog: true,
dialogProps: {
title: T.Delete,
children: MessageToConfirmAction,
dataCy: `modal-vnet-${VN_TEMPLATE_ACTIONS.DELETE}`,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => remove({ id })))
},
},
],
},
],
}),
[view]
)
return actions
}
export default Actions

View File

@ -13,43 +13,43 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import {
AddCircledOutline,
// Import,
Trash,
PlayOutline,
Lock,
Group,
} from 'iconoir-react'
import { Typography } from '@mui/material'
import { makeStyles } from '@mui/styles'
import {
AddCircledOutline,
Group,
Lock,
PlayOutline,
// Import,
Trash,
} from 'iconoir-react'
import { useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import { useViews } from 'client/features/Auth'
import { useAddNetworkToClusterMutation } from 'client/features/OneApi/cluster'
import {
useReserveAddressMutation,
useLockVNetMutation,
useUnlockVNetMutation,
useChangeVNetOwnershipMutation,
useRemoveVNetMutation,
useLockVNetMutation,
useRecoverVNetMutation,
useRemoveVNetMutation,
useReserveAddressMutation,
useUnlockVNetMutation,
} from 'client/features/OneApi/network'
import { isAvailableAction } from 'client/models/VirtualNetwork'
import { ChangeUserForm, ChangeGroupForm } from 'client/components/Forms/Vm'
import { ReserveForm, RecoverForm } from 'client/components/Forms/VNetwork'
import { ChangeClusterForm } from 'client/components/Forms/Cluster'
import { RecoverForm, ReserveForm } from 'client/components/Forms/VNetwork'
import { ChangeGroupForm, ChangeUserForm } from 'client/components/Forms/Vm'
import { Translate } from 'client/components/HOC'
import {
createActions,
GlobalAction,
createActions,
} from 'client/components/Tables/Enhanced/Utils'
import VNetworkTemplatesTable from 'client/components/Tables/VNetworkTemplates'
import { Translate } from 'client/components/HOC'
import { PATH } from 'client/apps/sunstone/routesOne'
import { T, VN_ACTIONS, RESOURCE_NAMES } from 'client/constants'
import { RESOURCE_NAMES, T, VN_ACTIONS } from 'client/constants'
const isDisabled = (action) => (rows) =>
!isAvailableAction(
@ -125,17 +125,6 @@ const Actions = () => {
icon: AddCircledOutline,
action: () => history.push(PATH.NETWORK.VNETS.CREATE),
},
/* {
// TODO: Import Virtual Network from vCenter
accessor: VN_ACTIONS.IMPORT_DIALOG,
tooltip: T.Import,
icon: Import,
selected: { max: 1 },
disabled: true,
action: (rows) => {
// TODO: go to IMPORT form
},
}, */
{
accessor: VN_ACTIONS.INSTANTIATE_DIALOG,
dataCy: `vnet-${VN_ACTIONS.INSTANTIATE_DIALOG}`,

View File

@ -13,18 +13,18 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Box, Stack } from '@mui/material'
import PropTypes from 'prop-types'
import { ReactElement } from 'react'
import { useGetVNetworkQuery } from 'client/features/OneApi/network'
import AddressRangeCard from 'client/components/Cards/AddressRangeCard'
import {
AddAddressRangeAction,
UpdateAddressRangeAction,
DeleteAddressRangeAction,
UpdateAddressRangeAction,
} from 'client/components/Buttons'
import AddressRangeCard from 'client/components/Cards/AddressRangeCard'
import { AddressRange, VN_ACTIONS } from 'client/constants'
@ -50,7 +50,7 @@ const AddressTab = ({
const { data: vnet } = useGetVNetworkQuery({ id })
/** @type {AddressRange[]} */
const addressRanges = [vnet.AR_POOL.AR ?? []].flat()
const addressRanges = [vnet?.AR_POOL?.AR ?? []].flat()
return (
<Box padding={{ sm: '0.8em' }}>

View File

@ -22,8 +22,7 @@ import { Box } from '@mui/material'
import { useViews } from 'client/features/Auth'
import { useGetClustersQuery } from 'client/features/OneApi/cluster'
import { useGetVNetworkQuery } from 'client/features/OneApi/network'
// import {} from 'client/components/Tabs/VNetwork/Address/Actions'
import { useGetVNTemplateQuery } from 'client/features/OneApi/networkTemplate'
import { ClustersTable } from 'client/components/Tables'
import { RESOURCE_NAMES } from 'client/constants'
@ -40,7 +39,7 @@ const { CLUSTER } = RESOURCE_NAMES
*/
const ClustersTab = ({ id }) => {
const { push: redirectTo } = useHistory()
const { data: vnet } = useGetVNetworkQuery({ id })
const { data: vnet } = useGetVNTemplateQuery({ id })
const { view, hasAccessToResource } = useViews()
const detailAccess = useMemo(() => hasAccessToResource(CLUSTER), [view])

View File

@ -0,0 +1,178 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Box, Stack } from '@mui/material'
import PropTypes from 'prop-types'
import { ReactElement } from 'react'
import {
AddAddressRangeAction,
DeleteAddressRangeAction,
UpdateAddressRangeAction,
} from 'client/components/Buttons'
import AddressRangeCard from 'client/components/Cards/AddressRangeCard'
import {
useGetVNTemplateQuery,
useUpdateVNTemplateMutation,
} from 'client/features/OneApi/networkTemplate'
import { jsonToXml } from 'client/models/Helper'
import { AddressRange, VN_ACTIONS } from 'client/constants'
const { ADD_AR, UPDATE_AR, DELETE_AR } = VN_ACTIONS
const handleAdd = async ({ value, id, update, template }) => {
const addressRanges = [template?.AR ?? []].flat()
addressRanges.push(value)
const templateJson = { ...template, AR: addressRanges }
const newTemplate = jsonToXml(templateJson)
await update({ id, template: newTemplate }).unwrap()
}
const handleUpdate = async ({ value, id, addressID, update, template }) => {
let templateJson = { ...template, AR: value }
if (Array.isArray(template?.AR)) {
const addressRanges = [template.AR ?? []].flat()
addressRanges[addressID] = value
templateJson = { ...template, AR: addressRanges }
}
const newTemplate = jsonToXml(templateJson)
await update({ id, template: newTemplate }).unwrap()
}
const handleDelete = async ({ id, addressID, update, template }) => {
const { AR, ...rest } = template
let templateJson = { ...rest }
if (Array.isArray(template?.AR)) {
const addressRanges = [template.AR ?? []].flat()
addressRanges.splice(addressID, 1)
templateJson = { ...template, AR: addressRanges }
}
const newTemplate = jsonToXml(templateJson)
await update({ id, template: newTemplate }).unwrap()
}
/**
* Renders the list of address ranges from a Virtual Network.
*
* @param {object} props - Props
* @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,
oneConfig,
adminGroup,
}) => {
const { data: vnet } = useGetVNTemplateQuery(
{ id },
{ refetchOnMountOrArgChange: true }
)
const [update] = useUpdateVNTemplateMutation()
/** @type {AddressRange[]} */
const addressRanges = [vnet?.TEMPLATE?.AR ?? []].flat()
const template = vnet?.TEMPLATE
return (
<Box padding={{ sm: '0.8em' }}>
{actions[ADD_AR] === true && (
<AddAddressRangeAction
vnetId={id}
oneConfig={oneConfig}
adminGroup={adminGroup}
onSubmit={(value) =>
handleAdd({
value,
id,
update,
template,
})
}
/>
)}
<Stack gap="1em" py="0.8em">
{addressRanges.map((ar, addressID) => (
<AddressRangeCard
key={addressID}
vnet={vnet}
ar={ar}
actions={
<>
{actions[UPDATE_AR] === true && (
<UpdateAddressRangeAction
vnetId={id}
ar={ar}
oneConfig={oneConfig}
adminGroup={adminGroup}
template={vnet}
onSubmit={(value) =>
handleUpdate({
value,
id,
addressID,
update,
template,
})
}
/>
)}
{actions[DELETE_AR] === true && (
<DeleteAddressRangeAction
vnetId={id}
ar={ar}
oneConfig={oneConfig}
adminGroup={adminGroup}
template={vnet}
onSubmit={() =>
handleDelete({
id,
addressID,
update,
template,
})
}
/>
)}
</>
}
/>
))}
</Stack>
</Box>
)
}
AddressTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
AddressTab.displayName = 'AddressTab'
export default AddressTab

View File

@ -0,0 +1,87 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import PropTypes from 'prop-types'
import { ReactElement, useMemo } from 'react'
import { Box } from '@mui/material'
import { useHistory } from 'react-router'
import { generatePath } from 'react-router-dom'
import { useViews } from 'client/features/Auth'
import { useGetClustersQuery } from 'client/features/OneApi/cluster'
import { useGetVNTemplateQuery } from 'client/features/OneApi/networkTemplate'
import { PATH } from 'client/apps/sunstone/routesOne'
import { ClustersTable } from 'client/components/Tables'
import { RESOURCE_NAMES } from 'client/constants'
const { CLUSTER } = RESOURCE_NAMES
/**
* Renders the list of clusters from a Virtual Network.
*
* @param {object} props - Props
* @param {string} props.id - Virtual Network id
* @returns {ReactElement} Clusters tab
*/
const ClustersTab = ({ id }) => {
const { push: redirectTo } = useHistory()
const { data: vnet } = useGetVNTemplateQuery(
{ id },
{ refetchOnMountOrArgChange: true }
)
const { view, hasAccessToResource } = useViews()
const detailAccess = useMemo(() => hasAccessToResource(CLUSTER), [view])
const clusters = [vnet?.TEMPLATE?.CLUSTER_IDS?.split(',') ?? []]
.flat()
.map((clId) => +clId)
const redirectToCluster = (row) => {
const clusterPath = PATH.INFRASTRUCTURE.CLUSTERS.DETAIL
redirectTo(generatePath(clusterPath, { id: row.ID }))
}
const useQuery = () =>
useGetClustersQuery(undefined, {
selectFromResult: ({ data: result = [], ...rest }) => ({
data: result?.filter((cluster) => clusters.includes(+cluster.ID)),
...rest,
}),
})
return (
<Box padding={{ sm: '0.8em', overflow: 'auto' }}>
<ClustersTable
disableGlobalSort
disableRowSelect
pageSize={5}
onRowClick={detailAccess ? redirectToCluster : undefined}
useQuery={useQuery}
/>
</Box>
)
}
ClustersTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
ClustersTab.displayName = 'ClustersTab'
export default ClustersTab

View File

@ -13,28 +13,30 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useCallback } from 'react'
import { Box, Stack } from '@mui/material'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { ReactElement, useCallback } from 'react'
import {
useGetVNTemplateQuery,
AttributePanel,
Ownership,
Permissions,
} from 'client/components/Tabs/Common'
import QOS from 'client/components/Tabs/VNetwork/Info/qos'
import Information from 'client/components/Tabs/VNetworkTemplate/Info/information'
import {
useChangeVNTemplateOwnershipMutation,
useChangeVNTemplatePermissionsMutation,
useGetVNTemplateQuery,
useUpdateVNTemplateMutation,
} from 'client/features/OneApi/networkTemplate'
import {
Permissions,
Ownership,
AttributePanel,
} from 'client/components/Tabs/Common'
import Information from 'client/components/Tabs/VNetworkTemplate/Info/information'
import makeStyles from '@mui/styles/makeStyles'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
import {
getActionsAvailable,
filterAttributes,
getActionsAvailable,
jsonToXml,
} from 'client/models/Helper'
import { cloneObject, set } from 'client/utils'
@ -44,6 +46,15 @@ const VCENTER_ATTRIBUTES_REG = /^VCENTER_/
const HIDDEN_ATTRIBUTES_REG =
/^(AR|CLUSTERS|SECURITY_GROUPS|INBOUND_AVG_BW|INBOUND_PEAK_BW|INBOUND_PEAK_KB|OUTBOUND_AVG_BW|OUTBOUND_PEAK_BW|OUTBOUND_PEAK_KB)$/
const useStyles = makeStyles({
container: {
gridColumn: '1 / -1',
display: 'grid',
gridTemplateColumns: 'auto auto',
gap: '1rem',
},
})
/**
* Renders mainly information tab.
*
@ -53,10 +64,13 @@ const HIDDEN_ATTRIBUTES_REG =
* @returns {ReactElement} Information tab
*/
const VNetTemplateInfoTab = ({ tabProps = {}, id }) => {
const classes = useStyles()
const {
information_panel: informationPanel,
permissions_panel: permissionsPanel,
ownership_panel: ownershipPanel,
qos_panel: qosPanel,
attributes_panel: attributesPanel,
vcenter_panel: vcenterPanel,
lxc_panel: lxcPanel,
@ -146,6 +160,11 @@ const VNetTemplateInfoTab = ({ tabProps = {}, id }) => {
groupName={GNAME}
/>
)}
{qosPanel?.enabled && (
<Box className={classes.container}>
<QOS vnet={vnetTemplate} />
</Box>
)}
{attributesPanel?.enabled && attributes && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}

View File

@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { useRenameVNTemplateMutation } from 'client/features/OneApi/networkTemplate'
import { List } from 'client/components/Tabs/Common'
import { T, VNetworkTemplate, VN_TEMPLATE_ACTIONS } from 'client/constants'
import { T, VN_TEMPLATE_ACTIONS, VNetworkTemplate } from 'client/constants'
import { useRenameVNTemplateMutation } from 'client/features/OneApi/networkTemplate'
import { booleanToString, stringToBoolean } from 'client/models/Helper'
import PropTypes from 'prop-types'
import { ReactElement } from 'react'
/**
* Renders mainly information tab.
@ -30,7 +30,7 @@ import { T, VNetworkTemplate, VN_TEMPLATE_ACTIONS } from 'client/constants'
*/
const InformationPanel = ({ vnetTemplate = {}, actions }) => {
const [rename] = useRenameVNTemplateMutation()
const { ID, NAME } = vnetTemplate
const { ID, NAME, VLAN_ID_AUTOMATIC, OUTER_VLAN_ID_AUTOMATIC } = vnetTemplate
const handleRename = async (_, newName) => {
await rename({ id: ID, name: newName })
@ -45,16 +45,30 @@ const InformationPanel = ({ vnetTemplate = {}, actions }) => {
canEdit: actions?.includes?.(VN_TEMPLATE_ACTIONS.RENAME),
handleEdit: handleRename,
},
{
name: T.AutomaticVlanId,
value: booleanToString(stringToBoolean(VLAN_ID_AUTOMATIC)),
dataCy: 'vlan_id_automatic',
handleEdit: handleRename,
},
{
name: T.OuterVlanId,
value: OUTER_VLAN_ID_AUTOMATIC || '-',
dataCy: 'outer_vlan_id_automatic',
},
{
name: T.AutomaticOuterVlanId,
value: booleanToString(stringToBoolean(OUTER_VLAN_ID_AUTOMATIC)),
dataCy: 'outer_vlan_automatic',
},
]
return (
<>
<List
title={T.Information}
list={info}
containerProps={{ sx: { gridRow: 'span 3' } }}
/>
</>
<List
title={T.Information}
list={info}
containerProps={{ sx: { gridRow: 'span 3' } }}
/>
)
}

View File

@ -0,0 +1,94 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import PropTypes from 'prop-types'
import { ReactElement, useMemo } from 'react'
import { Box } from '@mui/material'
import { useHistory } from 'react-router'
import { generatePath } from 'react-router-dom'
import { useViews } from 'client/features/Auth'
import { useGetVNTemplateQuery } from 'client/features/OneApi/networkTemplate'
import { useGetSecGroupsQuery } from 'client/features/OneApi/securityGroup'
// import {} from 'client/components/Tabs/VNetwork/Address/Actions'
import { PATH } from 'client/apps/sunstone/routesOne'
import { SecurityGroupsTable } from 'client/components/Tables'
import { RESOURCE_NAMES } from 'client/constants'
const { SEC_GROUP } = RESOURCE_NAMES
/**
* Renders the list of security groups from a Virtual Network.
*
* @param {object} props - Props
* @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,
oneConfig,
adminGroup,
}) => {
const { push: redirectTo } = useHistory()
const { data: vnet } = useGetVNTemplateQuery({ id })
const { view, hasAccessToResource } = useViews()
const detailAccess = useMemo(() => hasAccessToResource(SEC_GROUP), [view])
const splittedSecGroups = vnet?.TEMPLATE.SECURITY_GROUPS?.split(',') ?? []
const secGroups = [splittedSecGroups].flat().map((sgId) => +sgId)
const redirectToSecGroup = (row) => {
redirectTo(generatePath(PATH.NETWORK.SEC_GROUPS.DETAIL, { id: row.ID }))
}
const useQuery = () =>
useGetSecGroupsQuery(undefined, {
selectFromResult: ({ data: result = [], ...rest }) => ({
data: result?.filter((secgroup) => secGroups.includes(+secgroup.ID)),
...rest,
}),
})
return (
<Box padding={{ sm: '0.8em', overflow: 'auto' }}>
<SecurityGroupsTable
disableGlobalSort
disableRowSelect
pageSize={5}
onRowClick={detailAccess ? redirectToSecGroup : undefined}
useQuery={useQuery}
/>
</Box>
)
}
SecurityTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
SecurityTab.displayName = 'SecurityTab'
export default SecurityTab

View File

@ -23,11 +23,17 @@ import { useGetVNTemplateQuery } from 'client/features/OneApi/networkTemplate'
import { getAvailableInfoTabs } from 'client/models/Helper'
import Tabs from 'client/components/Tabs'
import Address from 'client/components/Tabs/VNetworkTemplate/Address'
import Clusters from 'client/components/Tabs/VNetworkTemplate/Clusters'
import Info from 'client/components/Tabs/VNetworkTemplate/Info'
import Security from 'client/components/Tabs/VNetworkTemplate/Security'
const getTabComponent = (tabName) =>
({
info: Info,
address: Address,
security: Security,
cluster: Clusters,
}[tabName])
const VNetTemplateTabs = memo(({ id }) => {

View File

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
// eslint-disable-next-line no-unused-vars
import { Permissions, LockInfo } from 'client/constants/common'
import * as ACTIONS from 'client/constants/actions'
// eslint-disable-next-line no-unused-vars
import { LockInfo, Permissions } from 'client/constants/common'
/**
* @typedef VNetworkTemplate
@ -35,6 +35,11 @@ import * as ACTIONS from 'client/constants/actions'
/** @enum {string} Virtual network template actions */
export const VN_TEMPLATE_ACTIONS = {
CREATE_DIALOG: 'create_dialog',
UPDATE_DIALOG: 'update_dialog',
INSTANTIATE_DIALOG: 'instantiate_dialog',
CHANGE_CLUSTER: 'change_cluster',
LOCK: 'lock',
UNLOCK: 'unlock',
DELETE: 'delete',
// INFORMATION

View File

@ -84,6 +84,7 @@ module.exports = {
CreateUser: 'Create User',
UpdateUser: 'Update User',
CreateVirtualNetwork: 'Create Virtual Network',
CreateVirtualNetworkTemplate: 'Create Virtual Network Template',
CreateVmTemplate: 'Create VM Template',
CreateVDC: 'Create VDC',
UpdateVDC: 'Update VDC',
@ -126,6 +127,7 @@ module.exports = {
Info: 'Info',
Instantiate: 'Instantiate',
InstantiateVmTemplate: 'Instantiate VM Template',
InstantiateVnTemplate: 'Instantiate Network Template',
LocateOnTable: 'Locate on table',
Lock: 'Lock',
Migrate: 'Migrate',
@ -212,6 +214,7 @@ module.exports = {
UpdateScheduleAction: 'Update schedule action: %s',
UpdateServiceTemplate: 'Update Service Template',
UpdateVirtualNetwork: 'Update Virtual Network',
UpdateVirtualNetworkTemplate: 'Update Virtual Network Template',
UpdateVmConfiguration: 'Update VM Configuration',
UpdateVmTemplate: 'Update VM Template',
@ -730,6 +733,7 @@ module.exports = {
Timezone: 'Timezone',
/* VM schema - info */
VmName: 'VM name',
VNName: 'Virtual Network Name',
UserTemplate: 'User Template',
Template: 'Template',
WhereIsRunning:
@ -847,6 +851,9 @@ module.exports = {
Virtualization: 'Virtualization',
CustomInformation: 'Custom information',
CustomVirtualization: 'Custom virtualization',
VnTemplateNameHelper: `
Defaults to 'template name-<vmid>' when empty.
When creating several Virtual Network, the wildcard %%idx will be replaced with a number starting from 0`,
VmTemplateNameHelper: `
Defaults to 'template name-<vmid>' when empty.
When creating several VMs, the wildcard %%idx will be
@ -1321,6 +1328,8 @@ module.exports = {
AddToExistingReservation: 'Add to an existing Reservation',
FirstAddress: 'First address',
IpOrMac: 'IP or MAC',
MessageQos:
'These values apply to each VM interface individually, they are not global values for the Virtual Network',
/* security group schema */
Security: 'Security',
@ -1470,6 +1479,8 @@ module.exports = {
ManualNetwork: 'Manual Network',
OpennebulaVirtualNetwork: 'OpenNebula Virtual Network',
SelectNewNetwork: 'Please select a network from the list',
MessageAddSecGroupDefault:
'The default Security Group 0 is automatically added to new Virtual Networks',
NotVmsCurrentySecGroups:
'There are currently no VMs associated with this Security Group',
CommitMessageSecGroups: `

View File

@ -0,0 +1,88 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import { useHistory, useLocation } from 'react-router'
import { useGeneralApi } from 'client/features/General'
import {
useAllocateVNTemplateMutation,
useGetVNTemplateQuery,
useUpdateVNTemplateMutation,
} from 'client/features/OneApi/networkTemplate'
import { PATH } from 'client/apps/sunstone/routesOne'
import {
DefaultFormStepper,
SkeletonStepsForm,
} from 'client/components/FormStepper'
import { CreateForm } from 'client/components/Forms/VNTemplate'
import { useSystemData } from 'client/features/Auth'
const _ = require('lodash')
/**
* Displays the creation or modification form to a Virtual Network.
*
* @returns {ReactElement} Virtual Network form
*/
const CreateVirtualNetworkTemplate = () => {
const history = useHistory()
const { state: { ID: vnetId, NAME } = {} } = useLocation()
const { enqueueSuccess } = useGeneralApi()
const [update] = useUpdateVNTemplateMutation()
const [allocate] = useAllocateVNTemplateMutation()
const { adminGroup, oneConfig } = useSystemData()
const { data } = useGetVNTemplateQuery(
{ id: vnetId, extended: true },
{ skip: vnetId === undefined }
)
const onSubmit = async (xml) => {
try {
if (!vnetId) {
const newVnetId = await allocate({ template: xml }).unwrap()
enqueueSuccess(`Virtual Network Template created - #${newVnetId}`)
} else {
await update({ id: vnetId, template: xml }).unwrap()
enqueueSuccess(`Virtual Network Template updated - #${vnetId} ${NAME}`)
}
history.push(PATH.NETWORK.VN_TEMPLATES.LIST)
} catch {}
}
return !_.isEmpty(oneConfig) && ((vnetId && data) || !vnetId) ? (
<CreateForm
initialValues={data}
stepProps={{
data,
oneConfig,
adminGroup,
}}
onSubmit={onSubmit}
fallback={<SkeletonStepsForm />}
>
{(config) => <DefaultFormStepper {...config} />}
</CreateForm>
) : (
<SkeletonStepsForm />
)
}
export default CreateVirtualNetworkTemplate

View File

@ -0,0 +1,89 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import { Redirect, useHistory, useLocation } from 'react-router'
import { useGeneralApi } from 'client/features/General'
import {
useGetVNTemplateQuery,
useInstantiateVNTemplateMutation,
} from 'client/features/OneApi/networkTemplate'
import { PATH } from 'client/apps/sunstone/routesOne'
import {
DefaultFormStepper,
SkeletonStepsForm,
} from 'client/components/FormStepper'
import { InstantiateForm } from 'client/components/Forms/VNTemplate'
import { useSystemData } from 'client/features/Auth'
const _ = require('lodash')
/**
* Displays the instantiation form for a VM Template.
*
* @returns {ReactElement} Instantiation form
*/
const InstantiateVnTemplate = () => {
const history = useHistory()
const { state: { ID: templateId, NAME: templateName } = {} } = useLocation()
const { enqueueInfo } = useGeneralApi()
const [instantiate] = useInstantiateVNTemplateMutation()
const { adminGroup, oneConfig } = useSystemData()
const { data: apiTemplateDataExtended, isError } = useGetVNTemplateQuery(
{ id: templateId },
{ skip: templateId === undefined }
)
const dataTemplateExtended = _.cloneDeep(apiTemplateDataExtended)
const onSubmit = async (template) => {
try {
await instantiate(template).unwrap()
history.push(PATH.NETWORK.VN_TEMPLATES.LIST)
const templateInfo = `#${templateId} ${templateName}`
enqueueInfo(`VN Template instantiated - ${templateInfo}`)
} catch {}
}
if (!templateId || isError) {
return <Redirect to={PATH.NETWORK.VN_TEMPLATES.LIST} />
}
return !dataTemplateExtended || _.isEmpty(oneConfig) ? (
<SkeletonStepsForm />
) : (
<InstantiateForm
initialValues={dataTemplateExtended}
stepProps={{
dataTemplateExtended,
oneConfig,
adminGroup,
}}
onSubmit={onSubmit}
fallback={<SkeletonStepsForm />}
>
{(config) => <DefaultFormStepper {...config} />}
</InstantiateForm>
)
}
export default InstantiateVnTemplate

View File

@ -13,25 +13,26 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useState, memo } from 'react'
import PropTypes from 'prop-types'
import { Box, Chip, Stack, Typography } from '@mui/material'
import Cancel from 'iconoir-react/dist/Cancel'
import GotoIcon from 'iconoir-react/dist/Pin'
import RefreshDouble from 'iconoir-react/dist/RefreshDouble'
import Cancel from 'iconoir-react/dist/Cancel'
import { Typography, Box, Stack, Chip } from '@mui/material'
import PropTypes from 'prop-types'
import { ReactElement, memo, useState } from 'react'
import { Row } from 'react-table'
import { SubmitButton } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
import MultipleTags from 'client/components/MultipleTags'
import SplitPane from 'client/components/SplitPane'
import { VNetworkTemplatesTable } from 'client/components/Tables'
import VNetworkTemplateActions from 'client/components/Tables/VNetworkTemplates/actions'
import VNetworkTemplateTabs from 'client/components/Tabs/VNetworkTemplate'
import { T, VNetworkTemplate } from 'client/constants'
import {
useLazyGetVNTemplateQuery,
useUpdateVNTemplateMutation,
} from 'client/features/OneApi/networkTemplate'
import { VNetworkTemplatesTable } from 'client/components/Tables'
import VNetworkTemplateTabs from 'client/components/Tabs/VNetworkTemplate'
import SplitPane from 'client/components/SplitPane'
import MultipleTags from 'client/components/MultipleTags'
import { SubmitButton } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
import { T, VNetworkTemplate } from 'client/constants'
/**
* Displays a list of VNet Templates with a split pane between the list and selected row(s).
@ -40,7 +41,7 @@ import { T, VNetworkTemplate } from 'client/constants'
*/
function VNetworkTemplates() {
const [selectedRows, onSelectedRowsChange] = useState(() => [])
const actions = VNetworkTemplateActions()
const hasSelectedRows = selectedRows?.length > 0
const moreThanOneSelected = selectedRows?.length > 1
@ -50,6 +51,7 @@ function VNetworkTemplates() {
<Box height={1} {...(hasSelectedRows && getGridProps())}>
<VNetworkTemplatesTable
onSelectedRowsChange={onSelectedRowsChange}
globalActions={actions}
useUpdateMutation={useUpdateVNTemplateMutation}
/>

View File

@ -13,13 +13,17 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Actions, Commands } from 'server/utils/constants/commands/vntemplate'
import { FilterFlag, Permission, VNetworkTemplate } from 'client/constants'
import {
oneApi,
ONE_RESOURCES,
ONE_RESOURCES_POOL,
oneApi,
} from 'client/features/OneApi'
import { FilterFlag, Permission, VNetworkTemplate } from 'client/constants'
import {
updateOwnershipOnResource,
updateTemplateOnResource,
} from 'client/features/OneApi/common'
import { Actions, Commands } from 'server/utils/constants/commands/vntemplate'
const { VNTEMPLATE } = ONE_RESOURCES
const { VNET_POOL, VNTEMPLATE_POOL } = ONE_RESOURCES_POOL
@ -168,7 +172,31 @@ const vNetworkTemplateApi = oneApi.injectEndpoints({
return { params, command }
},
providesTags: (_, __, { id }) => [{ type: VNTEMPLATE, id }],
invalidatesTags: (_, __, { id }) => [{ type: VNTEMPLATE, id }],
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
const patchVNTemplate = dispatch(
vNetworkTemplateApi.util.updateQueryData(
'getVNTemplate',
{ id: params.id },
updateTemplateOnResource(params)
)
)
const patchVNTemplates = dispatch(
vNetworkTemplateApi.util.updateQueryData(
'getVNTemplates',
undefined,
updateTemplateOnResource(params)
)
)
queryFulfilled.catch(() => {
patchVNTemplate.undo()
patchVNTemplates.undo()
})
} catch {}
},
}),
changeVNTemplatePermissions: builder.mutation({
/**
@ -215,10 +243,20 @@ const vNetworkTemplateApi = oneApi.injectEndpoints({
return { params, command }
},
invalidatesTags: (_, __, { id }) => [
{ type: VNTEMPLATE, id },
VNTEMPLATE_POOL,
],
invalidatesTags: (_, __, { id }) => [{ type: VNTEMPLATE, id }],
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
try {
const patchVNet = dispatch(
vNetworkTemplateApi.util.updateQueryData(
'getVNTemplate',
{ id: params.id },
updateOwnershipOnResource(getState(), params)
)
)
queryFulfilled.catch(patchVNet.undo)
} catch {}
},
}),
renameVNTemplate: builder.mutation({
/**
@ -270,15 +308,15 @@ const vNetworkTemplateApi = oneApi.injectEndpoints({
/**
* Unlocks a VN Template.
*
* @param {string|number} id - VN Template id
* @param {object} params - Request parameters
* @returns {number} VN Template id
* @throws Fails when response isn't code 200
*/
query: (id) => {
query: (params) => {
const name = Actions.VNTEMPLATE_UNLOCK
const command = { name, ...Commands[name] }
return { params: { id }, command }
return { params, command }
},
invalidatesTags: (_, __, id) => [
{ type: VNTEMPLATE, id },

View File

@ -174,11 +174,11 @@ module.exports = {
from: resource,
default: 0,
},
userId: {
user: {
from: postBody,
default: -1,
},
groupId: {
group: {
from: postBody,
default: -1,
},
@ -220,10 +220,14 @@ module.exports = {
from: resource,
default: 0,
},
lock: {
level: {
from: postBody,
default: 4,
},
test: {
from: postBody,
default: false,
},
},
},
[VNTEMPLATE_UNLOCK]: {