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

F #5422: Improves to VM Template form (#2114)

This commit is contained in:
Sergio Betanzos 2022-05-31 17:35:13 +02:00 committed by GitHub
parent ebac963a14
commit a3a2dc7a8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 135 additions and 116 deletions

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string, number, boolean, lazy } from 'yup'
import { string, number, boolean, lazy, ObjectSchema } from 'yup'
import { useGetHostsQuery } from 'client/features/OneApi/host'
import { getHugepageSizes } from 'client/models/Host'
@ -31,12 +31,11 @@ import {
sentenceCase,
prettyBytes,
arrayToOptions,
getObjectSchemaFromFields,
} from 'client/utils'
const { vcenter, firecracker } = HYPERVISORS
const threadsValidation = number().nullable().notRequired().integer()
const ENABLE_NUMA = {
name: 'TOPOLOGY.ENABLE_NUMA',
label: T.NumaTopology,
@ -48,104 +47,105 @@ const ENABLE_NUMA = {
grid: { md: 12 },
}
/**
* @param {HYPERVISORS} hypervisor - VM hypervisor
* @returns {Field} Pin policy field
*/
const PIN_POLICY = (hypervisor) => {
const isVCenter = hypervisor === vcenter
const isFirecracker = hypervisor === firecracker
return {
name: 'TOPOLOGY.PIN_POLICY',
label: T.PinPolicy,
tooltip: [T.PinPolicyConcept, NUMA_PIN_POLICIES.join(', ')],
type: INPUT_TYPES.SELECT,
values: arrayToOptions(NUMA_PIN_POLICIES, {
addEmpty: false,
getText: sentenceCase,
}),
validation: string()
/** @type {Field} Pin policy field */
const PIN_POLICY = {
name: 'TOPOLOGY.PIN_POLICY',
label: T.PinPolicy,
tooltip: [T.PinPolicyConcept, NUMA_PIN_POLICIES.join(', ')],
type: INPUT_TYPES.SELECT,
values: arrayToOptions(NUMA_PIN_POLICIES, {
addEmpty: false,
getText: sentenceCase,
}),
dependOf: '$general.HYPERVISOR',
validation: lazy((_, { context }) =>
string()
.trim()
.notRequired()
.default(
() =>
isFirecracker
context?.general?.HYPERVISOR === firecracker
? NUMA_PIN_POLICIES[2] // SHARED
: NUMA_PIN_POLICIES[0] // NONE
),
fieldProps: { disabled: isVCenter || isFirecracker },
}
)
),
fieldProps: (hypervisor) => ({
disabled: [vcenter, firecracker].includes(hypervisor),
}),
}
/**
* @param {HYPERVISORS} hypervisor - VM hypervisor
* @returns {Field} Cores field
*/
const CORES = (hypervisor) => ({
/** @type {Field} Cores field */
const CORES = {
name: 'TOPOLOGY.CORES',
label: T.Cores,
tooltip: T.NumaCoresConcept,
dependOf: '$general.VCPU',
type: hypervisor === vcenter ? INPUT_TYPES.SELECT : INPUT_TYPES.TEXT,
dependOf: ['$general.VCPU', '$general.HYPERVISOR'],
type: ([, hypervisor] = []) =>
hypervisor === vcenter ? INPUT_TYPES.SELECT : INPUT_TYPES.TEXT,
htmlType: 'number',
values: (vcpu) => arrayToOptions(getFactorsOfNumber(vcpu ?? 0)),
values: ([vcpu] = []) => arrayToOptions(getFactorsOfNumber(vcpu ?? 0)),
validation: number()
.notRequired()
.integer()
.default(() => undefined),
})
}
/**
* @param {HYPERVISORS} hypervisor - VM hypervisor
* @returns {Field} Sockets field
*/
const SOCKETS = (hypervisor) => ({
/** @type {Field} Sockets field */
const SOCKETS = {
name: 'TOPOLOGY.SOCKETS',
label: T.Sockets,
tooltip: T.NumaSocketsConcept,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
dependOf: ['$general.HYPERVISOR', '$general.VCPU', 'TOPOLOGY.CORES'],
validation: number()
.notRequired()
.integer()
.default(() => 1),
fieldProps: {
disabled: hypervisor === firecracker,
},
...(hypervisor === vcenter && {
fieldProps: { disabled: true },
dependOf: ['$general.VCPU', 'TOPOLOGY.CORES'],
watcher: ([vcpu, cores] = []) => {
if (!isNaN(+vcpu) && !isNaN(+cores) && +cores !== 0) {
return vcpu / cores
}
},
fieldProps: (hypervisor) => ({
disabled: [vcenter, firecracker].includes(hypervisor),
}),
})
watcher: ([hypervisor, vcpu, cores] = []) => {
if (hypervisor === vcenter) return
/**
* @param {HYPERVISORS} hypervisor - VM hypervisor
* @returns {Field} Threads field
*/
const THREADS = (hypervisor) => ({
if (!isNaN(+vcpu) && !isNaN(+cores) && +cores !== 0) {
return vcpu / cores
}
},
}
const emptyStringToNull = (value, originalValue) =>
originalValue === '' ? null : value
const threadsValidation = number()
.nullable()
.integer()
.transform(emptyStringToNull)
/** @type {Field} Threads field */
const THREADS = {
name: 'TOPOLOGY.THREADS',
label: T.Threads,
tooltip: T.ThreadsConcept,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
validation: threadsValidation,
...(hypervisor === firecracker && {
type: INPUT_TYPES.SELECT,
values: arrayToOptions([1, 2]),
validation: threadsValidation.min(1).max(2),
}),
...(hypervisor === vcenter && {
type: INPUT_TYPES.SELECT,
values: arrayToOptions([1]),
validation: threadsValidation.min(1).max(1),
}),
})
dependOf: '$general.HYPERVISOR',
type: (hypervisor) =>
[firecracker, vcenter].includes(hypervisor)
? INPUT_TYPES.SELECT
: INPUT_TYPES.TEXT,
values: (hypervisor) =>
({
[firecracker]: arrayToOptions([1, 2]),
[vcenter]: arrayToOptions([1]),
}[hypervisor]),
validation: lazy(
(_, { context }) =>
({
[firecracker]: threadsValidation.min(1).max(2),
[vcenter]: threadsValidation.min(1).max(1),
}[context?.general?.HYPERVISOR] || threadsValidation)
),
}
/** @type {Field} Hugepage size field */
const HUGEPAGES = {
@ -200,4 +200,23 @@ const NUMA_FIELDS = (hypervisor) =>
*/
const SCHEMA_FIELDS = (hypervisor) => [ENABLE_NUMA, ...NUMA_FIELDS(hypervisor)]
export { NUMA_FIELDS, SCHEMA_FIELDS as FIELDS, ENABLE_NUMA }
/**
* @param {string} [hypervisor] - VM hypervisor
* @returns {ObjectSchema} Schema for NUMA fields
*/
const NUMA_SCHEMA = (hypervisor) =>
getObjectSchemaFromFields(SCHEMA_FIELDS(hypervisor)).afterSubmit((result) => {
const { TOPOLOGY, ...ensuredResult } = result
const { ENABLE_NUMA: isEnabled, ...restOfTopology } = TOPOLOGY
isEnabled && (ensuredResult.TOPOLOGY = { ...restOfTopology })
return { ...ensuredResult }
})
export {
NUMA_FIELDS,
SCHEMA_FIELDS as FIELDS,
NUMA_SCHEMA as SCHEMA,
ENABLE_NUMA,
}

View File

@ -19,7 +19,7 @@ import { HYPERVISORS } from 'client/constants'
import { getObjectSchemaFromFields } from 'client/utils'
import { FIELDS as PLACEMENT_FIELDS } from './placement/schema'
import { FIELDS as OS_FIELDS, BOOT_ORDER_FIELD } from './booting/schema'
import { FIELDS as NUMA_FIELDS } from './numa/schema'
import { SCHEMA as NUMA_SCHEMA, FIELDS as NUMA_FIELDS } from './numa/schema'
import { SCHEMA as IO_SCHEMA } from './inputOutput/schema'
import { SCHEMA as CONTEXT_SCHEMA } from './context/schema'
import { SCHEMA as STORAGE_SCHEMA } from './storage/schema'
@ -57,12 +57,9 @@ export const SCHEMA = (hypervisor) =>
.concat(CONTEXT_SCHEMA(hypervisor))
.concat(IO_SCHEMA(hypervisor))
.concat(
getObjectSchemaFromFields([
...PLACEMENT_FIELDS,
...OS_FIELDS(hypervisor),
...NUMA_FIELDS(hypervisor),
])
getObjectSchemaFromFields([...PLACEMENT_FIELDS, ...OS_FIELDS(hypervisor)])
)
.concat(NUMA_SCHEMA(hypervisor))
export {
mapNameByIndex,

View File

@ -78,12 +78,15 @@ const modificationTypeInput = (fieldName, { type: typeId }) => ({
getText: (type) => sentenceCase(type),
}),
validation: lazy((_, { context }) =>
string().default(() => {
const capacityUserInput = context.extra?.USER_INPUTS?.[fieldName]
const { type } = getUserInputParams(capacityUserInput)
string()
.default(() => {
const capacityUserInput = context.extra?.USER_INPUTS?.[fieldName]
const { type } = getUserInputParams(capacityUserInput)
return type
})
return type
})
// Modification type is not required in template
.afterSubmit(() => undefined)
),
grid: { md: 3 },
})

View File

@ -174,28 +174,23 @@ const Actions = () => {
},
},
form: (rows) => {
const vmTemplates = rows?.map(({ original }) => original)
const stepProps = { isMultiple: vmTemplates.length > 1 }
const initialValues = {
name: `Copy of ${vmTemplates?.[0]?.NAME}`,
}
const names = rows?.map(({ original }) => original?.NAME)
const stepProps = { isMultiple: names.length > 1 }
const initialValues = { name: `Copy of ${names?.[0]}` }
return CloneForm({ stepProps, initialValues })
},
onSubmit: (rows) => async (formData) => {
const { prefix, ...restOfData } = formData
onSubmit:
(rows) =>
async ({ prefix, name } = {}) => {
const vmTemplates = rows?.map?.(
({ original: { ID, NAME } = {} }) =>
// overwrite all names with prefix+NAME
({ id: ID, name: prefix ? `${prefix} ${NAME}` : name })
)
const vmTemplates = rows?.map?.(
({ original: { ID, NAME } = {} }) => {
// overwrite all names with prefix+NAME
const name = prefix ? `${prefix} ${NAME}` : NAME
return { id: ID, ...restOfData, name }
}
)
await Promise.all(vmTemplates.map(clone))
},
await Promise.all(vmTemplates.map(clone))
},
},
],
},

