1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-22 18:50:08 +03:00

F : Refactor scheduled actions ()

(cherry picked from commit 00dcdba88e64724f971d65a70393653f4655e9c2)
This commit is contained in:
Sergio Betanzos 2021-12-17 15:20:44 +01:00 committed by Tino Vazquez
parent 42358a2e25
commit a41261374d
33 changed files with 2754 additions and 1902 deletions

File diff suppressed because it is too large Load Diff

@ -0,0 +1,216 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo, useMemo, ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Trash, Edit, ClockOutline } from 'iconoir-react'
import { useAuth } from 'client/features/Auth'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import {
CreateCharterForm,
CreateRelativeCharterForm,
CreateSchedActionForm,
CreateRelativeSchedActionForm,
} from 'client/components/Forms/Vm'
import { Tr, Translate } from 'client/components/HOC'
import { ScheduledAction } from 'client/models/Scheduler'
import { sentenceCase } from 'client/utils'
import { T, VM_ACTIONS, VM_ACTIONS_IN_CHARTER } from 'client/constants'
/**
* Returns a button to trigger form to create a scheduled action.
*
* @param {object} props - Props
* @param {object} props.vm - Vm resource
* @param {boolean} [props.relative] - Applies to the form relative format
* @param {function():Promise} props.onSubmit - Submit function
* @returns {ReactElement} Button
*/
const CreateSchedButton = memo(({ vm, relative, onSubmit }) => (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': VM_ACTIONS.SCHED_ACTION_CREATE,
label: T.AddAction,
variant: 'outlined',
}}
options={[
{
name: T.PunctualAction,
dialogProps: {
title: T.ScheduledAction,
dataCy: 'modal-sched-actions',
},
form: () =>
relative
? CreateRelativeSchedActionForm(vm)
: CreateSchedActionForm(vm),
onSubmit,
},
]}
/>
))
/**
* Returns a button to trigger form to update a scheduled action.
*
* @param {object} props - Props
* @param {object} props.vm - Vm resource
* @param {ScheduledAction} props.schedule - Schedule action
* @param {boolean} [props.relative] - Applies to the form relative format
* @param {function():Promise} props.onSubmit - Submit function
* @returns {ReactElement} Button
*/
const UpdateSchedButton = memo(({ vm, schedule, relative, onSubmit }) => {
const { ID, ACTION } = schedule
const titleAction = `#${ID} ${sentenceCase(ACTION)}`
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VM_ACTIONS.SCHED_ACTION_UPDATE}-${ID}`,
icon: <Edit />,
tooltip: <Translate word={T.Edit} />,
}}
options={[
{
dialogProps: {
title: (
<Translate
word={T.UpdateScheduledAction}
values={[titleAction]}
/>
),
dataCy: 'modal-sched-actions',
},
form: () =>
relative
? CreateRelativeSchedActionForm(vm, schedule)
: CreateSchedActionForm(vm, schedule),
onSubmit,
},
]}
/>
)
})
/**
* Returns a button to trigger modal to delete a scheduled action.
*
* @param {object} props - Props
* @param {ScheduledAction} props.schedule - Schedule action
* @param {function():Promise} props.onSubmit - Submit function
* @returns {ReactElement} Button
*/
const DeleteSchedButton = memo(({ onSubmit, schedule }) => {
const { ID, ACTION } = schedule
const titleAction = `#${ID} ${sentenceCase(ACTION)}`
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VM_ACTIONS.SCHED_ACTION_DELETE}-${ID}`,
icon: <Trash />,
tooltip: <Translate word={T.Delete} />,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<Translate
word={T.DeleteScheduledAction}
values={[titleAction]}
/>
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
},
onSubmit,
},
]}
/>
)
})
/**
* Returns a button to trigger form to create a charter.
*
* @param {object} props - Props
* @param {boolean} [props.relative] - Applies to the form relative format
* @param {function():Promise} props.onSubmit - Submit function
* @returns {ReactElement} Button
*/
const CharterButton = memo(({ relative, onSubmit }) => {
const { config } = useAuth()
const leases = useMemo(
() =>
// filters if exists in the VM actions for charters
Object.entries(config?.leases ?? {}).filter(([action]) =>
VM_ACTIONS_IN_CHARTER.includes(action)
),
[config.leases]
)
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': VM_ACTIONS.CHARTER_CREATE,
icon: <ClockOutline />,
tooltip: <Translate word={T.Charter} />,
disabled: leases.length <= 0,
}}
options={[
{
dialogProps: {
title: T.ScheduledAction,
dataCy: 'modal-sched-actions',
},
form: () =>
relative
? CreateRelativeCharterForm(leases, leases)
: CreateCharterForm(leases, leases),
onSubmit,
},
]}
/>
)
})
const ButtonPropTypes = {
vm: PropTypes.object,
relative: PropTypes.bool,
onSubmit: PropTypes.func,
schedule: PropTypes.object,
}
CreateSchedButton.propTypes = ButtonPropTypes
CreateSchedButton.displayName = 'CreateSchedButton'
UpdateSchedButton.propTypes = ButtonPropTypes
UpdateSchedButton.displayName = 'UpdateSchedButton'
DeleteSchedButton.propTypes = ButtonPropTypes
DeleteSchedButton.displayName = 'DeleteSchedButton'
CharterButton.propTypes = ButtonPropTypes
CharterButton.displayName = 'CharterButton'
export {
CreateSchedButton,
DeleteSchedButton,
UpdateSchedButton,
CharterButton,
}

@ -13,7 +13,5 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import ImageSteps from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps'
import VolatileSteps from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps'
export { ImageSteps, VolatileSteps }
export * from 'client/components/Buttons/ScheduleAction'

@ -0,0 +1,125 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import PropTypes from 'prop-types'
import { WarningTriangleOutline as WarningIcon } from 'iconoir-react'
import { useTheme, Typography, Paper, Stack } from '@mui/material'
import Timer from 'client/components/Timer'
import { StatusChip } from 'client/components/Status'
import { UpdateSchedButton, DeleteSchedButton } from 'client/components/Buttons'
import { rowStyles } from 'client/components/Tables/styles'
import {
isRelative,
getPeriodicityByTimeInSeconds,
getRepeatInformation,
} from 'client/models/Scheduler'
import { timeFromMilliseconds } from 'client/models/Helper'
import { sentenceCase } from 'client/utils'
import { T } from 'client/constants'
const ScheduleActionCard = memo(
({ vm, schedule, handleRemove, handleUpdate }) => {
const classes = rowStyles()
const { palette } = useTheme()
const { ID, ACTION, TIME, MESSAGE, DONE, WARNING } = schedule
const titleAction = `#${ID} ${sentenceCase(ACTION)}`
const timeIsRelative = isRelative(TIME)
const time = timeIsRelative ? getPeriodicityByTimeInSeconds(TIME) : TIME
const formatTime =
!timeIsRelative && timeFromMilliseconds(+TIME).toFormat('ff')
const formatDoneTime = DONE && timeFromMilliseconds(+DONE).toFormat('ff')
const { repeat, end } = getRepeatInformation(schedule)
const noMore = !repeat && DONE
// const timeIsPast = new Date(+TIME * 1000) < new Date()
return (
<Paper variant="outlined" className={classes.root}>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{titleAction}</Typography>
{MESSAGE && (
<span className={classes.labels}>
<StatusChip text={MESSAGE} />
</span>
)}
</div>
<Stack
mt={0.5}
spacing={2}
alignItems="center"
flexWrap="wrap"
direction="row"
>
{repeat && <Typography variant="caption">{repeat}</Typography>}
{end && <Typography variant="caption">{end}</Typography>}
{DONE && (
<Typography variant="caption" title={formatDoneTime}>
<Timer initial={DONE} translateWord={T.DoneAgo} />
</Typography>
)}
{!noMore && (
<>
<Typography variant="caption">
{timeIsRelative ? (
<span>{Object.values(time).join(' ')}</span>
) : (
<span title={formatTime}>
<Timer initial={TIME} translateWord={T.FirstTime} />
</span>
)}
</Typography>
{WARNING && <WarningIcon color={palette.warning.main} />}
</>
)}
</Stack>
</div>
{(handleUpdate || handleRemove) && (
<div className={classes.actions}>
{!noMore && handleUpdate && (
<UpdateSchedButton
vm={vm}
relative={timeIsRelative}
schedule={schedule}
onSubmit={handleUpdate}
/>
)}
{handleRemove && (
<DeleteSchedButton onSubmit={handleRemove} schedule={schedule} />
)}
</div>
)}
</Paper>
)
}
)
ScheduleActionCard.propTypes = {
vm: PropTypes.object,
schedule: PropTypes.object.isRequired,
handleRemove: PropTypes.func,
handleUpdate: PropTypes.func,
}
ScheduleActionCard.displayName = 'ScheduleActionCard'
export default ScheduleActionCard

@ -24,6 +24,7 @@ import NetworkCard from 'client/components/Cards/NetworkCard'
import PolicyCard from 'client/components/Cards/PolicyCard'
import ProvisionCard from 'client/components/Cards/ProvisionCard'
import ProvisionTemplateCard from 'client/components/Cards/ProvisionTemplateCard'
import ScheduleActionCard from 'client/components/Cards/ScheduleActionCard'
import SelectCard from 'client/components/Cards/SelectCard'
import TierCard from 'client/components/Cards/TierCard'
import VirtualMachineCard from 'client/components/Cards/VirtualMachineCard'
@ -41,6 +42,7 @@ export {
PolicyCard,
ProvisionCard,
ProvisionTemplateCard,
ScheduleActionCard,
SelectCard,
TierCard,
VirtualMachineCard,

@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useCallback, ReactElement } from 'react'
import PropTypes from 'prop-types'
import { AnySchema } from 'yup'
import clsx from 'clsx'
import makeStyles from '@mui/styles/makeStyles'
@ -41,6 +42,17 @@ const useStyles = makeStyles((theme) => ({
},
}))
/**
* Creates dialog with a form inside.
*
* @param {object} props - Props
* @param {object} props.values - Default values
* @param {function():AnySchema} props.resolver - Resolver schema
* @param {function():Promise} props.handleSubmit - Submit function
* @param {object} props.dialogProps - Dialog props
* @param {ReactElement|ReactElement[]} props.children - Children element
* @returns {ReactElement} Dialog with form
*/
const DialogForm = ({
values,
resolver,
@ -63,9 +75,18 @@ const DialogForm = ({
resolver: yupResolver(resolver()),
})
const callbackSubmit = useCallback((formData) => {
const schemaData = resolver().cast(formData, {
context: formData,
isSubmit: true,
})
return handleSubmit(schemaData)
})
return (
<DialogConfirmation
handleAccept={handleSubmit && methods.handleSubmit(handleSubmit)}
handleAccept={handleSubmit && methods.handleSubmit(callbackSubmit)}
acceptButtonProps={{
isSubmitting: methods.formState.isSubmitting,
}}

@ -35,17 +35,13 @@ const SelectController = memo(
tooltip,
fieldProps = {},
}) => {
const defaultValue = multiple ? [values?.[0]?.value] : values?.[0]?.value
const firstValue = values?.[0]?.value ?? ''
const defaultValue = multiple ? [firstValue] : firstValue
const {
field: {
ref,
value: optionSelected = defaultValue,
onChange,
...inputProps
},
field: { ref, value: optionSelected, onChange, ...inputProps },
fieldState: { error },
} = useController({ name, control })
} = useController({ name, control, defaultValue })
const needShrink = useMemo(
() =>

@ -98,7 +98,7 @@ TextController.propTypes = {
PropTypes.string,
PropTypes.arrayOf(PropTypes.string),
]),
fieldProps: PropTypes.object,
fieldProps: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
}
TextController.displayName = 'TextController'

@ -14,7 +14,7 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useState, useMemo } from 'react'
import { useState } from 'react'
import PropTypes from 'prop-types'
import { Grow, Menu, MenuItem, Typography, ListItemIcon } from '@mui/material'
@ -43,21 +43,18 @@ const ButtonToTriggerForm = ({ buttonProps = {}, options = [] }) => {
const { display, show, hide, values: Form } = useDialog()
const {
onSubmit: handleSubmit,
form,
form: {
steps,
defaultValues,
resolver,
description,
fields,
transformBeforeSubmit,
} = {},
isConfirmDialog = false,
dialogProps = {},
} = Form ?? {}
const formConfig = useMemo(() => form?.() ?? {}, [form])
const {
steps,
defaultValues,
resolver,
description,
fields,
transformBeforeSubmit,
} = formConfig
const handleTriggerSubmit = async (formData) => {
try {
const data = transformBeforeSubmit?.(formData) ?? formData
@ -67,8 +64,10 @@ const ButtonToTriggerForm = ({ buttonProps = {}, options = [] }) => {
}
}
const openDialogForm = (formParams) => {
show(formParams)
const openDialogForm = async (formParams) => {
const formConfig = await formParams?.form?.()
show({ ...formParams, form: formConfig })
handleClose()
}
@ -129,25 +128,27 @@ const ButtonToTriggerForm = ({ buttonProps = {}, options = [] }) => {
{...dialogProps}
/>
) : (
<DialogForm
resolver={resolver}
values={defaultValues}
handleSubmit={!steps ? handleTriggerSubmit : undefined}
dialogProps={{ handleCancel: hide, ...dialogProps }}
>
{steps ? (
<FormStepper
steps={steps}
schema={resolver}
onSubmit={handleTriggerSubmit}
/>
) : (
<>
{description}
<FormWithSchema cy="form-dg" fields={fields} />
</>
)}
</DialogForm>
resolver && (
<DialogForm
resolver={resolver}
values={defaultValues}
handleSubmit={!steps ? handleTriggerSubmit : undefined}
dialogProps={{ handleCancel: hide, ...dialogProps }}
>
{steps ? (
<FormStepper
steps={steps}
schema={resolver}
onSubmit={handleTriggerSubmit}
/>
) : (
<>
{description}
<FormWithSchema cy="form-dg" fields={fields} />
</>
)}
</DialogForm>
)
))}
</>
)

