diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js
index 2a14e380ad..eacce5d774 100644
--- a/src/fireedge/src/client/apps/sunstone/routesOne.js
+++ b/src/fireedge/src/client/apps/sunstone/routesOne.js
@@ -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,
diff --git a/src/fireedge/src/client/components/Buttons/AddressRangeActions.js b/src/fireedge/src/client/components/Buttons/AddressRangeActions.js
index ac7d84aa68..d7c8e30018 100644
--- a/src/fireedge/src/client/components/Buttons/AddressRangeActions.js
+++ b/src/fireedge/src/client/components/Buttons/AddressRangeActions.js
@@ -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: (
- <>
-
- {`: #${AR_ID}`}
- >
- ),
+ title: AR_ID
+ ? `${Tr(T.DeleteAddressRange)}: #${AR_ID}`
+ : `${Tr(T.DeleteAddressRange)}`,
children:
{Tr(T.DoYouWantProceed)}
,
},
onSubmit: handleRemove,
@@ -189,6 +188,6 @@ DeleteAddressRangeAction.displayName = 'DeleteAddressRangeAction'
export {
AddAddressRangeAction,
- UpdateAddressRangeAction,
DeleteAddressRangeAction,
+ UpdateAddressRangeAction,
}
diff --git a/src/fireedge/src/client/components/Cards/AddressRangeCard.js b/src/fireedge/src/client/components/Cards/AddressRangeCard.js
index 1e3e3e77d5..01b7660100 100644
--- a/src/fireedge/src/client/components/Cards/AddressRangeCard.js
+++ b/src/fireedge/src/client/components/Cards/AddressRangeCard.js
@@ -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(
- {`#${AR_ID}`}
+ {`#${AR_ID || '-'}`}
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/addresses.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/addresses.js
new file mode 100644
index 0000000000..dd1c86c1f9
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/addresses.js
@@ -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 (
+ <>
+
+
+
+
+
+ {addresses?.map((ar, index) => {
+ const key = ar.ID ?? ar.NAME
+ const fakeValues = { ...ar, AR_ID: index }
+
+ return (
+
+ handleUpdate(updatedAr, index)}
+ oneConfig={oneConfig}
+ adminGroup={adminGroup}
+ />
+ handleRemove(index)}
+ />
+ >
+ }
+ />
+ )
+ })}
+
+ >
+ )
+}
+
+AddressesContent.propTypes = {
+ oneConfig: PropTypes.object,
+ adminGroup: PropTypes.bool,
+}
+
+const Content = ({ isUpdate, oneConfig, adminGroup }) =>
+ isUpdate ? (
+
+
+
+ ) : (
+
+ )
+
+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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/clusters.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/clusters.js
new file mode 100644
index 0000000000..c3963583ae
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/clusters.js
@@ -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 (
+
+ )
+}
+
+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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/index.js
new file mode 100644
index 0000000000..2bd453edcf
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/index.js
@@ -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 }) => (
+ <>
+
+ >
+)
+
+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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/schema.js
new file mode 100644
index 0000000000..696bffe105
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/schema.js
@@ -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 }
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js
new file mode 100644
index 0000000000..8db4c49b77
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js
@@ -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 (
+
+ }
+ allActionsEnabled
+ handleAdd={handleChangeAttribute}
+ handleEdit={handleChangeAttribute}
+ handleDelete={handleChangeAttribute}
+ attributes={unknownVars}
+ filtersSpecialAttributes={false}
+ />
+ )
+}
+
+export default ContextAttrsSection
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/index.js
new file mode 100644
index 0000000000..ae7f1ad560
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/index.js
@@ -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 }) => (
+ <>
+
+
+ >
+)
+
+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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/schema.js
new file mode 100644
index 0000000000..cf8d63dd25
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/schema.js
@@ -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))
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/index.js
new file mode 100644
index 0000000000..7da904ccb9
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/index.js
@@ -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: ,
+ renderContent: () => (
+
+ ),
+ error: getError?.(errors[STEP_ID]),
+ })),
+ [totalErrors, driver]
+ )
+
+ return
+}
+
+/**
+ * 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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/index.js
new file mode 100644
index 0000000000..1560a388cd
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/index.js
@@ -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 }) => (
+ <>
+
+
+
+ {SECTIONS(oneConfig, adminGroup).map(({ 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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/schema.js
new file mode 100644
index 0000000000..cce0e5df50
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/schema.js
@@ -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 }
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/schema.js
new file mode 100644
index 0000000000..d2a2b3b14f
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/schema.js
@@ -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 }
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/index.js
new file mode 100644
index 0000000000..30824b8ed7
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/index.js
@@ -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 }) => (
+
+ ))}
+ >
+ )
+}
+
+/**
+ * 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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/informationSchema.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/informationSchema.js
new file mode 100644
index 0000000000..227a112c54
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/informationSchema.js
@@ -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)
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/schema.js
new file mode 100644
index 0000000000..83f10005b0
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/schema.js
@@ -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 }
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/index.js
new file mode 100644
index 0000000000..78e7455bd2
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/index.js
@@ -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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/index.js
new file mode 100644
index 0000000000..4a3add5b73
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/index.js
@@ -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'
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/addresses.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/addresses.js
new file mode 100644
index 0000000000..9d855a10fd
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/addresses.js
@@ -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 (
+ <>
+
+
+
+
+
+ {addresses?.map((ar, index) => {
+ const key = ar.ID ?? ar.NAME
+ const fakeValues = { ...ar, AR_ID: index }
+
+ return (
+
+ handleUpdate(updatedAr, index)}
+ oneConfig={oneConfig}
+ adminGroup={adminGroup}
+ />
+ handleRemove(index)}
+ />
+ >
+ }
+ />
+ )
+ })}
+
+ >
+ )
+}
+
+AddressesContent.propTypes = {
+ oneConfig: PropTypes.object,
+ adminGroup: PropTypes.bool,
+}
+
+const Content = ({ isUpdate, oneConfig, adminGroup }) =>
+ isUpdate ? (
+
+
+
+ ) : (
+
+ )
+
+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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/customAttributes.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/customAttributes.js
new file mode 100644
index 0000000000..7eed221eca
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/customAttributes.js
@@ -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 (
+
+ }
+ allActionsEnabled
+ handleAdd={handleChangeAttribute}
+ handleEdit={handleChangeAttribute}
+ handleDelete={handleChangeAttribute}
+ attributes={unknownVars}
+ filtersSpecialAttributes={false}
+ />
+ )
+}
+
+export default ContextAttrsSection
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/index.js
new file mode 100644
index 0000000000..f0f8fdcfd7
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/index.js
@@ -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 }) => (
+ <>
+
+
+ >
+)
+
+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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/schema.js
new file mode 100644
index 0000000000..cf8d63dd25
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/schema.js
@@ -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))
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js
new file mode 100644
index 0000000000..db0aaf8b35
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js
@@ -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: ,
+ renderContent: () => (
+
+ ),
+ error: getError?.(errors[STEP_ID]),
+ })),
+ [totalErrors]
+ )
+
+ return
+}
+
+/**
+ * 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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js
new file mode 100644
index 0000000000..500222f86e
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js
@@ -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 }
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/security.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/security.js
new file mode 100644
index 0000000000..e202895f7c
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/security.js
@@ -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 (
+ <>
+
+
+
+
+ >
+ )
+}
+
+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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/index.js
new file mode 100644
index 0000000000..5072019930
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/index.js
@@ -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 (
+
+ {sections.map(({ id, legend, fields }) => (
+
+ ))}
+
+ )
+}
+
+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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/informationSchema.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/informationSchema.js
new file mode 100644
index 0000000000..01b3cf84ec
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/informationSchema.js
@@ -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]
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/schema.js
new file mode 100644
index 0000000000..71cf89c880
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/schema.js
@@ -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 }
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/styles.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/styles.js
new file mode 100644
index 0000000000..e2f47d3853
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/styles.js
@@ -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',
+ },
+}))
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/index.js
new file mode 100644
index 0000000000..f80be0b75b
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/index.js
@@ -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
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/index.js
new file mode 100644
index 0000000000..d01be02af5
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/index.js
@@ -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'
diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/index.js
new file mode 100644
index 0000000000..ffd77b91a4
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNTemplate/index.js
@@ -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 }
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js
index 5e894bd73f..6098dc3bd6 100644
--- a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js
@@ -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
diff --git a/src/fireedge/src/client/components/Tables/VNetworkTemplates/actions.js b/src/fireedge/src/client/components/Tables/VNetworkTemplates/actions.js
new file mode 100644
index 0000000000..2ca055fb13
--- /dev/null
+++ b/src/fireedge/src/client/components/Tables/VNetworkTemplates/actions.js
@@ -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 (
+
+ {`#${ID} ${NAME}`}
+
+ )
+ })
+
+const SubHeader = (rows) =>
+
+const MessageToConfirmAction = (rows) => {
+ const names = rows?.map?.(({ original }) => original?.NAME)
+
+ return (
+ <>
+
+
+ {`: ${names.join(', ')}`}
+
+
+
+
+ >
+ )
+}
+
+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
diff --git a/src/fireedge/src/client/components/Tables/VNetworks/actions.js b/src/fireedge/src/client/components/Tables/VNetworks/actions.js
index d13fa18e5c..bb2873fddb 100644
--- a/src/fireedge/src/client/components/Tables/VNetworks/actions.js
+++ b/src/fireedge/src/client/components/Tables/VNetworks/actions.js
@@ -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}`,
diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Address.js b/src/fireedge/src/client/components/Tabs/VNetwork/Address.js
index dcff10d79f..5f63c6ca38 100644
--- a/src/fireedge/src/client/components/Tabs/VNetwork/Address.js
+++ b/src/fireedge/src/client/components/Tabs/VNetwork/Address.js
@@ -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 (
diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js b/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js
index 72beb82ef6..55f52d322f 100644
--- a/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js
+++ b/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js
@@ -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])
diff --git a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Address.js b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Address.js
new file mode 100644
index 0000000000..3107a61ebf
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Address.js
@@ -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 (
+
+ {actions[ADD_AR] === true && (
+
+ handleAdd({
+ value,
+ id,
+ update,
+ template,
+ })
+ }
+ />
+ )}
+
+
+ {addressRanges.map((ar, addressID) => (
+
+ {actions[UPDATE_AR] === true && (
+
+ handleUpdate({
+ value,
+ id,
+ addressID,
+ update,
+ template,
+ })
+ }
+ />
+ )}
+ {actions[DELETE_AR] === true && (
+
+ handleDelete({
+ id,
+ addressID,
+ update,
+ template,
+ })
+ }
+ />
+ )}
+ >
+ }
+ />
+ ))}
+
+
+ )
+}
+
+AddressTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+ oneConfig: PropTypes.object,
+ adminGroup: PropTypes.bool,
+}
+
+AddressTab.displayName = 'AddressTab'
+
+export default AddressTab
diff --git a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Clusters.js b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Clusters.js
new file mode 100644
index 0000000000..29463eca45
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Clusters.js
@@ -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 (
+
+
+
+ )
+}
+
+ClustersTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+}
+
+ClustersTab.displayName = 'ClustersTab'
+
+export default ClustersTab
diff --git a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Info/index.js b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Info/index.js
index 7e24f4c844..f85cfc5fb7 100644
--- a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Info/index.js
+++ b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Info/index.js
@@ -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 && (
+
+
+
+ )}
{attributesPanel?.enabled && attributes && (
{
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 (
- <>
-
- >
+
)
}
diff --git a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Security.js b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Security.js
new file mode 100644
index 0000000000..b89f54f5fc
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Security.js
@@ -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 (
+
+
+
+ )
+}
+
+SecurityTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+ oneConfig: PropTypes.object,
+ adminGroup: PropTypes.bool,
+}
+
+SecurityTab.displayName = 'SecurityTab'
+
+export default SecurityTab
diff --git a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/index.js b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/index.js
index e7dc67ddf8..076bfd41b9 100644
--- a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/index.js
+++ b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/index.js
@@ -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 }) => {
diff --git a/src/fireedge/src/client/constants/networkTemplate.js b/src/fireedge/src/client/constants/networkTemplate.js
index e696d78988..f2db1cc93f 100644
--- a/src/fireedge/src/client/constants/networkTemplate.js
+++ b/src/fireedge/src/client/constants/networkTemplate.js
@@ -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
diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js
index 1b9a7ffadb..9224b9a0f8 100644
--- a/src/fireedge/src/client/constants/translates.js
+++ b/src/fireedge/src/client/constants/translates.js
@@ -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-' when empty.
+ When creating several Virtual Network, the wildcard %%idx will be replaced with a number starting from 0`,
VmTemplateNameHelper: `
Defaults to 'template name-' 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: `
diff --git a/src/fireedge/src/client/containers/VNetworkTemplates/Create.js b/src/fireedge/src/client/containers/VNetworkTemplates/Create.js
new file mode 100644
index 0000000000..e8006089d1
--- /dev/null
+++ b/src/fireedge/src/client/containers/VNetworkTemplates/Create.js
@@ -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) ? (
+ }
+ >
+ {(config) => }
+
+ ) : (
+
+ )
+}
+
+export default CreateVirtualNetworkTemplate
diff --git a/src/fireedge/src/client/containers/VNetworkTemplates/Instantiate.js b/src/fireedge/src/client/containers/VNetworkTemplates/Instantiate.js
new file mode 100644
index 0000000000..6cc7f7fadc
--- /dev/null
+++ b/src/fireedge/src/client/containers/VNetworkTemplates/Instantiate.js
@@ -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
+ }
+
+ return !dataTemplateExtended || _.isEmpty(oneConfig) ? (
+
+ ) : (
+ }
+ >
+ {(config) => }
+
+ )
+}
+
+export default InstantiateVnTemplate
diff --git a/src/fireedge/src/client/containers/VNetworkTemplates/index.js b/src/fireedge/src/client/containers/VNetworkTemplates/index.js
index 7a334b7624..8591f0e4d6 100644
--- a/src/fireedge/src/client/containers/VNetworkTemplates/index.js
+++ b/src/fireedge/src/client/containers/VNetworkTemplates/index.js
@@ -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() {
diff --git a/src/fireedge/src/client/features/OneApi/networkTemplate.js b/src/fireedge/src/client/features/OneApi/networkTemplate.js
index b55a4fd489..f96be881ff 100644
--- a/src/fireedge/src/client/features/OneApi/networkTemplate.js
+++ b/src/fireedge/src/client/features/OneApi/networkTemplate.js
@@ -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 },
diff --git a/src/fireedge/src/server/utils/constants/commands/vntemplate.js b/src/fireedge/src/server/utils/constants/commands/vntemplate.js
index 3358de0942..0b5ba06226 100644
--- a/src/fireedge/src/server/utils/constants/commands/vntemplate.js
+++ b/src/fireedge/src/server/utils/constants/commands/vntemplate.js
@@ -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]: {