mirror of
https://github.com/OpenNebula/one.git
synced 2025-02-14 01:57:24 +03:00
parent
5d1129910f
commit
ec33af3a3c
@ -26,7 +26,7 @@ import Image from 'client/components/Image'
|
||||
|
||||
import { timeFromMilliseconds } from 'client/models/Helper'
|
||||
import { isExternalURL } from 'client/utils'
|
||||
import { VM, LOGO_IMAGES_URL } from 'client/constants'
|
||||
import { VM, STATIC_FILES_URL } from 'client/constants'
|
||||
|
||||
const VmTemplateCard = memo(
|
||||
/**
|
||||
@ -48,13 +48,10 @@ const VmTemplateCard = memo(
|
||||
LOGO = '',
|
||||
} = template
|
||||
|
||||
const [logoSource] = useMemo(() => {
|
||||
const external = isExternalURL(LOGO)
|
||||
const cleanLogoAttribute = String(LOGO).split('/').at(-1)
|
||||
const src = external ? LOGO : `${LOGO_IMAGES_URL}/${cleanLogoAttribute}`
|
||||
|
||||
return [src, external]
|
||||
}, [LOGO])
|
||||
const logoSource = useMemo(
|
||||
() => (isExternalURL(LOGO) ? LOGO : `${STATIC_FILES_URL}/${LOGO}`),
|
||||
[LOGO]
|
||||
)
|
||||
|
||||
const time = timeFromMilliseconds(+REGTIME)
|
||||
|
||||
|
@ -33,6 +33,7 @@ const AutocompleteController = memo(
|
||||
multiple = false,
|
||||
values = [],
|
||||
fieldProps: { separators, ...fieldProps } = {},
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const {
|
||||
field: { value: renderValue, onBlur, onChange },
|
||||
@ -78,6 +79,7 @@ const AutocompleteController = memo(
|
||||
<TextField
|
||||
label={labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
inputProps={{ ...inputProps, 'data-cy': cy }}
|
||||
InputProps={{ readOnly }}
|
||||
error={Boolean(error)}
|
||||
helperText={
|
||||
Boolean(error) && <ErrorHelper label={error?.message} />
|
||||
@ -115,6 +117,7 @@ AutocompleteController.propTypes = {
|
||||
multiple: PropTypes.bool,
|
||||
values: PropTypes.arrayOf(PropTypes.object),
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
}
|
||||
|
||||
AutocompleteController.displayName = 'AutocompleteController'
|
||||
|
@ -43,6 +43,7 @@ const CheckboxController = memo(
|
||||
label = '',
|
||||
tooltip,
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const {
|
||||
field: { value = false, onChange },
|
||||
@ -56,6 +57,7 @@ const CheckboxController = memo(
|
||||
<Checkbox
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
name={name}
|
||||
readOnly={readOnly}
|
||||
checked={Boolean(value)}
|
||||
color="secondary"
|
||||
inputProps={{ 'data-cy': cy }}
|
||||
@ -87,6 +89,7 @@ CheckboxController.propTypes = {
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
}
|
||||
|
||||
CheckboxController.displayName = 'CheckboxController'
|
||||
|
@ -54,6 +54,7 @@ ErrorHelper.propTypes = {
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
PropTypes.shape({
|
||||
word: PropTypes.string,
|
||||
values: PropTypes.oneOfType([
|
||||
|
@ -49,6 +49,7 @@ const FileController = memo(
|
||||
validationBeforeTransform,
|
||||
transform,
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
formContext = {},
|
||||
}) => {
|
||||
const { setValue, setError, clearErrors, watch } = formContext
|
||||
@ -115,12 +116,14 @@ const FileController = memo(
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl fullWidth margin="dense">
|
||||
<FormControl margin="dense">
|
||||
<HiddenInput
|
||||
{...inputProps}
|
||||
ref={ref}
|
||||
id={cy}
|
||||
type="file"
|
||||
readOnly={readOnly}
|
||||
disabled={readOnly}
|
||||
onChange={handleChange}
|
||||
{...fieldProps}
|
||||
/>
|
||||
@ -128,6 +131,7 @@ const FileController = memo(
|
||||
<SubmitButton
|
||||
color={success ? 'success' : 'secondary'}
|
||||
component="span"
|
||||
disabled={readOnly}
|
||||
data-cy={`${cy}-button`}
|
||||
isSubmitting={isLoading}
|
||||
label={success ? <CheckIcon /> : <FileIcon />}
|
||||
@ -161,6 +165,7 @@ FileController.propTypes = {
|
||||
),
|
||||
transform: PropTypes.func,
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
formContext: PropTypes.shape({
|
||||
setValue: PropTypes.func,
|
||||
setError: PropTypes.func,
|
||||
|
@ -34,6 +34,7 @@ const SelectController = memo(
|
||||
renderValue,
|
||||
tooltip,
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const firstValue = values?.[0]?.value ?? ''
|
||||
const defaultValue = multiple ? [firstValue] : firstValue
|
||||
@ -50,8 +51,15 @@ const SelectController = memo(
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (multiple && !Array.isArray(optionSelected)) {
|
||||
onChange([optionSelected])
|
||||
if (!optionSelected && !optionSelected.length) return
|
||||
|
||||
if (multiple) {
|
||||
const exists = values.some((v) => optionSelected.includes(v.value))
|
||||
!exists && onChange([firstValue])
|
||||
} else {
|
||||
const exists = values.some((v) => `${v.value}` === `${optionSelected}`)
|
||||
|
||||
!exists && onChange(firstValue)
|
||||
}
|
||||
}, [multiple])
|
||||
|
||||
@ -82,6 +90,7 @@ const SelectController = memo(
|
||||
label={labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
InputLabelProps={{ shrink: needShrink }}
|
||||
InputProps={{
|
||||
readOnly,
|
||||
startAdornment:
|
||||
(optionSelected && renderValue?.(optionSelected)) ||
|
||||
(tooltip && <Tooltip title={tooltip} position="start" />),
|
||||
@ -118,6 +127,7 @@ SelectController.propTypes = {
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
renderValue: PropTypes.func,
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
}
|
||||
|
||||
SelectController.displayName = 'SelectController'
|
||||
|
@ -31,6 +31,7 @@ const SliderController = memo(
|
||||
label = '',
|
||||
tooltip,
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const {
|
||||
field: { value, onChange, ...inputProps },
|
||||
@ -48,6 +49,7 @@ const SliderController = memo(
|
||||
value={typeof value === 'number' ? value : 0}
|
||||
aria-labelledby={sliderId}
|
||||
valueLabelDisplay="auto"
|
||||
disabled={readOnly}
|
||||
data-cy={sliderId}
|
||||
onChange={(_, val) => onChange(val)}
|
||||
{...fieldProps}
|
||||
@ -60,6 +62,7 @@ const SliderController = memo(
|
||||
error={Boolean(error)}
|
||||
label={labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
InputProps={{
|
||||
readOnly,
|
||||
endAdornment: tooltip && <Tooltip title={tooltip} />,
|
||||
}}
|
||||
inputProps={{
|
||||
@ -98,6 +101,7 @@ SliderController.propTypes = {
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
}
|
||||
|
||||
SliderController.displayName = 'SliderController'
|
||||
|
@ -43,6 +43,7 @@ const SwitchController = memo(
|
||||
label = '',
|
||||
tooltip,
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const {
|
||||
field: { value = false, onChange },
|
||||
@ -50,10 +51,11 @@ const SwitchController = memo(
|
||||
} = useController({ name, control })
|
||||
|
||||
return (
|
||||
<FormControl fullWidth error={Boolean(error)} margin="dense">
|
||||
<FormControl error={Boolean(error)} margin="dense">
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
readOnly={readOnly}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
name={name}
|
||||
checked={Boolean(value)}
|
||||
@ -87,6 +89,7 @@ SwitchController.propTypes = {
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
}
|
||||
|
||||
SwitchController.displayName = 'SwitchController'
|
||||
|
@ -43,6 +43,7 @@ const TableController = memo(
|
||||
singleSelect = true,
|
||||
getRowId = defaultGetRowId,
|
||||
formContext = {},
|
||||
readOnly = false,
|
||||
fieldProps: { initialState, ...fieldProps } = {},
|
||||
}) => {
|
||||
const { clearErrors } = formContext
|
||||
@ -74,7 +75,10 @@ const TableController = memo(
|
||||
onlyGlobalSelectedRows
|
||||
getRowId={getRowId}
|
||||
initialState={{ ...initialState, selectedRowIds: initialRows }}
|
||||
disableRowSelect={readOnly}
|
||||
onSelectedRowsChange={(rows) => {
|
||||
if (readOnly) return
|
||||
|
||||
const rowValues = rows?.map(({ original }) => getRowId(original))
|
||||
|
||||
onChange(singleSelect ? rowValues?.[0] : rowValues)
|
||||
@ -102,6 +106,7 @@ TableController.propTypes = {
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
formContext: PropTypes.shape({
|
||||
setValue: PropTypes.func,
|
||||
setError: PropTypes.func,
|
||||
|
@ -35,6 +35,7 @@ const TextController = memo(
|
||||
watcher,
|
||||
dependencies,
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const watch =
|
||||
dependencies &&
|
||||
@ -68,9 +69,17 @@ const TextController = memo(
|
||||
type={type}
|
||||
label={labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
InputProps={{
|
||||
readOnly,
|
||||
endAdornment: tooltip && <Tooltip title={tooltip} />,
|
||||
}}
|
||||
inputProps={{ 'data-cy': cy }}
|
||||
inputProps={{
|
||||
'data-cy': cy,
|
||||
...(type === 'number' && {
|
||||
min: fieldProps.min,
|
||||
max: fieldProps.max,
|
||||
step: fieldProps.step,
|
||||
}),
|
||||
}}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
@ -99,6 +108,7 @@ TextController.propTypes = {
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
fieldProps: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
|
||||
readOnly: PropTypes.bool,
|
||||
}
|
||||
|
||||
TextController.displayName = 'TextController'
|
||||
|
@ -33,7 +33,7 @@ const TimeController = memo(
|
||||
label = '',
|
||||
tooltip,
|
||||
fieldProps: { defaultValue, ...fieldProps } = {},
|
||||
...props
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const {
|
||||
field: { value, ...controllerProps },
|
||||
@ -50,6 +50,7 @@ const TimeController = memo(
|
||||
clearText={<Translate word={T.Clear} />}
|
||||
todayText={<Translate word={T.Today} />}
|
||||
InputProps={{
|
||||
readOnly,
|
||||
autoComplete: 'off',
|
||||
startAdornment: tooltip && <Tooltip title={tooltip} />,
|
||||
}}
|
||||
@ -80,6 +81,7 @@ TimeController.propTypes = {
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
}
|
||||
|
||||
TimeController.displayName = 'TimeController'
|
||||
|
@ -48,6 +48,7 @@ const ToggleController = memo(
|
||||
values = [],
|
||||
tooltip,
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const defaultValue = multiple ? [values?.[0]?.value] : values?.[0]?.value
|
||||
|
||||
@ -72,17 +73,17 @@ const ToggleController = memo(
|
||||
</Label>
|
||||
)}
|
||||
<ToggleButtonGroup
|
||||
onChange={(_, newValues) => onChange(newValues)}
|
||||
fullWidth
|
||||
ref={ref}
|
||||
id={cy}
|
||||
onChange={(_, newValues) => !readOnly && onChange(newValues)}
|
||||
value={optionSelected}
|
||||
fullWidth
|
||||
exclusive={!multiple}
|
||||
data-cy={cy}
|
||||
{...fieldProps}
|
||||
>
|
||||
{values?.map(({ text, value = '' }) => (
|
||||
<ToggleButton key={`${name}-${value}`} value={value}>
|
||||
<ToggleButton key={`${name}-${value}`} value={value} sx={{ p: 1 }}>
|
||||
{text}
|
||||
</ToggleButton>
|
||||
))}
|
||||
@ -111,6 +112,7 @@ ToggleController.propTypes = {
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
renderValue: PropTypes.func,
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
}
|
||||
|
||||
ToggleController.displayName = 'ToggleController'
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { object, array, string, boolean, number, ref, ObjectSchema } from 'yup'
|
||||
|
||||
import { userInputsToObject } from 'client/models/Helper'
|
||||
import { userInputsToObject, userInputsToArray } from 'client/models/Helper'
|
||||
import {
|
||||
UserInputType,
|
||||
T,
|
||||
@ -40,7 +40,7 @@ const {
|
||||
boolean: uiBoolean,
|
||||
} = USER_INPUT_TYPES
|
||||
|
||||
const { array: __, ...userInputTypes } = USER_INPUT_TYPES
|
||||
const { array: _array, fixed: _fixed, ...userInputTypes } = USER_INPUT_TYPES
|
||||
|
||||
/** @type {UserInputType[]} User inputs types */
|
||||
const valuesOfUITypes = Object.values(userInputTypes)
|
||||
@ -90,7 +90,7 @@ const DESCRIPTION = {
|
||||
const OPTIONS = {
|
||||
name: 'options',
|
||||
label: T.Options,
|
||||
tooltip: 'Press ENTER key to add a value',
|
||||
tooltip: [T.PressKeysToAddAValue, ['ENTER']],
|
||||
dependOf: TYPE.name,
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
multiple: true,
|
||||
@ -100,7 +100,7 @@ const OPTIONS = {
|
||||
.default(() => [])
|
||||
.when(TYPE.name, (type, schema) =>
|
||||
[uiList, uiListMultiple].includes(type)
|
||||
? schema.required()
|
||||
? schema.required().min(1)
|
||||
: schema.strip().notRequired()
|
||||
),
|
||||
fieldProps: {
|
||||
@ -211,10 +211,22 @@ export const USER_INPUT_SCHEMA = getObjectSchemaFromFields(USER_INPUT_FIELDS)
|
||||
export const USER_INPUTS_SCHEMA = object({
|
||||
USER_INPUTS: array(USER_INPUT_SCHEMA)
|
||||
.ensure()
|
||||
.afterSubmit((userInputs) => userInputsToObject(userInputs)),
|
||||
.afterSubmit((userInputs, { context }) => {
|
||||
const capacityInputs = userInputsToArray(context?.general?.MODIFICATION, {
|
||||
filterCapacityInputs: false,
|
||||
}).map(({ name, ...userInput }) => ({
|
||||
name,
|
||||
...userInput,
|
||||
// set default value from MEMORY, CPU and VCPU fields
|
||||
default: context.general?.[name],
|
||||
...(['MEMORY', 'CPU'].includes(name) && { mandatory: true }),
|
||||
}))
|
||||
|
||||
return userInputsToObject([...capacityInputs, ...userInputs])
|
||||
}),
|
||||
INPUTS_ORDER: string()
|
||||
.trim()
|
||||
.afterSubmit((_, { context }) => {
|
||||
.afterSubmit((_inputsOrder_, { context }) => {
|
||||
const userInputs = context?.extra?.USER_INPUTS
|
||||
|
||||
return userInputs?.map(({ name }) => String(name).toUpperCase()).join(',')
|
||||
|
@ -13,105 +13,95 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { number, boolean } from 'yup'
|
||||
import { number } from 'yup'
|
||||
|
||||
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
import {
|
||||
generateModificationInputs,
|
||||
generateHotResizeInputs,
|
||||
generateCapacityInput,
|
||||
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/capacityUtils'
|
||||
import { Field } from 'client/utils'
|
||||
import { T, HYPERVISORS } from 'client/constants'
|
||||
|
||||
const commonValidation = number()
|
||||
.positive()
|
||||
.default(() => undefined)
|
||||
|
||||
// --------------------------------------------------------
|
||||
// MEMORY fields
|
||||
// --------------------------------------------------------
|
||||
|
||||
/** @type {Field} Memory field */
|
||||
export const MEMORY = {
|
||||
export const MEMORY = generateCapacityInput({
|
||||
name: 'MEMORY',
|
||||
label: T.Memory,
|
||||
tooltip: T.MemoryConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: commonValidation
|
||||
.required()
|
||||
.when('HYPERVISOR', (hypervisor, schema) =>
|
||||
hypervisor === HYPERVISORS.vcenter ? schema.isDivisibleBy(4) : schema
|
||||
),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
})
|
||||
|
||||
/** @type {Field} Hot reloading on memory field */
|
||||
export const ENABLE_HR_MEMORY = {
|
||||
name: 'HOT_RESIZE.MEMORY_HOT_ADD_ENABLED',
|
||||
label: T.EnableHotResize,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().yesOrNo(),
|
||||
grid: { xs: 4, md: 6 },
|
||||
}
|
||||
/** @type {Field[]} Hot resize on memory field */
|
||||
export const HR_MEMORY_FIELDS = generateHotResizeInputs(
|
||||
{ fieldName: MEMORY.name },
|
||||
{
|
||||
name: `${MEMORY.name}_MAX`,
|
||||
label: T.MaxMemory,
|
||||
tooltip: T.MaxMemoryConcept,
|
||||
}
|
||||
)
|
||||
|
||||
/** @type {Field} Maximum memory field */
|
||||
export const MEMORY_MAX = {
|
||||
name: 'MEMORY_MAX',
|
||||
label: T.MaxMemory,
|
||||
dependOf: ENABLE_HR_MEMORY.name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: (enabledHr) => (enabledHr ? 'number' : INPUT_TYPES.HIDDEN),
|
||||
validation: commonValidation.when(
|
||||
ENABLE_HR_MEMORY.name,
|
||||
(enabledHr, schema) =>
|
||||
enabledHr ? schema.required() : schema.notRequired()
|
||||
),
|
||||
grid: { xs: 8, md: 6 },
|
||||
}
|
||||
/** @type {Field[]} Modification inputs on memory field */
|
||||
export const MOD_MEMORY_FIELDS = generateModificationInputs(MEMORY.name)
|
||||
|
||||
/** @type {Field[]} List of memory fields */
|
||||
export const MEMORY_FIELDS = [MEMORY, ...HR_MEMORY_FIELDS, ...MOD_MEMORY_FIELDS]
|
||||
|
||||
// --------------------------------------------------------
|
||||
// CPU fields
|
||||
// --------------------------------------------------------
|
||||
|
||||
/** @type {Field} Physical CPU field */
|
||||
export const PHYSICAL_CPU = {
|
||||
export const PHYSICAL_CPU = generateCapacityInput({
|
||||
name: 'CPU',
|
||||
label: T.PhysicalCpu,
|
||||
tooltip: T.CpuConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: commonValidation.required(),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
})
|
||||
|
||||
/** @type {Field[]} Modification inputs on CPU field */
|
||||
export const MOD_CPU_FIELDS = generateModificationInputs(PHYSICAL_CPU.name)
|
||||
|
||||
/** @type {Field[]} List of CPU fields */
|
||||
export const CPU_FIELDS = [PHYSICAL_CPU, ...MOD_CPU_FIELDS]
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Virtual CPU fields
|
||||
// --------------------------------------------------------
|
||||
|
||||
/** @type {Field} Virtual CPU field */
|
||||
export const VIRTUAL_CPU = {
|
||||
export const VIRTUAL_CPU = generateCapacityInput({
|
||||
name: 'VCPU',
|
||||
label: T.VirtualCpu,
|
||||
tooltip: T.VirtualCpuConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: commonValidation,
|
||||
grid: { md: 12 },
|
||||
}
|
||||
grid: { md: 3 },
|
||||
})
|
||||
|
||||
/** @type {Field} Hot reloading on virtual CPU field */
|
||||
export const ENABLE_HR_VCPU = {
|
||||
name: 'HOT_RESIZE.CPU_HOT_ADD_ENABLED',
|
||||
label: T.EnableHotResize,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().yesOrNo(),
|
||||
grid: { xs: 4, md: 6 },
|
||||
}
|
||||
/** @type {Field[]} Hot resize on memory field */
|
||||
export const HR_CPU_FIELDS = generateHotResizeInputs(
|
||||
{ name: PHYSICAL_CPU.name },
|
||||
{
|
||||
name: `${VIRTUAL_CPU.name}_MAX`,
|
||||
label: T.MaxVirtualCpu,
|
||||
tooltip: T.MaxVirtualCpuConcept,
|
||||
}
|
||||
)
|
||||
|
||||
/** @type {Field} Maximum virtual CPU field */
|
||||
export const VCPU_MAX = {
|
||||
name: 'VCPU_MAX',
|
||||
label: T.MaxVirtualCpu,
|
||||
dependOf: ENABLE_HR_VCPU.name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: (enabledHr) => (enabledHr ? 'number' : INPUT_TYPES.HIDDEN),
|
||||
validation: commonValidation.when(ENABLE_HR_VCPU.name, (enabledHr, schema) =>
|
||||
enabledHr ? schema.required() : schema.notRequired()
|
||||
),
|
||||
grid: { xs: 8, md: 6 },
|
||||
}
|
||||
/** @type {Field[]} Modification inputs on Virtual CPU field */
|
||||
export const MOD_VCPU_FIELDS = generateModificationInputs(VIRTUAL_CPU.name)
|
||||
|
||||
/** @type {Field[]} List of capacity fields */
|
||||
export const FIELDS = [
|
||||
MEMORY,
|
||||
ENABLE_HR_MEMORY,
|
||||
MEMORY_MAX,
|
||||
PHYSICAL_CPU,
|
||||
VIRTUAL_CPU,
|
||||
ENABLE_HR_VCPU,
|
||||
VCPU_MAX,
|
||||
]
|
||||
/** @type {Field[]} List of Virtual CPU fields */
|
||||
export const VCPU_FIELDS = [VIRTUAL_CPU, ...HR_CPU_FIELDS, ...MOD_VCPU_FIELDS]
|
||||
|
@ -0,0 +1,266 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import {
|
||||
boolean,
|
||||
number,
|
||||
lazy,
|
||||
string,
|
||||
array,
|
||||
ref,
|
||||
BaseSchema,
|
||||
NumberSchema,
|
||||
} from 'yup'
|
||||
|
||||
import { getUserInputParams } from 'client/models/Helper'
|
||||
import { Field, arrayToOptions, sentenceCase } from 'client/utils'
|
||||
import { T, INPUT_TYPES, HYPERVISORS, USER_INPUT_TYPES } from 'client/constants'
|
||||
|
||||
const { fixed, range, rangeFloat, list } = USER_INPUT_TYPES
|
||||
|
||||
const MODIFICATION_TRANSLATES = {
|
||||
MEMORY: {
|
||||
label: T.MemoryModification,
|
||||
tooltip: T.AllowUsersToModifyMemory,
|
||||
},
|
||||
CPU: {
|
||||
label: T.CpuModification,
|
||||
tooltip: T.AllowUsersToModifyCpu,
|
||||
},
|
||||
VCPU: {
|
||||
label: T.VirtualCpuModification,
|
||||
tooltip: T.AllowUsersToModifyVirtualCpu,
|
||||
},
|
||||
}
|
||||
|
||||
const isRangeType = (type) => [range, rangeFloat].includes(type)
|
||||
|
||||
/** @typedef {'MEMORY'|'CPU'|'VCPU'} CapacityFieldName */
|
||||
/** @typedef {{ type: string, min: string, max: string, options: string }} ModificationIds */
|
||||
|
||||
/**
|
||||
* @param {CapacityFieldName} fieldName - Capacity field name
|
||||
* @returns {ModificationIds} - Modification ids
|
||||
*/
|
||||
const getModificationIdsByFieldName = (fieldName) => ({
|
||||
type: `MODIFICATION.${fieldName}.type`,
|
||||
min: `MODIFICATION.${fieldName}.min`,
|
||||
max: `MODIFICATION.${fieldName}.max`,
|
||||
options: `MODIFICATION.${fieldName}.options`,
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {CapacityFieldName} fieldName - Capacity field name
|
||||
* @param {ModificationIds} [modificationIds] - Modification ids
|
||||
* @returns {Field} - Type field for capacity modification
|
||||
*/
|
||||
const modificationTypeInput = (fieldName, { type: typeId }) => ({
|
||||
name: typeId,
|
||||
...MODIFICATION_TRANSLATES[fieldName],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([fixed, range, list], {
|
||||
addEmpty: 'Any value',
|
||||
getValue: (type) =>
|
||||
// allow float numbers on CPU and VCPU
|
||||
['CPU', 'VCPU'].includes(fieldName) && type === range ? rangeFloat : type,
|
||||
getText: (type) => sentenceCase(type),
|
||||
}),
|
||||
validation: lazy((_, { context }) =>
|
||||
string().default(() => {
|
||||
const capacityUserInput = context.extra?.USER_INPUTS?.[fieldName]
|
||||
const { type } = getUserInputParams(capacityUserInput) ?? {}
|
||||
|
||||
return type
|
||||
})
|
||||
),
|
||||
grid: { md: 3 },
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {CapacityFieldName} fieldName - Capacity field name
|
||||
* @param {ModificationIds} [modificationIds] - Modification ids
|
||||
* @returns {Field[]} - Range fields for capacity modification
|
||||
*/
|
||||
const modificationRangeInputs = (fieldName, { type, min, max }) => {
|
||||
// Common attributes for range inputs (min, max)
|
||||
const commonRangeAttributes = {
|
||||
type: INPUT_TYPES.TEXT,
|
||||
dependOf: type,
|
||||
htmlType: (modificationType) =>
|
||||
isRangeType(modificationType) ? 'number' : INPUT_TYPES.HIDDEN,
|
||||
grid: { md: 3 },
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates validation for range inputs.
|
||||
*
|
||||
* @param {object} config - Validation config
|
||||
* @param {NumberSchema} config.thenFn - Schema validation when the modification type is range
|
||||
* @param {'min'|'max'} config.rangeParam - Range parameter: min or max
|
||||
* @returns {BaseSchema} Schema validation for range inputs
|
||||
*/
|
||||
const commonRangeValidation = ({ thenFn, rangeParam }) =>
|
||||
lazy((_, { context }) =>
|
||||
number()
|
||||
.positive()
|
||||
.transform((value) => (!isNaN(value) ? value : null))
|
||||
.when('HYPERVISOR', (hypervisor, schema) =>
|
||||
hypervisor === HYPERVISORS.vcenter ? schema.isDivisibleBy(4) : schema
|
||||
)
|
||||
.when(`$general.${type}`, {
|
||||
is: isRangeType,
|
||||
then: thenFn,
|
||||
otherwise: (schema) => schema.optional().nullable(),
|
||||
})
|
||||
.default(() => {
|
||||
const capacityUserInput = context.extra?.USER_INPUTS?.[fieldName]
|
||||
const params = getUserInputParams(capacityUserInput)
|
||||
|
||||
return params[rangeParam]
|
||||
})
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
name: min,
|
||||
label: T.Min,
|
||||
...commonRangeAttributes,
|
||||
validation: commonRangeValidation({
|
||||
thenFn: (schema) => schema.required().lessThan(ref(`$general.${max}`)),
|
||||
rangeParam: 'min',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: max,
|
||||
label: T.Max,
|
||||
...commonRangeAttributes,
|
||||
validation: commonRangeValidation({
|
||||
thenFn: (schema) => schema.required().moreThan(ref(`$general.${min}`)),
|
||||
rangeParam: 'max',
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CapacityFieldName} fieldName - Capacity field name
|
||||
* @param {ModificationIds} [modificationIds] - Modification ids
|
||||
* @returns {Field} - Options field for capacity modification
|
||||
*/
|
||||
const modificationOptionsInput = (fieldName, { type, options: optionsId }) => ({
|
||||
name: optionsId,
|
||||
label: T.Options,
|
||||
tooltip: [T.PressKeysToAddAValue, ['ENTER']],
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
multiple: true,
|
||||
dependOf: type,
|
||||
htmlType: (modificationType) =>
|
||||
modificationType !== list && INPUT_TYPES.HIDDEN,
|
||||
validation: lazy((_, { context }) =>
|
||||
array(number().positive())
|
||||
.when(`$general.${type}`, {
|
||||
is: (modificationType) => modificationType === list,
|
||||
then: (schema) => schema.required().min(1),
|
||||
otherwise: (schema) => schema.nullable(),
|
||||
})
|
||||
.default(() => {
|
||||
const capacityUserInput = context.extra?.USER_INPUTS?.[fieldName]
|
||||
const { options = [] } = getUserInputParams(capacityUserInput) ?? {}
|
||||
|
||||
return options
|
||||
})
|
||||
),
|
||||
fieldProps: { freeSolo: true },
|
||||
grid: fieldName === 'CPU' ? { md: 6 } : { md: 9 },
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {CapacityFieldName} fieldName - Capacity field name
|
||||
* @returns {Field[]} - Fields for capacity modification
|
||||
*/
|
||||
export const generateModificationInputs = (fieldName) => {
|
||||
const modificationIds = getModificationIdsByFieldName(fieldName)
|
||||
|
||||
return [
|
||||
modificationTypeInput(fieldName, modificationIds),
|
||||
modificationOptionsInput(fieldName, modificationIds),
|
||||
...modificationRangeInputs(fieldName, modificationIds),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Field} hrField - Hot resize field
|
||||
* @param {CapacityFieldName} hrField.fieldName - Capacity field name
|
||||
* @param {Field} maxField - Max field
|
||||
* @returns {Field[]} Hot resize fields
|
||||
*/
|
||||
export const generateHotResizeInputs = (
|
||||
{ name: hrFieldName, ...hrField },
|
||||
maxField
|
||||
) => [
|
||||
{
|
||||
...hrField,
|
||||
name: `HOT_RESIZE.${hrFieldName}_HOT_ADD_ENABLED`,
|
||||
dependOf: `HOT_RESIZE.${hrFieldName}_HOT_ADD_ENABLED`,
|
||||
label: T.EnableHotResize,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().yesOrNo(),
|
||||
grid: (enabledHr) =>
|
||||
enabledHr ? { xs: 7, sm: 5, md: 3 } : { xs: 12, sm: 9, md: 9 },
|
||||
},
|
||||
{
|
||||
...maxField,
|
||||
dependOf: `HOT_RESIZE.${hrFieldName}_HOT_ADD_ENABLED`,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: (enabledHr) => (enabledHr ? 'number' : INPUT_TYPES.HIDDEN),
|
||||
validation: number()
|
||||
.positive()
|
||||
.default(() => undefined)
|
||||
.when(`HOT_RESIZE.${hrFieldName}_HOT_ADD_ENABLED`, (enabledHr, schema) =>
|
||||
enabledHr ? schema.required() : schema.notRequired()
|
||||
),
|
||||
grid: { xs: 5, sm: 7, md: 6 },
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* @param {Field} config - Configuration
|
||||
* @param {CapacityFieldName} config.name - Capacity field name
|
||||
* @param {BaseSchema} config.validation - Validation schema
|
||||
* @returns {Field} - Field with validation modification conditions
|
||||
*/
|
||||
export const generateCapacityInput = ({ validation, ...field }) => ({
|
||||
...field,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: validation
|
||||
.when(`$general.MODIFICATION.${field.name}.type`, {
|
||||
is: (modificationType) => modificationType === range,
|
||||
then: (schema) =>
|
||||
schema
|
||||
.max(ref(`$general.MODIFICATION.${field.name}.max`))
|
||||
.min(ref(`$general.MODIFICATION.${field.name}.min`)),
|
||||
otherwise: (schema) => schema,
|
||||
})
|
||||
.when(
|
||||
[
|
||||
`$general.MODIFICATION.${field.name}.type`,
|
||||
`$general.MODIFICATION.${field.name}.options`,
|
||||
],
|
||||
(modificationType, options = [], schema) =>
|
||||
modificationType === list ? schema.oneOf(options) : schema
|
||||
),
|
||||
grid: { md: 3 },
|
||||
})
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
@ -22,13 +21,13 @@ import { useViews } from 'client/features/Auth'
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import useStyles from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/styles'
|
||||
|
||||
import { HYPERVISOR_FIELD } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/informationSchema'
|
||||
import {
|
||||
SCHEMA,
|
||||
SECTIONS,
|
||||
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/schema'
|
||||
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
|
||||
import { T, RESOURCE_NAMES } from 'client/constants'
|
||||
import { generateKey } from 'client/utils'
|
||||
import { T, RESOURCE_NAMES, VmTemplate } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'general'
|
||||
|
||||
@ -42,22 +41,21 @@ const Content = ({ isUpdate }) => {
|
||||
const dialog = getResourceView(resource)?.dialogs?.create_dialog
|
||||
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
|
||||
|
||||
return SECTIONS(hypervisor, isUpdate).filter(
|
||||
({ id, required }) => required || sectionsAvailable.includes(id)
|
||||
return (
|
||||
SECTIONS(hypervisor, isUpdate)
|
||||
.filter(
|
||||
({ id, required }) => required || sectionsAvailable.includes(id)
|
||||
)
|
||||
// unique keys to avoid duplicates
|
||||
.map((section) => ({ key: generateKey(), ...section }))
|
||||
)
|
||||
}, [view, hypervisor])
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormWithSchema
|
||||
cy={`${STEP_ID}-hypervisor`}
|
||||
fields={[HYPERVISOR_FIELD]}
|
||||
legend={T.Hypervisor}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
{sections.map(({ id, ...section }) => (
|
||||
{sections.map(({ key, id, ...section }) => (
|
||||
<FormWithSchema
|
||||
key={id}
|
||||
key={key}
|
||||
id={STEP_ID}
|
||||
className={classes[id]}
|
||||
cy={`${STEP_ID}-${id}`}
|
||||
@ -68,9 +66,15 @@ const Content = ({ isUpdate }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const General = (initialValues) => {
|
||||
const isUpdate = initialValues?.NAME
|
||||
const initialHypervisor = initialValues?.TEMPLATE?.HYPERVISOR
|
||||
/**
|
||||
* General configuration about VM Template.
|
||||
*
|
||||
* @param {VmTemplate} vmTemplate - VM Template
|
||||
* @returns {object} General configuration step
|
||||
*/
|
||||
const General = (vmTemplate) => {
|
||||
const isUpdate = vmTemplate?.NAME
|
||||
const initialHypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
|
@ -16,7 +16,7 @@
|
||||
import { string } from 'yup'
|
||||
|
||||
import Image from 'client/components/Image'
|
||||
import { T, LOGO_IMAGES_URL, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
import { T, STATIC_FILES_URL, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
import { Field, arrayToOptions } from 'client/utils'
|
||||
|
||||
/**
|
||||
@ -31,7 +31,7 @@ export const NAME = (isUpdate) => ({
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 6 },
|
||||
grid: { md: 12 },
|
||||
...(isUpdate && { fieldProps: { disabled: true } }),
|
||||
})
|
||||
|
||||
@ -40,10 +40,12 @@ export const DESCRIPTION = {
|
||||
name: 'DESCRIPTION',
|
||||
label: T.Description,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
multiline: true,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
/** @type {Field} Hypervisor field */
|
||||
@ -68,40 +70,38 @@ export const LOGO = {
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: [
|
||||
{ text: '-', value: '' },
|
||||
// client/assets/images/logos
|
||||
{ text: 'Alpine Linux', value: 'alpine.png' },
|
||||
{ text: 'ALT', value: 'alt.png' },
|
||||
{ text: 'Arch', value: 'arch.png' },
|
||||
{ text: 'CentOS', value: 'centos.png' },
|
||||
{ text: 'Debian', value: 'debian.png' },
|
||||
{ text: 'Devuan', value: 'devuan.png' },
|
||||
{ text: 'Fedora', value: 'fedora.png' },
|
||||
{ text: 'FreeBSD', value: 'freebsd.png' },
|
||||
{ text: 'HardenedBSD', value: 'hardenedbsd.png' },
|
||||
{ text: 'Knoppix', value: 'knoppix.png' },
|
||||
{ text: 'Linux', value: 'linux.png' },
|
||||
{ text: 'Oracle', value: 'oracle.png' },
|
||||
{ text: 'RedHat', value: 'redhat.png' },
|
||||
{ text: 'Suse', value: 'suse.png' },
|
||||
{ text: 'Ubuntu', value: 'ubuntu.png' },
|
||||
{ text: 'Windows xp', value: 'windowsxp.png' },
|
||||
{ text: 'Windows 10', value: 'windows8.png' },
|
||||
{ text: 'Alpine Linux', value: 'images/logos/alpine.png' },
|
||||
{ text: 'ALT', value: 'images/logos/alt.png' },
|
||||
{ text: 'Arch', value: 'images/logos/arch.png' },
|
||||
{ text: 'CentOS', value: 'images/logos/centos.png' },
|
||||
{ text: 'Debian', value: 'images/logos/debian.png' },
|
||||
{ text: 'Devuan', value: 'images/logos/devuan.png' },
|
||||
{ text: 'Fedora', value: 'images/logos/fedora.png' },
|
||||
{ text: 'FreeBSD', value: 'images/logos/freebsd.png' },
|
||||
{ text: 'HardenedBSD', value: 'images/logos/hardenedbsd.png' },
|
||||
{ text: 'Knoppix', value: 'images/logos/knoppix.png' },
|
||||
{ text: 'Linux', value: 'images/logos/linux.png' },
|
||||
{ text: 'Oracle', value: 'images/logos/oracle.png' },
|
||||
{ text: 'RedHat', value: 'images/logos/redhat.png' },
|
||||
{ text: 'Suse', value: 'images/logos/suse.png' },
|
||||
{ text: 'Ubuntu', value: 'images/logos/ubuntu.png' },
|
||||
{ text: 'Windows xp', value: 'images/logos/windowsxp.png' },
|
||||
{ text: 'Windows 10', value: 'images/logos/windows8.png' },
|
||||
],
|
||||
renderValue: (value) => (
|
||||
<Image
|
||||
alt="logo"
|
||||
imgProps={{
|
||||
height: 25,
|
||||
width: 25,
|
||||
style: { marginRight: 10 },
|
||||
}}
|
||||
src={`${LOGO_IMAGES_URL}/${value}`}
|
||||
imgProps={{ height: 25, width: 25, style: { marginRight: 10 } }}
|
||||
// expected url for Ruby Sunstone compatibility
|
||||
// => client/assets/images/logos/{value}.png
|
||||
src={`${STATIC_FILES_URL}/${value}`}
|
||||
/>
|
||||
),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,6 +29,7 @@ export const UID_FIELD = {
|
||||
const { data: users = [] } = useGetUsersQuery()
|
||||
|
||||
return arrayToOptions(users, {
|
||||
addEmpty: false,
|
||||
getText: ({ ID, NAME }) => `#${ID} ${NAME}`,
|
||||
getValue: ({ ID }) => ID,
|
||||
sorter: OPTION_SORTERS.numeric,
|
||||
@ -50,6 +51,7 @@ export const GID_FIELD = {
|
||||
const { data: groups = [] } = useGetGroupsQuery()
|
||||
|
||||
return arrayToOptions(groups, {
|
||||
addEmpty: false,
|
||||
getText: ({ ID, NAME }) => `#${ID} ${NAME}`,
|
||||
getValue: ({ ID }) => ID,
|
||||
sorter: OPTION_SORTERS.numeric,
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
FIELDS as INFORMATION_FIELDS,
|
||||
HYPERVISOR_FIELD,
|
||||
} from './informationSchema'
|
||||
import { FIELDS as CAPACITY_FIELDS } from './capacitySchema'
|
||||
import { MEMORY_FIELDS, CPU_FIELDS, VCPU_FIELDS } from './capacitySchema'
|
||||
import { FIELDS as VM_GROUP_FIELDS } from './vmGroupSchema'
|
||||
import { FIELDS as OWNERSHIP_FIELDS } from './ownershipSchema'
|
||||
import { FIELDS as VCENTER_FIELDS } from './vcenterSchema'
|
||||
@ -41,12 +41,28 @@ const SECTIONS = (hypervisor, isUpdate) => [
|
||||
id: 'information',
|
||||
legend: T.Information,
|
||||
required: true,
|
||||
fields: filterFieldsByHypervisor(INFORMATION_FIELDS(isUpdate), hypervisor),
|
||||
fields: INFORMATION_FIELDS(isUpdate),
|
||||
},
|
||||
{
|
||||
id: 'hypervisor',
|
||||
legend: T.Hypervisor,
|
||||
required: true,
|
||||
fields: [HYPERVISOR_FIELD],
|
||||
},
|
||||
{
|
||||
id: 'capacity',
|
||||
legend: T.Capacity,
|
||||
fields: filterFieldsByHypervisor(CAPACITY_FIELDS, hypervisor),
|
||||
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: 'ownership',
|
||||
@ -70,11 +86,10 @@ const SECTIONS = (hypervisor, isUpdate) => [
|
||||
* @returns {BaseSchema} Step schema
|
||||
*/
|
||||
const SCHEMA = (hypervisor) =>
|
||||
getObjectSchemaFromFields([
|
||||
HYPERVISOR_FIELD,
|
||||
...SECTIONS(hypervisor)
|
||||
getObjectSchemaFromFields(
|
||||
SECTIONS(hypervisor)
|
||||
.map(({ fields }) => fields)
|
||||
.flat(),
|
||||
])
|
||||
.flat()
|
||||
)
|
||||
|
||||
export { SECTIONS, SCHEMA }
|
||||
|
@ -20,11 +20,11 @@ export default makeStyles((theme) => ({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: theme.spacing(1),
|
||||
[theme.breakpoints.down('lg')]: {
|
||||
[theme.breakpoints.down('md')]: {
|
||||
gridTemplateColumns: '1fr',
|
||||
},
|
||||
},
|
||||
information: {
|
||||
capacity: {
|
||||
gridColumn: '1 / -1',
|
||||
},
|
||||
}))
|
||||
|
@ -28,6 +28,7 @@ export const VM_GROUP_FIELD = {
|
||||
const { data: vmGroups = [] } = useGetVMGroupsQuery()
|
||||
|
||||
return arrayToOptions(vmGroups, {
|
||||
addEmpty: false,
|
||||
getText: ({ ID, NAME }) => `#${ID} ${NAME}`,
|
||||
getValue: ({ ID }) => ID,
|
||||
sorter: OPTION_SORTERS.numeric,
|
||||
@ -58,7 +59,7 @@ export const ROLE_FIELD = {
|
||||
)
|
||||
?.flat()
|
||||
|
||||
return arrayToOptions(roles)
|
||||
return arrayToOptions(roles, { addEmpty: false })
|
||||
},
|
||||
grid: { md: 12 },
|
||||
validation: string()
|
||||
|
@ -23,14 +23,13 @@ import CustomVariables, {
|
||||
STEP_ID as CUSTOM_ID,
|
||||
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/CustomVariables'
|
||||
import { jsonToXml, userInputsToArray } from 'client/models/Helper'
|
||||
import { createSteps, isBase64 } from 'client/utils'
|
||||
import { createSteps, isBase64, encodeBase64 } from 'client/utils'
|
||||
|
||||
const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
|
||||
transformInitialValue: (vmTemplate, schema) => {
|
||||
const userInputs = userInputsToArray(
|
||||
vmTemplate?.TEMPLATE?.USER_INPUTS,
|
||||
vmTemplate?.TEMPLATE?.INPUTS_ORDER
|
||||
)
|
||||
const userInputs = userInputsToArray(vmTemplate?.TEMPLATE?.USER_INPUTS, {
|
||||
order: vmTemplate?.TEMPLATE?.INPUTS_ORDER,
|
||||
})
|
||||
|
||||
const knownTemplate = schema.cast(
|
||||
{
|
||||
@ -56,7 +55,7 @@ const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
|
||||
},
|
||||
transformBeforeSubmit: (formData) => {
|
||||
const {
|
||||
[GENERAL_ID]: general = {},
|
||||
[GENERAL_ID]: { MODIFICATION: _, ...general } = {},
|
||||
[CUSTOM_ID]: customVariables = {},
|
||||
[EXTRA_ID]: {
|
||||
CONTEXT: { START_SCRIPT, ENCODE_START_SCRIPT, ...restOfContext },
|
||||
@ -70,7 +69,7 @@ const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
|
||||
// transform start script to base64 if needed
|
||||
[ENCODE_START_SCRIPT ? 'START_SCRIPT_BASE64' : 'START_SCRIPT']:
|
||||
ENCODE_START_SCRIPT && !isBase64(START_SCRIPT)
|
||||
? btoa(unescape(encodeURIComponent(START_SCRIPT)))
|
||||
? encodeBase64(START_SCRIPT)
|
||||
: START_SCRIPT,
|
||||
}
|
||||
const topology = ENABLE_NUMA ? { TOPOLOGY: restOfTopology } : {}
|
||||
|
@ -13,54 +13,73 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { number } from 'yup'
|
||||
import { NumberSchema } from 'yup'
|
||||
import { getUserInputParams } from 'client/models/Helper'
|
||||
import {
|
||||
Field,
|
||||
schemaUserInput,
|
||||
prettyBytes,
|
||||
isDivisibleBy,
|
||||
} from 'client/utils'
|
||||
import { T, HYPERVISORS, VmTemplate } from 'client/constants'
|
||||
|
||||
import { Field } from 'client/utils'
|
||||
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
|
||||
const commonValidation = number()
|
||||
.positive()
|
||||
.default(() => undefined)
|
||||
|
||||
/** @type {Field} Memory field */
|
||||
const MEMORY = (hypervisor) => {
|
||||
let validation = commonValidation.required()
|
||||
|
||||
if (hypervisor === HYPERVISORS.vcenter) {
|
||||
validation = validation.isDivisibleBy(4)
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'MEMORY',
|
||||
label: T.Memory,
|
||||
tooltip: T.MemoryConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation,
|
||||
grid: { md: 12 },
|
||||
}
|
||||
const TRANSLATES = {
|
||||
MEMORY: { name: 'MEMORY', label: T.Memory, tooltip: T.MemoryConcept },
|
||||
CPU: { name: 'CPU', label: T.PhysicalCpu, tooltip: T.CpuConcept },
|
||||
VCPU: { name: 'VCPU', label: T.VirtualCpu, tooltip: T.VirtualCpuConcept },
|
||||
}
|
||||
|
||||
/** @type {Field} Physical CPU field */
|
||||
const PHYSICAL_CPU = {
|
||||
name: 'CPU',
|
||||
label: T.PhysicalCpu,
|
||||
tooltip: T.PhysicalCpuConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: commonValidation.required(),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
const valueLabelFormat = (value) => prettyBytes(value, 'MB')
|
||||
|
||||
/** @type {Field} Virtual CPU field */
|
||||
const VIRTUAL_CPU = {
|
||||
name: 'VCPU',
|
||||
label: T.VirtualCpu,
|
||||
tooltip: T.VirtualCpuConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: commonValidation.notRequired(),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
/**
|
||||
* @param {VmTemplate} [vmTemplate] - VM Template
|
||||
* @returns {Field[]} Basic configuration fields
|
||||
*/
|
||||
export const FIELDS = (vmTemplate) => {
|
||||
const {
|
||||
HYPERVISOR,
|
||||
USER_INPUTS = {},
|
||||
MEMORY = '',
|
||||
CPU = '',
|
||||
VCPU = '',
|
||||
} = vmTemplate?.TEMPLATE || {}
|
||||
|
||||
export const FIELDS = [MEMORY, PHYSICAL_CPU, VIRTUAL_CPU]
|
||||
const {
|
||||
MEMORY: memoryInput = `M|number|||${MEMORY}`,
|
||||
CPU: cpuInput = `M|number-float|||${CPU}`,
|
||||
VCPU: vcpuInput = `O|number|||${VCPU}`,
|
||||
} = USER_INPUTS
|
||||
|
||||
return [
|
||||
{ name: 'MEMORY', ...getUserInputParams(memoryInput) },
|
||||
{ name: 'CPU', ...getUserInputParams(cpuInput) },
|
||||
{ name: 'VCPU', ...getUserInputParams(vcpuInput) },
|
||||
].map(({ name, options, ...userInput }) => {
|
||||
const isMemory = name === 'MEMORY'
|
||||
const isVCenter = HYPERVISOR === HYPERVISORS.vcenter
|
||||
const divisibleBy4 = isVCenter && isMemory
|
||||
|
||||
const ensuredOptions = divisibleBy4
|
||||
? options?.filter((value) => isDivisibleBy(+value, 4))
|
||||
: options
|
||||
|
||||
const schemaUi = schemaUserInput({ options: ensuredOptions, ...userInput })
|
||||
const isNumber = schemaUi.validation instanceof NumberSchema
|
||||
|
||||
if (isNumber) {
|
||||
// add positive number validator
|
||||
isNumber && (schemaUi.validation &&= schemaUi.validation.positive())
|
||||
|
||||
// add label format on pretty bytes
|
||||
isMemory &&
|
||||
(schemaUi.fieldProps = { ...schemaUi.fieldProps, valueLabelFormat })
|
||||
|
||||
if (divisibleBy4) {
|
||||
schemaUi.validation &&= schemaUi.validation.isDivisibleBy(4)
|
||||
schemaUi.fieldProps = { ...schemaUi.fieldProps, step: 4 }
|
||||
}
|
||||
}
|
||||
|
||||
return { ...TRANSLATES[name], ...schemaUi, grid: { md: 12 } }
|
||||
})
|
||||
}
|
||||
|
@ -22,23 +22,26 @@ import useStyles from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/
|
||||
|
||||
import {
|
||||
SCHEMA,
|
||||
FIELDS,
|
||||
SECTIONS,
|
||||
} from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema'
|
||||
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
|
||||
import { T, RESOURCE_NAMES, VmTemplate } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'configuration'
|
||||
|
||||
const Content = ({ hypervisor }) => {
|
||||
const Content = ({ vmTemplate }) => {
|
||||
const classes = useStyles()
|
||||
const { view, getResourceView } = useViews()
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
|
||||
const resource = RESOURCE_NAMES.VM_TEMPLATE
|
||||
const dialog = getResourceView(resource)?.dialogs?.instantiate_dialog
|
||||
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
|
||||
|
||||
return FIELDS(hypervisor).filter(({ id }) => sectionsAvailable.includes(id))
|
||||
return SECTIONS(vmTemplate).filter(({ id }) =>
|
||||
sectionsAvailable.includes(id)
|
||||
)
|
||||
}, [view])
|
||||
|
||||
return (
|
||||
@ -57,26 +60,22 @@ const Content = ({ hypervisor }) => {
|
||||
)
|
||||
}
|
||||
|
||||
Content.propTypes = {
|
||||
vmTemplate: PropTypes.object,
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic configuration about VM Template.
|
||||
*
|
||||
* @param {VmTemplate} vmTemplate - VM Template
|
||||
* @returns {object} Basic configuration step
|
||||
*/
|
||||
const BasicConfiguration = (vmTemplate) => {
|
||||
const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
label: T.Configuration,
|
||||
resolver: () => SCHEMA(hypervisor),
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: (props) => Content({ ...props, hypervisor }),
|
||||
}
|
||||
}
|
||||
|
||||
Content.propTypes = {
|
||||
hypervisor: PropTypes.string,
|
||||
}
|
||||
const BasicConfiguration = (vmTemplate) => ({
|
||||
id: STEP_ID,
|
||||
label: T.Configuration,
|
||||
resolver: () => SCHEMA(vmTemplate),
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: (props) => Content({ ...props, vmTemplate }),
|
||||
})
|
||||
|
||||
export default BasicConfiguration
|
||||
|
@ -28,50 +28,59 @@ import {
|
||||
filterFieldsByHypervisor,
|
||||
getObjectSchemaFromFields,
|
||||
Field,
|
||||
Section,
|
||||
} from 'client/utils'
|
||||
import { T, HYPERVISORS } from 'client/constants'
|
||||
import { T, VmTemplate } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
|
||||
* @returns {function(string):{ id: string, legend: string, fields: Field[] }[]} Fields
|
||||
* @param {VmTemplate} [vmTemplate] - VM Template
|
||||
* @returns {Section[]} Sections
|
||||
*/
|
||||
const FIELDS = (hypervisor) => [
|
||||
{
|
||||
id: 'information',
|
||||
legend: T.Information,
|
||||
fields: filterFieldsByHypervisor(INFORMATION_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'capacity',
|
||||
legend: T.Capacity,
|
||||
fields: filterFieldsByHypervisor(CAPACITY_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_FOLDER_FIELD], hypervisor),
|
||||
},
|
||||
]
|
||||
const SECTIONS = (vmTemplate) => {
|
||||
const hypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'information',
|
||||
legend: T.Information,
|
||||
fields: filterFieldsByHypervisor(INFORMATION_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'capacity',
|
||||
legend: T.Capacity,
|
||||
fields: filterFieldsByHypervisor(CAPACITY_FIELDS(vmTemplate), 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_FOLDER_FIELD], hypervisor),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
|
||||
* @param {VmTemplate} [vmTemplate] - VM Template
|
||||
* @returns {Field[]} Basic configuration fields
|
||||
*/
|
||||
const FIELDS = (vmTemplate) =>
|
||||
SECTIONS(vmTemplate)
|
||||
.map(({ fields }) => fields)
|
||||
.flat()
|
||||
|
||||
/**
|
||||
* @param {VmTemplate} [vmTemplate] - VM Template
|
||||
* @returns {BaseSchema} Step schema
|
||||
*/
|
||||
const SCHEMA = (hypervisor) =>
|
||||
getObjectSchemaFromFields(
|
||||
FIELDS(hypervisor)
|
||||
.map(({ fields }) => fields)
|
||||
.flat()
|
||||
)
|
||||
const SCHEMA = (vmTemplate) => getObjectSchemaFromFields(FIELDS(vmTemplate))
|
||||
|
||||
export { FIELDS, SCHEMA }
|
||||
export { SECTIONS, FIELDS, SCHEMA }
|
||||
|
@ -20,8 +20,7 @@ import {
|
||||
FIELDS,
|
||||
SCHEMA,
|
||||
} from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/UserInputs/schema'
|
||||
import { userInputsToArray } from 'client/models/Helper'
|
||||
import { T, VmTemplate } from 'client/constants'
|
||||
import { T, UserInputObject } from 'client/constants'
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
|
||||
export const STEP_ID = 'user_inputs'
|
||||
@ -42,22 +41,15 @@ Content.propTypes = {
|
||||
/**
|
||||
* User inputs step.
|
||||
*
|
||||
* @param {VmTemplate} vmTemplate - VM Template
|
||||
* @param {UserInputObject[]} userInputs - User inputs
|
||||
* @returns {object} User inputs step
|
||||
*/
|
||||
const UserInputsStep = (vmTemplate) => {
|
||||
const userInputs = userInputsToArray(
|
||||
vmTemplate?.TEMPLATE?.USER_INPUTS,
|
||||
vmTemplate?.TEMPLATE?.INPUTS_ORDER
|
||||
)
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
label: T.UserInputs,
|
||||
optionsValidate: { abortEarly: false },
|
||||
resolver: SCHEMA(userInputs),
|
||||
content: (props) => Content({ ...props, userInputs }),
|
||||
}
|
||||
}
|
||||
const UserInputsStep = (userInputs) => ({
|
||||
id: STEP_ID,
|
||||
label: T.UserInputs,
|
||||
optionsValidate: { abortEarly: false },
|
||||
resolver: SCHEMA(userInputs),
|
||||
content: (props) => Content({ ...props, userInputs }),
|
||||
})
|
||||
|
||||
export default UserInputsStep
|
||||
|
@ -22,16 +22,21 @@ import UserInputs, {
|
||||
import ExtraConfiguration, {
|
||||
STEP_ID as EXTRA_ID,
|
||||
} from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { jsonToXml, userInputsToArray } from 'client/models/Helper'
|
||||
import { createSteps } from 'client/utils'
|
||||
|
||||
const Steps = createSteps(
|
||||
(vmTemplate) =>
|
||||
[
|
||||
(vmTemplate) => {
|
||||
const userInputs = userInputsToArray(vmTemplate?.TEMPLATE?.USER_INPUTS, {
|
||||
order: vmTemplate?.TEMPLATE?.INPUTS_ORDER,
|
||||
})
|
||||
|
||||
return [
|
||||
BasicConfiguration,
|
||||
vmTemplate?.TEMPLATE?.USER_INPUTS && UserInputs,
|
||||
!!userInputs.length && (() => UserInputs(userInputs)),
|
||||
ExtraConfiguration,
|
||||
].filter(Boolean),
|
||||
].filter(Boolean)
|
||||
},
|
||||
{
|
||||
transformInitialValue: (vmTemplate, schema) => {
|
||||
const initialValue = schema.cast(
|
||||
|
@ -124,4 +124,4 @@ AsyncLoadForm.propTypes = {
|
||||
componentToLoad: PropTypes.string,
|
||||
}
|
||||
|
||||
export default AsyncLoadForm
|
||||
export { AsyncLoadForm }
|
||||
|
@ -16,9 +16,6 @@
|
||||
export { default as AuthLayout } from 'client/components/HOC/AuthLayout'
|
||||
export { default as ConditionalWrap } from 'client/components/HOC/ConditionalWrap'
|
||||
export { default as InternalLayout } from 'client/components/HOC/InternalLayout'
|
||||
export {
|
||||
default as AsyncLoadForm,
|
||||
ConfigurationProps,
|
||||
} from 'client/components/HOC/AsyncLoadForm'
|
||||
|
||||
export * from 'client/components/HOC/AsyncLoadForm'
|
||||
export * from 'client/components/HOC/Translate'
|
||||
|
@ -41,17 +41,17 @@ const VmTemplateInfoTab = ({ tabProps = {}, id }) => {
|
||||
ownership_panel: ownershipPanel,
|
||||
} = tabProps
|
||||
|
||||
const [changeOwnership] = useChangeTemplatePermissionsMutation()
|
||||
const [changePermissions] = useChangeTemplateOwnershipMutation()
|
||||
const [changePermissions] = useChangeTemplatePermissionsMutation()
|
||||
const [changeOwnership] = useChangeTemplateOwnershipMutation()
|
||||
const { data: template = {} } = useGetTemplateQuery({ id })
|
||||
const { ID, UNAME, UID, GNAME, GID, PERMISSIONS } = template
|
||||
const { UNAME, UID, GNAME, GID, PERMISSIONS } = template
|
||||
|
||||
const handleChangeOwnership = async (newOwnership) => {
|
||||
await changeOwnership({ id: ID, ...newOwnership })
|
||||
await changeOwnership({ id, ...newOwnership })
|
||||
}
|
||||
|
||||
const handleChangePermission = async (newPermission) => {
|
||||
await changePermissions({ id: ID, ...newPermission })
|
||||
await changePermissions({ id, ...newPermission })
|
||||
}
|
||||
|
||||
const getActions = (actions) => Helper.getActionsAvailable(actions)
|
||||
@ -72,6 +72,7 @@ const VmTemplateInfoTab = ({ tabProps = {}, id }) => {
|
||||
{permissionsPanel?.enabled && (
|
||||
<Permissions
|
||||
actions={getActions(permissionsPanel?.actions)}
|
||||
handleEdit={handleChangePermission}
|
||||
ownerUse={PERMISSIONS.OWNER_U}
|
||||
ownerManage={PERMISSIONS.OWNER_M}
|
||||
ownerAdmin={PERMISSIONS.OWNER_A}
|
||||
@ -81,17 +82,16 @@ const VmTemplateInfoTab = ({ tabProps = {}, id }) => {
|
||||
otherUse={PERMISSIONS.OTHER_U}
|
||||
otherManage={PERMISSIONS.OTHER_M}
|
||||
otherAdmin={PERMISSIONS.OTHER_A}
|
||||
handleEdit={handleChangePermission}
|
||||
/>
|
||||
)}
|
||||
{ownershipPanel?.enabled && (
|
||||
<Ownership
|
||||
actions={getActions(ownershipPanel?.actions)}
|
||||
handleEdit={handleChangeOwnership}
|
||||
userId={UID}
|
||||
userName={UNAME}
|
||||
groupId={GID}
|
||||
groupName={GNAME}
|
||||
handleEdit={handleChangeOwnership}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
@ -24,7 +24,7 @@ import { timeToString, levelLockToString } from 'client/models/Helper'
|
||||
import {
|
||||
T,
|
||||
VM_TEMPLATE_ACTIONS,
|
||||
LOGO_IMAGES_URL,
|
||||
STATIC_FILES_URL,
|
||||
VmTemplate,
|
||||
} from 'client/constants'
|
||||
|
||||
@ -67,7 +67,7 @@ const InformationPanel = ({ template = {}, actions }) => {
|
||||
},
|
||||
LOGO && {
|
||||
name: T.Logo,
|
||||
value: <Image alt="logo" src={`${LOGO_IMAGES_URL}/${LOGO}`} />,
|
||||
value: <Image alt="logo" src={`${STATIC_FILES_URL}/${LOGO}`} />,
|
||||
dataCy: 'logo',
|
||||
},
|
||||
].filter(Boolean)
|
||||
|
@ -484,6 +484,9 @@ module.exports = {
|
||||
DefaultNicFilter: 'Default network filtering rule for all NICs',
|
||||
/* VM Template schema - capacity */
|
||||
MaxMemory: 'Max memory',
|
||||
MaxMemoryConcept: `
|
||||
This value sets the maximum value of the MEMORY allowed to be modified
|
||||
after instantiation, through the Capacity Resize options of instantiated VMs`,
|
||||
MemoryModification: 'Memory modification',
|
||||
AllowUsersToModifyMemory:
|
||||
"Allow users to modify this template's default memory on instantiate",
|
||||
@ -492,11 +495,15 @@ module.exports = {
|
||||
Percentage of CPU divided by 100 required for the
|
||||
Virtual Machine. Half a processor is written 0.5`,
|
||||
MaxVirtualCpu: 'Max Virtual CPU',
|
||||
MaxVirtualCpuConcept: `
|
||||
This value sets the maximum value of the VCPU allowed to be modified
|
||||
after instantiation, through the Capacity Resize options of instantiated VMs`,
|
||||
CpuModification: 'CPU modification',
|
||||
AllowUsersToModifyCpu:
|
||||
"Allow users to modify this template's default CPU on instantiate",
|
||||
VirtualCpuConcept: `
|
||||
Number of virtual cpus. This value is optional, the default
|
||||
hypervisor behavior is used, usually one virtual CPU`,
|
||||
AllowUsersToModifyCpu:
|
||||
"Allow users to modify this template's default CPU on instantiate",
|
||||
VirtualCpuModification: 'Virtual CPU modification',
|
||||
AllowUsersToModifyVirtualCpu:
|
||||
"Allow users to modify this template's default Virtual CPU on instantiate",
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
/**
|
||||
* @typedef {(
|
||||
* 'fixed' |
|
||||
* 'text' |
|
||||
* 'text64' |
|
||||
* 'password' |
|
||||
@ -64,6 +65,7 @@
|
||||
|
||||
/** @enum {UserInputType} User input types */
|
||||
export const USER_INPUT_TYPES = {
|
||||
fixed: 'fixed',
|
||||
text: 'text',
|
||||
text64: 'text64',
|
||||
password: 'password',
|
||||
|
@ -215,6 +215,34 @@ const vmTemplateApi = oneApi.injectEndpoints({
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: TEMPLATE, id }],
|
||||
async onQueryStarted(
|
||||
{ id, ...permissions },
|
||||
{ dispatch, queryFulfilled }
|
||||
) {
|
||||
const patchResult = dispatch(
|
||||
vmTemplateApi.util.updateQueryData('getTemplate', { id }, (draft) => {
|
||||
Object.entries(permissions)
|
||||
.filter(([_, value]) => value !== '-1')
|
||||
.forEach(([name, value]) => {
|
||||
const ensuredName = {
|
||||
ownerUse: 'OWNER_U',
|
||||
ownerManage: 'OWNER_M',
|
||||
ownerAdmin: 'OWNER_A',
|
||||
groupUse: 'GROUP_U',
|
||||
groupManage: 'GROUP_M',
|
||||
groupAdmin: 'GROUP_A',
|
||||
otherUse: 'OTHER_U',
|
||||
otherManage: 'OTHER_M',
|
||||
otherAdmin: 'OTHER_A',
|
||||
}[name]
|
||||
|
||||
draft.PERMISSIONS[ensuredName] = value
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
queryFulfilled.catch(patchResult.undo)
|
||||
},
|
||||
}),
|
||||
changeTemplateOwnership: builder.mutation({
|
||||
/**
|
||||
@ -235,6 +263,19 @@ const vmTemplateApi = oneApi.injectEndpoints({
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: TEMPLATE, id }],
|
||||
async onQueryStarted(
|
||||
{ id, user, group },
|
||||
{ dispatch, queryFulfilled, getState }
|
||||
) {
|
||||
const patchResult = dispatch(
|
||||
vmTemplateApi.util.updateQueryData('getTemplate', id, (draft) => {
|
||||
user > 0 && (draft.UID = user)
|
||||
group > 0 && (draft.GID = group)
|
||||
})
|
||||
)
|
||||
|
||||
queryFulfilled.catch(patchResult.undo)
|
||||
},
|
||||
}),
|
||||
renameTemplate: builder.mutation({
|
||||
/**
|
||||
|
@ -339,7 +339,7 @@ export const getUserInputString = (userInput) => {
|
||||
// mandatory|type|description|range/options/' '|defaultValue
|
||||
const uiString = [mandatoryString, type, description]
|
||||
|
||||
range?.length > 0
|
||||
;[USER_INPUT_TYPES.range, USER_INPUT_TYPES.rangeFloat].includes(type)
|
||||
? uiString.push(range)
|
||||
: options?.length > 0
|
||||
? uiString.push(options.join(LIST_SEPARATOR))
|
||||
@ -352,16 +352,19 @@ export const getUserInputString = (userInput) => {
|
||||
* Get list of user inputs defined in OpenNebula template.
|
||||
*
|
||||
* @param {object} userInputs - List of user inputs in string format
|
||||
* @param {string} [inputsOrder] - List separated by comma of input names
|
||||
* @param {object} [options] - Options to filter user inputs
|
||||
* @param {boolean} [options.filterCapacityInputs]
|
||||
* - If false, will not filter capacity inputs: MEMORY, CPU, VCPU. By default `true`
|
||||
* @param {string} [options.order] - List separated by comma of input names
|
||||
* @example
|
||||
* const userInputs = {
|
||||
* "INPUT-1": "O|text|Description1| |text1",
|
||||
* "INPUT-2": "M|text|Description2| |text2"
|
||||
* }
|
||||
*
|
||||
* const inputsOrder = "INPUT-2,INPUT-1"
|
||||
* const order = "INPUT-2,INPUT-1"
|
||||
*
|
||||
* => userInputsToArray(userInputs, inputsOrder) => [{
|
||||
* => userInputsToArray(userInputs, { order }) => [{
|
||||
* name: 'INPUT-1',
|
||||
* mandatory: false,
|
||||
* type: 'text',
|
||||
@ -377,17 +380,33 @@ export const getUserInputString = (userInput) => {
|
||||
* }]
|
||||
* @returns {UserInputObject[]} User input object
|
||||
*/
|
||||
export const userInputsToArray = (userInputs = {}, inputsOrder) => {
|
||||
const orderedList = inputsOrder?.split(',') ?? []
|
||||
export const userInputsToArray = (
|
||||
userInputs = {},
|
||||
{ filterCapacityInputs = true, order } = {}
|
||||
) => {
|
||||
const orderedList = order?.split(',') ?? []
|
||||
const userInputsArray = Object.entries(userInputs)
|
||||
|
||||
return Object.entries(userInputs)
|
||||
.map(([name, ui]) => ({ name, ...getUserInputParams(ui) }))
|
||||
.sort((a, b) => {
|
||||
let list = userInputsArray.map(([name, ui]) => ({
|
||||
name: `${name}`.toUpperCase(),
|
||||
...(typeof ui === 'string' ? getUserInputParams(ui) : ui),
|
||||
}))
|
||||
|
||||
if (filterCapacityInputs) {
|
||||
const capacityInputs = ['MEMORY', 'CPU', 'VCPU']
|
||||
list = list.filter((ui) => !capacityInputs.includes(ui.name))
|
||||
}
|
||||
|
||||
if (orderedList.length) {
|
||||
list = list.sort((a, b) => {
|
||||
const upperAName = a.name?.toUpperCase?.()
|
||||
const upperBName = b.name?.toUpperCase?.()
|
||||
|
||||
return orderedList.indexOf(upperAName) - orderedList.indexOf(upperBName)
|
||||
})
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { object, reach, ObjectSchema, BaseSchema } from 'yup'
|
||||
import { isMergeableObject } from 'client/utils/merge'
|
||||
@ -41,8 +42,7 @@ export const isExternalURL = (url) => /^(http|https):/g.test(url)
|
||||
*
|
||||
* @returns {string} Random key
|
||||
*/
|
||||
export const generateKey = () =>
|
||||
String(new Date().getTime() + Math.random()).replace('.', '')
|
||||
export const generateKey = () => uuidv4()
|
||||
|
||||
/**
|
||||
* Sanitizes HTML and prevents XSS attacks.
|
||||
@ -443,15 +443,6 @@ export const isBase64 = (stringToValidate, options = {}) => {
|
||||
return regex.test(stringToValidate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if value is divisible by 4.
|
||||
*
|
||||
* @param {string|number} value - Number to check
|
||||
* @returns {boolean} Returns `true` if string is divisible by 4
|
||||
*/
|
||||
export const isDivisibleBy4 = (value) =>
|
||||
/[048]|\d*([02468][048]|[13579][26])/g.test(value)
|
||||
|
||||
/**
|
||||
* Check if value is divisible by another number.
|
||||
*
|
||||
|
@ -115,6 +115,8 @@ import { stringToBoolean } from 'client/models/Helper'
|
||||
* - Filters the field when the driver is not include on list
|
||||
* @property {TextFieldProps|CheckboxProps|InputBaseComponentProps} [fieldProps]
|
||||
* - Extra properties to material-ui field
|
||||
* @property {boolean|DependOfCallback} [readOnly]
|
||||
* - If `true`, the field is read only
|
||||
* @property {function(string|number):any} [renderValue]
|
||||
* - Render the current selected value inside selector input
|
||||
* - **Only for select inputs.**
|
||||
@ -218,12 +220,8 @@ const getRange = (options) => options?.split?.('..').map(parseFloat)
|
||||
const getValuesFromArray = (options, separator = SEMICOLON_CHAR) =>
|
||||
options?.split(separator)
|
||||
|
||||
const getOptionsFromList = (options) =>
|
||||
options
|
||||
?.map((option) =>
|
||||
typeof option === 'string' ? { text: option, value: option } : option
|
||||
)
|
||||
?.filter(({ text, value } = {}) => text && value)
|
||||
const getOptionsFromList = (options = []) =>
|
||||
arrayToOptions([...new Set(options)])
|
||||
|
||||
const parseUserInputValue = (value) => {
|
||||
if (value === true) {
|
||||
@ -257,6 +255,22 @@ export const schemaUserInput = ({
|
||||
default: defaultValue,
|
||||
}) => {
|
||||
switch (type) {
|
||||
case USER_INPUT_TYPES.fixed: {
|
||||
const isNumeric = !isNaN(defaultValue)
|
||||
const ensuredValue = isNumeric ? parseFloat(defaultValue) : defaultValue
|
||||
const validation = isNumeric ? number() : string().trim()
|
||||
|
||||
return {
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: isNaN(+defaultValue) ? 'text' : 'number',
|
||||
validation: validation
|
||||
.default(ensuredValue)
|
||||
// ensures to send the value
|
||||
.afterSubmit(() => defaultValue),
|
||||
fieldProps: { disabled: true },
|
||||
readOnly: true,
|
||||
}
|
||||
}
|
||||
case USER_INPUT_TYPES.text:
|
||||
case USER_INPUT_TYPES.text64:
|
||||
case USER_INPUT_TYPES.password:
|
||||
@ -311,7 +325,8 @@ export const schemaUserInput = ({
|
||||
}
|
||||
case USER_INPUT_TYPES.list: {
|
||||
const values = getOptionsFromList(options)
|
||||
const firstOption = values?.[0]?.value ?? undefined
|
||||
const optionValues = values.map(({ value }) => value).filter(Boolean)
|
||||
const firstOption = optionValues[0] ?? undefined
|
||||
|
||||
return {
|
||||
values,
|
||||
@ -319,7 +334,7 @@ export const schemaUserInput = ({
|
||||
validation: string()
|
||||
.trim()
|
||||
.concat(requiredSchema(mandatory, string()))
|
||||
.oneOf(values.map(({ value }) => value))
|
||||
.oneOf(optionValues)
|
||||
.default(() => defaultValue || firstOption),
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user