mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-22 18:50:08 +03:00
(cherry picked from commit d183658ddc7009b65ab990b8b4b8e0a680377887)
This commit is contained in:
parent
496f2a2be4
commit
11dbb1e418
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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,
|
||||
}
|
@ -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'
|
||||
|
185
src/fireedge/src/client/components/Cards/AddressRangeCard.js
Normal file
185
src/fireedge/src/client/components/Cards/AddressRangeCard.js
Normal 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
|
@ -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,
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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)
|
||||
|
27
src/fireedge/src/client/components/Forms/Cluster/index.js
Normal file
27
src/fireedge/src/client/components/Forms/Cluster/index.js
Normal 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 }
|
@ -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 }
|
||||
|
@ -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
|
@ -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
|
@ -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 }
|
@ -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
|
@ -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
|
@ -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
|
@ -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)
|
@ -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
|
@ -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
|
@ -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 }
|
@ -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 }
|
@ -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
|
@ -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(',')
|
||||
}),
|
||||
}
|
@ -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
|
@ -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
|
||||
)
|
@ -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 }
|
@ -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
|
@ -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'
|
@ -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
|
@ -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 }
|
41
src/fireedge/src/client/components/Forms/VNetwork/index.js
Normal file
41
src/fireedge/src/client/components/Forms/VNetwork/index.js
Normal 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 }
|
@ -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(
|
||||
() =>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -199,6 +199,7 @@ const EnhancedTable = ({
|
||||
singleSelect={singleSelect}
|
||||
disableRowSelect={disableRowSelect}
|
||||
globalActions={globalActions}
|
||||
selectedRows={selectedRows}
|
||||
useTableProps={useTableProps}
|
||||
/>
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
() =>
|
||||
|
333
src/fireedge/src/client/components/Tables/VNetworks/actions.js
Normal file
333
src/fireedge/src/client/components/Tables/VNetworks/actions.js
Normal 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
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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']
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
|
82
src/fireedge/src/client/components/Tabs/VNetwork/Address.js
Normal file
82
src/fireedge/src/client/components/Tabs/VNetwork/Address.js
Normal 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
|
83
src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js
Normal file
83
src/fireedge/src/client/components/Tabs/VNetwork/Clusters.js
Normal 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
|
@ -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)}`}
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
86
src/fireedge/src/client/components/Tabs/VNetwork/Info/qos.js
Normal file
86
src/fireedge/src/client/components/Tabs/VNetwork/Info/qos.js
Normal 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
|
@ -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
|
170
src/fireedge/src/client/components/Tabs/VNetwork/Leases/index.js
Normal file
170
src/fireedge/src/client/components/Tabs/VNetwork/Leases/index.js
Normal 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
|
107
src/fireedge/src/client/components/Tabs/VNetwork/Security.js
Normal file
107
src/fireedge/src/client/components/Tabs/VNetwork/Security.js
Normal 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
|
82
src/fireedge/src/client/components/Tabs/VNetwork/VRouters.js
Normal file
82
src/fireedge/src/client/components/Tabs/VNetwork/VRouters.js
Normal 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
|
@ -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 }) => {
|
||||
|
@ -39,7 +39,7 @@ const getTabComponent = (tabName) =>
|
||||
info: Info,
|
||||
network: Network,
|
||||
history: History,
|
||||
schedActions: SchedActions,
|
||||
sched_actions: SchedActions,
|
||||
snapshot: Snapshot,
|
||||
storage: Storage,
|
||||
configuration: Configuration,
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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',
|
||||
|
79
src/fireedge/src/client/containers/VirtualNetworks/Create.js
Normal file
79
src/fireedge/src/client/containers/VirtualNetworks/Create.js
Normal 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
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -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} />,
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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'
|
||||
|
77
src/fireedge/src/client/utils/ip.js
Normal file
77
src/fireedge/src/client/utils/ip.js
Normal 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)
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user