mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-21 14:50:08 +03:00
parent
434be6e5af
commit
a6744fb8eb
@ -68,7 +68,7 @@ const TextController = memo(
|
||||
InputProps={{
|
||||
endAdornment: tooltip && <Tooltip title={tooltip} />
|
||||
}}
|
||||
inputProps={{ 'data-cy': cy, ...fieldProps }}
|
||||
inputProps={{ 'data-cy': cy }}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
@ -97,14 +97,7 @@ TextController.propTypes = {
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string)
|
||||
]),
|
||||
fieldProps: PropTypes.object,
|
||||
formContext: PropTypes.shape({
|
||||
setValue: PropTypes.func,
|
||||
setError: PropTypes.func,
|
||||
clearErrors: PropTypes.func,
|
||||
watch: PropTypes.func,
|
||||
register: PropTypes.func
|
||||
})
|
||||
fieldProps: PropTypes.object
|
||||
}
|
||||
|
||||
TextController.displayName = 'TextController'
|
||||
|
@ -54,7 +54,7 @@ const FormWithSchema = ({ id, cy, fields, rootProps, className, legend, legendTo
|
||||
|
||||
const getFields = useMemo(() => typeof fields === 'function' ? fields() : fields, [])
|
||||
|
||||
if (getFields.length === 0) return null
|
||||
if (!getFields || getFields?.length === 0) return null
|
||||
|
||||
const addIdToName = name => name.startsWith('$')
|
||||
? name.slice(1) // removes character '$' and returns
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Stack, Box } from '@mui/material'
|
||||
import { Stack, FormControl } from '@mui/material'
|
||||
import { SystemShut as OsIcon } from 'iconoir-react'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
@ -44,10 +44,11 @@ const Booting = ({ hypervisor, ...props }) => {
|
||||
!!props.data?.[STORAGE_ID]?.length ||
|
||||
!!props.data?.[NIC_ID]?.length
|
||||
) && (
|
||||
<Box component='fieldset' gridColumn='1/-1'>
|
||||
|
||||
<FormControl component='fieldset' sx={{ width: '100%', gridColumn: '1 / -1' }}>
|
||||
<Legend title={T.BootOrder} tooltip={T.BootOrderConcept} />
|
||||
<BootOrder {...props} />
|
||||
</Box>
|
||||
</FormControl>
|
||||
)}
|
||||
{sections.map(({ id, ...section }) => (
|
||||
<FormWithSchema
|
||||
|
@ -0,0 +1,117 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { string, boolean, ref, ObjectSchema } from 'yup'
|
||||
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
import { Field, getObjectSchemaFromFields } from 'client/utils'
|
||||
|
||||
const switchField = {
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean()
|
||||
.notRequired()
|
||||
.transform(value => {
|
||||
if (typeof value === 'boolean') return value
|
||||
|
||||
return String(value).toUpperCase() === 'YES'
|
||||
}),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} SSH public key field */
|
||||
export const SSH_PUBLIC_KEY = {
|
||||
name: 'CONTEXT.SSH_PUBLIC_KEY',
|
||||
label: T.SshPublicKey,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
multiline: true,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired(),
|
||||
grid: { md: 12 },
|
||||
fieldProps: { rows: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Network context field */
|
||||
const NETWORK = {
|
||||
name: 'CONTEXT.NETWORK',
|
||||
label: T.AddNetworkContextualization,
|
||||
tooltip: T.AddNetworkContextualizationConcept,
|
||||
...switchField
|
||||
}
|
||||
|
||||
/** @type {Field} Token OneGate token field */
|
||||
const TOKEN = {
|
||||
name: 'CONTEXT.TOKEN',
|
||||
label: T.AddOneGateToken,
|
||||
tooltip: T.AddOneGateTokenConcept,
|
||||
...switchField
|
||||
}
|
||||
|
||||
/** @type {Field} Report READY to OneGate field */
|
||||
const REPORT_READY = {
|
||||
name: 'CONTEXT.REPORT_READY',
|
||||
label: T.ReportReadyToOneGate,
|
||||
tooltip: T.ReportReadyToOneGateConcept,
|
||||
...switchField
|
||||
}
|
||||
|
||||
/** @type {Field} Encode start script field */
|
||||
export const ENCODE_START_SCRIPT = {
|
||||
name: 'CONTEXT.ENCODE_START_SCRIPT',
|
||||
label: T.EncodeScriptInBase64,
|
||||
...switchField,
|
||||
validation: boolean()
|
||||
.transform(value => Boolean(value))
|
||||
.default(() => ref('$extra.CONTEXT.START_SCRIPT_BASE64'))
|
||||
}
|
||||
|
||||
/** @type {Field} Start script field */
|
||||
export const START_SCRIPT = {
|
||||
name: 'CONTEXT.START_SCRIPT',
|
||||
label: T.StartScript,
|
||||
tooltip: T.StartScriptConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
multiline: true,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.when(
|
||||
'$extra.CONTEXT.START_SCRIPT_BASE64',
|
||||
(scriptEncoded, schema) => scriptEncoded
|
||||
? schema.default(() => decodeURIComponent(escape(atob(scriptEncoded))))
|
||||
: schema
|
||||
),
|
||||
grid: { md: 12 },
|
||||
fieldProps: { rows: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Start script in base64 field */
|
||||
export const START_SCRIPT_BASE64 = {
|
||||
name: 'CONTEXT.START_SCRIPT_BASE64',
|
||||
validation: string().strip()
|
||||
}
|
||||
|
||||
export const SCRIPT_FIELDS = [START_SCRIPT, ENCODE_START_SCRIPT]
|
||||
|
||||
/** @type {Field[]} List of other fields */
|
||||
export const OTHER_FIELDS = [NETWORK, TOKEN, REPORT_READY]
|
||||
|
||||
/** @type {ObjectSchema} User context configuration schema */
|
||||
export const CONFIGURATION_SCHEMA = getObjectSchemaFromFields([
|
||||
SSH_PUBLIC_KEY,
|
||||
START_SCRIPT_BASE64,
|
||||
...SCRIPT_FIELDS,
|
||||
...OTHER_FIELDS
|
||||
])
|
@ -0,0 +1,82 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { JSXElementConstructor } from 'react'
|
||||
import { Stack, FormControl, Button } from '@mui/material'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
import { FormWithSchema, Legend } from 'client/components/Forms'
|
||||
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { SSH_PUBLIC_KEY, SCRIPT_FIELDS, OTHER_FIELDS } from './schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const SECTION_ID = 'CONTEXT'
|
||||
|
||||
const SSH_KEY_USER = '$USER[SSH_PUBLIC_KEY]'
|
||||
|
||||
/** @returns {JSXElementConstructor} - Configuration section */
|
||||
const ConfigurationSection = () => {
|
||||
const { setValue, getValues } = useFormContext()
|
||||
const SSH_PUBLIC_KEY_PATH = `${EXTRA_ID}.${SSH_PUBLIC_KEY.name}`
|
||||
|
||||
const handleClearKey = () => setValue(SSH_PUBLIC_KEY_PATH)
|
||||
|
||||
const handleAddUserKey = () => {
|
||||
const currentSshPublicKey = getValues(SSH_PUBLIC_KEY_PATH) ?? ''
|
||||
|
||||
setValue(SSH_PUBLIC_KEY_PATH, currentSshPublicKey + `\n${SSH_KEY_USER}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl component='fieldset' sx={{ width: '100%' }}>
|
||||
<Legend title={T.Configuration} />
|
||||
<Stack
|
||||
display='grid'
|
||||
gap='1em'
|
||||
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
|
||||
>
|
||||
<FormWithSchema
|
||||
cy={`create-vm-template-${EXTRA_ID}.context-configuration-others`}
|
||||
fields={OTHER_FIELDS}
|
||||
id={EXTRA_ID}
|
||||
/>
|
||||
<div>
|
||||
<FormWithSchema
|
||||
cy={`create-vm-template-${EXTRA_ID}.context-ssh-public-key`}
|
||||
fields={[SSH_PUBLIC_KEY]}
|
||||
id={EXTRA_ID}
|
||||
/>
|
||||
<Stack direction='row' gap='1em'>
|
||||
<Button onClick={handleAddUserKey} variant='contained'>
|
||||
{T.AddUserSshPublicKey}
|
||||
</Button>
|
||||
<Button onClick={handleClearKey} variant='outlined'>
|
||||
{T.Clear}
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
<FormWithSchema
|
||||
cy={`create-vm-template-${EXTRA_ID}.context-script`}
|
||||
fields={SCRIPT_FIELDS}
|
||||
id={EXTRA_ID}
|
||||
rootProps={{ sx: { width: '100%', gridColumn: '1 / -1' } }}
|
||||
/>
|
||||
</Stack>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfigurationSection
|
@ -13,177 +13,23 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { forwardRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Folder as ContextIcon,
|
||||
WarningCircledOutline as WarningIcon,
|
||||
DeleteCircledOutline,
|
||||
AddCircledOutline
|
||||
} from 'iconoir-react'
|
||||
import {
|
||||
styled,
|
||||
FormControl,
|
||||
Stack,
|
||||
IconButton,
|
||||
Button,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from '@mui/material'
|
||||
import { Folder as ContextIcon } from 'iconoir-react'
|
||||
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd'
|
||||
import { useFieldArray, useForm, FormProvider, useFormContext, get } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import ConfigurationSection, { SECTION_ID as CONFIGURATION_ID } from './configurationSection'
|
||||
import UserInputsSection, { SECTION_ID as USER_INPUTS_ID } from './userInputsSection'
|
||||
|
||||
import { Tooltip } from 'client/components/FormControl'
|
||||
import { FormWithSchema, Legend } from 'client/components/Forms'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID as EXTRA_ID, TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { FIELDS, USER_INPUT_SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/schema'
|
||||
import { getUserInputString } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const TAB_ID = 'USER_INPUTS'
|
||||
|
||||
const UserItemDraggable = styled(ListItem)(({ theme }) => ({
|
||||
'&:before': {
|
||||
content: "''",
|
||||
display: 'block',
|
||||
width: 16,
|
||||
height: 10,
|
||||
background: `linear-gradient(
|
||||
to bottom,
|
||||
${theme.palette.action.active} 4px,
|
||||
transparent 4px,
|
||||
transparent 6px,
|
||||
${theme.palette.action.active} 6px
|
||||
)`
|
||||
}
|
||||
}))
|
||||
|
||||
const UserInputItem = forwardRef(({
|
||||
removeAction,
|
||||
error,
|
||||
userInput: { name, ...ui } = {},
|
||||
...props
|
||||
}, ref) => (
|
||||
<UserItemDraggable
|
||||
ref={ref}
|
||||
secondaryAction={
|
||||
<IconButton onClick={removeAction}>
|
||||
<DeleteCircledOutline />
|
||||
</IconButton>
|
||||
}
|
||||
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
|
||||
{...props}
|
||||
>
|
||||
{!!error && (
|
||||
<ListItemIcon sx={{ '& svg': { color: 'error.dark' } }}>
|
||||
<Tooltip title={error?.default.message}>
|
||||
<WarningIcon />
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<ListItemText
|
||||
inset={!error}
|
||||
primary={name}
|
||||
primaryTypographyProps={{ variant: 'body1' }}
|
||||
secondary={getUserInputString(ui)}
|
||||
/>
|
||||
</UserItemDraggable>
|
||||
))
|
||||
|
||||
UserInputItem.propTypes = {
|
||||
removeAction: PropTypes.func,
|
||||
error: PropTypes.object,
|
||||
userInput: PropTypes.object
|
||||
}
|
||||
|
||||
UserInputItem.displayName = 'UserInputItem'
|
||||
export const TAB_ID = [CONFIGURATION_ID, USER_INPUTS_ID]
|
||||
|
||||
const Context = () => {
|
||||
const { formState: { errors } } = useFormContext()
|
||||
const { fields: userInputs, append, remove, move } = useFieldArray({
|
||||
name: `${EXTRA_ID}.${TAB_ID}`
|
||||
})
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: USER_INPUT_SCHEMA.default(),
|
||||
resolver: yupResolver(USER_INPUT_SCHEMA)
|
||||
})
|
||||
|
||||
const onSubmit = newInput => {
|
||||
append(newInput)
|
||||
methods.reset()
|
||||
}
|
||||
|
||||
/** @param {DropResult} result - Drop result */
|
||||
const onDragEnd = result => {
|
||||
const { destination, source } = result ?? {}
|
||||
|
||||
if (destination && destination.index !== source.index) {
|
||||
move(source.index, destination.index)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl component='fieldset' sx={{ width: '100%' }}>
|
||||
<Legend title={T.UserInputs} tooltip={T.UserInputsConcept} />
|
||||
<FormProvider {...methods}>
|
||||
<Stack
|
||||
direction='row' alignItems='flex-start' gap='0.5rem'
|
||||
component='form'
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormWithSchema
|
||||
cy={`create-vm-template-${EXTRA_ID}.context-user-input`}
|
||||
fields={FIELDS}
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
/>
|
||||
<Button
|
||||
variant='outlined'
|
||||
type='submit'
|
||||
startIcon={<AddCircledOutline />}
|
||||
sx={{ mt: '1em' }}
|
||||
>
|
||||
<Translate word={T.Add} />
|
||||
</Button>
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
<Divider />
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId='context'>
|
||||
{({ droppableProps, innerRef: outerRef, placeholder }) => (
|
||||
<List ref={outerRef} {...droppableProps}>
|
||||
{userInputs?.map(({ id, ...userInput }, index) => (
|
||||
<Draggable
|
||||
key={`ui[${index}]`}
|
||||
draggableId={`ui-${index}`}
|
||||
index={index}
|
||||
>
|
||||
{({ draggableProps, dragHandleProps, innerRef }) => (
|
||||
<UserInputItem
|
||||
key={id}
|
||||
ref={innerRef}
|
||||
userInput={userInput}
|
||||
error={get(errors, `${EXTRA_ID}.${TAB_ID}.${index}`)}
|
||||
removeAction={() => remove(index)}
|
||||
{...draggableProps}
|
||||
{...dragHandleProps}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{placeholder}
|
||||
</List>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</FormControl>
|
||||
<>
|
||||
<ConfigurationSection />
|
||||
<UserInputsSection />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -200,7 +46,7 @@ const TAB = {
|
||||
name: T.Context,
|
||||
icon: ContextIcon,
|
||||
Content: Context,
|
||||
getError: error => !!error?.[TAB_ID]
|
||||
getError: error => TAB_ID.some(id => error?.[id])
|
||||
}
|
||||
|
||||
export default TAB
|
||||
|
@ -13,161 +13,14 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { array, string, boolean, number, ref } from 'yup'
|
||||
import { object } from 'yup'
|
||||
|
||||
import { UserInputType, T, INPUT_TYPES, USER_INPUT_TYPES } from 'client/constants'
|
||||
import { Field, arrayToOptions, sentenceCase, getObjectSchemaFromFields } from 'client/utils'
|
||||
import { USER_INPUTS_SCHEMA } from './userInputsSchema'
|
||||
import { CONFIGURATION_SCHEMA } from './configurationSchema'
|
||||
|
||||
const {
|
||||
password: uiPassword,
|
||||
list: uiList,
|
||||
listMultiple: uiListMultiple,
|
||||
number: uiNumber,
|
||||
numberFloat: uiNumberFloat,
|
||||
range: uiRange,
|
||||
rangeFloat: uiRangeFloat,
|
||||
boolean: uiBoolean
|
||||
} = USER_INPUT_TYPES
|
||||
export const SCHEMA = object()
|
||||
.concat(CONFIGURATION_SCHEMA)
|
||||
.concat(USER_INPUTS_SCHEMA)
|
||||
|
||||
const { array: _, ...userInputTypes } = USER_INPUT_TYPES
|
||||
|
||||
/** @type {UserInputType[]} User inputs types */
|
||||
const valuesOfUITypes = Object.values(userInputTypes)
|
||||
|
||||
/** @type {Field} Type field */
|
||||
export const TYPE = {
|
||||
name: 'type',
|
||||
label: T.Type,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(valuesOfUITypes, {
|
||||
addEmpty: false,
|
||||
getText: type => sentenceCase(type)
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.oneOf(valuesOfUITypes)
|
||||
.default(() => valuesOfUITypes[0]),
|
||||
grid: { sm: 6, md: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Name field */
|
||||
export const NAME = {
|
||||
name: 'name',
|
||||
label: T.Name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 6, md: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Description field */
|
||||
export const DESCRIPTION = {
|
||||
name: 'description',
|
||||
label: T.Description,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 6, md: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Options field */
|
||||
const OPTIONS = {
|
||||
name: 'options',
|
||||
label: T.Options,
|
||||
tooltip: 'Press ENTER key to add a value',
|
||||
dependOf: TYPE.name,
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
multiple: true,
|
||||
htmlType: type =>
|
||||
![uiList, uiListMultiple].includes(type) && INPUT_TYPES.HIDDEN,
|
||||
validation: array(string().trim())
|
||||
.default(() => [])
|
||||
.when(
|
||||
TYPE.name,
|
||||
(type, schema) => [uiList, uiListMultiple].includes(type)
|
||||
? schema.required()
|
||||
: schema.strip().notRequired()
|
||||
),
|
||||
fieldProps: {
|
||||
freeSolo: true,
|
||||
placeholder: 'optA,optB,optC'
|
||||
},
|
||||
grid: { md: 8 }
|
||||
}
|
||||
|
||||
/** @type {{ MIN: Field, MAX: Field }} Range fields */
|
||||
const { MIN, MAX } = (() => {
|
||||
const validation = number()
|
||||
.positive()
|
||||
.default(() => undefined)
|
||||
.when(TYPE.name, (type, schema) => [uiRange, uiRangeFloat].includes(type)
|
||||
? schema.required()
|
||||
: schema.strip().notRequired()
|
||||
)
|
||||
|
||||
const common = {
|
||||
dependOf: TYPE.name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: type =>
|
||||
[uiRange, uiRangeFloat].includes(type) ? 'number' : INPUT_TYPES.HIDDEN,
|
||||
grid: { sm: 6, md: 4 },
|
||||
fieldProps: type => ({ step: type === uiRangeFloat ? 0.01 : 1 })
|
||||
}
|
||||
|
||||
return {
|
||||
MIN: { ...common, name: 'min', label: T.Min, validation: validation.lessThan(ref('max')) },
|
||||
MAX: { ...common, name: 'max', label: T.Max, validation: validation.moreThan(ref('min')) }
|
||||
}
|
||||
})()
|
||||
|
||||
/** @type {Field} Default value field */
|
||||
const DEFAULT_VALUE = {
|
||||
name: 'default',
|
||||
label: T.DefaultValue,
|
||||
dependOf: [TYPE.name, OPTIONS.name],
|
||||
type: ([type] = []) => [uiBoolean, uiList, uiListMultiple].includes(type)
|
||||
? INPUT_TYPES.SELECT
|
||||
: INPUT_TYPES.TEXT,
|
||||
htmlType: ([type] = []) => ({
|
||||
[uiNumber]: 'number',
|
||||
[uiNumberFloat]: 'number',
|
||||
[uiPassword]: INPUT_TYPES.HIDDEN
|
||||
}[type]),
|
||||
multiple: ([type] = []) => type === uiListMultiple,
|
||||
values: ([type, options = []] = []) => type === uiBoolean
|
||||
? arrayToOptions(['NO', 'YES'])
|
||||
: arrayToOptions(options),
|
||||
validation: string()
|
||||
.trim()
|
||||
.default(() => undefined)
|
||||
.when([TYPE.name, OPTIONS.name], (type, options = [], schema) => {
|
||||
return {
|
||||
[uiList]: schema.oneOf(options).notRequired(),
|
||||
[uiListMultiple]: schema.includesInOptions(options),
|
||||
[uiRange]: number().min(ref(MIN.name)).max(ref(MAX.name)).integer(),
|
||||
[uiRangeFloat]: number().min(ref(MIN.name)).max(ref(MAX.name)),
|
||||
[uiPassword]: schema.strip().notRequired()
|
||||
}[type] ?? schema
|
||||
}),
|
||||
grid: { sm: 6, md: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Mandatory field */
|
||||
const MANDATORY = {
|
||||
name: 'mandatory',
|
||||
label: T.Mandatory,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().default(() => false),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
export const FIELDS = [TYPE, NAME, DESCRIPTION, DEFAULT_VALUE, OPTIONS, MIN, MAX, MANDATORY]
|
||||
|
||||
export const USER_INPUT_SCHEMA = getObjectSchemaFromFields(FIELDS)
|
||||
|
||||
export const SCHEMA = array(USER_INPUT_SCHEMA).ensure()
|
||||
export * from './userInputsSchema'
|
||||
export * from './configurationSchema'
|
||||
|
@ -0,0 +1,187 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { object, array, string, boolean, number, ref, ObjectSchema } from 'yup'
|
||||
|
||||
import { UserInputType, T, INPUT_TYPES, USER_INPUT_TYPES } from 'client/constants'
|
||||
import { Field, arrayToOptions, sentenceCase, getObjectSchemaFromFields } from 'client/utils'
|
||||
|
||||
const {
|
||||
password: uiPassword,
|
||||
list: uiList,
|
||||
listMultiple: uiListMultiple,
|
||||
number: uiNumber,
|
||||
numberFloat: uiNumberFloat,
|
||||
range: uiRange,
|
||||
rangeFloat: uiRangeFloat,
|
||||
boolean: uiBoolean
|
||||
} = USER_INPUT_TYPES
|
||||
|
||||
const { array: _, ...userInputTypes } = USER_INPUT_TYPES
|
||||
|
||||
/** @type {UserInputType[]} User inputs types */
|
||||
const valuesOfUITypes = Object.values(userInputTypes)
|
||||
|
||||
/** @type {Field} Type field */
|
||||
const TYPE = {
|
||||
name: 'type',
|
||||
label: T.Type,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(valuesOfUITypes, {
|
||||
addEmpty: false,
|
||||
getText: type => sentenceCase(type)
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.oneOf(valuesOfUITypes)
|
||||
.default(() => valuesOfUITypes[0]),
|
||||
grid: { sm: 6, md: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Name field */
|
||||
const NAME = {
|
||||
name: 'name',
|
||||
label: T.Name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 6, md: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Description field */
|
||||
const DESCRIPTION = {
|
||||
name: 'description',
|
||||
label: T.Description,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 6, md: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Options field */
|
||||
const OPTIONS = {
|
||||
name: 'options',
|
||||
label: T.Options,
|
||||
tooltip: 'Press ENTER key to add a value',
|
||||
dependOf: TYPE.name,
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
multiple: true,
|
||||
htmlType: type =>
|
||||
![uiList, uiListMultiple].includes(type) && INPUT_TYPES.HIDDEN,
|
||||
validation: array(string().trim())
|
||||
.default(() => [])
|
||||
.when(
|
||||
TYPE.name,
|
||||
(type, schema) => [uiList, uiListMultiple].includes(type)
|
||||
? schema.required()
|
||||
: schema.strip().notRequired()
|
||||
),
|
||||
fieldProps: {
|
||||
freeSolo: true,
|
||||
placeholder: 'optA,optB,optC'
|
||||
},
|
||||
grid: { md: 8 }
|
||||
}
|
||||
|
||||
/** @type {{ MIN: Field, MAX: Field }} Range fields */
|
||||
const { MIN, MAX } = (() => {
|
||||
const validation = number()
|
||||
.positive()
|
||||
.default(() => undefined)
|
||||
.when(TYPE.name, (type, schema) => [uiRange, uiRangeFloat].includes(type)
|
||||
? schema.required()
|
||||
: schema.strip().notRequired()
|
||||
)
|
||||
|
||||
const common = {
|
||||
dependOf: TYPE.name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: type =>
|
||||
[uiRange, uiRangeFloat].includes(type) ? 'number' : INPUT_TYPES.HIDDEN,
|
||||
grid: { sm: 6, md: 4 },
|
||||
fieldProps: type => ({ step: type === uiRangeFloat ? 0.01 : 1 })
|
||||
}
|
||||
|
||||
return {
|
||||
MIN: { ...common, name: 'min', label: T.Min, validation: validation.lessThan(ref('max')) },
|
||||
MAX: { ...common, name: 'max', label: T.Max, validation: validation.moreThan(ref('min')) }
|
||||
}
|
||||
})()
|
||||
|
||||
/** @type {Field} Default value field */
|
||||
const DEFAULT_VALUE = {
|
||||
name: 'default',
|
||||
label: T.DefaultValue,
|
||||
dependOf: [TYPE.name, OPTIONS.name],
|
||||
type: ([type] = []) => [uiBoolean, uiList, uiListMultiple].includes(type)
|
||||
? INPUT_TYPES.SELECT
|
||||
: INPUT_TYPES.TEXT,
|
||||
htmlType: ([type] = []) => ({
|
||||
[uiNumber]: 'number',
|
||||
[uiNumberFloat]: 'number',
|
||||
[uiPassword]: INPUT_TYPES.HIDDEN
|
||||
}[type]),
|
||||
multiple: ([type] = []) => type === uiListMultiple,
|
||||
values: ([type, options = []] = []) => type === uiBoolean
|
||||
? arrayToOptions(['NO', 'YES'])
|
||||
: arrayToOptions(options),
|
||||
validation: string()
|
||||
.trim()
|
||||
.default(() => undefined)
|
||||
.when([TYPE.name, OPTIONS.name], (type, options = [], schema) => {
|
||||
return {
|
||||
[uiList]: schema.oneOf(options).notRequired(),
|
||||
[uiListMultiple]: schema.includesInOptions(options),
|
||||
[uiRange]: number().min(ref(MIN.name)).max(ref(MAX.name)).integer(),
|
||||
[uiRangeFloat]: number().min(ref(MIN.name)).max(ref(MAX.name)),
|
||||
[uiPassword]: schema.strip().notRequired()
|
||||
}[type] ?? schema
|
||||
}),
|
||||
grid: { sm: 6, md: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Mandatory field */
|
||||
const MANDATORY = {
|
||||
name: 'mandatory',
|
||||
label: T.Mandatory,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().default(() => false),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field[]} List of User Inputs fields */
|
||||
export const USER_INPUT_FIELDS = [
|
||||
TYPE,
|
||||
NAME,
|
||||
DESCRIPTION,
|
||||
DEFAULT_VALUE,
|
||||
OPTIONS,
|
||||
MIN,
|
||||
MAX,
|
||||
MANDATORY
|
||||
]
|
||||
|
||||
/** @type {ObjectSchema} User Input object schema */
|
||||
export const USER_INPUT_SCHEMA = getObjectSchemaFromFields(USER_INPUT_FIELDS)
|
||||
|
||||
/** @type {ObjectSchema} User Inputs schema */
|
||||
export const USER_INPUTS_SCHEMA = object({
|
||||
USER_INPUTS: array(USER_INPUT_SCHEMA).ensure()
|
||||
})
|
@ -0,0 +1,191 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { forwardRef, JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
WarningCircledOutline as WarningIcon,
|
||||
DeleteCircledOutline,
|
||||
AddCircledOutline
|
||||
} from 'iconoir-react'
|
||||
import {
|
||||
styled,
|
||||
FormControl,
|
||||
Stack,
|
||||
IconButton,
|
||||
Button,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from '@mui/material'
|
||||
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd'
|
||||
import { useFieldArray, useForm, FormProvider, useFormContext, get } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import { Tooltip } from 'client/components/FormControl'
|
||||
import { FormWithSchema, Legend } from 'client/components/Forms'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { USER_INPUT_SCHEMA, USER_INPUT_FIELDS } from './schema'
|
||||
import { getUserInputString } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const SECTION_ID = 'USER_INPUTS'
|
||||
|
||||
const UserItemDraggable = styled(ListItem)(({ theme }) => ({
|
||||
'&:before': {
|
||||
content: "''",
|
||||
display: 'block',
|
||||
width: 16,
|
||||
height: 10,
|
||||
background: `linear-gradient(
|
||||
to bottom,
|
||||
${theme.palette.action.active} 4px,
|
||||
transparent 4px,
|
||||
transparent 6px,
|
||||
${theme.palette.action.active} 6px
|
||||
)`
|
||||
}
|
||||
}))
|
||||
|
||||
const UserInputItem = forwardRef(({
|
||||
removeAction,
|
||||
error,
|
||||
userInput: { name, ...ui } = {},
|
||||
...props
|
||||
}, ref) => (
|
||||
<UserItemDraggable
|
||||
ref={ref}
|
||||
secondaryAction={
|
||||
<IconButton onClick={removeAction}>
|
||||
<DeleteCircledOutline />
|
||||
</IconButton>
|
||||
}
|
||||
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
|
||||
{...props}
|
||||
>
|
||||
{!!error && (
|
||||
<ListItemIcon sx={{ '& svg': { color: 'error.dark' } }}>
|
||||
<Tooltip title={error?.default.message}>
|
||||
<WarningIcon />
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<ListItemText
|
||||
inset={!error}
|
||||
primary={name}
|
||||
primaryTypographyProps={{ variant: 'body1' }}
|
||||
secondary={getUserInputString(ui)}
|
||||
/>
|
||||
</UserItemDraggable>
|
||||
))
|
||||
|
||||
UserInputItem.propTypes = {
|
||||
removeAction: PropTypes.func,
|
||||
error: PropTypes.object,
|
||||
userInput: PropTypes.object
|
||||
}
|
||||
|
||||
UserInputItem.displayName = 'UserInputItem'
|
||||
|
||||
/** @returns {JSXElementConstructor} - User Inputs section */
|
||||
const UserInputsSection = () => {
|
||||
const { formState: { errors } } = useFormContext()
|
||||
const { fields: userInputs, append, remove, move } = useFieldArray({
|
||||
name: `${EXTRA_ID}.${SECTION_ID}`
|
||||
})
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: USER_INPUT_SCHEMA.default(),
|
||||
resolver: yupResolver(USER_INPUT_SCHEMA)
|
||||
})
|
||||
|
||||
const onSubmit = newInput => {
|
||||
append(newInput)
|
||||
methods.reset()
|
||||
}
|
||||
|
||||
/** @param {DropResult} result - Drop result */
|
||||
const onDragEnd = result => {
|
||||
const { destination, source } = result ?? {}
|
||||
|
||||
if (destination && destination.index !== source.index) {
|
||||
move(source.index, destination.index)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl component='fieldset' sx={{ width: '100%' }}>
|
||||
<Legend title={T.UserInputs} tooltip={T.UserInputsConcept} />
|
||||
<FormProvider {...methods}>
|
||||
<Stack
|
||||
direction='row' alignItems='flex-start' gap='0.5rem'
|
||||
component='form'
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormWithSchema
|
||||
cy={`create-vm-template-${EXTRA_ID}.context-user-input`}
|
||||
fields={USER_INPUT_FIELDS}
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
/>
|
||||
<Button
|
||||
variant='contained'
|
||||
type='submit'
|
||||
color='secondary'
|
||||
startIcon={<AddCircledOutline />}
|
||||
sx={{ mt: '1em' }}
|
||||
>
|
||||
<Translate word={T.Add} />
|
||||
</Button>
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
<Divider />
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId='context'>
|
||||
{({ droppableProps, innerRef: outerRef, placeholder }) => (
|
||||
<List ref={outerRef} {...droppableProps}>
|
||||
{userInputs?.map(({ id, ...userInput }, index) => (
|
||||
<Draggable
|
||||
key={`ui[${index}]`}
|
||||
draggableId={`ui-${index}`}
|
||||
index={index}
|
||||
>
|
||||
{({ draggableProps, dragHandleProps, innerRef }) => (
|
||||
<UserInputItem
|
||||
key={id}
|
||||
ref={innerRef}
|
||||
userInput={userInput}
|
||||
error={get(errors, `${EXTRA_ID}.${SECTION_ID}.${index}`)}
|
||||
removeAction={() => remove(index)}
|
||||
{...draggableProps}
|
||||
{...dragHandleProps}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{placeholder}
|
||||
</List>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserInputsSection
|
@ -67,8 +67,9 @@ const InputsSection = ({ fields }) => {
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
/>
|
||||
<Button
|
||||
variant='outlined'
|
||||
variant='contained'
|
||||
type='submit'
|
||||
color='secondary'
|
||||
startIcon={<AddCircledOutline />}
|
||||
sx={{ mt: '1em' }}
|
||||
>
|
||||
|
@ -13,11 +13,11 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string, array, object, ObjectSchema, ArraySchema } from 'yup'
|
||||
import { string, array, ObjectSchema, ArraySchema } from 'yup'
|
||||
|
||||
import { useHost } from 'client/features/One'
|
||||
import { getPciDevices } from 'client/models/Host'
|
||||
import { Field, arrayToOptions, filterFieldsByHypervisor, getValidationFromFields } from 'client/utils'
|
||||
import { Field, arrayToOptions, filterFieldsByHypervisor, getObjectSchemaFromFields } from 'client/utils'
|
||||
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
|
||||
const { vcenter, lxc, firecracker } = HYPERVISORS
|
||||
@ -86,7 +86,7 @@ export const PCI_FIELDS = (hypervisor) =>
|
||||
filterFieldsByHypervisor([DEVICE_NAME, DEVICE, VENDOR, CLASS], hypervisor)
|
||||
|
||||
/** @type {ObjectSchema} PCI devices object schema */
|
||||
export const PCI_SCHEMA = object(getValidationFromFields([DEVICE, VENDOR, CLASS]))
|
||||
export const PCI_SCHEMA = getObjectSchemaFromFields([DEVICE, VENDOR, CLASS])
|
||||
|
||||
/** @type {ArraySchema} PCI devices schema */
|
||||
export const PCI_DEVICES_SCHEMA = array(PCI_SCHEMA).ensure()
|
||||
|
@ -72,8 +72,9 @@ const PciDevicesSection = ({ fields }) => {
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
/>
|
||||
<Button
|
||||
variant='outlined'
|
||||
variant='contained'
|
||||
type='submit'
|
||||
color='secondary'
|
||||
startIcon={<AddCircledOutline />}
|
||||
sx={{ mt: '1em' }}
|
||||
>
|
||||
@ -84,8 +85,8 @@ const PciDevicesSection = ({ fields }) => {
|
||||
<Divider />
|
||||
<List>
|
||||
{pciDevices?.map(({ id, DEVICE, VENDOR, CLASS }, index) => {
|
||||
const deviceName = pciDevicesAvailable
|
||||
.find(pciDevice => pciDevice?.DEVICE === DEVICE)?.DEVICE_NAME
|
||||
const { DEVICE_NAME, VENDOR_NAME } = pciDevicesAvailable
|
||||
.find(pciDevice => pciDevice?.DEVICE === DEVICE) ?? {}
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@ -98,11 +99,11 @@ const PciDevicesSection = ({ fields }) => {
|
||||
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={deviceName}
|
||||
primary={DEVICE_NAME}
|
||||
primaryTypographyProps={{ variant: 'body1' }}
|
||||
secondary={[
|
||||
`#${DEVICE}`,
|
||||
`Vendor: ${VENDOR}`,
|
||||
`Vendor: ${VENDOR_NAME}(${VENDOR})`,
|
||||
`Class: ${CLASS}`].join(' | ')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
@ -45,9 +45,9 @@ export const SCHED_ACTION_SCHEMA = array()
|
||||
export const SCHEMA = hypervisor => object({
|
||||
DISK: DISK_SCHEMA,
|
||||
NIC: NIC_SCHEMA,
|
||||
SCHED_ACTION: SCHED_ACTION_SCHEMA,
|
||||
USER_INPUTS: CONTEXT_SCHEMA
|
||||
SCHED_ACTION: SCHED_ACTION_SCHEMA
|
||||
})
|
||||
.concat(CONTEXT_SCHEMA)
|
||||
.concat(IO_SCHEMA(hypervisor))
|
||||
.concat(getObjectSchemaFromFields([
|
||||
...PLACEMENT_FIELDS,
|
||||
|
@ -17,7 +17,7 @@ import General, { STEP_ID as GENERAL_ID } from 'client/components/Forms/VmTempla
|
||||
import ExtraConfiguration, { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
// import { jsonToXml } from 'client/models/Helper'
|
||||
import { userInputsToArray, userInputsToObject } from 'client/models/Helper'
|
||||
import { createSteps } from 'client/utils'
|
||||
import { createSteps, isBase64 } from 'client/utils'
|
||||
|
||||
const Steps = createSteps(
|
||||
[General, ExtraConfiguration],
|
||||
@ -36,18 +36,29 @@ const Steps = createSteps(
|
||||
transformBeforeSubmit: formData => {
|
||||
const {
|
||||
[GENERAL_ID]: general = {},
|
||||
[EXTRA_ID]: { USER_INPUTS, ...extraTemplate } = {}
|
||||
[EXTRA_ID]: { USER_INPUTS, CONTEXT, ...extraTemplate } = {}
|
||||
} = formData ?? {}
|
||||
|
||||
// const templateXML = jsonToXml({ ...general, ...extraTemplate })
|
||||
// return { template: templateXML }
|
||||
|
||||
const { START_SCRIPT, ENCODE_START_SCRIPT, ...restOfContext } = CONTEXT
|
||||
|
||||
const context = {
|
||||
...restOfContext,
|
||||
[ENCODE_START_SCRIPT ? 'START_SCRIPT_BASE64' : 'START_SCRIPT']:
|
||||
ENCODE_START_SCRIPT && !isBase64(START_SCRIPT)
|
||||
? btoa(unescape(encodeURIComponent(START_SCRIPT)))
|
||||
: START_SCRIPT
|
||||
}
|
||||
|
||||
const userInputs = userInputsToObject(USER_INPUTS)
|
||||
const inputsOrder = USER_INPUTS.map(({ name }) => name).join(',')
|
||||
|
||||
return {
|
||||
...general,
|
||||
...extraTemplate,
|
||||
CONTEXT: context,
|
||||
USER_INPUTS: userInputs,
|
||||
INPUTS_ORDER: inputsOrder
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ const User = () => {
|
||||
color='secondary'
|
||||
href={`${APP_URL}/${appName}`}
|
||||
>
|
||||
<DevTypography label={appName} />
|
||||
<DevTypography>{appName}</DevTypography>
|
||||
</Link>
|
||||
</MenuItem>
|
||||
))
|
||||
|
@ -37,7 +37,7 @@ import { useGeneral } from 'client/features/General'
|
||||
import SidebarLink from 'client/components/Sidebar/SidebarLink'
|
||||
import sidebarStyles from 'client/components/Sidebar/styles'
|
||||
|
||||
const SidebarCollapseItem = ({ label, routes, icon: Icon }) => {
|
||||
const SidebarCollapseItem = ({ label = '', routes = [], icon: Icon }) => {
|
||||
const classes = sidebarStyles()
|
||||
const { pathname } = useLocation()
|
||||
const { isFixMenu } = useGeneral()
|
||||
@ -63,29 +63,26 @@ const SidebarCollapseItem = ({ label, routes, icon: Icon }) => {
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<ListItemText
|
||||
className={classes.itemText}
|
||||
data-max-label={label}
|
||||
data-cy={label}
|
||||
data-min-label={label.slice(0, 3)}
|
||||
primary={label}
|
||||
primaryTypographyProps={{ variant: 'body1' }}
|
||||
/>
|
||||
{expanded ? <CollapseIcon/> : <ExpandMoreIcon />}
|
||||
</ListItemButton>
|
||||
{routes
|
||||
?.filter(({ sidebar = false, label }) => sidebar && typeof label === 'string')
|
||||
?.map((subItem, index) => (
|
||||
<Collapse
|
||||
key={`subitem-${index}`}
|
||||
in={expanded}
|
||||
timeout='auto'
|
||||
unmountOnExit
|
||||
className={clsx({ [classes.subItemWrapper]: isUpLg && !isFixMenu })}
|
||||
>
|
||||
<List component='div'>
|
||||
<SidebarLink {...subItem} isSubItem />
|
||||
</List>
|
||||
</Collapse>
|
||||
))
|
||||
}
|
||||
<Collapse
|
||||
in={expanded}
|
||||
timeout='auto'
|
||||
unmountOnExit
|
||||
className={clsx({ [classes.subItemWrapper]: isUpLg && !isFixMenu })}
|
||||
>
|
||||
<List component='div' disablePadding>
|
||||
{routes
|
||||
?.filter(({ sidebar = false, label }) => sidebar && typeof label === 'string')
|
||||
?.map((subItem, index) => (
|
||||
<SidebarLink key={`subitem-${index}`} isSubItem {...subItem} />
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -107,10 +104,4 @@ SidebarCollapseItem.propTypes = {
|
||||
)
|
||||
}
|
||||
|
||||
SidebarCollapseItem.defaultProps = {
|
||||
label: '',
|
||||
icon: null,
|
||||
routes: []
|
||||
}
|
||||
|
||||
export default SidebarCollapseItem
|
||||
|
@ -14,6 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useHistory, useLocation } from 'react-router-dom'
|
||||
|
||||
@ -25,17 +26,19 @@ import {
|
||||
} from '@mui/material'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import sidebarStyles from 'client/components/Sidebar/styles'
|
||||
import { DevTypography } from 'client/components/Typography'
|
||||
|
||||
const STATIC_LABEL_PROPS = {
|
||||
'data-cy': 'main-menu-item-text',
|
||||
variant: 'body1'
|
||||
}
|
||||
|
||||
const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => {
|
||||
const classes = sidebarStyles()
|
||||
const isUpLg = useMediaQuery(theme => theme.breakpoints.up('lg'), { noSsr: true })
|
||||
const SidebarLink = memo(({
|
||||
label = '',
|
||||
path = '/',
|
||||
icon: Icon,
|
||||
devMode = false,
|
||||
isSubItem = false
|
||||
}) => {
|
||||
const isUpLg = useMediaQuery(
|
||||
theme => theme.breakpoints.up('lg'),
|
||||
{ noSsr: true }
|
||||
)
|
||||
|
||||
const history = useHistory()
|
||||
const { pathname } = useLocation()
|
||||
@ -48,10 +51,10 @@ const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => {
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
data-cy='main-menu-item'
|
||||
onClick={handleClick}
|
||||
selected={pathname === path}
|
||||
className={isSubItem && classes.subItem}
|
||||
data-cy='main-menu-item'
|
||||
{...(isSubItem && { sx: { pl: 4 } })}
|
||||
>
|
||||
{Icon && (
|
||||
<ListItemIcon>
|
||||
@ -59,41 +62,25 @@ const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => {
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<ListItemText
|
||||
disableTypography={devMode}
|
||||
primaryTypographyProps={STATIC_LABEL_PROPS}
|
||||
primary={
|
||||
devMode ? (
|
||||
<DevTypography label={label} labelProps={STATIC_LABEL_PROPS}/>
|
||||
) : label
|
||||
}
|
||||
primary={label}
|
||||
primaryTypographyProps={{
|
||||
...(devMode && { component: DevTypography }),
|
||||
'data-cy': 'main-menu-item-text',
|
||||
variant: 'body1'
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
SidebarLink.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
icon: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.node,
|
||||
PropTypes.func,
|
||||
PropTypes.string,
|
||||
PropTypes.symbol,
|
||||
PropTypes.object
|
||||
]),
|
||||
icon: PropTypes.any,
|
||||
devMode: PropTypes.bool,
|
||||
isSubItem: PropTypes.bool
|
||||
}
|
||||
|
||||
SidebarLink.defaultProps = {
|
||||
label: '',
|
||||
path: '/',
|
||||
icon: undefined,
|
||||
devMode: false,
|
||||
isSubItem: false
|
||||
}
|
||||
|
||||
SidebarLink.displayName = 'SidebarLink'
|
||||
|
||||
export default SidebarLink
|
||||
|
@ -66,7 +66,6 @@ const Sidebar = ({ endpoints }) => {
|
||||
return (
|
||||
<Drawer
|
||||
variant='permanent'
|
||||
className={clsx({ [classes.drawerFixed]: isFixMenu })}
|
||||
classes={{
|
||||
paper: clsx(classes.drawerPaper, {
|
||||
[classes.drawerFixed]: isFixMenu
|
||||
@ -81,7 +80,7 @@ const Sidebar = ({ endpoints }) => {
|
||||
width='100%'
|
||||
height={50}
|
||||
withText
|
||||
className={classes.svg}
|
||||
className={classes.logo}
|
||||
disabledBetaText
|
||||
/>
|
||||
{!isUpLg || isFixMenu ? (
|
||||
@ -96,7 +95,7 @@ const Sidebar = ({ endpoints }) => {
|
||||
</Box>
|
||||
<Divider />
|
||||
<Box className={classes.menu}>
|
||||
<List className={classes.list} data-cy='main-menu'>
|
||||
<List data-cy='main-menu'>
|
||||
{SidebarEndpoints}
|
||||
</List>
|
||||
</Box>
|
||||
|
@ -54,9 +54,6 @@ export default makeStyles(theme => ({
|
||||
},
|
||||
'& $subItemWrapper': {
|
||||
display: 'none'
|
||||
},
|
||||
'& $itemText::before': {
|
||||
content: 'attr(data-min-label)'
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,9 +75,6 @@ export default makeStyles(theme => ({
|
||||
},
|
||||
'& $subItemWrapper': {
|
||||
display: 'block !important'
|
||||
},
|
||||
'& $itemText::before': {
|
||||
content: 'attr(data-max-label) !important'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -104,7 +98,7 @@ export default makeStyles(theme => ({
|
||||
minHeight: toolbar.sm
|
||||
}
|
||||
},
|
||||
svg: {
|
||||
logo: {
|
||||
minWidth: 100
|
||||
},
|
||||
// -------------------------------
|
||||
@ -114,11 +108,7 @@ export default makeStyles(theme => ({
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
textTransform: 'capitalize',
|
||||
color: 'transparent',
|
||||
transition: 'color 0.3s',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.light
|
||||
},
|
||||
'&::-webkit-scrollbar': {
|
||||
width: 14
|
||||
},
|
||||
@ -130,19 +120,5 @@ export default makeStyles(theme => ({
|
||||
color: theme.palette.secondary.light
|
||||
}
|
||||
},
|
||||
list: {
|
||||
color: theme.palette.text.primary
|
||||
},
|
||||
itemText: {
|
||||
'&::before': {
|
||||
...theme.typography.body1,
|
||||
display: 'block',
|
||||
minWidth: 100,
|
||||
content: 'attr(data-max-label)'
|
||||
}
|
||||
},
|
||||
subItemWrapper: {},
|
||||
subItem: {
|
||||
paddingLeft: theme.spacing(4)
|
||||
}
|
||||
subItemWrapper: {}
|
||||
}))
|
||||
|
@ -50,7 +50,7 @@ const GlobalSelectedRows = ({ withAlert = false, useTableProps }) => {
|
||||
const allSelected = numberOfRowSelected === preFilteredRows.length
|
||||
|
||||
return withAlert ? (
|
||||
<MessageStyled icon={false} severity='debug' variant='outlined'>
|
||||
<MessageStyled icon={false} severity='info' variant='outlined'>
|
||||
<span>
|
||||
<Translate word={T.NumberOfResourcesSelected} values={numberOfRowSelected} />{'.'}
|
||||
</span>
|
||||
|
@ -42,9 +42,7 @@ const Accordion = styled(props => (
|
||||
'&:before': { display: 'none' }
|
||||
}))
|
||||
|
||||
const Summary = styled(props => (
|
||||
<AccordionSummary expandIcon={<ExpandIcon />} {...props} />
|
||||
))(({
|
||||
const Summary = styled(AccordionSummary)(({
|
||||
[`&.${accordionSummaryClasses.root}`]: {
|
||||
backgroundColor: 'rgba(0, 0, 0, .03)',
|
||||
flexDirection: 'row-reverse',
|
||||
@ -90,7 +88,8 @@ const NetworkItem = ({ nic = {}, actions }) => {
|
||||
const { detachNic } = useVmApi()
|
||||
const { handleRefetch, data: vm } = useContext(TabContext)
|
||||
|
||||
const { NIC_ID, NETWORK = '-', IP, MAC, PCI_ID, ALIAS, SECURITY_GROUPS } = nic
|
||||
const { NIC_ID, NETWORK = '-', IP, MAC, PCI_ID, ADDRESS, ALIAS, SECURITY_GROUPS } = nic
|
||||
const isPciDevice = PCI_ID !== undefined
|
||||
|
||||
const hasDetails = useMemo(
|
||||
() => !!ALIAS.length || !!SECURITY_GROUPS?.length,
|
||||
@ -117,7 +116,7 @@ const NetworkItem = ({ nic = {}, actions }) => {
|
||||
return (
|
||||
<>
|
||||
<Accordion>
|
||||
<Summary>
|
||||
<Summary {...(hasDetails && { expandIcon: <ExpandIcon /> })}>
|
||||
<Row>
|
||||
<Typography noWrap>
|
||||
{`${NIC_ID} | ${NETWORK}`}
|
||||
@ -126,10 +125,10 @@ const NetworkItem = ({ nic = {}, actions }) => {
|
||||
<MultipleTags
|
||||
clipboard
|
||||
limitTags={isMobile ? 1 : 3}
|
||||
tags={[IP, MAC, PCI_ID].filter(Boolean)}
|
||||
tags={[IP, MAC, ADDRESS].filter(Boolean)}
|
||||
/>
|
||||
</Labels>
|
||||
{!isMobile && detachAction(NIC_ID)}
|
||||
{!isMobile && !isPciDevice && detachAction(NIC_ID)}
|
||||
</Row>
|
||||
</Summary>
|
||||
{hasDetails && (
|
||||
@ -146,7 +145,7 @@ const NetworkItem = ({ nic = {}, actions }) => {
|
||||
tags={[IP, MAC, BRIDGE && `BRIDGE - ${BRIDGE}`].filter(Boolean)}
|
||||
/>
|
||||
</Labels>
|
||||
{!isMobile && detachAction(NIC_ID, true)}
|
||||
{!isMobile && !isPciDevice && detachAction(NIC_ID, true)}
|
||||
</Row>
|
||||
))}
|
||||
{!!SECURITY_GROUPS?.length && (
|
||||
|
@ -26,8 +26,8 @@ const NetworkList = ({ nics, actions }) => (
|
||||
paddingBlock: '0.8em'
|
||||
}}>
|
||||
{nics.map(nic => {
|
||||
const { IP, MAC } = nic
|
||||
const key = IP ?? MAC
|
||||
const { IP, MAC, ADDRESS } = nic
|
||||
const key = IP ?? MAC ?? ADDRESS // address only exists form PCI nics
|
||||
|
||||
return <NetworkItem key={key} actions={actions} nic={nic} />
|
||||
})}
|
||||
|
@ -18,7 +18,12 @@ import PropTypes from 'prop-types'
|
||||
|
||||
import { Typography, Chip, Box } from '@mui/material'
|
||||
|
||||
const DevTypography = memo(({ label, labelProps, color, chipProps }) => (
|
||||
const DevTypography = memo(({
|
||||
labelProps = {},
|
||||
color = 'secondary',
|
||||
chipProps = {},
|
||||
children = ''
|
||||
}) => (
|
||||
<Box
|
||||
component='span'
|
||||
display='inline-flex'
|
||||
@ -31,7 +36,7 @@ const DevTypography = memo(({ label, labelProps, color, chipProps }) => (
|
||||
sx={{ textTransform: 'capitalize' }}
|
||||
{...labelProps}
|
||||
>
|
||||
{label}
|
||||
{children}
|
||||
</Typography>
|
||||
<Chip
|
||||
size='small'
|
||||
@ -49,18 +54,8 @@ const DevTypography = memo(({ label, labelProps, color, chipProps }) => (
|
||||
DevTypography.propTypes = {
|
||||
chipProps: PropTypes.object,
|
||||
color: PropTypes.string,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string.isRequired
|
||||
]),
|
||||
labelProps: PropTypes.object
|
||||
}
|
||||
|
||||
DevTypography.defaultProps = {
|
||||
chipProps: undefined,
|
||||
color: 'secondary',
|
||||
label: '',
|
||||
labelProps: undefined
|
||||
labelProps: PropTypes.object,
|
||||
children: PropTypes.any
|
||||
}
|
||||
|
||||
DevTypography.displayName = 'DevTypography'
|
||||
|
@ -459,8 +459,8 @@ module.exports = {
|
||||
`Number of vCPU queues to use in the virtio-scsi controller.
|
||||
Leave blank to use the default value`,
|
||||
IoThreads: 'Iothreads',
|
||||
IoThreadsConcept:
|
||||
`Number of iothreads for virtio disks.
|
||||
IoThreadsConcept: `
|
||||
Number of iothreads for virtio disks.
|
||||
By default threads will be assign to disk by round robin algorithm.
|
||||
Disk thread id can be forced by disk IOTHREAD attribute`,
|
||||
RawData: 'Raw data',
|
||||
@ -470,6 +470,24 @@ module.exports = {
|
||||
By default, the data will be checked against the libvirt schema`,
|
||||
/* VM Template schema - context */
|
||||
Context: 'Context',
|
||||
SshPublicKey: 'SSH public key',
|
||||
AddUserSshPublicKey: 'Add user SSH public key',
|
||||
AddNetworkContextualization: 'Add Network contextualization',
|
||||
AddNetworkContextualizationConcept: `
|
||||
Add network contextualization parameters. For each NIC defined in
|
||||
the NETWORK section, ETH$i_IP, ETH$i_NETWORK... parameters will be
|
||||
included in the CONTEXT section and will be available in the Virtual Machine`,
|
||||
AddOneGateToken: 'Add OneGate token',
|
||||
AddOneGateTokenConcept: `
|
||||
Add a file (token.txt) to the context containing the token to push
|
||||
custom metrics to the Virtual Machine through OneGate`,
|
||||
ReportReadyToOneGate: 'Report Ready to OneGate',
|
||||
ReportReadyToOneGateConcept: 'Sends READY=YES to OneGate, useful for OneFlow',
|
||||
StartScript: 'Start script',
|
||||
StartScriptConcept: `
|
||||
Text of the script executed when the machine starts up. It can contain
|
||||
shebang in case it is not shell script`,
|
||||
EncodeScriptInBase64: 'Encode script in Base64',
|
||||
/* VM Template schema - Input/Output */
|
||||
InputOrOutput: 'Input / Output',
|
||||
Inputs: 'Inputs',
|
||||
|
@ -159,11 +159,13 @@ export const getNics = (vm, options = {}) => {
|
||||
const { NIC = [], NIC_ALIAS = [], PCI = [] } = TEMPLATE
|
||||
const { GUEST_IP, GUEST_IP_ADDRESSES = '' } = MONITORING
|
||||
|
||||
const pciNics = PCI.filter(({ NIC_ID } = {}) => NIC_ID !== undefined)
|
||||
|
||||
const extraIps = [GUEST_IP, ...GUEST_IP_ADDRESSES?.split(',')]
|
||||
.filter(Boolean)
|
||||
.map(ip => ({ NIC_ID: '-', IP: ip, NETWORK: 'Additional IP', BRIDGE: '-' }))
|
||||
|
||||
let nics = [NIC, NIC_ALIAS, PCI, extraIps].flat().filter(Boolean)
|
||||
let nics = [NIC, NIC_ALIAS, pciNics, extraIps].flat().filter(Boolean)
|
||||
|
||||
if (groupAlias) {
|
||||
nics = nics
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import DOMPurify from 'dompurify'
|
||||
import { object, reach, BaseSchema } from 'yup'
|
||||
import { object, reach, ObjectSchema, BaseSchema } from 'yup'
|
||||
import { HYPERVISORS } from 'client/constants'
|
||||
|
||||
/**
|
||||
@ -107,7 +107,7 @@ export const getValidationFromFields = fields =>
|
||||
* Returns fields in schema object.
|
||||
*
|
||||
* @param {{name: string, validation: BaseSchema}[]} fields - Fields
|
||||
* @returns {BaseSchema} Object schema
|
||||
* @returns {ObjectSchema} Object schema
|
||||
* @example
|
||||
* [{ name: 'VM.NAME', validation: string() }]
|
||||
* => object({ 'VM': object({ NAME: string() }) })
|
||||
|
@ -197,7 +197,10 @@ const parseUserInputValue = value => {
|
||||
return 'YES'
|
||||
} else if (value === false) {
|
||||
return 'NO'
|
||||
} else if (Array.isArray(value)) {
|
||||
} else if (
|
||||
Array.isArray(value) &&
|
||||
value.every(v => typeof v === 'string')
|
||||
) {
|
||||
return value.join(',')
|
||||
} else return value
|
||||
}
|
||||
@ -215,9 +218,9 @@ const parseUserInputValue = value => {
|
||||
*/
|
||||
export const schemaUserInput = ({ mandatory, name, type, options, default: defaultValue }) => {
|
||||
switch (type) {
|
||||
case [USER_INPUT_TYPES.text]:
|
||||
case [USER_INPUT_TYPES.text64]:
|
||||
case [USER_INPUT_TYPES.password]: return {
|
||||
case USER_INPUT_TYPES.text:
|
||||
case USER_INPUT_TYPES.text64:
|
||||
case USER_INPUT_TYPES.password: return {
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: type === 'password' ? 'password' : 'text',
|
||||
validation: string()
|
||||
@ -225,8 +228,8 @@ export const schemaUserInput = ({ mandatory, name, type, options, default: defau
|
||||
.concat(requiredSchema(mandatory, name, string()))
|
||||
.default(defaultValue || undefined)
|
||||
}
|
||||
case [USER_INPUT_TYPES.number]:
|
||||
case [USER_INPUT_TYPES.numberFloat]: return {
|
||||
case USER_INPUT_TYPES.number:
|
||||
case USER_INPUT_TYPES.numberFloat: return {
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: number()
|
||||
@ -235,8 +238,8 @@ export const schemaUserInput = ({ mandatory, name, type, options, default: defau
|
||||
.transform(value => !isNaN(value) ? value : null)
|
||||
.default(() => parseFloat(defaultValue) ?? undefined)
|
||||
}
|
||||
case [USER_INPUT_TYPES.range]:
|
||||
case [USER_INPUT_TYPES.rangeFloat]: {
|
||||
case USER_INPUT_TYPES.range:
|
||||
case USER_INPUT_TYPES.rangeFloat: {
|
||||
const [min, max] = getRange(options)
|
||||
|
||||
return {
|
||||
@ -251,13 +254,13 @@ export const schemaUserInput = ({ mandatory, name, type, options, default: defau
|
||||
fieldProps: { min, max, step: type === 'range-float' ? 0.01 : 1 }
|
||||
}
|
||||
}
|
||||
case [USER_INPUT_TYPES.boolean]: return {
|
||||
case USER_INPUT_TYPES.boolean: return {
|
||||
type: INPUT_TYPES.CHECKBOX,
|
||||
validation: boolean()
|
||||
.concat(requiredSchema(mandatory, name, boolean()))
|
||||
.default(defaultValue === 'YES' ?? false)
|
||||
}
|
||||
case [USER_INPUT_TYPES.list]: {
|
||||
case USER_INPUT_TYPES.list: {
|
||||
const values = getOptionsFromList(options)
|
||||
const firstOption = values?.[0]?.value ?? undefined
|
||||
|
||||
@ -271,7 +274,7 @@ export const schemaUserInput = ({ mandatory, name, type, options, default: defau
|
||||
.default(defaultValue ?? firstOption)
|
||||
}
|
||||
}
|
||||
case [USER_INPUT_TYPES.array]: {
|
||||
case USER_INPUT_TYPES.array: {
|
||||
const defaultValues = getValuesFromArray(defaultValue)
|
||||
|
||||
return {
|
||||
@ -283,7 +286,7 @@ export const schemaUserInput = ({ mandatory, name, type, options, default: defau
|
||||
fieldProps: { freeSolo: true }
|
||||
}
|
||||
}
|
||||
case [USER_INPUT_TYPES.listMultiple]: {
|
||||
case USER_INPUT_TYPES.listMultiple: {
|
||||
const values = getOptionsFromList(options)
|
||||
const defaultValues = defaultValue?.split(',') ?? undefined
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user