diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js index 8d96526d1f..65b05186f8 100644 --- a/src/fireedge/src/client/apps/sunstone/routesOne.js +++ b/src/fireedge/src/client/apps/sunstone/routesOne.js @@ -113,6 +113,10 @@ const VirtualNetworks = loadable( () => import('client/containers/VirtualNetworks'), { ssr: false } ) +const CreateVirtualNetwork = loadable( + () => import('client/containers/VirtualNetworks/Create'), + { ssr: false } +) const VNetworkTemplates = loadable( () => import('client/containers/VNetworkTemplates'), { ssr: false } @@ -156,7 +160,7 @@ export const PATH = { DETAIL: `/${RESOURCE_NAMES.VM}/:id`, }, VROUTERS: { - LIST: `/${RESOURCE_NAMES.V_ROUTER}`, + LIST: `/${RESOURCE_NAMES.VROUTER}`, }, SERVICES: { LIST: `/${RESOURCE_NAMES.SERVICE}`, @@ -200,6 +204,7 @@ export const PATH = { VNETS: { LIST: `/${RESOURCE_NAMES.VNET}`, DETAIL: `/${RESOURCE_NAMES.VNET}/:id`, + CREATE: `/${RESOURCE_NAMES.VNET}/create`, }, VN_TEMPLATES: { LIST: `/${RESOURCE_NAMES.VN_TEMPLATE}`, @@ -398,6 +403,16 @@ const ENDPOINTS = [ icon: NetworkTemplateIcon, Component: VNetworkTemplates, }, + { + title: (_, state) => + state?.ID !== undefined + ? T.UpdateVirtualNetwork + : T.CreateVirtualNetwork, + description: (_, state) => + state?.ID !== undefined && `#${state.ID} ${state.NAME}`, + path: PATH.NETWORK.VNETS.CREATE, + Component: CreateVirtualNetwork, + }, ], }, { diff --git a/src/fireedge/src/client/components/Buttons/AddressRangeActions.js b/src/fireedge/src/client/components/Buttons/AddressRangeActions.js new file mode 100644 index 0000000000..8d8e513d17 --- /dev/null +++ b/src/fireedge/src/client/components/Buttons/AddressRangeActions.js @@ -0,0 +1,162 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { memo } from 'react' +import PropTypes from 'prop-types' +import AddIcon from 'iconoir-react/dist/Plus' +import EditIcon from 'iconoir-react/dist/Edit' +import TrashIcon from 'iconoir-react/dist/Trash' + +import { + useAddRangeToVNetMutation, + useUpdateVNetRangeMutation, + useRemoveRangeFromVNetMutation, +} from 'client/features/OneApi/network' +import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm' +import { AddRangeForm } from 'client/components/Forms/VNetwork' + +import { jsonToXml } from 'client/models/Helper' +import { Tr, Translate } from 'client/components/HOC' +import { T, VN_ACTIONS } from 'client/constants' + +const AddAddressRangeAction = memo(({ vnetId, onSubmit }) => { + const [addAR] = useAddRangeToVNetMutation() + + const handleAdd = async (formData) => { + if (onSubmit && typeof onSubmit === 'function') { + return await onSubmit(formData) + } + + const template = jsonToXml({ AR: formData }) + await addAR({ id: vnetId, template }).unwrap() + } + + return ( + , + label: T.AddressRange, + variant: 'outlined', + }} + options={[ + { + dialogProps: { + title: T.AddressRange, + dataCy: 'modal-add-ar', + }, + form: AddRangeForm, + onSubmit: handleAdd, + }, + ]} + /> + ) +}) + +const UpdateAddressRangeAction = memo(({ vnetId, ar, onSubmit }) => { + const [updateAR] = useUpdateVNetRangeMutation() + const { AR_ID } = ar + + const handleUpdate = async (formData) => { + if (onSubmit && typeof onSubmit === 'function') { + return await onSubmit(formData) + } + + const template = jsonToXml({ AR: formData }) + await updateAR({ id: vnetId, template }) + } + + return ( + , + tooltip: T.Edit, + }} + options={[ + { + dialogProps: { + title: `${Tr(T.AddressRange)}: #${AR_ID}`, + dataCy: 'modal-update-ar', + }, + form: () => + AddRangeForm({ + initialValues: ar, + stepProps: { isUpdate: !onSubmit && AR_ID !== undefined }, + }), + onSubmit: handleUpdate, + }, + ]} + /> + ) +}) + +const DeleteAddressRangeAction = memo(({ vnetId, ar, onSubmit }) => { + const [removeAR] = useRemoveRangeFromVNetMutation() + const { AR_ID } = ar + + const handleRemove = async () => { + if (onSubmit && typeof onSubmit === 'function') { + return await onSubmit(AR_ID) + } + + await removeAR({ id: vnetId, address: AR_ID }) + } + + return ( + , + tooltip: T.Delete, + }} + options={[ + { + isConfirmDialog: true, + dialogProps: { + title: ( + <> + + {`: #${AR_ID}`} + + ), + children:

{Tr(T.DoYouWantProceed)}

, + }, + onSubmit: handleRemove, + }, + ]} + /> + ) +}) + +const ActionPropTypes = { + vnetId: PropTypes.string, + ar: PropTypes.object, + onSubmit: PropTypes.func, +} + +AddAddressRangeAction.propTypes = ActionPropTypes +AddAddressRangeAction.displayName = 'AddAddressRangeActionButton' +UpdateAddressRangeAction.propTypes = ActionPropTypes +UpdateAddressRangeAction.displayName = 'UpdateAddressRangeActionButton' +DeleteAddressRangeAction.propTypes = ActionPropTypes +DeleteAddressRangeAction.displayName = 'DeleteAddressRangeAction' + +export { + AddAddressRangeAction, + UpdateAddressRangeAction, + DeleteAddressRangeAction, +} diff --git a/src/fireedge/src/client/components/Buttons/index.js b/src/fireedge/src/client/components/Buttons/index.js index e402cc2da7..16694ca847 100644 --- a/src/fireedge/src/client/components/Buttons/index.js +++ b/src/fireedge/src/client/components/Buttons/index.js @@ -14,5 +14,6 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ +export * from 'client/components/Buttons/AddressRangeActions' export * from 'client/components/Buttons/ScheduleAction' export * from 'client/components/Buttons/ConsoleAction' diff --git a/src/fireedge/src/client/components/Cards/AddressRangeCard.js b/src/fireedge/src/client/components/Cards/AddressRangeCard.js new file mode 100644 index 0000000000..0f0fd93ecc --- /dev/null +++ b/src/fireedge/src/client/components/Cards/AddressRangeCard.js @@ -0,0 +1,185 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement, memo, useMemo } from 'react' +import PropTypes from 'prop-types' +import { Link as RouterLink, generatePath } from 'react-router-dom' +import { Typography, Link, Box } from '@mui/material' + +import { useViews } from 'client/features/Auth' + +import { rowStyles } from 'client/components/Tables/styles' +import MultipleTags from 'client/components/MultipleTags' +import { LinearProgressWithLabel } from 'client/components/Status' + +import { getARLeasesInfo } from 'client/models/VirtualNetwork' +import { Tr, Translate } from 'client/components/HOC' +import { + T, + VirtualNetwork, + AddressRange, + VNET_THRESHOLD, + RESOURCE_NAMES, +} from 'client/constants' +import { PATH } from 'client/apps/sunstone/routesOne' + +const { VNET } = RESOURCE_NAMES + +const AddressRangeCard = memo( + /** + * @param {object} props - Props + * @param {VirtualNetwork} props.vnet - Virtual network + * @param {AddressRange} props.ar - Address Range + * @param {ReactElement} [props.actions] - Actions + * @returns {ReactElement} - Card + */ + ({ vnet, ar = {}, actions }) => { + const classes = rowStyles() + + const { PARENT_NETWORK_ID: parentId } = vnet || {} + + const { + AR_ID, + TYPE, + IPAM_MAD, + USED_LEASES, + SIZE, + MAC, + MAC_END = '-', + IP, + IP_END = '-', + IP6, + IP6_END = '-', + IP6_GLOBAL, + IP6_GLOBAL_END = '-', + IP6_ULA, + IP6_ULA_END = '-', + } = ar + + const { view, hasAccessToResource } = useViews() + const canLinkToParent = useMemo( + () => hasAccessToResource(VNET) && parentId, + [view, parentId] + ) + + const leasesInfo = useMemo(() => getARLeasesInfo(ar), [ar]) + const { percentOfUsed, percentLabel } = leasesInfo + + const labels = [ + { text: TYPE, dataCy: 'type' }, + IPAM_MAD && { text: IPAM_MAD, dataCy: 'ipam-mad' }, + !USED_LEASES && + SIZE && { text: `${Tr(T.Size)}: ${SIZE}`, dataCy: 'size' }, + canLinkToParent && { + text: ( + + + + ), + dataCy: 'parent', + }, + ].filter(Boolean) + + return ( + `1px solid ${theme.palette.divider}`, + }} + > +
+
+ + {`#${AR_ID}`} + + + + +
+ + {MAC && ( + {`MAC: ${MAC} | ${MAC_END}`} + )} + {IP && ( + {`IP: ${IP} | ${IP_END}`} + )} + {IP6 && ( + {`IP6: ${IP6} | ${IP6_END}`} + )} + {IP6_GLOBAL && ( + {`IP6 GLOBAL: ${IP6_GLOBAL} | ${IP6_GLOBAL_END}`} + )} + {IP6_ULA && ( + {`IP6 ULA: ${IP6_ULA} | ${IP6_ULA_END}`} + )} + +
+
+ {USED_LEASES && ( + + )} + {actions &&
{actions}
} +
+
+ ) + } +) + +AddressRangeCard.propTypes = { + vnet: PropTypes.object, + ar: PropTypes.object, + actions: PropTypes.node, +} + +AddressRangeCard.displayName = 'AddressRangeCard' + +export default AddressRangeCard diff --git a/src/fireedge/src/client/components/Cards/index.js b/src/fireedge/src/client/components/Cards/index.js index 29752ff131..9f341cff7d 100644 --- a/src/fireedge/src/client/components/Cards/index.js +++ b/src/fireedge/src/client/components/Cards/index.js @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ +import AddressRangeCard from 'client/components/Cards/AddressRangeCard' import ApplicationCard from 'client/components/Cards/ApplicationCard' import ApplicationNetworkCard from 'client/components/Cards/ApplicationNetworkCard' import ApplicationTemplateCard from 'client/components/Cards/ApplicationTemplateCard' @@ -41,6 +42,7 @@ import VmTemplateCard from 'client/components/Cards/VmTemplateCard' import WavesCard from 'client/components/Cards/WavesCard' export { + AddressRangeCard, ApplicationCard, ApplicationNetworkCard, ApplicationTemplateCard, diff --git a/src/fireedge/src/client/components/FormControl/ToggleController.js b/src/fireedge/src/client/components/FormControl/ToggleController.js index 3f1b4dfeda..b2f77548ec 100644 --- a/src/fireedge/src/client/components/FormControl/ToggleController.js +++ b/src/fireedge/src/client/components/FormControl/ToggleController.js @@ -48,6 +48,7 @@ const ToggleController = memo( values = [], tooltip, fieldProps = {}, + notNull = false, readOnly = false, }) => { const { @@ -74,7 +75,9 @@ const ToggleController = memo( fullWidth ref={ref} id={cy} - onChange={(_, newValues) => !readOnly && onChange(newValues)} + onChange={(_, newValues) => + !readOnly && (!notNull || newValues) && onChange(newValues) + } value={optionSelected} exclusive={!multiple} data-cy={cy} @@ -110,6 +113,7 @@ ToggleController.propTypes = { values: PropTypes.arrayOf(PropTypes.object).isRequired, renderValue: PropTypes.func, fieldProps: PropTypes.object, + notNull: PropTypes.bool, readOnly: PropTypes.bool, } diff --git a/src/fireedge/src/client/components/FormStepper/index.js b/src/fireedge/src/client/components/FormStepper/index.js index 17823a316d..617df083e5 100644 --- a/src/fireedge/src/client/components/FormStepper/index.js +++ b/src/fireedge/src/client/components/FormStepper/index.js @@ -50,7 +50,11 @@ const DefaultFormStepper = ({ }) return ( - + resolver(methods.watch())} + > ) diff --git a/src/fireedge/src/client/components/Forms/Host/ChangeClusterForm/index.js b/src/fireedge/src/client/components/Forms/Cluster/ChangeClusterForm/index.js similarity index 95% rename from src/fireedge/src/client/components/Forms/Host/ChangeClusterForm/index.js rename to src/fireedge/src/client/components/Forms/Cluster/ChangeClusterForm/index.js index 3c7e5fab38..d34fc8eaca 100644 --- a/src/fireedge/src/client/components/Forms/Host/ChangeClusterForm/index.js +++ b/src/fireedge/src/client/components/Forms/Cluster/ChangeClusterForm/index.js @@ -17,7 +17,7 @@ import { createForm } from 'client/utils' import { SCHEMA, FIELDS, -} from 'client/components/Forms/Host/ChangeClusterForm/schema' +} from 'client/components/Forms/Cluster/ChangeClusterForm/schema' const ChangeClusterForm = createForm(SCHEMA, FIELDS) diff --git a/src/fireedge/src/client/components/Forms/Host/ChangeClusterForm/schema.js b/src/fireedge/src/client/components/Forms/Cluster/ChangeClusterForm/schema.js similarity index 100% rename from src/fireedge/src/client/components/Forms/Host/ChangeClusterForm/schema.js rename to src/fireedge/src/client/components/Forms/Cluster/ChangeClusterForm/schema.js diff --git a/src/fireedge/src/client/components/Forms/Cluster/index.js b/src/fireedge/src/client/components/Forms/Cluster/index.js new file mode 100644 index 0000000000..9c4af095d8 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Cluster/index.js @@ -0,0 +1,27 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement } from 'react' +import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC' +import { CreateFormCallback } from 'client/utils/schema' + +/** + * @param {ConfigurationProps} configProps - Configuration + * @returns {ReactElement|CreateFormCallback} Asynchronous loaded form + */ +const ChangeClusterForm = (configProps) => + AsyncLoadForm({ formPath: 'Cluster/ChangeClusterForm' }, configProps) + +export { ChangeClusterForm } diff --git a/src/fireedge/src/client/components/Forms/Host/index.js b/src/fireedge/src/client/components/Forms/Host/index.js index a8c056676e..c7aac9775f 100644 --- a/src/fireedge/src/client/components/Forms/Host/index.js +++ b/src/fireedge/src/client/components/Forms/Host/index.js @@ -15,14 +15,7 @@ * ------------------------------------------------------------------------- */ import { ReactElement } from 'react' import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC' -import { CreateFormCallback, CreateStepsCallback } from 'client/utils/schema' - -/** - * @param {ConfigurationProps} configProps - Configuration - * @returns {ReactElement|CreateFormCallback} Asynchronous loaded form - */ -const ChangeClusterForm = (configProps) => - AsyncLoadForm({ formPath: 'Host/ChangeClusterForm' }, configProps) +import { CreateStepsCallback } from 'client/utils/schema' /** * @param {ConfigurationProps} configProps - Configuration @@ -31,4 +24,4 @@ const ChangeClusterForm = (configProps) => const CreateForm = (configProps) => AsyncLoadForm({ formPath: 'Host/CreateForm' }, configProps) -export { ChangeClusterForm, CreateForm } +export { CreateForm } diff --git a/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/content.js b/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/content.js new file mode 100644 index 0000000000..4b62732875 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/content.js @@ -0,0 +1,68 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement } from 'react' +import PropTypes from 'prop-types' +import { useFormContext, useWatch } from 'react-hook-form' +import { Box } from '@mui/material' + +import { FormWithSchema } from 'client/components/Forms' +import { AttributePanel } from 'client/components/Tabs/Common' +import { + FIELDS, + MUTABLE_FIELDS, +} from 'client/components/Forms/VNetwork/AddRangeForm/schema' +import { cleanEmpty, cloneObject, set } from 'client/utils' +import { T } from 'client/constants' + +export const CUSTOM_ATTRS_ID = 'custom-attributes' + +/** + * @param {object} props - Props + * @param {boolean} [props.isUpdate] - Is `true` the form will be filter immutable attributes + * @returns {ReactElement} Form content component + */ +const Content = ({ isUpdate }) => { + const { setValue } = useFormContext() + const customAttrs = useWatch({ name: CUSTOM_ATTRS_ID }) || {} + + const handleChangeAttribute = (path, newValue) => { + const newCustomAttrs = cloneObject(customAttrs) + + set(newCustomAttrs, path, newValue) + setValue(CUSTOM_ATTRS_ID, cleanEmpty(newCustomAttrs)) + } + + return ( + + + + + ) +} + +Content.propTypes = { isUpdate: PropTypes.bool } + +export default Content diff --git a/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/index.js b/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/index.js new file mode 100644 index 0000000000..e1ede1a688 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/index.js @@ -0,0 +1,61 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import ContentForm, { + CUSTOM_ATTRS_ID, +} from 'client/components/Forms/VNetwork/AddRangeForm/content' +import { SCHEMA } from 'client/components/Forms/VNetwork/AddRangeForm/schema' +import { createForm } from 'client/utils' + +// List of attributes that can't be changed in update operation +const IMMUTABLE_ATTRS = [ + 'AR_ID', + 'TYPE', + 'IP', + 'IP_END', + 'IP6', + 'IP6_END', + 'MAC', + 'MAC_END', + 'IP6_GLOBAL', + 'IP6_GLOBAL_END', + 'GLOBAL_PREFIX', + 'ULA_PREFIX', + 'USED_LEASES', + 'PARENT_NETWORK_AR_ID', + 'LEASES', + 'IPAM_MAD', +] + +const AddRangeForm = createForm(SCHEMA, undefined, { + ContentForm, + transformInitialValue: (addressRange) => { + if (!addressRange) return {} + + const mutableAttrs = {} + for (const attr of Object.keys(addressRange)) { + !IMMUTABLE_ATTRS[attr] && (mutableAttrs[attr] = addressRange[attr]) + } + + return { ...mutableAttrs } + }, + transformBeforeSubmit: (formData) => { + const { [CUSTOM_ATTRS_ID]: customAttrs = {}, ...rest } = formData ?? {} + + return { ...customAttrs, ...rest } + }, +}) + +export default AddRangeForm diff --git a/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/schema.js b/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/schema.js new file mode 100644 index 0000000000..2f5d9fd4b9 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/AddRangeForm/schema.js @@ -0,0 +1,155 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { BaseSchema, string, number } from 'yup' + +import { + Field, + getObjectSchemaFromFields, + OPTION_SORTERS, + arrayToOptions, + REG_V4, + REG_MAC, +} from 'client/utils' +import { T, INPUT_TYPES } from 'client/constants' + +const AR_TYPES = { IP4: 'IP4', IP4_6: 'IP4_6', IP6: 'IP6', ETHER: 'ETHER' } + +const AR_TYPES_STR = { + [AR_TYPES.IP4]: 'IPv4', + [AR_TYPES.IP4_6]: 'IPv4/6', + [AR_TYPES.IP6]: 'IPv6', + [AR_TYPES.ETHER]: 'Ethernet', +} + +/** @type {Field} Type field */ +const TYPE_FIELD = { + name: 'TYPE', + type: INPUT_TYPES.TOGGLE, + values: () => + arrayToOptions(Object.keys(AR_TYPES), { + addEmpty: false, + getText: (type) => AR_TYPES_STR[type], + sorter: OPTION_SORTERS.unsort, + }), + validation: string() + .trim() + .required() + .default(() => 'IP4'), + notNull: true, +} + +/** @type {Field} IP field */ +const IP_FIELD = { + name: 'IP', + label: T.FirstIPv4Address, + type: INPUT_TYPES.TEXT, + dependOf: TYPE_FIELD.name, + htmlType: (arType) => + [AR_TYPES.IP6, AR_TYPES.ETHER].includes(arType) && INPUT_TYPES.HIDDEN, + validation: string() + .trim() + .default(() => undefined) + .when(TYPE_FIELD.name, { + is: (arType) => [AR_TYPES.IP6, AR_TYPES.ETHER].includes(arType), + then: (schema) => schema.strip().notRequired(), + otherwise: (schema) => + schema.required().matches(REG_V4, { message: T.InvalidIPv4 }), + }), +} + +/** @type {Field} MAC field */ +const MAC_FIELD = { + name: 'MAC', + label: T.FirstMacAddress, + tooltip: T.MacConcept, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .notRequired() + .matches(REG_MAC, { message: T.InvalidMAC }) + .default(() => undefined), +} + +/** @type {Field} Size field */ +const SIZE_FIELD = { + name: 'SIZE', + label: T.Size, + type: INPUT_TYPES.TEXT, + htmlType: 'number', + validation: number() + .integer() + .required() + .positive() + .default(() => 1), +} + +/** @type {Field} IPv6 Global prefix field */ +const GLOBAL_PREFIX_FIELD = { + name: 'GLOBAL_PREFIX', + label: T.IPv6GlobalPrefix, + type: INPUT_TYPES.TEXT, + dependOf: TYPE_FIELD.name, + htmlType: (arType) => + [AR_TYPES.IP4, AR_TYPES.ETHER].includes(arType) && INPUT_TYPES.HIDDEN, + validation: string() + .trim() + .notRequired() + .default(() => undefined) + .when(TYPE_FIELD.name, { + is: (arType) => [AR_TYPES.IP4, AR_TYPES.ETHER].includes(arType), + then: (schema) => schema.strip(), + }), +} + +/** @type {Field} IPv6 ULA prefix field */ +const ULA_PREFIX_FIELD = { + name: 'ULA_PREFIX', + label: T.IPv6ULAPrefix, + type: INPUT_TYPES.TEXT, + dependOf: TYPE_FIELD.name, + htmlType: (arType) => + [AR_TYPES.IP4, AR_TYPES.ETHER].includes(arType) && INPUT_TYPES.HIDDEN, + validation: string() + .trim() + .notRequired() + .default(() => undefined) + .when(TYPE_FIELD.name, { + is: (arType) => [AR_TYPES.IP4, AR_TYPES.ETHER].includes(arType), + then: (schema) => schema.strip(), + }), +} + +/** @type {Field[]} Fields */ +const FIELDS = [ + TYPE_FIELD, + IP_FIELD, + MAC_FIELD, + SIZE_FIELD, + GLOBAL_PREFIX_FIELD, + ULA_PREFIX_FIELD, +] + +const MUTABLE_FIELDS = [SIZE_FIELD] + +/** + * @param {object} stepProps - Step props + * @param {boolean} stepProps.isUpdate - If true the form is to update the AR + * @returns {BaseSchema} Schema + */ +const SCHEMA = ({ isUpdate }) => + getObjectSchemaFromFields([...(isUpdate ? MUTABLE_FIELDS : FIELDS)]) + +export { FIELDS, MUTABLE_FIELDS, SCHEMA } diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/addresses.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/addresses.js new file mode 100644 index 0000000000..8accf01f93 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/addresses.js @@ -0,0 +1,125 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import PropTypes from 'prop-types' +import { useFieldArray } from 'react-hook-form' +import { Stack, Typography } from '@mui/material' +import AddressesIcon from 'iconoir-react/dist/Menu' + +import { AddressRangeCard } from 'client/components/Cards' +import { + AddAddressRangeAction, + UpdateAddressRangeAction, + DeleteAddressRangeAction, +} from 'client/components/Buttons' +import { Translate } from 'client/components/HOC' + +import { + STEP_ID as EXTRA_ID, + TabType, +} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration' +import { mapNameByIndex } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/schema' +import { T } from 'client/constants' + +export const TAB_ID = 'AR' + +const mapNameFunction = mapNameByIndex('AR') + +const AddressesContent = () => { + const { + fields: addresses, + remove, + update, + append, + } = useFieldArray({ + name: `${EXTRA_ID}.${TAB_ID}`, + keyName: 'ID', + }) + + const handleCreateAction = (action) => { + append(mapNameFunction(action, addresses.length)) + } + + const handleUpdate = (action, index) => { + update(index, mapNameFunction(action, index)) + } + + const handleRemove = (index) => { + remove(index) + } + + return ( + <> + + + + + + {addresses?.map((ar, index) => { + const key = ar.ID ?? ar.NAME + const fakeValues = { ...ar, AR_ID: index } + + return ( + + handleUpdate(updatedAr, index)} + /> + handleRemove(index)} + /> + + } + /> + ) + })} + + + ) +} + +const Content = ({ isUpdate }) => + isUpdate ? ( + + + + ) : ( + + ) + +Content.propTypes = { isUpdate: PropTypes.bool } + +/** @type {TabType} */ +const TAB = { + id: 'addresses', + name: T.Addresses, + icon: AddressesIcon, + Content, + getError: (error) => !!error?.[TAB_ID], +} + +export default TAB diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js new file mode 100644 index 0000000000..7635ae1229 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes.js @@ -0,0 +1,81 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement, useCallback, useMemo } from 'react' +import { reach } from 'yup' +import { useFormContext, useWatch } from 'react-hook-form' + +import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration' +import { getUnknownVars } from 'client/components/Forms/VNetwork/CreateForm/Steps' +import { useGeneralApi } from 'client/features/General' + +import { Legend } from 'client/components/Forms' +import { AttributePanel } from 'client/components/Tabs/Common' +import { T } from 'client/constants' + +/** + * Renders the context attributes to Virtual Network form. + * + * @returns {ReactElement} - Context attributes section + */ +const ContextAttrsSection = () => { + const { enqueueError } = useGeneralApi() + const { setValue, getResolver } = useFormContext() + const extraStepVars = useWatch({ name: EXTRA_ID }) || {} + + const unknownVars = useMemo( + () => getUnknownVars(extraStepVars, getResolver()), + [extraStepVars] + ) + + const handleChangeAttribute = useCallback( + (path, newValue) => { + try { + const extraSchema = reach(getResolver(), EXTRA_ID) + + // retrieve the schema for the given path + const existsSchema = reach(extraSchema, path) + console.log(existsSchema) + enqueueError(T.InvalidAttribute) + } catch (e) { + // When the path is not found, it means that + // the attribute is correct and we can set it + setValue(`${EXTRA_ID}.${path}`, newValue) + } + }, + [setValue] + ) + + return ( + + } + allActionsEnabled + handleAdd={handleChangeAttribute} + handleEdit={handleChangeAttribute} + handleDelete={handleChangeAttribute} + attributes={unknownVars} + filtersSpecialAttributes={false} + /> + ) +} + +export default ContextAttrsSection diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/index.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/index.js new file mode 100644 index 0000000000..38bb1055de --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/index.js @@ -0,0 +1,44 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import ContextIcon from 'iconoir-react/dist/Folder' + +import { + TabType, + STEP_ID as EXTRA_ID, +} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration' +import { FIELDS } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/schema' +import CustomAttributes from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/customAttributes' + +import FormWithSchema from 'client/components/Forms/FormWithSchema' +import { T } from 'client/constants' + +const ContextContent = () => ( + <> + + + +) + +/** @type {TabType} */ +const TAB = { + id: 'context', + name: T.Context, + icon: ContextIcon, + Content: ContextContent, + getError: (error) => FIELDS.some(({ name }) => error?.[name]), +} + +export default TAB diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/schema.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/schema.js new file mode 100644 index 0000000000..160f7c5970 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/schema.js @@ -0,0 +1,110 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { string } from 'yup' + +import { Field, arrayToOptions, getObjectSchemaFromFields } from 'client/utils' +import { T, INPUT_TYPES, VNET_METHODS, VNET_METHODS6 } from 'client/constants' + +/** @type {Field} Network address field */ +const NETWORK_ADDRESS_FIELD = { + name: 'NETWORK_ADDRESS', + label: T.NetworkAddress, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Network mask field */ +const NETWORK_MASK_FIELD = { + name: 'NETWORK_MASK', + label: T.NetworkMask, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Gateway field */ +const GATEWAY_FIELD = { + name: 'GATEWAY', + label: T.Gateway, + tooltip: T.GatewayConcept, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Gateway for IPv6 field */ +const GATEWAY6_FIELD = { + name: 'GATEWAY6', + label: T.Gateway6, + tooltip: T.Gateway6Concept, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} DNS field */ +const DNS_FIELD = { + name: 'DNS', + label: T.DNS, + tooltip: T.DNSConcept, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Guest MTU field */ +const GUEST_MTU_FIELD = { + name: 'GUEST_MTU', + label: T.GuestMTU, + tooltip: T.GuestMTUConcept, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired(), +} + +/** @type {Field} Method field */ +const METHOD_FIELD = { + name: 'METHOD', + label: T.NetMethod, + type: INPUT_TYPES.SELECT, + values: arrayToOptions(Object.entries(VNET_METHODS), { + addEmpty: 'none (Use default)', + getText: ([, text]) => text, + getValue: ([value]) => value, + }), + validation: string().trim().notRequired(), +} + +/** @type {Field} Method for IPv6 field */ +const IP6_METHOD_FIELD = { + name: 'IP6_METHOD', + label: T.NetMethod6, + type: INPUT_TYPES.SELECT, + values: arrayToOptions(Object.entries(VNET_METHODS6), { + addEmpty: 'none (Use default)', + getText: ([, text]) => text, + getValue: ([value]) => value, + }), + validation: string().trim().notRequired(), +} + +export const FIELDS = [ + NETWORK_ADDRESS_FIELD, + NETWORK_MASK_FIELD, + GATEWAY_FIELD, + GATEWAY6_FIELD, + DNS_FIELD, + GUEST_MTU_FIELD, + METHOD_FIELD, + IP6_METHOD_FIELD, +] + +export const SCHEMA = getObjectSchemaFromFields(FIELDS) diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/index.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/index.js new file mode 100644 index 0000000000..5b45f16944 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/index.js @@ -0,0 +1,97 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +// eslint-disable-next-line no-unused-vars +import { useMemo, ReactElement } from 'react' +import PropTypes from 'prop-types' +// eslint-disable-next-line no-unused-vars +import { useFormContext, FieldErrors } from 'react-hook-form' + +import Tabs from 'client/components/Tabs' +import Addresses from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/addresses' +import Security from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/security' +import QoS from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos' +import Context from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context' +import { Translate } from 'client/components/HOC' + +import { STEP_ID as GENERAL_ID } from 'client/components/Forms/VNetwork/CreateForm/Steps/General' +import { SCHEMA } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/schema' +import { T, VirtualNetwork } from 'client/constants' + +/** + * @typedef {object} TabType + * @property {string} id - Id will be to use in view yaml to hide/display the tab + * @property {string} name - Label of tab + * @property {ReactElement} Content - Content tab + * @property {object} [icon] - Icon of tab + * @property {boolean} [immutable] - If `true`, the section will not be displayed whe is updating + * @property {function(FieldErrors):boolean} [getError] - Returns `true` if the tab contains an error in form + */ + +export const STEP_ID = 'extra' + +/** @type {TabType[]} */ +export const TABS = [Addresses, Security, QoS, Context] + +const Content = ({ isUpdate }) => { + const { + watch, + formState: { errors }, + } = useFormContext() + + const driver = useMemo(() => watch(`${GENERAL_ID}.VN_MAD`), []) + + const totalErrors = Object.keys(errors[STEP_ID] ?? {}).length + + const tabs = useMemo( + () => + TABS.map(({ Content: TabContent, name, getError, ...section }) => ({ + ...section, + name, + label: , + renderContent: () => , + error: getError?.(errors[STEP_ID]), + })), + [totalErrors, driver] + ) + + return +} + +/** + * Optional configuration about Virtual network. + * + * @param {VirtualNetwork} vnet - Virtual network + * @returns {object} Optional configuration step + */ +const ExtraConfiguration = (vnet) => { + const isUpdate = vnet?.NAME !== undefined + + return { + id: STEP_ID, + label: T.AdvancedOptions, + resolver: SCHEMA(isUpdate), + optionsValidate: { abortEarly: false }, + content: (formProps) => Content({ ...formProps, isUpdate }), + } +} + +Content.propTypes = { + data: PropTypes.any, + setFormData: PropTypes.func, + isUpdate: PropTypes.bool, +} + +export default ExtraConfiguration diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/index.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/index.js new file mode 100644 index 0000000000..636978e4c4 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/index.js @@ -0,0 +1,52 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import QoSIcon from 'iconoir-react/dist/DataTransferBoth' + +import { + STEP_ID as EXTRA_ID, + TabType, +} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration' +import { + SECTIONS, + FIELDS, +} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/schema' + +import FormWithSchema from 'client/components/Forms/FormWithSchema' +import { T } from 'client/constants' + +const QoSContent = () => ( + <> + {SECTIONS.map(({ id, ...section }) => ( + + ))} + +) + +/** @type {TabType} */ +const TAB = { + id: 'qos', + name: T.QoS, + icon: QoSIcon, + Content: QoSContent, + getError: (error) => FIELDS.some(({ name }) => error?.[name]), +} + +export default TAB diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/schema.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/schema.js new file mode 100644 index 0000000000..74d27ca318 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/schema.js @@ -0,0 +1,103 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ObjectSchema, string } from 'yup' + +import { Field, Section, getObjectSchemaFromFields } from 'client/utils' +import { T, INPUT_TYPES } from 'client/constants' + +const commonFieldProps = { + type: INPUT_TYPES.TEXT, + htmlType: 'number', + validation: string().trim().notRequired(), +} + +/** @type {Field} Inbound AVG Bandwidth field */ +const INBOUND_AVG_BW_FIELD = { + ...commonFieldProps, + name: 'INBOUND_AVG_BW', + label: T.AverageBandwidth, + tooltip: T.InboundAverageBandwidthConcept, +} + +/** @type {Field} Inbound Peak Bandwidth field */ +const INBOUND_PEAK_BW_FIELD = { + ...commonFieldProps, + name: 'INBOUND_PEAK_BW', + label: T.PeakBandwidth, + tooltip: T.InboundPeakBandwidthConcept, +} + +/** @type {Field} Inbound Peak Burst field */ +const INBOUND_PEAK_KB_FIELD = { + ...commonFieldProps, + name: 'INBOUND_PEAK_KB', + label: T.PeakBurst, + tooltip: T.PeakBurstConcept, +} + +/** @type {Field} Outbound AVG Bandwidth field */ +const OUTBOUND_AVG_BW_FIELD = { + ...commonFieldProps, + name: 'OUTBOUND_AVG_BW', + label: T.AverageBandwidth, + tooltip: T.OutboundAverageBandwidthConcept, +} + +/** @type {Field} Outbound Peak Bandwidth field */ +const OUTBOUND_PEAK_BW_FIELD = { + ...commonFieldProps, + name: 'OUTBOUND_PEAK_BW', + label: T.PeakBandwidth, + tooltip: T.OutboundPeakBandwidthConcept, +} + +/** @type {Field} Outbound Peak Burst field */ +const OUTBOUND_PEAK_KB_FIELD = { + ...commonFieldProps, + name: 'OUTBOUND_PEAK_KB', + label: T.PeakBurst, + tooltip: T.PeakBurstConcept, +} + +/** @type {Section[]} Sections */ +const SECTIONS = [ + { + id: 'qos-inbound', + legend: T.InboundTraffic, + fields: [ + INBOUND_AVG_BW_FIELD, + INBOUND_PEAK_BW_FIELD, + INBOUND_PEAK_KB_FIELD, + ], + }, + { + id: 'qos-outbound', + legend: T.OutboundTraffic, + fields: [ + OUTBOUND_AVG_BW_FIELD, + OUTBOUND_PEAK_BW_FIELD, + OUTBOUND_PEAK_KB_FIELD, + ], + }, +] + +/** @type {Field[]} List of QoS fields */ +const FIELDS = SECTIONS.map(({ fields }) => fields).flat() + +/** @type {ObjectSchema} QoS schema */ +const SCHEMA = getObjectSchemaFromFields(FIELDS) + +export { SCHEMA, SECTIONS, FIELDS } diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/schema.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/schema.js new file mode 100644 index 0000000000..01d31b2273 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/schema.js @@ -0,0 +1,55 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { array, object, ObjectSchema } from 'yup' + +import { SCHEMA as QOS_SCHEMA } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/qos/schema' +import { SCHEMA as CONTEXT_SCHEMA } from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/context/schema' + +/** + * Map name attribute if not exists. + * + * @param {string} prefixName - Prefix to add in name + * @returns {object[]} Resource object + */ +const mapNameByIndex = (prefixName) => (resource, idx) => ({ + ...resource, + NAME: + resource?.NAME?.startsWith(prefixName) || !resource?.NAME + ? `${prefixName}${idx}` + : resource?.NAME, +}) + +const AR_SCHEMA = object({ + AR: array() + .ensure() + .transform((actions) => actions.map(mapNameByIndex('AR'))), +}) + +/** + * @param {boolean} isUpdate - If `true`, the form is being updated + * @returns {ObjectSchema} Extra configuration schema + */ +export const SCHEMA = (isUpdate) => { + const schema = object({ SECURITY_GROUPS: array().ensure() }) + .concat(CONTEXT_SCHEMA) + .concat(QOS_SCHEMA) + + !isUpdate && schema.concat(AR_SCHEMA) + + return schema +} + +export { mapNameByIndex, AR_SCHEMA } diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/security.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/security.js new file mode 100644 index 0000000000..9c9ad00b66 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration/security.js @@ -0,0 +1,66 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +// import { Stack } from '@mui/material' +import { useFormContext, useWatch } from 'react-hook-form' +import SecurityIcon from 'iconoir-react/dist/HistoricShield' + +import { SecurityGroupsTable } from 'client/components/Tables' + +import { + STEP_ID as EXTRA_ID, + TabType, +} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration' +import { T } from 'client/constants' + +export const TAB_ID = 'SECURITY_GROUPS' + +const SecurityContent = () => { + const TAB_PATH = `${EXTRA_ID}.${TAB_ID}` + + const { setValue } = useFormContext() + const secGroups = useWatch({ name: TAB_PATH }) + + const selectedRowIds = secGroups?.reduce( + (res, id) => ({ ...res, [id]: true }), + {} + ) + + const handleSelectedRows = (rows) => { + const newValue = rows?.map((row) => row.original.ID) || [] + setValue(TAB_PATH, newValue) + } + + return ( + + ) +} + +/** @type {TabType} */ +const TAB = { + id: 'security', + name: T.Security, + icon: SecurityIcon, + Content: SecurityContent, + getError: (error) => !!error?.[TAB_ID], +} + +export default TAB diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/commonFields.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/commonFields.js new file mode 100644 index 0000000000..4f9e124d7f --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/commonFields.js @@ -0,0 +1,235 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { lazy, string, boolean, object } from 'yup' + +import { DRIVER_FIELD } from 'client/components/Forms/VNetwork/CreateForm/Steps/General/informationSchema' +import { Field, arrayToOptions } from 'client/utils' +import { T, INPUT_TYPES, VN_DRIVERS } from 'client/constants' + +const { + fw, + ebtables, + dot1Q, + vxlan, + ovswitch, + ovswitch_vxlan: openVSwitchVXLAN, +} = VN_DRIVERS + +/** @type {Field} Bridge linux field */ +const BRIDGE_FIELD = { + name: 'BRIDGE', + label: T.Bridge, + tooltip: T.BridgeConcept, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .notRequired() + .default(() => undefined), +} + +/** @type {Field} Physical device field */ +const PHYDEV_FIELD = { + name: 'PHYDEV', + label: T.PhysicalDevice, + tooltip: T.PhysicalDeviceConcept, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .default(() => undefined) + .when(DRIVER_FIELD.name, { + is: (driver) => [dot1Q, vxlan, openVSwitchVXLAN].includes(driver), + then: (schema) => schema.required(), + }), +} + +/** @type {Field} Filter MAC spoofing field */ +const FILTER_MAC_SPOOFING_FIELD = { + name: 'FILTER_MAC_SPOOFING', + label: T.MacSpoofingFilter, + type: INPUT_TYPES.SWITCH, + onlyOnHypervisors: [fw, ebtables], + validation: boolean().yesOrNo(), + grid: { md: 12 }, +} + +/** @type {Field} Filter IP spoofing field */ +const FILTER_IP_SPOOFING_FIELD = { + name: 'FILTER_IP_SPOOFING', + label: T.IpSpoofingFilter, + type: INPUT_TYPES.SWITCH, + onlyOnHypervisors: [fw, ebtables], + validation: boolean().yesOrNo(), + grid: { md: 12 }, +} + +/** @type {Field} MTU field */ +const MTU_FIELD = { + name: 'MTU', + label: T.MTU, + tooltip: T.MTUConcept, + onlyOnHypervisors: [dot1Q, vxlan, ovswitch, openVSwitchVXLAN], + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .default(() => undefined), +} + +/** @type {Field} Automatic VLAN field */ +const AUTOMATIC_VLAN_FIELD = { + name: 'AUTOMATIC_VLAN_ID', + label: T.AutomaticVlanId, + dependOf: 'AUTOMATIC_VLAN_ID', + type: INPUT_TYPES.SWITCH, + onlyOnHypervisors: [dot1Q, vxlan, ovswitch, openVSwitchVXLAN], + validation: lazy((_, { context }) => + boolean() + .yesOrNo() + .default(() => context?.AUTOMATIC_VLAN_ID === '1') + ), + grid: (self) => (self ? { md: 12 } : { sm: 6 }), +} + +/** @type {Field} VLAN ID field */ +const VLAN_ID_FIELD = { + name: 'VLAN_ID', + label: T.VlanId, + type: INPUT_TYPES.TEXT, + onlyOnHypervisors: [dot1Q, vxlan, ovswitch, openVSwitchVXLAN], + dependOf: AUTOMATIC_VLAN_FIELD.name, + htmlType: (automatic) => automatic && INPUT_TYPES.HIDDEN, + validation: string() + .trim() + .default(() => undefined) + .when(AUTOMATIC_VLAN_FIELD.name, { + is: (automatic) => !automatic, + then: (schema) => schema.required(), + }), + grid: { sm: 6 }, +} + +/** @type {Field} Automatic Outer VLAN field */ +const AUTOMATIC_OUTER_VLAN_ID_FIELD = { + name: 'AUTOMATIC_OUTER_VLAN_ID', + label: T.AutomaticOuterVlanId, + type: INPUT_TYPES.SWITCH, + onlyOnHypervisors: [openVSwitchVXLAN], + validation: lazy((_, { context }) => + boolean() + .yesOrNo() + .default(() => context?.AUTOMATIC_OUTER_VLAN_ID === '1') + ), + dependOf: 'AUTOMATIC_OUTER_VLAN_ID', + grid: (self) => (self ? { md: 12 } : { sm: 6 }), +} + +/** @type {Field} Outer VLAN ID field */ +const OUTER_VLAN_ID_FIELD = { + name: 'OUTER_VLAN_ID', + label: T.OuterVlanId, + type: INPUT_TYPES.TEXT, + onlyOnHypervisors: [openVSwitchVXLAN], + dependOf: AUTOMATIC_OUTER_VLAN_ID_FIELD.name, + htmlType: (oAutomatic) => oAutomatic && INPUT_TYPES.HIDDEN, + validation: string() + .trim() + .default(() => undefined) + .when(AUTOMATIC_OUTER_VLAN_ID_FIELD.name, { + is: (oAutomatic) => !oAutomatic, + then: (schema) => schema.required(), + }), + grid: { sm: 6 }, +} + +/** @type {Field} VXLAN mode field */ +const VXLAN_MODE_FIELD = { + name: 'VXLAN_MODE', + label: T.VxlanMode, + tooltip: T.VxlanModeConcept, + type: INPUT_TYPES.SELECT, + onlyOnHypervisors: [vxlan], + values: arrayToOptions(['evpn', 'multicast']), + validation: string() + .trim() + .default(() => undefined), +} + +/** @type {Field} VXLAN tunnel endpoint field */ +const VXLAN_TEP_FIELD = { + name: 'VXLAN_TEP', + label: T.VxlanTunnelEndpoint, + tooltip: T.VxlanTunnelEndpointConcept, + type: INPUT_TYPES.SELECT, + onlyOnHypervisors: [vxlan], + dependOf: VXLAN_MODE_FIELD.name, + htmlType: (mode) => mode !== 'evpn' && INPUT_TYPES.HIDDEN, + values: arrayToOptions(['dev', 'local_ip']), + validation: string() + .trim() + .default(() => undefined) + .when(VXLAN_MODE_FIELD.name, { + is: (mode) => mode !== 'evpn', + then: (schema) => schema.strip(), + }), +} + +/** @type {Field} VXLAN multicast field */ +const VXLAN_MC_FIELD = { + name: 'VXLAN_MC', + label: T.VxlanMulticast, + tooltip: T.VxlanMulticastConcept, + type: INPUT_TYPES.TEXT, + onlyOnHypervisors: [vxlan], + dependOf: VXLAN_MODE_FIELD.name, + htmlType: (mode) => mode !== 'multicast' && INPUT_TYPES.HIDDEN, + validation: string() + .trim() + .default(() => undefined) + .when(VXLAN_MODE_FIELD.name, { + is: (mode) => mode !== 'multicast', + then: (schema) => schema.strip(), + }), +} + +/** @type {Field[]} List of common fields */ +export const FIELDS = [ + BRIDGE_FIELD, + PHYDEV_FIELD, + + FILTER_MAC_SPOOFING_FIELD, + FILTER_IP_SPOOFING_FIELD, + + AUTOMATIC_VLAN_FIELD, + VLAN_ID_FIELD, + AUTOMATIC_OUTER_VLAN_ID_FIELD, + OUTER_VLAN_ID_FIELD, + + MTU_FIELD, + VXLAN_MODE_FIELD, + VXLAN_TEP_FIELD, + VXLAN_MC_FIELD, +] + +export const IP_LINK_CONF_FIELD = { + name: 'IP_LINK_CONF', + validation: object().afterSubmit((conf, { parent }) => { + if (vxlan !== parent[DRIVER_FIELD.name]) return + + // => string result: IP_LINK_CONF="option1=value1,option2=,..." + return Object.entries(conf || {}) + .map(([k, v]) => `${k}=${v}`) + .join(',') + }), +} diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/index.js new file mode 100644 index 0000000000..ac6be8f4ef --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/index.js @@ -0,0 +1,113 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement, useMemo } from 'react' +import PropTypes from 'prop-types' +import { useFormContext, useWatch } from 'react-hook-form' + +import FormWithSchema from 'client/components/Forms/FormWithSchema' +import { Legend } from 'client/components/Forms' +import { AttributePanel } from 'client/components/Tabs/Common' + +import { + SCHEMA, + SECTIONS, + IP_LINK_CONF_FIELD, +} from 'client/components/Forms/VNetwork/CreateForm/Steps/General/schema' +import { cleanEmpty, cloneObject, set } from 'client/utils' +import { T, VirtualNetwork, VN_DRIVERS } from 'client/constants' + +export const STEP_ID = 'general' +const DRIVER_PATH = `${STEP_ID}.VN_MAD` +const IP_CONF_PATH = `${STEP_ID}.${IP_LINK_CONF_FIELD.name}` + +/** + * @param {object} props - Props + * @param {boolean} [props.isUpdate] - Is `true` the form will be filter immutable attributes + * @returns {ReactElement} Form content component + */ +const Content = ({ isUpdate }) => { + const { setValue } = useFormContext() + + const driver = useWatch({ name: DRIVER_PATH }) + const ipConf = useWatch({ name: IP_CONF_PATH }) || {} + + const sections = useMemo(() => SECTIONS(driver, isUpdate), [driver]) + + const handleChangeAttribute = (path, newValue) => { + const newConf = cloneObject(ipConf) + + set(newConf, path, newValue) + setValue(IP_CONF_PATH, cleanEmpty(newConf)) + } + + return ( + <> + {sections.map(({ id, ...section }) => ( + + ))} + {driver === VN_DRIVERS.vxlan && ( + + } + allActionsEnabled + handleAdd={handleChangeAttribute} + handleEdit={handleChangeAttribute} + handleDelete={handleChangeAttribute} + attributes={ipConf} + filtersSpecialAttributes={false} + /> + )} + + ) +} + +/** + * General configuration about Virtual network. + * + * @param {VirtualNetwork} vnet - Virtual network + * @returns {object} General configuration step + */ +const General = (vnet) => { + const isUpdate = vnet?.NAME !== undefined + const initialDriver = vnet?.VN_MAD + + return { + id: STEP_ID, + label: T.General, + resolver: (formData) => + SCHEMA(formData?.[STEP_ID]?.VN_MAD ?? initialDriver, isUpdate), + optionsValidate: { abortEarly: false }, + content: () => Content({ isUpdate }), + } +} + +Content.propTypes = { + isUpdate: PropTypes.bool, +} + +export default General diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/informationSchema.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/informationSchema.js new file mode 100644 index 0000000000..c174b8ec66 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/informationSchema.js @@ -0,0 +1,99 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { string } from 'yup' + +import { useGetClustersQuery } from 'client/features/OneApi/cluster' +import { OPTION_SORTERS, Field, arrayToOptions } from 'client/utils' +import { T, INPUT_TYPES, VN_DRIVERS_STR } from 'client/constants' + +/** + * @param {boolean} isUpdate - If `true`, the form is being updated + * @returns {Field} Name field + */ +export const NAME_FIELD = (isUpdate) => ({ + name: 'NAME', + label: T.Name, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .required() + .default(() => undefined) + // if the form is updating then display the name but not change it + .afterSubmit((name) => (isUpdate ? undefined : name)), + ...(isUpdate && { fieldProps: { disabled: true } }), +}) + +/** @type {Field} Description field */ +export const DESCRIPTION_FIELD = { + name: 'DESCRIPTION', + label: T.Description, + type: INPUT_TYPES.TEXT, + multiline: true, + validation: string() + .trim() + .notRequired() + .default(() => undefined), + grid: { md: 12 }, +} + +/** @type {Field} Cluster field */ +export const CLUSTER_FIELD = { + name: 'CLUSTER', + label: T.Cluster, + type: INPUT_TYPES.AUTOCOMPLETE, + values: () => { + const { data: clusters = [] } = useGetClustersQuery() + + return arrayToOptions(clusters, { + addEmpty: false, + getText: ({ ID, NAME }) => `#${ID} ${NAME}`, + getValue: ({ ID }) => ID, + sorter: OPTION_SORTERS.numeric, + }) + }, + validation: string() + .trim() + .notRequired() + .default(() => undefined), +} + +const drivers = Object.keys(VN_DRIVERS_STR) + +/** @type {Field} Driver field */ +export const DRIVER_FIELD = { + name: 'VN_MAD', + type: INPUT_TYPES.TOGGLE, + values: () => + arrayToOptions(drivers, { + addEmpty: false, + getText: (key) => VN_DRIVERS_STR[key], + sorter: OPTION_SORTERS.unsort, + }), + validation: string() + .trim() + .required() + .default(() => drivers[0]), + grid: { md: 12 }, +} + +/** + * @param {boolean} isUpdate - If `true`, the form is being updated + * @returns {Field[]} List of information fields + */ +export const FIELDS = (isUpdate) => + [NAME_FIELD(isUpdate), !isUpdate && CLUSTER_FIELD, DESCRIPTION_FIELD].filter( + Boolean + ) diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/schema.js new file mode 100644 index 0000000000..395ae3838b --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/General/schema.js @@ -0,0 +1,61 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { BaseSchema, object } from 'yup' + +import { DRIVER_FIELD, FIELDS as INFORMATION_FIELDS } from './informationSchema' +import { FIELDS as FIELDS_BY_DRIVER, IP_LINK_CONF_FIELD } from './commonFields' + +import { + Section, + getObjectSchemaFromFields, + filterFieldsByHypervisor, +} from 'client/utils' +import { T, VN_DRIVERS } from 'client/constants' + +/** + * @param {VN_DRIVERS} driver - Virtual network driver + * @param {boolean} [isUpdate] - If `true`, the form is being updated + * @returns {Section[]} Fields + */ +const SECTIONS = (driver, isUpdate) => [ + { + id: 'information', + legend: T.Information, + fields: INFORMATION_FIELDS(isUpdate), + }, + { + id: 'configuration', + legend: T.Configuration, + fields: filterFieldsByHypervisor( + [DRIVER_FIELD, ...FIELDS_BY_DRIVER], + driver + ), + }, +] + +/** + * @param {VN_DRIVERS} driver - Virtual network driver + * @param {boolean} [isUpdate] - If `true`, the form is being updated + * @returns {BaseSchema} Step schema + */ +const SCHEMA = (driver, isUpdate) => + getObjectSchemaFromFields( + SECTIONS(driver, isUpdate) + .map(({ schema, fields }) => schema ?? fields) + .flat() + ).concat(object({ [IP_LINK_CONF_FIELD.name]: IP_LINK_CONF_FIELD.validation })) + +export { SECTIONS, SCHEMA, IP_LINK_CONF_FIELD } diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/index.js new file mode 100644 index 0000000000..8285130cd5 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/Steps/index.js @@ -0,0 +1,84 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { reach, ObjectSchema } from 'yup' + +import General, { + STEP_ID as GENERAL_ID, +} from 'client/components/Forms/VNetwork/CreateForm/Steps/General' +import ExtraConfiguration, { + STEP_ID as EXTRA_ID, +} from 'client/components/Forms/VNetwork/CreateForm/Steps/ExtraConfiguration' + +import { jsonToXml } from 'client/models/Helper' +import { createSteps } from 'client/utils' + +const existsOnSchema = (schema, key) => { + try { + return reach(schema, key) && true + } catch (e) { + return false + } +} + +/** + * @param {object} fromAttributes - Attributes to check + * @param {ObjectSchema} schema - Current form schema + * @returns {object} List of unknown attributes + */ +export const getUnknownVars = (fromAttributes = {}, schema) => { + const unknown = {} + + for (const [key, value] of Object.entries(fromAttributes)) { + if ( + !!value && + !existsOnSchema(schema, `${GENERAL_ID}.${key}`) && + !existsOnSchema(schema, `${EXTRA_ID}.${key}`) + ) { + // When the path is not found, it means that + // the attribute is correct and we can set it + unknown[key] = value + } + } + + return unknown +} + +const Steps = createSteps([General, ExtraConfiguration], { + transformInitialValue: ({ TEMPLATE, AR_POOL, ...vnet } = {}, schema) => { + const initialValue = schema.cast( + { + [GENERAL_ID]: { ...vnet }, + [EXTRA_ID]: { ...TEMPLATE, AR: AR_POOL.AR, ...vnet }, + }, + { stripUnknown: true, context: vnet } + ) + + initialValue[EXTRA_ID] = { + ...getUnknownVars(TEMPLATE, schema), + ...initialValue[EXTRA_ID], + } + + return initialValue + }, + transformBeforeSubmit: (formData) => { + const { [GENERAL_ID]: general = {}, [EXTRA_ID]: extra = {} } = + formData ?? {} + + return jsonToXml({ ...extra, ...general }) + }, +}) + +export default Steps diff --git a/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/index.js b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/index.js new file mode 100644 index 0000000000..13aa1307a8 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/CreateForm/index.js @@ -0,0 +1,16 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +export { default } from 'client/components/Forms/VNetwork/CreateForm/Steps' diff --git a/src/fireedge/src/client/components/Forms/VNetwork/ReserveForm/index.js b/src/fireedge/src/client/components/Forms/VNetwork/ReserveForm/index.js new file mode 100644 index 0000000000..59a35c368a --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/ReserveForm/index.js @@ -0,0 +1,27 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { + SCHEMA, + FIELDS, +} from 'client/components/Forms/VNetwork/ReserveForm/schema' +import { jsonToXml } from 'client/models/Helper' +import { createForm } from 'client/utils' + +const AddRangeForm = createForm(SCHEMA, FIELDS, { + transformBeforeSubmit: (formData) => jsonToXml({ ...formData }), +}) + +export default AddRangeForm diff --git a/src/fireedge/src/client/components/Forms/VNetwork/ReserveForm/schema.js b/src/fireedge/src/client/components/Forms/VNetwork/ReserveForm/schema.js new file mode 100644 index 0000000000..52f1c5dcb3 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/ReserveForm/schema.js @@ -0,0 +1,187 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { BaseSchema, string, number } from 'yup' + +import { useGetVNetworksQuery } from 'client/features/OneApi/network' +import { getAddressType } from 'client/models/VirtualNetwork' +import { + Field, + getObjectSchemaFromFields, + OPTION_SORTERS, + arrayToOptions, + REG_ADDR, +} from 'client/utils' +import { T, INPUT_TYPES, VirtualNetwork, AddressRange } from 'client/constants' + +const SWITCH_TYPES = { newVnet: 'vnet', fromAr: 'ar' } + +const SWITCH_STR = { + [SWITCH_TYPES.newVnet]: T.AddToNewVirtualNetwork, + [SWITCH_TYPES.fromAr]: T.AddToExistingReservation, +} + +/** @type {Field} Number of addresses field */ +const SIZE_FIELD = { + name: 'SIZE', + label: T.NumberOfAddresses, + type: INPUT_TYPES.TEXT, + htmlType: 'number', + validation: number() + .positive() + .required() + .default(() => 1), +} + +/** @type {Field} Switcher for vnet OR existing reservation */ +const SWITCH_FIELD = { + name: '__SWITCH__', + type: INPUT_TYPES.TOGGLE, + values: () => + arrayToOptions(Object.entries(SWITCH_STR), { + addEmpty: false, + getText: ([, text]) => text, + getValue: ([value]) => value, + }), + validation: string() + .trim() + .required() + .default(() => SWITCH_TYPES.newVnet) + .afterSubmit(() => undefined), + grid: { sm: 12, md: 12 }, + notNull: true, +} + +/** @type {Field} Name of the new virtual network */ +const NAME_FIELD = { + name: 'NAME', + label: T.Name, + type: INPUT_TYPES.TEXT, + dependOf: SWITCH_FIELD.name, + htmlType: (switcher) => + switcher === SWITCH_TYPES.fromAr && INPUT_TYPES.HIDDEN, + validation: string() + .trim() + .default(() => undefined) + .when(SWITCH_FIELD.name, { + is: SWITCH_TYPES.fromAr, + then: (schema) => schema.strip(), + otherwise: (schema) => schema.required(), + }), +} + +/** + * @param {object} stepProps - Step props + * @param {VirtualNetwork} stepProps.vnet - Virtual Network + * @returns {Field} Add to an existing reservation + */ +const EXISTING_RESERVE_FIELD = ({ vnet = {} }) => ({ + name: 'NETWORK_ID', + label: T.SelectNetwork, + type: INPUT_TYPES.SELECT, + dependOf: SWITCH_FIELD.name, + htmlType: (switcher) => + switcher === SWITCH_TYPES.newVnet && INPUT_TYPES.HIDDEN, + values: () => { + const { data: reservedVNets } = useGetVNetworksQuery(undefined, { + selectFromResult: ({ data: result = [] }) => ({ + data: result?.filter((vn) => +vn?.PARENT_NETWORK_ID === +vnet.ID), + }), + }) + + return arrayToOptions(reservedVNets, { + getText: ({ ID, NAME }) => `#${ID} ${NAME}`, + getValue: ({ ID }) => ID, + sorter: OPTION_SORTERS.numeric, + }) + }, + validation: string() + .trim() + .default(() => undefined) + .when(SWITCH_FIELD.name, { + is: SWITCH_TYPES.newVnet, + then: (schema) => schema.strip(), + otherwise: (schema) => schema.required(), + }), +}) + +/** + * @param {object} stepProps - Step props + * @param {AddressRange[]} stepProps.arPool - Address Ranges + * @returns {Field} Add to an existing reservation + */ +const AR_FIELD = ({ arPool = {} }) => ({ + name: 'AR_ID', + label: T.CanSelectAddressFromAR, + type: INPUT_TYPES.SELECT, + dependOf: SWITCH_FIELD.name, + values: arrayToOptions(arPool, { + getText: ({ AR_ID, IP, MAC }) => + `#${AR_ID} ${IP ? 'IP' : 'MAC'} range: ${IP ?? MAC}`, + getValue: ({ AR_ID }) => AR_ID, + sorter: OPTION_SORTERS.numeric, + }), + validation: string() + .trim() + .default(() => undefined), +}) + +/** @type {Field} First address field */ +const ADDR_FIELD = { + name: 'ADDR', + label: T.FirstAddress, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .notRequired() + .matches(REG_ADDR, { message: T.InvalidAddress }) + .default(() => undefined), + fieldProps: { placeholder: T.IpOrMac }, +} + +/** + * @param {object} stepProps - Step props + * @param {VirtualNetwork} stepProps.vnet - Virtual network + * @returns {Field[]} Fields + */ +const FIELDS = (stepProps) => { + const arPool = [stepProps?.vnet?.AR_POOL?.AR ?? []].flat() + + return [ + SWITCH_FIELD, + SIZE_FIELD, + NAME_FIELD, + EXISTING_RESERVE_FIELD(stepProps), + arPool.length > 0 && AR_FIELD({ arPool }), + ADDR_FIELD, + ].filter(Boolean) +} + +/** + * @param {object} stepProps - Step props + * @param {VirtualNetwork} stepProps.vnet - Virtual network + * @returns {BaseSchema} Schema + */ +const SCHEMA = (stepProps) => + getObjectSchemaFromFields([...FIELDS(stepProps)]).afterSubmit( + ({ [ADDR_FIELD.name]: addr, ...result }) => { + const addrType = getAddressType(addr) + addrType && (result[addrType] = addr) + + return result + } + ) + +export { FIELDS, SCHEMA } diff --git a/src/fireedge/src/client/components/Forms/VNetwork/index.js b/src/fireedge/src/client/components/Forms/VNetwork/index.js new file mode 100644 index 0000000000..1cfbd0337b --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VNetwork/index.js @@ -0,0 +1,41 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement } from 'react' +import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC' +import { CreateStepsCallback, CreateFormCallback } from 'client/utils/schema' + +/** + * @param {ConfigurationProps} configProps - Configuration + * @returns {ReactElement|CreateFormCallback} Asynchronous loaded form + */ +const AddRangeForm = (configProps) => + AsyncLoadForm({ formPath: 'VNetwork/AddRangeForm' }, configProps) + +/** + * @param {ConfigurationProps} configProps - Configuration + * @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form + */ +const CreateForm = (configProps) => + AsyncLoadForm({ formPath: 'VNetwork/CreateForm' }, configProps) + +/** + * @param {ConfigurationProps} configProps - Configuration + * @returns {ReactElement|CreateFormCallback} Asynchronous loaded form + */ +const ReserveForm = (configProps) => + AsyncLoadForm({ formPath: 'VNetwork/ReserveForm' }, configProps) + +export { AddRangeForm, CreateForm, ReserveForm } diff --git a/src/fireedge/src/client/components/Tables/Clusters/index.js b/src/fireedge/src/client/components/Tables/Clusters/index.js index 4a09e10113..50912bb7ef 100644 --- a/src/fireedge/src/client/components/Tables/Clusters/index.js +++ b/src/fireedge/src/client/components/Tables/Clusters/index.js @@ -30,12 +30,17 @@ const DEFAULT_DATA_CY = 'clusters' * @returns {ReactElement} Clusters table */ const ClustersTable = (props) => { - const { rootProps = {}, searchProps = {}, ...rest } = props ?? {} + const { + rootProps = {}, + searchProps = {}, + useQuery = useGetClustersQuery, + ...rest + } = props ?? {} rootProps['data-cy'] ??= DEFAULT_DATA_CY searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}` const { view, getResourceView } = useViews() - const { data = [], isFetching, refetch } = useGetClustersQuery() + const { data = [], isFetching, refetch } = useQuery() const columns = useMemo( () => diff --git a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/index.js b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/index.js index 367145f16d..efa94b5eb3 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/index.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/index.js @@ -18,12 +18,7 @@ import PropTypes from 'prop-types' import { Stack, Checkbox } from '@mui/material' import { RefreshDouble } from 'iconoir-react' -import { - UseTableInstanceProps, - UseRowSelectState, - UseFiltersInstanceProps, - UseRowSelectInstanceProps, -} from 'react-table' +import { UseTableInstanceProps, UseRowSelectInstanceProps } from 'react-table' import { Action, @@ -44,6 +39,7 @@ import { T } from 'client/constants' * @param {boolean} props.disableRowSelect - Rows can't select * @param {GlobalAction[]} props.globalActions - Possible bulk actions * @param {UseTableInstanceProps} props.useTableProps - Table props + * @param {object[]} props.selectedRows - Selected Rows * @returns {ReactElement} Component JSX with all actions */ const GlobalActions = ({ @@ -53,20 +49,13 @@ const GlobalActions = ({ singleSelect = false, disableRowSelect = false, globalActions = [], + selectedRows, useTableProps = {}, }) => { /** @type {UseRowSelectInstanceProps} */ const { getToggleAllPageRowsSelectedProps, getToggleAllRowsSelectedProps } = useTableProps - /** @type {UseFiltersInstanceProps} */ - const { preFilteredRows } = useTableProps - - /** @type {UseRowSelectState} */ - const { selectedRowIds } = useTableProps?.state - - const selectedRows = preFilteredRows.filter((row) => !!selectedRowIds[row.id]) - return ( )} {!singleSelect && !disableRowSelect && ( - <> - - {globalActions?.map((item, idx) => { - const key = item.accessor ?? item.label ?? item.tooltip ?? idx - - return - })} - + )} + {globalActions?.map((item, idx) => { + if ((singleSelect || disableRowSelect) && item.selected) return null + + const key = item.accessor ?? item.label ?? item.tooltip ?? idx + + return + })} ) } @@ -111,6 +100,7 @@ GlobalActions.propTypes = { disableRowSelect: PropTypes.bool, globalActions: PropTypes.array, useTableProps: PropTypes.object, + selectedRows: PropTypes.array, } export default GlobalActions diff --git a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalLabel/index.js b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalLabel/index.js index 78bd97083e..fcc3fd0012 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalLabel/index.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalLabel/index.js @@ -57,7 +57,7 @@ const sortByFilteredFirst = (labels, filters) => * Button to filter rows by label or assign labels to selected rows. * * @param {UseFiltersInstanceProps} props - Component props - * @param {object} props.selectedRows - Selected rows + * @param {object[]} props.selectedRows - Selected rows * @param {Function} props.useUpdateMutation - Callback to update row labels * @returns {ReactElement} Button component */ diff --git a/src/fireedge/src/client/components/Tables/Enhanced/index.js b/src/fireedge/src/client/components/Tables/Enhanced/index.js index e8842030d6..8b271c3a68 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/index.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/index.js @@ -199,6 +199,7 @@ const EnhancedTable = ({ singleSelect={singleSelect} disableRowSelect={disableRowSelect} globalActions={globalActions} + selectedRows={selectedRows} useTableProps={useTableProps} /> diff --git a/src/fireedge/src/client/components/Tables/Hosts/actions.js b/src/fireedge/src/client/components/Tables/Hosts/actions.js index dcbe7b42bd..ddbcd89eba 100644 --- a/src/fireedge/src/client/components/Tables/Hosts/actions.js +++ b/src/fireedge/src/client/components/Tables/Hosts/actions.js @@ -27,7 +27,7 @@ import { } from 'client/features/OneApi/host' import { Translate } from 'client/components/HOC' -import { ChangeClusterForm } from 'client/components/Forms/Host' +import { ChangeClusterForm } from 'client/components/Forms/Cluster' import { createActions, GlobalAction, diff --git a/src/fireedge/src/client/components/Tables/SecurityGroups/index.js b/src/fireedge/src/client/components/Tables/SecurityGroups/index.js index 3a84b3f533..999c353f4d 100644 --- a/src/fireedge/src/client/components/Tables/SecurityGroups/index.js +++ b/src/fireedge/src/client/components/Tables/SecurityGroups/index.js @@ -30,12 +30,17 @@ const DEFAULT_DATA_CY = 'secgroup' * @returns {ReactElement} Security Groups table */ const SecurityGroupsTable = (props) => { - const { rootProps = {}, searchProps = {}, ...rest } = props ?? {} + const { + rootProps = {}, + searchProps = {}, + useQuery = useGetSecGroupsQuery, + ...rest + } = props ?? {} rootProps['data-cy'] ??= DEFAULT_DATA_CY searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}` const { view, getResourceView } = useViews() - const { data = [], isFetching, refetch } = useGetSecGroupsQuery() + const { data = [], isFetching, refetch } = useQuery() const columns = useMemo( () => diff --git a/src/fireedge/src/client/components/Tables/VNetworks/actions.js b/src/fireedge/src/client/components/Tables/VNetworks/actions.js new file mode 100644 index 0000000000..0b2b84aa2c --- /dev/null +++ b/src/fireedge/src/client/components/Tables/VNetworks/actions.js @@ -0,0 +1,333 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { useMemo } from 'react' +import { useHistory } from 'react-router-dom' +import { + AddCircledOutline, + // Import, + Trash, + PlayOutline, + Lock, + Group, +} from 'iconoir-react' +import { Typography } from '@mui/material' +import { makeStyles } from '@mui/styles' + +import { useViews } from 'client/features/Auth' +import { useAddNetworkToClusterMutation } from 'client/features/OneApi/cluster' +import { + useReserveAddressMutation, + useLockVNetMutation, + useUnlockVNetMutation, + useChangeVNetOwnershipMutation, + useRemoveVNetMutation, +} from 'client/features/OneApi/network' + +import { ChangeUserForm, ChangeGroupForm } from 'client/components/Forms/Vm' +import { ReserveForm } from 'client/components/Forms/VNetwork' +import { ChangeClusterForm } from 'client/components/Forms/Cluster' +import { + createActions, + GlobalAction, +} from 'client/components/Tables/Enhanced/Utils' +import VNetworkTemplatesTable from 'client/components/Tables/VNetworkTemplates' +import { Translate } from 'client/components/HOC' + +import { PATH } from 'client/apps/sunstone/routesOne' +import { T, VN_ACTIONS, RESOURCE_NAMES } from 'client/constants' + +const useTableStyles = makeStyles({ + body: { gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))' }, +}) + +const ListNames = ({ rows = [] }) => + rows?.map?.(({ id, original }) => { + const { ID, NAME } = original + + return ( + + {`#${ID} ${NAME}`} + + ) + }) + +const SubHeader = (rows) => + +const MessageToConfirmAction = (rows) => { + const names = rows?.map?.(({ original }) => original?.NAME) + + return ( + <> +

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

