1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-22 18:50:08 +03:00

F #5832: Add virtual network tab (#2221)

(cherry picked from commit d183658ddc7009b65ab990b8b4b8e0a680377887)
This commit is contained in:
Sergio Betanzos 2022-07-06 15:54:15 +02:00 committed by Tino Vazquez
parent 496f2a2be4
commit 11dbb1e418
77 changed files with 3989 additions and 181 deletions

View File

@ -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,
},
],
},
{

View File

@ -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 (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'add-ar',
startIcon: <AddIcon />,
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 (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VN_ACTIONS.UPDATE_AR}-${AR_ID}`,
icon: <EditIcon />,
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 (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VN_ACTIONS.DELETE_AR}-${AR_ID}`,
icon: <TrashIcon />,
tooltip: T.Delete,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<>
<Translate word={T.DeleteAddressRange} />
{`: #${AR_ID}`}
</>
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
},
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,
}

View File

@ -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'

View File

@ -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: (
<Link
color="secondary"
component={RouterLink}
to={generatePath(PATH.NETWORK.VNETS.DETAIL, { id: parentId })}
>
<Translate word={T.ReservedFromVNetId} values={parentId} />
</Link>
),
dataCy: 'parent',
},
].filter(Boolean)
return (
<Box
className={classes.root}
sx={{
'&:hover': { bgcolor: 'action.hover' },
border: (theme) => `1px solid ${theme.palette.divider}`,
}}
>
<div className={classes.main}>
<div className={classes.title}>
<Typography noWrap component="span" data-cy="id">
{`#${AR_ID}`}
</Typography>
<span className={classes.labels}>
<MultipleTags tags={labels} limitTags={labels.length} />
</span>
</div>
<Box
className={classes.caption}
sx={{
flexDirection: 'column',
alignItems: 'flex-start !important',
gap: '0.5em !important',
}}
>
{MAC && (
<span
data-cy="range-mac"
title={`${Tr(T.First)}: ${MAC} / ${Tr(T.Last)}: ${MAC_END}`}
>{`MAC: ${MAC} | ${MAC_END}`}</span>
)}
{IP && (
<span
data-cy="range-ip"
title={`${Tr(T.First)}: ${IP} / ${Tr(T.Last)}: ${IP_END}`}
>{`IP: ${IP} | ${IP_END}`}</span>
)}
{IP6 && (
<span
data-cy="range-ip6"
title={`${Tr(T.First)}: ${IP6} / ${Tr(T.Last)}: ${IP6_END}`}
>{`IP6: ${IP6} | ${IP6_END}`}</span>
)}
{IP6_GLOBAL && (
<span
data-cy="range-ip6-global"
title={`${Tr(T.First)}: ${IP6_GLOBAL} / ${Tr(
T.Last
)}: ${IP6_GLOBAL_END}`}
>{`IP6 GLOBAL: ${IP6_GLOBAL} | ${IP6_GLOBAL_END}`}</span>
)}
{IP6_ULA && (
<span
data-cy="range-ip6-ula"
title={`${Tr(T.First)}: ${IP6_ULA} / ${Tr(
T.Last
)}: ${IP6_ULA_END}`}
>{`IP6 ULA: ${IP6_ULA} | ${IP6_ULA_END}`}</span>
)}
</Box>
</div>
<div className={classes.secondary}>
{USED_LEASES && (
<LinearProgressWithLabel
value={percentOfUsed}
high={VNET_THRESHOLD.LEASES.high}
low={VNET_THRESHOLD.LEASES.low}
label={percentLabel}
title={`${Tr(T.Used)} / ${Tr(T.TotalLeases)}`}
/>
)}
{actions && <div className={classes.actions}>{actions}</div>}
</div>
</Box>
)
}
)
AddressRangeCard.propTypes = {
vnet: PropTypes.object,
ar: PropTypes.object,
actions: PropTypes.node,
}
AddressRangeCard.displayName = 'AddressRangeCard'
export default AddressRangeCard

View File

@ -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,

View File

@ -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,
}

View File

