From 745f75576048890529ae818a9f3b5265b62c5416 Mon Sep 17 00:00:00 2001 From: Sergio Betanzos Date: Mon, 4 Oct 2021 10:52:47 +0200 Subject: [PATCH] F #5422: Add vm recover form (#1504) --- .../FormControl/SelectController.js | 14 +++-- .../client/components/FormControl/Tooltip.js | 7 +-- .../components/Forms/ButtonToTriggerForm.js | 7 ++- .../components/Forms/Vm/RecoverForm/index.js | 41 +++++++++++++++ .../components/Forms/Vm/RecoverForm/schema.js | 51 +++++++++++++++++++ .../src/client/components/Forms/Vm/index.js | 2 + .../client/components/Tables/Vms/actions.js | 22 ++++++-- .../src/client/constants/translates.js | 1 + .../src/client/features/One/vm/actions.js | 1 + .../src/client/features/One/vm/hooks.js | 3 +- .../src/client/features/One/vm/services.js | 26 ++++++++++ 11 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 src/fireedge/src/client/components/Forms/Vm/RecoverForm/index.js create mode 100644 src/fireedge/src/client/components/Forms/Vm/RecoverForm/schema.js diff --git a/src/fireedge/src/client/components/FormControl/SelectController.js b/src/fireedge/src/client/components/FormControl/SelectController.js index ed15cf91d2..49032e3c11 100644 --- a/src/fireedge/src/client/components/FormControl/SelectController.js +++ b/src/fireedge/src/client/components/FormControl/SelectController.js @@ -19,11 +19,11 @@ import PropTypes from 'prop-types' import { TextField } from '@material-ui/core' import { Controller } from 'react-hook-form' -import { ErrorHelper } from 'client/components/FormControl' +import { ErrorHelper, Tooltip } from 'client/components/FormControl' import { Tr } from 'client/components/HOC' const SelectController = memo( - ({ control, cy, name, label, multiple, values, error, fieldProps }) => { + ({ control, cy, name, label, multiple, values, tooltip, error, fieldProps }) => { const defaultValue = multiple ? [values?.[0]?.value] : values?.[0]?.value return ( @@ -55,6 +55,11 @@ const SelectController = memo( margin='dense' SelectProps={{ native: true, multiple }} label={Tr(label)} + InputProps={{ + startAdornment: tooltip && ( + + ) + }} inputProps={{ 'data-cy': cy }} error={Boolean(error)} helperText={Boolean(error) && } @@ -76,7 +81,9 @@ const SelectController = memo( }, (prevProps, nextProps) => prevProps.error === nextProps.error && - prevProps.values.length === nextProps.values.length + prevProps.values.length === nextProps.values.length && + prevProps.label === nextProps.label && + prevProps.tooltip === nextProps.tooltip ) SelectController.propTypes = { @@ -86,6 +93,7 @@ SelectController.propTypes = { label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), multiple: PropTypes.bool, values: PropTypes.arrayOf(PropTypes.object).isRequired, + tooltip: PropTypes.string, error: PropTypes.oneOfType([ PropTypes.bool, PropTypes.objectOf(PropTypes.any) diff --git a/src/fireedge/src/client/components/FormControl/Tooltip.js b/src/fireedge/src/client/components/FormControl/Tooltip.js index 03dbe8ac40..acba2fe02e 100644 --- a/src/fireedge/src/client/components/FormControl/Tooltip.js +++ b/src/fireedge/src/client/components/FormControl/Tooltip.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types' import { QuestionMarkCircle } from 'iconoir-react' import { InputAdornment, Typography, Tooltip } from '@material-ui/core' -const AdornmentWithTooltip = memo(({ title, children }) => ( +const AdornmentWithTooltip = memo(({ title, position = 'end', children }) => ( ( } > - + {children ?? } @@ -37,7 +37,8 @@ const AdornmentWithTooltip = memo(({ title, children }) => ( AdornmentWithTooltip.propTypes = { title: PropTypes.string, - children: PropTypes.any + children: PropTypes.any, + position: PropTypes.oneOf(['start', 'end']) } AdornmentWithTooltip.displayName = 'AdornmentWithTooltip' diff --git a/src/fireedge/src/client/components/Forms/ButtonToTriggerForm.js b/src/fireedge/src/client/components/Forms/ButtonToTriggerForm.js index 1936f04191..791b4e7b46 100644 --- a/src/fireedge/src/client/components/Forms/ButtonToTriggerForm.js +++ b/src/fireedge/src/client/components/Forms/ButtonToTriggerForm.js @@ -49,7 +49,7 @@ const ButtonToTriggerForm = ({ const { onSubmit: handleSubmit, form, isConfirmDialog = false, dialogProps = {} } = Form ?? {} const formConfig = useMemo(() => form?.() ?? {}, [form]) - const { steps, defaultValues, resolver, fields, transformBeforeSubmit } = formConfig + const { steps, defaultValues, resolver, description, fields, transformBeforeSubmit } = formConfig const handleTriggerSubmit = async formData => { try { @@ -139,7 +139,10 @@ const ButtonToTriggerForm = ({ onSubmit={handleTriggerSubmit} /> ) : ( - + <> + {description} + + )} ) diff --git a/src/fireedge/src/client/components/Forms/Vm/RecoverForm/index.js b/src/fireedge/src/client/components/Forms/Vm/RecoverForm/index.js new file mode 100644 index 0000000000..2e5fbbb885 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Vm/RecoverForm/index.js @@ -0,0 +1,41 @@ +/* ------------------------------------------------------------------------- * + * 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 { styled } from '@material-ui/core' + +import { createForm } from 'client/utils' +import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/RecoverForm/schema' + +const Description = styled('p')(({ theme }) => ({ + ...theme.typography.subtitle1, + paddingInline: '1rem' +})) + +const RecoverForm = createForm( + SCHEMA, + FIELDS, + { + description: ( + + {`Recovers a stuck VM that is waiting for a driver operation. + The recovery may be done by failing, succeeding or retrying the + current operation. YOU NEED TO MANUALLY CHECK THE VM STATUS ON THE HOST, + to decide if the operation was successful or not, or if it can be retried.`} + + ) + } +) + +export default RecoverForm diff --git a/src/fireedge/src/client/components/Forms/Vm/RecoverForm/schema.js b/src/fireedge/src/client/components/Forms/Vm/RecoverForm/schema.js new file mode 100644 index 0000000000..58fc191961 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Vm/RecoverForm/schema.js @@ -0,0 +1,51 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { string, object } from 'yup' +import { INPUT_TYPES } from 'client/constants' +import { getValidationFromFields } from 'client/utils' + +const OPERATION = { + name: 'operation', + label: 'Operation', + type: INPUT_TYPES.SELECT, + dependOf: 'operation', + tooltip: operation => ({ + 0: 'Recover a VM by failing the pending action', + 1: 'Recover a VM by succeeding the pending action', + 2: 'Recover a VM by retrying the last failed action', + 3: 'No recover action possible, delete the VM', + 4: 'No recover action possible, delete and recreate the VM', + 5: `No recover action possible, delete the VM from the DB. + It does not trigger any action on the hypervisor` + }[operation]), + values: [ + { text: 'Failure', value: 0 }, + { text: 'Success', value: 1 }, + { text: 'Retry', value: 2 }, + { text: 'Delete', value: 3 }, + { text: 'Recreate', value: 4 }, + { text: 'Delete database', value: 5 } + ], + validation: string() + .trim() + .required('Recover operation field is required') + .default(() => 2) +} + +export const FIELDS = [OPERATION] + +export const SCHEMA = object(getValidationFromFields(FIELDS)) diff --git a/src/fireedge/src/client/components/Forms/Vm/index.js b/src/fireedge/src/client/components/Forms/Vm/index.js index 7cc7ec6b60..b865462f3e 100644 --- a/src/fireedge/src/client/components/Forms/Vm/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/index.js @@ -16,6 +16,7 @@ import AttachNicForm from 'client/components/Forms/Vm/AttachNicForm' import CreateDiskSnapshotForm from 'client/components/Forms/Vm/CreateDiskSnapshotForm' import CreateSnapshotForm from 'client/components/Forms/Vm/CreateSnapshotForm' +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' @@ -26,6 +27,7 @@ export { AttachNicForm, CreateDiskSnapshotForm, CreateSnapshotForm, + RecoverForm, ResizeCapacityForm, ResizeDiskForm, SaveAsDiskForm diff --git a/src/fireedge/src/client/components/Tables/Vms/actions.js b/src/fireedge/src/client/components/Tables/Vms/actions.js index 9e3cf2c63e..155770bfc9 100644 --- a/src/fireedge/src/client/components/Tables/Vms/actions.js +++ b/src/fireedge/src/client/components/Tables/Vms/actions.js @@ -33,7 +33,7 @@ import { useAuth } from 'client/features/Auth' import { useVmApi } from 'client/features/One' import { Tr, Translate } from 'client/components/HOC' -// import { } from 'client/components/Forms/Vm' +import { RecoverForm } from 'client/components/Forms/Vm' import { createActions } from 'client/components/Tables/Enhanced/Utils' import { PATH } from 'client/apps/sunstone/routesOne' import { T, VM_ACTIONS, MARKETPLACE_APP_ACTIONS, VM_ACTIONS_BY_STATE } from 'client/constants' @@ -85,6 +85,7 @@ const Actions = () => { resume, resched, unresched, + recover, lock, unlock } = useVmApi() @@ -330,8 +331,23 @@ const Actions = () => { accessor: VM_ACTIONS.RECOVER, name: T.Recover, disabled: isDisabled(VM_ACTIONS.RECOVER), - isConfirmDialog: true, - onSubmit: () => undefined + dialogProps: { + title: rows => { + const isMultiple = rows?.length > 1 + const { ID, NAME } = rows?.[0]?.original + + return [ + Tr(isMultiple ? T.RecoverSeveralVMs : T.Recover), + !isMultiple && `#${ID} ${NAME}` + ].filter(Boolean).join(' - ') + } + }, + form: RecoverForm, + onSubmit: async (_, rows) => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all(ids.map(id => recover(id))) + ids?.length > 1 && await Promise.all(ids.map(id => getVm(id))) + } }] }, { diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index d6735ce04c..eb58b2a599 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -66,6 +66,7 @@ module.exports = { Reboot: 'Reboot', RebootHard: 'Reboot hard', Recover: 'Recover', + RecoverSeveralVMs: 'Recover several VMs', Refresh: 'Refresh', Release: 'Release', Remove: 'Remove', diff --git a/src/fireedge/src/client/features/One/vm/actions.js b/src/fireedge/src/client/features/One/vm/actions.js index ee4176d89c..4469d18f25 100644 --- a/src/fireedge/src/client/features/One/vm/actions.js +++ b/src/fireedge/src/client/features/One/vm/actions.js @@ -85,3 +85,4 @@ export const deleteSnapshot = createAction(`${VM}/delete/snapshot`, vmService.de export const addScheduledAction = createAction(`${VM}/add/scheduled-action`, vmService.addScheduledAction) export const updateScheduledAction = createAction(`${VM}/update/scheduled-action`, vmService.updateScheduledAction) export const deleteScheduledAction = createAction(`${VM}/delete/scheduled-action`, vmService.deleteScheduledAction) +export const recover = createAction(`${VM}/recover`, vmService.recover) diff --git a/src/fireedge/src/client/features/One/vm/hooks.js b/src/fireedge/src/client/features/One/vm/hooks.js index 4d7e964f27..bdef08ba6c 100644 --- a/src/fireedge/src/client/features/One/vm/hooks.js +++ b/src/fireedge/src/client/features/One/vm/hooks.js @@ -84,6 +84,7 @@ export const useVmApi = () => { updateScheduledAction: (id, data) => unwrapDispatch(actions.updateScheduledAction({ id, ...data })), deleteScheduledAction: (id, data) => - unwrapDispatch(actions.deleteScheduledAction({ id, ...data })) + unwrapDispatch(actions.deleteScheduledAction({ id, ...data })), + recover: (id, operation) => unwrapDispatch(actions.recover({ id, operation })) } } diff --git a/src/fireedge/src/client/features/One/vm/services.js b/src/fireedge/src/client/features/One/vm/services.js index 8bb9f584fd..49ecdb5312 100644 --- a/src/fireedge/src/client/features/One/vm/services.js +++ b/src/fireedge/src/client/features/One/vm/services.js @@ -568,6 +568,32 @@ export const vmService = ({ if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data + return res?.data + }, + + /** + * Recovers a stuck VM that is waiting for a driver operation. + * The recovery may be done by failing or succeeding the pending operation. + * + * You need to manually check the vm status on the host, to decide + * if the operation was successful or not. + * + * @param {object} params - Request parameters + * @param {string|number} params.id - Virtual machine id + * @param {0|1|2|3|4} params.operation - Recover operation: + * success (1), failure (0), retry (2), delete (3), delete-recreate (4) + * @returns {number} Virtual machine id + * @throws Fails when response isn't code 200 + */ + recover: async params => { + const name = Actions.VM_RECOVER + const command = { name, ...Commands[name] } + const config = requestConfig(params, command) + + const res = await RestClient.request(config) + + if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data + return res?.data } })