1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-21 14:50:08 +03:00

F #5422: Reformat info-tabs for VMs (#2080)

This commit is contained in:
Sergio Betanzos 2022-05-25 18:48:36 +02:00 committed by GitHub
parent b12b55945a
commit 265ccea559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 258 additions and 201 deletions

View File

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo, useMemo } from 'react'
import { ReactElement, memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { DatabaseSettings, Folder, ModernTv } from 'iconoir-react'
import { DatabaseSettings, Folder, PlugTypeC } from 'iconoir-react'
import { Box, Typography, Paper } from '@mui/material'
import DiskSnapshotCard from 'client/components/Cards/DiskSnapshotCard'
@ -26,126 +26,140 @@ import { Tr } from 'client/components/HOC'
import { getDiskName, getDiskType } from 'client/models/Image'
import { stringToBoolean } from 'client/models/Helper'
import { prettyBytes, sentenceCase } from 'client/utils'
import { T, Disk } from 'client/constants'
import { T, Disk, DiskSnapshot } from 'client/constants'
const DiskCard = memo(({ disk = {}, actions = [], snapshotActions = [] }) => {
const classes = rowStyles()
const DiskCard = memo(
/**
* @param {object} props - Props
* @param {Disk} props.disk - Disk
* @param {ReactElement} [props.actions] - Actions
* @param {function({ snapshot: DiskSnapshot }):ReactElement} [props.snapshotActions] - Snapshot actions
* @returns {ReactElement} - Card
*/
({ disk = {}, actions, snapshotActions }) => {
const classes = rowStyles()
/** @type {Disk} */
const {
DISK_ID,
DATASTORE,
TARGET,
TYPE,
SIZE,
MONITOR_SIZE,
READONLY,
PERSISTENT,
SAVE,
CLONE,
IS_CONTEXT,
SNAPSHOTS,
} = disk
const {
DISK_ID,
DATASTORE,
TARGET,
TYPE,
SIZE,
MONITOR_SIZE,
READONLY,
PERSISTENT,
SAVE,
CLONE,
IS_CONTEXT,
SNAPSHOTS,
} = disk
const size = +SIZE ? prettyBytes(+SIZE, 'MB') : '-'
const monitorSize = +MONITOR_SIZE ? prettyBytes(+MONITOR_SIZE, 'MB') : '-'
const size = useMemo(() => (+SIZE ? prettyBytes(+SIZE, 'MB') : '-'), [SIZE])
const labels = useMemo(
() =>
[
{ label: getDiskType(disk), dataCy: 'type' },
{
label: stringToBoolean(PERSISTENT) && T.Persistent,
dataCy: 'persistent',
},
{
label: stringToBoolean(READONLY) && T.ReadOnly,
dataCy: 'readonly',
},
{
label: stringToBoolean(SAVE) && T.Save,
dataCy: 'save',
},
{
label: stringToBoolean(CLONE) && T.Clone,
dataCy: 'clone',
},
].filter(({ label } = {}) => Boolean(label)),
[TYPE, PERSISTENT, READONLY, SAVE, CLONE]
)
const monitorSize = useMemo(
() => (+MONITOR_SIZE ? prettyBytes(+MONITOR_SIZE, 'MB') : '-'),
[MONITOR_SIZE]
)
return (
<Paper
variant="outlined"
className={classes.root}
sx={{ flexWrap: 'wrap', alignContent: 'start' }}
data-cy={`disk-${DISK_ID}`}
>
<div className={classes.main}>
<div className={classes.title}>
<Typography noWrap component="span" data-cy="name">
{getDiskName(disk)}
</Typography>
<span className={classes.labels}>
{labels.map(({ label, dataCy }) => (
<StatusChip
key={label}
text={sentenceCase(Tr(label))}
{...(dataCy && { dataCy: dataCy })}
const labels = useMemo(
() =>
[
{ label: getDiskType(disk), dataCy: 'type' },
{
label: stringToBoolean(PERSISTENT) && T.Persistent,
dataCy: 'persistent',
},
{
label: stringToBoolean(READONLY) && T.ReadOnly,
dataCy: 'readonly',
},
{
label: stringToBoolean(SAVE) && T.Save,
dataCy: 'save',
},
{
label: stringToBoolean(CLONE) && T.Clone,
dataCy: 'clone',
},
].filter(({ label } = {}) => Boolean(label)),
[TYPE, PERSISTENT, READONLY, SAVE, CLONE]
)
return (
<Paper
variant="outlined"
className={classes.root}
sx={{ flexWrap: 'wrap', alignContent: 'start' }}
data-cy={`disk-${DISK_ID}`}
>
<div className={classes.main}>
<div className={classes.title}>
<Typography noWrap component="span" data-cy="name">
{getDiskName(disk)}
</Typography>
<span className={classes.labels}>
{labels.map(({ label, dataCy }) => (
<StatusChip
key={label}
text={sentenceCase(Tr(label))}
{...(dataCy && { dataCy: dataCy })}
/>
))}
</span>
</div>
<div className={classes.caption}>
<span>{`#${DISK_ID}`}</span>
{TARGET && (
<span title={`${Tr(T.TargetDevice)}: ${TARGET}`}>
<PlugTypeC />
<span data-cy="target">{` ${TARGET}`}</span>
</span>
)}
{DATASTORE && (
<span title={`${Tr(T.Datastore)}: ${DATASTORE}`}>
<DatabaseSettings />
<span data-cy="datastore">{` ${DATASTORE}`}</span>
</span>
)}
{+MONITOR_SIZE ? (
<span
title={`${Tr(T.Monitoring)} / ${Tr(
T.DiskSize
)}: ${monitorSize}/${size}`}
>
<Folder />
<span data-cy="monitorsize">{` ${monitorSize}/${size}`}</span>
</span>
) : (
<span title={`${Tr(T.DiskSize)}: ${size}`}>
<Folder />
<span data-cy="disksize">{` ${size}`}</span>
</span>
)}
</div>
</div>
{!IS_CONTEXT && !!actions && (
<div className={classes.actions}>{actions}</div>
)}
{!!SNAPSHOTS?.length && (
<Box flexBasis="100%">
{SNAPSHOTS?.map((snapshot) => (
<DiskSnapshotCard
key={`${DISK_ID}-${snapshot.ID}`}
snapshot={snapshot}
actions={snapshotActions}
/>
))}
</span>
</div>
<div className={classes.caption}>
<span>{`#${DISK_ID}`}</span>
{TARGET && (
<span title={`Target: ${TARGET}`}>
<DatabaseSettings />
<span data-cy="target">{` ${TARGET}`}</span>
</span>
)}
{DATASTORE && (
<span title={`Datastore Name: ${DATASTORE}`}>
<Folder />
<span data-cy="datastore">{` ${DATASTORE}`}</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 && (
<div className={classes.actions}>{actions}</div>
)}
{!!SNAPSHOTS?.length && (
<Box flexBasis="100%">
{SNAPSHOTS?.map((snapshot) => (
<DiskSnapshotCard
key={`${DISK_ID}-${snapshot.ID}`}
snapshot={snapshot}
actions={snapshotActions}
/>
))}
</Box>
)}
</Paper>
)
})
</Box>
)}
</Paper>
)
}
)
DiskCard.propTypes = {
disk: PropTypes.object.isRequired,
actions: PropTypes.any,
extraActionProps: PropTypes.object,
extraSnapshotActionProps: PropTypes.object,
snapshotActions: PropTypes.any,
}

View File

@ -13,72 +13,85 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import { ReactElement, memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { ModernTv } from 'iconoir-react'
import { Typography, Paper } from '@mui/material'
import { StatusChip } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { Translate } from 'client/components/HOC'
import { Tr, Translate } from 'client/components/HOC'
import * as Helper from 'client/models/Helper'
import { stringToBoolean, timeFromMilliseconds } from 'client/models/Helper'
import { prettyBytes } from 'client/utils'
import { T, DiskSnapshot } from 'client/constants'
const DiskSnapshotCard = memo(({ snapshot = {}, actions = [] }) => {
const classes = rowStyles()
const DiskSnapshotCard = memo(
/**
* @param {object} props - Props
* @param {DiskSnapshot} props.snapshot - Disk snapshot
* @param {function({ snapshot: DiskSnapshot }):ReactElement} [props.actions] - Actions
* @returns {ReactElement} - Card
*/
({ snapshot = {}, actions }) => {
const classes = rowStyles()
/** @type {DiskSnapshot} */
const {
ID,
NAME,
ACTIVE,
DATE,
SIZE: SNAPSHOT_SIZE,
MONITOR_SIZE: SNAPSHOT_MONITOR_SIZE,
} = snapshot
const {
ID,
NAME,
ACTIVE,
DATE,
SIZE: SNAPSHOT_SIZE,
MONITOR_SIZE: SNAPSHOT_MONITOR_SIZE,
} = snapshot
const isActive = Helper.stringToBoolean(ACTIVE)
const time = Helper.timeFromMilliseconds(+DATE)
const timeAgo = `created ${time.toRelative()}`
const isActive = useMemo(() => stringToBoolean(ACTIVE), [ACTIVE])
const time = useMemo(() => timeFromMilliseconds(+DATE), [DATE])
const timeFormat = useMemo(() => time.toFormat('ff'), [DATE])
const timeAgo = useMemo(() => `created ${time.toRelative()}`, [DATE])
const size = +SNAPSHOT_SIZE ? prettyBytes(+SNAPSHOT_SIZE, 'MB') : '-'
const monitorSize = +SNAPSHOT_MONITOR_SIZE
? prettyBytes(+SNAPSHOT_MONITOR_SIZE, 'MB')
: '-'
const sizeInfo = useMemo(() => {
const size = +SNAPSHOT_SIZE ? prettyBytes(+SNAPSHOT_SIZE, 'MB') : '-'
const monitorSize = +SNAPSHOT_MONITOR_SIZE
? prettyBytes(+SNAPSHOT_MONITOR_SIZE, 'MB')
: '-'
return (
<Paper variant="outlined" className={classes.root}>
<div className={classes.main}>
<div className={classes.title}>
<Typography noWrap component="span">
{NAME}
</Typography>
<span className={classes.labels}>
{isActive && <StatusChip text={<Translate word={T.Active} />} />}
<StatusChip text={<Translate word={T.Snapshot} />} />
</span>
return `${monitorSize}/${size}`
}, [SNAPSHOT_SIZE, SNAPSHOT_MONITOR_SIZE])
return (
<Paper variant="outlined" className={classes.root}>
<div className={classes.main}>
<div className={classes.title}>
<Typography noWrap component="span">
{NAME}
</Typography>
<span className={classes.labels}>
{isActive && <StatusChip text={<Translate word={T.Active} />} />}
<StatusChip text={<Translate word={T.Snapshot} />} />
</span>
</div>
<div className={classes.caption}>
<span title={timeFormat}>{`#${ID} ${timeAgo}`}</span>
<span
title={`${Tr(T.Monitoring)} / ${Tr(T.DiskSize)}: ${sizeInfo}`}
>
<ModernTv />
<span>{` ${sizeInfo}`}</span>
</span>
</div>
</div>
<div className={classes.caption}>
<span title={time.toFormat('ff')}>{`#${ID} ${timeAgo}`}</span>
<span title={`Monitor Size / Disk Size: ${monitorSize}/${size}`}>
<ModernTv />
<span>{` ${monitorSize}/${size}`}</span>
</span>
</div>
</div>
{typeof actions === 'function' && (
<div className={classes.actions}>{actions({ snapshot })}</div>
)}
</Paper>
)
})
{typeof actions === 'function' && (
<div className={classes.actions}>{actions({ snapshot })}</div>
)}
</Paper>
)
}
)
DiskSnapshotCard.propTypes = {
snapshot: PropTypes.object.isRequired,
extraActionProps: PropTypes.object,
actions: PropTypes.arrayOf(PropTypes.string),
actions: PropTypes.func,
}
DiskSnapshotCard.displayName = 'DiskSnapshotCard'

View File

@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo, useMemo } from 'react'
import { ReactElement, memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Network } from 'iconoir-react'
import {
useMediaQuery,
Typography,
@ -37,6 +38,16 @@ import { groupBy } from 'client/utils'
import { T, Nic, NicAlias, PrettySecurityGroupRule } from 'client/constants'
const NicCard = memo(
/**
* @param {object} props - Props
* @param {Nic|NicAlias} props.nic - NIC
* @param {ReactElement} [props.actions] - Actions
* @param {function({ alias: NicAlias }):ReactElement} [props.aliasActions] - Alias actions
* @param {function({ securityGroupId: string }):ReactElement} [props.securityGroupActions] - Security group actions
* @param {boolean} [props.showParents] -
* @param {boolean} [props.clipboardOnTags] -
* @returns {ReactElement} - Card
*/
({
nic = {},
actions,
@ -48,7 +59,6 @@ const NicCard = memo(
const classes = rowStyles()
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('md'))
/** @type {Nic|NicAlias} */
const {
NIC_ID,
NETWORK = '-',
@ -65,24 +75,32 @@ const NicCard = memo(
const isAlias = !!PARENT?.length
const isPciDevice = PCI_ID !== undefined
const isAdditionalIp = NIC_ID !== undefined || NETWORK === 'Additional IP'
const isAdditionalIp = NIC_ID === undefined || NETWORK === 'Additional IP'
const dataCy = isAlias ? 'alias' : 'nic'
const noClipboardTags = [
{ text: stringToBoolean(RDP) && 'RDP', dataCy: `${dataCy}-rdp` },
{ text: stringToBoolean(SSH) && 'SSH', dataCy: `${dataCy}-ssh` },
showParents && {
text: isAlias ? `PARENT: ${PARENT}` : false,
dataCy: `${dataCy}-parent`,
},
].filter(({ text } = {}) => Boolean(text))
const noClipboardTags = useMemo(
() =>
[
{ text: stringToBoolean(RDP) && 'RDP', dataCy: `${dataCy}-rdp` },
{ text: stringToBoolean(SSH) && 'SSH', dataCy: `${dataCy}-ssh` },
showParents && {
text: isAlias ? `PARENT: ${PARENT}` : false,
dataCy: `${dataCy}-parent`,
},
].filter(({ text } = {}) => Boolean(text)),
[RDP, SSH, showParents, PARENT]
)
const tags = [
{ text: IP, dataCy: `${dataCy}-ip` },
{ text: MAC, dataCy: `${dataCy}-mac` },
{ text: ADDRESS, dataCy: `${dataCy}-address` },
].filter(({ text } = {}) => Boolean(text))
const tags = useMemo(
() =>
[
{ text: IP, dataCy: `${dataCy}-ip` },
{ text: MAC, dataCy: `${dataCy}-mac` },
{ text: ADDRESS, dataCy: `${dataCy}-address` },
].filter(({ text } = {}) => Boolean(text)),
[IP, MAC, ADDRESS]
)
return (
<Paper
@ -96,13 +114,8 @@ const NicCard = memo(
{...(!isAlias && !showParents && { pl: '1em' })}
>
<div className={classes.title}>
<Typography
noWrap
component="span"
fontWeight="bold"
data-cy={`${dataCy}-name`}
>
{`${NIC_ID} | ${NETWORK}`}
<Typography noWrap component="span" data-cy={`${dataCy}-name`}>
{NETWORK}
</Typography>
<span className={classes.labels}>
{isAlias && <StatusChip stateColor="info" text={'ALIAS'} />}
@ -113,11 +126,24 @@ const NicCard = memo(
dataCy={tag.dataCy}
/>
))}
<MultipleTags
clipboard={clipboardOnTags}
limitTags={isMobile ? 1 : 3}
tags={tags}
/>
</span>
</div>
<div className={classes.caption}>
{`#${NIC_ID}`}
<span>
<Network />
<Stack
direction="row"
justifyContent="end"
alignItems="center"
gap="0.5em"
>
<MultipleTags
tags={tags}
clipboard={clipboardOnTags}
limitTags={isMobile ? 1 : 3}
/>
</Stack>
</span>
</div>
</Box>

View File

@ -66,7 +66,7 @@ const VirtualMachineCard = memo(
ETIME,
LOCK,
USER_TEMPLATE: { LABELS } = {},
TEMPLATE: { CPU, MEMORY } = {},
TEMPLATE: { VCPU = '-', MEMORY } = {},
} = vm
const { HOSTNAME = '--', VM_MAD: hypervisor } = useMemo(
@ -74,10 +74,11 @@ const VirtualMachineCard = memo(
[vm.HISTORY_RECORDS]
)
const time = useMemo(
() => timeFromMilliseconds(+ETIME || +STIME),
[ETIME, STIME]
)
const [time, timeFormat] = useMemo(() => {
const fromMill = timeFromMilliseconds(+ETIME || +STIME)
return [fromMill, fromMill.toFormat('ff')]
}, [ETIME, STIME])
const { color: stateColor, name: stateName } = getState(vm)
const error = useMemo(() => getErrorMessage(vm), [vm])
@ -121,27 +122,24 @@ const VirtualMachineCard = memo(
</div>
<div className={classes.caption}>
<span data-cy="id">{`#${ID}`}</span>
<span title={useMemo(() => time.toFormat('ff'), [ETIME, STIME])}>
<span title={timeFormat}>
{`${+ETIME ? T.Done : T.Started} `}
<Timer initial={time} />
</span>
<span title={`${Tr(T.PhysicalCpu)}: ${CPU}`}>
<span title={`${Tr(T.VirtualCpu)}: ${VCPU}`}>
<Cpu />
<span data-cy="cpu">{CPU}</span>
<span data-cy="vcpu">{VCPU}</span>
</span>
<span title={`${Tr(T.Memory)}: ${memValue}`}>
<MemoryIcon width={20} height={20} />
<span data-cy="memory">{memValue}</span>
</span>
<span
className={classes.captionItem}
title={`${Tr(T.Hostname)}: ${HOSTNAME}`}
>
<span title={`${Tr(T.Hostname)}: ${HOSTNAME}`}>
<HardDrive />
<span data-cy="hostname">{HOSTNAME}</span>
</span>
{!!ips?.length && (
<span className={classes.captionItem}>
<span title={`${Tr(T.IP)}`}>
<Network />
<Stack direction="row" justifyContent="end" alignItems="center">
<MultipleTags tags={ips} clipboard />

View File

@ -90,6 +90,7 @@ const SelectController = memo(
label={labelCanBeTranslated(label) ? Tr(label) : label}
InputLabelProps={{ shrink: needShrink }}
InputProps={{
...(multiple && { sx: { paddingTop: '0.5em' } }),
readOnly,
startAdornment:
(optionSelected && renderValue?.(optionSelected)) ||

View File

@ -120,7 +120,11 @@ const AttributeList = ({
{/* TITLE */}
{title && (
<TitleElement>
<Typography noWrap>{Tr(title)}</Typography>
{typeof title === 'string' ? (
<Typography noWrap>{Tr(title)}</Typography>
) : (
title
)}
</TitleElement>
)}
<DetailsElement>

View File

@ -462,6 +462,7 @@ module.exports = {
Snapshot: 'Snapshot',
SnapshotName: 'Snapshot name',
DiskSnapshot: 'Disk snapshot',
DiskSize: 'Disk size',
NewImageName: 'New Image name',
NewImageNameConcept: 'Name for the new Image where the disk will be saved',
/* VM schema - network */