@ -50,7 +50,11 @@ const DefaultFormStepper = ({
})
return (
<FormProvider {...methods} initialValues={initialValues}>
<FormProvider
{...methods}
initialValues={initialValues}
getResolver={() => resolver(methods.watch())}
>
<FormStepper steps={steps} schema={resolver} onSubmit={onSubmit} />
</FormProvider>
)

View File

@ -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)

View File

@ -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 }

View File

@ -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 }

View File

@ -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 (
<Box display="grid" gap="1em">
<FormWithSchema fields={isUpdate ? MUTABLE_FIELDS : FIELDS} />
<AttributePanel
collapse
askToDelete={false}
allActionsEnabled
title={T.CustomInformation}
handleAdd={handleChangeAttribute}
handleEdit={handleChangeAttribute}
handleDelete={handleChangeAttribute}
attributes={customAttrs}
filtersSpecialAttributes={false}
/>
</Box>
)
}
Content.propTypes = { isUpdate: PropTypes.bool }
export default Content

View File

@ -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

View File

@ -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 }

View File

@ -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 (
<>
<Stack flexDirection="row" gap="1em">
<AddAddressRangeAction onSubmit={handleCreateAction} />
</Stack>
<Stack
pb="1em"
display="grid"
gridTemplateColumns="repeat(auto-fit, minmax(300px, 0.5fr))"
gap="1em"
mt="1em"
>
{addresses?.map((ar, index) => {
const key = ar.ID ?? ar.NAME
const fakeValues = { ...ar, AR_ID: index }
return (
<AddressRangeCard
key={key}
ar={fakeValues}
actions={
<>
<UpdateAddressRangeAction
vm={{}}
ar={fakeValues}
onSubmit={(updatedAr) => handleUpdate(updatedAr, index)}
/>
<DeleteAddressRangeAction
ar={fakeValues}
onSubmit={() => handleRemove(index)}
/>
</>
}
/>
)
})}
</Stack>
</>
)
}
const Content = ({ isUpdate }) =>
isUpdate ? (
<Typography variant="subtitle2">
<Translate word={T.DisabledAddressRangeInForm} />
</Typography>
) : (
<AddressesContent />
)
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

View File

@ -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 (
<AttributePanel
collapse
title={
<Legend
disableGutters
data-cy={'custom-attributes'}
title={T.CustomAttributes}
/>
}
allActionsEnabled
handleAdd={handleChangeAttribute}
handleEdit={handleChangeAttribute}
handleDelete={handleChangeAttribute}
attributes={unknownVars}
filtersSpecialAttributes={false}
/>
)
}
export default ContextAttrsSection

View File

@ -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 = () => (
<>
<FormWithSchema id={EXTRA_ID} cy="context" fields={FIELDS} />
<CustomAttributes />
</>
)
/** @type {TabType} */
const TAB = {
id: 'context',
name: T.Context,
icon: ContextIcon,
Content: ContextContent,
getError: (error) => FIELDS.some(({ name }) => error?.[name]),
}
export default TAB

View File

@ -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)

View File

@ -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: <Translate word={name} />,
renderContent: () => <TabContent isUpdate={isUpdate} driver={driver} />,
error: getError?.(errors[STEP_ID]),
})),
[totalErrors, driver]
)
return <Tabs tabs={tabs} />
}
/**
* 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

View File

@ -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 }) => (
<FormWithSchema
key={id}
id={EXTRA_ID}
cy={`${EXTRA_ID}-${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

View File

@ -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 }

View File

@ -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 }

View File

@ -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 (
<SecurityGroupsTable
disableGlobalSort
displaySelectedRows
pageSize={5}
initialState={{ selectedRowIds }}
onSelectedRowsChange={handleSelectedRows}
/>
)
}
/** @type {TabType} */
const TAB = {
id: 'security',
name: T.Security,
icon: SecurityIcon,
Content: SecurityContent,
getError: (error) => !!error?.[TAB_ID],
}
export default TAB

View File

@ -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(',')
}),
}

View File