@ -0,0 +1,111 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { Stack, Typography, Divider } from '@mui/material'
import {
timeToSecondsByPeriodicity,
transformChartersToSchedActions,
getFixedLeases,
getEditableLeases,
} from 'client/models/Scheduler'
import { createForm, sentenceCase } from 'client/utils'
import {
CHARTER_SCHEMA,
CHARTER_FIELDS,
RELATIVE_CHARTER_FIELDS,
RELATIVE_CHARTER_SCHEMA,
} from 'client/components/Forms/Vm/CreateCharterForm/schema'
const FixedLeases = ({ leases }) => {
const fixedLeases = useMemo(() => getFixedLeases(leases), [])
if (fixedLeases.length === 0) return null
return (
<>
<Stack spacing={2}>
{transformChartersToSchedActions(fixedLeases, true)?.map((action) => {
const { ACTION, TIME, PERIOD, WARNING, WARNING_PERIOD } = action
return (
<Stack
key={[ACTION, TIME, PERIOD].filter(Boolean).join('-')}
spacing={0.5}
>
<Typography noWrap variant="subtitle1" padding="1rem">
{`> ${sentenceCase(ACTION)} in ${TIME} ${PERIOD}`}
{WARNING && ` | Warning before ${WARNING} ${WARNING_PERIOD}`}
</Typography>
</Stack>
)
})}
</Stack>
<Divider />
</>
)
}
FixedLeases.propTypes = {
leases: PropTypes.arrayOf(
PropTypes.shape({
ACTION: PropTypes.string,
TIME: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
WARNING: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
})
),
}
const CreateCharterForm = createForm(CHARTER_SCHEMA, CHARTER_FIELDS, {
description: (leases) => <FixedLeases leases={leases} />,
transformInitialValue: (leases, schema) => {
const schedActions = transformChartersToSchedActions(
getEditableLeases(leases)
)
return schema.cast({ CHARTERS: schedActions }, { context: schedActions })
},
transformBeforeSubmit: (formData) => formData.CHARTERS,
})
const RelativeForm = createForm(
RELATIVE_CHARTER_SCHEMA,
RELATIVE_CHARTER_FIELDS,
{
description: (leases) => <FixedLeases leases={leases} />,
transformInitialValue: (leases, schema) => {
const schedActions = transformChartersToSchedActions(
getEditableLeases(leases),
true
)
return schema.cast({ CHARTERS: schedActions }, { context: schedActions })
},
transformBeforeSubmit: (formData) =>
formData.CHARTERS?.map(
({ TIME, PERIOD, WARNING, WARNING_PERIOD, ...lease }) => ({
...lease,
TIME: `+${timeToSecondsByPeriodicity(PERIOD, TIME)}`,
WARNING: `-${timeToSecondsByPeriodicity(WARNING_PERIOD, WARNING)}`,
})
),
}
)
export { RelativeForm }
export default CreateCharterForm

@ -0,0 +1,101 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { array, object, ObjectSchema } from 'yup'
import {
transformChartersToSchedActions,
getFixedLeases,
getEditableLeases,
} from 'client/models/Scheduler'
import { Field, getObjectSchemaFromFields } from 'client/utils'
import {
PUNCTUAL_FIELDS,
RELATIVE_FIELDS,
} from 'client/components/Forms/Vm/CreateSchedActionForm/fields'
const punctualFields = [
PUNCTUAL_FIELDS.ACTION_FIELD_FOR_CHARTERS,
PUNCTUAL_FIELDS.TIME_FIELD,
]
const relativeFields = [
PUNCTUAL_FIELDS.ACTION_FIELD_FOR_CHARTERS,
RELATIVE_FIELDS.RELATIVE_TIME_FIELD,
RELATIVE_FIELDS.PERIOD_FIELD,
]
/**
* @param {leases} leases - Leases from configuration yaml
* @param {Field[]} fields - Fields to map with charter index
* @returns {Field[]} Fields
*/
const mapFieldsWithIndex = (leases, fields) =>
getEditableLeases(leases)
?.map((_, idx) =>
fields.map(({ name, ...field }) => ({
...field,
name: `CHARTERS.${idx}.${name}`,
}))
)
.flat()
/**
* @param {leases} leases - Leases from configuration yaml
* @param {Field[]} fields - Fields
* @param {boolean} [relative]
* - If `true`, the result will be transformed to relative times
* @returns {ObjectSchema} Charter schema
*/
const createCharterSchema = (leases, fields, relative = false) =>
object({
CHARTERS: array(getObjectSchemaFromFields(fields))
.ensure()
.afterSubmit((values) => {
const fixedLeases = transformChartersToSchedActions(
getFixedLeases(leases),
relative
)
return [...values, ...fixedLeases]
}),
})
/**
* @param {object} leases - Leases from conf yaml
* @returns {Field[]} Punctual fields
*/
export const CHARTER_FIELDS = (leases) =>
mapFieldsWithIndex(leases, punctualFields)
/**
* @param {object} leases - Leases from conf yaml
* @returns {Field[]} Relative fields
*/
export const RELATIVE_CHARTER_FIELDS = (leases) =>
mapFieldsWithIndex(leases, relativeFields)
/**
* @param {object} leases - Leases from conf yaml
* @returns {ObjectSchema} Punctual Schema
*/
export const CHARTER_SCHEMA = (leases) =>
createCharterSchema(leases, punctualFields)
/**
* @param {object} leases - Leases from conf yaml
* @returns {ObjectSchema} Relative Schema
*/
export const RELATIVE_CHARTER_SCHEMA = (leases) =>
createCharterSchema(leases, relativeFields, true)

@ -1,170 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import * as yup from 'yup'
import {
INPUT_TYPES,
VM_ACTIONS,
VM_ACTIONS_WITH_SCHEDULE,
} from 'client/constants'
import { sentenceCase } from 'client/utils'
import { getSnapshotList, getDisks } from 'client/models/VirtualMachine'
// ----------------------------------------------------------
// Constants
// ----------------------------------------------------------
const ARGS_TYPES = {
DISK_ID: 'DISK_ID',
NAME: 'NAME',
SNAPSHOT_ID: 'SNAPSHOT_ID',
}
const SCHED_ACTION_OPTIONS = VM_ACTIONS_WITH_SCHEDULE.map((action) => ({
text: sentenceCase(action),
value: action,
})).sort()
const ARGS_BY_ACTION = (action) => {
const { DISK_ID, NAME, SNAPSHOT_ID } = ARGS_TYPES
return (
{
[VM_ACTIONS.SNAPSHOT_DISK_CREATE]: [DISK_ID, NAME],
[VM_ACTIONS.SNAPSHOT_DISK_REVERT]: [DISK_ID, SNAPSHOT_ID],
[VM_ACTIONS.SNAPSHOT_DISK_DELETE]: [DISK_ID, SNAPSHOT_ID],
[VM_ACTIONS.SNAPSHOT_CREATE]: [NAME],
[VM_ACTIONS.SNAPSHOT_REVERT]: [SNAPSHOT_ID],
[VM_ACTIONS.SNAPSHOT_DELETE]: [SNAPSHOT_ID],
}[action] ?? []
)
}
// ----------------------------------------------------------
// Fields
// ----------------------------------------------------------
const createArgField = (type) => ({
name: `ARGS.${type}`,
dependOf: ACTION_FIELD.name,
htmlType: (action) =>
ARGS_BY_ACTION(action)?.includes(type) ? undefined : INPUT_TYPES.HIDDEN,
})
const ACTION_FIELD = {
name: 'ACTION',
label: 'Action',
type: INPUT_TYPES.SELECT,
values: SCHED_ACTION_OPTIONS,
validation: yup
.string()
.trim()
.required('Action field is required')
.default(() => SCHED_ACTION_OPTIONS[0]?.value),
grid: { xs: 12 },
}
const ARGS_DISK_ID_FIELD = (vm) => {
const diskOptions = getDisks(vm).map(({ DISK_ID, IMAGE }) => ({
text: IMAGE,
value: DISK_ID,
}))
return {
...createArgField(ARGS_TYPES.DISK_ID),
label: 'Disk',
type: INPUT_TYPES.SELECT,
values: [{ text: '', value: '' }].concat(diskOptions),
}
}
const ARGS_NAME_FIELD = {
...createArgField(ARGS_TYPES.NAME),
label: 'Snapshot name',
type: INPUT_TYPES.TEXT,
}
const ARGS_SNAPSHOT_ID_FIELD = (vm) => {
const snapshotOptions = getSnapshotList(vm).map(({ SNAPSHOT_ID, NAME }) => ({
text: NAME,
value: SNAPSHOT_ID,
}))
return {
...createArgField(ARGS_TYPES.SNAPSHOT_ID),
label: 'Snapshot',
type: INPUT_TYPES.SELECT,
values: [{ text: '', value: '' }].concat(snapshotOptions),
}
}
export const COMMON_FIELDS = (vm) =>
[
ACTION_FIELD,
ARGS_DISK_ID_FIELD(vm),
ARGS_NAME_FIELD,
ARGS_SNAPSHOT_ID_FIELD(vm),
].map((field) => (typeof field === 'function' ? field(vm) : field))
// ----------------------------------------------------------
// Schema
// ----------------------------------------------------------
const transformStringToArgs = ({ ACTION, ARGS = {} }) => {
if (typeof ARGS !== 'string') return ARGS
// IMPORTANT - String data from ARGS has strict order: DISK_ID, NAME, SNAPSHOT_ID
const [arg1, arg2] = ARGS.split(',')
return (
{
[VM_ACTIONS.SNAPSHOT_DISK_CREATE]: { DISK_ID: arg1, NAME: arg2 },
[VM_ACTIONS.SNAPSHOT_DISK_REVERT]: { DISK_ID: arg1, SNAPSHOT_ID: arg2 },
[VM_ACTIONS.SNAPSHOT_DISK_DELETE]: { DISK_ID: arg1, SNAPSHOT_ID: arg2 },
[VM_ACTIONS.SNAPSHOT_CREATE]: { NAME: arg1 },
[VM_ACTIONS.SNAPSHOT_REVERT]: { SNAPSHOT_ID: arg1 },
[VM_ACTIONS.SNAPSHOT_DELETE]: { SNAPSHOT_ID: arg1 },
}[ACTION] ?? {}
)
}
const createArgSchema = (field) =>
yup
.string()
.trim()
.default(() => undefined)
.required(`${field} field is required`)
const ARG_SCHEMAS = {
[ARGS_TYPES.DISK_ID]: createArgSchema('Disk'),
[ARGS_TYPES.NAME]: createArgSchema('Snapshot name'),
[ARGS_TYPES.SNAPSHOT_ID]: createArgSchema('Snapshot'),
}
export const COMMON_SCHEMA = yup
.object({
[ACTION_FIELD.name]: ACTION_FIELD.validation,
ARGS: yup
.object()
.default(() => undefined)
.when(ACTION_FIELD.name, (action, schema) =>
ARGS_BY_ACTION(action)
.map((arg) => yup.object({ [arg]: ARG_SCHEMAS[arg] }))
.reduce((result, argSchema) => result.concat(argSchema), schema)
),
})
.transform((value) => ({ ...value, ARGS: transformStringToArgs(value) }))