+

+ +

+ + ) +} + +MessageToConfirmAction.displayName = 'MessageToConfirmAction' + +/** + * Generates the actions to operate resources on Virtual networks table. + * + * @returns {GlobalAction} - Actions + */ +const Actions = () => { + const history = useHistory() + const { view, getResourceView } = useViews() + const [reserve] = useReserveAddressMutation() + const [changeCluster] = useAddNetworkToClusterMutation() + const [lock] = useLockVNetMutation() + const [unlock] = useUnlockVNetMutation() + const [changeOwnership] = useChangeVNetOwnershipMutation() + const [remove] = useRemoveVNetMutation() + + const actions = useMemo( + () => + createActions({ + filters: getResourceView(RESOURCE_NAMES.VNET)?.actions, + actions: [ + { + accessor: VN_ACTIONS.CREATE_DIALOG, + dataCy: `vnet-${VN_ACTIONS.CREATE_DIALOG}`, + tooltip: T.Create, + icon: AddCircledOutline, + action: () => history.push(PATH.NETWORK.VNETS.CREATE), + }, + /* { + // TODO: Import Virtual Network from vCenter + accessor: VN_ACTIONS.IMPORT_DIALOG, + tooltip: T.Import, + icon: Import, + selected: { max: 1 }, + disabled: true, + action: (rows) => { + // TODO: go to IMPORT form + }, + }, */ + { + accessor: VN_ACTIONS.INSTANTIATE_DIALOG, + dataCy: `vnet-${VN_ACTIONS.INSTANTIATE_DIALOG}`, + tooltip: T.Instantiate, + selected: true, + icon: PlayOutline, + options: [ + { + isConfirmDialog: true, + dialogProps: { + title: T.Instantiate, + children: () => { + const classes = useTableStyles() + const path = PATH.NETWORK.VN_TEMPLATES.INSTANTIATE + + return ( + history.push(path, vnet)} + /> + ) + }, + fixedWidth: true, + fixedHeight: true, + handleAccept: undefined, + dataCy: `modal-${VN_ACTIONS.CREATE_DIALOG}`, + }, + }, + ], + }, + { + accessor: VN_ACTIONS.UPDATE_DIALOG, + dataCy: `vnet-${VN_ACTIONS.UPDATE_DIALOG}`, + label: T.Update, + tooltip: T.Update, + selected: { max: 1 }, + color: 'secondary', + action: (rows) => { + const vnet = rows?.[0]?.original ?? {} + const path = PATH.NETWORK.VNETS.CREATE + + history.push(path, vnet) + }, + }, + { + accessor: VN_ACTIONS.RESERVE_DIALOG, + dataCy: `vnet-${VN_ACTIONS.RESERVE_DIALOG}`, + label: T.Reserve, + selected: { max: 1 }, + color: 'secondary', + options: [ + { + dialogProps: { + title: T.ReservationFromVirtualNetwork, + dataCy: 'modal-reserve', + }, + form: (rows) => { + const vnet = rows?.[0]?.original || {} + + return ReserveForm({ stepProps: { vnet } }) + }, + onSubmit: (rows) => async (template) => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all(ids.map((id) => reserve({ id, template }))) + }, + }, + ], + }, + { + accessor: VN_ACTIONS.CHANGE_CLUSTER, + color: 'secondary', + dataCy: `vnet-${VN_ACTIONS.CHANGE_CLUSTER}`, + label: T.SelectCluster, + tooltip: T.SelectCluster, + selected: true, + options: [ + { + dialogProps: { + title: T.SelectCluster, + dataCy: 'modal-select-cluster', + }, + form: () => ChangeClusterForm(), + onSubmit: (rows) => async (formData) => { + const ids = rows?.map?.(({ original }) => original?.ID) + + await Promise.all( + ids.map((id) => + changeCluster({ id: formData.cluster, vnet: id }) + ) + ) + }, + }, + ], + }, + { + tooltip: T.Ownership, + icon: Group, + selected: true, + color: 'secondary', + dataCy: 'vnet-ownership', + options: [ + { + accessor: VN_ACTIONS.CHANGE_OWNER, + name: T.ChangeOwner, + dialogProps: { + title: T.ChangeOwner, + subheader: SubHeader, + dataCy: `modal-${VN_ACTIONS.CHANGE_OWNER}`, + }, + form: ChangeUserForm, + onSubmit: (rows) => (newOwnership) => { + rows?.map?.(({ original }) => + changeOwnership({ id: original?.ID, ...newOwnership }) + ) + }, + }, + { + accessor: VN_ACTIONS.CHANGE_GROUP, + name: T.ChangeGroup, + dialogProps: { + title: T.ChangeGroup, + subheader: SubHeader, + dataCy: `modal-${VN_ACTIONS.CHANGE_GROUP}`, + }, + form: ChangeGroupForm, + onSubmit: (rows) => async (newOwnership) => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all( + ids.map((id) => changeOwnership({ id, ...newOwnership })) + ) + }, + }, + ], + }, + { + tooltip: T.Lock, + icon: Lock, + selected: true, + color: 'secondary', + dataCy: 'vnet-lock', + options: [ + { + accessor: VN_ACTIONS.LOCK, + name: T.Lock, + isConfirmDialog: true, + dialogProps: { + title: T.Lock, + dataCy: `modal-${VN_ACTIONS.LOCK}`, + children: MessageToConfirmAction, + }, + onSubmit: (rows) => async () => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all(ids.map((id) => lock({ id }))) + }, + }, + { + accessor: VN_ACTIONS.UNLOCK, + name: T.Unlock, + isConfirmDialog: true, + dialogProps: { + title: T.Unlock, + dataCy: `modal-${VN_ACTIONS.UNLOCK}`, + children: MessageToConfirmAction, + }, + onSubmit: (rows) => async () => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all(ids.map((id) => unlock({ id }))) + }, + }, + ], + }, + { + accessor: VN_ACTIONS.DELETE, + dataCy: `vnet-${VN_ACTIONS.DELETE}`, + tooltip: T.Delete, + icon: Trash, + selected: true, + color: 'error', + options: [ + { + isConfirmDialog: true, + dialogProps: { + title: T.Delete, + children: MessageToConfirmAction, + dataCy: `modal-vnet-${VN_ACTIONS.DELETE}`, + }, + onSubmit: (rows) => async () => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all(ids.map((id) => remove({ id }))) + }, + }, + ], + }, + ], + }), + [view] + ) + + return actions +} + +export default Actions diff --git a/src/fireedge/src/client/components/Tables/VNetworks/columns.js b/src/fireedge/src/client/components/Tables/VNetworks/columns.js index bc541a7a70..216a1bce35 100644 --- a/src/fireedge/src/client/components/Tables/VNetworks/columns.js +++ b/src/fireedge/src/client/components/Tables/VNetworks/columns.js @@ -15,62 +15,31 @@ * ------------------------------------------------------------------------- */ import { Column } from 'react-table' -import { - getState, - getTotalLeases, - getVNManager, -} from 'client/models/VirtualNetwork' +import { getState, getVNManager } from 'client/models/VirtualNetwork' import { T } from 'client/constants' -const getTotalOfResources = (resources) => - [resources?.ID ?? []].flat().length || 0 - -/** @type {Column[]} VM columns */ +/** @type {Column[]} Virtual Network columns */ const COLUMNS = [ { Header: T.ID, id: 'id', accessor: 'ID', sortType: 'number' }, { Header: T.Name, id: 'name', accessor: 'NAME' }, - { - Header: T.State, - id: 'state', - accessor: (row) => getState(row)?.name, - }, + { Header: T.State, id: 'state', accessor: (row) => getState(row)?.name }, { Header: T.Owner, id: 'owner', accessor: 'UNAME' }, { Header: T.Group, id: 'group', accessor: 'GNAME' }, { Header: T.Locked, id: 'locked', accessor: 'LOCK' }, { Header: T.Driver, id: 'vn_mad', accessor: getVNManager }, - { - Header: T.TotalClusters, - id: 'clusters', - accessor: (row) => getTotalOfResources(row?.CLUSTERS), - sortType: 'number', - }, { Header: T.UsedLeases, id: 'used_leases', accessor: 'USED_LEASES', sortType: 'number', }, - { - Header: T.TotalLeases, - id: 'total_leases', - accessor: getTotalLeases, - sortType: 'number', - }, { Header: T.ProvisionId, id: 'provision_id', - accessor: (row) => row?.TEMPLATE?.PROVISION?.ID, - disableSortBy: true, + accessor: 'TEMPLATE.PROVISION.ID', }, ] -COLUMNS.noFilterIds = [ - 'id', - 'name', - 'clusters', - 'used_leases', - 'total_leases', - 'provision_id', -] +COLUMNS.noFilterIds = ['id', 'name', 'used_leases', 'provision_id'] export default COLUMNS diff --git a/src/fireedge/src/client/components/Tables/VNetworks/row.js b/src/fireedge/src/client/components/Tables/VNetworks/row.js index 17cb7b96a4..92b9c799cb 100644 --- a/src/fireedge/src/client/components/Tables/VNetworks/row.js +++ b/src/fireedge/src/client/components/Tables/VNetworks/row.js @@ -13,19 +13,28 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { memo } from 'react' +import { memo, useMemo } from 'react' import PropTypes from 'prop-types' + import api from 'client/features/OneApi/network' import { NetworkCard } from 'client/components/Cards' const Row = memo( - ({ original, ...props }) => { + ({ original, value, onClickLabel, ...props }) => { const state = api.endpoints.getVNetworks.useQueryState(undefined, { selectFromResult: ({ data = [] }) => data.find((network) => +network.ID === +original.ID), }) - return + const memoNetwork = useMemo(() => state ?? original, [state, original]) + + return ( + + ) }, (prev, next) => prev.className === next.className ) @@ -34,7 +43,9 @@ Row.propTypes = { original: PropTypes.object, value: PropTypes.object, isSelected: PropTypes.bool, - handleClick: PropTypes.func, + className: PropTypes.string, + onClick: PropTypes.func, + onClickLabel: PropTypes.func, } Row.displayName = 'VirtualNetworkRow' diff --git a/src/fireedge/src/client/components/Tables/VRouters/index.js b/src/fireedge/src/client/components/Tables/VRouters/index.js index 0bcbafbe74..c2a289aaf3 100644 --- a/src/fireedge/src/client/components/Tables/VRouters/index.js +++ b/src/fireedge/src/client/components/Tables/VRouters/index.js @@ -30,17 +30,22 @@ const DEFAULT_DATA_CY = 'vrouters' * @returns {ReactElement} Virtual Routers table */ const VRoutersTable = (props) => { - const { rootProps = {}, searchProps = {}, ...rest } = props ?? {} + const { + rootProps = {}, + searchProps = {}, + useQuery = useGetVRoutersQuery, + ...rest + } = props ?? {} rootProps['data-cy'] ??= DEFAULT_DATA_CY searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}` const { view, getResourceView } = useViews() - const { data = [], isFetching, refetch } = useGetVRoutersQuery() + const { data = [], isFetching, refetch } = useQuery() const columns = useMemo( () => createColumns({ - filters: getResourceView(RESOURCE_NAMES.V_ROUTER)?.filters, + filters: getResourceView(RESOURCE_NAMES.VROUTER)?.filters, columns: VRouterColumns, }), [view] diff --git a/src/fireedge/src/client/components/Tables/VmTemplates/actions.js b/src/fireedge/src/client/components/Tables/VmTemplates/actions.js index 410cc66915..f5a240df01 100644 --- a/src/fireedge/src/client/components/Tables/VmTemplates/actions.js +++ b/src/fireedge/src/client/components/Tables/VmTemplates/actions.js @@ -216,12 +216,6 @@ const Actions = () => { changeOwnership({ id: original?.ID, ...newOwnership }) ) }, - // onSubmit: (rows) => async (newOwnership) => { - // const ids = rows?.map?.(({ original }) => original?.ID) - // await Promise.all( - // ids.map((id) => changeOwnership({ id, ...newOwnership })) - // ) - // }, }, { accessor: VM_TEMPLATE_ACTIONS.CHANGE_GROUP, diff --git a/src/fireedge/src/client/components/Tables/VmTemplates/columns.js b/src/fireedge/src/client/components/Tables/VmTemplates/columns.js index 7d97d45c0c..d2502659ef 100644 --- a/src/fireedge/src/client/components/Tables/VmTemplates/columns.js +++ b/src/fireedge/src/client/components/Tables/VmTemplates/columns.js @@ -25,16 +25,8 @@ const COLUMNS = [ { Header: T.Group, id: 'group', accessor: 'GNAME' }, { Header: T.RegistrationTime, id: 'time', accessor: 'REGTIME' }, { Header: T.Locked, id: 'locked', accessor: 'LOCK' }, - { - Header: T.Logo, - id: 'logo', - accessor: 'TEMPLATE.LOGO', - }, - { - Header: T.VirtualRouter, - id: 'vrouter', - accessor: 'TEMPLATE.VROUTER', - }, + { Header: T.Logo, id: 'logo', accessor: 'TEMPLATE.LOGO' }, + { Header: T.VirtualRouter, id: 'vrouter', accessor: 'TEMPLATE.VROUTER' }, ] COLUMNS.noFilterIds = ['id', 'name', 'time', 'logo'] diff --git a/src/fireedge/src/client/components/Tables/index.js b/src/fireedge/src/client/components/Tables/index.js index 064407bedb..186d71c382 100644 --- a/src/fireedge/src/client/components/Tables/index.js +++ b/src/fireedge/src/client/components/Tables/index.js @@ -35,6 +35,8 @@ import VNetworkTemplatesTable from 'client/components/Tables/VNetworkTemplates' import VRoutersTable from 'client/components/Tables/VRouters' import ZonesTable from 'client/components/Tables/Zones' +export * from 'client/components/Tables/Enhanced/Utils' + export { SkeletonTable, EnhancedTable, diff --git a/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js b/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js index 5649a8204a..aa07704bb5 100644 --- a/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js +++ b/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js @@ -47,6 +47,7 @@ const Attribute = memo( handleDelete, handleEdit, handleGetOptionList, + askToDelete = true, link, icon, name, @@ -157,7 +158,12 @@ const Attribute = memo( handleClick={handleActiveEditForm} /> )} - {canDelete && } + {canDelete && ( + + )} )} @@ -186,6 +192,7 @@ export const AttributePropTypes = { handleDelete: PropTypes.func, handleEdit: PropTypes.func, handleGetOptionList: PropTypes.func, + askToDelete: PropTypes.bool, link: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), icon: PropTypes.any, name: PropTypes.string.isRequired, diff --git a/src/fireedge/src/client/components/Tabs/Common/AttributeCreateForm.js b/src/fireedge/src/client/components/Tabs/Common/AttributeCreateForm.js index a1ff395c0a..0455bae9de 100644 --- a/src/fireedge/src/client/components/Tabs/Common/AttributeCreateForm.js +++ b/src/fireedge/src/client/components/Tabs/Common/AttributeCreateForm.js @@ -47,7 +47,7 @@ const AttributeCreateForm = memo(({ handleAdd }) => { reset() } catch {} }, - [handleAdd] + [handleAdd, formState.isSubmitting, nameInputKey, valueInputKey] ) const handleKeyDown = (evt) => { @@ -95,10 +95,7 @@ const AttributeCreateForm = memo(({ handleAdd }) => { ) }) -AttributeCreateForm.propTypes = { - handleAdd: PropTypes.func, -} - +AttributeCreateForm.propTypes = { handleAdd: PropTypes.func } AttributeCreateForm.displayName = 'AttributeCreateForm' export default AttributeCreateForm diff --git a/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js b/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js index ad1f1b7cf0..71fe2fb7b5 100644 --- a/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js +++ b/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js @@ -93,6 +93,7 @@ const AttributePanel = memo( actions = allActionsEnabled ? ALL_ACTIONS : [], filtersSpecialAttributes = true, collapse = false, + askToDelete = true, }) => { const classes = useStyles() @@ -114,6 +115,7 @@ const AttributePanel = memo( canDelete: canUseAction(name, DELETE), handleEdit, handleDelete, + askToDelete, }) ) @@ -137,10 +139,11 @@ AttributePanel.propTypes = { handleAdd: PropTypes.func, handleEdit: PropTypes.func, handleDelete: PropTypes.func, - title: PropTypes.string, + title: PropTypes.any, filtersSpecialAttributes: PropTypes.bool, allActionsEnabled: PropTypes.bool, collapse: PropTypes.bool, + askToDelete: PropTypes.bool, } AttributePanel.displayName = 'AttributePanel' diff --git a/src/fireedge/src/client/components/Tabs/Common/Permissions.js b/src/fireedge/src/client/components/Tabs/Common/Permissions.js index 279e6f56a7..4b96058d84 100644 --- a/src/fireedge/src/client/components/Tabs/Common/Permissions.js +++ b/src/fireedge/src/client/components/Tabs/Common/Permissions.js @@ -58,7 +58,7 @@ const Permissions = memo(({ handleEdit, actions, ...permissions }) => { const getIcon = (checked) => (+checked ? : ) return ( - + {Tr(T.Permissions)} diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Address.js b/src/fireedge/src/client/components/Tabs/VNetwork/Address.js new file mode 100644 index 0000000000..cfd10e15da --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VNetwork/Address.js @@ -0,0 +1,82 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement } from 'react' +import PropTypes from 'prop-types' +import { Box, Stack } from '@mui/material' + +import { useGetVNetworkQuery } from 'client/features/OneApi/network' + +import AddressRangeCard from 'client/components/Cards/AddressRangeCard' +import { + AddAddressRangeAction, + UpdateAddressRangeAction, + DeleteAddressRangeAction, +} from 'client/components/Buttons' + +import { AddressRange, VN_ACTIONS } from 'client/constants' + +const { ADD_AR, UPDATE_AR, DELETE_AR } = VN_ACTIONS + +/** + * Renders the list of address ranges from a Virtual Network. + * + * @param {object} props - Props + * @param {object} props.tabProps - Tab information + * @param {string[]} props.tabProps.actions - Actions tab + * @param {string} props.id - Virtual Network id + * @returns {ReactElement} AR tab + */ +const AddressTab = ({ tabProps: { actions } = {}, id }) => { + const { data: vnet } = useGetVNetworkQuery({ id }) + + /** @type {AddressRange[]} */ + const addressRanges = [vnet.AR_POOL.AR ?? []].flat() + + return ( + + {actions[ADD_AR] === true && } + + + {addressRanges.map((ar) => ( + + {actions[UPDATE_AR] === true && ( + + )} + {actions[DELETE_AR] === true && ( + + )} + + } + /> + ))} + + + ) +} + +AddressTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +AddressTab.displayName = 'AddressTab' + +export default AddressTab diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js b/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js new file mode 100644 index 0000000000..662da3d835 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js @@ -0,0 +1,83 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement, useMemo } from 'react' +import PropTypes from 'prop-types' + +import { useHistory } from 'react-router' +import { generatePath } from 'react-router-dom' +import { Box } from '@mui/material' + +import { useViews } from 'client/features/Auth' +import { useGetClustersQuery } from 'client/features/OneApi/cluster' +import { useGetVNetworkQuery } from 'client/features/OneApi/network' +// import {} from 'client/components/Tabs/VNetwork/Address/Actions' + +import { ClustersTable } from 'client/components/Tables' +import { RESOURCE_NAMES } from 'client/constants' +import { PATH } from 'client/apps/sunstone/routesOne' + +const { CLUSTER } = RESOURCE_NAMES + +/** + * Renders the list of clusters from a Virtual Network. + * + * @param {object} props - Props + * @param {string} props.id - Virtual Network id + * @returns {ReactElement} Clusters tab + */ +const ClustersTab = ({ id }) => { + const { push: redirectTo } = useHistory() + const { data: vnet } = useGetVNetworkQuery({ id }) + + const { view, hasAccessToResource } = useViews() + const detailAccess = useMemo(() => hasAccessToResource(CLUSTER), [view]) + + const clusters = [vnet?.CLUSTERS?.ID ?? []].flat().map((clId) => +clId) + + const redirectToCluster = (row) => { + const clusterPath = PATH.INFRASTRUCTURE.CLUSTERS.DETAIL + redirectTo(generatePath(clusterPath, { id: row.ID })) + } + + const useQuery = () => + useGetClustersQuery(undefined, { + selectFromResult: ({ data: result = [], ...rest }) => ({ + data: result?.filter((cluster) => clusters.includes(+cluster.ID)), + ...rest, + }), + }) + + return ( + + + + ) +} + +ClustersTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +ClustersTab.displayName = 'ClustersTab' + +export default ClustersTab diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Info/index.js b/src/fireedge/src/client/components/Tabs/VNetwork/Info/index.js index 8ef9bfbf8e..360c8ae924 100644 --- a/src/fireedge/src/client/components/Tabs/VNetwork/Info/index.js +++ b/src/fireedge/src/client/components/Tabs/VNetwork/Info/index.js @@ -29,6 +29,7 @@ import { AttributePanel, } from 'client/components/Tabs/Common' import Information from 'client/components/Tabs/VNetwork/Info/information' +import QOS from 'client/components/Tabs/VNetwork/Info/qos' import { Tr } from 'client/components/HOC' import { T } from 'client/constants' @@ -57,6 +58,7 @@ const VNetworkInfoTab = ({ tabProps = {}, id }) => { information_panel: informationPanel, permissions_panel: permissionsPanel, ownership_panel: ownershipPanel, + qos_panel: qosPanel, attributes_panel: attributesPanel, vcenter_panel: vcenterPanel, lxc_panel: lxcPanel, @@ -146,9 +148,11 @@ const VNetworkInfoTab = ({ tabProps = {}, id }) => { groupName={GNAME} /> )} + {qosPanel?.enabled && } {attributesPanel?.enabled && attributes && ( { {vcenterPanel?.enabled && vcenterAttributes && ( { {lxcPanel?.enabled && lxcAttributes && ( { const [rename] = useRenameVNetMutation() - const { ID, NAME } = vnet + const { + ID, + NAME, + PARENT_NETWORK_ID: parentId, + LOCK, + VLAN_ID, + VLAN_ID_AUTOMATIC, + OUTER_VLAN_ID, + OUTER_VLAN_ID_AUTOMATIC, + } = vnet + + const { data: parent } = useGetVNetworkQuery( + { id: parentId }, + { skip: !parentId } + ) const { name: stateName, color: stateColor } = getState(vnet) @@ -51,13 +76,49 @@ const InformationPanel = ({ vnet = {}, actions }) => { canEdit: actions?.includes?.(VN_ACTIONS.RENAME), handleEdit: handleRename, }, + parentId && { + name: T.ReservationParent, + value: `#${parentId} ${parent?.NAME ?? '--'}`, + link: + !Number.isNaN(+parentId) && + generatePath(PATH.NETWORK.VNETS.DETAIL, { id: parentId }), + dataCy: 'parent', + }, { name: T.State, value: ( - + + + + ), }, - ] + { + name: T.Locked, + value: levelLockToString(LOCK?.LOCKED), + dataCy: 'locked', + }, + { + name: T.VlanId, + value: VLAN_ID || '-', + dataCy: 'vlan-id', + }, + { + name: T.AutomaticVlanId, + value: booleanToString(stringToBoolean(VLAN_ID_AUTOMATIC)), + dataCy: 'vlan-id-automatic', + }, + { + name: T.OuterVlanId, + value: OUTER_VLAN_ID || '-', + dataCy: 'outer-vlan-id', + }, + { + name: T.AutomaticOuterVlanId, + value: booleanToString(stringToBoolean(OUTER_VLAN_ID_AUTOMATIC)), + dataCy: 'outer-vlan-id-automatic', + }, + ].filter(Boolean) return ( <> diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Info/qos.js b/src/fireedge/src/client/components/Tabs/VNetwork/Info/qos.js new file mode 100644 index 0000000000..06831b6a4a --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VNetwork/Info/qos.js @@ -0,0 +1,86 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement } from 'react' +import PropTypes from 'prop-types' + +import { List } from 'client/components/Tabs/Common' +import { T, VNetwork } from 'client/constants' + +/** + * Renders mainly information tab. + * + * @param {object} props - Props + * @param {VNetwork} props.vnet - Virtual Network resource + * @returns {ReactElement} Information tab + */ +const QOSPanel = ({ vnet = {} }) => { + const { + INBOUND_AVG_BW, + INBOUND_PEAK_BW, + INBOUND_PEAK_KB, + OUTBOUND_AVG_BW, + OUTBOUND_PEAK_BW, + OUTBOUND_PEAK_KB, + } = vnet.TEMPLATE + + const inbound = [ + { + name: T.AverageBandwidth, + value: INBOUND_AVG_BW?.concat(' KBytes/s') ?? '-', + dataCy: 'inbound-avg', + }, + { + name: T.PeakBandwidth, + value: INBOUND_PEAK_BW?.concat(' KBytes/s') ?? '-', + dataCy: 'inbound-peak-bandwidth', + }, + { + name: T.PeakBurst, + value: INBOUND_PEAK_KB?.concat(' KBytes') ?? '-', + dataCy: 'inbound-peak', + }, + ] + + const outbound = [ + { + name: T.AverageBandwidth, + value: OUTBOUND_AVG_BW?.concat(' KBytes/s') ?? '-', + dataCy: 'outbound-avg', + }, + { + name: T.PeakBandwidth, + value: OUTBOUND_PEAK_BW?.concat(' KBytes/s') ?? '-', + dataCy: 'outbound-peak-bandwidth', + }, + { + name: T.PeakBurst, + value: OUTBOUND_PEAK_KB?.concat(' KBytes') ?? '-', + dataCy: 'outbound-peak', + }, + ] + + return ( + <> + + + + ) +} + +QOSPanel.propTypes = { vnet: PropTypes.object } +QOSPanel.displayName = 'QOSPanel' + +export default QOSPanel diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Leases/LeaseItem.js b/src/fireedge/src/client/components/Tabs/VNetwork/Leases/LeaseItem.js new file mode 100644 index 0000000000..bede332ac0 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VNetwork/Leases/LeaseItem.js @@ -0,0 +1,186 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement, memo, Fragment } from 'react' +import PropTypes from 'prop-types' +import ReleaseIcon from 'iconoir-react/dist/PlayOutline' +import { Link as RouterLink, generatePath } from 'react-router-dom' +import { Stack, Link, Typography } from '@mui/material' + +import { useReleaseLeaseMutation } from 'client/features/OneApi/network' + +import { SubmitButton } from 'client/components/FormControl' +import { StatusCircle } from 'client/components/Status' + +import { getAddressType } from 'client/models/VirtualNetwork' +import { T, VN_ACTIONS, ARLease } from 'client/constants' +import { PATH } from 'client/apps/sunstone/routesOne' + +const LEASE_TYPES = { VM: 'vm', NET: 'net', VR: 'vr' } + +/** + * Renders the name of lease. + * + * @param {object} props - Props + * @param {string} props.id - Resource id + * @param {'vm'|'net'|'vr'} props.type - Resource type: VM, VNET or VR + * @returns {ReactElement} Lease column name + */ +const LeaseName = ({ id, type }) => { + const colorState = { + [LEASE_TYPES.VM]: 'secondary.main', + [LEASE_TYPES.NET]: 'warning.main', + [LEASE_TYPES.VR]: 'success.main', + }[type] + + const path = { + [LEASE_TYPES.VM]: PATH.INSTANCE.VMS.DETAIL, + [LEASE_TYPES.NET]: PATH.NETWORK.VNETS.DETAIL, + [LEASE_TYPES.VR]: PATH.INSTANCE.VROUTERS.LIST, + }[type] + + return ( + svg': { mr: '1em' }, + }} + > + + {`#${id}`} + + ) +} + +LeaseName.propTypes = { + id: PropTypes.string, + type: PropTypes.oneOf(Object.values(LEASE_TYPES)), +} + +const LeaseItem = memo( + /** + * @param {object} props - Props + * @param {string} props.id - Virtual Network id + * @param {object} props.actions - Actions tab + * @param {ARLease} props.lease - Lease to render + * @param {Function} props.resetHoldState - Reset hold state mutation + * @returns {ReactElement} Lease component + */ + ({ id, actions, lease, resetHoldState }) => { + const [releaseLease, { isLoading: isReleasing }] = useReleaseLeaseMutation() + + /** @type {ARLease} */ + const { + IP, + MAC, + addr = IP || MAC, + IP6, + IP6_GLOBAL, + IP6_LINK, + IP6_ULA, + VM: vmId, + VNET: vnetId, + VROUTER: vrId, + } = lease + + const release = async () => { + const template = `LEASES = [ ${getAddressType(addr)} = ${addr} ]` + await releaseLease({ id, template }).unwrap() + await resetHoldState() + } + + const resType = + vmId >= 0 + ? LEASE_TYPES.VM + : vnetId >= 0 + ? LEASE_TYPES.NET + : LEASE_TYPES.VR + + const resId = { + [LEASE_TYPES.VM]: vmId, + [LEASE_TYPES.NET]: vnetId, + [LEASE_TYPES.VR]: vrId, + }[resType] + + return ( + + {+vmId === -1 ? ( + actions[VN_ACTIONS.RELEASE_LEASE] && ( + + + } + label={T.ReleaseIp} + /> + + ) + ) : ( + + )} + {[ + { text: IP, dataCy: 'ip' }, + { text: IP6, dataCy: 'ip6' }, + { text: MAC, dataCy: 'mac' }, + { text: IP6_LINK, dataCy: 'ip6-link' }, + { text: IP6_ULA, dataCy: 'ip6-ula' }, + { text: IP6_GLOBAL, dataCy: 'ip6-global' }, + ].map(({ text = '--', dataCy }) => ( + + {text} + + ))} + + ) + } +) + +LeaseItem.propTypes = { + lease: PropTypes.object, + actions: PropTypes.object, + id: PropTypes.string, + resetHoldState: PropTypes.func, +} + +LeaseItem.displayName = 'LeaseItem' + +export default LeaseItem diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Leases/index.js b/src/fireedge/src/client/components/Tabs/VNetwork/Leases/index.js new file mode 100644 index 0000000000..778a3136fb --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VNetwork/Leases/index.js @@ -0,0 +1,170 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement } from 'react' +import PropTypes from 'prop-types' +import { Box, Typography, TextField, Skeleton } from '@mui/material' + +import { + useGetVNetworkQuery, + useHoldLeaseMutation, +} from 'client/features/OneApi/network' + +import { SubmitButton } from 'client/components/FormControl' +import { Translate } from 'client/components/HOC' +import LeaseItem from 'client/components/Tabs/VNetwork/Leases/LeaseItem' + +import { getAddressType } from 'client/models/VirtualNetwork' +import { T, AddressRange, VN_ACTIONS } from 'client/constants' +import { useGeneralApi } from 'client/features/General' + +const LEASES_COLUMNS = [ + 'NAME', + 'IP', + 'IP6', + 'MAC', + 'IP6 GLOBAL', + 'IP6 LINK', + 'IP6 ULA', +] + +/** + * Renders the list of total leases from a Virtual Network. + * + * @param {object} props - Props + * @param {object} props.tabProps - Tab information + * @param {string[]} props.tabProps.actions - Actions tab + * @param {string} props.id - Virtual Network id + * @returns {ReactElement} AR tab + */ +const LeasesTab = ({ tabProps: { actions } = {}, id }) => { + const { data: vnet } = useGetVNetworkQuery({ id }) + const { enqueueError } = useGeneralApi() + + const [holdLease, { isLoading, isSuccess, reset, originalArgs }] = + useHoldLeaseMutation() + + /** @type {AddressRange[]} */ + const addressRanges = [vnet.AR_POOL.AR ?? []].flat() + const leases = addressRanges.map(({ LEASES }) => LEASES.LEASE ?? []).flat() + + const isHolding = + isLoading || + (isSuccess && + !leases.some( + (l) => + originalArgs?.template.includes(l.IP) || + originalArgs?.template.includes(l.IP6) || + originalArgs?.template.includes(l.MAC) + )) + + const hold = async (event) => { + try { + event.preventDefault() + const { addr } = Object.fromEntries(new FormData(event.target)) + const addrName = getAddressType(addr) + + if (!addrName) return enqueueError(T.SomethingWrong) + + const leasesToHold = `LEASES = [ ${addrName} = ${addr} ]` + await holdLease({ id, template: leasesToHold }).unwrap() + } catch {} + } + + return ( + + {actions[VN_ACTIONS.HOLD_LEASE] === true && ( + + + + + )} + + span': { + // header styles + borderBottom: (theme) => `1px solid ${theme.palette.divider}`, + marginLeft: '-8px', + paddingLeft: '8px', + alignSelf: 'end', + }, + }} + > + {LEASES_COLUMNS.map((col, index) => ( + + {index !== 0 && } + + ))} + {leases.map((lease) => ( + + ))} + + {isHolding && } + + ) +} + +LeasesTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +LeasesTab.displayName = 'LeasesTab' + +export default LeasesTab diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/Security.js b/src/fireedge/src/client/components/Tabs/VNetwork/Security.js new file mode 100644 index 0000000000..ddedbd1346 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VNetwork/Security.js @@ -0,0 +1,107 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement, useMemo } from 'react' +import PropTypes from 'prop-types' + +import AddIcon from 'iconoir-react/dist/AddCircledOutline' +import { useHistory } from 'react-router' +import { generatePath } from 'react-router-dom' +import { Box } from '@mui/material' + +import { useViews } from 'client/features/Auth' +import { useGetSecGroupsQuery } from 'client/features/OneApi/securityGroup' +import { useGetVNetworkQuery } from 'client/features/OneApi/network' +// import {} from 'client/components/Tabs/VNetwork/Address/Actions' + +import { SecurityGroupsTable, GlobalAction } from 'client/components/Tables' +import { T, VN_ACTIONS, RESOURCE_NAMES } from 'client/constants' +import { PATH } from 'client/apps/sunstone/routesOne' + +const { SEC_GROUP } = RESOURCE_NAMES +const { ADD_SECGROUP } = VN_ACTIONS + +/** + * Renders the list of security groups from a Virtual Network. + * + * @param {object} props - Props + * @param {object} props.tabProps - Tab information + * @param {string[]} props.tabProps.actions - Actions tab + * @param {string} props.id - Virtual Network id + * @returns {ReactElement} Security Groups tab + */ +const SecurityTab = ({ tabProps: { actions } = {}, id }) => { + const { push: redirectTo } = useHistory() + const { data: vnet } = useGetVNetworkQuery({ id }) + + const { view, hasAccessToResource } = useViews() + const detailAccess = useMemo(() => hasAccessToResource(SEC_GROUP), [view]) + + const splittedSecGroups = vnet?.TEMPLATE.SECURITY_GROUPS?.split(',') ?? [] + const secGroups = [splittedSecGroups].flat().map((sgId) => +sgId) + + const redirectToSecGroup = (row) => { + redirectTo(generatePath(PATH.NETWORK.SEC_GROUPS.DETAIL, { id: row.ID })) + } + + const useQuery = () => + useGetSecGroupsQuery(undefined, { + selectFromResult: ({ data: result = [], ...rest }) => ({ + data: result?.filter((secgroup) => secGroups.includes(+secgroup.ID)), + ...rest, + }), + }) + + /** @type {GlobalAction[]} */ + const globalActions = [ + actions[ADD_SECGROUP] && { + accessor: VN_ACTIONS.ADD_SECGROUP, + dataCy: VN_ACTIONS.ADD_SECGROUP, + tooltip: T.SecurityGroup, + icon: AddIcon, + options: [ + { + dialogProps: { title: T.SecurityGroup }, + form: undefined, + onSubmit: () => async (formData) => { + console.log({ formData }) + }, + }, + ], + }, + ].filter(Boolean) + + return ( + + + + ) +} + +SecurityTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +SecurityTab.displayName = 'SecurityTab' + +export default SecurityTab diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/VRouters.js b/src/fireedge/src/client/components/Tabs/VNetwork/VRouters.js new file mode 100644 index 0000000000..7b28dcbf3b --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VNetwork/VRouters.js @@ -0,0 +1,82 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement, useMemo } from 'react' +import PropTypes from 'prop-types' + +import { useHistory } from 'react-router' +import { generatePath } from 'react-router-dom' +import { Box } from '@mui/material' + +import { useViews } from 'client/features/Auth' +import { useGetVRoutersQuery } from 'client/features/OneApi/vrouter' +import { useGetVNetworkQuery } from 'client/features/OneApi/network' +// import {} from 'client/components/Tabs/VNetwork/Address/Actions' + +import { VRoutersTable } from 'client/components/Tables' +import { RESOURCE_NAMES } from 'client/constants' +import { PATH } from 'client/apps/sunstone/routesOne' + +const { VROUTER } = RESOURCE_NAMES + +/** + * Renders the list of virtual routers from a Virtual Network. + * + * @param {object} props - Props + * @param {string} props.id - Virtual Network id + * @returns {ReactElement} Virtual routers tab + */ +const VRoutersTab = ({ id }) => { + const { push: redirectTo } = useHistory() + const { data: vnet } = useGetVNetworkQuery({ id }) + + const { view, hasAccessToResource } = useViews() + const detailAccess = useMemo(() => hasAccessToResource(VROUTER), [view]) + + const vrouters = [vnet?.VROUTERS?.ID ?? []].flat().map((vrId) => +vrId) + + const redirectToVRouter = (row) => { + redirectTo(generatePath(PATH.INSTANCE.VROUTERS.DETAIL, { id: row.ID })) + } + + const useQuery = () => + useGetVRoutersQuery(undefined, { + selectFromResult: ({ data: result = [], ...rest }) => ({ + data: result?.filter((vrouter) => vrouters.includes(+vrouter.ID)), + ...rest, + }), + }) + + return ( + + + + ) +} + +VRoutersTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +VRoutersTab.displayName = 'VRoutersTab' + +export default VRoutersTab diff --git a/src/fireedge/src/client/components/Tabs/VNetwork/index.js b/src/fireedge/src/client/components/Tabs/VNetwork/index.js index b6635e7eed..afd4697bc1 100644 --- a/src/fireedge/src/client/components/Tabs/VNetwork/index.js +++ b/src/fireedge/src/client/components/Tabs/VNetwork/index.js @@ -24,10 +24,20 @@ import { RESOURCE_NAMES } from 'client/constants' import Tabs from 'client/components/Tabs' import Info from 'client/components/Tabs/VNetwork/Info' +import Address from 'client/components/Tabs/VNetwork/Address' +import Lease from 'client/components/Tabs/VNetwork/Leases' +import Security from 'client/components/Tabs/VNetwork/Security' +import VRouters from 'client/components/Tabs/VNetwork/VRouters' +import Clusters from 'client/components/Tabs/VNetwork/Clusters' const getTabComponent = (tabName) => ({ info: Info, + address: Address, + lease: Lease, + security: Security, + virtual_router: VRouters, + cluster: Clusters, }[tabName]) const VNetworkTabs = memo(({ id }) => { diff --git a/src/fireedge/src/client/components/Tabs/Vm/index.js b/src/fireedge/src/client/components/Tabs/Vm/index.js index 3837dc1513..da6379525c 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/index.js @@ -39,7 +39,7 @@ const getTabComponent = (tabName) => info: Info, network: Network, history: History, - schedActions: SchedActions, + sched_actions: SchedActions, snapshot: Snapshot, storage: Storage, configuration: Configuration, diff --git a/src/fireedge/src/client/constants/datastore.js b/src/fireedge/src/client/constants/datastore.js index 174ec6f60e..bb30399ac2 100644 --- a/src/fireedge/src/client/constants/datastore.js +++ b/src/fireedge/src/client/constants/datastore.js @@ -77,7 +77,6 @@ export const DATASTORE_ACTIONS = { // INFORMATION RENAME: ACTIONS.RENAME, - CHANGE_MODE: ACTIONS.CHANGE_MODE, CHANGE_OWNER: ACTIONS.CHANGE_OWNER, CHANGE_GROUP: ACTIONS.CHANGE_GROUP, } diff --git a/src/fireedge/src/client/constants/host.js b/src/fireedge/src/client/constants/host.js index 79d36ff187..979053abae 100644 --- a/src/fireedge/src/client/constants/host.js +++ b/src/fireedge/src/client/constants/host.js @@ -17,23 +17,25 @@ import * as ACTIONS from 'client/constants/actions' import COLOR from 'client/constants/color' import * as STATES from 'client/constants/states' import * as T from 'client/constants/translates' + /** * @typedef {object} PciDevice - PCI device * @property {string} ADDRESS - Address, bus, slot and function * @property {string} BUS - Address bus * @property {string} CLASS - Id of PCI device class - * @property {string} [CLASS_NAME] - Name of PCI device class + * @property {string} CLASS_NAME - Name of PCI device class * @property {string} DEVICE - Id of PCI device - * @property {string} [DEVICE_NAME] - Name of PCI device + * @property {string} DEVICE_NAME - Name of PCI device * @property {string} DOMAIN - Address domain * @property {string} FUNCTION - Address function * @property {string} NUMA_NODE - Numa node - * @property {string} PROFILES - PCI device available profiles + * @property {string} PROFILES - Available vGPU Profiles * @property {string} SHORT_ADDRESS - Short address * @property {string} SLOT - Address slot + * @property {string} [UUID] - UUID * @property {string} TYPE - Type * @property {string} VENDOR - Id of PCI device vendor - * @property {string} [VENDOR_NAME] - Name of PCI device vendor + * @property {string} VENDOR_NAME - Name of PCI device vendor * @property {string|number} VMID - Id using this device, -1 if free */ diff --git a/src/fireedge/src/client/constants/image.js b/src/fireedge/src/client/constants/image.js index e77b669f55..e7f4edccdd 100644 --- a/src/fireedge/src/client/constants/image.js +++ b/src/fireedge/src/client/constants/image.js @@ -168,7 +168,6 @@ export const IMAGE_ACTIONS = { // INFORMATION RENAME: ACTIONS.RENAME, - CHANGE_MODE: ACTIONS.CHANGE_MODE, CHANGE_OWNER: ACTIONS.CHANGE_OWNER, CHANGE_GROUP: ACTIONS.CHANGE_GROUP, CHANGE_TYPE: 'chtype', diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js index c305329831..368c435ff5 100644 --- a/src/fireedge/src/client/constants/index.js +++ b/src/fireedge/src/client/constants/index.js @@ -157,7 +157,7 @@ export const RESOURCE_NAMES = { MARKETPLACE: 'marketplace', SEC_GROUP: 'security-group', USER: 'user', - V_ROUTER: 'virtual-router', + VROUTER: 'virtual-router', VM_TEMPLATE: 'vm-template', VM: 'vm', VN_TEMPLATE: 'network-template', diff --git a/src/fireedge/src/client/constants/marketplace.js b/src/fireedge/src/client/constants/marketplace.js index 763402465f..48e2bed8ae 100644 --- a/src/fireedge/src/client/constants/marketplace.js +++ b/src/fireedge/src/client/constants/marketplace.js @@ -108,7 +108,6 @@ export const MARKETPLACE_ACTIONS = { // INFORMATION RENAME: ACTIONS.RENAME, - CHANGE_MODE: ACTIONS.CHANGE_MODE, CHANGE_OWNER: ACTIONS.CHANGE_OWNER, CHANGE_GROUP: ACTIONS.CHANGE_GROUP, } diff --git a/src/fireedge/src/client/constants/marketplaceApp.js b/src/fireedge/src/client/constants/marketplaceApp.js index d78e312d93..a904d2879c 100644 --- a/src/fireedge/src/client/constants/marketplaceApp.js +++ b/src/fireedge/src/client/constants/marketplaceApp.js @@ -61,7 +61,6 @@ export const MARKETPLACE_APP_ACTIONS = { // INFORMATION RENAME: ACTIONS.RENAME, - CHANGE_MODE: ACTIONS.CHANGE_MODE, CHANGE_OWNER: ACTIONS.CHANGE_OWNER, CHANGE_GROUP: ACTIONS.CHANGE_GROUP, } diff --git a/src/fireedge/src/client/constants/network.js b/src/fireedge/src/client/constants/network.js index 31e5eb6bec..5c7ceef60a 100644 --- a/src/fireedge/src/client/constants/network.js +++ b/src/fireedge/src/client/constants/network.js @@ -40,8 +40,9 @@ import * as ACTIONS from 'client/constants/actions' * @property {string} SIZE - Size * @property {AR_TYPES} TYPE - Type * @property {string} USED_LEASES - Used leases + * @property {string} [IPAM_MAD] - IPAM driver * @property {{ LEASE: ARLease|ARLease[] }} [LEASES] - Leases information - * @property {string} [GLOBAL_PREFIX] -Global prefix + * @property {string} [GLOBAL_PREFIX] - Global prefix * @property {string} [PARENT_NETWORK_AR_ID] - Parent address range id * @property {string} [ULA_PREFIX] - ULA prefix * @property {string} [VN_MAD] - Virtual network manager @@ -144,15 +145,33 @@ export const VN_STATES = [ /** @enum {string} Virtual network actions */ export const VN_ACTIONS = { CREATE_DIALOG: 'create_dialog', + IMPORT_DIALOG: 'import_dialog', + UPDATE_DIALOG: 'update_dialog', + INSTANTIATE_DIALOG: 'instantiate_dialog', + RESERVE_DIALOG: 'reserve_dialog', + CHANGE_CLUSTER: 'change_cluster', + LOCK: 'lock', + UNLOCK: 'unlock', DELETE: 'delete', - RECOVER: 'recover', - UPDATE: 'update', // INFORMATION RENAME: ACTIONS.RENAME, CHANGE_MODE: ACTIONS.CHANGE_MODE, CHANGE_OWNER: ACTIONS.CHANGE_OWNER, CHANGE_GROUP: ACTIONS.CHANGE_GROUP, + + // ADDRESS RANGE + ADD_AR: 'add_ar', + UPDATE_AR: 'update_ar', + DELETE_AR: 'delete_ar', + + // LEASES + HOLD_LEASE: 'hold_lease', + RELEASE_LEASE: 'release_lease', + + // SECURITY GROUPS + ADD_SECGROUP: 'add_secgroup', + DELETE_SECGROUP: 'delete_secgroup', } /** @enum {string} Virtual network actions by state */ @@ -187,19 +206,44 @@ export const AR_TYPES = { /** @enum {string} Virtual Network Drivers */ export const VN_DRIVERS = { - dummy: 'dummy', - dot1Q: '802.1Q', - ebtables: 'ebtables', - fw: 'fw', - ovswitch: 'ovswitch', - vxlan: 'vxlan', - vcenter: 'vcenter', - ovswitch_vxlan: 'ovswitch_vxlan', bridge: 'bridge', + fw: 'fw', + ebtables: 'ebtables', + dot1Q: '802.1Q', + vxlan: 'vxlan', + ovswitch: 'ovswitch', + ovswitch_vxlan: 'ovswitch_vxlan', + vcenter: 'vcenter', elastic: 'elastic', nodeport: 'nodeport', } +export const VNET_METHODS = { + static: 'static (Based on context)', + dhcp: 'dhcp (DHCPv4)', + skip: 'skip (Do not configure IPv4)', +} + +export const VNET_METHODS6 = { + static: 'static (Based on context)', + auto: 'auto (SLAAC)', + dhcp: 'dhcp (SLAAC & DHCPv6)', + disable: 'disable (Do not use IPv6)', + skip: 'skip (Do not configure IPv6)', +} + +/** @enum {string} Virtual Network Drivers names */ +export const VN_DRIVERS_STR = { + [VN_DRIVERS.bridge]: 'Bridged', + [VN_DRIVERS.fw]: 'Bridged & Security Groups', + [VN_DRIVERS.ebtables]: 'Bridged & ebtables VLAN', + [VN_DRIVERS.dot1Q]: '802.1Q', + [VN_DRIVERS.vxlan]: 'VXLAN', + [VN_DRIVERS.ovswitch]: 'Open vSwitch', + [VN_DRIVERS.ovswitch_vxlan]: 'Open vSwitch - VXLAN', + [VN_DRIVERS.vcenter]: 'vCenter', +} + /** * @enum {{ high: number, low: number }} * Virtual Network threshold to specify the maximum and minimum of the bar range diff --git a/src/fireedge/src/client/constants/networkTemplate.js b/src/fireedge/src/client/constants/networkTemplate.js index 5fdc3885fe..8483f795ef 100644 --- a/src/fireedge/src/client/constants/networkTemplate.js +++ b/src/fireedge/src/client/constants/networkTemplate.js @@ -39,7 +39,6 @@ export const VN_TEMPLATE_ACTIONS = { // INFORMATION RENAME: ACTIONS.RENAME, - CHANGE_MODE: ACTIONS.CHANGE_MODE, CHANGE_OWNER: ACTIONS.CHANGE_OWNER, CHANGE_GROUP: ACTIONS.CHANGE_GROUP, } diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index 185738da1f..cf18e13ad2 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -25,6 +25,8 @@ module.exports = { FilterBy: 'Filter by', FilterLabels: 'Filter labels', FilterByLabel: 'Filter by label', + First: 'First', + Last: 'Last', ApplyLabels: 'Apply labels', Label: 'Label', NoLabels: 'NoLabels', @@ -66,8 +68,9 @@ module.exports = { CreateMarketApp: 'Create Marketplace App', CreateProvider: 'Create Provider', CreateProvision: 'Create Provision', - CreateVmTemplate: 'Create VM Template', CreateServiceTemplate: 'Create Service Template', + CreateVirtualNetwork: 'Create VM Template', + CreateVmTemplate: 'Create VM Template', CurrentGroup: 'Current group: %s', CurrentOwner: 'Current owner: %s', Delete: 'Delete', @@ -75,8 +78,11 @@ module.exports = { DeleteDb: 'Delete database', DeleteScheduleAction: 'Delete schedule action: %s', DeleteSeveralTemplates: 'Delete several Templates', + DeleteSeveralVirtualNetworks: 'Delete several Virtual Networks', DeleteSomething: 'Delete: %s', + DeleteAddressRange: 'Delete Address Range', DeleteTemplate: 'Delete Template', + DeleteVirtualNetwork: 'Delete Virtual Network', Deploy: 'Deploy', DeployServiceTemplate: 'Deploy Service Template', Detach: 'Detach', @@ -111,10 +117,12 @@ module.exports = { Recreate: 'Recreate', Refresh: 'Refresh', Release: 'Release', + ReleaseIp: 'Release IP', Remove: 'Remove', Rename: 'Rename', RenameSomething: 'Rename: %s', Reschedule: 'Reschedule', + Reserve: 'Reserve', Resize: 'Resize', ResizeCapacity: 'Resize capacity', ResizeSomething: 'Resize: %s', @@ -164,11 +172,12 @@ module.exports = { UnReschedule: 'Un-Reschedule', Unshare: 'Unshare', Update: 'Update', - UpdateVmConfiguration: 'Update VM Configuration', UpdateProvider: 'Update Provider', UpdateScheduleAction: 'Update schedule action: %s', - UpdateVmTemplate: 'Update VM Template', UpdateServiceTemplate: 'Update Service Template', + UpdateVirtualNetwork: 'Update Virtual Network', + UpdateVmConfiguration: 'Update VM Configuration', + UpdateVmTemplate: 'Update VM Template', /* questions */ Yes: 'Yes', @@ -531,6 +540,7 @@ module.exports = { /* VM Template schema - general */ CustomHypervisor: 'Custom', CustomVariables: 'Custom Variables', + CustomAttributes: 'Custom Attributes', Hypervisor: 'Hypervisor', Logo: 'Logo', MakeNewImagePersistent: 'Make the new images persistent', @@ -807,6 +817,7 @@ module.exports = { NetworkAddress: 'Network address', NetworkMask: 'Network mask', Gateway: 'Gateway', + Gateway6: 'IPv6 Gateway', GatewayConcept: 'Default gateway for the network', Gateway6Concept: 'IPv6 router for this network', SearchDomainForDNSResolution: 'Search domains for DNS resolution', @@ -815,9 +826,9 @@ module.exports = { NetworkMethod6Concept: 'Sets IPv6 guest conf. method for NIC in this network', DNS: 'DNS', DNSConcept: 'DNS servers, a space separated list of servers', - AverageBandwidth: 'Average bandwidth (KBytes/s)', - PeakBandwidth: 'Peak bandwidth (KBytes/s)', - PeakBurst: 'Peak burst (KBytes)', + AverageBandwidth: 'Average bandwidth', + PeakBandwidth: 'Peak bandwidth', + PeakBurst: 'Peak burst', InboundAverageBandwidthConcept: 'Average bitrate for the interface in kilobytes/second for inbound traffic', InboundPeakBandwidthConcept: @@ -832,8 +843,10 @@ module.exports = { TransmissionQueue: 'Transmission queue', OnlySupportedForVirtioDriver: 'Only supported for virtio driver', GuestOptions: 'Guest options', - GuestMTU: 'GuestMTU', + GuestMTU: 'MTU of the Guest interfaces', GuestMTUConcept: 'Sets the MTU for the NICs in this network', + NetMethod: 'Method', + NetMethod6: 'IPv6 Method', UsedLeases: 'Used leases', TotalLeases: 'Total leases', TotalClusters: 'Total clusters', @@ -845,8 +858,63 @@ module.exports = { States for success/failure recovers: LOCK_CREATE, LOCK_DELETE state. States for a retry recover: LOCK_CREATE, LOCK_DELETE state. States for delete: Any but READY.`, + ReservationParent: 'Reservation parent', + ReservedFromVNetId: 'Reserved from VNET %s', + /* Virtual Network schema - driver configuration */ + NetworkMode: 'Network mode', + Bridge: 'Bridge', + BridgeConcept: 'Name of the physical bridge in the nodes to attach VM NICs', + PhysicalDevice: 'Physical device', + PhysicalDeviceConcept: 'Node NIC to send/receive virtual network traffic', + MacSpoofingFilter: ' MAC spoofing filter', + IpSpoofingFilter: ' IP spoofing filter', + MTU: 'MTU of the interface', + MTUConcept: 'Maximum Transmission Unit', + VlanId: 'VLAN ID', + AutomaticVlanId: 'Automatic VLAN ID', + VxlanMode: 'VXLAN mode', + VxlanModeConcept: 'Multicast protocol for multi destination BUM traffic', + VxlanTunnelEndpoint: 'VXLAN Tunnel endpoint', + VxlanTunnelEndpointConcept: 'Tunnel endpoint communication type', + VxlanMulticast: 'VXLAN Multicast', + VxlanMulticastConcept: + 'Base multicast address for each VLAN. The MC address is :vxlan_mc + :vlan_id', + IpConfiguration: 'IP Configuration', + IpConfigurationConcept: + 'Options passed to ip cmd on operations specific to this Virtual Network', + OuterVlanId: 'Outer VLAN ID', + AutomaticOuterVlanId: 'Automatic Outer VLAN ID', + InvalidAttribute: 'Invalid attribute', + /* Virtual Network schema - address range */ + Addresses: 'Addresses', + AddressRange: 'Address Range', + FirstIPv4Address: 'First IPv4 address', + FirstMacAddress: 'First MAC address', + SLAAC: 'SLAAC', + IPv6GlobalPrefix: 'IPv6 Global prefix', + IPv6ULAPrefix: 'IPv6 ULA prefix', + IPAMDriver: 'IPAM driver', + InvalidAddress: 'Invalid address', + InvalidIPv4: 'Invalid IPv4', + InvalidMAC: 'Invalid MAC', + DisabledAddressRangeInForm: + 'Address Ranges need to be managed in the individual Virtual Network panel', + /* Virtual Network schema - QoS */ + QoS: 'QoS', + InboundTraffic: 'Inbound traffic', + OutboundTraffic: 'Outbound traffic', + /* Virtual Network schema - reserve */ + ReservationFromVirtualNetwork: 'Reservation from Virtual Network', + CanSelectAddressFromAR: + 'You can select the addresses from an specific Address Range', + NumberOfAddresses: 'Number of addresses', + AddToNewVirtualNetwork: 'Add to a new Virtual Network', + AddToExistingReservation: 'Add to an existing Reservation', + FirstAddress: 'First address', + IpOrMac: 'IP or MAC', /* security group schema */ + Security: 'Security', TCP: 'TCP', UDP: 'UDP', ICMP: 'ICMP', diff --git a/src/fireedge/src/client/containers/VirtualNetworks/Create.js b/src/fireedge/src/client/containers/VirtualNetworks/Create.js new file mode 100644 index 0000000000..19da94f235 --- /dev/null +++ b/src/fireedge/src/client/containers/VirtualNetworks/Create.js @@ -0,0 +1,79 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +import { ReactElement } from 'react' +import { useHistory, useLocation } from 'react-router' + +import { useGeneralApi } from 'client/features/General' +import { + useUpdateVNetMutation, + useAllocateVnetMutation, + useGetVNetworkQuery, +} from 'client/features/OneApi/network' + +import { + DefaultFormStepper, + SkeletonStepsForm, +} from 'client/components/FormStepper' +import { CreateForm } from 'client/components/Forms/VNetwork' +import { PATH } from 'client/apps/sunstone/routesOne' + +/** + * Displays the creation or modification form to a Virtual Network. + * + * @returns {ReactElement} Virtual Network form + */ +function CreateVirtualNetwork() { + const history = useHistory() + const { state: { ID: vnetId, NAME } = {} } = useLocation() + + const { enqueueSuccess } = useGeneralApi() + const [update] = useUpdateVNetMutation() + const [allocate] = useAllocateVnetMutation() + + const { data } = useGetVNetworkQuery( + { id: vnetId, extended: true }, + { skip: vnetId === undefined } + ) + + const onSubmit = async (xml) => { + try { + if (!vnetId) { + const newVnetId = await allocate({ template: xml }).unwrap() + enqueueSuccess(`Virtual Network created - #${newVnetId}`) + } else { + await update({ id: vnetId, template: xml }).unwrap() + enqueueSuccess(`Virtual Network updated - #${vnetId} ${NAME}`) + } + + history.push(PATH.NETWORK.VNETS.LIST) + } catch {} + } + + return vnetId && !data ? ( + + ) : ( + } + > + {(config) => } + + ) +} + +export default CreateVirtualNetwork diff --git a/src/fireedge/src/client/containers/VirtualNetworks/index.js b/src/fireedge/src/client/containers/VirtualNetworks/index.js index 13556a1991..581a352eb1 100644 --- a/src/fireedge/src/client/containers/VirtualNetworks/index.js +++ b/src/fireedge/src/client/containers/VirtualNetworks/index.js @@ -15,14 +15,22 @@ * ------------------------------------------------------------------------- */ import { ReactElement, useState, memo } from 'react' import PropTypes from 'prop-types' -import { BookmarkEmpty } from 'iconoir-react' -import { Typography, Box, Stack, Chip, IconButton } from '@mui/material' +import GotoIcon from 'iconoir-react/dist/Pin' +import RefreshDouble from 'iconoir-react/dist/RefreshDouble' +import Cancel from 'iconoir-react/dist/Cancel' +import { Typography, Box, Stack, Chip } from '@mui/material' import { Row } from 'react-table' +import { + useLazyGetVNetworkQuery, + useUpdateVNetMutation, +} from 'client/features/OneApi/network' import { VNetworksTable } from 'client/components/Tables' +import VNetworkActions from 'client/components/Tables/VNetworks/actions' import VNetworkTabs from 'client/components/Tabs/VNetwork' import SplitPane from 'client/components/SplitPane' import MultipleTags from 'client/components/MultipleTags' +import { SubmitButton } from 'client/components/FormControl' import { Tr } from 'client/components/HOC' import { T, VirtualNetwork } from 'client/constants' @@ -33,6 +41,7 @@ import { T, VirtualNetwork } from 'client/constants' */ function VirtualNetworks() { const [selectedRows, onSelectedRowsChange] = useState(() => []) + const actions = VNetworkActions() const hasSelectedRows = selectedRows?.length > 0 const moreThanOneSelected = selectedRows?.length > 1 @@ -41,7 +50,11 @@ function VirtualNetworks() { {({ getGridProps, GutterComponent }) => ( - + {hasSelectedRows && ( <> @@ -52,6 +65,7 @@ function VirtualNetworks() { selectedRows[0]?.toggleRowSelected(false)} /> )} @@ -67,27 +81,53 @@ function VirtualNetworks() { * * @param {VirtualNetwork} vnet - Virtual Network to display * @param {Function} [gotoPage] - Function to navigate to a page of a Virtual Network + * @param {Function} [unselect] - Function to unselect * @returns {ReactElement} Virtual Network details */ -const InfoTabs = memo(({ vnet, gotoPage }) => ( - - - - {`#${vnet.ID} | ${vnet.NAME}`} - - {gotoPage && ( - - - - )} +const InfoTabs = memo(({ vnet, gotoPage, unselect }) => { + const [get, { data: lazyData, isFetching }] = useLazyGetVNetworkQuery() + const id = lazyData?.ID ?? vnet.ID + const name = lazyData?.NAME ?? vnet.NAME + + return ( + + + } + tooltip={Tr(T.Refresh)} + isSubmitting={isFetching} + onClick={() => get({ id })} + /> + {typeof gotoPage === 'function' && ( + } + tooltip={Tr(T.LocateOnTable)} + onClick={() => gotoPage()} + /> + )} + {typeof unselect === 'function' && ( + } + tooltip={Tr(T.Close)} + onClick={() => unselect()} + /> + )} + + {`#${id} | ${name}`} + + + - - -)) + ) +}) InfoTabs.propTypes = { - vnet: PropTypes.object.isRequired, + vnet: PropTypes.object, gotoPage: PropTypes.func, + unselect: PropTypes.func, } InfoTabs.displayName = 'InfoTabs' diff --git a/src/fireedge/src/client/features/OneApi/socket.js b/src/fireedge/src/client/features/OneApi/socket.js index 7df0feb747..ef276ecc1d 100644 --- a/src/fireedge/src/client/features/OneApi/socket.js +++ b/src/fireedge/src/client/features/OneApi/socket.js @@ -15,10 +15,12 @@ * ------------------------------------------------------------------------- */ import { ThunkDispatch, ThunkAction } from 'redux-thunk' import socketIO, { Socket } from 'socket.io-client' + +import { updateResourceOnPool } from 'client/features/OneApi/common' import { WEBSOCKET_URL, SOCKETS } from 'client/constants' /** - * @typedef {'VM'|'HOST'|'IMAGE'|'VNET'} HookObjectName + * @typedef {'VM'|'HOST'|'IMAGE'|'NET'} HookObjectName * - Hook object name to update from socket */ @@ -42,7 +44,7 @@ import { WEBSOCKET_URL, SOCKETS } from 'client/constants' * @property {object} [VM] - New data of the VM * @property {object} [HOST] - New data of the HOST * @property {object} [IMAGE] - New data of the IMAGE - * @property {object} [VNET] - New data of the VNET + * @property {object} [NET] - New data of the VNET */ /** @@ -66,9 +68,19 @@ const createWebsocket = (path, query) => * @returns {object} - New value of resource from socket */ const getResourceValueFromEventState = (data) => { - const { HOOK_OBJECT: name, [name]: value } = data?.HOOK_MESSAGE ?? {} + const hookMessage = data?.HOOK_MESSAGE || {} - return value + const { + HOOK_OBJECT: name, + [name]: valueFromObjectName, + /** + * Virtual Network object Type is NET, + * but in the `HOOK_OBJECT` (object XML) is VNET + */ + NET, + } = hookMessage + + return valueFromObjectName ?? NET } /** @@ -102,12 +114,8 @@ const UpdateFromSocket = if (!value) return - dispatch( - updateQueryData((draft) => { - const index = draft.findIndex(({ ID }) => +ID === +id) - index !== -1 ? (draft[index] = value) : draft.push(value) - }) - ) + const update = updateResourceOnPool({ id, resourceFromQuery: value }) + dispatch(updateQueryData(update)) updateCachedData((draft) => { Object.assign(draft, value) diff --git a/src/fireedge/src/client/models/Helper.js b/src/fireedge/src/client/models/Helper.js index c61c1ebcac..8b24a3bd34 100644 --- a/src/fireedge/src/client/models/Helper.js +++ b/src/fireedge/src/client/models/Helper.js @@ -21,7 +21,7 @@ import { J2xOptions, } from 'fast-xml-parser' -import { camelCase } from 'client/utils' +import { sentenceCase } from 'client/utils' import { T, Permission, @@ -318,12 +318,11 @@ export const getAvailableInfoTabs = (infoTabs = {}, getTabComponent, id) => Object.entries(infoTabs) ?.filter(([_, { enabled } = {}]) => !!enabled) ?.map(([tabName, tabProps]) => { - const camelName = camelCase(tabName) - const TabContent = getTabComponent?.(camelName) + const TabContent = getTabComponent?.(tabName) return ( TabContent && { - label: camelName, + label: TabContent?.label ?? sentenceCase(tabName), id: tabName, renderContent: () => , } diff --git a/src/fireedge/src/client/models/VirtualNetwork.js b/src/fireedge/src/client/models/VirtualNetwork.js index e6fd8a08b8..2312e28f35 100644 --- a/src/fireedge/src/client/models/VirtualNetwork.js +++ b/src/fireedge/src/client/models/VirtualNetwork.js @@ -13,7 +13,13 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { VirtualNetwork, VN_STATES, STATES } from 'client/constants' +import { isIPv6, isIPv4, isMAC } from 'client/utils' +import { + VirtualNetwork, + AddressRange, + VN_STATES, + STATES, +} from 'client/constants' /** * Returns the state of the virtual network. @@ -58,3 +64,31 @@ export const getLeasesInfo = ({ USED_LEASES, ...virtualNetwork } = {}) => { return { percentOfUsed, percentLabel } } + +/** + * Returns the address range leases information. + * + * @param {AddressRange} ar - Address range + * @returns {{ percentOfUsed: number, percentLabel: string }} Leases information + */ +export const getARLeasesInfo = ({ USED_LEASES, SIZE } = {}) => { + const percentOfUsed = (+USED_LEASES * 100) / +SIZE || 0 + const percentLabel = `${USED_LEASES} / ${SIZE} (${Math.round( + percentOfUsed + )}%)` + + return { percentOfUsed, percentLabel } +} + +/** + * Checks the address type: IP, IP6 or MAC + * Otherwise returns undefined. + * + * @param {string} addr - Address to check + * @returns {'IP'|'IP6'|'MAC'|undefined} Returns name of address type, undefined otherwise + */ +export const getAddressType = (addr) => { + if (isIPv4(addr)) return 'IP' + if (isIPv6(addr)) return 'IP6' + if (isMAC(addr)) return 'MAC' +} diff --git a/src/fireedge/src/client/utils/index.js b/src/fireedge/src/client/utils/index.js index 9c3a6eb358..547e24a726 100644 --- a/src/fireedge/src/client/utils/index.js +++ b/src/fireedge/src/client/utils/index.js @@ -15,6 +15,7 @@ * ------------------------------------------------------------------------- */ export * from 'client/utils/environments' export * from 'client/utils/helpers' +export * from 'client/utils/ip' export * from 'client/utils/merge' export * from 'client/utils/parser' export * from 'client/utils/request' diff --git a/src/fireedge/src/client/utils/ip.js b/src/fireedge/src/client/utils/ip.js new file mode 100644 index 0000000000..ad48081afe --- /dev/null +++ b/src/fireedge/src/client/utils/ip.js @@ -0,0 +1,77 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2022, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ + +// reference to `src/oca/ruby/opennebula/virtual_network.rb` +const mac = '([a-fA-F\\d]{2}:){5}[a-fA-F\\d]{2}' + +const v4 = + '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}' + +const v6segment = '[a-fA-F\\d]{1,4}' + +const v6 = ` +(?: +(?:${v6segment}:){7}(?:${v6segment}|:)| +(?:${v6segment}:){6}(?:${v4}|:${v6segment}|:)| +(?:${v6segment}:){5}(?::${v4}|(?::${v6segment}){1,2}|:)| +(?:${v6segment}:){4}(?:(?::${v6segment}){0,1}:${v4}|(?::${v6segment}){1,3}|:)| +(?:${v6segment}:){3}(?:(?::${v6segment}){0,2}:${v4}|(?::${v6segment}){1,4}|:)| +(?:${v6segment}:){2}(?:(?::${v6segment}){0,3}:${v4}|(?::${v6segment}){1,5}|:)| +(?:${v6segment}:){1}(?:(?::${v6segment}){0,4}:${v4}|(?::${v6segment}){1,6}|:)| +(?::(?:(?::${v6segment}){0,5}:${v4}|(?::${v6segment}){1,7}|:)) +)(?:%[0-9a-zA-Z]{1,})? +` + .replace(/\n/g, '') + .trim() + +// Pre-compile only the exact regexes because adding a global flag make regexes stateful +export const REG_ADDR = new RegExp(`(?:^${v4}$)|(?:^${v6}$)|(?:^${mac}$)`) +export const REG_IP = new RegExp(`(?:^${v4}$)|(?:^${v6}$)`) +export const REG_V4 = new RegExp(`^${v4}$`) +export const REG_V6 = new RegExp(`^${v6}$`) +export const REG_MAC = new RegExp(`^${mac}$`) + +/** + * Checks if string is IPv6 or IPv4. + * + * @param {string} string - String to check + * @returns {boolean} Returns `true` if the given value is an IP + */ +export const isIP = (string) => REG_IP.test(string) + +/** + * Checks if string is IPv6. + * + * @param {string} string - String to check + * @returns {boolean} Returns `true` if the given value is an IPv6 + */ +export const isIPv6 = (string) => REG_V6.test(string) + +/** + * Checks if string is IPv4. + * + * @param {string} string - String to check + * @returns {boolean} Returns `true` if the given value is an IPv4 + */ +export const isIPv4 = (string) => REG_V4.test(string) + +/** + * Checks if string is MAC address. + * + * @param {string} string - String to check + * @returns {boolean} Returns `true` if the given value is a MAC address + */ +export const isMAC = (string) => REG_MAC.test(string) diff --git a/src/fireedge/src/client/utils/translation.js b/src/fireedge/src/client/utils/translation.js index 310325175a..ba8219ed77 100644 --- a/src/fireedge/src/client/utils/translation.js +++ b/src/fireedge/src/client/utils/translation.js @@ -42,7 +42,8 @@ const buildMethods = () => { let result = resolvedSchema._cast(value, options) if (options.isSubmit) { - result = this.submit?.(result, options) ?? result + const needChangeAfterSubmit = typeof this.submit === 'function' + needChangeAfterSubmit && (result = this.submit(result, options)) } return result