View File

@ -15,18 +15,19 @@
* ------------------------------------------------------------------------- */
import { ReactElement, useState, memo } from 'react'
import PropTypes from 'prop-types'
import { BookmarkEmpty } from 'iconoir-react'
import { Pin as GotoIcon, RefreshDouble } from 'iconoir-react'
import { Typography, Box, Stack, Chip, IconButton } from '@mui/material'
import { Row } from 'react-table'
import vmApi from 'client/features/OneApi/vm'
import { useLazyGetVmQuery } from 'client/features/OneApi/vm'
import { VmsTable } from 'client/components/Tables'
import VmActions from 'client/components/Tables/Vms/actions'
import VmTabs from 'client/components/Tabs/Vm'
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 } from 'client/constants'
import { T, VM } from 'client/constants'
/**
* Displays a list of VMs with a split pane between the list and selected row(s).
@ -56,7 +57,7 @@ function VirtualMachines() {
<GroupedTags tags={selectedRows} />
) : (
<InfoTabs
id={selectedRows[0]?.original?.ID}
vm={selectedRows[0]?.original}
gotoPage={selectedRows[0]?.gotoPage}
/>
)}
@ -71,34 +72,39 @@ function VirtualMachines() {
/**
* Displays details of a VM.
*
* @param {string} id - VM id to display
* @param {VM} vm - VM to display
* @param {Function} [gotoPage] - Function to navigate to a page of a VM
* @returns {ReactElement} VM details
*/
const InfoTabs = memo(({ id, gotoPage }) => {
const vm = vmApi.endpoints.getVms.useQueryState(undefined, {
selectFromResult: ({ data = [] }) => data.find((item) => +item.ID === +id),
})
const InfoTabs = memo(({ vm, gotoPage }) => {
const [getVm, { isFetching }] = useLazyGetVmQuery()
return (
<Stack overflow="auto">
<Stack direction="row" alignItems="center" gap={1} mb={1}>
<Typography color="text.primary" noWrap>
{`#${id} | ${vm.NAME}`}
</Typography>
<SubmitButton
data-cy="detail-refresh"
icon={<RefreshDouble />}
tooltip={Tr(T.Refresh)}
isSubmitting={isFetching}
onClick={() => getVm({ id: vm.ID })}
/>
{gotoPage && (
<IconButton title={Tr(T.LocateOnTable)} onClick={gotoPage}>
<BookmarkEmpty />
<GotoIcon />
</IconButton>
)}
<Typography color="text.primary" noWrap>
{`#${vm.ID} | ${vm.NAME}`}
</Typography>
</Stack>
<VmTabs id={id} />
<VmTabs id={vm.ID} />
</Stack>
)
})
InfoTabs.propTypes = {
id: PropTypes.string.isRequired,
vm: PropTypes.object.isRequired,
gotoPage: PropTypes.func,
}

View File

@ -53,8 +53,7 @@ function InstantiateVmTemplate() {
const onSubmit = async (templates) => {
try {
const promises = await Promise.all(templates.map(instantiate))
promises.map((res) => res.unwrap?.())
await Promise.all(templates.map((t) => instantiate(t).unwrap()))
history.push(PATH.INSTANCE.VMS.LIST)