@ -1,52 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { isoDateToMilliseconds } from 'client/models/Helper'
import { createForm } from 'client/utils'
import {
SCHEMA,
FIELDS,
} from 'client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/schema'
const PunctualForm = createForm(SCHEMA, FIELDS, {
transformBeforeSubmit: (formData) => {
const {
ARGS,
TIME: time,
END_VALUE,
END_TYPE,
PERIODIC: _,
...restOfData
} = formData
const argValues = Object.values(ARGS)
const newSchedAction = {
TIME: isoDateToMilliseconds(time),
END_TYPE,
...restOfData,
}
argValues.length && (newSchedAction.ARGS = argValues.join(','))
if (END_VALUE) {
newSchedAction.END_VALUE =
END_TYPE === '1' ? END_VALUE : isoDateToMilliseconds(END_VALUE)
}
return newSchedAction
},
})
export default PunctualForm

@ -1,266 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import * as yup from 'yup'
import { DateTime } from 'luxon'
import { T, INPUT_TYPES } from 'client/constants'
import { getValidationFromFields, sentenceCase } from 'client/utils'
import { timeFromMilliseconds } from 'client/models/Helper'
import {
COMMON_FIELDS,
COMMON_SCHEMA,
} from 'client/components/Forms/Vm/CreateSchedActionForm/CommonSchema'
const ISO_FORMAT = "yyyy-MM-dd'T'HH:mm"
const ISO_REG = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/
const MONTH_DAYS_REG = /^(3[01]|[12][0-9]|[1-9])(,(3[01]|[12][0-9]|[1-9]))*$/
const YEAR_DAYS_REG =
/^(36[0-5]|3[0-5]\d|[12]\d{2}|[0-9]\d?)(,(36[0-5]|3[0-5]\d|[12]\d{2}|[1-9]\d?))*$/
const HOURS_REG = /^(16[0-8]|1[01][0-9]|[1-9]?[0-9])$/
const REPEAT_VALUES = {
WEEKLY: '0',
MONTHLY: '1',
YEARLY: '2',
HOURLY: '3',
}
const END_TYPE_VALUES = {
NEVER: '0',
REPETITION: '1',
DATE: '2',
}
const REPEAT_OPTIONS = Object.entries(REPEAT_VALUES).map(([text, value]) => ({
text: sentenceCase(text),
value,
}))
const END_TYPE_OPTIONS = Object.entries(END_TYPE_VALUES).map(
([text, value]) => ({ text: sentenceCase(text), value })
)
const isoDateValidation = (nameInput) =>
yup
.string()
.trim()
.default(() => DateTime.local().toFormat(ISO_FORMAT))
.matches(ISO_REG, {
message: `${nameInput} should be a date with ISO format: yyyy-MM-ddTHH:mm`,
})
.transform((value, originalValue) => {
if (
value.length < 10 ||
(isNaN(value) && value.match(ISO_REG) === null)
) {
return value
}
const newValue = isNaN(originalValue)
? DateTime.fromISO(originalValue)
: timeFromMilliseconds(originalValue)
return newValue.isValid ? newValue.toFormat(ISO_FORMAT) : originalValue
})
const PERIODIC_FIELD = {
name: 'PERIODIC',
label: 'Periodic',
type: INPUT_TYPES.CHECKBOX,
validation: yup.boolean().default(() => false),
grid: { md: 12 },
}
const TIME_FIELD = {
name: 'TIME',
label: 'Time',
type: INPUT_TYPES.TIME,
validation: yup
.string()
.required('Time field is required')
.concat(isoDateValidation('Time')),
}
const REPEAT_FIELD = {
name: 'REPEAT',
label: 'Granularity of the action',
type: INPUT_TYPES.SELECT,
dependOf: PERIODIC_FIELD.name,
htmlType: (isPeriodic) => (!isPeriodic ? INPUT_TYPES.HIDDEN : undefined),
values: REPEAT_OPTIONS,
validation: yup
.string()
.trim()
.default(REPEAT_OPTIONS[0].value)
.when(PERIODIC_FIELD.name, (isPeriodic, schema) =>
isPeriodic ? schema : schema.strip().notRequired()
),
}
const DAYS_FIELD = {
name: 'DAYS',
dependOf: [REPEAT_FIELD.name, PERIODIC_FIELD.name],
multiple: ([repeat] = []) => REPEAT_VALUES.WEEKLY === repeat,
type: ([repeat] = []) =>
REPEAT_VALUES.WEEKLY === repeat ? INPUT_TYPES.SELECT : INPUT_TYPES.TEXT,
label: ([repeat] = []) =>
({
[REPEAT_VALUES.WEEKLY]: 'Days of week',
[REPEAT_VALUES.MONTHLY]: 'Days of month',
[REPEAT_VALUES.YEARLY]: 'Days of year',
[REPEAT_VALUES.HOURLY]: "Each 'x' hours",
}[repeat]),
values: [
{ text: T.Sunday, value: '0' },
{ text: T.Monday, value: '1' },
{ text: T.Tuesday, value: '2' },
{ text: T.Wednesday, value: '3' },
{ text: T.Thursday, value: '4' },
{ text: T.Friday, value: '5' },
{ text: T.Saturday, value: '6' },
],
htmlType: ([repeat, isPeriodic] = []) => {
if (!isPeriodic) return INPUT_TYPES.HIDDEN
if (repeat === REPEAT_VALUES.HOURLY) return 'number'
},
validation: yup
.string()
.default(undefined)
.when(
REPEAT_FIELD.name,
(repeatType, schema) =>
({
[REPEAT_VALUES.WEEKLY]: schema
.transform((value) =>
Array.isArray(value) ? value.join(',') : value
)
.required(
'Days field is required: between 0 (Sunday) and 6 (Saturday)'
),
[REPEAT_VALUES.MONTHLY]: schema
.trim()
.matches(MONTH_DAYS_REG, {
message: 'Days should be between 1 and 31',
})
.required('Days field is required: between 1 and 31'),
[REPEAT_VALUES.YEARLY]: schema
.trim()
.matches(YEAR_DAYS_REG, {
message: 'Days should be between 0 and 365',
})
.required('Days field is required: between 0 and 365'),
[REPEAT_VALUES.HOURLY]: schema
.trim()
.matches(HOURS_REG, {
message: 'Hours should be between 0 and 168',
})
.required('Hours field is required: between 0 and 168'),
}[repeatType] ?? schema)
)
.when(PERIODIC_FIELD.name, (isPeriodic, schema) =>
isPeriodic ? schema : schema.strip().notRequired()
),
fieldProps: { min: 0, max: 168, step: 1 },
}
const END_TYPE_FIELD = {
name: 'END_TYPE',
label: 'End type',
type: INPUT_TYPES.SELECT,
dependOf: PERIODIC_FIELD.name,
htmlType: (isPeriodic) => (!isPeriodic ? INPUT_TYPES.HIDDEN : undefined),
values: END_TYPE_OPTIONS,
validation: yup
.string()
.trim()
.default(END_TYPE_OPTIONS[0].value)
.when(PERIODIC_FIELD.name, (isPeriodic, schema) =>
isPeriodic ? schema : schema.strip().notRequired()
),
}
const END_VALUE_FIELD = {
name: 'END_VALUE',
label: 'When you want that the action finishes',
dependOf: [PERIODIC_FIELD.name, END_TYPE_FIELD.name],
type: (dependValues = {}) => {
const { [END_TYPE_FIELD.name]: endType } = dependValues
return endType === END_TYPE_VALUES.DATE
? INPUT_TYPES.TIME
: INPUT_TYPES.TEXT
},
htmlType: (dependValues = {}) => {
const {
[PERIODIC_FIELD.name]: isPeriodic,
[END_TYPE_FIELD.name]: endType,
} = dependValues
if (!isPeriodic || END_TYPE_VALUES.NEVER === endType)
return INPUT_TYPES.HIDDEN
return {
[END_TYPE_VALUES.REPETITION]: 'number',
[END_TYPE_VALUES.DATE]: 'datetime-local',
}[endType]
},
validation: yup
.string()
.trim()
.default(undefined)
.when(
END_TYPE_FIELD.name,
(endType, schema) =>
({
[END_TYPE_VALUES.REPETITION]: schema.required(
'Number of repetitions is required'
),
[END_TYPE_VALUES.DATE]: schema
.concat(isoDateValidation('Date'))
.required('Date to finish the action is required'),
}[endType] ?? schema)
),
}
export const PUNCTUAL_FIELDS = [
TIME_FIELD,
PERIODIC_FIELD,
REPEAT_FIELD,
DAYS_FIELD,
END_TYPE_FIELD,
END_VALUE_FIELD,
]
export const FIELDS = (vm) => [...COMMON_FIELDS(vm), ...PUNCTUAL_FIELDS]
export const SCHEMA = yup
.object(getValidationFromFields(PUNCTUAL_FIELDS))
.concat(COMMON_SCHEMA)
.transform((value) => {
const {
[DAYS_FIELD.name]: DAYS,
[REPEAT_FIELD.name]: REPEAT,
...rest
} = value
return {
...rest,
[DAYS_FIELD.name]: DAYS,
[REPEAT_FIELD.name]: REPEAT,
[PERIODIC_FIELD.name]: !!(DAYS || REPEAT),
}
})

@ -1,35 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { createForm } from 'client/utils'
import {
SCHEMA,
FIELDS,
} from 'client/components/Forms/Vm/CreateSchedActionForm/RelativeForm/schema'
const RelativeForm = createForm(SCHEMA, FIELDS, {
transformBeforeSubmit: (formData) => {
const { ARGS, TIME: time, PERIOD: _, ...restOfData } = formData
const argValues = Object.values(ARGS)
const newSchedAction = { TIME: `+${time}`, ...restOfData }
argValues.length && (newSchedAction.ARGS = argValues.join(','))
return newSchedAction
},
})
export default RelativeForm

@ -1,102 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import * as yup from 'yup'
import { INPUT_TYPES } from 'client/constants'
import { getValidationFromFields, upperCaseFirst } from 'client/utils'
import {
COMMON_FIELDS,
COMMON_SCHEMA,
} from 'client/components/Forms/Vm/CreateSchedActionForm/CommonSchema'
const PERIOD_TYPES = {
YEARS: 'years',
MONTHS: 'months',
WEEKS: 'weeks',
DAYS: 'days',
HOURS: 'hours',
MINUTES: 'minutes',
}
const PERIOD_OPTIONS = Object.entries(PERIOD_TYPES).map(([text, value]) => ({
text: upperCaseFirst(text),
value,
}))
const TIME_FIELD = {
name: 'TIME',
label: 'Time after the VM is instantiated',
type: INPUT_TYPES.TEXT,
htmlType: 'number',
validation: yup
.number()
.typeError('Time value must be a number')
.required('Time field is required')
.positive()
.default(undefined),
}
const PERIOD_FIELD = {
name: 'PERIOD',
label: 'Period type',
type: INPUT_TYPES.SELECT,
values: PERIOD_OPTIONS,
validation: yup
.string()
.trim()
.notRequired()
.default(PERIOD_OPTIONS[0].value),
}
export const RELATIVE_FIELDS = [TIME_FIELD, PERIOD_FIELD]
export const FIELDS = (vm) => [...COMMON_FIELDS(vm), ...RELATIVE_FIELDS]
export const SCHEMA = yup
.object(getValidationFromFields(RELATIVE_FIELDS))
.concat(COMMON_SCHEMA)
.transform(
({ [PERIOD_FIELD.name]: PERIOD, [TIME_FIELD.name]: TIME, ...rest }) => {
if (String(TIME).includes('+')) {
const allPeriods = {
[PERIOD_TYPES.YEARS]: TIME / 365 / 24 / 3600,
[PERIOD_TYPES.MONTHS]: TIME / 30 / 24 / 3600,
[PERIOD_TYPES.WEEKS]: TIME / 7 / 24 / 3600,
[PERIOD_TYPES.DAYS]: TIME / 24 / 3600,
[PERIOD_TYPES.HOURS]: TIME / 3600,
[PERIOD_TYPES.MINUTES]: TIME / 60,
}
const [period, time] = Object.entries(allPeriods).find(
([_, value]) => value >= 1
)
return { ...rest, [PERIOD_FIELD.name]: period, [TIME_FIELD.name]: time }
}
const timeInMilliseconds = {
[PERIOD_TYPES.YEARS]: TIME * 365 * 24 * 3600,
[PERIOD_TYPES.MONTHS]: TIME * 30 * 24 * 3600,
[PERIOD_TYPES.WEEKS]: TIME * 7 * 24 * 3600,
[PERIOD_TYPES.DAYS]: TIME * 24 * 3600,
[PERIOD_TYPES.HOURS]: TIME * 3600,
[PERIOD_TYPES.MINUTES]: TIME * 60,
}[PERIOD]
return { ...rest, [TIME_FIELD.name]: timeInMilliseconds }
}
)

