diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js index 2a14e380ad..eacce5d774 100644 --- a/src/fireedge/src/client/apps/sunstone/routesOne.js +++ b/src/fireedge/src/client/apps/sunstone/routesOne.js @@ -19,7 +19,6 @@ import { Server as ClusterIcon, Db as DatastoreIcon, Archive as FileIcon, - Folder as VmGroupIcon, Group as GroupIcon, HardDrive as HostIcon, BoxIso as ImageIcon, @@ -40,6 +39,7 @@ import { User as UserIcon, List as VDCIcon, Shuffle as VRoutersIcons, + Folder as VmGroupIcon, ModernTv as VmsIcons, MinusPinAlt as ZoneIcon, KeyAlt as ACLIcon, @@ -198,6 +198,17 @@ const VNetworkTemplates = loadable( () => import('client/containers/VNetworkTemplates'), { ssr: false } ) + +const InstantiateVirtualNetwork = loadable( + () => import('client/containers/VNetworkTemplates/Instantiate'), + { ssr: false } +) + +const CreateVirtualNetworkTemplate = loadable( + () => import('client/containers/VNetworkTemplates/Create'), + { ssr: false } +) + // const NetworkTopologies = loadable(() => import('client/containers/NetworkTopologies'), { ssr: false }) const Clusters = loadable(() => import('client/containers/Clusters'), { @@ -352,7 +363,10 @@ export const PATH = { }, VN_TEMPLATES: { LIST: `/${RESOURCE_NAMES.VN_TEMPLATE}`, + INSTANTIATE: `/${RESOURCE_NAMES.VN_TEMPLATE}/instantiate`, DETAIL: `/${RESOURCE_NAMES.VN_TEMPLATE}/:id`, + CREATE: `/${RESOURCE_NAMES.VN_TEMPLATE}/create`, + UPDATE: `/${RESOURCE_NAMES.VN_TEMPLATE}/update`, }, SEC_GROUPS: { LIST: `/${RESOURCE_NAMES.SEC_GROUP}`, @@ -639,11 +653,19 @@ const ENDPOINTS = [ icon: NetworkIcon, Component: VirtualNetworks, }, + // JORGE + { + title: T.InstantiateVnTemplate, + description: (_, state) => + state?.ID !== undefined && `#${state.ID} ${state.NAME}`, + path: PATH.NETWORK.VN_TEMPLATES.INSTANTIATE, + Component: InstantiateVirtualNetwork, + }, { title: (_, state) => state?.ID !== undefined - ? T.UpdateVirtualNetwork - : T.CreateVirtualNetwork, + ? T.UpdateVirtualNetworkTemplate + : T.CreateVirtualNetworkTemplate, description: (_, state) => state?.ID !== undefined && `#${state.ID} ${state.NAME}`, path: PATH.NETWORK.VNETS.CREATE, @@ -669,6 +691,16 @@ const ENDPOINTS = [ icon: NetworkTemplateIcon, Component: VNetworkTemplates, }, + { + title: (_, state) => + state?.ID !== undefined + ? T.UpdateVirtualNetworkTemplate + : T.CreateVirtualNetworkTemplate, + description: (_, state) => + state?.ID !== undefined && `#${state.ID} ${state.NAME}`, + path: PATH.NETWORK.VN_TEMPLATES.CREATE, + Component: CreateVirtualNetworkTemplate, + }, { title: T.SecurityGroups, path: PATH.NETWORK.SEC_GROUPS.LIST, diff --git a/src/fireedge/src/client/components/Buttons/AddressRangeActions.js b/src/fireedge/src/client/components/Buttons/AddressRangeActions.js index ac7d84aa68..d7c8e30018 100644 --- a/src/fireedge/src/client/components/Buttons/AddressRangeActions.js +++ b/src/fireedge/src/client/components/Buttons/AddressRangeActions.js @@ -13,23 +13,23 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { memo } from 'react' -import PropTypes from 'prop-types' -import AddIcon from 'iconoir-react/dist/Plus' import EditIcon from 'iconoir-react/dist/Edit' +import AddIcon from 'iconoir-react/dist/Plus' import TrashIcon from 'iconoir-react/dist/Trash' +import PropTypes from 'prop-types' +import { memo } from 'react' -import { - useAddRangeToVNetMutation, - useUpdateVNetRangeMutation, - useRemoveRangeFromVNetMutation, -} from 'client/features/OneApi/network' import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm' import { AddRangeForm } from 'client/components/Forms/VNetwork' +import { + useAddRangeToVNetMutation, + useRemoveRangeFromVNetMutation, + useUpdateVNetRangeMutation, +} from 'client/features/OneApi/network' +import { Tr } from 'client/components/HOC' +import { RESTRICTED_ATTRIBUTES_TYPE, T, VN_ACTIONS } from 'client/constants' import { jsonToXml } from 'client/models/Helper' -import { Tr, Translate } from 'client/components/HOC' -import { T, VN_ACTIONS, RESTRICTED_ATTRIBUTES_TYPE } from 'client/constants' import { hasRestrictedAttributes } from 'client/utils' @@ -104,7 +104,9 @@ const UpdateAddressRangeAction = memo( options={[ { dialogProps: { - title: `${Tr(T.AddressRange)}: #${AR_ID}`, + title: AR_ID + ? `${Tr(T.AddressRange)}: #${AR_ID}` + : `${Tr(T.AddressRange)}`, dataCy: 'modal-update-ar', }, form: () => @@ -156,12 +158,9 @@ const DeleteAddressRangeAction = memo( { isConfirmDialog: true, dialogProps: { - title: ( - <> - - {`: #${AR_ID}`} - - ), + title: AR_ID + ? `${Tr(T.DeleteAddressRange)}: #${AR_ID}` + : `${Tr(T.DeleteAddressRange)}`, children:

{Tr(T.DoYouWantProceed)}

, }, onSubmit: handleRemove, @@ -189,6 +188,6 @@ DeleteAddressRangeAction.displayName = 'DeleteAddressRangeAction' export { AddAddressRangeAction, - UpdateAddressRangeAction, DeleteAddressRangeAction, + UpdateAddressRangeAction, } diff --git a/src/fireedge/src/client/components/Cards/AddressRangeCard.js b/src/fireedge/src/client/components/Cards/AddressRangeCard.js index 1e3e3e77d5..01b7660100 100644 --- a/src/fireedge/src/client/components/Cards/AddressRangeCard.js +++ b/src/fireedge/src/client/components/Cards/AddressRangeCard.js @@ -13,27 +13,27 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { ReactElement, memo, useMemo } from 'react' +import { Box, Link, Typography } from '@mui/material' import PropTypes from 'prop-types' +import { ReactElement, memo, useMemo } from 'react' import { Link as RouterLink, generatePath } from 'react-router-dom' -import { Typography, Link, Box } from '@mui/material' import { useViews } from 'client/features/Auth' -import { rowStyles } from 'client/components/Tables/styles' import MultipleTags from 'client/components/MultipleTags' import { LinearProgressWithLabel } from 'client/components/Status' +import { rowStyles } from 'client/components/Tables/styles' -import { getARLeasesInfo } from 'client/models/VirtualNetwork' +import { PATH } from 'client/apps/sunstone/routesOne' import { Tr, Translate } from 'client/components/HOC' import { - T, - VirtualNetwork, AddressRange, - VNET_THRESHOLD, RESOURCE_NAMES, + T, + VNET_THRESHOLD, + VirtualNetwork, } from 'client/constants' -import { PATH } from 'client/apps/sunstone/routesOne' +import { getARLeasesInfo } from 'client/models/VirtualNetwork' const { VNET } = RESOURCE_NAMES @@ -108,7 +108,7 @@ const AddressRangeCard = memo(
- {`#${AR_ID}`} + {`#${AR_ID || '-'}`} diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/addresses.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/addresses.js new file mode 100644 index 0000000000..dd1c86c1f9 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/addresses.js @@ -0,0 +1,140 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { Stack, Typography } from '@mui/material' +import AddressesIcon from 'iconoir-react/dist/Menu' +import PropTypes from 'prop-types' +import { useFieldArray } from 'react-hook-form' + +import { + AddAddressRangeAction, + DeleteAddressRangeAction, + UpdateAddressRangeAction, +} from 'client/components/Buttons' +import { AddressRangeCard } from 'client/components/Cards' +import { Translate } from 'client/components/HOC' + +import { + STEP_ID as EXTRA_ID, + TabType, +} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration' +import { mapNameByIndex } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/schema' +import { T } from 'client/constants' + +export const TAB_ID = 'AR' + +const mapNameFunction = mapNameByIndex('AR') + +const AddressesContent = ({ oneConfig, adminGroup }) => { + const { + fields: addresses, + remove, + update, + append, + } = useFieldArray({ + name: `${EXTRA_ID}.${TAB_ID}`, + keyName: 'ID', + }) + + const handleCreateAction = (action) => { + append(mapNameFunction(action, addresses.length)) + } + + const handleUpdate = (action, index) => { + update(index, mapNameFunction(action, index)) + } + + const handleRemove = (index) => { + remove(index) + } + + return ( + <> + + + + + + {addresses?.map((ar, index) => { + const key = ar.ID ?? ar.NAME + const fakeValues = { ...ar, AR_ID: index } + + return ( + + handleUpdate(updatedAr, index)} + oneConfig={oneConfig} + adminGroup={adminGroup} + /> + handleRemove(index)} + /> + + } + /> + ) + })} + + + ) +} + +AddressesContent.propTypes = { + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +const Content = ({ isUpdate, oneConfig, adminGroup }) => + isUpdate ? ( + + + + ) : ( + + ) + +Content.propTypes = { + isUpdate: PropTypes.bool, + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +/** @type {TabType} */ +const TAB = { + id: 'addresses', + name: T.Addresses, + icon: AddressesIcon, + Content, + getError: (error) => !!error?.[TAB_ID], +} + +export default TAB diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/clusters.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/clusters.js new file mode 100644 index 0000000000..c3963583ae --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/clusters.js @@ -0,0 +1,82 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +// import { Stack } from '@mui/material' +import ClusterIcon from 'iconoir-react/dist/Server' +import { useFormContext, useWatch } from 'react-hook-form' + +import { ClustersTable } from 'client/components/Tables' + +import { + STEP_ID as EXTRA_ID, + TabType, +} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration' +import { T } from 'client/constants' + +import PropTypes from 'prop-types' + +import { isRestrictedAttributes } from 'client/utils' + +export const TAB_ID = 'CLUSTER' + +const ClustersContent = ({ oneConfig, adminGroup }) => { + const TAB_PATH = `${EXTRA_ID}.${TAB_ID}` + + const { setValue } = useFormContext() + const clusters = useWatch({ name: TAB_PATH }) + + const selectedRowIds = + clusters?.reduce((res, id) => ({ ...res, [id]: true }), {}) || {} + + const handleSelectedRows = (rows) => { + const newValue = rows?.map((row) => row.original.ID) || [] + setValue(TAB_PATH, newValue) + } + + const readOnly = + !adminGroup && + isRestrictedAttributes( + 'CLUSTER', + undefined, + oneConfig?.VNET_RESTRICTED_ATTR + ) + + return ( + + ) +} + +ClustersContent.propTypes = { + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +/** @type {TabType} */ +const TAB = { + id: 'clusters', + name: T.Clusters, + icon: ClusterIcon, + Content: ClustersContent, + getError: (error) => !!error?.[TAB_ID], +} + +export default TAB diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/index.js new file mode 100644 index 0000000000..2bd453edcf --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/index.js @@ -0,0 +1,52 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import ConfigurationsIcon from 'iconoir-react/dist/Settings' +import PropTypes from 'prop-types' + +import { + STEP_ID as EXTRA_ID, + TabType, +} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration' +import { FIELDS } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/schema' + +import FormWithSchema from 'client/components/Forms/FormWithSchema' +import { T } from 'client/constants' + +const ConfigurationContent = ({ oneConfig, adminGroup }) => ( + <> + + +) + +ConfigurationContent.propTypes = { + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +/** @type {TabType} */ +const TAB = { + id: 'configuration', + name: T.Configuration, + icon: ConfigurationsIcon, + Content: ConfigurationContent, + getError: (error) => FIELDS().some(({ name }) => error?.[name]), +} + +export default TAB diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/schema.js new file mode 100644 index 0000000000..696bffe105 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration/schema.js @@ -0,0 +1,267 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ + +import { boolean, lazy, object, string } from 'yup' + +import { + INPUT_TYPES, + RESTRICTED_ATTRIBUTES_TYPE, + T, + VN_DRIVERS, + VN_DRIVERS_STR, +} from 'client/constants' +import { + Field, + OPTION_SORTERS, + arrayToOptions, + disableFields, + getObjectSchemaFromFields, +} from 'client/utils' + +const { + fw, + dot1Q, + vxlan, + ovswitch, + ovswitch_vxlan: openVSwitchVXLAN, +} = VN_DRIVERS + +const drivers = Object.keys(VN_DRIVERS_STR) + +/** @type {Field} Driver field */ +const DRIVER_FIELD = { + name: 'VN_MAD', + type: INPUT_TYPES.TOGGLE, + values: () => + arrayToOptions(drivers, { + addEmpty: false, + getText: (key) => VN_DRIVERS_STR[key], + sorter: OPTION_SORTERS.unsort, + }), + validation: string() + .trim() + .required() + .default(() => drivers[0]), + grid: { md: 12 }, + notNull: true, +} + +/** @type {Field} Bridge linux field */ +const BRIDGE_FIELD = { + name: 'BRIDGE', + label: T.Bridge, + tooltip: T.BridgeConcept, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .notRequired() + .default(() => undefined), +} + +/** @type {Field} Physical device field */ +const PHYDEV_FIELD = { + name: 'PHYDEV', + label: T.PhysicalDevice, + tooltip: T.PhysicalDeviceConcept, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .default(() => undefined) + .when(DRIVER_FIELD.name, { + is: (driver) => [dot1Q, vxlan, openVSwitchVXLAN].includes(driver), + then: (schema) => schema.required(), + }), +} + +/** @type {Field} Filter MAC spoofing field */ +const FILTER_MAC_SPOOFING_FIELD = { + name: 'FILTER_MAC_SPOOFING', + label: T.MacSpoofingFilter, + type: INPUT_TYPES.SWITCH, + dependOf: DRIVER_FIELD.name, + htmlType: (driver) => { + const allowedDrivers = [fw, dot1Q, vxlan, ovswitch, openVSwitchVXLAN] + + return !allowedDrivers.includes(driver) && INPUT_TYPES.HIDDEN + }, + validation: boolean().yesOrNo(), + grid: { md: 12 }, +} + +/** @type {Field} Filter IP spoofing field */ +const FILTER_IP_SPOOFING_FIELD = { + name: 'FILTER_IP_SPOOFING', + label: T.IpSpoofingFilter, + type: INPUT_TYPES.SWITCH, + dependOf: DRIVER_FIELD.name, + htmlType: (driver) => { + const allowedDrivers = [fw, dot1Q, vxlan, ovswitch, openVSwitchVXLAN] + + return !allowedDrivers.includes(driver) && INPUT_TYPES.HIDDEN + }, + validation: boolean().yesOrNo(), + grid: { md: 12 }, +} + +/** @type {Field} MTU field */ +const MTU_FIELD = { + name: 'MTU', + label: T.MTU, + tooltip: T.MTUConcept, + dependOf: DRIVER_FIELD.name, + htmlType: (driver) => { + const allowedDrivers = [dot1Q, vxlan, ovswitch, openVSwitchVXLAN] + + return !allowedDrivers.includes(driver) && INPUT_TYPES.HIDDEN + }, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .default(() => undefined), +} + +/** @type {Field} Automatic VLAN field */ +const AUTOMATIC_VLAN_FIELD = { + name: 'AUTOMATIC_VLAN_ID', + label: T.AutomaticVlanId, + type: INPUT_TYPES.SWITCH, + dependOf: DRIVER_FIELD.name, + htmlType: (driver) => { + const allowedDrivers = [dot1Q, vxlan, ovswitch, openVSwitchVXLAN] + + return !allowedDrivers.includes(driver) && INPUT_TYPES.HIDDEN + }, + validation: lazy((_, { context }) => + boolean() + .yesOrNo() + .default(() => context?.AUTOMATIC_VLAN_ID === '1') + ), + grid: (self) => (self ? { md: 12 } : { sm: 6 }), +} + +/** @type {Field} VLAN ID field */ +const VLAN_ID_FIELD = { + name: 'VLAN_ID', + label: T.VlanId, + type: INPUT_TYPES.TEXT, + dependOf: [DRIVER_FIELD.name, AUTOMATIC_VLAN_FIELD.name], + htmlType: ([driver, automatic] = []) => { + const allowedDrivers = [dot1Q, vxlan, ovswitch, openVSwitchVXLAN] + if (automatic) { + return INPUT_TYPES.HIDDEN + } else if (!allowedDrivers.includes(driver)) { + return INPUT_TYPES.HIDDEN + } + }, + validation: string() + .trim() + .default(() => undefined) + .when(AUTOMATIC_VLAN_FIELD.name, { + is: (automatic) => !automatic, + then: (schema) => schema.required(), + }), + grid: { sm: 6 }, +} + +/** @type {Field} Automatic Outer VLAN field */ +const AUTOMATIC_OUTER_VLAN_ID_FIELD = { + name: 'AUTOMATIC_OUTER_VLAN_ID', + label: T.AutomaticOuterVlanId, + type: INPUT_TYPES.SWITCH, + dependOf: DRIVER_FIELD.name, + htmlType: (driver) => { + const allowedDrivers = [openVSwitchVXLAN] + + return !allowedDrivers.includes(driver) && INPUT_TYPES.HIDDEN + }, + validation: lazy((_, { context }) => + boolean() + .yesOrNo() + .default(() => context?.AUTOMATIC_OUTER_VLAN_ID === '1') + ), + grid: (self) => (self ? { md: 12 } : { sm: 6 }), +} + +/** @type {Field} Outer VLAN ID field */ +const OUTER_VLAN_ID_FIELD = { + name: 'OUTER_VLAN_ID', + label: T.OuterVlanId, + type: INPUT_TYPES.TEXT, + dependOf: [DRIVER_FIELD.name, AUTOMATIC_OUTER_VLAN_ID_FIELD.name], + htmlType: ([driver, oAutomatic] = []) => { + const allowedDrivers = [openVSwitchVXLAN] + if (oAutomatic) { + return INPUT_TYPES.HIDDEN + } else if (!allowedDrivers.includes(driver)) { + return INPUT_TYPES.HIDDEN + } + }, + validation: string() + .trim() + .default(() => undefined) + .when(AUTOMATIC_OUTER_VLAN_ID_FIELD.name, { + is: (oAutomatic) => !oAutomatic, + then: (schema) => schema.required(), + }), + grid: { sm: 6 }, +} + +const IP_LINK_CONF_FIELD = { + name: 'IP_LINK_CONF', + validation: object().afterSubmit((conf, { parent }) => { + if (vxlan !== parent[DRIVER_FIELD.name]) return + + // => string result: IP_LINK_CONF="option1=value1,option2=,..." + return Object.entries(conf || {}) + .map(([k, v]) => `${k}=${v}`) + .join(',') + }), +} + +/** + * @param {object} oneConfig - Open Nebula configuration + * @param {boolean} adminGroup - If the user belongs to oneadmin group + * @returns {Array} Fields + */ +const FIELDS = (oneConfig, adminGroup) => + disableFields( + [ + DRIVER_FIELD, + BRIDGE_FIELD, + PHYDEV_FIELD, + FILTER_MAC_SPOOFING_FIELD, + FILTER_IP_SPOOFING_FIELD, + AUTOMATIC_VLAN_FIELD, + VLAN_ID_FIELD, + AUTOMATIC_OUTER_VLAN_ID_FIELD, + OUTER_VLAN_ID_FIELD, + MTU_FIELD, + ], + '', + oneConfig, + adminGroup, + RESTRICTED_ATTRIBUTES_TYPE.VNET + ) + +/** + * @param {object} oneConfig - Open Nebula configuration + * @param {boolean} adminGroup - If the user belongs to oneadmin group + * @returns {object} Schema + */ +const SCHEMA = (oneConfig, adminGroup) => + getObjectSchemaFromFields(FIELDS(oneConfig, adminGroup)) + +export { FIELDS, IP_LINK_CONF_FIELD, SCHEMA } diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js new file mode 100644 index 0000000000..8db4c49b77 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js @@ -0,0 +1,80 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement, useCallback, useMemo } from 'react' +import { useFormContext, useWatch } from 'react-hook-form' +import { reach } from 'yup' + +import { getUnknownVars } from 'client/components/Forms/VNTemplate/CreateForm/Steps' +import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration' +import { useGeneralApi } from 'client/features/General' + +import { Legend } from 'client/components/Forms' +import { AttributePanel } from 'client/components/Tabs/Common' +import { T } from 'client/constants' + +/** + * Renders the context attributes to Virtual Network form. + * + * @returns {ReactElement} - Context attributes section + */ +const ContextAttrsSection = () => { + const { enqueueError } = useGeneralApi() + const { setValue, getResolver } = useFormContext() + const extraStepVars = useWatch({ name: EXTRA_ID }) || {} + + const unknownVars = useMemo( + () => getUnknownVars(extraStepVars, getResolver()), + [extraStepVars] + ) + + const handleChangeAttribute = useCallback( + (path, newValue) => { + try { + const extraSchema = reach(getResolver(), EXTRA_ID) + + // retrieve the schema for the given path + reach(extraSchema, path) + enqueueError(T.InvalidAttribute) + } catch (e) { + // When the path is not found, it means that + // the attribute is correct and we can set it + setValue(`${EXTRA_ID}.${path}`, newValue) + } + }, + [setValue] + ) + + return ( + + } + allActionsEnabled + handleAdd={handleChangeAttribute} + handleEdit={handleChangeAttribute} + handleDelete={handleChangeAttribute} + attributes={unknownVars} + filtersSpecialAttributes={false} + /> + ) +} + +export default ContextAttrsSection diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/index.js new file mode 100644 index 0000000000..ae7f1ad560 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/index.js @@ -0,0 +1,53 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { + STEP_ID as EXTRA_ID, + TabType, +} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration' +import CustomAttributes from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/customAttributes' +import { FIELDS } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/schema' +import ContextIcon from 'iconoir-react/dist/Folder' +import PropTypes from 'prop-types' + +import FormWithSchema from 'client/components/Forms/FormWithSchema' +import { T } from 'client/constants' + +const ContextContent = ({ oneConfig, adminGroup }) => ( + <> + + + +) + +ContextContent.propTypes = { + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +/** @type {TabType} */ +const TAB = { + id: 'context', + name: T.Context, + icon: ContextIcon, + Content: ContextContent, + getError: (error) => FIELDS().some(({ name }) => error?.[name]), +} + +export default TAB diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/schema.js new file mode 100644 index 0000000000..cf8d63dd25 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/schema.js @@ -0,0 +1,139 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { string } from 'yup' + +import { + Field, + arrayToOptions, + getObjectSchemaFromFields, + disableFields, +} from 'client/utils' +import { + T, + INPUT_TYPES, + VNET_METHODS, + VNET_METHODS6, + RESTRICTED_ATTRIBUTES_TYPE, +} from 'client/constants' + +/** @type {Field} Network address field */ +const NETWORK_ADDRESS_FIELD = { + name: 'NETWORK_ADDRESS', + label: T.NetworkAddress, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Network mask field */ +const NETWORK_MASK_FIELD = { + name: 'NETWORK_MASK', + label: T.NetworkMask, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Gateway field */ +const GATEWAY_FIELD = { + name: 'GATEWAY', + label: T.Gateway, + tooltip: T.GatewayConcept, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Gateway for IPv6 field */ +const GATEWAY6_FIELD = { + name: 'GATEWAY6', + label: T.Gateway6, + tooltip: T.Gateway6Concept, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} DNS field */ +const DNS_FIELD = { + name: 'DNS', + label: T.DNS, + tooltip: T.DNSConcept, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Guest MTU field */ +const GUEST_MTU_FIELD = { + name: 'GUEST_MTU', + label: T.GuestMTU, + tooltip: T.GuestMTUConcept, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Method field */ +const METHOD_FIELD = { + name: 'METHOD', + label: T.NetMethod, + type: INPUT_TYPES.SELECT, + values: arrayToOptions(Object.entries(VNET_METHODS), { + addEmpty: 'none (Use default)', + getText: ([, text]) => text, + getValue: ([value]) => value, + }), + validation: string().trim().notRequired(), +} + +/** @type {Field} Method for IPv6 field */ +const IP6_METHOD_FIELD = { + name: 'IP6_METHOD', + label: T.NetMethod6, + type: INPUT_TYPES.SELECT, + values: arrayToOptions(Object.entries(VNET_METHODS6), { + addEmpty: 'none (Use default)', + getText: ([, text]) => text, + getValue: ([value]) => value, + }), + validation: string().trim().notRequired(), +} + +/** + * @param {object} oneConfig - Open Nebula configuration + * @param {boolean} adminGroup - If the user belongs to oneadmin group + * @returns {Array} Fields + */ +export const FIELDS = (oneConfig, adminGroup) => + disableFields( + [ + NETWORK_ADDRESS_FIELD, + NETWORK_MASK_FIELD, + GATEWAY_FIELD, + GATEWAY6_FIELD, + DNS_FIELD, + GUEST_MTU_FIELD, + METHOD_FIELD, + IP6_METHOD_FIELD, + ], + '', + oneConfig, + adminGroup, + RESTRICTED_ATTRIBUTES_TYPE.VNET + ) + +/** + * @param {object} oneConfig - Open Nebula configuration + * @param {boolean} adminGroup - If the user belongs to oneadmin group + * @returns {object} Schema + */ +export const SCHEMA = (oneConfig, adminGroup) => + getObjectSchemaFromFields(FIELDS(oneConfig, adminGroup)) diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/index.js new file mode 100644 index 0000000000..7da904ccb9 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/index.js @@ -0,0 +1,109 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +// eslint-disable-next-line no-unused-vars +import PropTypes from 'prop-types' +// eslint-disable-next-line no-unused-vars +import { ReactElement, useMemo } from 'react' +// eslint-disable-next-line no-unused-vars +import { FieldErrors, useFormContext } from 'react-hook-form' + +import Addresses from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/addresses' +import Clusters from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/clusters' +import Configuration from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/configuration' +import Context from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context' +import QoS from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos' +import { Translate } from 'client/components/HOC' +import Tabs from 'client/components/Tabs' + +import { SCHEMA } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/schema' +import { STEP_ID as GENERAL_ID } from 'client/components/Forms/VNTemplate/CreateForm/Steps/General' +import { T, VirtualNetwork } from 'client/constants' + +/** + * @typedef {object} TabType + * @property {string} id - Id will be to use in view yaml to hide/display the tab + * @property {string} name - Label of tab + * @property {ReactElement} Content - Content tab + * @property {object} [icon] - Icon of tab + * @property {boolean} [immutable] - If `true`, the section will not be displayed whe is updating + * @property {function(FieldErrors):boolean} [getError] - Returns `true` if the tab contains an error in form + */ + +export const STEP_ID = 'extra' + +/** @type {TabType[]} */ +export const TABS = [Configuration, Clusters, Addresses, QoS, Context] + +const Content = ({ isUpdate, oneConfig, adminGroup }) => { + const { + watch, + formState: { errors }, + } = useFormContext() + + const driver = useMemo(() => watch(`${GENERAL_ID}.VN_MAD`), []) + + const totalErrors = Object.keys(errors[STEP_ID] ?? {}).length + + const tabs = useMemo( + () => + TABS.map(({ Content: TabContent, name, getError, ...section }) => ({ + ...section, + name, + label: , + renderContent: () => ( + + ), + error: getError?.(errors[STEP_ID]), + })), + [totalErrors, driver] + ) + + return +} + +/** + * Optional configuration about Virtual network. + * + * @param {VirtualNetwork} data - Virtual network + * @returns {object} Optional configuration step + */ +const ExtraConfiguration = ({ data, oneConfig, adminGroup }) => { + const isUpdate = data?.NAME !== undefined + + return { + id: STEP_ID, + label: T.AdvancedOptions, + resolver: SCHEMA(isUpdate, oneConfig, adminGroup), + optionsValidate: { abortEarly: false }, + content: (formProps) => + Content({ ...formProps, isUpdate, oneConfig, adminGroup }), + } +} + +Content.propTypes = { + data: PropTypes.any, + setFormData: PropTypes.func, + isUpdate: PropTypes.bool, + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +export default ExtraConfiguration diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/index.js new file mode 100644 index 0000000000..1560a388cd --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/index.js @@ -0,0 +1,62 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { Alert } from '@mui/material' +import { + STEP_ID as EXTRA_ID, + TabType, +} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration' +import { + FIELDS, + SECTIONS, +} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/schema' +import { Translate } from 'client/components/HOC' +import QoSIcon from 'iconoir-react/dist/DataTransferBoth' +import PropTypes from 'prop-types' + +import FormWithSchema from 'client/components/Forms/FormWithSchema' +import { T } from 'client/constants' + +const QoSContent = ({ oneConfig, adminGroup }) => ( + <> + + + + {SECTIONS(oneConfig, adminGroup).map(({ id, ...section }) => ( + + ))} + +) + +QoSContent.propTypes = { + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +/** @type {TabType} */ +const TAB = { + id: 'qos', + name: T.QoS, + icon: QoSIcon, + Content: QoSContent, + getError: (error) => FIELDS().some(({ name }) => error?.[name]), +} + +export default TAB diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/schema.js new file mode 100644 index 0000000000..cce0e5df50 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/schema.js @@ -0,0 +1,116 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ObjectSchema, string } from 'yup' + +import { + Field, + Section, + getObjectSchemaFromFields, + disableFields, +} from 'client/utils' +import { T, INPUT_TYPES, RESTRICTED_ATTRIBUTES_TYPE } from 'client/constants' + +const commonFieldProps = { + type: INPUT_TYPES.TEXT, + htmlType: 'number', + validation: string().trim().notRequired(), +} + +/** @type {Field} Inbound AVG Bandwidth field */ +const INBOUND_AVG_BW_FIELD = { + ...commonFieldProps, + name: 'INBOUND_AVG_BW', + label: T.AverageBandwidth, + tooltip: T.InboundAverageBandwidthConcept, +} + +/** @type {Field} Inbound Peak Bandwidth field */ +const INBOUND_PEAK_BW_FIELD = { + ...commonFieldProps, + name: 'INBOUND_PEAK_BW', + label: T.PeakBandwidth, + tooltip: T.InboundPeakBandwidthConcept, +} + +/** @type {Field} Inbound Peak Burst field */ +const INBOUND_PEAK_KB_FIELD = { + ...commonFieldProps, + name: 'INBOUND_PEAK_KB', + label: T.PeakBurst, + tooltip: T.PeakBurstConcept, +} + +/** @type {Field} Outbound AVG Bandwidth field */ +const OUTBOUND_AVG_BW_FIELD = { + ...commonFieldProps, + name: 'OUTBOUND_AVG_BW', + label: T.AverageBandwidth, + tooltip: T.OutboundAverageBandwidthConcept, +} + +/** @type {Field} Outbound Peak Bandwidth field */ +const OUTBOUND_PEAK_BW_FIELD = { + ...commonFieldProps, + name: 'OUTBOUND_PEAK_BW', + label: T.PeakBandwidth, + tooltip: T.OutboundPeakBandwidthConcept, +} + +/** @type {Field} Outbound Peak Burst field */ +const OUTBOUND_PEAK_KB_FIELD = { + ...commonFieldProps, + name: 'OUTBOUND_PEAK_KB', + label: T.PeakBurst, + tooltip: T.PeakBurstConcept, +} + +/** @type {Section[]} Sections */ +const SECTIONS = (oneConfig, adminGroup) => [ + { + id: 'qos-inbound', + legend: T.InboundTraffic, + fields: disableFields( + [INBOUND_AVG_BW_FIELD, INBOUND_PEAK_BW_FIELD, INBOUND_PEAK_KB_FIELD], + '', + oneConfig, + adminGroup, + RESTRICTED_ATTRIBUTES_TYPE.VNET + ), + }, + { + id: 'qos-outbound', + legend: T.OutboundTraffic, + fields: disableFields( + [OUTBOUND_AVG_BW_FIELD, OUTBOUND_PEAK_BW_FIELD, OUTBOUND_PEAK_KB_FIELD], + '', + oneConfig, + adminGroup, + RESTRICTED_ATTRIBUTES_TYPE.VNET + ), + }, +] + +/** @type {Field[]} List of QoS fields */ +const FIELDS = (oneConfig, adminGroup) => + SECTIONS(oneConfig, adminGroup) + .map(({ fields }) => fields) + .flat() + +/** @type {ObjectSchema} QoS schema */ +const SCHEMA = (oneConfig, adminGroup) => + getObjectSchemaFromFields(FIELDS(oneConfig, adminGroup)) + +export { SCHEMA, SECTIONS, FIELDS } diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/schema.js new file mode 100644 index 0000000000..d2a2b3b14f --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/schema.js @@ -0,0 +1,57 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { array, object, ObjectSchema } from 'yup' + +import { SCHEMA as CONTEXT_SCHEMA } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/context/schema' +import { SCHEMA as QOS_SCHEMA } from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration/qos/schema' + +/** + * Map name attribute if not exists. + * + * @param {string} prefixName - Prefix to add in name + * @returns {object[]} Resource object + */ +const mapNameByIndex = (prefixName) => (resource, idx) => ({ + ...resource, + NAME: + resource?.NAME?.startsWith(prefixName) || !resource?.NAME + ? `${prefixName}${idx}` + : resource?.NAME, +}) + +const AR_SCHEMA = object({ + AR: array() + .ensure() + .transform((actions) => actions.map(mapNameByIndex('AR'))), +}) + +/** + * @param {boolean} isUpdate - If `true`, the form is being updated + * @param {object} oneConfig - Open Nebula configuration + * @param {boolean} adminGroup - If the user belongs to oneadmin group + * @returns {ObjectSchema} Extra configuration schema + */ +export const SCHEMA = (isUpdate, oneConfig, adminGroup) => { + const schema = object({ SECURITY_GROUPS: array().ensure() }) + .concat(CONTEXT_SCHEMA(oneConfig, adminGroup)) + .concat(QOS_SCHEMA(oneConfig, adminGroup)) + + !isUpdate && schema.concat(AR_SCHEMA) + + return schema +} + +export { AR_SCHEMA, mapNameByIndex } diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/index.js new file mode 100644 index 0000000000..30824b8ed7 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/index.js @@ -0,0 +1,75 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import PropTypes from 'prop-types' +import { ReactElement, useMemo } from 'react' + +import FormWithSchema from 'client/components/Forms/FormWithSchema' + +import { + SCHEMA, + SECTIONS, +} from 'client/components/Forms/VNTemplate/CreateForm/Steps/General/schema' + +import { T, VirtualNetwork } from 'client/constants' + +export const STEP_ID = 'general' + +/** + * @param {boolean} isUpdate - True if it is an update operation + * @param {object} oneConfig - Open Nebula configuration + * @param {boolean} adminGroup - If the user belongs to oneadmin group + * @returns {ReactElement} Form content component + */ +const Content = (isUpdate, oneConfig, adminGroup) => { + const sections = useMemo(() => SECTIONS(isUpdate, oneConfig, adminGroup)) + + return ( + <> + {sections.map(({ id, ...section }) => ( + + ))} + + ) +} + +/** + * General configuration about Virtual network. + * + * @param {VirtualNetwork} data - Virtual network + * @returns {object} General configuration step + */ +const General = ({ data, oneConfig, adminGroup }) => { + const isUpdate = data?.NAME !== undefined + + return { + id: STEP_ID, + label: T.General, + resolver: () => SCHEMA(isUpdate, oneConfig, adminGroup), + optionsValidate: { abortEarly: false }, + content: () => Content(isUpdate, oneConfig, adminGroup), + } +} + +Content.propTypes = { + isUpdate: PropTypes.bool, +} + +export default General diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/informationSchema.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/informationSchema.js new file mode 100644 index 0000000000..227a112c54 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/informationSchema.js @@ -0,0 +1,58 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { string } from 'yup' + +import { INPUT_TYPES, T } from 'client/constants' +import { Field } from 'client/utils' + +/** + * @param {boolean} isUpdate - If `true`, the form is being updated + * @returns {Field} Name field + */ +export const NAME_FIELD = (isUpdate = false) => ({ + name: 'NAME', + label: T.Name, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .required() + .default(() => undefined) + // if the form is updating then display the name but not change it + .afterSubmit((name) => (isUpdate ? undefined : name)), + grid: { md: 12 }, + fieldProps: { disabled: isUpdate }, +}) + +/** @type {Field} Description field */ +export const DESCRIPTION_FIELD = { + name: 'DESCRIPTION', + label: T.Description, + type: INPUT_TYPES.TEXT, + multiline: true, + validation: string() + .trim() + .notRequired() + .default(() => undefined) + .afterSubmit((description) => description), + grid: { md: 12 }, +} + +/** + * @param {boolean} isUpdate - If `true`, the form is being updated + * @returns {Field[]} List of information fields + */ +export const FIELDS = (isUpdate) => + [NAME_FIELD(isUpdate), DESCRIPTION_FIELD].filter(Boolean) diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/schema.js new file mode 100644 index 0000000000..83f10005b0 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/General/schema.js @@ -0,0 +1,56 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { BaseSchema } from 'yup' + +import { FIELDS as INFORMATION_FIELDS } from './informationSchema' + +import { RESTRICTED_ATTRIBUTES_TYPE, T } from 'client/constants' +import { Section, disableFields, getObjectSchemaFromFields } from 'client/utils' + +/** + * @param {boolean} [isUpdate] - If `true`, the form is being updated + * @param {object} oneConfig - Open Nebula configuration + * @param {boolean} adminGroup - If the user belongs to oneadmin group + * @returns {Section[]} Fields + */ +const SECTIONS = (isUpdate, oneConfig, adminGroup) => [ + { + id: 'information', + legend: T.Information, + fields: disableFields( + INFORMATION_FIELDS(isUpdate), + '', + oneConfig, + adminGroup, + RESTRICTED_ATTRIBUTES_TYPE.VNET + ), + }, +] + +/** + * @param {boolean} [isUpdate] - If `true`, the form is being updated + * @param {object} oneConfig - Open Nebula configuration + * @param {boolean} adminGroup - If the user belongs to oneadmin group + * @returns {BaseSchema} Step schema + */ +const SCHEMA = (isUpdate, oneConfig, adminGroup) => + getObjectSchemaFromFields( + SECTIONS(isUpdate, oneConfig, adminGroup) + .map(({ schema, fields }) => schema ?? fields) + .flat() + ) + +export { SCHEMA, SECTIONS } diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/index.js new file mode 100644 index 0000000000..78e7455bd2 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/Steps/index.js @@ -0,0 +1,85 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ObjectSchema, reach } from 'yup' + +import ExtraConfiguration, { + STEP_ID as EXTRA_ID, +} from 'client/components/Forms/VNTemplate/CreateForm/Steps/ExtraConfiguration' +import General, { + STEP_ID as GENERAL_ID, +} from 'client/components/Forms/VNTemplate/CreateForm/Steps/General' + +import { jsonToXml } from 'client/models/Helper' +import { createSteps } from 'client/utils' + +const existsOnSchema = (schema, key) => { + try { + return reach(schema, key) && true + } catch (e) { + return false + } +} + +/** + * @param {object} fromAttributes - Attributes to check + * @param {ObjectSchema} schema - Current form schema + * @returns {object} List of unknown attributes + */ +export const getUnknownVars = (fromAttributes = {}, schema) => { + const unknown = {} + + for (const [key, value] of Object.entries(fromAttributes)) { + if ( + !!value && + !existsOnSchema(schema, `${GENERAL_ID}.${key}`) && + !existsOnSchema(schema, `${EXTRA_ID}.${key}`) + ) { + // When the path is not found, it means that + // the attribute is correct and we can set it + unknown[key] = value + } + } + + return unknown +} + +const Steps = createSteps([General, ExtraConfiguration], { + transformInitialValue: ({ TEMPLATE, ...vnet } = {}, schema) => { + const { AR = {}, DESCRIPTION = '' } = TEMPLATE + const initialValue = schema.cast( + { + [GENERAL_ID]: { ...vnet, DESCRIPTION }, + [EXTRA_ID]: { ...TEMPLATE, AR, ...vnet }, + }, + { stripUnknown: true, context: vnet } + ) + + initialValue[EXTRA_ID] = { + ...getUnknownVars(TEMPLATE, schema), + ...initialValue[EXTRA_ID], + } + + return initialValue + }, + transformBeforeSubmit: (formData) => { + const { [GENERAL_ID]: general = {}, [EXTRA_ID]: extra = {} } = + formData ?? {} + + return jsonToXml({ ...extra, ...general }) + }, +}) + +export default Steps diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/index.js new file mode 100644 index 0000000000..4a3add5b73 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/CreateForm/index.js @@ -0,0 +1,16 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +export { default } from 'client/components/Forms/VNTemplate/CreateForm/Steps' diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/addresses.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/addresses.js new file mode 100644 index 0000000000..9d855a10fd --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/addresses.js @@ -0,0 +1,140 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { Stack, Typography } from '@mui/material' +import AddressesIcon from 'iconoir-react/dist/Menu' +import PropTypes from 'prop-types' +import { useFieldArray } from 'react-hook-form' + +import { + AddAddressRangeAction, + DeleteAddressRangeAction, + UpdateAddressRangeAction, +} from 'client/components/Buttons' +import { AddressRangeCard } from 'client/components/Cards' +import { Translate } from 'client/components/HOC' + +import { + STEP_ID as EXTRA_ID, + TabType, +} from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration' +import { mapNameByIndex } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/schema' +import { T } from 'client/constants' + +export const TAB_ID = 'AR' + +const mapNameFunction = mapNameByIndex('AR') + +const AddressesContent = ({ oneConfig, adminGroup }) => { + const { + fields: addresses, + remove, + update, + append, + } = useFieldArray({ + name: `${EXTRA_ID}.${TAB_ID}`, + keyName: 'ID', + }) + + const handleCreateAction = (action) => { + append(mapNameFunction(action, addresses.length)) + } + + const handleUpdate = (action, index) => { + update(index, mapNameFunction(action, index)) + } + + const handleRemove = (index) => { + remove(index) + } + + return ( + <> + + + + + + {addresses?.map((ar, index) => { + const key = ar.ID ?? ar.NAME + const fakeValues = { ...ar, AR_ID: index } + + return ( + + handleUpdate(updatedAr, index)} + oneConfig={oneConfig} + adminGroup={adminGroup} + /> + handleRemove(index)} + /> + + } + /> + ) + })} + + + ) +} + +AddressesContent.propTypes = { + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +const Content = ({ isUpdate, oneConfig, adminGroup }) => + isUpdate ? ( + + + + ) : ( + + ) + +Content.propTypes = { + isUpdate: PropTypes.bool, + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +/** @type {TabType} */ +const TAB = { + id: 'addresses', + name: T.Addresses, + icon: AddressesIcon, + Content, + getError: (error) => !!error?.[TAB_ID], +} + +export default TAB diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/customAttributes.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/customAttributes.js new file mode 100644 index 0000000000..7eed221eca --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/customAttributes.js @@ -0,0 +1,80 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement, useCallback, useMemo } from 'react' +import { useFormContext, useWatch } from 'react-hook-form' +import { reach } from 'yup' + +import { getUnknownVars } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps' +import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration' +import { useGeneralApi } from 'client/features/General' + +import { Legend } from 'client/components/Forms' +import { AttributePanel } from 'client/components/Tabs/Common' +import { T } from 'client/constants' + +/** + * Renders the context attributes to Virtual Network form. + * + * @returns {ReactElement} - Context attributes section + */ +const ContextAttrsSection = () => { + const { enqueueError } = useGeneralApi() + const { setValue, getResolver } = useFormContext() + const extraStepVars = useWatch({ name: EXTRA_ID }) || {} + + const unknownVars = useMemo( + () => getUnknownVars(extraStepVars, getResolver()), + [extraStepVars] + ) + + const handleChangeAttribute = useCallback( + (path, newValue) => { + try { + const extraSchema = reach(getResolver(), EXTRA_ID) + + // retrieve the schema for the given path + reach(extraSchema, path) + enqueueError(T.InvalidAttribute) + } catch (e) { + // When the path is not found, it means that + // the attribute is correct and we can set it + setValue(`${EXTRA_ID}.${path}`, newValue) + } + }, + [setValue] + ) + + return ( + + } + allActionsEnabled + handleAdd={handleChangeAttribute} + handleEdit={handleChangeAttribute} + handleDelete={handleChangeAttribute} + attributes={unknownVars} + filtersSpecialAttributes={false} + /> + ) +} + +export default ContextAttrsSection diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/index.js new file mode 100644 index 0000000000..f0f8fdcfd7 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/index.js @@ -0,0 +1,53 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { + STEP_ID as EXTRA_ID, + TabType, +} from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration' +import CustomAttributes from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/customAttributes' +import { FIELDS } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/schema' +import ContextIcon from 'iconoir-react/dist/Folder' +import PropTypes from 'prop-types' + +import FormWithSchema from 'client/components/Forms/FormWithSchema' +import { T } from 'client/constants' + +const ContextContent = ({ oneConfig, adminGroup }) => ( + <> + + + +) + +ContextContent.propTypes = { + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +/** @type {TabType} */ +const TAB = { + id: 'context', + name: T.Context, + icon: ContextIcon, + Content: ContextContent, + getError: (error) => FIELDS().some(({ name }) => error?.[name]), +} + +export default TAB diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/schema.js new file mode 100644 index 0000000000..cf8d63dd25 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/schema.js @@ -0,0 +1,139 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { string } from 'yup' + +import { + Field, + arrayToOptions, + getObjectSchemaFromFields, + disableFields, +} from 'client/utils' +import { + T, + INPUT_TYPES, + VNET_METHODS, + VNET_METHODS6, + RESTRICTED_ATTRIBUTES_TYPE, +} from 'client/constants' + +/** @type {Field} Network address field */ +const NETWORK_ADDRESS_FIELD = { + name: 'NETWORK_ADDRESS', + label: T.NetworkAddress, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Network mask field */ +const NETWORK_MASK_FIELD = { + name: 'NETWORK_MASK', + label: T.NetworkMask, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Gateway field */ +const GATEWAY_FIELD = { + name: 'GATEWAY', + label: T.Gateway, + tooltip: T.GatewayConcept, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Gateway for IPv6 field */ +const GATEWAY6_FIELD = { + name: 'GATEWAY6', + label: T.Gateway6, + tooltip: T.Gateway6Concept, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} DNS field */ +const DNS_FIELD = { + name: 'DNS', + label: T.DNS, + tooltip: T.DNSConcept, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Guest MTU field */ +const GUEST_MTU_FIELD = { + name: 'GUEST_MTU', + label: T.GuestMTU, + tooltip: T.GuestMTUConcept, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Method field */ +const METHOD_FIELD = { + name: 'METHOD', + label: T.NetMethod, + type: INPUT_TYPES.SELECT, + values: arrayToOptions(Object.entries(VNET_METHODS), { + addEmpty: 'none (Use default)', + getText: ([, text]) => text, + getValue: ([value]) => value, + }), + validation: string().trim().notRequired(), +} + +/** @type {Field} Method for IPv6 field */ +const IP6_METHOD_FIELD = { + name: 'IP6_METHOD', + label: T.NetMethod6, + type: INPUT_TYPES.SELECT, + values: arrayToOptions(Object.entries(VNET_METHODS6), { + addEmpty: 'none (Use default)', + getText: ([, text]) => text, + getValue: ([value]) => value, + }), + validation: string().trim().notRequired(), +} + +/** + * @param {object} oneConfig - Open Nebula configuration + * @param {boolean} adminGroup - If the user belongs to oneadmin group + * @returns {Array} Fields + */ +export const FIELDS = (oneConfig, adminGroup) => + disableFields( + [ + NETWORK_ADDRESS_FIELD, + NETWORK_MASK_FIELD, + GATEWAY_FIELD, + GATEWAY6_FIELD, + DNS_FIELD, + GUEST_MTU_FIELD, + METHOD_FIELD, + IP6_METHOD_FIELD, + ], + '', + oneConfig, + adminGroup, + RESTRICTED_ATTRIBUTES_TYPE.VNET + ) + +/** + * @param {object} oneConfig - Open Nebula configuration + * @param {boolean} adminGroup - If the user belongs to oneadmin group + * @returns {object} Schema + */ +export const SCHEMA = (oneConfig, adminGroup) => + getObjectSchemaFromFields(FIELDS(oneConfig, adminGroup)) diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js new file mode 100644 index 0000000000..db0aaf8b35 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js @@ -0,0 +1,102 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +// eslint-disable-next-line no-unused-vars +import PropTypes from 'prop-types' +// eslint-disable-next-line no-unused-vars +import { ReactElement, useMemo } from 'react' +// eslint-disable-next-line no-unused-vars +import { FieldErrors, useFormContext } from 'react-hook-form' + +import Addresses from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/addresses' +import Context from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context' +import Security from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/security' +import { Translate } from 'client/components/HOC' +import Tabs from 'client/components/Tabs' + +import { SCHEMA } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/schema' +import { T, VirtualNetwork } from 'client/constants' + +/** + * @typedef {object} TabType + * @property {string} id - Id will be to use in view yaml to hide/display the tab + * @property {string} name - Label of tab + * @property {ReactElement} Content - Content tab + * @property {object} [icon] - Icon of tab + * @property {boolean} [immutable] - If `true`, the section will not be displayed whe is updating + * @property {function(FieldErrors):boolean} [getError] - Returns `true` if the tab contains an error in form + */ + +export const STEP_ID = 'extra' + +/** @type {TabType[]} */ +export const TABS = [Addresses, Security, Context] + +const Content = ({ isUpdate, oneConfig, adminGroup }) => { + const { + formState: { errors }, + } = useFormContext() + + const totalErrors = Object.keys(errors[STEP_ID] ?? {}).length + + const tabs = useMemo( + () => + TABS.map(({ Content: TabContent, name, getError, ...section }) => ({ + ...section, + name, + label: , + renderContent: () => ( + + ), + error: getError?.(errors[STEP_ID]), + })), + [totalErrors] + ) + + return +} + +/** + * Optional configuration about Virtual network. + * + * @param {VirtualNetwork} data - Virtual network + * @returns {object} Optional configuration step + */ +const ExtraConfiguration = ({ data, oneConfig, adminGroup }) => { + const isUpdate = data?.NAME !== undefined + + return { + id: STEP_ID, + label: T.AdvancedOptions, + resolver: SCHEMA(isUpdate, oneConfig, adminGroup), + optionsValidate: { abortEarly: false }, + content: (formProps) => + Content({ ...formProps, isUpdate, oneConfig, adminGroup }), + } +} + +Content.propTypes = { + data: PropTypes.any, + setFormData: PropTypes.func, + isUpdate: PropTypes.bool, + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +export default ExtraConfiguration diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js new file mode 100644 index 0000000000..500222f86e --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js @@ -0,0 +1,56 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { array, object, ObjectSchema } from 'yup' + +import { SCHEMA as CONTEXT_SCHEMA } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/context/schema' + +/** + * Map name attribute if not exists. + * + * @param {string} prefixName - Prefix to add in name + * @returns {object[]} Resource object + */ +const mapNameByIndex = (prefixName) => (resource, idx) => ({ + ...resource, + NAME: + resource?.NAME?.startsWith(prefixName) || !resource?.NAME + ? `${prefixName}${idx}` + : resource?.NAME, +}) + +const AR_SCHEMA = object({ + AR: array() + .ensure() + .transform((actions) => actions.map(mapNameByIndex('AR'))), +}) + +/** + * @param {boolean} isUpdate - If `true`, the form is being updated + * @param {object} oneConfig - Open Nebula configuration + * @param {boolean} adminGroup - If the user belongs to oneadmin group + * @returns {ObjectSchema} Extra configuration schema + */ +export const SCHEMA = (isUpdate, oneConfig, adminGroup) => { + const schema = object({ SECURITY_GROUPS: array().ensure() }).concat( + CONTEXT_SCHEMA(oneConfig, adminGroup) + ) + + !isUpdate && schema.concat(AR_SCHEMA) + + return schema +} + +export { AR_SCHEMA, mapNameByIndex } diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/security.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/security.js new file mode 100644 index 0000000000..e202895f7c --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration/security.js @@ -0,0 +1,86 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { Alert } from '@mui/material' +import { + STEP_ID as EXTRA_ID, + TabType, +} from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration' +import { Translate } from 'client/components/HOC' +import { SecurityGroupsTable } from 'client/components/Tables' +import { T } from 'client/constants' +import { isRestrictedAttributes } from 'client/utils' +import SecurityIcon from 'iconoir-react/dist/HistoricShield' +import PropTypes from 'prop-types' +import { useFormContext, useWatch } from 'react-hook-form' + +export const TAB_ID = 'SECURITY_GROUPS' + +const SecurityContent = ({ oneConfig, adminGroup }) => { + const TAB_PATH = `${EXTRA_ID}.${TAB_ID}` + + const { setValue } = useFormContext() + const secGroups = useWatch({ name: TAB_PATH }) + + const selectedRowIds = secGroups?.reduce( + (res, id) => ({ ...res, [id]: true }), + {} + ) + + const handleSelectedRows = (rows) => { + const newValue = rows?.map((row) => row.original.ID) || [] + setValue(TAB_PATH, newValue) + } + + const readOnly = + !adminGroup && + isRestrictedAttributes( + 'SECURITY_GROUPS', + undefined, + oneConfig?.VNET_RESTRICTED_ATTR + ) + + return ( + <> + + + + + + ) +} + +SecurityContent.propTypes = { + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +/** @type {TabType} */ +const TAB = { + id: 'security', + name: T.Security, + icon: SecurityIcon, + Content: SecurityContent, + getError: (error) => !!error?.[TAB_ID], +} + +export default TAB diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/index.js new file mode 100644 index 0000000000..5072019930 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/index.js @@ -0,0 +1,105 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import PropTypes from 'prop-types' +import { useEffect, useMemo } from 'react' + +import FormWithSchema from 'client/components/Forms/FormWithSchema' +import { + SCHEMA, + SECTIONS, +} from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/General/schema' +import useStyles from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/General/styles' +import { RESOURCE_NAMES, T, VmTemplate } from 'client/constants' +import { useViews } from 'client/features/Auth' +import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper' +import { scaleVcpuByCpuFactor } from 'client/models/VirtualMachine' +import { useFormContext } from 'react-hook-form' + +let generalFeatures + +export const STEP_ID = 'general' + +const Content = ({ vmTemplate, oneConfig, adminGroup }) => { + const classes = useStyles() + const { view, getResourceView } = useViews() + const { getValues, setValue } = useFormContext() + + const resource = RESOURCE_NAMES.VM_TEMPLATE + const { features, dialogs } = getResourceView(resource) + + const sections = useMemo(() => { + const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR + const dialog = dialogs?.instantiate_dialog + const sectionsAvailable = getSectionsAvailable(dialog, hypervisor) + + generalFeatures = features + + return SECTIONS(vmTemplate, features, oneConfig, adminGroup).filter( + ({ id, required }) => required || sectionsAvailable.includes(id) + ) + }, [view]) + + useEffect(() => { + if (vmTemplate?.TEMPLATE?.VCPU && features?.cpu_factor) { + const oldValues = { + ...getValues(), + } + oldValues.configuration.CPU = `${scaleVcpuByCpuFactor( + vmTemplate.TEMPLATE.VCPU, + features.cpu_factor + )}` + + setValue(`${STEP_ID}`, oldValues) + } + }, []) + + return ( +
+ {sections.map(({ id, legend, fields }) => ( + + ))} +
+ ) +} + +Content.propTypes = { + vmTemplate: PropTypes.object, + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +/** + * Basic configuration about VM Template. + * + * @param {VmTemplate} vmTemplate - VM Template + * @returns {object} Basic configuration step + */ +const BasicConfiguration = ({ data: vmTemplate, oneConfig, adminGroup }) => ({ + id: STEP_ID, + label: T.Configuration, + resolver: () => SCHEMA(vmTemplate, generalFeatures), + optionsValidate: { abortEarly: false }, + content: (props) => Content({ ...props, vmTemplate, oneConfig, adminGroup }), +}) + +export default BasicConfiguration diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/informationSchema.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/informationSchema.js new file mode 100644 index 0000000000..01b3cf84ec --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/informationSchema.js @@ -0,0 +1,30 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { string } from 'yup' + +import { INPUT_TYPES, T } from 'client/constants' + +const NAME = { + name: 'name', + label: T.VNName, + tooltip: T.VnTemplateNameHelper, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .default(() => undefined), +} + +export const FIELDS = [NAME] diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/schema.js new file mode 100644 index 0000000000..71cf89c880 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/schema.js @@ -0,0 +1,73 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { BaseSchema } from 'yup' + +import { FIELDS as INFORMATION_FIELDS } from './informationSchema' + +// get schemas from VmTemplate/CreateForm + +import { T, VmTemplate, VmTemplateFeatures } from 'client/constants' +import { + Field, + Section, + disableFields, + filterFieldsByHypervisor, + getObjectSchemaFromFields, +} from 'client/utils' + +/** + * @param {VmTemplate} [vmTemplate] - VM Template + * @param {VmTemplateFeatures} [features] - Features + * @param {object} oneConfig - Config of oned.conf + * @param {boolean} adminGroup - User is admin or not + * @returns {Section[]} Sections + */ +const SECTIONS = (vmTemplate, features, oneConfig, adminGroup) => { + const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR + + return [ + { + id: 'information', + legend: T.Information, + fields: disableFields( + filterFieldsByHypervisor(INFORMATION_FIELDS, hypervisor), + '', + oneConfig, + adminGroup + ), + }, + ] +} + +/** + * @param {VmTemplate} [vmTemplate] - VM Template + * @param {boolean} [hideCpu] - If `true`, the CPU fields is hidden + * @returns {Field[]} Basic configuration fields + */ +const FIELDS = (vmTemplate, hideCpu) => + SECTIONS(vmTemplate, hideCpu) + .map(({ fields }) => fields) + .flat() + +/** + * @param {VmTemplate} [vmTemplate] - VM Template + * @param {boolean} [hideCpu] - If `true`, the CPU fields is hidden + * @returns {BaseSchema} Step schema + */ +const SCHEMA = (vmTemplate, hideCpu) => + getObjectSchemaFromFields(FIELDS(vmTemplate, hideCpu)) + +export { FIELDS, SCHEMA, SECTIONS } diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/styles.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/styles.js new file mode 100644 index 0000000000..e2f47d3853 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/General/styles.js @@ -0,0 +1,31 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import makeStyles from '@mui/styles/makeStyles' + +export default makeStyles((theme) => ({ + root: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: '2em', + overflow: 'auto', + [theme.breakpoints.down('lg')]: { + gridTemplateColumns: '1fr', + }, + }, + information: { + gridColumn: '1 / -1', + }, +})) diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/index.js new file mode 100644 index 0000000000..f80be0b75b --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/Steps/index.js @@ -0,0 +1,94 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import ExtraConfiguration, { + STEP_ID as EXTRA_ID, +} from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/ExtraConfiguration' +import General, { + STEP_ID as GENERAL_ID, +} from 'client/components/Forms/VNTemplate/InstantiateForm/Steps/General' +import { jsonToXml } from 'client/models/Helper' +import { createSteps } from 'client/utils' +import { ObjectSchema, reach } from 'yup' + +const DYNAMIC_FIELDS = ['AR'] + +const existsOnSchema = (schema, key) => { + try { + return reach(schema, key) && true + } catch (e) { + return false + } +} + +/** + * @param {object} fromAttributes - Attributes to check + * @param {ObjectSchema} schema - Current form schema + * @returns {object} List of unknown attributes + */ +export const getUnknownVars = (fromAttributes = {}, schema) => { + const unknown = {} + + for (const [key, value] of Object.entries(fromAttributes)) { + if ( + !!value && + !DYNAMIC_FIELDS.includes(key) && + !existsOnSchema(schema, `${GENERAL_ID}.${key}`) && + !existsOnSchema(schema, `${EXTRA_ID}.${key}`) + ) { + // When the path is not found, it means that + // the attribute is correct and we can set it + unknown[key] = value + } + } + + return unknown +} + +const Steps = createSteps(() => [General, ExtraConfiguration].filter(Boolean), { + transformInitialValue: (vmTemplate, schema) => { + const initialValue = schema.cast( + { + [GENERAL_ID]: vmTemplate?.TEMPLATE, + [EXTRA_ID]: vmTemplate?.TEMPLATE, + }, + { stripUnknown: true } + ) + + return initialValue + }, + transformBeforeSubmit: (formData, vnTemplate) => { + const { [GENERAL_ID]: { name } = {}, [EXTRA_ID]: extraTemplate = {} } = + formData ?? {} + + Array.isArray(extraTemplate?.SECURITY_GROUPS) && + extraTemplate?.SECURITY_GROUPS?.length && + (extraTemplate.SECURITY_GROUPS = extraTemplate.SECURITY_GROUPS.join(',')) + + const templateXML = jsonToXml({ + ...extraTemplate, + }) + + const templates = { + id: vnTemplate.ID, + name, + template: templateXML, + } + + return templates + }, +}) + +export default Steps diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/index.js new file mode 100644 index 0000000000..d01be02af5 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/InstantiateForm/index.js @@ -0,0 +1,16 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +export { default } from 'client/components/Forms/VNTemplate/InstantiateForm/Steps' diff --git a/src/fireedge/src/client/components/Forms/VNTemplate/index.js b/src/fireedge/src/client/components/Forms/VNTemplate/index.js new file mode 100644 index 0000000000..ffd77b91a4 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNTemplate/index.js @@ -0,0 +1,34 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC' +import { CreateStepsCallback } from 'client/utils/schema' +import { ReactElement } from 'react' + +/** + * @param {ConfigurationProps} configProps - Configuration + * @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form + */ +const CreateForm = (configProps) => + AsyncLoadForm({ formPath: 'VNTemplate/CreateForm' }, configProps) + +/** + * @param {ConfigurationProps} configProps - Configuration + * @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form + */ +const InstantiateForm = (configProps) => + AsyncLoadForm({ formPath: 'VNTemplate/InstantiateForm' }, configProps) + +export { CreateForm, InstantiateForm } diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js index 5e894bd73f..6098dc3bd6 100644 --- a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js @@ -14,11 +14,11 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ import { ReactElement, useCallback, useMemo } from 'react' -import { reach } from 'yup' import { useFormContext, useWatch } from 'react-hook-form' +import { reach } from 'yup' -import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration' import { getUnknownVars } from 'client/components/Forms/VNetwork/CreateForm/Steps' +import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration' import { useGeneralApi } from 'client/features/General' import { Legend } from 'client/components/Forms' @@ -46,8 +46,7 @@ const ContextAttrsSection = () => { const extraSchema = reach(getResolver(), EXTRA_ID) // retrieve the schema for the given path - const existsSchema = reach(extraSchema, path) - console.log(existsSchema) + reach(extraSchema, path) enqueueError(T.InvalidAttribute) } catch (e) { // When the path is not found, it means that diff --git a/src/fireedge/src/client/components/Tables/VNetworkTemplates/actions.js b/src/fireedge/src/client/components/Tables/VNetworkTemplates/actions.js new file mode 100644 index 0000000000..2ca055fb13 --- /dev/null +++ b/src/fireedge/src/client/components/Tables/VNetworkTemplates/actions.js @@ -0,0 +1,274 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { Typography } from '@mui/material' +import { + AddCircledOutline, + Group, + Lock, + PlayOutline, + // Import, + Trash, +} from 'iconoir-react' +import { useMemo } from 'react' +import { useHistory } from 'react-router-dom' + +import { useViews } from 'client/features/Auth' +import { useAddNetworkToClusterMutation } from 'client/features/OneApi/cluster' +import { + useChangeVNTemplateOwnershipMutation, + useLockVNTemplateMutation, + // useRecoverVNetMutation, + useRemoveVNTemplateMutation, + // useReserveAddressMutation, + useUnlockVNTemplateMutation, +} from 'client/features/OneApi/networkTemplate' + +import { ChangeClusterForm } from 'client/components/Forms/Cluster' +import { ChangeGroupForm, ChangeUserForm } from 'client/components/Forms/Vm' +import { Translate } from 'client/components/HOC' +import { + GlobalAction, + createActions, +} from 'client/components/Tables/Enhanced/Utils' + +import { PATH } from 'client/apps/sunstone/routesOne' +import { RESOURCE_NAMES, T, VN_TEMPLATE_ACTIONS } from 'client/constants' + +const ListNames = ({ rows = [] }) => + rows?.map?.(({ id, original }) => { + const { ID, NAME } = original + + return ( + + {`#${ID} ${NAME}`} + + ) + }) + +const SubHeader = (rows) => + +const MessageToConfirmAction = (rows) => { + const names = rows?.map?.(({ original }) => original?.NAME) + + return ( + <> +

+ + {`: ${names.join(', ')}`} +

+

+ +

+ + ) +} + +MessageToConfirmAction.displayName = 'MessageToConfirmAction' + +/** + * Generates the actions to operate resources on Virtual networks table. + * + * @returns {GlobalAction} - Actions + */ +const Actions = () => { + const history = useHistory() + const { view, getResourceView } = useViews() + // const [reserve] = useReserveAddressMutation() + // const [recover] = useRecoverVNetMutation() + const [changeCluster] = useAddNetworkToClusterMutation() + const [lock] = useLockVNTemplateMutation() + const [unlock] = useUnlockVNTemplateMutation() + const [changeOwnership] = useChangeVNTemplateOwnershipMutation() + const [remove] = useRemoveVNTemplateMutation() + + const actions = useMemo( + () => + createActions({ + filters: getResourceView(RESOURCE_NAMES.VN_TEMPLATE)?.actions, + actions: [ + { + accessor: VN_TEMPLATE_ACTIONS.CREATE_DIALOG, + dataCy: `vnettemplate-${VN_TEMPLATE_ACTIONS.CREATE_DIALOG}`, + tooltip: T.Create, + icon: AddCircledOutline, + action: () => history.push(PATH.NETWORK.VN_TEMPLATES.CREATE), + }, + { + accessor: VN_TEMPLATE_ACTIONS.INSTANTIATE_DIALOG, + dataCy: `vnettemplate-${VN_TEMPLATE_ACTIONS.INSTANTIATE_DIALOG}`, + tooltip: T.Instantiate, + icon: PlayOutline, + selected: { max: 1 }, + action: (rows) => { + const template = rows?.[0]?.original ?? {} + const path = PATH.NETWORK.VN_TEMPLATES.INSTANTIATE + + history.push(path, template) + }, + }, + { + accessor: VN_TEMPLATE_ACTIONS.UPDATE_DIALOG, + dataCy: `vnettemplate-${VN_TEMPLATE_ACTIONS.UPDATE_DIALOG}`, + label: T.Update, + tooltip: T.Update, + selected: { max: 1 }, + color: 'secondary', + action: (rows) => { + const vnet = rows?.[0]?.original ?? {} + const path = PATH.NETWORK.VN_TEMPLATES.CREATE + + history.push(path, vnet) + }, + }, + { + accessor: VN_TEMPLATE_ACTIONS.CHANGE_CLUSTER, + color: 'secondary', + dataCy: `vnettemplate-${VN_TEMPLATE_ACTIONS.CHANGE_CLUSTER}`, + label: T.SelectCluster, + tooltip: T.SelectCluster, + selected: true, + options: [ + { + dialogProps: { + title: T.SelectCluster, + dataCy: 'modal-select-cluster', + }, + form: () => ChangeClusterForm(), + onSubmit: (rows) => async (formData) => { + const ids = rows?.map?.(({ original }) => original?.ID) + + await Promise.all( + ids.map((id) => + changeCluster({ id: formData.cluster, vnet: id }) + ) + ) + }, + }, + ], + }, + { + tooltip: T.Ownership, + icon: Group, + selected: true, + color: 'secondary', + dataCy: 'vnettemplate-ownership', + options: [ + { + accessor: VN_TEMPLATE_ACTIONS.CHANGE_OWNER, + name: T.ChangeOwner, + dialogProps: { + title: T.ChangeOwner, + subheader: SubHeader, + dataCy: `modal-${VN_TEMPLATE_ACTIONS.CHANGE_OWNER}`, + }, + form: ChangeUserForm, + onSubmit: (rows) => (newOwnership) => { + rows?.map?.(({ original }) => + changeOwnership({ id: original?.ID, ...newOwnership }) + ) + }, + }, + { + accessor: VN_TEMPLATE_ACTIONS.CHANGE_GROUP, + name: T.ChangeGroup, + dialogProps: { + title: T.ChangeGroup, + subheader: SubHeader, + dataCy: `modal-${VN_TEMPLATE_ACTIONS.CHANGE_GROUP}`, + }, + form: ChangeGroupForm, + onSubmit: (rows) => async (newOwnership) => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all( + ids.map((id) => changeOwnership({ id, ...newOwnership })) + ) + }, + }, + ], + }, + { + tooltip: T.Lock, + icon: Lock, + selected: true, + color: 'secondary', + dataCy: 'vnettemplate-lock', + options: [ + { + accessor: VN_TEMPLATE_ACTIONS.LOCK, + name: T.Lock, + isConfirmDialog: true, + dialogProps: { + title: T.Lock, + dataCy: `modal-${VN_TEMPLATE_ACTIONS.LOCK}`, + children: MessageToConfirmAction, + }, + onSubmit: (rows) => async () => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all(ids.map((id) => lock({ id }))) + }, + }, + { + accessor: VN_TEMPLATE_ACTIONS.UNLOCK, + name: T.Unlock, + isConfirmDialog: true, + dialogProps: { + title: T.Unlock, + dataCy: `modal-${VN_TEMPLATE_ACTIONS.UNLOCK}`, + children: MessageToConfirmAction, + }, + onSubmit: (rows) => async () => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all(ids.map((id) => unlock({ id }))) + }, + }, + ], + }, + { + accessor: VN_TEMPLATE_ACTIONS.DELETE, + dataCy: `vnettemplate-${VN_TEMPLATE_ACTIONS.DELETE}`, + tooltip: T.Delete, + icon: Trash, + selected: true, + color: 'error', + options: [ + { + isConfirmDialog: true, + dialogProps: { + title: T.Delete, + children: MessageToConfirmAction, + dataCy: `modal-vnet-${VN_TEMPLATE_ACTIONS.DELETE}`, + }, + onSubmit: (rows) => async () => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all(ids.map((id) => remove({ id }))) + }, + }, + ], + }, + ], + }), + [view] + ) + + return actions +} + +export default Actions diff --git a/src/fireedge/src/client/components/Tables/VNetworks/actions.js b/src/fireedge/src/client/components/Tables/VNetworks/actions.js index d13fa18e5c..bb2873fddb 100644 --- a/src/fireedge/src/client/components/Tables/VNetworks/actions.js +++ b/src/fireedge/src/client/components/Tables/VNetworks/actions.js @@ -13,43 +13,43 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { useMemo } from 'react' -import { useHistory } from 'react-router-dom' -import { - AddCircledOutline, - // Import, - Trash, - PlayOutline, - Lock, - Group, -} from 'iconoir-react' import { Typography } from '@mui/material' import { makeStyles } from '@mui/styles' +import { + AddCircledOutline, + Group, + Lock, + PlayOutline, + // Import, + Trash, +} from 'iconoir-react' +import { useMemo } from 'react' +import { useHistory } from 'react-router-dom' import { useViews } from 'client/features/Auth' import { useAddNetworkToClusterMutation } from 'client/features/OneApi/cluster' import { - useReserveAddressMutation, - useLockVNetMutation, - useUnlockVNetMutation, useChangeVNetOwnershipMutation, - useRemoveVNetMutation, + useLockVNetMutation, useRecoverVNetMutation, + useRemoveVNetMutation, + useReserveAddressMutation, + useUnlockVNetMutation, } from 'client/features/OneApi/network' import { isAvailableAction } from 'client/models/VirtualNetwork' -import { ChangeUserForm, ChangeGroupForm } from 'client/components/Forms/Vm' -import { ReserveForm, RecoverForm } from 'client/components/Forms/VNetwork' import { ChangeClusterForm } from 'client/components/Forms/Cluster' +import { RecoverForm, ReserveForm } from 'client/components/Forms/VNetwork' +import { ChangeGroupForm, ChangeUserForm } from 'client/components/Forms/Vm' +import { Translate } from 'client/components/HOC' import { - createActions, GlobalAction, + createActions, } from 'client/components/Tables/Enhanced/Utils' import VNetworkTemplatesTable from 'client/components/Tables/VNetworkTemplates' -import { Translate } from 'client/components/HOC' import { PATH } from 'client/apps/sunstone/routesOne' -import { T, VN_ACTIONS, RESOURCE_NAMES } from 'client/constants' +import { RESOURCE_NAMES, T, VN_ACTIONS } from 'client/constants' const isDisabled = (action) => (rows) => !isAvailableAction( @@ -125,17 +125,6 @@ const Actions = () => { icon: AddCircledOutline, action: () => history.push(PATH.NETWORK.VNETS.CREATE), }, - /* { - // TODO: Import Virtual Network from vCenter - accessor: VN_ACTIONS.IMPORT_DIALOG, - tooltip: T.Import, - icon: Import, - selected: { max: 1 }, - disabled: true, - action: (rows) => { - // TODO: go to IMPORT form - }, - }, */ { accessor: VN_ACTIONS.INSTANTIATE_DIALOG, dataCy: `vnet-${VN_ACTIONS.INSTANTIATE_DIALOG}`, diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Address.js b/src/fireedge/src/client/components/Tabs/VNetwork/Address.js index dcff10d79f..5f63c6ca38 100644 --- a/src/fireedge/src/client/components/Tabs/VNetwork/Address.js +++ b/src/fireedge/src/client/components/Tabs/VNetwork/Address.js @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { ReactElement } from 'react' -import PropTypes from 'prop-types' import { Box, Stack } from '@mui/material' +import PropTypes from 'prop-types' +import { ReactElement } from 'react' import { useGetVNetworkQuery } from 'client/features/OneApi/network' -import AddressRangeCard from 'client/components/Cards/AddressRangeCard' import { AddAddressRangeAction, - UpdateAddressRangeAction, DeleteAddressRangeAction, + UpdateAddressRangeAction, } from 'client/components/Buttons' +import AddressRangeCard from 'client/components/Cards/AddressRangeCard' import { AddressRange, VN_ACTIONS } from 'client/constants' @@ -50,7 +50,7 @@ const AddressTab = ({ const { data: vnet } = useGetVNetworkQuery({ id }) /** @type {AddressRange[]} */ - const addressRanges = [vnet.AR_POOL.AR ?? []].flat() + const addressRanges = [vnet?.AR_POOL?.AR ?? []].flat() return ( diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js b/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js index 72beb82ef6..55f52d322f 100644 --- a/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js +++ b/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js @@ -22,8 +22,7 @@ import { Box } from '@mui/material' import { useViews } from 'client/features/Auth' import { useGetClustersQuery } from 'client/features/OneApi/cluster' -import { useGetVNetworkQuery } from 'client/features/OneApi/network' -// import {} from 'client/components/Tabs/VNetwork/Address/Actions' +import { useGetVNTemplateQuery } from 'client/features/OneApi/networkTemplate' import { ClustersTable } from 'client/components/Tables' import { RESOURCE_NAMES } from 'client/constants' @@ -40,7 +39,7 @@ const { CLUSTER } = RESOURCE_NAMES */ const ClustersTab = ({ id }) => { const { push: redirectTo } = useHistory() - const { data: vnet } = useGetVNetworkQuery({ id }) + const { data: vnet } = useGetVNTemplateQuery({ id }) const { view, hasAccessToResource } = useViews() const detailAccess = useMemo(() => hasAccessToResource(CLUSTER), [view]) diff --git a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Address.js b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Address.js new file mode 100644 index 0000000000..3107a61ebf --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Address.js @@ -0,0 +1,178 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { Box, Stack } from '@mui/material' +import PropTypes from 'prop-types' +import { ReactElement } from 'react' + +import { + AddAddressRangeAction, + DeleteAddressRangeAction, + UpdateAddressRangeAction, +} from 'client/components/Buttons' +import AddressRangeCard from 'client/components/Cards/AddressRangeCard' +import { + useGetVNTemplateQuery, + useUpdateVNTemplateMutation, +} from 'client/features/OneApi/networkTemplate' +import { jsonToXml } from 'client/models/Helper' + +import { AddressRange, VN_ACTIONS } from 'client/constants' + +const { ADD_AR, UPDATE_AR, DELETE_AR } = VN_ACTIONS + +const handleAdd = async ({ value, id, update, template }) => { + const addressRanges = [template?.AR ?? []].flat() + addressRanges.push(value) + const templateJson = { ...template, AR: addressRanges } + + const newTemplate = jsonToXml(templateJson) + await update({ id, template: newTemplate }).unwrap() +} + +const handleUpdate = async ({ value, id, addressID, update, template }) => { + let templateJson = { ...template, AR: value } + + if (Array.isArray(template?.AR)) { + const addressRanges = [template.AR ?? []].flat() + addressRanges[addressID] = value + templateJson = { ...template, AR: addressRanges } + } + + const newTemplate = jsonToXml(templateJson) + await update({ id, template: newTemplate }).unwrap() +} + +const handleDelete = async ({ id, addressID, update, template }) => { + const { AR, ...rest } = template + let templateJson = { ...rest } + + if (Array.isArray(template?.AR)) { + const addressRanges = [template.AR ?? []].flat() + addressRanges.splice(addressID, 1) + templateJson = { ...template, AR: addressRanges } + } + + const newTemplate = jsonToXml(templateJson) + await update({ id, template: newTemplate }).unwrap() +} + +/** + * Renders the list of address ranges from a Virtual Network. + * + * @param {object} props - Props + * @param {object} props.tabProps - Tab information + * @param {string[]} props.tabProps.actions - Actions tab + * @param {string} props.id - Virtual Network id + * @param {object} props.oneConfig - Open Nebula configuration + * @param {boolean} props.adminGroup - If the user belongs to oneadmin group + * @returns {ReactElement} AR tab + */ +const AddressTab = ({ + tabProps: { actions } = {}, + id, + oneConfig, + adminGroup, +}) => { + const { data: vnet } = useGetVNTemplateQuery( + { id }, + { refetchOnMountOrArgChange: true } + ) + const [update] = useUpdateVNTemplateMutation() + + /** @type {AddressRange[]} */ + const addressRanges = [vnet?.TEMPLATE?.AR ?? []].flat() + const template = vnet?.TEMPLATE + + return ( + + {actions[ADD_AR] === true && ( + + handleAdd({ + value, + id, + update, + template, + }) + } + /> + )} + + + {addressRanges.map((ar, addressID) => ( + + {actions[UPDATE_AR] === true && ( + + handleUpdate({ + value, + id, + addressID, + update, + template, + }) + } + /> + )} + {actions[DELETE_AR] === true && ( + + handleDelete({ + id, + addressID, + update, + template, + }) + } + /> + )} + + } + /> + ))} + + + ) +} + +AddressTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +AddressTab.displayName = 'AddressTab' + +export default AddressTab diff --git a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Clusters.js b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Clusters.js new file mode 100644 index 0000000000..29463eca45 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Clusters.js @@ -0,0 +1,87 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import PropTypes from 'prop-types' +import { ReactElement, useMemo } from 'react' + +import { Box } from '@mui/material' +import { useHistory } from 'react-router' +import { generatePath } from 'react-router-dom' + +import { useViews } from 'client/features/Auth' +import { useGetClustersQuery } from 'client/features/OneApi/cluster' +import { useGetVNTemplateQuery } from 'client/features/OneApi/networkTemplate' + +import { PATH } from 'client/apps/sunstone/routesOne' +import { ClustersTable } from 'client/components/Tables' +import { RESOURCE_NAMES } from 'client/constants' + +const { CLUSTER } = RESOURCE_NAMES + +/** + * Renders the list of clusters from a Virtual Network. + * + * @param {object} props - Props + * @param {string} props.id - Virtual Network id + * @returns {ReactElement} Clusters tab + */ +const ClustersTab = ({ id }) => { + const { push: redirectTo } = useHistory() + const { data: vnet } = useGetVNTemplateQuery( + { id }, + { refetchOnMountOrArgChange: true } + ) + + const { view, hasAccessToResource } = useViews() + const detailAccess = useMemo(() => hasAccessToResource(CLUSTER), [view]) + + const clusters = [vnet?.TEMPLATE?.CLUSTER_IDS?.split(',') ?? []] + .flat() + .map((clId) => +clId) + + const redirectToCluster = (row) => { + const clusterPath = PATH.INFRASTRUCTURE.CLUSTERS.DETAIL + redirectTo(generatePath(clusterPath, { id: row.ID })) + } + + const useQuery = () => + useGetClustersQuery(undefined, { + selectFromResult: ({ data: result = [], ...rest }) => ({ + data: result?.filter((cluster) => clusters.includes(+cluster.ID)), + ...rest, + }), + }) + + return ( + + + + ) +} + +ClustersTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +ClustersTab.displayName = 'ClustersTab' + +export default ClustersTab diff --git a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Info/index.js b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Info/index.js index 7e24f4c844..f85cfc5fb7 100644 --- a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Info/index.js +++ b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Info/index.js @@ -13,28 +13,30 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { ReactElement, useCallback } from 'react' +import { Box, Stack } from '@mui/material' import PropTypes from 'prop-types' -import { Stack } from '@mui/material' +import { ReactElement, useCallback } from 'react' import { - useGetVNTemplateQuery, + AttributePanel, + Ownership, + Permissions, +} from 'client/components/Tabs/Common' +import QOS from 'client/components/Tabs/VNetwork/Info/qos' +import Information from 'client/components/Tabs/VNetworkTemplate/Info/information' +import { useChangeVNTemplateOwnershipMutation, useChangeVNTemplatePermissionsMutation, + useGetVNTemplateQuery, useUpdateVNTemplateMutation, } from 'client/features/OneApi/networkTemplate' -import { - Permissions, - Ownership, - AttributePanel, -} from 'client/components/Tabs/Common' -import Information from 'client/components/Tabs/VNetworkTemplate/Info/information' +import makeStyles from '@mui/styles/makeStyles' import { Tr } from 'client/components/HOC' import { T } from 'client/constants' import { - getActionsAvailable, filterAttributes, + getActionsAvailable, jsonToXml, } from 'client/models/Helper' import { cloneObject, set } from 'client/utils' @@ -44,6 +46,15 @@ const VCENTER_ATTRIBUTES_REG = /^VCENTER_/ const HIDDEN_ATTRIBUTES_REG = /^(AR|CLUSTERS|SECURITY_GROUPS|INBOUND_AVG_BW|INBOUND_PEAK_BW|INBOUND_PEAK_KB|OUTBOUND_AVG_BW|OUTBOUND_PEAK_BW|OUTBOUND_PEAK_KB)$/ +const useStyles = makeStyles({ + container: { + gridColumn: '1 / -1', + display: 'grid', + gridTemplateColumns: 'auto auto', + gap: '1rem', + }, +}) + /** * Renders mainly information tab. * @@ -53,10 +64,13 @@ const HIDDEN_ATTRIBUTES_REG = * @returns {ReactElement} Information tab */ const VNetTemplateInfoTab = ({ tabProps = {}, id }) => { + const classes = useStyles() + const { information_panel: informationPanel, permissions_panel: permissionsPanel, ownership_panel: ownershipPanel, + qos_panel: qosPanel, attributes_panel: attributesPanel, vcenter_panel: vcenterPanel, lxc_panel: lxcPanel, @@ -146,6 +160,11 @@ const VNetTemplateInfoTab = ({ tabProps = {}, id }) => { groupName={GNAME} /> )} + {qosPanel?.enabled && ( + + + + )} {attributesPanel?.enabled && attributes && ( { const [rename] = useRenameVNTemplateMutation() - const { ID, NAME } = vnetTemplate + const { ID, NAME, VLAN_ID_AUTOMATIC, OUTER_VLAN_ID_AUTOMATIC } = vnetTemplate const handleRename = async (_, newName) => { await rename({ id: ID, name: newName }) @@ -45,16 +45,30 @@ const InformationPanel = ({ vnetTemplate = {}, actions }) => { canEdit: actions?.includes?.(VN_TEMPLATE_ACTIONS.RENAME), handleEdit: handleRename, }, + { + name: T.AutomaticVlanId, + value: booleanToString(stringToBoolean(VLAN_ID_AUTOMATIC)), + dataCy: 'vlan_id_automatic', + handleEdit: handleRename, + }, + { + name: T.OuterVlanId, + value: OUTER_VLAN_ID_AUTOMATIC || '-', + dataCy: 'outer_vlan_id_automatic', + }, + { + name: T.AutomaticOuterVlanId, + value: booleanToString(stringToBoolean(OUTER_VLAN_ID_AUTOMATIC)), + dataCy: 'outer_vlan_automatic', + }, ] return ( - <> - - + ) } diff --git a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Security.js b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Security.js new file mode 100644 index 0000000000..b89f54f5fc --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/Security.js @@ -0,0 +1,94 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import PropTypes from 'prop-types' +import { ReactElement, useMemo } from 'react' + +import { Box } from '@mui/material' +import { useHistory } from 'react-router' +import { generatePath } from 'react-router-dom' + +import { useViews } from 'client/features/Auth' +import { useGetVNTemplateQuery } from 'client/features/OneApi/networkTemplate' +import { useGetSecGroupsQuery } from 'client/features/OneApi/securityGroup' +// import {} from 'client/components/Tabs/VNetwork/Address/Actions' + +import { PATH } from 'client/apps/sunstone/routesOne' +import { SecurityGroupsTable } from 'client/components/Tables' +import { RESOURCE_NAMES } from 'client/constants' + +const { SEC_GROUP } = RESOURCE_NAMES + +/** + * Renders the list of security groups from a Virtual Network. + * + * @param {object} props - Props + * @param {object} props.tabProps - Tab information + * @param {string[]} props.tabProps.actions - Actions tab + * @param {string} props.id - Virtual Network id + * @param {object} props.oneConfig - OpenNebula configuration + * @param {boolean} props.adminGroup - If the user belongs to the oneadmin group + * @returns {ReactElement} Security Groups tab + */ +const SecurityTab = ({ + tabProps: { actions } = {}, + id, + oneConfig, + adminGroup, +}) => { + const { push: redirectTo } = useHistory() + const { data: vnet } = useGetVNTemplateQuery({ id }) + + const { view, hasAccessToResource } = useViews() + const detailAccess = useMemo(() => hasAccessToResource(SEC_GROUP), [view]) + + const splittedSecGroups = vnet?.TEMPLATE.SECURITY_GROUPS?.split(',') ?? [] + const secGroups = [splittedSecGroups].flat().map((sgId) => +sgId) + + const redirectToSecGroup = (row) => { + redirectTo(generatePath(PATH.NETWORK.SEC_GROUPS.DETAIL, { id: row.ID })) + } + + const useQuery = () => + useGetSecGroupsQuery(undefined, { + selectFromResult: ({ data: result = [], ...rest }) => ({ + data: result?.filter((secgroup) => secGroups.includes(+secgroup.ID)), + ...rest, + }), + }) + + return ( + + + + ) +} + +SecurityTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, + oneConfig: PropTypes.object, + adminGroup: PropTypes.bool, +} + +SecurityTab.displayName = 'SecurityTab' + +export default SecurityTab diff --git a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/index.js b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/index.js index e7dc67ddf8..076bfd41b9 100644 --- a/src/fireedge/src/client/components/Tabs/VNetworkTemplate/index.js +++ b/src/fireedge/src/client/components/Tabs/VNetworkTemplate/index.js @@ -23,11 +23,17 @@ import { useGetVNTemplateQuery } from 'client/features/OneApi/networkTemplate' import { getAvailableInfoTabs } from 'client/models/Helper' import Tabs from 'client/components/Tabs' +import Address from 'client/components/Tabs/VNetworkTemplate/Address' +import Clusters from 'client/components/Tabs/VNetworkTemplate/Clusters' import Info from 'client/components/Tabs/VNetworkTemplate/Info' +import Security from 'client/components/Tabs/VNetworkTemplate/Security' const getTabComponent = (tabName) => ({ info: Info, + address: Address, + security: Security, + cluster: Clusters, }[tabName]) const VNetTemplateTabs = memo(({ id }) => { diff --git a/src/fireedge/src/client/constants/networkTemplate.js b/src/fireedge/src/client/constants/networkTemplate.js index e696d78988..f2db1cc93f 100644 --- a/src/fireedge/src/client/constants/networkTemplate.js +++ b/src/fireedge/src/client/constants/networkTemplate.js @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -// eslint-disable-next-line no-unused-vars -import { Permissions, LockInfo } from 'client/constants/common' import * as ACTIONS from 'client/constants/actions' +// eslint-disable-next-line no-unused-vars +import { LockInfo, Permissions } from 'client/constants/common' /** * @typedef VNetworkTemplate @@ -35,6 +35,11 @@ import * as ACTIONS from 'client/constants/actions' /** @enum {string} Virtual network template actions */ export const VN_TEMPLATE_ACTIONS = { CREATE_DIALOG: 'create_dialog', + UPDATE_DIALOG: 'update_dialog', + INSTANTIATE_DIALOG: 'instantiate_dialog', + CHANGE_CLUSTER: 'change_cluster', + LOCK: 'lock', + UNLOCK: 'unlock', DELETE: 'delete', // INFORMATION diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index 1b9a7ffadb..9224b9a0f8 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -84,6 +84,7 @@ module.exports = { CreateUser: 'Create User', UpdateUser: 'Update User', CreateVirtualNetwork: 'Create Virtual Network', + CreateVirtualNetworkTemplate: 'Create Virtual Network Template', CreateVmTemplate: 'Create VM Template', CreateVDC: 'Create VDC', UpdateVDC: 'Update VDC', @@ -126,6 +127,7 @@ module.exports = { Info: 'Info', Instantiate: 'Instantiate', InstantiateVmTemplate: 'Instantiate VM Template', + InstantiateVnTemplate: 'Instantiate Network Template', LocateOnTable: 'Locate on table', Lock: 'Lock', Migrate: 'Migrate', @@ -212,6 +214,7 @@ module.exports = { UpdateScheduleAction: 'Update schedule action: %s', UpdateServiceTemplate: 'Update Service Template', UpdateVirtualNetwork: 'Update Virtual Network', + UpdateVirtualNetworkTemplate: 'Update Virtual Network Template', UpdateVmConfiguration: 'Update VM Configuration', UpdateVmTemplate: 'Update VM Template', @@ -730,6 +733,7 @@ module.exports = { Timezone: 'Timezone', /* VM schema - info */ VmName: 'VM name', + VNName: 'Virtual Network Name', UserTemplate: 'User Template', Template: 'Template', WhereIsRunning: @@ -847,6 +851,9 @@ module.exports = { Virtualization: 'Virtualization', CustomInformation: 'Custom information', CustomVirtualization: 'Custom virtualization', + VnTemplateNameHelper: ` + Defaults to 'template name-' when empty. + When creating several Virtual Network, the wildcard %%idx will be replaced with a number starting from 0`, VmTemplateNameHelper: ` Defaults to 'template name-' when empty. When creating several VMs, the wildcard %%idx will be @@ -1321,6 +1328,8 @@ module.exports = { AddToExistingReservation: 'Add to an existing Reservation', FirstAddress: 'First address', IpOrMac: 'IP or MAC', + MessageQos: + 'These values apply to each VM interface individually, they are not global values for the Virtual Network', /* security group schema */ Security: 'Security', @@ -1470,6 +1479,8 @@ module.exports = { ManualNetwork: 'Manual Network', OpennebulaVirtualNetwork: 'OpenNebula Virtual Network', SelectNewNetwork: 'Please select a network from the list', + MessageAddSecGroupDefault: + 'The default Security Group 0 is automatically added to new Virtual Networks', NotVmsCurrentySecGroups: 'There are currently no VMs associated with this Security Group', CommitMessageSecGroups: ` diff --git a/src/fireedge/src/client/containers/VNetworkTemplates/Create.js b/src/fireedge/src/client/containers/VNetworkTemplates/Create.js new file mode 100644 index 0000000000..e8006089d1 --- /dev/null +++ b/src/fireedge/src/client/containers/VNetworkTemplates/Create.js @@ -0,0 +1,88 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement } from 'react' +import { useHistory, useLocation } from 'react-router' + +import { useGeneralApi } from 'client/features/General' +import { + useAllocateVNTemplateMutation, + useGetVNTemplateQuery, + useUpdateVNTemplateMutation, +} from 'client/features/OneApi/networkTemplate' + +import { PATH } from 'client/apps/sunstone/routesOne' +import { + DefaultFormStepper, + SkeletonStepsForm, +} from 'client/components/FormStepper' +import { CreateForm } from 'client/components/Forms/VNTemplate' + +import { useSystemData } from 'client/features/Auth' + +const _ = require('lodash') + +/** + * Displays the creation or modification form to a Virtual Network. + * + * @returns {ReactElement} Virtual Network form + */ +const CreateVirtualNetworkTemplate = () => { + const history = useHistory() + const { state: { ID: vnetId, NAME } = {} } = useLocation() + + const { enqueueSuccess } = useGeneralApi() + const [update] = useUpdateVNTemplateMutation() + const [allocate] = useAllocateVNTemplateMutation() + const { adminGroup, oneConfig } = useSystemData() + + const { data } = useGetVNTemplateQuery( + { id: vnetId, extended: true }, + { skip: vnetId === undefined } + ) + + const onSubmit = async (xml) => { + try { + if (!vnetId) { + const newVnetId = await allocate({ template: xml }).unwrap() + enqueueSuccess(`Virtual Network Template created - #${newVnetId}`) + } else { + await update({ id: vnetId, template: xml }).unwrap() + enqueueSuccess(`Virtual Network Template updated - #${vnetId} ${NAME}`) + } + + history.push(PATH.NETWORK.VN_TEMPLATES.LIST) + } catch {} + } + + return !_.isEmpty(oneConfig) && ((vnetId && data) || !vnetId) ? ( + } + > + {(config) => } + + ) : ( + + ) +} + +export default CreateVirtualNetworkTemplate diff --git a/src/fireedge/src/client/containers/VNetworkTemplates/Instantiate.js b/src/fireedge/src/client/containers/VNetworkTemplates/Instantiate.js new file mode 100644 index 0000000000..6cc7f7fadc --- /dev/null +++ b/src/fireedge/src/client/containers/VNetworkTemplates/Instantiate.js @@ -0,0 +1,89 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement } from 'react' +import { Redirect, useHistory, useLocation } from 'react-router' + +import { useGeneralApi } from 'client/features/General' +import { + useGetVNTemplateQuery, + useInstantiateVNTemplateMutation, +} from 'client/features/OneApi/networkTemplate' + +import { PATH } from 'client/apps/sunstone/routesOne' +import { + DefaultFormStepper, + SkeletonStepsForm, +} from 'client/components/FormStepper' +import { InstantiateForm } from 'client/components/Forms/VNTemplate' + +import { useSystemData } from 'client/features/Auth' + +const _ = require('lodash') + +/** + * Displays the instantiation form for a VM Template. + * + * @returns {ReactElement} Instantiation form + */ +const InstantiateVnTemplate = () => { + const history = useHistory() + const { state: { ID: templateId, NAME: templateName } = {} } = useLocation() + + const { enqueueInfo } = useGeneralApi() + const [instantiate] = useInstantiateVNTemplateMutation() + + const { adminGroup, oneConfig } = useSystemData() + + const { data: apiTemplateDataExtended, isError } = useGetVNTemplateQuery( + { id: templateId }, + { skip: templateId === undefined } + ) + + const dataTemplateExtended = _.cloneDeep(apiTemplateDataExtended) + + const onSubmit = async (template) => { + try { + await instantiate(template).unwrap() + history.push(PATH.NETWORK.VN_TEMPLATES.LIST) + + const templateInfo = `#${templateId} ${templateName}` + enqueueInfo(`VN Template instantiated - ${templateInfo}`) + } catch {} + } + + if (!templateId || isError) { + return + } + + return !dataTemplateExtended || _.isEmpty(oneConfig) ? ( + + ) : ( + } + > + {(config) => } + + ) +} + +export default InstantiateVnTemplate diff --git a/src/fireedge/src/client/containers/VNetworkTemplates/index.js b/src/fireedge/src/client/containers/VNetworkTemplates/index.js index 7a334b7624..8591f0e4d6 100644 --- a/src/fireedge/src/client/containers/VNetworkTemplates/index.js +++ b/src/fireedge/src/client/containers/VNetworkTemplates/index.js @@ -13,25 +13,26 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { ReactElement, useState, memo } from 'react' -import PropTypes from 'prop-types' +import { Box, Chip, Stack, Typography } from '@mui/material' +import Cancel from 'iconoir-react/dist/Cancel' import GotoIcon from 'iconoir-react/dist/Pin' import RefreshDouble from 'iconoir-react/dist/RefreshDouble' -import Cancel from 'iconoir-react/dist/Cancel' -import { Typography, Box, Stack, Chip } from '@mui/material' +import PropTypes from 'prop-types' +import { ReactElement, memo, useState } from 'react' import { Row } from 'react-table' +import { SubmitButton } from 'client/components/FormControl' +import { Tr } from 'client/components/HOC' +import MultipleTags from 'client/components/MultipleTags' +import SplitPane from 'client/components/SplitPane' +import { VNetworkTemplatesTable } from 'client/components/Tables' +import VNetworkTemplateActions from 'client/components/Tables/VNetworkTemplates/actions' +import VNetworkTemplateTabs from 'client/components/Tabs/VNetworkTemplate' +import { T, VNetworkTemplate } from 'client/constants' import { useLazyGetVNTemplateQuery, useUpdateVNTemplateMutation, } from 'client/features/OneApi/networkTemplate' -import { VNetworkTemplatesTable } from 'client/components/Tables' -import VNetworkTemplateTabs from 'client/components/Tabs/VNetworkTemplate' -import SplitPane from 'client/components/SplitPane' -import MultipleTags from 'client/components/MultipleTags' -import { SubmitButton } from 'client/components/FormControl' -import { Tr } from 'client/components/HOC' -import { T, VNetworkTemplate } from 'client/constants' /** * Displays a list of VNet Templates with a split pane between the list and selected row(s). @@ -40,7 +41,7 @@ import { T, VNetworkTemplate } from 'client/constants' */ function VNetworkTemplates() { const [selectedRows, onSelectedRowsChange] = useState(() => []) - + const actions = VNetworkTemplateActions() const hasSelectedRows = selectedRows?.length > 0 const moreThanOneSelected = selectedRows?.length > 1 @@ -50,6 +51,7 @@ function VNetworkTemplates() { diff --git a/src/fireedge/src/client/features/OneApi/networkTemplate.js b/src/fireedge/src/client/features/OneApi/networkTemplate.js index b55a4fd489..f96be881ff 100644 --- a/src/fireedge/src/client/features/OneApi/networkTemplate.js +++ b/src/fireedge/src/client/features/OneApi/networkTemplate.js @@ -13,13 +13,17 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { Actions, Commands } from 'server/utils/constants/commands/vntemplate' +import { FilterFlag, Permission, VNetworkTemplate } from 'client/constants' import { - oneApi, ONE_RESOURCES, ONE_RESOURCES_POOL, + oneApi, } from 'client/features/OneApi' -import { FilterFlag, Permission, VNetworkTemplate } from 'client/constants' +import { + updateOwnershipOnResource, + updateTemplateOnResource, +} from 'client/features/OneApi/common' +import { Actions, Commands } from 'server/utils/constants/commands/vntemplate' const { VNTEMPLATE } = ONE_RESOURCES const { VNET_POOL, VNTEMPLATE_POOL } = ONE_RESOURCES_POOL @@ -168,7 +172,31 @@ const vNetworkTemplateApi = oneApi.injectEndpoints({ return { params, command } }, - providesTags: (_, __, { id }) => [{ type: VNTEMPLATE, id }], + invalidatesTags: (_, __, { id }) => [{ type: VNTEMPLATE, id }], + async onQueryStarted(params, { dispatch, queryFulfilled }) { + try { + const patchVNTemplate = dispatch( + vNetworkTemplateApi.util.updateQueryData( + 'getVNTemplate', + { id: params.id }, + updateTemplateOnResource(params) + ) + ) + + const patchVNTemplates = dispatch( + vNetworkTemplateApi.util.updateQueryData( + 'getVNTemplates', + undefined, + updateTemplateOnResource(params) + ) + ) + + queryFulfilled.catch(() => { + patchVNTemplate.undo() + patchVNTemplates.undo() + }) + } catch {} + }, }), changeVNTemplatePermissions: builder.mutation({ /** @@ -215,10 +243,20 @@ const vNetworkTemplateApi = oneApi.injectEndpoints({ return { params, command } }, - invalidatesTags: (_, __, { id }) => [ - { type: VNTEMPLATE, id }, - VNTEMPLATE_POOL, - ], + invalidatesTags: (_, __, { id }) => [{ type: VNTEMPLATE, id }], + async onQueryStarted(params, { getState, dispatch, queryFulfilled }) { + try { + const patchVNet = dispatch( + vNetworkTemplateApi.util.updateQueryData( + 'getVNTemplate', + { id: params.id }, + updateOwnershipOnResource(getState(), params) + ) + ) + + queryFulfilled.catch(patchVNet.undo) + } catch {} + }, }), renameVNTemplate: builder.mutation({ /** @@ -270,15 +308,15 @@ const vNetworkTemplateApi = oneApi.injectEndpoints({ /** * Unlocks a VN Template. * - * @param {string|number} id - VN Template id + * @param {object} params - Request parameters * @returns {number} VN Template id * @throws Fails when response isn't code 200 */ - query: (id) => { + query: (params) => { const name = Actions.VNTEMPLATE_UNLOCK const command = { name, ...Commands[name] } - return { params: { id }, command } + return { params, command } }, invalidatesTags: (_, __, id) => [ { type: VNTEMPLATE, id }, diff --git a/src/fireedge/src/server/utils/constants/commands/vntemplate.js b/src/fireedge/src/server/utils/constants/commands/vntemplate.js index 3358de0942..0b5ba06226 100644 --- a/src/fireedge/src/server/utils/constants/commands/vntemplate.js +++ b/src/fireedge/src/server/utils/constants/commands/vntemplate.js @@ -174,11 +174,11 @@ module.exports = { from: resource, default: 0, }, - userId: { + user: { from: postBody, default: -1, }, - groupId: { + group: { from: postBody, default: -1, }, @@ -220,10 +220,14 @@ module.exports = { from: resource, default: 0, }, - lock: { + level: { from: postBody, default: 4, }, + test: { + from: postBody, + default: false, + }, }, }, [VNTEMPLATE_UNLOCK]: {