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
}
})