diff --git a/install.sh b/install.sh index 97318df647..eeb9b03243 100755 --- a/install.sh +++ b/install.sh @@ -2767,7 +2767,7 @@ SUNSTONE_PUBLIC_FONT_AWSOME="src/sunstone/public/bower_components/fontawesome/we src/sunstone/public/bower_components/fontawesome/web-fonts-with-css/webfonts/fa-solid-900.woff2" SUNSTONE_PUBLIC_IMAGES_FILES="src/sunstone/public/images/ajax-loader.gif \ - src/sunstone/public/images/favicon.ico \ + src/sunstone/public/images/favicon.svg \ src/sunstone/public/images/advanced_layout.png \ src/sunstone/public/images/cloud_layout.png \ src/sunstone/public/images/vcenter_layout.png \ @@ -2905,11 +2905,13 @@ FIREEDGE_SUNSTONE_ETC="src/fireedge/etc/sunstone/sunstone-server.conf \ FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN="src/fireedge/etc/sunstone/admin/vm-tab.yaml \ src/fireedge/etc/sunstone/admin/vm-template-tab.yaml \ src/fireedge/etc/sunstone/admin/marketplace-app-tab.yaml \ + src/fireedge/etc/sunstone/admin/vnet-tab.yaml \ src/fireedge/etc/sunstone/admin/host-tab.yaml" FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml \ src/fireedge/etc/sunstone/user/vm-template-tab.yaml \ - src/fireedge/etc/sunstone/user/marketplace-app-tab.yaml" + src/fireedge/etc/sunstone/user/marketplace-app-tab.yaml \ + src/fireedge/etc/sunstone/user/vnet-tab.yaml" #----------------------------------------------------------------------------- # OneGate files diff --git a/src/dm/DispatchManagerStates.cc b/src/dm/DispatchManagerStates.cc index 63e61c3444..33d8b6bf46 100644 --- a/src/dm/DispatchManagerStates.cc +++ b/src/dm/DispatchManagerStates.cc @@ -200,8 +200,6 @@ void DispatchManager::trigger_poweroff_success(int vid) return; } - VirtualMachine::LcmState prev_state = vm->get_lcm_state(); - if ((vm->get_state() == VirtualMachine::ACTIVE) && (vm->get_lcm_state() == VirtualMachine::SHUTDOWN_POWEROFF || vm->get_lcm_state() == VirtualMachine::HOTPLUG_PROLOG_POWEROFF || @@ -235,9 +233,7 @@ void DispatchManager::trigger_poweroff_success(int vid) vm.reset(); - if (prev_state != VirtualMachine::DISK_SNAPSHOT_POWEROFF && - prev_state != VirtualMachine::DISK_SNAPSHOT_REVERT_POWEROFF && - prev_state != VirtualMachine::DISK_SNAPSHOT_DELETE_POWEROFF) + if (!quota_tmpl.empty()) { Quotas::vm_del(uid, gid, "a_tmpl); } diff --git a/src/fireedge/etc/sunstone/admin/vnet-tab.yaml b/src/fireedge/etc/sunstone/admin/vnet-tab.yaml index d3b27453a6..436fb25318 100644 --- a/src/fireedge/etc/sunstone/admin/vnet-tab.yaml +++ b/src/fireedge/etc/sunstone/admin/vnet-tab.yaml @@ -25,17 +25,16 @@ resource_name: "VIRTUAL-NETWORK" actions: create_dialog: true - instantiate_dialog: true import_dialog: true - update_dialog: true + instantiate_dialog: true reserve_dialog: true + update_dialog: true change_cluster: true chown: true chgrp: true - delete: true lock: true unlock: true - recover: true + delete: true # Filters - List of criteria to filter the resources @@ -88,13 +87,13 @@ info-tabs: hold_lease: true release_lease: true - sec-group: + security: enabled: true actions: add_secgroup: true - remove_secgroup: true + delete_secgroup: true - virtual-router: + virtual_router: enabled: true cluster: diff --git a/src/fireedge/etc/sunstone/user/vnet-tab.yaml b/src/fireedge/etc/sunstone/user/vnet-tab.yaml new file mode 100644 index 0000000000..1e1e995e51 --- /dev/null +++ b/src/fireedge/etc/sunstone/user/vnet-tab.yaml @@ -0,0 +1,100 @@ +# -------------------------------------------------------------------------- # +# 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. # +#--------------------------------------------------------------------------- # + +--- +# This file describes the information and actions available in the VIRTUAL NETWORK tab + +# Resource + +resource_name: "VIRTUAL-NETWORK" + +# Actions - Which buttons are visible to operate over the resources + +actions: + create_dialog: false + import_dialog: false + instantiate_dialog: true + reserve_dialog: true + update_dialog: true + change_cluster: false + chown: false + chgrp: false + lock: true + unlock: true + delete: true + +# Filters - List of criteria to filter the resources + +filters: + label: true + state: true + owner: true + group: true + locked: true + vn_mad: true + +# Info Tabs - Which info tabs are used to show extended information + +info-tabs: + info: + enabled: true + information_panel: + enabled: true + actions: + rename: true + permissions_panel: + enabled: true + actions: + chmod: true + ownership_panel: + enabled: true + actions: + chown: false + chgrp: false + qos_panel: + enabled: true + attributes_panel: + enabled: true + actions: + copy: true + add: true + edit: true + delete: true + + address: + enabled: true + actions: + add_ar: false + update_ar: true + delete_ar: true + + lease: + enabled: true + actions: + hold_lease: true + release_lease: true + + security: + enabled: true + actions: + add_secgroup: true + delete_secgroup: true + + virtual_router: + enabled: true + + cluster: + enabled: true 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/MarketplaceApp/CreateForm/Steps/BasicConfiguration/schema.js b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration/schema.js index 7309aba2d0..d4d8024693 100644 --- a/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration/schema.js +++ b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration/schema.js @@ -61,7 +61,7 @@ const TYPE = { /** @type {Field} App name field */ const NAME = { - name: 'name', + name: 'vmname', label: T.Name, type: INPUT_TYPES.TEXT, validation: string() 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/Forms/Vm/UpdateConfigurationForm/context/schema.js b/src/fireedge/src/client/components/Forms/Vm/UpdateConfigurationForm/context/schema.js index 04bcedeacb..d6c0ed7a11 100644 --- a/src/fireedge/src/client/components/Forms/Vm/UpdateConfigurationForm/context/schema.js +++ b/src/fireedge/src/client/components/Forms/Vm/UpdateConfigurationForm/context/schema.js @@ -19,6 +19,7 @@ import { CONFIGURATION_SCHEMA, FILES_SCHEMA, } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/schema' +import { HYPERVISORS } from 'client/constants' /** * @param {object} [formProps] - Form props diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSchema.js index dc6c5c133b..d4d71be503 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSchema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSchema.js @@ -28,15 +28,15 @@ import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants' const { vcenter, lxc, firecracker } = HYPERVISORS const transformPciToString = (pciDevice = {}) => { - const { DEVICE = '', VENDOR = '', CLASS = '' } = pciDevice + const { DEVICE = '', VENDOR = '', CLASS = '', PROFILES = '' } = pciDevice - return [DEVICE, VENDOR, CLASS].join(',') + return [DEVICE, VENDOR, CLASS, PROFILES].join(';') } const getPciAttributes = (pciDevice = '') => { - const [DEVICE, VENDOR, CLASS] = pciDevice.split(',') + const [DEVICE, VENDOR, CLASS, PROFILES] = pciDevice.split(';') - return { DEVICE, VENDOR, CLASS } + return { DEVICE, VENDOR, CLASS, PROFILES } } /** @type {Field} Name PCI device field */ @@ -58,6 +58,33 @@ const NAME_FIELD = { grid: { sm: 12, md: 3 }, } +/** @type {Field} Name PCI device field */ +const PROFILE_FIELD = { + name: 'PROFILE', + label: T.Profile, + notOnHypervisors: [vcenter, lxc, firecracker], + type: INPUT_TYPES.SELECT, + values: (pciDevice) => { + if (pciDevice) { + const { PROFILES } = getPciAttributes(pciDevice) + const profiles = PROFILES.trim() === '' ? [] : PROFILES.split(',') + + return arrayToOptions(profiles) + } + + return arrayToOptions([]) + }, + dependOf: NAME_FIELD.name, + htmlType: (pciDevice) => { + const { PROFILES } = getPciAttributes(pciDevice) + const emptyProfiles = !PROFILES || PROFILES === '' || PROFILES === '-' + + return emptyProfiles && INPUT_TYPES.HIDDEN + }, + validation: string().trim().notRequired(), + grid: { sm: 12, md: 3 }, +} + /** @type {Field} Common field properties */ const commonFieldProps = (name) => ({ name, @@ -73,7 +100,7 @@ const commonFieldProps = (name) => ({ }, validation: string().trim().required(), fieldProps: { disabled: true }, - grid: { xs: 12, sm: 4, md: 3 }, + grid: { xs: 12, sm: 3, md: 2 }, }) /** @type {Field} PCI device field */ @@ -91,12 +118,13 @@ const CLASS_FIELD = { label: T.Class, ...commonFieldProps('CLASS') } */ export const PCI_FIELDS = (hypervisor) => filterFieldsByHypervisor( - [NAME_FIELD, DEVICE_FIELD, VENDOR_FIELD, CLASS_FIELD], + [NAME_FIELD, PROFILE_FIELD, DEVICE_FIELD, VENDOR_FIELD, CLASS_FIELD], hypervisor ) /** @type {ObjectSchema} PCI devices object schema */ export const PCI_SCHEMA = getObjectSchemaFromFields([ + PROFILE_FIELD, DEVICE_FIELD, VENDOR_FIELD, CLASS_FIELD, diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSection.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSection.js index eca6505716..a5b300a8af 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSection.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/pciDevicesSection.js @@ -65,6 +65,7 @@ const PciDevicesSection = ({ stepId, hypervisor }) => { }) const onSubmit = (newInput) => { + delete newInput.DEVICE_NAME append(newInput) methods.reset() } @@ -101,34 +102,42 @@ const PciDevicesSection = ({ stepId, hypervisor }) => { - {pciDevices?.map(({ id, DEVICE, VENDOR, CLASS }, index) => { - const { DEVICE_NAME, VENDOR_NAME } = - pciDevicesAvailable.find( - (pciDevice) => pciDevice?.DEVICE === DEVICE - ) ?? {} + {pciDevices?.map( + ({ id, DEVICE, VENDOR, CLASS, PROFILE = '-' }, index) => { + const { DEVICE_NAME, VENDOR_NAME } = + pciDevicesAvailable.find( + (pciDevice) => pciDevice?.DEVICE === DEVICE + ) ?? {} - return ( - remove(index)}> - - - } - sx={{ '&:hover': { bgcolor: 'action.hover' } }} - > - - - ) - })} + const secondaryFields = [ + `#${DEVICE}`, + `${T.Vendor}: ${VENDOR_NAME}(${VENDOR})`, + `${T.Class}: ${CLASS}`, + ] + + if (PROFILE !== '' && PROFILE !== '-') { + secondaryFields.push(`${T.Profile}: ${PROFILE}`) + } + + return ( + remove(index)}> + + + } + sx={{ '&:hover': { bgcolor: 'action.hover' } }} + > + + + ) + } + )} ) 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/MarketplaceApps/actions.js b/src/fireedge/src/client/components/Tables/MarketplaceApps/actions.js index 61d2573d2d..b2ed9a07ef 100644 --- a/src/fireedge/src/client/components/Tables/MarketplaceApps/actions.js +++ b/src/fireedge/src/client/components/Tables/MarketplaceApps/actions.js @@ -228,7 +228,7 @@ const Actions = () => { icon: Group, selected: true, color: 'secondary', - dataCy: 'vm-ownership', + dataCy: 'marketapp-ownership', options: [ { accessor: MARKETPLACE_APP_ACTIONS.CHANGE_OWNER, 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/MarketplaceApp/Info/information.js b/src/fireedge/src/client/components/Tabs/MarketplaceApp/Info/information.js index c2dc570326..73fc73ee41 100644 --- a/src/fireedge/src/client/components/Tabs/MarketplaceApp/Info/information.js +++ b/src/fireedge/src/client/components/Tabs/MarketplaceApp/Info/information.js @@ -82,8 +82,13 @@ const InformationPanel = ({ app = {}, actions }) => { { name: T.State, value: , + dataCy: 'state', + }, + { + name: T.Locked, + value: levelLockToString(LOCK?.LOCKED), + dataCy: 'locked', }, - { name: T.Locked, value: levelLockToString(LOCK?.LOCKED) }, { name: T.Format, value: FORMAT }, { name: T.Version, value: VERSION }, ] 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/Snapshot/index.js b/src/fireedge/src/client/components/Tabs/Vm/Snapshot/index.js index dcc45b1ae2..dbd1bfd054 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Snapshot/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Snapshot/index.js @@ -15,7 +15,8 @@ * ------------------------------------------------------------------------- */ import { ReactElement, useMemo } from 'react' import PropTypes from 'prop-types' -import { Stack } from '@mui/material' +import HintIcon from 'iconoir-react/dist/QuestionMarkCircle' +import { Stack, Tooltip } from '@mui/material' import { useGetVmQuery } from 'client/features/OneApi/vm' import { @@ -24,6 +25,7 @@ import { DeleteAction, } from 'client/components/Tabs/Vm/Snapshot/Actions' import SnapshotCard from 'client/components/Cards/SnapshotCard' +import { Tr } from 'client/components/HOC' import { getSnapshotList, @@ -31,7 +33,7 @@ import { isAvailableAction, } from 'client/models/VirtualMachine' import { getActionsAvailable } from 'client/models/Helper' -import { VM_ACTIONS } from 'client/constants' +import { T, VM_ACTIONS } from 'client/constants' const { SNAPSHOT_CREATE, SNAPSHOT_REVERT, SNAPSHOT_DELETE } = VM_ACTIONS @@ -59,9 +61,14 @@ const VmSnapshotTab = ({ tabProps: { actions } = {}, id }) => { return (
- {actionsAvailable?.includes(SNAPSHOT_CREATE) && ( - - )} + + {actionsAvailable?.includes(SNAPSHOT_CREATE) && ( + + )} + + + + {snapshots.map((snapshot) => ( diff --git a/src/fireedge/src/client/components/Tabs/Vm/index.js b/src/fireedge/src/client/components/Tabs/Vm/index.js index 387d5fbfd3..7fcb476cd7 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/index.js @@ -37,7 +37,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 8d19e1e44b..979053abae 100644 --- a/src/fireedge/src/client/constants/host.js +++ b/src/fireedge/src/client/constants/host.js @@ -17,22 +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 - 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 0a5b3e061c..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', @@ -497,6 +506,9 @@ module.exports = { HostnamePortsForwardedToVmPorts: '%1$s ports %2$s forwarded to VM ports %3$s', /* VM schema - snapshot */ VmSnapshotNameConcept: 'The new snapshot name. It can be empty', + VmSnapshotHint: ` + Snapshots in this tab refer to System Snapshots, which includes all disks and + the memory state of the VM. For disk snapshots, see the Storage tab`, /* VM schema - actions */ EnforceCapacityChecks: 'Enforce capacity checks', EnforceCapacityChecksConcept: ` @@ -528,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', @@ -720,6 +733,7 @@ module.exports = { Input: 'Input', Inputs: 'Inputs', PciDevices: 'PCI Devices', + Profile: 'Profile', DeviceName: 'Device name', Device: 'Device', Vendor: 'Vendor', @@ -803,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', @@ -811,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: @@ -828,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', @@ -841,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 diff --git a/src/oca/ruby/opennebula/marketplaceapp_ext.rb b/src/oca/ruby/opennebula/marketplaceapp_ext.rb index 8ff1d1c1e9..6c341e975f 100644 --- a/src/oca/ruby/opennebula/marketplaceapp_ext.rb +++ b/src/oca/ruby/opennebula/marketplaceapp_ext.rb @@ -603,6 +603,7 @@ module OpenNebula::MarketPlaceAppExt # :names = [name_a, name_b, ...] exported = {} idx = 0 + idy = 0 # Store IDs of created resources images = [] @@ -627,9 +628,25 @@ module OpenNebula::MarketPlaceAppExt obj.extend(MarketPlaceAppExt) + # Fix name if duplcates exist + imgp = OpenNebula::ImagePool.new(@client) + imgp.info + img_names = imgp.retrieve_elements('/IMAGE_POOL/IMAGE/NAME') + + opt_name = options[:name] + + if img_names.include? "#{opt_name}-#{obj_name}-#{idx}" + idy = 0 + while img_names.include? \ + "#{opt_name}_#{idy}-#{obj_name}-#{idx}" + idy += 1 + end + opt_name = "#{opt_name}_#{idy}" + end + rc = obj.export( :dsid => options[:dsid], - :name => "#{options[:name]}-#{obj_name}-#{idx}", + :name => "#{opt_name}-#{obj_name}-#{idx}", :notemplate => options[:notemplate] ) diff --git a/src/onedb/fsck.rb b/src/onedb/fsck.rb index 6b5a314b41..a92a73c79b 100644 --- a/src/onedb/fsck.rb +++ b/src/onedb/fsck.rb @@ -150,6 +150,26 @@ EOT true end + # Check if object UID/GID exist + # + # @param doc [XML] XML document + def check_ugid(doc) + obj = doc.root.name + id = doc.root.at_xpath('ID').text + uid = Integer(doc.root.at_xpath('UID').text) + gid = Integer(doc.root.at_xpath('GID').text) + + return if @users.include?(uid) && @groups.include?(gid) + + if !@users.include?(uid) + log_error("#{obj} ID=#{id}, UID=#{uid} doesn't exist", false) + end + + if !@groups.include?(gid) + log_error("#{obj} ID=#{id}, GID=#{uid} doesn't exist", false) + end + end + ######################################################################## # Acl ######################################################################## @@ -241,6 +261,15 @@ EOT end def fsck + # ---------------------------------------------------------------------- + # Read existing UIDs and GIDs + # ---------------------------------------------------------------------- + @users = [] + @groups = [] + + @db.fetch('SELECT oid FROM user_pool') do |r| @users << r[:oid] end + @db.fetch('SELECT oid FROM group_pool') do |r| @groups << r[:oid] end + init_log_time() @errors = 0 diff --git a/src/onedb/fsck/datastore.rb b/src/onedb/fsck/datastore.rb index 15f9b3fb32..eca174b209 100644 --- a/src/onedb/fsck/datastore.rb +++ b/src/onedb/fsck/datastore.rb @@ -23,6 +23,8 @@ module OneDBFsck images_elem = doc.root.xpath('IMAGES').remove images_new_elem = doc.create_element('IMAGES') + check_ugid(doc) + doc.root.add_child(images_new_elem) datastore[ds_id][:images].each do |id| diff --git a/src/onedb/fsck/image.rb b/src/onedb/fsck/image.rb index 8e82bef1a1..0d0b6cd207 100644 --- a/src/onedb/fsck/image.rb +++ b/src/onedb/fsck/image.rb @@ -35,9 +35,10 @@ module OneDBFsck @db.transaction do @db[:image_pool].each do |row| doc = nokogiri_doc(row[:body], 'image_pool') - oid = row[:oid] + check_ugid(doc) + persistent = ( doc.root.xpath('PERSISTENT').text == "1" ) current_state = doc.root.xpath('STATE').text.to_i diff --git a/src/onedb/fsck/marketplace.rb b/src/onedb/fsck/marketplace.rb index d07ae8cb43..f7cd539b78 100644 --- a/src/onedb/fsck/marketplace.rb +++ b/src/onedb/fsck/marketplace.rb @@ -19,6 +19,8 @@ module OneDBFsck market_id = row[:oid] doc = nokogiri_doc(row[:body], 'marketplace_pool') + check_ugid(doc) + apps_elem = doc.root.at_xpath("MARKETPLACEAPPS") apps_elem.remove if !apps_elem.nil? diff --git a/src/onedb/fsck/marketplaceapp.rb b/src/onedb/fsck/marketplaceapp.rb index 9957d0367a..c26880d0a7 100644 --- a/src/onedb/fsck/marketplaceapp.rb +++ b/src/onedb/fsck/marketplaceapp.rb @@ -24,6 +24,8 @@ module OneDBFsck @db.fetch("SELECT oid,body FROM marketplaceapp_pool") do |row| doc = nokogiri_doc(row[:body], 'marketplaceapp_pool') + check_ugid(doc) + market_id = doc.root.xpath('MARKETPLACE_ID').text.to_i market_name = doc.root.xpath('MARKETPLACE').text diff --git a/src/onedb/fsck/network.rb b/src/onedb/fsck/network.rb index 2f7bd0df2e..19fb68c684 100644 --- a/src/onedb/fsck/network.rb +++ b/src/onedb/fsck/network.rb @@ -36,6 +36,8 @@ module OneDBFsck doc = nokogiri_doc(row[:body]) oid = row[:oid] + check_ugid(doc) + used_leases = doc.root.at_xpath('USED_LEASES').text.to_i counter_no_ar = counters[:vnet][row[:oid]][:no_ar_leases] ar_leases = counters[:vnet][row[:oid]][:ar_leases] diff --git a/src/onedb/fsck/template.rb b/src/onedb/fsck/template.rb index 3b677362c8..11dfd60eec 100644 --- a/src/onedb/fsck/template.rb +++ b/src/onedb/fsck/template.rb @@ -4,11 +4,11 @@ module OneDBFsck templates_fix = @fixes_template = {} @db[:template_pool].each do |row| - doc = nokogiri_doc(row[:body], 'template_pool') - + doc = nokogiri_doc(row[:body], 'template_pool') boot = doc.root.at_xpath("TEMPLATE/OS/BOOT") + uid = doc.root.at_xpath('UID').content - uid = doc.root.at_xpath('UID').content + check_ugid(doc) if boot.nil? || boot.text.downcase.match(/fd|hd|cdrom|network/).nil? next diff --git a/src/onedb/fsck/vm.rb b/src/onedb/fsck/vm.rb index 7dd111e2bd..2bc9f11233 100644 --- a/src/onedb/fsck/vm.rb +++ b/src/onedb/fsck/vm.rb @@ -13,6 +13,8 @@ module OneDBFsck @db.fetch("SELECT oid,body FROM vm_pool WHERE state<>6") do |row| vm_doc = nokogiri_doc(row[:body]) + check_ugid(vm_doc) + state = vm_doc.root.at_xpath('STATE').text.to_i lcm_state = vm_doc.root.at_xpath('LCM_STATE').text.to_i diff --git a/src/onedb/fsck/vrouter.rb b/src/onedb/fsck/vrouter.rb index 8f551022fd..8a9cd24d4d 100644 --- a/src/onedb/fsck/vrouter.rb +++ b/src/onedb/fsck/vrouter.rb @@ -7,6 +7,8 @@ module OneDBFsck @db.fetch("SELECT oid,body FROM vrouter_pool") do |row| vrouter_doc = nokogiri_doc(row[:body]) + check_ugid(vrouter_doc) + # DATA: VNets used by this Virtual Router vrouter_doc.root.xpath("TEMPLATE/NIC").each do |nic| net_id = nil diff --git a/src/sunstone/models/OpenNebulaJSON/ImageJSON.rb b/src/sunstone/models/OpenNebulaJSON/ImageJSON.rb index e2494351f9..06fa5fb4e7 100644 --- a/src/sunstone/models/OpenNebulaJSON/ImageJSON.rb +++ b/src/sunstone/models/OpenNebulaJSON/ImageJSON.rb @@ -63,8 +63,8 @@ module OpenNebulaJSON when "snapshot_flatten" then self.snapshot_flatten(action_hash['params']) when "snapshot_revert" then self.snapshot_revert(action_hash['params']) when "snapshot_delete" then self.snapshot_delete(action_hash['params']) - when "lock" then self.lock(action_hash['params']) - when "unlock" then self.unlock(action_hash['params']) + when "lock" then lock(action_hash['params']['level'].to_i) + when "unlock" then unlock() else error_msg = "#{action_hash['perform']} action not " << " available for this resource" @@ -137,13 +137,5 @@ module OpenNebulaJSON def snapshot_delete(params=Hash.new) super(params['snapshot_id'].to_i) end - - def lock(params=Hash.new) - super(params['level'].to_i) - end - - def unlock(params=Hash.new) - super() - end end end diff --git a/src/sunstone/models/OpenNebulaJSON/MarketPlaceAppJSON.rb b/src/sunstone/models/OpenNebulaJSON/MarketPlaceAppJSON.rb index 524ba0dede..62b4ff5644 100644 --- a/src/sunstone/models/OpenNebulaJSON/MarketPlaceAppJSON.rb +++ b/src/sunstone/models/OpenNebulaJSON/MarketPlaceAppJSON.rb @@ -57,8 +57,8 @@ module OpenNebulaJSON when "rename" then self.rename(action_hash['params']) when "disable" then self.disable when "enable" then self.enable - when "lock" then self.lock(action_hash['params']) - when "unlock" then self.unlock(action_hash['params']) + when "lock" then lock(action_hash['params']['level'].to_i) + when "unlock" then unlock() when "vm.import" then self.app_vm_import(action_hash['params']) when "vm-template.import" then self.app_vm_import(action_hash['params']) when "service_template.import" then self.app_service_import(action_hash['params']) @@ -234,13 +234,5 @@ module OpenNebulaJSON def rename(params=Hash.new) super(params['name']) end - - def lock(params=Hash.new) - super(params['level'].to_i) - end - - def unlock(params=Hash.new) - super() - end end end diff --git a/src/sunstone/models/OpenNebulaJSON/TemplateJSON.rb b/src/sunstone/models/OpenNebulaJSON/TemplateJSON.rb index 59dfe6fef0..a3e430562f 100644 --- a/src/sunstone/models/OpenNebulaJSON/TemplateJSON.rb +++ b/src/sunstone/models/OpenNebulaJSON/TemplateJSON.rb @@ -51,8 +51,8 @@ module OpenNebulaJSON when "clone" then self.clone(action_hash['params']) when "rename" then self.rename(action_hash['params']) when "delete_recursive" then self.delete_recursive(action_hash['params']) - when "lock" then self.lock_json(action_hash['params']) - when "unlock" then self.unlock_json(action_hash['params']) + when "lock" then lock(action_hash['params']['level'].to_i) + when "unlock" then unlock() else error_msg = "#{action_hash['perform']} action not " << " available for this resource" @@ -143,13 +143,5 @@ module OpenNebulaJSON recursive = (params['recursive'] == true) self.delete(recursive) end - - def lock_json(params=Hash.new) - self.lock(params['level'].to_i) - end - - def unlock_json(params=Hash.new) - self.unlock - end end end diff --git a/src/sunstone/models/OpenNebulaJSON/VMGroupJSON.rb b/src/sunstone/models/OpenNebulaJSON/VMGroupJSON.rb index 9b6f53ab63..9c9617b41b 100644 --- a/src/sunstone/models/OpenNebulaJSON/VMGroupJSON.rb +++ b/src/sunstone/models/OpenNebulaJSON/VMGroupJSON.rb @@ -47,8 +47,8 @@ module OpenNebulaJSON when "chmod" then self.chmod_octet(action_hash['params']) when "update" then self.update(action_hash['params']) when "rename" then self.rename(action_hash['params']) - when "lock" then self.lock(action_hash['params']) - when "unlock" then self.unlock(action_hash['params']) + when "lock" then lock(action_hash['params']['level'].to_i) + when "unlock" then unlock() else error_msg = "#{action_hash['perform']} action not " << " available for this resource" @@ -79,13 +79,5 @@ module OpenNebulaJSON def rename(params=Hash.new) super(params['name']) end - - def lock(params=Hash.new) - super(params['level'].to_i) - end - - def unlock(params=Hash.new) - super() - end end end diff --git a/src/sunstone/models/OpenNebulaJSON/VirtualNetworkJSON.rb b/src/sunstone/models/OpenNebulaJSON/VirtualNetworkJSON.rb index 7cf2af4e2f..854fd4cacb 100644 --- a/src/sunstone/models/OpenNebulaJSON/VirtualNetworkJSON.rb +++ b/src/sunstone/models/OpenNebulaJSON/VirtualNetworkJSON.rb @@ -59,8 +59,8 @@ module OpenNebulaJSON when "add_ar" then self.add_ar(action_hash['params']) when "update_ar" then self.update_ar(action_hash['params']) when "reserve" then self.reserve(action_hash['params']) - when "lock" then self.lock(action_hash['params']) - when "unlock" then self.unlock(action_hash['params']) + when "lock" then lock(action_hash['params']['level'].to_i) + when "unlock" then unlock() else error_msg = "#{action_hash['perform']} action not " << " available for this resource" @@ -118,13 +118,5 @@ module OpenNebulaJSON super(params['name'], params['size'], params['ar_id'], params['addr'], params['vnet']) end - - def lock(params=Hash.new) - super(params['level'].to_i) - end - - def unlock(params=Hash.new) - super() - end end end diff --git a/src/sunstone/models/OpenNebulaJSON/VirtualNetworkTemplateJSON.rb b/src/sunstone/models/OpenNebulaJSON/VirtualNetworkTemplateJSON.rb index a2fb7eb83c..03a57aae7a 100644 --- a/src/sunstone/models/OpenNebulaJSON/VirtualNetworkTemplateJSON.rb +++ b/src/sunstone/models/OpenNebulaJSON/VirtualNetworkTemplateJSON.rb @@ -50,8 +50,8 @@ module OpenNebulaJSON when "hold" then self.hold(action_hash['params']) when "release" then self.release(action_hash['params']) when "rename" then self.rename(action_hash['params']) - when "lock" then self.lock(action_hash['params']) - when "unlock" then self.unlock(action_hash['params']) + when "lock" then lock(action_hash['params']['level'].to_i) + when "unlock" then unlock() when "instantiate" then self.instantiate(action_hash['params']) else error_msg = "#{action_hash['perform']} action not " << diff --git a/src/sunstone/models/OpenNebulaJSON/VirtualRouterJSON.rb b/src/sunstone/models/OpenNebulaJSON/VirtualRouterJSON.rb index 3c4f8ee690..98d07310da 100644 --- a/src/sunstone/models/OpenNebulaJSON/VirtualRouterJSON.rb +++ b/src/sunstone/models/OpenNebulaJSON/VirtualRouterJSON.rb @@ -51,8 +51,8 @@ module OpenNebulaJSON when "rename" then self.rename(action_hash['params']) when "attachnic" then self.nic_attach(action_hash['params']) when "detachnic" then self.nic_detach(action_hash['params']) - when "lock" then self.lock(action_hash['params']) - when "unlock" then self.unlock(action_hash['params']) + when "lock" then lock(action_hash['params']['level'].to_i) + when "unlock" then unlock() else error_msg = "#{action_hash['perform']} action not " << " available for this resource" @@ -115,13 +115,5 @@ module OpenNebulaJSON def nic_detach(params=Hash.new) super(params['nic_id'].to_i) end - - def lock(params=Hash.new) - super(params['level'].to_i) - end - - def unlock(params=Hash.new) - super() - end end end diff --git a/src/sunstone/public/images/favicon.ico b/src/sunstone/public/images/favicon.ico deleted file mode 100644 index f74166852c..0000000000 Binary files a/src/sunstone/public/images/favicon.ico and /dev/null differ diff --git a/src/sunstone/public/images/favicon.png b/src/sunstone/public/images/favicon.png deleted file mode 100644 index da117d5ed7..0000000000 Binary files a/src/sunstone/public/images/favicon.png and /dev/null differ diff --git a/src/sunstone/public/images/favicon.svg b/src/sunstone/public/images/favicon.svg new file mode 100644 index 0000000000..9214d3588d --- /dev/null +++ b/src/sunstone/public/images/favicon.svg @@ -0,0 +1 @@ +favicon openebula ruby \ No newline at end of file diff --git a/src/sunstone/views/guac.erb b/src/sunstone/views/guac.erb index cb6e02160f..6e9529f566 100644 --- a/src/sunstone/views/guac.erb +++ b/src/sunstone/views/guac.erb @@ -4,7 +4,7 @@ - + diff --git a/src/sunstone/views/index.erb b/src/sunstone/views/index.erb index 423e03e9d5..43d3216345 100644 --- a/src/sunstone/views/index.erb +++ b/src/sunstone/views/index.erb @@ -6,9 +6,9 @@ OpenNebula Sunstone: Cloud Operations Center - + - + <% if session[:lang] %> diff --git a/src/sunstone/views/login.erb b/src/sunstone/views/login.erb index c10bb9ac30..3ec8df7e67 100644 --- a/src/sunstone/views/login.erb +++ b/src/sunstone/views/login.erb @@ -4,9 +4,9 @@ OpenNebula Sunstone Login - + - + <% if $conf[:env] == 'dev' %> diff --git a/src/vmm_mad/remotes/kvm/cancel b/src/vmm_mad/remotes/kvm/cancel index 204d39b98e..7b02cd704e 100755 --- a/src/vmm_mad/remotes/kvm/cancel +++ b/src/vmm_mad/remotes/kvm/cancel @@ -32,8 +32,8 @@ function destroy_and_monitor virsh --connect $LIBVIRT_URI destroy $deploy_id - # Destroy vGPU - sudo /var/tmp/one/vgpu "DELETE" "$DATASTORE/vm.xml" + # Destroy vGPU (only if suported by node) + (sudo -l | grep -q vgpu) && sudo /var/tmp/one/vgpu "DELETE" "$DATASTORE/vm.xml" virsh --connect $LIBVIRT_URI --readonly dominfo $deploy_id > /dev/null 2>&1 [ "x$?" != "x0" ] @@ -48,5 +48,5 @@ fi # Compact memory if [ "x$CLEANUP_MEMORY_ON_STOP" = "xyes" ]; then - sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null & + (sudo -l | grep -q sysctl) && (sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null &) || true fi diff --git a/src/vmm_mad/remotes/kvm/deploy b/src/vmm_mad/remotes/kvm/deploy index 6a3d4b51e7..004375a062 100755 --- a/src/vmm_mad/remotes/kvm/deploy +++ b/src/vmm_mad/remotes/kvm/deploy @@ -27,7 +27,7 @@ cat > $DEP_FILE # Compact memory if [ "x$CLEANUP_MEMORY_ON_START" = "xyes" ]; then - sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 >/dev/null + (sudo -l | grep -q sysctl) && sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 >/dev/null fi # Create non-volatile memory to store firmware variables if needed @@ -37,7 +37,8 @@ if [ -n "${nvram}" ]; then fi # Create vGPU following NVIDIA official guide: https://docs.nvidia.com/grid/latest/pdf/grid-vgpu-user-guide.pdf -sudo /var/tmp/one/vgpu "CREATE" "$DEP_FILE_LOCATION/vm.xml" +# Only if supported by node +(sudo -l | grep -q vgpu) && sudo /var/tmp/one/vgpu "CREATE" "$DEP_FILE_LOCATION/vm.xml" DATA=`virsh --connect $LIBVIRT_URI create $DEP_FILE` diff --git a/src/vmm_mad/remotes/kvm/migrate b/src/vmm_mad/remotes/kvm/migrate index 2180c7dde3..09ac53ee3c 100755 --- a/src/vmm_mad/remotes/kvm/migrate +++ b/src/vmm_mad/remotes/kvm/migrate @@ -117,7 +117,7 @@ done # Compact memory if [ "x$CLEANUP_MEMORY_ON_START" = "xyes" ]; then - ssh_exec_and_log "$DEST_HOST" "sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 >/dev/null" \ + ssh_exec_and_log "$DEST_HOST" "(sudo -l | grep -q sysctl) && sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 >/dev/null || true" \ "Failed compact memory on $DEST_HOST" fi @@ -285,6 +285,6 @@ fi # Compact memory if [ "x$CLEANUP_MEMORY_ON_STOP" = "xyes" ]; then - sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null & + (sudo -l | grep -q sysctl) && sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null & fi diff --git a/src/vmm_mad/remotes/kvm/migrate_local b/src/vmm_mad/remotes/kvm/migrate_local index 562f037c7b..640b646a64 100755 --- a/src/vmm_mad/remotes/kvm/migrate_local +++ b/src/vmm_mad/remotes/kvm/migrate_local @@ -36,7 +36,7 @@ done # Compact memory on dest host if [ "x$CLEANUP_MEMORY_ON_START" = "xyes" ]; then - ssh_exec_and_log "$dest_host" "sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 >/dev/null" \ + ssh_exec_and_log "$dest_host" "(sudo -l | grep -q sysctl) && sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 >/dev/null || true" \ "Failed compact memory on $dest_host" fi @@ -71,6 +71,6 @@ fi # Compact memory on src host if [ "x$CLEANUP_MEMORY_ON_STOP" = "xyes" ]; then - ssh_exec_and_log "$src_host" "sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null &" \ + ssh_exec_and_log "$src_host" "(sudo -l | grep -q sysctl) && (sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null &) || true" \ "Failed compact memory on $src_host" fi diff --git a/src/vmm_mad/remotes/kvm/resize b/src/vmm_mad/remotes/kvm/resize index 15ff343f05..9f129da93e 100755 --- a/src/vmm_mad/remotes/kvm/resize +++ b/src/vmm_mad/remotes/kvm/resize @@ -50,7 +50,7 @@ source $(dirname $0)/../../scripts_common.sh if [ ! -z "$MEM" -a "$MEM" -ne "$MEM_OLD" ]; then # Compact memory if [ "x$CLEANUP_MEMORY_ON_START" = "xyes" ]; then - sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 >/dev/null + (sudo -l | grep -q sysctl) && sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 >/dev/null fi # Add memory to VM @@ -58,7 +58,7 @@ if [ ! -z "$MEM" -a "$MEM" -ne "$MEM_OLD" ]; then # Compact memory if [ "x$CLEANUP_MEMORY_ON_STOP" = "xyes" ]; then - sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null & + (sudo -l | grep -q sysctl) && sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null & fi fi diff --git a/src/vmm_mad/remotes/kvm/restore b/src/vmm_mad/remotes/kvm/restore index 4c04104a65..06282cc9f9 100755 --- a/src/vmm_mad/remotes/kvm/restore +++ b/src/vmm_mad/remotes/kvm/restore @@ -78,7 +78,7 @@ multiline_exec_and_log "$RECALCULATE_CMD" \ # Compact memory if [ "x$CLEANUP_MEMORY_ON_START" = "xyes" ]; then - sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 >/dev/null + (sudo -l | grep -q sysctl) && sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 >/dev/null fi ### Restore with retry diff --git a/src/vmm_mad/remotes/kvm/save b/src/vmm_mad/remotes/kvm/save index c2c8155ef4..51c0fd9a6c 100755 --- a/src/vmm_mad/remotes/kvm/save +++ b/src/vmm_mad/remotes/kvm/save @@ -44,7 +44,7 @@ retry_if "active block job" 3 5 \ # Compact memory if [ "x$CLEANUP_MEMORY_ON_STOP" = "xyes" ]; then - sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null & + (sudo -l | grep -q sysctl) && sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null & fi #------------------------------------------------------------------------------- diff --git a/src/vmm_mad/remotes/kvm/shutdown b/src/vmm_mad/remotes/kvm/shutdown index fb4452b60a..a7d7a49394 100755 --- a/src/vmm_mad/remotes/kvm/shutdown +++ b/src/vmm_mad/remotes/kvm/shutdown @@ -73,12 +73,12 @@ retry $TIMEOUT monitor force_shutdown "$deploy_id" \ "virsh --connect $LIBVIRT_URI destroy $deploy_id" -# Destroy vGPU -sudo /var/tmp/one/vgpu "DELETE" "$DATASTORE/vm.xml" +# Destroy vGPU. Only if supported by node +(sudo -l | grep -q vgpu) && sudo /var/tmp/one/vgpu "DELETE" "$DATASTORE/vm.xml" # Compact memory if [ "x$CLEANUP_MEMORY_ON_STOP" = "xyes" ]; then - sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null & + (sudo -l | grep -q sysctl) && sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null & fi sleep 4 diff --git a/src/vnm_mad/remotes/lib/command.rb b/src/vnm_mad/remotes/lib/command.rb index 075eaace12..7f047776c6 100644 --- a/src/vnm_mad/remotes/lib/command.rb +++ b/src/vnm_mad/remotes/lib/command.rb @@ -18,6 +18,7 @@ require 'open3' module VNMMAD + # Network related commands for VN drivers module VNMNetwork # Command configuration for common network commands. This CAN be adjust @@ -37,6 +38,19 @@ module VNMMAD :ipset => 'sudo -n ipset' } + # Adjust :ip[6]tables commands to work with legacy versions + begin + stdout = Open3.capture3('sudo iptables --version')[0] + regex = /.*v(?\d+.\d+.\d+)/ + iptables_version = Gem::Version.new(stdout.match(regex)[:version]) + + if Gem::Version.new('1.6.1') > iptables_version + COMMANDS[:iptables] = 'sudo -n iptables -w 3' + COMMANDS[:ip6tables] = 'sudo -n ip6tables -w 3' + end + rescue StandardError + end + # Represents an Array of commands to be executed by the networking # drivers # The commands class Commands < Array