mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-16 22:50:10 +03:00
parent
847ea6d949
commit
745f755760
@ -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 && (
|
||||
<Tooltip title={tooltip} position='start' />
|
||||
)
|
||||
}}
|
||||
inputProps={{ 'data-cy': cy }}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
@ -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)
|
||||
|
@ -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 }) => (
|
||||
<Tooltip
|
||||
arrow
|
||||
placement='bottom'
|
||||
@ -29,7 +29,7 @@ const AdornmentWithTooltip = memo(({ title, children }) => (
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<InputAdornment position='end' style={{ cursor: 'help' }}>
|
||||
<InputAdornment position={position} style={{ cursor: 'help' }}>
|
||||
{children ?? <QuestionMarkCircle size={18} />}
|
||||
</InputAdornment>
|
||||
</Tooltip>
|
||||
@ -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'
|
||||
|
@ -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}
|
||||
/>
|
||||
) : (
|
||||
<FormWithSchema cy='form-dg' fields={fields} />
|
||||
<>
|
||||
{description}
|
||||
<FormWithSchema cy='form-dg' fields={fields} />
|
||||
</>
|
||||
)}
|
||||
</DialogForm>
|
||||
)
|
||||
|
@ -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: (
|
||||
<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.`}
|
||||
</Description>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default RecoverForm
|
@ -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))
|
@ -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
|
||||
|
@ -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)))
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
|
@ -66,6 +66,7 @@ module.exports = {
|
||||
Reboot: 'Reboot',
|
||||
RebootHard: 'Reboot hard',
|
||||
Recover: 'Recover',
|
||||
RecoverSeveralVMs: 'Recover several VMs',
|
||||
Refresh: 'Refresh',
|
||||
Release: 'Release',
|
||||
Remove: 'Remove',
|
||||
|
@ -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)
|
||||
|
@ -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 }))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user