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

F #5637: Refactor advanced options step on instantiate form (#1624)

This commit is contained in:
Sergio Betanzos 2021-11-26 15:16:42 +01:00 committed by GitHub
parent c7eba9750c
commit 3200326904
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 89 additions and 928 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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