mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-22 18:50:08 +03:00
parent
c7eba9750c
commit
3200326904
@ -57,15 +57,21 @@ const Content = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const BasicConfiguration = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.Configuration,
|
||||
resolver: formData => {
|
||||
const hypervisor = formData?.[TEMPLATE_ID]?.[0]?.TEMPLATE?.HYPERVISOR
|
||||
return SCHEMA(hypervisor)
|
||||
},
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content
|
||||
})
|
||||
const BasicConfiguration = initialValues => {
|
||||
const initialHypervisor = initialValues?.TEMPLATE?.HYPERVISOR
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
label: T.Configuration,
|
||||
resolver: formData => {
|
||||
const hypervisor =
|
||||
formData?.[TEMPLATE_ID]?.[0]?.TEMPLATE?.HYPERVISOR ?? initialHypervisor
|
||||
|
||||
return SCHEMA(hypervisor)
|
||||
},
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content
|
||||
}
|
||||
}
|
||||
|
||||
export default BasicConfiguration
|
||||
|
@ -1,226 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { SetStateAction } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
|
||||
import { NetworkAlt as NetworkIcon, BoxIso as ImageIcon } from 'iconoir-react'
|
||||
import { Stack, Checkbox, styled } from '@mui/material'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import { TAB_ID as STORAGE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/storage'
|
||||
import { TAB_ID as NIC_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const BootItem = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: '0.5em',
|
||||
padding: '1em',
|
||||
marginBottom: '1em',
|
||||
backgroundColor: theme.palette.background.default
|
||||
}))
|
||||
|
||||
const BootItemDraggable = styled(BootItem)(({ theme }) => ({
|
||||
'&:before': {
|
||||
content: "'.'",
|
||||
fontSize: 20,
|
||||
color: theme.palette.action.active,
|
||||
paddingBottom: 20,
|
||||
textShadow: `
|
||||
0 5px ${theme.palette.action.active},
|
||||
0 10px ${theme.palette.action.active},
|
||||
5px 0 ${theme.palette.action.active},
|
||||
5px 5px ${theme.palette.action.active},
|
||||
5px 10px ${theme.palette.action.active},
|
||||
10px 0 ${theme.palette.action.active},
|
||||
10px 5px ${theme.palette.action.active},
|
||||
10px 10px ${theme.palette.action.active}`
|
||||
}
|
||||
}))
|
||||
|
||||
export const TAB_ID = 'OS.BOOT'
|
||||
|
||||
/**
|
||||
* @param {string} id - Resource id: 'NIC<index>' or 'DISK<index>'
|
||||
* @param {Array} list - List of resources
|
||||
* @param {object} formData - Form data
|
||||
* @param {SetStateAction} setFormData - React set state action
|
||||
*/
|
||||
export const reorderBootAfterRemove = (id, list, formData, setFormData) => {
|
||||
const type = String(id).toLowerCase().replace(/\d+/g, '') // nic | disk
|
||||
const getIndexFromId = id => String(id).toLowerCase().replace(type, '')
|
||||
const idxToRemove = getIndexFromId(id)
|
||||
|
||||
const ids = list
|
||||
.filter(resource => resource.NAME !== id)
|
||||
.map(resource => String(resource.NAME).toLowerCase())
|
||||
|
||||
const newBootOrder = [...formData?.OS?.BOOT?.split(',').filter(Boolean)]
|
||||
.filter(bootId => !bootId.startsWith(type) || ids.includes(bootId))
|
||||
.map(bootId => {
|
||||
if (!bootId.startsWith(type)) return bootId
|
||||
|
||||
const resourceId = getIndexFromId(bootId)
|
||||
|
||||
return resourceId < idxToRemove
|
||||
? bootId
|
||||
: `${type}${resourceId - 1}`
|
||||
})
|
||||
|
||||
reorder(newBootOrder, setFormData)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} newBootOrder - New boot order
|
||||
* @param {SetStateAction} setFormData - React set state action
|
||||
*/
|
||||
const reorder = (newBootOrder, setFormData) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[EXTRA_ID]: {
|
||||
...prev[EXTRA_ID],
|
||||
OS: { BOOT: newBootOrder.join(',') }
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const Booting = ({ data, setFormData, control }) => {
|
||||
const booting = useWatch({ name: `${EXTRA_ID}.${TAB_ID}`, control })
|
||||
const bootOrder = booting?.split(',').filter(Boolean) ?? []
|
||||
|
||||
const disks = data?.[STORAGE_ID]
|
||||
?.map((disk, idx) => {
|
||||
const isVolatile = !disk?.IMAGE && !disk?.IMAGE_ID
|
||||
|
||||
return {
|
||||
ID: `disk${idx}`,
|
||||
NAME: (
|
||||
<>
|
||||
<ImageIcon />
|
||||
{isVolatile
|
||||
? <>{`${disk?.NAME}: `}<Translate word={T.VolatileDisk} /></>
|
||||
: [disk?.NAME, disk?.IMAGE].filter(Boolean).join(': ')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}) ?? []
|
||||
|
||||
const nics = data?.[NIC_ID]
|
||||
?.map((nic, idx) => ({
|
||||
ID: `nic${idx}`,
|
||||
NAME: (
|
||||
<>
|
||||
<NetworkIcon />
|
||||
{[nic?.NAME, nic.NETWORK].filter(Boolean).join(': ')}
|
||||
</>
|
||||
)
|
||||
})) ?? []
|
||||
|
||||
const enabledItems = [...disks, ...nics]
|
||||
.filter(item => bootOrder.includes(item.ID))
|
||||
.sort((a, b) => bootOrder.indexOf(a.ID) - bootOrder.indexOf(b.ID))
|
||||
|
||||
const restOfItems = [...disks, ...nics]
|
||||
.filter(item => !bootOrder.includes(item.ID))
|
||||
|
||||
/** @param {DropResult} result - Drop result */
|
||||
const onDragEnd = result => {
|
||||
const { destination, source, draggableId } = result
|
||||
const newBootOrder = [...bootOrder]
|
||||
|
||||
if (
|
||||
destination &&
|
||||
destination.index !== source.index &&
|
||||
newBootOrder.includes(draggableId)
|
||||
) {
|
||||
newBootOrder.splice(source.index, 1) // remove current position
|
||||
newBootOrder.splice(destination.index, 0, draggableId) // set in new position
|
||||
|
||||
reorder(newBootOrder, setFormData)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnable = itemId => {
|
||||
const newBootOrder = [...bootOrder]
|
||||
const itemIndex = bootOrder.indexOf(itemId)
|
||||
|
||||
itemIndex >= 0
|
||||
? newBootOrder.splice(itemIndex, 1)
|
||||
: newBootOrder.push(itemId)
|
||||
|
||||
reorder(newBootOrder, setFormData)
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Stack>
|
||||
<Droppable droppableId='booting'>
|
||||
{({ droppableProps, innerRef, placeholder }) => (
|
||||
<Stack {...droppableProps} ref={innerRef} m={2}>
|
||||
{enabledItems.map(({ ID, NAME }, idx) => (
|
||||
<Draggable key={ID} draggableId={ID} index={idx}>
|
||||
{({ draggableProps, dragHandleProps, innerRef }) => (
|
||||
<BootItemDraggable
|
||||
{...draggableProps}
|
||||
{...dragHandleProps}
|
||||
ref={innerRef}
|
||||
>
|
||||
<Checkbox
|
||||
checked
|
||||
color='secondary'
|
||||
data-cy={ID}
|
||||
onChange={() => handleEnable(ID)}
|
||||
/>
|
||||
{NAME}
|
||||
</BootItemDraggable>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{placeholder}
|
||||
</Stack>
|
||||
)}
|
||||
</Droppable>
|
||||
{restOfItems.map(({ ID, NAME }) => (
|
||||
<BootItem key={ID}>
|
||||
<Checkbox
|
||||
color='secondary'
|
||||
data-cy={ID}
|
||||
onChange={() => handleEnable(ID)}
|
||||
/>
|
||||
{NAME}
|
||||
</BootItem>
|
||||
))}
|
||||
</Stack>
|
||||
</DragDropContext>
|
||||
)
|
||||
}
|
||||
|
||||
Booting.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
Booting.displayName = 'Booting'
|
||||
|
||||
export default Booting
|
@ -16,20 +16,19 @@
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
import { useTheme } from '@mui/material'
|
||||
import { WarningCircledOutline as WarningIcon } from 'iconoir-react'
|
||||
import { SystemShut as OsIcon } from 'iconoir-react'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
import Tabs from 'client/components/Tabs'
|
||||
import Storage from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/storage'
|
||||
import Networking from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking'
|
||||
import Placement from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/placement'
|
||||
import ScheduleAction from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/scheduleAction'
|
||||
import Booting from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting'
|
||||
import { TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import Storage from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/storage'
|
||||
import Networking from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/networking'
|
||||
import Placement from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/placement'
|
||||
import Scheduling from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/scheduleAction'
|
||||
import BootOrder from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting/bootOrder'
|
||||
|
||||
import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable'
|
||||
import { SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
|
||||
@ -38,72 +37,69 @@ import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'extra'
|
||||
|
||||
/** @type {TabType[]} */
|
||||
export const TABS = [
|
||||
Storage,
|
||||
Networking,
|
||||
Placement,
|
||||
Scheduling,
|
||||
{
|
||||
id: 'booting',
|
||||
name: T.OSBooting,
|
||||
icon: OsIcon,
|
||||
Content: BootOrder,
|
||||
getError: error => !!error?.OS
|
||||
}
|
||||
]
|
||||
|
||||
const Content = ({ data, setFormData }) => {
|
||||
const theme = useTheme()
|
||||
const { watch, formState: { errors }, control } = useFormContext()
|
||||
const { view, getResourceView } = useAuth()
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
const hypervisor = watch(`${TEMPLATE_ID}[0].TEMPLATE.HYPERVISOR`)
|
||||
const dialog = getResourceView('VM-TEMPLATE')?.dialogs?.instantiate_dialog
|
||||
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
|
||||
const hypervisor = useMemo(() => watch(`${TEMPLATE_ID}.0.HYPERVISOR`), [])
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'storage',
|
||||
name: Tr(T.Storage),
|
||||
renderContent: <Storage {...{ data, setFormData, hypervisor, control }} />,
|
||||
icon: errors[STEP_ID]?.[0] && (
|
||||
<WarningIcon color={theme.palette.error.main} />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'network',
|
||||
name: Tr(T.Network),
|
||||
renderContent: <Networking {...{ data, setFormData, hypervisor, control }} />,
|
||||
icon: errors[STEP_ID]?.[1] && (
|
||||
<WarningIcon color={theme.palette.error.main} />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'placement',
|
||||
name: Tr(T.Placement),
|
||||
renderContent: <Placement {...{ data, setFormData, hypervisor, control }} />,
|
||||
icon: errors[STEP_ID]?.[2] && (
|
||||
<WarningIcon color={theme.palette.error.main} />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'sched_action',
|
||||
name: Tr(T.ScheduledAction),
|
||||
renderContent: <ScheduleAction {...{ data, setFormData, hypervisor, control }} />,
|
||||
icon: errors[STEP_ID]?.[3] && (
|
||||
<WarningIcon color={theme.palette.error.main} />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'booting',
|
||||
name: Tr(T.OSBooting),
|
||||
renderContent: <Booting {...{ data, setFormData, hypervisor, control }} />,
|
||||
icon: errors[STEP_ID]?.[4] && (
|
||||
<WarningIcon color={theme.palette.error.main} />
|
||||
)
|
||||
}
|
||||
].filter(({ id }) => sectionsAvailable.includes(id))
|
||||
}, [errors[STEP_ID], view, control])
|
||||
const sectionsAvailable = useMemo(() => {
|
||||
const dialog = getResourceView('VM-TEMPLATE')?.dialogs?.instantiate_dialog
|
||||
return getSectionsAvailable(dialog, hypervisor)
|
||||
}, [view])
|
||||
|
||||
const totalErrors = Object.keys(errors[STEP_ID] ?? {}).length
|
||||
|
||||
const tabs = useMemo(
|
||||
() => TABS
|
||||
.filter(({ id }) => sectionsAvailable.includes(id))
|
||||
.map(({ Content: TabContent, name, getError, ...section }) => ({
|
||||
...section,
|
||||
name,
|
||||
label: <Translate word={name} />,
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderContent: () => <TabContent {...{ data, setFormData, hypervisor, control }} />,
|
||||
error: getError?.(errors[STEP_ID])
|
||||
})),
|
||||
[totalErrors, view, control]
|
||||
)
|
||||
|
||||
return (
|
||||
<Tabs tabs={tabs} />
|
||||
)
|
||||
}
|
||||
|
||||
const ExtraConfiguration = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.AdvancedOptions,
|
||||
resolver: SCHEMA,
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content
|
||||
})
|
||||
const ExtraConfiguration = initialValues => {
|
||||
const initialHypervisor = initialValues?.TEMPLATE?.HYPERVISOR
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
label: T.AdvancedOptions,
|
||||
resolver: formData => {
|
||||
const hypervisor =
|
||||
formData?.[TEMPLATE_ID]?.[0]?.TEMPLATE?.HYPERVISOR ?? initialHypervisor
|
||||
|
||||
return SCHEMA(hypervisor)
|
||||
},
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content
|
||||
}
|
||||
}
|
||||
|
||||
Content.propTypes = {
|
||||
data: PropTypes.any,
|
||||
|
@ -1,148 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 PropTypes from 'prop-types'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Edit, Trash } from 'iconoir-react'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
|
||||
import { useListForm } from 'client/hooks'
|
||||
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
|
||||
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
|
||||
import { AttachNicForm } from 'client/components/Forms/Vm'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import { SCHEMA as EXTRA_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
|
||||
import { reorderBootAfterRemove } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting'
|
||||
import { stringToBoolean } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
paddingBlock: '1em',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, auto))',
|
||||
gap: '1em'
|
||||
}
|
||||
})
|
||||
|
||||
export const TAB_ID = 'NIC'
|
||||
|
||||
const Networking = ({ data, setFormData, control }) => {
|
||||
const classes = useStyles()
|
||||
const nics = useWatch({ name: `${EXTRA_ID}.${TAB_ID}`, control })
|
||||
|
||||
const { handleSetList, handleRemove, handleSave } = useListForm({
|
||||
parent: EXTRA_ID,
|
||||
key: TAB_ID,
|
||||
list: nics,
|
||||
setList: setFormData,
|
||||
getItemId: item => item.NAME,
|
||||
addItemId: (item, _, itemIndex) => ({ ...item, NAME: `${TAB_ID}${itemIndex}` })
|
||||
})
|
||||
|
||||
const reorderNics = () => {
|
||||
const diskSchema = EXTRA_SCHEMA.pick([TAB_ID])
|
||||
const { [TAB_ID]: newList } = diskSchema.cast({ [TAB_ID]: data?.[TAB_ID] })
|
||||
|
||||
handleSetList(newList)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
color: 'secondary',
|
||||
'data-cy': 'add-nic',
|
||||
label: T.AttachNic,
|
||||
variant: 'outlined'
|
||||
}}
|
||||
options={[{
|
||||
dialogProps: { title: T.AttachNic },
|
||||
form: () => AttachNicForm({ nics }),
|
||||
onSubmit: handleSave
|
||||
}]}
|
||||
/>
|
||||
<div className={classes.root}>
|
||||
{nics?.map(item => {
|
||||
const { NAME, RDP, SSH, NETWORK, PARENT, EXTERNAL } = item
|
||||
const hasAlias = nics?.some(nic => nic.PARENT === NAME)
|
||||
|
||||
return (
|
||||
<SelectCard
|
||||
key={NAME}
|
||||
title={[NAME, NETWORK].filter(Boolean).join(' - ')}
|
||||
subheader={<>
|
||||
{Object
|
||||
.entries({
|
||||
RDP: stringToBoolean(RDP),
|
||||
SSH: stringToBoolean(SSH),
|
||||
EXTERNAL: stringToBoolean(EXTERNAL),
|
||||
ALIAS: PARENT
|
||||
})
|
||||
.map(([k, v]) => v ? `${k}` : '')
|
||||
.filter(Boolean)
|
||||
.join(' | ')
|
||||
}
|
||||
</>}
|
||||
action={
|
||||
<>
|
||||
{!hasAlias &&
|
||||
<Action
|
||||
data-cy={`remove-${NAME}`}
|
||||
handleClick={() => {
|
||||
handleRemove(NAME)
|
||||
reorderNics()
|
||||
reorderBootAfterRemove(NAME, nics, data, setFormData)
|
||||
}}
|
||||
icon={<Trash />}
|
||||
/>
|
||||
}
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
'data-cy': `edit-${NAME}`,
|
||||
icon: <Edit />,
|
||||
tooltip: <Translate word={T.Edit} />
|
||||
}}
|
||||
options={[{
|
||||
dialogProps: {
|
||||
title: <Translate word={T.EditSomething} values={[`${NAME} - ${NETWORK}`]} />
|
||||
},
|
||||
form: () => AttachNicForm({ nics }, item),
|
||||
onSubmit: newValues => handleSave(newValues, NAME)
|
||||
}]}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Networking.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
Networking.displayName = 'Networking'
|
||||
|
||||
export default Networking
|
@ -1,77 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 PropTypes from 'prop-types'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import {
|
||||
HOST_REQ_FIELD,
|
||||
HOST_RANK_FIELD,
|
||||
DS_REQ_FIELD,
|
||||
DS_RANK_FIELD
|
||||
} from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
paddingBlock: '1em',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, auto))',
|
||||
gap: '1em'
|
||||
}
|
||||
})
|
||||
|
||||
const Placement = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
// TODO - Host requirements: add button to select HOST in list => ID="<id>"
|
||||
// TODO - Host policy options: Packing|Stripping|Load-aware
|
||||
|
||||
// TODO - DS requirements: add button to select DATASTORE in list => ID="<id>"
|
||||
// TODO - DS policy options: Packing|Stripping
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormWithSchema
|
||||
className={classes.information}
|
||||
cy='instantiate-vm-template-extra.host-placement'
|
||||
fields={[HOST_REQ_FIELD, HOST_RANK_FIELD]}
|
||||
legend={Tr(T.Host)}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
<FormWithSchema
|
||||
className={classes.information}
|
||||
cy='instantiate-vm-template-extra.ds-placement'
|
||||
fields={[DS_REQ_FIELD, DS_RANK_FIELD]}
|
||||
legend={Tr(T.Datastore)}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Placement.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func
|
||||
}
|
||||
|
||||
Placement.displayName = 'Placement'
|
||||
|
||||
export default Placement
|
@ -1,130 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 PropTypes from 'prop-types'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Edit, Trash } from 'iconoir-react'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
|
||||
import { useListForm } from 'client/hooks'
|
||||
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
|
||||
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
|
||||
import { PunctualForm, RelativeForm } from 'client/components/Forms/Vm'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
paddingBlock: '1em',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, auto))',
|
||||
gap: '1em'
|
||||
}
|
||||
})
|
||||
|
||||
export const TAB_ID = 'SCHED_ACTION'
|
||||
|
||||
const ScheduleAction = ({ setFormData, control }) => {
|
||||
const classes = useStyles()
|
||||
const scheduleActions = useWatch({ name: `${EXTRA_ID}.${TAB_ID}`, control })
|
||||
|
||||
const { handleRemove, handleSave } = useListForm({
|
||||
parent: EXTRA_ID,
|
||||
key: TAB_ID,
|
||||
list: scheduleActions,
|
||||
setList: setFormData,
|
||||
getItemId: item => item.NAME,
|
||||
addItemId: (item, _, itemIndex) => ({ ...item, NAME: `${TAB_ID}${itemIndex}` })
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
color: 'secondary',
|
||||
'data-cy': 'add-sched-action',
|
||||
label: T.AddAction,
|
||||
variant: 'outlined'
|
||||
}}
|
||||
options={[{
|
||||
cy: 'add-sched-action-punctual',
|
||||
name: 'Punctual action',
|
||||
dialogProps: { title: T.ScheduledAction },
|
||||
form: () => PunctualForm(),
|
||||
onSubmit: handleSave
|
||||
},
|
||||
{
|
||||
cy: 'add-sched-action-relative',
|
||||
name: 'Relative action',
|
||||
dialogProps: { title: T.ScheduledAction },
|
||||
form: () => RelativeForm(),
|
||||
onSubmit: handleSave
|
||||
}]}
|
||||
/>
|
||||
<div className={classes.root}>
|
||||
{scheduleActions?.map(item => {
|
||||
const { NAME, ACTION, TIME } = item
|
||||
const isRelative = String(TIME).includes('+')
|
||||
|
||||
return (
|
||||
<SelectCard
|
||||
key={NAME}
|
||||
title={`${NAME} - ${ACTION}`}
|
||||
action={
|
||||
<>
|
||||
<Action
|
||||
data-cy={`remove-${NAME}`}
|
||||
handleClick={() => handleRemove(NAME)}
|
||||
icon={<Trash />}
|
||||
/>
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
'data-cy': `edit-${NAME}`,
|
||||
icon: <Edit />,
|
||||
tooltip: <Translate word={T.Edit} />
|
||||
}}
|
||||
options={[{
|
||||
dialogProps: {
|
||||
title: <><Translate word={T.Edit} />{`: ${NAME}`}</>
|
||||
},
|
||||
form: () => isRelative
|
||||
? RelativeForm(undefined, item)
|
||||
: PunctualForm(undefined, item),
|
||||
onSubmit: newValues => handleSave(newValues, NAME)
|
||||
}]}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ScheduleAction.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
ScheduleAction.displayName = 'ScheduleAction'
|
||||
|
||||
export default ScheduleAction
|
@ -14,83 +14,14 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { array, object, string } from 'yup'
|
||||
import { ObjectSchema } from 'yup'
|
||||
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
import { getValidationFromFields } from 'client/utils'
|
||||
// get schemas from VmTemplate/CreateForm
|
||||
import { SCHEMA as CREATE_EXTRA_SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
|
||||
import { HYPERVISORS } from 'client/constants'
|
||||
|
||||
export const HOST_REQ_FIELD = {
|
||||
name: 'SCHED_REQUIREMENTS',
|
||||
label: 'Host requirements expression',
|
||||
tooltip: `
|
||||
Boolean expression that rules out provisioning hosts
|
||||
from list of machines suitable to run this VM`,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
export const HOST_RANK_FIELD = {
|
||||
name: 'SCHED_RANK',
|
||||
label: 'Host policy expression',
|
||||
tooltip: `
|
||||
This field sets which attribute will be used
|
||||
to sort the suitable hosts for this VM`,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
export const DS_REQ_FIELD = {
|
||||
name: 'DS_SCHED_REQUIREMENTS',
|
||||
label: 'Datastore requirements expression',
|
||||
tooltip: `
|
||||
Boolean expression that rules out entries from
|
||||
the pool of datastores suitable to run this VM.`,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
export const DS_RANK_FIELD = {
|
||||
name: 'DS_SCHED_RANK',
|
||||
label: 'Datastore policy expression',
|
||||
tooltip: `
|
||||
This field sets which attribute will be used to
|
||||
sort the suitable datastores for this VM`,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
export const SCHEMA = object({
|
||||
DISK: array()
|
||||
.ensure()
|
||||
.transform(disks => disks?.map((disk, idx) => ({
|
||||
...disk,
|
||||
NAME: disk?.NAME?.startsWith('DISK') || !disk?.NAME
|
||||
? `DISK${idx}`
|
||||
: disk?.NAME
|
||||
}))),
|
||||
NIC: array()
|
||||
.ensure()
|
||||
.transform(nics => nics?.map((nic, idx) => ({
|
||||
...nic,
|
||||
NAME: nic?.NAME?.startsWith('NIC') || !nic?.NAME
|
||||
? `NIC${idx}`
|
||||
: nic?.NAME
|
||||
}))),
|
||||
SCHED_ACTION: array()
|
||||
.ensure()
|
||||
.transform(actions => actions?.map((action, idx) => ({
|
||||
...action,
|
||||
NAME: action?.NAME?.startsWith('SCHED_ACTION') || !action?.NAME
|
||||
? `SCHED_ACTION${idx}`
|
||||
: action?.NAME
|
||||
}))),
|
||||
OS: object({
|
||||
BOOT: string().trim().notRequired()
|
||||
}),
|
||||
...getValidationFromFields([
|
||||
HOST_REQ_FIELD,
|
||||
HOST_RANK_FIELD,
|
||||
DS_REQ_FIELD,
|
||||
DS_RANK_FIELD
|
||||
])
|
||||
}).noUnknown(false)
|
||||
/**
|
||||
* @param {HYPERVISORS} hypervisor - VM hypervisor
|
||||
* @returns {ObjectSchema} Extra configuration schema
|
||||
*/
|
||||
export const SCHEMA = hypervisor => CREATE_EXTRA_SCHEMA(hypervisor).noUnknown(false)
|
||||
|
@ -1,192 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 PropTypes from 'prop-types'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Edit, Trash } from 'iconoir-react'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
|
||||
import { useListForm } from 'client/hooks'
|
||||
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
|
||||
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
|
||||
import { ImageSteps, VolatileSteps } from 'client/components/Forms/Vm'
|
||||
import { StatusCircle, StatusChip } from 'client/components/Status'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import { SCHEMA as EXTRA_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
|
||||
import { reorderBootAfterRemove } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting'
|
||||
import { getState, getDiskType } from 'client/models/Image'
|
||||
import { stringToBoolean } from 'client/models/Helper'
|
||||
import { prettyBytes } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
paddingBlock: '1em',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, auto))',
|
||||
gap: '1em'
|
||||
}
|
||||
})
|
||||
|
||||
export const TAB_ID = 'DISK'
|
||||
|
||||
const Storage = ({ data, setFormData, hypervisor, control }) => {
|
||||
const classes = useStyles()
|
||||
const disks = useWatch({ name: `${EXTRA_ID}.${TAB_ID}`, control })
|
||||
|
||||
const { handleSetList, handleRemove, handleSave } = useListForm({
|
||||
parent: EXTRA_ID,
|
||||
key: TAB_ID,
|
||||
list: disks,
|
||||
setList: setFormData,
|
||||
getItemId: item => item.NAME,
|
||||
addItemId: (item, _, itemIndex) => ({ ...item, NAME: `${TAB_ID}${itemIndex}` })
|
||||
})
|
||||
|
||||
const reorderDisks = () => {
|
||||
const diskSchema = EXTRA_SCHEMA.pick([TAB_ID])
|
||||
const { [TAB_ID]: newList } = diskSchema.cast({ [TAB_ID]: data?.[TAB_ID] })
|
||||
|
||||
handleSetList(newList)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
color: 'secondary',
|
||||
'data-cy': 'add-disk',
|
||||
label: T.AttachDisk,
|
||||
variant: 'outlined'
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
cy: 'attach-image-disk',
|
||||
name: T.Image,
|
||||
dialogProps: { title: T.AttachImage },
|
||||
form: () => ImageSteps({ hypervisor }),
|
||||
onSubmit: handleSave
|
||||
},
|
||||
{
|
||||
cy: 'attach-volatile-disk',
|
||||
name: T.Volatile,
|
||||
dialogProps: { title: T.AttachVolatile },
|
||||
form: () => VolatileSteps({ hypervisor }),
|
||||
onSubmit: handleSave
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<div className={classes.root}>
|
||||
{disks?.map(item => {
|
||||
const {
|
||||
NAME,
|
||||
TYPE,
|
||||
IMAGE,
|
||||
IMAGE_ID,
|
||||
IMAGE_STATE,
|
||||
ORIGINAL_SIZE,
|
||||
SIZE = ORIGINAL_SIZE,
|
||||
READONLY,
|
||||
DATASTORE,
|
||||
PERSISTENT
|
||||
} = item
|
||||
|
||||
const isVolatile = !IMAGE && !IMAGE_ID
|
||||
const isPersistent = stringToBoolean(PERSISTENT)
|
||||
const state = !isVolatile && getState({ STATE: IMAGE_STATE })
|
||||
const type = isVolatile ? TYPE : getDiskType(item)
|
||||
const originalSize = +ORIGINAL_SIZE ? prettyBytes(+ORIGINAL_SIZE, 'MB') : '-'
|
||||
const size = prettyBytes(+SIZE, 'MB')
|
||||
|
||||
return (
|
||||
<SelectCard
|
||||
key={NAME}
|
||||
title={isVolatile ? (
|
||||
<>
|
||||
{`${NAME} - `}
|
||||
<Translate word={T.VolatileDisk} />
|
||||
</>
|
||||
) : (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5em' }}>
|
||||
<StatusCircle color={state?.color} tooltip={state?.name} />
|
||||
{`${NAME}: ${IMAGE}`}
|
||||
{isPersistent && <StatusChip text='PERSISTENT' />}
|
||||
</span>
|
||||
)}
|
||||
subheader={<>
|
||||
{Object
|
||||
.entries({
|
||||
[DATASTORE]: DATASTORE,
|
||||
READONLY: stringToBoolean(READONLY),
|
||||
PERSISTENT: stringToBoolean(PERSISTENT),
|
||||
[isVolatile || ORIGINAL_SIZE === SIZE ? size : `${originalSize}/${size}`]: true,
|
||||
[type]: type
|
||||
})
|
||||
.map(([k, v]) => v ? `${k}` : '')
|
||||
.filter(Boolean)
|
||||
.join(' | ')
|
||||
}
|
||||
</>}
|
||||
action={
|
||||
<>
|
||||
<Action
|
||||
data-cy={`remove-${NAME}`}
|
||||
tooltip={<Translate word={T.Remove} />}
|
||||
handleClick={() => {
|
||||
handleRemove(NAME)
|
||||
reorderDisks()
|
||||
reorderBootAfterRemove(NAME, disks, data, setFormData)
|
||||
}}
|
||||
icon={<Trash />}
|
||||
/>
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
'data-cy': `edit-${NAME}`,
|
||||
icon: <Edit />,
|
||||
tooltip: <Translate word={T.Edit} />
|
||||
}}
|
||||
options={[{
|
||||
dialogProps: {
|
||||
title: <Translate word={T.EditSomething} values={[NAME]} />
|
||||
},
|
||||
form: () => isVolatile
|
||||
? VolatileSteps({ hypervisor }, item)
|
||||
: ImageSteps({ hypervisor }, item),
|
||||
onSubmit: newValues => handleSave(newValues, NAME)
|
||||
}]}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Storage.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
Storage.displayName = 'Storage'
|
||||
|
||||
export default Storage
|
@ -59,7 +59,7 @@ const Content = ({ data, setFormData }) => {
|
||||
// needs hypervisor to strip unknown attributes
|
||||
[CONFIGURATION_ID]: CONFIGURATION_SCHEMA?.({ [STEP_ID]: [extendedTemplate] })
|
||||
.cast(extendedTemplate?.TEMPLATE, { stripUnknown: true }),
|
||||
[EXTRA_ID]: EXTRA_SCHEMA
|
||||
[EXTRA_ID]: EXTRA_SCHEMA(extendedTemplate?.TEMPLATE?.HYPERVISOR)
|
||||
.cast(extendedTemplate?.TEMPLATE, { stripUnknown: true })
|
||||
}))
|
||||
|
||||
|
@ -25,12 +25,13 @@ import { isDevelopment } from 'client/utils'
|
||||
|
||||
function InstantiateVmTemplate () {
|
||||
const history = useHistory()
|
||||
const { state: { ID: templateId } = {} } = useLocation()
|
||||
const { state: template = {} } = useLocation()
|
||||
const { ID: templateId } = template ?? {}
|
||||
|
||||
const { enqueueInfo } = useGeneralApi()
|
||||
const { instantiate } = useVmTemplateApi()
|
||||
|
||||
const onSubmit = async ([templateSelected, templates]) => {
|
||||
const onSubmit = async ([templateSelected = template, templates]) => {
|
||||
try {
|
||||
const { ID, NAME } = templateSelected
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user