mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-22 18:50:08 +03:00
(cherry picked from commit 00dcdba88e64724f971d65a70393653f4655e9c2)
This commit is contained in:
parent
42358a2e25
commit
a41261374d
src/fireedge
package-lock.json
src/client
components
Buttons
Cards
Dialogs
FormControl
Forms
ButtonToTriggerForm.js
Vm
VmTemplate/CreateForm/Steps/ExtraConfiguration
Tabs/Vm
Timer
constants
models
utils
1298
src/fireedge/package-lock.json
generated
1298
src/fireedge/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
216
src/fireedge/src/client/components/Buttons/ScheduleAction.js
Normal file
216
src/fireedge/src/client/components/Buttons/ScheduleAction.js
Normal file
@ -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'
|
125
src/fireedge/src/client/components/Cards/ScheduleActionCard.js
Normal file
125
src/fireedge/src/client/components/Cards/ScheduleActionCard.js
Normal file
@ -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',
|
||||
|
178
src/fireedge/src/client/components/Tabs/Vm/SchedActions.js
Normal file
178
src/fireedge/src/client/components/Tabs/Vm/SchedActions.js
Normal file
@ -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
|
96
src/fireedge/src/client/components/Timer/index.js
Normal file
96
src/fireedge/src/client/components/Timer/index.js
Normal file
@ -0,0 +1,96 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
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
|
||||
|
286
src/fireedge/src/client/models/Scheduler.js
Normal file
286
src/fireedge/src/client/models/Scheduler.js
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user