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

F #5422: Add error message for VMs (#1917)

This commit is contained in:
Sergio Betanzos 2022-04-07 17:13:52 +02:00 committed by GitHub
parent 9d7f8e8a60
commit 226484619e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 172 additions and 45 deletions

View File

@ -21,11 +21,12 @@ import { Box, Typography, Paper } from '@mui/material'
import DiskSnapshotCard from 'client/components/Cards/DiskSnapshotCard'
import { StatusChip } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { Tr } from 'client/components/HOC'
import { getDiskName, getDiskType } from 'client/models/Image'
import { stringToBoolean } from 'client/models/Helper'
import { prettyBytes } from 'client/utils'
import { Disk } from 'client/constants'
import { prettyBytes, sentenceCase } from 'client/utils'
import { T, Disk } from 'client/constants'
const DiskCard = memo(({ disk = {}, actions = [], snapshotActions = [] }) => {
const classes = rowStyles()
@ -54,19 +55,19 @@ const DiskCard = memo(({ disk = {}, actions = [], snapshotActions = [] }) => {
[
{ label: getDiskType(disk), dataCy: 'type' },
{
label: stringToBoolean(PERSISTENT) && 'PERSISTENT',
label: stringToBoolean(PERSISTENT) && T.Persistent,
dataCy: 'persistent',
},
{
label: stringToBoolean(READONLY) && 'READONLY',
label: stringToBoolean(READONLY) && T.ReadOnly,
dataCy: 'readonly',
},
{
label: stringToBoolean(SAVE) && 'SAVE',
label: stringToBoolean(SAVE) && T.Save,
dataCy: 'save',
},
{
label: stringToBoolean(CLONE) && 'CLONE',
label: stringToBoolean(CLONE) && T.Clone,
dataCy: 'clone',
},
].filter(({ label } = {}) => Boolean(label)),
@ -89,7 +90,7 @@ const DiskCard = memo(({ disk = {}, actions = [], snapshotActions = [] }) => {
{labels.map(({ label, dataCy }) => (
<StatusChip
key={label}
text={label}
text={sentenceCase(Tr(label))}
{...(dataCy && { dataCy: dataCy })}
/>
))}
@ -109,10 +110,17 @@ const DiskCard = memo(({ disk = {}, actions = [], snapshotActions = [] }) => {
<span data-cy="datastore">{` ${DATASTORE}`}</span>
</span>
)}
<span title={`Monitor Size / Disk Size: ${monitorSize}/${size}`}>
<ModernTv />
<span data-cy="monitorsize">{` ${monitorSize}/${size}`}</span>
</span>
{+MONITOR_SIZE ? (
<span title={`Monitor Size / Disk Size: ${monitorSize}/${size}`}>
<ModernTv />
<span data-cy="monitorsize">{` ${monitorSize}/${size}`}</span>
</span>
) : (
<span title={`Disk Size: ${size}`}>
<ModernTv />
<span data-cy="disksize">{` ${size}`}</span>
</span>
)}
</div>
</div>
{!IS_CONTEXT && !!actions && (

View File

@ -16,8 +16,14 @@
import { ReactElement, memo } from 'react'
import PropTypes from 'prop-types'
import { User, Group, Lock, HardDrive } from 'iconoir-react'
import { Stack, Typography } from '@mui/material'
import {
User,
Group,
Lock,
HardDrive,
WarningCircledOutline as WarningIcon,
} from 'iconoir-react'
import { Box, Stack, Typography, Tooltip } from '@mui/material'
import Timer from 'client/components/Timer'
import MultipleTags from 'client/components/MultipleTags'
@ -27,6 +33,7 @@ import {
getState,
getLastHistory,
getHypervisor,
getErrorMessage,
} from 'client/models/VirtualMachine'
import { timeFromMilliseconds } from 'client/models/Helper'
import { VM } from 'client/constants'
@ -46,6 +53,7 @@ const VirtualMachineCard = memo(
const HOSTNAME = getLastHistory(vm)?.HOSTNAME ?? '--'
const hypervisor = getHypervisor(vm)
const time = timeFromMilliseconds(+ETIME || +STIME)
const error = getErrorMessage(vm)
const { color: stateColor, name: stateName } = getState(vm)
@ -59,6 +67,17 @@ const VirtualMachineCard = memo(
<Typography noWrap component="span">
{NAME}
</Typography>
{error && (
<Tooltip
arrow
placement="bottom"
title={<Typography variant="subtitle2">{error}</Typography>}
>
<Box color="error.dark" component="span">
<WarningIcon />
</Box>
</Tooltip>
)}
<span className={classes.labels}>
{hypervisor && <StatusChip text={hypervisor} />}
{LOCK && <Lock data-cy="lock" />}

View File

@ -68,7 +68,8 @@ export const rowStyles = makeStyles(
color: palette.text.secondary,
marginTop: 4,
display: 'flex',
gap: '0.5em',
gap: '0.75em',
alignItems: 'center',
flexWrap: 'wrap',
wordWrap: 'break-word',
'& > .full-width': {

View File

@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useCallback } from 'react'
import { ReactElement, useMemo, useCallback } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { Stack, Alert, Fade } from '@mui/material'
import { Cancel as CloseIcon } from 'iconoir-react'
import {
useGetVmQuery,
@ -29,10 +30,11 @@ import {
AttributePanel,
} from 'client/components/Tabs/Common'
import Information from 'client/components/Tabs/Vm/Info/information'
import { SubmitButton } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
import { Tr, Translate } from 'client/components/HOC'
import { T } from 'client/constants'
import { getHypervisor } from 'client/models/VirtualMachine'
import { getHypervisor, getErrorMessage } from 'client/models/VirtualMachine'
import {
getActionsAvailable,
filterAttributes,
@ -65,13 +67,17 @@ const VmInfoTab = ({ tabProps = {}, id }) => {
attributes_panel: attributesPanel,
} = tabProps
const { data: vm = {} } = useGetVmQuery(id)
const [changeVmOwnership] = useChangeVmOwnershipMutation()
const [changeVmPermissions] = useChangeVmPermissionsMutation()
const [updateUserTemplate] = useUpdateUserTemplateMutation()
const { data: vm = {} } = useGetVmQuery(id)
const [dismissError] = useUpdateUserTemplateMutation()
const { UNAME, UID, GNAME, GID, PERMISSIONS, USER_TEMPLATE, MONITORING } = vm
const error = useMemo(() => getErrorMessage(vm), [vm])
const hypervisor = useMemo(() => getHypervisor(vm), [vm])
const {
attributes,
lxc: lxcAttributes,
@ -104,9 +110,16 @@ const VmInfoTab = ({ tabProps = {}, id }) => {
await updateUserTemplate({ id, template: xml, replace: 0 })
}
const handleDismissError = async () => {
const { ERROR, SCHED_MESSAGE, ...templateWithoutError } = USER_TEMPLATE
const xml = jsonToXml({ ...templateWithoutError })
await dismissError({ id, template: xml, replace: 0 })
}
const getActions = useCallback(
(actions) => getActionsAvailable(actions, getHypervisor(vm)),
[vm]
(actions) => getActionsAvailable(actions, hypervisor),
[hypervisor]
)
const ATTRIBUTE_FUNCTION = {
@ -122,6 +135,22 @@ const VmInfoTab = ({ tabProps = {}, id }) => {
gridTemplateColumns="repeat(auto-fit, minmax(380px, 1fr))"
padding="0.8em"
>
<Fade in={!!error} unmountOnExit>
<Alert
variant="outlined"
severity="error"
sx={{ gridColumn: 'span 2' }}
action={
<SubmitButton
onClick={handleDismissError}
icon={<CloseIcon />}
tooltip={<Translate word={T.Dismiss} />}
/>
}
>
{error}
</Alert>
</Fade>
{informationPanel?.enabled && (
<Information actions={getActions(informationPanel?.actions)} vm={vm} />
)}
@ -155,7 +184,7 @@ const VmInfoTab = ({ tabProps = {}, id }) => {
{...ATTRIBUTE_FUNCTION}
attributes={attributes}
actions={getActions(attributesPanel?.actions)}
title={Tr(T.Attributes)}
title={`${Tr(T.Attributes)}`}
/>
)}
{vcenterPanel?.enabled && vcenterAttributes && (
@ -178,7 +207,7 @@ const VmInfoTab = ({ tabProps = {}, id }) => {
<AttributePanel
actions={getActions(monitoringPanel?.actions)}
attributes={monitoringAttributes}
title={Tr(T.Monitoring)}
title={`${Tr(T.Monitoring)}`}
/>
)}
</Stack>

View File

@ -64,6 +64,7 @@ module.exports = {
Deploy: 'Deploy',
Detach: 'Detach',
DetachSomething: 'Detach: %s',
Dismiss: 'Dismiss',
Done: 'Done',
Edit: 'Edit',
EditSomething: 'Edit: %s',
@ -336,6 +337,7 @@ module.exports = {
Description: 'Description',
RegistrationTime: 'Registration time',
StartTime: 'Start time',
StartedOnTime: 'Started on %s',
EndTime: 'End time',
Locked: 'Locked',
Attributes: 'Attributes',
@ -344,6 +346,7 @@ module.exports = {
Validate: 'Validate',
Format: 'Format',
Prefix: 'Prefix',
More: 'More',
/* permissions */
Permissions: 'Permissions',
@ -380,12 +383,15 @@ module.exports = {
Vmrc: 'VMRC',
Sdl: 'SDL',
Spice: 'SPICE',
SendCtrlAltDel: 'Send Ctrl-Alt-Del',
CtrlAltDel: 'Ctrl-Alt-Del',
Reconnect: 'Reconnect',
FullScreen: 'Full screen',
Screenshot: 'Screenshot',
LastConnection: 'Last connection',
VmIsNotOnVCenter: '%s is not located on vCenter Host',
PartOf: 'Part of',
GuacamoleState: 'Guacamole State',
VMRCState: 'VMRC State',
/* VM schema - info */
VmName: 'VM name',
UserTemplate: 'User Template',

View File

@ -26,6 +26,7 @@ import {
import { actions as guacamoleActions } from 'client/features/Guacamole/slice'
import { UpdateFromSocket } from 'client/features/OneApi/socket'
import http from 'client/utils/rest'
import { xmlToJson } from 'client/models/Helper'
import {
LockLevel,
FilterFlag,
@ -741,6 +742,32 @@ const vmApi = oneApi.injectEndpoints({
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VM, id }],
async onQueryStarted(
{ id, template: xml, replace },
{ dispatch, queryFulfilled }
) {
try {
if (+replace !== 0 || !xml) return
const patchVm = dispatch(
vmApi.util.updateQueryData('getVm', id, (draft) => {
draft.USER_TEMPLATE = xmlToJson(xml)
})
)
const patchVms = dispatch(
vmApi.util.updateQueryData('getVms', undefined, (draft) => {
const vm = draft.find(({ ID }) => +ID === +id)
vm && (vm.USER_TEMPLATE = xmlToJson(xml))
})
)
queryFulfilled.catch(() => {
patchVm.undo()
patchVms.undo()
})
} catch {}
},
}),
updateConfiguration: builder.mutation({
/**

View File

@ -14,7 +14,12 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { DateTime } from 'luxon'
import { j2xParser as Parser, J2xOptions } from 'fast-xml-parser'
import {
parse as ParserToJson,
X2jOptions,
j2xParser as ParserToXml,
J2xOptions,
} from 'fast-xml-parser'
import { T, UserInputObject, USER_INPUT_TYPES } from 'client/constants'
import { camelCase } from 'client/utils'
@ -26,11 +31,32 @@ import { camelCase } from 'client/utils'
* @returns {string} Xml in string format
*/
export const jsonToXml = (json, { addRoot = true, ...options } = {}) => {
const parser = new Parser(options)
const parser = new ParserToXml(options)
return parser.parse(addRoot ? { ROOT: json } : json)
}
/**
* @param {string} xml - XML in string format
* @param {X2jOptions} [options] - Options to parser
* @returns {object} JSON
*/
export const xmlToJson = (xml, options = {}) => {
const { ROOT, ...jsonWithoutROOT } = ParserToJson(xml, {
attributeNamePrefix: '',
attrNodeName: '',
ignoreAttributes: false,
ignoreNameSpace: true,
allowBooleanAttributes: false,
parseNodeValue: false,
parseAttributeValue: true,
trimValues: true,
...options,
})
return ROOT ?? jsonWithoutROOT
}
/**
* Converts the boolean value into a readable format.
*

View File

@ -105,6 +105,17 @@ export const getState = (vm) => {
return state?.name === STATES.ACTIVE ? VM_LCM_STATES[+LCM_STATE] : state
}
/**
* @param {VM} vm - Virtual machine
* @returns {string} Error message from resource
*/
export const getErrorMessage = (vm) => {
const { USER_TEMPLATE } = vm ?? {}
const { ERROR, SCHED_MESSAGE } = USER_TEMPLATE ?? {}
return [ERROR, SCHED_MESSAGE].filter(Boolean)[0]
}
/**
* @param {VM} vm - Virtual machine
* @returns {Disk[]} List of disks from resource

View File

@ -87,28 +87,28 @@ export default (appTheme, mode = SCHEMES.DARK) => {
default: isDarkMode ? primary.main : '#f2f4f8',
},
error: {
100: '#fdeae7',
200: '#f8c0b7',
300: '#f5aca0',
400: '#f39788',
500: '#ee6d58',
600: '#ec5840',
700: '#ec462b',
800: '#f2391b',
light: '#f8c0b7',
main: '#ec5840',
dark: '#f2391b',
100: '#e98e7f',
200: '#ee6d58',
300: '#e95f48',
400: '#e34e3b',
500: '#dd452c',
600: '#d73727',
700: '#cf231c',
800: '#c61414',
light: '#ee6d58',
main: '#cf231c',
dark: '#c61414',
contrastText: white,
},
warning: {
100: '#FFF4DB',
200: '#FFEDC2',
300: '#FFE4A3',
400: '#FFD980',
500: '#FCC419',
600: '#FAB005',
700: '#F1A204',
800: '#DB9A00',
100: '#fff4db',
200: '#ffedc2',
300: '#ffe4a3',
400: '#ffc980',
500: '#fcc419',
600: '#fab005',
700: '#f1a204',
800: '#db9a00',
light: '#ffe4a3',
main: '#f1a204',
dark: '#f1a204',