@ -0,0 +1,474 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { DateTime } from 'luxon'
import { ref, mixed, boolean, string, date, lazy, array, number } from 'yup'
import {
T,
INPUT_TYPES,
VM_ACTIONS_IN_CHARTER,
VM_ACTIONS_WITH_SCHEDULE,
} from 'client/constants'
import { Field, sentenceCase, arrayToOptions } from 'client/utils'
import {
isRelative,
END_TYPE_VALUES,
REPEAT_VALUES,
ARGS_TYPES,
getRequiredArgsByAction,
PERIOD_TYPES,
getPeriodicityByTimeInSeconds,
} from 'client/models/Scheduler'
import {
isDate,
timeFromMilliseconds,
dateToMilliseconds,
} from 'client/models/Helper'
import { getSnapshotList, getDisks } from 'client/models/VirtualMachine'
// --------------------------------------------------------
// Constants
// --------------------------------------------------------
/** @type {RegExp} Regex to days of month (1-31) */
const MONTH_DAYS_REG = /^(3[01]|[12][0-9]|[1-9])(,(3[01]|[12][0-9]|[1-9]))*$/
/** @type {RegExp} Regex to days of year (1-365) */
const YEAR_DAYS_REG =
/^(36[0-5]|3[0-5]\d|[12]\d{2}|[0-9]\d?)(,(36[0-5]|3[0-5]\d|[12]\d{2}|[1-9]\d?))*$/
const DAYS_OF_WEEK = [
T.Sunday,
T.Monday,
T.Tuesday,
T.Wednesday,
T.Thursday,
T.Friday,
T.Saturday,
]
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) => {
if (isDate(originalValue)) return originalValue // is JS Date
if (originalValue?.isValid) return originalValue.toJSDate() // is luxon DateTime
const newValueInSeconds = isRelative(originalValue)
? getPeriodicityByTimeInSeconds(originalValue)?.time
: originalValue
return timeFromMilliseconds(newValueInSeconds).toJSDate() // is millisecond format
}
// --------------------------------------------------------
// Fields
// --------------------------------------------------------
const createArgField = (argName) => ({
name: `ARGS.${argName}`,
dependOf: ACTION_FIELD.name,
htmlType: (action) =>
!getRequiredArgsByAction(action)?.includes(argName) && INPUT_TYPES.HIDDEN,
})
/** @type {Field} Action name field */
const ACTION_FIELD = {
name: 'ACTION',
label: T.Action,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(VM_ACTIONS_WITH_SCHEDULE, {
addEmpty: false,
getText: (action) => sentenceCase(action),
}),
validation: string().trim().required(),
grid: { xs: 12 },
}
/** @type {Field} Action name field */
const ACTION_FIELD_FOR_CHARTERS = {
...ACTION_FIELD,
values: arrayToOptions(VM_ACTIONS_IN_CHARTER, {
addEmpty: false,
getText: (action) => sentenceCase(action),
}),
}
/**
* @param {object} vm - Vm resource
* @returns {Field} Disk id field
*/
const ARGS_DISK_ID_FIELD = (vm) => ({
...createArgField(ARGS_TYPES.DISK_ID),
label: T.Disk,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(getDisks(vm), {
getText: ({ IMAGE_ID, IMAGE, NAME } = {}) => {
const isVolatile = !IMAGE && !IMAGE_ID
return isVolatile ? NAME : `${NAME}: ${IMAGE}`
},
getValue: ({ DISK_ID } = {}) => DISK_ID,
}),
})
/** @type {Field} Snapshot name field */
const ARGS_NAME_FIELD = {
...createArgField(ARGS_TYPES.NAME),
label: T.SnapshotName,
type: INPUT_TYPES.TEXT,
}
/**
* @param {object} vm - Vm resource
* @returns {Field} Snapshot id field
*/
const ARGS_SNAPSHOT_ID_FIELD = (vm) => ({
...createArgField(ARGS_TYPES.SNAPSHOT_ID),
label: T.Snapshot,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(getSnapshotList(vm), {
getText: ({ NAME } = {}) => NAME,
getValue: ({ SNAPSHOT_ID } = {}) => SNAPSHOT_ID,
}),
})
/** @type {Field} Periodic field */
const PERIODIC_FIELD = {
name: 'PERIODIC',
label: T.Periodic,
type: INPUT_TYPES.SWITCH,
validation: lazy((_, { context }) =>
boolean().default(
() => !!(context?.[DAYS_FIELD.name] || context?.[REPEAT_FIELD.name])
)
),
grid: { md: 12 },
}
/** @type {Field} Time field */
const TIME_FIELD = {
name: 'TIME',
label: T.Time,
type: INPUT_TYPES.TIME,
validation: lazy(() =>
date()
.min(getNow().toJSDate())
.required()
.transform(parseDateString)
.afterSubmit(dateToMilliseconds)
),
fieldProps: {
defaultValue: getTomorrowAtMidnight(),
minDateTime: getNow(),
},
}
// --------------------------------------------------------
// Repeat fields
// --------------------------------------------------------
/** @type {Field} Granularity of action */
const REPEAT_FIELD = {
name: 'REPEAT',
label: T.GranularityOfAction,
type: INPUT_TYPES.SELECT,
dependOf: PERIODIC_FIELD.name,
htmlType: (isPeriodic) => (!isPeriodic ? INPUT_TYPES.HIDDEN : undefined),
values: arrayToOptions(Object.keys(REPEAT_VALUES), {
addEmpty: false,
getText: (key) => sentenceCase(key),
getValue: (key) => REPEAT_VALUES[key],
}),
validation: string()
.trim()
.required()
.default(() => REPEAT_VALUES.WEEKLY)
.when(PERIODIC_FIELD.name, {
is: false,
then: (schema) => schema.strip().notRequired(),
}),
}
/** @type {Field} Weekly field */
const WEEKLY_FIELD = {
name: 'WEEKLY',
dependOf: [REPEAT_FIELD.name, PERIODIC_FIELD.name],
type: INPUT_TYPES.SELECT,
multiple: true,
label: T.DayOfWeek,
values: arrayToOptions(DAYS_OF_WEEK, {
addEmpty: false,
getValue: (_, index) => String(index),
}),
htmlType: ([repeatType, isPeriodic] = []) =>
(!isPeriodic || repeatType !== REPEAT_VALUES.WEEKLY) && INPUT_TYPES.HIDDEN,
validation: lazy((_, { context }) =>
array(string())
.required(T.DaysBetween0_6)
.min(1)
.default(() => context?.[DAYS_FIELD.name]?.split?.(',') ?? [])
.when([PERIODIC_FIELD.name, REPEAT_FIELD.name], {
is: (isPeriodic, repeatType) =>
!isPeriodic || repeatType !== REPEAT_VALUES.WEEKLY,
then: (schema) => schema.strip().notRequired(),
})
.afterSubmit((value) => value?.join?.(','))
),
}
/** @type {Field} Monthly field */
const MONTHLY_FIELD = {
name: 'MONTHLY',
dependOf: [REPEAT_FIELD.name, PERIODIC_FIELD.name],
type: INPUT_TYPES.TEXT,
label: T.DayOfMonth,
htmlType: ([repeatType, isPeriodic] = []) =>
(!isPeriodic || repeatType !== REPEAT_VALUES.MONTHLY) && INPUT_TYPES.HIDDEN,
validation: lazy((_, { context }) =>
string()
.trim()
.matches(MONTH_DAYS_REG, { message: T.DaysBetween1_31 })
.required()
.default(() => context?.[DAYS_FIELD.name])
.when([PERIODIC_FIELD.name, REPEAT_FIELD.name], {
is: (isPeriodic, repeatType) =>
!isPeriodic || repeatType !== REPEAT_VALUES.MONTHLY,
then: (schema) => schema.strip().notRequired(),
})
),
}
/** @type {Field} Yearly field */
const YEARLY_FIELD = {
name: 'YEARLY',
dependOf: [REPEAT_FIELD.name, PERIODIC_FIELD.name],
type: INPUT_TYPES.TEXT,
label: T.DayOfYear,
htmlType: ([repeatType, isPeriodic] = []) =>
(!isPeriodic || repeatType !== REPEAT_VALUES.YEARLY) && INPUT_TYPES.HIDDEN,
validation: lazy((_, { context }) =>
string()
.trim()
.matches(YEAR_DAYS_REG, { message: T.DaysBetween0_365 })
.required()
.default(() => context?.[DAYS_FIELD.name])
.when([PERIODIC_FIELD.name, REPEAT_FIELD.name], {
is: (isPeriodic, repeatType) =>
!isPeriodic || repeatType !== REPEAT_VALUES.YEARLY,
then: (schema) => schema.strip().notRequired(),
})
),
}
/** @type {Field} Hourly field */
const HOURLY_FIELD = {
name: 'HOURLY',
dependOf: [REPEAT_FIELD.name, PERIODIC_FIELD.name],
type: INPUT_TYPES.TEXT,
label: T.EachXHours,
htmlType: ([repeatType, isPeriodic] = []) =>
!isPeriodic || repeatType !== REPEAT_VALUES.HOURLY
? INPUT_TYPES.HIDDEN
: 'number',
validation: lazy((_, { context }) =>
number()
.min(0)
.max(168)
.integer()
.required()
.default(() => context?.[DAYS_FIELD.name])
.when([PERIODIC_FIELD.name, REPEAT_FIELD.name], {
is: (isPeriodic, repeatType) =>
!isPeriodic || repeatType !== REPEAT_VALUES.HOURLY,
then: (schema) => schema.strip().notRequired(),
})
.afterSubmit((value) => `${value}`)
),
fieldProps: { min: 0, max: 168, step: 1 },
}
/**
* This field is only used to transform the number of the days that
* the users wants execute the action: weekly, monthly, yearly or hourly
*
* ** Depends of {@link PERIODIC_FIELD} and {@link REPEAT_FIELD} fields **
*
* @type {Field} Days field
*/
const DAYS_FIELD = {
name: 'DAYS',
validation: string().afterSubmit((_, { parent }) => {
const isPeriodic = !!parent?.[PERIODIC_FIELD.name]
const repeatType = parent?.[REPEAT_FIELD.name]
if (!isPeriodic) return undefined
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]
}),
}
// --------------------------------------------------------
// End fields
// --------------------------------------------------------
/** @type {Field} End type field */
const END_TYPE_FIELD = {
name: 'END_TYPE',
label: T.EndType,
type: INPUT_TYPES.SELECT,
dependOf: PERIODIC_FIELD.name,
htmlType: (isPeriodic) => !isPeriodic && INPUT_TYPES.HIDDEN,
values: arrayToOptions(Object.keys(END_TYPE_VALUES), {
addEmpty: false,
getText: (value) => sentenceCase(value),
getValue: (value) => END_TYPE_VALUES[value],
}),
validation: string()
.trim()
.required()
.default(() => END_TYPE_VALUES.NEVER)
.when(PERIODIC_FIELD.name, {
is: false,
then: (schema) => schema.strip().notRequired(),
}),
}
/** @type {Field} End value field */
const END_VALUE_FIELD = {
name: 'END_VALUE',
label: T.WhenYouWantThatTheActionFinishes,
dependOf: [PERIODIC_FIELD.name, END_TYPE_FIELD.name],
type: ([_, endType] = []) =>
endType === END_TYPE_VALUES.DATE ? INPUT_TYPES.TIME : INPUT_TYPES.TEXT,
htmlType: ([isPeriodic, endType] = []) =>
!isPeriodic || endType === END_TYPE_VALUES.NEVER
? INPUT_TYPES.HIDDEN
: 'number',
validation: mixed().when(
END_TYPE_FIELD.name,
(endType) =>
({
[END_TYPE_VALUES.NEVER]: string().strip(),
[END_TYPE_VALUES.REPETITION]: number().required().min(1).default(1),
[END_TYPE_VALUES.DATE]: lazy(() =>
date()
.min(ref(TIME_FIELD.name))
.required()
.transform(parseDateString)
.afterSubmit(dateToMilliseconds)
),
}[endType])
),
fieldProps: ([_, endType] = []) =>
endType === END_TYPE_VALUES.DATE && { defaultValue: getNextWeek() },
}
// --------------------------------------------------------
// Relative fields
// --------------------------------------------------------
/** @type {Field} Relative time field */
export const RELATIVE_TIME_FIELD = {
name: 'TIME',
label: T.TimeAfterTheVmIsInstantiated,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
validation: number()
.required()
.positive()
.transform((value, originalValue) =>
isRelative(originalValue)
? getPeriodicityByTimeInSeconds(originalValue)?.time
: value
),
}
/** @type {Field} Periodicity type field */
export const PERIOD_FIELD = {
name: 'PERIOD',
label: T.PeriodType,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.keys(PERIOD_TYPES), {
addEmpty: false,
getText: (key) => sentenceCase(key),
getValue: (key) => PERIOD_TYPES[key],
}),
validation: lazy((_, { context }) =>
string()
.trim()
.required()
.default(
() =>
getPeriodicityByTimeInSeconds(context?.[TIME_FIELD.name])?.period ??
PERIOD_TYPES.YEARS
)
),
}
/**
* Filters the types to discard absolute times.
*
* @see {@linkplain https://github.com/OpenNebula/one/issues/5673 Waiting support from scheduler}
* @type {Field} End types available to relative actions
*/
const END_TYPE_FIELD_WITHOUT_DATE = {
...END_TYPE_FIELD,
values: END_TYPE_FIELD.values.filter(
({ value }) => value !== END_TYPE_VALUES.DATE
),
}
// --------------------------------------------------------
// Export
// --------------------------------------------------------
export const RELATIVE_FIELDS = {
RELATIVE_TIME_FIELD,
PERIOD_FIELD,
END_TYPE_FIELD_WITHOUT_DATE,
}
export const PUNCTUAL_FIELDS = {
ACTION_FIELD,
ACTION_FIELD_FOR_CHARTERS,
TIME_FIELD,
ARGS_NAME_FIELD,
ARGS_DISK_ID_FIELD,
ARGS_SNAPSHOT_ID_FIELD,
PERIODIC_FIELD,
REPEAT_FIELD,
WEEKLY_FIELD,
MONTHLY_FIELD,
YEARLY_FIELD,
HOURLY_FIELD,
DAYS_FIELD,
END_TYPE_FIELD,
END_VALUE_FIELD,
}

