1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-01-26 10:03:37 +03:00

F #5422: Add form to create & update VM Templates (#1583)

This commit is contained in:
Sergio Betanzos 2021-11-18 12:41:00 +01:00 committed by GitHub
parent 00a61d74a5
commit ab86d01b9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 279 additions and 341 deletions

View File

@ -20,7 +20,7 @@ import { Button, MobileStepper, Typography, Box, alpha } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { NavArrowLeft as PreviousIcon, NavArrowRight as NextIcon } from 'iconoir-react'
import { Tr, Translate, labelCanBeTranslated } from 'client/components/HOC'
import { Translate, labelCanBeTranslated } from 'client/components/HOC'
import { T } from 'client/constants'
const useStyles = makeStyles(theme => ({
@ -61,8 +61,8 @@ const CustomMobileStepper = ({
</Typography>
{Boolean(errors[id]) && (
<Typography className={classes.error} variant='caption' color='error'>
{labelCanBeTranslated(errors[id]?.message)
? Tr(errors[id]?.message) : errors[id]?.message}
{labelCanBeTranslated(label)
? <Translate word={errors[id]?.message} /> : errors[id]?.message}
</Typography>
)}
</Box>

View File

@ -114,7 +114,7 @@ const FormStepper = ({ steps = [], schema, onSubmit }) => {
if (activeStep === lastStep) {
const submitData = { ...formData, [id]: data }
const schemaData = schema().cast(submitData, { context: submitData })
const schemaData = schema().cast(submitData, { context: submitData, isSubmit: true })
onSubmit(schemaData)
} else {
setFormData(prev => ({ ...prev, [id]: data }))

View File

@ -13,45 +13,37 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import * as yup from 'yup'
import { boolean, string, object, ObjectSchema } from 'yup'
import { getValidationFromFields } from 'client/utils'
import { INPUT_TYPES } from 'client/constants'
import { Field, getValidationFromFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
/** @type {Field} RDP connection field */
const RDP_FIELD = {
name: 'RDP',
label: 'RDP connection',
label: T.RdpConnection,
type: INPUT_TYPES.SWITCH,
validation: yup
.boolean()
.transform(value => {
if (typeof value === 'boolean') return value
return String(value).toUpperCase() === 'YES'
})
.default(false),
validation: boolean().yesOrNo(),
grid: { md: 12 }
}
/** @type {Field} SSH connection field */
const SSH_FIELD = {
name: 'SSH',
label: 'SSH connection',
label: T.SshConnection,
type: INPUT_TYPES.SWITCH,
validation: yup
.boolean()
.transform(value => {
if (typeof value === 'boolean') return value
return String(value).toUpperCase() === 'YES'
})
.default(false),
validation: boolean().yesOrNo(),
grid: { md: 12 }
}
const ALIAS_FIELD = ({ nics = [] } = {}) => ({
/**
* @param {object} currentFormData - Current form data
* @param {object[]} currentFormData.nics - Nics
* @returns {Field} Alias field
*/
const ALIAS_FIELD = ({ nics = [] }) => ({
name: 'PARENT',
label: 'Attach as an alias',
label: T.AsAnAlias,
dependOf: 'NAME',
type: name => {
const hasAlias = nics?.some(nic => nic.PARENT === name)
@ -70,35 +62,33 @@ const ALIAS_FIELD = ({ nics = [] } = {}) => ({
return { text, value: NAME }
})
],
validation: yup
.string()
validation: string()
.trim()
.notRequired()
.default(undefined)
.default(() => undefined)
})
/** @type {Field} External field */
const EXTERNAL_FIELD = {
name: 'EXTERNAL',
label: 'External',
label: T.External,
tooltip: T.ExternalConcept,
type: INPUT_TYPES.SWITCH,
tooltip: 'The NIC will be attached as an external alias of the VM',
dependOf: ALIAS_FIELD().name,
htmlType: type => !type?.length ? INPUT_TYPES.HIDDEN : undefined,
validation: yup
.boolean()
.transform(value => {
if (typeof value === 'boolean') return value
return String(value).toUpperCase() === 'YES'
})
.default(false)
dependOf: 'PARENT',
htmlType: parent => !parent?.length && INPUT_TYPES.HIDDEN,
validation: boolean().yesOrNo()
}
export const FIELDS = props => [
/**
* @param {object} [currentFormData] - Current form data
* @returns {Field[]} List of Graphics fields
*/
export const FIELDS = (currentFormData = {}) => [
RDP_FIELD,
SSH_FIELD,
ALIAS_FIELD(props),
ALIAS_FIELD(currentFormData),
EXTERNAL_FIELD
]
export const SCHEMA = yup.object(getValidationFromFields(FIELDS()))
/** @type {ObjectSchema} Advanced options schema */
export const SCHEMA = object(getValidationFromFields(FIELDS()))

View File

@ -52,7 +52,7 @@ const Content = ({ data, setFormData }) => {
const NetworkStep = () => ({
id: STEP_ID,
label: T.Network,
label: T.SelectNetwork,
resolver: SCHEMA,
content: Content
})

View File

@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import * as yup from 'yup'
import { array, object, ArraySchema } from 'yup'
export const SCHEMA = yup
.array(yup.object())
.min(1, 'Select network')
.max(1, 'Max. one network selected')
.required('Network field is required')
/** @type {ArraySchema} Virtual Network table schema */
export const SCHEMA = array(object())
.min(1)
.max(1)
.required()
.ensure()
.default(() => [])

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import NetworksTable, { STEP_ID as NETWORK_ID } from 'client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable'
import AdvancedOptions, { STEP_ID as ADVANCED_ID } from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions'
import { mapUserInputs, createSteps } from 'client/utils'
import { createSteps } from 'client/utils'
const Steps = createSteps(
[NetworksTable, AdvancedOptions],
@ -52,7 +52,7 @@ const Steps = createSteps(
NETWORK_UID: UID,
NETWORK_UNAME: UNAME,
SECURITY_GROUPS,
...mapUserInputs(advanced)
...advanced
}
}
}

View File

@ -178,7 +178,7 @@ export const FIRMWARE = {
grid: { md: 12 }
}
/** @type {Field} Feature secure field */
/** @type {Field} Firmware secure field */
export const FIRMWARE_SECURE = {
name: 'OS.FIRMWARE_SECURE',
label: T.FirmwareSecure,
@ -186,13 +186,7 @@ export const FIRMWARE_SECURE = {
type: INPUT_TYPES.CHECKBOX,
dependOf: FEATURE_CUSTOM_ENABLED.name,
htmlType: custom => !custom && INPUT_TYPES.HIDDEN,
validation: boolean()
.default(() => false)
.transform(value => {
if (typeof value === 'boolean') return value
return String(value).toUpperCase() === 'YES'
}),
validation: boolean().yesOrNo(),
grid: { md: 12 }
}

View File

@ -54,13 +54,7 @@ const VALIDATE = {
tooltip: T.RawValidateConcept,
type: INPUT_TYPES.CHECKBOX,
notOnHypervisors: [lxc, vcenter, firecracker],
validation: boolean()
.transform(value => {
if (typeof value === 'boolean') return value
return String(value).toUpperCase() === 'YES'
})
.default(() => false),
validation: boolean().yesOrNo(),
grid: { md: 12 }
}

View File

@ -20,13 +20,7 @@ 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'
}),
validation: boolean().yesOrNo(),
grid: { md: 12 }
}

View File

@ -15,6 +15,7 @@
* ------------------------------------------------------------------------- */
import { object, array, string, boolean, number, ref, ObjectSchema } from 'yup'
import { userInputsToObject } from 'client/models/Helper'
import { UserInputType, T, INPUT_TYPES, USER_INPUT_TYPES } from 'client/constants'
import { Field, arrayToOptions, sentenceCase, getObjectSchemaFromFields } from 'client/utils'
@ -183,6 +184,13 @@ 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(),
INPUTS_ORDER: string().trim().strip()
USER_INPUTS: array(USER_INPUT_SCHEMA)
.ensure()
.afterSubmit(userInputs => userInputsToObject(userInputs)),
INPUTS_ORDER: string()
.trim()
.afterSubmit((_, { context }) => {
const userInputs = context?.extra?.USER_INPUTS
return userInputs?.map(({ name }) => name).join(',')
})
})

View File

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string, boolean } from 'yup'
import { string, boolean, ObjectSchema } from 'yup'
import { Field, arrayToOptions, filterFieldsByHypervisor } from 'client/utils'
import { Field, arrayToOptions, filterFieldsByHypervisor, getObjectSchemaFromFields } from 'client/utils'
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
const { vcenter, lxc, kvm } = HYPERVISORS
@ -90,13 +90,7 @@ const RANDOM_PASSWD = {
type: INPUT_TYPES.CHECKBOX,
dependOf: TYPE.name,
htmlType: noneType => !noneType && INPUT_TYPES.HIDDEN,
validation: boolean()
.default(() => false)
.transform(value => {
if (typeof value === 'boolean') return value
return String(value).toUpperCase() === 'YES'
}),
validation: boolean().yesOrNo(),
grid: { md: 12 }
}
@ -139,3 +133,7 @@ export const GRAPHICS_FIELDS = hypervisor =>
[TYPE, LISTEN, PORT, KEYMAP, PASSWD, RANDOM_PASSWD, COMMAND],
hypervisor
)
/** @type {ObjectSchema} Context Files schema */
export const GRAPHICS_SCHEMA = hypervisor =>
getObjectSchemaFromFields(GRAPHICS_FIELDS(hypervisor))

View File

@ -23,7 +23,7 @@ import { FormWithSchema } from 'client/components/Forms'
import { STEP_ID as EXTRA_ID, TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
import InputsSection, { SECTION_ID as INPUT_ID } from './inputsSection'
import PciDevicesSection, { SECTION_ID as PCI_ID } from './pciDevicesSection'
import { INPUT_OUTPUT_FIELDS, INPUTS_FIELDS, PCI_FIELDS } from './schema'
import { GRAPHICS_FIELDS, INPUTS_FIELDS, PCI_FIELDS } from './schema'
import { T } from 'client/constants'
export const TAB_ID = ['GRAPHICS', INPUT_ID, PCI_ID]
@ -40,7 +40,7 @@ const InputOutput = ({ hypervisor }) => {
>
<FormWithSchema
cy={`create-vm-template-${EXTRA_ID}.io-graphics`}
fields={INPUT_OUTPUT_FIELDS(hypervisor)}
fields={GRAPHICS_FIELDS(hypervisor)}
legend={T.Graphics}
id={EXTRA_ID}
/>

View File

@ -13,7 +13,7 @@
* 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, object, ObjectSchema } from 'yup'
import { PcMouse, PenTablet, Usb, PlugTypeG } from 'iconoir-react'
import { Field, arrayToOptions, filterFieldsByHypervisor, getValidationFromFields } from 'client/utils'
@ -69,5 +69,7 @@ export const INPUTS_FIELDS = (hypervisor) =>
/** @type {ObjectSchema} Graphic input object schema */
export const INPUT_SCHEMA = object(getValidationFromFields([TYPE, BUS]))
/** @type {ArraySchema} Graphic inputs schema */
export const INPUTS_SCHEMA = array(INPUT_SCHEMA).ensure()
/** @type {ObjectSchema} Graphic inputs schema */
export const INPUTS_SCHEMA = object({
INPUT: array(INPUT_SCHEMA).ensure()
})

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string, array, ObjectSchema, ArraySchema } from 'yup'
import { object, string, array, ObjectSchema } from 'yup'
import { useHost } from 'client/features/One'
import { getPciDevices } from 'client/models/Host'
@ -88,5 +88,7 @@ export const PCI_FIELDS = (hypervisor) =>
/** @type {ObjectSchema} PCI devices object schema */
export const PCI_SCHEMA = getObjectSchemaFromFields([DEVICE, VENDOR, CLASS])
/** @type {ArraySchema} PCI devices schema */
export const PCI_DEVICES_SCHEMA = array(PCI_SCHEMA).ensure()
/** @type {ObjectSchema} PCI devices schema */
export const PCI_DEVICES_SCHEMA = object({
PCI: array(PCI_SCHEMA).ensure()
})

View File

@ -15,28 +15,18 @@
* ------------------------------------------------------------------------- */
import { object, ObjectSchema } from 'yup'
import { GRAPHICS_FIELDS } from './graphicsSchema'
import { GRAPHICS_SCHEMA } from './graphicsSchema'
import { INPUTS_SCHEMA } from './inputsSchema'
import { PCI_DEVICES_SCHEMA } from './pciDevicesSchema'
import { Field, getObjectSchemaFromFields } from 'client/utils'
/**
* @param {string} [hypervisor] - VM hypervisor
* @returns {Field[]} List of I/O fields
*/
export const INPUT_OUTPUT_FIELDS = hypervisor =>
[...GRAPHICS_FIELDS(hypervisor)]
/**
* @param {string} [hypervisor] - VM hypervisor
* @returns {ObjectSchema} I/O schema
*/
export const SCHEMA = hypervisor => object({
INPUT: INPUTS_SCHEMA,
PCI: PCI_DEVICES_SCHEMA
}).concat(getObjectSchemaFromFields([
...GRAPHICS_FIELDS(hypervisor)
]))
export const SCHEMA = hypervisor => object()
.concat(INPUTS_SCHEMA)
.concat(PCI_DEVICES_SCHEMA)
.concat(GRAPHICS_SCHEMA(hypervisor))
export * from './graphicsSchema'
export * from './inputsSchema'

View File

@ -43,13 +43,7 @@ export const ENABLE_HR_MEMORY = {
name: 'HOT_RESIZE.MEMORY_HOT_ADD_ENABLED',
label: T.EnableHotResize,
type: INPUT_TYPES.SWITCH,
validation: boolean()
.transform(value => {
if (typeof value === 'boolean') return value
return String(value).toUpperCase() === 'YES'
})
.default(() => false),
validation: boolean().yesOrNo(),
grid: { xs: 4, md: 6 }
}
@ -94,13 +88,7 @@ export const ENABLE_HR_VCPU = {
name: 'HOT_RESIZE.CPU_HOT_ADD_ENABLED',
label: T.EnableHotResize,
type: INPUT_TYPES.SWITCH,
validation: boolean()
.transform(value => {
if (typeof value === 'boolean') return value
return String(value).toUpperCase() === 'YES'
})
.default(() => false),
validation: boolean().yesOrNo(),
grid: { xs: 4, md: 6 }
}

View File

@ -15,6 +15,7 @@
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { useWatch } from 'react-hook-form'
import { useAuth } from 'client/features/Auth'
@ -28,7 +29,7 @@ import { T } from 'client/constants'
export const STEP_ID = 'general'
const Content = () => {
const Content = ({ isUpdate }) => {
const classes = useStyles()
const { view, getResourceView } = useAuth()
const hypervisor = useWatch({ name: `${STEP_ID}.HYPERVISOR` })
@ -37,7 +38,7 @@ const Content = () => {
const dialog = getResourceView('VM-TEMPLATE')?.dialogs?.create_dialog
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
return SECTIONS(hypervisor)
return SECTIONS(hypervisor, isUpdate)
.filter(({ id, required }) => required || sectionsAvailable.includes(id))
}, [view, hypervisor])
@ -62,15 +63,23 @@ const Content = () => {
)
}
const General = () => ({
id: STEP_ID,
label: T.General,
resolver: formData => {
const hypervisor = formData?.[STEP_ID]?.HYPERVISOR
return SCHEMA(hypervisor)
},
optionsValidate: { abortEarly: false },
content: Content
})
const General = initialValues => {
const isUpdate = initialValues?.NAME
return {
id: STEP_ID,
label: T.General,
resolver: formData => {
const hypervisor = formData?.[STEP_ID]?.HYPERVISOR
return SCHEMA(hypervisor, isUpdate)
},
optionsValidate: { abortEarly: false },
content: () => Content({ isUpdate })
}
}
Content.propTypes = {
isUpdate: PropTypes.bool
}
export default General

View File

@ -19,8 +19,11 @@ import Image from 'client/components/Image'
import { T, LOGO_IMAGES_URL, INPUT_TYPES, HYPERVISORS } from 'client/constants'
import { Field, arrayToOptions } from 'client/utils'
/** @type {Field} Name field */
export const NAME = {
/**
* @param {boolean} isUpdate - If `true`, the form is being updated
* @returns {Field} Name field
*/
export const NAME = isUpdate => ({
name: 'NAME',
label: T.Name,
type: INPUT_TYPES.TEXT,
@ -28,8 +31,9 @@ export const NAME = {
.trim()
.required()
.default(() => undefined),
grid: { sm: 6 }
}
grid: { sm: 6 },
...(isUpdate && { fieldProps: { disabled: true } })
})
/** @type {Field} Description field */
export const DESCRIPTION = {
@ -100,9 +104,12 @@ export const LOGO = {
.default(() => undefined)
}
/** @type {Field[]} List of information fields */
export const FIELDS = [
NAME,
/**
* @param {boolean} isUpdate - If `true`, the form is being updated
* @returns {Field[]} List of information fields
*/
export const FIELDS = isUpdate => [
NAME(isUpdate),
DESCRIPTION,
LOGO
]

View File

@ -26,14 +26,15 @@ import { T, HYPERVISORS } from 'client/constants'
/**
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
* @param {boolean} [isUpdate] - If `true`, the form is being updated
* @returns {Section[]} Fields
*/
const SECTIONS = hypervisor => [
const SECTIONS = (hypervisor, isUpdate) => [
{
id: 'information',
legend: T.Information,
required: true,
fields: filterFieldsByHypervisor(INFORMATION_FIELDS, hypervisor)
fields: filterFieldsByHypervisor(INFORMATION_FIELDS(isUpdate), hypervisor)
},
{
id: 'capacity',

View File

@ -15,37 +15,43 @@
* ------------------------------------------------------------------------- */
import General, { STEP_ID as GENERAL_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General'
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 { jsonToXml, userInputsToArray } from 'client/models/Helper'
import { createSteps, isBase64 } from 'client/utils'
const Steps = createSteps(
[General, ExtraConfiguration],
{
transformInitialValue: (vmTemplate, schema) => ({
...schema.pick([GENERAL_ID]).cast({
[GENERAL_ID]: { ...vmTemplate, ...vmTemplate?.TEMPLATE }
}, { stripUnknown: true }),
...schema.pick([EXTRA_ID]).cast({
[EXTRA_ID]: {
...vmTemplate?.TEMPLATE,
USER_INPUTS: userInputsToArray(vmTemplate?.TEMPLATE?.USER_INPUTS)
}
}, { stripUnknown: true, context: { [EXTRA_ID]: vmTemplate.TEMPLATE } })
}),
transformInitialValue: (vmTemplate, schema) => {
const generalStep = schema
.pick([GENERAL_ID])
.cast(
{ [GENERAL_ID]: { ...vmTemplate, ...vmTemplate?.TEMPLATE } },
{ stripUnknown: true }
)
const inputsOrder = vmTemplate?.TEMPLATE?.INPUTS_ORDER?.split(',') ?? []
const userInputs = userInputsToArray(vmTemplate?.TEMPLATE?.USER_INPUTS)
.sort((a, b) => inputsOrder.indexOf(a.name) - inputsOrder.indexOf(b.name))
const configurationStep = schema
.pick([EXTRA_ID])
.cast(
{ [EXTRA_ID]: { ...vmTemplate?.TEMPLATE, USER_INPUTS: userInputs } },
{ stripUnknown: true, context: { [EXTRA_ID]: vmTemplate.TEMPLATE } }
)
return { ...generalStep, ...configurationStep }
},
transformBeforeSubmit: formData => {
const {
[GENERAL_ID]: general = {},
[EXTRA_ID]: { USER_INPUTS, CONTEXT, ...extraTemplate } = {}
[EXTRA_ID]: {
USER_INPUTS,
CONTEXT: { START_SCRIPT, ENCODE_START_SCRIPT, ...restOfContext },
...extraTemplate
} = {}
} = formData ?? {}
// const templateXML = jsonToXml({ ...general, ...extraTemplate })
// return { template: templateXML }
const userInputs = userInputsToObject(USER_INPUTS)
const inputsOrder = USER_INPUTS.map(({ name }) => name).join(',')
const { START_SCRIPT, ENCODE_START_SCRIPT, ...restOfContext } = CONTEXT
const context = {
...restOfContext,
// transform start script to base64 if needed
@ -56,18 +62,17 @@ const Steps = createSteps(
}
// add user inputs to context
for (const { name } of USER_INPUTS) {
const userInputsNames = Object.keys(USER_INPUTS).forEach(name => {
const upperName = String(name).toUpperCase()
context[upperName] = `$${upperName}`
}
})
return {
return jsonToXml({
...extraTemplate,
...general,
CONTEXT: context,
USER_INPUTS: userInputs,
INPUTS_ORDER: inputsOrder
}
USER_INPUTS: USER_INPUTS
})
}
}
)

View File

@ -126,7 +126,10 @@ TranslateProvider.propTypes = {
}
Translate.propTypes = {
word: PropTypes.string,
word: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array
]),
values: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,

View File

@ -102,6 +102,7 @@ module.exports = {
Select: 'Select',
SelectGroup: 'Select a group',
SelectHost: 'Select a host',
SelectNetwork: 'Select a network',
SelectRequest: 'Select request',
SelectVmTemplate: 'Select a VM Template',
Share: 'Share',
@ -212,6 +213,7 @@ module.exports = {
System: 'System',
Language: 'Language',
DisableDashboardAnimations: 'Disable dashboard animations',
ConfigurationUI: 'Configuration UI',
/* sections - system */
User: 'User',
@ -346,6 +348,11 @@ module.exports = {
/* VM schema - network */
NIC: 'NIC',
Alias: 'Alias',
AsAnAlias: 'Attach as an alias',
RdpConnection: 'RDP connection',
SshConnection: 'SSH connection',
External: 'External',
ExternalConcept: 'The NIC will be attached as an external alias of the VM',
/* VM Template schema */
/* VM schema - general */

View File

@ -13,17 +13,17 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useEffect } from 'react'
import { useEffect, JSXElementConstructor } from 'react'
import { Container, Box, Grid } from '@mui/material'
import { useAuth } from 'client/features/Auth'
import { useFetchAll } from 'client/hooks'
import { useProvisionApi, useProviderApi } from 'client/features/One'
import * as Widgets from 'client/components/Widgets'
import dashboardStyles from 'client/containers/Dashboard/Provision/styles'
import { stringToBoolean } from 'client/models/Helper'
function Dashboard () {
/** @returns {JSXElementConstructor} Provision dashboard container */
function ProvisionDashboard () {
const { status, fetchRequestAll, STATUS } = useFetchAll()
const { INIT, PENDING } = STATUS
@ -31,9 +31,6 @@ function Dashboard () {
const { getProviders } = useProviderApi()
const { settings: { disableanimations } = {} } = useAuth()
const classes = dashboardStyles({ disableanimations })
const withoutAnimations = String(disableanimations).toUpperCase() === 'YES'
useEffect(() => {
fetchRequestAll([
@ -45,8 +42,12 @@ function Dashboard () {
return (
<Container
disableGutters
{...withoutAnimations && {
className: classes.withoutAnimations
{...stringToBoolean(disableanimations) && {
sx: {
'& *, & *::before, & *::after': {
animation: 'none !important'
}
}
}}
>
<Box py={3}>
@ -72,4 +73,4 @@ function Dashboard () {
)
}
export default Dashboard
export default ProvisionDashboard

View File

@ -1,24 +0,0 @@
/* ------------------------------------------------------------------------- *
* 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 makeStyles from '@mui/styles/makeStyles'
export default makeStyles({
withoutAnimations: {
'& *, & *::before, & *::after': {
animation: 'none !important'
}
}
})

View File

@ -13,18 +13,17 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useEffect } from 'react'
import { useEffect, JSXElementConstructor } from 'react'
import { Container, Box, Grid } from '@mui/material'
import { useAuth } from 'client/features/Auth'
import { useFetchAll } from 'client/hooks'
import { useUserApi, useImageApi, useVNetworkApi, useDatastoreApi } from 'client/features/One'
import * as Widgets from 'client/components/Widgets'
import dashboardStyles from 'client/containers/Dashboard/Provision/styles'
import { stringToBoolean } from 'client/models/Helper'
function Dashboard () {
/** @returns {JSXElementConstructor} Sunstone dashboard container */
function SunstoneDashboard () {
const { status, fetchRequestAll, STATUS } = useFetchAll()
const { INIT, PENDING } = STATUS
@ -34,9 +33,6 @@ function Dashboard () {
const { getDatastores } = useDatastoreApi()
const { settings: { disableanimations } = {} } = useAuth()
const classes = dashboardStyles({ disableanimations })
const withoutAnimations = String(disableanimations).toUpperCase() === 'YES'
useEffect(() => {
fetchRequestAll([
@ -50,8 +46,12 @@ function Dashboard () {
return (
<Container
disableGutters
{...withoutAnimations && {
className: classes.withoutAnimations
{...stringToBoolean(disableanimations) && {
sx: {
'& *, & *::before, & *::after': {
animation: 'none !important'
}
}
}}
>
<Box py={3}>
@ -67,4 +67,4 @@ function Dashboard () {
)
}
export default Dashboard
export default SunstoneDashboard

View File

@ -1,24 +0,0 @@
/* ------------------------------------------------------------------------- *
* 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 makeStyles from '@mui/styles/makeStyles'
export default makeStyles({
withoutAnimations: {
'& *, & *::before, & *::after': {
animation: 'none !important'
}
}
})

View File

@ -13,12 +13,8 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useEffect } from 'react'
import { Container, Paper, Box, Typography } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { JSXElementConstructor } from 'react'
import { Container, Paper, Box, Typography, Divider } from '@mui/material'
import { useForm, FormProvider } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
@ -28,38 +24,14 @@ import SubmitButton from 'client/components/FormControl/SubmitButton'
import { useAuth, useAuthApi } from 'client/features/Auth'
import { useUserApi } from 'client/features/One'
import { useGeneralApi } from 'client/features/General'
import { Tr } from 'client/components/HOC'
import { Translate, Tr } from 'client/components/HOC'
import { T } from 'client/constants'
import { FORM_FIELDS, FORM_SCHEMA } from 'client/containers/Settings/schema'
import { mapUserInputs } from 'client/utils'
import * as Helper from 'client/models/Helper'
const useStyles = makeStyles(theme => ({
header: {
paddingTop: '1rem'
},
title: {
flexGrow: 1,
letterSpacing: 0.1,
fontWeight: 500
},
wrapper: {
backgroundColor: theme.palette.background.default,
maxWidth: 550,
padding: '1rem'
},
subheader: {
marginBottom: '1rem'
},
actions: {
padding: '1rem 0',
textAlign: 'end'
}
}))
/** @returns {JSXElementConstructor} Settings container */
const Settings = () => {
const classes = useStyles()
const { user, settings } = useAuth()
const { getAuthUser } = useAuthApi()
const { updateUser } = useUserApi()
@ -67,47 +39,43 @@ const Settings = () => {
const { handleSubmit, setError, reset, formState, ...methods } = useForm({
reValidateMode: 'onSubmit',
defaultValues: settings,
defaultValues: FORM_SCHEMA.cast(settings),
resolver: yupResolver(FORM_SCHEMA)
})
useEffect(() => {
reset(
FORM_SCHEMA.cast(settings),
{ keepIsSubmitted: false, keepErrors: false }
)
}, [settings])
const onSubmit = async dataForm => {
try {
const inputs = mapUserInputs(dataForm)
const template = Helper.jsonToXml({ FIREEDGE: inputs })
const template = Helper.jsonToXml({ FIREEDGE: dataForm })
await updateUser(user.ID, { template }).then(getAuthUser)
} catch {
enqueueError(Tr(T.SomethingWrong))
enqueueError(T.SomethingWrong)
}
}
return (
<Container disableGutters>
<div className={classes.header}>
<Typography variant='h5' className={classes.title}>
{Tr(T.Settings)}
</Typography>
</div>
<Typography variant='h5' pt='1em'>
<Translate word={T.Settings} />
</Typography>
<hr />
<Divider sx={{ my: '1em' }} />
<Paper className={classes.wrapper} variant='outlined'>
<Typography variant='overline' component='div' className={classes.subheader}>
{`${Tr(T.Configuration)} UI`}
</Typography>
<Paper
variant='outlined'
sx={{
p: '1em',
maxWidth: { sm: 'auto', md: 550 }
}}
>
<Box component="form" onSubmit={handleSubmit(onSubmit)}>
<FormProvider {...methods}>
<FormWithSchema cy='settings' fields={FORM_FIELDS} />
<FormWithSchema
cy='settings'
fields={FORM_FIELDS}
legend={T.ConfigurationUI}
/>
</FormProvider>
<div className={classes.actions}>
<Box py='1em' textAlign='end'>
<SubmitButton
color='secondary'
data-cy='settings-submit-button'
@ -116,7 +84,7 @@ const Settings = () => {
disabled={!formState.isDirty}
isSubmitting={formState.isSubmitting}
/>
</div>
</Box>
</Box>
</Paper>
</Container>

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import * as yup from 'yup'
import { object, boolean, string } from 'yup'
import { T, INPUT_TYPES, SCHEMES, DEFAULT_SCHEME, DEFAULT_LANGUAGE } from 'client/constants'
import { getValidationFromFields } from 'client/utils'
@ -26,11 +26,10 @@ const SCHEME = {
{ text: T.Dark, value: SCHEMES.DARK },
{ text: T.Light, value: SCHEMES.LIGHT }
],
validation: yup
.string()
validation: string()
.trim()
.required('Scheme field is required')
.default(DEFAULT_SCHEME),
.required()
.default(() => DEFAULT_SCHEME),
grid: { md: 12 }
}
@ -40,11 +39,10 @@ const LANGUAGES = {
type: INPUT_TYPES.SELECT,
values: () =>
window?.langs?.map(({ key, value }) => ({ text: value, value: key })) ?? [],
validation: yup
.string()
validation: string()
.trim()
.required('Language field is required')
.default(DEFAULT_LANGUAGE),
.required()
.default(() => DEFAULT_LANGUAGE),
grid: { md: 12 }
}
@ -52,19 +50,12 @@ const DISABLE_ANIMATIONS = {
name: 'disableanimations',
label: T.DisableDashboardAnimations,
type: INPUT_TYPES.CHECKBOX,
validation: yup
.boolean()
.transform(value => {
if (typeof value === 'boolean') return value
return String(value).toUpperCase() === 'YES'
})
.default(false),
validation: boolean()
.yesOrNo()
.default(() => false),
grid: { md: 12 }
}
export const FORM_FIELDS = [SCHEME, LANGUAGES, DISABLE_ANIMATIONS]
export const FORM_SCHEMA = yup.object(
getValidationFromFields(FORM_FIELDS)
)
export const FORM_SCHEMA = object(getValidationFromFields(FORM_FIELDS))

View File

@ -13,32 +13,39 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { /* useHistory, */ useLocation } from 'react-router'
import { JSXElementConstructor } from 'react'
import { useHistory, useLocation } from 'react-router'
import { Container } from '@mui/material'
// import { useGeneralApi } from 'client/features/General'
// import { useVmTemplateApi } from 'client/features/One'
import { useGeneralApi } from 'client/features/General'
import { useVmTemplateApi } from 'client/features/One'
import { CreateForm } from 'client/components/Forms/VmTemplate'
// import { PATH } from 'client/apps/sunstone/routesOne'
import { PATH } from 'client/apps/sunstone/routesOne'
import { isDevelopment } from 'client/utils'
/**
* Displays the creation or modification form to a VM Template.
*
* @returns {JSXElementConstructor} VM Template form
*/
function CreateVmTemplate () {
// const history = useHistory()
const { state: { ID: templateId } = {} } = useLocation()
const history = useHistory()
const { state: { ID: templateId, NAME } = {} } = useLocation()
// const { enqueueInfo } = useGeneralApi()
// const { instantiate } = useVmTemplateApi()
const { enqueueSuccess } = useGeneralApi()
const { update, allocate } = useVmTemplateApi()
const onSubmit = async formData => {
const onSubmit = async template => {
try {
console.log({ formData })
/* const { ID, NAME } = templateSelected
await Promise.all(templates.map(template => instantiate(ID, template)))
history.push(templateId ? PATH.TEMPLATE.VMS.LIST : PATH.INSTANCE.VMS.LIST)
enqueueInfo(`VM Template instantiated x${templates.length} - #${ID} ${NAME}`) */
if (templateId === undefined) {
await allocate(template)
history.push(PATH.TEMPLATE.VMS.LIST)
enqueueSuccess(`VM Template created - #${templateId}`)
} else {
await update(templateId, template)
history.push(PATH.TEMPLATE.VMS.LIST)
enqueueSuccess(`VM Template updated - #${templateId} ${NAME}`)
}
} catch (err) {
isDevelopment() && console.error(err)
}

View File

@ -33,7 +33,7 @@ const initial = () => ({
settings: {
scheme: DEFAULT_SCHEME,
lang: DEFAULT_LANGUAGE,
disableAnimations: 'NO'
disableanimations: 'NO'
},
isLoginInProgress: false,
isLoading: false

View File

@ -14,14 +14,41 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable react/display-name */
/* eslint-disable react/prop-types */
import { setLocale, addMethod, number, string } from 'yup'
import { setLocale, addMethod, number, string, boolean, object, array, date } from 'yup'
import { T } from 'client/constants'
import { isDivisibleBy, isBase64 } from 'client/utils/helpers'
const buildMethods = () => {
[number, string, boolean, object, array, date].forEach(schemaType => {
addMethod(schemaType, 'afterSubmit', function (fn) {
this.submit = (...args) => typeof fn === 'function' ? fn(...args) : args[0]
return this
})
addMethod(schemaType, 'cast', function (value, options = {}) {
const resolvedSchema = this.resolve({ value, ...options })
let result = resolvedSchema._cast(value, options)
if (options.isSubmit) {
result = this.submit?.(result, options) ?? result
}
return result
})
})
addMethod(boolean, 'yesOrNo', function (addAfterSubmit = true) {
const schema = this.transform(function (value) {
return !this.isType(value)
? String(value).toUpperCase() === 'YES'
: value
})
if (addAfterSubmit) {
schema.afterSubmit(value => value ? 'YES' : 'NO')
}
return schema
})
addMethod(number, 'isDivisibleBy', function (divisor) {
return this.test(
'is-divisible',