@ -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 }) => (
<FormWithSchema
key={id}
id={STEP_ID}
cy={`${STEP_ID}-${id}`}
{...section}
/>
))}
{driver === VN_DRIVERS.vxlan && (
<AttributePanel
collapse
title={
<Legend
disableGutters
data-cy={'ip-conf'}
title={T.IpConfiguration}
tooltip={T.IpConfigurationConcept}
/>
}
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

View File

@ -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
)

View File

@ -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 }

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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 }

View File

@ -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 }

View File

@ -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(
() =>

View File

@ -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 (
<Stack
className={className}
@ -85,20 +74,20 @@ const GlobalActions = ({
/>
)}
{!singleSelect && !disableRowSelect && (
<>
<Checkbox
{...getToggleAllPageRowsSelectedProps()}
title={Tr(T.ToggleAllCurrentPageRowsSelected)}
indeterminate={getToggleAllRowsSelectedProps().indeterminate}
color="secondary"
/>
{globalActions?.map((item, idx) => {
const key = item.accessor ?? item.label ?? item.tooltip ?? idx
return <Action key={key} item={item} selectedRows={selectedRows} />
})}
</>
<Checkbox
{...getToggleAllPageRowsSelectedProps()}
title={Tr(T.ToggleAllCurrentPageRowsSelected)}
indeterminate={getToggleAllRowsSelectedProps().indeterminate}
color="secondary"
/>
)}
{globalActions?.map((item, idx) => {
if ((singleSelect || disableRowSelect) && item.selected) return null
const key = item.accessor ?? item.label ?? item.tooltip ?? idx
return <Action key={key} item={item} selectedRows={selectedRows} />
})}
</Stack>
)
}
@ -111,6 +100,7 @@ GlobalActions.propTypes = {
disableRowSelect: PropTypes.bool,
globalActions: PropTypes.array,
useTableProps: PropTypes.object,
selectedRows: PropTypes.array,
}
export default GlobalActions

View File

@ -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
*/

View File

@ -199,6 +199,7 @@ const EnhancedTable = ({
singleSelect={singleSelect}
disableRowSelect={disableRowSelect}
globalActions={globalActions}
selectedRows={selectedRows}
useTableProps={useTableProps}
/>

View File

@ -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,

View File

@ -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(
() =>

View File

@ -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 (
<Typography
key={`vnet-${id}`}
variant="inherit"
component="span"
display="block"
>
{`#${ID} ${NAME}`}
</Typography>
)
})
const SubHeader = (rows) => <ListNames rows={rows} />
const MessageToConfirmAction = (rows) => {
const names = rows?.map?.(({ original }) => original?.NAME)
return (
<>
<p>
<Translate word={T.VirtualNetworks} />
{`: ${names.join(', ')}`}
</p>
<p>
<Translate word={T.DoYouWantProceed} />
</p>
</>
)
}
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 (
<VNetworkTemplatesTable
disableGlobalSort
disableRowSelect
classes={classes}
onRowClick={(vnet) => 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

View File

@ -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

View File

@ -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 <NetworkCard network={state ?? original} rootProps={props} />
const memoNetwork = useMemo(() => state ?? original, [state, original])
return (
<NetworkCard
network={memoNetwork}
rootProps={props}
onClickLabel={onClickLabel}
/>
)
},
(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'

View File

@ -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]

View File

@ -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,

View File

@ -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']

View File

@ -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,

View File

@ -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 && <Actions.Delete name={name} handleClick={show} />}
{canDelete && (
<Actions.Delete
name={name}
handleClick={askToDelete ? show : handleDeleteAttribute}
/>
)}
</ActionWrapper>
</>
)}
@ -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,

View File

@ -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

View File

@ -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'

View File