@ -13,7 +13,57 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import PunctualForm from 'client/components/Forms/Vm/CreateSchedActionForm/PunctualForm'
import RelativeForm from 'client/components/Forms/Vm/CreateSchedActionForm/RelativeForm'
import {
timeToSecondsByPeriodicity,
transformStringToArgsObject,
} from 'client/models/Scheduler'
import { createForm } from 'client/utils'
export { PunctualForm, RelativeForm }
import {
SCHED_SCHEMA,
SCHED_FIELDS,
RELATIVE_SCHED_SCHEMA,
RELATIVE_SCHED_FIELDS,
} from 'client/components/Forms/Vm/CreateSchedActionForm/schema'
const commonTransformInitialValue = (scheduledAction, schema) => {
const dataToCast = {
...scheduledAction,
// get action arguments from ARGS
ARGS: transformStringToArgsObject(scheduledAction),
}
return schema.cast(dataToCast, { context: scheduledAction })
}
const commonTransformBeforeSubmit = (formData) => {
const { WEEKLY, MONTHLY, YEARLY, HOURLY, PERIODIC, ARGS, ...filteredData } =
formData
// transform action arguments to string
const argValues = Object.values(ARGS ?? {})?.filter(Boolean)
argValues.length && (filteredData.ARGS = argValues.join(','))
return filteredData
}
const CreateSchedActionForm = createForm(SCHED_SCHEMA, SCHED_FIELDS, {
transformInitialValue: commonTransformInitialValue,
transformBeforeSubmit: commonTransformBeforeSubmit,
})
const RelativeForm = createForm(RELATIVE_SCHED_SCHEMA, RELATIVE_SCHED_FIELDS, {
transformInitialValue: commonTransformInitialValue,
transformBeforeSubmit: (formData) => {
const { PERIOD, TIME, ...restData } = commonTransformBeforeSubmit(formData)
return {
...restData,
TIME: `+${timeToSecondsByPeriodicity(PERIOD, TIME)}`,
}
},
})
export { RelativeForm }
export default CreateSchedActionForm

@ -0,0 +1,117 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string, object, ObjectSchema } from 'yup'
import { ARGS_TYPES, getRequiredArgsByAction } from 'client/models/Scheduler'
import { Field, getObjectSchemaFromFields } from 'client/utils'
import {
PUNCTUAL_FIELDS,
RELATIVE_FIELDS,
} from 'client/components/Forms/Vm/CreateSchedActionForm/fields'
const { ACTION_FIELD } = PUNCTUAL_FIELDS
const ARG_SCHEMA = string()
.trim()
.required()
.default(() => undefined)
const ARG_SCHEMAS = {
[ARGS_TYPES.DISK_ID]: ARG_SCHEMA,
[ARGS_TYPES.NAME]: ARG_SCHEMA,
[ARGS_TYPES.SNAPSHOT_ID]: ARG_SCHEMA,
}
/**
* @param {object} vm - Vm resource
* @returns {Field[]} Common fields
*/
const COMMON_FIELDS = (vm) => [
PUNCTUAL_FIELDS.ARGS_NAME_FIELD,
PUNCTUAL_FIELDS.ARGS_DISK_ID_FIELD(vm),
PUNCTUAL_FIELDS.ARGS_SNAPSHOT_ID_FIELD(vm),
PUNCTUAL_FIELDS.PERIODIC_FIELD,
PUNCTUAL_FIELDS.REPEAT_FIELD,
PUNCTUAL_FIELDS.WEEKLY_FIELD,
PUNCTUAL_FIELDS.MONTHLY_FIELD,
PUNCTUAL_FIELDS.YEARLY_FIELD,
PUNCTUAL_FIELDS.HOURLY_FIELD,
]
/** @type {ObjectSchema} Common schema with relative */
const COMMON_SCHEMA = object({
[ACTION_FIELD.name]: ACTION_FIELD.validation,
ARGS: object().when(ACTION_FIELD.name, (action) =>
getRequiredArgsByAction(action)
.map((arg) => object({ [arg]: ARG_SCHEMAS[arg] }))
.reduce((result, argSchema) => result.concat(argSchema), object())
),
})
/**
* @param {object} vm - Vm resource
* @returns {Field[]} Fields
*/
export const SCHED_FIELDS = (vm) => [
PUNCTUAL_FIELDS.ACTION_FIELD,
PUNCTUAL_FIELDS.TIME_FIELD,
...COMMON_FIELDS(vm),
PUNCTUAL_FIELDS.END_TYPE_FIELD,
PUNCTUAL_FIELDS.END_VALUE_FIELD,
]
/** @type {Field[]} Fields for relative actions */
export const RELATIVE_SCHED_FIELDS = (vm) => [
PUNCTUAL_FIELDS.ACTION_FIELD,
RELATIVE_FIELDS.RELATIVE_TIME_FIELD,
RELATIVE_FIELDS.PERIOD_FIELD,
...COMMON_FIELDS(vm),
RELATIVE_FIELDS.END_TYPE_FIELD_WITHOUT_DATE,
PUNCTUAL_FIELDS.END_VALUE_FIELD,
]
/** @type {ObjectSchema} Schema */
export const SCHED_SCHEMA = COMMON_SCHEMA.concat(
getObjectSchemaFromFields([
PUNCTUAL_FIELDS.TIME_FIELD,
PUNCTUAL_FIELDS.PERIODIC_FIELD,
PUNCTUAL_FIELDS.REPEAT_FIELD,
PUNCTUAL_FIELDS.WEEKLY_FIELD,
PUNCTUAL_FIELDS.MONTHLY_FIELD,
PUNCTUAL_FIELDS.YEARLY_FIELD,
PUNCTUAL_FIELDS.HOURLY_FIELD,
PUNCTUAL_FIELDS.DAYS_FIELD,
PUNCTUAL_FIELDS.END_TYPE_FIELD,
PUNCTUAL_FIELDS.END_VALUE_FIELD,
])
)
/** @type {ObjectSchema} Relative Schema */
export const RELATIVE_SCHED_SCHEMA = COMMON_SCHEMA.concat(
getObjectSchemaFromFields([
RELATIVE_FIELDS.RELATIVE_TIME_FIELD,
RELATIVE_FIELDS.PERIOD_FIELD,
PUNCTUAL_FIELDS.PERIODIC_FIELD,
PUNCTUAL_FIELDS.REPEAT_FIELD,
PUNCTUAL_FIELDS.WEEKLY_FIELD,
PUNCTUAL_FIELDS.MONTHLY_FIELD,
PUNCTUAL_FIELDS.YEARLY_FIELD,
PUNCTUAL_FIELDS.HOURLY_FIELD,
PUNCTUAL_FIELDS.DAYS_FIELD,
RELATIVE_FIELDS.END_TYPE_FIELD_WITHOUT_DATE,
PUNCTUAL_FIELDS.END_VALUE_FIELD,
])
)

