diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js
index 8d96526d1f..65b05186f8 100644
--- a/src/fireedge/src/client/apps/sunstone/routesOne.js
+++ b/src/fireedge/src/client/apps/sunstone/routesOne.js
@@ -113,6 +113,10 @@ const VirtualNetworks = loadable(
() => import('client/containers/VirtualNetworks'),
{ ssr: false }
)
+const CreateVirtualNetwork = loadable(
+ () => import('client/containers/VirtualNetworks/Create'),
+ { ssr: false }
+)
const VNetworkTemplates = loadable(
() => import('client/containers/VNetworkTemplates'),
{ ssr: false }
@@ -156,7 +160,7 @@ export const PATH = {
DETAIL: `/${RESOURCE_NAMES.VM}/:id`,
},
VROUTERS: {
- LIST: `/${RESOURCE_NAMES.V_ROUTER}`,
+ LIST: `/${RESOURCE_NAMES.VROUTER}`,
},
SERVICES: {
LIST: `/${RESOURCE_NAMES.SERVICE}`,
@@ -200,6 +204,7 @@ export const PATH = {
VNETS: {
LIST: `/${RESOURCE_NAMES.VNET}`,
DETAIL: `/${RESOURCE_NAMES.VNET}/:id`,
+ CREATE: `/${RESOURCE_NAMES.VNET}/create`,
},
VN_TEMPLATES: {
LIST: `/${RESOURCE_NAMES.VN_TEMPLATE}`,
@@ -398,6 +403,16 @@ const ENDPOINTS = [
icon: NetworkTemplateIcon,
Component: VNetworkTemplates,
},
+ {
+ title: (_, state) =>
+ state?.ID !== undefined
+ ? T.UpdateVirtualNetwork
+ : T.CreateVirtualNetwork,
+ description: (_, state) =>
+ state?.ID !== undefined && `#${state.ID} ${state.NAME}`,
+ path: PATH.NETWORK.VNETS.CREATE,
+ Component: CreateVirtualNetwork,
+ },
],
},
{
diff --git a/src/fireedge/src/client/components/Buttons/AddressRangeActions.js b/src/fireedge/src/client/components/Buttons/AddressRangeActions.js
new file mode 100644
index 0000000000..8d8e513d17
--- /dev/null
+++ b/src/fireedge/src/client/components/Buttons/AddressRangeActions.js
@@ -0,0 +1,162 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { memo } from 'react'
+import PropTypes from 'prop-types'
+import AddIcon from 'iconoir-react/dist/Plus'
+import EditIcon from 'iconoir-react/dist/Edit'
+import TrashIcon from 'iconoir-react/dist/Trash'
+
+import {
+ useAddRangeToVNetMutation,
+ useUpdateVNetRangeMutation,
+ useRemoveRangeFromVNetMutation,
+} from 'client/features/OneApi/network'
+import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
+import { AddRangeForm } from 'client/components/Forms/VNetwork'
+
+import { jsonToXml } from 'client/models/Helper'
+import { Tr, Translate } from 'client/components/HOC'
+import { T, VN_ACTIONS } from 'client/constants'
+
+const AddAddressRangeAction = memo(({ vnetId, onSubmit }) => {
+ const [addAR] = useAddRangeToVNetMutation()
+
+ const handleAdd = async (formData) => {
+ if (onSubmit && typeof onSubmit === 'function') {
+ return await onSubmit(formData)
+ }
+
+ const template = jsonToXml({ AR: formData })
+ await addAR({ id: vnetId, template }).unwrap()
+ }
+
+ return (
+ ,
+ label: T.AddressRange,
+ variant: 'outlined',
+ }}
+ options={[
+ {
+ dialogProps: {
+ title: T.AddressRange,
+ dataCy: 'modal-add-ar',
+ },
+ form: AddRangeForm,
+ onSubmit: handleAdd,
+ },
+ ]}
+ />
+ )
+})
+
+const UpdateAddressRangeAction = memo(({ vnetId, ar, onSubmit }) => {
+ const [updateAR] = useUpdateVNetRangeMutation()
+ const { AR_ID } = ar
+
+ const handleUpdate = async (formData) => {
+ if (onSubmit && typeof onSubmit === 'function') {
+ return await onSubmit(formData)
+ }
+
+ const template = jsonToXml({ AR: formData })
+ await updateAR({ id: vnetId, template })
+ }
+
+ return (
+ ,
+ tooltip: T.Edit,
+ }}
+ options={[
+ {
+ dialogProps: {
+ title: `${Tr(T.AddressRange)}: #${AR_ID}`,
+ dataCy: 'modal-update-ar',
+ },
+ form: () =>
+ AddRangeForm({
+ initialValues: ar,
+ stepProps: { isUpdate: !onSubmit && AR_ID !== undefined },
+ }),
+ onSubmit: handleUpdate,
+ },
+ ]}
+ />
+ )
+})
+
+const DeleteAddressRangeAction = memo(({ vnetId, ar, onSubmit }) => {
+ const [removeAR] = useRemoveRangeFromVNetMutation()
+ const { AR_ID } = ar
+
+ const handleRemove = async () => {
+ if (onSubmit && typeof onSubmit === 'function') {
+ return await onSubmit(AR_ID)
+ }
+
+ await removeAR({ id: vnetId, address: AR_ID })
+ }
+
+ return (
+ ,
+ tooltip: T.Delete,
+ }}
+ options={[
+ {
+ isConfirmDialog: true,
+ dialogProps: {
+ title: (
+ <>
+
+ {`: #${AR_ID}`}
+ >
+ ),
+ children:
{Tr(T.DoYouWantProceed)}
,
+ },
+ onSubmit: handleRemove,
+ },
+ ]}
+ />
+ )
+})
+
+const ActionPropTypes = {
+ vnetId: PropTypes.string,
+ ar: PropTypes.object,
+ onSubmit: PropTypes.func,
+}
+
+AddAddressRangeAction.propTypes = ActionPropTypes
+AddAddressRangeAction.displayName = 'AddAddressRangeActionButton'
+UpdateAddressRangeAction.propTypes = ActionPropTypes
+UpdateAddressRangeAction.displayName = 'UpdateAddressRangeActionButton'
+DeleteAddressRangeAction.propTypes = ActionPropTypes
+DeleteAddressRangeAction.displayName = 'DeleteAddressRangeAction'
+
+export {
+ AddAddressRangeAction,
+ UpdateAddressRangeAction,
+ DeleteAddressRangeAction,
+}
diff --git a/src/fireedge/src/client/components/Buttons/index.js b/src/fireedge/src/client/components/Buttons/index.js
index e402cc2da7..16694ca847 100644
--- a/src/fireedge/src/client/components/Buttons/index.js
+++ b/src/fireedge/src/client/components/Buttons/index.js
@@ -14,5 +14,6 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
+export * from 'client/components/Buttons/AddressRangeActions'
export * from 'client/components/Buttons/ScheduleAction'
export * from 'client/components/Buttons/ConsoleAction'
diff --git a/src/fireedge/src/client/components/Cards/AddressRangeCard.js b/src/fireedge/src/client/components/Cards/AddressRangeCard.js
new file mode 100644
index 0000000000..0f0fd93ecc
--- /dev/null
+++ b/src/fireedge/src/client/components/Cards/AddressRangeCard.js
@@ -0,0 +1,185 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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, memo, useMemo } from 'react'
+import PropTypes from 'prop-types'
+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 { getARLeasesInfo } from 'client/models/VirtualNetwork'
+import { Tr, Translate } from 'client/components/HOC'
+import {
+ T,
+ VirtualNetwork,
+ AddressRange,
+ VNET_THRESHOLD,
+ RESOURCE_NAMES,
+} from 'client/constants'
+import { PATH } from 'client/apps/sunstone/routesOne'
+
+const { VNET } = RESOURCE_NAMES
+
+const AddressRangeCard = memo(
+ /**
+ * @param {object} props - Props
+ * @param {VirtualNetwork} props.vnet - Virtual network
+ * @param {AddressRange} props.ar - Address Range
+ * @param {ReactElement} [props.actions] - Actions
+ * @returns {ReactElement} - Card
+ */
+ ({ vnet, ar = {}, actions }) => {
+ const classes = rowStyles()
+
+ const { PARENT_NETWORK_ID: parentId } = vnet || {}
+
+ const {
+ AR_ID,
+ TYPE,
+ IPAM_MAD,
+ USED_LEASES,
+ SIZE,
+ MAC,
+ MAC_END = '-',
+ IP,
+ IP_END = '-',
+ IP6,
+ IP6_END = '-',
+ IP6_GLOBAL,
+ IP6_GLOBAL_END = '-',
+ IP6_ULA,
+ IP6_ULA_END = '-',
+ } = ar
+
+ const { view, hasAccessToResource } = useViews()
+ const canLinkToParent = useMemo(
+ () => hasAccessToResource(VNET) && parentId,
+ [view, parentId]
+ )
+
+ const leasesInfo = useMemo(() => getARLeasesInfo(ar), [ar])
+ const { percentOfUsed, percentLabel } = leasesInfo
+
+ const labels = [
+ { text: TYPE, dataCy: 'type' },
+ IPAM_MAD && { text: IPAM_MAD, dataCy: 'ipam-mad' },
+ !USED_LEASES &&
+ SIZE && { text: `${Tr(T.Size)}: ${SIZE}`, dataCy: 'size' },
+ canLinkToParent && {
+ text: (
+
+
+
+ ),
+ dataCy: 'parent',
+ },
+ ].filter(Boolean)
+
+ return (
+ `1px solid ${theme.palette.divider}`,
+ }}
+ >
+
+
+
+ {`#${AR_ID}`}
+
+
+
+
+
+
+ {MAC && (
+ {`MAC: ${MAC} | ${MAC_END}`}
+ )}
+ {IP && (
+ {`IP: ${IP} | ${IP_END}`}
+ )}
+ {IP6 && (
+ {`IP6: ${IP6} | ${IP6_END}`}
+ )}
+ {IP6_GLOBAL && (
+ {`IP6 GLOBAL: ${IP6_GLOBAL} | ${IP6_GLOBAL_END}`}
+ )}
+ {IP6_ULA && (
+ {`IP6 ULA: ${IP6_ULA} | ${IP6_ULA_END}`}
+ )}
+
+
+
+ {USED_LEASES && (
+
+ )}
+ {actions &&
{actions}
}
+
+
+ )
+ }
+)
+
+AddressRangeCard.propTypes = {
+ vnet: PropTypes.object,
+ ar: PropTypes.object,
+ actions: PropTypes.node,
+}
+
+AddressRangeCard.displayName = 'AddressRangeCard'
+
+export default AddressRangeCard
diff --git a/src/fireedge/src/client/components/Cards/index.js b/src/fireedge/src/client/components/Cards/index.js
index 29752ff131..9f341cff7d 100644
--- a/src/fireedge/src/client/components/Cards/index.js
+++ b/src/fireedge/src/client/components/Cards/index.js
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
+import AddressRangeCard from 'client/components/Cards/AddressRangeCard'
import ApplicationCard from 'client/components/Cards/ApplicationCard'
import ApplicationNetworkCard from 'client/components/Cards/ApplicationNetworkCard'
import ApplicationTemplateCard from 'client/components/Cards/ApplicationTemplateCard'
@@ -41,6 +42,7 @@ import VmTemplateCard from 'client/components/Cards/VmTemplateCard'
import WavesCard from 'client/components/Cards/WavesCard'
export {
+ AddressRangeCard,
ApplicationCard,
ApplicationNetworkCard,
ApplicationTemplateCard,
diff --git a/src/fireedge/src/client/components/FormControl/ToggleController.js b/src/fireedge/src/client/components/FormControl/ToggleController.js
index 3f1b4dfeda..b2f77548ec 100644
--- a/src/fireedge/src/client/components/FormControl/ToggleController.js
+++ b/src/fireedge/src/client/components/FormControl/ToggleController.js
@@ -48,6 +48,7 @@ const ToggleController = memo(
values = [],
tooltip,
fieldProps = {},
+ notNull = false,
readOnly = false,
}) => {
const {
@@ -74,7 +75,9 @@ const ToggleController = memo(
fullWidth
ref={ref}
id={cy}
- onChange={(_, newValues) => !readOnly && onChange(newValues)}
+ onChange={(_, newValues) =>
+ !readOnly && (!notNull || newValues) && onChange(newValues)
+ }
value={optionSelected}
exclusive={!multiple}
data-cy={cy}
@@ -110,6 +113,7 @@ ToggleController.propTypes = {
values: PropTypes.arrayOf(PropTypes.object).isRequired,
renderValue: PropTypes.func,
fieldProps: PropTypes.object,
+ notNull: PropTypes.bool,
readOnly: PropTypes.bool,
}
diff --git a/src/fireedge/src/client/components/FormStepper/index.js b/src/fireedge/src/client/components/FormStepper/index.js
index 17823a316d..617df083e5 100644
--- a/src/fireedge/src/client/components/FormStepper/index.js
+++ b/src/fireedge/src/client/components/FormStepper/index.js
@@ -50,7 +50,11 @@ const DefaultFormStepper = ({
})
return (
-
+ resolver(methods.watch())}
+ >
)
diff --git a/src/fireedge/src/client/components/Forms/Host/ChangeClusterForm/index.js b/src/fireedge/src/client/components/Forms/Cluster/ChangeClusterForm/index.js
similarity index 95%
rename from src/fireedge/src/client/components/Forms/Host/ChangeClusterForm/index.js
rename to src/fireedge/src/client/components/Forms/Cluster/ChangeClusterForm/index.js
index 3c7e5fab38..d34fc8eaca 100644
--- a/src/fireedge/src/client/components/Forms/Host/ChangeClusterForm/index.js
+++ b/src/fireedge/src/client/components/Forms/Cluster/ChangeClusterForm/index.js
@@ -17,7 +17,7 @@ import { createForm } from 'client/utils'
import {
SCHEMA,
FIELDS,
-} from 'client/components/Forms/Host/ChangeClusterForm/schema'
+} from 'client/components/Forms/Cluster/ChangeClusterForm/schema'
const ChangeClusterForm = createForm(SCHEMA, FIELDS)
diff --git a/src/fireedge/src/client/components/Forms/Host/ChangeClusterForm/schema.js b/src/fireedge/src/client/components/Forms/Cluster/ChangeClusterForm/schema.js
similarity index 100%
rename from src/fireedge/src/client/components/Forms/Host/ChangeClusterForm/schema.js
rename to src/fireedge/src/client/components/Forms/Cluster/ChangeClusterForm/schema.js
diff --git a/src/fireedge/src/client/components/Forms/Cluster/index.js b/src/fireedge/src/client/components/Forms/Cluster/index.js
new file mode 100644
index 0000000000..9c4af095d8
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Cluster/index.js
@@ -0,0 +1,27 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC'
+import { CreateFormCallback } from 'client/utils/schema'
+
+/**
+ * @param {ConfigurationProps} configProps - Configuration
+ * @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
+ */
+const ChangeClusterForm = (configProps) =>
+ AsyncLoadForm({ formPath: 'Cluster/ChangeClusterForm' }, configProps)
+
+export { ChangeClusterForm }
diff --git a/src/fireedge/src/client/components/Forms/Host/index.js b/src/fireedge/src/client/components/Forms/Host/index.js
index a8c056676e..c7aac9775f 100644
--- a/src/fireedge/src/client/components/Forms/Host/index.js
+++ b/src/fireedge/src/client/components/Forms/Host/index.js
@@ -15,14 +15,7 @@
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC'
-import { CreateFormCallback, CreateStepsCallback } from 'client/utils/schema'
-
-/**
- * @param {ConfigurationProps} configProps - Configuration
- * @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
- */
-const ChangeClusterForm = (configProps) =>
- AsyncLoadForm({ formPath: 'Host/ChangeClusterForm' }, configProps)
+import { CreateStepsCallback } from 'client/utils/schema'
/**
* @param {ConfigurationProps} configProps - Configuration
@@ -31,4 +24,4 @@ const ChangeClusterForm = (configProps) =>
const CreateForm = (configProps) =>
AsyncLoadForm({ formPath: 'Host/CreateForm' }, configProps)
-export { ChangeClusterForm, CreateForm }
+export { CreateForm }
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/content.js b/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/content.js
new file mode 100644
index 0000000000..4b62732875
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/content.js
@@ -0,0 +1,68 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 PropTypes from 'prop-types'
+import { useFormContext, useWatch } from 'react-hook-form'
+import { Box } from '@mui/material'
+
+import { FormWithSchema } from 'client/components/Forms'
+import { AttributePanel } from 'client/components/Tabs/Common'
+import {
+ FIELDS,
+ MUTABLE_FIELDS,
+} from 'client/components/Forms/VNetwork/AddRangeForm/schema'
+import { cleanEmpty, cloneObject, set } from 'client/utils'
+import { T } from 'client/constants'
+
+export const CUSTOM_ATTRS_ID = 'custom-attributes'
+
+/**
+ * @param {object} props - Props
+ * @param {boolean} [props.isUpdate] - Is `true` the form will be filter immutable attributes
+ * @returns {ReactElement} Form content component
+ */
+const Content = ({ isUpdate }) => {
+ const { setValue } = useFormContext()
+ const customAttrs = useWatch({ name: CUSTOM_ATTRS_ID }) || {}
+
+ const handleChangeAttribute = (path, newValue) => {
+ const newCustomAttrs = cloneObject(customAttrs)
+
+ set(newCustomAttrs, path, newValue)
+ setValue(CUSTOM_ATTRS_ID, cleanEmpty(newCustomAttrs))
+ }
+
+ return (
+
+
+
+
+ )
+}
+
+Content.propTypes = { isUpdate: PropTypes.bool }
+
+export default Content
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/index.js b/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/index.js
new file mode 100644
index 0000000000..e1ede1a688
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/index.js
@@ -0,0 +1,61 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 ContentForm, {
+ CUSTOM_ATTRS_ID,
+} from 'client/components/Forms/VNetwork/AddRangeForm/content'
+import { SCHEMA } from 'client/components/Forms/VNetwork/AddRangeForm/schema'
+import { createForm } from 'client/utils'
+
+// List of attributes that can't be changed in update operation
+const IMMUTABLE_ATTRS = [
+ 'AR_ID',
+ 'TYPE',
+ 'IP',
+ 'IP_END',
+ 'IP6',
+ 'IP6_END',
+ 'MAC',
+ 'MAC_END',
+ 'IP6_GLOBAL',
+ 'IP6_GLOBAL_END',
+ 'GLOBAL_PREFIX',
+ 'ULA_PREFIX',
+ 'USED_LEASES',
+ 'PARENT_NETWORK_AR_ID',
+ 'LEASES',
+ 'IPAM_MAD',
+]
+
+const AddRangeForm = createForm(SCHEMA, undefined, {
+ ContentForm,
+ transformInitialValue: (addressRange) => {
+ if (!addressRange) return {}
+
+ const mutableAttrs = {}
+ for (const attr of Object.keys(addressRange)) {
+ !IMMUTABLE_ATTRS[attr] && (mutableAttrs[attr] = addressRange[attr])
+ }
+
+ return { ...mutableAttrs }
+ },
+ transformBeforeSubmit: (formData) => {
+ const { [CUSTOM_ATTRS_ID]: customAttrs = {}, ...rest } = formData ?? {}
+
+ return { ...customAttrs, ...rest }
+ },
+})
+
+export default AddRangeForm
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/schema.js b/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/schema.js
new file mode 100644
index 0000000000..2f5d9fd4b9
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/schema.js
@@ -0,0 +1,155 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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, string, number } from 'yup'
+
+import {
+ Field,
+ getObjectSchemaFromFields,
+ OPTION_SORTERS,
+ arrayToOptions,
+ REG_V4,
+ REG_MAC,
+} from 'client/utils'
+import { T, INPUT_TYPES } from 'client/constants'
+
+const AR_TYPES = { IP4: 'IP4', IP4_6: 'IP4_6', IP6: 'IP6', ETHER: 'ETHER' }
+
+const AR_TYPES_STR = {
+ [AR_TYPES.IP4]: 'IPv4',
+ [AR_TYPES.IP4_6]: 'IPv4/6',
+ [AR_TYPES.IP6]: 'IPv6',
+ [AR_TYPES.ETHER]: 'Ethernet',
+}
+
+/** @type {Field} Type field */
+const TYPE_FIELD = {
+ name: 'TYPE',
+ type: INPUT_TYPES.TOGGLE,
+ values: () =>
+ arrayToOptions(Object.keys(AR_TYPES), {
+ addEmpty: false,
+ getText: (type) => AR_TYPES_STR[type],
+ sorter: OPTION_SORTERS.unsort,
+ }),
+ validation: string()
+ .trim()
+ .required()
+ .default(() => 'IP4'),
+ notNull: true,
+}
+
+/** @type {Field} IP field */
+const IP_FIELD = {
+ name: 'IP',
+ label: T.FirstIPv4Address,
+ type: INPUT_TYPES.TEXT,
+ dependOf: TYPE_FIELD.name,
+ htmlType: (arType) =>
+ [AR_TYPES.IP6, AR_TYPES.ETHER].includes(arType) && INPUT_TYPES.HIDDEN,
+ validation: string()
+ .trim()
+ .default(() => undefined)
+ .when(TYPE_FIELD.name, {
+ is: (arType) => [AR_TYPES.IP6, AR_TYPES.ETHER].includes(arType),
+ then: (schema) => schema.strip().notRequired(),
+ otherwise: (schema) =>
+ schema.required().matches(REG_V4, { message: T.InvalidIPv4 }),
+ }),
+}
+
+/** @type {Field} MAC field */
+const MAC_FIELD = {
+ name: 'MAC',
+ label: T.FirstMacAddress,
+ tooltip: T.MacConcept,
+ type: INPUT_TYPES.TEXT,
+ validation: string()
+ .trim()
+ .notRequired()
+ .matches(REG_MAC, { message: T.InvalidMAC })
+ .default(() => undefined),
+}
+
+/** @type {Field} Size field */
+const SIZE_FIELD = {
+ name: 'SIZE',
+ label: T.Size,
+ type: INPUT_TYPES.TEXT,
+ htmlType: 'number',
+ validation: number()
+ .integer()
+ .required()
+ .positive()
+ .default(() => 1),
+}
+
+/** @type {Field} IPv6 Global prefix field */
+const GLOBAL_PREFIX_FIELD = {
+ name: 'GLOBAL_PREFIX',
+ label: T.IPv6GlobalPrefix,
+ type: INPUT_TYPES.TEXT,
+ dependOf: TYPE_FIELD.name,
+ htmlType: (arType) =>
+ [AR_TYPES.IP4, AR_TYPES.ETHER].includes(arType) && INPUT_TYPES.HIDDEN,
+ validation: string()
+ .trim()
+ .notRequired()
+ .default(() => undefined)
+ .when(TYPE_FIELD.name, {
+ is: (arType) => [AR_TYPES.IP4, AR_TYPES.ETHER].includes(arType),
+ then: (schema) => schema.strip(),
+ }),
+}
+
+/** @type {Field} IPv6 ULA prefix field */
+const ULA_PREFIX_FIELD = {
+ name: 'ULA_PREFIX',
+ label: T.IPv6ULAPrefix,
+ type: INPUT_TYPES.TEXT,
+ dependOf: TYPE_FIELD.name,
+ htmlType: (arType) =>
+ [AR_TYPES.IP4, AR_TYPES.ETHER].includes(arType) && INPUT_TYPES.HIDDEN,
+ validation: string()
+ .trim()
+ .notRequired()
+ .default(() => undefined)
+ .when(TYPE_FIELD.name, {
+ is: (arType) => [AR_TYPES.IP4, AR_TYPES.ETHER].includes(arType),
+ then: (schema) => schema.strip(),
+ }),
+}
+
+/** @type {Field[]} Fields */
+const FIELDS = [
+ TYPE_FIELD,
+ IP_FIELD,
+ MAC_FIELD,
+ SIZE_FIELD,
+ GLOBAL_PREFIX_FIELD,
+ ULA_PREFIX_FIELD,
+]
+
+const MUTABLE_FIELDS = [SIZE_FIELD]
+
+/**
+ * @param {object} stepProps - Step props
+ * @param {boolean} stepProps.isUpdate - If true the form is to update the AR
+ * @returns {BaseSchema} Schema
+ */
+const SCHEMA = ({ isUpdate }) =>
+ getObjectSchemaFromFields([...(isUpdate ? MUTABLE_FIELDS : FIELDS)])
+
+export { FIELDS, MUTABLE_FIELDS, SCHEMA }
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/addresses.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/addresses.js
new file mode 100644
index 0000000000..8accf01f93
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/addresses.js
@@ -0,0 +1,125 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { useFieldArray } from 'react-hook-form'
+import { Stack, Typography } from '@mui/material'
+import AddressesIcon from 'iconoir-react/dist/Menu'
+
+import { AddressRangeCard } from 'client/components/Cards'
+import {
+ AddAddressRangeAction,
+ UpdateAddressRangeAction,
+ DeleteAddressRangeAction,
+} from 'client/components/Buttons'
+import { Translate } from 'client/components/HOC'
+
+import {
+ STEP_ID as EXTRA_ID,
+ TabType,
+} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration'
+import { mapNameByIndex } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/schema'
+import { T } from 'client/constants'
+
+export const TAB_ID = 'AR'
+
+const mapNameFunction = mapNameByIndex('AR')
+
+const AddressesContent = () => {
+ 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)}
+ />
+ handleRemove(index)}
+ />
+ >
+ }
+ />
+ )
+ })}
+
+ >
+ )
+}
+
+const Content = ({ isUpdate }) =>
+ isUpdate ? (
+
+
+
+ ) : (
+
+ )
+
+Content.propTypes = { isUpdate: 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/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js
new file mode 100644
index 0000000000..7635ae1229
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js
@@ -0,0 +1,81 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { reach } from 'yup'
+import { useFormContext, useWatch } from 'react-hook-form'
+
+import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration'
+import { getUnknownVars } from 'client/components/Forms/VNetwork/CreateForm/Steps'
+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
+ const existsSchema = reach(extraSchema, path)
+ console.log(existsSchema)
+ 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/VNetwork/CreateForm/Steps/ExtraConfiguration/context/index.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/index.js
new file mode 100644
index 0000000000..38bb1055de
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/index.js
@@ -0,0 +1,44 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 ContextIcon from 'iconoir-react/dist/Folder'
+
+import {
+ TabType,
+ STEP_ID as EXTRA_ID,
+} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration'
+import { FIELDS } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/schema'
+import CustomAttributes from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes'
+
+import FormWithSchema from 'client/components/Forms/FormWithSchema'
+import { T } from 'client/constants'
+
+const ContextContent = () => (
+ <>
+
+
+ >
+)
+
+/** @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/VNetwork/CreateForm/Steps/ExtraConfiguration/context/schema.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/schema.js
new file mode 100644
index 0000000000..160f7c5970
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/schema.js
@@ -0,0 +1,110 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 } from 'client/utils'
+import { T, INPUT_TYPES, VNET_METHODS, VNET_METHODS6 } 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(),
+}
+
+export const FIELDS = [
+ NETWORK_ADDRESS_FIELD,
+ NETWORK_MASK_FIELD,
+ GATEWAY_FIELD,
+ GATEWAY6_FIELD,
+ DNS_FIELD,
+ GUEST_MTU_FIELD,
+ METHOD_FIELD,
+ IP6_METHOD_FIELD,
+]
+
+export const SCHEMA = getObjectSchemaFromFields(FIELDS)
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/index.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/index.js
new file mode 100644
index 0000000000..5b45f16944
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/index.js
@@ -0,0 +1,97 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { useMemo, ReactElement } from 'react'
+import PropTypes from 'prop-types'
+// eslint-disable-next-line no-unused-vars
+import { useFormContext, FieldErrors } from 'react-hook-form'
+
+import Tabs from 'client/components/Tabs'
+import Addresses from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/addresses'
+import Security from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/security'
+import QoS from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos'
+import Context from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context'
+import { Translate } from 'client/components/HOC'
+
+import { STEP_ID as GENERAL_ID } from 'client/components/Forms/VNetwork/CreateForm/Steps/General'
+import { SCHEMA } from 'client/components/Forms/VNetwork/CreateForm/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, QoS, Context]
+
+const Content = ({ isUpdate }) => {
+ 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} vnet - Virtual network
+ * @returns {object} Optional configuration step
+ */
+const ExtraConfiguration = (vnet) => {
+ const isUpdate = vnet?.NAME !== undefined
+
+ return {
+ id: STEP_ID,
+ label: T.AdvancedOptions,
+ resolver: SCHEMA(isUpdate),
+ optionsValidate: { abortEarly: false },
+ content: (formProps) => Content({ ...formProps, isUpdate }),
+ }
+}
+
+Content.propTypes = {
+ data: PropTypes.any,
+ setFormData: PropTypes.func,
+ isUpdate: PropTypes.bool,
+}
+
+export default ExtraConfiguration
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/index.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/index.js
new file mode 100644
index 0000000000..636978e4c4
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/index.js
@@ -0,0 +1,52 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 QoSIcon from 'iconoir-react/dist/DataTransferBoth'
+
+import {
+ STEP_ID as EXTRA_ID,
+ TabType,
+} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration'
+import {
+ SECTIONS,
+ FIELDS,
+} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/schema'
+
+import FormWithSchema from 'client/components/Forms/FormWithSchema'
+import { T } from 'client/constants'
+
+const QoSContent = () => (
+ <>
+ {SECTIONS.map(({ id, ...section }) => (
+
+ ))}
+ >
+)
+
+/** @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/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/schema.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/schema.js
new file mode 100644
index 0000000000..74d27ca318
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/schema.js
@@ -0,0 +1,103 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 } from 'client/utils'
+import { T, INPUT_TYPES } 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 = [
+ {
+ id: 'qos-inbound',
+ legend: T.InboundTraffic,
+ fields: [
+ INBOUND_AVG_BW_FIELD,
+ INBOUND_PEAK_BW_FIELD,
+ INBOUND_PEAK_KB_FIELD,
+ ],
+ },
+ {
+ id: 'qos-outbound',
+ legend: T.OutboundTraffic,
+ fields: [
+ OUTBOUND_AVG_BW_FIELD,
+ OUTBOUND_PEAK_BW_FIELD,
+ OUTBOUND_PEAK_KB_FIELD,
+ ],
+ },
+]
+
+/** @type {Field[]} List of QoS fields */
+const FIELDS = SECTIONS.map(({ fields }) => fields).flat()
+
+/** @type {ObjectSchema} QoS schema */
+const SCHEMA = getObjectSchemaFromFields(FIELDS)
+
+export { SCHEMA, SECTIONS, FIELDS }
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/schema.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/schema.js
new file mode 100644
index 0000000000..01d31b2273
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/schema.js
@@ -0,0 +1,55 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 QOS_SCHEMA } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/schema'
+import { SCHEMA as CONTEXT_SCHEMA } from 'client/components/Forms/VNetwork/CreateForm/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
+ * @returns {ObjectSchema} Extra configuration schema
+ */
+export const SCHEMA = (isUpdate) => {
+ const schema = object({ SECURITY_GROUPS: array().ensure() })
+ .concat(CONTEXT_SCHEMA)
+ .concat(QOS_SCHEMA)
+
+ !isUpdate && schema.concat(AR_SCHEMA)
+
+ return schema
+}
+
+export { mapNameByIndex, AR_SCHEMA }
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/security.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/security.js
new file mode 100644
index 0000000000..9c9ad00b66
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/security.js
@@ -0,0 +1,66 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { useFormContext, useWatch } from 'react-hook-form'
+import SecurityIcon from 'iconoir-react/dist/HistoricShield'
+
+import { SecurityGroupsTable } from 'client/components/Tables'
+
+import {
+ STEP_ID as EXTRA_ID,
+ TabType,
+} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration'
+import { T } from 'client/constants'
+
+export const TAB_ID = 'SECURITY_GROUPS'
+
+const SecurityContent = () => {
+ 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)
+ }
+
+ return (
+
+ )
+}
+
+/** @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/VNetwork/CreateForm/Steps/General/commonFields.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/commonFields.js
new file mode 100644
index 0000000000..4f9e124d7f
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/commonFields.js
@@ -0,0 +1,235 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { lazy, string, boolean, object } from 'yup'
+
+import { DRIVER_FIELD } from 'client/components/Forms/VNetwork/CreateForm/Steps/General/informationSchema'
+import { Field, arrayToOptions } from 'client/utils'
+import { T, INPUT_TYPES, VN_DRIVERS } from 'client/constants'
+
+const {
+ fw,
+ ebtables,
+ dot1Q,
+ vxlan,
+ ovswitch,
+ ovswitch_vxlan: openVSwitchVXLAN,
+} = VN_DRIVERS
+
+/** @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,
+ onlyOnHypervisors: [fw, ebtables],
+ 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,
+ onlyOnHypervisors: [fw, ebtables],
+ validation: boolean().yesOrNo(),
+ grid: { md: 12 },
+}
+
+/** @type {Field} MTU field */
+const MTU_FIELD = {
+ name: 'MTU',
+ label: T.MTU,
+ tooltip: T.MTUConcept,
+ onlyOnHypervisors: [dot1Q, vxlan, ovswitch, openVSwitchVXLAN],
+ 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,
+ dependOf: 'AUTOMATIC_VLAN_ID',
+ type: INPUT_TYPES.SWITCH,
+ onlyOnHypervisors: [dot1Q, vxlan, ovswitch, openVSwitchVXLAN],
+ 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,
+ onlyOnHypervisors: [dot1Q, vxlan, ovswitch, openVSwitchVXLAN],
+ dependOf: AUTOMATIC_VLAN_FIELD.name,
+ htmlType: (automatic) => automatic && 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,
+ onlyOnHypervisors: [openVSwitchVXLAN],
+ validation: lazy((_, { context }) =>
+ boolean()
+ .yesOrNo()
+ .default(() => context?.AUTOMATIC_OUTER_VLAN_ID === '1')
+ ),
+ dependOf: 'AUTOMATIC_OUTER_VLAN_ID',
+ 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,
+ onlyOnHypervisors: [openVSwitchVXLAN],
+ dependOf: AUTOMATIC_OUTER_VLAN_ID_FIELD.name,
+ htmlType: (oAutomatic) => oAutomatic && 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 },
+}
+
+/** @type {Field} VXLAN mode field */
+const VXLAN_MODE_FIELD = {
+ name: 'VXLAN_MODE',
+ label: T.VxlanMode,
+ tooltip: T.VxlanModeConcept,
+ type: INPUT_TYPES.SELECT,
+ onlyOnHypervisors: [vxlan],
+ values: arrayToOptions(['evpn', 'multicast']),
+ validation: string()
+ .trim()
+ .default(() => undefined),
+}
+
+/** @type {Field} VXLAN tunnel endpoint field */
+const VXLAN_TEP_FIELD = {
+ name: 'VXLAN_TEP',
+ label: T.VxlanTunnelEndpoint,
+ tooltip: T.VxlanTunnelEndpointConcept,
+ type: INPUT_TYPES.SELECT,
+ onlyOnHypervisors: [vxlan],
+ dependOf: VXLAN_MODE_FIELD.name,
+ htmlType: (mode) => mode !== 'evpn' && INPUT_TYPES.HIDDEN,
+ values: arrayToOptions(['dev', 'local_ip']),
+ validation: string()
+ .trim()
+ .default(() => undefined)
+ .when(VXLAN_MODE_FIELD.name, {
+ is: (mode) => mode !== 'evpn',
+ then: (schema) => schema.strip(),
+ }),
+}
+
+/** @type {Field} VXLAN multicast field */
+const VXLAN_MC_FIELD = {
+ name: 'VXLAN_MC',
+ label: T.VxlanMulticast,
+ tooltip: T.VxlanMulticastConcept,
+ type: INPUT_TYPES.TEXT,
+ onlyOnHypervisors: [vxlan],
+ dependOf: VXLAN_MODE_FIELD.name,
+ htmlType: (mode) => mode !== 'multicast' && INPUT_TYPES.HIDDEN,
+ validation: string()
+ .trim()
+ .default(() => undefined)
+ .when(VXLAN_MODE_FIELD.name, {
+ is: (mode) => mode !== 'multicast',
+ then: (schema) => schema.strip(),
+ }),
+}
+
+/** @type {Field[]} List of common fields */
+export const FIELDS = [
+ 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,
+ VXLAN_MODE_FIELD,
+ VXLAN_TEP_FIELD,
+ VXLAN_MC_FIELD,
+]
+
+export 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(',')
+ }),
+}
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/index.js
new file mode 100644
index 0000000000..ac6be8f4ef
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/index.js
@@ -0,0 +1,113 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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, useMemo } from 'react'
+import PropTypes from 'prop-types'
+import { useFormContext, useWatch } from 'react-hook-form'
+
+import FormWithSchema from 'client/components/Forms/FormWithSchema'
+import { Legend } from 'client/components/Forms'
+import { AttributePanel } from 'client/components/Tabs/Common'
+
+import {
+ SCHEMA,
+ SECTIONS,
+ IP_LINK_CONF_FIELD,
+} from 'client/components/Forms/VNetwork/CreateForm/Steps/General/schema'
+import { cleanEmpty, cloneObject, set } from 'client/utils'
+import { T, VirtualNetwork, VN_DRIVERS } from 'client/constants'
+
+export const STEP_ID = 'general'
+const DRIVER_PATH = `${STEP_ID}.VN_MAD`
+const IP_CONF_PATH = `${STEP_ID}.${IP_LINK_CONF_FIELD.name}`
+
+/**
+ * @param {object} props - Props
+ * @param {boolean} [props.isUpdate] - Is `true` the form will be filter immutable attributes
+ * @returns {ReactElement} Form content component
+ */
+const Content = ({ isUpdate }) => {
+ const { setValue } = useFormContext()
+
+ const driver = useWatch({ name: DRIVER_PATH })
+ const ipConf = useWatch({ name: IP_CONF_PATH }) || {}
+
+ const sections = useMemo(() => SECTIONS(driver, isUpdate), [driver])
+
+ const handleChangeAttribute = (path, newValue) => {
+ const newConf = cloneObject(ipConf)
+
+ set(newConf, path, newValue)
+ setValue(IP_CONF_PATH, cleanEmpty(newConf))
+ }
+
+ return (
+ <>
+ {sections.map(({ id, ...section }) => (
+
+ ))}
+ {driver === VN_DRIVERS.vxlan && (
+
+ }
+ allActionsEnabled
+ handleAdd={handleChangeAttribute}
+ handleEdit={handleChangeAttribute}
+ handleDelete={handleChangeAttribute}
+ attributes={ipConf}
+ filtersSpecialAttributes={false}
+ />
+ )}
+ >
+ )
+}
+
+/**
+ * General configuration about Virtual network.
+ *
+ * @param {VirtualNetwork} vnet - Virtual network
+ * @returns {object} General configuration step
+ */
+const General = (vnet) => {
+ const isUpdate = vnet?.NAME !== undefined
+ const initialDriver = vnet?.VN_MAD
+
+ return {
+ id: STEP_ID,
+ label: T.General,
+ resolver: (formData) =>
+ SCHEMA(formData?.[STEP_ID]?.VN_MAD ?? initialDriver, isUpdate),
+ optionsValidate: { abortEarly: false },
+ content: () => Content({ isUpdate }),
+ }
+}
+
+Content.propTypes = {
+ isUpdate: PropTypes.bool,
+}
+
+export default General
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/informationSchema.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/informationSchema.js
new file mode 100644
index 0000000000..c174b8ec66
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/informationSchema.js
@@ -0,0 +1,99 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { useGetClustersQuery } from 'client/features/OneApi/cluster'
+import { OPTION_SORTERS, Field, arrayToOptions } from 'client/utils'
+import { T, INPUT_TYPES, VN_DRIVERS_STR } from 'client/constants'
+
+/**
+ * @param {boolean} isUpdate - If `true`, the form is being updated
+ * @returns {Field} Name field
+ */
+export const NAME_FIELD = (isUpdate) => ({
+ 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)),
+ ...(isUpdate && { fieldProps: { disabled: true } }),
+})
+
+/** @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),
+ grid: { md: 12 },
+}
+
+/** @type {Field} Cluster field */
+export const CLUSTER_FIELD = {
+ name: 'CLUSTER',
+ label: T.Cluster,
+ type: INPUT_TYPES.AUTOCOMPLETE,
+ values: () => {
+ const { data: clusters = [] } = useGetClustersQuery()
+
+ return arrayToOptions(clusters, {
+ addEmpty: false,
+ getText: ({ ID, NAME }) => `#${ID} ${NAME}`,
+ getValue: ({ ID }) => ID,
+ sorter: OPTION_SORTERS.numeric,
+ })
+ },
+ validation: string()
+ .trim()
+ .notRequired()
+ .default(() => undefined),
+}
+
+const drivers = Object.keys(VN_DRIVERS_STR)
+
+/** @type {Field} Driver field */
+export 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 },
+}
+
+/**
+ * @param {boolean} isUpdate - If `true`, the form is being updated
+ * @returns {Field[]} List of information fields
+ */
+export const FIELDS = (isUpdate) =>
+ [NAME_FIELD(isUpdate), !isUpdate && CLUSTER_FIELD, DESCRIPTION_FIELD].filter(
+ Boolean
+ )
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/schema.js
new file mode 100644
index 0000000000..395ae3838b
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/schema.js
@@ -0,0 +1,61 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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, object } from 'yup'
+
+import { DRIVER_FIELD, FIELDS as INFORMATION_FIELDS } from './informationSchema'
+import { FIELDS as FIELDS_BY_DRIVER, IP_LINK_CONF_FIELD } from './commonFields'
+
+import {
+ Section,
+ getObjectSchemaFromFields,
+ filterFieldsByHypervisor,
+} from 'client/utils'
+import { T, VN_DRIVERS } from 'client/constants'
+
+/**
+ * @param {VN_DRIVERS} driver - Virtual network driver
+ * @param {boolean} [isUpdate] - If `true`, the form is being updated
+ * @returns {Section[]} Fields
+ */
+const SECTIONS = (driver, isUpdate) => [
+ {
+ id: 'information',
+ legend: T.Information,
+ fields: INFORMATION_FIELDS(isUpdate),
+ },
+ {
+ id: 'configuration',
+ legend: T.Configuration,
+ fields: filterFieldsByHypervisor(
+ [DRIVER_FIELD, ...FIELDS_BY_DRIVER],
+ driver
+ ),
+ },
+]
+
+/**
+ * @param {VN_DRIVERS} driver - Virtual network driver
+ * @param {boolean} [isUpdate] - If `true`, the form is being updated
+ * @returns {BaseSchema} Step schema
+ */
+const SCHEMA = (driver, isUpdate) =>
+ getObjectSchemaFromFields(
+ SECTIONS(driver, isUpdate)
+ .map(({ schema, fields }) => schema ?? fields)
+ .flat()
+ ).concat(object({ [IP_LINK_CONF_FIELD.name]: IP_LINK_CONF_FIELD.validation }))
+
+export { SECTIONS, SCHEMA, IP_LINK_CONF_FIELD }
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/index.js
new file mode 100644
index 0000000000..8285130cd5
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/index.js
@@ -0,0 +1,84 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { reach, ObjectSchema } from 'yup'
+
+import General, {
+ STEP_ID as GENERAL_ID,
+} from 'client/components/Forms/VNetwork/CreateForm/Steps/General'
+import ExtraConfiguration, {
+ STEP_ID as EXTRA_ID,
+} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration'
+
+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, AR_POOL, ...vnet } = {}, schema) => {
+ const initialValue = schema.cast(
+ {
+ [GENERAL_ID]: { ...vnet },
+ [EXTRA_ID]: { ...TEMPLATE, AR: AR_POOL.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/VNetwork/CreateForm/index.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/index.js
new file mode 100644
index 0000000000..13aa1307a8
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/index.js
@@ -0,0 +1,16 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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/VNetwork/CreateForm/Steps'
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/ReserveForm/index.js b/src/fireedge/src/client/components/Forms/VNetwork/ReserveForm/index.js
new file mode 100644
index 0000000000..59a35c368a
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/ReserveForm/index.js
@@ -0,0 +1,27 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 {
+ SCHEMA,
+ FIELDS,
+} from 'client/components/Forms/VNetwork/ReserveForm/schema'
+import { jsonToXml } from 'client/models/Helper'
+import { createForm } from 'client/utils'
+
+const AddRangeForm = createForm(SCHEMA, FIELDS, {
+ transformBeforeSubmit: (formData) => jsonToXml({ ...formData }),
+})
+
+export default AddRangeForm
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/ReserveForm/schema.js b/src/fireedge/src/client/components/Forms/VNetwork/ReserveForm/schema.js
new file mode 100644
index 0000000000..52f1c5dcb3
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/ReserveForm/schema.js
@@ -0,0 +1,187 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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, string, number } from 'yup'
+
+import { useGetVNetworksQuery } from 'client/features/OneApi/network'
+import { getAddressType } from 'client/models/VirtualNetwork'
+import {
+ Field,
+ getObjectSchemaFromFields,
+ OPTION_SORTERS,
+ arrayToOptions,
+ REG_ADDR,
+} from 'client/utils'
+import { T, INPUT_TYPES, VirtualNetwork, AddressRange } from 'client/constants'
+
+const SWITCH_TYPES = { newVnet: 'vnet', fromAr: 'ar' }
+
+const SWITCH_STR = {
+ [SWITCH_TYPES.newVnet]: T.AddToNewVirtualNetwork,
+ [SWITCH_TYPES.fromAr]: T.AddToExistingReservation,
+}
+
+/** @type {Field} Number of addresses field */
+const SIZE_FIELD = {
+ name: 'SIZE',
+ label: T.NumberOfAddresses,
+ type: INPUT_TYPES.TEXT,
+ htmlType: 'number',
+ validation: number()
+ .positive()
+ .required()
+ .default(() => 1),
+}
+
+/** @type {Field} Switcher for vnet OR existing reservation */
+const SWITCH_FIELD = {
+ name: '__SWITCH__',
+ type: INPUT_TYPES.TOGGLE,
+ values: () =>
+ arrayToOptions(Object.entries(SWITCH_STR), {
+ addEmpty: false,
+ getText: ([, text]) => text,
+ getValue: ([value]) => value,
+ }),
+ validation: string()
+ .trim()
+ .required()
+ .default(() => SWITCH_TYPES.newVnet)
+ .afterSubmit(() => undefined),
+ grid: { sm: 12, md: 12 },
+ notNull: true,
+}
+
+/** @type {Field} Name of the new virtual network */
+const NAME_FIELD = {
+ name: 'NAME',
+ label: T.Name,
+ type: INPUT_TYPES.TEXT,
+ dependOf: SWITCH_FIELD.name,
+ htmlType: (switcher) =>
+ switcher === SWITCH_TYPES.fromAr && INPUT_TYPES.HIDDEN,
+ validation: string()
+ .trim()
+ .default(() => undefined)
+ .when(SWITCH_FIELD.name, {
+ is: SWITCH_TYPES.fromAr,
+ then: (schema) => schema.strip(),
+ otherwise: (schema) => schema.required(),
+ }),
+}
+
+/**
+ * @param {object} stepProps - Step props
+ * @param {VirtualNetwork} stepProps.vnet - Virtual Network
+ * @returns {Field} Add to an existing reservation
+ */
+const EXISTING_RESERVE_FIELD = ({ vnet = {} }) => ({
+ name: 'NETWORK_ID',
+ label: T.SelectNetwork,
+ type: INPUT_TYPES.SELECT,
+ dependOf: SWITCH_FIELD.name,
+ htmlType: (switcher) =>
+ switcher === SWITCH_TYPES.newVnet && INPUT_TYPES.HIDDEN,
+ values: () => {
+ const { data: reservedVNets } = useGetVNetworksQuery(undefined, {
+ selectFromResult: ({ data: result = [] }) => ({
+ data: result?.filter((vn) => +vn?.PARENT_NETWORK_ID === +vnet.ID),
+ }),
+ })
+
+ return arrayToOptions(reservedVNets, {
+ getText: ({ ID, NAME }) => `#${ID} ${NAME}`,
+ getValue: ({ ID }) => ID,
+ sorter: OPTION_SORTERS.numeric,
+ })
+ },
+ validation: string()
+ .trim()
+ .default(() => undefined)
+ .when(SWITCH_FIELD.name, {
+ is: SWITCH_TYPES.newVnet,
+ then: (schema) => schema.strip(),
+ otherwise: (schema) => schema.required(),
+ }),
+})
+
+/**
+ * @param {object} stepProps - Step props
+ * @param {AddressRange[]} stepProps.arPool - Address Ranges
+ * @returns {Field} Add to an existing reservation
+ */
+const AR_FIELD = ({ arPool = {} }) => ({
+ name: 'AR_ID',
+ label: T.CanSelectAddressFromAR,
+ type: INPUT_TYPES.SELECT,
+ dependOf: SWITCH_FIELD.name,
+ values: arrayToOptions(arPool, {
+ getText: ({ AR_ID, IP, MAC }) =>
+ `#${AR_ID} ${IP ? 'IP' : 'MAC'} range: ${IP ?? MAC}`,
+ getValue: ({ AR_ID }) => AR_ID,
+ sorter: OPTION_SORTERS.numeric,
+ }),
+ validation: string()
+ .trim()
+ .default(() => undefined),
+})
+
+/** @type {Field} First address field */
+const ADDR_FIELD = {
+ name: 'ADDR',
+ label: T.FirstAddress,
+ type: INPUT_TYPES.TEXT,
+ validation: string()
+ .trim()
+ .notRequired()
+ .matches(REG_ADDR, { message: T.InvalidAddress })
+ .default(() => undefined),
+ fieldProps: { placeholder: T.IpOrMac },
+}
+
+/**
+ * @param {object} stepProps - Step props
+ * @param {VirtualNetwork} stepProps.vnet - Virtual network
+ * @returns {Field[]} Fields
+ */
+const FIELDS = (stepProps) => {
+ const arPool = [stepProps?.vnet?.AR_POOL?.AR ?? []].flat()
+
+ return [
+ SWITCH_FIELD,
+ SIZE_FIELD,
+ NAME_FIELD,
+ EXISTING_RESERVE_FIELD(stepProps),
+ arPool.length > 0 && AR_FIELD({ arPool }),
+ ADDR_FIELD,
+ ].filter(Boolean)
+}
+
+/**
+ * @param {object} stepProps - Step props
+ * @param {VirtualNetwork} stepProps.vnet - Virtual network
+ * @returns {BaseSchema} Schema
+ */
+const SCHEMA = (stepProps) =>
+ getObjectSchemaFromFields([...FIELDS(stepProps)]).afterSubmit(
+ ({ [ADDR_FIELD.name]: addr, ...result }) => {
+ const addrType = getAddressType(addr)
+ addrType && (result[addrType] = addr)
+
+ return result
+ }
+ )
+
+export { FIELDS, SCHEMA }
diff --git a/src/fireedge/src/client/components/Forms/VNetwork/index.js b/src/fireedge/src/client/components/Forms/VNetwork/index.js
new file mode 100644
index 0000000000..1cfbd0337b
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/VNetwork/index.js
@@ -0,0 +1,41 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC'
+import { CreateStepsCallback, CreateFormCallback } from 'client/utils/schema'
+
+/**
+ * @param {ConfigurationProps} configProps - Configuration
+ * @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
+ */
+const AddRangeForm = (configProps) =>
+ AsyncLoadForm({ formPath: 'VNetwork/AddRangeForm' }, configProps)
+
+/**
+ * @param {ConfigurationProps} configProps - Configuration
+ * @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
+ */
+const CreateForm = (configProps) =>
+ AsyncLoadForm({ formPath: 'VNetwork/CreateForm' }, configProps)
+
+/**
+ * @param {ConfigurationProps} configProps - Configuration
+ * @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
+ */
+const ReserveForm = (configProps) =>
+ AsyncLoadForm({ formPath: 'VNetwork/ReserveForm' }, configProps)
+
+export { AddRangeForm, CreateForm, ReserveForm }
diff --git a/src/fireedge/src/client/components/Tables/Clusters/index.js b/src/fireedge/src/client/components/Tables/Clusters/index.js
index 4a09e10113..50912bb7ef 100644
--- a/src/fireedge/src/client/components/Tables/Clusters/index.js
+++ b/src/fireedge/src/client/components/Tables/Clusters/index.js
@@ -30,12 +30,17 @@ const DEFAULT_DATA_CY = 'clusters'
* @returns {ReactElement} Clusters table
*/
const ClustersTable = (props) => {
- const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
+ const {
+ rootProps = {},
+ searchProps = {},
+ useQuery = useGetClustersQuery,
+ ...rest
+ } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews()
- const { data = [], isFetching, refetch } = useGetClustersQuery()
+ const { data = [], isFetching, refetch } = useQuery()
const columns = useMemo(
() =>
diff --git a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/index.js b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/index.js
index 367145f16d..efa94b5eb3 100644
--- a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/index.js
+++ b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/index.js
@@ -18,12 +18,7 @@ import PropTypes from 'prop-types'
import { Stack, Checkbox } from '@mui/material'
import { RefreshDouble } from 'iconoir-react'
-import {
- UseTableInstanceProps,
- UseRowSelectState,
- UseFiltersInstanceProps,
- UseRowSelectInstanceProps,
-} from 'react-table'
+import { UseTableInstanceProps, UseRowSelectInstanceProps } from 'react-table'
import {
Action,
@@ -44,6 +39,7 @@ import { T } from 'client/constants'
* @param {boolean} props.disableRowSelect - Rows can't select
* @param {GlobalAction[]} props.globalActions - Possible bulk actions
* @param {UseTableInstanceProps} props.useTableProps - Table props
+ * @param {object[]} props.selectedRows - Selected Rows
* @returns {ReactElement} Component JSX with all actions
*/
const GlobalActions = ({
@@ -53,20 +49,13 @@ const GlobalActions = ({
singleSelect = false,
disableRowSelect = false,
globalActions = [],
+ selectedRows,
useTableProps = {},
}) => {
/** @type {UseRowSelectInstanceProps} */
const { getToggleAllPageRowsSelectedProps, getToggleAllRowsSelectedProps } =
useTableProps
- /** @type {UseFiltersInstanceProps} */
- const { preFilteredRows } = useTableProps
-
- /** @type {UseRowSelectState} */
- const { selectedRowIds } = useTableProps?.state
-
- const selectedRows = preFilteredRows.filter((row) => !!selectedRowIds[row.id])
-
return (
)}
{!singleSelect && !disableRowSelect && (
- <>
-
- {globalActions?.map((item, idx) => {
- const key = item.accessor ?? item.label ?? item.tooltip ?? idx
-
- return
- })}
- >
+
)}
+ {globalActions?.map((item, idx) => {
+ if ((singleSelect || disableRowSelect) && item.selected) return null
+
+ const key = item.accessor ?? item.label ?? item.tooltip ?? idx
+
+ return
+ })}
)
}
@@ -111,6 +100,7 @@ GlobalActions.propTypes = {
disableRowSelect: PropTypes.bool,
globalActions: PropTypes.array,
useTableProps: PropTypes.object,
+ selectedRows: PropTypes.array,
}
export default GlobalActions
diff --git a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalLabel/index.js b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalLabel/index.js
index 78bd97083e..fcc3fd0012 100644
--- a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalLabel/index.js
+++ b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalLabel/index.js
@@ -57,7 +57,7 @@ const sortByFilteredFirst = (labels, filters) =>
* Button to filter rows by label or assign labels to selected rows.
*
* @param {UseFiltersInstanceProps} props - Component props
- * @param {object} props.selectedRows - Selected rows
+ * @param {object[]} props.selectedRows - Selected rows
* @param {Function} props.useUpdateMutation - Callback to update row labels
* @returns {ReactElement} Button component
*/
diff --git a/src/fireedge/src/client/components/Tables/Enhanced/index.js b/src/fireedge/src/client/components/Tables/Enhanced/index.js
index e8842030d6..8b271c3a68 100644
--- a/src/fireedge/src/client/components/Tables/Enhanced/index.js
+++ b/src/fireedge/src/client/components/Tables/Enhanced/index.js
@@ -199,6 +199,7 @@ const EnhancedTable = ({
singleSelect={singleSelect}
disableRowSelect={disableRowSelect}
globalActions={globalActions}
+ selectedRows={selectedRows}
useTableProps={useTableProps}
/>
diff --git a/src/fireedge/src/client/components/Tables/Hosts/actions.js b/src/fireedge/src/client/components/Tables/Hosts/actions.js
index dcbe7b42bd..ddbcd89eba 100644
--- a/src/fireedge/src/client/components/Tables/Hosts/actions.js
+++ b/src/fireedge/src/client/components/Tables/Hosts/actions.js
@@ -27,7 +27,7 @@ import {
} from 'client/features/OneApi/host'
import { Translate } from 'client/components/HOC'
-import { ChangeClusterForm } from 'client/components/Forms/Host'
+import { ChangeClusterForm } from 'client/components/Forms/Cluster'
import {
createActions,
GlobalAction,
diff --git a/src/fireedge/src/client/components/Tables/SecurityGroups/index.js b/src/fireedge/src/client/components/Tables/SecurityGroups/index.js
index 3a84b3f533..999c353f4d 100644
--- a/src/fireedge/src/client/components/Tables/SecurityGroups/index.js
+++ b/src/fireedge/src/client/components/Tables/SecurityGroups/index.js
@@ -30,12 +30,17 @@ const DEFAULT_DATA_CY = 'secgroup'
* @returns {ReactElement} Security Groups table
*/
const SecurityGroupsTable = (props) => {
- const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
+ const {
+ rootProps = {},
+ searchProps = {},
+ useQuery = useGetSecGroupsQuery,
+ ...rest
+ } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews()
- const { data = [], isFetching, refetch } = useGetSecGroupsQuery()
+ const { data = [], isFetching, refetch } = useQuery()
const columns = useMemo(
() =>
diff --git a/src/fireedge/src/client/components/Tables/VNetworks/actions.js b/src/fireedge/src/client/components/Tables/VNetworks/actions.js
new file mode 100644
index 0000000000..0b2b84aa2c
--- /dev/null
+++ b/src/fireedge/src/client/components/Tables/VNetworks/actions.js
@@ -0,0 +1,333 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { 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 { useViews } from 'client/features/Auth'
+import { useAddNetworkToClusterMutation } from 'client/features/OneApi/cluster'
+import {
+ useReserveAddressMutation,
+ useLockVNetMutation,
+ useUnlockVNetMutation,
+ useChangeVNetOwnershipMutation,
+ useRemoveVNetMutation,
+} from 'client/features/OneApi/network'
+
+import { ChangeUserForm, ChangeGroupForm } from 'client/components/Forms/Vm'
+import { ReserveForm } from 'client/components/Forms/VNetwork'
+import { ChangeClusterForm } from 'client/components/Forms/Cluster'
+import {
+ createActions,
+ GlobalAction,
+} 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'
+
+const useTableStyles = makeStyles({
+ body: { gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))' },
+})
+
+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 [changeCluster] = useAddNetworkToClusterMutation()
+ const [lock] = useLockVNetMutation()
+ const [unlock] = useUnlockVNetMutation()
+ const [changeOwnership] = useChangeVNetOwnershipMutation()
+ const [remove] = useRemoveVNetMutation()
+
+ const actions = useMemo(
+ () =>
+ createActions({
+ filters: getResourceView(RESOURCE_NAMES.VNET)?.actions,
+ actions: [
+ {
+ accessor: VN_ACTIONS.CREATE_DIALOG,
+ dataCy: `vnet-${VN_ACTIONS.CREATE_DIALOG}`,
+ tooltip: T.Create,
+ 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}`,
+ tooltip: T.Instantiate,
+ selected: true,
+ icon: PlayOutline,
+ options: [
+ {
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.Instantiate,
+ children: () => {
+ const classes = useTableStyles()
+ const path = PATH.NETWORK.VN_TEMPLATES.INSTANTIATE
+
+ return (
+ history.push(path, vnet)}
+ />
+ )
+ },
+ fixedWidth: true,
+ fixedHeight: true,
+ handleAccept: undefined,
+ dataCy: `modal-${VN_ACTIONS.CREATE_DIALOG}`,
+ },
+ },
+ ],
+ },
+ {
+ accessor: VN_ACTIONS.UPDATE_DIALOG,
+ dataCy: `vnet-${VN_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.VNETS.CREATE
+
+ history.push(path, vnet)
+ },
+ },
+ {
+ accessor: VN_ACTIONS.RESERVE_DIALOG,
+ dataCy: `vnet-${VN_ACTIONS.RESERVE_DIALOG}`,
+ label: T.Reserve,
+ selected: { max: 1 },
+ color: 'secondary',
+ options: [
+ {
+ dialogProps: {
+ title: T.ReservationFromVirtualNetwork,
+ dataCy: 'modal-reserve',
+ },
+ form: (rows) => {
+ const vnet = rows?.[0]?.original || {}
+
+ return ReserveForm({ stepProps: { vnet } })
+ },
+ onSubmit: (rows) => async (template) => {
+ const ids = rows?.map?.(({ original }) => original?.ID)
+ await Promise.all(ids.map((id) => reserve({ id, template })))
+ },
+ },
+ ],
+ },
+ {
+ accessor: VN_ACTIONS.CHANGE_CLUSTER,
+ color: 'secondary',
+ dataCy: `vnet-${VN_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: 'vnet-ownership',
+ options: [
+ {
+ accessor: VN_ACTIONS.CHANGE_OWNER,
+ name: T.ChangeOwner,
+ dialogProps: {
+ title: T.ChangeOwner,
+ subheader: SubHeader,
+ dataCy: `modal-${VN_ACTIONS.CHANGE_OWNER}`,
+ },
+ form: ChangeUserForm,
+ onSubmit: (rows) => (newOwnership) => {
+ rows?.map?.(({ original }) =>
+ changeOwnership({ id: original?.ID, ...newOwnership })
+ )
+ },
+ },
+ {
+ accessor: VN_ACTIONS.CHANGE_GROUP,
+ name: T.ChangeGroup,
+ dialogProps: {
+ title: T.ChangeGroup,
+ subheader: SubHeader,
+ dataCy: `modal-${VN_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: 'vnet-lock',
+ options: [
+ {
+ accessor: VN_ACTIONS.LOCK,
+ name: T.Lock,
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.Lock,
+ dataCy: `modal-${VN_ACTIONS.LOCK}`,
+ children: MessageToConfirmAction,
+ },
+ onSubmit: (rows) => async () => {
+ const ids = rows?.map?.(({ original }) => original?.ID)
+ await Promise.all(ids.map((id) => lock({ id })))
+ },
+ },
+ {
+ accessor: VN_ACTIONS.UNLOCK,
+ name: T.Unlock,
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.Unlock,
+ dataCy: `modal-${VN_ACTIONS.UNLOCK}`,
+ children: MessageToConfirmAction,
+ },
+ onSubmit: (rows) => async () => {
+ const ids = rows?.map?.(({ original }) => original?.ID)
+ await Promise.all(ids.map((id) => unlock({ id })))
+ },
+ },
+ ],
+ },
+ {
+ accessor: VN_ACTIONS.DELETE,
+ dataCy: `vnet-${VN_ACTIONS.DELETE}`,
+ tooltip: T.Delete,
+ icon: Trash,
+ selected: true,
+ color: 'error',
+ options: [
+ {
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.Delete,
+ children: MessageToConfirmAction,
+ dataCy: `modal-vnet-${VN_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/columns.js b/src/fireedge/src/client/components/Tables/VNetworks/columns.js
index bc541a7a70..216a1bce35 100644
--- a/src/fireedge/src/client/components/Tables/VNetworks/columns.js
+++ b/src/fireedge/src/client/components/Tables/VNetworks/columns.js
@@ -15,62 +15,31 @@
* ------------------------------------------------------------------------- */
import { Column } from 'react-table'
-import {
- getState,
- getTotalLeases,
- getVNManager,
-} from 'client/models/VirtualNetwork'
+import { getState, getVNManager } from 'client/models/VirtualNetwork'
import { T } from 'client/constants'
-const getTotalOfResources = (resources) =>
- [resources?.ID ?? []].flat().length || 0
-
-/** @type {Column[]} VM columns */
+/** @type {Column[]} Virtual Network columns */
const COLUMNS = [
{ Header: T.ID, id: 'id', accessor: 'ID', sortType: 'number' },
{ Header: T.Name, id: 'name', accessor: 'NAME' },
- {
- Header: T.State,
- id: 'state',
- accessor: (row) => getState(row)?.name,
- },
+ { Header: T.State, id: 'state', accessor: (row) => getState(row)?.name },
{ Header: T.Owner, id: 'owner', accessor: 'UNAME' },
{ Header: T.Group, id: 'group', accessor: 'GNAME' },
{ Header: T.Locked, id: 'locked', accessor: 'LOCK' },
{ Header: T.Driver, id: 'vn_mad', accessor: getVNManager },
- {
- Header: T.TotalClusters,
- id: 'clusters',
- accessor: (row) => getTotalOfResources(row?.CLUSTERS),
- sortType: 'number',
- },
{
Header: T.UsedLeases,
id: 'used_leases',
accessor: 'USED_LEASES',
sortType: 'number',
},
- {
- Header: T.TotalLeases,
- id: 'total_leases',
- accessor: getTotalLeases,
- sortType: 'number',
- },
{
Header: T.ProvisionId,
id: 'provision_id',
- accessor: (row) => row?.TEMPLATE?.PROVISION?.ID,
- disableSortBy: true,
+ accessor: 'TEMPLATE.PROVISION.ID',
},
]
-COLUMNS.noFilterIds = [
- 'id',
- 'name',
- 'clusters',
- 'used_leases',
- 'total_leases',
- 'provision_id',
-]
+COLUMNS.noFilterIds = ['id', 'name', 'used_leases', 'provision_id']
export default COLUMNS
diff --git a/src/fireedge/src/client/components/Tables/VNetworks/row.js b/src/fireedge/src/client/components/Tables/VNetworks/row.js
index 17cb7b96a4..92b9c799cb 100644
--- a/src/fireedge/src/client/components/Tables/VNetworks/row.js
+++ b/src/fireedge/src/client/components/Tables/VNetworks/row.js
@@ -13,19 +13,28 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
-import { memo } from 'react'
+import { memo, useMemo } from 'react'
import PropTypes from 'prop-types'
+
import api from 'client/features/OneApi/network'
import { NetworkCard } from 'client/components/Cards'
const Row = memo(
- ({ original, ...props }) => {
+ ({ original, value, onClickLabel, ...props }) => {
const state = api.endpoints.getVNetworks.useQueryState(undefined, {
selectFromResult: ({ data = [] }) =>
data.find((network) => +network.ID === +original.ID),
})
- return
+ const memoNetwork = useMemo(() => state ?? original, [state, original])
+
+ return (
+
+ )
},
(prev, next) => prev.className === next.className
)
@@ -34,7 +43,9 @@ Row.propTypes = {
original: PropTypes.object,
value: PropTypes.object,
isSelected: PropTypes.bool,
- handleClick: PropTypes.func,
+ className: PropTypes.string,
+ onClick: PropTypes.func,
+ onClickLabel: PropTypes.func,
}
Row.displayName = 'VirtualNetworkRow'
diff --git a/src/fireedge/src/client/components/Tables/VRouters/index.js b/src/fireedge/src/client/components/Tables/VRouters/index.js
index 0bcbafbe74..c2a289aaf3 100644
--- a/src/fireedge/src/client/components/Tables/VRouters/index.js
+++ b/src/fireedge/src/client/components/Tables/VRouters/index.js
@@ -30,17 +30,22 @@ const DEFAULT_DATA_CY = 'vrouters'
* @returns {ReactElement} Virtual Routers table
*/
const VRoutersTable = (props) => {
- const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
+ const {
+ rootProps = {},
+ searchProps = {},
+ useQuery = useGetVRoutersQuery,
+ ...rest
+ } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews()
- const { data = [], isFetching, refetch } = useGetVRoutersQuery()
+ const { data = [], isFetching, refetch } = useQuery()
const columns = useMemo(
() =>
createColumns({
- filters: getResourceView(RESOURCE_NAMES.V_ROUTER)?.filters,
+ filters: getResourceView(RESOURCE_NAMES.VROUTER)?.filters,
columns: VRouterColumns,
}),
[view]
diff --git a/src/fireedge/src/client/components/Tables/VmTemplates/actions.js b/src/fireedge/src/client/components/Tables/VmTemplates/actions.js
index 410cc66915..f5a240df01 100644
--- a/src/fireedge/src/client/components/Tables/VmTemplates/actions.js
+++ b/src/fireedge/src/client/components/Tables/VmTemplates/actions.js
@@ -216,12 +216,6 @@ const Actions = () => {
changeOwnership({ id: original?.ID, ...newOwnership })
)
},
- // onSubmit: (rows) => async (newOwnership) => {
- // const ids = rows?.map?.(({ original }) => original?.ID)
- // await Promise.all(
- // ids.map((id) => changeOwnership({ id, ...newOwnership }))
- // )
- // },
},
{
accessor: VM_TEMPLATE_ACTIONS.CHANGE_GROUP,
diff --git a/src/fireedge/src/client/components/Tables/VmTemplates/columns.js b/src/fireedge/src/client/components/Tables/VmTemplates/columns.js
index 7d97d45c0c..d2502659ef 100644
--- a/src/fireedge/src/client/components/Tables/VmTemplates/columns.js
+++ b/src/fireedge/src/client/components/Tables/VmTemplates/columns.js
@@ -25,16 +25,8 @@ const COLUMNS = [
{ Header: T.Group, id: 'group', accessor: 'GNAME' },
{ Header: T.RegistrationTime, id: 'time', accessor: 'REGTIME' },
{ Header: T.Locked, id: 'locked', accessor: 'LOCK' },
- {
- Header: T.Logo,
- id: 'logo',
- accessor: 'TEMPLATE.LOGO',
- },
- {
- Header: T.VirtualRouter,
- id: 'vrouter',
- accessor: 'TEMPLATE.VROUTER',
- },
+ { Header: T.Logo, id: 'logo', accessor: 'TEMPLATE.LOGO' },
+ { Header: T.VirtualRouter, id: 'vrouter', accessor: 'TEMPLATE.VROUTER' },
]
COLUMNS.noFilterIds = ['id', 'name', 'time', 'logo']
diff --git a/src/fireedge/src/client/components/Tables/index.js b/src/fireedge/src/client/components/Tables/index.js
index 064407bedb..186d71c382 100644
--- a/src/fireedge/src/client/components/Tables/index.js
+++ b/src/fireedge/src/client/components/Tables/index.js
@@ -35,6 +35,8 @@ import VNetworkTemplatesTable from 'client/components/Tables/VNetworkTemplates'
import VRoutersTable from 'client/components/Tables/VRouters'
import ZonesTable from 'client/components/Tables/Zones'
+export * from 'client/components/Tables/Enhanced/Utils'
+
export {
SkeletonTable,
EnhancedTable,
diff --git a/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js b/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js
index 5649a8204a..aa07704bb5 100644
--- a/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js
+++ b/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js
@@ -47,6 +47,7 @@ const Attribute = memo(
handleDelete,
handleEdit,
handleGetOptionList,
+ askToDelete = true,
link,
icon,
name,
@@ -157,7 +158,12 @@ const Attribute = memo(
handleClick={handleActiveEditForm}
/>
)}
- {canDelete && }
+ {canDelete && (
+
+ )}
>
)}
@@ -186,6 +192,7 @@ export const AttributePropTypes = {
handleDelete: PropTypes.func,
handleEdit: PropTypes.func,
handleGetOptionList: PropTypes.func,
+ askToDelete: PropTypes.bool,
link: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
icon: PropTypes.any,
name: PropTypes.string.isRequired,
diff --git a/src/fireedge/src/client/components/Tabs/Common/AttributeCreateForm.js b/src/fireedge/src/client/components/Tabs/Common/AttributeCreateForm.js
index a1ff395c0a..0455bae9de 100644
--- a/src/fireedge/src/client/components/Tabs/Common/AttributeCreateForm.js
+++ b/src/fireedge/src/client/components/Tabs/Common/AttributeCreateForm.js
@@ -47,7 +47,7 @@ const AttributeCreateForm = memo(({ handleAdd }) => {
reset()
} catch {}
},
- [handleAdd]
+ [handleAdd, formState.isSubmitting, nameInputKey, valueInputKey]
)
const handleKeyDown = (evt) => {
@@ -95,10 +95,7 @@ const AttributeCreateForm = memo(({ handleAdd }) => {
)
})
-AttributeCreateForm.propTypes = {
- handleAdd: PropTypes.func,
-}
-
+AttributeCreateForm.propTypes = { handleAdd: PropTypes.func }
AttributeCreateForm.displayName = 'AttributeCreateForm'
export default AttributeCreateForm
diff --git a/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js b/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js
index ad1f1b7cf0..71fe2fb7b5 100644
--- a/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js
+++ b/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js
@@ -93,6 +93,7 @@ const AttributePanel = memo(
actions = allActionsEnabled ? ALL_ACTIONS : [],
filtersSpecialAttributes = true,
collapse = false,
+ askToDelete = true,
}) => {
const classes = useStyles()
@@ -114,6 +115,7 @@ const AttributePanel = memo(
canDelete: canUseAction(name, DELETE),
handleEdit,
handleDelete,
+ askToDelete,
})
)
@@ -137,10 +139,11 @@ AttributePanel.propTypes = {
handleAdd: PropTypes.func,
handleEdit: PropTypes.func,
handleDelete: PropTypes.func,
- title: PropTypes.string,
+ title: PropTypes.any,
filtersSpecialAttributes: PropTypes.bool,
allActionsEnabled: PropTypes.bool,
collapse: PropTypes.bool,
+ askToDelete: PropTypes.bool,
}
AttributePanel.displayName = 'AttributePanel'
diff --git a/src/fireedge/src/client/components/Tabs/Common/Permissions.js b/src/fireedge/src/client/components/Tabs/Common/Permissions.js
index 279e6f56a7..4b96058d84 100644
--- a/src/fireedge/src/client/components/Tabs/Common/Permissions.js
+++ b/src/fireedge/src/client/components/Tabs/Common/Permissions.js
@@ -58,7 +58,7 @@ const Permissions = memo(({ handleEdit, actions, ...permissions }) => {
const getIcon = (checked) => (+checked ? : )
return (
-
+
{Tr(T.Permissions)}
diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Address.js b/src/fireedge/src/client/components/Tabs/VNetwork/Address.js
new file mode 100644
index 0000000000..cfd10e15da
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VNetwork/Address.js
@@ -0,0 +1,82 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 PropTypes from 'prop-types'
+import { Box, Stack } from '@mui/material'
+
+import { useGetVNetworkQuery } from 'client/features/OneApi/network'
+
+import AddressRangeCard from 'client/components/Cards/AddressRangeCard'
+import {
+ AddAddressRangeAction,
+ UpdateAddressRangeAction,
+ DeleteAddressRangeAction,
+} from 'client/components/Buttons'
+
+import { AddressRange, VN_ACTIONS } from 'client/constants'
+
+const { ADD_AR, UPDATE_AR, DELETE_AR } = VN_ACTIONS
+
+/**
+ * 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
+ * @returns {ReactElement} AR tab
+ */
+const AddressTab = ({ tabProps: { actions } = {}, id }) => {
+ const { data: vnet } = useGetVNetworkQuery({ id })
+
+ /** @type {AddressRange[]} */
+ const addressRanges = [vnet.AR_POOL.AR ?? []].flat()
+
+ return (
+
+ {actions[ADD_AR] === true && }
+
+
+ {addressRanges.map((ar) => (
+
+ {actions[UPDATE_AR] === true && (
+
+ )}
+ {actions[DELETE_AR] === true && (
+
+ )}
+ >
+ }
+ />
+ ))}
+
+
+ )
+}
+
+AddressTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+}
+
+AddressTab.displayName = 'AddressTab'
+
+export default AddressTab
diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js b/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js
new file mode 100644
index 0000000000..662da3d835
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js
@@ -0,0 +1,83 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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, useMemo } from 'react'
+import PropTypes from 'prop-types'
+
+import { useHistory } from 'react-router'
+import { generatePath } from 'react-router-dom'
+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 { ClustersTable } from 'client/components/Tables'
+import { RESOURCE_NAMES } from 'client/constants'
+import { PATH } from 'client/apps/sunstone/routesOne'
+
+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 } = useGetVNetworkQuery({ id })
+
+ const { view, hasAccessToResource } = useViews()
+ const detailAccess = useMemo(() => hasAccessToResource(CLUSTER), [view])
+
+ const clusters = [vnet?.CLUSTERS?.ID ?? []].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/VNetwork/Info/index.js b/src/fireedge/src/client/components/Tabs/VNetwork/Info/index.js
index 8ef9bfbf8e..360c8ae924 100644
--- a/src/fireedge/src/client/components/Tabs/VNetwork/Info/index.js
+++ b/src/fireedge/src/client/components/Tabs/VNetwork/Info/index.js
@@ -29,6 +29,7 @@ import {
AttributePanel,
} from 'client/components/Tabs/Common'
import Information from 'client/components/Tabs/VNetwork/Info/information'
+import QOS from 'client/components/Tabs/VNetwork/Info/qos'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
@@ -57,6 +58,7 @@ const VNetworkInfoTab = ({ tabProps = {}, id }) => {
information_panel: informationPanel,
permissions_panel: permissionsPanel,
ownership_panel: ownershipPanel,
+ qos_panel: qosPanel,
attributes_panel: attributesPanel,
vcenter_panel: vcenterPanel,
lxc_panel: lxcPanel,
@@ -146,9 +148,11 @@ const VNetworkInfoTab = ({ tabProps = {}, id }) => {
groupName={GNAME}
/>
)}
+ {qosPanel?.enabled && }
{attributesPanel?.enabled && attributes && (
{
{vcenterPanel?.enabled && vcenterAttributes && (
{
{lxcPanel?.enabled && lxcAttributes && (
{
const [rename] = useRenameVNetMutation()
- const { ID, NAME } = vnet
+ const {
+ ID,
+ NAME,
+ PARENT_NETWORK_ID: parentId,
+ LOCK,
+ VLAN_ID,
+ VLAN_ID_AUTOMATIC,
+ OUTER_VLAN_ID,
+ OUTER_VLAN_ID_AUTOMATIC,
+ } = vnet
+
+ const { data: parent } = useGetVNetworkQuery(
+ { id: parentId },
+ { skip: !parentId }
+ )
const { name: stateName, color: stateColor } = getState(vnet)
@@ -51,13 +76,49 @@ const InformationPanel = ({ vnet = {}, actions }) => {
canEdit: actions?.includes?.(VN_ACTIONS.RENAME),
handleEdit: handleRename,
},
+ parentId && {
+ name: T.ReservationParent,
+ value: `#${parentId} ${parent?.NAME ?? '--'}`,
+ link:
+ !Number.isNaN(+parentId) &&
+ generatePath(PATH.NETWORK.VNETS.DETAIL, { id: parentId }),
+ dataCy: 'parent',
+ },
{
name: T.State,
value: (
-
+
+
+
+
),
},
- ]
+ {
+ name: T.Locked,
+ value: levelLockToString(LOCK?.LOCKED),
+ dataCy: 'locked',
+ },
+ {
+ name: T.VlanId,
+ value: VLAN_ID || '-',
+ dataCy: 'vlan-id',
+ },
+ {
+ name: T.AutomaticVlanId,
+ value: booleanToString(stringToBoolean(VLAN_ID_AUTOMATIC)),
+ dataCy: 'vlan-id-automatic',
+ },
+ {
+ name: T.OuterVlanId,
+ value: OUTER_VLAN_ID || '-',
+ dataCy: 'outer-vlan-id',
+ },
+ {
+ name: T.AutomaticOuterVlanId,
+ value: booleanToString(stringToBoolean(OUTER_VLAN_ID_AUTOMATIC)),
+ dataCy: 'outer-vlan-id-automatic',
+ },
+ ].filter(Boolean)
return (
<>
diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Info/qos.js b/src/fireedge/src/client/components/Tabs/VNetwork/Info/qos.js
new file mode 100644
index 0000000000..06831b6a4a
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VNetwork/Info/qos.js
@@ -0,0 +1,86 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 PropTypes from 'prop-types'
+
+import { List } from 'client/components/Tabs/Common'
+import { T, VNetwork } from 'client/constants'
+
+/**
+ * Renders mainly information tab.
+ *
+ * @param {object} props - Props
+ * @param {VNetwork} props.vnet - Virtual Network resource
+ * @returns {ReactElement} Information tab
+ */
+const QOSPanel = ({ vnet = {} }) => {
+ const {
+ INBOUND_AVG_BW,
+ INBOUND_PEAK_BW,
+ INBOUND_PEAK_KB,
+ OUTBOUND_AVG_BW,
+ OUTBOUND_PEAK_BW,
+ OUTBOUND_PEAK_KB,
+ } = vnet.TEMPLATE
+
+ const inbound = [
+ {
+ name: T.AverageBandwidth,
+ value: INBOUND_AVG_BW?.concat(' KBytes/s') ?? '-',
+ dataCy: 'inbound-avg',
+ },
+ {
+ name: T.PeakBandwidth,
+ value: INBOUND_PEAK_BW?.concat(' KBytes/s') ?? '-',
+ dataCy: 'inbound-peak-bandwidth',
+ },
+ {
+ name: T.PeakBurst,
+ value: INBOUND_PEAK_KB?.concat(' KBytes') ?? '-',
+ dataCy: 'inbound-peak',
+ },
+ ]
+
+ const outbound = [
+ {
+ name: T.AverageBandwidth,
+ value: OUTBOUND_AVG_BW?.concat(' KBytes/s') ?? '-',
+ dataCy: 'outbound-avg',
+ },
+ {
+ name: T.PeakBandwidth,
+ value: OUTBOUND_PEAK_BW?.concat(' KBytes/s') ?? '-',
+ dataCy: 'outbound-peak-bandwidth',
+ },
+ {
+ name: T.PeakBurst,
+ value: OUTBOUND_PEAK_KB?.concat(' KBytes') ?? '-',
+ dataCy: 'outbound-peak',
+ },
+ ]
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
+QOSPanel.propTypes = { vnet: PropTypes.object }
+QOSPanel.displayName = 'QOSPanel'
+
+export default QOSPanel
diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Leases/LeaseItem.js b/src/fireedge/src/client/components/Tabs/VNetwork/Leases/LeaseItem.js
new file mode 100644
index 0000000000..bede332ac0
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VNetwork/Leases/LeaseItem.js
@@ -0,0 +1,186 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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, memo, Fragment } from 'react'
+import PropTypes from 'prop-types'
+import ReleaseIcon from 'iconoir-react/dist/PlayOutline'
+import { Link as RouterLink, generatePath } from 'react-router-dom'
+import { Stack, Link, Typography } from '@mui/material'
+
+import { useReleaseLeaseMutation } from 'client/features/OneApi/network'
+
+import { SubmitButton } from 'client/components/FormControl'
+import { StatusCircle } from 'client/components/Status'
+
+import { getAddressType } from 'client/models/VirtualNetwork'
+import { T, VN_ACTIONS, ARLease } from 'client/constants'
+import { PATH } from 'client/apps/sunstone/routesOne'
+
+const LEASE_TYPES = { VM: 'vm', NET: 'net', VR: 'vr' }
+
+/**
+ * Renders the name of lease.
+ *
+ * @param {object} props - Props
+ * @param {string} props.id - Resource id
+ * @param {'vm'|'net'|'vr'} props.type - Resource type: VM, VNET or VR
+ * @returns {ReactElement} Lease column name
+ */
+const LeaseName = ({ id, type }) => {
+ const colorState = {
+ [LEASE_TYPES.VM]: 'secondary.main',
+ [LEASE_TYPES.NET]: 'warning.main',
+ [LEASE_TYPES.VR]: 'success.main',
+ }[type]
+
+ const path = {
+ [LEASE_TYPES.VM]: PATH.INSTANCE.VMS.DETAIL,
+ [LEASE_TYPES.NET]: PATH.NETWORK.VNETS.DETAIL,
+ [LEASE_TYPES.VR]: PATH.INSTANCE.VROUTERS.LIST,
+ }[type]
+
+ return (
+ svg': { mr: '1em' },
+ }}
+ >
+
+ {`#${id}`}
+
+ )
+}
+
+LeaseName.propTypes = {
+ id: PropTypes.string,
+ type: PropTypes.oneOf(Object.values(LEASE_TYPES)),
+}
+
+const LeaseItem = memo(
+ /**
+ * @param {object} props - Props
+ * @param {string} props.id - Virtual Network id
+ * @param {object} props.actions - Actions tab
+ * @param {ARLease} props.lease - Lease to render
+ * @param {Function} props.resetHoldState - Reset hold state mutation
+ * @returns {ReactElement} Lease component
+ */
+ ({ id, actions, lease, resetHoldState }) => {
+ const [releaseLease, { isLoading: isReleasing }] = useReleaseLeaseMutation()
+
+ /** @type {ARLease} */
+ const {
+ IP,
+ MAC,
+ addr = IP || MAC,
+ IP6,
+ IP6_GLOBAL,
+ IP6_LINK,
+ IP6_ULA,
+ VM: vmId,
+ VNET: vnetId,
+ VROUTER: vrId,
+ } = lease
+
+ const release = async () => {
+ const template = `LEASES = [ ${getAddressType(addr)} = ${addr} ]`
+ await releaseLease({ id, template }).unwrap()
+ await resetHoldState()
+ }
+
+ const resType =
+ vmId >= 0
+ ? LEASE_TYPES.VM
+ : vnetId >= 0
+ ? LEASE_TYPES.NET
+ : LEASE_TYPES.VR
+
+ const resId = {
+ [LEASE_TYPES.VM]: vmId,
+ [LEASE_TYPES.NET]: vnetId,
+ [LEASE_TYPES.VR]: vrId,
+ }[resType]
+
+ return (
+
+ {+vmId === -1 ? (
+ actions[VN_ACTIONS.RELEASE_LEASE] && (
+
+
+ }
+ label={T.ReleaseIp}
+ />
+
+ )
+ ) : (
+
+ )}
+ {[
+ { text: IP, dataCy: 'ip' },
+ { text: IP6, dataCy: 'ip6' },
+ { text: MAC, dataCy: 'mac' },
+ { text: IP6_LINK, dataCy: 'ip6-link' },
+ { text: IP6_ULA, dataCy: 'ip6-ula' },
+ { text: IP6_GLOBAL, dataCy: 'ip6-global' },
+ ].map(({ text = '--', dataCy }) => (
+
+ {text}
+
+ ))}
+
+ )
+ }
+)
+
+LeaseItem.propTypes = {
+ lease: PropTypes.object,
+ actions: PropTypes.object,
+ id: PropTypes.string,
+ resetHoldState: PropTypes.func,
+}
+
+LeaseItem.displayName = 'LeaseItem'
+
+export default LeaseItem
diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Leases/index.js b/src/fireedge/src/client/components/Tabs/VNetwork/Leases/index.js
new file mode 100644
index 0000000000..778a3136fb
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VNetwork/Leases/index.js
@@ -0,0 +1,170 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 PropTypes from 'prop-types'
+import { Box, Typography, TextField, Skeleton } from '@mui/material'
+
+import {
+ useGetVNetworkQuery,
+ useHoldLeaseMutation,
+} from 'client/features/OneApi/network'
+
+import { SubmitButton } from 'client/components/FormControl'
+import { Translate } from 'client/components/HOC'
+import LeaseItem from 'client/components/Tabs/VNetwork/Leases/LeaseItem'
+
+import { getAddressType } from 'client/models/VirtualNetwork'
+import { T, AddressRange, VN_ACTIONS } from 'client/constants'
+import { useGeneralApi } from 'client/features/General'
+
+const LEASES_COLUMNS = [
+ 'NAME',
+ 'IP',
+ 'IP6',
+ 'MAC',
+ 'IP6 GLOBAL',
+ 'IP6 LINK',
+ 'IP6 ULA',
+]
+
+/**
+ * Renders the list of total leases 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
+ * @returns {ReactElement} AR tab
+ */
+const LeasesTab = ({ tabProps: { actions } = {}, id }) => {
+ const { data: vnet } = useGetVNetworkQuery({ id })
+ const { enqueueError } = useGeneralApi()
+
+ const [holdLease, { isLoading, isSuccess, reset, originalArgs }] =
+ useHoldLeaseMutation()
+
+ /** @type {AddressRange[]} */
+ const addressRanges = [vnet.AR_POOL.AR ?? []].flat()
+ const leases = addressRanges.map(({ LEASES }) => LEASES.LEASE ?? []).flat()
+
+ const isHolding =
+ isLoading ||
+ (isSuccess &&
+ !leases.some(
+ (l) =>
+ originalArgs?.template.includes(l.IP) ||
+ originalArgs?.template.includes(l.IP6) ||
+ originalArgs?.template.includes(l.MAC)
+ ))
+
+ const hold = async (event) => {
+ try {
+ event.preventDefault()
+ const { addr } = Object.fromEntries(new FormData(event.target))
+ const addrName = getAddressType(addr)
+
+ if (!addrName) return enqueueError(T.SomethingWrong)
+
+ const leasesToHold = `LEASES = [ ${addrName} = ${addr} ]`
+ await holdLease({ id, template: leasesToHold }).unwrap()
+ } catch {}
+ }
+
+ return (
+
+ {actions[VN_ACTIONS.HOLD_LEASE] === true && (
+
+
+
+
+ )}
+
+ span': {
+ // header styles
+ borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
+ marginLeft: '-8px',
+ paddingLeft: '8px',
+ alignSelf: 'end',
+ },
+ }}
+ >
+ {LEASES_COLUMNS.map((col, index) => (
+
+ {index !== 0 && }
+
+ ))}
+ {leases.map((lease) => (
+
+ ))}
+
+ {isHolding && }
+
+ )
+}
+
+LeasesTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+}
+
+LeasesTab.displayName = 'LeasesTab'
+
+export default LeasesTab
diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Security.js b/src/fireedge/src/client/components/Tabs/VNetwork/Security.js
new file mode 100644
index 0000000000..ddedbd1346
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VNetwork/Security.js
@@ -0,0 +1,107 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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, useMemo } from 'react'
+import PropTypes from 'prop-types'
+
+import AddIcon from 'iconoir-react/dist/AddCircledOutline'
+import { useHistory } from 'react-router'
+import { generatePath } from 'react-router-dom'
+import { Box } from '@mui/material'
+
+import { useViews } from 'client/features/Auth'
+import { useGetSecGroupsQuery } from 'client/features/OneApi/securityGroup'
+import { useGetVNetworkQuery } from 'client/features/OneApi/network'
+// import {} from 'client/components/Tabs/VNetwork/Address/Actions'
+
+import { SecurityGroupsTable, GlobalAction } from 'client/components/Tables'
+import { T, VN_ACTIONS, RESOURCE_NAMES } from 'client/constants'
+import { PATH } from 'client/apps/sunstone/routesOne'
+
+const { SEC_GROUP } = RESOURCE_NAMES
+const { ADD_SECGROUP } = VN_ACTIONS
+
+/**
+ * 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
+ * @returns {ReactElement} Security Groups tab
+ */
+const SecurityTab = ({ tabProps: { actions } = {}, id }) => {
+ const { push: redirectTo } = useHistory()
+ const { data: vnet } = useGetVNetworkQuery({ 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,
+ }),
+ })
+
+ /** @type {GlobalAction[]} */
+ const globalActions = [
+ actions[ADD_SECGROUP] && {
+ accessor: VN_ACTIONS.ADD_SECGROUP,
+ dataCy: VN_ACTIONS.ADD_SECGROUP,
+ tooltip: T.SecurityGroup,
+ icon: AddIcon,
+ options: [
+ {
+ dialogProps: { title: T.SecurityGroup },
+ form: undefined,
+ onSubmit: () => async (formData) => {
+ console.log({ formData })
+ },
+ },
+ ],
+ },
+ ].filter(Boolean)
+
+ return (
+
+
+
+ )
+}
+
+SecurityTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+}
+
+SecurityTab.displayName = 'SecurityTab'
+
+export default SecurityTab
diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/VRouters.js b/src/fireedge/src/client/components/Tabs/VNetwork/VRouters.js
new file mode 100644
index 0000000000..7b28dcbf3b
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VNetwork/VRouters.js
@@ -0,0 +1,82 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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, useMemo } from 'react'
+import PropTypes from 'prop-types'
+
+import { useHistory } from 'react-router'
+import { generatePath } from 'react-router-dom'
+import { Box } from '@mui/material'
+
+import { useViews } from 'client/features/Auth'
+import { useGetVRoutersQuery } from 'client/features/OneApi/vrouter'
+import { useGetVNetworkQuery } from 'client/features/OneApi/network'
+// import {} from 'client/components/Tabs/VNetwork/Address/Actions'
+
+import { VRoutersTable } from 'client/components/Tables'
+import { RESOURCE_NAMES } from 'client/constants'
+import { PATH } from 'client/apps/sunstone/routesOne'
+
+const { VROUTER } = RESOURCE_NAMES
+
+/**
+ * Renders the list of virtual routers from a Virtual Network.
+ *
+ * @param {object} props - Props
+ * @param {string} props.id - Virtual Network id
+ * @returns {ReactElement} Virtual routers tab
+ */
+const VRoutersTab = ({ id }) => {
+ const { push: redirectTo } = useHistory()
+ const { data: vnet } = useGetVNetworkQuery({ id })
+
+ const { view, hasAccessToResource } = useViews()
+ const detailAccess = useMemo(() => hasAccessToResource(VROUTER), [view])
+
+ const vrouters = [vnet?.VROUTERS?.ID ?? []].flat().map((vrId) => +vrId)
+
+ const redirectToVRouter = (row) => {
+ redirectTo(generatePath(PATH.INSTANCE.VROUTERS.DETAIL, { id: row.ID }))
+ }
+
+ const useQuery = () =>
+ useGetVRoutersQuery(undefined, {
+ selectFromResult: ({ data: result = [], ...rest }) => ({
+ data: result?.filter((vrouter) => vrouters.includes(+vrouter.ID)),
+ ...rest,
+ }),
+ })
+
+ return (
+
+
+
+ )
+}
+
+VRoutersTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+}
+
+VRoutersTab.displayName = 'VRoutersTab'
+
+export default VRoutersTab
diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/index.js b/src/fireedge/src/client/components/Tabs/VNetwork/index.js
index b6635e7eed..afd4697bc1 100644
--- a/src/fireedge/src/client/components/Tabs/VNetwork/index.js
+++ b/src/fireedge/src/client/components/Tabs/VNetwork/index.js
@@ -24,10 +24,20 @@ import { RESOURCE_NAMES } from 'client/constants'
import Tabs from 'client/components/Tabs'
import Info from 'client/components/Tabs/VNetwork/Info'
+import Address from 'client/components/Tabs/VNetwork/Address'
+import Lease from 'client/components/Tabs/VNetwork/Leases'
+import Security from 'client/components/Tabs/VNetwork/Security'
+import VRouters from 'client/components/Tabs/VNetwork/VRouters'
+import Clusters from 'client/components/Tabs/VNetwork/Clusters'
const getTabComponent = (tabName) =>
({
info: Info,
+ address: Address,
+ lease: Lease,
+ security: Security,
+ virtual_router: VRouters,
+ cluster: Clusters,
}[tabName])
const VNetworkTabs = memo(({ id }) => {
diff --git a/src/fireedge/src/client/components/Tabs/Vm/index.js b/src/fireedge/src/client/components/Tabs/Vm/index.js
index 3837dc1513..da6379525c 100644
--- a/src/fireedge/src/client/components/Tabs/Vm/index.js
+++ b/src/fireedge/src/client/components/Tabs/Vm/index.js
@@ -39,7 +39,7 @@ const getTabComponent = (tabName) =>
info: Info,
network: Network,
history: History,
- schedActions: SchedActions,
+ sched_actions: SchedActions,
snapshot: Snapshot,
storage: Storage,
configuration: Configuration,
diff --git a/src/fireedge/src/client/constants/datastore.js b/src/fireedge/src/client/constants/datastore.js
index 174ec6f60e..bb30399ac2 100644
--- a/src/fireedge/src/client/constants/datastore.js
+++ b/src/fireedge/src/client/constants/datastore.js
@@ -77,7 +77,6 @@ export const DATASTORE_ACTIONS = {
// INFORMATION
RENAME: ACTIONS.RENAME,
- CHANGE_MODE: ACTIONS.CHANGE_MODE,
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
}
diff --git a/src/fireedge/src/client/constants/host.js b/src/fireedge/src/client/constants/host.js
index 79d36ff187..979053abae 100644
--- a/src/fireedge/src/client/constants/host.js
+++ b/src/fireedge/src/client/constants/host.js
@@ -17,23 +17,25 @@ import * as ACTIONS from 'client/constants/actions'
import COLOR from 'client/constants/color'
import * as STATES from 'client/constants/states'
import * as T from 'client/constants/translates'
+
/**
* @typedef {object} PciDevice - PCI device
* @property {string} ADDRESS - Address, bus, slot and function
* @property {string} BUS - Address bus
* @property {string} CLASS - Id of PCI device class
- * @property {string} [CLASS_NAME] - Name of PCI device class
+ * @property {string} CLASS_NAME - Name of PCI device class
* @property {string} DEVICE - Id of PCI device
- * @property {string} [DEVICE_NAME] - Name of PCI device
+ * @property {string} DEVICE_NAME - Name of PCI device
* @property {string} DOMAIN - Address domain
* @property {string} FUNCTION - Address function
* @property {string} NUMA_NODE - Numa node
- * @property {string} PROFILES - PCI device available profiles
+ * @property {string} PROFILES - Available vGPU Profiles
* @property {string} SHORT_ADDRESS - Short address
* @property {string} SLOT - Address slot
+ * @property {string} [UUID] - UUID
* @property {string} TYPE - Type
* @property {string} VENDOR - Id of PCI device vendor
- * @property {string} [VENDOR_NAME] - Name of PCI device vendor
+ * @property {string} VENDOR_NAME - Name of PCI device vendor
* @property {string|number} VMID - Id using this device, -1 if free
*/
diff --git a/src/fireedge/src/client/constants/image.js b/src/fireedge/src/client/constants/image.js
index e77b669f55..e7f4edccdd 100644
--- a/src/fireedge/src/client/constants/image.js
+++ b/src/fireedge/src/client/constants/image.js
@@ -168,7 +168,6 @@ export const IMAGE_ACTIONS = {
// INFORMATION
RENAME: ACTIONS.RENAME,
- CHANGE_MODE: ACTIONS.CHANGE_MODE,
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
CHANGE_TYPE: 'chtype',
diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js
index c305329831..368c435ff5 100644
--- a/src/fireedge/src/client/constants/index.js
+++ b/src/fireedge/src/client/constants/index.js
@@ -157,7 +157,7 @@ export const RESOURCE_NAMES = {
MARKETPLACE: 'marketplace',
SEC_GROUP: 'security-group',
USER: 'user',
- V_ROUTER: 'virtual-router',
+ VROUTER: 'virtual-router',
VM_TEMPLATE: 'vm-template',
VM: 'vm',
VN_TEMPLATE: 'network-template',
diff --git a/src/fireedge/src/client/constants/marketplace.js b/src/fireedge/src/client/constants/marketplace.js
index 763402465f..48e2bed8ae 100644
--- a/src/fireedge/src/client/constants/marketplace.js
+++ b/src/fireedge/src/client/constants/marketplace.js
@@ -108,7 +108,6 @@ export const MARKETPLACE_ACTIONS = {
// INFORMATION
RENAME: ACTIONS.RENAME,
- CHANGE_MODE: ACTIONS.CHANGE_MODE,
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
}
diff --git a/src/fireedge/src/client/constants/marketplaceApp.js b/src/fireedge/src/client/constants/marketplaceApp.js
index d78e312d93..a904d2879c 100644
--- a/src/fireedge/src/client/constants/marketplaceApp.js
+++ b/src/fireedge/src/client/constants/marketplaceApp.js
@@ -61,7 +61,6 @@ export const MARKETPLACE_APP_ACTIONS = {
// INFORMATION
RENAME: ACTIONS.RENAME,
- CHANGE_MODE: ACTIONS.CHANGE_MODE,
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
}
diff --git a/src/fireedge/src/client/constants/network.js b/src/fireedge/src/client/constants/network.js
index 31e5eb6bec..5c7ceef60a 100644
--- a/src/fireedge/src/client/constants/network.js
+++ b/src/fireedge/src/client/constants/network.js
@@ -40,8 +40,9 @@ import * as ACTIONS from 'client/constants/actions'
* @property {string} SIZE - Size
* @property {AR_TYPES} TYPE - Type
* @property {string} USED_LEASES - Used leases
+ * @property {string} [IPAM_MAD] - IPAM driver
* @property {{ LEASE: ARLease|ARLease[] }} [LEASES] - Leases information
- * @property {string} [GLOBAL_PREFIX] -Global prefix
+ * @property {string} [GLOBAL_PREFIX] - Global prefix
* @property {string} [PARENT_NETWORK_AR_ID] - Parent address range id
* @property {string} [ULA_PREFIX] - ULA prefix
* @property {string} [VN_MAD] - Virtual network manager
@@ -144,15 +145,33 @@ export const VN_STATES = [
/** @enum {string} Virtual network actions */
export const VN_ACTIONS = {
CREATE_DIALOG: 'create_dialog',
+ IMPORT_DIALOG: 'import_dialog',
+ UPDATE_DIALOG: 'update_dialog',
+ INSTANTIATE_DIALOG: 'instantiate_dialog',
+ RESERVE_DIALOG: 'reserve_dialog',
+ CHANGE_CLUSTER: 'change_cluster',
+ LOCK: 'lock',
+ UNLOCK: 'unlock',
DELETE: 'delete',
- RECOVER: 'recover',
- UPDATE: 'update',
// INFORMATION
RENAME: ACTIONS.RENAME,
CHANGE_MODE: ACTIONS.CHANGE_MODE,
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
+
+ // ADDRESS RANGE
+ ADD_AR: 'add_ar',
+ UPDATE_AR: 'update_ar',
+ DELETE_AR: 'delete_ar',
+
+ // LEASES
+ HOLD_LEASE: 'hold_lease',
+ RELEASE_LEASE: 'release_lease',
+
+ // SECURITY GROUPS
+ ADD_SECGROUP: 'add_secgroup',
+ DELETE_SECGROUP: 'delete_secgroup',
}
/** @enum {string} Virtual network actions by state */
@@ -187,19 +206,44 @@ export const AR_TYPES = {
/** @enum {string} Virtual Network Drivers */
export const VN_DRIVERS = {
- dummy: 'dummy',
- dot1Q: '802.1Q',
- ebtables: 'ebtables',
- fw: 'fw',
- ovswitch: 'ovswitch',
- vxlan: 'vxlan',
- vcenter: 'vcenter',
- ovswitch_vxlan: 'ovswitch_vxlan',
bridge: 'bridge',
+ fw: 'fw',
+ ebtables: 'ebtables',
+ dot1Q: '802.1Q',
+ vxlan: 'vxlan',
+ ovswitch: 'ovswitch',
+ ovswitch_vxlan: 'ovswitch_vxlan',
+ vcenter: 'vcenter',
elastic: 'elastic',
nodeport: 'nodeport',
}
+export const VNET_METHODS = {
+ static: 'static (Based on context)',
+ dhcp: 'dhcp (DHCPv4)',
+ skip: 'skip (Do not configure IPv4)',
+}
+
+export const VNET_METHODS6 = {
+ static: 'static (Based on context)',
+ auto: 'auto (SLAAC)',
+ dhcp: 'dhcp (SLAAC & DHCPv6)',
+ disable: 'disable (Do not use IPv6)',
+ skip: 'skip (Do not configure IPv6)',
+}
+
+/** @enum {string} Virtual Network Drivers names */
+export const VN_DRIVERS_STR = {
+ [VN_DRIVERS.bridge]: 'Bridged',
+ [VN_DRIVERS.fw]: 'Bridged & Security Groups',
+ [VN_DRIVERS.ebtables]: 'Bridged & ebtables VLAN',
+ [VN_DRIVERS.dot1Q]: '802.1Q',
+ [VN_DRIVERS.vxlan]: 'VXLAN',
+ [VN_DRIVERS.ovswitch]: 'Open vSwitch',
+ [VN_DRIVERS.ovswitch_vxlan]: 'Open vSwitch - VXLAN',
+ [VN_DRIVERS.vcenter]: 'vCenter',
+}
+
/**
* @enum {{ high: number, low: number }}
* Virtual Network threshold to specify the maximum and minimum of the bar range
diff --git a/src/fireedge/src/client/constants/networkTemplate.js b/src/fireedge/src/client/constants/networkTemplate.js
index 5fdc3885fe..8483f795ef 100644
--- a/src/fireedge/src/client/constants/networkTemplate.js
+++ b/src/fireedge/src/client/constants/networkTemplate.js
@@ -39,7 +39,6 @@ export const VN_TEMPLATE_ACTIONS = {
// INFORMATION
RENAME: ACTIONS.RENAME,
- CHANGE_MODE: ACTIONS.CHANGE_MODE,
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
}
diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js
index 185738da1f..cf18e13ad2 100644
--- a/src/fireedge/src/client/constants/translates.js
+++ b/src/fireedge/src/client/constants/translates.js
@@ -25,6 +25,8 @@ module.exports = {
FilterBy: 'Filter by',
FilterLabels: 'Filter labels',
FilterByLabel: 'Filter by label',
+ First: 'First',
+ Last: 'Last',
ApplyLabels: 'Apply labels',
Label: 'Label',
NoLabels: 'NoLabels',
@@ -66,8 +68,9 @@ module.exports = {
CreateMarketApp: 'Create Marketplace App',
CreateProvider: 'Create Provider',
CreateProvision: 'Create Provision',
- CreateVmTemplate: 'Create VM Template',
CreateServiceTemplate: 'Create Service Template',
+ CreateVirtualNetwork: 'Create VM Template',
+ CreateVmTemplate: 'Create VM Template',
CurrentGroup: 'Current group: %s',
CurrentOwner: 'Current owner: %s',
Delete: 'Delete',
@@ -75,8 +78,11 @@ module.exports = {
DeleteDb: 'Delete database',
DeleteScheduleAction: 'Delete schedule action: %s',
DeleteSeveralTemplates: 'Delete several Templates',
+ DeleteSeveralVirtualNetworks: 'Delete several Virtual Networks',
DeleteSomething: 'Delete: %s',
+ DeleteAddressRange: 'Delete Address Range',
DeleteTemplate: 'Delete Template',
+ DeleteVirtualNetwork: 'Delete Virtual Network',
Deploy: 'Deploy',
DeployServiceTemplate: 'Deploy Service Template',
Detach: 'Detach',
@@ -111,10 +117,12 @@ module.exports = {
Recreate: 'Recreate',
Refresh: 'Refresh',
Release: 'Release',
+ ReleaseIp: 'Release IP',
Remove: 'Remove',
Rename: 'Rename',
RenameSomething: 'Rename: %s',
Reschedule: 'Reschedule',
+ Reserve: 'Reserve',
Resize: 'Resize',
ResizeCapacity: 'Resize capacity',
ResizeSomething: 'Resize: %s',
@@ -164,11 +172,12 @@ module.exports = {
UnReschedule: 'Un-Reschedule',
Unshare: 'Unshare',
Update: 'Update',
- UpdateVmConfiguration: 'Update VM Configuration',
UpdateProvider: 'Update Provider',
UpdateScheduleAction: 'Update schedule action: %s',
- UpdateVmTemplate: 'Update VM Template',
UpdateServiceTemplate: 'Update Service Template',
+ UpdateVirtualNetwork: 'Update Virtual Network',
+ UpdateVmConfiguration: 'Update VM Configuration',
+ UpdateVmTemplate: 'Update VM Template',
/* questions */
Yes: 'Yes',
@@ -531,6 +540,7 @@ module.exports = {
/* VM Template schema - general */
CustomHypervisor: 'Custom',
CustomVariables: 'Custom Variables',
+ CustomAttributes: 'Custom Attributes',
Hypervisor: 'Hypervisor',
Logo: 'Logo',
MakeNewImagePersistent: 'Make the new images persistent',
@@ -807,6 +817,7 @@ module.exports = {
NetworkAddress: 'Network address',
NetworkMask: 'Network mask',
Gateway: 'Gateway',
+ Gateway6: 'IPv6 Gateway',
GatewayConcept: 'Default gateway for the network',
Gateway6Concept: 'IPv6 router for this network',
SearchDomainForDNSResolution: 'Search domains for DNS resolution',
@@ -815,9 +826,9 @@ module.exports = {
NetworkMethod6Concept: 'Sets IPv6 guest conf. method for NIC in this network',
DNS: 'DNS',
DNSConcept: 'DNS servers, a space separated list of servers',
- AverageBandwidth: 'Average bandwidth (KBytes/s)',
- PeakBandwidth: 'Peak bandwidth (KBytes/s)',
- PeakBurst: 'Peak burst (KBytes)',
+ AverageBandwidth: 'Average bandwidth',
+ PeakBandwidth: 'Peak bandwidth',
+ PeakBurst: 'Peak burst',
InboundAverageBandwidthConcept:
'Average bitrate for the interface in kilobytes/second for inbound traffic',
InboundPeakBandwidthConcept:
@@ -832,8 +843,10 @@ module.exports = {
TransmissionQueue: 'Transmission queue',
OnlySupportedForVirtioDriver: 'Only supported for virtio driver',
GuestOptions: 'Guest options',
- GuestMTU: 'GuestMTU',
+ GuestMTU: 'MTU of the Guest interfaces',
GuestMTUConcept: 'Sets the MTU for the NICs in this network',
+ NetMethod: 'Method',
+ NetMethod6: 'IPv6 Method',
UsedLeases: 'Used leases',
TotalLeases: 'Total leases',
TotalClusters: 'Total clusters',
@@ -845,8 +858,63 @@ module.exports = {
States for success/failure recovers: LOCK_CREATE, LOCK_DELETE state.
States for a retry recover: LOCK_CREATE, LOCK_DELETE state.
States for delete: Any but READY.`,
+ ReservationParent: 'Reservation parent',
+ ReservedFromVNetId: 'Reserved from VNET %s',
+ /* Virtual Network schema - driver configuration */
+ NetworkMode: 'Network mode',
+ Bridge: 'Bridge',
+ BridgeConcept: 'Name of the physical bridge in the nodes to attach VM NICs',
+ PhysicalDevice: 'Physical device',
+ PhysicalDeviceConcept: 'Node NIC to send/receive virtual network traffic',
+ MacSpoofingFilter: ' MAC spoofing filter',
+ IpSpoofingFilter: ' IP spoofing filter',
+ MTU: 'MTU of the interface',
+ MTUConcept: 'Maximum Transmission Unit',
+ VlanId: 'VLAN ID',
+ AutomaticVlanId: 'Automatic VLAN ID',
+ VxlanMode: 'VXLAN mode',
+ VxlanModeConcept: 'Multicast protocol for multi destination BUM traffic',
+ VxlanTunnelEndpoint: 'VXLAN Tunnel endpoint',
+ VxlanTunnelEndpointConcept: 'Tunnel endpoint communication type',
+ VxlanMulticast: 'VXLAN Multicast',
+ VxlanMulticastConcept:
+ 'Base multicast address for each VLAN. The MC address is :vxlan_mc + :vlan_id',
+ IpConfiguration: 'IP Configuration',
+ IpConfigurationConcept:
+ 'Options passed to ip cmd on operations specific to this Virtual Network',
+ OuterVlanId: 'Outer VLAN ID',
+ AutomaticOuterVlanId: 'Automatic Outer VLAN ID',
+ InvalidAttribute: 'Invalid attribute',
+ /* Virtual Network schema - address range */
+ Addresses: 'Addresses',
+ AddressRange: 'Address Range',
+ FirstIPv4Address: 'First IPv4 address',
+ FirstMacAddress: 'First MAC address',
+ SLAAC: 'SLAAC',
+ IPv6GlobalPrefix: 'IPv6 Global prefix',
+ IPv6ULAPrefix: 'IPv6 ULA prefix',
+ IPAMDriver: 'IPAM driver',
+ InvalidAddress: 'Invalid address',
+ InvalidIPv4: 'Invalid IPv4',
+ InvalidMAC: 'Invalid MAC',
+ DisabledAddressRangeInForm:
+ 'Address Ranges need to be managed in the individual Virtual Network panel',
+ /* Virtual Network schema - QoS */
+ QoS: 'QoS',
+ InboundTraffic: 'Inbound traffic',
+ OutboundTraffic: 'Outbound traffic',
+ /* Virtual Network schema - reserve */
+ ReservationFromVirtualNetwork: 'Reservation from Virtual Network',
+ CanSelectAddressFromAR:
+ 'You can select the addresses from an specific Address Range',
+ NumberOfAddresses: 'Number of addresses',
+ AddToNewVirtualNetwork: 'Add to a new Virtual Network',
+ AddToExistingReservation: 'Add to an existing Reservation',
+ FirstAddress: 'First address',
+ IpOrMac: 'IP or MAC',
/* security group schema */
+ Security: 'Security',
TCP: 'TCP',
UDP: 'UDP',
ICMP: 'ICMP',
diff --git a/src/fireedge/src/client/containers/VirtualNetworks/Create.js b/src/fireedge/src/client/containers/VirtualNetworks/Create.js
new file mode 100644
index 0000000000..19da94f235
--- /dev/null
+++ b/src/fireedge/src/client/containers/VirtualNetworks/Create.js
@@ -0,0 +1,79 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 {
+ useUpdateVNetMutation,
+ useAllocateVnetMutation,
+ useGetVNetworkQuery,
+} from 'client/features/OneApi/network'
+
+import {
+ DefaultFormStepper,
+ SkeletonStepsForm,
+} from 'client/components/FormStepper'
+import { CreateForm } from 'client/components/Forms/VNetwork'
+import { PATH } from 'client/apps/sunstone/routesOne'
+
+/**
+ * Displays the creation or modification form to a Virtual Network.
+ *
+ * @returns {ReactElement} Virtual Network form
+ */
+function CreateVirtualNetwork() {
+ const history = useHistory()
+ const { state: { ID: vnetId, NAME } = {} } = useLocation()
+
+ const { enqueueSuccess } = useGeneralApi()
+ const [update] = useUpdateVNetMutation()
+ const [allocate] = useAllocateVnetMutation()
+
+ const { data } = useGetVNetworkQuery(
+ { id: vnetId, extended: true },
+ { skip: vnetId === undefined }
+ )
+
+ const onSubmit = async (xml) => {
+ try {
+ if (!vnetId) {
+ const newVnetId = await allocate({ template: xml }).unwrap()
+ enqueueSuccess(`Virtual Network created - #${newVnetId}`)
+ } else {
+ await update({ id: vnetId, template: xml }).unwrap()
+ enqueueSuccess(`Virtual Network updated - #${vnetId} ${NAME}`)
+ }
+
+ history.push(PATH.NETWORK.VNETS.LIST)
+ } catch {}
+ }
+
+ return vnetId && !data ? (
+
+ ) : (
+ }
+ >
+ {(config) => }
+
+ )
+}
+
+export default CreateVirtualNetwork
diff --git a/src/fireedge/src/client/containers/VirtualNetworks/index.js b/src/fireedge/src/client/containers/VirtualNetworks/index.js
index 13556a1991..581a352eb1 100644
--- a/src/fireedge/src/client/containers/VirtualNetworks/index.js
+++ b/src/fireedge/src/client/containers/VirtualNetworks/index.js
@@ -15,14 +15,22 @@
* ------------------------------------------------------------------------- */
import { ReactElement, useState, memo } from 'react'
import PropTypes from 'prop-types'
-import { BookmarkEmpty } from 'iconoir-react'
-import { Typography, Box, Stack, Chip, IconButton } from '@mui/material'
+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 { Row } from 'react-table'
+import {
+ useLazyGetVNetworkQuery,
+ useUpdateVNetMutation,
+} from 'client/features/OneApi/network'
import { VNetworksTable } from 'client/components/Tables'
+import VNetworkActions from 'client/components/Tables/VNetworks/actions'
import VNetworkTabs from 'client/components/Tabs/VNetwork'
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, VirtualNetwork } from 'client/constants'
@@ -33,6 +41,7 @@ import { T, VirtualNetwork } from 'client/constants'
*/
function VirtualNetworks() {
const [selectedRows, onSelectedRowsChange] = useState(() => [])
+ const actions = VNetworkActions()
const hasSelectedRows = selectedRows?.length > 0
const moreThanOneSelected = selectedRows?.length > 1
@@ -41,7 +50,11 @@ function VirtualNetworks() {
{({ getGridProps, GutterComponent }) => (
-
+
{hasSelectedRows && (
<>
@@ -52,6 +65,7 @@ function VirtualNetworks() {
selectedRows[0]?.toggleRowSelected(false)}
/>
)}
>
@@ -67,27 +81,53 @@ function VirtualNetworks() {
*
* @param {VirtualNetwork} vnet - Virtual Network to display
* @param {Function} [gotoPage] - Function to navigate to a page of a Virtual Network
+ * @param {Function} [unselect] - Function to unselect
* @returns {ReactElement} Virtual Network details
*/
-const InfoTabs = memo(({ vnet, gotoPage }) => (
-
-
-
- {`#${vnet.ID} | ${vnet.NAME}`}
-
- {gotoPage && (
-
-
-
- )}
+const InfoTabs = memo(({ vnet, gotoPage, unselect }) => {
+ const [get, { data: lazyData, isFetching }] = useLazyGetVNetworkQuery()
+ const id = lazyData?.ID ?? vnet.ID
+ const name = lazyData?.NAME ?? vnet.NAME
+
+ return (
+
+
+ }
+ tooltip={Tr(T.Refresh)}
+ isSubmitting={isFetching}
+ onClick={() => get({ id })}
+ />
+ {typeof gotoPage === 'function' && (
+ }
+ tooltip={Tr(T.LocateOnTable)}
+ onClick={() => gotoPage()}
+ />
+ )}
+ {typeof unselect === 'function' && (
+ }
+ tooltip={Tr(T.Close)}
+ onClick={() => unselect()}
+ />
+ )}
+
+ {`#${id} | ${name}`}
+
+
+
-
-
-))
+ )
+})
InfoTabs.propTypes = {
- vnet: PropTypes.object.isRequired,
+ vnet: PropTypes.object,
gotoPage: PropTypes.func,
+ unselect: PropTypes.func,
}
InfoTabs.displayName = 'InfoTabs'
diff --git a/src/fireedge/src/client/features/OneApi/socket.js b/src/fireedge/src/client/features/OneApi/socket.js
index 7df0feb747..ef276ecc1d 100644
--- a/src/fireedge/src/client/features/OneApi/socket.js
+++ b/src/fireedge/src/client/features/OneApi/socket.js
@@ -15,10 +15,12 @@
* ------------------------------------------------------------------------- */
import { ThunkDispatch, ThunkAction } from 'redux-thunk'
import socketIO, { Socket } from 'socket.io-client'
+
+import { updateResourceOnPool } from 'client/features/OneApi/common'
import { WEBSOCKET_URL, SOCKETS } from 'client/constants'
/**
- * @typedef {'VM'|'HOST'|'IMAGE'|'VNET'} HookObjectName
+ * @typedef {'VM'|'HOST'|'IMAGE'|'NET'} HookObjectName
* - Hook object name to update from socket
*/
@@ -42,7 +44,7 @@ import { WEBSOCKET_URL, SOCKETS } from 'client/constants'
* @property {object} [VM] - New data of the VM
* @property {object} [HOST] - New data of the HOST
* @property {object} [IMAGE] - New data of the IMAGE
- * @property {object} [VNET] - New data of the VNET
+ * @property {object} [NET] - New data of the VNET
*/
/**
@@ -66,9 +68,19 @@ const createWebsocket = (path, query) =>
* @returns {object} - New value of resource from socket
*/
const getResourceValueFromEventState = (data) => {
- const { HOOK_OBJECT: name, [name]: value } = data?.HOOK_MESSAGE ?? {}
+ const hookMessage = data?.HOOK_MESSAGE || {}
- return value
+ const {
+ HOOK_OBJECT: name,
+ [name]: valueFromObjectName,
+ /**
+ * Virtual Network object Type is NET,
+ * but in the `HOOK_OBJECT` (object XML) is VNET
+ */
+ NET,
+ } = hookMessage
+
+ return valueFromObjectName ?? NET
}
/**
@@ -102,12 +114,8 @@ const UpdateFromSocket =
if (!value) return
- dispatch(
- updateQueryData((draft) => {
- const index = draft.findIndex(({ ID }) => +ID === +id)
- index !== -1 ? (draft[index] = value) : draft.push(value)
- })
- )
+ const update = updateResourceOnPool({ id, resourceFromQuery: value })
+ dispatch(updateQueryData(update))
updateCachedData((draft) => {
Object.assign(draft, value)
diff --git a/src/fireedge/src/client/models/Helper.js b/src/fireedge/src/client/models/Helper.js
index c61c1ebcac..8b24a3bd34 100644
--- a/src/fireedge/src/client/models/Helper.js
+++ b/src/fireedge/src/client/models/Helper.js
@@ -21,7 +21,7 @@ import {
J2xOptions,
} from 'fast-xml-parser'
-import { camelCase } from 'client/utils'
+import { sentenceCase } from 'client/utils'
import {
T,
Permission,
@@ -318,12 +318,11 @@ export const getAvailableInfoTabs = (infoTabs = {}, getTabComponent, id) =>
Object.entries(infoTabs)
?.filter(([_, { enabled } = {}]) => !!enabled)
?.map(([tabName, tabProps]) => {
- const camelName = camelCase(tabName)
- const TabContent = getTabComponent?.(camelName)
+ const TabContent = getTabComponent?.(tabName)
return (
TabContent && {
- label: camelName,
+ label: TabContent?.label ?? sentenceCase(tabName),
id: tabName,
renderContent: () => ,
}
diff --git a/src/fireedge/src/client/models/VirtualNetwork.js b/src/fireedge/src/client/models/VirtualNetwork.js
index e6fd8a08b8..2312e28f35 100644
--- a/src/fireedge/src/client/models/VirtualNetwork.js
+++ b/src/fireedge/src/client/models/VirtualNetwork.js
@@ -13,7 +13,13 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
-import { VirtualNetwork, VN_STATES, STATES } from 'client/constants'
+import { isIPv6, isIPv4, isMAC } from 'client/utils'
+import {
+ VirtualNetwork,
+ AddressRange,
+ VN_STATES,
+ STATES,
+} from 'client/constants'
/**
* Returns the state of the virtual network.
@@ -58,3 +64,31 @@ export const getLeasesInfo = ({ USED_LEASES, ...virtualNetwork } = {}) => {
return { percentOfUsed, percentLabel }
}
+
+/**
+ * Returns the address range leases information.
+ *
+ * @param {AddressRange} ar - Address range
+ * @returns {{ percentOfUsed: number, percentLabel: string }} Leases information
+ */
+export const getARLeasesInfo = ({ USED_LEASES, SIZE } = {}) => {
+ const percentOfUsed = (+USED_LEASES * 100) / +SIZE || 0
+ const percentLabel = `${USED_LEASES} / ${SIZE} (${Math.round(
+ percentOfUsed
+ )}%)`
+
+ return { percentOfUsed, percentLabel }
+}
+
+/**
+ * Checks the address type: IP, IP6 or MAC
+ * Otherwise returns undefined.
+ *
+ * @param {string} addr - Address to check
+ * @returns {'IP'|'IP6'|'MAC'|undefined} Returns name of address type, undefined otherwise
+ */
+export const getAddressType = (addr) => {
+ if (isIPv4(addr)) return 'IP'
+ if (isIPv6(addr)) return 'IP6'
+ if (isMAC(addr)) return 'MAC'
+}
diff --git a/src/fireedge/src/client/utils/index.js b/src/fireedge/src/client/utils/index.js
index 9c3a6eb358..547e24a726 100644
--- a/src/fireedge/src/client/utils/index.js
+++ b/src/fireedge/src/client/utils/index.js
@@ -15,6 +15,7 @@
* ------------------------------------------------------------------------- */
export * from 'client/utils/environments'
export * from 'client/utils/helpers'
+export * from 'client/utils/ip'
export * from 'client/utils/merge'
export * from 'client/utils/parser'
export * from 'client/utils/request'
diff --git a/src/fireedge/src/client/utils/ip.js b/src/fireedge/src/client/utils/ip.js
new file mode 100644
index 0000000000..ad48081afe
--- /dev/null
+++ b/src/fireedge/src/client/utils/ip.js
@@ -0,0 +1,77 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+
+// reference to `src/oca/ruby/opennebula/virtual_network.rb`
+const mac = '([a-fA-F\\d]{2}:){5}[a-fA-F\\d]{2}'
+
+const v4 =
+ '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}'
+
+const v6segment = '[a-fA-F\\d]{1,4}'
+
+const v6 = `
+(?:
+(?:${v6segment}:){7}(?:${v6segment}|:)|
+(?:${v6segment}:){6}(?:${v4}|:${v6segment}|:)|
+(?:${v6segment}:){5}(?::${v4}|(?::${v6segment}){1,2}|:)|
+(?:${v6segment}:){4}(?:(?::${v6segment}){0,1}:${v4}|(?::${v6segment}){1,3}|:)|
+(?:${v6segment}:){3}(?:(?::${v6segment}){0,2}:${v4}|(?::${v6segment}){1,4}|:)|
+(?:${v6segment}:){2}(?:(?::${v6segment}){0,3}:${v4}|(?::${v6segment}){1,5}|:)|
+(?:${v6segment}:){1}(?:(?::${v6segment}){0,4}:${v4}|(?::${v6segment}){1,6}|:)|
+(?::(?:(?::${v6segment}){0,5}:${v4}|(?::${v6segment}){1,7}|:))
+)(?:%[0-9a-zA-Z]{1,})?
+`
+ .replace(/\n/g, '')
+ .trim()
+
+// Pre-compile only the exact regexes because adding a global flag make regexes stateful
+export const REG_ADDR = new RegExp(`(?:^${v4}$)|(?:^${v6}$)|(?:^${mac}$)`)
+export const REG_IP = new RegExp(`(?:^${v4}$)|(?:^${v6}$)`)
+export const REG_V4 = new RegExp(`^${v4}$`)
+export const REG_V6 = new RegExp(`^${v6}$`)
+export const REG_MAC = new RegExp(`^${mac}$`)
+
+/**
+ * Checks if string is IPv6 or IPv4.
+ *
+ * @param {string} string - String to check
+ * @returns {boolean} Returns `true` if the given value is an IP
+ */
+export const isIP = (string) => REG_IP.test(string)
+
+/**
+ * Checks if string is IPv6.
+ *
+ * @param {string} string - String to check
+ * @returns {boolean} Returns `true` if the given value is an IPv6
+ */
+export const isIPv6 = (string) => REG_V6.test(string)
+
+/**
+ * Checks if string is IPv4.
+ *
+ * @param {string} string - String to check
+ * @returns {boolean} Returns `true` if the given value is an IPv4
+ */
+export const isIPv4 = (string) => REG_V4.test(string)
+
+/**
+ * Checks if string is MAC address.
+ *
+ * @param {string} string - String to check
+ * @returns {boolean} Returns `true` if the given value is a MAC address
+ */
+export const isMAC = (string) => REG_MAC.test(string)
diff --git a/src/fireedge/src/client/utils/translation.js b/src/fireedge/src/client/utils/translation.js
index 310325175a..ba8219ed77 100644
--- a/src/fireedge/src/client/utils/translation.js
+++ b/src/fireedge/src/client/utils/translation.js
@@ -42,7 +42,8 @@ const buildMethods = () => {
let result = resolvedSchema._cast(value, options)
if (options.isSubmit) {
- result = this.submit?.(result, options) ?? result
+ const needChangeAfterSubmit = typeof this.submit === 'function'
+ needChangeAfterSubmit && (result = this.submit(result, options))
}
return result