@ -58,7 +58,7 @@ const Permissions = memo(({ handleEdit, actions, ...permissions }) => {
const getIcon = (checked) => (+checked ? <CheckIcon /> : <BlankSquareIcon />)
return (
<Paper variant="outlined">
<Paper variant="outlined" sx={{ height: 'fit-content' }}>
<List className={classes.list}>
<ListItem className={classes.title}>
<Typography noWrap>{Tr(T.Permissions)}</Typography>

View File

@ -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 (
<Box padding={{ sm: '0.8em' }}>
{actions[ADD_AR] === true && <AddAddressRangeAction vnetId={id} />}
<Stack gap="1em" py="0.8em">
{addressRanges.map((ar) => (
<AddressRangeCard
key={ar.AR_ID}
vnet={vnet}
ar={ar}
actions={
<>
{actions[UPDATE_AR] === true && (
<UpdateAddressRangeAction vnetId={id} ar={ar} />
)}
{actions[DELETE_AR] === true && (
<DeleteAddressRangeAction vnetId={id} ar={ar} />
)}
</>
}
/>
))}
</Stack>
</Box>
)
}
AddressTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
AddressTab.displayName = 'AddressTab'
export default AddressTab

View File

@ -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 (
<Box padding={{ sm: '0.8em', overflow: 'auto' }}>
<ClustersTable
disableGlobalSort
disableRowSelect
pageSize={5}
onRowClick={detailAccess ? redirectToCluster : undefined}
useQuery={useQuery}
/>
</Box>
)
}
ClustersTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
ClustersTab.displayName = 'ClustersTab'
export default ClustersTab

View File

@ -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 && <QOS vnet={vnet} />}
{attributesPanel?.enabled && attributes && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}
collapse
attributes={attributes}
actions={getActions(attributesPanel?.actions)}
title={Tr(T.Attributes)}
@ -157,6 +161,7 @@ const VNetworkInfoTab = ({ tabProps = {}, id }) => {
{vcenterPanel?.enabled && vcenterAttributes && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}
collapse
actions={getActions(vcenterPanel?.actions)}
attributes={vcenterAttributes}
title={`vCenter ${Tr(T.Information)}`}
@ -165,6 +170,7 @@ const VNetworkInfoTab = ({ tabProps = {}, id }) => {
{lxcPanel?.enabled && lxcAttributes && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}
collapse
actions={getActions(lxcPanel?.actions)}
attributes={lxcAttributes}
title={`LXC ${Tr(T.Information)}`}

View File

@ -15,14 +15,25 @@
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { generatePath } from 'react-router-dom'
import { Stack } from '@mui/material'
import { useRenameVNetMutation } from 'client/features/OneApi/network'
import {
useGetVNetworkQuery,
useRenameVNetMutation,
} from 'client/features/OneApi/network'
import { StatusChip } from 'client/components/Status'
import { StatusCircle, StatusChip } from 'client/components/Status'
import { List } from 'client/components/Tabs/Common'
import {
levelLockToString,
stringToBoolean,
booleanToString,
} from 'client/models/Helper'
import { getState } from 'client/models/VirtualNetwork'
import { T, VNetwork, VN_ACTIONS } from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
/**
* Renders mainly information tab.
@ -34,7 +45,21 @@ import { T, VNetwork, VN_ACTIONS } from 'client/constants'
*/
const InformationPanel = ({ vnet = {}, actions }) => {
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: (
<StatusChip dataCy="state" text={stateName} stateColor={stateColor} />
<Stack direction="row" alignItems="center" gap={1}>
<StatusCircle color={stateColor} />
<StatusChip dataCy="state" text={stateName} stateColor={stateColor} />
</Stack>
),
},
]
{
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 (
<>

View File

@ -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 (
<>
<List title={T.Inbound} list={inbound} />
<List title={T.Outbound} list={outbound} />
</>
)
}
QOSPanel.propTypes = { vnet: PropTypes.object }
QOSPanel.displayName = 'QOSPanel'
export default QOSPanel

View File

@ -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 (
<Link
noWrap
data-cy="name"
variant="subtitle2"
color="secondary"
component={RouterLink}
title={`${type.toUpperCase()} ${id}`}
to={generatePath(path, { id })}
sx={{
gap: '1.5em',
minHeight: '31px', // same as release button
'& > svg': { mr: '1em' },
}}
>
<StatusCircle color={colorState} />
{`#${id}`}
</Link>
)
}
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 (
<Fragment key={addr}>
{+vmId === -1 ? (
actions[VN_ACTIONS.RELEASE_LEASE] && (
<Stack
direction="row"
alignItems="center"
gap={1}
width="max-content"
>
<StatusCircle color="debug.main" />
<SubmitButton
isSubmitting={isReleasing}
onClick={release}
color="success"
variant="text"
startIcon={<ReleaseIcon />}
label={T.ReleaseIp}
/>
</Stack>
)
) : (
<LeaseName id={resId} type={resType} />
)}
{[
{ 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 }) => (
<Typography
noWrap
key={`${addr}-${dataCy}`}
data-cy={`${addr}-${dataCy}`.toLowerCase()}
variant="subtitle2"
title={typeof text === 'string' ? text : undefined}
display={{
xs: ['ip', 'mac'].includes(dataCy) ? 'block' : 'none',
sm: ['ip', 'ip6', 'mac'].includes(dataCy) ? 'block' : 'none',
md: 'block',
}}
>
{text}
</Typography>
))}
</Fragment>
)
}
)
LeaseItem.propTypes = {
lease: PropTypes.object,
actions: PropTypes.object,
id: PropTypes.string,
resetHoldState: PropTypes.func,
}
LeaseItem.displayName = 'LeaseItem'
export default LeaseItem