@ -13,30 +13,169 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import AttachNicForm from 'client/components/Forms/Vm/AttachNicForm'
import ChangeUserForm from 'client/components/Forms/Vm/ChangeUserForm'
import ChangeGroupForm from 'client/components/Forms/Vm/ChangeGroupForm'
import CreateDiskSnapshotForm from 'client/components/Forms/Vm/CreateDiskSnapshotForm'
import CreateSnapshotForm from 'client/components/Forms/Vm/CreateSnapshotForm'
import MigrateForm from 'client/components/Forms/Vm/MigrateForm'
import RecoverForm from 'client/components/Forms/Vm/RecoverForm'
import ResizeCapacityForm from 'client/components/Forms/Vm/ResizeCapacityForm'
import ResizeDiskForm from 'client/components/Forms/Vm/ResizeDiskForm'
import SaveAsDiskForm from 'client/components/Forms/Vm/SaveAsDiskForm'
import SaveAsTemplateForm from 'client/components/Forms/Vm/SaveAsTemplateForm'
export * from 'client/components/Forms/Vm/AttachDiskForm'
export * from 'client/components/Forms/Vm/CreateSchedActionForm'
import loadable, { Options } from '@loadable/component'
import { CreateFormCallback, CreateStepsCallback } from 'client/utils/schema'
/**
* @param {object} properties - Dynamic properties
* @param {string} properties.formPath - Form pathname
* @param {string} [properties.componentToLoad] - Load different component instead of default
* @param {Options} [properties.options] - Options
* @param {...any} args - Arguments
* @returns {CreateFormCallback|CreateStepsCallback} Resolved form
*/
const AsyncLoadForm = async (properties = {}, ...args) => {
const { formPath, componentToLoad = 'default', options } = properties
const form = await loadable(() => import(`./${formPath}`), {
cacheKey: () => formPath,
...options,
}).load()
return form[componentToLoad]?.(...args)
}
/**
* @param {...any} args - Arguments
* @returns {CreateStepsCallback} Asynchronous loaded form
*/
const ImageSteps = (...args) =>
AsyncLoadForm({ formPath: 'AttachDiskForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateStepsCallback} Asynchronous loaded form
*/
const VolatileSteps = (...args) =>
AsyncLoadForm({ formPath: 'AttachDiskForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const AttachNicForm = (...args) =>
AsyncLoadForm({ formPath: 'AttachNicForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const ChangeUserForm = (...args) =>
AsyncLoadForm({ formPath: 'ChangeUserForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const ChangeGroupForm = (...args) =>
AsyncLoadForm({ formPath: 'ChangeGroupForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const CreateDiskSnapshotForm = (...args) =>
AsyncLoadForm({ formPath: 'CreateDiskSnapshotForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const CreateSnapshotForm = (...args) =>
AsyncLoadForm({ formPath: 'CreateSnapshotForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const MigrateForm = (...args) =>
AsyncLoadForm({ formPath: 'MigrateForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const RecoverForm = (...args) =>
AsyncLoadForm({ formPath: 'RecoverForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const ResizeCapacityForm = (...args) =>
AsyncLoadForm({ formPath: 'ResizeCapacityForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const ResizeDiskForm = (...args) =>
AsyncLoadForm({ formPath: 'ResizeDiskForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const SaveAsDiskForm = (...args) =>
AsyncLoadForm({ formPath: 'SaveAsDiskForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const SaveAsTemplateForm = (...args) =>
AsyncLoadForm({ formPath: 'SaveAsTemplateForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const CreateSchedActionForm = (...args) =>
AsyncLoadForm({ formPath: 'CreateSchedActionForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const CreateRelativeSchedActionForm = (...args) =>
AsyncLoadForm(
{ formPath: 'CreateSchedActionForm', componentToLoad: 'RelativeForm' },
...args
)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const CreateCharterForm = (...args) =>
AsyncLoadForm({ formPath: 'CreateCharterForm' }, ...args)
/**
* @param {...any} args - Arguments
* @returns {CreateFormCallback} Asynchronous loaded form
*/
const CreateRelativeCharterForm = (...args) =>
AsyncLoadForm(
{ formPath: 'CreateCharterForm', componentToLoad: 'RelativeForm' },
...args
)
export {
AttachNicForm,
ChangeUserForm,
ChangeGroupForm,
ChangeUserForm,
CreateCharterForm,
CreateDiskSnapshotForm,
CreateRelativeCharterForm,
CreateRelativeSchedActionForm,
CreateSchedActionForm,
CreateSnapshotForm,
ImageSteps,
MigrateForm,
RecoverForm,
ResizeCapacityForm,
ResizeDiskForm,
SaveAsDiskForm,
SaveAsTemplateForm,
VolatileSteps,
}

@ -13,16 +13,12 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { Calendar as ActionIcon, Edit, Trash } from 'iconoir-react'
import { Calendar as ActionIcon } from 'iconoir-react'
import { useFieldArray } from 'react-hook-form'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import { PunctualForm, RelativeForm } from 'client/components/Forms/Vm'
import { Translate } from 'client/components/HOC'
import { ScheduleActionCard } from 'client/components/Cards'
import { CreateSchedButton, CharterButton } from 'client/components/Buttons'
import {
STEP_ID as EXTRA_ID,
@ -43,42 +39,36 @@ const ScheduleAction = () => {
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 handleUpdateAction = (action, index) => {
update(index, mapNameFunction(action, index))
}
const handleRemoveAction = (index) => {
remove(index)
}
return (
<>
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'add-sched-action',
label: T.AddAction,
variant: 'outlined',
}}
options={[
{
cy: 'add-sched-action-punctual',
name: 'Punctual action',
dialogProps: {
title: T.ScheduledAction,
dataCy: 'modal-sched-actions',
},
form: () => PunctualForm(),
onSubmit: (action) =>
append(mapNameFunction(action, scheduleActions.length)),
},
{
cy: 'add-sched-action-relative',
name: 'Relative action',
dialogProps: {
title: T.ScheduledAction,
dataCy: 'modal-sched-actions',
},
form: () => RelativeForm(),
onSubmit: (action) =>
append(mapNameFunction(action, scheduleActions.length)),
},
]}
/>
<Stack flexDirection="row" gap="1em">
<CreateSchedButton relative onSubmit={handleCreateAction} />
<CharterButton relative onSubmit={handleCreateCharter} />
</Stack>
<Stack
pb="1em"
display="grid"
@ -86,48 +76,16 @@ const ScheduleAction = () => {
gap="1em"
mt="1em"
>
{scheduleActions?.map((item, index) => {
const { id, NAME, ACTION, TIME } = item
const isRelative = String(TIME).includes('+')
{scheduleActions?.map((schedule, index) => {
const { ID, NAME } = schedule
return (
<SelectCard
key={id ?? NAME}
title={`${NAME} - ${ACTION}`}
action={
<>
<Action
data-cy={`remove-${NAME}`}
handleClick={() => remove(index)}
icon={<Trash />}
/>
<ButtonToTriggerForm
buttonProps={{
'data-cy': `edit-${NAME}`,
icon: <Edit />,
tooltip: <Translate word={T.Edit} />,
}}
options={[
{
dialogProps: {
title: (
<>
<Translate word={T.Edit} />
{`: ${NAME}`}
</>
),
},
form: () =>
isRelative
? RelativeForm(undefined, item)
: PunctualForm(undefined, item),
onSubmit: (updatedAction) =>
update(index, mapNameFunction(updatedAction, index)),
},
]}
/>
</>
}
<ScheduleActionCard
key={ID ?? NAME}
relative
schedule={{ ...schedule, ID: index }}
handleUpdate={(newAction) => handleUpdateAction(newAction, index)}
handleRemove={() => handleRemoveAction(index)}
/>
)
})}
@ -136,13 +94,6 @@ const ScheduleAction = () => {
)
}
ScheduleAction.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
hypervisor: PropTypes.string,
control: PropTypes.object,
}
/** @type {TabType} */
const TAB = {
id: 'sched_action',

@ -0,0 +1,178 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useContext, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { useAuth } from 'client/features/Auth'
import { useVmApi } from 'client/features/One'
import { TabContext } from 'client/components/Tabs/TabProvider'
import ScheduleActionCard from 'client/components/Cards/ScheduleActionCard'
import {
CreateSchedButton,
CharterButton,
} from 'client/components/Buttons/ScheduleAction'
import {
getScheduleActions,
getHypervisor,
isAvailableAction,
} from 'client/models/VirtualMachine'
import { getActionsAvailable, jsonToXml } from 'client/models/Helper'
import { VM_ACTIONS } from 'client/constants'
const {
SCHED_ACTION_CREATE,
SCHED_ACTION_UPDATE,
SCHED_ACTION_DELETE,
CHARTER_CREATE,
} = VM_ACTIONS
/**
* Renders the list of schedule action from a VM.
*
* @param {object} props - Props
* @param {object|boolean} props.tabProps - Tab properties
* @param {object} [props.tabProps.actions] - Actions from user view yaml
* @returns {ReactElement} List of schedule actions
*/
const VmSchedulingTab = ({ tabProps: { actions } = {} }) => {
const { config } = useAuth()
const { handleRefetch, data: vm } = useContext(TabContext)
const { addScheduledAction, updateScheduledAction, deleteScheduledAction } =
useVmApi()
const [scheduling, actionsAvailable] = useMemo(() => {
const hypervisor = getHypervisor(vm)
const actionsByHypervisor = getActionsAvailable(actions, hypervisor)
const actionsByState = actionsByHypervisor.filter(
(action) => !isAvailableAction(action)(vm)
)
return [getScheduleActions(vm), actionsByState]
}, [vm])
const iCreateEnabled = actionsAvailable?.includes?.(SCHED_ACTION_CREATE)
const isUpdateEnabled = actionsAvailable?.includes?.(SCHED_ACTION_UPDATE)
const isDeleteEnabled = actionsAvailable?.includes?.(SCHED_ACTION_DELETE)
const isCharterEnabled =
actionsAvailable?.includes?.(CHARTER_CREATE) && config?.leases
/**
* Add new schedule action to VM.
*
* @param {object} formData - New schedule action
* @returns {Promise} - Add schedule action and refetch VM data
*/
const handleCreateSchedAction = async (formData) => {
const data = { template: jsonToXml({ SCHED_ACTION: formData }) }
const response = await addScheduledAction(vm.ID, data)
String(response) === String(vm.ID) && (await handleRefetch?.(vm.ID))
}
/**
* Update schedule action to VM.
*
* @param {object} formData - Updated schedule action
* @param {string|number} id - Schedule action id
* @returns {Promise} - Update schedule action and refetch VM data
*/
const handleUpdate = async (formData, id) => {
const data = {
id_sched: id,
template: jsonToXml({ SCHED_ACTION: formData }),
}
const response = await updateScheduledAction(vm.ID, data)
String(response) === String(vm.ID) && (await handleRefetch?.(vm.ID))
}
/**
* Delete schedule action to VM.
*
* @param {string|number} id - Schedule action id
* @returns {Promise} - Delete schedule action and refetch VM data
*/
const handleRemove = async (id) => {
const data = { id_sched: id }
const response = await deleteScheduledAction(vm.ID, data)
String(response) === String(vm.ID) && (await handleRefetch?.(vm.ID))
}
/**
* Add leases from sunstone-server.conf to VM.
*
* @param {object[]} formData - List of leases (schedule action)
* @returns {Promise} - Add schedule actions and refetch VM data
*/
const handleCreateCharter = async (formData) => {
const responses = await Promise.all(
formData.map((schedAction) => {
const data = { template: jsonToXml({ SCHED_ACTION: schedAction }) }
return addScheduledAction(vm.ID, data)
})
)
responses.some((response) => String(response) === String(vm?.ID)) &&
(await handleRefetch?.(vm.ID))
}
return (
<>
{(iCreateEnabled || isCharterEnabled) && (
<Stack flexDirection="row" gap="1em">
{iCreateEnabled && (
<CreateSchedButton vm={vm} onSubmit={handleCreateSchedAction} />
)}
{isCharterEnabled && <CharterButton onSubmit={handleCreateCharter} />}
</Stack>
)}
<Stack display="grid" gap="1em" py="0.8em">
{scheduling.map((schedule) => {
const { ID, NAME } = schedule
return (
<ScheduleActionCard
key={ID ?? NAME}
vm={vm}
schedule={schedule}
{...(isUpdateEnabled && {
handleUpdate: (newAction) => handleUpdate(newAction, ID),
})}
{...(isDeleteEnabled && { handleRemove: () => handleRemove(ID) })}
/>
)
})}
</Stack>
</>
)
}
VmSchedulingTab.propTypes = {
tabProps: PropTypes.shape({
actions: PropTypes.object,
}),
}
VmSchedulingTab.displayName = 'VmSchedulingTab'
export default VmSchedulingTab

@ -1,239 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { memo, useContext } from 'react'
import PropTypes from 'prop-types'
import { Trash, Edit, ClockOutline } from 'iconoir-react'
import { useAuth } from 'client/features/Auth'
import { useVmApi } from 'client/features/One'
import { TabContext } from 'client/components/Tabs/TabProvider'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import { PunctualForm, RelativeForm } from 'client/components/Forms/Vm'
import * as Helper from 'client/models/Helper'
import { Tr, Translate } from 'client/components/HOC'
import { T, VM_ACTIONS } from 'client/constants'
const CreateSchedAction = memo(() => {
const { addScheduledAction } = useVmApi()
const { handleRefetch, data: vm } = useContext(TabContext)
const handleCreateSchedAction = async (formData) => {
const data = { template: Helper.jsonToXml({ SCHED_ACTION: formData }) }
const response = await addScheduledAction(vm.ID, data)
String(response) === String(vm.ID) && (await handleRefetch?.(vm.ID))
}
return (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'create-sched-action',
label: T.AddAction,
variant: 'outlined',
}}
options={[
{
cy: 'create-sched-action-punctual',
name: 'Punctual action',
dialogProps: { title: T.ScheduledAction },
form: () => PunctualForm(vm),
onSubmit: handleCreateSchedAction,
},
{
cy: 'create-sched-action-relative',
name: 'Relative action',
dialogProps: { title: T.ScheduledAction },
form: () => RelativeForm(vm),
onSubmit: handleCreateSchedAction,
},
]}
/>
)
})
const UpdateSchedAction = memo(({ schedule, name }) => {
const { ID, TIME } = schedule
const isRelative = String(TIME).includes('+')
const { updateScheduledAction } = useVmApi()
const { handleRefetch, data: vm } = useContext(TabContext)
const handleUpdate = async (formData) => {
const data = {
id_sched: ID,
template: Helper.jsonToXml({ SCHED_ACTION: formData }),
}
const response = await updateScheduledAction(vm.ID, data)
String(response) === String(vm.ID) && (await handleRefetch?.(vm.ID))
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VM_ACTIONS.SCHED_ACTION_UPDATE}-${ID}`,
icon: <Edit />,
tooltip: <Translate word={T.Edit} />,
}}
options={[
{
dialogProps: {
title: <Translate word={T.UpdateScheduledAction} values={[name]} />,
},
form: () =>
isRelative
? RelativeForm(vm, schedule)
: PunctualForm(vm, schedule),
onSubmit: handleUpdate,
},
]}
/>
)
})
const DeleteSchedAction = memo(({ schedule, name }) => {
const { ID } = schedule
const { deleteScheduledAction } = useVmApi()
const { handleRefetch, data: vm } = useContext(TabContext)
const handleDelete = async () => {
const data = { id_sched: ID }
const response = await deleteScheduledAction(vm.ID, data)
String(response) === String(vm.ID) && (await handleRefetch?.(vm.ID))
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `${VM_ACTIONS.SCHED_ACTION_DELETE}-${ID}`,
icon: <Trash />,
tooltip: <Translate word={T.Delete} />,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: <Translate word={T.DeleteScheduledAction} values={[name]} />,
children: <p>{Tr(T.DoYouWantProceed)}</p>,
},
onSubmit: handleDelete,
},
]}
/>
)
})
const CharterAction = memo(() => {
const { config } = useAuth()
const { addScheduledAction } = useVmApi()
const { handleRefetch, data: vm } = useContext(TabContext)
const leases = Object.entries(config?.leases ?? {})
const handleCreateCharter = async () => {
const schedActions = leases.map(
([action, { time, warning: { time: warningTime } = {} } = {}]) => ({
TIME: `+${+time}`,
ACTION: action,
...(warningTime && { WARNING: `-${+warningTime}` }),
})
)
const response = await Promise.all(
schedActions.map((schedAction) => {
const data = {
template: Helper.jsonToXml({ SCHED_ACTION: schedAction }),
}
return addScheduledAction(vm.ID, data)
})
)
response.some((res) => String(res) === String(vm?.ID)) &&
(await handleRefetch?.(vm.ID))
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': 'create-charter',
icon: <ClockOutline />,
tooltip: <Translate word={T.Charter} />,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: Tr(T.ScheduledAction),
children: (
<>
{leases.map(([action, { time } = {}], idx) => {
const allPeriods = {
years: time / 365 / 24 / 3600,
months: time / 30 / 24 / 3600,
weeks: time / 7 / 24 / 3600,
days: time / 24 / 3600,
hours: time / 3600,
minutes: time / 60,
}
const [period, parsedTime] = Object.entries(allPeriods).find(
([_, value]) => value >= 1
)
return (
<p key={`${action}-${idx}`}>
{`${action} - ${parsedTime} ${period}`}
</p>
)
})}
<hr />
<p>{Tr(T.DoYouWantProceed)}</p>
</>
),
},
onSubmit: handleCreateCharter,
},
]}
/>
)
})
const ActionPropTypes = {
schedule: PropTypes.object,
name: PropTypes.string,
}
CreateSchedAction.propTypes = ActionPropTypes
CreateSchedAction.displayName = 'CreateSchedActionButton'
UpdateSchedAction.propTypes = ActionPropTypes
UpdateSchedAction.displayName = 'UpdateSchedActionButton'
DeleteSchedAction.propTypes = ActionPropTypes
DeleteSchedAction.displayName = 'DeleteSchedActionButton'
CharterAction.propTypes = ActionPropTypes
CharterAction.displayName = 'CharterActionButton'
export {
CharterAction,
CreateSchedAction,
DeleteSchedAction,
UpdateSchedAction,
}

@ -1,103 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useContext } from 'react'
import PropTypes from 'prop-types'
import { WarningTriangleOutline as WarningIcon } from 'iconoir-react'
import { useTheme, Typography, Paper } from '@mui/material'
import { TabContext } from 'client/components/Tabs/TabProvider'
import * as Actions from 'client/components/Tabs/Vm/SchedActions/Actions'
import { StatusChip } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { Tr } from 'client/components/HOC'
import * as VirtualMachine from 'client/models/VirtualMachine'
import { timeFromMilliseconds } from 'client/models/Helper'
import { sentenceCase } from 'client/utils'
import { T, VM_ACTIONS } from 'client/constants'
const SchedulingItem = ({ schedule, actions = [] }) => {
const classes = rowStyles()
const { palette } = useTheme()
const { data: vm } = useContext(TabContext)
const vmStartTime = +vm?.STIME
const { ID, ACTION, TIME, MESSAGE, DONE, WARNING } = schedule
const titleAction = `#${ID} ${sentenceCase(ACTION)}`
const isRelative = String(TIME).includes('+')
const time = timeFromMilliseconds(isRelative ? vmStartTime + +TIME : +TIME)
const doneTime = timeFromMilliseconds(+DONE)
const now = Math.round(Date.now() / 1000)
const isWarning = WARNING && now - vmStartTime > +WARNING
const labels = [...new Set([MESSAGE])].filter(Boolean)
const { repeat, end } = VirtualMachine.periodicityToString(schedule)
return (
<Paper variant="outlined" className={classes.root}>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{titleAction}</Typography>
{!!labels.length && (
<span className={classes.labels}>
{labels.map((label) => (
<StatusChip key={label} text={label} />
))}
</span>
)}
</div>
<div className={classes.caption}>
{repeat && <span>{`${repeat} |`}</span>}
{end && <span>{`${end} |`}</span>}
{DONE && (
<span title={doneTime.toFormat('ff')}>
{`${Tr(T.Done)} ${doneTime.toRelative()} |`}
</span>
)}
<span style={{ display: 'flex', gap: '0.5em' }}>
<span title={time.toFormat('ff')}>{`${time.toRelative()}`}</span>
{isWarning && <WarningIcon color={palette.warning.main} />}
</span>
</div>
</div>
{!!actions.length && (
<div className={classes.actions}>
{actions?.includes?.(VM_ACTIONS.SCHED_ACTION_UPDATE) && (
<Actions.UpdateSchedAction schedule={schedule} name={titleAction} />
)}
{actions?.includes?.(VM_ACTIONS.SCHED_ACTION_DELETE) && (
<Actions.DeleteSchedAction schedule={schedule} name={titleAction} />
)}
</div>
)}
</Paper>
)
}
SchedulingItem.propTypes = {
schedule: PropTypes.object.isRequired,
actions: PropTypes.arrayOf(PropTypes.string),
}
SchedulingItem.displayName = 'SchedulingItem'
export default SchedulingItem

@ -1,36 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types'
import SchedulingItem from 'client/components/Tabs/Vm/SchedActions/Item'
const SchedulingList = ({ scheduling, actions }) => (
<div style={{ display: 'grid', gap: '1em', paddingBlock: '0.8em' }}>
{scheduling.map((schedule) => (
<SchedulingItem key={schedule.ID} schedule={schedule} actions={actions} />
))}
</div>
)
SchedulingList.propTypes = {
scheduling: PropTypes.array,
actions: PropTypes.arrayOf(PropTypes.string),
}
SchedulingList.displayName = 'SchedulingList'
export default SchedulingList

@ -1,69 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useContext, useMemo } from 'react'
import PropTypes from 'prop-types'
import { useAuth } from 'client/features/Auth'
import { TabContext } from 'client/components/Tabs/TabProvider'
import {
CreateSchedAction,
CharterAction,
} from 'client/components/Tabs/Vm/SchedActions/Actions'
import SchedulingList from 'client/components/Tabs/Vm/SchedActions/List'
import {
getScheduleActions,
getHypervisor,
isAvailableAction,
} from 'client/models/VirtualMachine'
import { getActionsAvailable } from 'client/models/Helper'
import { VM_ACTIONS } from 'client/constants'
const VmSchedulingTab = ({ tabProps: { actions } = {} }) => {
const { config } = useAuth()
const { data: vm } = useContext(TabContext)
const [scheduling, actionsAvailable] = useMemo(() => {
const hypervisor = getHypervisor(vm)
const actionsByHypervisor = getActionsAvailable(actions, hypervisor)
const actionsByState = actionsByHypervisor.filter(
(action) => !isAvailableAction(action)(vm)
)
return [getScheduleActions(vm), actionsByState]
}, [vm])
return (
<>
{actionsAvailable?.includes?.(VM_ACTIONS.SCHED_ACTION_CREATE) && (
<CreateSchedAction />
)}
{actionsAvailable?.includes?.(VM_ACTIONS.CHARTER_CREATE) &&
config?.leases && <CharterAction />}
<SchedulingList actions={actionsAvailable} scheduling={scheduling} />
</>
)
}
VmSchedulingTab.propTypes = {
tabProps: PropTypes.object,
}
VmSchedulingTab.displayName = 'VmSchedulingTab'
export default VmSchedulingTab

@ -0,0 +1,96 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useMemo, memo, useState, useEffect } from 'react'
import { DateTime } from 'luxon'
import { Translate } from 'client/components/HOC'
import { timeFromMilliseconds } from 'client/models/Helper'
const Timer = memo(
/**
* @param {object} config - Config
* @param {number|string|DateTime} config.initial - Initial time
* @param {boolean} [config.luxon] - If `true`, the time will be a parsed as luxon DateTime
* @param {number} [config.translateWord] - Add translate component to wrap the time
* @param {number} [config.interval] - Interval time to update the time
* @param {number} [config.finishAt] - Clear the interval once time is up (in ms)
* @returns {ReactElement} Relative DateTime
*/
({ initial, luxon, translateWord, interval = 1000, finishAt }) => {
const [time, setTime] = useState('...')
useEffect(() => {
const isLuxon = luxon || initial?.isValid
const initialValue = isLuxon ? initial : timeFromMilliseconds(+initial)
const tick = setInterval(() => {
const newTime = initialValue.toRelative()
console.log({ ms: initialValue.millisecond, finishAt })
if (finishAt && initialValue.millisecond === finishAt) {
clearInterval(tick)
}
newTime !== time && setTime(newTime)
}, interval)
return () => {
clearInterval(tick)
}
}, [])
if (translateWord) {
return <Translate word={translateWord} values={[time]} />
}
return <>{time}</>
},
(prev, next) =>
prev.initial === next.initial && prev.translateWord === next.translateWord
)
/* const Timer = memo(({ initial, luxon, translateWord, interval = 1000 }) => {
const ensuredInitial = useMemo(() => {
const isLuxon = luxon || initial?.isValid
const val = isLuxon ? initial : timeFromMilliseconds(+initial)
return val.toRelative()
}, [])
const [timer, setTimer] = useState(() => ensuredInitial)
useEffect(() => {
const tick = setInterval(() => {
const newTimer = DateTime.local().toRelative()
newTimer !== timer && setTimer(newTimer)
}, interval)
return () => {
clearInterval(tick)
}
}, [])
/* if (translateWord) {
return <Translate word={translateWord} values={[timer]} />
}
return <>{timer}</>
}) */
Timer.displayName = 'Timer'
export default Timer

@ -136,8 +136,14 @@ module.exports = {
DoYouWantProceed: 'Do you want proceed?',
/* Scheduling */
Action: 'Action',
ScheduledAction: 'Scheduled action',
Charter: 'Charter',
PunctualAction: 'Punctual action',
RelativeAction: 'Relative action',
DoneAgo: 'Done %s',
NextInTime: 'Next in %s',
FirstTime: 'First time %s',
Mon: 'Mon',
Monday: 'Monday',
Tue: 'Tue',
@ -155,9 +161,25 @@ module.exports = {
Weekly: 'Weekly',
Monthly: 'Monthly',
Yearly: 'Yearly',
Hourly: 'Hourly',
EachHours: 'Each %s hours',
AfterTimes: 'After %s times',
Today: 'Today',
Periodic: 'Periodic',
Time: 'Time',
TimeAfterTheVmIsInstantiated: 'Time after the VM is instantiated',
PeriodType: 'Period type',
GranularityOfAction: 'Granularity of the action',
DayOfWeek: 'Day of week',
DayOfMonth: 'Day of month',
DayOfYear: 'Day of year',
EachXHours: "Each 'x' hours",
EndType: 'End type',
DaysBetween0_6: 'Days should be between 0 (Sunday) and 6 (Saturday)',
DaysBetween1_31: 'Days should be between 1 and 31',
DaysBetween0_365: 'Days should be between 0 and 365',
HoursBetween0_168: 'Hours should be between 0 and 168',
WhenYouWantThatTheActionFinishes: 'When you want that the action finishes',
/* dashboard */
InTotal: 'In Total',
@ -351,6 +373,7 @@ module.exports = {
Volatile: 'Volatile',
VolatileDisk: 'Volatile disk',
Snapshot: 'Snapshot',
SnapshotName: 'Snapshot name',
DiskSnapshot: 'Disk snapshot',
/* VM schema - network */
NIC: 'NIC',

@ -798,6 +798,23 @@ export const VM_ACTIONS_WITH_SCHEDULE = [
VM_ACTIONS.SNAPSHOT_DELETE,
]
/** @type {string[]} Actions that can be used in charter */
export const VM_ACTIONS_IN_CHARTER = [
VM_ACTIONS.TERMINATE,
VM_ACTIONS.TERMINATE_HARD,
VM_ACTIONS.UNDEPLOY,
VM_ACTIONS.UNDEPLOY_HARD,
VM_ACTIONS.HOLD,
VM_ACTIONS.RELEASE,
VM_ACTIONS.STOP,
VM_ACTIONS.SUSPEND,
VM_ACTIONS.RESUME,
VM_ACTIONS.REBOOT,
VM_ACTIONS.REBOOT_HARD,
VM_ACTIONS.POWEROFF,
VM_ACTIONS.POWEROFF_HARD,
]
/**
* @enum {(
* 'none' |

@ -48,6 +48,25 @@ export const booleanToString = (bool) => (bool ? T.Yes : T.No)
export const stringToBoolean = (str) =>
String(str).toLowerCase() === 'yes' || +str === 1
/**
* Returns `true` if the given value is an instance of Date.
*
* @param {*} value - The value to check
* @returns {boolean} true if the given value is a date
* @example
* const result = isDate(new Date()) //=> true
* @example
* const result = isDate(new Date(NaN)) //=> true
* @example
* const result = isDate('2014-02-31') //=> false
* @example
* const result = isDate({}) //=> false
*/
export const isDate = (value) =>
value instanceof Date ||
(typeof value === 'object' &&
Object.prototype.toString.call(value) === '[object Date]')
/**
* Converts the time values into "mm/dd/yyyy, hh:mm:ss" format.
*
@ -73,7 +92,7 @@ export const timeFromMilliseconds = (time) => DateTime.fromMillis(+time * 1000)
* @returns {number} - Total milliseconds.
*/
export const dateToMilliseconds = (date) =>
DateTime.fromJSDate(date).toMillis() / 1000
Math.trunc(DateTime.fromJSDate(date).toMillis() / 1000)
/**
* Returns the epoch milliseconds of the date.
@ -82,7 +101,7 @@ export const dateToMilliseconds = (date) =>
* @returns {number} - Total milliseconds.
*/
export const isoDateToMilliseconds = (date) =>
DateTime.fromISO(date).toMillis() / 1000
Math.trunc(DateTime.fromISO(date).toMillis() / 1000)
/**
* Get the diff from two times and it converts them

@ -0,0 +1,286 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { isDate, timeToString } from 'client/models/Helper'
import { Tr } from 'client/components/HOC'
import { T, VM_ACTIONS } from 'client/constants'
const {
SNAPSHOT_DISK_CREATE,
SNAPSHOT_DISK_REVERT,
SNAPSHOT_DISK_DELETE,
SNAPSHOT_CREATE,
SNAPSHOT_REVERT,
SNAPSHOT_DELETE,
} = VM_ACTIONS
/**
* @typedef ScheduledAction
* @property {string} ACTION - Action to execute
* @property {string} ID - Id
* @property {string} TIME - Time
* @property {string} [WARNING] - Warning time
* @property {string} [ARGS] - Arguments separated by comma
* @property {string} [DAYS] - Days that the users wants execute the action.
* List separated by comma. Depend of REPEAT:
* - weekly: 0 (Sunday) to 6 (Saturday)
* - monthly: 1 to 31
* - yearly: 1 to 365
* - hourly: each x hours
* @property {'0'|'1'|'2'} [END_TYPE] - Way to end the repetition. Can be:
* - never: 0
* - repetition: 1
* - date: 2
* @property {string} [END_VALUE] - End value
* @property {'0'|'1'|'2'|'3'} [REPEAT] - Type of repetition. Can be:
* - weekly: '0',
* - monthly: '1',
* - yearly: '2',
* - hourly: '3',
*/
/**
* @typedef CharterOptions
* @property {boolean} [edit] - If `true`, the charter can be edited in form
* @property {number|string} execute_after_days - Days to execute the action
* @property {number|string} warn_before_days - Alert a time before the action (in days)
*/
/** @enum {string} Values to end an action */
export const END_TYPE_VALUES = {
NEVER: '0',
REPETITION: '1',
DATE: '2',
}
/** @enum {string} Values to repeat an action */
export const REPEAT_VALUES = {
WEEKLY: '0',
MONTHLY: '1',
YEARLY: '2',
HOURLY: '3',
}
/** @enum {string} Argument attributes */
export const ARGS_TYPES = {
DISK_ID: 'DISK_ID',
NAME: 'NAME',
SNAPSHOT_ID: 'SNAPSHOT_ID',
}
/** @enum {string} Period type */
export const PERIOD_TYPES = {
YEARS: 'years',
MONTHS: 'months',
WEEKS: 'weeks',
DAYS: 'days',
HOURS: 'hours',
MINUTES: 'minutes',
}
/**
* Checks if time is relative.
*
* @param {string} time - Time to check
* @returns {boolean} Returns `true` if time contains plus or minus signs
*/
export const isRelative = (time) =>
!isDate(time) && (String(time).includes('+') || String(time).includes('-'))
/**
* Filters leases to get only those that cannot be edited.
*
* @param {[VM_ACTIONS, CharterOptions]} leases - Leases from configuration yaml
* @returns {[VM_ACTIONS, CharterOptions]} Fixed leases
*/
export const getFixedLeases = (leases) =>
leases?.filter(([_, { edit } = {}]) => !edit)
/**
* Filters leases to get only those that can be edited.
*
* @param {[VM_ACTIONS, CharterOptions]} leases - Leases from configuration yaml
* @returns {[VM_ACTIONS, CharterOptions]} Editable leases
*/
export const getEditableLeases = (leases) =>
leases?.filter(([_, { edit } = {}]) => !!edit)
/**
* Returns the periodicity of time in seconds.
*
* @param {number} seconds - Time in seconds
* @returns {{ period: PERIOD_TYPES, time: number }} - Periodicity and time
*/
export const getPeriodicityByTimeInSeconds = (seconds) => {
const allPeriods = {
[PERIOD_TYPES.YEARS]: seconds / 365 / 24 / 3600,
[PERIOD_TYPES.MONTHS]: seconds / 30 / 24 / 3600,
[PERIOD_TYPES.WEEKS]: seconds / 7 / 24 / 3600,
[PERIOD_TYPES.DAYS]: seconds / 24 / 3600,
[PERIOD_TYPES.HOURS]: seconds / 3600,
[PERIOD_TYPES.MINUTES]: seconds / 60,
}
const [period, time] = Object.entries(allPeriods).find(
([_, value]) => value >= 1
)
return { period, time }
}
/**
* Transform period time to seconds.
*
* @param {PERIOD_TYPES} period - Periodicity
* @param {number} time - Time in period format
* @returns {number} Time in seconds
*/
export const timeToSecondsByPeriodicity = (period, time) =>
({
[PERIOD_TYPES.YEARS]: time * 365 * 24 * 3600,
[PERIOD_TYPES.MONTHS]: time * 30 * 24 * 3600,
[PERIOD_TYPES.WEEKS]: time * 7 * 24 * 3600,
[PERIOD_TYPES.DAYS]: time * 24 * 3600,
[PERIOD_TYPES.HOURS]: time * 3600,
[PERIOD_TYPES.MINUTES]: time * 60,
}[period])
/**
* Returns information about the repetition of an action: periodicity and the end.
*
* @param {ScheduledAction} action - Schedule action
* @returns {{repeat: string|string[], end: string}} - Periodicity of the action.
*/
export const getRepeatInformation = (action) => {
const { REPEAT, DAYS = '', END_TYPE, END_VALUE = '' } = action ?? {}
const daysOfWeek = [T.Sun, T.Mon, T.Tue, T.Wed, T.Thu, T.Fri, T.Sat]
const days = DAYS?.split(',')?.map((day) => Tr(daysOfWeek[day])) ?? []
const repeat = {
0: `${Tr(T.Weekly)} ${days.join(',')}`,
1: `${Tr(T.Monthly)} ${DAYS}`,
2: `${Tr(T.Yearly)} ${DAYS}`,
3: Tr([T.EachHours, DAYS]),
}[+REPEAT]
const end = {
0: Tr(T.None),
1: Tr([T.AfterTimes, END_VALUE]),
2: `${Tr(T.On)} ${timeToString(END_VALUE)}`,
}[+END_TYPE]
return { repeat, end }
}
/**
* Returns the arguments that action needs to execute.
*
* @param {string} action - Action will be executed
* @returns {ARGS_TYPES[]} Arguments
*/
export const getRequiredArgsByAction = (action) => {
const { DISK_ID, NAME, SNAPSHOT_ID } = ARGS_TYPES
return (
{
[SNAPSHOT_DISK_CREATE]: [DISK_ID, NAME],
[SNAPSHOT_DISK_REVERT]: [DISK_ID, SNAPSHOT_ID],
[SNAPSHOT_DISK_DELETE]: [DISK_ID, SNAPSHOT_ID],
[SNAPSHOT_CREATE]: [NAME],
[SNAPSHOT_REVERT]: [SNAPSHOT_ID],
[SNAPSHOT_DELETE]: [SNAPSHOT_ID],
}[action] ?? []
)
}
/**
* Transforms the arguments from the scheduled action to object.
*
* @param {scheduledAction} [scheduledAction] - Schedule action
* @returns {object} Arguments in object format
*/
export const transformStringToArgsObject = ({ ACTION, ARGS = {} } = {}) => {
if (typeof ARGS !== 'string') return ARGS
// IMPORTANT - String data from ARGS has strict order: DISK_ID, NAME, SNAPSHOT_ID
const [arg1, arg2] = ARGS.split(',')
const { DISK_ID, NAME, SNAPSHOT_ID } = ARGS_TYPES
return (
{
[SNAPSHOT_DISK_CREATE]: { [DISK_ID]: arg1, [NAME]: arg2 },
[SNAPSHOT_DISK_REVERT]: { [DISK_ID]: arg1, [SNAPSHOT_ID]: arg2 },
[SNAPSHOT_DISK_DELETE]: { [DISK_ID]: arg1, [SNAPSHOT_ID]: arg2 },
[SNAPSHOT_CREATE]: { [NAME]: arg1 },
[SNAPSHOT_REVERT]: { [SNAPSHOT_ID]: arg1 },
[SNAPSHOT_DELETE]: { [SNAPSHOT_ID]: arg1 },
}[ACTION] ?? {}
)
}
/**
* Returns the period type and time value from a charter options.
*
* @param {CharterOptions} options - Charter options
* @param {string} prefix - Prefix of period type
* @returns {[number, PERIOD_TYPES]} - Period type and time value
* @example ({ after_days: 5 }, 'after_') //=> [5, days]
* @example ({ before_hours: 16 }, 'before_') //=> [16, hours]
*/
const getTimeAndPeriodTypeFromCharter = (options, prefix) => {
const periodType = Object.values(PERIOD_TYPES).find(
(type) => options[`${prefix}${type}`]
)
return [options[`${prefix}${periodType}`], periodType]
}
/**
* Transforms charter to schedule action.
*
* @param {[string, CharterOptions][]} charters - Charters from configuration yaml
* @param {boolean} relative - If `true`, returns times in relative format
* @returns {ScheduledAction[]} - Scheduled action
*/
export const transformChartersToSchedActions = (charters, relative = false) => {
const now = Math.round(Date.now() / 1000)
return charters?.map(([action, options = {}] = []) => {
const [time, timePeriodType] = getTimeAndPeriodTypeFromCharter(
options,
'execute_after_'
)
const [warn, warnPeriodType] = getTimeAndPeriodTypeFromCharter(
options,
'warn_before_'
)
return relative
? {
ACTION: action,
TIME: time,
WARNING: warn,
PERIOD: timePeriodType,
WARNING_PERIOD: warnPeriodType,
}
: {
ACTION: action,
TIME: now + timeToSecondsByPeriodicity(timePeriodType, time),
WARNING: now + timeToSecondsByPeriodicity(warnPeriodType, warn),
}
})
}

@ -17,8 +17,7 @@ import {
getSecurityGroupsFromResource,
prettySecurityGroup,
} from 'client/models/SecurityGroup'
import { timeToString } from 'client/models/Helper'
import { Tr } from 'client/components/HOC'
import { isRelative } from 'client/models/Scheduler'
import {
STATES,
@ -28,7 +27,6 @@ import {
NIC_ALIAS_IP_ATTRS,
HISTORY_ACTIONS,
HYPERVISORS,
T,
StateInfo,
} from 'client/constants'
@ -245,37 +243,22 @@ export const getSnapshotList = (vm) => {
* @returns {Array} List of schedule actions from resource
*/
export const getScheduleActions = (vm) => {
const { TEMPLATE = {} } = vm ?? {}
const { STIME: vmStartTime, TEMPLATE = {} } = vm ?? {}
const now = Math.round(Date.now() / 1000)
return [TEMPLATE.SCHED_ACTION].filter(Boolean).flat()
}
return [TEMPLATE.SCHED_ACTION]
.filter(Boolean)
.flat()
.map((action) => {
const { TIME, WARNING } = action
const isRelativeTime = isRelative(TIME)
const isRelativeWarning = isRelative(WARNING)
/**
* Converts the periodicity of the action to string value.
*
* @param {object} scheduleAction - Schedule action
* @returns {{repeat: string|string[], end: string}} - Periodicity of the action.
*/
export const periodicityToString = (scheduleAction) => {
const { REPEAT, DAYS = '', END_TYPE, END_VALUE = '' } = scheduleAction ?? {}
const ensuredTime = isRelativeTime ? +TIME + +vmStartTime : +TIME
const ensuredWarn = isRelativeWarning && now > ensuredTime + +WARNING
const daysOfWeek = [T.Sun, T.Mon, T.Tue, T.Wed, T.Thu, T.Fri, T.Sat]
const days = DAYS?.split(',')?.map((day) => Tr(daysOfWeek[day])) ?? []
const repeat = {
0: `${Tr(T.Weekly)} ${days.join(',')}`,
1: `${Tr(T.Monthly)} ${DAYS}`,
2: `${Tr(T.Yearly)} ${DAYS}`,
3: Tr([T.EachHours, DAYS]),
}[+REPEAT]
const end = {
0: Tr(T.None),
1: Tr([T.AfterTimes, END_VALUE]),
2: `${Tr(T.On)} ${timeToString(END_VALUE)}`,
}[+END_TYPE]
return { repeat, end }
return { ...action, TIME: ensuredTime, WARNING: ensuredWarn }
})
}
/**

@ -443,16 +443,28 @@ export const createForm =
const defaultTransformInitialValue = (values) =>
schemaCallback.cast(values, { stripUnknown: true })
const { transformInitialValue = defaultTransformInitialValue } = extraParams
const {
transformBeforeSubmit,
transformInitialValue = defaultTransformInitialValue,
...restOfParams
} = extraParams
const defaultValues = initialValues
? transformInitialValue(initialValues, schemaCallback)
: schemaCallback.default()
const ensuredExtraParams = {}
for (const [name, param] of Object.entries(restOfParams)) {
const isFunction = typeof param === 'function'
ensuredExtraParams[name] = isFunction ? param(props) : param
}
return {
resolver: () => schemaCallback,
fields: () => fieldsCallback,
defaultValues,
...extraParams,
transformBeforeSubmit,
...ensuredExtraParams,
}
}