mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-22 18:50:08 +03:00
F OpenNebula/one#5833: Add oneflow tabs to 6.8 (#2927)
Signed-off-by: Victor Hansson <vhansson@opennebula.io>
This commit is contained in:
parent
8fd860a4eb
commit
f5c2da57ba
@ -104,8 +104,17 @@ const ServiceTemplates = loadable(
|
||||
() => import('client/containers/ServiceTemplates'),
|
||||
{ ssr: false }
|
||||
)
|
||||
// const DeployServiceTemplates = loadable(() => import('client/containers/ServiceTemplates/Instantiate'), { ssr: false })
|
||||
// const CreateServiceTemplates = loadable(() => import('client/containers/ServiceTemplates/Create'), { ssr: false })
|
||||
|
||||
const InstantiateServiceTemplate = loadable(
|
||||
() => import('client/containers/ServiceTemplates/Instantiate'),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
const CreateServiceTemplates = loadable(
|
||||
() => import('client/containers/ServiceTemplates/Create'),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
const ServiceTemplateDetail = loadable(
|
||||
() => import('client/containers/ServiceTemplates/Detail'),
|
||||
{ ssr: false }
|
||||
@ -314,7 +323,7 @@ export const PATH = {
|
||||
SERVICES: {
|
||||
LIST: `/${RESOURCE_NAMES.SERVICE_TEMPLATE}`,
|
||||
DETAIL: `/${RESOURCE_NAMES.SERVICE_TEMPLATE}/:id`,
|
||||
DEPLOY: `/${RESOURCE_NAMES.SERVICE_TEMPLATE}/deploy/`,
|
||||
INSTANTIATE: `/${RESOURCE_NAMES.SERVICE_TEMPLATE}/instantiate/`,
|
||||
CREATE: `/${RESOURCE_NAMES.SERVICE_TEMPLATE}/create`,
|
||||
},
|
||||
},
|
||||
@ -485,6 +494,13 @@ const ENDPOINTS = [
|
||||
path: PATH.TEMPLATE.VMS.DETAIL,
|
||||
Component: VMTemplateDetail,
|
||||
},
|
||||
{
|
||||
title: T.InstantiateServiceTemplate,
|
||||
description: (_, state) =>
|
||||
state?.ID !== undefined && `#${state.ID} ${state.NAME}`,
|
||||
path: PATH.TEMPLATE.SERVICES.INSTANTIATE,
|
||||
Component: InstantiateServiceTemplate,
|
||||
},
|
||||
{
|
||||
title: T.ServiceTemplates,
|
||||
path: PATH.TEMPLATE.SERVICES.LIST,
|
||||
@ -492,13 +508,6 @@ const ENDPOINTS = [
|
||||
icon: ServiceTemplateIcon,
|
||||
Component: ServiceTemplates,
|
||||
},
|
||||
/* {
|
||||
title: T.DeployServiceTemplate,
|
||||
description: (_, state) =>
|
||||
state?.ID !== undefined && `#${state.ID} ${state.NAME}`,
|
||||
path: PATH.TEMPLATE.SERVICES.DEPLOY,
|
||||
Component: DeployServiceTemplates,
|
||||
},
|
||||
{
|
||||
title: (_, state) =>
|
||||
state?.ID !== undefined
|
||||
@ -508,7 +517,7 @@ const ENDPOINTS = [
|
||||
state?.ID !== undefined && `#${state.ID} ${state.NAME}`,
|
||||
path: PATH.TEMPLATE.SERVICES.CREATE,
|
||||
Component: CreateServiceTemplates,
|
||||
}, */
|
||||
},
|
||||
{
|
||||
title: T.ServiceTemplate,
|
||||
description: (params) => `#${params?.id}`,
|
||||
|
@ -15,59 +15,35 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { WarningCircledOutline as WarningIcon } from 'iconoir-react'
|
||||
import { Typography } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
import Timer from 'client/components/Timer'
|
||||
import { StatusCircle } from 'client/components/Status'
|
||||
import { rowStyles } from 'client/components/Tables/styles'
|
||||
|
||||
import {
|
||||
timeFromMilliseconds,
|
||||
getUniqueLabels,
|
||||
getColorFromString,
|
||||
} from 'client/models/Helper'
|
||||
import { timeFromMilliseconds } from 'client/models/Helper'
|
||||
import { getState } from 'client/models/Service'
|
||||
import { T, Service, ACTIONS, RESOURCE_NAMES } from 'client/constants'
|
||||
import { T, Service } from 'client/constants'
|
||||
|
||||
const ServiceCard = memo(
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {Service} props.service - Service resource
|
||||
* @param {object} props.rootProps - Props to root component
|
||||
* @param {function(string):Promise} [props.onDeleteLabel] - Callback to delete label
|
||||
* @param {ReactElement} [props.actions] - Actions
|
||||
* @returns {ReactElement} - Card
|
||||
*/
|
||||
({ service, rootProps, actions, onDeleteLabel }) => {
|
||||
({ service, rootProps, actions }) => {
|
||||
const classes = rowStyles()
|
||||
const { [RESOURCE_NAMES.SERVICE]: serviceView } = useViews()
|
||||
|
||||
const enableEditLabels =
|
||||
serviceView?.actions?.[ACTIONS.EDIT_LABELS] === true && !!onDeleteLabel
|
||||
|
||||
const {
|
||||
ID,
|
||||
NAME,
|
||||
TEMPLATE: { BODY: { description, labels, start_time: startTime } = {} },
|
||||
TEMPLATE: { BODY: { description, start_time: startTime } = {} },
|
||||
} = service
|
||||
|
||||
const { color: stateColor, name: stateName } = getState(service)
|
||||
const time = useMemo(() => timeFromMilliseconds(+startTime), [startTime])
|
||||
|
||||
const uniqueLabels = useMemo(
|
||||
() =>
|
||||
getUniqueLabels(labels).map((label) => ({
|
||||
text: label,
|
||||
stateColor: getColorFromString(label),
|
||||
onDelete: enableEditLabels && onDeleteLabel,
|
||||
})),
|
||||
[labels, enableEditLabels, onDeleteLabel]
|
||||
)
|
||||
|
||||
return (
|
||||
<div {...rootProps} data-cy={`service-template-${ID}`}>
|
||||
<div className={classes.main}>
|
||||
@ -76,10 +52,6 @@ const ServiceCard = memo(
|
||||
<Typography noWrap component="span" title={description}>
|
||||
{NAME}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<WarningIcon title={description} />
|
||||
<MultipleTags tags={uniqueLabels} />
|
||||
</span>
|
||||
</div>
|
||||
<div className={classes.caption}>
|
||||
<span data-cy="id">{`#${ID}`}</span>
|
||||
|
@ -54,9 +54,9 @@ const ServiceTemplateCard = memo(
|
||||
TEMPLATE: {
|
||||
BODY: {
|
||||
description,
|
||||
labels,
|
||||
networks,
|
||||
roles,
|
||||
labels = {},
|
||||
networks = {},
|
||||
roles = {},
|
||||
registration_time: regTime,
|
||||
} = {},
|
||||
},
|
||||
|
@ -35,6 +35,7 @@ const SelectController = memo(
|
||||
tooltip,
|
||||
watcher,
|
||||
dependencies,
|
||||
defaultValueProp,
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
onConditionChange,
|
||||
@ -45,8 +46,18 @@ const SelectController = memo(
|
||||
defaultValue: Array.isArray(dependencies) ? [] : undefined,
|
||||
})
|
||||
|
||||
const firstValue = values?.[0]?.value ?? ''
|
||||
const defaultValue = multiple ? [firstValue] : firstValue
|
||||
const firstValue = defaultValueProp
|
||||
? values?.find((val) => val.value === defaultValueProp)
|
||||
: values?.[0]?.value ?? ''
|
||||
|
||||
const defaultValue =
|
||||
defaultValueProp !== undefined
|
||||
? multiple
|
||||
? [defaultValueProp]
|
||||
: defaultValueProp
|
||||
: multiple
|
||||
? [firstValue]
|
||||
: firstValue
|
||||
|
||||
const {
|
||||
field: { ref, value: optionSelected, onChange, ...inputProps },
|
||||
@ -55,7 +66,8 @@ const SelectController = memo(
|
||||
|
||||
const needShrink = useMemo(
|
||||
() =>
|
||||
multiple || values.find((o) => o.value === optionSelected)?.text !== '',
|
||||
multiple ||
|
||||
values?.find((o) => o.value === optionSelected)?.text !== '',
|
||||
[optionSelected]
|
||||
)
|
||||
|
||||
@ -167,6 +179,7 @@ SelectController.propTypes = {
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
defaultValueProp: PropTypes.string,
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
onConditionChange: PropTypes.func,
|
||||
|
@ -68,8 +68,13 @@ const TableController = memo(
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
onChange(singleSelect ? undefined : preserveState ? value : [])
|
||||
setInitialRows(preserveState ? initialRows : {})
|
||||
if (preserveState) {
|
||||
onChange(value)
|
||||
setInitialRows(initialRows)
|
||||
} else {
|
||||
onChange(singleSelect ? undefined : [])
|
||||
setInitialRows({})
|
||||
}
|
||||
}, [Table])
|
||||
|
||||
const handleSelectedRowsChange = useCallback(
|
||||
|
@ -218,10 +218,15 @@ const FieldComponent = memo(
|
||||
)
|
||||
const inputName = useMemo(() => addIdToName(name), [addIdToName, name])
|
||||
const isHidden = useMemo(() => htmlType === INPUT_TYPES.HIDDEN, [htmlType])
|
||||
// Key is computed in first hand based on it's type, meaning we re-render if type changes.
|
||||
const key = useMemo(
|
||||
() =>
|
||||
fieldProps?.values
|
||||
? `${name}-${JSON.stringify(fieldProps.values)}`
|
||||
fieldProps
|
||||
? `${name}-${JSON.stringify(
|
||||
fieldProps?.type ??
|
||||
fieldProps?.values ??
|
||||
Object.values(fieldProps)
|
||||
)}`
|
||||
: undefined,
|
||||
[fieldProps]
|
||||
)
|
||||
|
@ -0,0 +1,64 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { useForm, FormProvider } from 'react-hook-form'
|
||||
import { useMemo, memo } from 'react'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import {
|
||||
ADVANCED_PARAMS_FIELDS,
|
||||
ADVANCED_PARAMS_SCHEMA,
|
||||
} from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Extra/advancedParams/schema'
|
||||
import { FormWithSchema, Legend } from 'client/components/Forms'
|
||||
import { Stack, Divider, FormControl } from '@mui/material'
|
||||
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const SECTION_ID = 'ADVANCED'
|
||||
|
||||
const AdvancedParamsSection = () => {
|
||||
const fields = useMemo(() => ADVANCED_PARAMS_FIELDS, [])
|
||||
|
||||
const { handleSubmit, ...methods } = useForm({
|
||||
defaultValues: ADVANCED_PARAMS_SCHEMA?.default(),
|
||||
mode: 'all',
|
||||
resolver: yupResolver(ADVANCED_PARAMS_SCHEMA),
|
||||
})
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ width: '100%', gridColumn: '1 / -1' }}
|
||||
>
|
||||
<Legend title={T.AdvancedParams} />
|
||||
<FormProvider {...methods}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
gap="0.5rem"
|
||||
component="form"
|
||||
>
|
||||
<FormWithSchema
|
||||
cy={SECTION_ID}
|
||||
fields={fields}
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
/>
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
<Divider />
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AdvancedParamsSection)
|
@ -0,0 +1,87 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { INPUT_TYPES, T } from 'client/constants'
|
||||
import { getObjectSchemaFromFields, arrayToOptions } from 'client/utils'
|
||||
|
||||
// Define the CA types
|
||||
const STRATEGY_TYPES = {
|
||||
straight: 'Straight',
|
||||
none: 'None',
|
||||
}
|
||||
|
||||
const VM_SHUTDOWN_TYPES = {
|
||||
terminate: 'Terminate',
|
||||
terminateHard: 'Terminate Hard',
|
||||
}
|
||||
|
||||
const STRATEGY_TYPE = {
|
||||
label: 'Strategy',
|
||||
name: 'ADVANCED.DEPLOYMENT',
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(Object.keys(STRATEGY_TYPES), {
|
||||
addEmpty: false,
|
||||
getText: (key) => STRATEGY_TYPES[key],
|
||||
getValue: (key) => key,
|
||||
}),
|
||||
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.oneOf(Object.keys(STRATEGY_TYPES))
|
||||
.default(() => Object.keys(STRATEGY_TYPES)[0]),
|
||||
grid: { sm: 2, md: 2 },
|
||||
}
|
||||
|
||||
const VM_SHUTDOWN_TYPE = {
|
||||
label: 'VM Shutdown',
|
||||
name: 'ADVANCED.VMSHUTDOWN',
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(Object.values(VM_SHUTDOWN_TYPES), { addEmpty: false }),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.oneOf(Object.values(VM_SHUTDOWN_TYPES))
|
||||
.default(() => Object.values(VM_SHUTDOWN_TYPES)[0]),
|
||||
grid: { sm: 2, md: 2 },
|
||||
}
|
||||
|
||||
const WAIT_VMS = {
|
||||
label: T.WaitVmsReport,
|
||||
name: 'ADVANCED.READY_STATUS_GATE',
|
||||
type: INPUT_TYPES.CHECKBOX,
|
||||
validation: boolean().default(() => false),
|
||||
grid: { sd: 4, md: 4 },
|
||||
}
|
||||
|
||||
const AUTO_DELETE = {
|
||||
label: T.ServiceAutoDelete,
|
||||
name: 'ADVANCED.AUTOMATIC_DELETION',
|
||||
type: INPUT_TYPES.CHECKBOX,
|
||||
validation: boolean().default(() => false),
|
||||
grid: { sd: 4, md: 4 },
|
||||
}
|
||||
|
||||
export const ADVANCED_PARAMS_FIELDS = [
|
||||
STRATEGY_TYPE,
|
||||
VM_SHUTDOWN_TYPE,
|
||||
WAIT_VMS,
|
||||
AUTO_DELETE,
|
||||
]
|
||||
|
||||
export const ADVANCED_PARAMS_SCHEMA = getObjectSchemaFromFields(
|
||||
ADVANCED_PARAMS_FIELDS
|
||||
)
|
@ -0,0 +1,148 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { Component, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useFieldArray, useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import {
|
||||
CUSTOM_ATTRIBUTES_FIELDS,
|
||||
CUSTOM_ATTRIBUTES_SCHEMA,
|
||||
} from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Extra/customAttributes/schema'
|
||||
import { FormWithSchema, Legend } from 'client/components/Forms'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { DeleteCircledOutline, AddCircledOutline } from 'iconoir-react'
|
||||
import { Stack, FormControl, Divider, Button, Box } from '@mui/material'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import SubmitButton from 'client/components/FormControl/SubmitButton'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const SECTION_ID = 'CUSTOM_ATTRIBUTES'
|
||||
|
||||
/**
|
||||
* @param {object} root0 - Params
|
||||
* @param {string} root0.stepId - Step identifier
|
||||
* @returns {Component} - Custom Attributes sub-step
|
||||
*/
|
||||
const CustomAttributesSection = ({ stepId }) => {
|
||||
const fields = CUSTOM_ATTRIBUTES_FIELDS
|
||||
|
||||
const {
|
||||
fields: customattributes,
|
||||
append,
|
||||
remove,
|
||||
} = useFieldArray({
|
||||
name: useMemo(
|
||||
() => [stepId, SECTION_ID].filter(Boolean).join('.'),
|
||||
[stepId]
|
||||
),
|
||||
})
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: CUSTOM_ATTRIBUTES_SCHEMA.default(),
|
||||
resolver: yupResolver(CUSTOM_ATTRIBUTES_SCHEMA),
|
||||
})
|
||||
|
||||
const onSubmit = (newcustomAttribute) => {
|
||||
append(newcustomAttribute)
|
||||
methods.reset()
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ width: '100%', gridColumn: '1 / -1' }}
|
||||
>
|
||||
<Legend title={T.UserInputs} />
|
||||
<FormProvider {...methods}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
gap="0.5rem"
|
||||
component="form"
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormWithSchema
|
||||
cy={'extra-customAttributes'}
|
||||
fields={fields}
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
type="submit"
|
||||
color="secondary"
|
||||
startIcon={<AddCircledOutline />}
|
||||
data-cy={'extra-customAttributes'}
|
||||
sx={{ mt: '1em' }}
|
||||
>
|
||||
<Translate word={T.Add} />
|
||||
</Button>
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
<Divider />
|
||||
<List>
|
||||
{customattributes?.map(
|
||||
({ id, name, defaultvalue, description, mandatory, type }, index) => {
|
||||
const secondaryFields = [
|
||||
description && `Description: ${description}`,
|
||||
defaultvalue && `Default value: ${defaultvalue}`,
|
||||
type && `Type: ${type}`,
|
||||
mandatory && `Mandatory: ${mandatory ? 'Yes' : 'No'}`,
|
||||
].filter(Boolean)
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={id}
|
||||
sx={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<SubmitButton
|
||||
onClick={() => remove(index)}
|
||||
icon={<DeleteCircledOutline />}
|
||||
/>
|
||||
}
|
||||
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
primaryTypographyProps={{ variant: 'body1' }}
|
||||
secondary={secondaryFields.join(' | ')}
|
||||
/>
|
||||
</ListItem>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</List>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
CustomAttributesSection.propTypes = {
|
||||
stepId: PropTypes.string,
|
||||
}
|
||||
|
||||
export default CustomAttributesSection
|
@ -0,0 +1,181 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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, number } from 'yup'
|
||||
import { getObjectSchemaFromFields, arrayToOptions } from 'client/utils'
|
||||
import { INPUT_TYPES, T } from 'client/constants'
|
||||
|
||||
const getTypeProp = (type) => {
|
||||
switch (type) {
|
||||
case CA_TYPES.boolean:
|
||||
return INPUT_TYPES.SWITCH
|
||||
case CA_TYPES.text:
|
||||
case CA_TYPES.text64:
|
||||
case CA_TYPES.number:
|
||||
case CA_TYPES.numberfloat:
|
||||
return INPUT_TYPES.TEXT
|
||||
default:
|
||||
return INPUT_TYPES.TEXT
|
||||
}
|
||||
}
|
||||
|
||||
const getFieldProps = (type) => {
|
||||
switch (type) {
|
||||
case CA_TYPES.text:
|
||||
case CA_TYPES.text64:
|
||||
return { type: 'text' }
|
||||
case CA_TYPES.number:
|
||||
case CA_TYPES.numberfloat:
|
||||
return { type: 'number' }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// Define the CA types
|
||||
const CA_TYPES = {
|
||||
text64: 'text64',
|
||||
password: 'password',
|
||||
number: 'number',
|
||||
numberfloat: 'number-float',
|
||||
range: 'range',
|
||||
rangefloat: 'range-float',
|
||||
list: 'list',
|
||||
listmultiple: 'list-multiple',
|
||||
boolean: 'boolean',
|
||||
text: 'text',
|
||||
}
|
||||
|
||||
const CA_TYPE = {
|
||||
name: 'type',
|
||||
label: 'Type',
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(Object.values(CA_TYPES), { addEmpty: false }),
|
||||
defaultValueProp: CA_TYPES.text,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.oneOf(Object.values(CA_TYPES))
|
||||
.default(() => CA_TYPES.text),
|
||||
grid: {
|
||||
sm: 1.5,
|
||||
md: 1.5,
|
||||
},
|
||||
}
|
||||
|
||||
const NAME = {
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 2.5, md: 2.5 },
|
||||
}
|
||||
|
||||
const DESCRIPTION = {
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 2.5, md: 2.5 },
|
||||
}
|
||||
|
||||
const DEFAULT_VALUE_TEXT = {
|
||||
name: 'defaultvalue',
|
||||
label: 'Default value',
|
||||
dependOf: CA_TYPE.name,
|
||||
|
||||
htmlType: (type) => type === CA_TYPES.password && INPUT_TYPES.HIDDEN,
|
||||
|
||||
type: getTypeProp,
|
||||
|
||||
fieldProps: getFieldProps,
|
||||
|
||||
validation: string(),
|
||||
|
||||
grid: { sm: 2.5, md: 2.5 },
|
||||
}
|
||||
|
||||
const DEFAULT_VALUE_RANGE_MIN = {
|
||||
name: 'defaultvaluerangemin',
|
||||
label: 'Min range',
|
||||
dependOf: CA_TYPE.name,
|
||||
|
||||
htmlType: (type) =>
|
||||
![CA_TYPES.range, CA_TYPES.rangefloat].includes(type) && INPUT_TYPES.HIDDEN,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
validation: number(),
|
||||
grid: { sm: 4, md: 4.5 },
|
||||
}
|
||||
|
||||
const DEFAULT_VALUE_RANGE_MAX = {
|
||||
name: 'defaultvaluerangemax',
|
||||
label: 'Max range',
|
||||
dependOf: CA_TYPE.name,
|
||||
htmlType: (type) =>
|
||||
![CA_TYPES.range, CA_TYPES.rangefloat].includes(type) && INPUT_TYPES.HIDDEN,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
validation: number(),
|
||||
grid: { sm: 4, md: 4.5 },
|
||||
}
|
||||
|
||||
const DEFAULT_VALUE_LIST = {
|
||||
name: 'defaultvaluelist',
|
||||
label: 'Comma separated list of options',
|
||||
dependOf: CA_TYPE.name,
|
||||
htmlType: (type) =>
|
||||
![CA_TYPES.listmultiple, CA_TYPES.list].includes(type) &&
|
||||
INPUT_TYPES.HIDDEN,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
fieldProps: {
|
||||
type: 'text',
|
||||
},
|
||||
validation: string(),
|
||||
grid: { sm: 9, md: 9 },
|
||||
}
|
||||
|
||||
const MANDATORY = {
|
||||
name: 'mandatory',
|
||||
label: T.Mandatory,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().yesOrNo(),
|
||||
grid: { sm: 2, md: 2 },
|
||||
}
|
||||
|
||||
export const CUSTOM_ATTRIBUTES_FIELDS = [
|
||||
CA_TYPE,
|
||||
NAME,
|
||||
DESCRIPTION,
|
||||
DEFAULT_VALUE_TEXT,
|
||||
MANDATORY,
|
||||
DEFAULT_VALUE_RANGE_MIN,
|
||||
DEFAULT_VALUE_RANGE_MAX,
|
||||
DEFAULT_VALUE_LIST,
|
||||
]
|
||||
|
||||
export const CUSTOM_ATTRIBUTES_SCHEMA = getObjectSchemaFromFields(
|
||||
CUSTOM_ATTRIBUTES_FIELDS
|
||||
)
|
@ -0,0 +1,79 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { Component, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { SCHEMA } from './schema'
|
||||
import { Stack, FormControl, Divider } from '@mui/material'
|
||||
import NetworkingSection from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Extra/networking'
|
||||
|
||||
import CustomAttributesSection from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Extra/customAttributes'
|
||||
|
||||
import ScheduleActionsSection from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Extra/scheduledActions'
|
||||
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
|
||||
import { T } from 'client/constants'
|
||||
import { ADVANCED_PARAMS_FIELDS } from './advancedParams/schema'
|
||||
|
||||
export const STEP_ID = 'extra'
|
||||
|
||||
const Content = () =>
|
||||
useMemo(
|
||||
() => (
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
|
||||
>
|
||||
<NetworkingSection stepId={STEP_ID} />
|
||||
<CustomAttributesSection stepId={STEP_ID} />
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ width: '100%', gridColumn: '1 / -1' }}
|
||||
>
|
||||
<FormWithSchema
|
||||
id={STEP_ID}
|
||||
legend={T.AdvancedParams}
|
||||
fields={ADVANCED_PARAMS_FIELDS}
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
/>
|
||||
<Divider />
|
||||
</FormControl>
|
||||
<ScheduleActionsSection />
|
||||
</Stack>
|
||||
),
|
||||
[STEP_ID]
|
||||
)
|
||||
|
||||
Content.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object,
|
||||
}
|
||||
|
||||
/**
|
||||
*@returns {Component} - Extra step
|
||||
*/
|
||||
const Extra = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.Extra,
|
||||
resolver: SCHEMA,
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content,
|
||||
})
|
||||
|
||||
export default Extra
|
@ -0,0 +1,151 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 PropTypes from 'prop-types'
|
||||
import { Component, useMemo } from 'react'
|
||||
import { useFieldArray, useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import {
|
||||
NETWORK_INPUT_FIELDS,
|
||||
NETWORK_INPUT_SCHEMA,
|
||||
} from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Extra/networking/schema'
|
||||
import { FormWithSchema, Legend } from 'client/components/Forms'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { DeleteCircledOutline, AddCircledOutline } from 'iconoir-react'
|
||||
import { Stack, FormControl, Divider, Button, Box } from '@mui/material'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import SubmitButton from 'client/components/FormControl/SubmitButton'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const SECTION_ID = 'NETWORKING'
|
||||
|
||||
/**
|
||||
* @param {object} root0 - Params
|
||||
* @param {string} root0.stepId - Step identifier
|
||||
* @returns {Component} - Networking sub-section
|
||||
*/
|
||||
const NetworkingSection = ({ stepId }) => {
|
||||
const fields = useMemo(() => NETWORK_INPUT_FIELDS)
|
||||
|
||||
const {
|
||||
fields: networks,
|
||||
append,
|
||||
remove,
|
||||
} = useFieldArray({
|
||||
name: useMemo(
|
||||
() => [stepId, SECTION_ID].filter(Boolean).join('.'),
|
||||
[stepId]
|
||||
),
|
||||
})
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: NETWORK_INPUT_SCHEMA.default(),
|
||||
resolver: yupResolver(NETWORK_INPUT_SCHEMA),
|
||||
})
|
||||
|
||||
const onSubmit = async (newNetwork) => {
|
||||
const isValid = await methods.trigger()
|
||||
if (isValid) {
|
||||
append(newNetwork)
|
||||
methods.reset()
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ width: '100%', gridColumn: '1 / -1' }}
|
||||
>
|
||||
<Legend title={T.Networks} />
|
||||
<FormProvider {...methods}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
gap="0.5rem"
|
||||
component="form"
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormWithSchema
|
||||
cy={'extra-networking'}
|
||||
fields={fields}
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
type="submit"
|
||||
color="secondary"
|
||||
startIcon={<AddCircledOutline />}
|
||||
data-cy={'extra-networking'}
|
||||
sx={{ mt: '1em' }}
|
||||
>
|
||||
<Translate word={T.Add} />
|
||||
</Button>
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
<Divider />
|
||||
<List>
|
||||
{networks?.map(
|
||||
({ name, description, netextra, id, network, type }, index) => {
|
||||
const secondaryFields = [
|
||||
description && `Description: ${description}`,
|
||||
type && `Type: ${type}`,
|
||||
network && `Network: ${network}`,
|
||||
netextra && `Extra: ${netextra}`,
|
||||
].filter(Boolean)
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={id}
|
||||
sx={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<SubmitButton
|
||||
onClick={() => remove(index)}
|
||||
icon={<DeleteCircledOutline />}
|
||||
/>
|
||||
}
|
||||
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
primaryTypographyProps={{ variant: 'body1' }}
|
||||
secondary={secondaryFields.join(' | ')}
|
||||
/>
|
||||
</ListItem>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</List>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
NetworkingSection.propTypes = {
|
||||
stepId: PropTypes.string,
|
||||
}
|
||||
|
||||
export default NetworkingSection
|
@ -0,0 +1,107 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string } from 'yup'
|
||||
import { getObjectSchemaFromFields, arrayToOptions } from 'client/utils'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
import { useGetVNetworksQuery } from 'client/features/OneApi/network'
|
||||
|
||||
// Define the network types
|
||||
export const NETWORK_TYPES = {
|
||||
create: 'Create',
|
||||
reserve: 'Reserve',
|
||||
existing: 'Existing',
|
||||
}
|
||||
|
||||
// Network Type Field
|
||||
const NETWORK_TYPE = {
|
||||
name: 'type',
|
||||
label: 'Type',
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(Object.values(NETWORK_TYPES), { addEmpty: false }),
|
||||
validation: string()
|
||||
.trim()
|
||||
.oneOf([...Object.keys(NETWORK_TYPES), ...Object.values(NETWORK_TYPES)])
|
||||
.default(() => Object.values(NETWORK_TYPES)[0]),
|
||||
grid: { sm: 1.5, md: 1.5 },
|
||||
}
|
||||
|
||||
// Network Name Field
|
||||
const NAME = {
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().required(),
|
||||
grid: { sm: 2.5, md: 2.5 },
|
||||
}
|
||||
|
||||
// Network Description Field
|
||||
const DESCRIPTION = {
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
grid: { sm: 2.5, md: 2.5 },
|
||||
}
|
||||
|
||||
// Network Selection Field (for 'reserve' or 'existing')
|
||||
const NETWORK_SELECTION = {
|
||||
name: 'network',
|
||||
label: 'Network',
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: () => {
|
||||
const { data: vnets = [] } = useGetVNetworksQuery()
|
||||
const networks = vnets
|
||||
.map((vnet) => ({ NAME: vnet?.NAME, ID: vnet?.ID }))
|
||||
.flat()
|
||||
|
||||
return arrayToOptions(networks, {
|
||||
getText: (network = '') => network?.NAME,
|
||||
getValue: (network) => network?.ID,
|
||||
})
|
||||
},
|
||||
dependOf: NETWORK_TYPE.name,
|
||||
validation: string().trim().notRequired(),
|
||||
grid: { sm: 2, md: 2 },
|
||||
}
|
||||
|
||||
// NetExtra Field
|
||||
const NETEXTRA = {
|
||||
name: 'netextra',
|
||||
label: 'Extra',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
dependOf: NETWORK_TYPE.name,
|
||||
validation: string().trim().when(NETWORK_TYPE.name, {
|
||||
is: 'existing',
|
||||
then: string().strip(),
|
||||
otherwise: string().notRequired(),
|
||||
}),
|
||||
grid: { sm: 2.5, md: 2.5 },
|
||||
}
|
||||
|
||||
// List of Network Input Fields
|
||||
export const NETWORK_INPUT_FIELDS = [
|
||||
NETWORK_TYPE,
|
||||
NAME,
|
||||
DESCRIPTION,
|
||||
NETWORK_SELECTION,
|
||||
NETEXTRA,
|
||||
]
|
||||
|
||||
export const NETWORK_INPUT_SCHEMA =
|
||||
getObjectSchemaFromFields(NETWORK_INPUT_FIELDS)
|
@ -0,0 +1,147 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { Box, Stack, Divider } from '@mui/material'
|
||||
import { useFieldArray } from 'react-hook-form'
|
||||
import { array, object } from 'yup'
|
||||
|
||||
import { ScheduleActionCard } from 'client/components/Cards'
|
||||
import {
|
||||
CreateSchedButton,
|
||||
CharterButton,
|
||||
UpdateSchedButton,
|
||||
DeleteSchedButton,
|
||||
} from 'client/components/Buttons/ScheduleAction'
|
||||
|
||||
import PropTypes from 'prop-types'
|
||||
import { T } from 'client/constants'
|
||||
import { Legend } from 'client/components/Forms'
|
||||
|
||||
import { mapNameByIndex } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Extra'
|
||||
import { Component } from 'react'
|
||||
|
||||
export const TAB_ID = 'SCHED_ACTION'
|
||||
|
||||
const mapNameFunction = mapNameByIndex('SCHED_ACTION')
|
||||
|
||||
export const SCHED_ACTION_SCHEMA = object({
|
||||
SCHED_ACTION: array()
|
||||
.ensure()
|
||||
.transform((actions) => actions.map(mapNameByIndex('SCHED_ACTION'))),
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {object} root0 - Props
|
||||
* @param {object} root0.oneConfig - One config
|
||||
* @param {object} root0.adminGroup - oneadmin group
|
||||
* @returns {Component} - Scheduled actions component
|
||||
*/
|
||||
const ScheduleActionsSection = ({ oneConfig, adminGroup }) => {
|
||||
const {
|
||||
fields: scheduleActions,
|
||||
remove,
|
||||
update,
|
||||
append,
|
||||
} = useFieldArray({
|
||||
name: `${EXTRA_ID}.${TAB_ID}`,
|
||||
keyName: 'ID',
|
||||
})
|
||||
|
||||
const handleCreateAction = (action) => {
|
||||
append(mapNameFunction(action, scheduleActions.length))
|
||||
}
|
||||
|
||||
const handleCreateCharter = (actions) => {
|
||||
const mappedActions = actions?.map((action, idx) =>
|
||||
mapNameFunction(action, scheduleActions.length + idx)
|
||||
)
|
||||
|
||||
append(mappedActions)
|
||||
}
|
||||
|
||||
const handleUpdate = (action, index) => {
|
||||
update(index, mapNameFunction(action, index))
|
||||
}
|
||||
|
||||
const handleRemove = (index) => {
|
||||
remove(index)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Legend title={T.ScheduledActions} />
|
||||
<Box sx={{ width: '100%', gridColumn: '1 / -1' }}>
|
||||
<Stack flexDirection="row" gap="1em">
|
||||
<CreateSchedButton
|
||||
relative
|
||||
onSubmit={handleCreateAction}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
<CharterButton relative onSubmit={handleCreateCharter} />
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
pb="1em"
|
||||
display="grid"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(300px, 0.5fr))"
|
||||
gap="1em"
|
||||
mt="1em"
|
||||
>
|
||||
{scheduleActions?.map((schedule, index) => {
|
||||
const { ID, NAME } = schedule
|
||||
const fakeValues = { ...schedule, ID: index }
|
||||
|
||||
return (
|
||||
<ScheduleActionCard
|
||||
key={ID ?? NAME}
|
||||
schedule={fakeValues}
|
||||
actions={
|
||||
<>
|
||||
<UpdateSchedButton
|
||||
relative
|
||||
vm={{}}
|
||||
schedule={fakeValues}
|
||||
onSubmit={(newAction) => handleUpdate(newAction, index)}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
<DeleteSchedButton
|
||||
schedule={fakeValues}
|
||||
onSubmit={() => handleRemove(index)}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
<Divider />
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ScheduleActionsSection.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
oneConfig: PropTypes.object,
|
||||
adminGroup: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default ScheduleActionsSection
|
@ -0,0 +1,31 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { array, object } from 'yup'
|
||||
|
||||
import { NETWORK_INPUT_SCHEMA } from './networking/schema'
|
||||
import { CUSTOM_ATTRIBUTES_SCHEMA } from './customAttributes/schema'
|
||||
import { SCHED_ACTION_SCHEMA } from './scheduledActions'
|
||||
import { ADVANCED_PARAMS_SCHEMA } from './advancedParams/schema'
|
||||
|
||||
export const SCHEMA = object()
|
||||
.shape({
|
||||
NETWORKING: array().of(NETWORK_INPUT_SCHEMA),
|
||||
})
|
||||
.shape({
|
||||
CUSTOM_ATTRIBUTES: array().of(CUSTOM_ATTRIBUTES_SCHEMA),
|
||||
})
|
||||
.concat(ADVANCED_PARAMS_SCHEMA)
|
||||
.concat(SCHED_ACTION_SCHEMA)
|
@ -0,0 +1,59 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 PropTypes from 'prop-types'
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import { T } from 'client/constants'
|
||||
import { SCHEMA, NAME_FIELD, DESCRIPTION_FIELD } from './schema'
|
||||
|
||||
export const STEP_ID = 'general'
|
||||
|
||||
const Content = ({ isUpdate }) => (
|
||||
<FormWithSchema
|
||||
id={STEP_ID}
|
||||
cy={`${STEP_ID}`}
|
||||
fields={[
|
||||
{ ...NAME_FIELD, fieldProps: { disabled: !!isUpdate } },
|
||||
DESCRIPTION_FIELD,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
/**
|
||||
* General Service Template configuration.
|
||||
*
|
||||
* @param {object} data - Service Template data
|
||||
* @returns {object} General configuration step
|
||||
*/
|
||||
const General = (data) => {
|
||||
const isUpdate = data?.ID
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
label: T.General,
|
||||
resolver: SCHEMA,
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: () => Content({ isUpdate }),
|
||||
}
|
||||
}
|
||||
|
||||
General.propTypes = {
|
||||
data: PropTypes.object,
|
||||
setFormData: PropTypes.func,
|
||||
}
|
||||
|
||||
Content.propTypes = { isUpdate: PropTypes.bool }
|
||||
|
||||
export default General
|
@ -0,0 +1,53 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { INPUT_TYPES, T } from 'client/constants'
|
||||
import { Field, getObjectSchemaFromFields } from 'client/utils'
|
||||
import { string } from 'yup'
|
||||
|
||||
/** @type {Field} Name field */
|
||||
const NAME_FIELD = {
|
||||
name: 'NAME',
|
||||
label: T.Name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.min(3, 'Template name less than 3 characters')
|
||||
.max(128, 'Template name over 128 characters')
|
||||
.required('Name cannot be empty')
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
/** @type {Field} Description field */
|
||||
const DESCRIPTION_FIELD = {
|
||||
name: 'DESCRIPTION',
|
||||
label: T.Description,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.max(128, 'Description over 128 characters')
|
||||
.test(
|
||||
'is-not-numeric',
|
||||
'Description should not be a numeric value',
|
||||
(value) => isNaN(value) || value.trim() === ''
|
||||
)
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
const SCHEMA = getObjectSchemaFromFields([NAME_FIELD, DESCRIPTION_FIELD])
|
||||
|
||||
export { SCHEMA, NAME_FIELD, DESCRIPTION_FIELD }
|
@ -0,0 +1,111 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 PropTypes from 'prop-types'
|
||||
import { useMemo, useEffect, Component } from 'react'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
import { ADVANCED_PARAMS_FIELDS } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig/AdvancedParameters/schema'
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
FormControl,
|
||||
Box,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const SECTION_ID = 'ADVANCEDPARAMS'
|
||||
|
||||
/**
|
||||
* @param {object} root0 - props
|
||||
* @param {string} root0.stepId - Main step ID
|
||||
* @param {object} root0.roleConfigs - Roles config
|
||||
* @param {Function} root0.onChange - Callback handler
|
||||
* @returns {Component} - component
|
||||
*/
|
||||
const AdvancedParametersSection = ({ stepId, roleConfigs, onChange }) => {
|
||||
const { watch, setValue } = useFormContext()
|
||||
const { palette } = useTheme()
|
||||
const fields = useMemo(() => ADVANCED_PARAMS_FIELDS, [stepId])
|
||||
|
||||
useEffect(() => {
|
||||
setValue(
|
||||
`${stepId}.${SECTION_ID}.SHUTDOWNTYPE`,
|
||||
roleConfigs?.[SECTION_ID]?.[0] ?? ''
|
||||
)
|
||||
}, [roleConfigs])
|
||||
|
||||
const shutdownTypeValue = watch(`${stepId}.${SECTION_ID}.SHUTDOWNTYPE`)
|
||||
|
||||
useEffect(() => {
|
||||
if (shutdownTypeValue) {
|
||||
onChange('update', { [SECTION_ID]: shutdownTypeValue }, false)
|
||||
}
|
||||
}, [shutdownTypeValue])
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ width: '100%', gridColumn: '1 / -1' }}
|
||||
>
|
||||
<Accordion>
|
||||
<AccordionSummary
|
||||
aria-controls="panel-content"
|
||||
id="panel-header"
|
||||
sx={{
|
||||
backgroundColor: palette?.background?.paper,
|
||||
filter: 'brightness(90%)',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">{T.AdvancedParams}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box component="form" sx={{ flexGrow: 1, display: 'flex' }}>
|
||||
<FormWithSchema
|
||||
id={stepId}
|
||||
cy={'extra-networking'}
|
||||
fields={fields}
|
||||
rootProps={{ sx: { m: 0 } }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
AdvancedParametersSection.propTypes = {
|
||||
selectedRoleIndex: PropTypes.number,
|
||||
stepId: PropTypes.string,
|
||||
roleConfigs: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
export default AdvancedParametersSection
|
@ -0,0 +1,53 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { object, string } from 'yup'
|
||||
import { getValidationFromFields, arrayToOptions } from 'client/utils'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
import { SECTION_ID as ADVANCED_SECTION_ID } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig/AdvancedParameters'
|
||||
|
||||
const SHUTDOWN_TYPES = {
|
||||
none: '',
|
||||
terminate: 'Terminate',
|
||||
terminateHard: 'Terminate hard',
|
||||
}
|
||||
|
||||
const SHUTDOWN_ENUMS_ONEFLOW = {
|
||||
[SHUTDOWN_TYPES.terminate]: 'shutdown',
|
||||
[SHUTDOWN_TYPES.terminateHard]: 'shutdown-hard',
|
||||
}
|
||||
|
||||
const SHUTDOWN_TYPE = {
|
||||
name: `${ADVANCED_SECTION_ID}.SHUTDOWNTYPE`,
|
||||
label: 'VM Shutdown action',
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(Object.keys(SHUTDOWN_TYPES), {
|
||||
addEmpty: false,
|
||||
getText: (key) => SHUTDOWN_TYPES[key],
|
||||
getValue: (key) => SHUTDOWN_ENUMS_ONEFLOW[key],
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.oneOf(Object.values(SHUTDOWN_TYPES))
|
||||
.default(() => Object.values(SHUTDOWN_TYPES)[0]),
|
||||
grid: { xs: 12, sm: 12, md: 12 },
|
||||
}
|
||||
|
||||
export const ADVANCED_PARAMS_FIELDS = [SHUTDOWN_TYPE]
|
||||
|
||||
export const ADVANCED_PARAMS_SCHEMA = object(
|
||||
getValidationFromFields(ADVANCED_PARAMS_FIELDS)
|
||||
)
|
@ -0,0 +1,203 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 PropTypes from 'prop-types'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { useMemo, Component } from 'react'
|
||||
import {
|
||||
useForm,
|
||||
useFieldArray,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from 'react-hook-form'
|
||||
import {
|
||||
createElasticityPolicyFields,
|
||||
createElasticityPoliciesSchema,
|
||||
ELASTICITY_TYPES,
|
||||
} from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig/ElasticityPolicies/schema'
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { DeleteCircledOutline, AddCircledOutline } from 'iconoir-react'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
FormControl,
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import SubmitButton from 'client/components/FormControl/SubmitButton'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const SECTION_ID = 'ELASTICITYPOLICIES'
|
||||
|
||||
/**
|
||||
* @param {object} root0 - props
|
||||
* @param {string} root0.stepId - Main step ID
|
||||
* @param {number} root0.selectedRoleIndex - Active role index
|
||||
* @returns {Component} - component
|
||||
*/
|
||||
const ElasticityPoliciesSection = ({ stepId, selectedRoleIndex }) => {
|
||||
const { palette } = useTheme()
|
||||
const fields = createElasticityPolicyFields()
|
||||
const schema = createElasticityPoliciesSchema()
|
||||
const { getValues } = useFormContext()
|
||||
|
||||
const { append, remove } = useFieldArray({
|
||||
name: useMemo(
|
||||
() => `${stepId}.${SECTION_ID}.${selectedRoleIndex}`,
|
||||
[stepId, selectedRoleIndex]
|
||||
),
|
||||
})
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: schema.default(),
|
||||
resolver: yupResolver(schema),
|
||||
})
|
||||
|
||||
const onSubmit = async (newPolicy) => {
|
||||
const isValid = await methods.trigger(`${stepId}.${SECTION_ID}`)
|
||||
if (isValid) {
|
||||
append(newPolicy)
|
||||
methods.reset()
|
||||
}
|
||||
}
|
||||
|
||||
const currentPolicies =
|
||||
getValues(`${stepId}.${SECTION_ID}.${selectedRoleIndex}`) ?? []
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ width: '100%', gridColumn: '1 / -1' }}
|
||||
>
|
||||
<Accordion>
|
||||
<AccordionSummary
|
||||
aria-controls="panel-content"
|
||||
id="panel-header"
|
||||
data-cy="roleconfig-elasticitypolicies-accordion"
|
||||
sx={{
|
||||
backgroundColor: palette?.background?.paper,
|
||||
filter: 'brightness(90%)',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">{T.ElasticityPolicies}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<FormProvider {...methods}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<FormWithSchema fields={fields} rootProps={{ sx: { m: 0 } }} />
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
type="submit"
|
||||
color="secondary"
|
||||
startIcon={<AddCircledOutline />}
|
||||
data-cy={'roleconfig-elasticitypolicies'}
|
||||
sx={{ width: '100%', mt: 2 }}
|
||||
>
|
||||
<Translate word={T.Add} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<List sx={{ mt: 2 }}>
|
||||
{currentPolicies.map(
|
||||
(
|
||||
{
|
||||
TYPE,
|
||||
ADJUST,
|
||||
MIN,
|
||||
COOLDOWN,
|
||||
PERIOD_NUMBER,
|
||||
PERIOD,
|
||||
EXPRESSION,
|
||||
},
|
||||
index
|
||||
) => {
|
||||
const secondaryFields = [
|
||||
`Expression: ${EXPRESSION}`,
|
||||
`Adjust: ${ADJUST}`,
|
||||
`Cooldown: ${COOLDOWN}`,
|
||||
`Period: ${PERIOD}`,
|
||||
`#: ${PERIOD_NUMBER}`,
|
||||
]
|
||||
if (MIN !== undefined && TYPE === 'PERCENTAGE_CHANGE') {
|
||||
secondaryFields.push(`Min: ${MIN}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '6px',
|
||||
}}
|
||||
>
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<SubmitButton
|
||||
onClick={() => remove(index)}
|
||||
icon={<DeleteCircledOutline />}
|
||||
/>
|
||||
}
|
||||
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={ELASTICITY_TYPES?.[TYPE]}
|
||||
primaryTypographyProps={{ variant: 'body1' }}
|
||||
secondary={secondaryFields.join(' | ')}
|
||||
/>
|
||||
</ListItem>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</List>
|
||||
</FormProvider>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
ElasticityPoliciesSection.propTypes = {
|
||||
stepId: PropTypes.string,
|
||||
selectedRoleIndex: PropTypes.number,
|
||||
}
|
||||
|
||||
export default ElasticityPoliciesSection
|
@ -0,0 +1,145 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { object, string, number } from 'yup'
|
||||
import { getValidationFromFields, arrayToOptions } from 'client/utils'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
|
||||
// Define the CA types
|
||||
export const ELASTICITY_TYPES = {
|
||||
CHANGE: 'Change',
|
||||
CARDINALITY: 'Cardinality',
|
||||
PERCENTAGE_CHANGE: 'Percentage',
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates fields for elasticity policies schema based on a path prefix.
|
||||
*
|
||||
* @param {string} pathPrefix - Path prefix for field names.
|
||||
* @returns {object[]} - Array of field definitions for elasticity policies.
|
||||
*/
|
||||
export const createElasticityPolicyFields = (pathPrefix) => {
|
||||
const getPath = (fieldName) =>
|
||||
pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName
|
||||
|
||||
return [
|
||||
{
|
||||
name: getPath('TYPE'),
|
||||
label: 'Type',
|
||||
type: INPUT_TYPES.SELECT,
|
||||
cy: 'roleconfig-elasticitypolicies',
|
||||
values: arrayToOptions(Object.keys(ELASTICITY_TYPES), {
|
||||
addEmpty: false,
|
||||
getText: (key) => ELASTICITY_TYPES?.[key],
|
||||
getValue: (key) => key,
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.oneOf(Object.keys(ELASTICITY_TYPES))
|
||||
.default(() => Object.keys(ELASTICITY_TYPES)[0]),
|
||||
grid: { xs: 12, sm: 6, md: 6 },
|
||||
},
|
||||
{
|
||||
name: getPath('ADJUST'),
|
||||
label: 'Adjust',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'roleconfig-elasticitypolicies',
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
validation: number().required(),
|
||||
grid: { xs: 12, sm: 6, md: 6 },
|
||||
},
|
||||
{
|
||||
name: getPath('MIN'),
|
||||
label: 'Min',
|
||||
dependOf: getPath('TYPE'),
|
||||
htmlType: (type) =>
|
||||
// ONLY DISPLAY ON PERCENTAGE_CHANGE
|
||||
type !== Object.keys(ELASTICITY_TYPES)[2] && INPUT_TYPES.HIDDEN,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'roleconfig-elasticitypolicies',
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
validation: number().when(getPath('TYPE'), {
|
||||
is: (type) => type === Object.keys(ELASTICITY_TYPES)[2],
|
||||
then: number().required(),
|
||||
otherwise: number().notRequired().nullable(),
|
||||
}),
|
||||
grid: { xs: 12, sm: 6, md: 6 },
|
||||
},
|
||||
{
|
||||
name: getPath('EXPRESSION'),
|
||||
dependOf: getPath('TYPE'),
|
||||
label: 'Expression',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'roleconfig-elasticitypolicies',
|
||||
validation: string().trim().required(),
|
||||
grid: (type) => ({
|
||||
xs: 12,
|
||||
...(type !== Object.keys(ELASTICITY_TYPES)[2]
|
||||
? { sm: 12, md: 12 }
|
||||
: { sm: 6, md: 6 }),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: getPath('PERIOD_NUMBER'),
|
||||
label: '#',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'roleconfig-elasticitypolicies',
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
validation: number(),
|
||||
grid: { xs: 12, sm: 6, md: 4 },
|
||||
},
|
||||
{
|
||||
name: getPath('PERIOD'),
|
||||
label: 'Period',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'roleconfig-elasticitypolicies',
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
validation: number(),
|
||||
grid: { xs: 12, sm: 6, md: 4 },
|
||||
},
|
||||
{
|
||||
name: getPath('COOLDOWN'),
|
||||
label: 'Cooldown',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'roleconfig-elasticitypolicies',
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
validation: number(),
|
||||
grid: { xs: 12, sm: 12, md: 4 },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Yup schema for elasticity policies based on a given path prefix.
|
||||
*
|
||||
* @param {string} pathPrefix - Path prefix for field names in the schema.
|
||||
* @returns {object} - Yup schema object for elasticity policies.
|
||||
*/
|
||||
export const createElasticityPoliciesSchema = (pathPrefix) => {
|
||||
const fields = createElasticityPolicyFields(pathPrefix)
|
||||
|
||||
return object(getValidationFromFields(fields))
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 PropTypes from 'prop-types'
|
||||
import { Component, useMemo, useEffect } from 'react'
|
||||
import { Box, FormControl } from '@mui/material'
|
||||
import { createMinMaxVmsFields } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig/MinMaxVms/schema'
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form'
|
||||
import { STEP_ID as ROLE_DEFINITION_ID } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles'
|
||||
|
||||
export const SECTION_ID = 'MINMAXVMS'
|
||||
/**
|
||||
* @param {object} root0 - props
|
||||
* @param {string} root0.stepId - Main step ID
|
||||
* @param {number} root0.selectedRoleIndex - Active role index
|
||||
* @returns {Component} - component
|
||||
*/
|
||||
const MinMaxVms = ({ stepId, selectedRoleIndex }) => {
|
||||
const { control, setValue, getValues } = useFormContext()
|
||||
const cardinality = useMemo(
|
||||
() =>
|
||||
getValues(ROLE_DEFINITION_ID)?.[selectedRoleIndex]?.CARDINALITY ??
|
||||
undefined,
|
||||
[selectedRoleIndex]
|
||||
)
|
||||
|
||||
const fields = createMinMaxVmsFields(
|
||||
`${stepId}.${SECTION_ID}.${selectedRoleIndex}`,
|
||||
cardinality
|
||||
)
|
||||
|
||||
useFieldArray({
|
||||
name: useMemo(() => `${stepId}.${SECTION_ID}`, [stepId, selectedRoleIndex]),
|
||||
control: control,
|
||||
})
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Set default values
|
||||
useEffect(() => {
|
||||
fields.forEach((field) => {
|
||||
const defaultValue = field.validation.default()
|
||||
setValue(field.name, defaultValue || 0)
|
||||
})
|
||||
}, [fields])
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ width: '100%', gridColumn: '1 / -1' }}
|
||||
>
|
||||
<FormWithSchema fields={fields} rootProps={{ sx: { m: 0 } }} />
|
||||
</FormControl>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
MinMaxVms.propTypes = {
|
||||
stepId: PropTypes.string,
|
||||
selectedRoleIndex: PropTypes.number,
|
||||
}
|
||||
|
||||
export default MinMaxVms
|
@ -0,0 +1,96 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
import { object, number } from 'yup'
|
||||
import { getValidationFromFields } from 'client/utils'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
|
||||
const MAX_VALUE = 999999
|
||||
|
||||
/**
|
||||
* Creates fields for minmax vms schema based on a path prefix.
|
||||
*
|
||||
* @param {string} pathPrefix - Path prefix for field names.
|
||||
* @param { number } cardinality - Number of VMs defined in Role Def. step.
|
||||
* @returns {object[]} - Array of field definitions for minmax vms.
|
||||
*/
|
||||
export const createMinMaxVmsFields = (pathPrefix, cardinality) => {
|
||||
const getPath = (fieldName) =>
|
||||
pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName
|
||||
|
||||
return [
|
||||
{
|
||||
name: getPath('min_vms'),
|
||||
label: 'Min VMs',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'elasticity',
|
||||
validation: number()
|
||||
.integer('Min VMs must be an integer')
|
||||
.min(
|
||||
cardinality,
|
||||
`Min VMs cannot be less than defined cardinality: ${cardinality}`
|
||||
)
|
||||
.default(() => cardinality),
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
grid: { sm: 4, md: 4 },
|
||||
},
|
||||
{
|
||||
name: getPath('max_vms'),
|
||||
label: 'Max VMs',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'elasticity',
|
||||
validation: number()
|
||||
.integer('Max VMs must be an integer')
|
||||
.min(cardinality, `Max VMs cannot be less than ${cardinality}`)
|
||||
.max(MAX_VALUE, `Max VMs cannot exceed ${MAX_VALUE}`)
|
||||
.default(() => cardinality),
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
grid: { sm: 4, md: 4 },
|
||||
},
|
||||
{
|
||||
name: getPath('cooldown'),
|
||||
label: 'Cooldown',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'elasticity',
|
||||
validation: number()
|
||||
.integer('Cooldown must be an integer')
|
||||
.min(0, 'Cooldown cannot be less than 0')
|
||||
.max(MAX_VALUE, `Cooldown exceed ${MAX_VALUE}`)
|
||||
.default(() => 0),
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
grid: { sm: 4, md: 4 },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Yup schema for minmax vms based on a given path prefix.
|
||||
*
|
||||
* @param {string} pathPrefix - Path prefix for field names in the schema.
|
||||
* @param { number } cardinality - Number of VMs defined in Role Def. step.
|
||||
* @returns {object} - Yup schema object for minmax vms.
|
||||
*/
|
||||
export const createMinMaxVmsSchema = (pathPrefix, cardinality = 0) => {
|
||||
const fields = createMinMaxVmsFields(pathPrefix, cardinality)
|
||||
|
||||
return object(getValidationFromFields(fields))
|
||||
}
|
@ -0,0 +1,193 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 PropTypes from 'prop-types'
|
||||
import { Component, useMemo } from 'react'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import {
|
||||
useFormContext,
|
||||
useForm,
|
||||
useFieldArray,
|
||||
FormProvider,
|
||||
} from 'react-hook-form'
|
||||
import {
|
||||
createScheduledPolicyFields,
|
||||
createScheduledPoliciesSchema,
|
||||
SCHED_TYPES,
|
||||
} from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig/ScheduledPolicies/schema'
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { DeleteCircledOutline, AddCircledOutline } from 'iconoir-react'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
FormControl,
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import SubmitButton from 'client/components/FormControl/SubmitButton'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const SECTION_ID = 'SCHEDULEDPOLICIES'
|
||||
|
||||
/**
|
||||
* @param {object} root0 - props
|
||||
* @param {string} root0.stepId - Main step ID
|
||||
* @param {number} root0.selectedRoleIndex - Active role index
|
||||
* @returns {Component} - component
|
||||
*/
|
||||
const ScheduledPoliciesSection = ({ stepId, selectedRoleIndex }) => {
|
||||
const { palette } = useTheme()
|
||||
const fields = createScheduledPolicyFields()
|
||||
const schema = createScheduledPoliciesSchema()
|
||||
const { getValues } = useFormContext()
|
||||
|
||||
const { append, remove } = useFieldArray({
|
||||
name: useMemo(
|
||||
() => `${stepId}.${SECTION_ID}.${selectedRoleIndex}`,
|
||||
[stepId, selectedRoleIndex]
|
||||
),
|
||||
})
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: schema.default(),
|
||||
resolver: yupResolver(schema),
|
||||
})
|
||||
|
||||
const onSubmit = async (newPolicy) => {
|
||||
const isValid = await methods.trigger(`${stepId}.${SECTION_ID}`)
|
||||
if (isValid) {
|
||||
append(newPolicy)
|
||||
methods.reset()
|
||||
}
|
||||
}
|
||||
|
||||
const currentPolicies =
|
||||
getValues(`${stepId}.${SECTION_ID}.${selectedRoleIndex}`) ?? []
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ width: '100%', gridColumn: '1 / -1' }}
|
||||
>
|
||||
<Accordion>
|
||||
<AccordionSummary
|
||||
aria-controls="panel-content"
|
||||
id="panel-header"
|
||||
data-cy="roleconfig-scheduledpolicies-accordion"
|
||||
sx={{
|
||||
backgroundColor: palette?.background?.paper,
|
||||
filter: 'brightness(90%)',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">{T.ScheduledPolicies}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<FormProvider {...methods}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
<FormWithSchema fields={fields} rootProps={{ sx: { m: 0 } }} />
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
type="submit"
|
||||
color="secondary"
|
||||
startIcon={<AddCircledOutline />}
|
||||
data-cy={'roleconfig-scheduledpolicies'}
|
||||
sx={{ width: '100%', mt: 2 }}
|
||||
>
|
||||
<Translate word={T.Add} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<List>
|
||||
{currentPolicies.map(
|
||||
(
|
||||
{ TIMEFORMAT, SCHEDTYPE, ADJUST, MIN, TIMEEXPRESSION },
|
||||
index
|
||||
) => {
|
||||
const secondaryFields = [
|
||||
`Time Expression: ${TIMEEXPRESSION}`,
|
||||
`Adjust: ${ADJUST}`,
|
||||
`Time Format: ${TIMEFORMAT}`,
|
||||
]
|
||||
|
||||
if (MIN !== undefined) {
|
||||
secondaryFields?.push(`Min: ${MIN}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '6px',
|
||||
}}
|
||||
>
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<SubmitButton
|
||||
onClick={() => remove(index)}
|
||||
icon={<DeleteCircledOutline />}
|
||||
/>
|
||||
}
|
||||
sx={{ '&:hover': { bgcolor: 'action.hover' } }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={SCHED_TYPES?.[SCHEDTYPE]}
|
||||
primaryTypographyProps={{ variant: 'body1' }}
|
||||
secondary={secondaryFields.join(' | ')}
|
||||
/>
|
||||
</ListItem>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</List>
|
||||
</FormProvider>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
ScheduledPoliciesSection.propTypes = {
|
||||
stepId: PropTypes.string,
|
||||
selectedRoleIndex: PropTypes.number,
|
||||
}
|
||||
|
||||
export default ScheduledPoliciesSection
|
@ -0,0 +1,135 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { object, string, number } from 'yup'
|
||||
import { getValidationFromFields, arrayToOptions } from 'client/utils'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
|
||||
const TIME_TYPES = {
|
||||
none: '',
|
||||
recurrence: 'Recurrence',
|
||||
starttime: 'Start time',
|
||||
}
|
||||
|
||||
export const SCHED_TYPES = {
|
||||
CHANGE: 'Change',
|
||||
CARDINALITY: 'Cardinality',
|
||||
PERCENTAGE_CHANGE: 'Percentage',
|
||||
}
|
||||
/* eslint-disable no-useless-escape */
|
||||
const timeExpressionRegex =
|
||||
/^(\d{4}-\d{2}-\d{2}(?: [0-2]\d:[0-5]\d:[0-5]\d|\d{4}-\d{2}-\d{2}T[0-2]\d:[0-5]\d:[0-5]\dZ)?)$/
|
||||
|
||||
const cronExpressionRegex = /^([\d*\/,-]+ ){4}[\d*\/,-]+$/
|
||||
|
||||
/* eslint-enable no-useless-escape */
|
||||
|
||||
/**
|
||||
* Creates fields for scheduled policies schema based on a path prefix.
|
||||
*
|
||||
* @param {string} pathPrefix - Path prefix for field names.
|
||||
* @returns {object[]} - Array of field definitions for scheduled policies.
|
||||
*/
|
||||
export const createScheduledPolicyFields = (pathPrefix) => {
|
||||
const getPath = (fieldName) =>
|
||||
pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName
|
||||
|
||||
return [
|
||||
{
|
||||
name: getPath('SCHEDTYPE'),
|
||||
label: 'Type',
|
||||
type: INPUT_TYPES.SELECT,
|
||||
cy: 'roleconfig-scheduledpolicies',
|
||||
values: arrayToOptions(Object.keys(SCHED_TYPES), {
|
||||
addEmpty: false,
|
||||
getText: (key) => SCHED_TYPES?.[key],
|
||||
getValue: (key) => key,
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => Object.keys(SCHED_TYPES)[0]),
|
||||
grid: { xs: 12, sm: 6, md: 3.3 },
|
||||
},
|
||||
{
|
||||
name: getPath('ADJUST'),
|
||||
label: 'Adjust',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'roleconfig-scheduledpolicies',
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => ''),
|
||||
grid: { xs: 12, sm: 6, md: 3.1 },
|
||||
},
|
||||
{
|
||||
name: getPath('MIN'),
|
||||
label: 'Min',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'roleconfig-scheduledpolicies',
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
validation: number().notRequired(),
|
||||
grid: { xs: 12, sm: 6, md: 2.1 },
|
||||
},
|
||||
{
|
||||
name: getPath('TIMEFORMAT'),
|
||||
label: 'Time Format',
|
||||
type: INPUT_TYPES.SELECT,
|
||||
cy: 'roleconfig-scheduledpolicies',
|
||||
values: arrayToOptions(Object.values(TIME_TYPES), { addEmpty: false }),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.oneOf(Object.values(TIME_TYPES))
|
||||
.default(() => Object.values(TIME_TYPES)[0]),
|
||||
grid: { xs: 12, sm: 6, md: 3.5 },
|
||||
},
|
||||
{
|
||||
name: getPath('TIMEEXPRESSION'),
|
||||
label: 'Time Expression',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'roleconfig-scheduledpolicies',
|
||||
validation: string()
|
||||
.trim()
|
||||
.when(getPath('TIMEFORMAT'), {
|
||||
is: 'Start time',
|
||||
then: string().matches(
|
||||
timeExpressionRegex,
|
||||
'Time Expression must be in the format YYYY-MM-DD hh:mm:ss or YYYY-MM-DDThh:mm:ssZ'
|
||||
),
|
||||
otherwise: string().matches(
|
||||
cronExpressionRegex,
|
||||
'Time Expression must be a valid CRON expression'
|
||||
),
|
||||
})
|
||||
.required(),
|
||||
grid: { xs: 12, sm: 12, md: 12 },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Yup schema for scheduled policies based on a given path prefix.
|
||||
*
|
||||
* @param {string} pathPrefix - Path prefix for field names in the schema.
|
||||
* @returns {object} - Yup schema object for scheduled policies.
|
||||
*/
|
||||
export const createScheduledPoliciesSchema = (pathPrefix) => {
|
||||
const fields = createScheduledPolicyFields(pathPrefix)
|
||||
|
||||
return object(getValidationFromFields(fields))
|
||||
}
|
@ -0,0 +1,305 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 PropTypes from 'prop-types'
|
||||
import { useState, useRef, Component } from 'react'
|
||||
import _ from 'lodash'
|
||||
import { useFormContext, useForm, FormProvider } from 'react-hook-form'
|
||||
import { Box, Button, Grid } from '@mui/material'
|
||||
import { SCHEMA } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig/schema'
|
||||
import RoleColumn from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles/rolesColumn'
|
||||
import RoleSummary from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles/roleSummary'
|
||||
import RoleNetwork from './roleNetwork'
|
||||
import { STEP_ID as ROLE_DEFINITION_ID } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles'
|
||||
import ElasticityPoliciesSection from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig/ElasticityPolicies'
|
||||
import ScheduledPoliciesSection from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig/ScheduledPolicies'
|
||||
import AdvancedParametersSection from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig/AdvancedParameters'
|
||||
import VmTemplatesPanel from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles/vmTemplatesPanel'
|
||||
import MinMaxSection from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig/MinMaxVms'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import { Legend } from 'client/components/Forms'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { INPUT_TYPES, T } from 'client/constants'
|
||||
import { AddCircledOutline } from 'iconoir-react'
|
||||
import { Field, getObjectSchemaFromFields } from 'client/utils'
|
||||
import { string, number } from 'yup'
|
||||
|
||||
export const STEP_ID = 'roleconfig'
|
||||
|
||||
/** @type {Field} STANDALONE Name field */
|
||||
const STANDALONE_NAME_FIELD = {
|
||||
name: `${STEP_ID}.name`,
|
||||
label: T.Name,
|
||||
cy: 'role',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.min(3, 'Role name cannot be less than 3 characters')
|
||||
.max(128, 'Role name cannot be over 128 characters')
|
||||
.required('Role name cannot be empty')
|
||||
.default(() => undefined),
|
||||
grid: { md: 8 },
|
||||
}
|
||||
|
||||
/** @type {Field} STANDALONE Cardinality field */
|
||||
const STANDALONE_CARDINALITY_FIELD = {
|
||||
name: `${STEP_ID}.cardinality`,
|
||||
label: T.NumberOfVms,
|
||||
cy: 'role',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
validation: number()
|
||||
.positive('Number of VMs must be positive')
|
||||
.default(() => 1),
|
||||
grid: { md: 4 },
|
||||
}
|
||||
|
||||
const STANDALONE_SCHEMA = getObjectSchemaFromFields([
|
||||
STANDALONE_NAME_FIELD,
|
||||
STANDALONE_CARDINALITY_FIELD,
|
||||
])
|
||||
|
||||
/**
|
||||
* @param {object} root0 - Props
|
||||
* @param {boolean} root0.standaloneModal - Run as standalone modal
|
||||
* @param {Function} root0.standaloneModalCallback - API callback function
|
||||
* @returns {Component} - Role configuration component
|
||||
*/
|
||||
export const Content = ({
|
||||
standaloneModal = false,
|
||||
standaloneModalCallback = () => {},
|
||||
}) => {
|
||||
const [standaloneRole, setStandaloneRole] = useState([
|
||||
{ SELECTED_VM_TEMPLATE_ID: [] },
|
||||
])
|
||||
|
||||
const HANDLE_VM_SELECT_STANDALONE_ROLE = (updatedRole) => {
|
||||
setStandaloneRole(updatedRole)
|
||||
}
|
||||
|
||||
const formMethods = standaloneModal
|
||||
? useForm({
|
||||
defaultValues: SCHEMA.concat(STANDALONE_SCHEMA).default(),
|
||||
resolver: yupResolver(STANDALONE_SCHEMA),
|
||||
mode: 'onChange',
|
||||
})
|
||||
: useFormContext()
|
||||
const { getValues, setValue } = formMethods
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const handleAddRoleClick = async () => {
|
||||
const role = getValues(STEP_ID)
|
||||
|
||||
const formatRole = {
|
||||
name: role?.name,
|
||||
cardinality: role?.cardinality,
|
||||
vm_template: standaloneRole?.SELECTED_VM_TEMPLATE_ID?.[0],
|
||||
...(role?.ADVANCEDPARAMS?.SHUTDOWNTYPE && {
|
||||
shutdown_type: role.ADVANCEDPARAMS.SHUTDOWNTYPE,
|
||||
}),
|
||||
min_vms: +role?.MINMAXVMS?.[0]?.min_vms,
|
||||
max_vms: +role?.MINMAXVMS?.[0]?.max_vms,
|
||||
cooldown: role?.MINMAXVMS?.[0]?.cooldown,
|
||||
...(role?.ELASTICITYPOLICIES && {
|
||||
elasticity_policies: role?.ELASTICITYPOLICIES?.[0],
|
||||
}),
|
||||
|
||||
...(role?.SCHEDULEDPOLICIES && {
|
||||
scheduled_policies: role?.SCHEDULEDPOLICIES?.[0],
|
||||
}),
|
||||
}
|
||||
standaloneModalCallback({ role: formatRole })
|
||||
}
|
||||
|
||||
const definedConfigs = getValues(`${STEP_ID}.ROLES`)
|
||||
const roleConfigs = useRef(definedConfigs ?? [])
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const syncFormState = () => {
|
||||
setValue(`${STEP_ID}.ROLES`, roleConfigs.current)
|
||||
}
|
||||
|
||||
const [selectedRoleIndex, setSelectedRoleIndex] = useState(0)
|
||||
const roles = getValues(ROLE_DEFINITION_ID)
|
||||
|
||||
const handleConfigChange = (operationType, config, shouldReset = false) => {
|
||||
const configKey = Object.keys(config)[0]
|
||||
const configValue = Object.values(config)[0]
|
||||
|
||||
_.defaultsDeep(roleConfigs.current, {
|
||||
[selectedRoleIndex]: { [configKey]: [] },
|
||||
})
|
||||
|
||||
switch (operationType) {
|
||||
case 'add':
|
||||
_.get(roleConfigs.current, [selectedRoleIndex, configKey]).push(
|
||||
configValue
|
||||
)
|
||||
break
|
||||
case 'remove':
|
||||
_.remove(
|
||||
_.get(roleConfigs.current, [selectedRoleIndex, configKey]),
|
||||
(_v, index) => index === configValue
|
||||
)
|
||||
break
|
||||
case 'update':
|
||||
_.set(
|
||||
roleConfigs.current,
|
||||
[selectedRoleIndex, configKey, 0],
|
||||
configValue
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
syncFormState()
|
||||
}
|
||||
|
||||
const ComponentContent = (
|
||||
<Grid mt={2} container>
|
||||
{!standaloneModal && (
|
||||
<Grid item xs={2.2}>
|
||||
<RoleColumn
|
||||
roles={roles}
|
||||
selectedRoleIndex={selectedRoleIndex}
|
||||
setSelectedRoleIndex={setSelectedRoleIndex}
|
||||
disableModify={true}
|
||||
onChange={handleConfigChange}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={standaloneModal ? 12 : 7}>
|
||||
<Box
|
||||
margin={1}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{!standaloneModal && (
|
||||
<RoleNetwork
|
||||
stepId={STEP_ID}
|
||||
selectedRoleIndex={selectedRoleIndex}
|
||||
totalRoles={roles?.length}
|
||||
/>
|
||||
)}
|
||||
<Box mt={2}>
|
||||
<Legend title={T.RoleElasticity} />
|
||||
{standaloneModal && (
|
||||
<Box>
|
||||
<FormWithSchema
|
||||
fields={[STANDALONE_NAME_FIELD, STANDALONE_CARDINALITY_FIELD]}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{standaloneModal && (
|
||||
<Box>
|
||||
<VmTemplatesPanel
|
||||
roles={[standaloneRole]}
|
||||
selectedRoleIndex={selectedRoleIndex}
|
||||
onChange={HANDLE_VM_SELECT_STANDALONE_ROLE}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<MinMaxSection
|
||||
stepId={STEP_ID}
|
||||
selectedRoleIndex={selectedRoleIndex}
|
||||
/>
|
||||
<ElasticityPoliciesSection
|
||||
stepId={STEP_ID}
|
||||
selectedRoleIndex={selectedRoleIndex}
|
||||
/>
|
||||
<ScheduledPoliciesSection
|
||||
stepId={STEP_ID}
|
||||
selectedRoleIndex={selectedRoleIndex}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={2}>
|
||||
<Legend title={T.Extra} />
|
||||
<AdvancedParametersSection
|
||||
stepId={STEP_ID}
|
||||
roleConfigs={roleConfigs.current?.[selectedRoleIndex]}
|
||||
onChange={handleConfigChange}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{standaloneModal && (
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
type="submit"
|
||||
color="secondary"
|
||||
startIcon={<AddCircledOutline />}
|
||||
data-cy={'roleconfig-addrole'}
|
||||
onClick={handleAddRoleClick}
|
||||
sx={{ width: '100%', mt: 2 }}
|
||||
>
|
||||
<Translate word={T.AddRole} />
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{!standaloneModal && (
|
||||
<Grid item xs={2.8}>
|
||||
<RoleSummary
|
||||
role={roles?.[selectedRoleIndex] ?? []}
|
||||
selectedRoleIndex={selectedRoleIndex}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)
|
||||
|
||||
return standaloneModal ? (
|
||||
<FormProvider {...formMethods}>{ComponentContent}</FormProvider>
|
||||
) : (
|
||||
ComponentContent
|
||||
)
|
||||
}
|
||||
|
||||
Content.propTypes = {
|
||||
standaloneModal: PropTypes.Boolean,
|
||||
standaloneModalCallback: PropTypes.func,
|
||||
}
|
||||
|
||||
/**
|
||||
* Role definition configuration.
|
||||
*
|
||||
* @returns {object} Roles definition configuration step
|
||||
*/
|
||||
const RoleConfig = () => ({
|
||||
id: STEP_ID,
|
||||
label: 'Role Configuration',
|
||||
resolver: SCHEMA,
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content,
|
||||
})
|
||||
RoleConfig.propTypes = {
|
||||
data: PropTypes.array,
|
||||
setFormData: PropTypes.func,
|
||||
}
|
||||
|
||||
export default RoleConfig
|
@ -0,0 +1,300 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 PropTypes from 'prop-types'
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form'
|
||||
import { useEffect, useState, useRef, useMemo, Component } from 'react'
|
||||
import { DataGrid } from '@mui/x-data-grid'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Box, Checkbox, TextField, Autocomplete } from '@mui/material'
|
||||
import { T } from 'client/constants'
|
||||
import { Legend } from 'client/components/Forms'
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Extra'
|
||||
import _ from 'lodash'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
'& .MuiDataGrid-columnHeader:focus, & .MuiDataGrid-cell:focus': {
|
||||
outline: 'none !important',
|
||||
},
|
||||
'& .MuiDataGrid-columnHeader:focus-within, & .MuiDataGrid-cell:focus-within':
|
||||
{
|
||||
outline: 'none !important',
|
||||
},
|
||||
'& .MuiDataGrid-overlay': {
|
||||
top: '50% !important',
|
||||
left: '50% !important',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 'auto !important',
|
||||
height: 'auto !important',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const SECTION_ID = 'NETWORKS'
|
||||
|
||||
/**
|
||||
* @param {object} root0 - Props
|
||||
* @param {string} root0.stepId - Step ID
|
||||
* @param {number} root0.selectedRoleIndex - Active role index
|
||||
* @returns {Component} - Component
|
||||
*/
|
||||
const RoleNetwork = ({ stepId, selectedRoleIndex }) => {
|
||||
// Using a local state to keep track of the loading of initial row data
|
||||
// will overwrite modifications if stepId changes
|
||||
const loadInitialRowData = useRef({})
|
||||
const [networks, setNetworks] = useState([])
|
||||
const [fieldArrayLocation, setFieldArrayLocation] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
setFieldArrayLocation(`${stepId}.${SECTION_ID}.${selectedRoleIndex}`)
|
||||
}, [selectedRoleIndex, SECTION_ID, stepId])
|
||||
|
||||
const classes = useStyles()
|
||||
const { getValues, setValue } = useFormContext()
|
||||
|
||||
const { fields, update } = useFieldArray({
|
||||
name: fieldArrayLocation,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const networkDefinitions = getValues(EXTRA_ID)?.NETWORKING ?? []
|
||||
const networkMap = networkDefinitions.map((network) => ({
|
||||
name: network?.name,
|
||||
description: network?.description,
|
||||
id: network?.network,
|
||||
extra: network?.netextra,
|
||||
type: network?.type,
|
||||
}))
|
||||
|
||||
setNetworks(networkMap)
|
||||
}, [getValues])
|
||||
|
||||
const rows = useMemo(
|
||||
() =>
|
||||
networks.map((config, index) => ({
|
||||
...config,
|
||||
id: index,
|
||||
idx: index, // RHF overwrites the ID prop with a UUID
|
||||
rowSelected: false,
|
||||
aliasSelected: false,
|
||||
aliasIdx: -1,
|
||||
network: config.name,
|
||||
})),
|
||||
[networks]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const existingRows = getValues(fieldArrayLocation)
|
||||
|
||||
const mergedRows = rows?.map((row) => {
|
||||
const existingRow = existingRows?.find((er) => er?.name === row?.name)
|
||||
|
||||
return existingRow ? _.merge({}, row, existingRow) : row
|
||||
})
|
||||
|
||||
setValue(fieldArrayLocation, mergedRows)
|
||||
|
||||
if (!loadInitialRowData.current?.[selectedRoleIndex] && rows?.length) {
|
||||
const reversedNetworkDefs = getValues(stepId)?.NETWORKDEFS ?? []
|
||||
|
||||
if (reversedNetworkDefs?.[selectedRoleIndex]) {
|
||||
reversedNetworkDefs?.[selectedRoleIndex]?.forEach((def) => {
|
||||
const rowName = def.NETWORK_ID.slice(1).toLowerCase()
|
||||
const rowToSelect = rows.find(
|
||||
(row) => row?.name?.toLowerCase() === rowName
|
||||
)
|
||||
|
||||
if (rowToSelect) {
|
||||
handleSelectRow(rowToSelect, true)
|
||||
|
||||
if (def.PARENT) {
|
||||
handleSelectAlias(rowToSelect)
|
||||
const parentNetwork = reversedNetworkDefs[
|
||||
selectedRoleIndex
|
||||
]?.find((network) => network?.NAME === def.PARENT)
|
||||
|
||||
if (parentNetwork) {
|
||||
const parentNetworkName =
|
||||
parentNetwork.NETWORK_ID.slice(1).toLowerCase()
|
||||
const parentRow = rows.find(
|
||||
(row) => row?.name?.toLowerCase() === parentNetworkName
|
||||
)
|
||||
|
||||
handleSetAlias(rowToSelect, parentRow?.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
loadInitialRowData.current[selectedRoleIndex] = true
|
||||
}
|
||||
}, [fieldArrayLocation])
|
||||
|
||||
const handleSelectRow = (row, forceSelect = false) => {
|
||||
const fieldArray = getValues(fieldArrayLocation)
|
||||
const fieldArrayIndex = fieldArray?.findIndex((f) => f?.idx === row?.idx)
|
||||
const rowToggle = forceSelect
|
||||
? true
|
||||
: !fieldArray?.[fieldArrayIndex]?.rowSelected
|
||||
|
||||
const updatedFieldArray = fieldArray?.map((f, index) => {
|
||||
if (index === fieldArrayIndex) {
|
||||
return { ...f, rowSelected: rowToggle, aliasSelected: false }
|
||||
}
|
||||
if (f.aliasIdx === fieldArrayIndex) {
|
||||
return { ...f, aliasIdx: -1 }
|
||||
}
|
||||
|
||||
return f
|
||||
})
|
||||
|
||||
setValue(fieldArrayLocation, updatedFieldArray)
|
||||
}
|
||||
|
||||
const handleSelectAlias = (row) => {
|
||||
const fieldArray = getValues(fieldArrayLocation)
|
||||
const fieldArrayIndex = fieldArray?.findIndex((f) => f?.idx === row?.idx)
|
||||
const aliasToggle = !fieldArray?.[fieldArrayIndex]?.aliasSelected
|
||||
const aliasIdx = !fieldArray?.[fieldArrayIndex]?.aliasIdx
|
||||
update(fieldArrayIndex, {
|
||||
...fieldArray?.[fieldArrayIndex],
|
||||
aliasSelected: aliasToggle,
|
||||
aliasIdx: !aliasToggle ? -1 : aliasIdx,
|
||||
})
|
||||
}
|
||||
|
||||
const handleSetAlias = (row, aliasName) => {
|
||||
const fieldArray = getValues(fieldArrayLocation)
|
||||
const aliasIndex = fieldArray?.findIndex((f) => f?.network === aliasName)
|
||||
const fieldArrayIndex = fieldArray?.findIndex((f) => f?.idx === row?.idx)
|
||||
update(fieldArrayIndex, {
|
||||
...fieldArray?.[fieldArrayIndex],
|
||||
aliasIdx: aliasIndex,
|
||||
})
|
||||
}
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'select',
|
||||
disableColumnMenu: true,
|
||||
sortable: false,
|
||||
headerName: 'Select',
|
||||
width: 100,
|
||||
renderCell: (params) => (
|
||||
<Checkbox
|
||||
checked={params?.row?.rowSelected || false}
|
||||
onChange={() => handleSelectRow(params?.row)}
|
||||
inputProps={{
|
||||
'data-cy': `role-config-network-${params?.row?.idx}`,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'network',
|
||||
disableColumnMenu: true,
|
||||
flex: 1,
|
||||
headerName: 'Network',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'aliasToggle',
|
||||
disableColumnMenu: true,
|
||||
sortable: false,
|
||||
headerName: 'NIC Alias',
|
||||
width: 110,
|
||||
renderCell: (params) =>
|
||||
params?.row?.rowSelected && (
|
||||
<Checkbox
|
||||
checked={params?.row?.aliasSelected || false}
|
||||
onChange={() => handleSelectAlias(params?.row)}
|
||||
inputProps={{
|
||||
'data-cy': `role-config-network-alias-${params?.row?.idx}`,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'alias',
|
||||
disableColumnMenu: true,
|
||||
flex: 1,
|
||||
headerName: 'Alias',
|
||||
width: 200,
|
||||
renderCell: (params) =>
|
||||
params?.row?.aliasSelected && (
|
||||
<Autocomplete
|
||||
id={`role-config-network-alias-name-${params?.row?.id}`}
|
||||
options={networks
|
||||
?.filter((net, index) => {
|
||||
const fieldArray = getValues(fieldArrayLocation)?.[index]
|
||||
|
||||
return (
|
||||
net?.name !== params?.row?.network &&
|
||||
fieldArray?.rowSelected &&
|
||||
fieldArray?.aliasIdx === -1
|
||||
)
|
||||
})
|
||||
|
||||
?.map((net) => net?.name)}
|
||||
renderOption={(props, option) => (
|
||||
<li
|
||||
{...props}
|
||||
data-cy={`role-config-network-aliasname-option-${option}`}
|
||||
>
|
||||
{option}
|
||||
</li>
|
||||
)}
|
||||
renderInput={(props) => <TextField {...props} label="NIC" />}
|
||||
onChange={(_event, value) => handleSetAlias(params?.row, value)}
|
||||
value={
|
||||
getValues(fieldArrayLocation)?.[params?.row?.aliasIdx]?.name ??
|
||||
null
|
||||
}
|
||||
data-cy={`role-config-network-aliasname-${params?.row?.idx}`}
|
||||
sx={{ width: '100%', height: '100%' }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[networks, fieldArrayLocation]
|
||||
)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Legend title={T.RoleNetworks} />
|
||||
<DataGrid
|
||||
className={classes.root}
|
||||
rows={fields}
|
||||
columns={columns}
|
||||
localeText={{ noRowsLabel: 'No networks have been defined' }}
|
||||
autoHeight
|
||||
rowsPerPageOptions={[5, 10, 25, 50, 100]}
|
||||
disableSelectionOnClick
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
RoleNetwork.propTypes = {
|
||||
networks: PropTypes.array,
|
||||
roleConfigs: PropTypes.object,
|
||||
stepId: PropTypes.string,
|
||||
selectedRoleIndex: PropTypes.number,
|
||||
}
|
||||
|
||||
export default RoleNetwork
|
@ -0,0 +1,39 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { array, object } from 'yup'
|
||||
|
||||
import { ADVANCED_PARAMS_SCHEMA } from './AdvancedParameters/schema'
|
||||
import { createElasticityPoliciesSchema } from './ElasticityPolicies/schema'
|
||||
import { createScheduledPoliciesSchema } from './ScheduledPolicies/schema'
|
||||
import { createMinMaxVmsSchema } from './MinMaxVms/schema'
|
||||
|
||||
export const SCHEMA = object()
|
||||
.shape({
|
||||
MINMAXVMS: array().of(createMinMaxVmsSchema()),
|
||||
})
|
||||
.shape({
|
||||
ELASTICITYPOLICIES: array().of(
|
||||
array().of(createElasticityPoliciesSchema())
|
||||
),
|
||||
})
|
||||
.shape({
|
||||
SCHEDULEDPOLICIES: array().of(array().of(createScheduledPoliciesSchema())),
|
||||
})
|
||||
.shape({
|
||||
NETWORKS: array(),
|
||||
NETWORKDEFS: array(),
|
||||
})
|
||||
.concat(ADVANCED_PARAMS_SCHEMA)
|
@ -0,0 +1,131 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 PropTypes from 'prop-types'
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { useFormContext, useWatch } from 'react-hook-form'
|
||||
import { Box, Grid } from '@mui/material'
|
||||
import { SCHEMA } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles/schema'
|
||||
import RoleVmVmPanel from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles/rolesPanel'
|
||||
import RoleColumn from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles/rolesColumn'
|
||||
import VmTemplatesPanel from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles/vmTemplatesPanel'
|
||||
import RoleSummary from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles/roleSummary'
|
||||
|
||||
export const STEP_ID = 'roledefinition'
|
||||
|
||||
const Content = () => {
|
||||
const { getValues, setValue, reset } = useFormContext()
|
||||
const [selectedRoleIndex, setSelectedRoleIndex] = useState(0)
|
||||
|
||||
const defaultRole = [
|
||||
{ NAME: '', SELECTED_VM_TEMPLATE_ID: [], CARDINALITY: 0 },
|
||||
]
|
||||
|
||||
const watchedRoles = useWatch({
|
||||
name: STEP_ID,
|
||||
defaultValue: defaultRole,
|
||||
})
|
||||
const definedRoles = getValues(STEP_ID)
|
||||
|
||||
useEffect(() => {
|
||||
if (definedRoles) {
|
||||
reset({ [STEP_ID]: definedRoles ?? defaultRole })
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [roles, setRoles] = useState(getValues(STEP_ID))
|
||||
|
||||
useEffect(() => {
|
||||
setRoles(watchedRoles)
|
||||
}, [definedRoles, watchedRoles])
|
||||
|
||||
const handleChangeRoles = (updatedRoles) => {
|
||||
setValue(STEP_ID, updatedRoles)
|
||||
}
|
||||
|
||||
const handleRoleChange = useCallback(
|
||||
(updatedRole) => {
|
||||
const updatedRoles = [...roles]
|
||||
|
||||
if (selectedRoleIndex >= 0 && selectedRoleIndex < roles.length) {
|
||||
updatedRoles[selectedRoleIndex] = updatedRole
|
||||
} else {
|
||||
updatedRoles.push(updatedRole)
|
||||
}
|
||||
|
||||
handleChangeRoles(updatedRoles)
|
||||
},
|
||||
[roles, selectedRoleIndex]
|
||||
)
|
||||
|
||||
return (
|
||||
<Grid mt={2} container>
|
||||
<Grid item xs={2.2}>
|
||||
<RoleColumn
|
||||
roles={roles}
|
||||
onChange={handleChangeRoles}
|
||||
selectedRoleIndex={selectedRoleIndex}
|
||||
setSelectedRoleIndex={setSelectedRoleIndex}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={7}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1em',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<RoleVmVmPanel
|
||||
roles={roles}
|
||||
onChange={handleRoleChange}
|
||||
selectedRoleIndex={selectedRoleIndex}
|
||||
/>
|
||||
<VmTemplatesPanel
|
||||
roles={roles}
|
||||
selectedRoleIndex={selectedRoleIndex}
|
||||
onChange={handleRoleChange}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={2.8}>
|
||||
<RoleSummary
|
||||
role={roles?.[selectedRoleIndex] ?? []}
|
||||
selectedRoleIndex={selectedRoleIndex}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Role definition configuration.
|
||||
*
|
||||
* @returns {object} Roles definition configuration step
|
||||
*/
|
||||
const RoleDefinition = () => ({
|
||||
id: STEP_ID,
|
||||
label: 'Role Definition',
|
||||
resolver: SCHEMA,
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content,
|
||||
})
|
||||
RoleDefinition.propTypes = {
|
||||
data: PropTypes.array,
|
||||
setFormData: PropTypes.func,
|
||||
}
|
||||
|
||||
export default RoleDefinition
|
@ -0,0 +1,203 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 {
|
||||
Card,
|
||||
CardContent,
|
||||
CardActions,
|
||||
Typography,
|
||||
Divider,
|
||||
} from '@mui/material'
|
||||
import PropTypes from 'prop-types'
|
||||
import { T } from 'client/constants'
|
||||
import { Component } from 'react'
|
||||
/**
|
||||
* RoleSummary displays detailed information about a VM role, including its configuration and affinity settings.
|
||||
*
|
||||
* @param {object} props - The props that control the RoleSummary component.
|
||||
* @param {object} props.role - The role object containing the role's configuration.
|
||||
* @param {number} props.selectedRoleIndex - The index of the selected role.
|
||||
* @returns {Component} - Role summary component.
|
||||
*/
|
||||
const RoleSummary = ({ role, selectedRoleIndex }) => (
|
||||
<Card
|
||||
elevation={2}
|
||||
sx={{
|
||||
height: '100%',
|
||||
maxHeight: '630px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1em',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="div" gutterBottom>
|
||||
#{selectedRoleIndex + 1 ?? 0} Role Configuration
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={role?.NAME ? 'text.primary' : 'text.disabled'}
|
||||
gutterBottom
|
||||
>
|
||||
Name: {role?.NAME || 'Enter a name for this role.'}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={
|
||||
role?.CARDINALITY === undefined ||
|
||||
role?.CARDINALITY === 'None' ||
|
||||
+role?.CARDINALITY < 1
|
||||
? 'text.disabled'
|
||||
: 'text.primary'
|
||||
}
|
||||
gutterBottom
|
||||
>
|
||||
{T.NumberOfVms}: {role?.CARDINALITY}
|
||||
</Typography>
|
||||
|
||||
{role?.SELECTED_VM_TEMPLATE_ID ? (
|
||||
<>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={
|
||||
role?.SELECTED_VM_TEMPLATE_ID === undefined ||
|
||||
role?.SELECTED_VM_TEMPLATE_ID === 'None' ||
|
||||
role?.SELECTED_VM_TEMPLATE_ID?.length < 1
|
||||
? 'text.disabled'
|
||||
: 'text.primary'
|
||||
}
|
||||
gutterBottom
|
||||
>
|
||||
VM Template ID: {role?.SELECTED_VM_TEMPLATE_ID}
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.disabled" gutterBottom>
|
||||
Select a VM template.
|
||||
</Typography>
|
||||
)}
|
||||
<Divider />
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={role?.NETWORKS ? 'text.primary' : 'text.disabled'}
|
||||
gutterBottom
|
||||
>
|
||||
Networks: {role?.NETWORKS || 'Select a network for this role.'}
|
||||
</Typography>
|
||||
|
||||
<Typography color={'text.primary'} sx={{ fontSize: 16 }} gutterBottom>
|
||||
Role Elasticity
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={role?.MINVMS ? 'text.primary' : 'text.disabled'}
|
||||
gutterBottom
|
||||
>
|
||||
Min VMs:
|
||||
{role?.MINVMS || ' Minimum number of VMs for elasticity adjustments.'}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={role?.MAXVMS ? 'text.primary' : 'text.disabled'}
|
||||
gutterBottom
|
||||
>
|
||||
Max VMs:
|
||||
{role?.MAXVMS || ' Maximum number of VMs for elasticity adjustments.'}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={role?.MAXVMS ? 'text.primary' : 'text.disabled'}
|
||||
gutterBottom
|
||||
>
|
||||
Cooldown:
|
||||
{role?.COOLDOWN ||
|
||||
' Duration after a scale operation in seconds. If it is not set, the default set in oneflow-server.conf will be used.'}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
color={role?.ELASTICITYPOLICIES ? 'text.primary' : 'text.disabled'}
|
||||
sx={{ fontSize: 14 }}
|
||||
gutterBottom
|
||||
>
|
||||
Elasticity Policies
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={
|
||||
role?.ELASTICITYPOLICIES?.TYPE ? 'text.primary' : 'text.disabled'
|
||||
}
|
||||
gutterBottom
|
||||
>
|
||||
Type:
|
||||
{role?.ELASTICITYPOLICIES?.TYPE || ' Adjustment type'}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={
|
||||
role?.ELASTICITYPOLICIES?.ADJUST ? 'text.primary' : 'text.disabled'
|
||||
}
|
||||
gutterBottom
|
||||
>
|
||||
Adjust:
|
||||
{role?.ELASTICITYPOLICIES?.ADJUST || ' Positive or negative adjustment'}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions sx={{ p: 2, pt: 0 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
color="textSecondary"
|
||||
sx={{ opacity: 0.7 }}
|
||||
>
|
||||
<strong>VM Group Configuration:</strong>
|
||||
<ul>
|
||||
<li>Define roles and placement constraints.</li>
|
||||
<li>Optimize performance and fault tolerance.</li>
|
||||
<li>Manage multi-VM applications efficiently.</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
</CardActions>
|
||||
</Card>
|
||||
)
|
||||
|
||||
RoleSummary.propTypes = {
|
||||
role: PropTypes.oneOfType([
|
||||
PropTypes.shape({
|
||||
NAME: PropTypes.string,
|
||||
POLICY: PropTypes.oneOf(['AFFINED', 'ANTI_AFFINED', 'None', undefined]),
|
||||
HOST_AFFINED: PropTypes.arrayOf(PropTypes.number),
|
||||
HOST_ANTI_AFFINED: PropTypes.arrayOf(PropTypes.number),
|
||||
}),
|
||||
PropTypes.array,
|
||||
PropTypes.object,
|
||||
]),
|
||||
selectedRoleIndex: PropTypes.number,
|
||||
onRemoveAffinity: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default RoleSummary
|
@ -0,0 +1,168 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { useCallback, Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Box, Button, List, ListItem, IconButton } from '@mui/material'
|
||||
import { Cancel } from 'iconoir-react'
|
||||
|
||||
/**
|
||||
* RoleColumn component for displaying and managing roles.
|
||||
*
|
||||
* @param {object} props - The properties passed to the component.
|
||||
* @param {Array} props.roles - The list of roles.
|
||||
* @param {Function} props.onChange - Callback function when roles are changed.
|
||||
* @param {number|null} props.selectedRoleIndex - The index of the currently selected role.
|
||||
* @param {Function} props.setSelectedRoleIndex - Function to set the selected role index.
|
||||
* @param {boolean} props.disableModify - Disables the modification of roles.
|
||||
* @returns {Component} - Role columns component
|
||||
*/
|
||||
const RoleColumn = ({
|
||||
roles,
|
||||
onChange,
|
||||
selectedRoleIndex,
|
||||
setSelectedRoleIndex,
|
||||
disableModify = false,
|
||||
}) => {
|
||||
const newRole = { NAME: '', SELECTED_VM_TEMPLATE_ID: [], CARDINALITY: 0 }
|
||||
|
||||
const handleAddRole = useCallback(() => {
|
||||
const updatedRoles = [...roles, newRole]
|
||||
onChange(updatedRoles)
|
||||
setSelectedRoleIndex(roles?.length)
|
||||
}, [roles, onChange, selectedRoleIndex])
|
||||
|
||||
const handleRemoveRole = useCallback(
|
||||
(indexToRemove) => {
|
||||
const updatedRoles = [
|
||||
...roles.slice(0, indexToRemove),
|
||||
...roles.slice(indexToRemove + 1),
|
||||
]
|
||||
|
||||
onChange(updatedRoles)
|
||||
if (selectedRoleIndex === indexToRemove) {
|
||||
setSelectedRoleIndex(null)
|
||||
}
|
||||
},
|
||||
[roles, selectedRoleIndex]
|
||||
)
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
pt: 2,
|
||||
height: '100%',
|
||||
maxHeight: '630px',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
borderRight: 1,
|
||||
pr: 2,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{!disableModify && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleAddRole}
|
||||
size="large"
|
||||
data-cy="add-role"
|
||||
>
|
||||
Add Role
|
||||
</Button>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: '90%',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<List>
|
||||
{Array.isArray(roles) &&
|
||||
roles.length > 0 &&
|
||||
roles.map((role, index) => (
|
||||
<ListItem
|
||||
button
|
||||
selected={index === selectedRoleIndex}
|
||||
onClick={() => setSelectedRoleIndex(index)}
|
||||
key={index}
|
||||
sx={{
|
||||
my: 0.5,
|
||||
minHeight: '43.5px',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: '4px',
|
||||
overflowX: 'hidden',
|
||||
bgcolor:
|
||||
index === selectedRoleIndex
|
||||
? 'action.selected'
|
||||
: 'inherit',
|
||||
'&.Mui-selected, &.Mui-selected:hover': {
|
||||
bgcolor: 'action.selected',
|
||||
},
|
||||
'&:hover': {
|
||||
bgcolor: 'action.hover',
|
||||
},
|
||||
}}
|
||||
data-cy={`role-column-${index}`}
|
||||
>
|
||||
{!disableModify && (
|
||||
<IconButton
|
||||
aria-label="delete"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleRemoveRole(index)
|
||||
}}
|
||||
data-cy={`delete-role-${index}`}
|
||||
sx={{ mr: 1.5 }}
|
||||
>
|
||||
<Cancel />
|
||||
</IconButton>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{role?.NAME || 'New Role'}
|
||||
</div>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
RoleColumn.propTypes = {
|
||||
roles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
selectedRoleIndex: PropTypes.number,
|
||||
setSelectedRoleIndex: PropTypes.func.isRequired,
|
||||
disableModify: PropTypes.bool,
|
||||
}
|
||||
|
||||
RoleColumn.defaultProps = {
|
||||
roles: [],
|
||||
}
|
||||
|
||||
export default RoleColumn
|
@ -0,0 +1,93 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Box, TextField, Typography } from '@mui/material'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/**
|
||||
* Role Panel component for managing roles.
|
||||
*
|
||||
* @param {object} props - Component properties.
|
||||
* @param {Array} props.roles - List of roles.
|
||||
* @param {Function} props.onChange - Callback for when roles change.
|
||||
* @param {number} props.selectedRoleIndex - Currently selected role index.
|
||||
* @returns {Component} The rendered component.
|
||||
*/
|
||||
const RoleVmVmPanel = ({ roles, onChange, selectedRoleIndex }) => {
|
||||
const handleInputChange = (event, passedName = '') => {
|
||||
let value
|
||||
let name = passedName
|
||||
if (typeof event === 'object' && event?.target) {
|
||||
const { name: eventName = '', value: eventValue = '' } =
|
||||
event.target || {}
|
||||
value = eventValue
|
||||
name = passedName || eventName
|
||||
} else {
|
||||
value = event
|
||||
}
|
||||
onChange({ ...roles[selectedRoleIndex], [name]: value }) // updated role
|
||||
}
|
||||
|
||||
return (
|
||||
<Box p={2}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="h6">Role Details</Typography>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<TextField
|
||||
label="Role Name"
|
||||
name="NAME"
|
||||
value={roles?.[selectedRoleIndex]?.NAME ?? ''}
|
||||
onChange={handleInputChange}
|
||||
disabled={!roles?.[selectedRoleIndex]}
|
||||
inputProps={{ 'data-cy': `role-name-${selectedRoleIndex}` }}
|
||||
fullWidth
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<TextField
|
||||
type="number"
|
||||
label={T.NumberOfVms}
|
||||
value={roles?.[selectedRoleIndex]?.CARDINALITY ?? 1}
|
||||
onChange={handleInputChange}
|
||||
name="CARDINALITY"
|
||||
InputProps={{
|
||||
inputProps: {
|
||||
min: 0,
|
||||
'data-cy': `role-cardinality-${selectedRoleIndex}`,
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
RoleVmVmPanel.propTypes = {
|
||||
roles: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
NAME: PropTypes.string,
|
||||
CARDINALITY: PropTypes.number,
|
||||
})
|
||||
),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
selectedRoleIndex: PropTypes.number,
|
||||
}
|
||||
|
||||
export default RoleVmVmPanel
|
@ -0,0 +1,93 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { INPUT_TYPES, T } from 'client/constants'
|
||||
import { object, string, array, number } from 'yup'
|
||||
import { Field } from 'client/utils'
|
||||
|
||||
/** @type {Field} Name field for role */
|
||||
const ROLE_NAME_FIELD = {
|
||||
name: 'name',
|
||||
label: T.Name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required('Role name cannot be empty')
|
||||
.default(() => ''),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
const CARDINALITY_FIELD = {
|
||||
name: 'cardinality',
|
||||
label: T.NumberOfVms,
|
||||
|
||||
validation: number()
|
||||
.positive('Number of VMs must be positive')
|
||||
.default(() => 1),
|
||||
}
|
||||
|
||||
const SELECTED_VM_TEMPLATE_ID_FIELD = {
|
||||
name: 'selected_vm_template_id',
|
||||
validation: array()
|
||||
.required('VM Template ID is required')
|
||||
.min(1, 'At least one VM Template ID is required')
|
||||
.default(() => []),
|
||||
}
|
||||
|
||||
/** @type {object} Role schema */
|
||||
const ROLE_SCHEMA = object().shape({
|
||||
NAME: ROLE_NAME_FIELD.validation,
|
||||
CARDINALITY: CARDINALITY_FIELD.validation,
|
||||
SELECTED_VM_TEMPLATE_ID: SELECTED_VM_TEMPLATE_ID_FIELD.validation,
|
||||
})
|
||||
|
||||
/** @type {object} Roles schema for the step */
|
||||
export const SCHEMA = array()
|
||||
.of(ROLE_SCHEMA)
|
||||
.test(
|
||||
'is-non-empty',
|
||||
'Define at least one role!',
|
||||
(value) => value !== undefined && value.length > 0
|
||||
)
|
||||
.test(
|
||||
'has-valid-role-names',
|
||||
'Some roles have invalid names, max 128 characters',
|
||||
(roles) =>
|
||||
roles.every(
|
||||
(role) =>
|
||||
role.NAME &&
|
||||
role.NAME.trim().length > 0 &&
|
||||
role.NAME.trim().length <= 128
|
||||
)
|
||||
)
|
||||
.test('non-negative', 'Number of VMs must be a positive number', (roles) =>
|
||||
roles.every((role) => role?.CARDINALITY >= 1)
|
||||
)
|
||||
.test(
|
||||
'valid-characters',
|
||||
'Role names can only contain letters and numbers',
|
||||
(roles) =>
|
||||
roles.every((role) => role.NAME && /^[a-zA-Z0-9]+$/.test(role.NAME))
|
||||
)
|
||||
.test(
|
||||
'has-unique-name',
|
||||
'All roles must have unique names',
|
||||
(roles) => new Set(roles.map((role) => role.NAME)).size === roles.length
|
||||
)
|
||||
|
||||
/**
|
||||
* @returns {Field[]} Fields
|
||||
*/
|
||||
export const FIELDS = [ROLE_NAME_FIELD, ROLE_SCHEMA]
|
@ -0,0 +1,177 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { useState, useEffect, Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Box,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TablePagination,
|
||||
Checkbox,
|
||||
Typography,
|
||||
Paper,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { useLazyGetTemplatesQuery } from 'client/features/OneApi/vmTemplate'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
const convertTimestampToDate = (timestamp) =>
|
||||
DateTime.fromSeconds(parseInt(timestamp)).toFormat('dd/MM/yyyy HH:mm:ss')
|
||||
|
||||
/**
|
||||
* VmTemplatesPanel component.
|
||||
*
|
||||
* @param {object} props - The props that are passed to this component.
|
||||
* @param {Array} props.roles - The roles available for selection.
|
||||
* @param {number} props.selectedRoleIndex - The index of the currently selected role.
|
||||
* @param {Function} props.onChange - Callback to be called when affinity settings are changed.
|
||||
* @returns {Component} The VmTemplatesPanel component.
|
||||
*/
|
||||
const VmTemplatesPanel = ({ roles, selectedRoleIndex, onChange }) => {
|
||||
const theme = useTheme()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
const [vmTemplates, setVmTemplates] = useState([])
|
||||
const [fetch, { data, error }] = useLazyGetTemplatesQuery()
|
||||
const templateID = roles?.[selectedRoleIndex]?.SELECTED_VM_TEMPLATE_ID ?? []
|
||||
|
||||
useEffect(() => {
|
||||
fetch()
|
||||
}, [fetch])
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
enqueueError(
|
||||
`Error fetching VM templates data: ${error?.message ?? error}`
|
||||
)
|
||||
}
|
||||
}, [error, enqueueError])
|
||||
|
||||
useEffect(() => {
|
||||
setVmTemplates(data)
|
||||
}, [data])
|
||||
|
||||
const [page, setPage] = useState(0)
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10)
|
||||
|
||||
const handleChangePage = (_event, newPage) => {
|
||||
setPage(newPage)
|
||||
}
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10))
|
||||
setPage(0)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
my: 2,
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
maxHeight: '40%',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
VM Templates
|
||||
</Typography>
|
||||
<Paper sx={{ overflow: 'auto', marginBottom: 2 }}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell padding="checkbox"></TableCell>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Owner</TableCell>
|
||||
<TableCell>Group</TableCell>
|
||||
<TableCell>Registration time</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{vmTemplates
|
||||
?.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
?.map((vmTemplate) => (
|
||||
<TableRow
|
||||
key={vmTemplate.ID}
|
||||
hover
|
||||
sx={{
|
||||
'&:hover': {
|
||||
filter: 'brightness(85%)',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}}
|
||||
onClick={() =>
|
||||
onChange({
|
||||
...roles[selectedRoleIndex],
|
||||
SELECTED_VM_TEMPLATE_ID: [vmTemplate.ID],
|
||||
})
|
||||
}
|
||||
name="SELECTED_VM_TEMPLATE_ID"
|
||||
role="checkbox"
|
||||
aria-checked={templateID.includes(vmTemplate.ID)}
|
||||
style={{
|
||||
backgroundColor: templateID?.includes(vmTemplate.ID)
|
||||
? theme?.palette?.action?.selected
|
||||
: theme?.palette.action?.disabledBackground,
|
||||
}}
|
||||
data-cy={`role-vmtemplate-${vmTemplate.ID}`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox checked={templateID.includes(vmTemplate.ID)} />
|
||||
</TableCell>
|
||||
<TableCell>{vmTemplate.ID}</TableCell>
|
||||
<TableCell>{vmTemplate.NAME}</TableCell>
|
||||
<TableCell>{vmTemplate.UNAME}</TableCell>
|
||||
<TableCell>{vmTemplate.GNAME}</TableCell>
|
||||
<TableCell>
|
||||
{convertTimestampToDate(vmTemplate.REGTIME)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[10, 25, 50]}
|
||||
component="div"
|
||||
count={vmTemplates?.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
VmTemplatesPanel.propTypes = {
|
||||
roles: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
NAME: PropTypes.string,
|
||||
POLICY: PropTypes.string,
|
||||
SELECTED_VM_TEMPLATE_ID: PropTypes.array,
|
||||
})
|
||||
),
|
||||
selectedRoleIndex: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
templateID: PropTypes.array,
|
||||
}
|
||||
|
||||
export default VmTemplatesPanel
|
@ -0,0 +1,255 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 _ from 'lodash'
|
||||
import General, {
|
||||
STEP_ID as GENERAL_ID,
|
||||
} from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/General'
|
||||
import Extra, {
|
||||
STEP_ID as EXTRA_ID,
|
||||
} from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Extra'
|
||||
import RoleDefinition, {
|
||||
STEP_ID as ROLE_DEFINITION_ID,
|
||||
} from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles'
|
||||
|
||||
import RoleConfig, {
|
||||
STEP_ID as ROLE_CONFIG_ID,
|
||||
} from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig'
|
||||
|
||||
import {
|
||||
parseNetworkString,
|
||||
parseCustomInputString,
|
||||
parseVmTemplateContents,
|
||||
createSteps,
|
||||
} from 'client/utils'
|
||||
|
||||
const convertKeysToCase = (obj, toLower = true) => {
|
||||
if (_.isArray(obj)) {
|
||||
return obj.map((item) => convertKeysToCase(item, toLower))
|
||||
}
|
||||
|
||||
if (_.isObject(obj) && !_.isDate(obj) && !_.isFunction(obj)) {
|
||||
return _.mapValues(
|
||||
_.mapKeys(obj, (_value, key) =>
|
||||
toLower ? key.toLowerCase() : key.toUpperCase()
|
||||
),
|
||||
(value) => convertKeysToCase(value, toLower)
|
||||
)
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
const Steps = createSteps([General, Extra, RoleDefinition, RoleConfig], {
|
||||
transformInitialValue: (ServiceTemplate, schema) => {
|
||||
const definedNetworks = Object.entries(
|
||||
ServiceTemplate?.TEMPLATE?.BODY?.networks || {}
|
||||
)
|
||||
?.map(([name, networkString]) =>
|
||||
parseNetworkString(`${name}|${networkString}`, true)
|
||||
)
|
||||
.filter(Boolean)
|
||||
|
||||
const customAttributes = Object.entries(
|
||||
ServiceTemplate?.TEMPLATE?.BODY?.custom_attrs || {}
|
||||
)
|
||||
?.map(([name, customInputString]) =>
|
||||
parseCustomInputString(`${name}|${customInputString}`, true)
|
||||
)
|
||||
.filter(Boolean)
|
||||
|
||||
const reversedVmTc = ServiceTemplate?.TEMPLATE?.BODY?.roles?.map((role) =>
|
||||
parseVmTemplateContents(role?.vm_template_contents, true)
|
||||
)
|
||||
|
||||
const generalData = {
|
||||
NAME: ServiceTemplate?.TEMPLATE?.BODY?.name,
|
||||
DESCRIPTION: ServiceTemplate?.TEMPLATE?.BODY.description,
|
||||
}
|
||||
|
||||
const definedRoles = ServiceTemplate?.TEMPLATE?.BODY?.roles
|
||||
?.filter((role) => role != null)
|
||||
?.map((role) => ({
|
||||
NAME: role?.name,
|
||||
CARDINALITY: role?.cardinality,
|
||||
SELECTED_VM_TEMPLATE_ID: [role?.vm_template.toString()],
|
||||
}))
|
||||
|
||||
const roleDefinitionData = definedRoles?.map((role) => ({
|
||||
...role,
|
||||
}))
|
||||
|
||||
const roleConfigData = {
|
||||
ELASTICITYPOLICIES: convertKeysToCase(
|
||||
ServiceTemplate?.TEMPLATE?.BODY?.roles
|
||||
?.filter((role) => role != null)
|
||||
?.reduce((acc, role, index) => {
|
||||
if (role?.elasticity_policies) {
|
||||
acc[index] = role.elasticity_policies.reduce(
|
||||
(policyAcc, policy) => {
|
||||
policyAcc.push({
|
||||
...policy,
|
||||
COOLDOWN: +policy.cooldown,
|
||||
...(policy?.min && { MIN: +policy.min }),
|
||||
PERIOD: +policy.period,
|
||||
PERIOD_NUMBER: +policy.period_number,
|
||||
})
|
||||
|
||||
return policyAcc
|
||||
},
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, []),
|
||||
false
|
||||
),
|
||||
SCHEDULEDPOLICIES: convertKeysToCase(
|
||||
ServiceTemplate?.TEMPLATE?.BODY?.roles
|
||||
?.filter((role) => role != null)
|
||||
?.reduce((acc, role, index) => {
|
||||
if (role?.scheduled_policies) {
|
||||
acc[index] = role.scheduled_policies.reduce(
|
||||
(policyAcc, policy) => {
|
||||
policyAcc.push({
|
||||
...(+policy?.min && { MIN: policy?.min }),
|
||||
SCHEDTYPE: policy?.type,
|
||||
TIMEFORMAT: policy?.recurrence
|
||||
? 'Recurrence'
|
||||
: 'Start time',
|
||||
TIMEEXPRESSION: policy?.recurrence || policy?.start_time,
|
||||
})
|
||||
|
||||
return policyAcc
|
||||
},
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, []),
|
||||
false
|
||||
),
|
||||
MINMAXVMS: ServiceTemplate?.TEMPLATE?.BODY?.roles
|
||||
?.filter((role) => role != null)
|
||||
?.map((role) => ({
|
||||
min_vms: role.min_vms,
|
||||
max_vms: role.max_vms,
|
||||
cooldown: role.cooldown,
|
||||
}))
|
||||
?.filter((role) =>
|
||||
Object.values(role).some((val) => val !== undefined)
|
||||
),
|
||||
|
||||
NETWORKDEFS: reversedVmTc?.map((rtc) => rtc.networks),
|
||||
}
|
||||
|
||||
const knownTemplate = schema.cast(
|
||||
{
|
||||
[EXTRA_ID]: {
|
||||
NETWORKING: definedNetworks,
|
||||
CUSTOM_ATTRIBUTES: customAttributes,
|
||||
// Sched actions are same for all roles, so just grab the first one
|
||||
SCHED_ACTION: reversedVmTc?.[0]?.schedActions,
|
||||
},
|
||||
[GENERAL_ID]: { ...generalData },
|
||||
[ROLE_DEFINITION_ID]: roleDefinitionData,
|
||||
[ROLE_CONFIG_ID]: { ...roleConfigData },
|
||||
},
|
||||
{ stripUnknown: true }
|
||||
)
|
||||
|
||||
return knownTemplate
|
||||
},
|
||||
|
||||
transformBeforeSubmit: (formData) => {
|
||||
const {
|
||||
[GENERAL_ID]: generalData,
|
||||
[ROLE_DEFINITION_ID]: roleDefinitionData,
|
||||
[EXTRA_ID]: extraData,
|
||||
[ROLE_CONFIG_ID]: roleConfigData,
|
||||
} = formData
|
||||
|
||||
const formatTemplate = {
|
||||
...generalData,
|
||||
roles: roleDefinitionData?.map((roleDef, index) => {
|
||||
const scheduledPolicies = roleConfigData?.SCHEDULEDPOLICIES?.[
|
||||
index
|
||||
]?.map((policy) => {
|
||||
const newPolicy = {
|
||||
...policy,
|
||||
TYPE: policy?.SCHEDTYPE,
|
||||
ADJUST: +policy?.ADJUST,
|
||||
[policy.TIMEFORMAT?.split(' ')?.join('_')?.toLowerCase()]:
|
||||
policy.TIMEEXPRESSION,
|
||||
}
|
||||
delete newPolicy.SCHEDTYPE
|
||||
delete newPolicy.TIMEFORMAT
|
||||
delete newPolicy.TIMEEXPRESSION
|
||||
|
||||
return newPolicy
|
||||
})
|
||||
|
||||
const newRoleDef = {
|
||||
vm_template_contents: parseVmTemplateContents({
|
||||
networks: roleConfigData?.NETWORKS?.[index] ?? undefined,
|
||||
schedActions: extraData?.SCHED_ACTION ?? undefined,
|
||||
}),
|
||||
...roleDef,
|
||||
|
||||
...roleConfigData?.MINMAXVMS?.[index],
|
||||
VM_TEMPLATE: +roleDef?.SELECTED_VM_TEMPLATE_ID?.[0],
|
||||
...(scheduledPolicies &&
|
||||
scheduledPolicies.length > 0 && {
|
||||
scheduled_policies: scheduledPolicies,
|
||||
}),
|
||||
elasticity_policies: [
|
||||
...roleConfigData?.ELASTICITYPOLICIES?.[index].flatMap((elap) => ({
|
||||
...elap,
|
||||
...(elap?.ADJUST && { adjust: +elap?.ADJUST }),
|
||||
})),
|
||||
],
|
||||
}
|
||||
|
||||
delete newRoleDef.SELECTED_VM_TEMPLATE_ID
|
||||
delete newRoleDef.MINMAXVMS
|
||||
|
||||
return newRoleDef
|
||||
}),
|
||||
...extraData?.ADVANCED,
|
||||
...(extraData?.NETWORKING?.length && {
|
||||
networks: extraData?.NETWORKING?.reduce((acc, network) => {
|
||||
if (network?.name) {
|
||||
acc[network.name] = parseNetworkString(network)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {}),
|
||||
}),
|
||||
custom_attrs: extraData?.CUSTOM_ATTRIBUTES?.reduce((acc, cinput) => {
|
||||
if (cinput?.name) {
|
||||
acc[cinput.name] = parseCustomInputString(cinput)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {}),
|
||||
}
|
||||
|
||||
return convertKeysToCase(formatTemplate)
|
||||
},
|
||||
})
|
||||
|
||||
export default Steps
|
@ -0,0 +1,16 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
export { default } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps'
|
@ -0,0 +1,164 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { Box, Stack, Divider } from '@mui/material'
|
||||
import { useFieldArray } from 'react-hook-form'
|
||||
import { array, object } from 'yup'
|
||||
|
||||
import { ScheduleActionCard } from 'client/components/Cards'
|
||||
import {
|
||||
CreateSchedButton,
|
||||
CharterButton,
|
||||
UpdateSchedButton,
|
||||
DeleteSchedButton,
|
||||
} from 'client/components/Buttons/ScheduleAction'
|
||||
|
||||
import PropTypes from 'prop-types'
|
||||
import { T } from 'client/constants'
|
||||
import { Legend } from 'client/components/Forms'
|
||||
|
||||
import { mapNameByIndex } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Extra'
|
||||
|
||||
import { Component, useMemo } from 'react'
|
||||
|
||||
export const TAB_ID = 'SCHED_ACTION'
|
||||
|
||||
const mapNameFunction = mapNameByIndex('SCHED_ACTION')
|
||||
|
||||
export const SCHED_ACTION_SCHEMA = object({
|
||||
SCHED_ACTION: array()
|
||||
.ensure()
|
||||
.transform((actions) => actions.map(mapNameByIndex('SCHED_ACTION'))),
|
||||
})
|
||||
|
||||
const ScheduleActionsSection = ({ oneConfig, adminGroup }) => {
|
||||
const {
|
||||
fields: scheduleActions,
|
||||
remove,
|
||||
update,
|
||||
append,
|
||||
} = useFieldArray({
|
||||
name: `${EXTRA_ID}.${TAB_ID}`,
|
||||
keyName: 'ID',
|
||||
})
|
||||
|
||||
const handleCreateAction = (action) => {
|
||||
append(mapNameFunction(action, scheduleActions.length))
|
||||
}
|
||||
|
||||
const handleCreateCharter = (actions) => {
|
||||
const mappedActions = actions?.map((action, idx) =>
|
||||
mapNameFunction(action, scheduleActions.length + idx)
|
||||
)
|
||||
|
||||
append(mappedActions)
|
||||
}
|
||||
|
||||
const handleUpdate = (action, index) => {
|
||||
update(index, mapNameFunction(action, index))
|
||||
}
|
||||
|
||||
const handleRemove = (index) => {
|
||||
remove(index)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box mt={2}>
|
||||
<Legend title={'Add Charters Values Configuration'} />
|
||||
<Box sx={{ width: '100%', gridColumn: '1 / -1' }}>
|
||||
<Stack flexDirection="row" gap="1em">
|
||||
<CreateSchedButton
|
||||
relative
|
||||
onSubmit={handleCreateAction}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
<CharterButton relative onSubmit={handleCreateCharter} />
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
pb="1em"
|
||||
display="grid"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(300px, 0.5fr))"
|
||||
gap="1em"
|
||||
mt="1em"
|
||||
>
|
||||
{scheduleActions?.map((schedule, index) => {
|
||||
const { ID, NAME } = schedule
|
||||
const fakeValues = { ...schedule, ID: index }
|
||||
|
||||
return (
|
||||
<ScheduleActionCard
|
||||
key={ID ?? NAME}
|
||||
schedule={fakeValues}
|
||||
actions={
|
||||
<>
|
||||
<UpdateSchedButton
|
||||
relative
|
||||
vm={{}}
|
||||
schedule={fakeValues}
|
||||
onSubmit={(newAction) => handleUpdate(newAction, index)}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
<DeleteSchedButton
|
||||
schedule={fakeValues}
|
||||
onSubmit={() => handleRemove(index)}
|
||||
oneConfig={oneConfig}
|
||||
adminGroup={adminGroup}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
<Divider />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
ScheduleActionsSection.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
oneConfig: PropTypes.object,
|
||||
adminGroup: PropTypes.bool,
|
||||
}
|
||||
export const STEP_ID = 'charter'
|
||||
|
||||
const Content = () => useMemo(() => <ScheduleActionsSection />, [STEP_ID])
|
||||
|
||||
Content.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
hypervisor: PropTypes.string,
|
||||
control: PropTypes.object,
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Component} - Charters step
|
||||
*/
|
||||
const Charter = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.Charter,
|
||||
resolver: SCHED_ACTION_SCHEMA,
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content,
|
||||
})
|
||||
|
||||
export default Charter
|
@ -0,0 +1,59 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 PropTypes from 'prop-types'
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import { T } from 'client/constants'
|
||||
import { SCHEMA, NAME_FIELD, INSTANCE_FIELD } from './schema'
|
||||
|
||||
export const STEP_ID = 'general'
|
||||
|
||||
const Content = ({ isUpdate }) => (
|
||||
<FormWithSchema
|
||||
id={STEP_ID}
|
||||
cy={`${STEP_ID}`}
|
||||
fields={[
|
||||
{ ...NAME_FIELD, fieldProps: { disabled: !!isUpdate } },
|
||||
INSTANCE_FIELD,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
/**
|
||||
* General Service Template configuration.
|
||||
*
|
||||
* @param {object} data - Service Template data
|
||||
* @returns {object} General configuration step
|
||||
*/
|
||||
const General = (data) => {
|
||||
const isUpdate = data?.ID
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
label: T.General,
|
||||
resolver: SCHEMA,
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: () => Content({ isUpdate }),
|
||||
}
|
||||
}
|
||||
|
||||
General.propTypes = {
|
||||
data: PropTypes.object,
|
||||
setFormData: PropTypes.func,
|
||||
}
|
||||
|
||||
Content.propTypes = { isUpdate: PropTypes.bool }
|
||||
|
||||
export default General
|
@ -0,0 +1,48 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { INPUT_TYPES } from 'client/constants'
|
||||
import { Field, getObjectSchemaFromFields } from 'client/utils'
|
||||
import { string, number } from 'yup'
|
||||
|
||||
/** @type {Field} Name field */
|
||||
const NAME_FIELD = {
|
||||
name: 'NAME',
|
||||
label: 'Service Name',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.min(3, 'Service name less than 3 characters')
|
||||
.max(128, 'Service name over 128 characters')
|
||||
.required('Name cannot be empty')
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
/** @type {Field} Description field */
|
||||
const INSTANCE_FIELD = {
|
||||
name: 'INSTANCES',
|
||||
label: 'Number of instances',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: number().required(),
|
||||
fieldProps: {
|
||||
type: 'number',
|
||||
},
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
const SCHEMA = getObjectSchemaFromFields([NAME_FIELD, INSTANCE_FIELD])
|
||||
|
||||
export { SCHEMA, NAME_FIELD, INSTANCE_FIELD }
|
@ -0,0 +1,144 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 PropTypes from 'prop-types'
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import { NETWORK_TYPES } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Extra/networking/schema'
|
||||
import { T } from 'client/constants'
|
||||
import { Box, Grid, TextField } from '@mui/material'
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import { createNetworkFields, createNetworkSchema } from './schema'
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form'
|
||||
|
||||
export const STEP_ID = 'network'
|
||||
export const FIELD_ARRAY = 'NETWORKS'
|
||||
|
||||
const Content = (props) => {
|
||||
const { control, setValue } = useFormContext()
|
||||
|
||||
const templatePath = props?.dataTemplate?.TEMPLATE?.BODY?.networks
|
||||
const networkInfo = Object.entries(templatePath || {}).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const extraPart = value.split('::')?.[1]
|
||||
acc[key] = extraPart
|
||||
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const [tableIndex, setTableIndex] = useState(0)
|
||||
|
||||
const [tableType, setTableType] = useState(
|
||||
Object.keys(NETWORK_TYPES)?.[0] ?? ''
|
||||
)
|
||||
|
||||
const fields = useMemo(
|
||||
() =>
|
||||
createNetworkFields(`${STEP_ID}.${FIELD_ARRAY}.${tableIndex}`, tableType),
|
||||
[tableIndex, tableType, STEP_ID]
|
||||
)
|
||||
|
||||
useFieldArray({
|
||||
name: useMemo(() => `${STEP_ID}.${FIELD_ARRAY}`, [STEP_ID, tableIndex]),
|
||||
control: control,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setValue(`${STEP_ID}.${FIELD_ARRAY}.${tableIndex}.tableType`, tableType)
|
||||
}, [tableType, tableIndex, STEP_ID])
|
||||
|
||||
if (fields?.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
select
|
||||
label="Network ID"
|
||||
onChange={(e) => {
|
||||
setTableIndex(e.target.selectedIndex)
|
||||
}}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
inputProps: { 'data-cy': `select-${STEP_ID}-id` },
|
||||
}}
|
||||
variant="outlined"
|
||||
>
|
||||
{Object.keys(networkInfo).map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{key}
|
||||
</option>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
select
|
||||
label="Network Type"
|
||||
value={tableType}
|
||||
onChange={(e) => setTableType(e.target.value)}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
inputProps: { 'data-cy': `select-${STEP_ID}-type` },
|
||||
}}
|
||||
variant="outlined"
|
||||
>
|
||||
{Object.entries(NETWORK_TYPES).map(([key, value]) => (
|
||||
<option key={key} value={key}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<FormWithSchema cy={`${STEP_ID}`} fields={fields} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Content.propTypes = {
|
||||
dataTemplate: PropTypes.object,
|
||||
isUpdate: PropTypes.bool,
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} template - Service Template
|
||||
* @returns {object} - Step
|
||||
*/
|
||||
const Network = (template) => ({
|
||||
id: STEP_ID,
|
||||
label: T.Network,
|
||||
resolver: createNetworkSchema(),
|
||||
optionsValidate: { abortEarly: false },
|
||||
defaultDisabled: {
|
||||
condition: () => {
|
||||
const disableStep = !template?.dataTemplate?.TEMPLATE?.BODY?.networks
|
||||
|
||||
return disableStep
|
||||
},
|
||||
},
|
||||
content: () => Content(template),
|
||||
})
|
||||
Network.propTypes = {
|
||||
data: PropTypes.object,
|
||||
setFormData: PropTypes.func,
|
||||
}
|
||||
|
||||
export default Network
|
@ -0,0 +1,75 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { INPUT_TYPES, T } from 'client/constants'
|
||||
import { getValidationFromFields } from 'client/utils'
|
||||
import { mixed, string, object, array } from 'yup'
|
||||
import {
|
||||
VNetworkTemplatesTable,
|
||||
VNetworksTable,
|
||||
} from 'client/components/Tables'
|
||||
|
||||
/**
|
||||
* @param {string} pathPrefix - Field array path prefix
|
||||
* @param {string} tableType - Table type
|
||||
* @returns {Array} - List of fields
|
||||
*/
|
||||
export const createNetworkFields = (pathPrefix, tableType) => {
|
||||
const getPath = (fieldName) =>
|
||||
pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName
|
||||
|
||||
return [
|
||||
{
|
||||
name: getPath('extra'),
|
||||
label: T.Extra,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
cy: 'network',
|
||||
validation: string()
|
||||
.notRequired()
|
||||
.default(() => null),
|
||||
grid: { xs: 12, sm: 12, md: 12 },
|
||||
},
|
||||
{
|
||||
name: getPath('netid'),
|
||||
type: INPUT_TYPES.TABLE,
|
||||
cy: 'network',
|
||||
Table: () =>
|
||||
['existing', 'reserve'].includes(tableType)
|
||||
? VNetworksTable
|
||||
: VNetworkTemplatesTable,
|
||||
singleSelect: true,
|
||||
fieldProps: {
|
||||
preserveState: true,
|
||||
},
|
||||
validation: mixed()
|
||||
.required('Network ID missing or malformed!')
|
||||
.default(() => null),
|
||||
grid: { xs: 12, sm: 12, md: 12 },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pathPrefix - Path
|
||||
* @param {string} tableType - Type of table to display
|
||||
* @returns {object} - Yup schema
|
||||
*/
|
||||
export const createNetworkSchema = (pathPrefix, tableType) => {
|
||||
const fields = createNetworkFields(pathPrefix, tableType)
|
||||
|
||||
return object().shape({
|
||||
NETWORKS: array().of(object(getValidationFromFields(fields))),
|
||||
})
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 PropTypes from 'prop-types'
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import { T } from 'client/constants'
|
||||
import { generateFormFields } from './schema'
|
||||
import { getValidationFromFields } from 'client/utils'
|
||||
import { object } from 'yup'
|
||||
import { useState } from 'react'
|
||||
import { Box, Pagination, Stack } from '@mui/material'
|
||||
|
||||
const FIELDS_PER_PAGE = 10
|
||||
|
||||
export const STEP_ID = 'custom_attrs_values'
|
||||
|
||||
const Content = (formFields) => {
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
|
||||
const pageCount = Math.ceil(formFields?.length / FIELDS_PER_PAGE)
|
||||
|
||||
const fieldsForCurrentPage = formFields.slice(
|
||||
(currentPage - 1) * FIELDS_PER_PAGE,
|
||||
currentPage * FIELDS_PER_PAGE
|
||||
)
|
||||
|
||||
const handlePageChange = (_event, page) => {
|
||||
setCurrentPage(page)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<FormWithSchema
|
||||
id={STEP_ID}
|
||||
cy={`${STEP_ID}`}
|
||||
fields={fieldsForCurrentPage}
|
||||
/>
|
||||
<Stack spacing={2} alignItems="center" mt={2}>
|
||||
<Pagination
|
||||
count={pageCount}
|
||||
page={currentPage}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* UserInputs Service Template configuration.
|
||||
*
|
||||
* @param {object} data - Service Template data
|
||||
* @returns {object} UserInputs configuration step
|
||||
*/
|
||||
const UserInputs = (data) => {
|
||||
const customAttrs = data?.dataTemplate?.TEMPLATE?.BODY?.custom_attrs ?? {}
|
||||
|
||||
const userInputs = Object.entries(customAttrs)
|
||||
.map(([key, value]) => {
|
||||
const parts = value.split('|')
|
||||
if (parts.length < 5) return null
|
||||
|
||||
const [mandatory, type, description, rangeOrList, defaultValue] = parts
|
||||
|
||||
return {
|
||||
key,
|
||||
mandatory,
|
||||
type,
|
||||
description,
|
||||
rangeOrList,
|
||||
defaultValue,
|
||||
}
|
||||
})
|
||||
.filter((entry) => entry !== null)
|
||||
|
||||
const formFields = generateFormFields(userInputs)
|
||||
|
||||
const formSchema = object(getValidationFromFields(formFields))
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
label: T.UserInputs,
|
||||
resolver: formSchema,
|
||||
optionsValidate: { abortEarly: false },
|
||||
defaultDisabled: {
|
||||
condition: () => {
|
||||
const exists = !Object.keys(customAttrs ?? {})?.length > 0
|
||||
|
||||
return exists
|
||||
},
|
||||
},
|
||||
content: () => Content(formFields),
|
||||
}
|
||||
}
|
||||
|
||||
UserInputs.propTypes = {
|
||||
data: PropTypes.object,
|
||||
setFormData: PropTypes.func,
|
||||
}
|
||||
|
||||
Content.propTypes = { isUpdate: PropTypes.bool }
|
||||
|
||||
export default UserInputs
|
@ -0,0 +1,108 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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, number } from 'yup'
|
||||
import { INPUT_TYPES } from 'client/constants'
|
||||
|
||||
const getTypeProp = (type) => {
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
return INPUT_TYPES.SWITCH
|
||||
case 'text':
|
||||
case 'text64':
|
||||
case 'number':
|
||||
case 'numberfloat':
|
||||
return INPUT_TYPES.TEXT
|
||||
default:
|
||||
return INPUT_TYPES.TEXT
|
||||
}
|
||||
}
|
||||
|
||||
const getFieldProps = (type) => {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
case 'text64':
|
||||
return { type: 'text' }
|
||||
case 'number':
|
||||
case 'numberfloat':
|
||||
case 'range':
|
||||
case 'rangefloat':
|
||||
return { type: 'number' }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const getValidation = (type, mandatory) => {
|
||||
const isMandatory = mandatory === 'M'
|
||||
|
||||
switch (type) {
|
||||
case 'text':
|
||||
case 'text64':
|
||||
return isMandatory
|
||||
? string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined)
|
||||
: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
case 'number':
|
||||
case 'numberfloat':
|
||||
return isMandatory
|
||||
? number()
|
||||
.required()
|
||||
.default(() => undefined)
|
||||
: number()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
case 'boolean':
|
||||
return isMandatory
|
||||
? boolean().yesOrNo().required()
|
||||
: boolean().yesOrNo().notRequired()
|
||||
default:
|
||||
return isMandatory
|
||||
? string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined)
|
||||
: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const generateField = (input) => {
|
||||
const { key, type, defaultValue, mandatory } = input
|
||||
|
||||
return {
|
||||
name: key.toLowerCase(),
|
||||
label: key,
|
||||
type: getTypeProp(type),
|
||||
fieldProps: getFieldProps(type),
|
||||
validation: getValidation(type, mandatory),
|
||||
defaultValue: defaultValue,
|
||||
grid: { md: 12 },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} inputs - Array of user inputs
|
||||
* @returns {Array} - User input fields
|
||||
*/
|
||||
export const generateFormFields = (inputs) =>
|
||||
inputs.map((input) => generateField(input))
|
@ -0,0 +1,97 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import General, {
|
||||
STEP_ID as GENERAL_ID,
|
||||
} from 'client/components/Forms/ServiceTemplate/InstantiateForm/Steps/General'
|
||||
|
||||
import UserInputs, {
|
||||
STEP_ID as USERINPUTS_ID,
|
||||
} from 'client/components/Forms/ServiceTemplate/InstantiateForm/Steps/UserInputs'
|
||||
|
||||
import Network, {
|
||||
STEP_ID as NETWORK_ID,
|
||||
} from 'client/components/Forms/ServiceTemplate/InstantiateForm/Steps/Network'
|
||||
|
||||
import Charter, {
|
||||
STEP_ID as CHARTER_ID,
|
||||
} from 'client/components/Forms/ServiceTemplate/InstantiateForm/Steps/Charters'
|
||||
|
||||
import { createSteps, parseVmTemplateContents } from 'client/utils'
|
||||
|
||||
const Steps = createSteps([General, UserInputs, Network, Charter], {
|
||||
transformInitialValue: (ServiceTemplate, schema) => {
|
||||
const templatePath = ServiceTemplate?.TEMPLATE?.BODY
|
||||
const roles = templatePath?.roles ?? []
|
||||
|
||||
const networks = Object.entries(templatePath?.networks ?? {}).map(
|
||||
([key, value]) => {
|
||||
const extra = value.split(':').pop()
|
||||
|
||||
return {
|
||||
netid: null,
|
||||
extra: extra,
|
||||
name: key,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const knownTemplate = schema.cast({
|
||||
[GENERAL_ID]: {},
|
||||
[USERINPUTS_ID]: {},
|
||||
[NETWORK_ID]: { NETWORKS: networks },
|
||||
[CHARTER_ID]: {},
|
||||
})
|
||||
|
||||
return { ...knownTemplate, roles: roles }
|
||||
},
|
||||
|
||||
transformBeforeSubmit: (formData) => {
|
||||
const {
|
||||
[GENERAL_ID]: generalData,
|
||||
[USERINPUTS_ID]: userInputsData,
|
||||
[NETWORK_ID]: networkData,
|
||||
[CHARTER_ID]: charterData,
|
||||
} = formData
|
||||
|
||||
const formatTemplate = {
|
||||
custom_attrs_values: { ...userInputsData },
|
||||
networks_values: networkData?.NETWORKS?.map((network) => ({
|
||||
[network?.name]: {
|
||||
[['existing', 'reserve'].includes(network?.tableType)
|
||||
? 'id'
|
||||
: 'template_id']: network?.netid,
|
||||
},
|
||||
})),
|
||||
roles: formData?.roles?.map((role) => ({
|
||||
...role,
|
||||
vm_template_contents: parseVmTemplateContents(
|
||||
{
|
||||
vmTemplateContents: role?.vm_template_contents,
|
||||
customAttrsValues: userInputsData,
|
||||
},
|
||||
false,
|
||||
true
|
||||
),
|
||||
})),
|
||||
...(!!charterData?.SCHED_ACTION?.length && { ...charterData }),
|
||||
name: generalData?.NAME,
|
||||
}
|
||||
|
||||
return formatTemplate
|
||||
},
|
||||
})
|
||||
|
||||
export default Steps
|
@ -0,0 +1,16 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
export { default } from 'client/components/Forms/ServiceTemplate/InstantiateForm/Steps'
|
@ -0,0 +1,41 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { ReactElement } from 'react'
|
||||
import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC'
|
||||
import { CreateFormCallback, CreateStepsCallback } from 'client/utils/schema'
|
||||
|
||||
/**
|
||||
* @param {ConfigurationProps} configProps - Configuration
|
||||
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
|
||||
*/
|
||||
const CloneForm = (configProps) =>
|
||||
AsyncLoadForm({ formPath: 'ServiceTemplate/CloneForm' }, configProps)
|
||||
|
||||
/**
|
||||
* @param {ConfigurationProps} configProps - Configuration
|
||||
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
|
||||
*/
|
||||
const CreateForm = (configProps) =>
|
||||
AsyncLoadForm({ formPath: 'ServiceTemplate/CreateForm' }, configProps)
|
||||
|
||||
/**
|
||||
* @param {ConfigurationProps} configProps - Configuration
|
||||
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
|
||||
*/
|
||||
const InstantiateForm = (configProps) =>
|
||||
AsyncLoadForm({ formPath: 'ServiceTemplate/InstantiateForm' }, configProps)
|
||||
|
||||
export { CloneForm, CreateForm, InstantiateForm }
|
@ -23,7 +23,6 @@ import {
|
||||
PERIOD_TYPES,
|
||||
REPEAT_VALUES,
|
||||
SCHEDULE_TYPE,
|
||||
SERVER_CONFIG,
|
||||
T,
|
||||
TEMPLATE_SCHEDULE_TYPE_STRING,
|
||||
VM_ACTIONS_IN_CHARTER,
|
||||
@ -68,13 +67,13 @@ const DAYS_OF_WEEK = [
|
||||
T.Saturday,
|
||||
]
|
||||
|
||||
const getNow = () =>
|
||||
SERVER_CONFIG?.currentTimeZone
|
||||
? DateTime.now().setZone(SERVER_CONFIG.currentTimeZone)
|
||||
: DateTime.now()
|
||||
const getNow = () => DateTime.now()
|
||||
|
||||
const getTomorrow = () => getNow().plus({ days: 1 })
|
||||
|
||||
const getTomorrowAtMidnight = () =>
|
||||
getTomorrow().set({ hour: 12, minute: 0, second: 0 })
|
||||
|
||||
const getNextWeek = () => getNow().plus({ weeks: 1 })
|
||||
|
||||
const parseDateString = (_, originalValue) => {
|
||||
@ -240,7 +239,7 @@ const TIME_FIELD = {
|
||||
typeAction !== SCHEDULE_TYPE.RELATIVE ? schema.required() : schema
|
||||
),
|
||||
fieldProps: {
|
||||
defaultValue: getTomorrow(),
|
||||
defaultValue: getTomorrowAtMidnight(),
|
||||
minDateTime: getNow(),
|
||||
},
|
||||
}
|
||||
@ -283,24 +282,13 @@ const WEEKLY_FIELD = {
|
||||
addEmpty: false,
|
||||
getValue: (_, index) => String(index),
|
||||
}),
|
||||
fieldProps: ([_, repeat] = [], form) => {
|
||||
if (repeat === REPEAT_VALUES.DAILY) {
|
||||
const allDays = Array.from(
|
||||
{ length: DAYS_OF_WEEK.length },
|
||||
(__, index) => `${index}`
|
||||
)
|
||||
|
||||
form?.setValue('WEEKLY', allDays)
|
||||
}
|
||||
},
|
||||
htmlType: (_, context) => {
|
||||
const values = context?.getValues() || {}
|
||||
|
||||
return (
|
||||
!(
|
||||
values?.PERIODIC === SCHEDULE_TYPE.PERIODIC &&
|
||||
(values?.REPEAT === REPEAT_VALUES.WEEKLY ||
|
||||
values?.REPEAT === REPEAT_VALUES.DAILY)
|
||||
values?.REPEAT === REPEAT_VALUES.WEEKLY
|
||||
) && INPUT_TYPES.HIDDEN
|
||||
)
|
||||
},
|
||||
@ -309,10 +297,9 @@ const WEEKLY_FIELD = {
|
||||
.min(1)
|
||||
.default(() => context?.[DAYS_FIELD.name]?.split?.(',') ?? [])
|
||||
.when(REPEAT_FIELD.name, (repeatType, schema) =>
|
||||
repeatType === REPEAT_VALUES.WEEKLY ||
|
||||
repeatType === REPEAT_VALUES.DAILY
|
||||
? schema.required(T.DaysBetween0_6)
|
||||
: schema.strip()
|
||||
repeatType !== REPEAT_VALUES.WEEKLY
|
||||
? schema.strip()
|
||||
: schema.required(T.DaysBetween0_6)
|
||||
)
|
||||
.afterSubmit((value) => value?.join?.(','))
|
||||
),
|
||||
@ -418,21 +405,25 @@ const HOURLY_FIELD = {
|
||||
*/
|
||||
const DAYS_FIELD = {
|
||||
name: 'DAYS',
|
||||
validation: string().afterSubmit((_, { parent }) => {
|
||||
const isPeriodic = !!parent?.[PERIODIC_FIELD_NAME]
|
||||
const repeatType = parent?.[REPEAT_FIELD.name]
|
||||
validation: mixed()
|
||||
.notRequired()
|
||||
.transform((value, _originalValue, context) => {
|
||||
const isPeriodic = !!context?.parent?.[PERIODIC_FIELD_NAME]
|
||||
const repeatType = context?.parent?.[REPEAT_FIELD.name]
|
||||
|
||||
if (!isPeriodic) return undefined
|
||||
if (!isPeriodic) return undefined
|
||||
|
||||
const { WEEKLY, MONTHLY, YEARLY, HOURLY } = REPEAT_VALUES
|
||||
const { WEEKLY, MONTHLY, YEARLY, HOURLY } = REPEAT_VALUES
|
||||
|
||||
return {
|
||||
[WEEKLY]: parent?.[WEEKLY_FIELD.name],
|
||||
[MONTHLY]: parent?.[MONTHLY_FIELD.name],
|
||||
[YEARLY]: parent?.[YEARLY_FIELD.name],
|
||||
[HOURLY]: parent?.[HOURLY_FIELD.name],
|
||||
}[repeatType]
|
||||
}),
|
||||
const dayValues = {
|
||||
[WEEKLY]: context?.parent?.[WEEKLY_FIELD.name],
|
||||
[MONTHLY]: context?.parent?.[MONTHLY_FIELD.name],
|
||||
[YEARLY]: context?.parent?.[YEARLY_FIELD.name],
|
||||
[HOURLY]: context?.parent?.[HOURLY_FIELD.name],
|
||||
}
|
||||
|
||||
return dayValues[repeatType] ?? value
|
||||
}),
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
@ -452,21 +443,13 @@ const END_TYPE_FIELD = {
|
||||
getText: (value) => sentenceCase(value),
|
||||
getValue: (value) => END_TYPE_VALUES[value],
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.default(() => END_TYPE_VALUES.NEVER)
|
||||
.when(PERIODIC_FIELD_NAME, (typeAction, schema) =>
|
||||
typeAction === SCHEDULE_TYPE.PERIODIC ? schema.required() : schema
|
||||
),
|
||||
validation: mixed().notRequired(),
|
||||
}
|
||||
|
||||
/** @type {Field} End value field */
|
||||
const END_VALUE_FIELD = {
|
||||
name: 'END_VALUE',
|
||||
label: ([_, endType] = []) =>
|
||||
endType === END_TYPE_VALUES.REPETITION
|
||||
? T.NumberOfRepetitions
|
||||
: T.WhenDoYouWantThisActionToStop,
|
||||
label: T.WhenYouWantThatTheActionFinishes,
|
||||
dependOf: [PERIODIC_FIELD_NAME, END_TYPE_FIELD.name],
|
||||
type: ([typeAction, endType] = []) =>
|
||||
typeAction === SCHEDULE_TYPE.PERIODIC && endType === END_TYPE_VALUES.DATE
|
||||
@ -501,9 +484,7 @@ const END_VALUE_FIELD = {
|
||||
}
|
||||
),
|
||||
fieldProps: ([_, endType] = []) =>
|
||||
endType === END_TYPE_VALUES.DATE
|
||||
? { defaultValue: getNextWeek() }
|
||||
: { min: 1 },
|
||||
endType === END_TYPE_VALUES.DATE && { defaultValue: getNextWeek() },
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
|
@ -0,0 +1,232 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { Typography } from '@mui/material'
|
||||
import { AddCircledOutline, Trash, PlayOutline, Group } from 'iconoir-react'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import {
|
||||
// useCloneTemplateMutation,
|
||||
useRemoveServiceTemplateMutation,
|
||||
useChangeServiceTemplateOwnershipMutation,
|
||||
} from 'client/features/OneApi/serviceTemplate'
|
||||
|
||||
import { ChangeUserForm, ChangeGroupForm } from 'client/components/Forms/Vm'
|
||||
// import { CloneForm } from 'client/components/Forms/ServiceTemplate'
|
||||
import {
|
||||
createActions,
|
||||
GlobalAction,
|
||||
} from 'client/components/Tables/Enhanced/Utils'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
import { T, SERVICE_TEMPLATE_ACTIONS, RESOURCE_NAMES } from 'client/constants'
|
||||
|
||||
const ListServiceTemplateNames = ({ rows = [] }) =>
|
||||
rows?.map?.(({ id, original }) => {
|
||||
const { ID, NAME } = original
|
||||
|
||||
return (
|
||||
<Typography
|
||||
key={`service-template-${id}`}
|
||||
variant="inherit"
|
||||
component="span"
|
||||
display="block"
|
||||
>
|
||||
{`#${ID} ${NAME}`}
|
||||
</Typography>
|
||||
)
|
||||
})
|
||||
|
||||
const SubHeader = (rows) => <ListServiceTemplateNames rows={rows} />
|
||||
|
||||
const MessageToConfirmAction = (rows, description) => (
|
||||
<>
|
||||
<ListServiceTemplateNames rows={rows} />
|
||||
{description && <Translate word={description} />}
|
||||
<Translate word={T.DoYouWantProceed} />
|
||||
</>
|
||||
)
|
||||
|
||||
MessageToConfirmAction.displayName = 'MessageToConfirmAction'
|
||||
|
||||
/**
|
||||
* Generates the actions to operate resources on VM Template table.
|
||||
*
|
||||
* @returns {GlobalAction} - Actions
|
||||
*/
|
||||
const Actions = () => {
|
||||
const history = useHistory()
|
||||
const { view, getResourceView } = useViews()
|
||||
|
||||
// const [clone] = useCloneTemplateMutation()
|
||||
const [remove] = useRemoveServiceTemplateMutation()
|
||||
const [changeOwnership] = useChangeServiceTemplateOwnershipMutation()
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
createActions({
|
||||
filters: getResourceView(RESOURCE_NAMES.SERVICE_TEMPLATE)?.actions,
|
||||
actions: [
|
||||
{
|
||||
accessor: SERVICE_TEMPLATE_ACTIONS.CREATE_DIALOG,
|
||||
tooltip: T.Create,
|
||||
icon: AddCircledOutline,
|
||||
action: () => history.push(PATH.TEMPLATE.SERVICES.CREATE),
|
||||
},
|
||||
{
|
||||
accessor: SERVICE_TEMPLATE_ACTIONS.INSTANTIATE_DIALOG,
|
||||
tooltip: T.Instantiate,
|
||||
icon: PlayOutline,
|
||||
selected: { max: 1 },
|
||||
action: (rows) => {
|
||||
const template = rows?.[0]?.original ?? {}
|
||||
const path = PATH.TEMPLATE.SERVICES.INSTANTIATE
|
||||
|
||||
history.push(path, template)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: SERVICE_TEMPLATE_ACTIONS.UPDATE_DIALOG,
|
||||
label: T.Update,
|
||||
tooltip: T.Update,
|
||||
selected: { max: 1 },
|
||||
color: 'secondary',
|
||||
action: (rows) => {
|
||||
const serviceTemplate = rows?.[0]?.original ?? {}
|
||||
const path = PATH.TEMPLATE.SERVICES.CREATE
|
||||
|
||||
history.push(path, serviceTemplate)
|
||||
},
|
||||
},
|
||||
// {
|
||||
// accessor: SERVICE_TEMPLATE_ACTIONS.CLONE,
|
||||
// label: T.Clone,
|
||||
// tooltip: T.Clone,
|
||||
// selected: true,
|
||||
// color: 'secondary',
|
||||
// options: [
|
||||
// {
|
||||
// dialogProps: {
|
||||
// title: (rows) => {
|
||||
// const isMultiple = rows?.length > 1
|
||||
// const { ID, NAME } = rows?.[0]?.original ?? {}
|
||||
|
||||
// return [
|
||||
// Tr(
|
||||
// isMultiple ? T.CloneSeveralTemplates : T.CloneTemplate
|
||||
// ),
|
||||
// !isMultiple && `#${ID} ${NAME}`,
|
||||
// ]
|
||||
// .filter(Boolean)
|
||||
// .join(' - ')
|
||||
// },
|
||||
// dataCy: 'modal-clone',
|
||||
// },
|
||||
// form: (rows) => {
|
||||
// const names = rows?.map(({ original }) => original?.NAME)
|
||||
// const stepProps = { isMultiple: names.length > 1 }
|
||||
// const initialValues = { name: `Copy of ${names?.[0]}` }
|
||||
|
||||
// return CloneForm({ stepProps, initialValues })
|
||||
// },
|
||||
// onSubmit:
|
||||
// (rows) =>
|
||||
// async ({ prefix, name, image } = {}) => {
|
||||
// const serviceTemplates = rows?.map?.(
|
||||
// ({ original: { ID, NAME } = {} }) =>
|
||||
// // overwrite all names with prefix+NAME
|
||||
// ({
|
||||
// id: ID,
|
||||
// name: prefix ? `${prefix} ${NAME}` : name,
|
||||
// image,
|
||||
// })
|
||||
// )
|
||||
|
||||
// await Promise.all(serviceTemplates.map(clone))
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
tooltip: T.Ownership,
|
||||
icon: Group,
|
||||
selected: true,
|
||||
color: 'secondary',
|
||||
dataCy: 'template-ownership',
|
||||
options: [
|
||||
{
|
||||
accessor: SERVICE_TEMPLATE_ACTIONS.CHANGE_OWNER,
|
||||
name: T.ChangeOwner,
|
||||
dialogProps: {
|
||||
title: T.ChangeOwner,
|
||||
subheader: SubHeader,
|
||||
dataCy: `modal-${SERVICE_TEMPLATE_ACTIONS.CHANGE_OWNER}`,
|
||||
},
|
||||
form: ChangeUserForm,
|
||||
onSubmit: (rows) => (newOwnership) => {
|
||||
rows?.map?.(({ original }) =>
|
||||
changeOwnership({ id: original?.ID, ...newOwnership })
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: SERVICE_TEMPLATE_ACTIONS.CHANGE_GROUP,
|
||||
name: T.ChangeGroup,
|
||||
dialogProps: {
|
||||
title: T.ChangeGroup,
|
||||
subheader: SubHeader,
|
||||
dataCy: `modal-${SERVICE_TEMPLATE_ACTIONS.CHANGE_GROUP}`,
|
||||
},
|
||||
form: ChangeGroupForm,
|
||||
onSubmit: (rows) => async (newOwnership) => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(
|
||||
ids.map((id) => changeOwnership({ id, ...newOwnership }))
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
accessor: SERVICE_TEMPLATE_ACTIONS.DELETE,
|
||||
tooltip: T.Delete,
|
||||
icon: Trash,
|
||||
selected: { min: 1 },
|
||||
color: 'error',
|
||||
options: [
|
||||
{
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: T.Delete,
|
||||
dataCy: `modal-${SERVICE_TEMPLATE_ACTIONS.DELETE}`,
|
||||
children: MessageToConfirmAction,
|
||||
},
|
||||
onSubmit: (rows) => async () => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(ids.map((id) => remove({ id })))
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[view]
|
||||
)
|
||||
}
|
||||
|
||||
export default Actions
|
260
src/fireedge/src/client/components/Tables/Services/actions.js
Normal file
260
src/fireedge/src/client/components/Tables/Services/actions.js
Normal file
@ -0,0 +1,260 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { Typography } from '@mui/material'
|
||||
import { AddCircledOutline, Trash, Group, RefreshCircular } from 'iconoir-react'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import {
|
||||
useRemoveServiceMutation,
|
||||
useChangeServiceOwnerMutation,
|
||||
useRecoverServiceMutation,
|
||||
} from 'client/features/OneApi/service'
|
||||
|
||||
import { ChangeUserForm, ChangeGroupForm } from 'client/components/Forms/Vm'
|
||||
import {
|
||||
createActions,
|
||||
GlobalAction,
|
||||
} from 'client/components/Tables/Enhanced/Utils'
|
||||
|
||||
import ServiceTemplatesTable from 'client/components/Tables/ServiceTemplates'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
import { T, SERVICE_TEMPLATE_ACTIONS, RESOURCE_NAMES } from 'client/constants'
|
||||
|
||||
const useTableStyles = makeStyles({
|
||||
body: { gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))' },
|
||||
})
|
||||
|
||||
const ListVmTemplateNames = ({ rows = [] }) =>
|
||||
rows?.map?.(({ id, original }) => {
|
||||
const { ID, NAME } = original
|
||||
|
||||
return (
|
||||
<Typography
|
||||
key={`vm-template-${id}`}
|
||||
variant="inherit"
|
||||
component="span"
|
||||
display="block"
|
||||
>
|
||||
{`#${ID} ${NAME}`}
|
||||
</Typography>
|
||||
)
|
||||
})
|
||||
|
||||
const SubHeader = (rows) => <ListVmTemplateNames rows={rows} />
|
||||
|
||||
const MessageToConfirmAction = (rows, description) => (
|
||||
<>
|
||||
<ListVmTemplateNames rows={rows} />
|
||||
{description && <Translate word={description} />}
|
||||
<Translate word={T.DoYouWantProceed} />
|
||||
</>
|
||||
)
|
||||
|
||||
MessageToConfirmAction.displayName = 'MessageToConfirmAction'
|
||||
|
||||
/**
|
||||
* Generates the actions to operate resources on VM Template table.
|
||||
*
|
||||
* @returns {GlobalAction} - Actions
|
||||
*/
|
||||
const Actions = () => {
|
||||
const history = useHistory()
|
||||
const { view, getResourceView } = useViews()
|
||||
|
||||
const [remove] = useRemoveServiceMutation()
|
||||
const [recover] = useRecoverServiceMutation()
|
||||
const [changeOwnership] = useChangeServiceOwnerMutation()
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
createActions({
|
||||
filters: getResourceView(RESOURCE_NAMES.SERVICE_TEMPLATE)?.actions,
|
||||
actions: [
|
||||
{
|
||||
accessor: SERVICE_TEMPLATE_ACTIONS.CREATE_DIALOG,
|
||||
tooltip: T.Create,
|
||||
icon: AddCircledOutline,
|
||||
options: [
|
||||
{
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: T.Instantiate,
|
||||
children: () => {
|
||||
const classes = useTableStyles()
|
||||
|
||||
const redirectToInstantiate = (template) =>
|
||||
history.push(PATH.TEMPLATE.SERVICES.INSTANTIATE, template)
|
||||
|
||||
return (
|
||||
<ServiceTemplatesTable
|
||||
disableGlobalSort
|
||||
disableRowSelect
|
||||
classes={classes}
|
||||
onRowClick={redirectToInstantiate}
|
||||
/>
|
||||
)
|
||||
},
|
||||
fixedWidth: true,
|
||||
fixedHeight: true,
|
||||
handleAccept: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tooltip: T.Ownership,
|
||||
icon: Group,
|
||||
selected: { min: 1 },
|
||||
color: 'secondary',
|
||||
dataCy: 'template-ownership',
|
||||
options: [
|
||||
{
|
||||
accessor: SERVICE_TEMPLATE_ACTIONS.CHANGE_OWNER,
|
||||
name: T.ChangeOwner,
|
||||
dialogProps: {
|
||||
title: T.ChangeOwner,
|
||||
subheader: SubHeader,
|
||||
dataCy: `modal-${SERVICE_TEMPLATE_ACTIONS.CHANGE_OWNER}`,
|
||||
},
|
||||
form: ChangeUserForm,
|
||||
onSubmit: (rows) => (newOwnership) => {
|
||||
rows?.map?.(({ original }) =>
|
||||
changeOwnership({ id: original?.ID, ...newOwnership })
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: SERVICE_TEMPLATE_ACTIONS.CHANGE_GROUP,
|
||||
name: T.ChangeGroup,
|
||||
dialogProps: {
|
||||
title: T.ChangeGroup,
|
||||
subheader: SubHeader,
|
||||
dataCy: `modal-${SERVICE_TEMPLATE_ACTIONS.CHANGE_GROUP}`,
|
||||
},
|
||||
form: ChangeGroupForm,
|
||||
onSubmit: (rows) => async (newOwnership) => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(
|
||||
ids.map((id) => changeOwnership({ id, ...newOwnership }))
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tooltip: T.Recover,
|
||||
icon: RefreshCircular,
|
||||
selected: true,
|
||||
color: 'secondary',
|
||||
dataCy: 'service-recover',
|
||||
options: [
|
||||
{
|
||||
accessor: SERVICE_TEMPLATE_ACTIONS.RECOVER,
|
||||
name: T.RecoverService,
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
dataCy: `modal-${SERVICE_TEMPLATE_ACTIONS.RECOVER}`,
|
||||
title: (rows) => {
|
||||
const isMultiple = rows?.length > 1
|
||||
const { ID, NAME } = rows?.[0]?.original ?? {}
|
||||
|
||||
return [
|
||||
Tr(
|
||||
isMultiple ? T.RecoverSeveralServices : T.RecoverService
|
||||
),
|
||||
!isMultiple && `#${ID} ${NAME}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' - ')
|
||||
},
|
||||
},
|
||||
onSubmit: (rows) => async () => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(ids.map((id) => recover({ id })))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: SERVICE_TEMPLATE_ACTIONS.RECOVER,
|
||||
name: `${T.Recover} ${T.Delete.toLowerCase()}`,
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
dataCy: `modal-${SERVICE_TEMPLATE_ACTIONS.RECOVER}`,
|
||||
title: (rows) => {
|
||||
const isMultiple = rows?.length > 1
|
||||
const { ID, NAME } = rows?.[0]?.original ?? {}
|
||||
|
||||
return [
|
||||
Tr(
|
||||
isMultiple ? T.RecoverSeveralServices : T.RecoverService
|
||||
),
|
||||
!isMultiple && `#${ID} ${NAME}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' - ')
|
||||
},
|
||||
},
|
||||
onSubmit: (rows) => async () => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(
|
||||
ids.map((id) => recover({ id, delete: true }))
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
accessor: SERVICE_TEMPLATE_ACTIONS.DELETE,
|
||||
tooltip: T.Delete,
|
||||
icon: Trash,
|
||||
selected: true,
|
||||
color: 'error',
|
||||
options: [
|
||||
{
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
dataCy: `modal-${SERVICE_TEMPLATE_ACTIONS.DELETE}`,
|
||||
title: (rows) => {
|
||||
const isMultiple = rows?.length > 1
|
||||
const { ID, NAME } = rows?.[0]?.original ?? {}
|
||||
|
||||
return [
|
||||
Tr(
|
||||
isMultiple ? T.DeleteSeveralTemplates : T.DeleteTemplate
|
||||
),
|
||||
!isMultiple && `#${ID} ${NAME}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' - ')
|
||||
},
|
||||
},
|
||||
onSubmit: (rows) => async () => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(ids.map((id) => remove({ id })))
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[view]
|
||||
)
|
||||
}
|
||||
|
||||
export default Actions
|
@ -37,6 +37,8 @@ const VmsTable = (props) => {
|
||||
host,
|
||||
backupjobs,
|
||||
backupjobsState,
|
||||
filterData = [],
|
||||
filterLoose = true,
|
||||
...rest
|
||||
} = props ?? {}
|
||||
|
||||
@ -94,6 +96,9 @@ const VmsTable = (props) => {
|
||||
// This is for return data without filters
|
||||
return true
|
||||
})
|
||||
?.filter(({ ID }) =>
|
||||
filterData?.length ? filterData?.includes(ID) : filterLoose
|
||||
)
|
||||
?.filter(({ STATE }) => VM_STATES[STATE]?.name !== STATES.DONE) ?? [],
|
||||
}),
|
||||
})
|
||||
|
@ -0,0 +1,132 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { useState, Component } from 'react'
|
||||
import { Box, Button, Menu, MenuItem, IconButton } from '@mui/material'
|
||||
import PropTypes from 'prop-types'
|
||||
import { NavArrowDown } from 'iconoir-react'
|
||||
|
||||
/**
|
||||
* @param {object} root0 - Props
|
||||
* @param {object} root0.items - Button props
|
||||
* @param {object} root0.options - Button styles
|
||||
* @returns {Component} - Custom Button
|
||||
*/
|
||||
export const ButtonGenerator = ({ items, options = {} }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const open = Boolean(anchorEl)
|
||||
|
||||
const handleClick = (event, onClick) => {
|
||||
if (Array.isArray(items)) {
|
||||
setAnchorEl(event.currentTarget)
|
||||
} else if (onClick) {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
if (Array.isArray(items)) {
|
||||
return (
|
||||
<Box>
|
||||
{options?.button?.type === 'icon' ? (
|
||||
<IconButton
|
||||
aria-controls="customized-menu"
|
||||
aria-haspopup="true"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleClick}
|
||||
{...options?.button}
|
||||
sx={{
|
||||
...options?.button?.sx,
|
||||
}}
|
||||
>
|
||||
{options?.button?.icon ?? <NavArrowDown />}
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button
|
||||
aria-controls="customized-menu"
|
||||
aria-haspopup="true"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleClick}
|
||||
endIcon={items.length > 1 ? <NavArrowDown /> : null}
|
||||
{...options?.button}
|
||||
>
|
||||
{options?.button?.title || ''}
|
||||
</Button>
|
||||
)}
|
||||
<Menu
|
||||
id="customized-menu"
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
{...options?.menu}
|
||||
>
|
||||
{items.map(({ name, onClick }, index) => (
|
||||
<MenuItem
|
||||
key={index}
|
||||
onClick={() => {
|
||||
onClick()
|
||||
handleClose()
|
||||
}}
|
||||
{...options?.menuItem}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
return options?.singleButton?.type === 'icon' ? (
|
||||
<IconButton
|
||||
aria-controls="customized-menu"
|
||||
aria-haspopup="true"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleClick}
|
||||
{...options?.button}
|
||||
sx={{
|
||||
...options?.singleButton?.sx,
|
||||
}}
|
||||
>
|
||||
{options?.singleButton?.icon ?? <NavArrowDown />}
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={(event) => handleClick(event, items.onClick)}
|
||||
startIcon={items.icon ? <items.icon /> : null}
|
||||
{...options?.singleButton}
|
||||
sx={{
|
||||
...options?.singleButton?.sx,
|
||||
}}
|
||||
>
|
||||
{options?.singleButton?.title || items.name || ''}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ButtonGenerator.propTypes = {
|
||||
items: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
|
||||
options: PropTypes.object,
|
||||
}
|
@ -17,10 +17,14 @@ import { ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Stack } from '@mui/material'
|
||||
|
||||
import { useGetServiceQuery } from 'client/features/OneApi/service'
|
||||
import {
|
||||
useGetServiceQuery,
|
||||
useServiceAddActionMutation,
|
||||
} from 'client/features/OneApi/service'
|
||||
import { Permissions, Ownership } from 'client/components/Tabs/Common'
|
||||
import Information from 'client/components/Tabs/Service/Info/information'
|
||||
import { getActionsAvailable } from 'client/models/Helper'
|
||||
import { toSnakeCase } from 'client/utils'
|
||||
import { getActionsAvailable, permissionsToOctal } from 'client/models/Helper'
|
||||
|
||||
/**
|
||||
* Renders mainly information tab.
|
||||
@ -38,10 +42,49 @@ const ServiceInfoTab = ({ tabProps = {}, id }) => {
|
||||
} = tabProps
|
||||
|
||||
const { data: service = {} } = useGetServiceQuery({ id })
|
||||
const [addServiceAction] = useServiceAddActionMutation()
|
||||
|
||||
/* eslint-disable no-shadow */
|
||||
const changePermissions = ({ id, octet }) => {
|
||||
addServiceAction({
|
||||
id: id,
|
||||
perform: 'chmod',
|
||||
params: { octet: octet },
|
||||
})
|
||||
}
|
||||
/* eslint-enable no-shadow */
|
||||
|
||||
const changeOwnership = (newOwnership) => {
|
||||
addServiceAction({
|
||||
id: id,
|
||||
perform: 'chown',
|
||||
params: {
|
||||
group_id: newOwnership?.group || '-1',
|
||||
owner_id: newOwnership?.user || '-1',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const { UNAME, UID, GNAME, GID, PERMISSIONS = {} } = service
|
||||
|
||||
const getActions = (actions) => getActionsAvailable(actions)
|
||||
|
||||
const handleChangeOwnership = async (newOwnership) => {
|
||||
await changeOwnership({ id, ...newOwnership })
|
||||
}
|
||||
|
||||
const handleChangePermission = async (newPermission) => {
|
||||
const [key, value] = Object.entries(newPermission)[0]
|
||||
|
||||
const [member, permission] = toSnakeCase(key).toUpperCase().split('_')
|
||||
const fullPermissionName = `${member}_${permission[0]}`
|
||||
|
||||
const newPermissions = { ...PERMISSIONS, [fullPermissionName]: value }
|
||||
const octet = permissionsToOctal(newPermissions)
|
||||
|
||||
await changePermissions({ id, octet })
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
display="grid"
|
||||
@ -58,6 +101,7 @@ const ServiceInfoTab = ({ tabProps = {}, id }) => {
|
||||
{permissionsPanel?.enabled && (
|
||||
<Permissions
|
||||
actions={getActions(permissionsPanel?.actions)}
|
||||
handleEdit={handleChangePermission}
|
||||
ownerUse={PERMISSIONS.OWNER_U}
|
||||
ownerManage={PERMISSIONS.OWNER_M}
|
||||
ownerAdmin={PERMISSIONS.OWNER_A}
|
||||
@ -72,6 +116,7 @@ const ServiceInfoTab = ({ tabProps = {}, id }) => {
|
||||
{ownershipPanel?.enabled && (
|
||||
<Ownership
|
||||
actions={getActions(ownershipPanel?.actions)}
|
||||
handleEdit={handleChangeOwnership}
|
||||
userId={UID}
|
||||
userName={UNAME}
|
||||
groupId={GID}
|
||||
|
@ -21,7 +21,8 @@ import { List } from 'client/components/Tabs/Common'
|
||||
import { StatusCircle, StatusChip } from 'client/components/Status'
|
||||
import { getState } from 'client/models/Service'
|
||||
import { timeToString, booleanToString } from 'client/models/Helper'
|
||||
import { T, Service } from 'client/constants'
|
||||
import { T, Service, VM_TEMPLATE_ACTIONS } from 'client/constants'
|
||||
import { useServiceAddActionMutation } from 'client/features/OneApi/service'
|
||||
|
||||
/**
|
||||
* Renders mainly information tab.
|
||||
@ -32,6 +33,7 @@ import { T, Service } from 'client/constants'
|
||||
* @returns {ReactElement} Information tab
|
||||
*/
|
||||
const InformationPanel = ({ service = {}, actions }) => {
|
||||
const [addServiceAction] = useServiceAddActionMutation()
|
||||
const {
|
||||
ID,
|
||||
NAME,
|
||||
@ -46,11 +48,29 @@ const InformationPanel = ({ service = {}, actions }) => {
|
||||
},
|
||||
} = service || {}
|
||||
|
||||
const handleRename = async (_, newName) => {
|
||||
await renameTemplate({ id: ID, name: newName })
|
||||
}
|
||||
|
||||
const renameTemplate = ({ id, name }) => {
|
||||
addServiceAction({
|
||||
id: id,
|
||||
perform: 'rename',
|
||||
params: { name },
|
||||
})
|
||||
}
|
||||
|
||||
const { name: stateName, color: stateColor } = getState(service)
|
||||
|
||||
const info = [
|
||||
{ name: T.ID, value: ID, dataCy: 'id' },
|
||||
{ name: T.Name, value: NAME, dataCy: 'name' },
|
||||
{
|
||||
name: T.Name,
|
||||
value: NAME,
|
||||
canEdit: actions?.includes(VM_TEMPLATE_ACTIONS.RENAME),
|
||||
dataCy: 'name',
|
||||
handleEdit: handleRename,
|
||||
},
|
||||
{
|
||||
name: T.State,
|
||||
value: (
|
||||
|
@ -13,49 +13,230 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement } from 'react'
|
||||
import { useState, useMemo, Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
|
||||
import {
|
||||
Stack,
|
||||
Typography,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
Pagination,
|
||||
Box,
|
||||
InputLabel,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
ButtonGroup,
|
||||
Button,
|
||||
} from '@mui/material'
|
||||
import { useGetServiceQuery } from 'client/features/OneApi/service'
|
||||
import { timeFromMilliseconds } from 'client/models/Helper'
|
||||
import { Service, SERVICE_LOG_SEVERITY } from 'client/constants'
|
||||
import { SERVICE_LOG_SEVERITY } from 'client/constants'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
const severityDisplayNames = {
|
||||
I: 'Info',
|
||||
D: 'Debug',
|
||||
E: 'Error',
|
||||
}
|
||||
|
||||
const sortOptions = {
|
||||
TIME: 'Time',
|
||||
SEVERITY: 'Severity',
|
||||
MESSAGE: 'Message',
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders log tab.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {string} props.id - Service id
|
||||
* @returns {ReactElement} Log tab
|
||||
* @param {object} root - Params
|
||||
* @param {number} root.id - Service Instance ID
|
||||
* @returns {Component} - Logging component
|
||||
*/
|
||||
const LogTab = ({ id }) => {
|
||||
const { data: service = {} } = useGetServiceQuery({ id })
|
||||
|
||||
/** @type {Service} */
|
||||
const { TEMPLATE: { BODY: { log = [] } = {} } = {} } = service
|
||||
|
||||
return (
|
||||
<Stack gap="0.5em" p="1em" bgcolor="background.default">
|
||||
{log?.map(({ severity, message, timestamp } = {}) => {
|
||||
const time = timeFromMilliseconds(+timestamp)
|
||||
const isError = severity === SERVICE_LOG_SEVERITY.ERROR
|
||||
const [filter, setFilter] = useState('')
|
||||
const [severityFilter, setSeverityFilter] = useState('ALL')
|
||||
const [sortType, setSortType] = useState('TIME')
|
||||
const [sortAscending, setSortAscending] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
return (
|
||||
<Typography
|
||||
key={`message-${timestamp}`}
|
||||
noWrap
|
||||
variant="body2"
|
||||
color={isError ? 'error' : 'textPrimary'}
|
||||
const sortedLogs = useMemo(() => {
|
||||
const sorted = [...log]
|
||||
sorted.sort((a, b) => {
|
||||
switch (sortType) {
|
||||
case 'SEVERITY':
|
||||
return sortAscending
|
||||
? a.severity.localeCompare(b.severity)
|
||||
: b.severity.localeCompare(a.severity)
|
||||
case 'MESSAGE':
|
||||
return sortAscending
|
||||
? a.message.localeCompare(b.message)
|
||||
: b.message.localeCompare(a.message)
|
||||
default:
|
||||
return sortAscending
|
||||
? a.timestamp - b.timestamp
|
||||
: b.timestamp - a.timestamp
|
||||
}
|
||||
})
|
||||
|
||||
return sorted
|
||||
}, [sortType, sortAscending])
|
||||
|
||||
const filteredLogs = useMemo(
|
||||
() =>
|
||||
sortedLogs.filter(
|
||||
({ severity, message }) =>
|
||||
(severityFilter === 'ALL' || severity === severityFilter) &&
|
||||
message.toLowerCase().includes(filter.toLowerCase())
|
||||
),
|
||||
[sortedLogs, severityFilter, filter]
|
||||
)
|
||||
|
||||
const paginatedLogs = useMemo(
|
||||
() => filteredLogs.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
||||
[filteredLogs, page]
|
||||
)
|
||||
|
||||
const handleSortChange = (key) => {
|
||||
if (key === sortType) {
|
||||
setSortAscending(!sortAscending)
|
||||
} else {
|
||||
setSortType(key)
|
||||
setSortAscending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
p: '1em',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={2}
|
||||
mb={2}
|
||||
alignItems="center"
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Search"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel id="severity-select-label">Severity</InputLabel>
|
||||
<Select
|
||||
labelId="severity-select-label"
|
||||
label="Severity"
|
||||
value={severityFilter}
|
||||
onChange={(e) => setSeverityFilter(e.target.value)}
|
||||
>
|
||||
{`${time.toFormat('ff')} [${severity}] ${message}`}
|
||||
</Typography>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
<MenuItem value="ALL">All Severities</MenuItem>
|
||||
{Object.entries(severityDisplayNames).map(([key, name]) => (
|
||||
<MenuItem key={key} value={key}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<ButtonGroup size="small" variant="outlined">
|
||||
{Object.entries(sortOptions).map(([key, name]) => (
|
||||
<Button
|
||||
key={key}
|
||||
onClick={() => handleSortChange(key)}
|
||||
variant={sortType === key ? 'contained' : 'outlined'}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={sortAscending}
|
||||
onChange={() => setSortAscending(!sortAscending)}
|
||||
/>
|
||||
}
|
||||
label={sortAscending ? 'Asc' : 'Desc'}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 2,
|
||||
borderRadius: '4px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
maxHeight: '85%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
spacing={1}
|
||||
sx={{
|
||||
overflowY: 'auto',
|
||||
maxHeight: 'calc(100% - 70px)',
|
||||
}}
|
||||
>
|
||||
{paginatedLogs.map(({ severity, message, timestamp }, index) => {
|
||||
const time = timeFromMilliseconds(+timestamp)
|
||||
|
||||
return (
|
||||
<Typography
|
||||
key={`message-${timestamp}-${index}`}
|
||||
variant="body2"
|
||||
sx={{
|
||||
color:
|
||||
severity === SERVICE_LOG_SEVERITY.ERROR
|
||||
? 'error.dark'
|
||||
: severity === SERVICE_LOG_SEVERITY.INFO
|
||||
? 'info.dark'
|
||||
: 'text.primary',
|
||||
fontWeight:
|
||||
severity === SERVICE_LOG_SEVERITY.ERROR ? 'bold' : 'normal',
|
||||
backgroundColor:
|
||||
severity === SERVICE_LOG_SEVERITY.ERROR
|
||||
? 'error.light'
|
||||
: severity === SERVICE_LOG_SEVERITY.INFO
|
||||
? 'info.light'
|
||||
: 'success.light',
|
||||
p: 1,
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
{`${time.toFormat('ff')} [${
|
||||
severityDisplayNames[severity]
|
||||
}] ${message}`}
|
||||
</Typography>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Pagination
|
||||
sx={{ mt: 2, mb: 2 }}
|
||||
count={Math.ceil(filteredLogs.length / PAGE_SIZE)}
|
||||
page={page}
|
||||
onChange={(_e, value) => setPage(value)}
|
||||
variant="outlined"
|
||||
shape="rounded"
|
||||
size="medium"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
LogTab.propTypes = { tabProps: PropTypes.object, id: PropTypes.string }
|
||||
LogTab.displayName = 'RolesTab'
|
||||
LogTab.displayName = 'Roles'
|
||||
|
||||
export default LogTab
|
||||
|
@ -13,18 +13,38 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, memo, useMemo } from 'react'
|
||||
import { ReactElement, memo, useState, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Link as RouterLink, generatePath } from 'react-router-dom'
|
||||
import { Box, Typography, Link, CircularProgress } from '@mui/material'
|
||||
|
||||
import { useGetServiceQuery } from 'client/features/OneApi/service'
|
||||
import { useGetTemplatesQuery } from 'client/features/OneApi/vmTemplate'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T, ServiceTemplateRole } from 'client/constants'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
import { ButtonGenerator } from 'client/components/Tabs/Service/ButtonGenerator'
|
||||
import {
|
||||
useGetServiceQuery,
|
||||
useServiceAddRoleMutation,
|
||||
useServiceRoleActionMutation,
|
||||
useServiceScaleRoleMutation,
|
||||
} from 'client/features/OneApi/service'
|
||||
|
||||
const COLUMNS = [T.Name, T.Cardinality, T.VMTemplate, T.Parents]
|
||||
import { VmsTable } from 'client/components/Tables'
|
||||
import VmActions from 'client/components/Tables/Vms/actions'
|
||||
import { StatusCircle } from 'client/components/Status'
|
||||
import { getRoleState } from 'client/models/Service'
|
||||
import { Box, Dialog, Typography } from '@mui/material'
|
||||
import { Content as RoleAddDialog } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/RoleConfig'
|
||||
import { ScaleDialog } from 'client/components/Tabs/Service/ScaleDialog'
|
||||
import {
|
||||
Plus,
|
||||
Trash,
|
||||
SystemShut,
|
||||
TransitionRight,
|
||||
NavArrowDown,
|
||||
Refresh,
|
||||
PlayOutline,
|
||||
} from 'iconoir-react'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
// Filters actions based on the data-cy key
|
||||
const filterActions = ['vm_resume', 'vm-manage', 'vm-host', 'vm-terminate']
|
||||
|
||||
/**
|
||||
* Renders template tab.
|
||||
@ -34,30 +54,371 @@ const COLUMNS = [T.Name, T.Cardinality, T.VMTemplate, T.Parents]
|
||||
* @returns {ReactElement} Roles tab
|
||||
*/
|
||||
const RolesTab = ({ id }) => {
|
||||
const { enqueueError, enqueueSuccess, enqueueInfo } = useGeneralApi()
|
||||
// wrapper
|
||||
const createApiCallback = (apiFunction) => async (params) => {
|
||||
const payload = { id, ...params }
|
||||
const response = await apiFunction(payload)
|
||||
|
||||
return response
|
||||
}
|
||||
// api calls
|
||||
const [addRole] = useServiceAddRoleMutation()
|
||||
const [addRoleAction] = useServiceRoleActionMutation()
|
||||
const [scaleRole] = useServiceScaleRoleMutation()
|
||||
// api handlers
|
||||
const handleAddRole = createApiCallback(addRole)
|
||||
|
||||
const handleAddRoleAction = async (actionType) => {
|
||||
for (const roleIdx of selectedRoles) {
|
||||
const roleName = roles?.[roleIdx]?.name
|
||||
|
||||
try {
|
||||
enqueueInfo(`Starting '${actionType}' action on role: ${roleName}`)
|
||||
|
||||
await createApiCallback(addRoleAction)({
|
||||
perform: actionType,
|
||||
role: roleName,
|
||||
})
|
||||
|
||||
enqueueSuccess(`Action '${actionType}' completed on role: ${roleName}`)
|
||||
} catch (error) {
|
||||
enqueueError(
|
||||
`Action '${actionType}' failed on role: ${roleName}. Error: ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleScaleRole = createApiCallback(scaleRole)
|
||||
|
||||
const [activeRole, setActiveRole] = useState({ idx: null, roleName: null })
|
||||
|
||||
const [isAddRoleOpen, setAddRoleOpen] = useState(false)
|
||||
const [isScaleDialogOpen, setScaleDialogOpen] = useState(false)
|
||||
|
||||
const { data: template = {} } = useGetServiceQuery({ id })
|
||||
const [selectedRoles, setSelectedRoles] = useState([])
|
||||
const filteredActions = VmActions()?.filter((action) =>
|
||||
filterActions?.includes(action?.dataCy)
|
||||
)
|
||||
const roles = template?.TEMPLATE?.BODY?.roles || []
|
||||
|
||||
const roleVms = useMemo(
|
||||
() =>
|
||||
roles?.reduce((acc, role) => {
|
||||
acc[role?.name] = role?.nodes?.map((node) => node?.vm_info?.VM.ID)
|
||||
|
||||
return acc
|
||||
}, {}),
|
||||
[roles]
|
||||
)
|
||||
|
||||
/* eslint-disable react/prop-types */
|
||||
const AddRoleDialog = ({ open, onClose }) => (
|
||||
<Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
|
||||
<RoleAddDialog
|
||||
standaloneModal
|
||||
standaloneModalCallback={(params) => {
|
||||
handleAddRole(params)
|
||||
onClose()
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
/* eslint-enable react/prop-types */
|
||||
|
||||
const handleRoleClick = (idx, role, event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
setSelectedRoles((prevSelectedRoles) =>
|
||||
prevSelectedRoles.includes(idx)
|
||||
? prevSelectedRoles.filter((roleIdx) => roleIdx !== idx)
|
||||
: [...prevSelectedRoles, idx]
|
||||
)
|
||||
} else {
|
||||
setSelectedRoles((prevSelectedRoles) => {
|
||||
if (prevSelectedRoles.length > 1 || !prevSelectedRoles.includes(idx)) {
|
||||
return [idx]
|
||||
}
|
||||
|
||||
return prevSelectedRoles
|
||||
})
|
||||
|
||||
setActiveRole((prevActiveRole) =>
|
||||
prevActiveRole.idx === idx
|
||||
? { idx: null, roleName: null }
|
||||
: { idx: idx, roleName: role.name }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenAddRole = () => {
|
||||
setAddRoleOpen(true)
|
||||
}
|
||||
|
||||
const handleCloseAddRole = () => {
|
||||
setAddRoleOpen(false)
|
||||
}
|
||||
|
||||
const handleOpenScale = () => {
|
||||
setScaleDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleCloseScale = () => {
|
||||
setScaleDialogOpen(false)
|
||||
}
|
||||
|
||||
const isSelected = (idx) => selectedRoles.includes(idx)
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns="repeat(4, 1fr)"
|
||||
padding="1em"
|
||||
bgcolor="background.default"
|
||||
>
|
||||
{COLUMNS.map((col) => (
|
||||
<Typography key={col} noWrap variant="subtitle1" padding="0.5em">
|
||||
<Translate word={col} />
|
||||
</Typography>
|
||||
))}
|
||||
<Box display="flex" flexDirection="column" padding="1em" width="100%">
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
alignItems="stretch"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
marginBottom="2em"
|
||||
>
|
||||
<Box display="flex" gap="2em" marginRight="2em">
|
||||
<>
|
||||
<ButtonGenerator
|
||||
items={{
|
||||
name: 'Add Role',
|
||||
onClick: handleOpenAddRole,
|
||||
icon: Plus,
|
||||
}}
|
||||
options={{
|
||||
singleButton: {
|
||||
sx: {
|
||||
fontSize: '0.95rem',
|
||||
padding: '6px 8px',
|
||||
minWidth: '80px',
|
||||
minHeight: '30px',
|
||||
maxHeight: '40px',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
'data-cy': 'AddRole',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AddRoleDialog open={isAddRoleOpen} onClose={handleCloseAddRole} />
|
||||
</>
|
||||
|
||||
<ButtonGenerator
|
||||
items={{
|
||||
name: 'Scale',
|
||||
onClick: handleOpenScale,
|
||||
icon: Plus,
|
||||
}}
|
||||
options={{
|
||||
singleButton: {
|
||||
disabled: !selectedRoles?.length > 0,
|
||||
sx: {
|
||||
fontSize: '0.95rem',
|
||||
padding: '6px 12px',
|
||||
minWidth: '80px',
|
||||
minHeight: '30px',
|
||||
maxHeight: '40px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ScaleDialog
|
||||
open={isScaleDialogOpen}
|
||||
onClose={handleCloseScale}
|
||||
onScale={handleScaleRole}
|
||||
roleName={roles?.[selectedRoles?.[0]]?.name}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" gap="1em">
|
||||
<ButtonGenerator
|
||||
items={{
|
||||
onClick: () => handleAddRoleAction('resume'),
|
||||
}}
|
||||
options={{
|
||||
singleButton: {
|
||||
disabled: !selectedRoles?.length > 0,
|
||||
startIcon: <PlayOutline />,
|
||||
sx: {
|
||||
fontSize: 20,
|
||||
padding: '0px 8px',
|
||||
},
|
||||
title: null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonGenerator
|
||||
items={[
|
||||
{
|
||||
name: 'Suspend',
|
||||
onClick: () =>
|
||||
handleAddRoleAction({
|
||||
perform: 'suspend',
|
||||
role: roles?.[selectedRoles?.[0]]?.name,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'Poweroff',
|
||||
|
||||
onClick: () => handleAddRoleAction('poweroff'),
|
||||
},
|
||||
{
|
||||
name: 'Poweroff Hard',
|
||||
|
||||
onClick: () => handleAddRoleAction('poweroff-hard'),
|
||||
},
|
||||
]}
|
||||
options={{
|
||||
button: {
|
||||
disabled: !selectedRoles?.length > 0,
|
||||
startIcon: <SystemShut />,
|
||||
endIcon: <NavArrowDown />,
|
||||
sx: {
|
||||
fontSize: 20,
|
||||
padding: '8px 16px',
|
||||
},
|
||||
title: null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonGenerator
|
||||
items={[
|
||||
{
|
||||
name: 'Stop',
|
||||
|
||||
onClick: () => handleAddRoleAction('stop'),
|
||||
},
|
||||
{
|
||||
name: 'Undeploy',
|
||||
onClick: () => handleAddRoleAction('undeploy'),
|
||||
},
|
||||
{
|
||||
name: 'Undeploy Hard',
|
||||
onClick: () => handleAddRoleAction('undeploy-hard'),
|
||||
},
|
||||
]}
|
||||
options={{
|
||||
button: {
|
||||
disabled: !selectedRoles?.length > 0,
|
||||
startIcon: <TransitionRight />,
|
||||
endIcon: <NavArrowDown />,
|
||||
sx: {
|
||||
fontSize: 20,
|
||||
padding: '8px 16px',
|
||||
},
|
||||
title: null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonGenerator
|
||||
items={[
|
||||
{
|
||||
name: 'Reboot',
|
||||
onClick: () => handleAddRoleAction('reboot'),
|
||||
},
|
||||
{
|
||||
name: 'Reboot Hard',
|
||||
onClick: () => handleAddRoleAction('reboot-hard'),
|
||||
},
|
||||
]}
|
||||
options={{
|
||||
button: {
|
||||
disabled: !selectedRoles?.length > 0,
|
||||
startIcon: <Refresh />,
|
||||
endIcon: <NavArrowDown />,
|
||||
sx: {
|
||||
fontSize: 20,
|
||||
padding: '8px 16px',
|
||||
marginRight: '1em',
|
||||
},
|
||||
title: null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonGenerator
|
||||
items={[
|
||||
{
|
||||
name: 'Terminate',
|
||||
onClick: () => handleAddRoleAction('terminate'),
|
||||
},
|
||||
{
|
||||
name: 'Terminate Hard',
|
||||
onClick: () => handleAddRoleAction('terminate-hard'),
|
||||
},
|
||||
]}
|
||||
options={{
|
||||
button: {
|
||||
disabled: !selectedRoles?.length > 0,
|
||||
startIcon: <Trash />,
|
||||
endIcon: <NavArrowDown />,
|
||||
sx: {
|
||||
fontSize: 20,
|
||||
padding: '8px 16px',
|
||||
marginLeft: '2em',
|
||||
},
|
||||
title: null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{roles.map((role, idx) => (
|
||||
<Box
|
||||
key={`role-${role.name ?? idx}`}
|
||||
display="contents"
|
||||
padding="0.5em"
|
||||
// hover except for the circular progress component
|
||||
sx={{ '&:hover > *:not(span)': { bgcolor: 'action.hover' } }}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
padding="0.75em"
|
||||
marginY="0.25em"
|
||||
sx={(theme) => ({
|
||||
'&:hover': { bgcolor: 'action.hover', boxShadow: 3 },
|
||||
boxShadow: 1,
|
||||
transition: 'all 0.1s ease-in-out',
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
borderRadius: '8px',
|
||||
bgcolor: 'background.paper',
|
||||
border: `2px solid ${
|
||||
isSelected(idx)
|
||||
? theme.palette.grey[600]
|
||||
: theme.palette.grey[400]
|
||||
}`,
|
||||
})}
|
||||
onClick={(event) => handleRoleClick(idx, role, event)}
|
||||
>
|
||||
<RoleComponent role={role} />
|
||||
<RoleComponent
|
||||
role={role}
|
||||
selected={isSelected(idx)}
|
||||
status={role?.state}
|
||||
/>
|
||||
|
||||
{activeRole.idx === idx && (
|
||||
<Box
|
||||
padding="20px"
|
||||
marginLeft="20px"
|
||||
paddingTop="10px"
|
||||
border="1px solid rgba(0, 0, 0, 0.12)"
|
||||
borderRadius="4px"
|
||||
width="calc(100% - 20px)"
|
||||
height="calc(100% - 20px)"
|
||||
minHeight="500px"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<VmsTable
|
||||
globalActions={filteredActions}
|
||||
filterData={roleVms?.[activeRole?.roleName]}
|
||||
filterLoose={false}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
@ -67,52 +428,49 @@ const RolesTab = ({ id }) => {
|
||||
RolesTab.propTypes = { tabProps: PropTypes.object, id: PropTypes.string }
|
||||
RolesTab.displayName = 'RolesTab'
|
||||
|
||||
const RoleComponent = memo(({ role }) => {
|
||||
/** @type {ServiceTemplateRole} */
|
||||
const { name, cardinality, vm_template: templateId, parents } = role
|
||||
|
||||
const { data: template, isLoading } = useGetTemplatesQuery(undefined, {
|
||||
selectFromResult: ({ data = [], ...restOfQuery }) => ({
|
||||
data: data.find((item) => +item.ID === +templateId),
|
||||
...restOfQuery,
|
||||
}),
|
||||
})
|
||||
|
||||
const linkToVmTemplate = useMemo(
|
||||
() => generatePath(PATH.TEMPLATE.VMS.DETAIL, { id: templateId }),
|
||||
[templateId]
|
||||
)
|
||||
|
||||
const commonProps = { noWrap: true, variant: 'subtitle2', padding: '0.5em' }
|
||||
const RoleComponent = memo(({ role, selected, status }) => {
|
||||
const { name, cardinality, vm_template: templateId } = role
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography {...commonProps} data-cy="name">
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography {...commonProps} data-cy="cardinality">
|
||||
{cardinality}
|
||||
</Typography>
|
||||
{isLoading ? (
|
||||
<CircularProgress color="secondary" size={20} />
|
||||
) : (
|
||||
<Link
|
||||
{...commonProps}
|
||||
color="secondary"
|
||||
component={RouterLink}
|
||||
to={linkToVmTemplate}
|
||||
>
|
||||
{`#${template?.ID} ${template?.NAME}`}
|
||||
</Link>
|
||||
)}
|
||||
<Typography {...commonProps} data-cy="parents">
|
||||
{parents?.join?.()}
|
||||
</Typography>
|
||||
</>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
alignItems="flex-start"
|
||||
padding="0.5em"
|
||||
marginY="0.25em"
|
||||
borderRadius="8px"
|
||||
boxShadow={1}
|
||||
sx={(theme) => ({
|
||||
'&:hover': { boxShadow: 2, transition: 'box-shadow 0.3s' },
|
||||
bgcolor: theme.palette.background,
|
||||
filter: selected ? 'brightness(100%)' : 'brightness(90%)',
|
||||
})}
|
||||
>
|
||||
<Box mr={2} mt={-1} alignSelf="start">
|
||||
<StatusCircle
|
||||
color={getRoleState(status)?.color || 'red'}
|
||||
tooltip={getRoleState(status)?.name}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="subtitle1" mb={1}>
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography variant="body1" mb={1}>
|
||||
VM Template ID: {templateId}
|
||||
</Typography>
|
||||
<Typography variant="body1">Cardinality: {cardinality}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
RoleComponent.propTypes = { role: PropTypes.object }
|
||||
RoleComponent.propTypes = {
|
||||
role: PropTypes.object,
|
||||
selected: PropTypes.bool,
|
||||
status: PropTypes.number,
|
||||
}
|
||||
RoleComponent.displayName = 'RoleComponent'
|
||||
|
||||
export default RolesTab
|
||||
|
102
src/fireedge/src/client/components/Tabs/Service/ScaleDialog.js
Normal file
102
src/fireedge/src/client/components/Tabs/Service/ScaleDialog.js
Normal file
@ -0,0 +1,102 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import {
|
||||
Dialog,
|
||||
TextField,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
|
||||
/**
|
||||
* Dialog for scaling the number of VMs.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {boolean} props.open - Determines if the dialog is open
|
||||
* @param {Function} props.onClose - Function to call when closing the dialog
|
||||
* @param {Function} props.onScale - API call when the form is submitted
|
||||
* @param {string} props.roleName - Selected role name
|
||||
* @returns {Component} The scale dialog component
|
||||
*/
|
||||
export const ScaleDialog = ({ open, onClose, onScale, roleName }) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm()
|
||||
|
||||
/**
|
||||
* Handles the form submission.
|
||||
*
|
||||
* @param {object} data - The data from the form
|
||||
*/
|
||||
const onSubmit = (data) => {
|
||||
onScale({
|
||||
action: {
|
||||
force: data?.force,
|
||||
cardinality: data?.numberOfVms,
|
||||
role_name: roleName,
|
||||
},
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} fullWidth maxWidth="sm">
|
||||
<Box padding={4}>
|
||||
<Typography variant="h6">Scale</Typography>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<TextField
|
||||
margin="normal"
|
||||
fullWidth
|
||||
label="Number of VMs"
|
||||
type="number"
|
||||
{...register('numberOfVms', {
|
||||
required: 'Number of VMs is required',
|
||||
})}
|
||||
error={!!errors.numberOfVms}
|
||||
helperText={errors.numberOfVms?.message}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Switch {...register('force')} />}
|
||||
label="Force"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: 2, fontSize: '1rem' }}
|
||||
>
|
||||
Scale
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
ScaleDialog.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onScale: PropTypes.func.isRequired,
|
||||
roleName: PropTypes.string,
|
||||
}
|
@ -216,4 +216,5 @@ export * from 'client/constants/vdc'
|
||||
export * from 'client/constants/vm'
|
||||
export * from 'client/constants/vmGroup'
|
||||
export * from 'client/constants/vmTemplate'
|
||||
export * from 'client/constants/serviceTemplate'
|
||||
export * from 'client/constants/zone'
|
||||
|
64
src/fireedge/src/client/constants/serviceTemplate.js
Normal file
64
src/fireedge/src/client/constants/serviceTemplate.js
Normal file
@ -0,0 +1,64 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 * as ACTIONS from 'client/constants/actions'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { LockInfo, Permissions } from 'client/constants/common'
|
||||
|
||||
/**
|
||||
* @typedef ServiceTemplate
|
||||
* @property {string|number} ID - Id
|
||||
* @property {string} NAME - Name
|
||||
* @property {string|number} UID - User id
|
||||
* @property {string|number} GID - Group id
|
||||
* @property {string} UNAME - User name
|
||||
* @property {string} GNAME - Group name
|
||||
* @property {Permissions} PERMISSIONS - Permissions
|
||||
* @property {LockInfo} [LOCK] - Lock information
|
||||
* @property {string|number} REGTIME - Registration time
|
||||
* @property {object} TEMPLATE - Template information
|
||||
* @property {string} [TEMPLATE.CONTEXT] - Context
|
||||
* @property {string} [TEMPLATE.VCENTER_CCR_REF] - vCenter information
|
||||
* @property {string} [TEMPLATE.VCENTER_INSTANCE_ID] - vCenter information
|
||||
* @property {string} [TEMPLATE.VCENTER_TEMPLATE_REF] - vCenter information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef ServiceTemplateFeatures
|
||||
* @property {boolean} hide_cpu - If `true`, the CPU fields is hidden
|
||||
* @property {false|number} cpu_factor - Scales CPU by VCPU
|
||||
* - ``1``: Set it to 1 to tie CPU and vCPU
|
||||
* - ``{number}``: CPU = cpu_factor * VCPU
|
||||
* - ``{false}``: False to not scale the CPU
|
||||
*/
|
||||
|
||||
export const SERVICE_TEMPLATE_ACTIONS = {
|
||||
CREATE_DIALOG: 'create_dialog',
|
||||
IMPORT_DIALOG: 'import_dialog',
|
||||
UPDATE_DIALOG: 'update_dialog',
|
||||
INSTANTIATE_DIALOG: 'instantiate_dialog',
|
||||
CREATE_APP_DIALOG: 'create_app_dialog',
|
||||
CLONE: 'clone',
|
||||
DELETE: 'delete',
|
||||
RECOVER: 'recover',
|
||||
LOCK: 'lock',
|
||||
UNLOCK: 'unlock',
|
||||
SHARE: 'share',
|
||||
UNSHARE: 'unshare',
|
||||
|
||||
RENAME: ACTIONS.RENAME,
|
||||
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
|
||||
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
|
||||
}
|
@ -44,6 +44,7 @@ module.exports = {
|
||||
Accept: 'Accept',
|
||||
Active: 'Active',
|
||||
Add: 'Add',
|
||||
AddRole: 'Add role',
|
||||
AddAction: 'Add action',
|
||||
Append: 'Append',
|
||||
Attach: 'Attach',
|
||||
@ -81,6 +82,7 @@ module.exports = {
|
||||
CreateProvision: 'Create Provision',
|
||||
CreateSecurityGroup: 'Create Security Group',
|
||||
CreateServiceTemplate: 'Create Service Template',
|
||||
InstantiateServiceTemplate: 'Instantiate Service Template',
|
||||
CreateUser: 'Create User',
|
||||
UpdateUser: 'Update User',
|
||||
CreateVirtualNetwork: 'Create Virtual Network',
|
||||
@ -141,6 +143,8 @@ module.exports = {
|
||||
RebootHard: 'Reboot hard',
|
||||
Recover: 'Recover',
|
||||
RecoverSeveralVMs: 'Recover several VMs',
|
||||
RecoverSeveralServices: 'Recover several services',
|
||||
RecoverService: 'Recover service',
|
||||
RecoverSomething: 'Recover: %s',
|
||||
Recreate: 'Recreate',
|
||||
Refresh: 'Refresh',
|
||||
@ -226,6 +230,7 @@ module.exports = {
|
||||
/* Scheduling */
|
||||
Action: 'Action',
|
||||
ScheduleAction: 'Schedule action',
|
||||
ScheduledActions: 'Scheduled Actions',
|
||||
ScheduleActionType: 'Schedule action type',
|
||||
Charter: 'Charter',
|
||||
OneTimeAction: 'One time',
|
||||
@ -324,6 +329,7 @@ module.exports = {
|
||||
|
||||
/* steps form */
|
||||
AdvancedOptions: 'Advanced options',
|
||||
AdvancedParams: 'Advanced Parameters',
|
||||
/* steps form - flow */
|
||||
ApplicationOverview: 'Application overview',
|
||||
WhereWillItRun: 'Where will it run?',
|
||||
@ -427,8 +433,10 @@ module.exports = {
|
||||
/* sections - network */
|
||||
Network: 'Network',
|
||||
Networks: 'Networks',
|
||||
VirtualNetwork: 'Virtual Network',
|
||||
VirtualNetworks: 'Virtual Networks',
|
||||
VirtualNetwork: 'Virtual network',
|
||||
VirtualNetworks: 'Virtual networks',
|
||||
RoleNetwork: 'Role Network',
|
||||
RoleNetworks: 'Role Networks',
|
||||
NetworkTemplate: 'Network Template',
|
||||
NetworkTemplates: 'Network Templates',
|
||||
NetworkTopology: 'Network topology',
|
||||
@ -648,6 +656,7 @@ module.exports = {
|
||||
/* tabs */
|
||||
Drivers: 'Drivers',
|
||||
General: 'General',
|
||||
Extra: 'Extra',
|
||||
Information: 'Information',
|
||||
Placement: 'Placement',
|
||||
|
||||
@ -860,6 +869,7 @@ module.exports = {
|
||||
When creating several VMs, the wildcard %%idx will be
|
||||
replaced with a number starting from 0`,
|
||||
NumberOfInstances: 'Number of instances',
|
||||
NumberOfVms: 'Number of VMs',
|
||||
MakeTemplateAvailableForVROnly:
|
||||
'Make this template available for Virtual Router machines only',
|
||||
VmOnHoldState: 'Start VM on hold state',
|
||||
@ -927,6 +937,11 @@ module.exports = {
|
||||
Policy: 'Policy',
|
||||
VmAffinity: 'VM Affinity',
|
||||
RolesAffinity: 'Roles Affinity',
|
||||
RoleElasticity: 'Role Elasticity',
|
||||
ElasticityPolicy: 'Elasticity Policy',
|
||||
ElasticityPolicies: 'Elasticity Policies',
|
||||
ScheduledPolicy: 'Scheduled Policy',
|
||||
ScheduledPolicies: 'Scheduled Policies',
|
||||
AssociateToVMGroup: 'Associate VM to a VM Group',
|
||||
/* VM Template schema - vCenter */
|
||||
vCenterTemplateRef: 'vCenter Template reference',
|
||||
@ -1206,6 +1221,9 @@ module.exports = {
|
||||
be unshared with the group's users. Permission changed: GROUP USE`,
|
||||
|
||||
/* Service Template schema */
|
||||
WaitVmsReport:
|
||||
'Consider VMs as running only when they report READY status via OneGate',
|
||||
ServiceAutoDelete: 'Automatic deletion of service when all VMs terminated',
|
||||
/* Service Template schema - general */
|
||||
Strategy: 'Strategy',
|
||||
ShutdownAction: 'Shutdown action',
|
||||
|
@ -777,10 +777,10 @@ export const VM_ACTIONS = {
|
||||
// STORAGE
|
||||
ATTACH_DISK: 'attach_disk',
|
||||
DETACH_DISK: 'detach_disk',
|
||||
SNAPSHOT_DISK_CREATE: 'disk-snapshot-create',
|
||||
SNAPSHOT_DISK_RENAME: 'disk-snapshot-rename',
|
||||
SNAPSHOT_DISK_REVERT: 'disk-snapshot-revert',
|
||||
SNAPSHOT_DISK_DELETE: 'disk-snapshot-delete',
|
||||
SNAPSHOT_DISK_CREATE: 'snapshot-disk-create',
|
||||
SNAPSHOT_DISK_RENAME: 'snapshot-disk-rename',
|
||||
SNAPSHOT_DISK_REVERT: 'snapshot-disk-revert',
|
||||
SNAPSHOT_DISK_DELETE: 'snapshot-disk-delete',
|
||||
RESIZE_DISK: 'resize_disk',
|
||||
DISK_SAVEAS: 'disk_saveas',
|
||||
|
||||
|
101
src/fireedge/src/client/containers/ServiceTemplates/Create.js
Normal file
101
src/fireedge/src/client/containers/ServiceTemplates/Create.js
Normal file
@ -0,0 +1,101 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { ReactElement } from 'react'
|
||||
import { useHistory, useLocation } from 'react-router'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import {
|
||||
useUpdateServiceTemplateMutation,
|
||||
useCreateServiceTemplateMutation,
|
||||
useGetServiceTemplateQuery,
|
||||
} from 'client/features/OneApi/serviceTemplate'
|
||||
import { useGetVMGroupsQuery } from 'client/features/OneApi/vmGroup'
|
||||
import { useGetHostsQuery } from 'client/features/OneApi/host'
|
||||
import { useGetImagesQuery } from 'client/features/OneApi/image'
|
||||
import { useGetUsersQuery } from 'client/features/OneApi/user'
|
||||
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
|
||||
|
||||
import {
|
||||
DefaultFormStepper,
|
||||
SkeletonStepsForm,
|
||||
} from 'client/components/FormStepper'
|
||||
import { CreateForm } from 'client/components/Forms/ServiceTemplate'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
* Displays the creation or modification form to a VM Template.
|
||||
*
|
||||
* @returns {ReactElement} VM Template form
|
||||
*/
|
||||
function CreateServiceTemplate() {
|
||||
const history = useHistory()
|
||||
const { state: { ID: templateId, NAME } = {} } = useLocation()
|
||||
|
||||
const { enqueueSuccess } = useGeneralApi()
|
||||
const [update] = useUpdateServiceTemplateMutation()
|
||||
const [allocate] = useCreateServiceTemplateMutation()
|
||||
|
||||
const { data: apiTemplateData } = useGetServiceTemplateQuery({
|
||||
id: templateId,
|
||||
})
|
||||
|
||||
const dataTemplate = _.cloneDeep(apiTemplateData)
|
||||
|
||||
useGetVMGroupsQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetHostsQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetImagesQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetUsersQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetDatastoresQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
|
||||
const onSubmit = async (jsonTemplate) => {
|
||||
try {
|
||||
if (!templateId) {
|
||||
const newTemplateId = await allocate({
|
||||
template: jsonTemplate,
|
||||
}).unwrap()
|
||||
history.push(PATH.TEMPLATE.SERVICES.LIST)
|
||||
enqueueSuccess(`Service Template created - #${newTemplateId} ${NAME}`)
|
||||
} else {
|
||||
await update({
|
||||
id: templateId,
|
||||
template: jsonTemplate,
|
||||
merge: false,
|
||||
}).unwrap()
|
||||
history.push(PATH.TEMPLATE.SERVICES.LIST)
|
||||
enqueueSuccess(`Service Template updated - #${templateId} ${NAME}`)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return templateId && !dataTemplate ? (
|
||||
<SkeletonStepsForm />
|
||||
) : (
|
||||
<CreateForm
|
||||
initialValues={dataTemplate}
|
||||
stepProps={{
|
||||
dataTemplate,
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
fallback={<SkeletonStepsForm />}
|
||||
>
|
||||
{(config) => <DefaultFormStepper {...config} />}
|
||||
</CreateForm>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateServiceTemplate
|
@ -0,0 +1,90 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { ReactElement } from 'react'
|
||||
import { useHistory, useLocation } from 'react-router'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import {
|
||||
useDeployServiceTemplateMutation,
|
||||
useGetServiceTemplateQuery,
|
||||
} from 'client/features/OneApi/serviceTemplate'
|
||||
import { useGetVMGroupsQuery } from 'client/features/OneApi/vmGroup'
|
||||
import { useGetHostsQuery } from 'client/features/OneApi/host'
|
||||
import { useGetImagesQuery } from 'client/features/OneApi/image'
|
||||
import { useGetUsersQuery } from 'client/features/OneApi/user'
|
||||
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
|
||||
|
||||
import {
|
||||
DefaultFormStepper,
|
||||
SkeletonStepsForm,
|
||||
} from 'client/components/FormStepper'
|
||||
import { InstantiateForm } from 'client/components/Forms/ServiceTemplate'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
* Displays the instantiate form for a Service Template.
|
||||
*
|
||||
* @returns {ReactElement} Service Template form
|
||||
*/
|
||||
function CreateServiceTemplate() {
|
||||
const history = useHistory()
|
||||
const { state: { ID: templateId, NAME } = {} } = useLocation()
|
||||
|
||||
const { enqueueSuccess } = useGeneralApi()
|
||||
const [instantiate] = useDeployServiceTemplateMutation()
|
||||
|
||||
const { data: apiTemplateData } = useGetServiceTemplateQuery({
|
||||
id: templateId,
|
||||
})
|
||||
|
||||
const dataTemplate = _.cloneDeep(apiTemplateData)
|
||||
|
||||
useGetVMGroupsQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetHostsQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetImagesQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetUsersQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
useGetDatastoresQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
|
||||
const onSubmit = async (jsonTemplate) => {
|
||||
try {
|
||||
await instantiate({
|
||||
id: templateId,
|
||||
template: jsonTemplate,
|
||||
}).unwrap()
|
||||
history.push(PATH.INSTANCE.SERVICES.LIST)
|
||||
enqueueSuccess(`Service Template initiated - #${templateId} ${NAME}`)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return templateId && !dataTemplate ? (
|
||||
<SkeletonStepsForm />
|
||||
) : (
|
||||
<InstantiateForm
|
||||
initialValues={dataTemplate}
|
||||
stepProps={{
|
||||
dataTemplate,
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
fallback={<SkeletonStepsForm />}
|
||||
>
|
||||
{(config) => <DefaultFormStepper {...config} />}
|
||||
</InstantiateForm>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateServiceTemplate
|
@ -26,6 +26,7 @@ import { ServiceTemplatesTable } from 'client/components/Tables'
|
||||
import ServiceTemplateTabs from 'client/components/Tabs/ServiceTemplate'
|
||||
import SplitPane from 'client/components/SplitPane'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
import ServiceTemplateActions from 'client/components/Tables/ServiceTemplates/actions'
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
@ -38,6 +39,7 @@ import { T } from 'client/constants'
|
||||
*/
|
||||
function ServiceTemplates() {
|
||||
const [selectedRows, onSelectedRowsChange] = useState(() => [])
|
||||
const actions = ServiceTemplateActions()
|
||||
|
||||
const hasSelectedRows = selectedRows?.length > 0
|
||||
const moreThanOneSelected = selectedRows?.length > 1
|
||||
@ -46,7 +48,10 @@ function ServiceTemplates() {
|
||||
<SplitPane gridTemplateRows="1fr auto 1fr">
|
||||
{({ getGridProps, GutterComponent }) => (
|
||||
<Box height={1} {...(hasSelectedRows && getGridProps())}>
|
||||
<ServiceTemplatesTable onSelectedRowsChange={onSelectedRowsChange} />
|
||||
<ServiceTemplatesTable
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
globalActions={actions}
|
||||
/>
|
||||
|
||||
{hasSelectedRows && (
|
||||
<>
|
||||
@ -56,6 +61,7 @@ function ServiceTemplates() {
|
||||
) : (
|
||||
<InfoTabs
|
||||
id={selectedRows[0]?.original?.ID}
|
||||
template={selectedRows[0]?.original}
|
||||
gotoPage={selectedRows[0]?.gotoPage}
|
||||
unselect={() => selectedRows[0]?.toggleRowSelected(false)}
|
||||
/>
|
||||
|
@ -26,6 +26,7 @@ import { ServicesTable } from 'client/components/Tables'
|
||||
import ServiceTabs from 'client/components/Tabs/Service'
|
||||
import SplitPane from 'client/components/SplitPane'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
import ServiceActions from 'client/components/Tables/Services/actions'
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
@ -38,6 +39,7 @@ import { T } from 'client/constants'
|
||||
*/
|
||||
function Services() {
|
||||
const [selectedRows, onSelectedRowsChange] = useState(() => [])
|
||||
const actions = ServiceActions()
|
||||
|
||||
const hasSelectedRows = selectedRows?.length > 0
|
||||
const moreThanOneSelected = selectedRows?.length > 1
|
||||
@ -46,7 +48,10 @@ function Services() {
|
||||
<SplitPane gridTemplateRows="1fr auto 1fr">
|
||||
{({ getGridProps, GutterComponent }) => (
|
||||
<Box height={1} {...(hasSelectedRows && getGridProps())}>
|
||||
<ServicesTable onSelectedRowsChange={onSelectedRowsChange} />
|
||||
<ServicesTable
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
globalActions={actions}
|
||||
/>
|
||||
|
||||
{hasSelectedRows && (
|
||||
<>
|
||||
@ -55,7 +60,7 @@ function Services() {
|
||||
<GroupedTags tags={selectedRows} />
|
||||
) : (
|
||||
<InfoTabs
|
||||
id={selectedRows[0]?.original?.ID}
|
||||
service={selectedRows[0]?.original}
|
||||
gotoPage={selectedRows[0]?.gotoPage}
|
||||
unselect={() => selectedRows[0]?.toggleRowSelected(false)}
|
||||
/>
|
||||
|
@ -19,6 +19,7 @@ import { oneApi, DOCUMENT, DOCUMENT_POOL } from 'client/features/OneApi'
|
||||
import {
|
||||
updateResourceOnPool,
|
||||
removeResourceOnPool,
|
||||
updateOwnershipOnResource,
|
||||
} from 'client/features/OneApi/common'
|
||||
import { Service } from 'client/constants'
|
||||
|
||||
@ -92,6 +93,284 @@ const serviceApi = oneApi.injectEndpoints({
|
||||
}
|
||||
},
|
||||
}),
|
||||
removeService: builder.mutation({
|
||||
/**
|
||||
* Removes a service instance.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @param {string} params.id - Service id
|
||||
* @returns {Service} Remove service id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: (params) => {
|
||||
const name = Actions.SERVICE_DELETE
|
||||
const command = { name, ...Commands[name] }
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: [SERVICE_POOL],
|
||||
}),
|
||||
|
||||
changeServiceOwner: builder.mutation({
|
||||
/**
|
||||
* Changes a service instance owner.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @param {string} params.id - Service id
|
||||
* @param {string} params.user - Service id
|
||||
* @param {string} params.group - Service id
|
||||
* @returns {Service} Updated service id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: ({ user = '-1', group = '-1', ...params }) => {
|
||||
params.action = {
|
||||
perform: 'chown',
|
||||
params: { owner_id: user, group_id: group },
|
||||
}
|
||||
const name = Actions.SERVICE_ADD_ACTION
|
||||
const command = { name, ...Commands[name] }
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: SERVICE, id }],
|
||||
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const patchServiceTemplate = dispatch(
|
||||
serviceApi.util.updateQueryData(
|
||||
'getService',
|
||||
{ id: params.id },
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
const patchServiceTemplates = dispatch(
|
||||
serviceApi.util.updateQueryData(
|
||||
'getServices',
|
||||
undefined,
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchServiceTemplate.undo()
|
||||
patchServiceTemplates.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
recoverService: builder.mutation({
|
||||
/**
|
||||
* Tries to recover a failed service.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @param {string} params.id - Service id
|
||||
* @returns {Service} Recovered service id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: (params) => {
|
||||
params.action = {
|
||||
perform: 'recover',
|
||||
...(params?.delete && { params: { delete: true } }),
|
||||
}
|
||||
const name = Actions.SERVICE_ADD_ACTION
|
||||
const command = { name, ...Commands[name] }
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, id) => [{ type: SERVICE, id }, SERVICE_POOL],
|
||||
}),
|
||||
|
||||
serviceAddRole: builder.mutation({
|
||||
/**
|
||||
* Tries to add a role to a service.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @param {string} params.id - Service id
|
||||
* @param {string} params.role - Role config
|
||||
* @returns {Service} Service id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: (params) => {
|
||||
params.action = {
|
||||
perform: 'add_role',
|
||||
...(params?.role && { params: { role: params.role } }),
|
||||
}
|
||||
const name = Actions.SERVICE_ADD_ROLE
|
||||
const command = { name, ...Commands[name] }
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, params) => [{ type: 'SERVICE', id: params.id }],
|
||||
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const patchServiceTemplate = dispatch(
|
||||
serviceApi.util.updateQueryData(
|
||||
'getService',
|
||||
{ id: params.id },
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
const patchServiceTemplates = dispatch(
|
||||
serviceApi.util.updateQueryData(
|
||||
'getServices',
|
||||
undefined,
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchServiceTemplate.undo()
|
||||
patchServiceTemplates.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
serviceScaleRole: builder.mutation({
|
||||
/**
|
||||
* Tries to scale a role.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @param {string} params.id - Service id
|
||||
* @param {string} params.role - Role config
|
||||
* @returns {Service} Service id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: (params) => {
|
||||
const name = Actions.SERVICE_ADD_SCALE
|
||||
const command = { name, ...Commands[name] }
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, params) => [{ type: 'SERVICE', id: params.id }],
|
||||
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const patchServiceTemplate = dispatch(
|
||||
serviceApi.util.updateQueryData(
|
||||
'getService',
|
||||
{ id: params.id },
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
const patchServiceTemplates = dispatch(
|
||||
serviceApi.util.updateQueryData(
|
||||
'getServices',
|
||||
undefined,
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchServiceTemplate.undo()
|
||||
patchServiceTemplates.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
serviceRoleAction: builder.mutation({
|
||||
/**
|
||||
* Tries to perform a role action.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @param {string} params.id - Service id
|
||||
* @param {string} params.role - Role config
|
||||
* @returns {Service} Service id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: (params) => {
|
||||
params.action = {
|
||||
perform: params?.perform,
|
||||
params: {
|
||||
...params.params,
|
||||
number: params?.number || '',
|
||||
period: params?.period || '',
|
||||
},
|
||||
}
|
||||
|
||||
const name = Actions.SERVICE_ADD_ROLEACTION
|
||||
const command = { name, ...Commands[name] }
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, params) => [{ type: 'SERVICE', id: params.id }],
|
||||
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const patchServiceTemplate = dispatch(
|
||||
serviceApi.util.updateQueryData(
|
||||
'getService',
|
||||
{ id: params.id },
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
const patchServiceTemplates = dispatch(
|
||||
serviceApi.util.updateQueryData(
|
||||
'getServices',
|
||||
undefined,
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchServiceTemplate.undo()
|
||||
patchServiceTemplates.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
serviceAddAction: builder.mutation({
|
||||
/**
|
||||
* Tries to perform a role action.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @param {string} params.id - Service id
|
||||
* @param {string} params.role - Role config
|
||||
* @returns {Service} Service id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: (params) => {
|
||||
params.action = {
|
||||
perform: params?.perform,
|
||||
params: {
|
||||
...params.params,
|
||||
},
|
||||
}
|
||||
|
||||
const name = Actions.SERVICE_ADD_ACTION
|
||||
const command = { name, ...Commands[name] }
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, params) => [{ type: 'SERVICE', id: params.id }],
|
||||
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const patchServiceTemplate = dispatch(
|
||||
serviceApi.util.updateQueryData(
|
||||
'getService',
|
||||
{ id: params.id },
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
const patchServiceTemplates = dispatch(
|
||||
serviceApi.util.updateQueryData(
|
||||
'getServices',
|
||||
undefined,
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchServiceTemplate.undo()
|
||||
patchServiceTemplates.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@ -101,6 +380,13 @@ export const {
|
||||
useLazyGetServicesQuery,
|
||||
useGetServiceQuery,
|
||||
useLazyGetServiceQuery,
|
||||
useRemoveServiceMutation,
|
||||
useChangeServiceOwnerMutation,
|
||||
useRecoverServiceMutation,
|
||||
useServiceAddRoleMutation,
|
||||
useServiceAddActionMutation,
|
||||
useServiceRoleActionMutation,
|
||||
useServiceScaleRoleMutation,
|
||||
} = serviceApi
|
||||
|
||||
export default serviceApi
|
||||
|
@ -191,17 +191,11 @@ const serviceTemplateApi = oneApi.injectEndpoints({
|
||||
* @returns {number} Service id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: (params) => {
|
||||
/*
|
||||
data: {
|
||||
action: {
|
||||
perform: 'instantiate',
|
||||
params: { merge_template: data },
|
||||
},
|
||||
},
|
||||
method: PUT,
|
||||
url: `/api/${SERVICE_TEMPLATE}/action/${id}`,
|
||||
*/
|
||||
query: ({ template, ...params }) => {
|
||||
params.action = {
|
||||
perform: 'instantiate',
|
||||
params: { merge_template: template },
|
||||
}
|
||||
const name = Actions.SERVICE_TEMPLATE_ACTION
|
||||
const command = { name, ...Commands[name] }
|
||||
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
X2jOptions,
|
||||
} from 'fast-xml-parser'
|
||||
import { DateTime, Settings } from 'luxon'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import {
|
||||
CURRENCY,
|
||||
@ -555,7 +556,7 @@ export const userInputsToObject = (userInputs) =>
|
||||
* @returns {string[]} List of unique labels
|
||||
*/
|
||||
export const getUniqueLabels = (labels) => {
|
||||
if (labels?.length < 1) {
|
||||
if (labels?.length < 1 || labels === undefined || isEmpty(labels)) {
|
||||
return []
|
||||
}
|
||||
|
||||
|
@ -24,3 +24,11 @@ import { Service, SERVICE_STATES, STATES } from 'client/constants'
|
||||
*/
|
||||
export const getState = ({ TEMPLATE = {} } = {}) =>
|
||||
SERVICE_STATES[TEMPLATE?.BODY?.state]
|
||||
|
||||
/**
|
||||
* Returns information about Service state.
|
||||
*
|
||||
* @param {number} state - Role state
|
||||
* @returns {STATES.StateInfo} - Service state object
|
||||
*/
|
||||
export const getRoleState = (state) => SERVICE_STATES?.[state]
|
||||
|
@ -18,6 +18,11 @@ import parseApplicationToForm from 'client/utils/parser/parseApplicationToForm'
|
||||
import parseFormToApplication from 'client/utils/parser/parseFormToApplication'
|
||||
import parseFormToDeployApplication from 'client/utils/parser/parseFormToDeployApplication'
|
||||
import { parseAcl } from 'client/utils/parser/parseACL'
|
||||
import {
|
||||
parseNetworkString,
|
||||
parseCustomInputString,
|
||||
} from 'client/utils/parser/parseServiceTemplate'
|
||||
import parseVmTemplateContents from 'client/utils/parser/parseVmTemplateContents'
|
||||
|
||||
export {
|
||||
templateToObject,
|
||||
@ -25,4 +30,7 @@ export {
|
||||
parseFormToApplication,
|
||||
parseFormToDeployApplication,
|
||||
parseAcl,
|
||||
parseNetworkString,
|
||||
parseCustomInputString,
|
||||
parseVmTemplateContents,
|
||||
}
|
||||
|
141
src/fireedge/src/client/utils/parser/parseServiceTemplate.js
Normal file
141
src/fireedge/src/client/utils/parser/parseServiceTemplate.js
Normal file
@ -0,0 +1,141 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 NETWORK_TYPE = {
|
||||
template_id: 'create',
|
||||
id: 'existing',
|
||||
reserve_from: 'reserve',
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a formatted network string back into an object.
|
||||
*
|
||||
* @param {string} networkString - The formatted network string to parse.
|
||||
* @returns {object | null} An object with properties describing the network, or null if the string is invalid.
|
||||
*/
|
||||
const formatNetworkString = (networkString) => {
|
||||
const parts = networkString?.split('|')
|
||||
const [netType, id, extra] = parts?.slice(-1)[0]?.split(':')
|
||||
|
||||
const networkType = NETWORK_TYPE?.[netType]
|
||||
if (parts.length < 3 || !networkType) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
type: networkType,
|
||||
name: parts[0],
|
||||
description: parts[3],
|
||||
...(id && { network: id }),
|
||||
...(extra && { netextra: extra }),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a network object into a string or reverses the operation based on the reverse flag.
|
||||
*
|
||||
* @param {object | string} network - The network object to format or the network string to parse.
|
||||
* @param {boolean} [reverse=false] - Reverse operation flag.
|
||||
* @returns {string | object | null} A formatted network string or an object representing the network, or null for invalid input in reverse mode.
|
||||
*/
|
||||
export const parseNetworkString = (network, reverse = false) => {
|
||||
if (reverse) {
|
||||
return formatNetworkString(typeof network === 'string' ? network : '')
|
||||
}
|
||||
|
||||
const type = Object.keys(NETWORK_TYPE).find(
|
||||
(key) => NETWORK_TYPE[key] === network?.type?.toLowerCase()
|
||||
)
|
||||
|
||||
const result = `M|network|${network?.description ?? ''}| |${type ?? ''}:${
|
||||
network?.network ?? ''
|
||||
}:${network?.netextra ?? ''}`
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a formatted customInputs string back into an object.
|
||||
*
|
||||
* @param {string} customInputsString - The formatted customInputs string to parse.
|
||||
* @returns {object | null} An object with properties describing the customInputs, or null if the string is invalid.
|
||||
*/
|
||||
const formatCustomInputString = (customInputsString) => {
|
||||
const parts = customInputsString?.split('|')
|
||||
if (!parts || parts.length < 5) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [name, mandatory, type, description, rangeOrList, defaultValue] = parts
|
||||
|
||||
let defaultvaluerangemin, defaultvaluerangemax, defaultvaluelist
|
||||
const isRange = ['range', 'range-float'].includes(type)
|
||||
const isList = ['list', 'list-multiple'].includes(type)
|
||||
|
||||
if (isRange) {
|
||||
;[defaultvaluerangemin, defaultvaluerangemax] = rangeOrList
|
||||
.split('..')
|
||||
.map(Number)
|
||||
} else if (isList) {
|
||||
defaultvaluelist = rangeOrList
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
mandatory: mandatory === 'M',
|
||||
type,
|
||||
description,
|
||||
...(isRange && { defaultvaluerangemin, defaultvaluerangemax }),
|
||||
...(isList && { defaultvaluelist }),
|
||||
defaultvalue: defaultValue,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object|string} attribute - User input
|
||||
* @param {boolean} reverse - Reverse formatting?
|
||||
* @returns {object|string} - Depending on reverse flag
|
||||
*/
|
||||
export const parseCustomInputString = (attribute, reverse = false) => {
|
||||
if (reverse) {
|
||||
const res = formatCustomInputString(attribute)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
const {
|
||||
mandatory,
|
||||
type,
|
||||
description,
|
||||
defaultvaluerangemin,
|
||||
defaultvaluerangemax,
|
||||
defaultvaluelist,
|
||||
defaultvalue,
|
||||
} = attribute
|
||||
|
||||
const isList = ['list', 'list-multiple'].includes(type)
|
||||
const isRange = ['range', 'range-float'].includes(type)
|
||||
|
||||
return `${mandatory !== 'NO' ? 'M' : '0'}|${type ?? ''}|${
|
||||
description ?? ''
|
||||
}|${
|
||||
(isRange
|
||||
? `${defaultvaluerangemin}..${defaultvaluerangemax}`
|
||||
: isList
|
||||
? `${defaultvaluelist}`
|
||||
: '') ?? ''
|
||||
}|${defaultvalue ?? ''}`
|
||||
}
|
163
src/fireedge/src/client/utils/parser/parseVmTemplateContents.js
Normal file
163
src/fireedge/src/client/utils/parser/parseVmTemplateContents.js
Normal file
@ -0,0 +1,163 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 no-useless-escape */
|
||||
const formatNic = (nic, parent) => {
|
||||
const [[NIC, NETWORK_ID]] = Object.entries(nic)
|
||||
|
||||
return `${
|
||||
parent ? 'NIC_ALIAS' : 'NIC'
|
||||
} = [\n NAME = \"${NIC}\",\n NETWORK_ID = \"$${
|
||||
NETWORK_ID !== undefined ? NETWORK_ID.toLowerCase() : ''
|
||||
}\"${parent ? `,\n PARENT = \"${parent}\"` : ''} ]\n`
|
||||
}
|
||||
|
||||
const formatAlias = (fNics) => {
|
||||
fNics?.map((fnic) => {
|
||||
if (fnic?.alias) {
|
||||
const parent = fNics?.find(
|
||||
(nic) => nic?.NIC_NAME === fnic?.alias?.name
|
||||
)?.NIC_ID
|
||||
fnic.formatNic = formatNic({ [fnic?.NIC_ID]: fnic?.NIC_NAME }, parent)
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
}
|
||||
|
||||
const formatSchedActions = (schedAction) => {
|
||||
const { ACTION, TIME, DAYS, END_TYPE, END_VALUE, REPEAT, ID } = schedAction
|
||||
const formattedProperties = [
|
||||
END_TYPE != null ? ` END_TYPE = \"${END_TYPE}\"` : '',
|
||||
END_VALUE != null ? ` END_VALUE = \"${END_VALUE}\"` : '',
|
||||
TIME != null ? ` TIME = \"${TIME}\"` : '',
|
||||
ACTION != null ? ` ACTION = \"${ACTION}\"` : '',
|
||||
ID != null ? ` ID = \"${ID}\"` : '',
|
||||
DAYS != null ? ` DAYS = \"${DAYS}\"` : '',
|
||||
REPEAT != null ? ` REPEAT = \"${REPEAT}\"` : '',
|
||||
]
|
||||
.filter((line) => line)
|
||||
.join(`,\n`)
|
||||
|
||||
return ` SCHED_ACTION = [\n${formattedProperties} ]\n`
|
||||
}
|
||||
/* eslint-enable no-useless-escape */
|
||||
|
||||
const parseProperties = (section) => {
|
||||
const properties = {}
|
||||
const regex = /(\w+)\s*=\s*"([^"]*)"/g
|
||||
let match
|
||||
while ((match = regex.exec(section))) {
|
||||
properties[match[1]] = match[2]
|
||||
}
|
||||
|
||||
return properties
|
||||
}
|
||||
|
||||
const parseSection = (section) => {
|
||||
const headerMatch = section.match(/^(NIC|NIC_ALIAS|SCHED_ACTION)/)
|
||||
if (!headerMatch) return null
|
||||
|
||||
const header = headerMatch[0]
|
||||
const content = parseProperties(section)
|
||||
|
||||
return { header, content }
|
||||
}
|
||||
|
||||
const formatInstantiate = (contents) => {
|
||||
const { vmTemplateContents, customAttrsValues } = contents
|
||||
|
||||
const formatUserInputs = Object.entries(customAttrsValues)
|
||||
?.map(([input, value]) => `${input.toLowerCase()} = "${value}"`)
|
||||
?.join('\n')
|
||||
?.concat('\n')
|
||||
|
||||
return vmTemplateContents + formatUserInputs
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} contents - Vm template contents
|
||||
* @param {boolean} reverse - Reverse Vm template string?
|
||||
* @param {boolean} instantiate - Instantiate dialog
|
||||
* @returns {string} - Formatted Vm template content
|
||||
*/
|
||||
const formatVmTemplateContents = (
|
||||
contents,
|
||||
reverse = false,
|
||||
instantiate = false
|
||||
) => {
|
||||
if (!contents) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (instantiate) {
|
||||
return formatInstantiate(contents)
|
||||
}
|
||||
|
||||
if (reverse) {
|
||||
const nics = []
|
||||
const schedActions = []
|
||||
const sections = contents.match(
|
||||
/(NIC_ALIAS|NIC|SCHED_ACTION)\s*=\s*\[[^\]]+\]/g
|
||||
)
|
||||
|
||||
if (!sections) return { networks: nics, schedActions }
|
||||
|
||||
sections.forEach((section) => {
|
||||
const parsedSection = parseSection(section)
|
||||
if (!parsedSection) return
|
||||
|
||||
const { header, content } = parsedSection
|
||||
if (header === 'NIC' || header === 'NIC_ALIAS') {
|
||||
nics.push(content)
|
||||
} else if (header === 'SCHED_ACTION') {
|
||||
schedActions.push(content)
|
||||
}
|
||||
})
|
||||
|
||||
return { networks: nics, schedActions }
|
||||
} else {
|
||||
const { networks, schedActions } = contents
|
||||
if (!networks) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const formattedActions = schedActions?.map((action, index) =>
|
||||
formatSchedActions({ ...action, ID: index })
|
||||
)
|
||||
const formattedNics = networks
|
||||
?.filter((net) => net?.rowSelected)
|
||||
?.map((nic, index) => ({
|
||||
formatNic: formatNic({
|
||||
[`_NIC${index}`]: nic?.name,
|
||||
}),
|
||||
NIC_ID: `_NIC${index}`,
|
||||
NIC_NAME: nic?.name,
|
||||
...(nic?.aliasIdx !== -1 && { alias: networks?.[nic?.aliasIdx] }),
|
||||
}))
|
||||
|
||||
formatAlias(formattedNics)
|
||||
|
||||
const vmTemplateContents = formattedNics
|
||||
?.map((nic) => nic.formatNic)
|
||||
.join('')
|
||||
.concat(formattedActions?.join('') ?? '')
|
||||
|
||||
return vmTemplateContents
|
||||
}
|
||||
}
|
||||
|
||||
export default formatVmTemplateContents
|
@ -24,6 +24,7 @@ const {
|
||||
serviceAddAction,
|
||||
serviceAddScale,
|
||||
serviceAddRoleAction,
|
||||
serviceAddRole,
|
||||
serviceAddSchedAction,
|
||||
serviceUpdateSchedAction,
|
||||
serviceDeleteSchedAction,
|
||||
@ -45,6 +46,7 @@ const {
|
||||
SERVICE_ADD_ACTION,
|
||||
SERVICE_ADD_SCALE,
|
||||
SERVICE_ADD_ROLEACTION,
|
||||
SERVICE_ADD_ROLE,
|
||||
SERVICE_ADD_SCHEDACTION,
|
||||
SERVICE_UPDATE_SCHEDACTION,
|
||||
SERVICE_DELETE_SCHEDACTION,
|
||||
@ -72,6 +74,10 @@ const services = [
|
||||
...CommandsService[SERVICE_ADD_SCALE],
|
||||
action: serviceAddScale,
|
||||
},
|
||||
{
|
||||
...CommandsService[SERVICE_ADD_ROLE],
|
||||
action: serviceAddRole,
|
||||
},
|
||||
{
|
||||
...CommandsService[SERVICE_ADD_ROLEACTION],
|
||||
action: serviceAddRoleAction,
|
||||
|
@ -135,6 +135,23 @@ const role = {
|
||||
},
|
||||
}
|
||||
|
||||
const action = {
|
||||
id: '/Action',
|
||||
type: 'object',
|
||||
properties: {
|
||||
perform: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
},
|
||||
// Not required for some actions
|
||||
params: {
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const service = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -191,6 +208,6 @@ const service = {
|
||||
},
|
||||
}
|
||||
|
||||
const schemas = { role, service }
|
||||
const schemas = { role, service, action }
|
||||
|
||||
module.exports = schemas
|
||||
|
@ -185,11 +185,12 @@ const serviceAddAction = (
|
||||
// validate if "action" is required
|
||||
const config = {
|
||||
method: POST,
|
||||
path: '/service/{0}/action',
|
||||
path: `/service/{0}/action`,
|
||||
user,
|
||||
password,
|
||||
request: params.id,
|
||||
post: postAction,
|
||||
// the oneflow server parses and looks for the action key
|
||||
post: { action: postAction },
|
||||
}
|
||||
oneFlowConnection(
|
||||
config,
|
||||
@ -271,6 +272,64 @@ const serviceAddScale = (
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add service role action.
|
||||
*
|
||||
* @param {object} res - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params
|
||||
* @param {number} params.id - service ID
|
||||
* @param {string} params.action - service action
|
||||
* @param {string} params.role - service role
|
||||
* @param {object} userData - user data
|
||||
* @param {string} userData.user - username
|
||||
* @param {string} userData.password - user password
|
||||
*/
|
||||
const serviceAddRole = (
|
||||
res = {},
|
||||
next = defaultEmptyFunction,
|
||||
params = {},
|
||||
userData = {}
|
||||
) => {
|
||||
const { user, password } = userData
|
||||
const { id, action: serviceAction } = params
|
||||
if (Number.isInteger(parseInt(id, 10)) && serviceAction && user && password) {
|
||||
const v = new Validator()
|
||||
const postAction = parsePostData(serviceAction)
|
||||
const valSchema = v.validate(postAction, action)
|
||||
if (valSchema.valid) {
|
||||
// validate if "action" is required
|
||||
const config = {
|
||||
method: POST,
|
||||
path: '/service/{0}/role_action',
|
||||
user,
|
||||
password,
|
||||
request: id,
|
||||
post: { action: postAction },
|
||||
}
|
||||
oneFlowConnection(
|
||||
config,
|
||||
(data) => success(next, res, data),
|
||||
(data) => error(next, res, data)
|
||||
)
|
||||
} else {
|
||||
res.locals.httpCode = httpResponse(
|
||||
internalServerError,
|
||||
'',
|
||||
`invalid schema ${returnSchemaError(valSchema.errors)}`
|
||||
)
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
res.locals.httpCode = httpResponse(
|
||||
methodNotAllowed,
|
||||
'',
|
||||
'invalid action, id service or role'
|
||||
)
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add service role action.
|
||||
*
|
||||
@ -306,11 +365,11 @@ const serviceAddRoleAction = (
|
||||
// validate if "action" is required
|
||||
const config = {
|
||||
method: POST,
|
||||
path: '/service/{0}/role/{1}',
|
||||
path: '/service/{0}/role/{1}/action',
|
||||
user,
|
||||
password,
|
||||
request: [id, role],
|
||||
post: postAction,
|
||||
post: { action: postAction },
|
||||
}
|
||||
oneFlowConnection(
|
||||
config,
|
||||
@ -610,6 +669,7 @@ const serviceApi = {
|
||||
serviceDelete,
|
||||
serviceAddAction,
|
||||
serviceAddScale,
|
||||
serviceAddRole,
|
||||
serviceAddRoleAction,
|
||||
serviceAddSchedAction,
|
||||
serviceUpdateSchedAction,
|
||||
|
@ -27,6 +27,7 @@ const SERVICE_SHOW = 'service.show'
|
||||
const SERVICE_ADD_ACTION = 'service.addaction'
|
||||
const SERVICE_ADD_SCALE = 'service.addscale'
|
||||
const SERVICE_ADD_ROLEACTION = 'service.addroleaction'
|
||||
const SERVICE_ADD_ROLE = 'service.addrole'
|
||||
const SERVICE_ADD_SCHEDACTION = 'service.addscheaction'
|
||||
const SERVICE_UPDATE_SCHEDACTION = 'service.updateschedaction'
|
||||
const SERVICE_DELETE_SCHEDACTION = 'service.deleteschedaction'
|
||||
@ -36,6 +37,7 @@ const Actions = {
|
||||
SERVICE_SHOW,
|
||||
SERVICE_ADD_ACTION,
|
||||
SERVICE_ADD_SCALE,
|
||||
SERVICE_ADD_ROLE,
|
||||
SERVICE_ADD_ROLEACTION,
|
||||
SERVICE_ADD_SCHEDACTION,
|
||||
SERVICE_UPDATE_SCHEDACTION,
|
||||
@ -70,7 +72,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
[SERVICE_ADD_SCALE]: {
|
||||
path: `${basepath}/scale/:id`,
|
||||
path: `${basepath}/:id/scale`,
|
||||
httpMethod: POST,
|
||||
auth: true,
|
||||
params: {
|
||||
@ -82,8 +84,23 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[SERVICE_ADD_ROLE]: {
|
||||
path: `${basepath}/:id/role_action`,
|
||||
httpMethod: POST,
|
||||
auth: true,
|
||||
params: {
|
||||
id: {
|
||||
from: resource,
|
||||
},
|
||||
action: {
|
||||
from: postBody,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[SERVICE_ADD_ROLEACTION]: {
|
||||
path: `${basepath}/role_action/:id/:role`,
|
||||
path: `${basepath}/:id/role/:role/action`,
|
||||
httpMethod: POST,
|
||||
auth: true,
|
||||
params: {
|
||||
|
@ -64,7 +64,7 @@ const error = (next = defaultEmptyFunction, res = {}, data = '') => {
|
||||
) {
|
||||
res.locals.httpCode = httpResponse(
|
||||
internalServerError,
|
||||
data && data.message
|
||||
data && data?.response?.data
|
||||
)
|
||||
next()
|
||||
}
|
||||
@ -159,37 +159,60 @@ const serviceTemplateCreate = (
|
||||
params = {},
|
||||
userData = {}
|
||||
) => {
|
||||
const { user, password } = userData
|
||||
if (params && params.template && user && password) {
|
||||
const v = new Validator()
|
||||
const template = parsePostData(params.template)
|
||||
v.addSchema(role, '/Role')
|
||||
const valSchema = v.validate(template, service)
|
||||
if (valSchema.valid) {
|
||||
const config = {
|
||||
method: POST,
|
||||
path: '/service_template',
|
||||
user,
|
||||
password,
|
||||
post: template,
|
||||
try {
|
||||
const { user, password } = userData
|
||||
|
||||
if (params && params.template && user && password) {
|
||||
const v = new Validator()
|
||||
const template = parsePostData(params.template)
|
||||
|
||||
v.addSchema(role, '/Role')
|
||||
const valSchema = v.validate(template, service)
|
||||
|
||||
if (valSchema.valid) {
|
||||
try {
|
||||
const config = {
|
||||
method: POST,
|
||||
path: '/service_template',
|
||||
user,
|
||||
password,
|
||||
post: template,
|
||||
}
|
||||
oneFlowConnection(
|
||||
config,
|
||||
(data) => success(next, res, data),
|
||||
(data) => error(next, res, data)
|
||||
)
|
||||
} catch (err) {
|
||||
res.locals.httpCode = httpResponse(
|
||||
internalServerError,
|
||||
'Error in service template creation',
|
||||
`Unexpected error occurred: ${error.message}`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
res.locals.httpCode = httpResponse(
|
||||
internalServerError,
|
||||
'Invalid schema',
|
||||
`Invalid schema: ${returnSchemaError(
|
||||
valSchema.errors
|
||||
)}, Received template: ${JSON.stringify(template)}`
|
||||
)
|
||||
next()
|
||||
}
|
||||
oneFlowConnection(
|
||||
config,
|
||||
(data) => success(next, res, data),
|
||||
(data) => error(next, res, data)
|
||||
)
|
||||
} else {
|
||||
res.locals.httpCode = httpResponse(
|
||||
internalServerError,
|
||||
'',
|
||||
`invalid schema ${returnSchemaError(valSchema.errors)}`
|
||||
methodNotAllowed,
|
||||
'Invalid service json',
|
||||
`Invalid service json: Received params: ${JSON.stringify(params)}`
|
||||
)
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
} catch (error) {
|
||||
res.locals.httpCode = httpResponse(
|
||||
methodNotAllowed,
|
||||
'',
|
||||
'invalid service json'
|
||||
internalServerError,
|
||||
'Error in service template creation',
|
||||
`Unexpected error occurred: ${error.message}`
|
||||
)
|
||||
next()
|
||||
}
|
||||
|
@ -469,7 +469,8 @@ post '/service/:id/role_action' do
|
||||
when 'add_role'
|
||||
begin
|
||||
# Check that the JSON is valid
|
||||
json_template = JSON.parse(opts['role'])
|
||||
# Use directly if already parsed
|
||||
json_template = opts['role'].is_a?(Hash) ? opts['role'] : JSON.parse(opts['role'])
|
||||
|
||||
# Check the schema of the new template
|
||||
ServiceTemplate.validate_role(json_template)
|
||||
|
Loading…
x
Reference in New Issue
Block a user