mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-21 14:50:08 +03:00
parent
9e6a155a5d
commit
62afe391b2
@ -87,13 +87,22 @@ dialogs:
|
||||
sched_action: true
|
||||
booting: true
|
||||
create_dialog:
|
||||
general: true
|
||||
capacity: true
|
||||
ownership: true
|
||||
vm_group: true
|
||||
vcenter:
|
||||
enabled: true
|
||||
not_on:
|
||||
- vcenter
|
||||
storage:
|
||||
enabled: true
|
||||
- kvm
|
||||
- lxc
|
||||
- firecracker
|
||||
network: true
|
||||
storage: true
|
||||
placement: true
|
||||
input_output: true
|
||||
sched_action: true
|
||||
context: true
|
||||
booting: true
|
||||
numa:
|
||||
enabled: true
|
||||
no_on:
|
||||
|
@ -87,15 +87,25 @@ dialogs:
|
||||
sched_action: true
|
||||
booting: true
|
||||
create_dialog:
|
||||
general: true
|
||||
information: true
|
||||
capacity: true
|
||||
ownership: true
|
||||
vm_group: true
|
||||
vcenter:
|
||||
enabled: true
|
||||
hypervisor:
|
||||
- vcenter
|
||||
storage:
|
||||
enabled: true
|
||||
not_on:
|
||||
- kvm
|
||||
- lxc
|
||||
- firecracker
|
||||
network: true
|
||||
storage: true
|
||||
placement: true
|
||||
input_output: true
|
||||
sched_action: true
|
||||
context: true
|
||||
booting: true
|
||||
numa:
|
||||
enabled: true
|
||||
hypervisors:
|
||||
no_on:
|
||||
- vcenter
|
||||
- kvm
|
||||
|
5457
src/fireedge/package-lock.json
generated
5457
src/fireedge/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -30,7 +30,7 @@
|
||||
"vms"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.0-rc.3",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.1",
|
||||
"cross-env": "7.0.2",
|
||||
"eslint": "7.11.0",
|
||||
"eslint-config-prettier": "6.11.0",
|
||||
@ -48,22 +48,23 @@
|
||||
"opennebula-generatepotfile": "1.0.0",
|
||||
"opennebula-potojson": "1.0.0",
|
||||
"react-refresh": "0.10.0",
|
||||
"webpack-dev-middleware": "5.0.0",
|
||||
"webpack-hot-middleware": "2.25.0"
|
||||
"webpack-dev-middleware": "5.2.1",
|
||||
"webpack-hot-middleware": "2.25.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "7.14.5",
|
||||
"@babel/core": "7.12.13",
|
||||
"@babel/node": "7.12.13",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.13",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.12.13",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.12.13",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.12.13",
|
||||
"@babel/preset-env": "7.12.13",
|
||||
"@babel/preset-react": "7.12.13",
|
||||
"@babel/cli": "7.15.7",
|
||||
"@babel/core": "7.15.8",
|
||||
"@babel/node": "7.15.8",
|
||||
"@babel/plugin-proposal-class-properties": "7.14.5",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.14.5",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.15.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.14.5",
|
||||
"@babel/preset-env": "7.15.8",
|
||||
"@babel/preset-react": "7.14.5",
|
||||
"@emotion/react": "11.4.1",
|
||||
"@emotion/styled": "11.3.0",
|
||||
"@hookform/resolvers": "1.3.7",
|
||||
"@hookform/devtools": "4.0.1",
|
||||
"@hookform/resolvers": "2.8.2",
|
||||
"@loadable/babel-plugin": "5.13.2",
|
||||
"@loadable/component": "5.15.0",
|
||||
"@loadable/server": "5.15.1",
|
||||
@ -72,9 +73,9 @@
|
||||
"@mui/material": "5.0.2",
|
||||
"@mui/styles": "5.0.1",
|
||||
"@mui/system": "5.0.2",
|
||||
"@reduxjs/toolkit": "1.5.1",
|
||||
"@reduxjs/toolkit": "1.6.2",
|
||||
"atob": "2.1.2",
|
||||
"axios": "0.21.1",
|
||||
"axios": "0.23.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-loader": "8.2.1",
|
||||
"babel-plugin-module-resolver": "4.0.0",
|
||||
@ -117,17 +118,17 @@
|
||||
"react-dom": "17.0.2",
|
||||
"react-flatpickr": "3.10.7",
|
||||
"react-flow-renderer": "9.6.0",
|
||||
"react-hook-form": "6.12.0",
|
||||
"react-hook-form": "7.17.4",
|
||||
"react-json-pretty": "2.2.0",
|
||||
"react-minimal-pie-chart": "8.2.0",
|
||||
"react-opennebula-ace": "1.0.1",
|
||||
"react-redux": "7.2.4",
|
||||
"react-redux": "7.2.5",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
"react-table": "7.7.0",
|
||||
"react-transition-group": "4.4.1",
|
||||
"react-virtual": "2.7.1",
|
||||
"redux": "4.1.0",
|
||||
"redux": "4.1.1",
|
||||
"redux-thunk": "2.3.0",
|
||||
"rimraf": "3.0.2",
|
||||
"socket.io": "4.1.2",
|
||||
|
@ -29,6 +29,9 @@ import { TranslateProvider } from 'client/components/HOC'
|
||||
import App, { APP_NAME as ProvisionAppName } from 'client/apps/provision/_app'
|
||||
import theme from 'client/apps/provision/theme'
|
||||
import { APP_URL } from 'client/constants'
|
||||
import { buildTranslationLocale } from 'client/utils'
|
||||
|
||||
buildTranslationLocale()
|
||||
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
|
@ -28,6 +28,9 @@ import { TranslateProvider } from 'client/components/HOC'
|
||||
import App, { APP_NAME as SunstoneAppName } from 'client/apps/sunstone/_app'
|
||||
import theme from 'client/apps/sunstone/theme'
|
||||
import { APP_URL } from 'client/constants'
|
||||
import { buildTranslationLocale } from 'client/utils'
|
||||
|
||||
buildTranslationLocale()
|
||||
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
|
@ -49,8 +49,9 @@ const VirtualMachineDetail = loadable(() => import('client/containers/VirtualMac
|
||||
const VirtualRouters = loadable(() => import('client/containers/VirtualRouters'), { ssr: false })
|
||||
|
||||
const VmTemplates = loadable(() => import('client/containers/VmTemplates'), { ssr: false })
|
||||
const InstantiateVmTemplates =
|
||||
const InstantiateVmTemplate =
|
||||
loadable(() => import('client/containers/VmTemplates/Instantiate'), { ssr: false })
|
||||
const CreateVmTemplate = loadable(() => import('client/containers/VmTemplates/Create'), { ssr: false })
|
||||
// const VrTemplates = loadable(() => import('client/containers/VrTemplates'), { ssr: false })
|
||||
// const VmGroups = loadable(() => import('client/containers/VmGroups'), { ssr: false })
|
||||
|
||||
@ -92,7 +93,8 @@ export const PATH = {
|
||||
VMS: {
|
||||
LIST: '/vm-template',
|
||||
DETAIL: '/vm-template/:id',
|
||||
INSTANTIATE: '/vm-template/instantiate'
|
||||
INSTANTIATE: '/vm-template/instantiate',
|
||||
CREATE: '/vm-template/create'
|
||||
}
|
||||
},
|
||||
STORAGE: {
|
||||
@ -199,7 +201,12 @@ const ENDPOINTS = [
|
||||
{
|
||||
label: 'Instantiate VM Template',
|
||||
path: PATH.TEMPLATE.VMS.INSTANTIATE,
|
||||
Component: InstantiateVmTemplates
|
||||
Component: InstantiateVmTemplate
|
||||
},
|
||||
{
|
||||
label: 'Create VM Template',
|
||||
path: PATH.TEMPLATE.VMS.CREATE,
|
||||
Component: CreateVmTemplate
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -19,7 +19,7 @@
|
||||
export const _regANSI = /(?:(?:\u001b\[)|\u009b)(?:(?:[0-9]{1,3})?(?:(?:;[0-9]{0,3})*)?[A-M|f-m])|\u001b[A-M]/
|
||||
|
||||
const _defColors = {
|
||||
reset: ['fff', '000'], // [FOREGROUD_COLOR, BACKGROUND_COLOR]
|
||||
reset: ['fff', '000'], // [FOREGROUND_COLOR, BACKGROUND_COLOR]
|
||||
black: '000',
|
||||
red: 'ff0000',
|
||||
green: '209805',
|
||||
|
@ -17,7 +17,9 @@ import { memo, useEffect } from 'react'
|
||||
|
||||
import { styled, Link, Typography } from '@mui/material'
|
||||
|
||||
import { useFetch } from 'client/hooks'
|
||||
import { useSystem, useSystemApi } from 'client/features/One'
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { BY } from 'client/constants'
|
||||
|
||||
const FooterBox = styled('footer')(({ theme }) => ({
|
||||
@ -44,9 +46,10 @@ const HeartIcon = styled('span')(({ theme }) => ({
|
||||
const Footer = memo(() => {
|
||||
const { version } = useSystem()
|
||||
const { getOneVersion } = useSystemApi()
|
||||
const { fetchRequest } = useFetch(getOneVersion)
|
||||
|
||||
useEffect(() => {
|
||||
!version && getOneVersion()
|
||||
!version && fetchRequest()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@ -56,8 +59,10 @@ const Footer = memo(() => {
|
||||
<HeartIcon role='img' aria-label='heart-emoji' />
|
||||
<Link href={BY.url} color='primary.contrastText'>
|
||||
{BY.text}
|
||||
{version}
|
||||
</Link>
|
||||
{version && (
|
||||
<StatusChip stateColor='secondary' text={version} mx={1} />
|
||||
)}
|
||||
</Typography>
|
||||
</FooterBox>
|
||||
)
|
||||
|
@ -17,70 +17,76 @@ import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { TextField, Chip, Autocomplete } from '@mui/material'
|
||||
import { Controller } from 'react-hook-form'
|
||||
import { useController } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { generateKey } from 'client/utils'
|
||||
|
||||
const AutocompleteController = memo(
|
||||
({ control, cy, name, label, multiple, values, error, fieldProps }) => (
|
||||
<Controller
|
||||
render={({ value: renderValue, onBlur, onChange }) => {
|
||||
const selected = multiple
|
||||
? renderValue ?? []
|
||||
: values.find(({ value }) => value === renderValue) ?? null
|
||||
({
|
||||
control,
|
||||
cy = `autocomplete-${generateKey()}`,
|
||||
name = '',
|
||||
label = '',
|
||||
multiple = false,
|
||||
values = [],
|
||||
fieldProps = {}
|
||||
}) => {
|
||||
const {
|
||||
field: { value: renderValue, onBlur, onChange },
|
||||
fieldState: { error }
|
||||
} = useController({ name, control })
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
color='secondary'
|
||||
onBlur={onBlur}
|
||||
onChange={(_, newValue) => {
|
||||
const newValueToChange = multiple
|
||||
? newValue?.map(value =>
|
||||
typeof value === 'string' ? value : ({ text: value, value })
|
||||
)
|
||||
: newValue?.value
|
||||
const selected = multiple
|
||||
? renderValue ?? []
|
||||
: values.find(({ value }) => value === renderValue) ?? null
|
||||
|
||||
return onChange(newValueToChange ?? '')
|
||||
}}
|
||||
options={values}
|
||||
value={selected}
|
||||
multiple={multiple}
|
||||
renderTags={(tags, getTagProps) =>
|
||||
// render when freesolo prop
|
||||
tags.map((tag, index) => (
|
||||
<Chip
|
||||
key={tag}
|
||||
size='small'
|
||||
variant='outlined'
|
||||
label={tag}
|
||||
{...getTagProps({ index })}
|
||||
/>
|
||||
))
|
||||
}
|
||||
getOptionLabel={option => option.text}
|
||||
isOptionEqualToValue={option => option.value === renderValue}
|
||||
renderInput={({ inputProps, ...inputParams }) => (
|
||||
<TextField
|
||||
label={Tr(label)}
|
||||
inputProps={{ ...inputProps, 'data-cy': cy }}
|
||||
error={Boolean(error)}
|
||||
helperText={
|
||||
Boolean(error) && <ErrorHelper label={error?.message} />
|
||||
}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
{...inputParams}
|
||||
/>
|
||||
)}
|
||||
{...fieldProps}
|
||||
return (
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
color='secondary'
|
||||
onBlur={onBlur}
|
||||
onChange={(_, newValue) => {
|
||||
const newValueToChange = multiple
|
||||
? newValue?.map(value =>
|
||||
typeof value === 'string' ? value : ({ text: value, value })
|
||||
)
|
||||
: newValue?.value
|
||||
|
||||
return onChange(newValueToChange ?? '')
|
||||
}}
|
||||
options={values}
|
||||
value={selected}
|
||||
multiple={multiple}
|
||||
renderTags={(tags, getTagProps) =>
|
||||
// render when freesolo prop
|
||||
tags.map((tag, index) => (
|
||||
<Chip
|
||||
key={tag}
|
||||
size='small'
|
||||
variant='outlined'
|
||||
label={tag}
|
||||
{...getTagProps({ index })}
|
||||
/>
|
||||
))
|
||||
}
|
||||
getOptionLabel={option => option.text}
|
||||
isOptionEqualToValue={option => option.value === renderValue}
|
||||
renderInput={({ inputProps, ...inputParams }) => (
|
||||
<TextField
|
||||
label={labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
inputProps={{ ...inputProps, 'data-cy': cy }}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
{...inputParams}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
name={name}
|
||||
control={control}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{...fieldProps}
|
||||
/>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) => (
|
||||
prevProps.error === nextProps.error &&
|
||||
prevProps.values === nextProps.values
|
||||
@ -90,27 +96,12 @@ AutocompleteController.propTypes = {
|
||||
control: PropTypes.object,
|
||||
cy: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
label: PropTypes.any,
|
||||
multiple: PropTypes.bool,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.objectOf(PropTypes.any)
|
||||
]),
|
||||
fieldProps: PropTypes.object
|
||||
}
|
||||
|
||||
AutocompleteController.defaultProps = {
|
||||
control: {},
|
||||
cy: 'cy',
|
||||
name: '',
|
||||
label: '',
|
||||
multiple: false,
|
||||
values: [],
|
||||
error: false,
|
||||
fieldProps: undefined
|
||||
}
|
||||
|
||||
AutocompleteController.displayName = 'AutocompleteController'
|
||||
|
||||
export default AutocompleteController
|
||||
|
@ -16,43 +16,62 @@
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { FormControl, FormControlLabel, Checkbox } from '@mui/material'
|
||||
import { Controller } from 'react-hook-form'
|
||||
import { styled, FormControl, FormControlLabel, FormHelperText, Checkbox } from '@mui/material'
|
||||
import { useController } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper, Tooltip } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { generateKey } from 'client/utils'
|
||||
|
||||
const Label = styled('span')({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em'
|
||||
})
|
||||
|
||||
const CheckboxController = memo(
|
||||
({ control, cy, name, label, tooltip, error, fieldProps }) => (
|
||||
<Controller
|
||||
render={({ onChange, value = false }) => (
|
||||
<FormControl error={Boolean(error)} margin='dense'>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
onChange={e => onChange(e.target.checked)}
|
||||
name={name}
|
||||
checked={Boolean(value)}
|
||||
color='secondary'
|
||||
inputProps={{ 'data-cy': cy }}
|
||||
{...fieldProps}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5em' }}>
|
||||
{Tr(label)}
|
||||
{tooltip && <Tooltip title={tooltip} />}
|
||||
</span>
|
||||
}
|
||||
labelPlacement='end'
|
||||
/>
|
||||
{Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
</FormControl>
|
||||
)}
|
||||
name={name}
|
||||
control={control}
|
||||
/>
|
||||
),
|
||||
({
|
||||
control,
|
||||
cy = `checkbox-${generateKey()}`,
|
||||
name = '',
|
||||
label = '',
|
||||
tooltip,
|
||||
fieldProps = {}
|
||||
}) => {
|
||||
const {
|
||||
field: { value = false, onChange },
|
||||
fieldState: { error }
|
||||
} = useController({ name, control })
|
||||
|
||||
return (
|
||||
<FormControl fullWidth error={Boolean(error)} margin='dense'>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
onChange={e => onChange(e.target.checked)}
|
||||
name={name}
|
||||
checked={Boolean(value)}
|
||||
color='secondary'
|
||||
inputProps={{ 'data-cy': cy }}
|
||||
{...fieldProps}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Label>
|
||||
{labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
{tooltip && <Tooltip title={tooltip} />}
|
||||
</Label>
|
||||
}
|
||||
labelPlacement='end'
|
||||
/>
|
||||
{Boolean(error) && (
|
||||
<FormHelperText data-cy={`${cy}-error`}>
|
||||
<ErrorHelper label={error?.message} />
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) => prevProps.error === nextProps.error
|
||||
)
|
||||
|
||||
@ -60,24 +79,11 @@ CheckboxController.propTypes = {
|
||||
control: PropTypes.object,
|
||||
cy: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
tooltip: PropTypes.string,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.objectOf(PropTypes.any)
|
||||
]),
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
fieldProps: PropTypes.object
|
||||
}
|
||||
|
||||
CheckboxController.defaultProps = {
|
||||
control: {},
|
||||
cy: 'cy',
|
||||
name: '',
|
||||
label: '',
|
||||
values: [],
|
||||
error: false
|
||||
}
|
||||
|
||||
CheckboxController.displayName = 'CheckboxController'
|
||||
|
||||
export default CheckboxController
|
||||
|
@ -14,46 +14,30 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo } from 'react'
|
||||
import { string } from 'prop-types'
|
||||
import { oneOfType, string, node } from 'prop-types'
|
||||
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Stack, Typography, styled } from '@mui/material'
|
||||
import { WarningCircledOutline as WarningIcon } from 'iconoir-react'
|
||||
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
color: theme.palette.error.dark,
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
text: {
|
||||
...theme.typography.body1,
|
||||
paddingLeft: theme.spacing(1),
|
||||
overflowWrap: 'anywhere'
|
||||
}
|
||||
const ErrorTypo = styled(Typography)(({ theme }) => ({
|
||||
...theme.typography.body1,
|
||||
paddingLeft: theme.spacing(1),
|
||||
overflowWrap: 'anywhere'
|
||||
}))
|
||||
|
||||
const ErrorHelper = memo(({ label, ...rest }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Box component='span' className={classes.root} {...rest}>
|
||||
<WarningIcon />
|
||||
<Typography className={classes.text} component='span' data-cy='error-text'>
|
||||
{Tr(label)}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
const ErrorHelper = memo(({ label, ...rest }) => (
|
||||
<Stack component='span' color='error.dark' direction='row' alignItems='center' {...rest}>
|
||||
<WarningIcon />
|
||||
<ErrorTypo component='span' data-cy='error-text'>
|
||||
{labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
</ErrorTypo>
|
||||
</Stack>
|
||||
))
|
||||
|
||||
ErrorHelper.propTypes = {
|
||||
label: string
|
||||
}
|
||||
|
||||
ErrorHelper.defaultProps = {
|
||||
label: 'Error'
|
||||
label: oneOfType([string, node])
|
||||
}
|
||||
|
||||
ErrorHelper.displayName = 'ErrorHelper'
|
||||
|
@ -15,44 +15,45 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useState, useRef, useEffect, ChangeEvent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { FormControl, FormHelperText } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { styled, FormControl, FormHelperText } from '@mui/material'
|
||||
import { Check as CheckIcon, Page as FileIcon } from 'iconoir-react'
|
||||
import { Controller } from 'react-hook-form'
|
||||
import { useController } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper, SubmitButton } from 'client/components/FormControl'
|
||||
import { ErrorHelper, Tooltip, SubmitButton } from 'client/components/FormControl'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { generateKey } from 'client/utils'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
hide: {
|
||||
display: 'none'
|
||||
},
|
||||
label: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1em',
|
||||
padding: '0.5em',
|
||||
borderBottom: `1px solid ${theme.palette.text.secondary}`
|
||||
},
|
||||
button: {
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.secondary.dark
|
||||
}
|
||||
},
|
||||
buttonSuccess: {
|
||||
backgroundColor: theme.palette.success.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.success.dark
|
||||
}
|
||||
}
|
||||
const HiddenInput = styled('input')({ display: 'none' })
|
||||
|
||||
const Label = styled('label')(({ theme, error }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1em',
|
||||
...(error && {
|
||||
color: theme.palette.error.main
|
||||
})
|
||||
}))
|
||||
|
||||
const FileController = memo(
|
||||
({ control, cy, name, label, error, fieldProps, validationBeforeTransform, transform, formContext }) => {
|
||||
({
|
||||
control,
|
||||
cy = `input-file-${generateKey()}`,
|
||||
name = '',
|
||||
label = '',
|
||||
tooltip = '',
|
||||
validationBeforeTransform,
|
||||
transform,
|
||||
fieldProps = {},
|
||||
formContext = {}
|
||||
}) => {
|
||||
const { setValue, setError, clearErrors, watch } = formContext
|
||||
|
||||
const classes = useStyles()
|
||||
const {
|
||||
field: { ref, value, onChange, ...inputProps },
|
||||
fieldState: { error }
|
||||
} = useController({ name, control })
|
||||
|
||||
const [isLoading, setLoading] = useState(() => false)
|
||||
const [success, setSuccess] = useState(() => !error && !!watch(name))
|
||||
const timer = useRef()
|
||||
@ -107,33 +108,26 @@ const FileController = memo(
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl fullWidth>
|
||||
<Controller
|
||||
render={() => (
|
||||
<input
|
||||
className={classes.hide}
|
||||
id={cy}
|
||||
type='file'
|
||||
onChange={handleChange}
|
||||
{...fieldProps}
|
||||
/>
|
||||
)}
|
||||
name={name}
|
||||
control={control}
|
||||
<FormControl fullWidth margin='dense'>
|
||||
<HiddenInput
|
||||
{...inputProps}
|
||||
ref={ref}
|
||||
id={cy}
|
||||
type='file'
|
||||
onChange={handleChange}
|
||||
{...fieldProps}
|
||||
/>
|
||||
<label htmlFor={cy} className={classes.label}>
|
||||
<Label htmlFor={cy} error={error ? 'error' : undefined}>
|
||||
<SubmitButton
|
||||
color='secondary'
|
||||
color={success ? 'success' : 'secondary'}
|
||||
component='span'
|
||||
data-cy={`${cy}-button`}
|
||||
isSubmitting={isLoading}
|
||||
label={success ? <CheckIcon /> : <FileIcon />}
|
||||
className={clsx({
|
||||
[classes.buttonSuccess]: success
|
||||
})}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
{labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
{tooltip && <Tooltip title={tooltip} />}
|
||||
</Label>
|
||||
{Boolean(error) && (
|
||||
<FormHelperText data-cy={`${cy}-error`}>
|
||||
<ErrorHelper label={error?.message} />
|
||||
@ -143,18 +137,17 @@ const FileController = memo(
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.error === nextProps.error && prevProps.type === nextProps.type
|
||||
prevProps.error === nextProps.error &&
|
||||
prevProps.type === nextProps.type &&
|
||||
prevProps.label === nextProps.label
|
||||
)
|
||||
|
||||
FileController.propTypes = {
|
||||
control: PropTypes.object,
|
||||
cy: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.objectOf(PropTypes.any)
|
||||
]),
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
validationBeforeTransform: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
message: PropTypes.string,
|
||||
@ -172,17 +165,6 @@ FileController.propTypes = {
|
||||
})
|
||||
}
|
||||
|
||||
FileController.defaultProps = {
|
||||
control: {},
|
||||
cy: 'cy',
|
||||
name: '',
|
||||
label: '',
|
||||
error: false,
|
||||
validationBeforeTransform: undefined,
|
||||
transform: undefined,
|
||||
fieldProps: undefined
|
||||
}
|
||||
|
||||
FileController.displayName = 'FileController'
|
||||
|
||||
export default FileController
|
||||
|
@ -38,7 +38,7 @@ const PasswordController = memo(({ fieldProps, ...props }) => {
|
||||
<IconButton
|
||||
aria-label='toggle password visibility'
|
||||
onClick={handleClickShowPassword}
|
||||
size='large'>
|
||||
>
|
||||
{showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
@ -49,7 +49,8 @@ const PasswordController = memo(({ fieldProps, ...props }) => {
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.error === nextProps.error && prevProps.type === nextProps.type
|
||||
prevProps.error === nextProps.error &&
|
||||
prevProps.type === nextProps.type
|
||||
)
|
||||
|
||||
PasswordController.propTypes = {
|
||||
|
@ -13,67 +13,79 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo } from 'react'
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { TextField } from '@mui/material'
|
||||
import { Controller } from 'react-hook-form'
|
||||
import { useController } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper, Tooltip } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { generateKey } from 'client/utils'
|
||||
|
||||
const SelectController = memo(
|
||||
({ control, cy, name, label, multiple, values, tooltip, error, fieldProps }) => {
|
||||
({
|
||||
control,
|
||||
cy = `select-${generateKey()}`,
|
||||
name = '',
|
||||
label = '',
|
||||
multiple = false,
|
||||
values = [],
|
||||
renderValue,
|
||||
tooltip,
|
||||
fieldProps = {}
|
||||
}) => {
|
||||
const defaultValue = multiple ? [values?.[0]?.value] : values?.[0]?.value
|
||||
|
||||
const {
|
||||
field: { ref, value: optionSelected = defaultValue, onChange, ...inputProps },
|
||||
fieldState: { error }
|
||||
} = useController({ name, control })
|
||||
|
||||
const needShrink = useMemo(
|
||||
() => values.find(v => v.value === optionSelected)?.text !== '',
|
||||
[optionSelected]
|
||||
)
|
||||
|
||||
return (
|
||||
<Controller
|
||||
render={({ value: optionSelected, onChange, onBlur }) => (
|
||||
<TextField
|
||||
value={optionSelected ?? defaultValue}
|
||||
onBlur={onBlur}
|
||||
onChange={
|
||||
multiple
|
||||
? event => {
|
||||
const { options } = event.target
|
||||
const newValue = []
|
||||
<TextField
|
||||
{...inputProps}
|
||||
inputRef={ref}
|
||||
value={optionSelected}
|
||||
onChange={
|
||||
multiple
|
||||
? event => {
|
||||
const { options = [] } = event.target
|
||||
const newValue = options
|
||||
.filter(option => option.selected)
|
||||
.map(option => option.value)
|
||||
|
||||
for (let i = 0, l = options.length; i < l; i += 1) {
|
||||
if (options[i].selected) {
|
||||
newValue.push(options[i].value)
|
||||
}
|
||||
}
|
||||
|
||||
onChange(newValue)
|
||||
}
|
||||
: onChange
|
||||
onChange(newValue)
|
||||
}
|
||||
select
|
||||
fullWidth
|
||||
SelectProps={{ native: true, multiple }}
|
||||
label={Tr(label)}
|
||||
InputProps={{
|
||||
startAdornment: tooltip && (
|
||||
<Tooltip title={tooltip} position='start' />
|
||||
)
|
||||
}}
|
||||
inputProps={{ 'data-cy': cy }}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
{...fieldProps}
|
||||
>
|
||||
{values?.map(({ text, value = '' }) =>
|
||||
<option key={`${name}-${value}`} value={value}>
|
||||
{text}
|
||||
</option>
|
||||
)}
|
||||
</TextField>
|
||||
: onChange
|
||||
}
|
||||
select
|
||||
fullWidth
|
||||
SelectProps={{ native: true, multiple }}
|
||||
label={labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
InputLabelProps={{ shrink: needShrink }}
|
||||
InputProps={{
|
||||
startAdornment:
|
||||
(optionSelected && renderValue?.(optionSelected)) ||
|
||||
(tooltip && <Tooltip title={tooltip} position='start' />)
|
||||
}}
|
||||
inputProps={{ 'data-cy': cy }}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
{...fieldProps}
|
||||
>
|
||||
{values?.map(({ text, value = '' }) =>
|
||||
<option key={`${name}-${value}`} value={value}>
|
||||
{text}
|
||||
</option>
|
||||
)}
|
||||
name={name}
|
||||
control={control}
|
||||
multiple={multiple}
|
||||
/>
|
||||
</TextField>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
@ -87,28 +99,14 @@ SelectController.propTypes = {
|
||||
control: PropTypes.object,
|
||||
cy: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
multiple: PropTypes.bool,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tooltip: PropTypes.string,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.objectOf(PropTypes.any)
|
||||
]),
|
||||
renderValue: PropTypes.func,
|
||||
fieldProps: PropTypes.object
|
||||
}
|
||||
|
||||
SelectController.defaultProps = {
|
||||
control: {},
|
||||
cy: 'cy',
|
||||
name: '',
|
||||
label: '',
|
||||
multiple: false,
|
||||
values: [],
|
||||
error: false,
|
||||
fieldProps: undefined
|
||||
}
|
||||
|
||||
SelectController.displayName = 'SelectController'
|
||||
|
||||
export default SelectController
|
||||
|
@ -17,60 +17,71 @@ import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Typography, TextField, Slider, FormHelperText, Grid } from '@mui/material'
|
||||
import { Controller } from 'react-hook-form'
|
||||
import { useController } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { generateKey } from 'client/utils'
|
||||
|
||||
const SliderController = memo(
|
||||
({ control, cy, name, label, error, fieldProps }) => (
|
||||
<>
|
||||
<Typography id={`slider-${name}`} gutterBottom>
|
||||
{Tr(label)}
|
||||
</Typography>
|
||||
<Controller
|
||||
render={({ value, onChange, onBlur }) =>
|
||||
<Grid container spacing={2} alignItems='center'>
|
||||
<Grid item xs>
|
||||
<Slider
|
||||
color='secondary'
|
||||
value={typeof value === 'number' ? value : 0}
|
||||
aria-labelledby={`slider-${name}`}
|
||||
valueLabelDisplay='auto'
|
||||
data-cy={`${cy}-slider`}
|
||||
{...fieldProps}
|
||||
onChange={(_, val) => onChange(val)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={value ?? ''}
|
||||
error={Boolean(error)}
|
||||
type='number'
|
||||
inputProps={{
|
||||
'data-cy': `${cy}-input`,
|
||||
'aria-labelledby': `slider-${name}`,
|
||||
...fieldProps
|
||||
}}
|
||||
onChange={evt => onChange(
|
||||
evt.target.value === '' ? '0' : Number(evt.target.value)
|
||||
)}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</Grid>
|
||||
({
|
||||
control,
|
||||
cy = `slider-${generateKey()}`,
|
||||
name = '',
|
||||
label = '',
|
||||
fieldProps = {}
|
||||
}) => {
|
||||
const {
|
||||
field: { value, onChange, ...inputProps },
|
||||
fieldState: { error }
|
||||
} = useController({ name, control })
|
||||
|
||||
const sliderId = `${cy}-slider`
|
||||
const inputId = `${cy}-input`
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography id={sliderId} gutterBottom>
|
||||
{labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
</Typography>
|
||||
<Grid container spacing={2} alignItems='center'>
|
||||
<Grid item xs>
|
||||
<Slider
|
||||
color='secondary'
|
||||
value={typeof value === 'number' ? value : 0}
|
||||
aria-labelledby={sliderId}
|
||||
valueLabelDisplay='auto'
|
||||
data-cy={sliderId}
|
||||
onChange={(_, val) => onChange(val)}
|
||||
{...fieldProps}
|
||||
/>
|
||||
</Grid>
|
||||
}
|
||||
name={name}
|
||||
control={control}
|
||||
/>
|
||||
{Boolean(error) && (
|
||||
<FormHelperText data-cy={`${cy}-error`}>
|
||||
<ErrorHelper label={error?.message} />
|
||||
</FormHelperText>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
<Grid item>
|
||||
<TextField
|
||||
{...inputProps}
|
||||
fullWidth
|
||||
value={value}
|
||||
error={Boolean(error)}
|
||||
type='number'
|
||||
inputProps={{
|
||||
'data-cy': inputId,
|
||||
'aria-labelledby': sliderId,
|
||||
...fieldProps
|
||||
}}
|
||||
onChange={evt => onChange(
|
||||
evt.target.value === '' ? '0' : Number(evt.target.value)
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{Boolean(error) && (
|
||||
<FormHelperText data-cy={`${cy}-error`}>
|
||||
<ErrorHelper label={error?.message} />
|
||||
</FormHelperText>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) => prevProps.error === nextProps.error
|
||||
)
|
||||
|
||||
@ -78,27 +89,13 @@ SliderController.propTypes = {
|
||||
control: PropTypes.object,
|
||||
cy: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
multiple: PropTypes.bool,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.objectOf(PropTypes.any)
|
||||
]),
|
||||
fieldProps: PropTypes.object
|
||||
}
|
||||
|
||||
SliderController.defaultProps = {
|
||||
control: {},
|
||||
cy: 'cy',
|
||||
name: '',
|
||||
label: '',
|
||||
multiple: false,
|
||||
values: [],
|
||||
error: false,
|
||||
fieldProps: undefined
|
||||
}
|
||||
|
||||
SliderController.displayName = 'SliderController'
|
||||
|
||||
export default SliderController
|
||||
|
@ -16,43 +16,62 @@
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { FormControl, FormControlLabel, Switch } from '@mui/material'
|
||||
import { Controller } from 'react-hook-form'
|
||||
import { styled, FormControl, FormControlLabel, FormHelperText, Switch } from '@mui/material'
|
||||
import { useController } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper, Tooltip } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { generateKey } from 'client/utils'
|
||||
|
||||
const Label = styled('span')({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em'
|
||||
})
|
||||
|
||||
const SwitchController = memo(
|
||||
({ control, cy, name, label, tooltip, error, fieldProps }) => (
|
||||
<Controller
|
||||
render={({ onChange, value = false }) => (
|
||||
<FormControl error={Boolean(error)} margin='dense'>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
onChange={e => onChange(e.target.checked)}
|
||||
name={name}
|
||||
checked={Boolean(value)}
|
||||
color='secondary'
|
||||
inputProps={{ 'data-cy': cy }}
|
||||
{...fieldProps}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5em' }}>
|
||||
{Tr(label)}
|
||||
{tooltip && <Tooltip title={tooltip} />}
|
||||
</span>
|
||||
}
|
||||
labelPlacement='end'
|
||||
/>
|
||||
{Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
</FormControl>
|
||||
)}
|
||||
name={name}
|
||||
control={control}
|
||||
/>
|
||||
),
|
||||
({
|
||||
control,
|
||||
cy = `switch-${generateKey()}`,
|
||||
name = '',
|
||||
label = '',
|
||||
tooltip,
|
||||
fieldProps = {}
|
||||
}) => {
|
||||
const {
|
||||
field: { value = false, onChange },
|
||||
fieldState: { error }
|
||||
} = useController({ name, control })
|
||||
|
||||
return (
|
||||
<FormControl fullWidth error={Boolean(error)} margin='dense'>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
onChange={e => onChange(e.target.checked)}
|
||||
name={name}
|
||||
checked={Boolean(value)}
|
||||
color='secondary'
|
||||
inputProps={{ 'data-cy': cy }}
|
||||
{...fieldProps}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Label>
|
||||
{labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
{tooltip && <Tooltip title={tooltip} />}
|
||||
</Label>
|
||||
}
|
||||
labelPlacement='end'
|
||||
/>
|
||||
{Boolean(error) && (
|
||||
<FormHelperText data-cy={`${cy}-error`}>
|
||||
<ErrorHelper label={error?.message} />
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) => prevProps.error === nextProps.error
|
||||
)
|
||||
|
||||
@ -60,24 +79,11 @@ SwitchController.propTypes = {
|
||||
control: PropTypes.object,
|
||||
cy: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
tooltip: PropTypes.string,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.objectOf(PropTypes.any)
|
||||
]),
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
fieldProps: PropTypes.object
|
||||
}
|
||||
|
||||
SwitchController.defaultProps = {
|
||||
control: {},
|
||||
cy: 'cy',
|
||||
name: '',
|
||||
label: '',
|
||||
values: [],
|
||||
error: false
|
||||
}
|
||||
|
||||
SwitchController.displayName = 'SwitchController'
|
||||
|
||||
export default SwitchController
|
||||
|
@ -17,47 +17,61 @@ import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Typography } from '@mui/material'
|
||||
import { Controller } from 'react-hook-form'
|
||||
import { useController } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper, Tooltip } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { generateKey } from 'client/utils'
|
||||
|
||||
const defaultGetRowId = item => typeof item === 'object' ? item?.id ?? item?.ID : item
|
||||
|
||||
const TableController = memo(
|
||||
({ control, cy, name, Table, singleSelect = true, getRowId = defaultGetRowId, label, tooltip, error, formContext }) => {
|
||||
({
|
||||
control,
|
||||
cy = `table-${generateKey()}`,
|
||||
name = '',
|
||||
label = '',
|
||||
tooltip,
|
||||
Table,
|
||||
singleSelect = true,
|
||||
getRowId = defaultGetRowId,
|
||||
formContext = {}
|
||||
}) => {
|
||||
const { clearErrors } = formContext
|
||||
|
||||
const {
|
||||
field: { onChange },
|
||||
fieldState: { error }
|
||||
} = useController({ name, control })
|
||||
|
||||
return (
|
||||
<>
|
||||
{error ? (
|
||||
<ErrorHelper data-cy={`${cy}-error`} label={error?.message} mb={2} />
|
||||
<ErrorHelper
|
||||
data-cy={`${cy}-error`}
|
||||
label={error?.message}
|
||||
mb={2}
|
||||
/>
|
||||
) : (
|
||||
label && (
|
||||
<Typography variant='body1' mb={2}>
|
||||
{tooltip && <Tooltip title={tooltip} position='start' />}
|
||||
{typeof label === 'string' ? Tr(label) : label}
|
||||
{labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
</Typography>
|
||||
)
|
||||
)}
|
||||
<Controller
|
||||
render={({ onChange }) => (
|
||||
<Table
|
||||
pageSize={4}
|
||||
singleSelect={singleSelect}
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
getRowId={getRowId}
|
||||
onSelectedRowsChange={rows => {
|
||||
const rowValues = rows?.map(({ original }) => getRowId(original))
|
||||
<Table
|
||||
pageSize={4}
|
||||
singleSelect={singleSelect}
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
getRowId={getRowId}
|
||||
onSelectedRowsChange={rows => {
|
||||
const rowValues = rows?.map(({ original }) => getRowId(original))
|
||||
|
||||
onChange(singleSelect ? rowValues?.[0] : rowValues)
|
||||
clearErrors(name)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
name={name}
|
||||
control={control}
|
||||
onChange(singleSelect ? rowValues?.[0] : rowValues)
|
||||
clearErrors(name)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@ -76,15 +90,8 @@ TableController.propTypes = {
|
||||
Table: PropTypes.any,
|
||||
getRowId: PropTypes.func,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node
|
||||
]),
|
||||
tooltip: PropTypes.string,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.objectOf(PropTypes.any)
|
||||
]),
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
fieldProps: PropTypes.object,
|
||||
formContext: PropTypes.shape({
|
||||
setValue: PropTypes.func,
|
||||
|
@ -13,45 +13,74 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo } from 'react'
|
||||
import { memo, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { TextField } from '@mui/material'
|
||||
import { Controller } from 'react-hook-form'
|
||||
import { useController, useWatch } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper, Tooltip } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { generateKey } from 'client/utils'
|
||||
|
||||
const TextController = memo(
|
||||
({ control, cy, type, multiline, name, label, tooltip, error, fieldProps }) => (
|
||||
<Controller
|
||||
render={({ value, ...controllerProps }) =>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline={multiline}
|
||||
value={value ?? ''}
|
||||
type={type}
|
||||
label={typeof label === 'string' ? Tr(label) : label}
|
||||
InputProps={{
|
||||
endAdornment: tooltip && <Tooltip title={tooltip} />
|
||||
}}
|
||||
inputProps={{ 'data-cy': cy, ...fieldProps }}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
{...controllerProps}
|
||||
{...fieldProps}
|
||||
/>
|
||||
({
|
||||
control,
|
||||
cy = `input-${generateKey()}`,
|
||||
name = '',
|
||||
label = '',
|
||||
type = 'text',
|
||||
multiline = false,
|
||||
tooltip,
|
||||
watcher,
|
||||
dependencies,
|
||||
fieldProps = {}
|
||||
}) => {
|
||||
const watch = dependencies && useWatch({
|
||||
control,
|
||||
name: dependencies,
|
||||
disabled: dependencies === null
|
||||
})
|
||||
|
||||
const {
|
||||
field: { ref, value = '', onChange, ...inputProps },
|
||||
fieldState: { error }
|
||||
} = useController({ name, control })
|
||||
|
||||
useEffect(() => {
|
||||
if (watch && watcher) {
|
||||
const watcherValue = watcher(watch)
|
||||
watcherValue && onChange(watcherValue)
|
||||
}
|
||||
name={name}
|
||||
control={control}
|
||||
/>
|
||||
),
|
||||
}, [watch])
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...inputProps}
|
||||
fullWidth
|
||||
inputRef={ref}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
multiline={multiline}
|
||||
type={type}
|
||||
label={labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
InputProps={{
|
||||
endAdornment: tooltip && <Tooltip title={tooltip} />
|
||||
}}
|
||||
inputProps={{ 'data-cy': cy, ...fieldProps }}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
{...fieldProps}
|
||||
/>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.error === nextProps.error &&
|
||||
prevProps.type === nextProps.type &&
|
||||
prevProps.label === nextProps.label &&
|
||||
prevProps.tooltip === nextProps.tooltip
|
||||
prevProps.tooltip === nextProps.tooltip &&
|
||||
prevProps.fieldProps?.value === nextProps.fieldProps?.value
|
||||
)
|
||||
|
||||
TextController.propTypes = {
|
||||
@ -60,14 +89,12 @@ TextController.propTypes = {
|
||||
type: PropTypes.string,
|
||||
multiline: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node
|
||||
]),
|
||||
tooltip: PropTypes.string,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.objectOf(PropTypes.any)
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
watcher: PropTypes.func,
|
||||
dependencies: PropTypes.oneOfType([
|
||||
PropTypes.strin,
|
||||
PropTypes.arrayOf(PropTypes.string)
|
||||
]),
|
||||
fieldProps: PropTypes.object,
|
||||
formContext: PropTypes.shape({
|
||||
@ -79,16 +106,6 @@ TextController.propTypes = {
|
||||
})
|
||||
}
|
||||
|
||||
TextController.defaultProps = {
|
||||
control: {},
|
||||
cy: 'cy',
|
||||
type: 'text',
|
||||
multiline: false,
|
||||
name: '',
|
||||
label: '',
|
||||
error: false
|
||||
}
|
||||
|
||||
TextController.displayName = 'TextController'
|
||||
|
||||
export default TextController
|
||||
|
@ -73,7 +73,7 @@ const TimeController = memo(
|
||||
inputProps={{ 'data-cy': cy }}
|
||||
inputRef={ref}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error} />}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
{...fieldProps}
|
||||
/>
|
||||
@ -94,11 +94,9 @@ TimeController.propTypes = {
|
||||
cy: PropTypes.string,
|
||||
multiline: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.objectOf(PropTypes.any)
|
||||
]),
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
error: PropTypes.any,
|
||||
fieldProps: PropTypes.object,
|
||||
formContext: PropTypes.shape({
|
||||
setValue: PropTypes.func,
|
||||
|
@ -17,32 +17,41 @@ import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { TextField } from '@mui/material'
|
||||
import { Controller } from 'react-hook-form'
|
||||
import { useController } from 'react-hook-form'
|
||||
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { ErrorHelper } from 'client/components/FormControl'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { generateKey } from 'client/utils'
|
||||
|
||||
const TimeController = memo(
|
||||
({ control, cy, name, type, label, error, fieldProps }) => (
|
||||
<Controller
|
||||
render={({ value, ...props }) =>
|
||||
<TextField
|
||||
{...props}
|
||||
fullWidth
|
||||
value={value}
|
||||
{...(label && { label: Tr(label) })}
|
||||
type={type}
|
||||
inputProps={{ 'data-cy': cy, ...fieldProps }}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
/>
|
||||
}
|
||||
name={name}
|
||||
control={control}
|
||||
/>
|
||||
),
|
||||
({
|
||||
control,
|
||||
cy = `datetime-${generateKey()}`,
|
||||
name = '',
|
||||
label = '',
|
||||
type = 'datetime-local',
|
||||
fieldProps = {}
|
||||
}) => {
|
||||
const {
|
||||
field: { ref, value, ...inputProps },
|
||||
fieldState: { error }
|
||||
} = useController({ name, control })
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...inputProps}
|
||||
fullWidth
|
||||
label={labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
inputRef={ref}
|
||||
type={type}
|
||||
inputProps={{ 'data-cy': cy, ...fieldProps }}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.error === nextProps.error &&
|
||||
prevProps.label === nextProps.label
|
||||
@ -53,12 +62,9 @@ TimeController.propTypes = {
|
||||
cy: PropTypes.string,
|
||||
multiline: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
type: PropTypes.string,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.objectOf(PropTypes.any)
|
||||
]),
|
||||
fieldProps: PropTypes.object,
|
||||
formContext: PropTypes.shape({
|
||||
setValue: PropTypes.func,
|
||||
@ -69,16 +75,6 @@ TimeController.propTypes = {
|
||||
})
|
||||
}
|
||||
|
||||
TimeController.defaultProps = {
|
||||
control: {},
|
||||
cy: 'cy',
|
||||
name: '',
|
||||
label: '',
|
||||
type: 'datetime-local',
|
||||
error: false,
|
||||
fieldProps: undefined
|
||||
}
|
||||
|
||||
TimeController.displayName = 'TimeController'
|
||||
|
||||
export default TimeController
|
||||
|
@ -0,0 +1,120 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { memo, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {
|
||||
styled,
|
||||
FormControl,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
FormHelperText
|
||||
} from '@mui/material'
|
||||
import { useController } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper, Tooltip } from 'client/components/FormControl'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { generateKey } from 'client/utils'
|
||||
|
||||
const Label = styled('label')(({ theme, error }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1em',
|
||||
...(error && {
|
||||
color: theme.palette.error.main
|
||||
})
|
||||
}))
|
||||
|
||||
const ToggleController = memo(
|
||||
({
|
||||
control,
|
||||
cy = `toggle-${generateKey()}`,
|
||||
name = '',
|
||||
label = '',
|
||||
multiple = false,
|
||||
values = [],
|
||||
tooltip,
|
||||
fieldProps = {}
|
||||
}) => {
|
||||
const defaultValue = multiple ? [values?.[0]?.value] : values?.[0]?.value
|
||||
|
||||
const {
|
||||
field: { ref, value: optionSelected = defaultValue, onChange, ...inputProps },
|
||||
fieldState: { error }
|
||||
} = useController({ name, control })
|
||||
|
||||
useEffect(() => {
|
||||
if (optionSelected) {
|
||||
const exists = values?.find(option => option.value === optionSelected)
|
||||
!exists && onChange()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<FormControl fullWidth margin='dense'>
|
||||
{label && (
|
||||
<Label htmlFor={cy} error={error ? 'error' : undefined}>
|
||||
{labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
{tooltip && <Tooltip title={tooltip} />}
|
||||
</Label>
|
||||
)}
|
||||
<ToggleButtonGroup
|
||||
{...inputProps}
|
||||
onChange={(_, newValues) => onChange(newValues)}
|
||||
ref={ref}
|
||||
id={cy}
|
||||
value={optionSelected}
|
||||
fullWidth
|
||||
exclusive={!multiple}
|
||||
data-cy={cy}
|
||||
{...fieldProps}
|
||||
>
|
||||
{values?.map(({ text, value = '' }) =>
|
||||
<ToggleButton key={`${name}-${value}`} value={value}>
|
||||
{text}
|
||||
</ToggleButton>
|
||||
)}
|
||||
</ToggleButtonGroup>
|
||||
{Boolean(error) && (
|
||||
<FormHelperText data-cy={`${cy}-error`}>
|
||||
<ErrorHelper label={error?.message} />
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.error === nextProps.error &&
|
||||
prevProps.values.length === nextProps.values.length &&
|
||||
prevProps.label === nextProps.label &&
|
||||
prevProps.tooltip === nextProps.tooltip
|
||||
)
|
||||
|
||||
ToggleController.propTypes = {
|
||||
control: PropTypes.object,
|
||||
cy: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
multiple: PropTypes.bool,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
renderValue: PropTypes.func,
|
||||
fieldProps: PropTypes.object
|
||||
}
|
||||
|
||||
ToggleController.displayName = 'ToggleController'
|
||||
|
||||
export default ToggleController
|
@ -18,25 +18,40 @@ import PropTypes from 'prop-types'
|
||||
|
||||
import { QuestionMarkCircle } from 'iconoir-react'
|
||||
import { InputAdornment, Typography, Tooltip } from '@mui/material'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
|
||||
const AdornmentWithTooltip = memo(({ title, position = 'end', children }) => (
|
||||
<Tooltip
|
||||
arrow
|
||||
placement='bottom'
|
||||
title={
|
||||
<Typography variant='subtitle2'>
|
||||
{title}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<InputAdornment position={position} style={{ cursor: 'help' }}>
|
||||
{children ?? <QuestionMarkCircle />}
|
||||
</InputAdornment>
|
||||
</Tooltip>
|
||||
), (prevProps, nextProps) => prevProps.title === nextProps.title)
|
||||
const AdornmentWithTooltip = memo(({ title, position = 'end', children }) => {
|
||||
if (!title || title === '' || (Array.isArray(title) && title.length === 0)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
arrow
|
||||
placement='bottom'
|
||||
title={
|
||||
<Typography variant='subtitle2'>
|
||||
{labelCanBeTranslated(title) ? Tr(title) : title}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<InputAdornment position={position} style={{ cursor: 'help' }}>
|
||||
{children ?? <QuestionMarkCircle />}
|
||||
</InputAdornment>
|
||||
</Tooltip>
|
||||
)
|
||||
}, (prevProps, nextProps) =>
|
||||
Array.isArray(nextProps.title)
|
||||
? prevProps.title?.[0] === nextProps.title?.[0] || prevProps.title === nextProps.title?.[0]
|
||||
: prevProps.title === nextProps.title
|
||||
)
|
||||
|
||||
AdornmentWithTooltip.propTypes = {
|
||||
title: PropTypes.string,
|
||||
title: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node,
|
||||
PropTypes.object
|
||||
]),
|
||||
children: PropTypes.any,
|
||||
position: PropTypes.oneOf(['start', 'end'])
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import SwitchController from 'client/components/FormControl/SwitchController'
|
||||
import TableController from 'client/components/FormControl/TableController'
|
||||
import TextController from 'client/components/FormControl/TextController'
|
||||
import TimeController from 'client/components/FormControl/TimeController'
|
||||
import ToggleController from 'client/components/FormControl/ToggleController'
|
||||
|
||||
import SubmitButton, { SubmitButtonPropTypes } from 'client/components/FormControl/SubmitButton'
|
||||
import InputCode from 'client/components/FormControl/InputCode'
|
||||
@ -40,6 +41,7 @@ export {
|
||||
TableController,
|
||||
TextController,
|
||||
TimeController,
|
||||
ToggleController,
|
||||
|
||||
SubmitButton,
|
||||
SubmitButtonPropTypes,
|
||||
|
@ -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 } from 'client/components/HOC'
|
||||
import { Tr, Translate, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
@ -57,7 +57,7 @@ const CustomMobileStepper = ({
|
||||
<Box className={classes.root}>
|
||||
<Box minHeight={60}>
|
||||
<Typography className={classes.title}>
|
||||
{typeof label === 'string' ? Tr(label) : label}
|
||||
{labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
</Typography>
|
||||
{Boolean(errors[id]) && (
|
||||
<Typography className={classes.error} variant='caption' color='error'>
|
||||
@ -79,12 +79,16 @@ const CustomMobileStepper = ({
|
||||
onClick={handleBack}
|
||||
disabled={disabledBack}
|
||||
>
|
||||
<PreviousIcon /> {Tr(T.Back)}
|
||||
<PreviousIcon />
|
||||
<Translate word={T.Back} />
|
||||
</Button>
|
||||
}
|
||||
nextButton={
|
||||
<Button className={classes.button} size='small' onClick={handleNext}>
|
||||
{activeStep === lastStep ? Tr(T.Finish) : Tr(T.Next)}
|
||||
{activeStep === lastStep
|
||||
? <Translate word={T.Finish} />
|
||||
: <Translate word={T.Next} />
|
||||
}
|
||||
<NextIcon />
|
||||
</Button>
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import StepConnector, { stepConnectorClasses } from '@mui/material/StepConnector
|
||||
import { styled } from '@mui/styles'
|
||||
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
import { Tr, Translate, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { T, SCHEMES } from 'client/constants'
|
||||
|
||||
const StepperStyled = styled(Stepper)(({ theme }) => ({
|
||||
@ -65,10 +65,7 @@ const ConnectorStyled = styled(StepConnector)(({ theme }) => ({
|
||||
const StepIconStyled = styled(StepIcon)(({ theme }) => ({
|
||||
color: theme.palette.text.hint,
|
||||
display: 'block',
|
||||
[`&.${stepIconClasses.completed}`]: {
|
||||
color: theme.palette.secondary[700]
|
||||
},
|
||||
[`&.${stepIconClasses.active}`]: {
|
||||
[`&.${stepIconClasses.completed}, &.${stepIconClasses.active}`]: {
|
||||
color: theme.palette.secondary[700]
|
||||
},
|
||||
[`&.${stepIconClasses.error}`]: {
|
||||
@ -104,7 +101,7 @@ const CustomStepper = ({
|
||||
StepIconComponent={StepIconStyled}
|
||||
error={Boolean(errors[id]?.message)}
|
||||
>
|
||||
{typeof label === 'string' ? Tr(label) : label}
|
||||
{labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
</StepLabel>
|
||||
</StepButton>
|
||||
</Step>
|
||||
|
@ -18,13 +18,14 @@ import PropTypes from 'prop-types'
|
||||
|
||||
import { BaseSchema } from 'yup'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
import { DevTool } from '@hookform/devtools'
|
||||
import { useMediaQuery } from '@mui/material'
|
||||
|
||||
import { useGeneral } from 'client/features/General'
|
||||
import CustomMobileStepper from 'client/components/FormStepper/MobileStepper'
|
||||
import CustomStepper from 'client/components/FormStepper/Stepper'
|
||||
import SkeletonStepsForm from 'client/components/FormStepper/Skeleton'
|
||||
import { groupBy, Step } from 'client/utils'
|
||||
import { groupBy, Step, isDevelopment } from 'client/utils'
|
||||
|
||||
const FIRST_STEP = 0
|
||||
|
||||
@ -40,7 +41,7 @@ const FIRST_STEP = 0
|
||||
*/
|
||||
const FormStepper = ({ steps = [], schema, onSubmit }) => {
|
||||
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
|
||||
const { watch, reset, errors, setError } = useFormContext()
|
||||
const { control, watch, reset, formState: { errors }, setError } = useFormContext()
|
||||
const { isLoading } = useGeneral()
|
||||
|
||||
const [formData, setFormData] = useState(() => watch())
|
||||
@ -51,7 +52,7 @@ const FormStepper = ({ steps = [], schema, onSubmit }) => {
|
||||
const disabledBack = useMemo(() => activeStep === FIRST_STEP, [activeStep])
|
||||
|
||||
useEffect(() => {
|
||||
reset({ ...formData }, { errors: false })
|
||||
reset({ ...formData }, { keepErrors: false })
|
||||
}, [formData])
|
||||
|
||||
const validateSchema = async stepIdx => {
|
||||
@ -164,6 +165,8 @@ const FormStepper = ({ steps = [], schema, onSubmit }) => {
|
||||
), [isLoading, isMobile, activeStep, errors[id]])}
|
||||
{/* FORM CONTENT */}
|
||||
{Content && <Content data={formData[id]} setFormData={setFormData} />}
|
||||
|
||||
{isDevelopment() && <DevTool control={control} placement='top-left' />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -17,23 +17,14 @@
|
||||
import { createElement, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { styled, Grid } from '@mui/material'
|
||||
import { Grid, Typography } from '@mui/material'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
import * as FC from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
import { get } from 'client/utils'
|
||||
|
||||
const Fieldset = styled('fieldset')({ border: 'none' })
|
||||
|
||||
const Legend = styled('legend')(({ theme }) => ({
|
||||
...theme.typography.subtitle1,
|
||||
marginBottom: '1em',
|
||||
padding: '0em 1em 0.2em 0.5em',
|
||||
borderBottom: `2px solid ${theme.palette.secondary.main}`
|
||||
}))
|
||||
|
||||
const NOT_DEPEND_ATTRIBUTES = ['transform', 'Table']
|
||||
const NOT_DEPEND_ATTRIBUTES = ['watcher', 'transform', 'Table', 'renderValue']
|
||||
|
||||
const INPUT_CONTROLLER = {
|
||||
[INPUT_TYPES.TEXT]: FC.TextController,
|
||||
@ -45,28 +36,39 @@ const INPUT_CONTROLLER = {
|
||||
[INPUT_TYPES.AUTOCOMPLETE]: FC.AutocompleteController,
|
||||
[INPUT_TYPES.FILE]: FC.FileController,
|
||||
[INPUT_TYPES.TIME]: FC.TimeController,
|
||||
[INPUT_TYPES.TABLE]: FC.TableController
|
||||
[INPUT_TYPES.TABLE]: FC.TableController,
|
||||
[INPUT_TYPES.TOGGLE]: FC.ToggleController
|
||||
}
|
||||
|
||||
const FormWithSchema = ({ id, cy, fields, className, legend }) => {
|
||||
const formContext = useFormContext()
|
||||
const { control, errors, watch } = formContext
|
||||
const { control, watch } = formContext
|
||||
|
||||
const getFields = useMemo(() => typeof fields === 'function' ? fields() : fields, [])
|
||||
|
||||
if (getFields.length === 0) return null
|
||||
|
||||
const addIdToName = name => name.startsWith('$')
|
||||
? name.slice(1) // removes character '$' and returns
|
||||
: id ? `${id}.${name}` : name // concat form ID if exists
|
||||
|
||||
return (
|
||||
<Fieldset className={className}>
|
||||
{legend && <Legend>{legend}</Legend>}
|
||||
<fieldset className={className}>
|
||||
{legend && (
|
||||
<Typography variant='subtitle1' component='legend'>
|
||||
{Tr(legend)}
|
||||
</Typography>
|
||||
)}
|
||||
<Grid container spacing={1} alignContent='flex-start'>
|
||||
{getFields?.map?.(
|
||||
({ dependOf, ...attributes }) => {
|
||||
let valueOfDependField = null
|
||||
let nameOfDependField = null
|
||||
|
||||
if (dependOf) {
|
||||
const nameOfDependField = id
|
||||
? Array.isArray(dependOf) ? dependOf.map(d => `${id}.${d}`) : `${id}.${dependOf}`
|
||||
: dependOf
|
||||
nameOfDependField = Array.isArray(dependOf)
|
||||
? dependOf.map(addIdToName)
|
||||
: addIdToName(dependOf)
|
||||
|
||||
valueOfDependField = watch(nameOfDependField)
|
||||
}
|
||||
@ -78,16 +80,14 @@ const FormWithSchema = ({ id, cy, fields, className, legend }) => {
|
||||
const isNotDependAttribute = NOT_DEPEND_ATTRIBUTES.includes(key)
|
||||
|
||||
const finalValue = typeof value === 'function' && !isNotDependAttribute
|
||||
? value(valueOfDependField)
|
||||
? value(valueOfDependField, formContext)
|
||||
: value
|
||||
|
||||
return { ...field, [key]: finalValue }
|
||||
}, {})
|
||||
|
||||
const dataCy = `${cy}-${name}`
|
||||
const inputName = id ? `${id}.${name}` : name
|
||||
|
||||
const inputError = get(errors, inputName) ?? false
|
||||
const inputName = addIdToName(name)
|
||||
|
||||
const isHidden = htmlType === INPUT_TYPES.HIDDEN
|
||||
|
||||
@ -99,8 +99,8 @@ const FormWithSchema = ({ id, cy, fields, className, legend }) => {
|
||||
{createElement(INPUT_CONTROLLER[type], {
|
||||
control,
|
||||
cy: dataCy,
|
||||
error: inputError,
|
||||
formContext,
|
||||
dependencies: nameOfDependField,
|
||||
name: inputName,
|
||||
type: htmlType === false ? undefined : htmlType,
|
||||
...fieldProps
|
||||
@ -111,7 +111,7 @@ const FormWithSchema = ({ id, cy, fields, className, legend }) => {
|
||||
}
|
||||
)}
|
||||
</Grid>
|
||||
</Fieldset>
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import * as yup from 'yup'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
import { getValidationFromFields, isBase64, prettyBytes } from 'client/utils'
|
||||
import { getValidationFromFields, prettyBytes } from 'client/utils'
|
||||
|
||||
const MAX_SIZE_JSON = 102_400
|
||||
const JSON_FORMAT = 'application/json'
|
||||
@ -29,11 +29,11 @@ export const FORM_FIELDS = ({ connection, fileCredentials }) =>
|
||||
let validation = yup
|
||||
.string()
|
||||
.trim()
|
||||
.required(`${name} field is required`)
|
||||
.required()
|
||||
.default(undefined)
|
||||
|
||||
if (isInputFile) {
|
||||
validation = validation.test('is-base64', 'File has invalid format', isBase64)
|
||||
validation = validation.isBase64()
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -0,0 +1,228 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { SetStateAction } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
|
||||
import { NetworkAlt as NetworkIcon, BoxIso as ImageIcon } from 'iconoir-react'
|
||||
import { Stack, Checkbox, styled } from '@mui/material'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { TAB_ID as BOOTING_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting'
|
||||
import { TAB_ID as STORAGE_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/storage'
|
||||
import { TAB_ID as NIC_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/networking'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const BOOT_ORDER_ID = 'BOOT'
|
||||
|
||||
const BootItem = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: '0.5em',
|
||||
padding: '1em',
|
||||
marginBottom: '1em',
|
||||
backgroundColor: theme.palette.background.default
|
||||
}))
|
||||
|
||||
const BootItemDraggable = styled(BootItem)(({ theme }) => ({
|
||||
'&:before': {
|
||||
content: "''",
|
||||
display: 'block',
|
||||
width: 16,
|
||||
height: 10,
|
||||
backgroundImage: `linear-gradient(
|
||||
to bottom,
|
||||
${theme.palette.action.active} 4px,
|
||||
transparent 4px,
|
||||
transparent 6px,
|
||||
${theme.palette.action.active} 6px
|
||||
)`
|
||||
}
|
||||
}))
|
||||
|
||||
/**
|
||||
* @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 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?.[BOOTING_ID]?.[BOOT_ORDER_ID]?.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 => ({
|
||||
...prev,
|
||||
[EXTRA_ID]: {
|
||||
...prev[EXTRA_ID],
|
||||
[BOOTING_ID]: {
|
||||
...prev[EXTRA_ID]?.[BOOTING_ID],
|
||||
[BOOT_ORDER_ID]: newBootOrder.join(',')
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const BootOrder = ({ data, setFormData, control }) => {
|
||||
const booting = useWatch({ name: `${EXTRA_ID}.${BOOTING_ID}.${BOOT_ORDER_ID}`, control })
|
||||
const bootOrder = booting?.split(',').filter(Boolean) ?? []
|
||||
|
||||
const disks = data?.[STORAGE_ID]
|
||||
?.map((disk, idx) => {
|
||||
const isVolatile = !disk?.IMAGE && !disk?.IMAGE_ID
|
||||
|
||||
return {
|
||||
ID: `disk${idx}`,
|
||||
NAME: (
|
||||
<>
|
||||
<ImageIcon />
|
||||
{isVolatile
|
||||
? <>{`${disk?.NAME}: `}<Translate word={T.VolatileDisk} /></>
|
||||
: [disk?.NAME, disk?.IMAGE].filter(Boolean).join(': ')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}) ?? []
|
||||
|
||||
const nics = data?.[NIC_ID]
|
||||
?.map((nic, idx) => ({
|
||||
ID: `nic${idx}`,
|
||||
NAME: (
|
||||
<>
|
||||
<NetworkIcon />
|
||||
{[nic?.NAME, nic.NETWORK].filter(Boolean).join(': ')}
|
||||
</>
|
||||
)
|
||||
})) ?? []
|
||||
|
||||
const enabledItems = [...disks, ...nics]
|
||||
.filter(item => bootOrder.includes(item.ID))
|
||||
.sort((a, b) => bootOrder.indexOf(a.ID) - bootOrder.indexOf(b.ID))
|
||||
|
||||
const restOfItems = [...disks, ...nics]
|
||||
.filter(item => !bootOrder.includes(item.ID))
|
||||
|
||||
/** @param {DropResult} result - Drop result */
|
||||
const onDragEnd = result => {
|
||||
const { destination, source, draggableId } = result
|
||||
const newBootOrder = [...bootOrder]
|
||||
|
||||
if (
|
||||
destination &&
|
||||
destination.index !== source.index &&
|
||||
newBootOrder.includes(draggableId)
|
||||
) {
|
||||
newBootOrder.splice(source.index, 1) // remove current position
|
||||
newBootOrder.splice(destination.index, 0, draggableId) // set in new position
|
||||
|
||||
reorder(newBootOrder, setFormData)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnable = itemId => {
|
||||
const newBootOrder = [...bootOrder]
|
||||
const itemIndex = bootOrder.indexOf(itemId)
|
||||
|
||||
itemIndex >= 0
|
||||
? newBootOrder.splice(itemIndex, 1)
|
||||
: newBootOrder.push(itemId)
|
||||
|
||||
reorder(newBootOrder, setFormData)
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Stack>
|
||||
<Droppable droppableId='booting'>
|
||||
{({ droppableProps, innerRef, placeholder }) => (
|
||||
<Stack {...droppableProps} ref={innerRef} m={2}>
|
||||
{enabledItems.map(({ ID, NAME }, idx) => (
|
||||
<Draggable key={ID} draggableId={ID} index={idx}>
|
||||
{({ draggableProps, dragHandleProps, innerRef }) => (
|
||||
<BootItemDraggable
|
||||
{...draggableProps}
|
||||
{...dragHandleProps}
|
||||
ref={innerRef}
|
||||
>
|
||||
<Checkbox
|
||||
checked
|
||||
color='secondary'
|
||||
data-cy={ID}
|
||||
onChange={() => handleEnable(ID)}
|
||||
/>
|
||||
{NAME}
|
||||
</BootItemDraggable>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{placeholder}
|
||||
</Stack>
|
||||
)}
|
||||
</Droppable>
|
||||
{restOfItems.map(({ ID, NAME }) => (
|
||||
<BootItem key={ID}>
|
||||
<Checkbox
|
||||
color='secondary'
|
||||
data-cy={ID}
|
||||
onChange={() => handleEnable(ID)}
|
||||
/>
|
||||
{NAME}
|
||||
</BootItem>
|
||||
))}
|
||||
</Stack>
|
||||
</DragDropContext>
|
||||
)
|
||||
}
|
||||
|
||||
BootOrder.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
BootOrder.displayName = 'BootOrder'
|
||||
|
||||
export default BootOrder
|
@ -0,0 +1,219 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string, boolean } from 'yup'
|
||||
|
||||
import { useHost } from 'client/features/One'
|
||||
import { getKvmMachines, getKvmCpuModels } from 'client/models/Host'
|
||||
import { Field, arrayToOptions, filterFieldsByHypervisor } from 'client/utils'
|
||||
import {
|
||||
T,
|
||||
INPUT_TYPES,
|
||||
CPU_ARCHITECTURES,
|
||||
SD_DISK_BUSES,
|
||||
FIRMWARE_TYPES,
|
||||
KVM_FIRMWARE_TYPES,
|
||||
VCENTER_FIRMWARE_TYPES,
|
||||
HYPERVISORS
|
||||
} from 'client/constants'
|
||||
|
||||
const { vcenter, firecracker, lxc, kvm } = HYPERVISORS
|
||||
|
||||
/** @type {Field} CPU architecture field */
|
||||
export const ARCH = {
|
||||
name: 'OS.ARCH',
|
||||
label: T.CpuArchitecture,
|
||||
notOnHypervisors: [vcenter, firecracker, lxc],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: () => arrayToOptions(CPU_ARCHITECTURES),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} Bus for SD disks field */
|
||||
export const SD_DISK_BUS = {
|
||||
name: 'OS.SD_DISK_BUS',
|
||||
label: T.BusForSdDisks,
|
||||
notOnHypervisors: [vcenter, firecracker, lxc],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(SD_DISK_BUSES, { getText: o => o.toUpperCase() }),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} Machine type field */
|
||||
export const MACHINE_TYPES = {
|
||||
name: 'OS.MACHINE',
|
||||
label: T.MachineType,
|
||||
notOnHypervisors: [vcenter, firecracker, lxc],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: () => {
|
||||
const hosts = useHost()
|
||||
const kvmMachines = getKvmMachines(hosts)
|
||||
|
||||
return arrayToOptions(kvmMachines)
|
||||
},
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} CPU Model field */
|
||||
export const CPU_MODEL = {
|
||||
name: 'OS.MODEL',
|
||||
label: T.CpuModel,
|
||||
notOnHypervisors: [vcenter, firecracker, lxc],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: () => {
|
||||
const hosts = useHost()
|
||||
const kvmCpuModels = getKvmCpuModels(hosts)
|
||||
|
||||
return arrayToOptions(kvmCpuModels)
|
||||
},
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} Root device field */
|
||||
export const ROOT_DEVICE = {
|
||||
name: 'OS.ROOT',
|
||||
label: T.RootDevice,
|
||||
notOnHypervisors: [vcenter, firecracker, lxc],
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} Kernel CMD field */
|
||||
export const KERNEL_CMD = {
|
||||
name: 'OS.KERNEL_CMD',
|
||||
label: T.KernelBootParameters,
|
||||
notOnHypervisors: [vcenter, lxc],
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
fieldProps: { placeholder: 'ro console=tty1' }
|
||||
}
|
||||
|
||||
/** @type {Field} Path bootloader field */
|
||||
export const BOOTLOADER = {
|
||||
name: 'OS.BOOTLOADER',
|
||||
label: T.PathBootloader,
|
||||
notOnHypervisors: [vcenter, lxc],
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} UUID field */
|
||||
export const UUID = {
|
||||
name: 'OS.UUID',
|
||||
label: T.UniqueIdOfTheVm,
|
||||
tooltip: T.UniqueIdOfTheVmConcept,
|
||||
notOnHypervisors: [firecracker, lxc],
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} Feature custom field */
|
||||
export const FEATURE_CUSTOM_ENABLED = {
|
||||
name: 'OS.FEATURE_CUSTOM_ENABLED',
|
||||
label: T.CustomPath,
|
||||
notOnHypervisors: [vcenter, firecracker, lxc],
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().strip().default(() => false),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} Firmware field */
|
||||
export const FIRMWARE = {
|
||||
name: 'OS.FIRMWARE',
|
||||
label: T.Firmware,
|
||||
tooltip: T.FirmwareConcept,
|
||||
notOnHypervisors: [firecracker, lxc],
|
||||
type: ([_, custom]) => custom ? INPUT_TYPES.TEXT : INPUT_TYPES.SELECT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
dependOf: ['$general.HYPERVISOR', FEATURE_CUSTOM_ENABLED.name],
|
||||
values: ([hypervisor] = []) => {
|
||||
const types = {
|
||||
[vcenter]: VCENTER_FIRMWARE_TYPES,
|
||||
[kvm]: KVM_FIRMWARE_TYPES
|
||||
}[hypervisor] ?? FIRMWARE_TYPES
|
||||
|
||||
return arrayToOptions(types)
|
||||
},
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} Feature secure field */
|
||||
export const FIRMWARE_SECURE = {
|
||||
name: 'OS.FIRMWARE_SECURE',
|
||||
label: T.FirmwareSecure,
|
||||
notOnHypervisors: [vcenter, firecracker, lxc],
|
||||
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'
|
||||
}),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of Boot fields
|
||||
*/
|
||||
export const BOOT_FIELDS = hypervisor =>
|
||||
filterFieldsByHypervisor(
|
||||
[
|
||||
ARCH,
|
||||
SD_DISK_BUS,
|
||||
MACHINE_TYPES,
|
||||
CPU_MODEL,
|
||||
ROOT_DEVICE,
|
||||
KERNEL_CMD,
|
||||
BOOTLOADER,
|
||||
UUID,
|
||||
FEATURE_CUSTOM_ENABLED,
|
||||
FIRMWARE,
|
||||
FIRMWARE_SECURE
|
||||
],
|
||||
hypervisor
|
||||
)
|
@ -0,0 +1,150 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string, number } from 'yup'
|
||||
|
||||
import { Field, arrayToOptions, filterFieldsByHypervisor } from 'client/utils'
|
||||
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
|
||||
const { vcenter, lxc, firecracker } = HYPERVISORS
|
||||
|
||||
/** @type {Field} ACPI field */
|
||||
export const ACPI = {
|
||||
name: 'OS.ACPI',
|
||||
label: T.Acpi,
|
||||
tooltip: T.AcpiConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([T.Yes, T.No]),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} PAE field */
|
||||
export const PAE = {
|
||||
name: 'OS.PAE',
|
||||
label: T.Pae,
|
||||
tooltip: T.PaeConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([T.Yes, T.No]),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} APIC field */
|
||||
export const APIC = {
|
||||
name: 'OS.APIC',
|
||||
label: T.Apic,
|
||||
tooltip: T.ApicConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([T.Yes, T.No]),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} HYPER-V field */
|
||||
export const HYPERV = {
|
||||
name: 'OS.HYPERV',
|
||||
label: T.Hyperv,
|
||||
tooltip: T.HypervConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([T.Yes, T.No]),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} Local time field */
|
||||
export const LOCALTIME = {
|
||||
name: 'OS.LOCALTIME',
|
||||
label: T.Localtime,
|
||||
tooltip: T.LocaltimeConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([T.Yes, T.No]),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} Guest agent field */
|
||||
export const GUEST_AGENT = {
|
||||
name: 'OS.GUEST_AGENT',
|
||||
label: T.GuestAgent,
|
||||
tooltip: T.GuestAgentConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([T.Yes, T.No]),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} Virtio-SCSI queues field */
|
||||
export const VIRTIO_SCSI_QUEUES = {
|
||||
name: 'OS.VIRTIO_SCSI_QUEUES',
|
||||
label: T.VirtioQueues,
|
||||
tooltip: T.VirtioQueuesConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(Array.from({ length: 16 }, (_, i) => i + 1)),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} IO threads field */
|
||||
export const IO_THREADS = {
|
||||
name: 'OS.IOTHREADS',
|
||||
label: T.IoThreads,
|
||||
tooltip: T.IoThreadsConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: number()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of Features fields
|
||||
*/
|
||||
export const FEATURES_FIELDS = hypervisor =>
|
||||
filterFieldsByHypervisor(
|
||||
[
|
||||
ACPI,
|
||||
PAE,
|
||||
APIC,
|
||||
HYPERV,
|
||||
LOCALTIME,
|
||||
GUEST_AGENT,
|
||||
VIRTIO_SCSI_QUEUES,
|
||||
IO_THREADS
|
||||
],
|
||||
hypervisor
|
||||
)
|
@ -0,0 +1,102 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 PropTypes from 'prop-types'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
import { STEP_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import BootOrder, { reorderBootAfterRemove } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting/bootOrder'
|
||||
import { TAB_ID as STORAGE_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/storage'
|
||||
import { TAB_ID as NIC_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/networking'
|
||||
import { BOOT_FIELDS, KERNEL_FIELDS, RAMDISK_FIELDS, FEATURES_FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting/schema'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
import AdornmentWithTooltip from 'client/components/FormControl/Tooltip'
|
||||
|
||||
export const TAB_ID = 'OS'
|
||||
|
||||
const Booting = props => {
|
||||
const { hypervisor } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
{(
|
||||
!!props.data?.[STORAGE_ID]?.length ||
|
||||
!!props.data?.[NIC_ID]?.length
|
||||
) && (
|
||||
<fieldset>
|
||||
<Typography
|
||||
variant='subtitle1'
|
||||
component='legend'
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Translate word={T.BootOrder} />
|
||||
<AdornmentWithTooltip title={T.BootOrderConcept} />
|
||||
</Typography>
|
||||
<BootOrder {...props} />
|
||||
</fieldset>
|
||||
)}
|
||||
<Stack
|
||||
display='grid'
|
||||
gap='2em'
|
||||
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
|
||||
>
|
||||
<FormWithSchema
|
||||
cy='create-vm-template-extra.os-boot'
|
||||
fields={BOOT_FIELDS(hypervisor)}
|
||||
legend={T.Boot}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
<FormWithSchema
|
||||
cy='create-vm-template-extra.os-features'
|
||||
fields={FEATURES_FIELDS(hypervisor)}
|
||||
legend={T.Features}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
<FormWithSchema
|
||||
cy='create-vm-template-extra.os-kernel'
|
||||
fields={KERNEL_FIELDS(hypervisor)}
|
||||
legend={T.Kernel}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
<FormWithSchema
|
||||
cy='create-vm-template-extra.os-ramdisk'
|
||||
fields={RAMDISK_FIELDS(hypervisor)}
|
||||
legend={T.Ramdisk}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Booting.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
Booting.displayName = 'Booting'
|
||||
|
||||
export default Booting
|
||||
|
||||
export { reorderBootAfterRemove }
|
@ -0,0 +1,84 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { boolean, string } from 'yup'
|
||||
|
||||
import { useImage } from 'client/features/One'
|
||||
import { getType } from 'client/models/Image'
|
||||
import { Field, clearNames, filterFieldsByHypervisor } from 'client/utils'
|
||||
import { T, INPUT_TYPES, HYPERVISORS, IMAGE_TYPES_STR } from 'client/constants'
|
||||
|
||||
const { vcenter, lxc } = HYPERVISORS
|
||||
|
||||
const kernelValidation = string().trim().notRequired().default(() => undefined)
|
||||
|
||||
/** @type {Field} Kernel path field */
|
||||
export const KERNEL_PATH_ENABLED = {
|
||||
name: 'OS.KERNEL_PATH_ENABLED',
|
||||
label: T.CustomPath,
|
||||
notOnHypervisors: [vcenter, lxc],
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().strip().default(() => false)
|
||||
}
|
||||
|
||||
/** @type {Field} Kernel DS field */
|
||||
export const KERNEL_DS = {
|
||||
name: 'OS.KERNEL_DS',
|
||||
label: T.Kernel,
|
||||
notOnHypervisors: [vcenter, lxc],
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
dependOf: KERNEL_PATH_ENABLED.name,
|
||||
htmlType: enabled => enabled && INPUT_TYPES.HIDDEN,
|
||||
values: () => {
|
||||
const images = useImage()
|
||||
|
||||
return images
|
||||
?.filter(image => getType(image) === IMAGE_TYPES_STR.KERNEL)
|
||||
?.map(({ ID, NAME }) => ({ text: `#${ID} ${NAME}`, value: `$FILE[IMAGE_ID=${ID}]` }))
|
||||
?.sort((a, b) => {
|
||||
const compareOptions = { numeric: true, ignorePunctuation: true }
|
||||
|
||||
return a.value.localeCompare(b.value, undefined, compareOptions)
|
||||
})
|
||||
},
|
||||
validation: kernelValidation.when(
|
||||
clearNames(KERNEL_PATH_ENABLED.name),
|
||||
(enabled, schema) => enabled ? schema.strip() : schema
|
||||
)
|
||||
}
|
||||
|
||||
/** @type {Field} Kernel path field */
|
||||
export const KERNEL = {
|
||||
name: 'OS.KERNEL',
|
||||
label: T.KernelPath,
|
||||
notOnHypervisors: [vcenter, lxc],
|
||||
type: INPUT_TYPES.TEXT,
|
||||
dependOf: KERNEL_PATH_ENABLED.name,
|
||||
htmlType: enabled => !enabled && INPUT_TYPES.HIDDEN,
|
||||
validation: kernelValidation.when(
|
||||
clearNames(KERNEL_PATH_ENABLED.name),
|
||||
(enabled, schema) => enabled ? schema : schema.strip()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of Kernel fields
|
||||
*/
|
||||
export const KERNEL_FIELDS = hypervisor =>
|
||||
filterFieldsByHypervisor(
|
||||
[KERNEL_PATH_ENABLED, KERNEL, KERNEL_DS],
|
||||
hypervisor
|
||||
)
|
@ -0,0 +1,84 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string, boolean } from 'yup'
|
||||
|
||||
import { useImage } from 'client/features/One'
|
||||
import { getType } from 'client/models/Image'
|
||||
import { Field, clearNames, filterFieldsByHypervisor } from 'client/utils'
|
||||
import { T, INPUT_TYPES, HYPERVISORS, IMAGE_TYPES_STR } from 'client/constants'
|
||||
|
||||
const { vcenter, lxc, firecracker } = HYPERVISORS
|
||||
|
||||
const ramdiskValidation = string().trim().notRequired().default(() => undefined)
|
||||
|
||||
/** @type {Field} Ramdisk path field */
|
||||
export const RAMDISK_PATH_ENABLED = {
|
||||
name: 'OS.RAMDISK_PATH_ENABLED',
|
||||
label: T.CustomPath,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().strip().default(() => false)
|
||||
}
|
||||
|
||||
/** @type {Field} Ramdisk DS field */
|
||||
export const RAMDISK_DS = {
|
||||
name: 'OS.INITRD_DS',
|
||||
label: T.Ramdisk,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
dependOf: RAMDISK_PATH_ENABLED.name,
|
||||
htmlType: enabled => enabled && INPUT_TYPES.HIDDEN,
|
||||
values: () => {
|
||||
const images = useImage()
|
||||
|
||||
return images
|
||||
?.filter(image => getType(image) === IMAGE_TYPES_STR.RAMDISK)
|
||||
?.map(({ ID, NAME }) => ({ text: `#${ID} ${NAME}`, value: `$FILE[IMAGE_ID=${ID}]` }))
|
||||
?.sort((a, b) => {
|
||||
const compareOptions = { numeric: true, ignorePunctuation: true }
|
||||
|
||||
return a.value.localeCompare(b.value, undefined, compareOptions)
|
||||
})
|
||||
},
|
||||
validation: ramdiskValidation.when(
|
||||
clearNames(RAMDISK_PATH_ENABLED.name),
|
||||
(enabled, schema) => enabled ? schema.strip() : schema
|
||||
)
|
||||
}
|
||||
|
||||
/** @type {Field} Ramdisk path field */
|
||||
export const RAMDISK = {
|
||||
name: 'OS.INITRD',
|
||||
label: T.RamdiskPath,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.TEXT,
|
||||
dependOf: RAMDISK_PATH_ENABLED.name,
|
||||
htmlType: enabled => !enabled && INPUT_TYPES.HIDDEN,
|
||||
validation: ramdiskValidation.when(
|
||||
clearNames(RAMDISK_PATH_ENABLED.name),
|
||||
(enabled, schema) => enabled ? schema : schema.strip()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of Ramdisk fields
|
||||
*/
|
||||
export const RAMDISK_FIELDS = hypervisor =>
|
||||
filterFieldsByHypervisor(
|
||||
[RAMDISK_PATH_ENABLED, RAMDISK, RAMDISK_DS],
|
||||
hypervisor
|
||||
)
|
@ -0,0 +1,50 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
import { string } from 'yup'
|
||||
|
||||
import { Field } from 'client/utils'
|
||||
import { BOOT_FIELDS } from './bootSchema'
|
||||
import { KERNEL_FIELDS } from './kernelSchema'
|
||||
import { RAMDISK_FIELDS } from './ramdiskSchema'
|
||||
import { FEATURES_FIELDS } from './featuresSchema'
|
||||
|
||||
export {
|
||||
BOOT_FIELDS,
|
||||
KERNEL_FIELDS,
|
||||
RAMDISK_FIELDS,
|
||||
FEATURES_FIELDS
|
||||
}
|
||||
|
||||
/** @type {Field} Boot order field */
|
||||
export const BOOT_ORDER = {
|
||||
name: 'OS.BOOT',
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => '')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} All 'OS & CPU' fields
|
||||
*/
|
||||
export const OS_FIELDS = hypervisor => [
|
||||
...BOOT_FIELDS(hypervisor),
|
||||
...KERNEL_FIELDS(hypervisor),
|
||||
...RAMDISK_FIELDS(hypervisor),
|
||||
...FEATURES_FIELDS(hypervisor)
|
||||
]
|
@ -0,0 +1,40 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 PropTypes from 'prop-types'
|
||||
|
||||
// import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
// import { STEP_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
// import {} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/schema'
|
||||
// import { T } from 'client/constants'
|
||||
|
||||
const Context = () => {
|
||||
return (
|
||||
<>
|
||||
{'Context'}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Context.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func
|
||||
}
|
||||
|
||||
Context.displayName = 'Context'
|
||||
|
||||
export default Context
|
@ -0,0 +1,29 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 } from 'yup'
|
||||
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
|
||||
const FIELD = {
|
||||
name: '',
|
||||
label: '',
|
||||
tooltip: '',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
export const CONTEXT_FIELDS = [FIELD]
|
@ -0,0 +1,111 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { useTheme } from '@mui/material'
|
||||
import {
|
||||
WarningCircledOutline as WarningIcon,
|
||||
Db as DatastoreIcon,
|
||||
ServerConnection as NetworkIcon,
|
||||
SystemShut as OsIcon,
|
||||
DataTransferBoth as IOIcon,
|
||||
Calendar as ActionIcon,
|
||||
NetworkAlt as PlacementIcon,
|
||||
Folder as ContextIcon,
|
||||
ElectronicsChip as NumaIcon
|
||||
} from 'iconoir-react'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
|
||||
import Tabs from 'client/components/Tabs'
|
||||
import Storage from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/storage'
|
||||
import Networking from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/networking'
|
||||
import Placement from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/placement'
|
||||
import ScheduleAction from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/scheduleAction'
|
||||
import Booting from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting'
|
||||
import Context from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context'
|
||||
import InputOutput from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput'
|
||||
import Numa from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/numa'
|
||||
|
||||
import { STEP_ID as GENERAL_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General'
|
||||
import { SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
|
||||
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'extra'
|
||||
|
||||
export const STEP_SECTION = [
|
||||
{ id: 'storage', name: T.Storage, icon: DatastoreIcon, Content: Storage },
|
||||
{ id: 'network', name: T.Network, icon: NetworkIcon, Content: Networking },
|
||||
{ id: 'booting', name: T.OSBooting, icon: OsIcon, Content: Booting },
|
||||
{ id: 'input_output', name: T.InputOrOutput, icon: IOIcon, Content: InputOutput },
|
||||
{ id: 'context', name: T.Context, icon: ContextIcon, Content: Context },
|
||||
{ id: 'sched_action', name: T.ScheduledAction, icon: ActionIcon, Content: ScheduleAction },
|
||||
{ id: 'placement', name: T.Placement, icon: PlacementIcon, Content: Placement },
|
||||
{ id: 'numa', name: T.Numa, icon: NumaIcon, Content: Numa }
|
||||
]
|
||||
|
||||
const Content = ({ data, setFormData }) => {
|
||||
const theme = useTheme()
|
||||
const { watch, formState: { errors }, control } = useFormContext()
|
||||
const { view, getResourceView } = useAuth()
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
const hypervisor = watch(`${GENERAL_ID}.HYPERVISOR`)
|
||||
const dialog = getResourceView('VM-TEMPLATE')?.dialogs?.create_dialog
|
||||
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
|
||||
|
||||
return STEP_SECTION
|
||||
.filter(({ id }) => sectionsAvailable.includes(id))
|
||||
.map(({ Content, name, icon, ...section }, idx) => ({
|
||||
...section,
|
||||
name,
|
||||
label: <Translate word={name} />,
|
||||
renderContent: <Content {...{ data, setFormData, hypervisor, control }} />,
|
||||
icon: errors[STEP_ID]?.[idx] ? (
|
||||
<WarningIcon color={theme.palette.error.main} />
|
||||
) : icon
|
||||
}))
|
||||
}, [errors[STEP_ID], view, control])
|
||||
|
||||
return tabs.length > 0 ? (
|
||||
<Tabs tabs={tabs} />
|
||||
) : (
|
||||
<span>{Tr(T.Empty)}</span>
|
||||
)
|
||||
}
|
||||
|
||||
const ExtraConfiguration = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.AdvancedOptions,
|
||||
resolver: formData => {
|
||||
const hypervisor = formData?.[GENERAL_ID]?.HYPERVISOR
|
||||
return SCHEMA(hypervisor)
|
||||
},
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content
|
||||
})
|
||||
|
||||
Content.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func
|
||||
}
|
||||
|
||||
export default ExtraConfiguration
|
@ -0,0 +1,52 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 PropTypes from 'prop-types'
|
||||
import { Stack } from '@mui/material'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
import { STEP_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { INPUT_OUTPUT_FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const InputOutput = ({ hypervisor }) => {
|
||||
return (
|
||||
<Stack
|
||||
display='grid'
|
||||
gap='2em'
|
||||
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
|
||||
>
|
||||
<FormWithSchema
|
||||
cy='create-vm-template-extra.io-graphics'
|
||||
fields={INPUT_OUTPUT_FIELDS(hypervisor)}
|
||||
legend={T.Graphics}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
InputOutput.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
InputOutput.displayName = 'InputOutput'
|
||||
|
||||
export default InputOutput
|
@ -0,0 +1,143 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 } from 'yup'
|
||||
|
||||
import { Field, arrayToOptions, filterFieldsByHypervisor } from 'client/utils'
|
||||
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
|
||||
const { vcenter, lxc, kvm } = HYPERVISORS
|
||||
|
||||
/** @type {Field} Type field */
|
||||
export const TYPE = {
|
||||
name: 'GRAPHICS.TYPE',
|
||||
label: T.Type,
|
||||
type: INPUT_TYPES.TOGGLE,
|
||||
dependOf: '$general.HYPERVISOR',
|
||||
values: (hypervisor = kvm) => {
|
||||
const types = {
|
||||
[vcenter]: [T.VMRC],
|
||||
[lxc]: [T.VNC]
|
||||
}[hypervisor] ?? [T.VNC, T.SDL, T.SPICE]
|
||||
|
||||
return arrayToOptions(types)
|
||||
},
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} Listen field */
|
||||
export const LISTEN = {
|
||||
name: 'GRAPHICS.LISTEN',
|
||||
label: T.ListenOnIp,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
dependOf: TYPE.name,
|
||||
htmlType: noneType => !noneType && INPUT_TYPES.HIDDEN,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
fieldProps: { placeholder: '0.0.0.0' },
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} Port field */
|
||||
export const PORT = {
|
||||
name: 'GRAPHICS.PORT',
|
||||
label: T.ServerPort,
|
||||
tooltip: T.ServerPortConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
dependOf: TYPE.name,
|
||||
htmlType: noneType => !noneType && INPUT_TYPES.HIDDEN,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} Keymap field */
|
||||
export const KEYMAP = {
|
||||
name: 'GRAPHICS.KEYMAP',
|
||||
label: T.Keymap,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
dependOf: TYPE.name,
|
||||
htmlType: noneType => !noneType && INPUT_TYPES.HIDDEN,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
fieldProps: { placeholder: 'en-us' }
|
||||
}
|
||||
|
||||
/** @type {Field} Password random field */
|
||||
export const RANDOM_PASSWD = {
|
||||
name: 'OS.RANDOM_PASSWD',
|
||||
label: T.GenerateRandomPassword,
|
||||
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'
|
||||
}),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} Password field */
|
||||
export const PASSWD = {
|
||||
name: 'GRAPHICS.PASSWD',
|
||||
label: T.Password,
|
||||
type: INPUT_TYPES.PASSWORD,
|
||||
dependOf: [TYPE.name, RANDOM_PASSWD.name],
|
||||
htmlType: ([noneType, random] = []) =>
|
||||
(!noneType || random) && INPUT_TYPES.HIDDEN,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} Command field */
|
||||
export const COMMAND = {
|
||||
name: 'GRAPHICS.COMMAND',
|
||||
label: T.Command,
|
||||
notOnHypervisors: [lxc],
|
||||
type: INPUT_TYPES.TEXT,
|
||||
dependOf: TYPE.name,
|
||||
htmlType: noneType => !noneType && INPUT_TYPES.HIDDEN,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of I/O fields
|
||||
*/
|
||||
export const INPUT_OUTPUT_FIELDS = hypervisor =>
|
||||
filterFieldsByHypervisor(
|
||||
[TYPE, LISTEN, PORT, KEYMAP, PASSWD, RANDOM_PASSWD, COMMAND],
|
||||
hypervisor
|
||||
)
|
@ -0,0 +1,148 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 PropTypes from 'prop-types'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Edit, Trash } from 'iconoir-react'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
|
||||
import { useListForm } from 'client/hooks'
|
||||
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
|
||||
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
|
||||
import { AttachNicForm } from 'client/components/Forms/Vm'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { SCHEMA as EXTRA_SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
|
||||
import { reorderBootAfterRemove } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting'
|
||||
import { stringToBoolean } from 'client/models/Helper'
|
||||
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 = 'NIC'
|
||||
|
||||
const Networking = ({ data, setFormData, control }) => {
|
||||
const classes = useStyles()
|
||||
const nics = useWatch({ name: `${EXTRA_ID}.${TAB_ID}`, control })
|
||||
|
||||
const { handleSetList, handleRemove, handleSave } = useListForm({
|
||||
parent: EXTRA_ID,
|
||||
key: TAB_ID,
|
||||
list: nics,
|
||||
setList: setFormData,
|
||||
getItemId: item => item.NAME,
|
||||
addItemId: (item, _, itemIndex) => ({ ...item, NAME: `${TAB_ID}${itemIndex}` })
|
||||
})
|
||||
|
||||
const reorderNics = () => {
|
||||
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-nic',
|
||||
label: T.AttachNic,
|
||||
variant: 'outlined'
|
||||
}}
|
||||
options={[{
|
||||
dialogProps: { title: T.AttachNic },
|
||||
form: () => AttachNicForm({ nics }),
|
||||
onSubmit: handleSave
|
||||
}]}
|
||||
/>
|
||||
<div className={classes.root}>
|
||||
{nics?.map(item => {
|
||||
const { NAME, RDP, SSH, NETWORK, PARENT, EXTERNAL } = item
|
||||
const hasAlias = nics?.some(nic => nic.PARENT === NAME)
|
||||
|
||||
return (
|
||||
<SelectCard
|
||||
key={NAME}
|
||||
title={[NAME, NETWORK].filter(Boolean).join(' - ')}
|
||||
subheader={<>
|
||||
{Object
|
||||
.entries({
|
||||
RDP: stringToBoolean(RDP),
|
||||
SSH: stringToBoolean(SSH),
|
||||
EXTERNAL: stringToBoolean(EXTERNAL),
|
||||
ALIAS: PARENT
|
||||
})
|
||||
.map(([k, v]) => v ? `${k}` : '')
|
||||
.filter(Boolean)
|
||||
.join(' | ')
|
||||
}
|
||||
</>}
|
||||
action={
|
||||
<>
|
||||
{!hasAlias &&
|
||||
<Action
|
||||
data-cy={`remove-${NAME}`}
|
||||
handleClick={() => {
|
||||
handleRemove(NAME)
|
||||
reorderNics()
|
||||
reorderBootAfterRemove(NAME, nics, data, setFormData)
|
||||
}}
|
||||
icon={<Trash />}
|
||||
/>
|
||||
}
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
'data-cy': `edit-${NAME}`,
|
||||
icon: <Edit />,
|
||||
tooltip: <Translate word={T.Edit} />
|
||||
}}
|
||||
options={[{
|
||||
dialogProps: {
|
||||
title: <Translate word={T.EditSomething} values={[`${NAME} - ${NETWORK}`]} />
|
||||
},
|
||||
form: () => AttachNicForm({ nics }, item),
|
||||
onSubmit: newValues => handleSave(newValues, NAME)
|
||||
}]}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Networking.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
Networking.displayName = 'Networking'
|
||||
|
||||
export default Networking
|
@ -0,0 +1,51 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 PropTypes from 'prop-types'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import { STEP_ID as GENERAL_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General'
|
||||
import { VIRTUAL_CPU } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/capacitySchema'
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { NUMA_FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/numa/schema'
|
||||
|
||||
const Placement = ({ hypervisor }) => {
|
||||
return (
|
||||
<>
|
||||
<FormWithSchema
|
||||
cy='create-vm-template-extra.vcpu'
|
||||
fields={[VIRTUAL_CPU]}
|
||||
id={GENERAL_ID}
|
||||
/>
|
||||
<FormWithSchema
|
||||
cy='create-vm-template-extra.numa'
|
||||
fields={NUMA_FIELDS(hypervisor)}
|
||||
id={EXTRA_ID}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Placement.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
Placement.displayName = 'Placement'
|
||||
|
||||
export default Placement
|
@ -0,0 +1,178 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string, number } from 'yup'
|
||||
|
||||
import { useHost } from 'client/features/One'
|
||||
import { getHugepageSizes } from 'client/models/Host'
|
||||
import { T, INPUT_TYPES, NUMA_PIN_POLICIES, NUMA_MEMORY_ACCESS, HYPERVISORS } from 'client/constants'
|
||||
import {
|
||||
Field,
|
||||
filterFieldsByHypervisor,
|
||||
getFactorsOfNumber,
|
||||
sentenceCase,
|
||||
prettyBytes,
|
||||
arrayToOptions
|
||||
} from 'client/utils'
|
||||
|
||||
const { vcenter, firecracker } = HYPERVISORS
|
||||
|
||||
const threadsValidation = number()
|
||||
.nullable()
|
||||
.notRequired()
|
||||
.integer()
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} hypervisor - VM hypervisor
|
||||
* @returns {Field} Pin policy field
|
||||
*/
|
||||
const PIN_POLICY = hypervisor => {
|
||||
const isVCenter = hypervisor === vcenter
|
||||
const isFirecracker = hypervisor === firecracker
|
||||
|
||||
return {
|
||||
name: 'TOPOLOGY.PIN_POLICY',
|
||||
label: T.PinPolicy,
|
||||
tooltip: [T.PinPolicyConcept, NUMA_PIN_POLICIES.join(', ')],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(NUMA_PIN_POLICIES, {
|
||||
addEmpty: false,
|
||||
getText: sentenceCase
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => isFirecracker
|
||||
? NUMA_PIN_POLICIES[2] // SHARED
|
||||
: NUMA_PIN_POLICIES[0] // NONE
|
||||
),
|
||||
fieldProps: { disabled: isVCenter || isFirecracker }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} hypervisor - VM hypervisor
|
||||
* @returns {Field} Cores field
|
||||
*/
|
||||
const CORES = hypervisor => ({
|
||||
name: 'TOPOLOGY.CORES',
|
||||
label: T.Cores,
|
||||
tooltip: T.NumaCoresConcept,
|
||||
dependOf: '$general.VCPU',
|
||||
type: hypervisor === vcenter ? INPUT_TYPES.SELECT : INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
values: vcpu => arrayToOptions(getFactorsOfNumber(vcpu ?? 0)),
|
||||
validation: number()
|
||||
.notRequired()
|
||||
.integer()
|
||||
.default(() => undefined)
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} hypervisor - VM hypervisor
|
||||
* @returns {Field} Sockets field
|
||||
*/
|
||||
const SOCKETS = hypervisor => ({
|
||||
name: 'TOPOLOGY.SOCKETS',
|
||||
label: T.Sockets,
|
||||
tooltip: T.NumaSocketsConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: number()
|
||||
.notRequired()
|
||||
.integer()
|
||||
.default(() => 1),
|
||||
fieldProps: {
|
||||
disabled: hypervisor === firecracker
|
||||
},
|
||||
...(hypervisor === vcenter && {
|
||||
fieldProps: { disabled: true },
|
||||
dependOf: ['$general.VCPU', 'TOPOLOGY.CORES'],
|
||||
watcher: ([vcpu, cores] = []) => {
|
||||
if (!isNaN(+vcpu) && !isNaN(+cores) && +cores !== 0) {
|
||||
return vcpu / cores
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} hypervisor - VM hypervisor
|
||||
* @returns {Field} Threads field
|
||||
*/
|
||||
const THREADS = hypervisor => ({
|
||||
name: 'TOPOLOGY.THREADS',
|
||||
label: T.Threads,
|
||||
tooltip: T.ThreadsConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: threadsValidation,
|
||||
...(hypervisor === firecracker && {
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([1, 2]),
|
||||
validation: threadsValidation.min(1).max(2)
|
||||
}),
|
||||
...(hypervisor === vcenter && {
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([1]),
|
||||
validation: threadsValidation.min(1).max(1)
|
||||
})
|
||||
})
|
||||
|
||||
/** @type {Field} Hugepage size field */
|
||||
const HUGEPAGES = {
|
||||
name: 'TOPOLOGY.HUGEPAGE_SIZE',
|
||||
label: T.HugepagesSize,
|
||||
tooltip: T.HugepagesSizeConcept,
|
||||
notOnHypervisors: [vcenter, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: () => {
|
||||
const hosts = useHost()
|
||||
const sizes = hosts
|
||||
.reduce((res, host) => res.concat(getHugepageSizes(host)), [])
|
||||
|
||||
return arrayToOptions([...new Set(sizes)], {
|
||||
getText: size => prettyBytes(+size, 'MB')
|
||||
})
|
||||
},
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @returns {Field} Memory access field */
|
||||
const MEMORY_ACCESS = {
|
||||
name: 'TOPOLOGY.MEMORY_ACCESS',
|
||||
label: T.MemoryAccess,
|
||||
tooltip: [T.MemoryAccessConcept, NUMA_MEMORY_ACCESS.join(', ')],
|
||||
notOnHypervisors: [vcenter, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(NUMA_MEMORY_ACCESS, { getText: sentenceCase }),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of NUMA fields
|
||||
*/
|
||||
export const NUMA_FIELDS = hypervisor =>
|
||||
filterFieldsByHypervisor(
|
||||
[PIN_POLICY, CORES, SOCKETS, THREADS, HUGEPAGES, MEMORY_ACCESS],
|
||||
hypervisor
|
||||
)
|
@ -0,0 +1,60 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 PropTypes from 'prop-types'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
import { STEP_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import {
|
||||
PLACEMENT_HOST_FIELDS,
|
||||
PLACEMENT_DS_FIELDS
|
||||
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/placement/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const Placement = () => {
|
||||
// TODO - Host requirements: add button to select HOST in list => ID="<id>"
|
||||
// TODO - Host policy options: Packing|Stripping|Load-aware
|
||||
|
||||
// TODO - DS requirements: add button to select DATASTORE in list => ID="<id>"
|
||||
// TODO - DS policy options: Packing|Stripping
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormWithSchema
|
||||
cy='create-vm-template-extra.host-placement'
|
||||
fields={PLACEMENT_HOST_FIELDS}
|
||||
legend={T.Host}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
<FormWithSchema
|
||||
cy='create-vm-template-extra.ds-placement'
|
||||
fields={PLACEMENT_DS_FIELDS}
|
||||
legend={T.Datastore}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Placement.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func
|
||||
}
|
||||
|
||||
Placement.displayName = 'Placement'
|
||||
|
||||
export default Placement
|
@ -0,0 +1,57 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 } from 'yup'
|
||||
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
|
||||
const HOST_REQ_FIELD = {
|
||||
name: 'SCHED_REQUIREMENTS',
|
||||
label: T.HostReqExpression,
|
||||
tooltip: T.HostReqExpressionConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
const HOST_RANK_FIELD = {
|
||||
name: 'SCHED_RANK',
|
||||
label: T.HostPolicyExpression,
|
||||
tooltip: T.HostPolicyExpressionConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
const DS_REQ_FIELD = {
|
||||
name: 'DS_SCHED_REQUIREMENTS',
|
||||
label: T.DatastoreReqExpression,
|
||||
tooltip: T.DatastoreReqExpressionConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
const DS_RANK_FIELD = {
|
||||
name: 'DS_SCHED_RANK',
|
||||
label: T.DatastorePolicyExpression,
|
||||
tooltip: T.DatastorePolicyExpressionConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
export const PLACEMENT_HOST_FIELDS = [HOST_REQ_FIELD, HOST_RANK_FIELD]
|
||||
|
||||
export const PLACEMENT_DS_FIELDS = [DS_REQ_FIELD, DS_RANK_FIELD]
|
||||
|
||||
export const PLACEMENT_FIELDS = [PLACEMENT_HOST_FIELDS, PLACEMENT_DS_FIELDS]
|
@ -0,0 +1,130 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 PropTypes from 'prop-types'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Edit, Trash } from 'iconoir-react'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
|
||||
import { useListForm } from 'client/hooks'
|
||||
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
|
||||
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
|
||||
import { PunctualForm, RelativeForm } from 'client/components/Forms/Vm'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
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 = 'SCHED_ACTION'
|
||||
|
||||
const ScheduleAction = ({ setFormData, control }) => {
|
||||
const classes = useStyles()
|
||||
const scheduleActions = useWatch({ name: `${EXTRA_ID}.${TAB_ID}`, control })
|
||||
|
||||
const { handleRemove, handleSave } = useListForm({
|
||||
parent: EXTRA_ID,
|
||||
key: TAB_ID,
|
||||
list: scheduleActions,
|
||||
setList: setFormData,
|
||||
getItemId: item => item.NAME,
|
||||
addItemId: (item, _, itemIndex) => ({ ...item, NAME: `${TAB_ID}${itemIndex}` })
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
color: 'secondary',
|
||||
'data-cy': 'add-sched-action',
|
||||
label: T.AddAction,
|
||||
variant: 'outlined'
|
||||
}}
|
||||
options={[{
|
||||
cy: 'add-sched-action-punctual',
|
||||
name: 'Punctual action',
|
||||
dialogProps: { title: T.ScheduledAction },
|
||||
form: () => PunctualForm(),
|
||||
onSubmit: handleSave
|
||||
},
|
||||
{
|
||||
cy: 'add-sched-action-relative',
|
||||
name: 'Relative action',
|
||||
dialogProps: { title: T.ScheduledAction },
|
||||
form: () => RelativeForm(),
|
||||
onSubmit: handleSave
|
||||
}]}
|
||||
/>
|
||||
<div className={classes.root}>
|
||||
{scheduleActions?.map(item => {
|
||||
const { NAME, ACTION, TIME } = item
|
||||
const isRelative = String(TIME).includes('+')
|
||||
|
||||
return (
|
||||
<SelectCard
|
||||
key={NAME}
|
||||
title={`${NAME} - ${ACTION}`}
|
||||
action={
|
||||
<>
|
||||
<Action
|
||||
data-cy={`remove-${NAME}`}
|
||||
handleClick={() => handleRemove(NAME)}
|
||||
icon={<Trash />}
|
||||
/>
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
'data-cy': `edit-${NAME}`,
|
||||
icon: <Edit />,
|
||||
tooltip: <Translate word={T.Edit} />
|
||||
}}
|
||||
options={[{
|
||||
dialogProps: {
|
||||
title: <><Translate word={T.Edit} />{`: ${NAME}`}</>
|
||||
},
|
||||
form: () => isRelative
|
||||
? RelativeForm(undefined, item)
|
||||
: PunctualForm(undefined, item),
|
||||
onSubmit: newValues => handleSave(newValues, NAME)
|
||||
}]}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ScheduleAction.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
ScheduleAction.displayName = 'ScheduleAction'
|
||||
|
||||
export default ScheduleAction
|
@ -0,0 +1,55 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { array, object } from 'yup'
|
||||
|
||||
import { PLACEMENT_FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/placement/schema'
|
||||
import { OS_FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting/schema'
|
||||
import { NUMA_FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/numa/schema'
|
||||
import { getObjectSchemaFromFields } from 'client/utils'
|
||||
|
||||
export const SCHEMA = hypervisor => object({
|
||||
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()
|
||||
.ensure()
|
||||
.transform(actions => actions?.map((action, idx) => ({
|
||||
...action,
|
||||
NAME: action?.NAME?.startsWith('SCHED_ACTION') || !action?.NAME
|
||||
? `SCHED_ACTION${idx}`
|
||||
: action?.NAME
|
||||
})))
|
||||
})
|
||||
.concat(getObjectSchemaFromFields([
|
||||
...PLACEMENT_FIELDS,
|
||||
...OS_FIELDS(hypervisor),
|
||||
...NUMA_FIELDS(hypervisor)
|
||||
]))
|
||||
.noUnknown(false)
|
@ -0,0 +1,192 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 PropTypes from 'prop-types'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Edit, Trash } from 'iconoir-react'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
|
||||
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 { Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { SCHEMA as EXTRA_SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
|
||||
import { reorderBootAfterRemove } from 'client/components/Forms/VmTemplate/CreateForm/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, hypervisor, control }) => {
|
||||
const classes = useStyles()
|
||||
const disks = useWatch({ name: `${EXTRA_ID}.${TAB_ID}`, control })
|
||||
|
||||
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: T.AttachDisk,
|
||||
variant: 'outlined'
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
cy: 'attach-image-disk',
|
||||
name: T.Image,
|
||||
dialogProps: { title: T.AttachImage },
|
||||
form: () => ImageSteps({ hypervisor }),
|
||||
onSubmit: handleSave
|
||||
},
|
||||
{
|
||||
cy: 'attach-volatile-disk',
|
||||
name: T.Volatile,
|
||||
dialogProps: { title: T.AttachVolatile },
|
||||
form: () => VolatileSteps({ 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 />}
|
||||
/>
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
'data-cy': `edit-${NAME}`,
|
||||
icon: <Edit />,
|
||||
tooltip: <Translate word={T.Edit} />
|
||||
}}
|
||||
options={[{
|
||||
dialogProps: {
|
||||
title: <Translate word={T.EditSomething} values={[NAME]} />
|
||||
},
|
||||
form: () => isVolatile
|
||||
? VolatileSteps({ hypervisor }, item)
|
||||
: ImageSteps({ hypervisor }, item),
|
||||
onSubmit: newValues => handleSave(newValues, NAME)
|
||||
}]}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Storage.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
Storage.displayName = 'Storage'
|
||||
|
||||
export default Storage
|
@ -0,0 +1,139 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { number, boolean } from 'yup'
|
||||
|
||||
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
import { Field } from 'client/utils'
|
||||
|
||||
/**
|
||||
* @param {Field} field - Field params
|
||||
* @param {boolean} field.required - If `true`, add to validation if it's required
|
||||
* @param {boolean} field.divBy4 - If `true`, add to validation if it's divisible by 4
|
||||
* @returns {Field} Capacity field params
|
||||
*/
|
||||
const CAPACITY_FIELD = ({ dependOf, required, divBy4, ...field }) => {
|
||||
let validation = number()
|
||||
.integer('Should be integer number')
|
||||
.positive('Should be positive number')
|
||||
.typeError('Must be a number')
|
||||
.default(() => undefined)
|
||||
|
||||
if (required) {
|
||||
validation = validation.required()
|
||||
}
|
||||
|
||||
if (dependOf) {
|
||||
validation = validation.when(
|
||||
dependOf,
|
||||
(enabledHr, schema) => enabledHr
|
||||
? schema.required()
|
||||
: schema.strip().notRequired()
|
||||
)
|
||||
}
|
||||
|
||||
if (divBy4) {
|
||||
validation = validation.when(
|
||||
'HYPERVISOR',
|
||||
(hypervisor, schema) => hypervisor === HYPERVISORS.vcenter
|
||||
? schema.isDivisibleBy(4)
|
||||
: schema
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
dependOf,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
...(dependOf && {
|
||||
htmlType: dependValue =>
|
||||
dependValue ? 'number' : INPUT_TYPES.HIDDEN
|
||||
}),
|
||||
validation
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Field} Memory field */
|
||||
export const MEMORY = CAPACITY_FIELD({
|
||||
name: 'MEMORY',
|
||||
label: T.Memory,
|
||||
tooltip: T.MemoryConcept,
|
||||
divBy4: true,
|
||||
required: true,
|
||||
grid: { md: 12 }
|
||||
})
|
||||
|
||||
/** @type {Field} Hot reloading on memory field */
|
||||
export const ENABLE_HR_MEMORY = {
|
||||
name: 'ENABLE_HR_MEMORY',
|
||||
label: T.EnableHotResize,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().default(() => false),
|
||||
grid: { xs: 4, md: 6 }
|
||||
}
|
||||
|
||||
/** @type {Field} Maximum memory field */
|
||||
export const MEMORY_MAX = CAPACITY_FIELD({
|
||||
name: 'MEMORY_MAX',
|
||||
label: T.MaxMemory,
|
||||
dependOf: ENABLE_HR_MEMORY.name,
|
||||
grid: { xs: 8, md: 6 }
|
||||
})
|
||||
|
||||
/** @type {Field} Physical CPU field */
|
||||
export const PHYSICAL_CPU = CAPACITY_FIELD({
|
||||
name: 'CPU',
|
||||
label: T.PhysicalCpu,
|
||||
tooltip: T.CpuConcept,
|
||||
required: true,
|
||||
grid: { md: 12 }
|
||||
})
|
||||
|
||||
/** @type {Field} Virtual CPU field */
|
||||
export const VIRTUAL_CPU = CAPACITY_FIELD({
|
||||
name: 'VCPU',
|
||||
label: T.VirtualCpu,
|
||||
tooltip: T.VirtualCpuConcept,
|
||||
grid: { md: 12 }
|
||||
})
|
||||
|
||||
/** @type {Field} Hot reloading on virtual CPU field */
|
||||
export const ENABLE_HR_VCPU = {
|
||||
name: 'ENABLE_HR_VCPU',
|
||||
label: T.EnableHotResize,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().default(() => false),
|
||||
grid: { xs: 4, md: 6 }
|
||||
}
|
||||
|
||||
/** @type {Field} Maximum virtual CPU field */
|
||||
export const VCPU_MAX = CAPACITY_FIELD({
|
||||
name: 'VCPU_MAX',
|
||||
label: T.MaxVirtualCpu,
|
||||
dependOf: ENABLE_HR_VCPU.name,
|
||||
grid: { xs: 8, md: 6 }
|
||||
})
|
||||
|
||||
/** @type {Field[]} List of capacity fields */
|
||||
export const FIELDS = [
|
||||
MEMORY,
|
||||
ENABLE_HR_MEMORY,
|
||||
MEMORY_MAX,
|
||||
PHYSICAL_CPU,
|
||||
VIRTUAL_CPU,
|
||||
ENABLE_HR_VCPU,
|
||||
VCPU_MAX
|
||||
]
|
@ -0,0 +1,77 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { useWatch } from 'react-hook-form'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import useStyles from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/styles'
|
||||
|
||||
import { HYPERVISOR_FIELD } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/informationSchema'
|
||||
import { SCHEMA, FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/schema'
|
||||
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'general'
|
||||
|
||||
const Content = () => {
|
||||
const classes = useStyles()
|
||||
const { view, getResourceView } = useAuth()
|
||||
const hypervisor = useWatch({ name: `${STEP_ID}.HYPERVISOR` })
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const dialog = getResourceView('VM-TEMPLATE')?.dialogs?.create_dialog
|
||||
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
|
||||
|
||||
return FIELDS(hypervisor)
|
||||
.filter(({ id, required }) => required || sectionsAvailable.includes(id))
|
||||
}, [view, hypervisor])
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormWithSchema
|
||||
cy={'create-vm-template-general.hypervisor'}
|
||||
fields={[HYPERVISOR_FIELD]}
|
||||
legend={T.Hypervisor}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
{groups.map(({ id, legend, fields }) => (
|
||||
<FormWithSchema
|
||||
key={id}
|
||||
className={classes[id]}
|
||||
cy={`create-vm-template-general.${id}`}
|
||||
fields={fields}
|
||||
legend={legend}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const General = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.General,
|
||||
resolver: formData => {
|
||||
const hypervisor = formData?.[STEP_ID]?.HYPERVISOR
|
||||
return SCHEMA(hypervisor)
|
||||
},
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content
|
||||
})
|
||||
|
||||
export default General
|
@ -0,0 +1,108 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string } from 'yup'
|
||||
|
||||
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 = {
|
||||
name: 'NAME',
|
||||
label: T.Name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 6 }
|
||||
}
|
||||
|
||||
/** @type {Field} Description field */
|
||||
export const DESCRIPTION = {
|
||||
name: 'DESCRIPTION',
|
||||
label: T.Description,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} Hypervisor field */
|
||||
export const HYPERVISOR_FIELD = {
|
||||
name: 'HYPERVISOR',
|
||||
type: INPUT_TYPES.TOGGLE,
|
||||
values: arrayToOptions(Object.values(HYPERVISORS), {
|
||||
addEmpty: false,
|
||||
getText: hypervisor => hypervisor.toUpperCase()
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => HYPERVISORS.kvm),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} Logo field */
|
||||
export const LOGO = {
|
||||
name: 'LOGO',
|
||||
label: T.Logo,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: [
|
||||
{ text: '-', value: '' },
|
||||
// client/assets/images/logos
|
||||
{ text: 'Alpine Linux', value: 'alpine.png' },
|
||||
{ text: 'ALT', value: 'alt.png' },
|
||||
{ text: 'Arch', value: 'arch.png' },
|
||||
{ text: 'CentOS', value: 'centos.png' },
|
||||
{ text: 'Debian', value: 'debian.png' },
|
||||
{ text: 'Devuan', value: 'devuan.png' },
|
||||
{ text: 'Fedora', value: 'fedora.png' },
|
||||
{ text: 'FreeBSD', value: 'freebsd.png' },
|
||||
{ text: 'HardenedBSD', value: 'hardenedbsd.png' },
|
||||
{ text: 'Knoppix', value: 'knoppix.png' },
|
||||
{ text: 'Linux', value: 'linux.png' },
|
||||
{ text: 'Oracle', value: 'oracle.png' },
|
||||
{ text: 'RedHat', value: 'redhat.png' },
|
||||
{ text: 'Suse', value: 'suse.png' },
|
||||
{ text: 'Ubuntu', value: 'ubuntu.png' },
|
||||
{ text: 'Windows xp', value: 'windowsxp.png' },
|
||||
{ text: 'Windows 10', value: 'windows8.png' }
|
||||
],
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderValue: value => (
|
||||
<Image
|
||||
imgProps={{
|
||||
height: 25,
|
||||
width: 25,
|
||||
style: { marginRight: 10 }
|
||||
}}
|
||||
src={`${LOGO_IMAGES_URL}/${value}`}
|
||||
/>
|
||||
),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field[]} List of information fields */
|
||||
export const FIELDS = [
|
||||
NAME,
|
||||
DESCRIPTION,
|
||||
LOGO
|
||||
]
|
@ -0,0 +1,64 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string } from 'yup'
|
||||
|
||||
import { useGroup, useUser } from 'client/features/One'
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
import { Field } from 'client/utils'
|
||||
|
||||
/** @type {Field} User id field */
|
||||
export const UID_FIELD = {
|
||||
name: 'AS_UID',
|
||||
label: T.InstantiateAsUser,
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
values: () => {
|
||||
const users = useUser()
|
||||
|
||||
return users
|
||||
.map(({ ID: value, NAME: text }) => ({ text, value }))
|
||||
.sort((a, b) => a.value - b.value)
|
||||
},
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(undefined),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} Group id field */
|
||||
export const GID_FIELD = {
|
||||
name: 'AS_GID',
|
||||
label: T.InstantiateAsGroup,
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
values: () => {
|
||||
const groups = useGroup()
|
||||
|
||||
return groups
|
||||
.map(({ ID: value, NAME: text }) => ({ text, value }))
|
||||
.sort((a, b) => a.value - b.value)
|
||||
},
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(undefined),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field[]} List of ownership fields */
|
||||
export const FIELDS = [
|
||||
UID_FIELD,
|
||||
GID_FIELD
|
||||
]
|
@ -0,0 +1,69 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { BaseSchema } from 'yup'
|
||||
|
||||
import { FIELDS as INFORMATION_FIELDS, HYPERVISOR_FIELD } from './informationSchema'
|
||||
import { FIELDS as CAPACITY_FIELDS } from './capacitySchema'
|
||||
import { FIELDS as VM_GROUP_FIELDS } from './vmGroupSchema'
|
||||
import { FIELDS as OWNERSHIP_FIELDS } from './ownershipSchema'
|
||||
import { FIELDS as VCENTER_FIELDS } from './vcenterSchema'
|
||||
|
||||
import { filterFieldsByHypervisor, getObjectSchemaFromFields, Field } from 'client/utils'
|
||||
import { T, HYPERVISORS } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
|
||||
* @returns {{ id: string, legend: string, fields: Field[] }[]} Fields
|
||||
*/
|
||||
const FIELDS = hypervisor => [
|
||||
{
|
||||
id: 'information',
|
||||
legend: T.Information,
|
||||
required: true,
|
||||
fields: filterFieldsByHypervisor(INFORMATION_FIELDS, hypervisor)
|
||||
},
|
||||
{
|
||||
id: 'capacity',
|
||||
legend: T.Capacity,
|
||||
fields: filterFieldsByHypervisor(CAPACITY_FIELDS, hypervisor)
|
||||
},
|
||||
{
|
||||
id: 'ownership',
|
||||
legend: T.Ownership,
|
||||
fields: filterFieldsByHypervisor(OWNERSHIP_FIELDS, hypervisor)
|
||||
},
|
||||
{
|
||||
id: 'vm_group',
|
||||
legend: T.VMGroup,
|
||||
fields: filterFieldsByHypervisor(VM_GROUP_FIELDS, hypervisor)
|
||||
},
|
||||
{
|
||||
id: 'vcenter',
|
||||
legend: T.vCenterDeployment,
|
||||
fields: filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor)
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
|
||||
* @returns {BaseSchema} Step schema
|
||||
*/
|
||||
const SCHEMA = hypervisor => getObjectSchemaFromFields([
|
||||
HYPERVISOR_FIELD,
|
||||
...FIELDS(hypervisor).map(({ fields }) => fields).flat()
|
||||
])
|
||||
|
||||
export { FIELDS, SCHEMA }
|
@ -0,0 +1,30 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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(theme => ({
|
||||
root: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '2em',
|
||||
[theme.breakpoints.down('lg')]: {
|
||||
gridTemplateColumns: '1fr'
|
||||
}
|
||||
},
|
||||
information: {
|
||||
gridColumn: '1 / -1'
|
||||
}
|
||||
}))
|
@ -0,0 +1,73 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string } from 'yup'
|
||||
|
||||
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
import { Field } from 'client/utils'
|
||||
|
||||
const { vcenter, ...hypervisors } = HYPERVISORS
|
||||
|
||||
/** @type {Field} Common field attributes */
|
||||
const commonAttributes = {
|
||||
notOnHypervisors: Object.values(hypervisors),
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} vCenter template reference field */
|
||||
const VCENTER_TEMPLATE_FIELD = {
|
||||
...commonAttributes,
|
||||
name: 'VCENTER_TEMPLATE_REF',
|
||||
label: T.vCenterTemplateRef
|
||||
}
|
||||
|
||||
/** @type {Field} vCenter cluster reference field */
|
||||
const VCENTER_CCR_FIELD = {
|
||||
...commonAttributes,
|
||||
name: 'VCENTER_CCR_REF',
|
||||
label: T.vCenterClusterRef
|
||||
}
|
||||
|
||||
/** @type {Field} vCenter instance id field */
|
||||
const VCENTER_INSTANCE_ID = {
|
||||
...commonAttributes,
|
||||
name: 'VCENTER_INSTANCE_ID',
|
||||
label: T.vCenterInstanceId
|
||||
}
|
||||
|
||||
/** @type {Field} vCenter VM folder field */
|
||||
const VCENTER_FOLDER_FIELD = {
|
||||
...commonAttributes,
|
||||
name: 'VCENTER_VM_FOLDER',
|
||||
label: T.vCenterVmFolder,
|
||||
tooltip: T.vCenterVmFolderConcept,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field[]} List of vCenter fields */
|
||||
export const FIELDS = [
|
||||
VCENTER_TEMPLATE_FIELD,
|
||||
VCENTER_CCR_FIELD,
|
||||
VCENTER_INSTANCE_ID,
|
||||
VCENTER_FOLDER_FIELD
|
||||
]
|
@ -0,0 +1,78 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string } from 'yup'
|
||||
|
||||
import { useVmGroup } from 'client/features/One'
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
import { Field } from 'client/utils'
|
||||
|
||||
/** @type {Field} VM Group field */
|
||||
export const VM_GROUP_FIELD = {
|
||||
name: 'VMGROUP.VMGROUP_ID',
|
||||
label: T.AssociateToVMGroup,
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
values: () => {
|
||||
const vmGroups = useVmGroup()
|
||||
|
||||
return vmGroups
|
||||
?.map(({ ID, NAME }) => ({ text: `#${ID} ${NAME}`, value: ID }))
|
||||
?.sort((a, b) => {
|
||||
const compareOptions = { numeric: true, ignorePunctuation: true }
|
||||
|
||||
return a.value.localeCompare(b.value, undefined, compareOptions)
|
||||
})
|
||||
},
|
||||
grid: { md: 12 },
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/** @type {Field} Role field */
|
||||
export const ROLE_FIELD = {
|
||||
name: 'VMGROUP.ROLE',
|
||||
label: T.Role,
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
dependOf: VM_GROUP_FIELD.name,
|
||||
htmlType: vmGroup => vmGroup && vmGroup !== '' ? undefined : INPUT_TYPES.HIDDEN,
|
||||
values: vmGroupSelected => {
|
||||
const vmGroups = useVmGroup()
|
||||
|
||||
const roles = vmGroups
|
||||
?.filter(({ ID }) => ID === vmGroupSelected)
|
||||
?.map(({ ROLES }) =>
|
||||
[ROLES?.ROLE ?? []].flat().map(({ NAME: ROLE_NAME }) => ROLE_NAME)
|
||||
)
|
||||
?.flat()
|
||||
|
||||
return roles.map(role => ({ text: role, value: role }))
|
||||
},
|
||||
grid: { md: 12 },
|
||||
validation: string()
|
||||
.trim()
|
||||
.default(() => undefined)
|
||||
.when(
|
||||
'VMGROUP_ID',
|
||||
(vmGroup, schema) => vmGroup ? schema.required() : schema
|
||||
)
|
||||
}
|
||||
|
||||
/** @type {Field[]} List of VM Group fields */
|
||||
export const FIELDS = [
|
||||
VM_GROUP_FIELD,
|
||||
ROLE_FIELD
|
||||
]
|
@ -0,0 +1,37 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 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 { createSteps } from 'client/utils'
|
||||
|
||||
const Steps = createSteps(
|
||||
[General, ExtraConfiguration],
|
||||
{
|
||||
// transformInitialValue: (vmTemplate, schema) =>,
|
||||
transformBeforeSubmit: formData => {
|
||||
const {
|
||||
[GENERAL_ID]: general = {},
|
||||
[EXTRA_ID]: extraTemplate = {}
|
||||
} = formData ?? {}
|
||||
|
||||
const templateXML = jsonToXml({ ...general, ...extraTemplate })
|
||||
return { template: templateXML }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default Steps
|
@ -0,0 +1,83 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { useEffect, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import { useFetch } from 'client/hooks'
|
||||
// import { useUserApi, useVmGroupApi, useVmTemplateApi } from 'client/features/One'
|
||||
import { useVmTemplateApi, useHostApi, useImageApi } from 'client/features/One'
|
||||
import FormStepper, { SkeletonStepsForm } from 'client/components/FormStepper'
|
||||
import Steps from 'client/components/Forms/VmTemplate/CreateForm/Steps'
|
||||
|
||||
const CreateForm = ({ template, onSubmit }) => {
|
||||
const stepsForm = useMemo(() => Steps(template, {}), [])
|
||||
const { steps, defaultValues, resolver, transformBeforeSubmit } = stepsForm
|
||||
|
||||
const methods = useForm({
|
||||
mode: 'onSubmit',
|
||||
defaultValues,
|
||||
resolver: yupResolver(resolver?.())
|
||||
})
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<FormStepper
|
||||
steps={steps}
|
||||
schema={resolver}
|
||||
onSubmit={data => onSubmit(transformBeforeSubmit?.(data) ?? data)}
|
||||
/>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const PreFetchingForm = ({ templateId, onSubmit }) => {
|
||||
// const { getUsers } = useUserApi()
|
||||
// const { getVmGroups } = useVmGroupApi()
|
||||
const { getHosts } = useHostApi()
|
||||
const { getImages } = useImageApi()
|
||||
const { getVmTemplate } = useVmTemplateApi()
|
||||
const { fetchRequest, data } = useFetch(
|
||||
() => getVmTemplate(templateId, { extended: true })
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
templateId && fetchRequest()
|
||||
getHosts()
|
||||
getImages()
|
||||
// getUsers()
|
||||
// getVmGroups()
|
||||
}, [])
|
||||
|
||||
return (templateId && !data)
|
||||
? <SkeletonStepsForm />
|
||||
: <CreateForm template={data} onSubmit={onSubmit} />
|
||||
}
|
||||
|
||||
PreFetchingForm.propTypes = {
|
||||
templateId: PropTypes.string,
|
||||
onSubmit: PropTypes.func
|
||||
}
|
||||
|
||||
CreateForm.propTypes = {
|
||||
template: PropTypes.object,
|
||||
onSubmit: PropTypes.func
|
||||
}
|
||||
|
||||
export default PreFetchingForm
|
@ -17,7 +17,6 @@
|
||||
import { number } from 'yup'
|
||||
|
||||
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
import { isDivisibleBy4 } from 'client/utils'
|
||||
|
||||
const MEMORY = hypervisor => {
|
||||
let validation = number()
|
||||
@ -28,8 +27,7 @@ const MEMORY = hypervisor => {
|
||||
.default(() => undefined)
|
||||
|
||||
if (hypervisor === HYPERVISORS.vcenter) {
|
||||
validation = validation
|
||||
.test('is-divisible-by-4', 'Memory should be divisible by 4', isDivisibleBy4)
|
||||
validation = validation.isDivisibleBy(4)
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -20,11 +20,10 @@ import { useFormContext } from 'react-hook-form'
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import useStyles from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/styles'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
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 { getActionsAvailable } from 'client/models/Helper'
|
||||
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'configuration'
|
||||
@ -37,9 +36,9 @@ const Content = () => {
|
||||
const groups = useMemo(() => {
|
||||
const hypervisor = watch(`${TEMPLATE_ID}[0].TEMPLATE.HYPERVISOR`)
|
||||
const dialog = getResourceView('VM-TEMPLATE')?.dialogs?.instantiate_dialog
|
||||
const groupsAvailable = getActionsAvailable(dialog, hypervisor)
|
||||
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
|
||||
|
||||
return FIELDS(hypervisor).filter(({ id }) => groupsAvailable.includes(id))
|
||||
return FIELDS(hypervisor).filter(({ id }) => sectionsAvailable.includes(id))
|
||||
}, [view])
|
||||
|
||||
return (
|
||||
@ -50,7 +49,7 @@ const Content = () => {
|
||||
className={classes[id]}
|
||||
cy={`instantiate-vm-template-configuration.${id}`}
|
||||
fields={fields}
|
||||
legend={Tr(legend)}
|
||||
legend={legend}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
))}
|
||||
|
@ -27,7 +27,7 @@ import { T, HYPERVISORS } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
|
||||
* @returns {{ id: string, legend: string, fields: Field[] }[]} Fields
|
||||
* @returns {function(string):{ id: string, legend: string, fields: Field[] }[]} Fields
|
||||
*/
|
||||
const FIELDS = hypervisor => [
|
||||
{
|
||||
|
@ -33,20 +33,20 @@ import Booting from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/Ex
|
||||
|
||||
import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable'
|
||||
import { SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
|
||||
import { getActionsAvailable } from 'client/models/Helper'
|
||||
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'extra'
|
||||
|
||||
const Content = ({ data, setFormData }) => {
|
||||
const theme = useTheme()
|
||||
const { watch, errors, control } = useFormContext()
|
||||
const { watch, formState: { errors }, control } = useFormContext()
|
||||
const { view, getResourceView } = useAuth()
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
const hypervisor = watch(`${TEMPLATE_ID}[0].TEMPLATE.HYPERVISOR`)
|
||||
const dialog = getResourceView('VM-TEMPLATE')?.dialogs?.instantiate_dialog
|
||||
const groupsAvailable = getActionsAvailable(dialog, hypervisor)
|
||||
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
|
||||
|
||||
return [
|
||||
{
|
||||
@ -89,7 +89,7 @@ const Content = ({ data, setFormData }) => {
|
||||
<WarningIcon color={theme.palette.error.main} />
|
||||
)
|
||||
}
|
||||
].filter(({ id }) => groupsAvailable.includes(id))
|
||||
].filter(({ id }) => sectionsAvailable.includes(id))
|
||||
}, [errors[STEP_ID], view, control])
|
||||
|
||||
return (
|
||||
|
@ -14,9 +14,11 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import CloneForm from 'client/components/Forms/VmTemplate/CloneForm'
|
||||
import CreateForm from 'client/components/Forms/VmTemplate/CreateForm'
|
||||
import InstantiateForm from 'client/components/Forms/VmTemplate/InstantiateForm'
|
||||
|
||||
export {
|
||||
CloneForm,
|
||||
CreateForm,
|
||||
InstantiateForm
|
||||
}
|
||||
|
@ -26,6 +26,9 @@ import { isDevelopment } from 'client/utils'
|
||||
const TranslateContext = createContext()
|
||||
let languageScript = root.document?.createElement('script')
|
||||
|
||||
const labelCanBeTranslated = val =>
|
||||
typeof val === 'string' || (Array.isArray(val) && val.length === 2)
|
||||
|
||||
const GenerateScript = (
|
||||
language = DEFAULT_LANGUAGE,
|
||||
setHash = () => undefined
|
||||
@ -48,7 +51,7 @@ const RemoveScript = () => {
|
||||
root.document.body.removeChild(languageScript)
|
||||
}
|
||||
|
||||
const TranslateProvider = ({ children }) => {
|
||||
const TranslateProvider = ({ children = [] }) => {
|
||||
const [hash, setHash] = useState({})
|
||||
const { settings: { lang } = {} } = useAuth()
|
||||
|
||||
@ -104,7 +107,7 @@ const Tr = (str = '') => {
|
||||
return translateString(key, valuesTr)
|
||||
}
|
||||
|
||||
const Translate = ({ word = '', values }) => {
|
||||
const Translate = ({ word = '', values = [] }) => {
|
||||
const valuesTr = !Array.isArray(values) ? [values] : values
|
||||
return translateString(word, valuesTr)
|
||||
}
|
||||
@ -116,18 +119,20 @@ TranslateProvider.propTypes = {
|
||||
])
|
||||
}
|
||||
|
||||
TranslateProvider.defaultProps = {
|
||||
children: []
|
||||
}
|
||||
|
||||
Translate.propTypes = {
|
||||
word: PropTypes.string,
|
||||
values: PropTypes.oneOfType([PropTypes.string, PropTypes.array])
|
||||
values: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
PropTypes.array
|
||||
])
|
||||
}
|
||||
|
||||
Translate.defaultProps = {
|
||||
word: '',
|
||||
values: ''
|
||||
export {
|
||||
TranslateContext,
|
||||
TranslateProvider,
|
||||
Translate,
|
||||
Tr,
|
||||
labelCanBeTranslated
|
||||
}
|
||||
|
||||
export { TranslateContext, TranslateProvider, Translate, Tr }
|
||||
|
@ -15,9 +15,10 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useEffect, JSXElementConstructor } from 'react'
|
||||
|
||||
import { MenuList, MenuItem } from '@mui/material'
|
||||
import { MenuList, MenuItem, LinearProgress } from '@mui/material'
|
||||
import { Language as ZoneIcon } from 'iconoir-react'
|
||||
|
||||
import { useFetch } from 'client/hooks'
|
||||
import { useZone, useZoneApi } from 'client/features/One'
|
||||
import HeaderPopover from 'client/components/Header/Popover'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
@ -31,10 +32,7 @@ import { T } from 'client/constants'
|
||||
const Zone = () => {
|
||||
const zones = useZone()
|
||||
const { getZones } = useZoneApi()
|
||||
|
||||
useEffect(() => {
|
||||
!zones?.length && getZones()
|
||||
}, [])
|
||||
const { fetchRequest, loading } = useFetch(getZones)
|
||||
|
||||
return (
|
||||
<HeaderPopover
|
||||
@ -44,18 +42,31 @@ const Zone = () => {
|
||||
buttonProps={{ 'data-cy': 'header-zone-button' }}
|
||||
headerTitle={<Translate word={T.Zones} />}
|
||||
>
|
||||
{({ handleClose }) => (
|
||||
<MenuList>
|
||||
{zones?.length
|
||||
? zones?.map(({ ID, NAME }) => (
|
||||
<MenuItem key={`zone-${ID}`} onClick={handleClose}>
|
||||
{NAME}
|
||||
</MenuItem>
|
||||
))
|
||||
: <MenuItem disabled>{'Not zones found'}</MenuItem>
|
||||
}
|
||||
</MenuList>
|
||||
)}
|
||||
{({ handleClose }) => {
|
||||
useEffect(() => {
|
||||
fetchRequest()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <LinearProgress color='secondary' />}
|
||||
<MenuList>
|
||||
{zones?.length
|
||||
? zones?.map(({ ID, NAME }) => (
|
||||
<MenuItem key={`zone-${ID}`} onClick={handleClose}>
|
||||
{NAME}
|
||||
</MenuItem>
|
||||
))
|
||||
: (
|
||||
<MenuItem disabled>
|
||||
<Translate word={T.NotFound} />
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
</MenuList>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</HeaderPopover>
|
||||
)
|
||||
}
|
||||
|
@ -93,7 +93,11 @@ const Header = () => {
|
||||
{title}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction='row' flexGrow={1} justifyContent='end'>
|
||||
<Stack
|
||||
direction='row'
|
||||
justifyContent='end'
|
||||
sx={{ flexGrow: { xs: 1, sm: 0 } }}
|
||||
>
|
||||
<User />
|
||||
<View />
|
||||
{!isOneAdmin && <Group />}
|
||||
|
@ -18,7 +18,7 @@ import { number, string, oneOfType } from 'prop-types'
|
||||
|
||||
const DockerLogo = memo(({ viewBox, width, height, color, ...props }) => {
|
||||
return (
|
||||
<svg viewBox={viewBox} width={width} height={height} {...props} {...props}>
|
||||
<svg viewBox={viewBox} width={width} height={height} {...props}>
|
||||
<path
|
||||
fill={color}
|
||||
d="M296 245h42v-38h-42zm-50 0h42v-38h-42zm-49 0h42v-38h-42zm-49 0h41v-38h-41zm-50 0h42v-38H98zm50-46h41v-38h-41zm49 0h42v-38h-42zm49 0h42v-38h-42zm0-46h42v-38h-42zm226 75s-18-17-55-11c-4-29-35-46-35-46s-29 35-8 74c-6 3-16 7-31 7H68c-5 19-5 145 133 145 99 0 173-46 208-130 52 4 63-39 63-39z"
|
@ -13,8 +13,8 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import DockerLogo from 'client/components/Icons/docker'
|
||||
import OpenNebulaLogo from 'client/components/Icons/opennebula'
|
||||
import DockerLogo from 'client/components/Icons/DockerIcon'
|
||||
import OpenNebulaLogo from 'client/components/Icons/OpenNebulaIcon'
|
||||
|
||||
export {
|
||||
DockerLogo,
|
||||
|
@ -29,65 +29,67 @@ const INITIAL_STATE = { fail: false, retries: 0 }
|
||||
* @param {string} props.imgProps - Properties to image element
|
||||
* @returns {JSXElementConstructor} Picture with all images format
|
||||
*/
|
||||
const Image = memo(({ src, imageInError, withSources, imgProps }) => {
|
||||
const [error, setError] = useState(INITIAL_STATE)
|
||||
const Image = memo(
|
||||
({
|
||||
src,
|
||||
imageInError = DEFAULT_IMAGE,
|
||||
withSources = false,
|
||||
pictureProps = {},
|
||||
imgProps = {}
|
||||
}) => {
|
||||
const [error, setError] = useState(INITIAL_STATE)
|
||||
|
||||
/** Increment retries by one in error state. */
|
||||
const addRetry = () => {
|
||||
setError(prev => ({ ...prev, retries: prev.retries + 1 }))
|
||||
}
|
||||
const imageProps = {
|
||||
decoding: 'async',
|
||||
draggable: false,
|
||||
loading: 'lazy',
|
||||
...imgProps
|
||||
}
|
||||
|
||||
/** Set failed state. */
|
||||
const onImageFail = () => {
|
||||
setError(prev => ({ fail: true, retries: prev.retries + 1 }))
|
||||
}
|
||||
/** Increment retries by one in error state. */
|
||||
const addRetry = () => {
|
||||
setError(prev => ({ ...prev, retries: prev.retries + 1 }))
|
||||
}
|
||||
|
||||
if (error.retries >= MAX_RETRIES) {
|
||||
return null
|
||||
}
|
||||
/** Set failed state. */
|
||||
const onImageFail = () => {
|
||||
setError(prev => ({ fail: true, retries: prev.retries + 1 }))
|
||||
}
|
||||
|
||||
if (error.fail) {
|
||||
return <img
|
||||
{...imgProps}
|
||||
src={imageInError}
|
||||
draggable={false}
|
||||
onError={addRetry}
|
||||
/>
|
||||
}
|
||||
if (error.retries >= MAX_RETRIES) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<picture>
|
||||
{withSources && IMAGE_FORMATS.map(format => (
|
||||
<source key={format}
|
||||
srcSet={`${src}.${format}`}
|
||||
type={`image/${format}`}
|
||||
/>
|
||||
))}
|
||||
<img {...imgProps} src={src} onError={onImageFail} />
|
||||
</picture>
|
||||
)
|
||||
}, (prev, next) => prev.src === next.src)
|
||||
if (error.fail) {
|
||||
return <img
|
||||
{...imageProps}
|
||||
src={imageInError}
|
||||
draggable={false}
|
||||
onError={addRetry}
|
||||
/>
|
||||
}
|
||||
|
||||
return withSources ? (
|
||||
<picture {...pictureProps}>
|
||||
{withSources && IMAGE_FORMATS.map(format => (
|
||||
<source key={format}
|
||||
srcSet={`${src}.${format}`}
|
||||
type={`image/${format}`}
|
||||
/>
|
||||
))}
|
||||
<img {...imageProps} src={src} onError={onImageFail} />
|
||||
</picture>
|
||||
) : (
|
||||
<img {...imageProps} src={src} onError={onImageFail} />
|
||||
)
|
||||
}, (prev, next) => prev.src === next.src)
|
||||
|
||||
Image.propTypes = {
|
||||
src: PropTypes.string,
|
||||
imageInError: PropTypes.string,
|
||||
withSources: PropTypes.bool,
|
||||
imgProps: PropTypes.shape({
|
||||
decoding: PropTypes.oneOf(['sync', 'async', 'auto']),
|
||||
draggable: PropTypes.bool,
|
||||
loading: PropTypes.oneOf(['eager', 'lazy'])
|
||||
})
|
||||
}
|
||||
|
||||
Image.defaultProps = {
|
||||
src: undefined,
|
||||
imageInError: DEFAULT_IMAGE,
|
||||
withSources: false,
|
||||
imgProps: {
|
||||
decoding: 'async',
|
||||
draggable: false,
|
||||
loading: 'lazy'
|
||||
}
|
||||
pictureProps: PropTypes.object,
|
||||
imgProps: PropTypes.object
|
||||
}
|
||||
|
||||
Image.displayName = 'Image'
|
||||
|
@ -27,17 +27,24 @@ const useStyles = makeStyles(({ spacing, palette, typography }) => {
|
||||
const defaultStateColor = palette.grey[palette.mode === SCHEMES.DARK ? 300 : 700]
|
||||
|
||||
return {
|
||||
root: ({ stateColor = defaultStateColor }) => ({
|
||||
color: getBackgroundColor(stateColor, 0.75),
|
||||
backgroundColor: addOpacityToColor(stateColor, 0.2),
|
||||
cursor: 'default',
|
||||
padding: spacing('0.25rem', '0.5rem'),
|
||||
borderRadius: 2,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: typography.overline.fontSize,
|
||||
fontWeight: 500,
|
||||
lineHeight: 'normal'
|
||||
})
|
||||
root: ({ stateColor = defaultStateColor }) => {
|
||||
const paletteColor = palette[stateColor]
|
||||
|
||||
const color = paletteColor?.contrastText ?? getBackgroundColor(stateColor, 0.75)
|
||||
const bgColor = paletteColor?.dark ?? stateColor
|
||||
|
||||
return {
|
||||
color,
|
||||
backgroundColor: addOpacityToColor(bgColor, 0.2),
|
||||
cursor: 'default',
|
||||
padding: spacing('0.25rem', '0.5rem'),
|
||||
borderRadius: 2,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: typography.overline.fontSize,
|
||||
fontWeight: 500,
|
||||
lineHeight: 'normal'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -97,6 +97,6 @@ GlobalActions.propTypes = {
|
||||
useTableProps: PropTypes.object
|
||||
}
|
||||
|
||||
export { Action, ActionPropTypes, GlobalAction }
|
||||
export { Action, ActionPropTypes }
|
||||
|
||||
export default GlobalActions
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import CategoryFilter from 'client/components/Tables/Enhanced/Utils/CategoryFilter'
|
||||
import GlobalActions, { Action, ActionPropTypes, GlobalAction } from 'client/components/Tables/Enhanced/Utils/GlobalActions'
|
||||
import GlobalActions, { Action, ActionPropTypes } from 'client/components/Tables/Enhanced/Utils/GlobalActions'
|
||||
import GlobalFilter from 'client/components/Tables/Enhanced/Utils/GlobalFilter'
|
||||
import GlobalSelectedRows from 'client/components/Tables/Enhanced/Utils/GlobalSelectedRows'
|
||||
import GlobalSort from 'client/components/Tables/Enhanced/Utils/GlobalSort'
|
||||
@ -26,7 +26,6 @@ export {
|
||||
Action,
|
||||
ActionPropTypes,
|
||||
CategoryFilter,
|
||||
GlobalAction,
|
||||
GlobalActions,
|
||||
GlobalFilter,
|
||||
GlobalSelectedRows,
|
||||
|
@ -81,13 +81,10 @@ const Actions = () => {
|
||||
accessor: VM_TEMPLATE_ACTIONS.CREATE_DIALOG,
|
||||
tooltip: T.Create,
|
||||
icon: AddSquare,
|
||||
disabled: true,
|
||||
action: rows => {
|
||||
// TODO: go to CREATE form
|
||||
// const { ID } = rows?.[0]?.original ?? {}
|
||||
// const path = generatePath(PATH.TEMPLATE.VMS.CREATE, { id: ID })
|
||||
action: () => {
|
||||
const path = PATH.TEMPLATE.VMS.CREATE
|
||||
|
||||
// history.push(path)
|
||||
history.push(path)
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -174,25 +171,25 @@ const Actions = () => {
|
||||
selected: true,
|
||||
color: 'secondary',
|
||||
options: [{
|
||||
cy: `action.${VM_TEMPLATE_ACTIONS.CHANGE_OWNER}`,
|
||||
accessor: VM_TEMPLATE_ACTIONS.CHANGE_OWNER,
|
||||
name: T.ChangeOwner,
|
||||
disabled: true,
|
||||
isConfirmDialog: true,
|
||||
onSubmit: () => undefined
|
||||
}, {
|
||||
cy: `action.${VM_TEMPLATE_ACTIONS.CHANGE_GROUP}`,
|
||||
accessor: VM_TEMPLATE_ACTIONS.CHANGE_GROUP,
|
||||
name: T.ChangeGroup,
|
||||
disabled: true,
|
||||
isConfirmDialog: true,
|
||||
onSubmit: () => undefined
|
||||
}, {
|
||||
cy: `action.${VM_TEMPLATE_ACTIONS.SHARE}`,
|
||||
accessor: VM_TEMPLATE_ACTIONS.SHARE,
|
||||
disabled: true,
|
||||
name: T.Share,
|
||||
isConfirmDialog: true,
|
||||
onSubmit: () => undefined
|
||||
}, {
|
||||
cy: `action.${VM_TEMPLATE_ACTIONS.UNSHARE}`,
|
||||
accessor: VM_TEMPLATE_ACTIONS.UNSHARE,
|
||||
disabled: true,
|
||||
name: T.Unshare,
|
||||
isConfirmDialog: true,
|
||||
@ -203,8 +200,9 @@ const Actions = () => {
|
||||
tooltip: T.Lock,
|
||||
icon: Lock,
|
||||
selected: true,
|
||||
color: 'secondary',
|
||||
options: [{
|
||||
cy: `action.${VM_TEMPLATE_ACTIONS.LOCK}`,
|
||||
accessor: VM_TEMPLATE_ACTIONS.LOCK,
|
||||
name: T.Lock,
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
@ -217,7 +215,7 @@ const Actions = () => {
|
||||
await Promise.all(ids.map(id => getVmTemplate(id)))
|
||||
}
|
||||
}, {
|
||||
cy: `action.${VM_TEMPLATE_ACTIONS.UNLOCK}`,
|
||||
accessor: VM_TEMPLATE_ACTIONS.UNLOCK,
|
||||
name: T.Unlock,
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
|
@ -26,7 +26,6 @@ const Content = ({ name, renderContent: Content, hidden }) => (
|
||||
sx={{
|
||||
p: theme => theme.spacing(2, 1),
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
display: hidden ? 'none' : 'block'
|
||||
}}
|
||||
>
|
||||
@ -45,13 +44,13 @@ const Tabs = ({ tabs = [], renderHiddenTabs = false }) => {
|
||||
scrollButtons='auto'
|
||||
onChange={(_, tab) => setTab(tab)}
|
||||
>
|
||||
{tabs.map(({ value, name, icon: Icon }, idx) =>
|
||||
{tabs.map(({ value, name, label, icon: Icon }, idx) =>
|
||||
<MTab
|
||||
key={`tab-${name}`}
|
||||
id={`tab-${name}`}
|
||||
icon={Icon && <Icon />}
|
||||
value={value ?? idx}
|
||||
label={name}
|
||||
label={label ?? name}
|
||||
/>
|
||||
)}
|
||||
</MTabs>
|
||||
|
@ -83,7 +83,10 @@ const TotalProviders = ({ isLoading }) => {
|
||||
|
||||
return useMemo(() => (
|
||||
!totalProviders && isLoading ? (
|
||||
<Skeleton variant='rectangular' height={350} />
|
||||
<Skeleton
|
||||
variant='rectangular'
|
||||
sx={{ height: { xs: 210, sm: 350 } }}
|
||||
/>
|
||||
) : (
|
||||
<Paper
|
||||
data-cy='dashboard-widget-total-providers-by-type'
|
||||
|
@ -55,7 +55,7 @@ const TotalProvisionsByState = ({ isLoading }) => {
|
||||
!totalProvisions && isLoading ? (
|
||||
<Skeleton
|
||||
variant='rectangular'
|
||||
sx={{ height: { xs: 210, sm: 210, md: 380 } }}
|
||||
sx={{ height: { xs: 210, sm: 350 } }}
|
||||
/>
|
||||
) : (
|
||||
<Paper
|
||||
|
@ -69,7 +69,8 @@ export const INPUT_TYPES = {
|
||||
SELECT: 'select',
|
||||
SLIDER: 'slider',
|
||||
TEXT: 'text',
|
||||
TABLE: 'table'
|
||||
TABLE: 'table',
|
||||
TOGGLE: 'toggle'
|
||||
}
|
||||
|
||||
export const DEBUG_LEVEL = {
|
||||
|
@ -271,6 +271,7 @@ module.exports = {
|
||||
Provisions: 'Provisions',
|
||||
|
||||
/* tabs */
|
||||
General: 'General',
|
||||
Information: 'Information',
|
||||
Placement: 'Placement',
|
||||
|
||||
@ -314,7 +315,8 @@ module.exports = {
|
||||
/* VM schema - info */
|
||||
UserTemplate: 'User Template',
|
||||
Template: 'Template',
|
||||
WhereIsRunning: 'VM %1$s is currently running on Host %2$s and Datastore %3$s',
|
||||
WhereIsRunning:
|
||||
'VM %1$s is currently running on Host %2$s and Datastore %3$s',
|
||||
/* VM schema - capacity */
|
||||
Capacity: 'Capacity',
|
||||
PhysicalCpu: 'Physical CPU',
|
||||
@ -338,9 +340,154 @@ module.exports = {
|
||||
Alias: 'Alias',
|
||||
|
||||
/* VM Template schema */
|
||||
/* VM Template schema - booting */
|
||||
/* VM schema - general */
|
||||
Logo: 'Logo',
|
||||
Hypervisor: 'Hypervisor',
|
||||
/* VM schema - ownership */
|
||||
InstantiateAsUser: 'Instantiate as different User',
|
||||
InstantiateAsGroup: 'Instantiate as different Group',
|
||||
/* VM Template schema - capacity */
|
||||
MaxMemory: 'Max memory',
|
||||
MemoryModification: 'Memory modification',
|
||||
AllowUsersToModifyMemory:
|
||||
"Allow users to modify this template's default memory on instantiate",
|
||||
MemoryConcept: 'Amount of RAM required for the VM',
|
||||
CpuConcept: `
|
||||
Percentage of CPU divided by 100 required for the
|
||||
Virtual Machine. Half a processor is written 0.5`,
|
||||
MaxVirtualCpu: 'Max Virtual CPU',
|
||||
VirtualCpuConcept: `
|
||||
Number of virtual cpus. This value is optional, the default
|
||||
hypervisor behavior is used, usually one virtual CPU`,
|
||||
AllowUsersToModifyCpu:
|
||||
"Allow users to modify this template's default CPU on instantiate",
|
||||
VirtualCpuModification: 'Virtual CPU modification',
|
||||
AllowUsersToModifyVirtualCpu:
|
||||
"Allow users to modify this template's default Virtual CPU on instantiate",
|
||||
EnableHotResize: 'Enable hot resize',
|
||||
/* VM schema - VM Group */
|
||||
AssociateToVMGroup: 'Associate VM to a VM Group',
|
||||
Role: 'Role',
|
||||
/* VM Template schema - vCenter */
|
||||
vCenterTemplateRef: 'vCenter Template reference',
|
||||
vCenterClusterRef: 'vCenter Cluster reference',
|
||||
vCenterInstanceId: 'vCenter instance ID',
|
||||
vCenterVmFolder: 'vCenter VM folder',
|
||||
vCenterVmFolderConcept: `
|
||||
If specified, the the VMs and Template folder path where
|
||||
the VM will be created inside the data center.
|
||||
The path is delimited by slashes (e.g /Management/VMs).
|
||||
If no path is set the VM will be placed in the same folder where
|
||||
the template is located.
|
||||
`,
|
||||
/* VM Template schema - placement */
|
||||
HostReqExpression: 'Host requirements expression',
|
||||
HostReqExpressionConcept: `
|
||||
Boolean expression that rules out provisioning hosts
|
||||
from list of machines suitable to run this VM`,
|
||||
HostPolicyExpression: 'Host policy expression',
|
||||
HostPolicyExpressionConcept: `
|
||||
This field sets which attribute will be used
|
||||
to sort the suitable hosts for this VM`,
|
||||
DatastoreReqExpression: 'Datastore requirements expression',
|
||||
DatastoreReqExpressionConcept: `
|
||||
Boolean expression that rules out entries from
|
||||
the pool of datastores suitable to run this VM`,
|
||||
DatastorePolicyExpression: 'Datastore policy expression',
|
||||
DatastorePolicyExpressionConcept: `
|
||||
This field sets which attribute will be used to
|
||||
sort the suitable datastores for this VM`,
|
||||
/* VM Template schema - OS & CPU */
|
||||
/* VM Template schema - OS & CPU - boot */
|
||||
Boot: 'Boot',
|
||||
OSAndCpu: 'OS & CPU',
|
||||
OSBooting: 'OS Booting',
|
||||
BootOrder: 'Boot order',
|
||||
BootOrderConcept: 'Select the devices to boot from, and their order',
|
||||
CpuArchitecture: 'CPU Architecture',
|
||||
BusForSdDisks: 'Bus for SD disks',
|
||||
MachineType: 'Machine type',
|
||||
RootDevice: 'Root devide',
|
||||
KernelBootParameters: 'Kernel boot parameters',
|
||||
PathBootloader: 'Path to the bootloader executable',
|
||||
UniqueIdOfTheVm: 'Unique ID of the VM',
|
||||
UniqueIdOfTheVmConcept: `
|
||||
It’s referenced as machine ID inside the VM.
|
||||
Could be used to force ID for licensing purposes`,
|
||||
Firmware: 'Firmware',
|
||||
FirmwareConcept:
|
||||
'This attribute allows to define the type of firmware used to boot the VM',
|
||||
FirmwareSecure: 'Firmware secure',
|
||||
CpuModel: 'CPU Model',
|
||||
CustomPath: 'Customize with path',
|
||||
/* VM Template schema - OS & CPU - kernel */
|
||||
Kernel: 'Kernel',
|
||||
KernelExpression: 'Kernel expression',
|
||||
KernelPath: 'Path to the OS kernel to boot the image',
|
||||
/* VM Template schema - OS & CPU - ramdisk */
|
||||
Ramdisk: 'Ramdisk',
|
||||
RamdiskExpression: 'Ramdisk expression',
|
||||
RamdiskPath: 'Path to the initrd image',
|
||||
/* VM Template schema - OS & CPU - features */
|
||||
Features: 'Features',
|
||||
Acpi: 'ACPI',
|
||||
AcpiConcept:
|
||||
'Add support in the VM for Advanced Configuration and Power Interface (ACPI)',
|
||||
Pae: 'PAE',
|
||||
PaeConcept: 'Add support in the VM for Physical Address Extension (PAE)',
|
||||
Apic: 'APIC',
|
||||
ApicConcept: 'Enables the advanced programmable IRQ management',
|
||||
Hyperv: 'Hyper-v',
|
||||
HypervConcept: 'Add support in the VM for hyper-v features (HYPERV)',
|
||||
Localtime: 'Localtime',
|
||||
LocaltimeConcept:
|
||||
'The guest clock will be synchronized to the hosts configured timezone when booted',
|
||||
GuestAgent: 'QEMU Guest Agent',
|
||||
GuestAgentConcept:
|
||||
`Enables the QEMU Guest Agent communication.
|
||||
This does not start the Guest Agent inside the VM`,
|
||||
VirtioQueues: 'Virtio-scsi Queues',
|
||||
VirtioQueuesConcept:
|
||||
`Number of vCPU queues to use in the virtio-scsi controller.
|
||||
Leave blank to use the default value`,
|
||||
IoThreads: 'Iothreads',
|
||||
IoThreadsConcept:
|
||||
`Number of iothreads for virtio disks.
|
||||
By default threads will be assign to disk by round robin algorithm.
|
||||
Disk thread id can be forced by disk IOTHREAD attribute`,
|
||||
/* VM Template schema - context */
|
||||
Context: 'Context',
|
||||
/* VM Template schema - Input/Output */
|
||||
InputOrOutput: 'Input / Output',
|
||||
/* VM Template schema - Input/Output - graphics */
|
||||
Graphics: 'Graphics',
|
||||
VMRC: 'VMRC',
|
||||
VNC: 'VNC',
|
||||
SDL: 'SDL',
|
||||
SPICE: 'SPICE',
|
||||
Type: 'Type',
|
||||
ListenOnIp: 'Listen on IP',
|
||||
ServerPort: 'Server port',
|
||||
ServerPortConcept: 'Port for the VNC/SPICE server',
|
||||
Keymap: 'Keymap',
|
||||
GenerateRandomPassword: 'Generate random password',
|
||||
Command: 'Command',
|
||||
/* VM Template schema - NUMA */
|
||||
PinPolicy: 'Pin Policy',
|
||||
PinPolicyConcept: 'Virtual CPU pinning preference: %s',
|
||||
NumaSocketsConcept: 'Number of sockets or NUMA nodes',
|
||||
NumaCoresConcept: 'Number of cores per node',
|
||||
Threads: 'Threads',
|
||||
ThreadsConcept: 'Number of threads per core',
|
||||
HugepagesSize: 'Hugepages size',
|
||||
HugepagesSizeConcept:
|
||||
'Size of hugepages (MB). If not defined no hugepages will be used',
|
||||
MemoryAccess: 'Memory Access',
|
||||
MemoryAccessConcept: 'Control if the memory is to be mapped: %s',
|
||||
VirtualCpuSelected: 'Virtual Cpu selected',
|
||||
VirtualCpuSelectedConcept: `
|
||||
Number of virtual CPUs. This value is optional, the default
|
||||
hypervisor behavior is used, usually one virtual CPU`,
|
||||
|
||||
/* security group schema */
|
||||
TCP: 'TCP',
|
||||
@ -356,7 +503,7 @@ module.exports = {
|
||||
VM_MAD: 'VM MAD',
|
||||
Wilds: 'Wilds',
|
||||
Zombies: 'Zombies',
|
||||
Numa: 'Numa',
|
||||
Numa: 'NUMA',
|
||||
/* Host schema - capacity */
|
||||
AllocatedMemory: 'Allocated Memory',
|
||||
AllocatedCpu: 'Allocated CPU',
|
||||
@ -367,5 +514,58 @@ module.exports = {
|
||||
/* Cluster schema */
|
||||
/* Cluster schema - capacity */
|
||||
ReservedMemory: 'Allocated Memory',
|
||||
ReservedCpu: 'Allocated CPU'
|
||||
ReservedCpu: 'Allocated CPU',
|
||||
|
||||
/* User inputs */
|
||||
Fixed: 'Fixed',
|
||||
Range: 'Range',
|
||||
List: 'List',
|
||||
AnyValue: 'Any value',
|
||||
|
||||
/* Validation */
|
||||
/* Validation - mixed */
|
||||
'validation.mixed.default': 'Is invalid',
|
||||
'validation.mixed.required': 'Is a required field',
|
||||
'validation.mixed.oneOf': 'Must be one of the following values: %s',
|
||||
'validation.mixed.notOneOf': 'Must not be one of the following values: %s',
|
||||
'validation.mixed.notType': 'Invalid type',
|
||||
'validation.mixed.notType.string': 'Must be a string type',
|
||||
'validation.mixed.notType.number': 'Must be a number type',
|
||||
'validation.mixed.notType.date': 'Must be a date type',
|
||||
'validation.mixed.notType.boolean': 'Must be a boolean type',
|
||||
'validation.mixed.notType.object': 'Must be an object type',
|
||||
'validation.mixed.notType.array': 'Must be an array type',
|
||||
'validation.mixed.defined': 'Must be defined',
|
||||
/* Validation - string */
|
||||
'validation.string.length': 'Must be exactly %s characters',
|
||||
'validation.string.min': 'Must be at least %s characters',
|
||||
'validation.string.max': 'Must be at most %s characters',
|
||||
'validation.string.matches': 'Must match the following: "%s"',
|
||||
'validation.string.email': 'Must be a valid email',
|
||||
'validation.string.url': 'Must be a valid URL',
|
||||
'validation.string.uuid': 'Must be a valid UUID',
|
||||
'validation.string.trim': 'Must be a trimmed string',
|
||||
'validation.string.lowercase': 'Must be a lowercase string',
|
||||
'validation.string.uppercase': 'Must be a upper case string',
|
||||
'validation.string.invalidFormat': 'File has invalid format',
|
||||
/* Validation - number */
|
||||
'validation.number.min': 'Must be greater than or equal to %s',
|
||||
'validation.number.max': 'Must be less than or equal to %s',
|
||||
'validation.number.lessThan': 'Must be less than %s',
|
||||
'validation.number.moreThan': 'Must be greater than %s',
|
||||
'validation.number.positive': 'Must be a positive number',
|
||||
'validation.number.negative': 'Must be a negative number',
|
||||
'validation.number.integer': 'Must be an integer',
|
||||
'validation.number.isDivisible': 'Should be divisible by %s',
|
||||
/* Validation - date */
|
||||
'validation.date.min': 'Must be later than %s',
|
||||
'validation.date.max': 'Must be at earlier than %s',
|
||||
/* Validation - boolean */
|
||||
'validation.boolean.isValue': 'Must be %s',
|
||||
/* Validation - object */
|
||||
'validation.object.noUnknown': 'Has unspecified keys: %s',
|
||||
/* Validation - array */
|
||||
'validation.array.min': 'Must have at least %s items',
|
||||
'validation.array.max': 'Must have less than or equal to %s items',
|
||||
'validation.array.length': 'Must have %s items'
|
||||
}
|
||||
|
@ -32,3 +32,22 @@ export const VM_TEMPLATE_ACTIONS = {
|
||||
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
|
||||
CHANGE_GROUP: ACTIONS.CHANGE_GROUP
|
||||
}
|
||||
|
||||
export const NUMA_PIN_POLICIES = ['NONE', 'THREAD', 'SHARED', 'CORE']
|
||||
|
||||
export const NUMA_MEMORY_ACCESS = ['shared', 'private']
|
||||
|
||||
export const CPU_ARCHITECTURES = ['i686', 'x86_64']
|
||||
|
||||
export const DEFAULT_CPU_MODELS = ['host-passthrough']
|
||||
|
||||
export const SD_DISK_BUSES = ['scsi', 'sata']
|
||||
|
||||
export const FIRMWARE_TYPES = ['BIOS']
|
||||
|
||||
export const KVM_FIRMWARE_TYPES = FIRMWARE_TYPES.concat([
|
||||
'/usr/share/OVMF/OVMF_CODE.fd',
|
||||
'/usr/share/OVMF/OVMF_CODE.secboot.fd'
|
||||
])
|
||||
|
||||
export const VCENTER_FIRMWARE_TYPES = FIRMWARE_TYPES.concat(['uefi'])
|
||||
|
@ -69,7 +69,7 @@ const Policies = () => ({
|
||||
const [tabSelected, setTab] = useState(TABS.elasticity.name)
|
||||
|
||||
const theme = useTheme()
|
||||
const { watch, errors } = useFormContext()
|
||||
const { watch, formState: { errors } } = useFormContext()
|
||||
const { handleSetList } = useListForm({
|
||||
key: STEP_ID,
|
||||
setList: setFormData
|
||||
|
@ -69,7 +69,7 @@ function ApplicationsTemplatesCreateForm () {
|
||||
useEffect(() => {
|
||||
const formData = data ? parseApplicationToForm(data) : {}
|
||||
|
||||
methods.reset(resolvers().cast(formData), { errors: false })
|
||||
methods.reset(resolvers().cast(formData), { keepErrors: false })
|
||||
}, [data])
|
||||
|
||||
if (error) {
|
||||
|
@ -43,7 +43,7 @@ const Tiers = ({ tiers, vmTemplates }) => {
|
||||
const [tabSelected, setTab] = useState(tiers?.[0]?.id)
|
||||
|
||||
const theme = useTheme()
|
||||
const { errors } = useFormContext()
|
||||
const { formState: { errors } } = useFormContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -17,7 +17,7 @@
|
||||
import * as yup from 'yup'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { getValidationFromFields } from 'client/utils'
|
||||
import { getValidationFromFields, arrayToOptions } from 'client/utils'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T, INPUT_TYPES, FILTER_POOL } from 'client/constants'
|
||||
|
||||
@ -29,7 +29,7 @@ export const USERNAME = {
|
||||
.string()
|
||||
.trim()
|
||||
.required('Username is a required field')
|
||||
.default(null),
|
||||
.default(() => ''),
|
||||
grid: { md: 12 },
|
||||
fieldProps: {
|
||||
autoFocus: true,
|
||||
@ -47,7 +47,7 @@ export const PASSWORD = {
|
||||
.string()
|
||||
.trim()
|
||||
.required('Password is a required field')
|
||||
.default(null),
|
||||
.default(() => ''),
|
||||
grid: { md: 12 },
|
||||
fieldProps: {
|
||||
required: true,
|
||||
@ -62,7 +62,7 @@ export const REMEMBER = {
|
||||
type: INPUT_TYPES.CHECKBOX,
|
||||
validation: yup
|
||||
.boolean()
|
||||
.default(false),
|
||||
.default(() => false),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
@ -74,7 +74,7 @@ export const TOKEN = {
|
||||
.string()
|
||||
.trim()
|
||||
.required('Authenticator is a required field')
|
||||
.default(null),
|
||||
.default(() => ''),
|
||||
grid: { md: 12 },
|
||||
fieldProps: {
|
||||
autoFocus: true,
|
||||
@ -92,13 +92,13 @@ export const GROUP = {
|
||||
|
||||
const sortedGroupsById = groups?.sort((a, b) => a.ID - b.ID)
|
||||
|
||||
const formatGroups = sortedGroupsById.map(({ ID, NAME }) => {
|
||||
const isPrimary = user?.GID === ID ? `(${Tr(T.Primary)})` : ''
|
||||
|
||||
return {
|
||||
text: `${ID} - ${NAME} ${isPrimary}`,
|
||||
value: String(ID)
|
||||
}
|
||||
const formatGroups = arrayToOptions(sortedGroupsById, {
|
||||
addEmpty: false,
|
||||
getText: ({ ID, NAME }) => {
|
||||
const isPrimary = user?.GID === ID ? `(${Tr(T.Primary)})` : ''
|
||||
return `${ID} - ${NAME} ${isPrimary}`
|
||||
},
|
||||
getValue: ({ ID }) => String(ID)
|
||||
})
|
||||
|
||||
return [{ text: T.ShowAll, value: FILTER_POOL.ALL_RESOURCES }]
|
||||
|
@ -74,7 +74,7 @@ const Settings = () => {
|
||||
useEffect(() => {
|
||||
reset(
|
||||
FORM_SCHEMA.cast(settings),
|
||||
{ isSubmitted: false, error: false }
|
||||
{ keepIsSubmitted: false, keepErrors: false }
|
||||
)
|
||||
}, [settings])
|
||||
|
||||
|
54
src/fireedge/src/client/containers/VmTemplates/Create.js
Normal file
54
src/fireedge/src/client/containers/VmTemplates/Create.js
Normal file
@ -0,0 +1,54 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { /* useHistory, */ useLocation } from 'react-router'
|
||||
import { Container } from '@mui/material'
|
||||
|
||||
// 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 { isDevelopment } from 'client/utils'
|
||||
|
||||
function CreateVmTemplate () {
|
||||
// const history = useHistory()
|
||||
const { state: { ID: templateId } = {} } = useLocation()
|
||||
|
||||
// const { enqueueInfo } = useGeneralApi()
|
||||
// const { instantiate } = useVmTemplateApi()
|
||||
|
||||
const onSubmit = async formData => {
|
||||
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}`) */
|
||||
} catch (err) {
|
||||
isDevelopment() && console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container style={{ display: 'flex', flexFlow: 'column' }} disableGutters>
|
||||
<CreateForm templateId={templateId} onSubmit={onSubmit} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateVmTemplate
|
@ -89,6 +89,10 @@ const { name, actions, reducer } = createSlice({
|
||||
extraReducers: builder => {
|
||||
builder
|
||||
.addMatcher(({ type }) => type === logout.type, () => initial)
|
||||
.addMatcher(
|
||||
({ type }) => type.startsWith(RESOURCES.system) && type.endsWith('/fulfilled'),
|
||||
(state, { payload }) => ({ ...state, ...payload })
|
||||
)
|
||||
.addMatcher(
|
||||
({ type }) =>
|
||||
type === updateResourceFromFetch.type ||
|
||||
|
@ -104,8 +104,12 @@ const useFetch = (request, socket) => {
|
||||
}
|
||||
}, [isFetched])
|
||||
|
||||
useEffect(() => () => {
|
||||
cancelRequest.current = true
|
||||
useEffect(() => {
|
||||
cancelRequest.current = false
|
||||
|
||||
return () => {
|
||||
cancelRequest.current = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const doFetch = useCallback(async (payload, reload = false) => {
|
||||
|
@ -118,7 +118,7 @@ const useListForm = ({
|
||||
|
||||
const getIndexById = useCallback(
|
||||
(listToFind, searchId = -1) =>
|
||||
listToFind.findIndex((item, idx) => getItemId(item, idx) === searchId),
|
||||
listToFind?.findIndex((item, idx) => getItemId(item, idx) === searchId),
|
||||
[]
|
||||
)
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { prettyBytes } from 'client/utils'
|
||||
import { HOST_STATES, StateInfo } from 'client/constants'
|
||||
import { DEFAULT_CPU_MODELS, HOST_STATES, HYPERVISORS, StateInfo } from 'client/constants'
|
||||
|
||||
/**
|
||||
* Returns information about the host state.
|
||||
@ -63,3 +63,43 @@ export const getAllocatedInfo = host => {
|
||||
percentMemLabel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of hugepage sizes from the host numa nodes.
|
||||
*
|
||||
* @param {object} host - Host
|
||||
* @returns {Array} List of hugepages sizes from resource
|
||||
*/
|
||||
export const getHugepageSizes = host => {
|
||||
const numaNodes = [host?.HOST_SHARE?.NUMA_NODES?.NODE ?? []].flat()
|
||||
|
||||
return numaNodes.filter(node => node?.NODE_ID &&
|
||||
[node?.HUGEPAGE?.SIZE ?? []].flat().map(size => +size)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of KVM CPU Models available from the host pool.
|
||||
*
|
||||
* @param {object[]} hosts - Hosts
|
||||
* @returns {Array} List of KVM CPU Models from the pool
|
||||
*/
|
||||
export const getKvmCpuModels = (hosts = []) => hosts
|
||||
.filter(host => host?.TEMPLATE?.HYPERVISOR === HYPERVISORS.kvm)
|
||||
.map(host => host.TEMPLATE?.KVM_CPU_MODELS.split(' '))
|
||||
.flat()
|
||||
|
||||
/**
|
||||
* Returns list of KVM Machines available from the host pool.
|
||||
*
|
||||
* @param {object[]} hosts - Hosts
|
||||
* @returns {Array} List of KVM Machines from the pool
|
||||
*/
|
||||
export const getKvmMachines = (hosts = []) => {
|
||||
const machineTypes = hosts
|
||||
.filter(host => host?.TEMPLATE?.HYPERVISOR === HYPERVISORS.kvm)
|
||||
.map(host => host.TEMPLATE?.KVM_MACHINES.split(' '))
|
||||
.flat()
|
||||
|
||||
return [DEFAULT_CPU_MODELS, ...machineTypes]
|
||||
}
|
||||
|
@ -83,8 +83,8 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
white
|
||||
},
|
||||
background: {
|
||||
paper: isDarkMode ? '#2a2d3d' : white,
|
||||
default: isDarkMode ? '#222431' : '#f2f4f8'
|
||||
paper: isDarkMode ? primary.light : white,
|
||||
default: isDarkMode ? primary.main : '#f2f4f8'
|
||||
},
|
||||
error: {
|
||||
100: '#fdeae7',
|
||||
@ -244,9 +244,20 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
'&:hover': {
|
||||
textDecoration: 'underline'
|
||||
}
|
||||
}
|
||||
},
|
||||
fieldset: { border: 'none' }
|
||||
}
|
||||
},
|
||||
MuiTypography: {
|
||||
variants: [{
|
||||
props: { component: 'legend' },
|
||||
style: {
|
||||
marginBottom: '1em',
|
||||
padding: '0em 1em 0.2em 0.5em',
|
||||
borderBottom: `2px solid ${secondary.main}`
|
||||
}
|
||||
}]
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: { backgroundImage: 'unset' }
|
||||
@ -386,6 +397,31 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiToggleButtonGroup: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: isDarkMode ? primary.main : '#f2f4f8'
|
||||
}
|
||||
},
|
||||
defaultProps: {
|
||||
color: 'secondary'
|
||||
}
|
||||
},
|
||||
MuiToggleButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
fontWeight: 700,
|
||||
color: isDarkMode ? grey[300] : grey[700],
|
||||
borderColor: isDarkMode ? secondary[500] : grey[400],
|
||||
'&.Mui-selected': {
|
||||
borderColor: `${secondary[500]} !important`,
|
||||
color: isDarkMode ? white : secondary[800],
|
||||
backgroundColor: isDarkMode ? alpha(secondary[800], 0.2) : secondary[100]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiList: {
|
||||
defaultProps: {
|
||||
dense: true,
|
||||
|
@ -290,7 +290,25 @@ export const isBase64 = (stringToValidate, options = {}) => {
|
||||
/**
|
||||
* Check if value is divisible by 4.
|
||||
*
|
||||
* @param {number} value - Number to check
|
||||
* @param {string|number} value - Number to check
|
||||
* @returns {boolean} Returns `true` if string is divisible by 4
|
||||
*/
|
||||
export const isDivisibleBy4 = value => /[048]|\d*([02468][048]|[13579][26])/g.test(value)
|
||||
|
||||
/**
|
||||
* Check if value is divisible by another number.
|
||||
*
|
||||
* @param {string|number} number - Value to check
|
||||
* @param {string|number} divisor - Divisor number
|
||||
* @returns {boolean} Returns `true` if value is divisible by another
|
||||
*/
|
||||
export const isDivisibleBy = (number, divisor) => !(number % divisor)
|
||||
|
||||
/**
|
||||
* Returns factors of a number.
|
||||
*
|
||||
* @param {number} value - Number
|
||||
* @returns {number[]} Returns list of numbers
|
||||
*/
|
||||
export const getFactorsOfNumber = value =>
|
||||
[...Array(+value + 1).keys()].filter(idx => value % idx === 0)
|
||||
|
@ -22,3 +22,4 @@ export * from 'client/utils/rest'
|
||||
export * from 'client/utils/schema'
|
||||
export * from 'client/utils/storage'
|
||||
export * from 'client/utils/string'
|
||||
export * from 'client/utils/translation'
|
||||
|
@ -49,10 +49,16 @@ import { INPUT_TYPES } from 'client/constants'
|
||||
* Callback of field parameter when depend of another field.
|
||||
*
|
||||
* @callback DependOfCallback
|
||||
* @param {string|string[]} value - Value
|
||||
* @param {any|any[]} value - Value
|
||||
* @returns {any|any[]}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} SelectOption - Option of select field
|
||||
* @property {string|JSXElementConstructor} text - Text to display on select list
|
||||
* @property {any} value - Value to option
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Field
|
||||
* @property {string|DependOfCallback} name
|
||||
@ -69,13 +75,15 @@ import { INPUT_TYPES } from 'client/constants'
|
||||
* @property {string|DependOfCallback} [tooltip]
|
||||
* - Text description
|
||||
* @property {INPUT_TYPES|DependOfCallback} type
|
||||
* - Field type to draw
|
||||
* - Field type to draw: text, select, autocomplete, etc
|
||||
* @property {string|DependOfCallback} [htmlType]
|
||||
* - Type of the input element. It should be a valid HTML5 input type
|
||||
* @property {{text: string|JSXElementConstructor, value: any}[]|DependOfCallback} [values]
|
||||
* @property {function(any|any[]):any} [watcher]
|
||||
* - Function to watch other values
|
||||
* @property {SelectOption[]|DependOfCallback} [values]
|
||||
* - Type of the input element. It should be a valid HTML5 input type
|
||||
* @property {boolean|DependOfCallback} [multiline]
|
||||
* - If `true`, a textarea element will be rendered instead of an input.
|
||||
* - If `true`, a textarea element will be rendered instead of an input
|
||||
* @property {GridProps|DependOfCallback} [grid]
|
||||
* - Grid properties to override in the wrapper element
|
||||
* - Default: { xs: 12, md: 6 }
|
||||
@ -299,6 +307,36 @@ export const mapUserInputs = (userInputs = {}) =>
|
||||
...res, [key]: parseUserInputValue(value)
|
||||
}), {})
|
||||
|
||||
/**
|
||||
* Converts a list of values to usable options.
|
||||
*
|
||||
* @param {any[]} array - List of option values
|
||||
* @param {object} [options] - Options to conversion
|
||||
* @param {boolean} [options.addEmpty] - If `true`, add an empty option
|
||||
* @param {function(any):any} [options.getText] - Function to get the text option
|
||||
* @param {function(any):any} [options.getValue] - Function to get the value option
|
||||
* @returns {SelectOption} Options
|
||||
*/
|
||||
export const arrayToOptions = (array = [], options = {}) => {
|
||||
const { addEmpty = true, getText = o => o, getValue = o => o } = options
|
||||
|
||||
const values = array.map(item => ({ text: getText(item), value: getValue(item) }))
|
||||
|
||||
addEmpty && values.unshift({ text: '-', value: '' })
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the names from object.
|
||||
*
|
||||
* @param {string|string[]} names - List of names
|
||||
* @returns {string|string[]} Sanitized names
|
||||
* @example 'TEST.NAME' => 'NAME'
|
||||
*/
|
||||
export const clearNames = (names = '') =>
|
||||
Array.isArray(names) ? names.map(clearNames) : names.split(/[,[\].]+?/).at(-1)
|
||||
|
||||
/**
|
||||
* Returns parameters needed to create stepper form.
|
||||
*
|
||||
|
96
src/fireedge/src/client/utils/translation.js
Normal file
96
src/fireedge/src/client/utils/translation.js
Normal file
@ -0,0 +1,96 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 react/display-name */
|
||||
/* eslint-disable react/prop-types */
|
||||
import { setLocale, addMethod, number, string } from 'yup'
|
||||
|
||||
import { T } from 'client/constants'
|
||||
import { isDivisibleBy, isBase64 } from 'client/utils/helpers'
|
||||
|
||||
const buildMethods = () => {
|
||||
addMethod(number, 'isDivisibleBy', function (divisor) {
|
||||
return this.test(
|
||||
'is-divisible',
|
||||
[T['validation.number.isDivisible'], divisor],
|
||||
value => isDivisibleBy(value, divisor)
|
||||
)
|
||||
})
|
||||
addMethod(string, 'isBase64', function () {
|
||||
return this.test(
|
||||
'is-base64',
|
||||
T['validation.string.invalidFormat'],
|
||||
value => isBase64(value)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that runs the yup.setLocale().
|
||||
*/
|
||||
const buildTranslationLocale = () => {
|
||||
buildMethods()
|
||||
|
||||
setLocale({
|
||||
mixed: {
|
||||
default: () => T['validation.mixed.default'],
|
||||
required: () => T['validation.mixed.required'],
|
||||
defined: () => T['validation.mixed.defined'],
|
||||
oneOf: ({ values }) => [T['validation.mixed.oneOf'], values],
|
||||
notOneOf: ({ values }) => [T['validation.mixed.notOneOf'], values],
|
||||
notType: ({ type }) =>
|
||||
T[`validation.mixed.notType.${type}`] ?? T['validation.mixed.notType']
|
||||
},
|
||||
string: {
|
||||
length: ({ length }) => [T['validation.string.length'], length],
|
||||
min: ({ min }) => [T['validation.string.min'], min],
|
||||
max: ({ max }) => [T['validation.string.max'], max],
|
||||
matches: ({ matches }) => [T['validation.string.matches'], matches],
|
||||
email: () => T['validation.string.email'],
|
||||
url: () => T['validation.string.url'],
|
||||
uuid: () => T['validation.string.uuid'],
|
||||
trim: () => T['validation.string.trim'],
|
||||
lowercase: () => T['validation.string.lowercase'],
|
||||
uppercase: () => T['validation.string.uppercase']
|
||||
},
|
||||
number: {
|
||||
min: ({ min }) => [T['validation.number.min'], min],
|
||||
max: ({ max }) => [T['validation.number.max'], max],
|
||||
lessThan: ({ less }) => [T['validation.number.lessThan'], less],
|
||||
moreThan: ({ more }) => [T['validation.number.moreThan'], more],
|
||||
positive: () => T['validation.number.positive'],
|
||||
negative: () => T['validation.number.negative'],
|
||||
integer: () => T['validation.number.integer']
|
||||
},
|
||||
boolean: {
|
||||
isValue: ({ value }) => [T['validation.boolean.isValue'], value]
|
||||
},
|
||||
date: {
|
||||
min: ({ min }) => [T['validation.date.min'], min],
|
||||
max: ({ max }) => [T['validation.date.max'], max]
|
||||
},
|
||||
object: {
|
||||
noUnknown: ({ nounknown }) => [T['validation.object.noUnknown'], nounknown]
|
||||
},
|
||||
array: {
|
||||
min: ({ min }) => [T['validation.array.min'], min],
|
||||
max: ({ max }) => [T['validation.array.max'], max],
|
||||
length: ({ length }) => [T['validation.array.length'], length]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export { buildTranslationLocale }
|
Loading…
x
Reference in New Issue
Block a user