View File

@ -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 (
<Box padding={{ sm: '0.8em', overflow: 'auto' }}>
{actions[VN_ACTIONS.HOLD_LEASE] === true && (
<Box
component="form"
onSubmit={hold}
display="inline-flex"
gap="1em"
alignItems="center"
>
<TextField
name="addr"
inputProps={{ 'data-cy': 'addr' }}
sx={{ '& input': { paddingBlock: '6px' } }}
placeholder={'10.0.0.4'}
/>
<SubmitButton
type="submit"
isSubmitting={isHolding}
color="secondary"
variant="outlined"
label={'Hold IP'}
/>
</Box>
)}
<Box
component="section"
display="grid"
gridTemplateColumns={{
xs: 'repeat(3, 1fr)',
sm: 'repeat(4, 1fr)',
md: 'repeat(7, 1fr)',
}}
alignItems="center"
gap="0.25em"
py="0.8em"
sx={{
'& > span': {
// header styles
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
marginLeft: '-8px',
paddingLeft: '8px',
alignSelf: 'end',
},
}}
>
{LEASES_COLUMNS.map((col, index) => (
<Typography
key={col}
noWrap
component="span"
variant="body1"
display={{
xs: ['NAME', 'IP', 'MAC'].includes(col) ? 'block' : 'none',
sm: ['NAME', 'IP', 'IP6', 'MAC'].includes(col) ? 'block' : 'none',
md: 'block',
}}
>
{index !== 0 && <Translate word={col} />}
</Typography>
))}
{leases.map((lease) => (
<LeaseItem
key={lease?.IP || lease?.MAC}
lease={lease}
actions={actions}
id={id}
resetHoldState={reset}
/>
))}
</Box>
{isHolding && <Skeleton variant="rectangular" />}
</Box>
)
}
LeasesTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
LeasesTab.displayName = 'LeasesTab'
export default LeasesTab

View File

@ -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 (
<Box padding={{ sm: '0.8em', overflow: 'auto' }}>
<SecurityGroupsTable
disableGlobalSort
disableRowSelect
pageSize={5}
onRowClick={detailAccess ? redirectToSecGroup : undefined}
globalActions={globalActions}
useQuery={useQuery}
/>
</Box>
)
}
SecurityTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
SecurityTab.displayName = 'SecurityTab'
export default SecurityTab

View File

@ -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 (
<Box padding={{ sm: '0.8em', overflow: 'auto' }}>
<VRoutersTable
disableGlobalSort
disableRowSelect
pageSize={5}
onRowClick={detailAccess ? redirectToVRouter : undefined}
useQuery={useQuery}
/>
</Box>
)
}
VRoutersTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
VRoutersTab.displayName = 'VRoutersTab'
export default VRoutersTab

View File

@ -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 }) => {

View File

@ -39,7 +39,7 @@ const getTabComponent = (tabName) =>
info: Info,
network: Network,
history: History,
schedActions: SchedActions,
sched_actions: SchedActions,
snapshot: Snapshot,
storage: Storage,
configuration: Configuration,

View File

@ -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,
}

