BIN
src/fireedge/src/client/assets/images/logos/alpine.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/fireedge/src/client/assets/images/logos/alt.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/fireedge/src/client/assets/images/logos/arch.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/fireedge/src/client/assets/images/logos/centos.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src/fireedge/src/client/assets/images/logos/debian.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/fireedge/src/client/assets/images/logos/devuan.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
src/fireedge/src/client/assets/images/logos/fedora.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/fireedge/src/client/assets/images/logos/freebsd.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/fireedge/src/client/assets/images/logos/hardenedbsd.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
src/fireedge/src/client/assets/images/logos/knoppix.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
src/fireedge/src/client/assets/images/logos/linux.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
src/fireedge/src/client/assets/images/logos/oracle.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
src/fireedge/src/client/assets/images/logos/redhat.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src/fireedge/src/client/assets/images/logos/suse.png
Normal file
After Width: | Height: | Size: 1022 B |
BIN
src/fireedge/src/client/assets/images/logos/ubuntu.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/fireedge/src/client/assets/images/logos/windows8.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/fireedge/src/client/assets/images/logos/windowsxp.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
@ -39,7 +39,6 @@ export const READONLY = {
|
||||
notOnHypervisors: [vcenter],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: [
|
||||
{ text: '', value: '' },
|
||||
{ text: T.Yes, value: 'YES' },
|
||||
{ text: T.No, value: 'NO' }
|
||||
],
|
||||
@ -47,7 +46,7 @@ export const READONLY = {
|
||||
.string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(undefined)
|
||||
.default(() => 'NO')
|
||||
}
|
||||
|
||||
export const DEV_PREFIX = {
|
||||
|
@ -14,33 +14,34 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
import {
|
||||
SCHEMA,
|
||||
FIELDS
|
||||
} from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions/schema'
|
||||
import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'advanced'
|
||||
|
||||
const Content = ({ hypervisor }) => (
|
||||
<FormWithSchema
|
||||
cy='attach-disk-advanced'
|
||||
id={STEP_ID}
|
||||
fields={FIELDS(hypervisor)}
|
||||
/>
|
||||
)
|
||||
|
||||
const AdvancedOptions = ({ hypervisor } = {}) => ({
|
||||
id: STEP_ID,
|
||||
label: T.AdvancedOptions,
|
||||
resolver: () => SCHEMA(hypervisor),
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: useCallback(
|
||||
() => (
|
||||
<FormWithSchema
|
||||
cy='attach-disk-advanced'
|
||||
id={STEP_ID}
|
||||
fields={FIELDS(hypervisor)}
|
||||
/>
|
||||
),
|
||||
[hypervisor]
|
||||
)
|
||||
content: () => Content({ hypervisor })
|
||||
})
|
||||
|
||||
Content.propTypes = {
|
||||
hypervisor: PropTypes.any,
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func
|
||||
}
|
||||
|
||||
export default AdvancedOptions
|
||||
|
@ -14,55 +14,50 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useListForm } from 'client/hooks'
|
||||
import { ImagesTable } from 'client/components/Tables'
|
||||
|
||||
import {
|
||||
SCHEMA
|
||||
} from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/schema'
|
||||
import { SCHEMA } from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'image'
|
||||
|
||||
const Content = ({ data, setFormData }) => {
|
||||
const { ID } = data?.[0] ?? {}
|
||||
|
||||
const {
|
||||
handleSelect,
|
||||
handleClear
|
||||
} = useListForm({ key: STEP_ID, setList: setFormData })
|
||||
|
||||
const handleSelectedRows = rows => {
|
||||
const { original = {} } = rows?.[0] ?? {}
|
||||
|
||||
original.ID !== undefined ? handleSelect(original) : handleClear()
|
||||
}
|
||||
|
||||
return (
|
||||
<ImagesTable
|
||||
singleSelect
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
initialState={{ selectedRowIds: { [ID]: true } }}
|
||||
onSelectedRowsChange={handleSelectedRows}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ImageStep = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.Image,
|
||||
resolver: () => SCHEMA,
|
||||
content: useCallback(
|
||||
({ data, setFormData }) => {
|
||||
const selectedImage = data?.[0]
|
||||
|
||||
const {
|
||||
handleSelect,
|
||||
handleClear
|
||||
} = useListForm({ key: STEP_ID, setList: setFormData })
|
||||
|
||||
const handleSelectedRows = rows => {
|
||||
const { original } = rows?.[0] ?? {}
|
||||
const { ID, NAME, UID, UNAME } = original ?? {}
|
||||
|
||||
const image = {
|
||||
IMAGE_ID: ID,
|
||||
IMAGE: NAME,
|
||||
IMAGE_UID: UID,
|
||||
IMAGE_UNAME: UNAME
|
||||
}
|
||||
|
||||
ID !== undefined ? handleSelect(image) : handleClear()
|
||||
}
|
||||
|
||||
return (
|
||||
<ImagesTable
|
||||
singleSelect
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
initialState={{ selectedRowIds: { [selectedImage?.IMAGE_ID]: true } }}
|
||||
onSelectedRowsChange={handleSelectedRows}
|
||||
/>
|
||||
)
|
||||
}, [])
|
||||
resolver: SCHEMA,
|
||||
content: Content
|
||||
})
|
||||
|
||||
Content.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func
|
||||
}
|
||||
|
||||
export default ImageStep
|
||||
|
@ -20,4 +20,5 @@ export const SCHEMA = yup
|
||||
.min(1, 'Select image')
|
||||
.max(1, 'Max. one image selected')
|
||||
.required('Image field is required')
|
||||
.default([])
|
||||
.ensure()
|
||||
.default(() => [])
|
||||
|
@ -13,10 +13,51 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import ImagesTable from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable'
|
||||
import AdvancedOptions from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions'
|
||||
import { createSteps } from 'client/utils'
|
||||
import ImagesTable, { STEP_ID as STEP_IMAGE } from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable'
|
||||
import AdvancedOptions, { STEP_ID as STEP_ADVANCED } from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions'
|
||||
import { mapUserInputs, createSteps } from 'client/utils'
|
||||
|
||||
const Steps = createSteps([ImagesTable, AdvancedOptions])
|
||||
const Steps = createSteps(
|
||||
[ImagesTable, AdvancedOptions],
|
||||
{
|
||||
transformInitialValue: initialValue => {
|
||||
const {
|
||||
IMAGE,
|
||||
IMAGE_ID,
|
||||
IMAGE_UID,
|
||||
IMAGE_UNAME,
|
||||
IMAGE_STATE,
|
||||
...diskProps
|
||||
} = initialValue ?? {}
|
||||
|
||||
return {
|
||||
[STEP_IMAGE]: [{
|
||||
...diskProps,
|
||||
NAME: IMAGE,
|
||||
ID: IMAGE_ID,
|
||||
UID: IMAGE_UID,
|
||||
UNAME: IMAGE_UNAME,
|
||||
STATE: IMAGE_STATE
|
||||
}],
|
||||
[STEP_ADVANCED]: initialValue
|
||||
}
|
||||
},
|
||||
transformBeforeSubmit: formData => {
|
||||
const { [STEP_IMAGE]: [image] = [], [STEP_ADVANCED]: advanced } = formData
|
||||
const { ID, NAME, UID, UNAME, STATE, SIZE, ...imageProps } = image ?? {}
|
||||
|
||||
return {
|
||||
...imageProps,
|
||||
IMAGE: NAME,
|
||||
IMAGE_ID: ID,
|
||||
IMAGE_UID: UID,
|
||||
IMAGE_UNAME: UNAME,
|
||||
IMAGE_STATE: STATE,
|
||||
ORIGINAL_SIZE: SIZE,
|
||||
...mapUserInputs(advanced)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default Steps
|
||||
|
@ -14,31 +14,34 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
import {
|
||||
SCHEMA,
|
||||
FIELDS
|
||||
} from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions/schema'
|
||||
import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'advanced'
|
||||
|
||||
const Content = ({ hypervisor }) => (
|
||||
<FormWithSchema
|
||||
cy='attach-disk-advanced'
|
||||
fields={FIELDS(hypervisor)}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
)
|
||||
|
||||
const AdvancedOptions = ({ hypervisor } = {}) => ({
|
||||
id: STEP_ID,
|
||||
label: T.AdvancedOptions,
|
||||
resolver: () => SCHEMA(hypervisor),
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: useCallback(
|
||||
() => <FormWithSchema
|
||||
cy='attach-disk-advanced'
|
||||
fields={FIELDS(hypervisor)}
|
||||
id={STEP_ID}
|
||||
/>,
|
||||
[hypervisor]
|
||||
)
|
||||
content: () => Content({ hypervisor })
|
||||
})
|
||||
|
||||
Content.propTypes = {
|
||||
hypervisor: PropTypes.any,
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func
|
||||
}
|
||||
|
||||
export default AdvancedOptions
|
||||
|
@ -14,31 +14,34 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
import {
|
||||
SCHEMA,
|
||||
FIELDS
|
||||
} from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration/schema'
|
||||
import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'configuration'
|
||||
|
||||
const Content = ({ hypervisor }) => (
|
||||
<FormWithSchema
|
||||
cy='attach-disk-configuration'
|
||||
fields={FIELDS(hypervisor)}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
)
|
||||
|
||||
const BasicConfiguration = ({ hypervisor } = {}) => ({
|
||||
id: STEP_ID,
|
||||
label: T.Configuration,
|
||||
resolver: () => SCHEMA(hypervisor),
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: useCallback(
|
||||
() => <FormWithSchema
|
||||
cy='attach-disk-configuration'
|
||||
fields={FIELDS(hypervisor)}
|
||||
id={STEP_ID}
|
||||
/>,
|
||||
[hypervisor]
|
||||
)
|
||||
content: () => Content({ hypervisor })
|
||||
})
|
||||
|
||||
Content.propTypes = {
|
||||
hypervisor: PropTypes.any,
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func
|
||||
}
|
||||
|
||||
export default BasicConfiguration
|
||||
|
@ -13,10 +13,28 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import BasicConfiguration from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration'
|
||||
import AdvancedOptions from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions'
|
||||
import { createSteps } from 'client/utils'
|
||||
import BasicConfiguration, { STEP_ID as BASIC_ID } from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration'
|
||||
import AdvancedOptions, { STEP_ID as ADVANCED_ID } from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions'
|
||||
import { mapUserInputs, createSteps } from 'client/utils'
|
||||
|
||||
const Steps = createSteps([BasicConfiguration, AdvancedOptions])
|
||||
const Steps = createSteps(
|
||||
[BasicConfiguration, AdvancedOptions],
|
||||
{
|
||||
transformInitialValue: (disk = {}, schema) => ({
|
||||
...schema.cast({
|
||||
[BASIC_ID]: disk,
|
||||
[ADVANCED_ID]: disk
|
||||
}, { stripUnknown: true })
|
||||
}),
|
||||
transformBeforeSubmit: formData => {
|
||||
const {
|
||||
[BASIC_ID]: configuration = {},
|
||||
[ADVANCED_ID]: advanced = {}
|
||||
} = formData ?? {}
|
||||
|
||||
return { ...mapUserInputs(advanced), ...mapUserInputs(configuration) }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default Steps
|
||||
|
@ -20,4 +20,5 @@ export const SCHEMA = yup
|
||||
.min(1, 'Select network')
|
||||
.max(1, 'Max. one network selected')
|
||||
.required('Network field is required')
|
||||
.default([])
|
||||
.ensure()
|
||||
.default(() => [])
|
||||
|
@ -13,16 +13,38 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import NetworksTable, { STEP_ID as NETWORK_STEP } from 'client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable'
|
||||
import AdvancedOptions, { STEP_ID as ADVANCED_STEP } from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions'
|
||||
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'
|
||||
|
||||
const Steps = createSteps(
|
||||
[NetworksTable, AdvancedOptions],
|
||||
{
|
||||
transformInitialValue: nic => {
|
||||
const {
|
||||
NETWORK,
|
||||
NETWORK_ID: ID,
|
||||
NETWORK_UID,
|
||||
NETWORK_UNAME,
|
||||
SECURITY_GROUPS,
|
||||
...rest
|
||||
} = nic ?? {}
|
||||
|
||||
return {
|
||||
[NETWORK_ID]: [{
|
||||
...nic,
|
||||
ID,
|
||||
NAME: NETWORK,
|
||||
UID: NETWORK_UID,
|
||||
UNAME: NETWORK_UNAME,
|
||||
SECURITY_GROUPS
|
||||
}],
|
||||
[ADVANCED_ID]: rest
|
||||
}
|
||||
},
|
||||
transformBeforeSubmit: formData => {
|
||||
const { [NETWORK_STEP]: network, [ADVANCED_STEP]: advanced } = formData
|
||||
const { ID, NAME, UID, UNAME, SECURITY_GROUPS } = network?.[0]
|
||||
const { [NETWORK_ID]: [network] = [], [ADVANCED_ID]: advanced } = formData
|
||||
const { ID, NAME, UID, UNAME, SECURITY_GROUPS } = network ?? {}
|
||||
|
||||
return {
|
||||
NETWORK_ID: ID,
|
||||
@ -32,19 +54,6 @@ const Steps = createSteps(
|
||||
SECURITY_GROUPS,
|
||||
...mapUserInputs(advanced)
|
||||
}
|
||||
},
|
||||
transformInitialValue: initialValue => {
|
||||
const { NETWORK_ID, NETWORK, NETWORK_UID, NETWORK_UNAME, ...rest } = initialValue ?? {}
|
||||
|
||||
return {
|
||||
[NETWORK_STEP]: [{
|
||||
ID: NETWORK_ID,
|
||||
NAME: NETWORK,
|
||||
UID: NETWORK_UID,
|
||||
UNAME: NETWORK_UNAME
|
||||
}],
|
||||
[ADVANCED_STEP]: rest
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -0,0 +1,21 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { createForm } from 'client/utils'
|
||||
import { SCHEMA, FIELDS } from 'client/components/Forms/VmTemplate/CloneForm/schema'
|
||||
|
||||
const CloneForm = createForm(SCHEMA, FIELDS)
|
||||
|
||||
export default CloneForm
|
@ -0,0 +1,59 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { string, boolean, object } from 'yup'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
import { getValidationFromFields } from 'client/utils'
|
||||
|
||||
const PREFIX = {
|
||||
name: 'prefix',
|
||||
label: 'Prefix',
|
||||
tooltip: `
|
||||
Several templates are selected,
|
||||
please choose prefix to name the new copies.`,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required('Prefix field is required')
|
||||
.default(() => 'Copy of ')
|
||||
}
|
||||
|
||||
const NAME = {
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
tooltip: 'Name for the new template.',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required('Name field is required')
|
||||
.default(() => '')
|
||||
}
|
||||
|
||||
const IMAGES = {
|
||||
name: 'image',
|
||||
label: 'Clone with images',
|
||||
tooltip: `
|
||||
You can also clone any Image referenced inside this Template.
|
||||
They will be cloned to a new Image, and made persistent.`,
|
||||
type: INPUT_TYPES.CHECKBOX,
|
||||
validation: boolean().default(() => false),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
export const FIELDS = ({ isMultiple } = {}) =>
|
||||
[isMultiple ? PREFIX : NAME, IMAGES]
|
||||
|
||||
export const SCHEMA = props => object(getValidationFromFields(FIELDS(props)))
|
@ -14,13 +14,9 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useMemo } from 'react'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import useStyles from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/styles'
|
||||
|
||||
import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable'
|
||||
import { SCHEMA, FIELDS } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
@ -29,8 +25,6 @@ export const STEP_ID = 'configuration'
|
||||
|
||||
const Content = () => {
|
||||
const classes = useStyles()
|
||||
const { watch } = useFormContext()
|
||||
const selectedVmTemplate = useMemo(() => watch(`${TEMPLATE_ID}[0]`), [])
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
@ -47,12 +41,6 @@ const Content = () => {
|
||||
legend={Tr(T.Capacity)}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
<FormWithSchema
|
||||
cy='instantiate-vm-template-configuration.disk'
|
||||
fields={FIELDS.DISK(selectedVmTemplate)}
|
||||
legend={Tr(T.Disks)}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
<FormWithSchema
|
||||
cy='instantiate-vm-template-configuration.ownership'
|
||||
fields={FIELDS.OWNERSHIP}
|
||||
|
@ -13,12 +13,11 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { object } from 'yup'
|
||||
|
||||
import { FIELDS as INFORMATION_FIELDS, SCHEMA as INFORMATION_SCHEMA } from './informationSchema'
|
||||
import { FIELDS as CAPACITY_FIELDS, SCHEMA as CAPACITY_SCHEMA } from './capacitySchema'
|
||||
import { FIELDS as DISK_FIELDS, SCHEMA as DISK_SCHEMA } from './diskSchema'
|
||||
// import { FIELDS as DISK_FIELDS, SCHEMA as DISK_SCHEMA } from './diskSchema'
|
||||
import { FIELDS as VM_GROUP_FIELDS, SCHEMA as VM_GROUP_SCHEMA } from './vmGroupSchema'
|
||||
import { FIELDS as OWNERSHIP_FIELDS, SCHEMA as OWNERSHIP_SCHEMA } from './ownershipSchema'
|
||||
import { FIELDS as VCENTER_FIELDS, SCHEMA as VCENTER_SCHEMA } from './vcenterSchema'
|
||||
@ -26,7 +25,7 @@ import { FIELDS as VCENTER_FIELDS, SCHEMA as VCENTER_SCHEMA } from './vcenterSch
|
||||
export const FIELDS = {
|
||||
INFORMATION: INFORMATION_FIELDS,
|
||||
CAPACITY: CAPACITY_FIELDS,
|
||||
DISK: vmTemplate => DISK_FIELDS(vmTemplate),
|
||||
// DISK: vmTemplate => DISK_FIELDS(vmTemplate),
|
||||
OWNERSHIP: OWNERSHIP_FIELDS,
|
||||
VM_GROUP: VM_GROUP_FIELDS,
|
||||
VCENTER: VCENTER_FIELDS
|
||||
@ -35,7 +34,7 @@ export const FIELDS = {
|
||||
export const SCHEMA = object()
|
||||
.concat(INFORMATION_SCHEMA)
|
||||
.concat(CAPACITY_SCHEMA)
|
||||
.concat(DISK_SCHEMA)
|
||||
// .concat(DISK_SCHEMA)
|
||||
.concat(OWNERSHIP_SCHEMA)
|
||||
.concat(VM_GROUP_SCHEMA)
|
||||
.concat(VCENTER_SCHEMA)
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useMemo, SetStateAction } from 'react'
|
||||
import { SetStateAction } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {
|
||||
@ -25,11 +25,10 @@ import {
|
||||
} from 'iconoir-react'
|
||||
import { Divider, makeStyles } from '@material-ui/core'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { Action } from 'client/components/Cards/SelectCard'
|
||||
import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable'
|
||||
import { TAB_ID as STORAGE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/storage'
|
||||
import { TAB_ID as NIC_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking'
|
||||
import { set } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
@ -54,10 +53,40 @@ const useStyles = makeStyles(theme => ({
|
||||
}))
|
||||
|
||||
/**
|
||||
* @param {string[]} newBootOrder - New boot order
|
||||
* @param {SetStateAction} setFormData - New boot order
|
||||
* @param {string} id - Resource id: 'NIC<index>' or 'DISK<index>'
|
||||
* @param {Array} list - List of resources
|
||||
* @param {object} formData - Form data
|
||||
* @param {SetStateAction} setFormData - React set state action
|
||||
*/
|
||||
export const reorder = (newBootOrder, setFormData) => {
|
||||
export const reorderBootAfterRemove = (id, list, formData, setFormData) => {
|
||||
const type = String(id).toLowerCase().replace(/\d+/g, '') // nic | disk
|
||||
const getIndexFromId = id => String(id).toLowerCase().replace(type, '')
|
||||
const idxToRemove = getIndexFromId(id)
|
||||
|
||||
const ids = list
|
||||
.filter(resource => resource.NAME !== id)
|
||||
.map(resource => String(resource.NAME).toLowerCase())
|
||||
|
||||
const newBootOrder = [...formData?.OS?.BOOT?.split(',').filter(Boolean)]
|
||||
.filter(bootId => !bootId.startsWith(type) || ids.includes(bootId))
|
||||
.map(bootId => {
|
||||
if (!bootId.startsWith(type)) return bootId
|
||||
|
||||
const resourceId = getIndexFromId(bootId)
|
||||
|
||||
return resourceId < idxToRemove
|
||||
? bootId
|
||||
: `${type}${resourceId - 1}`
|
||||
})
|
||||
|
||||
reorder(newBootOrder, setFormData)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} newBootOrder - New boot order
|
||||
* @param {SetStateAction} setFormData - React set state action
|
||||
*/
|
||||
const reorder = (newBootOrder, setFormData) => {
|
||||
setFormData(prev => {
|
||||
const newData = set({ ...prev }, 'extra.OS.BOOT', newBootOrder.join(','))
|
||||
|
||||
@ -67,41 +96,32 @@ export const reorder = (newBootOrder, setFormData) => {
|
||||
|
||||
const Booting = ({ data, setFormData }) => {
|
||||
const classes = useStyles()
|
||||
const { watch } = useFormContext()
|
||||
const bootOrder = data?.OS?.BOOT?.split(',').filter(Boolean) ?? []
|
||||
|
||||
const disks = useMemo(() => {
|
||||
const templateSeleted = watch(`${TEMPLATE_ID}[0]`)
|
||||
const listOfDisks = [templateSeleted?.TEMPLATE?.DISK ?? []].flat()
|
||||
|
||||
return listOfDisks?.map(disk => {
|
||||
const { DISK_ID, IMAGE, IMAGE_ID } = disk
|
||||
const isVolatile = !IMAGE && !IMAGE_ID
|
||||
|
||||
const name = isVolatile
|
||||
? <>`DISK ${DISK_ID}: `<Translate word={T.VolatileDisk} /></>
|
||||
: `DISK ${DISK_ID}: ${IMAGE}`
|
||||
const disks = data?.[STORAGE_ID]
|
||||
?.map((disk, idx) => {
|
||||
const isVolatile = !disk?.IMAGE && !disk?.IMAGE_ID
|
||||
|
||||
return {
|
||||
ID: `disk${DISK_ID}`,
|
||||
ID: `disk${idx}`,
|
||||
NAME: (
|
||||
<>
|
||||
<ImageIcon size={16} />
|
||||
{name}
|
||||
{isVolatile
|
||||
? <>{`${disk?.NAME}: `}<Translate word={T.VolatileDisk} /></>
|
||||
: [disk?.NAME, disk?.IMAGE].filter(Boolean).join(': ')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
}) ?? []
|
||||
|
||||
const nics = data?.[NIC_ID]
|
||||
?.map((nic, idx) => ({ ...nic, NAME: nic?.NAME ?? `NIC${idx}` }))
|
||||
?.map((nic, idx) => ({
|
||||
ID: `nic${idx}`,
|
||||
NAME: (
|
||||
<>
|
||||
<NetworkIcon size={16} />
|
||||
{[nic?.NAME ?? `NIC${idx}`, nic.NETWORK].filter(Boolean).join(': ')}
|
||||
{[nic?.NAME, nic.NETWORK].filter(Boolean).join(': ')}
|
||||
</>
|
||||
)
|
||||
})) ?? []
|
||||
|
@ -22,6 +22,7 @@ import { WarningCircledOutline as WarningIcon } from 'iconoir-react'
|
||||
|
||||
import Tabs from 'client/components/Tabs'
|
||||
import { SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
|
||||
import Storage from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/storage'
|
||||
import Networking from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking'
|
||||
import Placement from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/placement'
|
||||
import ScheduleAction from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/scheduleAction'
|
||||
@ -35,6 +36,10 @@ const Content = ({ data, setFormData }) => {
|
||||
const { errors } = useFormContext()
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: 'storage',
|
||||
renderContent: Storage({ data, setFormData })
|
||||
},
|
||||
{
|
||||
name: 'network',
|
||||
renderContent: Networking({ data, setFormData })
|
||||
|
@ -25,8 +25,9 @@ import { AttachNicForm } from 'client/components/Forms/Vm'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import { NIC_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
|
||||
import { reorder } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting'
|
||||
import { SCHEMA as EXTRA_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
|
||||
import { reorderBootAfterRemove } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting'
|
||||
import { stringToBoolean } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@ -43,36 +44,21 @@ export const TAB_ID = 'NIC'
|
||||
const Networking = ({ data, setFormData }) => {
|
||||
const classes = useStyles()
|
||||
const nics = data?.[TAB_ID]
|
||||
?.map((nic, idx) => ({ ...nic, NAME: nic?.NAME ?? `NIC${idx}` }))
|
||||
|
||||
const { handleRemove, handleSave } = useListForm({
|
||||
const { handleSetList, handleRemove, handleSave } = useListForm({
|
||||
parent: EXTRA_ID,
|
||||
key: TAB_ID,
|
||||
list: nics,
|
||||
setList: setFormData,
|
||||
getItemId: (item) => item.NAME ?? `NIC${data.length + 1}`,
|
||||
addItemId: (item, id) => ({ ...item, NAME: id })
|
||||
getItemId: item => item.NAME,
|
||||
addItemId: (item, _, itemIndex) => ({ ...item, NAME: `${TAB_ID}${itemIndex}` })
|
||||
})
|
||||
|
||||
const reorderBootOrder = nicId => {
|
||||
const getIndexFromNicId = id => String(id).toLowerCase().replace('nic', '')
|
||||
const idxToRemove = getIndexFromNicId(nicId)
|
||||
const reorderNics = () => {
|
||||
const diskSchema = EXTRA_SCHEMA.pick([TAB_ID])
|
||||
const { [TAB_ID]: newList } = diskSchema.cast({ [TAB_ID]: data?.[TAB_ID] })
|
||||
|
||||
const nicIds = nics
|
||||
.filter(nic => nic.NAME !== nicId)
|
||||
.map(nic => String(nic.NAME).toLowerCase())
|
||||
|
||||
const newBootOrder = [...data?.OS?.BOOT?.split(',').filter(Boolean)]
|
||||
.filter(bootId => !bootId.startsWith('nic') || nicIds.includes(bootId))
|
||||
.map(bootId => {
|
||||
if (!bootId.startsWith('nic')) return bootId
|
||||
|
||||
const nicId = getIndexFromNicId(bootId)
|
||||
|
||||
return nicId < idxToRemove ? bootId : `nic${nicId - 1}`
|
||||
})
|
||||
|
||||
reorder(newBootOrder, setFormData)
|
||||
handleSetList(newList)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -88,8 +74,7 @@ const Networking = ({ data, setFormData }) => {
|
||||
}}
|
||||
options={[{
|
||||
form: () => AttachNicForm({ nics }),
|
||||
onSubmit: formData =>
|
||||
handleSave(NIC_SCHEMA.cast(formData))
|
||||
onSubmit: handleSave
|
||||
}]}
|
||||
/>
|
||||
<div className={classes.root}>
|
||||
@ -103,7 +88,12 @@ const Networking = ({ data, setFormData }) => {
|
||||
title={[NAME, NETWORK].filter(Boolean).join(' - ')}
|
||||
subheader={<>
|
||||
{Object
|
||||
.entries({ RDP, SSH, ALIAS: PARENT, EXTERNAL })
|
||||
.entries({
|
||||
RDP: stringToBoolean(RDP),
|
||||
SSH: stringToBoolean(SSH),
|
||||
EXTERNAL: stringToBoolean(EXTERNAL),
|
||||
ALIAS: PARENT
|
||||
})
|
||||
.map(([k, v]) => v ? `${k}` : '')
|
||||
.filter(Boolean)
|
||||
.join(' | ')
|
||||
@ -116,7 +106,8 @@ const Networking = ({ data, setFormData }) => {
|
||||
data-cy={`remove-${NAME}`}
|
||||
handleClick={() => {
|
||||
handleRemove(NAME)
|
||||
reorderBootOrder(NAME)
|
||||
reorderNics()
|
||||
reorderBootAfterRemove(NAME, nics, data, setFormData)
|
||||
}}
|
||||
icon={<Trash size={18} />}
|
||||
/>
|
||||
|
@ -17,7 +17,6 @@
|
||||
import { array, object, string, lazy } from 'yup'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { SCHEMA as NETWORK_SCHEMA } from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/schema'
|
||||
import { SCHEMA as PUNCTUAL_SCHEMA } from 'client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/schema'
|
||||
import { SCHEMA as RELATIVE_SCHEMA } from 'client/components/Forms/Vm/CreateSchedActionForm/RelativeForm/schema'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
@ -65,14 +64,6 @@ export const DS_RANK_FIELD = {
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
export const NIC_SCHEMA = object({
|
||||
NAME: string().trim(),
|
||||
NETWORK_ID: string().trim(),
|
||||
NETWORK: string().trim(),
|
||||
NETWORK_UNAME: string().trim(),
|
||||
SECURITY_GROUPS: string().trim()
|
||||
}).concat(NETWORK_SCHEMA)
|
||||
|
||||
export const SCHED_ACTION_SCHEMA = lazy(({ TIME } = {}) => {
|
||||
const isRelative = String(TIME).includes('+')
|
||||
const schema = isRelative ? RELATIVE_SCHEMA : PUNCTUAL_SCHEMA
|
||||
@ -81,7 +72,22 @@ export const SCHED_ACTION_SCHEMA = lazy(({ TIME } = {}) => {
|
||||
})
|
||||
|
||||
export const SCHEMA = object({
|
||||
NIC: array(NIC_SCHEMA).ensure(),
|
||||
DISK: array()
|
||||
.ensure()
|
||||
.transform(disks => disks?.map((disk, idx) => ({
|
||||
...disk,
|
||||
NAME: disk?.NAME?.startsWith('DISK') || !disk?.NAME
|
||||
? `DISK${idx}`
|
||||
: disk?.NAME
|
||||
}))),
|
||||
NIC: array()
|
||||
.ensure()
|
||||
.transform(nics => nics?.map((nic, idx) => ({
|
||||
...nic,
|
||||
NAME: nic?.NAME?.startsWith('NIC') || !nic?.NAME
|
||||
? `NIC${idx}`
|
||||
: nic?.NAME
|
||||
}))),
|
||||
SCHED_ACTION: array(SCHED_ACTION_SCHEMA).ensure(),
|
||||
OS: object({
|
||||
BOOT: string().trim().notRequired()
|
||||
@ -92,4 +98,4 @@ export const SCHEMA = object({
|
||||
DS_REQ_FIELD,
|
||||
DS_RANK_FIELD
|
||||
])
|
||||
})
|
||||
}).noUnknown(false)
|
||||
|
@ -0,0 +1,194 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import { Edit, Trash } from 'iconoir-react'
|
||||
|
||||
import { useListForm } from 'client/hooks'
|
||||
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
|
||||
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
|
||||
import { ImageSteps, VolatileSteps } from 'client/components/Forms/Vm'
|
||||
import { StatusCircle, StatusChip } from 'client/components/Status'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable'
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import { SCHEMA as EXTRA_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
|
||||
import { reorderBootAfterRemove } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting'
|
||||
import { getState, getDiskType } from 'client/models/Image'
|
||||
import { stringToBoolean } from 'client/models/Helper'
|
||||
import { prettyBytes } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
paddingBlock: '1em',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, auto))',
|
||||
gap: '1em'
|
||||
}
|
||||
})
|
||||
|
||||
export const TAB_ID = 'DISK'
|
||||
|
||||
const Storage = ({ data, setFormData }) => {
|
||||
const classes = useStyles()
|
||||
const { watch } = useFormContext()
|
||||
const { HYPERVISOR } = useMemo(() => watch(`${TEMPLATE_ID}[0]`) ?? {}, [])
|
||||
const disks = data?.[TAB_ID]
|
||||
|
||||
const { handleSetList, handleRemove, handleSave } = useListForm({
|
||||
parent: EXTRA_ID,
|
||||
key: TAB_ID,
|
||||
list: disks,
|
||||
setList: setFormData,
|
||||
getItemId: item => item.NAME,
|
||||
addItemId: (item, _, itemIndex) => ({ ...item, NAME: `${TAB_ID}${itemIndex}` })
|
||||
})
|
||||
|
||||
const reorderDisks = () => {
|
||||
const diskSchema = EXTRA_SCHEMA.pick([TAB_ID])
|
||||
const { [TAB_ID]: newList } = diskSchema.cast({ [TAB_ID]: data?.[TAB_ID] })
|
||||
|
||||
handleSetList(newList)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
color: 'secondary',
|
||||
'data-cy': 'add-disk',
|
||||
label: 'Add disk'
|
||||
}}
|
||||
dialogProps={{
|
||||
title: `Add new: ${Tr(T.Disk)}`
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
cy: 'attach-image-disk',
|
||||
name: T.Image,
|
||||
form: () => ImageSteps({ hypervisor: HYPERVISOR }),
|
||||
onSubmit: handleSave
|
||||
},
|
||||
{
|
||||
cy: 'attach-volatile-disk',
|
||||
name: T.Volatile,
|
||||
form: () => VolatileSteps({ hypervisor: HYPERVISOR }),
|
||||
onSubmit: handleSave
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<div className={classes.root}>
|
||||
{disks?.map(item => {
|
||||
const {
|
||||
NAME,
|
||||
TYPE,
|
||||
IMAGE,
|
||||
IMAGE_ID,
|
||||
IMAGE_STATE,
|
||||
ORIGINAL_SIZE,
|
||||
SIZE = ORIGINAL_SIZE,
|
||||
READONLY,
|
||||
DATASTORE,
|
||||
PERSISTENT
|
||||
} = item
|
||||
|
||||
const isVolatile = !IMAGE && !IMAGE_ID
|
||||
const isPersistent = stringToBoolean(PERSISTENT)
|
||||
const state = !isVolatile && getState({ STATE: IMAGE_STATE })
|
||||
const type = isVolatile ? TYPE : getDiskType(item)
|
||||
const originalSize = +ORIGINAL_SIZE ? prettyBytes(+ORIGINAL_SIZE, 'MB') : '-'
|
||||
const size = prettyBytes(+SIZE, 'MB')
|
||||
|
||||
return (
|
||||
<SelectCard
|
||||
key={NAME}
|
||||
title={isVolatile ? (
|
||||
<>
|
||||
{`${NAME} - `}
|
||||
<Translate word={T.VolatileDisk} />
|
||||
</>
|
||||
) : (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5em' }}>
|
||||
<StatusCircle color={state?.color} tooltip={state?.name} />
|
||||
{`${NAME}: ${IMAGE}`}
|
||||
{isPersistent && <StatusChip text='PERSISTENT' />}
|
||||
</span>
|
||||
)}
|
||||
subheader={<>
|
||||
{Object
|
||||
.entries({
|
||||
[DATASTORE]: DATASTORE,
|
||||
READONLY: stringToBoolean(READONLY),
|
||||
PERSISTENT: stringToBoolean(PERSISTENT),
|
||||
[isVolatile || ORIGINAL_SIZE === SIZE ? size : `${originalSize}/${size}`]: true,
|
||||
[type]: type
|
||||
})
|
||||
.map(([k, v]) => v ? `${k}` : '')
|
||||
.filter(Boolean)
|
||||
.join(' | ')
|
||||
}
|
||||
</>}
|
||||
action={
|
||||
<>
|
||||
<Action
|
||||
data-cy={`remove-${NAME}`}
|
||||
tooltip={<Translate word={T.Remove} />}
|
||||
handleClick={() => {
|
||||
handleRemove(NAME)
|
||||
reorderDisks()
|
||||
reorderBootAfterRemove(NAME, disks, data, setFormData)
|
||||
}}
|
||||
icon={<Trash size={18} />}
|
||||
/>
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
'data-cy': `edit-${NAME}`,
|
||||
icon: <Edit size={18} />,
|
||||
tooltip: <Translate word={T.Edit} />
|
||||
}}
|
||||
dialogProps={{
|
||||
title: <><Translate word={T.Edit} />{`: ${NAME}`}</>
|
||||
}}
|
||||
options={[{
|
||||
form: () => isVolatile
|
||||
? VolatileSteps({ hypervisor: HYPERVISOR }, item)
|
||||
: ImageSteps({ hypervisor: HYPERVISOR }, item),
|
||||
onSubmit: newValues => handleSave(newValues, NAME)
|
||||
}]}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Storage.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func
|
||||
}
|
||||
|
||||
Storage.displayName = 'Storage'
|
||||
|
||||
export default Storage
|
@ -15,6 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import PropTypes from 'prop-types'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
|
||||
import { useListForm } from 'client/hooks'
|
||||
import { useVmTemplateApi } from 'client/features/One'
|
||||
@ -29,7 +30,14 @@ import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'template'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
body: {
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))'
|
||||
}
|
||||
})
|
||||
|
||||
const Content = ({ data, setFormData }) => {
|
||||
const classes = useStyles()
|
||||
const selectedTemplate = data?.[0]
|
||||
const { getVmTemplate } = useVmTemplateApi()
|
||||
|
||||
@ -62,6 +70,7 @@ const Content = ({ data, setFormData }) => {
|
||||
singleSelect
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
classes={classes}
|
||||
initialState={{ selectedRowIds: { [selectedTemplate?.ID]: true } }}
|
||||
onSelectedRowsChange={handleSelectedRows}
|
||||
/>
|
||||
|
@ -17,7 +17,7 @@ import VmTemplatesTable, { STEP_ID as TEMPLATE_ID } from 'client/components/Form
|
||||
import BasicConfiguration, { STEP_ID as BASIC_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration'
|
||||
import ExtraConfiguration, { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { createSteps, deepmerge } from 'client/utils'
|
||||
import { createSteps } from 'client/utils'
|
||||
|
||||
const Steps = createSteps(
|
||||
[VmTemplatesTable, BasicConfiguration, ExtraConfiguration],
|
||||
@ -27,7 +27,7 @@ const Steps = createSteps(
|
||||
[TEMPLATE_ID]: [vmTemplate],
|
||||
[BASIC_ID]: vmTemplate?.TEMPLATE,
|
||||
[EXTRA_ID]: vmTemplate?.TEMPLATE
|
||||
}, { stripUnknown: true })
|
||||
}, { stripUnknown: true, context: { [TEMPLATE_ID]: [vmTemplate] } })
|
||||
}),
|
||||
transformBeforeSubmit: formData => {
|
||||
const {
|
||||
@ -37,8 +37,7 @@ const Steps = createSteps(
|
||||
} = formData ?? {}
|
||||
|
||||
// merge with template disks to get TYPE attribute
|
||||
const DISK = deepmerge(templateSelected.TEMPLATE?.DISK, restOfConfig?.DISK)
|
||||
const templateXML = jsonToXml({ ...extraTemplate, ...restOfConfig, DISK })
|
||||
const templateXML = jsonToXml({ ...extraTemplate, ...restOfConfig })
|
||||
const data = { instances, hold, persistent, template: templateXML }
|
||||
|
||||
const templates = [...new Array(instances)]
|
||||
|
@ -13,8 +13,10 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import CloneForm from 'client/components/Forms/VmTemplate/CloneForm'
|
||||
import InstantiateForm from 'client/components/Forms/VmTemplate/InstantiateForm'
|
||||
|
||||
export {
|
||||
CloneForm,
|
||||
InstantiateForm
|
||||
}
|
||||
|
@ -60,26 +60,28 @@ const OpenNebulaLogo = memo(({ width, height, spinner, withText, viewBox, ...pro
|
||||
</defs>
|
||||
)}
|
||||
{/* --------------- CLOUD ------------------ */}
|
||||
<path
|
||||
fill={spinner ? 'url(#gradient__child1)' : cloudColor.child1.static}
|
||||
d="M124.9,130.9c-4.6,1.1-9.1,2.2-13.7,3.2c-28,6-56.5,9.8-85.1,11.3c-1.8-0.2,0.7,1.1,1.1,1.5c7.6,5.5,17.5,6.5,26.9,6.6c22.9,0,45.7,0,68.6,0c0.8,0,1.3-0.2,1.7-0.7c0.5-0.5,0.5-1.7,0.5-1.7V130.9z"
|
||||
/>
|
||||
<path
|
||||
fill={spinner ? 'url(#gradient__child2)' : cloudColor.child2.static}
|
||||
d="M124.9,106.1c-14.4,6.8-29.5,12.3-44.8,16.9c-20.6,5.9-41.7,10.3-63,12.7c1.2,1.8,2.1,4.5,4.2,5.3c34.8-1.7,69.7-6.4,103.6-14.9V106.1z"
|
||||
/>
|
||||
<path
|
||||
fill={spinner ? 'url(#gradient__child3)' : cloudColor.child3.static}
|
||||
d="M124.9,81.6c-26,17.6-55.7,29.3-85.9,37.2c-8.5,2.1-17.1,4.4-25.7,5.8c0.4,2.3,0.9,4.4,1.8,6.6c36.5-4.3,72.7-13.2,106.2-28.6c1.2-0.6,2.5-1.2,3.7-1.8V81.6z"
|
||||
/>
|
||||
<path
|
||||
fill={spinner ? 'url(#gradient__child4)' : cloudColor.child4.static}
|
||||
d="M124.9,47.1c-13.1,15.6-29.7,28.1-47.4,38.2c-18.8,10.6-39,18.8-59.6,24.9c-1.5,0.8-4.4,0.5-4.3,2.8c-0.5,2.3-0.7,4.6-0.7,6.9c33.6-6.4,66.7-17,96.5-34.1c5.3-3.1,10.5-6.4,15.6-9.9V47.1z"
|
||||
/>
|
||||
<path
|
||||
fill={spinner ? 'url(#gradient__child5)' : cloudColor.child5.static}
|
||||
d="M124.9,12.9c-0.4-0.9-1.8-0.4-3.3,0.3c-5.7,2.4-28.8,13.7-32.3,28.7c-1.4,1.9-3.5-0.5-5.1-1c-13.1-6.4-29.7-4.3-40.9,5.1c-11.2,9-17.3,25.1-12.2,39c0.8,1.6,1.6,3.8-0.1,5.2c-2.1,1.4-4.4,2.5-6.2,4.3c-3.7,3.1-6.9,7.1-8.9,11.3c30.7-9.3,60.8-22.5,85.9-42.8c8.4-7,16.3-14.7,22.9-23.4V12.9z"
|
||||
/>
|
||||
<g id='logo__cloud'>
|
||||
<path
|
||||
fill={spinner ? 'url(#gradient__child1)' : cloudColor.child1.static}
|
||||
d="M124.9,130.9c-4.6,1.1-9.1,2.2-13.7,3.2c-28,6-56.5,9.8-85.1,11.3c-1.8-0.2,0.7,1.1,1.1,1.5c7.6,5.5,17.5,6.5,26.9,6.6c22.9,0,45.7,0,68.6,0c0.8,0,1.3-0.2,1.7-0.7c0.5-0.5,0.5-1.7,0.5-1.7V130.9z"
|
||||
/>
|
||||
<path
|
||||
fill={spinner ? 'url(#gradient__child2)' : cloudColor.child2.static}
|
||||
d="M124.9,106.1c-14.4,6.8-29.5,12.3-44.8,16.9c-20.6,5.9-41.7,10.3-63,12.7c1.2,1.8,2.1,4.5,4.2,5.3c34.8-1.7,69.7-6.4,103.6-14.9V106.1z"
|
||||
/>
|
||||
<path
|
||||
fill={spinner ? 'url(#gradient__child3)' : cloudColor.child3.static}
|
||||
d="M124.9,81.6c-26,17.6-55.7,29.3-85.9,37.2c-8.5,2.1-17.1,4.4-25.7,5.8c0.4,2.3,0.9,4.4,1.8,6.6c36.5-4.3,72.7-13.2,106.2-28.6c1.2-0.6,2.5-1.2,3.7-1.8V81.6z"
|
||||
/>
|
||||
<path
|
||||
fill={spinner ? 'url(#gradient__child4)' : cloudColor.child4.static}
|
||||
d="M124.9,47.1c-13.1,15.6-29.7,28.1-47.4,38.2c-18.8,10.6-39,18.8-59.6,24.9c-1.5,0.8-4.4,0.5-4.3,2.8c-0.5,2.3-0.7,4.6-0.7,6.9c33.6-6.4,66.7-17,96.5-34.1c5.3-3.1,10.5-6.4,15.6-9.9V47.1z"
|
||||
/>
|
||||
<path
|
||||
fill={spinner ? 'url(#gradient__child5)' : cloudColor.child5.static}
|
||||
d="M124.9,12.9c-0.4-0.9-1.8-0.4-3.3,0.3c-5.7,2.4-28.8,13.7-32.3,28.7c-1.4,1.9-3.5-0.5-5.1-1c-13.1-6.4-29.7-4.3-40.9,5.1c-11.2,9-17.3,25.1-12.2,39c0.8,1.6,1.6,3.8-0.1,5.2c-2.1,1.4-4.4,2.5-6.2,4.3c-3.7,3.1-6.9,7.1-8.9,11.3c30.7-9.3,60.8-22.5,85.9-42.8c8.4-7,16.3-14.7,22.9-23.4V12.9z"
|
||||
/>
|
||||
</g>
|
||||
|
||||
{withText && (
|
||||
<g id='logo__text'>
|
||||
|
@ -47,7 +47,12 @@ const Image = memo(({ src, imageInError, withSources, imgProps }) => {
|
||||
}
|
||||
|
||||
if (error.fail) {
|
||||
return <img src={imageInError} draggable={false} onError={addRetry} />
|
||||
return <img
|
||||
{...imgProps}
|
||||
src={imageInError}
|
||||
draggable={false}
|
||||
onError={addRetry}
|
||||
/>
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -17,6 +17,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { InfoEmpty } from 'iconoir-react'
|
||||
import { Box, LinearProgress } from '@material-ui/core'
|
||||
import {
|
||||
@ -54,9 +55,10 @@ const EnhancedTable = ({
|
||||
pageSize = 10,
|
||||
RowComponent,
|
||||
showPageCount,
|
||||
singleSelect = false
|
||||
singleSelect = false,
|
||||
classes = {}
|
||||
}) => {
|
||||
const classes = EnhancedTableStyles()
|
||||
const styles = EnhancedTableStyles()
|
||||
|
||||
const isFetching = isLoading && data === undefined
|
||||
|
||||
@ -127,8 +129,8 @@ const EnhancedTable = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box {...getTableProps()} className={classes.root}>
|
||||
<div className={classes.toolbar}>
|
||||
<Box {...getTableProps()} className={clsx(styles.root, classes.root)}>
|
||||
<div className={styles.toolbar}>
|
||||
{/* TOOLBAR */}
|
||||
{!isFetching && (
|
||||
<Toolbar
|
||||
@ -139,7 +141,7 @@ const EnhancedTable = ({
|
||||
)}
|
||||
|
||||
{/* PAGINATION */}
|
||||
<div className={classes.pagination}>
|
||||
<div className={styles.pagination}>
|
||||
{page?.length > 0 && (
|
||||
<Pagination
|
||||
handleChangePage={handleChangePage}
|
||||
@ -152,10 +154,10 @@ const EnhancedTable = ({
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<LinearProgress size='1em' color='secondary' className={classes.loading} />
|
||||
<LinearProgress size='1em' color='secondary' className={styles.loading} />
|
||||
)}
|
||||
|
||||
<div className={classes.table}>
|
||||
<div className={styles.table}>
|
||||
{/* FILTERS */}
|
||||
{!isFetching && (
|
||||
<Filters
|
||||
@ -164,10 +166,10 @@ const EnhancedTable = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={classes.body}>
|
||||
<div className={clsx(styles.body, classes.body)}>
|
||||
{/* NO DATA MESSAGE */}
|
||||
{!isFetching && page?.length === 0 && (
|
||||
<span className={classes.noDataMessage}>
|
||||
<span className={styles.noDataMessage}>
|
||||
<InfoEmpty />
|
||||
<Translate word={T.NoDataAvailable} />
|
||||
</span>
|
||||
@ -209,6 +211,10 @@ export const EnhancedTableProps = {
|
||||
fetchMore: PropTypes.func,
|
||||
getRowId: PropTypes.func,
|
||||
initialState: PropTypes.object,
|
||||
classes: PropTypes.shape({
|
||||
root: PropTypes.string,
|
||||
body: PropTypes.string
|
||||
}),
|
||||
isLoading: PropTypes.bool,
|
||||
onlyGlobalSearch: PropTypes.bool,
|
||||
onlyGlobalSelectedRows: PropTypes.bool,
|
||||
|
@ -58,6 +58,7 @@ export default makeStyles(
|
||||
display: 'grid',
|
||||
gap: '1em',
|
||||
gridTemplateColumns: 'minmax(0, 1fr)',
|
||||
// gridTemplateRows: 'repeat(auto-fill, 10em)',
|
||||
gridAutoRows: 'max-content',
|
||||
paddingBlock: '0.8em',
|
||||
'& > [role=row]': {
|
||||
@ -68,9 +69,9 @@ export default makeStyles(
|
||||
fontWeight: typography.fontWeightMedium,
|
||||
fontSize: '1em',
|
||||
border: `1px solid ${palette.divider}`,
|
||||
borderRadius: 6,
|
||||
borderRadius: '0.5em',
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
gap: '1em',
|
||||
'&:hover': {
|
||||
backgroundColor: palette.action.hover
|
||||
},
|
||||
|
@ -33,6 +33,7 @@ import {
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useVmTemplateApi } from 'client/features/One'
|
||||
|
||||
import { CloneForm } from 'client/components/Forms/VmTemplate'
|
||||
import { createActions } from 'client/components/Tables/Enhanced/Utils'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
import { VM_TEMPLATE_ACTIONS, MARKETPLACE_APP_ACTIONS } from 'client/constants'
|
||||
@ -40,7 +41,14 @@ import { VM_TEMPLATE_ACTIONS, MARKETPLACE_APP_ACTIONS } from 'client/constants'
|
||||
const Actions = () => {
|
||||
const history = useHistory()
|
||||
const { view, getResourceView } = useAuth()
|
||||
const { getVmTemplate, getVmTemplates, lock, unlock, remove } = useVmTemplateApi()
|
||||
const {
|
||||
getVmTemplate,
|
||||
getVmTemplates,
|
||||
lock,
|
||||
unlock,
|
||||
clone,
|
||||
remove
|
||||
} = useVmTemplateApi()
|
||||
|
||||
const vmTemplateActions = useMemo(() => createActions({
|
||||
filters: getResourceView('VM-TEMPLATE')?.actions,
|
||||
@ -106,8 +114,42 @@ const Actions = () => {
|
||||
label: 'Clone',
|
||||
tooltip: 'Clone',
|
||||
selected: true,
|
||||
disabled: true,
|
||||
options: [{ onSubmit: () => undefined }]
|
||||
dialogProps: {
|
||||
title: rows => {
|
||||
const isMultiple = rows?.length > 1
|
||||
const { ID, NAME } = rows?.[0]?.original
|
||||
|
||||
return [
|
||||
isMultiple ? 'Clone several Templates' : 'Clone Template',
|
||||
!isMultiple && `#${ID} ${NAME}`
|
||||
].filter(Boolean).join(' - ')
|
||||
}
|
||||
},
|
||||
options: [{
|
||||
form: rows => {
|
||||
const vmTemplates = rows?.map(({ original }) => original)
|
||||
const stepProps = { isMultiple: vmTemplates.length > 1 }
|
||||
const initialValues = { name: `Copy of ${vmTemplates?.[0]?.NAME}` }
|
||||
|
||||
return CloneForm(stepProps, initialValues)
|
||||
},
|
||||
onSubmit: async (formData, rows) => {
|
||||
try {
|
||||
const { prefix } = formData
|
||||
|
||||
const vmTemplates = rows?.map?.(({ original: { ID, NAME } = {} }) => {
|
||||
// overwrite all names with prefix+NAME
|
||||
const formatData = prefix ? { name: `${prefix} ${NAME}` } : {}
|
||||
|
||||
return { id: ID, data: { ...formData, ...formatData } }
|
||||
})
|
||||
|
||||
await Promise.all(vmTemplates.map(({ id, data }) => clone(id, data)))
|
||||
} finally {
|
||||
await getVmTemplates()
|
||||
}
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
tooltip: 'Change ownership',
|
||||
@ -157,7 +199,7 @@ const Actions = () => {
|
||||
isConfirmDialog: true,
|
||||
onSubmit: async (_, rows) => {
|
||||
const templateIds = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all([...new Array(templateIds)].map(id => lock(id)))
|
||||
await Promise.all(templateIds.map(id => lock(id)))
|
||||
await Promise.all(templateIds.map(id => getVmTemplate(id)))
|
||||
}
|
||||
}]
|
||||
@ -178,7 +220,7 @@ const Actions = () => {
|
||||
options: [{
|
||||
isConfirmDialog: true,
|
||||
onSubmit: async (_, rows) => {
|
||||
const templateIds = [...new Array(rows?.map?.(({ original }) => original?.ID))]
|
||||
const templateIds = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(templateIds.map(id => unlock(id)))
|
||||
await Promise.all(templateIds.map(id => getVmTemplate(id)))
|
||||
}
|
||||
@ -199,7 +241,7 @@ const Actions = () => {
|
||||
options: [{
|
||||
isConfirmDialog: true,
|
||||
onSubmit: async (_, rows) => {
|
||||
const templateIds = [...new Array(rows?.map?.(({ original }) => original?.ID))]
|
||||
const templateIds = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(templateIds.map(id => remove(id)))
|
||||
await getVmTemplates()
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { CategoryFilter } from 'client/components/Tables/Enhanced/Utils'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import { } from 'client/constants'
|
||||
|
||||
export default [
|
||||
{ Header: 'ID', accessor: 'ID', sortType: 'number' },
|
||||
@ -24,6 +25,11 @@ export default [
|
||||
{ Header: 'Group', accessor: 'GNAME' },
|
||||
{ Header: 'Start Time', accessor: 'REGTIME' },
|
||||
{ Header: 'Locked', accessor: 'LOCK' },
|
||||
{
|
||||
Header: 'Logo',
|
||||
id: 'LOGO',
|
||||
accessor: row => row?.TEMPLATE?.LOGO
|
||||
},
|
||||
{
|
||||
Header: 'Virtual Router',
|
||||
id: 'VROUTER',
|
||||
|
@ -14,6 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { User, Group, Lock } from 'iconoir-react'
|
||||
@ -21,18 +22,38 @@ import { Typography } from '@material-ui/core'
|
||||
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { rowStyles } from 'client/components/Tables/styles'
|
||||
import Image from 'client/components/Image'
|
||||
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import { isExternalURL } from 'client/utils'
|
||||
import { LOGO_IMAGES_URL } from 'client/constants'
|
||||
|
||||
const Row = ({ original, value, ...props }) => {
|
||||
const classes = rowStyles()
|
||||
const { ID, NAME, UNAME, GNAME, REGTIME, LOCK, VROUTER } = value
|
||||
const { ID, NAME, UNAME, GNAME, REGTIME, LOCK, VROUTER, LOGO = '' } = value
|
||||
|
||||
const [logoSource] = useMemo(() => {
|
||||
const external = isExternalURL(LOGO)
|
||||
const cleanLogoAttribute = String(LOGO).split('/').at(-1)
|
||||
const src = external ? LOGO : `${LOGO_IMAGES_URL}/${cleanLogoAttribute}`
|
||||
|
||||
return [src, external]
|
||||
}, [LOGO])
|
||||
|
||||
const logo = String(LOGO).split('/').at(-1)
|
||||
const time = Helper.timeFromMilliseconds(+REGTIME)
|
||||
const timeAgo = `registered ${time.toRelative()}`
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
{logo && (
|
||||
<div className={classes.figure}>
|
||||
<Image
|
||||
src={logoSource}
|
||||
imgProps={{ className: classes.image }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.main}>
|
||||
<div className={classes.title}>
|
||||
<Typography component='span'>
|
||||
@ -44,7 +65,7 @@ const Row = ({ original, value, ...props }) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className={classes.caption}>
|
||||
<span title={time.toFormat('ff')}>
|
||||
<span title={time.toFormat('ff')} className='full-width'>
|
||||
{`#${ID} ${timeAgo}`}
|
||||
</span>
|
||||
<span title={`Owner: ${UNAME}`}>
|
||||
|
@ -31,9 +31,25 @@ export const rowStyles = makeStyles(
|
||||
flexWrap: 'wrap'
|
||||
}
|
||||
},
|
||||
figure: {
|
||||
flexBasis: '20%',
|
||||
paddingTop: '12.5%',
|
||||
overflow: 'hidden',
|
||||
position: 'relative'
|
||||
},
|
||||
image: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
position: 'absolute',
|
||||
userSelect: 'none'
|
||||
},
|
||||
main: {
|
||||
flex: 'auto',
|
||||
overflow: 'hidden'
|
||||
overflow: 'hidden',
|
||||
alignSelf: 'center'
|
||||
},
|
||||
title: {
|
||||
color: palette.text.primary,
|
||||
@ -52,8 +68,12 @@ export const rowStyles = makeStyles(
|
||||
color: palette.text.secondary,
|
||||
marginTop: 4,
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
wordWrap: 'break-word'
|
||||
gap: '0.5em',
|
||||
flexWrap: 'wrap',
|
||||
wordWrap: 'break-word',
|
||||
'& > .full-width': {
|
||||
flexBasis: '100%'
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
width: '25%',
|
||||
|
@ -38,11 +38,8 @@ const VmStorageTab = ({ tabProps: { actions } = {} }) => {
|
||||
const hypervisor = VirtualMachine.getHypervisor(vm)
|
||||
const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor)
|
||||
|
||||
const handleAttachDisk = async ({ image, advanced, configuration }) => {
|
||||
const imageSelected = image?.[0]
|
||||
const root = { ...imageSelected, ...advanced, ...configuration }
|
||||
|
||||
const template = Helper.jsonToXml({ DISK: root })
|
||||
const handleAttachDisk = async formData => {
|
||||
const template = Helper.jsonToXml({ DISK: formData })
|
||||
|
||||
await attachDisk(vm.ID, template)
|
||||
}
|
||||
|
@ -16,31 +16,51 @@
|
||||
import * as STATES from 'client/constants/states'
|
||||
import COLOR from 'client/constants/color'
|
||||
|
||||
/**
|
||||
* @enum {(
|
||||
* 'OS'|
|
||||
* 'CD ROM'|
|
||||
* 'DATABLOCK'|
|
||||
* 'KERNEL'|
|
||||
* 'RAMDISK'|
|
||||
* 'CONTEXT'
|
||||
* )} Image type
|
||||
*/
|
||||
/** @enum {string} Image type */
|
||||
export const IMAGE_TYPES_STR = {
|
||||
OS: 'OS',
|
||||
CDROM: 'CDROM',
|
||||
DATABLOCK: 'DATABLOCK',
|
||||
KERNEL: 'KERNEL',
|
||||
RAMDISK: 'RAMDISK',
|
||||
CONTEXT: 'CONTEXT'
|
||||
}
|
||||
|
||||
/** @type {IMAGE_TYPES_STR[]} Return the string representation of an Image type */
|
||||
export const IMAGE_TYPES = [
|
||||
'OS',
|
||||
'CD ROM',
|
||||
'DATABLOCK',
|
||||
'KERNEL',
|
||||
'RAMDISK',
|
||||
'CONTEXT'
|
||||
IMAGE_TYPES_STR.OS,
|
||||
IMAGE_TYPES_STR.CDROM,
|
||||
IMAGE_TYPES_STR.DATABLOCK,
|
||||
IMAGE_TYPES_STR.KERNEL,
|
||||
IMAGE_TYPES_STR.RAMDISK,
|
||||
IMAGE_TYPES_STR.CONTEXT
|
||||
]
|
||||
|
||||
/** @enum {('FILE'|'CD ROM'|'BLOCK'|'RBD')} Disk type */
|
||||
/** @enum {string} Disk type */
|
||||
export const DISK_TYPES_STR = {
|
||||
FILE: 'FILE',
|
||||
CDROM: 'CDROM',
|
||||
BLOCK: 'BLOCK',
|
||||
RBD: 'RBD',
|
||||
RBD_CDROM: 'RBD CDROM',
|
||||
GLUSTER: 'GLUSTER',
|
||||
GLUSTER_CDROM: 'GLUSTER CDROM',
|
||||
SHEEPDOG: 'SHEEPDOG',
|
||||
SHEEPDOG_CDROM: 'SHEEPDOG CDROM',
|
||||
ISCSI: 'ISCII'
|
||||
}
|
||||
/** @enum {DISK_TYPES_STR[]} Return the string representation of a Disk type */
|
||||
export const DISK_TYPES = [
|
||||
'FILE',
|
||||
'CD ROM',
|
||||
'BLOCK',
|
||||
'RBD'
|
||||
DISK_TYPES_STR.FILE,
|
||||
DISK_TYPES_STR.CDROM,
|
||||
DISK_TYPES_STR.BLOCK,
|
||||
DISK_TYPES_STR.RBD,
|
||||
DISK_TYPES_STR.RBD_CDROM,
|
||||
DISK_TYPES_STR.GLUSTER,
|
||||
DISK_TYPES_STR.GLUSTER_CDROM,
|
||||
DISK_TYPES_STR.SHEEPDOG,
|
||||
DISK_TYPES_STR.SHEEPDOG_CDROM,
|
||||
DISK_TYPES_STR.ISCSI
|
||||
]
|
||||
|
||||
/** @type {STATES.StateInfo[]} Image states */
|
||||
|
@ -31,6 +31,7 @@ export const WEBSOCKET_URL = `${APP_URL}/websockets`
|
||||
export const STATIC_FILES_URL = `${APP_URL}/client/assets`
|
||||
|
||||
export const IMAGES_URL = `${STATIC_FILES_URL}/images`
|
||||
export const LOGO_IMAGES_URL = `${IMAGES_URL}/logos`
|
||||
export const PROVIDER_IMAGES_URL = `${IMAGES_URL}/providers`
|
||||
export const PROVISION_IMAGES_URL = `${IMAGES_URL}/provisions`
|
||||
export const DEFAULT_IMAGE = `${IMAGES_URL}/default.webp`
|
||||
|
@ -61,7 +61,7 @@ export const updateResourceList = (currentList, value) => {
|
||||
// update if exists in current list, if not add it to list
|
||||
const updatedList = currentItem
|
||||
? currentList?.map(item => item?.ID === id ? value : item)
|
||||
: [value, currentList]
|
||||
: [value, ...currentList]
|
||||
|
||||
return updatedList
|
||||
}
|
||||
|
@ -40,4 +40,4 @@ export const changePermissions = createAction(`${TEMPLATE}/chmod`, vmTemplateSer
|
||||
export const changeOwnership = createAction(`${TEMPLATE}/chown`, vmTemplateService.changeOwnership)
|
||||
export const rename = createAction(`${TEMPLATE}/rename`, vmTemplateService.rename)
|
||||
export const lock = createAction(`${TEMPLATE}/lock`, vmTemplateService.lock)
|
||||
export const unlock = createAction(`${TEMPLATE}/unlock`, vmTemplateService.lock)
|
||||
export const unlock = createAction(`${TEMPLATE}/unlock`, vmTemplateService.unlock)
|
||||
|
@ -61,7 +61,7 @@ import { set } from 'client/utils'
|
||||
* @property {SimpleCallback} handleSelect - Add an item to data form list
|
||||
* @property {SimpleCallback} handleClone - Clones an item and change two attributes
|
||||
* @property {SimpleCallback} handleEdit - Find the element by id and set value to editing state
|
||||
* @property {function(newValues, id)} handleSave - Saves the data from editing state
|
||||
* @property {SaveCallback} handleSave - Saves the data from editing state
|
||||
*/
|
||||
|
||||
// ----------------------------------------------------------
|
||||
@ -182,7 +182,7 @@ const useListForm = ({
|
||||
const index = EXISTS_INDEX(itemIndex) ? itemIndex : list.length
|
||||
|
||||
const newList = Object.assign([], [...list],
|
||||
{ [index]: getItemId(values) ? values : addItemId(values, id, itemIndex) }
|
||||
{ [index]: getItemId(values) ? values : addItemId(values, id, index) }
|
||||
)
|
||||
|
||||
handleSetList(newList)
|
||||
|
@ -22,7 +22,8 @@ import { IMAGE_TYPES, DISK_TYPES, IMAGE_STATES, StateInfo } from 'client/constan
|
||||
* @param {number|string} image.TYPE - Type numeric code
|
||||
* @returns {IMAGE_TYPES} - Image type
|
||||
*/
|
||||
export const getType = ({ TYPE } = {}) => IMAGE_TYPES[+TYPE]
|
||||
export const getType = ({ TYPE } = {}) =>
|
||||
isNaN(+TYPE) ? TYPE : IMAGE_TYPES[+TYPE]
|
||||
|
||||
/**
|
||||
* Returns the image state.
|
||||
@ -40,4 +41,5 @@ export const getState = ({ STATE } = {}) => IMAGE_STATES[+STATE]
|
||||
* @param {number|string} image.DISK_TYPE - Disk type numeric code
|
||||
* @returns {DISK_TYPES} - Disk type
|
||||
*/
|
||||
export const getDiskType = ({ DISK_TYPE } = {}) => DISK_TYPES[+DISK_TYPE]
|
||||
export const getDiskType = ({ DISK_TYPE } = {}) =>
|
||||
isNaN(+DISK_TYPE) ? DISK_TYPE : DISK_TYPES[+DISK_TYPE]
|
||||
|
@ -111,7 +111,7 @@ import { INPUT_TYPES } from 'client/constants'
|
||||
/**
|
||||
* @typedef {object} StepsForm
|
||||
* @property {Step[]} steps - Steps
|
||||
* @property {BaseSchema} resolver - Schema
|
||||
* @property {BaseSchema|function():BaseSchema} resolver - Schema
|
||||
* @property {object} defaultValues - Default values
|
||||
*/
|
||||
|
||||
@ -347,8 +347,13 @@ export const createForm = (schema, fields, extraParams = {}) =>
|
||||
const schemaCallback = typeof schema === 'function' ? schema(props) : schema
|
||||
const fieldsCallback = typeof fields === 'function' ? fields(props) : fields
|
||||
|
||||
const defaultTransformInitialValue = (values, schema) =>
|
||||
schema.cast(values, { stripUnknown: true })
|
||||
|
||||
const { transformInitialValue = defaultTransformInitialValue } = extraParams
|
||||
|
||||
const defaultValues = initialValues
|
||||
? schemaCallback.cast(initialValues, { stripUnknown: true })
|
||||
? transformInitialValue(initialValues, schemaCallback)
|
||||
: schemaCallback.default()
|
||||
|
||||
return {
|
||||
|