1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-01-26 10:03:37 +03:00

F OpenNebula/one#5422: Add attach nic form (#1391)

This commit is contained in:
Sergio Betanzos 2021-08-02 11:14:22 +02:00 committed by GitHub
parent 599f79bf09
commit 3e703ec4e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 339 additions and 35 deletions

View File

@ -44,12 +44,12 @@ const ButtonToTriggerForm = ({ buttonProps = {}, title, options = [] }) => {
const { steps, defaultValues, resolver, fields, onSubmit: handleSubmit } = Form ?? {}
const handleTriggerSubmit = async formData => {
await handleSubmit(formData)
await handleSubmit?.(formData)
hide()
}
const openDialogForm = form => {
show(form)
const openDialogForm = ({ form = {}, onSubmit }) => {
show({ ...form, onSubmit })
handleClose()
}
@ -65,8 +65,9 @@ const ButtonToTriggerForm = ({ buttonProps = {}, title, options = [] }) => {
aria-describedby={buttonProps.cy ?? 'main-button-form'}
disabled={!options.length}
endIcon={isGroupButton && <NavArrowDown />}
onClick={evt =>
!isGroupButton ? openDialogForm(options[0].form) : handleToggle(evt)
onClick={evt => !isGroupButton
? openDialogForm(options[0])
: handleToggle(evt)
}
{...buttonProps}
>
@ -86,11 +87,11 @@ const ButtonToTriggerForm = ({ buttonProps = {}, title, options = [] }) => {
<Paper variant='outlined'>
<ClickAwayListener onClickAway={handleClose}>
<MenuList disablePadding>
{options.map(({ cy, name, form = {}, onSubmit }) => (
{options.map(({ cy, name, ...option }) => (
<MenuItem
key={name}
data-cy={cy}
onClick={() => openDialogForm({ ...form, onSubmit })}
onClick={() => openDialogForm(option)}
>
<Translate word={name} />
</MenuItem>

View File

@ -0,0 +1,46 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 jsdoc/require-jsdoc */
import React, { useCallback } from 'react'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import {
SCHEMA,
FIELDS
} from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/schema'
import { T } from 'client/constants'
export const STEP_ID = 'advanced'
const AdvancedOptions = ({ nics = [] } = {}) => ({
id: STEP_ID,
label: T.AdvancedOptions,
resolver: () => SCHEMA(nics),
optionsValidate: { abortEarly: false },
content: useCallback(
() => (
<FormWithSchema
cy='attach-nic-advanced'
id={STEP_ID}
fields={FIELDS(nics)}
/>
),
[nics?.length, nics?.[0]?.ID]
)
})
export default AdvancedOptions

View File

@ -0,0 +1,77 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 jsdoc/require-jsdoc */
import * as yup from 'yup'
import { getValidationFromFields } from 'client/utils'
import { INPUT_TYPES } from 'client/constants'
const RDP = {
name: 'RDP',
label: 'RDP connection',
type: INPUT_TYPES.CHECKBOX,
validation: yup
.boolean()
.transform(value => {
if (typeof value === 'boolean') return value
return String(value).toUpperCase() === 'YES'
})
.default(false),
grid: { md: 12 }
}
const ALIAS = nics => ({
name: 'PARENT',
label: 'Attach as an alias',
type: INPUT_TYPES.SELECT,
values: [{ text: '', value: '' }]
.concat(nics?.map?.(({ NAME, IP = '', NETWORK = '', NIC_ID = '' } = {}) =>
({ text: `${NIC_ID} - ${NETWORK} ${IP}`, value: NAME })
)),
validation: yup
.string()
.trim()
.notRequired()
.default(undefined)
})
const EXTERNAL = {
name: 'EXTERNAL',
label: 'External',
type: INPUT_TYPES.CHECKBOX,
tooltip: 'The NIC will be attached as an external alias of the VM',
dependOf: ALIAS.name,
htmlType: type => !type?.length ? INPUT_TYPES.HIDDEN : undefined,
validation: yup
.boolean()
.transform(value => {
if (typeof value === 'boolean') return value
return String(value).toUpperCase() === 'YES'
})
.default(false),
grid: { md: 12 }
}
export const FIELDS = nics => [
RDP,
ALIAS(nics),
EXTERNAL
]
export const SCHEMA = nics =>
yup.object(getValidationFromFields(FIELDS(nics)))

View File

@ -0,0 +1,69 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 jsdoc/require-jsdoc */
import React, { useCallback } from 'react'
import { useListForm } from 'client/hooks'
import { VNetworksTable } from 'client/components/Tables'
import {
SCHEMA
} from 'client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable/schema'
import { T } from 'client/constants'
export const STEP_ID = 'network'
const NetworkStep = () => ({
id: STEP_ID,
label: T.Network,
resolver: () => SCHEMA,
content: useCallback(
({ data, setFormData }) => {
const selectedNetwork = data?.[0]
const {
handleSelect,
handleClear
} = useListForm({ key: STEP_ID, setList: setFormData })
const handleSelectedRows = rows => {
const { original } = rows?.[0] ?? {}
const { ID, NAME, UID, UNAME, SECURITY_GROUPS } = original ?? {}
const network = {
NETWORK_ID: ID,
NETWORK: NAME,
NETWORK_UID: UID,
NETWORK_UNAME: UNAME,
SECURITY_GROUPS
}
ID !== undefined ? handleSelect(network) : handleClear()
}
return (
<VNetworksTable
singleSelect
onlyGlobalSearch
onlyGlobalSelectedRows
initialState={{ selectedRowIds: { [selectedNetwork?.ID]: true } }}
onSelectedRowsChange={handleSelectedRows}
/>
)
}, [])
})
export default NetworkStep

View File

@ -0,0 +1,23 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 * as yup from 'yup'
export const SCHEMA = yup
.array(yup.object())
.min(1, 'Select network')
.max(1, 'Max. one network selected')
.required('Network field is required')
.default([])

View File

@ -0,0 +1,38 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 jsdoc/require-jsdoc */
import * as yup from 'yup'
import NetworksTable from 'client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable'
import AdvancedOptions from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions'
const Steps = stepProps => {
const network = NetworksTable(stepProps)
const advanced = AdvancedOptions(stepProps)
const steps = [network, advanced]
const resolver = () => yup.object({
[network.id]: network.resolver(),
[advanced.id]: advanced.resolver()
})
const defaultValues = resolver().default()
return { steps, defaultValues, resolver }
}
export default Steps

View File

@ -0,0 +1,16 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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/Vm/AttachNicForm/Steps'

View File

@ -14,8 +14,10 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import ResizeCapacityForm from 'client/components/Forms/Vm/ResizeCapacityForm'
import AttachNicForm from 'client/components/Forms/Vm/AttachNicForm'
export * from 'client/components/Forms/Vm/AttachDiskForm'
export {
AttachNicForm,
ResizeCapacityForm
}

View File

@ -19,11 +19,11 @@ import React, { useEffect } from 'react'
import { useFetch } from 'client/hooks'
import { useVNetwork, useVNetworkApi } from 'client/features/One'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
import { SkeletonTable, EnhancedTable, EnhancedTableProps } from 'client/components/Tables'
import VNetworkColumns from 'client/components/Tables/VNetworks/columns'
import VNetworkRow from 'client/components/Tables/VNetworks/row'
const VNetworksTable = () => {
const VNetworksTable = props => {
const columns = React.useMemo(() => VNetworkColumns, [])
const vNetworks = useVNetwork()
@ -45,8 +45,12 @@ const VNetworksTable = () => {
isLoading={loading || reloading}
getRowId={row => String(row.ID)}
RowComponent={VNetworkRow}
{...props}
/>
)
}
VNetworksTable.propTypes = EnhancedTableProps
VNetworksTable.displayName = 'VNetworksTable'
export default VNetworksTable

View File

@ -101,7 +101,7 @@ const NetworkItem = ({ nic = {}, actions }) => {
const hasDetails = React.useMemo(
() => !!ALIAS.length || !!SECURITY_GROUPS?.length,
[ALIAS.length, SECURITY_GROUPS?.length]
[ALIAS?.length, SECURITY_GROUPS?.length]
)
const handleDetach = async () => {

View File

@ -16,21 +16,23 @@
/* eslint-disable jsdoc/require-jsdoc */
import * as React from 'react'
import PropTypes from 'prop-types'
import { Button } from '@material-ui/core'
import { useDialog } from 'client/hooks'
import { useVmApi } from 'client/features/One'
import { TabContext } from 'client/components/Tabs/TabProvider'
import { DialogConfirmation } from 'client/components/Dialogs'
import NetworkList from 'client/components/Tabs/Vm/Network/List'
import { Tr } from 'client/components/HOC'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import { AttachNicForm } from 'client/components/Forms/Vm'
import * as VirtualMachine from 'client/models/VirtualMachine'
import * as Helper from 'client/models/Helper'
import { mapUserInputs } from 'client/utils'
import { T, VM_ACTIONS } from 'client/constants'
const VmNetworkTab = ({ tabProps = {} }) => {
const { display, show, hide } = useDialog()
const { data: vm } = React.useContext(TabContext)
const { attachNic } = useVmApi()
const { handleRefetch, data: vm } = React.useContext(TabContext)
const { actions = [] } = tabProps
const nics = VirtualMachine.getNics(vm, {
@ -41,31 +43,33 @@ const VmNetworkTab = ({ tabProps = {} }) => {
const hypervisor = VirtualMachine.getHypervisor(vm)
const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor)
const handleAttachNic = async ({ network, advanced }) => {
const networkSelected = network?.[0]
const isAlias = !!advanced?.PARENT?.length
const root = { ...networkSelected, ...mapUserInputs(advanced) }
const template = Helper.jsonToXml({
[isAlias ? 'NIC_ALIAS' : 'NIC']: root
})
const response = await attachNic(vm.ID, template)
String(response) === String(vm.ID) && await handleRefetch?.()
}
return (
<>
{actionsAvailable?.includes?.(VM_ACTIONS.ATTACH_NIC) && (
<Button
data-cy='attach-nic'
size='small'
color='secondary'
onClick={show}
variant='contained'
>
{Tr(T.AttachNic)}
</Button>
<ButtonToTriggerForm
buttonProps={{ 'data-cy': 'attach-nic' }}
title={T.AttachNic}
options={[{
form: AttachNicForm({ nics }),
onSubmit: handleAttachNic
}]}
/>
)}
<NetworkList actions={actionsAvailable} nics={nics} />
{display && (
<DialogConfirmation
title={T.AttachNic}
handleAccept={hide}
handleCancel={hide}
>
<p>TODO: should define in view yaml ??</p>
</DialogConfirmation>
)}
</>
)
}

View File

@ -46,4 +46,5 @@ export const resize = createAction('vm/resize', vmService.resize)
export const changePermissions = createAction('vm/chmod', vmService.changePermissions)
export const changeOwnership = createAction('vm/chown', vmService.changeOwnership)
export const attachDisk = createAction('vm/attach/disk', vmService.attachDisk)
export const attachNic = createAction('vm/attach/nic', vmService.attachNic)
export const detachNic = createAction('vm/detach/nic', vmService.detachNic)

View File

@ -45,6 +45,7 @@ export const useVmApi = () => {
changeOwnership: (id, ownership) =>
unwrapDispatch(actions.changeOwnership({ id, ownership })),
attachDisk: (id, template) => unwrapDispatch(actions.attachDisk({ id, template })),
attachNic: (id, template) => unwrapDispatch(actions.attachNic({ id, template })),
detachNic: (id, nic) => unwrapDispatch(actions.detachNic({ id, nic }))
}
}

View File

@ -242,6 +242,28 @@ export const vmService = ({
return res?.data
},
/**
* Attaches a new network interface to the virtual machine.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Virtual machine id
* @param {string} params.template
* - A string containing a single NIC vector attribute
* @returns {number} Virtual machine id
* @throws Fails when response isn't code 200
*/
attachNic: async ({ id, template }) => {
const name = Actions.VM_NIC_ATTACH
const command = { name, ...Commands[name] }
const config = requestConfig({ id, template }, command)
const res = await RestClient.request(config)
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
return res?.data
},
/**
* Detaches a network interface from a virtual machine.
*