View File

@ -17,23 +17,25 @@ import * as ACTIONS from 'client/constants/actions'
import COLOR from 'client/constants/color'
import * as STATES from 'client/constants/states'
import * as T from 'client/constants/translates'
/**
* @typedef {object} PciDevice - PCI device
* @property {string} ADDRESS - Address, bus, slot and function
* @property {string} BUS - Address bus
* @property {string} CLASS - Id of PCI device class
* @property {string} [CLASS_NAME] - Name of PCI device class
* @property {string} CLASS_NAME - Name of PCI device class
* @property {string} DEVICE - Id of PCI device
* @property {string} [DEVICE_NAME] - Name of PCI device
* @property {string} DEVICE_NAME - Name of PCI device
* @property {string} DOMAIN - Address domain
* @property {string} FUNCTION - Address function
* @property {string} NUMA_NODE - Numa node
* @property {string} PROFILES - PCI device available profiles
* @property {string} PROFILES - Available vGPU Profiles
* @property {string} SHORT_ADDRESS - Short address
* @property {string} SLOT - Address slot
* @property {string} [UUID] - UUID
* @property {string} TYPE - Type
* @property {string} VENDOR - Id of PCI device vendor
* @property {string} [VENDOR_NAME] - Name of PCI device vendor
* @property {string} VENDOR_NAME - Name of PCI device vendor
* @property {string|number} VMID - Id using this device, -1 if free
*/

View File

@ -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',

View File

@ -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',

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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

View File

@ -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,
}

View File

