1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-12 08:58:17 +03:00

F OpenNebula/one#5833: Add missing parents/rdp selector (#3130)

Signed-off-by: Victor Hansson <vhansson@opennebula.io>
This commit is contained in:
vichansson 2024-06-28 15:39:29 +03:00 committed by GitHub
parent fe8c1ca14a
commit 6c32a90526
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 251 additions and 153 deletions

View File

@ -29,6 +29,15 @@ const SHUTDOWN_ENUMS_ONEFLOW = {
[SHUTDOWN_TYPES.terminateHard]: 'shutdown-hard',
}
const RDP_FIELD = {
name: 'rdp',
label: T.Rdp,
validation: string()
.trim()
.notRequired()
.default(() => undefined),
}
const SHUTDOWN_TYPE = {
name: `${ADVANCED_SECTION_ID}.SHUTDOWNTYPE`,
label: T.VMShutdownAction,
@ -50,5 +59,5 @@ const SHUTDOWN_TYPE = {
export const ADVANCED_PARAMS_FIELDS = [SHUTDOWN_TYPE]
export const ADVANCED_PARAMS_SCHEMA = object(
getValidationFromFields(ADVANCED_PARAMS_FIELDS)
getValidationFromFields([...ADVANCED_PARAMS_FIELDS, RDP_FIELD])
)

View File

@ -149,12 +149,12 @@ const ElasticityPoliciesSection = ({ stepId, selectedRoleIndex }) => {
index
) => {
const secondaryFields = [
`${Tr(T.Expression)}: ${EXPRESSION}`,
`${Tr(T.Adjust)}: ${ADJUST}`,
`${Tr(T.Cooldown)}: ${COOLDOWN}`,
`${Tr(T.Period)}: ${PERIOD}`,
`#: ${PERIOD_NUMBER}`,
]
EXPRESSION && `${Tr(T.Expression)}: ${EXPRESSION}`,
ADJUST && `${Tr(T.Adjust)}: ${ADJUST}`,
COOLDOWN && `${Tr(T.Cooldown)}: ${COOLDOWN}`,
PERIOD && `${Tr(T.Period)}: ${PERIOD}`,
PERIOD_NUMBER && `#: ${PERIOD_NUMBER}`,
].filter(Boolean)
if (MIN !== undefined && TYPE === 'PERCENTAGE_CHANGE') {
secondaryFields.push(`${Tr(T.Min)}: ${MIN}`)
}

View File

@ -14,15 +14,14 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import PropTypes from 'prop-types'
import { Component, useMemo, useEffect } from 'react'
import { Component, useMemo } from 'react'
import { Box, FormControl } from '@mui/material'
import { createMinMaxVmsFields } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig/MinMaxVms/schema'
import { FormWithSchema } from 'client/components/Forms'
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'
import { STEP_ID as ROLE_DEFINITION_ID } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles'
import { STEP_ID as ROLE_CONFIG_ID } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig'
import { useFieldArray, useFormContext } from 'react-hook-form'
export const SECTION_ID = 'MINMAXVMS'
/**
* @param {object} root0 - props
* @param {string} root0.stepId - Main step ID
@ -30,29 +29,7 @@ export const SECTION_ID = 'MINMAXVMS'
* @returns {Component} - component
*/
const MinMaxVms = ({ stepId, selectedRoleIndex }) => {
const { control, setValue, getValues, setError, clearErrors } =
useFormContext()
const watchedActiveField = useWatch({
control,
name: `${stepId}.${SECTION_ID}.${selectedRoleIndex}`,
})
const cardinality = useMemo(() => {
const baseCardinality =
getValues(ROLE_DEFINITION_ID)?.[selectedRoleIndex]?.CARDINALITY
const minVms =
getValues(ROLE_CONFIG_ID)?.[SECTION_ID]?.[selectedRoleIndex]?.min_vms
const maxVms =
getValues(ROLE_CONFIG_ID)?.[SECTION_ID]?.[selectedRoleIndex]?.max_vms
return {
min_vms: minVms,
max_vms: maxVms,
default: baseCardinality,
}
}, [selectedRoleIndex])
const { control } = useFormContext()
const fields = createMinMaxVmsFields(
`${stepId}.${SECTION_ID}.${selectedRoleIndex}`
@ -67,38 +44,6 @@ const MinMaxVms = ({ stepId, selectedRoleIndex }) => {
return null
}
useEffect(() => {
const validateFields = (activeFields) => {
const { min_vms: minVms, max_vms: maxVms } = activeFields
if (maxVms < minVms) {
;['min_vms', 'max_vms'].forEach((field) => {
setError(`${stepId}.${SECTION_ID}.${selectedRoleIndex}.${field}`, {
type: 'manual',
message: `Min/Max validation error`,
})
})
} else if (maxVms >= minVms) {
;['min_vms', 'max_vms'].forEach((field) => {
clearErrors(`${stepId}.${SECTION_ID}.${selectedRoleIndex}.${field}`)
})
}
}
watchedActiveField && validateFields(watchedActiveField)
}, [watchedActiveField])
// Set default values
useEffect(() => {
fields.forEach((field) => {
setValue(
field.name,
cardinality?.[field?.name?.split('.')?.at(-1)] ??
cardinality?.default ??
0
)
})
}, [cardinality])
return (
<Box>
<FormControl

View File

@ -139,13 +139,15 @@ const ScheduledPoliciesSection = ({ stepId, selectedRoleIndex }) => {
{ TIMEFORMAT, SCHEDTYPE, ADJUST, MIN, TIMEEXPRESSION },
index
) => {
const timeformatTrans = Tr(TIMEFORMAT)
const timeFormatTrans = Tr(TIMEFORMAT)
const secondaryFields = [
`${Tr(T.TimeExpression)}: ${TIMEEXPRESSION}`,
`${Tr(T.Adjust)}: ${ADJUST}`,
`${Tr(T.TimeFormat)}: ${timeformatTrans}`,
]
TIMEEXPRESSION &&
`${Tr(T.TimeExpression)}: ${TIMEEXPRESSION}`,
ADJUST && `${Tr(T.Adjust)}: ${ADJUST}`,
timeFormatTrans &&
`${Tr(T.TimeFormat)}: ${timeFormatTrans}`,
].filter(Boolean)
if (MIN !== undefined) {
secondaryFields?.push(`${Tr(T.Min)}: ${MIN}`)

View File

@ -60,7 +60,7 @@ export const createScheduledPolicyFields = (pathPrefix) => {
}),
validation: string()
.trim()
.required()
.oneOf(Object.keys(SCHED_TYPES))
.default(() => Object.keys(SCHED_TYPES)[0]),
grid: { xs: 12, sm: 6, md: 3.3 },
},
@ -71,7 +71,6 @@ export const createScheduledPolicyFields = (pathPrefix) => {
cy: 'roleconfig-scheduledpolicies',
validation: string()
.trim()
.required()
.default(() => ''),
grid: { xs: 12, sm: 6, md: 3.1 },
},

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import PropTypes from 'prop-types'
import { useFieldArray, useFormContext } from 'react-hook-form'
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'
import { useEffect, useState, useRef, useMemo, Component } from 'react'
import { DataGrid } from '@mui/x-data-grid'
import makeStyles from '@mui/styles/makeStyles'
@ -65,12 +65,18 @@ const RoleNetwork = ({ stepId, selectedRoleIndex }) => {
}, [selectedRoleIndex, SECTION_ID, stepId])
const classes = useStyles()
const { getValues, setValue } = useFormContext()
const { control, getValues, setValue } = useFormContext()
const { fields, update } = useFieldArray({
name: fieldArrayLocation,
})
const watchedRdpConfig = useWatch({
control,
name: `${stepId}.RDP`,
defaultValue: {},
})
useEffect(() => {
const networkDefinitions = getValues(EXTRA_ID)?.NETWORKING ?? []
const networkMap = networkDefinitions.map((network) => ({
@ -145,6 +151,16 @@ const RoleNetwork = ({ stepId, selectedRoleIndex }) => {
}
}, [fieldArrayLocation])
const handleSetRdp = (row) => {
const existing = getValues(`${stepId}.RDP`) || {}
const updatedRdp = {
...existing,
[selectedRoleIndex]:
typeof row === 'object' && row !== null ? row?.name : '',
}
setValue(`${stepId}.RDP`, updatedRdp)
}
const handleSelectRow = (row, forceSelect = false) => {
const fieldArray = getValues(fieldArrayLocation)
const fieldArrayIndex = fieldArray?.findIndex((f) => f?.idx === row?.idx)
@ -152,6 +168,14 @@ const RoleNetwork = ({ stepId, selectedRoleIndex }) => {
? true
: !fieldArray?.[fieldArrayIndex]?.rowSelected
if (
// if rowSelected === true, its being deselected
row?.rowSelected &&
getValues(`${stepId}.RDP`)?.[selectedRoleIndex] === row?.name
) {
handleSetRdp(null) // Deselect
}
const updatedFieldArray = fieldArray?.map((f, index) => {
if (index === fieldArrayIndex) {
return { ...f, rowSelected: rowToggle, aliasSelected: false }
@ -292,6 +316,22 @@ const RoleNetwork = ({ stepId, selectedRoleIndex }) => {
rowsPerPageOptions={[5, 10, 25, 50, 100]}
disableSelectionOnClick
/>
{networks?.length > 0 && (
<Box sx={{ mb: 2, mt: 4 }}>
<Autocomplete
options={(getValues(fieldArrayLocation) || [])?.filter(
(row) => row?.rowSelected
)}
value={watchedRdpConfig?.[selectedRoleIndex] ?? ''}
getOptionLabel={(option) => option?.name || option || ''}
onChange={(_event, value) => handleSetRdp(value)}
renderInput={(params) => (
<TextField {...params} name="RDP" placeholder={Tr(T.Rdp)} />
)}
/>
</Box>
)}
</Box>
)
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { array, object } from 'yup'
import { array, object, mixed } from 'yup'
import { ADVANCED_PARAMS_SCHEMA } from './AdvancedParameters/schema'
import { createElasticityPoliciesSchema } from './ElasticityPolicies/schema'
@ -35,5 +35,8 @@ export const SCHEMA = object()
.shape({
NETWORKS: array(),
NETWORKDEFS: array(),
// Set to mixed, casting wont work for dynamically calculated keys
// In reality should be [number()]: string()
RDP: mixed(),
})
.concat(ADVANCED_PARAMS_SCHEMA)

View File

@ -13,9 +13,15 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Component } from 'react'
import Component from 'react'
import {
Box,
TextField,
Typography,
Checkbox,
Autocomplete,
} from '@mui/material'
import PropTypes from 'prop-types'
import { Box, TextField, Typography } from '@mui/material'
import { T } from 'client/constants'
import { Tr } from 'client/components/HOC'
@ -29,21 +35,27 @@ import { Tr } from 'client/components/HOC'
* @returns {Component} The rendered component.
*/
const RoleVmVmPanel = ({ roles, onChange, selectedRoleIndex }) => {
const handleInputChange = (event, passedName = '') => {
let value
let name = passedName
if (typeof event === 'object' && event?.target) {
const { name: eventName = '', value: eventValue = '' } =
event.target || {}
value = eventValue
name = passedName || eventName
} else {
value = event
}
onChange({ ...roles[selectedRoleIndex], [name]: value }) // updated role
const handleInputChange = (name, value) => {
const updatedRole = { ...roles[selectedRoleIndex], [name]: value }
onChange(updatedRole)
}
const handleTextFieldChange = (event) => {
const { name, value } = event.target
handleInputChange(name, value)
}
const handleAutocompleteChange = (event, value) => {
const parentNames = value.map((option) => option.NAME)
handleInputChange('PARENTS', parentNames)
}
const isDisabled = !roles?.[selectedRoleIndex] || roles?.length <= 0
const selectedRole = roles?.[selectedRoleIndex] || {}
const selectedParentRoles = roles?.filter((role) =>
selectedRole?.PARENTS?.includes(role?.NAME)
)
return (
<Box p={2}>
@ -53,8 +65,8 @@ const RoleVmVmPanel = ({ roles, onChange, selectedRoleIndex }) => {
<TextField
label={Tr(T.RoleName)}
name="NAME"
value={roles?.[selectedRoleIndex]?.NAME ?? ''}
onChange={handleInputChange}
value={selectedRole?.NAME || ''}
onChange={handleTextFieldChange}
disabled={isDisabled}
inputProps={{ 'data-cy': `role-name-${selectedRoleIndex}` }}
fullWidth
@ -65,10 +77,10 @@ const RoleVmVmPanel = ({ roles, onChange, selectedRoleIndex }) => {
<TextField
type="number"
label={Tr(T.NumberOfVms)}
value={roles?.[selectedRoleIndex]?.CARDINALITY ?? 0}
onChange={handleInputChange}
disabled={isDisabled}
name="CARDINALITY"
value={selectedRole?.CARDINALITY || 0}
onChange={handleTextFieldChange}
disabled={isDisabled}
InputProps={{
inputProps: {
min: 0,
@ -78,6 +90,32 @@ const RoleVmVmPanel = ({ roles, onChange, selectedRoleIndex }) => {
fullWidth
/>
</Box>
{roles?.length >= 2 && (
<Box sx={{ mb: 2 }}>
<Autocomplete
multiple
options={roles?.filter((_, idx) => idx !== selectedRoleIndex)}
disableCloseOnSelect
getOptionLabel={(option) => option?.NAME}
value={selectedParentRoles}
onChange={handleAutocompleteChange}
renderOption={(props, option, { selected }) => (
<li {...props}>
<Checkbox style={{ marginRight: 8 }} checked={selected} />
{option?.NAME}
</li>
)}
renderInput={(params) => (
<TextField
{...params}
name="PARENTS"
placeholder={Tr(T.ParentRoles)}
/>
)}
/>
</Box>
)}
</Box>
</Box>
)

View File

@ -38,6 +38,14 @@ const CARDINALITY_FIELD = {
.default(() => 0),
}
const PARENTS_FIELD = {
name: 'parents',
label: T.ParentRoles,
validation: array()
.notRequired()
.default(() => []),
}
const SELECTED_VM_TEMPLATE_ID_FIELD = {
name: 'selected_vm_template_id',
validation: array()
@ -50,6 +58,7 @@ const SELECTED_VM_TEMPLATE_ID_FIELD = {
const ROLE_SCHEMA = object().shape({
NAME: ROLE_NAME_FIELD.validation,
CARDINALITY: CARDINALITY_FIELD.validation,
PARENTS: PARENTS_FIELD.validation,
SELECTED_VM_TEMPLATE_ID: SELECTED_VM_TEMPLATE_ID_FIELD.validation,
})

View File

@ -68,12 +68,15 @@ const Steps = createSteps([General, Extra, RoleDefinition, RoleConfig], {
NAME: role?.name,
CARDINALITY: role?.cardinality,
SELECTED_VM_TEMPLATE_ID: [role?.vm_template.toString()],
...(role?.parents ? { PARENTS: role?.parents } : {}),
}))
const roleDefinitionData = definedRoles?.map((role) => ({
...role,
}))
const networkDefs = reversedVmTc?.map((rtc) => rtc.networks)
const roleConfigData = {
ELASTICITYPOLICIES: convertKeysToCase(
ServiceTemplate?.TEMPLATE?.BODY?.roles
@ -137,7 +140,14 @@ const Steps = createSteps([General, Extra, RoleDefinition, RoleConfig], {
Object.values(role).some((val) => val !== undefined)
),
NETWORKDEFS: reversedVmTc?.map((rtc) => rtc.networks),
NETWORKDEFS: networkDefs,
RDP: networkDefs?.reduce((acc, nics, idx) => {
const rdpRow =
nics?.filter((nic) => nic?.RDP)?.[0]?.NETWORK_ID?.slice(1) ?? ''
acc[idx] = rdpRow
return acc
}, {}),
}
const knownTemplate = schema.cast(
@ -166,72 +176,108 @@ const Steps = createSteps([General, Extra, RoleDefinition, RoleConfig], {
[ROLE_CONFIG_ID]: roleConfigData,
} = formData
const formatTemplate = {
...generalData,
roles: roleDefinitionData?.map((roleDef, index) => {
const scheduledPolicies = roleConfigData?.SCHEDULEDPOLICIES?.[
index
]?.map((policy) => {
const newPolicy = {
...policy,
TYPE: policy?.SCHEDTYPE,
ADJUST: +policy?.ADJUST,
[policy.TIMEFORMAT?.split(' ')?.join('_')?.toLowerCase()]:
policy.TIMEEXPRESSION,
const getVmTemplateContents = (index) => {
const contents = parseVmTemplateContents({
networks: roleConfigData?.NETWORKS?.[index],
rdpConfig: roleConfigData?.RDP?.[index],
schedActions: extraData?.SCHED_ACTION,
})
return contents || ''
}
const getScheduledPolicies = (index) => {
const policies = roleConfigData?.SCHEDULEDPOLICIES?.[index]?.map(
(policy) => {
const { SCHEDTYPE, ADJUST, TIMEFORMAT, TIMEEXPRESSION, ...rest } =
policy
return {
...rest,
TYPE: SCHEDTYPE,
ADJUST: Number(ADJUST),
[TIMEFORMAT?.split(' ')?.join('_')?.toLowerCase()]: TIMEEXPRESSION,
}
delete newPolicy.SCHEDTYPE
delete newPolicy.TIMEFORMAT
delete newPolicy.TIMEEXPRESSION
}
)
return newPolicy
})
return policies?.length ? policies : undefined
}
const newRoleDef = {
vm_template_contents: parseVmTemplateContents({
networks: roleConfigData?.NETWORKS?.[index] ?? undefined,
schedActions: extraData?.SCHED_ACTION ?? undefined,
}),
...roleDef,
const getElasticityPolicies = (index) => {
const elasticityPolicies = roleConfigData?.ELASTICITYPOLICIES?.[index]
if (!elasticityPolicies || elasticityPolicies.length === 0)
return undefined
...roleConfigData?.MINMAXVMS?.[index],
VM_TEMPLATE: +roleDef?.SELECTED_VM_TEMPLATE_ID?.[0],
...(scheduledPolicies &&
scheduledPolicies.length > 0 && {
scheduled_policies: scheduledPolicies,
}),
elasticity_policies: [
...roleConfigData?.ELASTICITYPOLICIES?.[index].flatMap((elap) => ({
...elap,
...(elap?.ADJUST && { adjust: +elap?.ADJUST }),
})),
],
return elasticityPolicies.map(({ ADJUST, ...rest }) => ({
...rest,
...(ADJUST && { adjust: Number(ADJUST) }),
}))
}
const getNetworks = () => {
if (!extraData?.NETWORKING?.length) return undefined
return extraData.NETWORKING.reduce((acc, network) => {
if (network?.name) {
acc[network.name] = parseNetworkString(network)
}
delete newRoleDef.SELECTED_VM_TEMPLATE_ID
delete newRoleDef.MINMAXVMS
return acc
}, {})
}
return newRoleDef
}),
...extraData?.ADVANCED,
...(extraData?.NETWORKING?.length && {
networks: extraData?.NETWORKING?.reduce((acc, network) => {
if (network?.name) {
acc[network.name] = parseNetworkString(network)
}
const getCustomAttributes = () => {
if (!extraData?.CUSTOM_ATTRIBUTES?.length) return undefined
return acc
}, {}),
}),
custom_attrs: extraData?.CUSTOM_ATTRIBUTES?.reduce((acc, cinput) => {
return extraData.CUSTOM_ATTRIBUTES.reduce((acc, cinput) => {
if (cinput?.name) {
acc[cinput.name] = parseCustomInputString(cinput)
}
return acc
}, {}),
}, {})
}
return convertKeysToCase(formatTemplate)
const getRoleParents = (index) => {
if (
!roleDefinitionData?.[index]?.PARENTS ||
!Array.isArray(roleDefinitionData?.[index]?.PARENTS) ||
roleDefinitionData?.[index]?.PARENTS?.length <= 0
)
return undefined
return roleDefinitionData?.[index]?.PARENTS
}
try {
const formatTemplate = {
...generalData,
...extraData?.ADVANCED,
roles: roleDefinitionData?.map((roleDef, index) => {
const newRoleDef = {
...roleDef,
...roleConfigData?.MINMAXVMS?.[index],
VM_TEMPLATE: Number(roleDef?.SELECTED_VM_TEMPLATE_ID?.[0]),
vm_template_contents: getVmTemplateContents(index),
parents: getRoleParents(index),
scheduled_policies: getScheduledPolicies(index),
elasticity_policies: getElasticityPolicies(index),
}
delete newRoleDef.SELECTED_VM_TEMPLATE_ID
delete newRoleDef.MINMAXVMS
return newRoleDef
}),
networks: getNetworks(),
custom_attrs: getCustomAttributes(),
}
const cleanedTemplate = convertKeysToCase(formatTemplate)
return cleanedTemplate
} catch (error) {}
},
})

View File

@ -1497,6 +1497,7 @@ module.exports = {
Roles: 'Roles',
Cardinality: 'Cardinality',
Parents: 'Parents',
ParentRoles: 'Parent roles',
AddChartes: 'Add Charters Values Configuration',
RecoverDelete: 'Recover delete',
/* Service Template schema - extra */

View File

@ -15,14 +15,16 @@
* ------------------------------------------------------------------------- */
/* eslint-disable no-useless-escape */
const formatNic = (nic, parent) => {
const formatNic = (nic, parent, rdp) => {
const [[NIC, NETWORK_ID]] = Object.entries(nic)
return `${
parent ? 'NIC_ALIAS' : 'NIC'
} = [\n NAME = \"${NIC}\",\n NETWORK_ID = \"$${
NETWORK_ID !== undefined ? NETWORK_ID.toLowerCase() : ''
}\"${parent ? `,\n PARENT = \"${parent}\"` : ''} ]\n`
}\"${rdp ? `,\n RDP = \"YES\"` : ''}${
parent ? `,\n PARENT = \"${parent}\"` : ''
} ]\n`
}
const formatAlias = (fNics) => {
@ -130,7 +132,7 @@ const formatVmTemplateContents = (
return { networks: nics, schedActions }
} else {
const { networks, schedActions } = contents
const { networks, rdpConfig, schedActions } = contents
if (!networks) {
return ''
}
@ -141,9 +143,13 @@ const formatVmTemplateContents = (
const formattedNics = networks
?.filter((net) => net?.rowSelected)
?.map((nic, index) => ({
formatNic: formatNic({
[`_NIC${index}`]: nic?.name,
}),
formatNic: formatNic(
{
[`_NIC${index}`]: nic?.name,
},
false,
nic?.name === rdpConfig
),
NIC_ID: `_NIC${index}`,
NIC_NAME: nic?.name,
...(nic?.aliasIdx !== -1 && { alias: networks?.[nic?.aliasIdx] }),