mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-21 14:50:08 +03:00
parent
23a1de0162
commit
129e252c12
@ -80,6 +80,7 @@ export const ENDPOINTS = [
|
||||
*
|
||||
* @typedef {object} ResourceView - Resource view file selected in redux (auth-reducer)
|
||||
* @property {string} resource_name - Resource view name
|
||||
* @property {object} features - Features about the resources
|
||||
* @property {object} actions - Bulk actions, including dialogs
|
||||
* Which buttons are visible to operate over the resources
|
||||
* @property {object} filters - List of criteria to filter the resources
|
||||
|
@ -52,7 +52,9 @@ const AutocompleteController = memo(
|
||||
onChange={(_, newValue) => {
|
||||
const newValueToChange = multiple
|
||||
? newValue?.map((value) =>
|
||||
typeof value === 'string' ? value : { text: value, value }
|
||||
['string', 'number'].includes(typeof value)
|
||||
? value
|
||||
: { text: value, value }
|
||||
)
|
||||
: newValue?.value
|
||||
|
||||
|
@ -18,7 +18,7 @@ import PropTypes from 'prop-types'
|
||||
|
||||
import { styled, FormControl, FormHelperText } from '@mui/material'
|
||||
import { Check as CheckIcon, Page as FileIcon } from 'iconoir-react'
|
||||
import { useController } from 'react-hook-form'
|
||||
import { useFormContext, useController } from 'react-hook-form'
|
||||
|
||||
import {
|
||||
ErrorHelper,
|
||||
@ -50,9 +50,8 @@ const FileController = memo(
|
||||
transform,
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
formContext = {},
|
||||
}) => {
|
||||
const { setValue, setError, clearErrors, watch } = formContext
|
||||
const { setValue, setError, clearErrors, watch } = useFormContext()
|
||||
|
||||
const {
|
||||
field: { ref, value, onChange, ...inputProps },
|
||||
@ -166,13 +165,6 @@ FileController.propTypes = {
|
||||
transform: PropTypes.func,
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
formContext: PropTypes.shape({
|
||||
setValue: PropTypes.func,
|
||||
setError: PropTypes.func,
|
||||
clearErrors: PropTypes.func,
|
||||
watch: PropTypes.func,
|
||||
register: PropTypes.func,
|
||||
}),
|
||||
}
|
||||
|
||||
FileController.displayName = 'FileController'
|
||||
|
@ -17,11 +17,11 @@ import { memo, useMemo, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { TextField } from '@mui/material'
|
||||
import { useController } from 'react-hook-form'
|
||||
import { useController, useWatch } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper, Tooltip } from 'client/components/FormControl'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { generateKey } from 'client/utils'
|
||||
import { generateKey, findClosestValue } from 'client/utils'
|
||||
|
||||
const SelectController = memo(
|
||||
({
|
||||
@ -33,9 +33,17 @@ const SelectController = memo(
|
||||
values = [],
|
||||
renderValue,
|
||||
tooltip,
|
||||
watcher,
|
||||
dependencies,
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const watch = useWatch({
|
||||
name: dependencies,
|
||||
disabled: dependencies == null,
|
||||
defaultValue: Array.isArray(dependencies) ? [] : undefined,
|
||||
})
|
||||
|
||||
const firstValue = values?.[0]?.value ?? ''
|
||||
const defaultValue = multiple ? [firstValue] : firstValue
|
||||
|
||||
@ -46,7 +54,7 @@ const SelectController = memo(
|
||||
|
||||
const needShrink = useMemo(
|
||||
() =>
|
||||
multiple || values.find((v) => v.value === optionSelected)?.text !== '',
|
||||
multiple || values.find((o) => o.value === optionSelected)?.text !== '',
|
||||
[optionSelected]
|
||||
)
|
||||
|
||||
@ -54,15 +62,29 @@ const SelectController = memo(
|
||||
if (!optionSelected && !optionSelected.length) return
|
||||
|
||||
if (multiple) {
|
||||
const exists = values.some((v) => optionSelected.includes(v.value))
|
||||
const exists = values.some((o) => optionSelected.includes(o.value))
|
||||
!exists && onChange([firstValue])
|
||||
} else {
|
||||
const exists = values.some((v) => `${v.value}` === `${optionSelected}`)
|
||||
const exists = values.some((o) => `${o.value}` === `${optionSelected}`)
|
||||
|
||||
!exists && onChange(firstValue)
|
||||
}
|
||||
}, [multiple])
|
||||
|
||||
useEffect(() => {
|
||||
if (!watcher || !dependencies) return
|
||||
if (!watch) return onChange(defaultValue)
|
||||
|
||||
const watcherValue = watcher(watch)
|
||||
const optionValues = values.map((o) => o.value)
|
||||
|
||||
const ensuredWatcherValue = isNaN(watcherValue)
|
||||
? optionValues.find((o) => `${o}` === `${watcherValue}`)
|
||||
: findClosestValue(watcherValue, optionValues)
|
||||
|
||||
onChange(ensuredWatcherValue ?? defaultValue)
|
||||
}, [watch, watcher, dependencies])
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...inputProps}
|
||||
@ -86,12 +108,12 @@ const SelectController = memo(
|
||||
}
|
||||
select
|
||||
fullWidth
|
||||
disabled={readOnly}
|
||||
SelectProps={{ native: true, multiple }}
|
||||
label={labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
InputLabelProps={{ shrink: needShrink }}
|
||||
InputProps={{
|
||||
...(multiple && { sx: { paddingTop: '0.5em' } }),
|
||||
readOnly,
|
||||
startAdornment:
|
||||
(optionSelected && renderValue?.(optionSelected)) ||
|
||||
(tooltip && <Tooltip title={tooltip} position="start" />),
|
||||
@ -115,7 +137,8 @@ const SelectController = memo(
|
||||
prev.values.length === next.values.length &&
|
||||
prev.label === next.label &&
|
||||
prev.tooltip === next.tooltip &&
|
||||
prev.multiple === next.multiple
|
||||
prev.multiple === next.multiple &&
|
||||
prev.readOnly === next.readOnly
|
||||
)
|
||||
|
||||
SelectController.propTypes = {
|
||||
@ -127,6 +150,11 @@ SelectController.propTypes = {
|
||||
multiple: PropTypes.bool,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
renderValue: PropTypes.func,
|
||||
watcher: PropTypes.func,
|
||||
dependencies: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
}
|
||||
|
@ -13,11 +13,11 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo } from 'react'
|
||||
import { memo, useEffect, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { TextField, Slider, FormHelperText, Stack } from '@mui/material'
|
||||
import { useController } from 'react-hook-form'
|
||||
import { useController, useWatch } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper, Tooltip } from 'client/components/FormControl'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
@ -30,9 +30,17 @@ const SliderController = memo(
|
||||
name = '',
|
||||
label = '',
|
||||
tooltip,
|
||||
watcher,
|
||||
dependencies,
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const watch = useWatch({
|
||||
name: dependencies,
|
||||
disabled: dependencies == null,
|
||||
defaultValue: Array.isArray(dependencies) ? [] : undefined,
|
||||
})
|
||||
|
||||
const { min, max, step } = fieldProps ?? {}
|
||||
|
||||
const {
|
||||
@ -40,12 +48,33 @@ const SliderController = memo(
|
||||
fieldState: { error },
|
||||
} = useController({ name, control })
|
||||
|
||||
const handleEnsuredChange = useCallback(
|
||||
(newValue) => {
|
||||
if (min && newValue < min) return onChange(min)
|
||||
if (max && newValue > max) return onChange(max)
|
||||
},
|
||||
[onChange, min, max]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!watcher || !dependencies || !watch) return
|
||||
|
||||
const watcherValue = watcher(watch)
|
||||
watcherValue !== undefined && handleEnsuredChange(watcherValue)
|
||||
}, [watch, watcher, dependencies])
|
||||
|
||||
const sliderId = `${cy}-slider`
|
||||
const inputId = `${cy}-input`
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" mt="0.5rem" spacing={2} alignItems="center">
|
||||
<Stack
|
||||
direction="row"
|
||||
pl="1em"
|
||||
mt="0.5rem"
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
>
|
||||
<Slider
|
||||
color="secondary"
|
||||
value={typeof value === 'number' ? value : 0}
|
||||
@ -75,15 +104,11 @@ const SliderController = memo(
|
||||
step,
|
||||
}}
|
||||
onChange={(evt) =>
|
||||
onChange(!evt.target.value ? '0' : Number(evt.target.value))
|
||||
handleEnsuredChange(
|
||||
!evt.target.value ? '0' : Number(evt.target.value)
|
||||
)
|
||||
}
|
||||
onBlur={() => {
|
||||
if (min && value < min) {
|
||||
onChange(min)
|
||||
} else if (max && value > max) {
|
||||
onChange(max)
|
||||
}
|
||||
}}
|
||||
onBlur={() => handleEnsuredChange(value)}
|
||||
/>
|
||||
</Stack>
|
||||
{Boolean(error) && (
|
||||
@ -102,6 +127,11 @@ SliderController.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
watcher: PropTypes.func,
|
||||
dependencies: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useController } from 'react-hook-form'
|
||||
import { useFormContext, useController } from 'react-hook-form'
|
||||
|
||||
import Legend from 'client/components/Forms/Legend'
|
||||
import { ErrorHelper } from 'client/components/FormControl'
|
||||
@ -42,11 +42,10 @@ const TableController = memo(
|
||||
Table,
|
||||
singleSelect = true,
|
||||
getRowId = defaultGetRowId,
|
||||
formContext = {},
|
||||
readOnly = false,
|
||||
fieldProps: { initialState, ...fieldProps } = {},
|
||||
}) => {
|
||||
const { clearErrors } = formContext
|
||||
const { clearErrors } = useFormContext()
|
||||
|
||||
const {
|
||||
field: { value, onChange },
|
||||
@ -107,13 +106,6 @@ TableController.propTypes = {
|
||||
tooltip: PropTypes.any,
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
formContext: PropTypes.shape({
|
||||
setValue: PropTypes.func,
|
||||
setError: PropTypes.func,
|
||||
clearErrors: PropTypes.func,
|
||||
watch: PropTypes.func,
|
||||
register: PropTypes.func,
|
||||
}),
|
||||
}
|
||||
|
||||
TableController.displayName = 'TableController'
|
||||
|
@ -37,13 +37,11 @@ const TextController = memo(
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const watch =
|
||||
dependencies &&
|
||||
useWatch({
|
||||
control,
|
||||
name: dependencies,
|
||||
disabled: dependencies === null,
|
||||
})
|
||||
const watch = useWatch({
|
||||
name: dependencies,
|
||||
disabled: dependencies == null,
|
||||
defaultValue: Array.isArray(dependencies) ? [] : undefined,
|
||||
})
|
||||
|
||||
const {
|
||||
field: { ref, value = '', onChange, ...inputProps },
|
||||
@ -51,11 +49,11 @@ const TextController = memo(
|
||||
} = useController({ name, control })
|
||||
|
||||
useEffect(() => {
|
||||
if (watch && watcher) {
|
||||
const watcherValue = watcher(watch)
|
||||
watcherValue && onChange(watcherValue)
|
||||
}
|
||||
}, [watch])
|
||||
if (!watcher || !dependencies || !watch) return
|
||||
|
||||
const watcherValue = watcher(watch)
|
||||
watcherValue !== undefined && onChange(watcherValue)
|
||||
}, [watch, watcher, dependencies])
|
||||
|
||||
return (
|
||||
<TextField
|
||||
@ -94,7 +92,8 @@ const TextController = memo(
|
||||
prevProps.label === nextProps.label &&
|
||||
prevProps.tooltip === nextProps.tooltip &&
|
||||
prevProps.fieldProps?.value === nextProps.fieldProps?.value &&
|
||||
prevProps.fieldProps?.helperText === nextProps.fieldProps?.helperText
|
||||
prevProps.fieldProps?.helperText === nextProps.fieldProps?.helperText &&
|
||||
prevProps.readOnly === nextProps.readOnly
|
||||
)
|
||||
|
||||
TextController.propTypes = {
|
||||
|
@ -13,21 +13,23 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import {
|
||||
ReactElement,
|
||||
Fragment,
|
||||
createElement,
|
||||
memo,
|
||||
useMemo,
|
||||
useCallback,
|
||||
isValidElement,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
import { useFormContext, useWatch } from 'react-hook-form'
|
||||
import { FormControl, Accordion, AccordionSummary, Grid } from '@mui/material'
|
||||
|
||||
import * as FC from 'client/components/FormControl'
|
||||
import Legend from 'client/components/Forms/Legend'
|
||||
import { Field } from 'client/utils'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
|
||||
const NOT_DEPEND_ATTRIBUTES = [
|
||||
@ -51,19 +53,28 @@ const INPUT_CONTROLLER = {
|
||||
[INPUT_TYPES.TOGGLE]: FC.ToggleController,
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a form with a schema and a legend for each section.
|
||||
*
|
||||
* @param {object} props - Component props
|
||||
* @param {boolean} [props.accordion] - If true, the accordion will be rendered
|
||||
* @param {string} [props.id] - The form id to be used as a prefix for the field name
|
||||
* @param {string} [props.cy] - The id to be used on testing purposes
|
||||
* @param {function():Field[]|Field[]} [props.fields] - The fields to be rendered
|
||||
* @param {object} props.rootProps - The props to be passed to the root element
|
||||
* @param {*} props.legend - The legend
|
||||
* @param {string} props.legendTooltip - The legend tooltip
|
||||
* @returns {ReactElement} - The form component
|
||||
*/
|
||||
const FormWithSchema = ({
|
||||
accordion = false,
|
||||
id,
|
||||
cy,
|
||||
fields,
|
||||
rootProps,
|
||||
className,
|
||||
legend,
|
||||
legendTooltip,
|
||||
}) => {
|
||||
const formContext = useFormContext()
|
||||
const { control, watch } = formContext
|
||||
|
||||
const { sx: sxRoot, ...restOfRootProps } = rootProps ?? {}
|
||||
|
||||
const RootWrapper = useMemo(
|
||||
@ -88,25 +99,14 @@ const FormWithSchema = ({
|
||||
|
||||
const getFields = useMemo(
|
||||
() => (typeof fields === 'function' ? fields() : fields),
|
||||
[fields?.length]
|
||||
[fields]
|
||||
)
|
||||
|
||||
if (!getFields || getFields?.length === 0) return null
|
||||
|
||||
const addIdToName = useCallback(
|
||||
(name) =>
|
||||
name.startsWith('$')
|
||||
? name.slice(1) // removes character '$' and returns
|
||||
: id
|
||||
? `${id}.${name}` // concat form ID if exists
|
||||
: name,
|
||||
[id]
|
||||
)
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
className={className}
|
||||
sx={{ width: '100%', ...sxRoot }}
|
||||
{...restOfRootProps}
|
||||
>
|
||||
@ -122,56 +122,9 @@ const FormWithSchema = ({
|
||||
)}
|
||||
</LegendWrapper>
|
||||
<Grid container spacing={1} alignContent="flex-start">
|
||||
{getFields?.map?.(({ dependOf, ...attributes }) => {
|
||||
let valueOfDependField = null
|
||||
let nameOfDependField = null
|
||||
|
||||
if (dependOf) {
|
||||
nameOfDependField = Array.isArray(dependOf)
|
||||
? dependOf.map(addIdToName)
|
||||
: addIdToName(dependOf)
|
||||
|
||||
valueOfDependField = watch(nameOfDependField)
|
||||
}
|
||||
|
||||
const { name, type, htmlType, grid, ...fieldProps } =
|
||||
Object.entries(attributes).reduce((field, attribute) => {
|
||||
const [key, value] = attribute
|
||||
const isNotDependAttribute = NOT_DEPEND_ATTRIBUTES.includes(key)
|
||||
|
||||
const finalValue =
|
||||
typeof value === 'function' &&
|
||||
!isNotDependAttribute &&
|
||||
!isValidElement(value())
|
||||
? value(valueOfDependField, formContext)
|
||||
: value
|
||||
|
||||
return { ...field, [key]: finalValue }
|
||||
}, {})
|
||||
|
||||
const dataCy = `${cy}-${name}`.replaceAll('.', '-')
|
||||
const inputName = addIdToName(name)
|
||||
|
||||
const isHidden = htmlType === INPUT_TYPES.HIDDEN
|
||||
|
||||
if (isHidden) return null
|
||||
|
||||
return (
|
||||
INPUT_CONTROLLER[type] && (
|
||||
<Grid key={dataCy} item xs={12} md={6} {...grid}>
|
||||
{createElement(INPUT_CONTROLLER[type], {
|
||||
control,
|
||||
cy: dataCy,
|
||||
formContext,
|
||||
dependencies: nameOfDependField,
|
||||
name: inputName,
|
||||
type: htmlType === false ? undefined : htmlType,
|
||||
...fieldProps,
|
||||
})}
|
||||
</Grid>
|
||||
)
|
||||
)
|
||||
})}
|
||||
{getFields?.map?.((field) => (
|
||||
<FieldComponent key={field?.name} cy={cy} id={id} {...field} />
|
||||
))}
|
||||
</Grid>
|
||||
</RootWrapper>
|
||||
</FormControl>
|
||||
@ -189,7 +142,89 @@ FormWithSchema.propTypes = {
|
||||
legend: PropTypes.any,
|
||||
legendTooltip: PropTypes.string,
|
||||
rootProps: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
const FieldComponent = memo(({ id, cy, dependOf, ...attributes }) => {
|
||||
const formContext = useFormContext()
|
||||
|
||||
const addIdToName = useCallback(
|
||||
(n) => {
|
||||
// removes character '$' and returns
|
||||
if (n.startsWith('$')) return n.slice(1)
|
||||
|
||||
// concat form ID if exists
|
||||
return id ? `${id}.${n}` : n
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
const nameOfDependField = useMemo(() => {
|
||||
if (!dependOf) return null
|
||||
|
||||
return Array.isArray(dependOf)
|
||||
? dependOf.map(addIdToName)
|
||||
: addIdToName(dependOf)
|
||||
}, [dependOf, addIdToName])
|
||||
|
||||
const valueOfDependField = useWatch({
|
||||
name: nameOfDependField,
|
||||
disabled: dependOf === undefined,
|
||||
defaultValue: Array.isArray(dependOf) ? [] : undefined,
|
||||
})
|
||||
|
||||
/* const valueOfDependField = useMemo(() => {
|
||||
if (!dependOf) return null
|
||||
|
||||
return watch(nameOfDependField)
|
||||
}, [dependOf, watch, nameOfDependField]) */
|
||||
|
||||
const { name, type, htmlType, grid, ...fieldProps } = Object.entries(
|
||||
attributes
|
||||
).reduce((field, attribute) => {
|
||||
const [key, value] = attribute
|
||||
const isNotDependAttribute = NOT_DEPEND_ATTRIBUTES.includes(key)
|
||||
|
||||
const finalValue =
|
||||
typeof value === 'function' &&
|
||||
!isNotDependAttribute &&
|
||||
!isValidElement(value())
|
||||
? value(valueOfDependField, formContext)
|
||||
: value
|
||||
|
||||
return { ...field, [key]: finalValue }
|
||||
}, {})
|
||||
|
||||
const dataCy = useMemo(() => `${cy}-${name ?? ''}`.replaceAll('.', '-'), [cy])
|
||||
const inputName = useMemo(() => addIdToName(name), [addIdToName, name])
|
||||
const isHidden = useMemo(() => htmlType === INPUT_TYPES.HIDDEN, [htmlType])
|
||||
|
||||
if (isHidden) return null
|
||||
|
||||
return (
|
||||
INPUT_CONTROLLER[type] && (
|
||||
<Grid item xs={12} md={6} {...grid}>
|
||||
{createElement(INPUT_CONTROLLER[type], {
|
||||
control: formContext.control,
|
||||
cy: dataCy,
|
||||
dependencies: nameOfDependField,
|
||||
name: inputName,
|
||||
type: htmlType === false ? undefined : htmlType,
|
||||
...fieldProps,
|
||||
})}
|
||||
</Grid>
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
FieldComponent.propTypes = {
|
||||
id: PropTypes.string,
|
||||
cy: PropTypes.string,
|
||||
dependOf: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
}
|
||||
|
||||
FieldComponent.displayName = 'FieldComponent'
|
||||
|
||||
export default FormWithSchema
|
||||
|
@ -24,7 +24,7 @@ import {
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { formatNumberByCurrency } from 'client/models/Helper'
|
||||
import { Field } from 'client/utils'
|
||||
import { T, HYPERVISORS } from 'client/constants'
|
||||
import { T, HYPERVISORS, VmTemplateFeatures } from 'client/constants'
|
||||
|
||||
const commonValidation = number()
|
||||
.positive()
|
||||
@ -186,5 +186,9 @@ export const DISK_COST = generateCostCapacityInput({
|
||||
},
|
||||
})
|
||||
|
||||
/** @type {Field[]} List of showback fields */
|
||||
export const SHOWBACK_FIELDS = [MEMORY_COST, CPU_COST, DISK_COST]
|
||||
/**
|
||||
* @param {VmTemplateFeatures} features - Features of the template
|
||||
* @returns {Field[]} List of showback fields
|
||||
*/
|
||||
export const SHOWBACK_FIELDS = (features) =>
|
||||
[MEMORY_COST, !features?.hide_cpu && CPU_COST, DISK_COST].filter(Boolean)
|
||||
|
@ -38,11 +38,12 @@ const Content = ({ isUpdate }) => {
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.VM_TEMPLATE
|
||||
const dialog = getResourceView(resource)?.dialogs?.create_dialog
|
||||
const { features, dialogs } = getResourceView(resource)
|
||||
const dialog = dialogs?.create_dialog
|
||||
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
|
||||
|
||||
return (
|
||||
SECTIONS(hypervisor, isUpdate)
|
||||
SECTIONS(hypervisor, isUpdate, features)
|
||||
.filter(
|
||||
({ id, required }) => required || sectionsAvailable.includes(id)
|
||||
)
|
||||
@ -57,8 +58,8 @@ const Content = ({ isUpdate }) => {
|
||||
<FormWithSchema
|
||||
key={key}
|
||||
id={STEP_ID}
|
||||
className={classes[id]}
|
||||
cy={`${STEP_ID}-${id}`}
|
||||
rootProps={{ className: classes[id] }}
|
||||
{...section}
|
||||
/>
|
||||
))}
|
||||
|
@ -35,62 +35,64 @@ import {
|
||||
filterFieldsByHypervisor,
|
||||
getObjectSchemaFromFields,
|
||||
} from 'client/utils'
|
||||
import { T, HYPERVISORS } from 'client/constants'
|
||||
import { T, HYPERVISORS, VmTemplateFeatures } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
|
||||
* @param {boolean} [isUpdate] - If `true`, the form is being updated
|
||||
* @param {VmTemplateFeatures} [features] - Features
|
||||
* @returns {Section[]} Fields
|
||||
*/
|
||||
const SECTIONS = (hypervisor, isUpdate) => [
|
||||
{
|
||||
id: 'information',
|
||||
legend: T.Information,
|
||||
required: true,
|
||||
fields: INFORMATION_FIELDS(isUpdate),
|
||||
},
|
||||
{
|
||||
id: 'hypervisor',
|
||||
legend: T.Hypervisor,
|
||||
required: true,
|
||||
fields: [HYPERVISOR_FIELD, VROUTER_FIELD],
|
||||
},
|
||||
{
|
||||
id: 'capacity',
|
||||
legend: T.Memory,
|
||||
fields: filterFieldsByHypervisor(MEMORY_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'capacity',
|
||||
legend: T.PhysicalCpu,
|
||||
fields: filterFieldsByHypervisor(CPU_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'capacity',
|
||||
legend: T.VirtualCpu,
|
||||
fields: filterFieldsByHypervisor(VCPU_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'showback',
|
||||
legend: T.Cost,
|
||||
fields: filterFieldsByHypervisor(SHOWBACK_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'ownership',
|
||||
legend: T.Ownership,
|
||||
fields: filterFieldsByHypervisor(OWNERSHIP_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'vm_group',
|
||||
legend: T.VMGroup,
|
||||
fields: filterFieldsByHypervisor(VM_GROUP_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'vcenter',
|
||||
legend: T.vCenterDeployment,
|
||||
fields: filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor),
|
||||
},
|
||||
]
|
||||
const SECTIONS = (hypervisor, isUpdate, features) =>
|
||||
[
|
||||
{
|
||||
id: 'information',
|
||||
legend: T.Information,
|
||||
required: true,
|
||||
fields: INFORMATION_FIELDS(isUpdate),
|
||||
},
|
||||
{
|
||||
id: 'hypervisor',
|
||||
legend: T.Hypervisor,
|
||||
required: true,
|
||||
fields: [HYPERVISOR_FIELD, VROUTER_FIELD],
|
||||
},
|
||||
{
|
||||
id: 'capacity',
|
||||
legend: T.Memory,
|
||||
fields: filterFieldsByHypervisor(MEMORY_FIELDS, hypervisor),
|
||||
},
|
||||
!features?.hide_cpu && {
|
||||
id: 'capacity',
|
||||
legend: T.PhysicalCpu,
|
||||
fields: filterFieldsByHypervisor(CPU_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'capacity',
|
||||
legend: T.VirtualCpu,
|
||||
fields: filterFieldsByHypervisor(VCPU_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'showback',
|
||||
legend: T.Cost,
|
||||
fields: filterFieldsByHypervisor(SHOWBACK_FIELDS(features), hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'ownership',
|
||||
legend: T.Ownership,
|
||||
fields: filterFieldsByHypervisor(OWNERSHIP_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'vm_group',
|
||||
legend: T.VMGroup,
|
||||
fields: filterFieldsByHypervisor(VM_GROUP_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'vcenter',
|
||||
legend: T.vCenterDeployment,
|
||||
fields: filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor),
|
||||
},
|
||||
].filter(Boolean)
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
|
||||
|
@ -14,6 +14,8 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { NumberSchema } from 'yup'
|
||||
|
||||
import { scaleVcpuByCpuFactor } from 'client/models/VirtualMachine'
|
||||
import { getUserInputParams } from 'client/models/Helper'
|
||||
import {
|
||||
Field,
|
||||
@ -21,7 +23,13 @@ import {
|
||||
prettyBytes,
|
||||
isDivisibleBy,
|
||||
} from 'client/utils'
|
||||
import { T, HYPERVISORS, USER_INPUT_TYPES, VmTemplate } from 'client/constants'
|
||||
import {
|
||||
T,
|
||||
HYPERVISORS,
|
||||
USER_INPUT_TYPES,
|
||||
VmTemplate,
|
||||
VmTemplateFeatures,
|
||||
} from 'client/constants'
|
||||
|
||||
const { number, numberFloat, range, rangeFloat } = USER_INPUT_TYPES
|
||||
|
||||
@ -35,9 +43,13 @@ const valueLabelFormat = (value) => prettyBytes(value, 'MB')
|
||||
|
||||
/**
|
||||
* @param {VmTemplate} [vmTemplate] - VM Template
|
||||
* @param {VmTemplateFeatures} [features] - Features
|
||||
* @returns {Field[]} Basic configuration fields
|
||||
*/
|
||||
export const FIELDS = (vmTemplate) => {
|
||||
export const FIELDS = (
|
||||
vmTemplate,
|
||||
{ hide_cpu: hideCpu, cpu_factor: cpuFactor } = {}
|
||||
) => {
|
||||
const {
|
||||
HYPERVISOR,
|
||||
USER_INPUTS = {},
|
||||
@ -52,18 +64,21 @@ export const FIELDS = (vmTemplate) => {
|
||||
VCPU: vcpuInput = `O|${number}|| |${VCPU}`,
|
||||
} = USER_INPUTS
|
||||
|
||||
return [
|
||||
const fields = [
|
||||
{ name: 'MEMORY', ...getUserInputParams(memoryInput) },
|
||||
{ name: 'CPU', ...getUserInputParams(cpuInput) },
|
||||
!hideCpu && { name: 'CPU', ...getUserInputParams(cpuInput) },
|
||||
{ name: 'VCPU', ...getUserInputParams(vcpuInput) },
|
||||
].map(({ name, options, ...userInput }) => {
|
||||
].filter(Boolean)
|
||||
|
||||
return fields.map(({ name, options, ...userInput }) => {
|
||||
const isMemory = name === 'MEMORY'
|
||||
const isCPU = name === 'CPU'
|
||||
const isVCenter = HYPERVISOR === HYPERVISORS.vcenter
|
||||
const divisibleBy4 = isVCenter && isMemory
|
||||
const isRange = [range, rangeFloat].includes(userInput.type)
|
||||
|
||||
// set default type to number
|
||||
userInput.type ??= name === 'CPU' ? numberFloat : number
|
||||
userInput.type ??= isCPU ? numberFloat : number
|
||||
|
||||
const ensuredOptions = divisibleBy4
|
||||
? options?.filter((value) => isDivisibleBy(+value, 4))
|
||||
@ -85,6 +100,12 @@ export const FIELDS = (vmTemplate) => {
|
||||
schemaUi.fieldProps = { ...schemaUi.fieldProps, step: 4 }
|
||||
}
|
||||
|
||||
if (cpuFactor && isCPU) {
|
||||
schemaUi.readOnly = true
|
||||
schemaUi.dependOf = 'VCPU'
|
||||
schemaUi.watcher = (vcpu) => scaleVcpuByCpuFactor(vcpu, cpuFactor)
|
||||
}
|
||||
|
||||
return { ...TRANSLATES[name], ...schemaUi, grid: { md: 12 } }
|
||||
})
|
||||
}
|
||||
|
@ -36,11 +36,12 @@ const Content = ({ vmTemplate }) => {
|
||||
const sections = useMemo(() => {
|
||||
const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
|
||||
const resource = RESOURCE_NAMES.VM_TEMPLATE
|
||||
const dialog = getResourceView(resource)?.dialogs?.instantiate_dialog
|
||||
const { features, dialogs } = getResourceView(resource)
|
||||
const dialog = dialogs?.instantiate_dialog
|
||||
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
|
||||
|
||||
return SECTIONS(vmTemplate).filter(({ id }) =>
|
||||
sectionsAvailable.includes(id)
|
||||
return SECTIONS(vmTemplate, features).filter(
|
||||
({ id, required }) => required || sectionsAvailable.includes(id)
|
||||
)
|
||||
}, [view])
|
||||
|
||||
@ -49,8 +50,8 @@ const Content = ({ vmTemplate }) => {
|
||||
{sections.map(({ id, legend, fields }) => (
|
||||
<FormWithSchema
|
||||
key={id}
|
||||
className={classes[id]}
|
||||
cy={id}
|
||||
rootProps={{ className: classes[id] }}
|
||||
fields={fields}
|
||||
legend={legend}
|
||||
id={STEP_ID}
|
||||
|
@ -30,13 +30,14 @@ import {
|
||||
Field,
|
||||
Section,
|
||||
} from 'client/utils'
|
||||
import { T, VmTemplate } from 'client/constants'
|
||||
import { T, VmTemplate, VmTemplateFeatures } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @param {VmTemplate} [vmTemplate] - VM Template
|
||||
* @param {VmTemplateFeatures} [features] - Features
|
||||
* @returns {Section[]} Sections
|
||||
*/
|
||||
const SECTIONS = (vmTemplate) => {
|
||||
const SECTIONS = (vmTemplate, features) => {
|
||||
const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
|
||||
|
||||
return [
|
||||
@ -48,7 +49,10 @@ const SECTIONS = (vmTemplate) => {
|
||||
{
|
||||
id: 'capacity',
|
||||
legend: T.Capacity,
|
||||
fields: filterFieldsByHypervisor(CAPACITY_FIELDS(vmTemplate), hypervisor),
|
||||
fields: filterFieldsByHypervisor(
|
||||
CAPACITY_FIELDS(vmTemplate, features),
|
||||
hypervisor
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'ownership',
|
||||
@ -70,17 +74,20 @@ const SECTIONS = (vmTemplate) => {
|
||||
|
||||
/**
|
||||
* @param {VmTemplate} [vmTemplate] - VM Template
|
||||
* @param {boolean} [hideCpu] - If `true`, the CPU fields is hidden
|
||||
* @returns {Field[]} Basic configuration fields
|
||||
*/
|
||||
const FIELDS = (vmTemplate) =>
|
||||
SECTIONS(vmTemplate)
|
||||
const FIELDS = (vmTemplate, hideCpu) =>
|
||||
SECTIONS(vmTemplate, hideCpu)
|
||||
.map(({ fields }) => fields)
|
||||
.flat()
|
||||
|
||||
/**
|
||||
* @param {VmTemplate} [vmTemplate] - VM Template
|
||||
* @param {boolean} [hideCpu] - If `true`, the CPU fields is hidden
|
||||
* @returns {BaseSchema} Step schema
|
||||
*/
|
||||
const SCHEMA = (vmTemplate) => getObjectSchemaFromFields(FIELDS(vmTemplate))
|
||||
const SCHEMA = (vmTemplate, hideCpu) =>
|
||||
getObjectSchemaFromFields(FIELDS(vmTemplate, hideCpu))
|
||||
|
||||
export { SECTIONS, FIELDS, SCHEMA }
|
||||
|
@ -18,7 +18,7 @@ import * as ACTIONS from 'client/constants/actions'
|
||||
import { Permissions, LockInfo } from 'client/constants/common'
|
||||
|
||||
/**
|
||||
* @typedef {object} VmTemplate
|
||||
* @typedef VmTemplate
|
||||
* @property {string|number} ID - Id
|
||||
* @property {string} NAME - Name
|
||||
* @property {string|number} UID - User id
|
||||
@ -35,6 +35,15 @@ import { Permissions, LockInfo } from 'client/constants/common'
|
||||
* @property {string} [TEMPLATE.VCENTER_TEMPLATE_REF] - vCenter information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef VmTemplateFeatures
|
||||
* @property {boolean} hide_cpu - If `true`, the CPU fields is hidden
|
||||
* @property {false|number} cpu_factor - Scales CPU by VCPU
|
||||
* - ``1``: Set it to 1 to tie CPU and vCPU
|
||||
* - ``{number}``: CPU = cpu_factor * VCPU
|
||||
* - ``{false}``: False to not scale the CPU
|
||||
*/
|
||||
|
||||
export const VM_TEMPLATE_ACTIONS = {
|
||||
REFRESH: ACTIONS.REFRESH,
|
||||
CREATE_DIALOG: 'create_dialog',
|
||||
|
@ -330,3 +330,18 @@ export const nicsIncludesTheConnectionType = (vm, type) => {
|
||||
|
||||
return getNics(vm).some((nic) => stringToBoolean(nic[ensuredConnection]))
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales the VCPU value by CPU factor to get the real CPU value.
|
||||
*
|
||||
* @param {number} [vcpu] - VCPU value
|
||||
* @param {number} cpuFactor - Factor CPU
|
||||
* @returns {number|undefined} Real CPU value
|
||||
*/
|
||||
export const scaleVcpuByCpuFactor = (vcpu, cpuFactor) => {
|
||||
if (!cpuFactor || isNaN(+vcpu) || +vcpu === 0) return
|
||||
if (+cpuFactor === 1) return vcpu
|
||||
|
||||
// round 2 decimals to avoid floating point errors
|
||||
return Math.round(+vcpu * +cpuFactor * 100) / 100
|
||||
}
|
||||
|
@ -420,47 +420,6 @@ export const cleanEmpty = (variable) =>
|
||||
? cleanEmptyArray(variable)
|
||||
: cleanEmptyObject(variable)
|
||||
|
||||
/**
|
||||
* Check if value is in base64.
|
||||
*
|
||||
* @param {string} stringToValidate - String to check
|
||||
* @param {object} options - Options
|
||||
* @param {boolean} options.exact - Only match and exact string
|
||||
* @returns {boolean} Returns `true` if string is a base64
|
||||
*/
|
||||
export const isBase64 = (stringToValidate, options = {}) => {
|
||||
if (stringToValidate === '') return false
|
||||
|
||||
const { exact = true } = options
|
||||
|
||||
const BASE64_REG =
|
||||
/(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)/g
|
||||
const EXACT_BASE64_REG =
|
||||
/(?:^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$)/
|
||||
|
||||
const regex = exact ? EXACT_BASE64_REG : BASE64_REG
|
||||
|
||||
return regex.test(stringToValidate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if value is divisible by another number.
|
||||
*
|
||||
* @param {string|number} number - Value to check
|
||||
* @param {string|number} divisor - Divisor number
|
||||
* @returns {boolean} Returns `true` if value is divisible by another
|
||||
*/
|
||||
export const isDivisibleBy = (number, divisor) => !(number % divisor)
|
||||
|
||||
/**
|
||||
* Returns factors of a number.
|
||||
*
|
||||
* @param {number} value - Number
|
||||
* @returns {number[]} Returns list of numbers
|
||||
*/
|
||||
export const getFactorsOfNumber = (value) =>
|
||||
[...Array(+value + 1).keys()].filter((idx) => value % idx === 0)
|
||||
|
||||
/**
|
||||
* Returns an array with the separator interspersed between elements of the given array.
|
||||
*
|
||||
|
@ -22,4 +22,5 @@ export * from 'client/utils/rest'
|
||||
export * from 'client/utils/schema'
|
||||
export * from 'client/utils/storage'
|
||||
export * from 'client/utils/string'
|
||||
export * from 'client/utils/number'
|
||||
export * from 'client/utils/translation'
|
||||
|
75
src/fireedge/src/client/utils/number.js
Normal file
75
src/fireedge/src/client/utils/number.js
Normal file
@ -0,0 +1,75 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Check if value is in base64.
|
||||
*
|
||||
* @param {string} stringToValidate - String to check
|
||||
* @param {object} options - Options
|
||||
* @param {boolean} options.exact - Only match and exact string
|
||||
* @returns {boolean} Returns `true` if string is a base64
|
||||
*/
|
||||
export const isBase64 = (stringToValidate, options = {}) => {
|
||||
if (stringToValidate === '') return false
|
||||
|
||||
const { exact = true } = options
|
||||
|
||||
const BASE64_REG =
|
||||
/(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)/g
|
||||
const EXACT_BASE64_REG =
|
||||
/(?:^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$)/
|
||||
|
||||
const regex = exact ? EXACT_BASE64_REG : BASE64_REG
|
||||
|
||||
return regex.test(stringToValidate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if value is divisible by another number.
|
||||
*
|
||||
* @param {string|number} number - Value to check
|
||||
* @param {string|number} divisor - Divisor number
|
||||
* @returns {boolean} Returns `true` if value is divisible by another
|
||||
*/
|
||||
export const isDivisibleBy = (number, divisor) => !(number % divisor)
|
||||
|
||||
/**
|
||||
* Returns factors of a number.
|
||||
*
|
||||
* @param {number} value - Number
|
||||
* @returns {number[]} Returns list of numbers
|
||||
*/
|
||||
export const getFactorsOfNumber = (value) =>
|
||||
[...Array(+value + 1).keys()].filter((idx) => value % idx === 0)
|
||||
|
||||
/**
|
||||
* Finds the closest number in list.
|
||||
*
|
||||
* @param {number} value - The value to compare
|
||||
* @param {number} list - List of numbers
|
||||
* @returns {number} Closest number
|
||||
*/
|
||||
export const findClosestValue = (value, list) => {
|
||||
const closestValue = list.reduce((closest, current) => {
|
||||
if (closest === null) return current
|
||||
|
||||
return Math.abs(current - value) < Math.abs(closest - value)
|
||||
? current
|
||||
: closest
|
||||
}, null)
|
||||
|
||||
return closestValue ?? value
|
||||
}
|
@ -224,7 +224,7 @@ const getValuesFromArray = (options, separator = SEMICOLON_CHAR) =>
|
||||
options?.split(separator)
|
||||
|
||||
const getOptionsFromList = (options = []) =>
|
||||
arrayToOptions([...new Set(options)])
|
||||
arrayToOptions([...new Set(options)], { addEmpty: false })
|
||||
|
||||
const parseUserInputValue = (value) => {
|
||||
if (value === true) {
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
} from 'yup'
|
||||
|
||||
import { T } from 'client/constants'
|
||||
import { isDivisibleBy, isBase64 } from 'client/utils/helpers'
|
||||
import { isDivisibleBy, isBase64 } from 'client/utils/number'
|
||||
|
||||
const buildMethods = () => {
|
||||
;[number, string, boolean, object, array, date].forEach((schemaType) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user