mirror of
https://github.com/OpenNebula/one.git
synced 2025-01-27 14:03:40 +03:00
bring developments set aside due to 6.2.0 release
This commit is contained in:
parent
7e750c72a6
commit
03e93e8ea5
@ -89,24 +89,28 @@ info-tabs:
|
||||
vcenter_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
copy: true
|
||||
add: true
|
||||
edit: true
|
||||
delete: true
|
||||
lxc_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
copy: true
|
||||
add: true
|
||||
edit: true
|
||||
delete: true
|
||||
monitoring_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
copy: true
|
||||
add: false
|
||||
edit: false
|
||||
delete: false
|
||||
attributes_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
copy: true
|
||||
add: true
|
||||
edit: true
|
||||
delete: true
|
||||
@ -147,7 +151,7 @@ info-tabs:
|
||||
snapshot_revert: true
|
||||
snapshot_delete: true
|
||||
|
||||
placement:
|
||||
history:
|
||||
enabled: true
|
||||
|
||||
sched_actions:
|
||||
|
@ -5,7 +5,7 @@ vcenter_prepend_command: ''
|
||||
sunstone_prepend: ''
|
||||
|
||||
# Support
|
||||
support_url: ''
|
||||
support_url: 'https://opennebula.zendesk.com/api/v2'
|
||||
support_token: ''
|
||||
|
||||
# this display button and clock icon in table of vm
|
||||
|
@ -89,24 +89,28 @@ info-tabs:
|
||||
vcenter_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
copy: true
|
||||
add: false
|
||||
edit: false
|
||||
delete: false
|
||||
lxc_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
copy: true
|
||||
add: false
|
||||
edit: false
|
||||
delete: false
|
||||
monitoring_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
copy: true
|
||||
add: false
|
||||
edit: false
|
||||
delete: false
|
||||
attributes_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
copy: true
|
||||
add: true
|
||||
edit: false
|
||||
delete: false
|
||||
@ -146,7 +150,7 @@ info-tabs:
|
||||
snapshot_revert: true
|
||||
snapshot_delete: true
|
||||
|
||||
placement:
|
||||
history:
|
||||
enabled: true
|
||||
|
||||
sched_actions:
|
||||
|
14403
src/fireedge/package-lock.json
generated
14403
src/fireedge/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -107,7 +107,8 @@
|
||||
"luxon": "1.28.0",
|
||||
"marked": "2.0.0",
|
||||
"morgan": "1.10.0",
|
||||
"node-zendesk": "^2.1.0",
|
||||
"multer": "1.4.3",
|
||||
"node-zendesk": "2.1.0",
|
||||
"notistack": "2.0.2",
|
||||
"opennebula-guacamole": "1.0.0",
|
||||
"path": "0.12.7",
|
||||
@ -119,7 +120,7 @@
|
||||
"react-dom": "17.0.2",
|
||||
"react-flatpickr": "3.10.7",
|
||||
"react-flow-renderer": "9.6.0",
|
||||
"react-hook-form": "7.17.4",
|
||||
"react-hook-form": "7.18.1",
|
||||
"react-json-pretty": "2.2.0",
|
||||
"react-minimal-pie-chart": "8.2.0",
|
||||
"react-opennebula-ace": "1.0.1",
|
||||
|
@ -29,6 +29,7 @@ const AutocompleteController = memo(
|
||||
cy = `autocomplete-${generateKey()}`,
|
||||
name = '',
|
||||
label = '',
|
||||
tooltip = '',
|
||||
multiple = false,
|
||||
values = [],
|
||||
fieldProps = {}
|
||||
@ -83,6 +84,10 @@ const AutocompleteController = memo(
|
||||
{...inputParams}
|
||||
/>
|
||||
)}
|
||||
{...(tooltip && {
|
||||
loading: true,
|
||||
loadingText: labelCanBeTranslated(tooltip) ? Tr(tooltip) : tooltip
|
||||
})}
|
||||
{...fieldProps}
|
||||
/>
|
||||
)
|
||||
@ -97,8 +102,9 @@ AutocompleteController.propTypes = {
|
||||
cy: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
multiple: PropTypes.bool,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object),
|
||||
fieldProps: PropTypes.object
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import { memo, useMemo, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { TextField } from '@mui/material'
|
||||
@ -43,27 +43,31 @@ const SelectController = memo(
|
||||
} = useController({ name, control })
|
||||
|
||||
const needShrink = useMemo(
|
||||
() => values.find(v => v.value === optionSelected)?.text !== '',
|
||||
() => multiple || values.find(v => v.value === optionSelected)?.text !== '',
|
||||
[optionSelected]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (multiple && !Array.isArray(optionSelected)) {
|
||||
onChange([optionSelected])
|
||||
}
|
||||
}, [multiple])
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...inputProps}
|
||||
inputRef={ref}
|
||||
value={optionSelected}
|
||||
onChange={
|
||||
multiple
|
||||
? event => {
|
||||
const { options = [] } = event.target
|
||||
const newValue = options
|
||||
.filter(option => option.selected)
|
||||
.map(option => option.value)
|
||||
onChange={!multiple ? onChange : evt => {
|
||||
const { target: { options } } = evt
|
||||
const newValue = []
|
||||
|
||||
for (const option of options) {
|
||||
option.selected && newValue.push(option.value)
|
||||
}
|
||||
|
||||
onChange(newValue)
|
||||
}
|
||||
: onChange
|
||||
}
|
||||
}}
|
||||
select
|
||||
fullWidth
|
||||
SelectProps={{ native: true, multiple }}
|
||||
@ -88,11 +92,12 @@ const SelectController = memo(
|
||||
</TextField>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.error === nextProps.error &&
|
||||
prevProps.values.length === nextProps.values.length &&
|
||||
prevProps.label === nextProps.label &&
|
||||
prevProps.tooltip === nextProps.tooltip
|
||||
(prev, next) =>
|
||||
prev.error === next.error &&
|
||||
prev.values.length === next.values.length &&
|
||||
prev.label === next.label &&
|
||||
prev.tooltip === next.tooltip &&
|
||||
prev.multiple === next.multiple
|
||||
)
|
||||
|
||||
SelectController.propTypes = {
|
||||
|
@ -62,6 +62,7 @@ const TextController = memo(
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
multiline={multiline}
|
||||
rows={3}
|
||||
type={type}
|
||||
label={labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
InputProps={{
|
||||
@ -93,7 +94,7 @@ TextController.propTypes = {
|
||||
tooltip: PropTypes.any,
|
||||
watcher: PropTypes.func,
|
||||
dependencies: PropTypes.oneOfType([
|
||||
PropTypes.strin,
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string)
|
||||
]),
|
||||
fieldProps: PropTypes.object,
|
||||
|
@ -57,11 +57,12 @@ const CustomMobileStepper = ({
|
||||
<Box className={classes.root}>
|
||||
<Box minHeight={60}>
|
||||
<Typography className={classes.title}>
|
||||
{labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
{labelCanBeTranslated(label) ? <Translate word={label} /> : label}
|
||||
</Typography>
|
||||
{Boolean(errors[id]) && (
|
||||
<Typography className={classes.error} variant='caption' color='error'>
|
||||
{errors[id]?.message}
|
||||
{labelCanBeTranslated(errors[id]?.message)
|
||||
? Tr(errors[id]?.message) : errors[id]?.message}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
@ -93,7 +93,8 @@ const CustomStepper = ({
|
||||
disabled={activeStep + 1 < stepIdx}
|
||||
optional={errors[id] && (
|
||||
<Typography variant='caption' color='error'>
|
||||
{errors[id]?.message}
|
||||
{labelCanBeTranslated(errors[id]?.message)
|
||||
? Tr(errors[id]?.message) : errors[id]?.message}
|
||||
</Typography>
|
||||
)}
|
||||
>
|
||||
@ -101,7 +102,7 @@ const CustomStepper = ({
|
||||
StepIconComponent={StepIconStyled}
|
||||
error={Boolean(errors[id]?.message)}
|
||||
>
|
||||
{labelCanBeTranslated(label) ? Tr(label) : label}
|
||||
{labelCanBeTranslated(label) ? <Translate word={label} /> : label}
|
||||
</StepLabel>
|
||||
</StepButton>
|
||||
</Step>
|
||||
|
@ -16,6 +16,7 @@
|
||||
import { useState, useMemo, useCallback, useEffect, JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { sprintf } from 'sprintf-js'
|
||||
import { BaseSchema } from 'yup'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
import { DevTool } from '@hookform/devtools'
|
||||
@ -26,6 +27,7 @@ import CustomMobileStepper from 'client/components/FormStepper/MobileStepper'
|
||||
import CustomStepper from 'client/components/FormStepper/Stepper'
|
||||
import SkeletonStepsForm from 'client/components/FormStepper/Skeleton'
|
||||
import { groupBy, Step, isDevelopment } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const FIRST_STEP = 0
|
||||
|
||||
@ -67,20 +69,23 @@ const FormStepper = ({ steps = [], schema, onSubmit }) => {
|
||||
return { id, data: stepData, ...step }
|
||||
}
|
||||
|
||||
const setErrors = ({ inner = [], ...rest }) => {
|
||||
const setErrors = ({ inner = [], ...rest } = {}) => {
|
||||
const errorsByPath = groupBy(inner, 'path') ?? {}
|
||||
const totalErrors = Object.keys(errorsByPath).length
|
||||
|
||||
totalErrors > 0
|
||||
? setError(id, {
|
||||
type: 'manual',
|
||||
message: `${totalErrors} error(s) occurred`
|
||||
})
|
||||
? setError(id, { type: 'manual', message: [T.ErrorsOcurred, totalErrors] })
|
||||
: setError(id, rest)
|
||||
|
||||
inner?.forEach(({ path, type, message }) =>
|
||||
inner?.forEach(({ path, type, errors: message }) => {
|
||||
if (isDevelopment()) {
|
||||
// the package @hookform/devtools requires message as string
|
||||
const [key, ...values] = [message].flat()
|
||||
setError(`${id}.${path}`, { type, message: sprintf(key, ...values) })
|
||||
} else {
|
||||
setError(`${id}.${path}`, { type, message })
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleStep = stepToAdvance => {
|
||||
|
@ -69,7 +69,7 @@ const ButtonToTriggerForm = ({
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
aria-haspopup={isGroupButton ? 'true' : false}
|
||||
disabled={!options.length}
|
||||
disableElevation
|
||||
|
||||
endicon={isGroupButton ? <NavArrowDown /> : undefined}
|
||||
onClick={evt => !isGroupButton
|
||||
? openDialogForm(options[0])
|
||||
|
@ -17,14 +17,20 @@
|
||||
import { createElement, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Grid, Typography } from '@mui/material'
|
||||
import { FormControl, Grid } from '@mui/material'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
import * as FC from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import Legend from 'client/components/Forms/Legend'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
|
||||
const NOT_DEPEND_ATTRIBUTES = ['watcher', 'transform', 'Table', 'renderValue']
|
||||
const NOT_DEPEND_ATTRIBUTES = [
|
||||
'watcher',
|
||||
'transform',
|
||||
'Table',
|
||||
'getRowId',
|
||||
'renderValue'
|
||||
]
|
||||
|
||||
const INPUT_CONTROLLER = {
|
||||
[INPUT_TYPES.TEXT]: FC.TextController,
|
||||
@ -40,10 +46,12 @@ const INPUT_CONTROLLER = {
|
||||
[INPUT_TYPES.TOGGLE]: FC.ToggleController
|
||||
}
|
||||
|
||||
const FormWithSchema = ({ id, cy, fields, className, legend }) => {
|
||||
const FormWithSchema = ({ id, cy, fields, rootProps, className, legend, legendTooltip }) => {
|
||||
const formContext = useFormContext()
|
||||
const { control, watch } = formContext
|
||||
|
||||
const { sx: sxRoot, restOfRootProps } = rootProps ?? {}
|
||||
|
||||
const getFields = useMemo(() => typeof fields === 'function' ? fields() : fields, [])
|
||||
|
||||
if (getFields.length === 0) return null
|
||||
@ -53,11 +61,14 @@ const FormWithSchema = ({ id, cy, fields, className, legend }) => {
|
||||
: id ? `${id}.${name}` : name // concat form ID if exists
|
||||
|
||||
return (
|
||||
<fieldset className={className}>
|
||||
<FormControl
|
||||
component='fieldset'
|
||||
className={className}
|
||||
sx={{ width: '100%', ...sxRoot }}
|
||||
{...restOfRootProps}
|
||||
>
|
||||
{legend && (
|
||||
<Typography variant='subtitle1' component='legend'>
|
||||
{Tr(legend)}
|
||||
</Typography>
|
||||
<Legend title={legend} tooltip={legendTooltip} />
|
||||
)}
|
||||
<Grid container spacing={1} alignContent='flex-start'>
|
||||
{getFields?.map?.(
|
||||
@ -111,7 +122,7 @@ const FormWithSchema = ({ id, cy, fields, className, legend }) => {
|
||||
}
|
||||
)}
|
||||
</Grid>
|
||||
</fieldset>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
@ -123,6 +134,8 @@ FormWithSchema.propTypes = {
|
||||
PropTypes.arrayOf(PropTypes.object)
|
||||
]),
|
||||
legend: PropTypes.string,
|
||||
legendTooltip: PropTypes.string,
|
||||
rootProps: PropTypes.object,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
|
54
src/fireedge/src/client/components/Forms/Legend.js
Normal file
54
src/fireedge/src/client/components/Forms/Legend.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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { styled, Typography } from '@mui/material'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import AdornmentWithTooltip from 'client/components/FormControl/Tooltip'
|
||||
|
||||
const StyledLegend = styled(props => (
|
||||
<Typography variant='subtitle1' component='legend' {...props} />
|
||||
))(({ theme, tooltip }) => ({
|
||||
marginBottom: '1em',
|
||||
padding: '0em 1em 0.2em 0.5em',
|
||||
borderBottom: `2px solid ${theme.palette.secondary.main}`,
|
||||
...(!!tooltip && {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center'
|
||||
})
|
||||
}))
|
||||
|
||||
const Legend = memo(({ title, tooltip }) => {
|
||||
return (
|
||||
<StyledLegend tooltip={tooltip}>
|
||||
<Translate word={title} />
|
||||
{!!tooltip && <AdornmentWithTooltip title={tooltip} />}
|
||||
</StyledLegend>
|
||||
)
|
||||
}, (prev, next) =>
|
||||
prev.title === next.title &&
|
||||
prev.tooltip === next.tooltip
|
||||
)
|
||||
|
||||
Legend.propTypes = {
|
||||
title: PropTypes.string,
|
||||
tooltip: PropTypes.string
|
||||
}
|
||||
|
||||
Legend.displayName = 'FieldsetLegend'
|
||||
|
||||
export default Legend
|
@ -37,7 +37,7 @@ export const FORM_FIELDS = inputs =>
|
||||
name,
|
||||
type,
|
||||
options: optionsValue,
|
||||
defaultValue
|
||||
default: defaultValue
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -73,7 +73,7 @@ const PERIODIC_FIELD = {
|
||||
type: INPUT_TYPES.CHECKBOX,
|
||||
validation: yup
|
||||
.boolean()
|
||||
.default(false),
|
||||
.default(() => false),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
@ -106,27 +106,16 @@ const REPEAT_FIELD = {
|
||||
|
||||
const DAYS_FIELD = {
|
||||
name: 'DAYS',
|
||||
dependOf: [PERIODIC_FIELD.name, REPEAT_FIELD.name],
|
||||
multiple: (dependValues = {}) => {
|
||||
const { [REPEAT_FIELD.name]: repeat } = dependValues
|
||||
|
||||
return REPEAT_VALUES.WEEKLY === repeat
|
||||
},
|
||||
type: (dependValues = {}) => {
|
||||
const { [REPEAT_FIELD.name]: repeat } = dependValues
|
||||
|
||||
return REPEAT_VALUES.WEEKLY === repeat ? INPUT_TYPES.SELECT : INPUT_TYPES.TEXT
|
||||
},
|
||||
label: (dependValues = {}) => {
|
||||
const { [REPEAT_FIELD.name]: repeat } = dependValues
|
||||
|
||||
return {
|
||||
dependOf: [REPEAT_FIELD.name, PERIODIC_FIELD.name],
|
||||
multiple: ([repeat] = []) => REPEAT_VALUES.WEEKLY === repeat,
|
||||
type: ([repeat] = []) =>
|
||||
REPEAT_VALUES.WEEKLY === repeat ? INPUT_TYPES.SELECT : INPUT_TYPES.TEXT,
|
||||
label: ([repeat] = []) => ({
|
||||
[REPEAT_VALUES.WEEKLY]: 'Days of week',
|
||||
[REPEAT_VALUES.MONTHLY]: 'Days of month',
|
||||
[REPEAT_VALUES.YEARLY]: 'Days of year',
|
||||
[REPEAT_VALUES.HOURLY]: "Each 'x' hours"
|
||||
}[repeat]
|
||||
},
|
||||
}[repeat]),
|
||||
values: [
|
||||
{ text: T.Sunday, value: '0' },
|
||||
{ text: T.Monday, value: '1' },
|
||||
@ -136,12 +125,9 @@ const DAYS_FIELD = {
|
||||
{ text: T.Friday, value: '5' },
|
||||
{ text: T.Saturday, value: '6' }
|
||||
],
|
||||
htmlType: (dependValues = {}) => {
|
||||
const { [PERIODIC_FIELD.name]: periodic, [REPEAT_FIELD.name]: repeat } = dependValues
|
||||
|
||||
if (!periodic) return INPUT_TYPES.HIDDEN
|
||||
|
||||
return REPEAT_VALUES.HOURLY === repeat ? 'number' : undefined
|
||||
htmlType: ([repeat, isPeriodic] = []) => {
|
||||
if (!isPeriodic) return INPUT_TYPES.HIDDEN
|
||||
if (repeat === REPEAT_VALUES.HOURLY) return 'number'
|
||||
},
|
||||
validation: yup
|
||||
.string()
|
||||
|
@ -49,7 +49,7 @@ const Content = ({ data, setFormData }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const NetworkStep = () => ({
|
||||
const HostsTableStep = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.SelectHost,
|
||||
resolver: SCHEMA,
|
||||
@ -61,4 +61,4 @@ Content.propTypes = {
|
||||
setFormData: PropTypes.func
|
||||
}
|
||||
|
||||
export default NetworkStep
|
||||
export default HostsTableStep
|
||||
|
@ -13,10 +13,9 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { SetStateAction } from 'react'
|
||||
import { useMemo, useState, JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
import { NetworkAlt as NetworkIcon, BoxIso as ImageIcon } from 'iconoir-react'
|
||||
import { Stack, Checkbox, styled } from '@mui/material'
|
||||
@ -24,31 +23,30 @@ import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautif
|
||||
|
||||
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 OS_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 }) => ({
|
||||
/** @returns {string} Boot order path in form */
|
||||
export const BOOT_ORDER_NAME = () => `${EXTRA_ID}.${OS_ID}.${BOOT_ORDER_ID}`
|
||||
|
||||
const BootItemDraggable = styled('div')(({ theme, disabled }) => ({
|
||||
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 }) => ({
|
||||
backgroundColor: theme.palette.background.default,
|
||||
'&:before': {
|
||||
content: "''",
|
||||
display: 'block',
|
||||
width: 16,
|
||||
height: 10,
|
||||
backgroundImage: `linear-gradient(
|
||||
background: !disabled && `linear-gradient(
|
||||
to bottom,
|
||||
${theme.palette.action.active} 4px,
|
||||
transparent 4px,
|
||||
@ -61,20 +59,20 @@ const BootItemDraggable = styled(BootItem)(({ theme }) => ({
|
||||
/**
|
||||
* @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
|
||||
* @param {object} currentBootOrder - Current boot order
|
||||
* @returns {string} Updated boot order after remove
|
||||
*/
|
||||
export const reorderBootAfterRemove = (id, list, formData, setFormData) => {
|
||||
export const reorderBootAfterRemove = (id, list, currentBootOrder) => {
|
||||
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)
|
||||
const otherIds = list
|
||||
.filter(resource => resource.NAME !== String(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))
|
||||
const newBootOrder = [...currentBootOrder?.split(',').filter(Boolean)]
|
||||
.filter(bootId => !bootId.startsWith(type) || otherIds.includes(bootId))
|
||||
.map(bootId => {
|
||||
if (!bootId.startsWith(type)) return bootId
|
||||
|
||||
@ -85,31 +83,22 @@ export const reorderBootAfterRemove = (id, list, formData, setFormData) => {
|
||||
: `${type}${resourceId - 1}`
|
||||
})
|
||||
|
||||
reorder(newBootOrder, setFormData)
|
||||
return newBootOrder.join(',')
|
||||
}
|
||||
|
||||
/**
|
||||
* @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(',')
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
/** @returns {JSXElementConstructor} Boot order component */
|
||||
const BootOrder = () => {
|
||||
const { setValue, getValues } = useFormContext()
|
||||
const [bootOrder, setBootOrder] = useState(
|
||||
getValues(BOOT_ORDER_NAME())?.split(',')?.filter(Boolean) ?? []
|
||||
)
|
||||
|
||||
const BootOrder = ({ data, setFormData, control }) => {
|
||||
const booting = useWatch({ name: `${EXTRA_ID}.${BOOTING_ID}.${BOOT_ORDER_ID}`, control })
|
||||
const bootOrder = booting?.split(',').filter(Boolean) ?? []
|
||||
const updateValues = updatedBootOrder => {
|
||||
setValue(BOOT_ORDER_NAME(), updatedBootOrder.join(','))
|
||||
setBootOrder(updatedBootOrder)
|
||||
}
|
||||
|
||||
const disks = data?.[STORAGE_ID]
|
||||
const disks = useMemo(() => getValues(`${EXTRA_ID}.${STORAGE_ID}`)
|
||||
?.map((disk, idx) => {
|
||||
const isVolatile = !disk?.IMAGE && !disk?.IMAGE_ID
|
||||
|
||||
@ -124,9 +113,9 @@ const BootOrder = ({ data, setFormData, control }) => {
|
||||
</>
|
||||
)
|
||||
}
|
||||
}) ?? []
|
||||
}) ?? [], [])
|
||||
|
||||
const nics = data?.[NIC_ID]
|
||||
const nics = useMemo(() => getValues(`${EXTRA_ID}.${NIC_ID}`)
|
||||
?.map((nic, idx) => ({
|
||||
ID: `nic${idx}`,
|
||||
NAME: (
|
||||
@ -135,7 +124,7 @@ const BootOrder = ({ data, setFormData, control }) => {
|
||||
{[nic?.NAME, nic.NETWORK].filter(Boolean).join(': ')}
|
||||
</>
|
||||
)
|
||||
})) ?? []
|
||||
})) ?? [], [])
|
||||
|
||||
const enabledItems = [...disks, ...nics]
|
||||
.filter(item => bootOrder.includes(item.ID))
|
||||
@ -145,7 +134,7 @@ const BootOrder = ({ data, setFormData, control }) => {
|
||||
.filter(item => !bootOrder.includes(item.ID))
|
||||
|
||||
/** @param {DropResult} result - Drop result */
|
||||
const onDragEnd = result => {
|
||||
const onDragEnd = async result => {
|
||||
const { destination, source, draggableId } = result
|
||||
const newBootOrder = [...bootOrder]
|
||||
|
||||
@ -154,10 +143,8 @@ const BootOrder = ({ data, setFormData, control }) => {
|
||||
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)
|
||||
newBootOrder.splice(destination.index, 0, newBootOrder.splice(source.index, 1)[0])
|
||||
updateValues(newBootOrder)
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,25 +156,29 @@ const BootOrder = ({ data, setFormData, control }) => {
|
||||
? newBootOrder.splice(itemIndex, 1)
|
||||
: newBootOrder.push(itemId)
|
||||
|
||||
reorder(newBootOrder, setFormData)
|
||||
updateValues(newBootOrder)
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Stack>
|
||||
<Stack gap='1em'>
|
||||
<Droppable droppableId='booting'>
|
||||
{({ droppableProps, innerRef, placeholder }) => (
|
||||
<Stack {...droppableProps} ref={innerRef} m={2}>
|
||||
{enabledItems.map(({ ID, NAME }, idx) => (
|
||||
<Draggable key={ID} draggableId={ID} index={idx}>
|
||||
<Stack {...droppableProps} ref={innerRef} gap={1}>
|
||||
{[...enabledItems, ...restOfItems].map(({ ID, NAME }, idx) => {
|
||||
const disabled = !bootOrder.includes(ID)
|
||||
|
||||
return (
|
||||
<Draggable key={ID} isDragDisabled={disabled} draggableId={ID} index={idx}>
|
||||
{({ draggableProps, dragHandleProps, innerRef }) => (
|
||||
<BootItemDraggable
|
||||
{...draggableProps}
|
||||
{...dragHandleProps}
|
||||
disabled={disabled}
|
||||
ref={innerRef}
|
||||
>
|
||||
<Checkbox
|
||||
checked
|
||||
checked={!disabled}
|
||||
color='secondary'
|
||||
data-cy={ID}
|
||||
onChange={() => handleEnable(ID)}
|
||||
@ -196,21 +187,12 @@ const BootOrder = ({ data, setFormData, control }) => {
|
||||
</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>
|
||||
)
|
||||
|
@ -17,7 +17,7 @@ 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 { Field, arrayToOptions } from 'client/utils'
|
||||
import {
|
||||
T,
|
||||
INPUT_TYPES,
|
||||
@ -196,13 +196,8 @@ export const FIRMWARE_SECURE = {
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of Boot fields
|
||||
*/
|
||||
export const BOOT_FIELDS = hypervisor =>
|
||||
filterFieldsByHypervisor(
|
||||
[
|
||||
/** @type {Field[]} List of Boot fields */
|
||||
export const BOOT_FIELDS = [
|
||||
ARCH,
|
||||
SD_DISK_BUS,
|
||||
MACHINE_TYPES,
|
||||
@ -214,6 +209,4 @@ export const BOOT_FIELDS = hypervisor =>
|
||||
FEATURE_CUSTOM_ENABLED,
|
||||
FIRMWARE,
|
||||
FIRMWARE_SECURE
|
||||
],
|
||||
hypervisor
|
||||
)
|
||||
]
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string, number } from 'yup'
|
||||
|
||||
import { Field, arrayToOptions, filterFieldsByHypervisor } from 'client/utils'
|
||||
import { Field, arrayToOptions } from 'client/utils'
|
||||
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
|
||||
const { vcenter, lxc, firecracker } = HYPERVISORS
|
||||
@ -130,13 +130,8 @@ export const IO_THREADS = {
|
||||
.default(() => undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of Features fields
|
||||
*/
|
||||
export const FEATURES_FIELDS = hypervisor =>
|
||||
filterFieldsByHypervisor(
|
||||
[
|
||||
/** @type {Field[]} List of Features fields */
|
||||
export const FEATURES_FIELDS = [
|
||||
ACPI,
|
||||
PAE,
|
||||
APIC,
|
||||
@ -145,6 +140,4 @@ export const FEATURES_FIELDS = hypervisor =>
|
||||
GUEST_AGENT,
|
||||
VIRTIO_SCSI_QUEUES,
|
||||
IO_THREADS
|
||||
],
|
||||
hypervisor
|
||||
)
|
||||
]
|
||||
|
@ -13,78 +13,51 @@
|
||||
* 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 { Stack, Typography } from '@mui/material'
|
||||
import { Stack, Box } from '@mui/material'
|
||||
import { SystemShut as OsIcon } from 'iconoir-react'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import Legend from 'client/components/Forms/Legend'
|
||||
|
||||
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 { STEP_ID as EXTRA_ID, TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import BootOrder, { BOOT_ORDER_NAME, 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 { SECTIONS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting/schema'
|
||||
|
||||
import { T } from 'client/constants'
|
||||
import AdornmentWithTooltip from 'client/components/FormControl/Tooltip'
|
||||
|
||||
export const TAB_ID = 'OS'
|
||||
|
||||
const Booting = props => {
|
||||
const { hypervisor } = props
|
||||
const Booting = ({ hypervisor, ...props }) => {
|
||||
const sections = useMemo(() => SECTIONS(hypervisor), [hypervisor])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
display='grid'
|
||||
gap='1em'
|
||||
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
|
||||
>
|
||||
{(
|
||||
!!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>
|
||||
<Box component='fieldset' gridColumn='1/-1'>
|
||||
<Legend title={T.BootOrder} tooltip={T.BootOrderConcept} />
|
||||
<BootOrder {...props} />
|
||||
</fieldset>
|
||||
</Box>
|
||||
)}
|
||||
<Stack
|
||||
display='grid'
|
||||
gap='2em'
|
||||
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
|
||||
>
|
||||
{sections.map(({ id, ...section }) => (
|
||||
<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}
|
||||
key={id}
|
||||
id={EXTRA_ID}
|
||||
cy={`create-vm-template-${EXTRA_ID}.${id}`}
|
||||
{...section}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -95,8 +68,15 @@ Booting.propTypes = {
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
Booting.displayName = 'Booting'
|
||||
/** @type {TabType} */
|
||||
const TAB = {
|
||||
id: 'booting',
|
||||
name: T.OSAndCpu,
|
||||
icon: OsIcon,
|
||||
Content: Booting,
|
||||
getError: error => !!error?.[TAB_ID]
|
||||
}
|
||||
|
||||
export default Booting
|
||||
export default TAB
|
||||
|
||||
export { reorderBootAfterRemove }
|
||||
export { reorderBootAfterRemove, BOOT_ORDER_NAME }
|
||||
|
@ -17,7 +17,7 @@ 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 { Field, clearNames } from 'client/utils'
|
||||
import { T, INPUT_TYPES, HYPERVISORS, IMAGE_TYPES_STR } from 'client/constants'
|
||||
|
||||
const { vcenter, lxc } = HYPERVISORS
|
||||
@ -73,12 +73,5 @@ export const KERNEL = {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of Kernel fields
|
||||
*/
|
||||
export const KERNEL_FIELDS = hypervisor =>
|
||||
filterFieldsByHypervisor(
|
||||
[KERNEL_PATH_ENABLED, KERNEL, KERNEL_DS],
|
||||
hypervisor
|
||||
)
|
||||
/** @type {Field[]} List of Kernel fields */
|
||||
export const KERNEL_FIELDS = [KERNEL_PATH_ENABLED, KERNEL, KERNEL_DS]
|
||||
|
@ -17,7 +17,7 @@ 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 { Field, clearNames } from 'client/utils'
|
||||
import { T, INPUT_TYPES, HYPERVISORS, IMAGE_TYPES_STR } from 'client/constants'
|
||||
|
||||
const { vcenter, lxc, firecracker } = HYPERVISORS
|
||||
@ -73,12 +73,5 @@ export const RAMDISK = {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of Ramdisk fields
|
||||
*/
|
||||
export const RAMDISK_FIELDS = hypervisor =>
|
||||
filterFieldsByHypervisor(
|
||||
[RAMDISK_PATH_ENABLED, RAMDISK, RAMDISK_DS],
|
||||
hypervisor
|
||||
)
|
||||
/** @type {Field[]} List of Ramdisk fields */
|
||||
export const RAMDISK_FIELDS = [RAMDISK_PATH_ENABLED, RAMDISK, RAMDISK_DS]
|
||||
|
@ -0,0 +1,68 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 } from 'client/utils'
|
||||
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
|
||||
const { kvm, lxc, vcenter, firecracker } = HYPERVISORS
|
||||
|
||||
/** @type {Field} Raw type field */
|
||||
const TYPE = {
|
||||
name: 'RAW.TYPE',
|
||||
label: T.Type,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
notOnHypervisors: [lxc, vcenter, firecracker],
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.equals([kvm])
|
||||
.default(() => kvm),
|
||||
fieldProps: { disabled: true },
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} Raw data field */
|
||||
const DATA = {
|
||||
name: 'RAW.DATA',
|
||||
label: T.Data,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
multiline: true,
|
||||
notOnHypervisors: [lxc, vcenter, firecracker],
|
||||
validation: string().trim().notRequired(),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field} Raw validate field */
|
||||
const VALIDATE = {
|
||||
name: 'RAW.VALIDATE',
|
||||
label: T.Validate,
|
||||
tooltip: T.RawValidateConcept,
|
||||
type: INPUT_TYPES.CHECKBOX,
|
||||
notOnHypervisors: [lxc, vcenter, firecracker],
|
||||
validation: boolean()
|
||||
.transform(value => {
|
||||
if (typeof value === 'boolean') return value
|
||||
|
||||
return String(value).toUpperCase() === 'YES'
|
||||
})
|
||||
.default(() => false),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
/** @type {Field[]} List of Boot fields */
|
||||
export const RAW_FIELDS = [TYPE, DATA, VALIDATE]
|
@ -14,23 +14,52 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
import { string } from 'yup'
|
||||
import { BaseSchema, 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'
|
||||
import { RAW_FIELDS } from './rawSchema'
|
||||
|
||||
export {
|
||||
BOOT_FIELDS,
|
||||
KERNEL_FIELDS,
|
||||
RAMDISK_FIELDS,
|
||||
FEATURES_FIELDS
|
||||
}
|
||||
import { Field, Section, getObjectSchemaFromFields, filterFieldsByHypervisor } from 'client/utils'
|
||||
import { T, HYPERVISORS } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
|
||||
* @returns {Section[]} Sections
|
||||
*/
|
||||
const SECTIONS = hypervisor => [
|
||||
{
|
||||
id: 'os-boot',
|
||||
legend: T.Boot,
|
||||
fields: filterFieldsByHypervisor(BOOT_FIELDS, hypervisor)
|
||||
},
|
||||
{
|
||||
id: 'os-features',
|
||||
legend: T.Features,
|
||||
fields: filterFieldsByHypervisor(FEATURES_FIELDS, hypervisor)
|
||||
},
|
||||
{
|
||||
id: 'os-kernel',
|
||||
legend: T.Kernel,
|
||||
fields: filterFieldsByHypervisor(KERNEL_FIELDS, hypervisor)
|
||||
},
|
||||
{
|
||||
id: 'os-ramdisk',
|
||||
legend: T.Ramdisk,
|
||||
fields: filterFieldsByHypervisor(RAMDISK_FIELDS, hypervisor)
|
||||
},
|
||||
{
|
||||
id: 'os-raw',
|
||||
legend: T.RawData,
|
||||
legendTooltip: T.RawDataConcept,
|
||||
fields: filterFieldsByHypervisor(RAW_FIELDS, hypervisor)
|
||||
}
|
||||
]
|
||||
|
||||
/** @type {Field} Boot order field */
|
||||
export const BOOT_ORDER = {
|
||||
const BOOT_ORDER = {
|
||||
name: 'OS.BOOT',
|
||||
validation: string()
|
||||
.trim()
|
||||
@ -42,9 +71,15 @@ export const BOOT_ORDER = {
|
||||
* @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)
|
||||
const FIELDS = hypervisor => [
|
||||
BOOT_ORDER,
|
||||
...SECTIONS(hypervisor).map(({ fields }) => fields).flat()
|
||||
]
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} [hypervisor] - VM hypervisor
|
||||
* @returns {BaseSchema} Step schema
|
||||
*/
|
||||
const SCHEMA = hypervisor => getObjectSchemaFromFields(FIELDS(hypervisor))
|
||||
|
||||
export { SECTIONS, FIELDS, SCHEMA }
|
||||
|
@ -13,28 +13,194 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { forwardRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Folder as ContextIcon,
|
||||
WarningCircledOutline as WarningIcon,
|
||||
DeleteCircledOutline,
|
||||
AddCircledOutline
|
||||
} from 'iconoir-react'
|
||||
import {
|
||||
styled,
|
||||
FormControl,
|
||||
Stack,
|
||||
IconButton,
|
||||
Button,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from '@mui/material'
|
||||
|
||||
// import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd'
|
||||
import { useFieldArray, useForm, FormProvider, useFormContext, get } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
// import { 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'
|
||||
import { Tooltip } from 'client/components/FormControl'
|
||||
import { FormWithSchema, Legend } from 'client/components/Forms'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID as EXTRA_ID, TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { FIELDS, USER_INPUT_SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/schema'
|
||||
import { getUserInputString } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const TAB_ID = 'USER_INPUTS'
|
||||
|
||||
const UserItemDraggable = styled(ListItem)(({ theme }) => ({
|
||||
'&:before': {
|
||||
content: "''",
|
||||
display: 'block',
|
||||
width: 16,
|
||||
height: 10,
|
||||
background: `linear-gradient(
|
||||
to bottom,
|
||||
${theme.palette.action.active} 4px,
|
||||
transparent 4px,
|
||||
transparent 6px,
|
||||
${theme.palette.action.active} 6px
|
||||
)`
|
||||
}
|
||||
}))
|
||||
|
||||
const UserInputItem = forwardRef(({
|
||||
removeAction,
|
||||
error,
|
||||
userInput: { name, ...ui } = {},
|
||||
...props
|
||||
}, ref) => (
|
||||
<UserItemDraggable
|
||||
ref={ref}
|
||||
secondaryAction={
|
||||
<IconButton onClick={removeAction}>
|
||||
<DeleteCircledOutline />
|
||||
</IconButton>
|
||||
}
|
||||
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
|
||||
{...props}
|
||||
>
|
||||
{!!error && (
|
||||
<ListItemIcon sx={{ '& svg': { color: 'error.dark' } }}>
|
||||
<Tooltip title={error?.default.message}>
|
||||
<WarningIcon />
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<ListItemText
|
||||
inset={!error}
|
||||
primary={name}
|
||||
primaryTypographyProps={{ variant: 'body1' }}
|
||||
secondary={getUserInputString(ui)}
|
||||
/>
|
||||
</UserItemDraggable>
|
||||
))
|
||||
|
||||
UserInputItem.propTypes = {
|
||||
removeAction: PropTypes.func,
|
||||
error: PropTypes.object,
|
||||
userInput: PropTypes.object
|
||||
}
|
||||
|
||||
UserInputItem.displayName = 'UserInputItem'
|
||||
|
||||
const Context = () => {
|
||||
const { formState: { errors } } = useFormContext()
|
||||
const { fields: userInputs, append, remove, move } = useFieldArray({
|
||||
name: `${EXTRA_ID}.${TAB_ID}`
|
||||
})
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: USER_INPUT_SCHEMA.default(),
|
||||
resolver: yupResolver(USER_INPUT_SCHEMA)
|
||||
})
|
||||
|
||||
const onSubmit = newInput => {
|
||||
append(newInput)
|
||||
methods.reset()
|
||||
}
|
||||
|
||||
/** @param {DropResult} result - Drop result */
|
||||
const onDragEnd = result => {
|
||||
const { destination, source } = result ?? {}
|
||||
|
||||
if (destination && destination.index !== source.index) {
|
||||
move(source.index, destination.index)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{'Context'}
|
||||
</>
|
||||
<FormControl component='fieldset' sx={{ width: '100%' }}>
|
||||
<Legend title={T.UserInputs} tooltip={T.UserInputsConcept} />
|
||||
<FormProvider {...methods}>
|
||||
<Stack
|
||||
direction='row' alignItems='flex-start' gap='0.5rem'
|
||||
component='form'
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormWithSchema
|
||||
cy={`create-vm-template-${EXTRA_ID}.context-user-input`}
|
||||
fields={FIELDS}
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
/>
|
||||
<Button
|
||||
variant='outlined'
|
||||
type='submit'
|
||||
startIcon={<AddCircledOutline />}
|
||||
sx={{ mt: '1em' }}
|
||||
>
|
||||
<Translate word={T.Add} />
|
||||
</Button>
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
<Divider />
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId='context'>
|
||||
{({ droppableProps, innerRef: outerRef, placeholder }) => (
|
||||
<List ref={outerRef} {...droppableProps}>
|
||||
{userInputs?.map(({ id, ...userInput }, index) => (
|
||||
<Draggable
|
||||
key={`ui[${index}]`}
|
||||
draggableId={`ui-${index}`}
|
||||
index={index}
|
||||
>
|
||||
{({ draggableProps, dragHandleProps, innerRef }) => (
|
||||
<UserInputItem
|
||||
key={id}
|
||||
ref={innerRef}
|
||||
userInput={userInput}
|
||||
error={get(errors, `${EXTRA_ID}.${TAB_ID}.${index}`)}
|
||||
removeAction={() => remove(index)}
|
||||
{...draggableProps}
|
||||
{...dragHandleProps}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{placeholder}
|
||||
</List>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
Context.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
Context.displayName = 'Context'
|
||||
/** @type {TabType} */
|
||||
const TAB = {
|
||||
id: 'context',
|
||||
name: T.Context,
|
||||
icon: ContextIcon,
|
||||
Content: Context,
|
||||
getError: error => !!error?.[TAB_ID]
|
||||
}
|
||||
|
||||
export default Context
|
||||
export default TAB
|
||||
|
@ -13,17 +13,161 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { string } from 'yup'
|
||||
import { array, string, boolean, number, ref } from 'yup'
|
||||
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
import { UserInputType, T, INPUT_TYPES, USER_INPUT_TYPES } from 'client/constants'
|
||||
import { Field, arrayToOptions, sentenceCase, getObjectSchemaFromFields } from 'client/utils'
|
||||
|
||||
const FIELD = {
|
||||
name: '',
|
||||
label: '',
|
||||
tooltip: '',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().notRequired()
|
||||
const {
|
||||
password: uiPassword,
|
||||
list: uiList,
|
||||
listMultiple: uiListMultiple,
|
||||
number: uiNumber,
|
||||
numberFloat: uiNumberFloat,
|
||||
range: uiRange,
|
||||
rangeFloat: uiRangeFloat,
|
||||
boolean: uiBoolean
|
||||
} = USER_INPUT_TYPES
|
||||
|
||||
const { array: _, ...userInputTypes } = USER_INPUT_TYPES
|
||||
|
||||
/** @type {UserInputType[]} User inputs types */
|
||||
const valuesOfUITypes = Object.values(userInputTypes)
|
||||
|
||||
/** @type {Field} Type field */
|
||||
export const TYPE = {
|
||||
name: 'type',
|
||||
label: T.Type,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(valuesOfUITypes, {
|
||||
addEmpty: false,
|
||||
getText: type => sentenceCase(type)
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.oneOf(valuesOfUITypes)
|
||||
.default(() => valuesOfUITypes[0]),
|
||||
grid: { sm: 6, md: 4 }
|
||||
}
|
||||
|
||||
export const CONTEXT_FIELDS = [FIELD]
|
||||
/** @type {Field} Name field */
|
||||
export const NAME = {
|
||||
name: 'name',
|
||||
label: T.Name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 6, md: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Description field */
|
||||
export const DESCRIPTION = {
|
||||
name: 'description',
|
||||
label: T.Description,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 6, md: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Options field */
|
||||
const OPTIONS = {
|
||||
name: 'options',
|
||||
label: T.Options,
|
||||
tooltip: 'Press ENTER key to add a value',
|
||||
dependOf: TYPE.name,
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
multiple: true,
|
||||
htmlType: type =>
|
||||
![uiList, uiListMultiple].includes(type) && INPUT_TYPES.HIDDEN,
|
||||
validation: array(string().trim())
|
||||
.default(() => [])
|
||||
.when(
|
||||
TYPE.name,
|
||||
(type, schema) => [uiList, uiListMultiple].includes(type)
|
||||
? schema.required()
|
||||
: schema.strip().notRequired()
|
||||
),
|
||||
fieldProps: {
|
||||
freeSolo: true,
|
||||
placeholder: 'optA,optB,optC'
|
||||
},
|
||||
grid: { md: 8 }
|
||||
}
|
||||
|
||||
/** @type {{ MIN: Field, MAX: Field }} Range fields */
|
||||
const { MIN, MAX } = (() => {
|
||||
const validation = number()
|
||||
.positive()
|
||||
.default(() => undefined)
|
||||
.when(TYPE.name, (type, schema) => [uiRange, uiRangeFloat].includes(type)
|
||||
? schema.required()
|
||||
: schema.strip().notRequired()
|
||||
)
|
||||
|
||||
const common = {
|
||||
dependOf: TYPE.name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: type =>
|
||||
[uiRange, uiRangeFloat].includes(type) ? 'number' : INPUT_TYPES.HIDDEN,
|
||||
grid: { sm: 6, md: 4 },
|
||||
fieldProps: type => ({ step: type === uiRangeFloat ? 0.01 : 1 })
|
||||
}
|
||||
|
||||
return {
|
||||
MIN: { ...common, name: 'min', label: T.Min, validation: validation.lessThan(ref('max')) },
|
||||
MAX: { ...common, name: 'max', label: T.Max, validation: validation.moreThan(ref('min')) }
|
||||
}
|
||||
})()
|
||||
|
||||
/** @type {Field} Default value field */
|
||||
const DEFAULT_VALUE = {
|
||||
name: 'default',
|
||||
label: T.DefaultValue,
|
||||
dependOf: [TYPE.name, OPTIONS.name],
|
||||
type: ([type] = []) => [uiBoolean, uiList, uiListMultiple].includes(type)
|
||||
? INPUT_TYPES.SELECT
|
||||
: INPUT_TYPES.TEXT,
|
||||
htmlType: ([type] = []) => ({
|
||||
[uiNumber]: 'number',
|
||||
[uiNumberFloat]: 'number',
|
||||
[uiPassword]: INPUT_TYPES.HIDDEN
|
||||
}[type]),
|
||||
multiple: ([type] = []) => type === uiListMultiple,
|
||||
values: ([type, options = []] = []) => type === uiBoolean
|
||||
? arrayToOptions(['NO', 'YES'])
|
||||
: arrayToOptions(options),
|
||||
validation: string()
|
||||
.trim()
|
||||
.default(() => undefined)
|
||||
.when([TYPE.name, OPTIONS.name], (type, options = [], schema) => {
|
||||
return {
|
||||
[uiList]: schema.oneOf(options).notRequired(),
|
||||
[uiListMultiple]: schema.includesInOptions(options),
|
||||
[uiRange]: number().min(ref(MIN.name)).max(ref(MAX.name)).integer(),
|
||||
[uiRangeFloat]: number().min(ref(MIN.name)).max(ref(MAX.name)),
|
||||
[uiPassword]: schema.strip().notRequired()
|
||||
}[type] ?? schema
|
||||
}),
|
||||
grid: { sm: 6, md: 4 }
|
||||
}
|
||||
|
||||
/** @type {Field} Mandatory field */
|
||||
const MANDATORY = {
|
||||
name: 'mandatory',
|
||||
label: T.Mandatory,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().default(() => false),
|
||||
grid: { md: 12 }
|
||||
}
|
||||
|
||||
export const FIELDS = [TYPE, NAME, DESCRIPTION, DEFAULT_VALUE, OPTIONS, MIN, MAX, MANDATORY]
|
||||
|
||||
export const USER_INPUT_SCHEMA = getObjectSchemaFromFields(FIELDS)
|
||||
|
||||
export const SCHEMA = array(USER_INPUT_SCHEMA).ensure()
|
||||
|
@ -14,22 +14,11 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useMemo } from 'react'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { useMemo, JSXElementConstructor } 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'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { useFormContext, FieldErrors } from 'react-hook-form'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
@ -49,41 +38,46 @@ import { SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/Extr
|
||||
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @typedef {object} TabType
|
||||
* @property {string} id - Id will be to use in view yaml to hide/display the tab
|
||||
* @property {string} name - Label of tab
|
||||
* @property {JSXElementConstructor} Content - Content tab
|
||||
* @property {object} [icon] - Icon of tab
|
||||
* @property {function(FieldErrors):boolean} [getError] - Returns `true` if the tab contains an error in form
|
||||
*/
|
||||
|
||||
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 }
|
||||
]
|
||||
/** @type {TabType[]} */
|
||||
export const TABS = [Storage, Networking, Booting, InputOutput, Context, ScheduleAction, Placement, 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)
|
||||
const hypervisor = useMemo(() => watch(`${GENERAL_ID}.HYPERVISOR`), [])
|
||||
|
||||
return STEP_SECTION
|
||||
const sectionsAvailable = useMemo(() => {
|
||||
const dialog = getResourceView('VM-TEMPLATE')?.dialogs?.create_dialog
|
||||
return getSectionsAvailable(dialog, hypervisor)
|
||||
}, [view])
|
||||
|
||||
const totalErrors = Object.keys(errors[STEP_ID] ?? {}).length
|
||||
|
||||
const tabs = useMemo(
|
||||
() => TABS
|
||||
.filter(({ id }) => sectionsAvailable.includes(id))
|
||||
.map(({ Content, name, icon, ...section }, idx) => ({
|
||||
.map(({ Content: TabContent, name, getError, ...section }) => ({
|
||||
...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])
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderContent: () => <TabContent {...{ data, setFormData, hypervisor, control }} />,
|
||||
error: getError?.(errors[STEP_ID])
|
||||
})),
|
||||
[totalErrors, view, control]
|
||||
)
|
||||
|
||||
return tabs.length > 0 ? (
|
||||
<Tabs tabs={tabs} />
|
||||
|
@ -0,0 +1,141 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { Field, arrayToOptions, filterFieldsByHypervisor } from 'client/utils'
|
||||
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
|
||||
const { vcenter, lxc, kvm } = HYPERVISORS
|
||||
|
||||
/** @type {Field} Type field */
|
||||
const TYPE = {
|
||||
name: 'GRAPHICS.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 */
|
||||
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 */
|
||||
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 */
|
||||
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 */
|
||||
const RANDOM_PASSWD = {
|
||||
name: 'GRAPHICS.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 */
|
||||
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 */
|
||||
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 Graphics fields
|
||||
*/
|
||||
export const GRAPHICS_FIELDS = hypervisor =>
|
||||
filterFieldsByHypervisor(
|
||||
[TYPE, LISTEN, PORT, KEYMAP, PASSWD, RANDOM_PASSWD, COMMAND],
|
||||
hypervisor
|
||||
)
|
@ -13,29 +13,38 @@
|
||||
* 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 { Stack } from '@mui/material'
|
||||
import { DataTransferBoth as IOIcon } from 'iconoir-react'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
|
||||
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 { STEP_ID as EXTRA_ID, TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import InputsSection, { SECTION_ID as INPUT_ID } from './inputsSection'
|
||||
import { INPUT_OUTPUT_FIELDS, INPUTS_FIELDS } from './schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const TAB_ID = ['GRAPHICS', INPUT_ID]
|
||||
|
||||
const InputOutput = ({ hypervisor }) => {
|
||||
const inputsFields = useMemo(() => INPUTS_FIELDS(hypervisor), [hypervisor])
|
||||
|
||||
return (
|
||||
<Stack
|
||||
display='grid'
|
||||
gap='2em'
|
||||
gap='1em'
|
||||
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
|
||||
>
|
||||
<FormWithSchema
|
||||
cy='create-vm-template-extra.io-graphics'
|
||||
cy={`create-vm-template-${EXTRA_ID}.io-graphics`}
|
||||
fields={INPUT_OUTPUT_FIELDS(hypervisor)}
|
||||
legend={T.Graphics}
|
||||
id={STEP_ID}
|
||||
id={EXTRA_ID}
|
||||
/>
|
||||
{inputsFields.length > 0 && (
|
||||
<InputsSection fields={inputsFields} />
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@ -49,4 +58,13 @@ InputOutput.propTypes = {
|
||||
|
||||
InputOutput.displayName = 'InputOutput'
|
||||
|
||||
export default InputOutput
|
||||
/** @type {TabType} */
|
||||
const TAB = {
|
||||
id: 'input_output',
|
||||
name: T.InputOrOutput,
|
||||
icon: IOIcon,
|
||||
Content: InputOutput,
|
||||
getError: error => TAB_ID.some(id => error?.[id])
|
||||
}
|
||||
|
||||
export default TAB
|
||||
|
@ -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, array, object, ObjectSchema, ArraySchema } from 'yup'
|
||||
import { PcMouse, PenTablet, Usb, PlugTypeG } from 'iconoir-react'
|
||||
|
||||
import { Field, arrayToOptions, filterFieldsByHypervisor, getValidationFromFields } from 'client/utils'
|
||||
import { T, INPUT_TYPES, DEVICE_TYPES, DEVICE_BUS_TYPES, HYPERVISORS } from 'client/constants'
|
||||
|
||||
const { vcenter, lxc } = HYPERVISORS
|
||||
|
||||
export const deviceTypeIcons = {
|
||||
[DEVICE_TYPES.mouse]: <PcMouse />,
|
||||
[DEVICE_TYPES.tablet]: <PenTablet />
|
||||
}
|
||||
|
||||
export const busTypeIcons = {
|
||||
[DEVICE_BUS_TYPES.usb]: <Usb />,
|
||||
[DEVICE_BUS_TYPES.ps2]: <PlugTypeG />
|
||||
}
|
||||
|
||||
/** @type {Field} Type field */
|
||||
const TYPE = {
|
||||
name: 'TYPE',
|
||||
label: T.Type,
|
||||
notOnHypervisors: [lxc, vcenter],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(Object.values(DEVICE_TYPES)),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 6, md: 6 }
|
||||
}
|
||||
|
||||
/** @type {Field} Bus field */
|
||||
const BUS = {
|
||||
name: 'BUS',
|
||||
label: T.Bus,
|
||||
notOnHypervisors: [lxc, vcenter],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(Object.values(DEVICE_BUS_TYPES)),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 6, md: 6 }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of Graphic inputs fields
|
||||
*/
|
||||
export const INPUTS_FIELDS = (hypervisor) =>
|
||||
filterFieldsByHypervisor([TYPE, BUS], hypervisor)
|
||||
|
||||
/** @type {ObjectSchema} Graphic input object schema */
|
||||
export const INPUT_SCHEMA = object(getValidationFromFields([TYPE, BUS]))
|
||||
|
||||
/** @type {ArraySchema} Graphic inputs schema */
|
||||
export const INPUTS_SCHEMA = array(INPUT_SCHEMA).ensure()
|
@ -0,0 +1,128 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Stack, FormControl, Divider, Button, IconButton } from '@mui/material'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import { DeleteCircledOutline, AddCircledOutline } from 'iconoir-react'
|
||||
import { useFieldArray, useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import { FormWithSchema, Legend } from 'client/components/Forms'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { INPUT_SCHEMA, deviceTypeIcons, busTypeIcons } from './schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const SECTION_ID = 'INPUT'
|
||||
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {Array} props.fields - Fields
|
||||
* @returns {JSXElementConstructor} - Inputs section
|
||||
*/
|
||||
const InputsSection = ({ fields }) => {
|
||||
const { fields: inputs, append, remove } = useFieldArray({
|
||||
name: `${EXTRA_ID}.${SECTION_ID}`
|
||||
})
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: INPUT_SCHEMA.default(),
|
||||
resolver: yupResolver(INPUT_SCHEMA)
|
||||
})
|
||||
|
||||
const onSubmit = newInput => {
|
||||
append(newInput)
|
||||
methods.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl component='fieldset' sx={{ width: '100%' }}>
|
||||
<Legend title={T.Inputs} />
|
||||
<FormProvider {...methods}>
|
||||
<Stack
|
||||
direction='row' alignItems='flex-start' gap='0.5rem'
|
||||
component='form'
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormWithSchema
|
||||
cy={`create-vm-template-${EXTRA_ID}.io-inputs`}
|
||||
fields={fields}
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
/>
|
||||
<Button
|
||||
variant='outlined'
|
||||
type='submit'
|
||||
startIcon={<AddCircledOutline />}
|
||||
sx={{ mt: '1em' }}
|
||||
>
|
||||
<Translate word={T.Add} />
|
||||
</Button>
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
<Divider />
|
||||
<List>
|
||||
{inputs?.map(({ id, TYPE, BUS }, index) => {
|
||||
const deviceIcon = deviceTypeIcons[TYPE]
|
||||
const deviceInfo = `${TYPE}`
|
||||
const busIcon = busTypeIcons[BUS]
|
||||
const busInfo = `${BUS}`
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={id}
|
||||
secondaryAction={
|
||||
<IconButton onClick={() => remove(index)}>
|
||||
<DeleteCircledOutline />
|
||||
</IconButton>
|
||||
}
|
||||
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Stack
|
||||
component='span'
|
||||
direction='row'
|
||||
spacing={2}
|
||||
sx={{ '& > *': { width: 36 } }}
|
||||
>
|
||||
{deviceIcon}
|
||||
<span>{deviceInfo}</span>
|
||||
<Divider orientation='vertical' flexItem />
|
||||
{busIcon}
|
||||
<span>{busInfo}</span>
|
||||
</Stack>
|
||||
}
|
||||
primaryTypographyProps={{ variant: 'body1' }}
|
||||
/>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
InputsSection.propTypes = {
|
||||
fields: PropTypes.array
|
||||
}
|
||||
|
||||
InputsSection.displayName = 'InputsSection'
|
||||
|
||||
export default InputsSection
|
@ -13,131 +13,28 @@
|
||||
* 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 { object, ObjectSchema } 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 }
|
||||
}
|
||||
import { GRAPHICS_FIELDS } from './graphicsSchema'
|
||||
import { INPUTS_SCHEMA } from './inputsSchema'
|
||||
import { Field, getObjectSchemaFromFields } from 'client/utils'
|
||||
|
||||
/**
|
||||
* @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
|
||||
)
|
||||
[...GRAPHICS_FIELDS(hypervisor)]
|
||||
|
||||
/**
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {ObjectSchema} I/O schema
|
||||
*/
|
||||
export const SCHEMA = hypervisor => object({
|
||||
INPUT: INPUTS_SCHEMA
|
||||
}).concat(getObjectSchemaFromFields([
|
||||
...GRAPHICS_FIELDS(hypervisor)
|
||||
]))
|
||||
|
||||
export * from './graphicsSchema'
|
||||
export * from './inputsSchema'
|
||||
|
@ -13,53 +13,39 @@
|
||||
* 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 { Stack } from '@mui/material'
|
||||
import { ServerConnection as NetworkIcon, Edit, Trash } from 'iconoir-react'
|
||||
import { useFormContext, useFieldArray } 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 { STEP_ID as EXTRA_ID, TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { mapNameByIndex } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
|
||||
import { BOOT_ORDER_NAME, 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 mapNameFunction = mapNameByIndex('NIC')
|
||||
|
||||
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 Networking = () => {
|
||||
const { setValue, getValues } = useFormContext()
|
||||
const { fields: nics, replace, update, append } = useFieldArray({
|
||||
name: `${EXTRA_ID}.${TAB_ID}`
|
||||
})
|
||||
|
||||
const reorderNics = () => {
|
||||
const diskSchema = EXTRA_SCHEMA.pick([TAB_ID])
|
||||
const { [TAB_ID]: newList } = diskSchema.cast({ [TAB_ID]: data?.[TAB_ID] })
|
||||
const removeAndReorder = nicName => {
|
||||
const updatedNics = nics.filter(({ NAME }) => NAME !== nicName).map(mapNameFunction)
|
||||
const currentBootOrder = getValues(BOOT_ORDER_NAME())
|
||||
const updatedBootOrder = reorderBootAfterRemove(nicName, nics, currentBootOrder)
|
||||
|
||||
handleSetList(newList)
|
||||
replace(updatedNics)
|
||||
setValue(BOOT_ORDER_NAME(), updatedBootOrder)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -74,17 +60,23 @@ const Networking = ({ data, setFormData, control }) => {
|
||||
options={[{
|
||||
dialogProps: { title: T.AttachNic },
|
||||
form: () => AttachNicForm({ nics }),
|
||||
onSubmit: handleSave
|
||||
onSubmit: nic => append(mapNameFunction(nic, nics.length))
|
||||
}]}
|
||||
/>
|
||||
<div className={classes.root}>
|
||||
{nics?.map(item => {
|
||||
const { NAME, RDP, SSH, NETWORK, PARENT, EXTERNAL } = item
|
||||
<Stack
|
||||
pb='1em'
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(auto-fit, minmax(300px, 0.5fr))'
|
||||
gap='1em'
|
||||
mt='1em'
|
||||
>
|
||||
{nics?.map((item, index) => {
|
||||
const { id, NAME, RDP, SSH, NETWORK, PARENT, EXTERNAL } = item
|
||||
const hasAlias = nics?.some(nic => nic.PARENT === NAME)
|
||||
|
||||
return (
|
||||
<SelectCard
|
||||
key={NAME}
|
||||
key={id ?? NAME}
|
||||
title={[NAME, NETWORK].filter(Boolean).join(' - ')}
|
||||
subheader={<>
|
||||
{Object
|
||||
@ -92,7 +84,7 @@ const Networking = ({ data, setFormData, control }) => {
|
||||
RDP: stringToBoolean(RDP),
|
||||
SSH: stringToBoolean(SSH),
|
||||
EXTERNAL: stringToBoolean(EXTERNAL),
|
||||
ALIAS: PARENT
|
||||
[`PARENT: ${PARENT}`]: PARENT
|
||||
})
|
||||
.map(([k, v]) => v ? `${k}` : '')
|
||||
.filter(Boolean)
|
||||
@ -104,11 +96,7 @@ const Networking = ({ data, setFormData, control }) => {
|
||||
{!hasAlias &&
|
||||
<Action
|
||||
data-cy={`remove-${NAME}`}
|
||||
handleClick={() => {
|
||||
handleRemove(NAME)
|
||||
reorderNics()
|
||||
reorderBootAfterRemove(NAME, nics, data, setFormData)
|
||||
}}
|
||||
handleClick={() => removeAndReorder(NAME)}
|
||||
icon={<Trash />}
|
||||
/>
|
||||
}
|
||||
@ -120,10 +108,16 @@ const Networking = ({ data, setFormData, control }) => {
|
||||
}}
|
||||
options={[{
|
||||
dialogProps: {
|
||||
title: <Translate word={T.EditSomething} values={[`${NAME} - ${NETWORK}`]} />
|
||||
title: (
|
||||
<Translate
|
||||
word={T.EditSomething}
|
||||
values={[`${NAME} - ${NETWORK}`]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
form: () => AttachNicForm({ nics }, item),
|
||||
onSubmit: newValues => handleSave(newValues, NAME)
|
||||
onSubmit: updatedNic =>
|
||||
update(index, mapNameFunction(updatedNic, index))
|
||||
}]}
|
||||
/>
|
||||
</>
|
||||
@ -131,7 +125,7 @@ const Networking = ({ data, setFormData, control }) => {
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -143,6 +137,13 @@ Networking.propTypes = {
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
Networking.displayName = 'Networking'
|
||||
/** @type {TabType} */
|
||||
const TAB = {
|
||||
id: 'network',
|
||||
name: T.Network,
|
||||
icon: NetworkIcon,
|
||||
Content: Networking,
|
||||
getError: error => !!error?.[TAB_ID]
|
||||
}
|
||||
|
||||
export default Networking
|
||||
export default TAB
|
||||
|
@ -13,25 +13,28 @@
|
||||
* 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 { ElectronicsChip as NumaIcon } from 'iconoir-react'
|
||||
|
||||
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'
|
||||
import { STEP_ID as EXTRA_ID, TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { VIRTUAL_CPU as VCPU_FIELD } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/capacitySchema'
|
||||
import { FIELDS as NUMA_FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/numa/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const Placement = ({ hypervisor }) => {
|
||||
export const TAB_ID = 'NUMA'
|
||||
|
||||
const Numa = ({ hypervisor }) => {
|
||||
return (
|
||||
<>
|
||||
<FormWithSchema
|
||||
cy='create-vm-template-extra.vcpu'
|
||||
fields={[VIRTUAL_CPU]}
|
||||
cy={`create-vm-template-${EXTRA_ID}.vcpu`}
|
||||
fields={[VCPU_FIELD]}
|
||||
id={GENERAL_ID}
|
||||
/>
|
||||
<FormWithSchema
|
||||
cy='create-vm-template-extra.numa'
|
||||
cy={`create-vm-template-${EXTRA_ID}.numa`}
|
||||
fields={NUMA_FIELDS(hypervisor)}
|
||||
id={EXTRA_ID}
|
||||
/>
|
||||
@ -39,13 +42,22 @@ const Placement = ({ hypervisor }) => {
|
||||
)
|
||||
}
|
||||
|
||||
Placement.propTypes = {
|
||||
Numa.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
Placement.displayName = 'Placement'
|
||||
/** @type {TabType} */
|
||||
const TAB = {
|
||||
id: 'numa',
|
||||
name: T.Numa,
|
||||
icon: NumaIcon,
|
||||
Content: Numa,
|
||||
getError: error =>
|
||||
!!error?.[TAB_ID] ||
|
||||
!!error?.[VCPU_FIELD.name]
|
||||
}
|
||||
|
||||
export default Placement
|
||||
export default TAB
|
||||
|
@ -142,9 +142,10 @@ const HUGEPAGES = {
|
||||
const hosts = useHost()
|
||||
const sizes = hosts
|
||||
.reduce((res, host) => res.concat(getHugepageSizes(host)), [])
|
||||
.flat()
|
||||
|
||||
return arrayToOptions([...new Set(sizes)], {
|
||||
getText: size => prettyBytes(+size, 'MB')
|
||||
getText: size => prettyBytes(+size)
|
||||
})
|
||||
},
|
||||
validation: string()
|
||||
@ -171,8 +172,10 @@ const MEMORY_ACCESS = {
|
||||
* @param {string} [hypervisor] - VM hypervisor
|
||||
* @returns {Field[]} List of NUMA fields
|
||||
*/
|
||||
export const NUMA_FIELDS = hypervisor =>
|
||||
const FIELDS = hypervisor =>
|
||||
filterFieldsByHypervisor(
|
||||
[PIN_POLICY, CORES, SOCKETS, THREADS, HUGEPAGES, MEMORY_ACCESS],
|
||||
hypervisor
|
||||
)
|
||||
|
||||
export { FIELDS }
|
||||
|
@ -13,16 +13,13 @@
|
||||
* 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 { NetworkAlt as PlacementIcon } from 'iconoir-react'
|
||||
|
||||
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 { STEP_ID as EXTRA_ID, TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { SECTIONS, FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/placement/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const Placement = () => {
|
||||
@ -34,18 +31,14 @@ const Placement = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{SECTIONS.map(({ id, ...section }) => (
|
||||
<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}
|
||||
key={id}
|
||||
id={EXTRA_ID}
|
||||
cy={`create-vm-template-${EXTRA_ID}.${id}`}
|
||||
{...section}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -57,4 +50,13 @@ Placement.propTypes = {
|
||||
|
||||
Placement.displayName = 'Placement'
|
||||
|
||||
export default Placement
|
||||
/** @type {TabType} */
|
||||
const TAB = {
|
||||
id: 'placement',
|
||||
name: T.Placement,
|
||||
icon: PlacementIcon,
|
||||
Content: Placement,
|
||||
getError: error => FIELDS.some(({ name }) => error?.[name])
|
||||
}
|
||||
|
||||
export default TAB
|
||||
|
@ -13,11 +13,12 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { string } from 'yup'
|
||||
|
||||
import { Field, Section } from 'client/utils'
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
|
||||
/** @type {Field} Host requirement field */
|
||||
const HOST_REQ_FIELD = {
|
||||
name: 'SCHED_REQUIREMENTS',
|
||||
label: T.HostReqExpression,
|
||||
@ -26,6 +27,7 @@ const HOST_REQ_FIELD = {
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
/** @type {Field} Host rank requirement field */
|
||||
const HOST_RANK_FIELD = {
|
||||
name: 'SCHED_RANK',
|
||||
label: T.HostPolicyExpression,
|
||||
@ -34,6 +36,7 @@ const HOST_RANK_FIELD = {
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
/** @type {Field} Datastore requirement field */
|
||||
const DS_REQ_FIELD = {
|
||||
name: 'DS_SCHED_REQUIREMENTS',
|
||||
label: T.DatastoreReqExpression,
|
||||
@ -42,6 +45,7 @@ const DS_REQ_FIELD = {
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
/** @type {Field} Datastore rank requirement field */
|
||||
const DS_RANK_FIELD = {
|
||||
name: 'DS_SCHED_RANK',
|
||||
label: T.DatastorePolicyExpression,
|
||||
@ -50,8 +54,26 @@ const DS_RANK_FIELD = {
|
||||
validation: string().trim().notRequired()
|
||||
}
|
||||
|
||||
export const PLACEMENT_HOST_FIELDS = [HOST_REQ_FIELD, HOST_RANK_FIELD]
|
||||
/** @type {Section[]} Sections */
|
||||
const SECTIONS = [
|
||||
{
|
||||
id: 'placement-host',
|
||||
legend: T.Host,
|
||||
fields: [HOST_REQ_FIELD, HOST_RANK_FIELD]
|
||||
},
|
||||
{
|
||||
id: 'placement-ds',
|
||||
legend: T.Datastore,
|
||||
fields: [DS_REQ_FIELD, DS_RANK_FIELD]
|
||||
}
|
||||
]
|
||||
|
||||
export const PLACEMENT_DS_FIELDS = [DS_REQ_FIELD, DS_RANK_FIELD]
|
||||
/** @type {Field[]} List of Placement fields */
|
||||
const FIELDS = [
|
||||
HOST_REQ_FIELD,
|
||||
HOST_RANK_FIELD,
|
||||
DS_REQ_FIELD,
|
||||
DS_RANK_FIELD
|
||||
]
|
||||
|
||||
export const PLACEMENT_FIELDS = [PLACEMENT_HOST_FIELDS, PLACEMENT_DS_FIELDS]
|
||||
export { SECTIONS, FIELDS }
|
||||
|
@ -15,41 +15,26 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* 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 { Stack } from '@mui/material'
|
||||
import { Calendar as ActionIcon, Edit, Trash } from 'iconoir-react'
|
||||
import { useFieldArray } 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 { STEP_ID as EXTRA_ID, TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { mapNameByIndex } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
|
||||
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 mapNameFunction = mapNameByIndex('SCHED_ACTION')
|
||||
|
||||
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}` })
|
||||
const ScheduleAction = () => {
|
||||
const { fields: scheduleActions, remove, update, append } = useFieldArray({
|
||||
name: `${EXTRA_ID}.${TAB_ID}`
|
||||
})
|
||||
|
||||
return (
|
||||
@ -66,30 +51,36 @@ const ScheduleAction = ({ setFormData, control }) => {
|
||||
name: 'Punctual action',
|
||||
dialogProps: { title: T.ScheduledAction },
|
||||
form: () => PunctualForm(),
|
||||
onSubmit: handleSave
|
||||
onSubmit: action => append(mapNameFunction(action, scheduleActions.length))
|
||||
},
|
||||
{
|
||||
cy: 'add-sched-action-relative',
|
||||
name: 'Relative action',
|
||||
dialogProps: { title: T.ScheduledAction },
|
||||
form: () => RelativeForm(),
|
||||
onSubmit: handleSave
|
||||
onSubmit: action => append(mapNameFunction(action, scheduleActions.length))
|
||||
}]}
|
||||
/>
|
||||
<div className={classes.root}>
|
||||
{scheduleActions?.map(item => {
|
||||
const { NAME, ACTION, TIME } = item
|
||||
<Stack
|
||||
pb='1em'
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(auto-fit, minmax(300px, 0.5fr))'
|
||||
gap='1em'
|
||||
mt='1em'
|
||||
>
|
||||
{scheduleActions?.map((item, index) => {
|
||||
const { id, NAME, ACTION, TIME } = item
|
||||
const isRelative = String(TIME).includes('+')
|
||||
|
||||
return (
|
||||
<SelectCard
|
||||
key={NAME}
|
||||
key={id ?? NAME}
|
||||
title={`${NAME} - ${ACTION}`}
|
||||
action={
|
||||
<>
|
||||
<Action
|
||||
data-cy={`remove-${NAME}`}
|
||||
handleClick={() => handleRemove(NAME)}
|
||||
handleClick={() => remove(index)}
|
||||
icon={<Trash />}
|
||||
/>
|
||||
<ButtonToTriggerForm
|
||||
@ -105,7 +96,8 @@ const ScheduleAction = ({ setFormData, control }) => {
|
||||
form: () => isRelative
|
||||
? RelativeForm(undefined, item)
|
||||
: PunctualForm(undefined, item),
|
||||
onSubmit: newValues => handleSave(newValues, NAME)
|
||||
onSubmit: updatedAction =>
|
||||
update(index, mapNameFunction(updatedAction, index))
|
||||
}]}
|
||||
/>
|
||||
</>
|
||||
@ -113,7 +105,7 @@ const ScheduleAction = ({ setFormData, control }) => {
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -125,6 +117,13 @@ ScheduleAction.propTypes = {
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
ScheduleAction.displayName = 'ScheduleAction'
|
||||
/** @type {TabType} */
|
||||
const TAB = {
|
||||
id: 'sched_action',
|
||||
name: T.ScheduledAction,
|
||||
icon: ActionIcon,
|
||||
Content: ScheduleAction,
|
||||
getError: error => !!error?.[TAB_ID]
|
||||
}
|
||||
|
||||
export default ScheduleAction
|
||||
export default TAB
|
||||
|
@ -16,37 +16,39 @@
|
||||
/* 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 { FIELDS as PLACEMENT_FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/placement/schema'
|
||||
import { FIELDS as OS_FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting/schema'
|
||||
import { FIELDS as NUMA_FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/numa/schema'
|
||||
import { SCHEMA as IO_SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput/schema'
|
||||
import { SCHEMA as CONTEXT_SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/context/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
|
||||
})))
|
||||
export const mapNameByIndex = (prefixName) => (resource, idx) => ({
|
||||
...resource,
|
||||
NAME: resource?.NAME?.startsWith(prefixName) || !resource?.NAME
|
||||
? `${prefixName}${idx}`
|
||||
: resource?.NAME
|
||||
})
|
||||
|
||||
export const DISK_SCHEMA = array()
|
||||
.ensure()
|
||||
.transform(disks => disks.map(mapNameByIndex('DISK')))
|
||||
|
||||
export const NIC_SCHEMA = array()
|
||||
.ensure()
|
||||
.transform(nics => nics.map(mapNameByIndex('NIC')))
|
||||
|
||||
export const SCHED_ACTION_SCHEMA = array()
|
||||
.ensure()
|
||||
.transform(actions => actions.map(mapNameByIndex('SCHED_ACTION')))
|
||||
|
||||
export const SCHEMA = hypervisor => object({
|
||||
DISK: DISK_SCHEMA,
|
||||
NIC: NIC_SCHEMA,
|
||||
SCHED_ACTION: SCHED_ACTION_SCHEMA,
|
||||
USER_INPUTS: CONTEXT_SCHEMA
|
||||
})
|
||||
.concat(IO_SCHEMA(hypervisor))
|
||||
.concat(getObjectSchemaFromFields([
|
||||
...PLACEMENT_FIELDS,
|
||||
...OS_FIELDS(hypervisor),
|
||||
|
@ -15,54 +15,41 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* 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 { Stack } from '@mui/material'
|
||||
import { Db as DatastoreIcon, Edit, Trash } from 'iconoir-react'
|
||||
import { useFormContext, useFieldArray } 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 { STEP_ID as EXTRA_ID, TabType } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration'
|
||||
import { mapNameByIndex } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
|
||||
import { BOOT_ORDER_NAME, 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 mapNameFunction = mapNameByIndex('DISK')
|
||||
|
||||
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 Storage = ({ hypervisor }) => {
|
||||
const { getValues, setValue } = useFormContext()
|
||||
const { fields: disks, replace, update, append } = useFieldArray({
|
||||
name: `${EXTRA_ID}.${TAB_ID}`
|
||||
})
|
||||
|
||||
const reorderDisks = () => {
|
||||
const diskSchema = EXTRA_SCHEMA.pick([TAB_ID])
|
||||
const { [TAB_ID]: newList } = diskSchema.cast({ [TAB_ID]: data?.[TAB_ID] })
|
||||
const removeAndReorder = diskName => {
|
||||
const updatedDisks = disks.filter(({ NAME }) => NAME !== diskName).map(mapNameFunction)
|
||||
const currentBootOrder = getValues(BOOT_ORDER_NAME())
|
||||
const updatedBootOrder = reorderBootAfterRemove(diskName, disks, currentBootOrder)
|
||||
|
||||
handleSetList(newList)
|
||||
replace(updatedDisks)
|
||||
setValue(BOOT_ORDER_NAME(), updatedBootOrder)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -80,20 +67,27 @@ const Storage = ({ data, setFormData, hypervisor, control }) => {
|
||||
name: T.Image,
|
||||
dialogProps: { title: T.AttachImage },
|
||||
form: () => ImageSteps({ hypervisor }),
|
||||
onSubmit: handleSave
|
||||
onSubmit: image => append(mapNameFunction(image, disks.length))
|
||||
},
|
||||
{
|
||||
cy: 'attach-volatile-disk',
|
||||
name: T.Volatile,
|
||||
dialogProps: { title: T.AttachVolatile },
|
||||
form: () => VolatileSteps({ hypervisor }),
|
||||
onSubmit: handleSave
|
||||
onSubmit: image => append(mapNameFunction(image, disks.length))
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<div className={classes.root}>
|
||||
{disks?.map(item => {
|
||||
<Stack
|
||||
pb='1em'
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(auto-fit, minmax(300px, 0.5fr))'
|
||||
gap='1em'
|
||||
mt='1em'
|
||||
>
|
||||
{disks?.map((item, index) => {
|
||||
const {
|
||||
id,
|
||||
NAME,
|
||||
TYPE,
|
||||
IMAGE,
|
||||
@ -115,18 +109,18 @@ const Storage = ({ data, setFormData, hypervisor, control }) => {
|
||||
|
||||
return (
|
||||
<SelectCard
|
||||
key={NAME}
|
||||
key={id ?? NAME}
|
||||
title={isVolatile ? (
|
||||
<>
|
||||
{`${NAME} - `}
|
||||
<Translate word={T.VolatileDisk} />
|
||||
</>
|
||||
) : (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5em' }}>
|
||||
<Stack component='span' alignItems='center' gap='0.5em'>
|
||||
<StatusCircle color={state?.color} tooltip={state?.name} />
|
||||
{`${NAME}: ${IMAGE}`}
|
||||
{isPersistent && <StatusChip text='PERSISTENT' />}
|
||||
</span>
|
||||
</Stack>
|
||||
)}
|
||||
subheader={<>
|
||||
{Object
|
||||
@ -134,7 +128,8 @@ const Storage = ({ data, setFormData, hypervisor, control }) => {
|
||||
[DATASTORE]: DATASTORE,
|
||||
READONLY: stringToBoolean(READONLY),
|
||||
PERSISTENT: stringToBoolean(PERSISTENT),
|
||||
[isVolatile || ORIGINAL_SIZE === SIZE ? size : `${originalSize}/${size}`]: true,
|
||||
[isVolatile || ORIGINAL_SIZE === SIZE
|
||||
? size : `${originalSize}/${size}`]: true,
|
||||
[type]: type
|
||||
})
|
||||
.map(([k, v]) => v ? `${k}` : '')
|
||||
@ -147,11 +142,7 @@ const Storage = ({ data, setFormData, hypervisor, control }) => {
|
||||
<Action
|
||||
data-cy={`remove-${NAME}`}
|
||||
tooltip={<Translate word={T.Remove} />}
|
||||
handleClick={() => {
|
||||
handleRemove(NAME)
|
||||
reorderDisks()
|
||||
reorderBootAfterRemove(NAME, disks, data, setFormData)
|
||||
}}
|
||||
handleClick={() => removeAndReorder(NAME, index)}
|
||||
icon={<Trash />}
|
||||
/>
|
||||
<ButtonToTriggerForm
|
||||
@ -167,7 +158,8 @@ const Storage = ({ data, setFormData, hypervisor, control }) => {
|
||||
form: () => isVolatile
|
||||
? VolatileSteps({ hypervisor }, item)
|
||||
: ImageSteps({ hypervisor }, item),
|
||||
onSubmit: newValues => handleSave(newValues, NAME)
|
||||
onSubmit: updatedDisk =>
|
||||
update(index, mapNameFunction(updatedDisk, index))
|
||||
}]}
|
||||
/>
|
||||
</>
|
||||
@ -175,7 +167,7 @@ const Storage = ({ data, setFormData, hypervisor, control }) => {
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -187,6 +179,13 @@ Storage.propTypes = {
|
||||
control: PropTypes.object
|
||||
}
|
||||
|
||||
Storage.displayName = 'Storage'
|
||||
/** @type {TabType} */
|
||||
const TAB = {
|
||||
id: 'storage',
|
||||
name: T.Storage,
|
||||
icon: DatastoreIcon,
|
||||
Content: Storage,
|
||||
getError: error => !!error?.[TAB_ID]
|
||||
}
|
||||
|
||||
export default Storage
|
||||
export default TAB
|
||||
|
@ -18,63 +18,25 @@ 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')
|
||||
const commonValidation = number()
|
||||
.positive()
|
||||
.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({
|
||||
export const MEMORY = {
|
||||
name: 'MEMORY',
|
||||
label: T.Memory,
|
||||
tooltip: T.MemoryConcept,
|
||||
divBy4: true,
|
||||
required: true,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: commonValidation
|
||||
.required()
|
||||
.when('HYPERVISOR', (hypervisor, schema) => hypervisor === HYPERVISORS.vcenter
|
||||
? schema.isDivisibleBy(4)
|
||||
: schema
|
||||
),
|
||||
grid: { md: 12 }
|
||||
})
|
||||
}
|
||||
|
||||
/** @type {Field} Hot reloading on memory field */
|
||||
export const ENABLE_HR_MEMORY = {
|
||||
@ -86,29 +48,40 @@ export const ENABLE_HR_MEMORY = {
|
||||
}
|
||||
|
||||
/** @type {Field} Maximum memory field */
|
||||
export const MEMORY_MAX = CAPACITY_FIELD({
|
||||
export const MEMORY_MAX = {
|
||||
name: 'MEMORY_MAX',
|
||||
label: T.MaxMemory,
|
||||
dependOf: ENABLE_HR_MEMORY.name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: enabledHr => enabledHr ? 'number' : INPUT_TYPES.HIDDEN,
|
||||
validation: commonValidation
|
||||
.when(ENABLE_HR_MEMORY.name, (enabledHr, schema) =>
|
||||
enabledHr ? schema.required() : schema.strip().notRequired()
|
||||
),
|
||||
grid: { xs: 8, md: 6 }
|
||||
})
|
||||
}
|
||||
|
||||
/** @type {Field} Physical CPU field */
|
||||
export const PHYSICAL_CPU = CAPACITY_FIELD({
|
||||
export const PHYSICAL_CPU = {
|
||||
name: 'CPU',
|
||||
label: T.PhysicalCpu,
|
||||
tooltip: T.CpuConcept,
|
||||
required: true,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: commonValidation.required(),
|
||||
grid: { md: 12 }
|
||||
})
|
||||
}
|
||||
|
||||
/** @type {Field} Virtual CPU field */
|
||||
export const VIRTUAL_CPU = CAPACITY_FIELD({
|
||||
export const VIRTUAL_CPU = {
|
||||
name: 'VCPU',
|
||||
label: T.VirtualCpu,
|
||||
tooltip: T.VirtualCpuConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: commonValidation,
|
||||
grid: { md: 12 }
|
||||
})
|
||||
}
|
||||
|
||||
/** @type {Field} Hot reloading on virtual CPU field */
|
||||
export const ENABLE_HR_VCPU = {
|
||||
@ -120,12 +93,18 @@ export const ENABLE_HR_VCPU = {
|
||||
}
|
||||
|
||||
/** @type {Field} Maximum virtual CPU field */
|
||||
export const VCPU_MAX = CAPACITY_FIELD({
|
||||
export const VCPU_MAX = {
|
||||
name: 'VCPU_MAX',
|
||||
label: T.MaxVirtualCpu,
|
||||
dependOf: ENABLE_HR_VCPU.name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: enabledHr => enabledHr ? 'number' : INPUT_TYPES.HIDDEN,
|
||||
validation: commonValidation
|
||||
.when(ENABLE_HR_VCPU.name, (enabledHr, schema) =>
|
||||
enabledHr ? schema.required() : schema.strip().notRequired()
|
||||
),
|
||||
grid: { xs: 8, md: 6 }
|
||||
})
|
||||
}
|
||||
|
||||
/** @type {Field[]} List of capacity fields */
|
||||
export const FIELDS = [
|
||||
|
@ -22,7 +22,7 @@ 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 { SCHEMA, SECTIONS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/schema'
|
||||
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
@ -33,11 +33,11 @@ const Content = () => {
|
||||
const { view, getResourceView } = useAuth()
|
||||
const hypervisor = useWatch({ name: `${STEP_ID}.HYPERVISOR` })
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const sections = useMemo(() => {
|
||||
const dialog = getResourceView('VM-TEMPLATE')?.dialogs?.create_dialog
|
||||
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
|
||||
|
||||
return FIELDS(hypervisor)
|
||||
return SECTIONS(hypervisor)
|
||||
.filter(({ id, required }) => required || sectionsAvailable.includes(id))
|
||||
}, [view, hypervisor])
|
||||
|
||||
@ -49,14 +49,13 @@ const Content = () => {
|
||||
legend={T.Hypervisor}
|
||||
id={STEP_ID}
|
||||
/>
|
||||
{groups.map(({ id, legend, fields }) => (
|
||||
{sections.map(({ id, ...section }) => (
|
||||
<FormWithSchema
|
||||
key={id}
|
||||
className={classes[id]}
|
||||
cy={`create-vm-template-general.${id}`}
|
||||
fields={fields}
|
||||
legend={legend}
|
||||
id={STEP_ID}
|
||||
className={classes[id]}
|
||||
cy={`create-vm-template-${STEP_ID}.${id}`}
|
||||
{...section}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -21,14 +21,14 @@ 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 { Section, filterFieldsByHypervisor, getObjectSchemaFromFields } from 'client/utils'
|
||||
import { T, HYPERVISORS } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @param {HYPERVISORS} [hypervisor] - Template hypervisor
|
||||
* @returns {{ id: string, legend: string, fields: Field[] }[]} Fields
|
||||
* @returns {Section[]} Fields
|
||||
*/
|
||||
const FIELDS = hypervisor => [
|
||||
const SECTIONS = hypervisor => [
|
||||
{
|
||||
id: 'information',
|
||||
legend: T.Information,
|
||||
@ -63,7 +63,7 @@ const FIELDS = hypervisor => [
|
||||
*/
|
||||
const SCHEMA = hypervisor => getObjectSchemaFromFields([
|
||||
HYPERVISOR_FIELD,
|
||||
...FIELDS(hypervisor).map(({ fields }) => fields).flat()
|
||||
...SECTIONS(hypervisor).map(({ fields }) => fields).flat()
|
||||
])
|
||||
|
||||
export { FIELDS, SCHEMA }
|
||||
export { SECTIONS, SCHEMA }
|
||||
|
@ -19,7 +19,7 @@ export default makeStyles(theme => ({
|
||||
root: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '2em',
|
||||
gap: theme.spacing(1),
|
||||
[theme.breakpoints.down('lg')]: {
|
||||
gridTemplateColumns: '1fr'
|
||||
}
|
||||
|
@ -15,21 +15,42 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
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 { jsonToXml } from 'client/models/Helper'
|
||||
import { userInputsToArray, userInputsToObject } from 'client/models/Helper'
|
||||
import { createSteps } from 'client/utils'
|
||||
|
||||
const Steps = createSteps(
|
||||
[General, ExtraConfiguration],
|
||||
{
|
||||
// transformInitialValue: (vmTemplate, schema) =>,
|
||||
transformInitialValue: (vmTemplate, schema) => ({
|
||||
...schema.pick([GENERAL_ID]).cast({
|
||||
[GENERAL_ID]: { ...vmTemplate, ...vmTemplate?.TEMPLATE }
|
||||
}, { stripUnknown: true }),
|
||||
...schema.pick([EXTRA_ID]).cast({
|
||||
[EXTRA_ID]: {
|
||||
...vmTemplate?.TEMPLATE,
|
||||
USER_INPUTS: userInputsToArray(vmTemplate?.TEMPLATE?.USER_INPUTS)
|
||||
}
|
||||
}, { context: { [EXTRA_ID]: vmTemplate.TEMPLATE } })
|
||||
}),
|
||||
transformBeforeSubmit: formData => {
|
||||
const {
|
||||
[GENERAL_ID]: general = {},
|
||||
[EXTRA_ID]: extraTemplate = {}
|
||||
[EXTRA_ID]: { USER_INPUTS, ...extraTemplate } = {}
|
||||
} = formData ?? {}
|
||||
|
||||
const templateXML = jsonToXml({ ...general, ...extraTemplate })
|
||||
return { template: templateXML }
|
||||
// const templateXML = jsonToXml({ ...general, ...extraTemplate })
|
||||
// return { template: templateXML }
|
||||
|
||||
const userInputs = userInputsToObject(USER_INPUTS)
|
||||
const inputsOrder = USER_INPUTS.map(({ name }) => name).join(',')
|
||||
|
||||
return {
|
||||
...general,
|
||||
...extraTemplate,
|
||||
USER_INPUTS: userInputs,
|
||||
INPUTS_ORDER: inputsOrder
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -27,7 +27,7 @@ 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 stepsForm = useMemo(() => Steps(template, template), [])
|
||||
const { steps, defaultValues, resolver, transformBeforeSubmit } = stepsForm
|
||||
|
||||
const methods = useForm({
|
||||
|
@ -26,7 +26,7 @@ const NAME = {
|
||||
label: 'VM name',
|
||||
tooltip: `
|
||||
Defaults to 'template name-<vmid>' when empty.
|
||||
When creating several VMs, the wildcard %idx will be
|
||||
When creating several VMs, the wildcard %%idx will be
|
||||
replaced with a number starting from 0`,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().default(() => undefined)
|
||||
|
@ -14,10 +14,12 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import ButtonToTriggerForm, { ButtonToTriggerFormPropTypes } from 'client/components/Forms/ButtonToTriggerForm'
|
||||
import Legend from 'client/components/Forms/Legend'
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
export {
|
||||
ButtonToTriggerForm,
|
||||
ButtonToTriggerFormPropTypes,
|
||||
Legend,
|
||||
FormWithSchema
|
||||
}
|
||||
|
@ -87,7 +87,11 @@ const translateString = (str = '', values) => {
|
||||
}
|
||||
|
||||
if (key && Array.isArray(values)) {
|
||||
try {
|
||||
key = sprintf(key, ...values)
|
||||
} catch (e) {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
return key
|
||||
@ -104,12 +108,14 @@ const Tr = (str = '') => {
|
||||
|
||||
const valuesTr = !Array.isArray(values) ? [values] : values
|
||||
|
||||
return translateString(key, valuesTr)
|
||||
return translateString(key, valuesTr.filter(Boolean))
|
||||
}
|
||||
|
||||
const Translate = ({ word = '', values = [] }) => {
|
||||
const valuesTr = !Array.isArray(values) ? [values] : values
|
||||
return translateString(word, valuesTr)
|
||||
const [w, v = values] = Array.isArray(word) ? word : [word, values]
|
||||
const valuesTr = !Array.isArray(v) ? [v] : v
|
||||
|
||||
return translateString(w, valuesTr)
|
||||
}
|
||||
|
||||
TranslateProvider.propTypes = {
|
||||
|
@ -43,7 +43,7 @@ const User = () => {
|
||||
disablePadding
|
||||
>
|
||||
{() => (
|
||||
<MenuList>
|
||||
<MenuList disablePadding>
|
||||
<MenuItem onClick={logout} data-cy='header-logout-button'>
|
||||
<Translate word={T.SignOut} />
|
||||
</MenuItem>
|
||||
|
@ -20,7 +20,7 @@ import { Tooltip, Typography } from '@mui/material'
|
||||
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
|
||||
const MultipleTags = ({ tags, limitTags = 1 }) => {
|
||||
const MultipleTags = ({ tags, limitTags = 1, clipboard }) => {
|
||||
if (tags?.length === 0) {
|
||||
return null
|
||||
}
|
||||
@ -29,7 +29,9 @@ const MultipleTags = ({ tags, limitTags = 1 }) => {
|
||||
|
||||
const Tags = tags
|
||||
.splice(0, limitTags)
|
||||
.map((tag, idx) => <StatusChip key={`${idx}-${tag}`} text={tag} />)
|
||||
.map((tag, idx) => (
|
||||
<StatusChip key={`${idx}-${tag}`} text={tag} clipboard={clipboard} />
|
||||
))
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -62,6 +64,10 @@ const MultipleTags = ({ tags, limitTags = 1 }) => {
|
||||
|
||||
MultipleTags.propTypes = {
|
||||
tags: PropTypes.array,
|
||||
clipboard: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.string
|
||||
]),
|
||||
limitTags: PropTypes.number
|
||||
}
|
||||
|
||||
|
@ -16,18 +16,21 @@
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Typography, lighten, darken } from '@mui/material'
|
||||
import { Typography, Tooltip, lighten, darken } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Copy as CopyIcon } from 'iconoir-react'
|
||||
|
||||
import { useClipboard } from 'client/hooks'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
import { addOpacityToColor } from 'client/utils'
|
||||
import { SCHEMES } from 'client/constants'
|
||||
import { T, SCHEMES } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles(({ spacing, palette, typography }) => {
|
||||
const getBackgroundColor = palette.mode === SCHEMES.DARK ? lighten : darken
|
||||
const defaultStateColor = palette.grey[palette.mode === SCHEMES.DARK ? 300 : 700]
|
||||
|
||||
return {
|
||||
root: ({ stateColor = defaultStateColor }) => {
|
||||
text: ({ stateColor = defaultStateColor, clipboard }) => {
|
||||
const paletteColor = palette[stateColor]
|
||||
|
||||
const color = paletteColor?.contrastText ?? getBackgroundColor(stateColor, 0.75)
|
||||
@ -36,30 +39,72 @@ const useStyles = makeStyles(({ spacing, palette, typography }) => {
|
||||
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'
|
||||
lineHeight: 'normal',
|
||||
cursor: 'default',
|
||||
...(clipboard && {
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em',
|
||||
'&:hover > .copy-icon': {
|
||||
color: bgColor
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const StatusChip = memo(({ stateColor, text = '', ...props }) => {
|
||||
const classes = useStyles({ stateColor })
|
||||
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn?.(...args))
|
||||
|
||||
const StatusChip = memo(({
|
||||
stateColor,
|
||||
text = '',
|
||||
clipboard = false,
|
||||
onClick,
|
||||
...props
|
||||
}) => {
|
||||
const { copy, isCopied } = useClipboard()
|
||||
const textToCopy = typeof clipboard === 'string' ? clipboard : text
|
||||
const classes = useStyles({ stateColor, clipboard })
|
||||
|
||||
const handleCopy = evt => {
|
||||
!isCopied && copy(textToCopy)
|
||||
evt.stopPropagation()
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography component='span' className={classes.root} {...props}>
|
||||
<Tooltip
|
||||
arrow
|
||||
open={isCopied}
|
||||
title={<>{'✔️'}<Translate word={T.CopiedToClipboard} /></>}
|
||||
>
|
||||
<Typography
|
||||
component='span'
|
||||
className={classes.text}
|
||||
onClick={callAll(onClick, handleCopy)}
|
||||
{...props}
|
||||
>
|
||||
{text}
|
||||
{clipboard && (
|
||||
<CopyIcon
|
||||
className='copy-icon'
|
||||
title={Tr(T.ClickToCopy)}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
(prev, next) =>
|
||||
prev.stateColor === next.stateColor &&
|
||||
prev.text === next.text
|
||||
prev.text === next.text &&
|
||||
prev.clipboard === next.clipboard
|
||||
)
|
||||
|
||||
StatusChip.propTypes = {
|
||||
@ -67,7 +112,12 @@ StatusChip.propTypes = {
|
||||
text: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node
|
||||
])
|
||||
]),
|
||||
clipboard: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.string
|
||||
]),
|
||||
onClick: PropTypes.func
|
||||
}
|
||||
|
||||
StatusChip.displayName = 'StatusChip'
|
||||
|
@ -14,7 +14,8 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { Column } from 'react-table'
|
||||
import { CategoryFilter } from 'client/components/Tables/Enhanced/Utils'
|
||||
import CategoryFilter from 'client/components/Tables/Enhanced/Utils/CategoryFilter'
|
||||
import { GlobalAction } from 'client/components/Tables/Enhanced/Utils/GlobalActions/Action'
|
||||
|
||||
/**
|
||||
* Add filters defined in view yaml to columns.
|
||||
@ -65,7 +66,7 @@ export const createCategoryFilter = title => ({
|
||||
*
|
||||
* @param {object} params - Config parameters
|
||||
* @param {object[]} params.filters - Which buttons are visible to operate over the resources
|
||||
* @param {object[]} params.actions - Actions
|
||||
* @param {GlobalAction[]} params.actions - Actions
|
||||
* @returns {object} Action with filters
|
||||
*/
|
||||
export const createActions = ({ filters = {}, actions = [] }) => {
|
||||
|
@ -114,12 +114,13 @@ const Actions = () => {
|
||||
label: T.Update,
|
||||
tooltip: T.Update,
|
||||
selected: { max: 1 },
|
||||
disabled: true,
|
||||
color: 'secondary',
|
||||
action: rows => {
|
||||
// const { ID } = rows?.[0]?.original ?? {}
|
||||
// const path = generatePath(PATH.TEMPLATE.VMS.CREATE, { id: ID })
|
||||
const vmTemplate = rows?.[0]?.original ?? {}
|
||||
// const path = generatePath(PATH.TEMPLATE.VMS.CREATE, vmTemplate)
|
||||
const path = PATH.TEMPLATE.VMS.CREATE
|
||||
|
||||
// history.push(path)
|
||||
history.push(path, vmTemplate)
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -13,10 +13,11 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useMemo, JSXElementConstructor } from 'react'
|
||||
import { memo, useMemo, JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {
|
||||
Copy as CopyIcon,
|
||||
AddSquare as AddIcon,
|
||||
Edit as EditIcon,
|
||||
Trash as DeleteIcon,
|
||||
@ -24,8 +25,11 @@ import {
|
||||
Cancel as CancelIcon
|
||||
} from 'iconoir-react'
|
||||
|
||||
import { useClipboard } from 'client/hooks'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { Action } from 'client/components/Cards/SelectCard'
|
||||
import { camelCase } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @param {string} action - Action name
|
||||
@ -66,6 +70,25 @@ ActionButton.propTypes = {
|
||||
handleClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ActionButtonProps} props - Action button props
|
||||
* @returns {JSXElementConstructor} Action button with props
|
||||
*/
|
||||
const Copy = memo(({ value, ...props }) => {
|
||||
const { copy, isCopied } = useClipboard(1000)
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
action='copy'
|
||||
tooltip={<>{'✔️'}<Translate word={T.CopiedToClipboard} /></>}
|
||||
tooltipProps={{ open: isCopied }}
|
||||
handleClick={async () => await copy(value)}
|
||||
icon={CopyIcon}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}, (prev, next) => prev.value === next.value)
|
||||
|
||||
/**
|
||||
* @param {ActionButtonProps} props - Action button props
|
||||
* @returns {JSXElementConstructor} Action button with props
|
||||
@ -96,9 +119,17 @@ const Accept = props => <ActionButton action='accept' icon={AcceptIcon} {...prop
|
||||
*/
|
||||
const Cancel = props => <ActionButton action='cancel' icon={CancelIcon} {...props}/>
|
||||
|
||||
Copy.displayName = 'CopyActionButton'
|
||||
|
||||
Copy.propTypes = {
|
||||
...ActionButton,
|
||||
value: PropTypes.string
|
||||
}
|
||||
|
||||
export {
|
||||
getAttributeCy,
|
||||
ActionButton,
|
||||
Copy,
|
||||
Add,
|
||||
Accept,
|
||||
Cancel,
|
||||
|
@ -17,46 +17,41 @@ import { memo, useMemo, useState, createRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Link as RouterLink } from 'react-router-dom'
|
||||
|
||||
import { Typography, Link } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Typography, Link, Stack } from '@mui/material'
|
||||
|
||||
import { useDialog } from 'client/hooks'
|
||||
import { DialogConfirmation } from 'client/components/Dialogs'
|
||||
import { Actions, Inputs } from 'client/components/Tabs/Common/Attribute'
|
||||
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'& > *:first-child': {
|
||||
flexGrow: 1
|
||||
}
|
||||
},
|
||||
select: {
|
||||
textOverflow: 'ellipsis'
|
||||
},
|
||||
nested: ({ numberOfParents }) => numberOfParents > 0 && ({
|
||||
paddingLeft: `${numberOfParents}em`
|
||||
})
|
||||
})
|
||||
const Column = props => (
|
||||
<Stack direction='row' alignItems='center'
|
||||
sx={{ '&:hover > .actions': { display: 'contents' } }}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const ActionWrapper = props => (
|
||||
<Stack direction='row' component='span' className='actions' {...props} />
|
||||
)
|
||||
|
||||
const Attribute = memo(({
|
||||
canCopy = false,
|
||||
canDelete,
|
||||
canEdit,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
handleEdit,
|
||||
handleGetOptionList,
|
||||
link,
|
||||
name,
|
||||
path = name,
|
||||
showActionsOnHover = false,
|
||||
value,
|
||||
valueInOptionList
|
||||
}) => {
|
||||
const numberOfParents = useMemo(() => path.split('.').length - 1, [path])
|
||||
const classes = useStyles({ numberOfParents })
|
||||
|
||||
const [isEditing, setIsEditing] = useState(() => false)
|
||||
const [options, setOptions] = useState(() => [])
|
||||
@ -90,10 +85,22 @@ const Attribute = memo(({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography noWrap variant='body2' title={Tr(name)} className={classes.nested}>
|
||||
{Tr(name)}
|
||||
<Column>
|
||||
<Typography
|
||||
noWrap
|
||||
component='span'
|
||||
variant='body2'
|
||||
title={typeof name === 'string' ? name : undefined}
|
||||
flexGrow={1}
|
||||
sx={numberOfParents > 0 ? { pl: `${numberOfParents}em` } : undefined}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
<div className={classes.wrapper}>
|
||||
<ActionWrapper {...(showActionsOnHover && { display: 'none' })}>
|
||||
{canCopy && <Actions.Copy name={name} value={name} />}
|
||||
</ActionWrapper>
|
||||
</Column>
|
||||
<Column>
|
||||
{isEditing ? (
|
||||
<>
|
||||
{handleGetOptionList ? (
|
||||
@ -117,6 +124,7 @@ const Attribute = memo(({
|
||||
noWrap
|
||||
component='span'
|
||||
variant='body2'
|
||||
flexGrow={1}
|
||||
title={typeof value === 'string' ? value : undefined}
|
||||
>
|
||||
{link
|
||||
@ -128,12 +136,17 @@ const Attribute = memo(({
|
||||
: value
|
||||
}
|
||||
</Typography>
|
||||
<ActionWrapper {...(showActionsOnHover && { display: 'none' })}>
|
||||
{canCopy && (
|
||||
<Actions.Copy name={name} value={value} />
|
||||
)}
|
||||
{canEdit && (
|
||||
<Actions.Edit name={name} handleClick={handleActiveEditForm} />
|
||||
)}
|
||||
{canDelete && (
|
||||
<Actions.Delete name={name} handleClick={show} />
|
||||
)}
|
||||
</ActionWrapper>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -148,24 +161,26 @@ const Attribute = memo(({
|
||||
</p>
|
||||
</DialogConfirmation>
|
||||
)}
|
||||
</div>
|
||||
</Column>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export const AttributePropTypes = {
|
||||
canCopy: PropTypes.bool,
|
||||
canDelete: PropTypes.bool,
|
||||
canEdit: PropTypes.bool,
|
||||
handleEdit: PropTypes.func,
|
||||
handleDelete: PropTypes.func,
|
||||
handleEdit: PropTypes.func,
|
||||
handleGetOptionList: PropTypes.func,
|
||||
link: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string,
|
||||
showActionsOnHover: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.object
|
||||
]),
|
||||
path: PropTypes.string,
|
||||
valueInOptionList: PropTypes.string
|
||||
}
|
||||
|
||||
|
@ -35,14 +35,39 @@ import makeStyles from '@mui/styles/makeStyles'
|
||||
import { List } from 'client/components/Tabs/Common'
|
||||
import { ACTIONS } from 'client/constants'
|
||||
|
||||
const {
|
||||
COPY_ATTRIBUTE: COPY,
|
||||
ADD_ATTRIBUTE: ADD,
|
||||
EDIT_ATTRIBUTE: EDIT,
|
||||
DELETE_ATTRIBUTE: DELETE
|
||||
} = ACTIONS
|
||||
|
||||
// This attributes has special restrictions
|
||||
const SPECIAL_ATTRIBUTES = {
|
||||
VCENTER_CCR_REF: { edit: false, delete: false },
|
||||
VCENTER_HOST: { edit: false, delete: false },
|
||||
VCENTER_INSTANCE_ID: { edit: false, delete: false },
|
||||
VCENTER_PASSWORD: { edit: true, delete: false },
|
||||
VCENTER_USER: { edit: false, delete: false },
|
||||
VCENTER_VERSION: { edit: false, delete: false }
|
||||
VCENTER_CCR_REF: {
|
||||
[EDIT]: false,
|
||||
[DELETE]: false
|
||||
},
|
||||
VCENTER_HOST: {
|
||||
[EDIT]: false,
|
||||
[DELETE]: false
|
||||
},
|
||||
VCENTER_INSTANCE_ID: {
|
||||
[EDIT]: false,
|
||||
[DELETE]: false
|
||||
},
|
||||
VCENTER_PASSWORD: {
|
||||
[EDIT]: true,
|
||||
[DELETE]: false
|
||||
},
|
||||
VCENTER_USER: {
|
||||
[EDIT]: false,
|
||||
[DELETE]: false
|
||||
},
|
||||
VCENTER_VERSION: {
|
||||
[EDIT]: false,
|
||||
[DELETE]: false
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@ -70,12 +95,13 @@ const AttributePanel = memo(({
|
||||
.map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
showActionsOnHover: true,
|
||||
canCopy:
|
||||
actions?.includes?.(COPY) && !SPECIAL_ATTRIBUTES[name]?.[COPY],
|
||||
canEdit:
|
||||
actions?.includes?.(ACTIONS.EDIT_ATTRIBUTE) &&
|
||||
SPECIAL_ATTRIBUTES[name]?.edit !== false,
|
||||
actions?.includes?.(EDIT) && !SPECIAL_ATTRIBUTES[name]?.[EDIT],
|
||||
canDelete:
|
||||
actions?.includes?.(ACTIONS.DELETE_ATTRIBUTE) &&
|
||||
SPECIAL_ATTRIBUTES[name]?.delete !== false,
|
||||
actions?.includes?.(DELETE) && !SPECIAL_ATTRIBUTES[name]?.[DELETE],
|
||||
handleEdit,
|
||||
handleDelete
|
||||
}))
|
||||
@ -87,7 +113,7 @@ const AttributePanel = memo(({
|
||||
subListProps={{ disablePadding: true }}
|
||||
title={title}
|
||||
list={formatAttributes}
|
||||
handleAdd={actions?.includes?.(ACTIONS.ADD_ATTRIBUTE) && handleAdd}
|
||||
handleAdd={actions?.includes?.(ADD) && handleAdd}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
@ -32,6 +32,7 @@ const useStyles = makeStyles(theme => ({
|
||||
borderBottom: `1px solid ${theme.palette.divider}`
|
||||
},
|
||||
item: {
|
||||
height: '2.4em',
|
||||
gap: '1em',
|
||||
'& > *': {
|
||||
flex: '1 1 50%',
|
||||
|
@ -46,6 +46,6 @@ export default makeStyles(theme => ({
|
||||
}
|
||||
},
|
||||
title: {
|
||||
fontWeight: theme.typography.fontWeightBold
|
||||
fontWeight: theme.typography.fontWeightMedium
|
||||
}
|
||||
}))
|
||||
|
@ -16,7 +16,7 @@
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import HistoryItem from 'client/components/Tabs/Vm/Placement/Item'
|
||||
import HistoryItem from 'client/components/Tabs/Vm/History/Item'
|
||||
|
||||
const HistoryList = ({ records, actions }) => (
|
||||
<div style={{ display: 'grid', gap: '1em', paddingBlock: '0.8em' }}>
|
@ -18,12 +18,12 @@ import { useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { TabContext } from 'client/components/Tabs/TabProvider'
|
||||
import HistoryList from 'client/components/Tabs/Vm/Placement/List'
|
||||
import HistoryList from 'client/components/Tabs/Vm/History/List'
|
||||
|
||||
import { getHypervisor, getHistoryRecords, isAvailableAction } from 'client/models/VirtualMachine'
|
||||
import { getActionsAvailable } from 'client/models/Helper'
|
||||
|
||||
const VmPlacementTab = ({ tabProps: { actions } = {} }) => {
|
||||
const VmHistoryTab = ({ tabProps: { actions } = {} }) => {
|
||||
const { data: vm } = useContext(TabContext)
|
||||
|
||||
const [records, actionsAvailable] = useMemo(() => {
|
||||
@ -40,10 +40,10 @@ const VmPlacementTab = ({ tabProps: { actions } = {} }) => {
|
||||
)
|
||||
}
|
||||
|
||||
VmPlacementTab.propTypes = {
|
||||
VmHistoryTab.propTypes = {
|
||||
tabProps: PropTypes.object
|
||||
}
|
||||
|
||||
VmPlacementTab.displayName = 'VmPlacementTab'
|
||||
VmHistoryTab.displayName = 'VmHistoryTab'
|
||||
|
||||
export default VmPlacementTab
|
||||
export default VmHistoryTab
|
@ -24,7 +24,7 @@ import Information from 'client/components/Tabs/Vm/Info/information'
|
||||
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
import { getHypervisor, isAvailableAction } from 'client/models/VirtualMachine'
|
||||
import { getHypervisor } from 'client/models/VirtualMachine'
|
||||
import { getActionsAvailable, filterAttributes, jsonToXml } from 'client/models/Helper'
|
||||
import { cloneObject, set } from 'client/utils'
|
||||
|
||||
@ -76,14 +76,10 @@ const VmInfoTab = ({ tabProps = {} }) => {
|
||||
String(response) === String(ID) && (await handleRefetch?.())
|
||||
}
|
||||
|
||||
const getActions = useCallback(actions => {
|
||||
const hypervisor = getHypervisor(vm)
|
||||
const actionsByHypervisor = getActionsAvailable(actions, hypervisor)
|
||||
const actionsByState = actionsByHypervisor
|
||||
.filter(action => !isAvailableAction(action)(vm))
|
||||
|
||||
return actionsByState
|
||||
}, [vm])
|
||||
const getActions = useCallback(
|
||||
actions => getActionsAvailable(actions, getHypervisor(vm)),
|
||||
[vm]
|
||||
)
|
||||
|
||||
const {
|
||||
attributes,
|
||||
@ -172,6 +168,7 @@ const VmInfoTab = ({ tabProps = {} }) => {
|
||||
)}
|
||||
{monitoringPanel?.enabled && monitoringAttributes && (
|
||||
<AttributePanel
|
||||
actions={getActions(monitoringPanel?.actions)}
|
||||
attributes={monitoringAttributes}
|
||||
title={Tr(T.Monitoring)}
|
||||
/>
|
||||
|
@ -17,16 +17,13 @@
|
||||
import { useMemo, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Trash } from 'iconoir-react'
|
||||
import {
|
||||
Typography,
|
||||
Accordion,
|
||||
AccordionSummary as MAccordionSummary,
|
||||
AccordionDetails,
|
||||
useMediaQuery,
|
||||
Paper
|
||||
} from '@mui/material'
|
||||
import { withStyles, makeStyles } from '@mui/styles'
|
||||
import { styled, useMediaQuery } from '@mui/material'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import MAccordion from '@mui/material/Accordion'
|
||||
import AccordionDetails from '@mui/material/AccordionDetails'
|
||||
import AccordionSummary, { accordionSummaryClasses } from '@mui/material/AccordionSummary'
|
||||
import { Trash as TrashIcon, NavArrowRight as ExpandIcon } from 'iconoir-react'
|
||||
|
||||
import { useVmApi } from 'client/features/One'
|
||||
import { useDialog } from 'client/hooks'
|
||||
@ -38,63 +35,62 @@ import MultipleTags from 'client/components/MultipleTags'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T, VM_ACTIONS } from 'client/constants'
|
||||
|
||||
const AccordionSummary = withStyles({
|
||||
root: {
|
||||
backgroundColor: 'rgba(0, 0, 0, .03)',
|
||||
borderBottom: '1px solid rgba(0, 0, 0, .125)',
|
||||
marginBottom: -1,
|
||||
minHeight: 56,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 0, 0, .07)'
|
||||
},
|
||||
'&$expanded': {
|
||||
backgroundColor: 'rgba(0, 0, 0, .07)',
|
||||
minHeight: 56
|
||||
}
|
||||
},
|
||||
content: {
|
||||
overflow: 'hidden',
|
||||
'&$expanded': {
|
||||
margin: '12px 0'
|
||||
}
|
||||
},
|
||||
expanded: {}
|
||||
})(MAccordionSummary)
|
||||
const Accordion = styled(props => (
|
||||
<MAccordion disableGutters elevation={0} square {...props} />
|
||||
))(({ theme }) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
'&:before': { display: 'none' }
|
||||
}))
|
||||
|
||||
const useStyles = makeStyles(({
|
||||
row: {
|
||||
width: '100%',
|
||||
const Summary = styled(props => (
|
||||
<AccordionSummary expandIcon={<ExpandIcon />} {...props} />
|
||||
))(({
|
||||
[`&.${accordionSummaryClasses.root}`]: {
|
||||
backgroundColor: 'rgba(0, 0, 0, .03)',
|
||||
flexDirection: 'row-reverse',
|
||||
'&:hover': { backgroundColor: 'rgba(0, 0, 0, .07)' }
|
||||
},
|
||||
[`& .${accordionSummaryClasses.expandIconWrapper}.${accordionSummaryClasses.expanded}`]: {
|
||||
transform: 'rotate(90deg)'
|
||||
}
|
||||
}))
|
||||
|
||||
const Row = styled('div')({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
gap: '0.5em',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap'
|
||||
},
|
||||
labels: {
|
||||
})
|
||||
|
||||
const Labels = styled('span')({
|
||||
display: 'inline-flex',
|
||||
gap: '0.5em',
|
||||
alignItems: 'center'
|
||||
},
|
||||
details: {
|
||||
marginLeft: '1em',
|
||||
})
|
||||
|
||||
const Details = styled(AccordionDetails)({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5em'
|
||||
},
|
||||
securityGroups: {
|
||||
gap: '0.5em',
|
||||
marginLeft: '1em'
|
||||
})
|
||||
|
||||
const SecGroups = styled(Paper)({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5em',
|
||||
padding: '0.8em'
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
const NetworkItem = ({ nic = {}, actions }) => {
|
||||
const classes = useStyles()
|
||||
const isMobile = useMediaQuery(theme => theme.breakpoints.down('md'))
|
||||
|
||||
const { display, show, hide, values } = useDialog()
|
||||
const { detachNic } = useVmApi()
|
||||
const { handleRefetch, data: vm } = useContext(TabContext)
|
||||
|
||||
const { NIC_ID, NETWORK = '-', BRIDGE, IP, MAC, PCI_ID, ALIAS, SECURITY_GROUPS } = nic
|
||||
const { NIC_ID, NETWORK = '-', IP, MAC, PCI_ID, ALIAS, SECURITY_GROUPS } = nic
|
||||
|
||||
const hasDetails = useMemo(
|
||||
() => !!ALIAS.length || !!SECURITY_GROUPS?.length,
|
||||
@ -112,7 +108,7 @@ const NetworkItem = ({ nic = {}, actions }) => {
|
||||
actions?.includes?.(VM_ACTIONS.DETACH_NIC) && (
|
||||
<Action
|
||||
cy={`${VM_ACTIONS.DETACH_NIC}-${id}`}
|
||||
icon={<Trash />}
|
||||
icon={<TrashIcon />}
|
||||
stopPropagation
|
||||
handleClick={() => show({ id, isAlias })}
|
||||
/>
|
||||
@ -120,50 +116,52 @@ const NetworkItem = ({ nic = {}, actions }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Accordion variant='outlined'>
|
||||
<AccordionSummary>
|
||||
<div className={classes.row}>
|
||||
<Accordion>
|
||||
<Summary>
|
||||
<Row>
|
||||
<Typography noWrap>
|
||||
{`${NIC_ID} | ${NETWORK}`}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<Labels>
|
||||
<MultipleTags
|
||||
limitTags={isMobile ? 1 : 4}
|
||||
tags={[IP, MAC, BRIDGE && `BRIDGE - ${BRIDGE}`, PCI_ID].filter(Boolean)}
|
||||
clipboard
|
||||
limitTags={isMobile ? 1 : 3}
|
||||
tags={[IP, MAC, PCI_ID].filter(Boolean)}
|
||||
/>
|
||||
</span>
|
||||
</Labels>
|
||||
{!isMobile && detachAction(NIC_ID)}
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
</Row>
|
||||
</Summary>
|
||||
{hasDetails && (
|
||||
<AccordionDetails className={classes.details}>
|
||||
<Details>
|
||||
{ALIAS?.map(({ NIC_ID, NETWORK = '-', BRIDGE, IP, MAC }) => (
|
||||
<div key={NIC_ID} className={classes.row}>
|
||||
<Row key={NIC_ID}>
|
||||
<Typography noWrap variant='body2'>
|
||||
<Translate word={T.Alias} />{`${NIC_ID} | ${NETWORK}`}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<Labels>
|
||||
<MultipleTags
|
||||
limitTags={isMobile ? 1 : 4}
|
||||
clipboard
|
||||
limitTags={isMobile ? 1 : 3}
|
||||
tags={[IP, MAC, BRIDGE && `BRIDGE - ${BRIDGE}`].filter(Boolean)}
|
||||
/>
|
||||
</span>
|
||||
</Labels>
|
||||
{!isMobile && detachAction(NIC_ID, true)}
|
||||
</div>
|
||||
</Row>
|
||||
))}
|
||||
{!!SECURITY_GROUPS?.length && (
|
||||
<Paper variant='outlined' className={classes.securityGroups}>
|
||||
<SecGroups variant='outlined'>
|
||||
<Typography variant='body1'>
|
||||
<Translate word={T.SecurityGroups} />
|
||||
</Typography>
|
||||
|
||||
{SECURITY_GROUPS
|
||||
?.map(({ ID, NAME, PROTOCOL, RULE_TYPE, ICMP_TYPE, RANGE, NETWORK_ID }, idx) => (
|
||||
<div key={`${idx}-${NAME}`} className={classes.row}>
|
||||
<Row key={`${idx}-${NAME}`}>
|
||||
<Typography noWrap variant='body2'>
|
||||
{`${ID} | ${NAME}`}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<Labels>
|
||||
<MultipleTags
|
||||
limitTags={isMobile ? 2 : 5}
|
||||
tags={[
|
||||
@ -174,12 +172,12 @@ const NetworkItem = ({ nic = {}, actions }) => {
|
||||
ICMP_TYPE
|
||||
].filter(Boolean)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</Labels>
|
||||
</Row>
|
||||
))}
|
||||
</Paper>
|
||||
</SecGroups>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Details>
|
||||
)}
|
||||
</Accordion>
|
||||
|
||||
@ -193,9 +191,9 @@ const NetworkItem = ({ nic = {}, actions }) => {
|
||||
handleAccept={handleDetach}
|
||||
handleCancel={hide}
|
||||
>
|
||||
<p>
|
||||
<Typography>
|
||||
<Translate word={T.DoYouWantProceed} />
|
||||
</p>
|
||||
</Typography>
|
||||
</DialogConfirmation>
|
||||
)}
|
||||
</>
|
||||
|
@ -31,7 +31,7 @@ import Configuration from 'client/components/Tabs/Vm/Configuration'
|
||||
import Info from 'client/components/Tabs/Vm/Info'
|
||||
import Log from 'client/components/Tabs/Vm/Log'
|
||||
import Network from 'client/components/Tabs/Vm/Network'
|
||||
import Placement from 'client/components/Tabs/Vm/Placement'
|
||||
import History from 'client/components/Tabs/Vm/History'
|
||||
import SchedActions from 'client/components/Tabs/Vm/SchedActions'
|
||||
import Snapshot from 'client/components/Tabs/Vm/Snapshot'
|
||||
import Storage from 'client/components/Tabs/Vm/Storage'
|
||||
@ -42,7 +42,7 @@ const getTabComponent = tabName => ({
|
||||
info: Info,
|
||||
log: Log,
|
||||
network: Network,
|
||||
placement: Placement,
|
||||
history: History,
|
||||
schedActions: SchedActions,
|
||||
snapshot: Snapshot,
|
||||
storage: Storage
|
||||
|
@ -13,14 +13,17 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useState, useMemo, JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Tabs as MTabs, Tab as MTab, Fade } from '@mui/material'
|
||||
import { Box } from '@mui/system'
|
||||
import { styled, Tabs as MTabs, TabsProps, Tab as MTab, Box, Fade } from '@mui/material'
|
||||
import { WarningCircledOutline } from 'iconoir-react'
|
||||
|
||||
const Content = ({ name, renderContent: Content, hidden }) => (
|
||||
const WarningIcon = styled(WarningCircledOutline)(({ theme }) => ({
|
||||
color: theme.palette.error.main
|
||||
}))
|
||||
|
||||
const Content = ({ name, renderContent: RenderContent, hidden }) => (
|
||||
<Fade in timeout={400} key={`tab-${name}`}>
|
||||
<Box
|
||||
sx={{
|
||||
@ -29,12 +32,25 @@ const Content = ({ name, renderContent: Content, hidden }) => (
|
||||
display: hidden ? 'none' : 'block'
|
||||
}}
|
||||
>
|
||||
{typeof Content === 'function' ? <Content /> : Content}
|
||||
{typeof RenderContent === 'function'
|
||||
? <RenderContent />
|
||||
: RenderContent}
|
||||
</Box>
|
||||
</Fade>
|
||||
)
|
||||
|
||||
const Tabs = ({ tabs = [], renderHiddenTabs = false }) => {
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {Array} props.tabs - Tabs
|
||||
* @param {TabsProps} props.tabsProps - Props to tabs component
|
||||
* @param {boolean} props.renderHiddenTabs - If `true`, will be render hidden tabs
|
||||
* @returns {JSXElementConstructor} Tabs component with content
|
||||
*/
|
||||
const Tabs = ({
|
||||
tabs = [],
|
||||
tabsProps: { sx, ...tabsProps } = {},
|
||||
renderHiddenTabs = false
|
||||
}) => {
|
||||
const [tabSelected, setTab] = useState(() => 0)
|
||||
|
||||
const renderTabs = useMemo(() => (
|
||||
@ -43,18 +59,26 @@ const Tabs = ({ tabs = [], renderHiddenTabs = false }) => {
|
||||
variant='scrollable'
|
||||
scrollButtons='auto'
|
||||
onChange={(_, tab) => setTab(tab)}
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: theme => theme.zIndex.appBar,
|
||||
...sx
|
||||
}}
|
||||
{...tabsProps}
|
||||
>
|
||||
{tabs.map(({ value, name, label, icon: Icon }, idx) =>
|
||||
<MTab
|
||||
{tabs.map(({ value, name, label, error, icon: Icon }, idx) => {
|
||||
return <MTab
|
||||
key={`tab-${name}`}
|
||||
id={`tab-${name}`}
|
||||
icon={Icon && <Icon />}
|
||||
icon={error ? <WarningIcon /> : (Icon && <Icon />)}
|
||||
value={value ?? idx}
|
||||
label={label ?? name}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</MTabs>
|
||||
), [tabs.length, tabSelected])
|
||||
), [tabs, tabSelected])
|
||||
|
||||
const renderAllHiddenTabContents = useMemo(() =>
|
||||
tabs.map((tabProps, idx) => {
|
||||
@ -83,6 +107,7 @@ Content.displayName = 'Content'
|
||||
|
||||
Tabs.propTypes = {
|
||||
tabs: PropTypes.array,
|
||||
tabsProps: PropTypes.object,
|
||||
renderHiddenTabs: PropTypes.bool
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@
|
||||
export const RENAME = 'rename'
|
||||
|
||||
// ATTRIBUTES
|
||||
export const COPY_ATTRIBUTE = 'copy'
|
||||
export const ADD_ATTRIBUTE = 'add'
|
||||
export const EDIT_ATTRIBUTE = 'edit'
|
||||
export const DELETE_ATTRIBUTE = 'delete'
|
||||
|
@ -90,6 +90,7 @@ export const SOCKETS = {
|
||||
export * as T from 'client/constants/translates'
|
||||
export * as ACTIONS from 'client/constants/actions'
|
||||
export * as STATES from 'client/constants/states'
|
||||
export * from 'client/constants/userInput'
|
||||
export * from 'client/constants/flow'
|
||||
export * from 'client/constants/provision'
|
||||
export * from 'client/constants/cluster'
|
||||
|
@ -31,7 +31,9 @@ module.exports = {
|
||||
/* actions */
|
||||
Accept: 'Accept',
|
||||
Active: 'Active',
|
||||
Add: 'Add',
|
||||
AddAction: 'Add action',
|
||||
Append: 'Append',
|
||||
Attach: 'Attach',
|
||||
AttachDisk: 'Attach disk',
|
||||
AttachImage: 'Attach image disk',
|
||||
@ -41,18 +43,20 @@ module.exports = {
|
||||
Cancel: 'Cancel',
|
||||
Change: 'Change',
|
||||
ChangeGroup: 'Change group',
|
||||
CurrentGroup: 'Current group: %s',
|
||||
ChangeOwner: 'Change owner',
|
||||
CurrentOwner: 'Current owner: %s',
|
||||
Clear: 'Clear',
|
||||
ClickToCopy: 'Click to copy',
|
||||
Clone: 'Clone',
|
||||
CloneSeveralTemplates: 'Clone several Templates',
|
||||
CloneTemplate: 'Clone Template',
|
||||
Close: 'Close',
|
||||
Collapse: 'Collapse',
|
||||
Configuration: 'Configuration',
|
||||
CopiedToClipboard: 'Copied to clipboard',
|
||||
Create: 'Create',
|
||||
CreateMarketApp: 'Create Marketplace App',
|
||||
CurrentGroup: 'Current group: %s',
|
||||
CurrentOwner: 'Current owner: %s',
|
||||
Delete: 'Delete',
|
||||
DeleteScheduledAction: 'Delete scheduled action: %s',
|
||||
DeleteSomething: 'Delete: %s',
|
||||
@ -76,8 +80,8 @@ module.exports = {
|
||||
Reboot: 'Reboot',
|
||||
RebootHard: 'Reboot hard',
|
||||
Recover: 'Recover',
|
||||
RecoverSomething: 'Recover: %s',
|
||||
RecoverSeveralVMs: 'Recover several VMs',
|
||||
RecoverSomething: 'Recover: %s',
|
||||
Refresh: 'Refresh',
|
||||
Release: 'Release',
|
||||
Remove: 'Remove',
|
||||
@ -96,8 +100,8 @@ module.exports = {
|
||||
SaveAsTemplate: 'Save as Template',
|
||||
Search: 'Search',
|
||||
Select: 'Select',
|
||||
SelectHost: 'Select a host',
|
||||
SelectGroup: 'Select a group',
|
||||
SelectHost: 'Select a host',
|
||||
SelectRequest: 'Select request',
|
||||
SelectVmTemplate: 'Select a VM Template',
|
||||
Share: 'Share',
|
||||
@ -175,6 +179,7 @@ module.exports = {
|
||||
None: 'None',
|
||||
Empty: 'Empty',
|
||||
NoDataAvailable: 'There is no data available',
|
||||
ErrorsOcurred: '%s error(s) occurred',
|
||||
|
||||
/* steps form */
|
||||
AdvancedOptions: 'Advanced options',
|
||||
@ -285,6 +290,9 @@ module.exports = {
|
||||
EndTime: 'End time',
|
||||
Locked: 'Locked',
|
||||
Attributes: 'Attributes',
|
||||
Type: 'Type',
|
||||
Data: 'Data',
|
||||
Validate: 'Validate',
|
||||
|
||||
/* permissions */
|
||||
Permissions: 'Permissions',
|
||||
@ -455,23 +463,29 @@ module.exports = {
|
||||
`Number of iothreads for virtio disks.
|
||||
By default threads will be assign to disk by round robin algorithm.
|
||||
Disk thread id can be forced by disk IOTHREAD attribute`,
|
||||
RawData: 'Raw data',
|
||||
RawDataConcept: 'Raw data to be passed directly to the hypervisor',
|
||||
RawValidateConcept: `
|
||||
Disable validation of the RAW data.
|
||||
By default, the data will be checked against the libvirt schema`,
|
||||
/* VM Template schema - context */
|
||||
Context: 'Context',
|
||||
/* VM Template schema - Input/Output */
|
||||
InputOrOutput: 'Input / Output',
|
||||
Inputs: 'Inputs',
|
||||
/* 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',
|
||||
Bus: 'Bus',
|
||||
/* VM Template schema - NUMA */
|
||||
PinPolicy: 'Pin Policy',
|
||||
PinPolicyConcept: 'Virtual CPU pinning preference: %s',
|
||||
@ -517,10 +531,21 @@ module.exports = {
|
||||
ReservedCpu: 'Allocated CPU',
|
||||
|
||||
/* User inputs */
|
||||
UserInputs: 'User Inputs',
|
||||
UserInputsConcept: `
|
||||
These attributes must be provided by the user when a new VM is instantiated.
|
||||
They will be included in the VM context`,
|
||||
Fixed: 'Fixed',
|
||||
Range: 'Range',
|
||||
List: 'List',
|
||||
AnyValue: 'Any value',
|
||||
Options: 'Options',
|
||||
UIOptionsConcept:
|
||||
'Comma-separated list of options for the drop-down select input',
|
||||
Min: 'Min',
|
||||
Max: 'Max',
|
||||
DefaultValue: 'Default value',
|
||||
Mandatory: 'Mandatory',
|
||||
|
||||
/* Validation */
|
||||
/* Validation - mixed */
|
||||
|
59
src/fireedge/src/client/constants/userInput.js
Normal file
59
src/fireedge/src/client/constants/userInput.js
Normal file
@ -0,0 +1,59 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @typedef {(
|
||||
* 'text' |
|
||||
* 'text64' |
|
||||
* 'password' |
|
||||
* 'number' |
|
||||
* 'number-float' |
|
||||
* 'range' |
|
||||
* 'range-float' |
|
||||
* 'boolean' |
|
||||
* 'list' |
|
||||
* 'array' |
|
||||
* 'list-multiple'
|
||||
* )} UserInputType
|
||||
* - OpenNebula types for user inputs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} UserInputObject
|
||||
* @property {boolean} mandatory - If `true`, the input will be required
|
||||
* @property {UserInputType} type - Input type
|
||||
* @property {string} name - Name of input
|
||||
* @property {string} [description] - Description of input
|
||||
* @property {number|string} [min] - Minimum value of range type input
|
||||
* @property {number|string} [max] - Maximum value of range type input
|
||||
* @property {string[]} [options] - Options available for the input
|
||||
* @property {number|string|string[]} [default] - Default value for the input
|
||||
*/
|
||||
|
||||
/** @enum {UserInputType} User input types */
|
||||
export const USER_INPUT_TYPES = {
|
||||
text: 'text',
|
||||
text64: 'text64',
|
||||
password: 'password',
|
||||
number: 'number',
|
||||
numberFloat: 'number-float',
|
||||
range: 'range',
|
||||
rangeFloat: 'range-float',
|
||||
boolean: 'boolean',
|
||||
list: 'list',
|
||||
array: 'array',
|
||||
listMultiple: 'list-multiple'
|
||||
}
|
@ -43,6 +43,9 @@ export const DEFAULT_CPU_MODELS = ['host-passthrough']
|
||||
|
||||
export const SD_DISK_BUSES = ['scsi', 'sata']
|
||||
|
||||
export const DEVICE_TYPES = { mouse: 'mouse', tablet: 'tablet' }
|
||||
export const DEVICE_BUS_TYPES = { usb: 'usb', ps2: 'ps2' }
|
||||
|
||||
export const FIRMWARE_TYPES = ['BIOS']
|
||||
|
||||
export const KVM_FIRMWARE_TYPES = FIRMWARE_TYPES.concat([
|
||||
|
@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import useClipboard, { CLIPBOARD_STATUS } from 'client/hooks/useClipboard'
|
||||
import useDialog from 'client/hooks/useDialog'
|
||||
import useFetch from 'client/hooks/useFetch'
|
||||
import useFetchAll from 'client/hooks/useFetchAll'
|
||||
@ -23,6 +24,7 @@ import useSearch from 'client/hooks/useSearch'
|
||||
import useSocket from 'client/hooks/useSocket'
|
||||
|
||||
export {
|
||||
useClipboard,
|
||||
useDialog,
|
||||
useFetch,
|
||||
useFetchAll,
|
||||
@ -30,5 +32,7 @@ export {
|
||||
useListForm,
|
||||
useNearScreen,
|
||||
useSearch,
|
||||
useSocket
|
||||
useSocket,
|
||||
|
||||
CLIPBOARD_STATUS
|
||||
}
|
||||
|
71
src/fireedge/src/client/hooks/useClipboard.js
Normal file
71
src/fireedge/src/client/hooks/useClipboard.js
Normal file
@ -0,0 +1,71 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { useMemo, useState } from 'react'
|
||||
|
||||
/** @enum {string} Clipboard state */
|
||||
export const CLIPBOARD_STATUS = {
|
||||
INIT: 'INIT',
|
||||
ERROR: 'ERROR',
|
||||
COPIED: 'COPIED'
|
||||
}
|
||||
|
||||
const { INIT, ERROR, COPIED } = CLIPBOARD_STATUS
|
||||
|
||||
/**
|
||||
* @callback CallbackCopy
|
||||
* @param {string} text - Text to write on clipboard
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} UseClipboard
|
||||
* @property {CallbackCopy} copy - Function to write text on clipboard
|
||||
* @property {CLIPBOARD_STATUS} state - Result state of copy action
|
||||
* @property {boolean} isCopied
|
||||
* - If the text is copied successfully, will be `true` temporally (2s)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hook to manage a clipboard.
|
||||
*
|
||||
* @param {string|number} tooltipDelay - Time in milliseconds to hide the tooltip
|
||||
* @returns {UseClipboard} Returns management attributes
|
||||
*/
|
||||
const useClipboard = ({ tooltipDelay = 2000 } = {}) => {
|
||||
const [state, setState] = useState(() => INIT)
|
||||
const isCopied = useMemo(() => state === COPIED, [state])
|
||||
|
||||
const copy = async text => {
|
||||
try {
|
||||
// Use the Async Clipboard API when available.
|
||||
// Requires a secure browsing context (i.e. HTTPS)
|
||||
!navigator?.clipboard && setState(ERROR)
|
||||
|
||||
await navigator.clipboard.writeText(String(text))
|
||||
|
||||
setState(COPIED)
|
||||
if (+tooltipDelay > 0) {
|
||||
setTimeout(() => setState(INIT), +tooltipDelay)
|
||||
}
|
||||
} catch (error) {
|
||||
setState(ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
return { copy, state, isCopied }
|
||||
}
|
||||
|
||||
export default useClipboard
|
@ -21,7 +21,8 @@ const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn?.(...args))
|
||||
* Hook to manage a dialog.
|
||||
*
|
||||
* @returns {{
|
||||
* on: boolean,
|
||||
* display: boolean,
|
||||
* values: any,
|
||||
* show: Function,
|
||||
* hide: Function,
|
||||
* toggle: Function,
|
||||
|
@ -53,6 +53,7 @@ import { set } from 'client/utils'
|
||||
|
||||
/**
|
||||
* @typedef {object} HookListForm
|
||||
* @property {object[]} list - Form list
|
||||
* @property {object} editingData - Current editing data
|
||||
* @property {NoParamsCallback} handleClear - Clear the data form list
|
||||
* @property {NewListCallback} handleSetList - Resets the list with a new value
|
||||
@ -201,6 +202,7 @@ const useListForm = ({
|
||||
)
|
||||
|
||||
return {
|
||||
list,
|
||||
editingData,
|
||||
handleSelect,
|
||||
handleUnselect,
|
||||
|
@ -13,10 +13,11 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { T } from 'client/constants'
|
||||
import { DateTime } from 'luxon'
|
||||
import { j2xParser as Parser } from 'fast-xml-parser'
|
||||
|
||||
import { T, UserInputObject, USER_INPUT_TYPES } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @param {object} json - JSON
|
||||
* @param {boolean} [addRoot] - Add ROOT element as parent
|
||||
@ -124,6 +125,21 @@ export const levelLockToString = level => ({
|
||||
4: T.All
|
||||
}[level] || '-')
|
||||
|
||||
/**
|
||||
* Returns the permission numeric code.
|
||||
*
|
||||
* @param {string[]} category - Array with Use, Manage and Access permissions.
|
||||
* @param {('YES'|'NO')} category.0 - `true` if use permission is allowed
|
||||
* @param {('YES'|'NO')} category.1 - `true` if manage permission is allowed
|
||||
* @param {('YES'|'NO')} category.2 - `true` if access permission is allowed
|
||||
* @returns {number} Permission code number.
|
||||
*/
|
||||
const getCategoryValue = ([u, m, a]) => (
|
||||
(stringToBoolean(u) ? 4 : 0) +
|
||||
(stringToBoolean(m) ? 2 : 0) +
|
||||
(stringToBoolean(a) ? 1 : 0)
|
||||
)
|
||||
|
||||
/**
|
||||
* Transform the permission from OpenNebula template to octal format.
|
||||
*
|
||||
@ -146,21 +162,6 @@ export const permissionsToOctal = permissions => {
|
||||
OTHER_U, OTHER_M, OTHER_A
|
||||
} = permissions
|
||||
|
||||
/**
|
||||
* Returns the permission numeric code.
|
||||
*
|
||||
* @param {string[]} category - Array with Use, Manage and Access permissions.
|
||||
* @param {('YES'|'NO')} category.0 - `true` if use permission is allowed
|
||||
* @param {('YES'|'NO')} category.1 - `true` if manage permission is allowed
|
||||
* @param {('YES'|'NO')} category.2 - `true` if access permission is allowed
|
||||
* @returns {number} Permission code number.
|
||||
*/
|
||||
const getCategoryValue = ([u, m, a]) => (
|
||||
(stringToBoolean(u) ? 4 : 0) +
|
||||
(stringToBoolean(m) ? 2 : 0) +
|
||||
(stringToBoolean(a) ? 1 : 0)
|
||||
)
|
||||
|
||||
return [
|
||||
[OWNER_U, OWNER_M, OWNER_A],
|
||||
[GROUP_U, GROUP_M, GROUP_A],
|
||||
@ -219,3 +220,95 @@ export const filterAttributes = (list = {}, options = {}) => {
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// User inputs
|
||||
// ----------------------------------------------------------
|
||||
|
||||
const PARAMS_SEPARATOR = '|'
|
||||
const RANGE_SEPARATOR = '..'
|
||||
const LIST_SEPARATOR = ','
|
||||
const OPTIONS_DEFAULT = ' '
|
||||
const MANDATORY = 'M'
|
||||
const OPTIONAL = 'O'
|
||||
|
||||
/**
|
||||
* Get object attributes from user input as string.
|
||||
*
|
||||
* @param {string} userInputString - User input as string with format:
|
||||
* mandatory | type | description | options | defaultValue
|
||||
* @returns {UserInputObject} User input object
|
||||
*/
|
||||
export const getUserInputParams = userInputString => {
|
||||
const params = String(userInputString).split(PARAMS_SEPARATOR)
|
||||
|
||||
const options = [
|
||||
USER_INPUT_TYPES.range,
|
||||
USER_INPUT_TYPES.rangeFloat
|
||||
].includes(params[1])
|
||||
? params[3].split(RANGE_SEPARATOR)
|
||||
: params[3].split(LIST_SEPARATOR)
|
||||
|
||||
return {
|
||||
mandatory: params[0] === MANDATORY,
|
||||
type: params[1],
|
||||
description: params[2],
|
||||
options: options,
|
||||
default: params[4],
|
||||
min: options[0],
|
||||
max: options[1]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UserInputObject} userInput - User input object
|
||||
* @returns {string} User input in string format
|
||||
*/
|
||||
export const getUserInputString = userInput => {
|
||||
const {
|
||||
mandatory,
|
||||
mandatoryString = mandatory ? MANDATORY : OPTIONAL,
|
||||
type,
|
||||
description,
|
||||
min,
|
||||
max,
|
||||
range = [min, max].filter(Boolean).join(RANGE_SEPARATOR),
|
||||
options,
|
||||
default: defaultValue
|
||||
} = userInput
|
||||
|
||||
// mandatory|type|description|range/options/' '|defaultValue
|
||||
const uiString = [mandatoryString, type, description]
|
||||
|
||||
range?.length > 0
|
||||
? uiString.push(range)
|
||||
: options?.length > 0
|
||||
? uiString.push(options.join(LIST_SEPARATOR))
|
||||
: uiString.push(OPTIONS_DEFAULT)
|
||||
|
||||
return uiString.concat(defaultValue).join(PARAMS_SEPARATOR)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of user inputs defined in OpenNebula template.
|
||||
*
|
||||
* @param {object} userInputs - List of user inputs in string format
|
||||
* @returns {UserInputObject[]} User input object
|
||||
*/
|
||||
export const userInputsToArray = userInputs => {
|
||||
return Object
|
||||
.entries(userInputs)
|
||||
.map(([name, ui]) => ({ name, ...getUserInputParams(ui) }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of user inputs in format valid to forms.
|
||||
*
|
||||
* @param {UserInputObject[]} userInputs - List of user inputs in object format
|
||||
* @returns {object} User input object
|
||||
*/
|
||||
export const userInputsToObject = userInputs =>
|
||||
userInputs.reduce((res, { name, ...userInput }) => ({
|
||||
...res,
|
||||
[String(name).toUpperCase()]: getUserInputString(userInput)
|
||||
}), {})
|
||||
|
@ -73,9 +73,10 @@ export const getAllocatedInfo = host => {
|
||||
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)
|
||||
)
|
||||
return numaNodes
|
||||
.filter(node => node?.NODE_ID && node?.HUGEPAGE)
|
||||
.map(node => node.HUGEPAGE.map(({ SIZE }) => +SIZE))
|
||||
.flat()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -80,7 +80,7 @@ export const getSecurityGroupsFromResource = (resource, securityGroups) => {
|
||||
|
||||
const groups = Array.isArray(securityGroups)
|
||||
? securityGroups
|
||||
: securityGroups.split(',')
|
||||
: securityGroups?.split(',')
|
||||
|
||||
return rules.filter(({ SECURITY_GROUP_ID }) => groups.includes?.(SECURITY_GROUP_ID))
|
||||
return rules.filter(({ SECURITY_GROUP_ID }) => groups?.includes?.(SECURITY_GROUP_ID))
|
||||
}
|
||||
|
@ -248,16 +248,6 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
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' }
|
||||
@ -424,8 +414,7 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
},
|
||||
MuiList: {
|
||||
defaultProps: {
|
||||
dense: true,
|
||||
disablePadding: true
|
||||
dense: true
|
||||
}
|
||||
},
|
||||
MuiChip: {
|
||||
|
@ -132,8 +132,8 @@ export const getObjectSchemaFromFields = fields =>
|
||||
}
|
||||
|
||||
const paths = name.split('.')
|
||||
const path = paths.pop()
|
||||
const fieldSchema = object({ [path]: validation })
|
||||
const pathname = paths.pop()
|
||||
const fieldSchema = object({ [pathname]: validation })
|
||||
|
||||
/**
|
||||
* @param {string} [path] - Path
|
||||
|
@ -20,8 +20,10 @@ import { JSXElementConstructor, SetStateAction } from 'react'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { GridProps, TextFieldProps, CheckboxProps, InputBaseComponentProps } from '@mui/material'
|
||||
import { string, number, boolean, array, object, BaseSchema } from 'yup'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Row } from 'react-table'
|
||||
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
import { UserInputObject, INPUT_TYPES, USER_INPUT_TYPES } from 'client/constants'
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Types
|
||||
@ -90,16 +92,37 @@ import { INPUT_TYPES } from 'client/constants'
|
||||
* @property {BaseSchema|DependOfCallback} [validation]
|
||||
* - Schema to validate the field value
|
||||
* @property {TextFieldProps|CheckboxProps|InputBaseComponentProps} [fieldProps]
|
||||
* - Extra properties to material field
|
||||
* - Extra properties to material-ui field
|
||||
* @property {function(string|number):any} [renderValue]
|
||||
* - Render the current selected value inside selector input
|
||||
* - **Only for select inputs.**
|
||||
* @property {JSXElementConstructor} [Table]
|
||||
* - Table component. One of table defined in: `client/components/Tables`
|
||||
* - **Only for table inputs.**
|
||||
* @property {boolean|DependOfCallback} [singleSelect]
|
||||
* If `true`, the table component only will allows to select one row
|
||||
* - **Only for table inputs.**
|
||||
* @property {function(Row, number):string} [getRowId]
|
||||
* This function changes how React Table detects unique rows
|
||||
* and also how it constructs each row's underlying id property.
|
||||
* - **Only for table inputs.**
|
||||
* @property {{message: string, test: Function}[]|DependOfCallback} [validationBeforeTransform]
|
||||
* - Tests to validate the field value.
|
||||
* - **Only for file inputs.**
|
||||
* @property {Function|DependOfCallback} [transform]
|
||||
* @property {Function} [transform]
|
||||
* - Transform the file value.
|
||||
* - For example: to save file as string value in base64 format.
|
||||
* - **Only for file inputs.**
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Section
|
||||
* @property {string} id - Section id
|
||||
* @property {string} legend - Legend text
|
||||
* @property {string} legendTooltip - Legend tooltip
|
||||
* @property {Field[]} fields - The Fields will be includes on section
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Form
|
||||
* @property {BaseSchema|function(object):BaseSchema} resolver - Schema
|
||||
@ -149,12 +172,6 @@ import { INPUT_TYPES } from 'client/constants'
|
||||
* @returns {Form & ExtraParams}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {('text'|'text64'|'password'|'number'|'number-float'|'range'|
|
||||
* 'range-float'|'boolean'|'list'|'array'|'list-multiple')} UserInputType
|
||||
* - OpenNebula types for user inputs
|
||||
*/
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Constants
|
||||
// ----------------------------------------------------------
|
||||
@ -192,19 +209,15 @@ const parseUserInputValue = value => {
|
||||
/**
|
||||
* Get input schema for the user input defined in OpenNebula resource.
|
||||
*
|
||||
* @param {object} userInput - User input from OpenNebula document
|
||||
* @param {boolean} userInput.mandatory - If `true`, the input will be required
|
||||
* @param {string} userInput.name - Name of input
|
||||
* @param {UserInputType} userInput.type - Input type
|
||||
* @param {string} [userInput.options] - Options available for the input
|
||||
* @param {number|string|string[]} [userInput.defaultValue] - Default value for the input
|
||||
* @param {UserInputObject} userInput - User input from OpenNebula document
|
||||
* @param {number|string|string[]} [userInput.default] - Default value for the input
|
||||
* @returns {Field} Field properties
|
||||
*/
|
||||
export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }) => {
|
||||
export const schemaUserInput = ({ mandatory, name, type, options, default: defaultValue }) => {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
case 'text64':
|
||||
case 'password': return {
|
||||
case [USER_INPUT_TYPES.text]:
|
||||
case [USER_INPUT_TYPES.text64]:
|
||||
case [USER_INPUT_TYPES.password]: return {
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: type === 'password' ? 'password' : 'text',
|
||||
validation: string()
|
||||
@ -212,8 +225,8 @@ export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }
|
||||
.concat(requiredSchema(mandatory, name, string()))
|
||||
.default(defaultValue || undefined)
|
||||
}
|
||||
case 'number':
|
||||
case 'number-float': return {
|
||||
case [USER_INPUT_TYPES.number]:
|
||||
case [USER_INPUT_TYPES.numberFloat]: return {
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: number()
|
||||
@ -222,8 +235,8 @@ export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }
|
||||
.transform(value => !isNaN(value) ? value : null)
|
||||
.default(() => parseFloat(defaultValue) ?? undefined)
|
||||
}
|
||||
case 'range':
|
||||
case 'range-float': {
|
||||
case [USER_INPUT_TYPES.range]:
|
||||
case [USER_INPUT_TYPES.rangeFloat]: {
|
||||
const [min, max] = getRange(options)
|
||||
|
||||
return {
|
||||
@ -238,13 +251,13 @@ export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }
|
||||
fieldProps: { min, max, step: type === 'range-float' ? 0.01 : 1 }
|
||||
}
|
||||
}
|
||||
case 'boolean': return {
|
||||
case [USER_INPUT_TYPES.boolean]: return {
|
||||
type: INPUT_TYPES.CHECKBOX,
|
||||
validation: boolean()
|
||||
.concat(requiredSchema(mandatory, name, boolean()))
|
||||
.default(defaultValue === 'YES' ?? false)
|
||||
}
|
||||
case 'list': {
|
||||
case [USER_INPUT_TYPES.list]: {
|
||||
const values = getOptionsFromList(options)
|
||||
const firstOption = values?.[0]?.value ?? undefined
|
||||
|
||||
@ -258,7 +271,7 @@ export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }
|
||||
.default(defaultValue ?? firstOption)
|
||||
}
|
||||
}
|
||||
case 'array': {
|
||||
case [USER_INPUT_TYPES.array]: {
|
||||
const defaultValues = getValuesFromArray(defaultValue)
|
||||
|
||||
return {
|
||||
@ -270,7 +283,7 @@ export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }
|
||||
fieldProps: { freeSolo: true }
|
||||
}
|
||||
}
|
||||
case 'list-multiple': {
|
||||
case [USER_INPUT_TYPES.listMultiple]: {
|
||||
const values = getOptionsFromList(options)
|
||||
const defaultValues = defaultValue?.split(',') ?? undefined
|
||||
|
||||
|
@ -36,6 +36,18 @@ const buildMethods = () => {
|
||||
value => isBase64(value)
|
||||
)
|
||||
})
|
||||
addMethod(string, 'includesInOptions', function (options, separator = ',') {
|
||||
return this.test({
|
||||
name: 'includes-string-of-values',
|
||||
message: [T['validation.string.invalidFormat'], options.join(separator)],
|
||||
exclusive: true,
|
||||
test: function (values) {
|
||||
return values
|
||||
?.split(separator)
|
||||
?.every(value => this.resolve(options).includes(value))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -573,7 +573,8 @@ const functionRoutes = {
|
||||
setRes,
|
||||
updaterResponse,
|
||||
setNodeConnect,
|
||||
connectOpennebula
|
||||
connectOpennebula,
|
||||
getCreatedTokenOpennebula
|
||||
}
|
||||
|
||||
module.exports = functionRoutes
|
||||
|
@ -14,19 +14,51 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const questions = {
|
||||
datastores: [
|
||||
const DATASTORES = 'datastores'
|
||||
const TEMPLATES = 'templates'
|
||||
const NETWORKS = 'networks'
|
||||
const IMAGES = 'images'
|
||||
|
||||
],
|
||||
templates: [
|
||||
|
||||
],
|
||||
networks: [
|
||||
|
||||
],
|
||||
images: [
|
||||
|
||||
]
|
||||
const resources = {
|
||||
DATASTORES,
|
||||
TEMPLATES,
|
||||
NETWORKS,
|
||||
IMAGES
|
||||
}
|
||||
|
||||
module.exports = questions
|
||||
const questions = {
|
||||
[DATASTORES]: [],
|
||||
[TEMPLATES]: [
|
||||
{
|
||||
question: 'Type',
|
||||
flag: '--type'
|
||||
}, {
|
||||
question: 'Folder',
|
||||
flag: '--folder'
|
||||
}, {
|
||||
question: 'Linked Clone',
|
||||
flag: '--linked-clone'
|
||||
}
|
||||
],
|
||||
[NETWORKS]: [
|
||||
{
|
||||
question: 'Size',
|
||||
flag: '--size'
|
||||
}, {
|
||||
question: 'Type',
|
||||
flag: '--type'
|
||||
}, {
|
||||
question: 'MAC',
|
||||
flag: '--mac'
|
||||
}, {
|
||||
question: 'Cluster(s) to import',
|
||||
flag: '--cluster-imports'
|
||||
}, {
|
||||
question: 'Unimported Cluster(s)',
|
||||
flag: '--cluster-unimported'
|
||||
}
|
||||
],
|
||||
[IMAGES]: []
|
||||
}
|
||||
|
||||
module.exports = { resources, questions }
|
||||
|
262
src/fireedge/src/server/routes/api/vcenter/functions.js
Normal file
262
src/fireedge/src/server/routes/api/vcenter/functions.js
Normal file
@ -0,0 +1,262 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const {
|
||||
defaultEmptyFunction,
|
||||
defaultCommandVcenter
|
||||
} = require('server/utils/constants/defaults')
|
||||
|
||||
const {
|
||||
ok,
|
||||
internalServerError,
|
||||
badRequest
|
||||
} = require('server/utils/constants/http-codes')
|
||||
const { httpResponse, executeCommand } = require('server/utils/server')
|
||||
const { consoleParseToString, consoleParseToJSON } = require('server/utils/opennebula')
|
||||
const { resources } = require('./command-flags')
|
||||
|
||||
const { getSunstoneConfig } = require('server/utils/yml')
|
||||
|
||||
const httpBadRequest = httpResponse(badRequest, '', '')
|
||||
const appConfig = getSunstoneConfig()
|
||||
const prependCommand = appConfig.vcenter_prepend_command || ''
|
||||
const regexExclude = [
|
||||
/^Connecting to.*/gi,
|
||||
/^Exploring vCenter.*/gi,
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/^\u001b\[.*?m\u001b\[.*?m# vCenter.*/gi
|
||||
]
|
||||
const validObjects = Object.values(resources)
|
||||
|
||||
/**
|
||||
* Import the the desired vCenter object.
|
||||
*
|
||||
* @param {object} res - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {object} userData - user of http request
|
||||
*/
|
||||
const importVcenter = (res = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
|
||||
let rtn = httpBadRequest
|
||||
// check params
|
||||
if (params && params.vobject && validObjects.includes(params.vobject) && params.host) {
|
||||
const vobject = `${params.vobject}`.toLowerCase()
|
||||
|
||||
let paramsCommand = [
|
||||
params.answers ? 'import' : 'import_defaults'
|
||||
]
|
||||
|
||||
if (params.id) {
|
||||
paramsCommand.push(
|
||||
`${params.id}`
|
||||
)
|
||||
}
|
||||
|
||||
let vobjectAndHost = [
|
||||
'-o',
|
||||
`${vobject}`,
|
||||
'-h',
|
||||
`${params.host}`
|
||||
]
|
||||
|
||||
if (vobject === resources.IMAGES && params.datastore) {
|
||||
const datastoreParameter = ['-d', params.datastore]
|
||||
vobjectAndHost = [...vobjectAndHost, ...datastoreParameter]
|
||||
}
|
||||
|
||||
// flags by questions import command
|
||||
/* if (params.answers && questions[vobject]) {
|
||||
const answers = params.answers.split(',')
|
||||
} */
|
||||
|
||||
paramsCommand = [...paramsCommand, ...vobjectAndHost]
|
||||
const executedCommand = executeCommand(defaultCommandVcenter, paramsCommand, prependCommand)
|
||||
const response = executedCommand.success ? ok : internalServerError
|
||||
let message = ''
|
||||
if (executedCommand.data) {
|
||||
message = consoleParseToString(
|
||||
executedCommand.data,
|
||||
regexExclude
|
||||
)
|
||||
}
|
||||
rtn = httpResponse(response, message)
|
||||
}
|
||||
res.locals.httpCode = rtn
|
||||
next()
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a list with unimported vCenter objects excluding all filters.
|
||||
*
|
||||
* @param {object} res - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {object} userData - user of http request
|
||||
*/
|
||||
const list = (res = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
|
||||
let rtn = httpBadRequest
|
||||
if (params && params.vobject && validObjects.includes(params.vobject) && params.host) {
|
||||
const vobject = `${params.vobject}`.toLowerCase()
|
||||
let paramsCommand = [
|
||||
'list',
|
||||
'-o',
|
||||
`${vobject}`,
|
||||
'-h',
|
||||
`${params.host}`,
|
||||
'--csv'
|
||||
]
|
||||
if (vobject === resources.IMAGES && params.datastore) {
|
||||
const newParameters = ['-d', params.datastore]
|
||||
paramsCommand = [...paramsCommand, ...newParameters]
|
||||
}
|
||||
const executedCommand = executeCommand(defaultCommandVcenter, paramsCommand, prependCommand)
|
||||
|
||||
const response = executedCommand.success ? ok : internalServerError
|
||||
let message = ''
|
||||
if (executedCommand.data) {
|
||||
message = consoleParseToJSON(
|
||||
consoleParseToString(
|
||||
executedCommand.data,
|
||||
regexExclude
|
||||
),
|
||||
/^IMID,.*/gi
|
||||
)
|
||||
}
|
||||
rtn = httpResponse(response, message)
|
||||
}
|
||||
res.locals.httpCode = rtn
|
||||
next()
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a list with unimported vCenter objects excluding all filters.
|
||||
*
|
||||
* @param {object} res - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {object} userData - user of http request
|
||||
*/
|
||||
const listAll = (res = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
|
||||
let rtn = httpBadRequest
|
||||
if (params && params.vobject && validObjects.includes(params.vobject) && params.host) {
|
||||
const vobject = `${params.vobject}`.toLowerCase()
|
||||
let paramsCommand = [
|
||||
'list_all',
|
||||
'-o',
|
||||
`${vobject}`,
|
||||
'-h',
|
||||
`${params.host}`,
|
||||
'--csv'
|
||||
]
|
||||
if (vobject === resources.IMAGES && params.datastore) {
|
||||
const newParameters = ['-d', params.datastore]
|
||||
paramsCommand = [...paramsCommand, ...newParameters]
|
||||
}
|
||||
const executedCommand = executeCommand(defaultCommandVcenter, paramsCommand, prependCommand)
|
||||
|
||||
const response = executedCommand.success ? ok : internalServerError
|
||||
let message = ''
|
||||
if (executedCommand.data) {
|
||||
message = consoleParseToJSON(
|
||||
consoleParseToString(
|
||||
executedCommand.data,
|
||||
regexExclude
|
||||
),
|
||||
/^IMID,.*/gi
|
||||
)
|
||||
}
|
||||
rtn = httpResponse(response, message)
|
||||
}
|
||||
res.locals.httpCode = rtn
|
||||
next()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear extraconfig tags from a vCenter VM, useful when a VM has been launched by OpenNebula and needs to be reimported.
|
||||
*
|
||||
* @param {object} res - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {object} userData - user of http request
|
||||
*/
|
||||
const cleartags = (res = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
|
||||
let rtn = httpBadRequest
|
||||
// check params
|
||||
if (params && params.id) {
|
||||
const paramsCommand = [
|
||||
'cleartags',
|
||||
`${params.id}`
|
||||
]
|
||||
const executedCommand = executeCommand(defaultCommandVcenter, paramsCommand, prependCommand)
|
||||
const response = executedCommand.success ? ok : internalServerError
|
||||
let message = ''
|
||||
if (executedCommand.data) {
|
||||
message = consoleParseToString(
|
||||
executedCommand.data,
|
||||
regexExclude
|
||||
)
|
||||
}
|
||||
rtn = httpResponse(response, message)
|
||||
}
|
||||
res.locals.httpCode = rtn
|
||||
next()
|
||||
}
|
||||
|
||||
/**
|
||||
* Import vCenter cluster as Opennebula host.
|
||||
*
|
||||
* @param {object} res - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {object} userData - user of http request
|
||||
*/
|
||||
const hosts = (res = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
|
||||
let rtn = httpBadRequest
|
||||
// check params
|
||||
if (params && params.vcenter && params.user && params.pass) {
|
||||
const paramsCommand = [
|
||||
'hosts',
|
||||
'--vcenter',
|
||||
`${params.vcenter}`.toLowerCase(),
|
||||
'--vuser',
|
||||
`${params.user}`,
|
||||
'--vpass',
|
||||
`${params.pass}`,
|
||||
'--use-defaults'
|
||||
]
|
||||
const executedCommand = executeCommand(defaultCommandVcenter, paramsCommand, prependCommand)
|
||||
const response = executedCommand.success ? ok : internalServerError
|
||||
let message = ''
|
||||
if (executedCommand.data) {
|
||||
message = consoleParseToString(
|
||||
executedCommand.data,
|
||||
regexExclude
|
||||
)
|
||||
}
|
||||
rtn = httpResponse(response, message)
|
||||
}
|
||||
res.locals.httpCode = rtn
|
||||
next()
|
||||
}
|
||||
|
||||
const functionRoutes = {
|
||||
importVcenter,
|
||||
list,
|
||||
listAll,
|
||||
cleartags,
|
||||
hosts
|
||||
}
|
||||
module.exports = functionRoutes
|
@ -14,12 +14,46 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const privateRoutes = []
|
||||
const { addFunctionAsRoute, setFunctionRoute } = require('server/utils/server')
|
||||
const { routes: vcenterRoutes } = require('./vcenter')
|
||||
const { VCENTER } = require('./string-routes')
|
||||
|
||||
const privateRoutes = []
|
||||
const publicRoutes = []
|
||||
|
||||
/**
|
||||
* Set private routes.
|
||||
*
|
||||
* @param {object} routes - object of routes
|
||||
* @param {string} path - principal route
|
||||
* @param {Function} action - function of route
|
||||
*/
|
||||
const setPrivateRoutes = (routes = {}, path = '', action = () => undefined) => {
|
||||
if (Object.keys(routes).length > 0 && routes.constructor === Object) {
|
||||
Object.keys(routes).forEach((route) => {
|
||||
privateRoutes.push(
|
||||
setFunctionRoute(route, path,
|
||||
(req, res, next, connection, userId, user) => {
|
||||
action(req, res, next, routes[route], user, connection)
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add routes.
|
||||
*
|
||||
* @returns {Array} routes
|
||||
*/
|
||||
const generatePrivateRoutes = () => {
|
||||
setPrivateRoutes(vcenterRoutes, VCENTER, addFunctionAsRoute)
|
||||
return privateRoutes
|
||||
}
|
||||
|
||||
const functionRoutes = {
|
||||
private: privateRoutes,
|
||||
private: generatePrivateRoutes(),
|
||||
public: publicRoutes
|
||||
}
|
||||
|
||||
|
23
src/fireedge/src/server/routes/api/vcenter/string-routes.js
Normal file
23
src/fireedge/src/server/routes/api/vcenter/string-routes.js
Normal file
@ -0,0 +1,23 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const VCENTER = 'vcenter'
|
||||
|
||||
const Actions = {
|
||||
VCENTER
|
||||
}
|
||||
|
||||
module.exports = Actions
|
116
src/fireedge/src/server/routes/api/vcenter/vcenter.js
Normal file
116
src/fireedge/src/server/routes/api/vcenter/vcenter.js
Normal file
@ -0,0 +1,116 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const { httpMethod, from: fromData } = require('server/utils/constants/defaults')
|
||||
const { importVcenter, list, listAll, cleartags, hosts } = require('./functions')
|
||||
const { POST, GET } = httpMethod
|
||||
|
||||
const routes = {
|
||||
[POST]: {
|
||||
import: {
|
||||
action: importVcenter,
|
||||
params: {
|
||||
vobject: {
|
||||
from: fromData.resource,
|
||||
name: 'id'
|
||||
},
|
||||
host: {
|
||||
from: fromData.postBody,
|
||||
name: 'host'
|
||||
},
|
||||
datastore: {
|
||||
from: fromData.postBody,
|
||||
name: 'datastore'
|
||||
},
|
||||
id: {
|
||||
from: fromData.postBody,
|
||||
name: 'id'
|
||||
},
|
||||
answers: {
|
||||
from: fromData.postBody,
|
||||
name: 'answers'
|
||||
}
|
||||
}
|
||||
},
|
||||
cleartags: {
|
||||
action: cleartags,
|
||||
params: {
|
||||
id: {
|
||||
from: fromData.resource,
|
||||
name: 'id'
|
||||
}
|
||||
}
|
||||
},
|
||||
hosts: {
|
||||
action: hosts,
|
||||
params: {
|
||||
vcenter: {
|
||||
from: fromData.postBody,
|
||||
name: 'vcenter'
|
||||
},
|
||||
user: {
|
||||
from: fromData.postBody,
|
||||
name: 'user'
|
||||
},
|
||||
pass: {
|
||||
from: fromData.postBody,
|
||||
name: 'pass'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[GET]: {
|
||||
list: {
|
||||
action: list,
|
||||
params: {
|
||||
vobject: {
|
||||
from: fromData.resource,
|
||||
name: 'id'
|
||||
},
|
||||
host: {
|
||||
from: fromData.query,
|
||||
name: 'host'
|
||||
},
|
||||
datastore: {
|
||||
from: fromData.query,
|
||||
name: 'datastore'
|
||||
}
|
||||
}
|
||||
},
|
||||
listall: {
|
||||
action: listAll,
|
||||
params: {
|
||||
vobject: {
|
||||
from: fromData.resource,
|
||||
name: 'id'
|
||||
},
|
||||
host: {
|
||||
from: fromData.query,
|
||||
name: 'host'
|
||||
},
|
||||
datastore: {
|
||||
from: fromData.query,
|
||||
name: 'datastore'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const authApi = {
|
||||
routes
|
||||
}
|
||||
module.exports = authApi
|
65
src/fireedge/src/server/routes/api/vm/functions.js
Normal file
65
src/fireedge/src/server/routes/api/vm/functions.js
Normal file
@ -0,0 +1,65 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const {
|
||||
defaultEmptyFunction,
|
||||
defaultCommandVM
|
||||
} = require('server/utils/constants/defaults')
|
||||
|
||||
const {
|
||||
ok,
|
||||
internalServerError,
|
||||
badRequest
|
||||
} = require('server/utils/constants/http-codes')
|
||||
const { httpResponse, executeCommand } = require('server/utils/server')
|
||||
|
||||
const { getSunstoneConfig } = require('server/utils/yml')
|
||||
|
||||
const httpBadRequest = httpResponse(badRequest, '', '')
|
||||
const appConfig = getSunstoneConfig()
|
||||
const prependCommand = appConfig.sunstone_prepend || ''
|
||||
const regexpSplitLine = /\r|\n/
|
||||
|
||||
/**
|
||||
* Save as template.
|
||||
*
|
||||
* @param {object} res - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {object} userData - user of http request
|
||||
*/
|
||||
const saveAsTemplate = (res = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
|
||||
let rtn = httpBadRequest
|
||||
if (params && params.id && params.name) {
|
||||
const paramsCommand = ['save', `${params.id}`, `${params.name}`]
|
||||
|
||||
const executedCommand = executeCommand(defaultCommandVM, paramsCommand, prependCommand)
|
||||
|
||||
const response = executedCommand.success ? ok : internalServerError
|
||||
let message = ''
|
||||
if (executedCommand.data) {
|
||||
message = executedCommand.data.replace(regexpSplitLine, '')
|
||||
}
|
||||
rtn = httpResponse(response, message)
|
||||
}
|
||||
res.locals.httpCode = rtn
|
||||
next()
|
||||
}
|
||||
|
||||
const functionRoutes = {
|
||||
saveAsTemplate
|
||||
}
|
||||
module.exports = functionRoutes
|
60
src/fireedge/src/server/routes/api/vm/index.js
Normal file
60
src/fireedge/src/server/routes/api/vm/index.js
Normal file
@ -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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const { addFunctionAsRoute, setFunctionRoute } = require('server/utils/server')
|
||||
const { routes: vmRoutes } = require('./vm')
|
||||
const { VM } = require('./string-routes')
|
||||
|
||||
const privateRoutes = []
|
||||
const publicRoutes = []
|
||||
|
||||
/**
|
||||
* Set private routes.
|
||||
*
|
||||
* @param {object} routes - object of routes
|
||||
* @param {string} path - principal route
|
||||
* @param {Function} action - function of route
|
||||
*/
|
||||
const setPrivateRoutes = (routes = {}, path = '', action = () => undefined) => {
|
||||
if (Object.keys(routes).length > 0 && routes.constructor === Object) {
|
||||
Object.keys(routes).forEach((route) => {
|
||||
privateRoutes.push(
|
||||
setFunctionRoute(route, path,
|
||||
(req, res, next, connection, userId, user) => {
|
||||
action(req, res, next, routes[route], user, connection)
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add routes.
|
||||
*
|
||||
* @returns {Array} routes
|
||||
*/
|
||||
const generatePrivateRoutes = () => {
|
||||
setPrivateRoutes(vmRoutes, VM, addFunctionAsRoute)
|
||||
return privateRoutes
|
||||
}
|
||||
|
||||
const functionRoutes = {
|
||||
private: generatePrivateRoutes(),
|
||||
public: publicRoutes
|
||||
}
|
||||
|
||||
module.exports = functionRoutes
|
23
src/fireedge/src/server/routes/api/vm/string-routes.js
Normal file
23
src/fireedge/src/server/routes/api/vm/string-routes.js
Normal file
@ -0,0 +1,23 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const VM = 'vm'
|
||||
|
||||
const Actions = {
|
||||
VM
|
||||
}
|
||||
|
||||
module.exports = Actions
|
42
src/fireedge/src/server/routes/api/vm/vm.js
Normal file
42
src/fireedge/src/server/routes/api/vm/vm.js
Normal file
@ -0,0 +1,42 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const { httpMethod, from: fromData } = require('server/utils/constants/defaults')
|
||||
const { saveAsTemplate } = require('./functions')
|
||||
const { POST } = httpMethod
|
||||
|
||||
const routes = {
|
||||
[POST]: {
|
||||
save: {
|
||||
action: saveAsTemplate,
|
||||
params: {
|
||||
id: {
|
||||
from: fromData.resource,
|
||||
name: 'id'
|
||||
},
|
||||
name: {
|
||||
from: fromData.postBody,
|
||||
name: 'name'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const authApi = {
|
||||
routes
|
||||
}
|
||||
module.exports = authApi
|
48
src/fireedge/src/server/routes/api/zendesk/functions.js
Normal file
48
src/fireedge/src/server/routes/api/zendesk/functions.js
Normal file
@ -0,0 +1,48 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
const { global } = require('window-or-global')
|
||||
|
||||
/**
|
||||
* Get data fireedge session.
|
||||
*
|
||||
* @param {string} username - username
|
||||
* @param {string} token - pass
|
||||
* @returns {object} user session
|
||||
*/
|
||||
const getSession = (username = '', token = '') => {
|
||||
if (
|
||||
username &&
|
||||
token &&
|
||||
global &&
|
||||
global.users &&
|
||||
username &&
|
||||
global.users[username] &&
|
||||
global.users[username].tokens
|
||||
) {
|
||||
const session = global.users[username].tokens.find(
|
||||
(curr = {}, index = 0) => {
|
||||
return curr.token && curr.token === token
|
||||
}
|
||||
)
|
||||
return session
|
||||
}
|
||||
}
|
||||
|
||||
const functions = {
|
||||
getSession
|
||||
}
|
||||
|
||||
module.exports = functions
|
@ -14,12 +14,46 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const privateRoutes = []
|
||||
const { addFunctionAsRoute, setFunctionRoute } = require('server/utils/server')
|
||||
const { routes: zendeskRoutes } = require('./zendesk')
|
||||
const { ZENDESK } = require('./string-routes')
|
||||
|
||||
const privateRoutes = []
|
||||
const publicRoutes = []
|
||||
|
||||
/**
|
||||
* Set private routes.
|
||||
*
|
||||
* @param {object} routes - object of routes
|
||||
* @param {string} path - principal route
|
||||
* @param {Function} action - function of route
|
||||
*/
|
||||
const setPrivateRoutes = (routes = {}, path = '', action = () => undefined) => {
|
||||
if (Object.keys(routes).length > 0 && routes.constructor === Object) {
|
||||
Object.keys(routes).forEach((route) => {
|
||||
privateRoutes.push(
|
||||
setFunctionRoute(route, path,
|
||||
(req, res, next, connection, userId, user) => {
|
||||
action(req, res, next, routes[route], user, connection)
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add routes.
|
||||
*
|
||||
* @returns {Array} routes
|
||||
*/
|
||||
const generatePrivateRoutes = () => {
|
||||
setPrivateRoutes(zendeskRoutes, ZENDESK, addFunctionAsRoute)
|
||||
return privateRoutes
|
||||
}
|
||||
|
||||
const functionRoutes = {
|
||||
private: privateRoutes,
|
||||
private: generatePrivateRoutes(),
|
||||
public: publicRoutes
|
||||
}
|
||||
|
||||
|
23
src/fireedge/src/server/routes/api/zendesk/string-routes.js
Normal file
23
src/fireedge/src/server/routes/api/zendesk/string-routes.js
Normal file
@ -0,0 +1,23 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const ZENDESK = 'zendesk'
|
||||
|
||||
const Actions = {
|
||||
ZENDESK
|
||||
}
|
||||
|
||||
module.exports = Actions
|
407
src/fireedge/src/server/routes/api/zendesk/zendesk-functions.js
Normal file
407
src/fireedge/src/server/routes/api/zendesk/zendesk-functions.js
Normal file
@ -0,0 +1,407 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const { env } = require('process')
|
||||
const zendesk = require('node-zendesk')
|
||||
const { getSunstoneConfig } = require('server/utils/yml')
|
||||
const { defaultEmptyFunction, defaultSeverities, defaultWebpackMode } = require('server/utils/constants/defaults')
|
||||
const { httpResponse } = require('server/utils/server')
|
||||
const { getSession } = require('./functions')
|
||||
|
||||
const {
|
||||
ok,
|
||||
internalServerError,
|
||||
badRequest,
|
||||
unauthorized
|
||||
} = require('server/utils/constants/http-codes')
|
||||
|
||||
const formatCreate = (
|
||||
{
|
||||
subject = '',
|
||||
body = '',
|
||||
version = '',
|
||||
severity = ''
|
||||
}
|
||||
) => {
|
||||
let rtn
|
||||
if (subject && body && version && severity) {
|
||||
rtn = {
|
||||
request: {
|
||||
subject,
|
||||
comment: {
|
||||
body
|
||||
},
|
||||
custom_fields: [
|
||||
{ id: 391130, value: version }, // version
|
||||
{ id: 391197, value: severity } // severity
|
||||
],
|
||||
can_be_solved_by_me: false,
|
||||
tags: [severity]
|
||||
}
|
||||
}
|
||||
}
|
||||
return rtn
|
||||
}
|
||||
|
||||
const formatComment = ({ body = '', solved = '', attachments = [] }) => {
|
||||
let rtn
|
||||
if (body) {
|
||||
rtn = {
|
||||
request: {
|
||||
comment: {
|
||||
body,
|
||||
public: true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (solved) {
|
||||
rtn.solved = 'true'
|
||||
}
|
||||
if (attachments && Array.isArray(attachments) && attachments.length > 0) {
|
||||
rtn.request.comment.uploads = attachments.filter(att => att)
|
||||
}
|
||||
}
|
||||
return rtn
|
||||
}
|
||||
|
||||
const parseBufferError = (err) => {
|
||||
let rtn = ''
|
||||
let errorJson = {}
|
||||
if (err && err.result) {
|
||||
try {
|
||||
errorJson = JSON.parse(
|
||||
err.result.toString()
|
||||
)
|
||||
} catch {}
|
||||
if (errorJson && errorJson.error) {
|
||||
rtn = errorJson.error.title ? `${errorJson.error.title}: ` : ''
|
||||
rtn += errorJson.error.message ? errorJson.error.message : ''
|
||||
}
|
||||
}
|
||||
return rtn
|
||||
}
|
||||
|
||||
const httpBadRequest = httpResponse(badRequest, '', '')
|
||||
|
||||
/**
|
||||
* Login on Zendesk.
|
||||
*
|
||||
* @param {object} response - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {object} userData - user of http request
|
||||
*/
|
||||
const login = (response = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
|
||||
const sunstoneConfig = getSunstoneConfig()
|
||||
const remoteUri = sunstoneConfig.support_url || ''
|
||||
|
||||
if (
|
||||
remoteUri &&
|
||||
params &&
|
||||
params.user &&
|
||||
params.pass &&
|
||||
userData &&
|
||||
userData.user &&
|
||||
userData.password
|
||||
) {
|
||||
const zendeskData = {
|
||||
username: params.user,
|
||||
password: params.pass,
|
||||
remoteUri,
|
||||
debug: env.NODE_ENV === defaultWebpackMode
|
||||
}
|
||||
const session = getSession(userData.user, userData.password)
|
||||
/** ZENDESK AUTH */
|
||||
const zendeskClient = zendesk.createClient(zendeskData)
|
||||
zendeskClient.users.auth((err, res, result) => {
|
||||
let method = ok
|
||||
let data = result
|
||||
if (err) {
|
||||
if (session.zendesk) {
|
||||
delete session.zendesk
|
||||
}
|
||||
method = internalServerError
|
||||
data = parseBufferError(err)
|
||||
}
|
||||
if (result && result.authenticity_token) {
|
||||
const zendeskUserData = {
|
||||
...zendeskData,
|
||||
id: result.id
|
||||
}
|
||||
session.zendesk = zendeskUserData
|
||||
}
|
||||
|
||||
response.locals.httpCode = httpResponse(method, data)
|
||||
next()
|
||||
})
|
||||
} else {
|
||||
response.locals.httpCode = httpBadRequest
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List on Zendesk.
|
||||
*
|
||||
* @param {object} response - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {object} userData - user of http request
|
||||
*/
|
||||
const list = (response = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
|
||||
if (
|
||||
userData &&
|
||||
userData.user &&
|
||||
userData.password
|
||||
) {
|
||||
const session = getSession(userData.user, userData.password)
|
||||
if (session.zendesk && session.zendesk.id) {
|
||||
/** LIST ZENDESK */
|
||||
const zendeskClient = zendesk.createClient(session.zendesk)
|
||||
zendeskClient.requests.getRequest(
|
||||
{ status: 'open,pending' },
|
||||
(err, req, result) => {
|
||||
let method = ok
|
||||
let data = ''
|
||||
|
||||
if (err) {
|
||||
method = internalServerError
|
||||
data = parseBufferError(err)
|
||||
} else if (result) {
|
||||
let pendings = 0
|
||||
let opens = 0
|
||||
const tickets = Array.isArray(result) ? result : result
|
||||
tickets.forEach(ticket => {
|
||||
if (ticket && ticket.status) {
|
||||
switch (ticket.status) {
|
||||
case 'pending':
|
||||
pendings += 1
|
||||
break
|
||||
default:
|
||||
opens += 1
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
data = {
|
||||
tickets: result,
|
||||
pendings,
|
||||
opens
|
||||
}
|
||||
}
|
||||
|
||||
response.locals.httpCode = httpResponse(
|
||||
method,
|
||||
data
|
||||
)
|
||||
next()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
response.locals.httpCode = httpResponse(unauthorized)
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
response.locals.httpCode = httpBadRequest
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Comments on ticket.
|
||||
*
|
||||
* @param {object} response - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {object} userData - user of http request
|
||||
*/
|
||||
const comments = (response = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
|
||||
if (
|
||||
params.id &&
|
||||
userData &&
|
||||
userData.user &&
|
||||
userData.password
|
||||
) {
|
||||
const session = getSession(userData.user, userData.password)
|
||||
if (session.zendesk) {
|
||||
/** GET COMMENTS ON TICKET ZENDESK */
|
||||
const zendeskClient = zendesk.createClient(session.zendesk)
|
||||
zendeskClient.requests.listComments(
|
||||
params.id,
|
||||
(err, req, result) => {
|
||||
let method = ok
|
||||
let data = ''
|
||||
|
||||
if (err) {
|
||||
method = internalServerError
|
||||
data = parseBufferError(err)
|
||||
} else if (result) {
|
||||
data = result
|
||||
}
|
||||
|
||||
response.locals.httpCode = httpResponse(method, data)
|
||||
next()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
response.locals.httpCode = httpResponse(unauthorized)
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
response.locals.httpCode = httpBadRequest
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ticket.
|
||||
*
|
||||
* @param {object} response - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {object} userData - user of http request
|
||||
*/
|
||||
const create = (response = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
|
||||
if (
|
||||
params &&
|
||||
params.subject &&
|
||||
params.body &&
|
||||
params.version &&
|
||||
params.severity &&
|
||||
defaultSeverities.includes(params.severity) &&
|
||||
userData &&
|
||||
userData.user &&
|
||||
userData.password
|
||||
) {
|
||||
const session = getSession(userData.user, userData.password)
|
||||
if (session.zendesk && session.zendesk.id) {
|
||||
/** CREATE TICKET ZENDESK */
|
||||
const zendeskClient = zendesk.createClient(session.zendesk)
|
||||
const ticket = formatCreate(params)
|
||||
zendeskClient.requests.create(
|
||||
ticket,
|
||||
(err, req, result) => {
|
||||
let method = ok
|
||||
let data = ''
|
||||
if (err) {
|
||||
method = internalServerError
|
||||
data = parseBufferError(err)
|
||||
} else if (result) {
|
||||
data = result
|
||||
}
|
||||
response.locals.httpCode = httpResponse(method, data)
|
||||
next()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
response.locals.httpCode = httpResponse(unauthorized)
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
response.locals.httpCode = httpBadRequest
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Ticket.
|
||||
*
|
||||
* @param {object} response - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {object} userData - user of http request
|
||||
*/
|
||||
const update = (response = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
|
||||
if (
|
||||
params.id &&
|
||||
params.body &&
|
||||
userData &&
|
||||
userData.user &&
|
||||
userData.password
|
||||
) {
|
||||
const session = getSession(userData.user, userData.password)
|
||||
if (session.zendesk && session.zendesk.id) {
|
||||
const zendeskClient = zendesk.createClient(session.zendesk)
|
||||
|
||||
const sendRequest = (params = {}) => {
|
||||
/** UPDATE TICKET ZENDESK */
|
||||
const ticket = formatComment(params)
|
||||
zendeskClient.requests.update(
|
||||
params.id,
|
||||
ticket,
|
||||
(err, req, result) => {
|
||||
let method = ok
|
||||
let data = ''
|
||||
|
||||
if (err) {
|
||||
method = internalServerError
|
||||
data = parseBufferError(err)
|
||||
} else if (result) {
|
||||
data = result
|
||||
}
|
||||
response.locals.httpCode = httpResponse(method, data)
|
||||
next()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** UPLOAD FILES */
|
||||
let attachments
|
||||
if (params && params.attachments && zendeskClient.attachments && typeof zendeskClient.attachments.upload === 'function') {
|
||||
params.attachments.forEach((att = {}) => {
|
||||
if (att && att.originalname && att.path) {
|
||||
zendeskClient.attachments.upload(
|
||||
att.path,
|
||||
{
|
||||
filename: att.originalname
|
||||
},
|
||||
(err, req, result) => {
|
||||
const token = (result && result.upload && result.upload.token) || ''
|
||||
if (attachments) {
|
||||
attachments.push(token)
|
||||
} else {
|
||||
attachments = [token]
|
||||
}
|
||||
if (!err && token) {
|
||||
if (attachments && attachments.length === params.attachments.length) {
|
||||
sendRequest({ ...params, attachments })
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
sendRequest({ ...params, attachments })
|
||||
}
|
||||
} else {
|
||||
response.locals.httpCode = httpResponse(unauthorized)
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
response.locals.httpCode = httpBadRequest
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
const functionRoutes = {
|
||||
login,
|
||||
list,
|
||||
comments,
|
||||
create,
|
||||
update
|
||||
}
|
||||
module.exports = functionRoutes
|
101
src/fireedge/src/server/routes/api/zendesk/zendesk.js
Normal file
101
src/fireedge/src/server/routes/api/zendesk/zendesk.js
Normal file
@ -0,0 +1,101 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const { httpMethod, from: fromData } = require('server/utils/constants/defaults')
|
||||
const { login, list, comments, create, update } = require('./zendesk-functions')
|
||||
const { POST, GET, PUT } = httpMethod
|
||||
|
||||
const routes = {
|
||||
[POST]: {
|
||||
login: {
|
||||
action: login,
|
||||
params: {
|
||||
user: {
|
||||
from: fromData.postBody,
|
||||
name: 'user'
|
||||
},
|
||||
pass: {
|
||||
from: fromData.postBody,
|
||||
name: 'pass'
|
||||
}
|
||||
}
|
||||
},
|
||||
create: {
|
||||
action: create,
|
||||
params: {
|
||||
subject: {
|
||||
from: fromData.postBody,
|
||||
name: 'subject'
|
||||
},
|
||||
body: {
|
||||
from: fromData.postBody,
|
||||
name: 'body'
|
||||
},
|
||||
version: {
|
||||
from: fromData.postBody,
|
||||
name: 'version'
|
||||
},
|
||||
severity: {
|
||||
from: fromData.postBody,
|
||||
name: 'severity'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[PUT]: {
|
||||
update: {
|
||||
action: update,
|
||||
params: {
|
||||
id: {
|
||||
from: fromData.resource,
|
||||
name: 'id'
|
||||
},
|
||||
body: {
|
||||
from: fromData.postBody,
|
||||
name: 'body'
|
||||
},
|
||||
solved: {
|
||||
from: fromData.postBody,
|
||||
name: 'solved'
|
||||
},
|
||||
attachments: {
|
||||
from: 'files',
|
||||
name: 'attachments'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[GET]: {
|
||||
list: {
|
||||
action: list,
|
||||
params: {}
|
||||
},
|
||||
comments: {
|
||||
action: comments,
|
||||
params: {
|
||||
id: {
|
||||
from: fromData.resource,
|
||||
name: 'id'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const authApi = {
|
||||
routes
|
||||
}
|
||||
module.exports = authApi
|
@ -124,10 +124,11 @@ router.all(
|
||||
routeFunction,
|
||||
httpMethod
|
||||
)
|
||||
req.serverDataSource = dataSources
|
||||
if (valRouteFunction) {
|
||||
const userIdOpennebula = getIdUserOpennebula()
|
||||
valRouteFunction(
|
||||
dataSources,
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
connectOpennebula,
|
||||
|
@ -21,6 +21,7 @@ const internalSunstonePath = `${appName}/${appNameSunstone}`
|
||||
const internalProvisionPath = `${appName}/${appNameProvision}`
|
||||
const baseUrl = `${appName ? `/${appName}/` : '/'}`
|
||||
const baseUrlWebsockets = 'websockets/'
|
||||
const severityPrepend = 'severity_'
|
||||
const apps = {
|
||||
[appNameSunstone]: {
|
||||
theme: appNameSunstone,
|
||||
@ -68,6 +69,7 @@ const defaults = {
|
||||
'oneflow',
|
||||
'support',
|
||||
'vcenter',
|
||||
'vm',
|
||||
'zendesk',
|
||||
appNameProvision,
|
||||
appNameSunstone
|
||||
@ -111,6 +113,7 @@ const defaults = {
|
||||
defaultCommandProvisionTemplate: 'oneprovision-template',
|
||||
defaultCommandProvider: 'oneprovider',
|
||||
defaultCommandVcenter: 'onevcenter',
|
||||
defaultCommandVM: 'onevm',
|
||||
defaultFolderTmpProvision: 'tmp',
|
||||
defaultHideCredentials: true,
|
||||
defaultHideCredentialReplacer: '****',
|
||||
@ -145,6 +148,7 @@ const defaults = {
|
||||
defaultGetMethod: 'info',
|
||||
defaultMessageProblemOpennebula: 'Problem with connection or xml parser',
|
||||
defaultIP: defaultIp,
|
||||
defaultSeverities: [`${severityPrepend}1`, `${severityPrepend}2`, `${severityPrepend}3`, `${severityPrepend}4`],
|
||||
defaultProtocolHotReload: 'http',
|
||||
defaultHost: '0.0.0.0',
|
||||
defaultPort: 2616,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user