@ -25,6 +25,8 @@ module.exports = {
FilterBy: 'Filter by',
FilterLabels: 'Filter labels',
FilterByLabel: 'Filter by label',
First: 'First',
Last: 'Last',
ApplyLabels: 'Apply labels',
Label: 'Label',
NoLabels: 'NoLabels',
@ -66,8 +68,9 @@ module.exports = {
CreateMarketApp: 'Create Marketplace App',
CreateProvider: 'Create Provider',
CreateProvision: 'Create Provision',
CreateVmTemplate: 'Create VM Template',
CreateServiceTemplate: 'Create Service Template',
CreateVirtualNetwork: 'Create VM Template',
CreateVmTemplate: 'Create VM Template',
CurrentGroup: 'Current group: %s',
CurrentOwner: 'Current owner: %s',
Delete: 'Delete',
@ -75,8 +78,11 @@ module.exports = {
DeleteDb: 'Delete database',
DeleteScheduleAction: 'Delete schedule action: %s',
DeleteSeveralTemplates: 'Delete several Templates',
DeleteSeveralVirtualNetworks: 'Delete several Virtual Networks',
DeleteSomething: 'Delete: %s',
DeleteAddressRange: 'Delete Address Range',
DeleteTemplate: 'Delete Template',
DeleteVirtualNetwork: 'Delete Virtual Network',
Deploy: 'Deploy',
DeployServiceTemplate: 'Deploy Service Template',
Detach: 'Detach',
@ -111,10 +117,12 @@ module.exports = {
Recreate: 'Recreate',
Refresh: 'Refresh',
Release: 'Release',
ReleaseIp: 'Release IP',
Remove: 'Remove',
Rename: 'Rename',
RenameSomething: 'Rename: %s',
Reschedule: 'Reschedule',
Reserve: 'Reserve',
Resize: 'Resize',
ResizeCapacity: 'Resize capacity',
ResizeSomething: 'Resize: %s',
@ -164,11 +172,12 @@ module.exports = {
UnReschedule: 'Un-Reschedule',
Unshare: 'Unshare',
Update: 'Update',
UpdateVmConfiguration: 'Update VM Configuration',
UpdateProvider: 'Update Provider',
UpdateScheduleAction: 'Update schedule action: %s',
UpdateVmTemplate: 'Update VM Template',
UpdateServiceTemplate: 'Update Service Template',
UpdateVirtualNetwork: 'Update Virtual Network',
UpdateVmConfiguration: 'Update VM Configuration',
UpdateVmTemplate: 'Update VM Template',
/* questions */
Yes: 'Yes',
@ -531,6 +540,7 @@ module.exports = {
/* VM Template schema - general */
CustomHypervisor: 'Custom',
CustomVariables: 'Custom Variables',
CustomAttributes: 'Custom Attributes',
Hypervisor: 'Hypervisor',
Logo: 'Logo',
MakeNewImagePersistent: 'Make the new images persistent',
@ -807,6 +817,7 @@ module.exports = {
NetworkAddress: 'Network address',
NetworkMask: 'Network mask',
Gateway: 'Gateway',
Gateway6: 'IPv6 Gateway',
GatewayConcept: 'Default gateway for the network',
Gateway6Concept: 'IPv6 router for this network',
SearchDomainForDNSResolution: 'Search domains for DNS resolution',
@ -815,9 +826,9 @@ module.exports = {
NetworkMethod6Concept: 'Sets IPv6 guest conf. method for NIC in this network',
DNS: 'DNS',
DNSConcept: 'DNS servers, a space separated list of servers',
AverageBandwidth: 'Average bandwidth (KBytes/s)',
PeakBandwidth: 'Peak bandwidth (KBytes/s)',
PeakBurst: 'Peak burst (KBytes)',
AverageBandwidth: 'Average bandwidth',
PeakBandwidth: 'Peak bandwidth',
PeakBurst: 'Peak burst',
InboundAverageBandwidthConcept:
'Average bitrate for the interface in kilobytes/second for inbound traffic',
InboundPeakBandwidthConcept:
@ -832,8 +843,10 @@ module.exports = {
TransmissionQueue: 'Transmission queue',
OnlySupportedForVirtioDriver: 'Only supported for virtio driver',
GuestOptions: 'Guest options',
GuestMTU: 'GuestMTU',
GuestMTU: 'MTU of the Guest interfaces',
GuestMTUConcept: 'Sets the MTU for the NICs in this network',
NetMethod: 'Method',
NetMethod6: 'IPv6 Method',
UsedLeases: 'Used leases',
TotalLeases: 'Total leases',
TotalClusters: 'Total clusters',
@ -845,8 +858,63 @@ module.exports = {
States for success/failure recovers: LOCK_CREATE, LOCK_DELETE state.
States for a retry recover: LOCK_CREATE, LOCK_DELETE state.
States for delete: Any but READY.`,
ReservationParent: 'Reservation parent',
ReservedFromVNetId: 'Reserved from VNET %s',
/* Virtual Network schema - driver configuration */
NetworkMode: 'Network mode',
Bridge: 'Bridge',
BridgeConcept: 'Name of the physical bridge in the nodes to attach VM NICs',
PhysicalDevice: 'Physical device',
PhysicalDeviceConcept: 'Node NIC to send/receive virtual network traffic',
MacSpoofingFilter: ' MAC spoofing filter',
IpSpoofingFilter: ' IP spoofing filter',
MTU: 'MTU of the interface',
MTUConcept: 'Maximum Transmission Unit',
VlanId: 'VLAN ID',
AutomaticVlanId: 'Automatic VLAN ID',
VxlanMode: 'VXLAN mode',
VxlanModeConcept: 'Multicast protocol for multi destination BUM traffic',
VxlanTunnelEndpoint: 'VXLAN Tunnel endpoint',
VxlanTunnelEndpointConcept: 'Tunnel endpoint communication type',
VxlanMulticast: 'VXLAN Multicast',
VxlanMulticastConcept:
'Base multicast address for each VLAN. The MC address is :vxlan_mc + :vlan_id',
IpConfiguration: 'IP Configuration',
IpConfigurationConcept:
'Options passed to ip cmd on operations specific to this Virtual Network',
OuterVlanId: 'Outer VLAN ID',
AutomaticOuterVlanId: 'Automatic Outer VLAN ID',
InvalidAttribute: 'Invalid attribute',
/* Virtual Network schema - address range */
Addresses: 'Addresses',
AddressRange: 'Address Range',
FirstIPv4Address: 'First IPv4 address',
FirstMacAddress: 'First MAC address',
SLAAC: 'SLAAC',
IPv6GlobalPrefix: 'IPv6 Global prefix',
IPv6ULAPrefix: 'IPv6 ULA prefix',
IPAMDriver: 'IPAM driver',
InvalidAddress: 'Invalid address',
InvalidIPv4: 'Invalid IPv4',
InvalidMAC: 'Invalid MAC',
DisabledAddressRangeInForm:
'Address Ranges need to be managed in the individual Virtual Network panel',
/* Virtual Network schema - QoS */
QoS: 'QoS',
InboundTraffic: 'Inbound traffic',
OutboundTraffic: 'Outbound traffic',
/* Virtual Network schema - reserve */
ReservationFromVirtualNetwork: 'Reservation from Virtual Network',
CanSelectAddressFromAR:
'You can select the addresses from an specific Address Range',
NumberOfAddresses: 'Number of addresses',
AddToNewVirtualNetwork: 'Add to a new Virtual Network',
AddToExistingReservation: 'Add to an existing Reservation',
FirstAddress: 'First address',
IpOrMac: 'IP or MAC',
/* security group schema */
Security: 'Security',
TCP: 'TCP',
UDP: 'UDP',
ICMP: 'ICMP',

View File

@ -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 ? (
<SkeletonStepsForm />
) : (
<CreateForm
initialValues={data}
stepProps={data}
onSubmit={onSubmit}
fallback={<SkeletonStepsForm />}
>
{(config) => <DefaultFormStepper {...config} />}
</CreateForm>
)
}
export default CreateVirtualNetwork

View File

@ -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() {
<SplitPane gridTemplateRows="1fr auto 1fr">
{({ getGridProps, GutterComponent }) => (
<Box height={1} {...(hasSelectedRows && getGridProps())}>
<VNetworksTable onSelectedRowsChange={onSelectedRowsChange} />
<VNetworksTable
onSelectedRowsChange={onSelectedRowsChange}
globalActions={actions}
useUpdateMutation={useUpdateVNetMutation}
/>
{hasSelectedRows && (
<>
@ -52,6 +65,7 @@ function VirtualNetworks() {
<InfoTabs
vnet={selectedRows[0]?.original}
gotoPage={selectedRows[0]?.gotoPage}
unselect={() => 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 }) => (
<Stack overflow="auto">
<Stack direction="row" alignItems="center" gap={1} mb={1}>
<Typography color="text.primary" noWrap>
{`#${vnet.ID} | ${vnet.NAME}`}
</Typography>
{gotoPage && (
<IconButton title={Tr(T.LocateOnTable)} onClick={gotoPage}>
<BookmarkEmpty />
</IconButton>
)}
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 (
<Stack overflow="auto">
<Stack direction="row" alignItems="center" gap={1} mb={1}>
<SubmitButton
data-cy="detail-refresh"
icon={<RefreshDouble />}
tooltip={Tr(T.Refresh)}
isSubmitting={isFetching}
onClick={() => get({ id })}
/>
{typeof gotoPage === 'function' && (
<SubmitButton
data-cy="locate-on-table"
icon={<GotoIcon />}
tooltip={Tr(T.LocateOnTable)}
onClick={() => gotoPage()}
/>
)}
{typeof unselect === 'function' && (
<SubmitButton
data-cy="unselect"
icon={<Cancel />}
tooltip={Tr(T.Close)}
onClick={() => unselect()}
/>
)}
<Typography color="text.primary" noWrap>
{`#${id} | ${name}`}
</Typography>
</Stack>
<VNetworkTabs id={id} />
</Stack>
<VNetworkTabs id={vnet.ID} />
</Stack>
))
)
})
InfoTabs.propTypes = {
vnet: PropTypes.object.isRequired,
vnet: PropTypes.object,
gotoPage: PropTypes.func,
unselect: PropTypes.func,
}
InfoTabs.displayName = 'InfoTabs'

View File

@ -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)

View File

@ -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: () => <TabContent tabProps={tabProps} id={id} />,
}

View File

@ -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'
}

View File

@ -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'

View File

@ -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)

View File

@ -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