mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-30 22:50:10 +03:00
parent
9d7f8e8a60
commit
226484619e
@ -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 && (
|
||||
|
@ -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" />}
|
||||
|
@ -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': {
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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({
|
||||
/**
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user