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

F #5637: Refactor nic & disk components (#1771)

(cherry picked from commit d078518b15ba499527afea83e9f9b65f4e062fce)
This commit is contained in:
Sergio Betanzos 2022-02-15 19:09:11 +01:00 committed by Tino Vazquez
parent 80fc9b2566
commit 941d4f33ae
No known key found for this signature in database
GPG Key ID: 14201E424D02047E
37 changed files with 1725 additions and 989 deletions

View File

@ -22,144 +22,116 @@ import DiskSnapshotCard from 'client/components/Cards/DiskSnapshotCard'
import { StatusChip } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { getDiskName, getDiskType } from 'client/models/Image'
import { stringToBoolean } from 'client/models/Helper'
import { prettyBytes } from 'client/utils'
import { Disk } from 'client/constants'
const DiskCard = memo(
({
disk = {},
actions = [],
extraActionProps = {},
snapshotActions = [],
extraSnapshotActionProps = {},
}) => {
const classes = rowStyles()
const DiskCard = memo(({ disk = {}, actions = [], snapshotActions = [] }) => {
const classes = rowStyles()
/** @type {Disk} */
const {
DISK_ID,
DATASTORE,
TARGET,
IMAGE,
TYPE,
FORMAT,
SIZE,
MONITOR_SIZE,
READONLY,
PERSISTENT,
SAVE,
CLONE,
IS_CONTEXT,
SNAPSHOTS,
} = disk
/** @type {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 = +SIZE ? prettyBytes(+SIZE, 'MB') : '-'
const monitorSize = +MONITOR_SIZE ? prettyBytes(+MONITOR_SIZE, 'MB') : '-'
const type = String(TYPE).toLowerCase()
const labels = useMemo(
() =>
[
{ label: getDiskType(disk), dataCy: 'type' },
{
label: stringToBoolean(PERSISTENT) && 'PERSISTENT',
dataCy: 'persistent',
},
{
label: stringToBoolean(READONLY) && 'READONLY',
dataCy: 'readonly',
},
{
label: stringToBoolean(SAVE) && 'SAVE',
dataCy: 'save',
},
{
label: stringToBoolean(CLONE) && 'CLONE',
dataCy: 'clone',
},
].filter(({ label } = {}) => Boolean(label)),
[TYPE, PERSISTENT, READONLY, SAVE, CLONE]
)
const image =
IMAGE ??
{
fs: `${FORMAT} - ${size}`,
swap: size,
}[type]
const labels = useMemo(
() =>
[
{ label: TYPE, dataCy: 'type' },
{
label: stringToBoolean(PERSISTENT) && 'PERSISTENT',
dataCy: 'persistent',
},
{
label: stringToBoolean(READONLY) && 'READONLY',
dataCy: 'readonly',
},
{
label: stringToBoolean(SAVE) && 'SAVE',
dataCy: 'save',
},
{
label: stringToBoolean(CLONE) && 'CLONE',
dataCy: 'clone',
},
].filter(({ label } = {}) => Boolean(label)),
[TYPE, PERSISTENT, READONLY, SAVE, CLONE]
)
return (
<Paper
variant="outlined"
className={classes.root}
sx={{ flexWrap: 'wrap' }}
data-cy={`disk-${DISK_ID}`}
>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span" data-cy="name">
{image}
</Typography>
<span className={classes.labels}>
{labels.map(({ label, dataCy }) => (
<StatusChip
key={label}
text={label}
{...(dataCy && { dataCy: dataCy })}
/>
))}
</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>
)}
<span title={`Monitor Size / Disk Size: ${monitorSize}/${size}`}>
<ModernTv />
<span data-cy="monitorsize">{` ${monitorSize}/${size}`}</span>
</span>
</div>
return (
<Paper
variant="outlined"
className={classes.root}
sx={{ flexWrap: 'wrap' }}
data-cy={`disk-${DISK_ID}`}
>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span" data-cy="name">
{getDiskName(disk)}
</Typography>
<span className={classes.labels}>
{labels.map(({ label, dataCy }) => (
<StatusChip
key={label}
text={label}
{...(dataCy && { dataCy: dataCy })}
/>
))}
</span>
</div>
{!IS_CONTEXT && !!actions.length && (
<div className={classes.actions}>
{actions.map((Action, idx) => (
<Action
key={`${Action.displayName ?? idx}-${DISK_ID}`}
{...extraActionProps}
name={image}
disk={disk}
/>
))}
</div>
)}
{!!SNAPSHOTS?.length && (
<Box flexBasis="100%">
{SNAPSHOTS?.map((snapshot) => (
<DiskSnapshotCard
key={`${DISK_ID}-${snapshot.ID}`}
snapshot={snapshot}
actions={snapshotActions}
extraActionProps={extraSnapshotActionProps}
/>
))}
</Box>
)}
</Paper>
)
}
)
<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>
)}
<span title={`Monitor Size / Disk Size: ${monitorSize}/${size}`}>
<ModernTv />
<span data-cy="monitorsize">{` ${monitorSize}/${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>
)
})
DiskCard.propTypes = {
disk: PropTypes.object.isRequired,

View File

@ -26,62 +26,52 @@ import * as Helper from 'client/models/Helper'
import { prettyBytes } from 'client/utils'
import { T, DiskSnapshot } from 'client/constants'
const DiskSnapshotCard = memo(
({ snapshot = {}, actions = [], extraActionProps = {} }) => {
const classes = rowStyles()
const DiskSnapshotCard = memo(({ snapshot = {}, actions = [] }) => {
const classes = rowStyles()
/** @type {DiskSnapshot} */
const {
ID,
NAME,
ACTIVE,
DATE,
SIZE: SNAPSHOT_SIZE,
MONITOR_SIZE: SNAPSHOT_MONITOR_SIZE,
} = snapshot
/** @type {DiskSnapshot} */
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 = Helper.stringToBoolean(ACTIVE)
const time = Helper.timeFromMilliseconds(+DATE)
const timeAgo = `created ${time.toRelative()}`
const size = +SNAPSHOT_SIZE ? prettyBytes(+SNAPSHOT_SIZE, 'MB') : '-'
const monitorSize = +SNAPSHOT_MONITOR_SIZE
? prettyBytes(+SNAPSHOT_MONITOR_SIZE, 'MB')
: '-'
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 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={time.toFormat('ff')}>{`#${ID} ${timeAgo}`}</span>
<span title={`Monitor Size / Disk Size: ${monitorSize}/${size}`}>
<ModernTv />
<span>{` ${monitorSize}/${size}`}</span>
</span>
</div>
return (
<Paper variant="outlined" className={classes.root}>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{NAME}</Typography>
<span className={classes.labels}>
{isActive && <StatusChip text={<Translate word={T.Active} />} />}
<StatusChip text={<Translate word={T.Snapshot} />} />
</span>
</div>
{!!actions.length && (
<div className={classes.actions}>
{actions.map((Action, idx) => (
<Action
key={`${Action.displayName ?? idx}-${ID}`}
{...extraActionProps}
snapshot={snapshot}
/>
))}
</div>
)}
</Paper>
)
}
)
<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>
)
})
DiskSnapshotCard.propTypes = {
snapshot: PropTypes.object.isRequired,

View File

@ -26,19 +26,21 @@ import {
} from '@mui/material'
import { rowStyles } from 'client/components/Tables/styles'
import { StatusChip } from 'client/components/Status'
import MultipleTags from 'client/components/MultipleTags'
import SecurityGroupCard from 'client/components/Cards/SecurityGroupCard'
import { Translate } from 'client/components/HOC'
import { stringToBoolean } from 'client/models/Helper'
import { T, Nic, NicAlias } from 'client/constants'
const NicCard = memo(
({
nic = {},
actions = [],
extraActionProps = {},
aliasActions = [],
extraAliasActionProps = {},
showParents = false,
clipboardOnTags = true,
}) => {
const classes = rowStyles()
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('md'))
@ -50,6 +52,8 @@ const NicCard = memo(
IP,
MAC,
PCI_ID,
RDP,
SSH,
PARENT,
ADDRESS,
ALIAS,
@ -60,6 +64,16 @@ const NicCard = memo(
const isPciDevice = PCI_ID !== undefined
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 tags = [
{ text: IP, dataCy: `${dataCy}-ip` },
{ text: MAC, dataCy: `${dataCy}-mac` },
@ -73,32 +87,34 @@ const NicCard = memo(
data-cy={`${dataCy}-${NIC_ID}`}
sx={{
flexWrap: 'wrap',
...(isAlias && { boxShadow: 'none !important' }),
boxShadow: 'none !important',
}}
>
<Box className={classes.main} {...(!isAlias && { pl: '1em' })}>
<Box
className={classes.main}
{...(!isAlias && !showParents && { pl: '1em' })}
>
<div className={classes.title}>
<Typography component="span" data-cy={`${dataCy}-name`}>
{`${NIC_ID} | ${NETWORK}`}
</Typography>
<span className={classes.labels}>
{noClipboardTags.map((tag) => (
<StatusChip
key={`${dataCy}-${NIC_ID}-${tag.dataCy}`}
text={tag.text}
dataCy={tag.dataCy}
/>
))}
<MultipleTags
clipboard
clipboard={clipboardOnTags}
limitTags={isMobile ? 1 : 3}
tags={tags}
/>
</span>
</div>
</Box>
{!isMobile &&
!isPciDevice &&
actions.map((Action, idx) => (
<Action
key={`${Action.displayName ?? idx}-${NIC_ID}`}
{...extraActionProps}
nic={nic}
/>
))}
{!isPciDevice && <div className={classes.actions}>{actions}</div>}
{!!ALIAS?.length && (
<Box flexBasis="100%">
{ALIAS?.map((alias) => (
@ -106,7 +122,7 @@ const NicCard = memo(
key={alias.NIC_ID}
nic={alias}
actions={aliasActions}
extraActionProps={extraAliasActionProps}
showParents={showParents}
/>
))}
</Box>
@ -148,14 +164,12 @@ const NicCard = memo(
NicCard.propTypes = {
nic: PropTypes.object,
actions: PropTypes.array,
extraActionProps: PropTypes.object,
aliasActions: PropTypes.array,
extraAliasActionProps: PropTypes.object,
actions: PropTypes.node,
aliasActions: PropTypes.node,
showParents: PropTypes.bool,
clipboardOnTags: PropTypes.bool,
}
NicCard.displayName = 'NicCard'
NicCard.displayName = 'NicCard'
export default NicCard

View File

@ -32,9 +32,6 @@ const useStyles = makeStyles((theme) => ({
height: '60vh',
maxWidth: '100%',
maxHeight: '100%',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
[theme.breakpoints.only('xs')]: {
width: '100vw',
height: '100vh',

View File

@ -13,121 +13,363 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import * as yup from 'yup'
import { INPUT_TYPES, T, HYPERVISORS } from 'client/constants'
import { string, number } from 'yup'
import { INPUT_TYPES, T, HYPERVISORS, Field } from 'client/constants'
const { kvm, vcenter, firecracker, lxc } = HYPERVISORS
export const TARGET = {
name: 'TARGET',
label: 'Target device',
notOnHypervisors: [vcenter, firecracker, lxc],
tooltip: `
Device to map image disk.
If set, it will overwrite the default device mapping.`,
type: INPUT_TYPES.TEXT,
validation: yup.string().trim().notRequired().default(undefined),
}
/** @type {Field[]} List of general fields */
export const GENERAL_FIELDS = [
{
name: 'TARGET',
label: T.TargetDevice,
tooltip: T.TargetDeviceConcept,
type: INPUT_TYPES.TEXT,
notOnHypervisors: [vcenter, firecracker, lxc],
validation: string().trim().notRequired().default(undefined),
fieldProps: { placeholder: 'sdc' },
},
{
name: 'READONLY',
label: T.ReadOnly,
notOnHypervisors: [vcenter],
type: INPUT_TYPES.SELECT,
values: [
{ text: T.Yes, value: 'YES' },
{ text: T.No, value: 'NO' },
],
validation: string()
.trim()
.notRequired()
.default(() => 'NO'),
},
{
name: 'DEV_PREFIX',
label: T.Bus,
notOnHypervisors: [vcenter, firecracker, lxc],
type: INPUT_TYPES.SELECT,
values: [
{ text: '', value: '' },
{ text: 'Virtio', value: 'vd' },
{ text: 'CSI/SATA', value: 'sd' },
{ text: 'Parallel ATA (IDE)', value: 'hd' },
{ text: 'Custom', value: 'custom' },
],
validation: string().trim().notRequired().default(undefined),
},
{
name: 'CACHE',
label: T.Cache,
notOnHypervisors: [vcenter, firecracker, lxc],
type: INPUT_TYPES.SELECT,
values: [
{ text: '', value: '' },
{ text: 'Default', value: 'default' },
{ text: 'Writethrough', value: 'writethrough' },
{ text: 'Writeback', value: 'writeback' },
{ text: 'Directsync', value: 'directsync' },
{ text: 'Unsafe', value: 'unsafe' },
],
validation: string().trim().notRequired().default(undefined),
},
{
name: 'IO',
label: T.IoPolicy,
notOnHypervisors: [vcenter, firecracker, lxc],
type: INPUT_TYPES.SELECT,
values: [
{ text: '', value: '' },
{ text: 'Threads', value: 'threads' },
{ text: 'Native', value: 'native' },
],
validation: string().trim().notRequired().default(undefined),
},
{
name: 'DISCARD',
label: T.Discard,
notOnHypervisors: [vcenter, firecracker, lxc],
type: INPUT_TYPES.SELECT,
values: [
{ text: '', value: '' },
{ text: 'Ignore', value: 'ignore' },
{ text: 'Unmap', value: 'unmap' },
],
validation: string().trim().notRequired().default(undefined),
},
{
name: 'SIZE_IOPS_SEC',
label: T.IopsSize,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'IOTHREADS',
label: T.IoThreadId,
tooltip: T.IoThreadIdConcept,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
]
export const READONLY = {
name: 'READONLY',
label: 'Read only',
notOnHypervisors: [vcenter],
type: INPUT_TYPES.SELECT,
values: [
{ text: T.Yes, value: 'YES' },
{ text: T.No, value: 'NO' },
],
validation: yup
.string()
.trim()
.notRequired()
.default(() => 'NO'),
}
/** @type {Field[]} List of vCenter fields */
export const VCENTER_FIELDS = [
{
name: 'VCENTER_ADAPTER_TYPE',
label: T.BusAdapterController,
notOnHypervisors: [kvm, firecracker],
type: INPUT_TYPES.SELECT,
values: [
{ text: '', value: '' },
{ text: 'lsiLogic', value: 'lsiLogic' },
{ text: 'ide', value: 'ide' },
{ text: 'busLogic', value: 'busLogic' },
{ text: 'Custom', value: 'custom' },
],
validation: string().trim().notRequired().default(undefined),
},
{
name: 'VCENTER_DISK_TYPE',
label: T.DiskProvisioningType,
notOnHypervisors: [kvm, firecracker],
type: INPUT_TYPES.SELECT,
values: [
{ text: '', value: '' },
{ text: 'Thin', value: 'thin' },
{ text: 'Thick', value: 'thick' },
{ text: 'Eager Zeroed Thick', value: 'eagerZeroedThick' },
{ text: 'Custom', value: 'custom' },
],
validation: string().trim().notRequired().default(undefined),
},
]
export const DEV_PREFIX = {
name: 'DEV_PREFIX',
label: 'BUS',
notOnHypervisors: [vcenter, firecracker, lxc],
type: INPUT_TYPES.SELECT,
values: [
{ text: '', value: '' },
{ text: 'Virtio', value: 'vd' },
{ text: 'CSI/SATA', value: 'sd' },
{ text: 'Parallel ATA (IDE)', value: 'hd' },
{ text: 'Custom', value: 'custom' },
],
validation: yup.string().trim().notRequired().default(undefined),
}
/** @type {Field[]} List of throttling bytes fields */
export const THROTTLING_BYTES_FIELDS = [
{
name: 'TOTAL_BYTES_SEC',
label: T.TotalValue,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'TOTAL_BYTES_SEC_MAX',
label: T.TotalMaximum,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'TOTAL_BYTES_SEC_MAX_LENGTH',
label: T.TotalMaximumLength,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'READ_BYTES_SEC',
label: T.ReadValue,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'READ_BYTES_SEC_MAX',
label: T.ReadMaximum,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'READ_BYTES_SEC_MAX_LENGTH',
label: T.ReadMaximumLength,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'WRITE_BYTES_SEC',
label: T.WriteValue,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'WRITE_BYTES_SEC_MAX',
label: T.WriteMaximum,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'WRITE_BYTES_SEC_MAX_LENGTH',
label: T.WriteMaximumLength,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
]
export const VCENTER_ADAPTER_TYPE = {
name: 'VCENTER_ADAPTER_TYPE',
label: 'Bus adapter controller',
notOnHypervisors: [kvm, firecracker],
type: INPUT_TYPES.SELECT,
values: [
{ text: '', value: '' },
{ text: 'lsiLogic', value: 'lsiLogic' },
{ text: 'ide', value: 'ide' },
{ text: 'busLogic', value: 'busLogic' },
{ text: 'Custom', value: 'custom' },
],
validation: yup.string().trim().notRequired().default(undefined),
}
/** @type {Field[]} List of throttling IOPS fields */
export const THROTTLING_IOPS_FIELDS = [
{
name: 'TOTAL_IOPS_SEC',
label: T.TotalValue,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'TOTAL_IOPS_SEC_MAX',
label: T.TotalMaximum,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'TOTAL_IOPS_SEC_MAX_LENGTH',
label: T.TotalMaximumLength,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'READ_IOPS_SEC',
label: T.ReadValue,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'READ_IOPS_SEC_MAX',
label: T.ReadMaximum,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'READ_IOPS_SEC_MAX_LENGTH',
label: T.ReadMaximumLength,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'WRITE_IOPS_SEC',
label: T.WriteValue,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'WRITE_IOPS_SEC_MAX',
label: T.WriteMaximum,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
{
name: 'WRITE_IOPS_SEC_MAX_LENGTH',
label: T.WriteMaximumLength,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [lxc, firecracker, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
]
export const VCENTER_DISK_TYPE = {
name: 'VCENTER_DISK_TYPE',
label: 'Disk provisioning type',
notOnHypervisors: [kvm, firecracker],
type: INPUT_TYPES.SELECT,
values: [
{ text: '', value: '' },
{ text: 'Thin', value: 'thin' },
{ text: 'Thick', value: 'thick' },
{ text: 'Eager Zeroed Thick', value: 'eagerZeroedThick' },
{ text: 'Custom', value: 'custom' },
],
validation: yup.string().trim().notRequired().default(undefined),
}
export const CACHE = {
name: 'CACHE',
label: 'Cache',
notOnHypervisors: [vcenter, firecracker, lxc],
type: INPUT_TYPES.SELECT,
values: [
{ text: '', value: '' },
{ text: 'Default', value: 'default' },
{ text: 'Writethrough', value: 'writethrough' },
{ text: 'Writeback', value: 'writeback' },
{ text: 'Directsync', value: 'directsync' },
{ text: 'Unsafe', value: 'unsafe' },
],
validation: yup.string().trim().notRequired().default(undefined),
}
export const IO = {
name: 'IO',
label: 'IO Policy',
notOnHypervisors: [vcenter, firecracker, lxc],
type: INPUT_TYPES.SELECT,
values: [
{ text: '', value: '' },
{ text: 'Threads', value: 'threads' },
{ text: 'Native', value: 'native' },
],
validation: yup.string().trim().notRequired().default(undefined),
}
export const DISCARD = {
name: 'DISCARD',
label: 'Discard',
notOnHypervisors: [vcenter, firecracker, lxc],
type: INPUT_TYPES.SELECT,
values: [
{ text: '', value: '' },
{ text: 'Ignore', value: 'ignore' },
{ text: 'Unmap', value: 'unmap' },
],
validation: yup.string().trim().notRequired().default(undefined),
}
/** @type {Field[]} List of edge cluster fields */
export const EDGE_CLUSTER_FIELDS = [
{
name: 'RECOVERY_SNAPSHOT_FREQ',
label: T.SnapshotFrequency,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
notOnHypervisors: [firecracker, lxc, vcenter],
validation: number()
.min(0)
.notRequired()
.default(() => undefined),
},
]

View File

@ -13,22 +13,50 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { Box } from '@mui/material'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import {
SCHEMA,
FIELDS,
SECTIONS,
} from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions/schema'
import { T } from 'client/constants'
import { Step } from 'client/utils'
import { T, HYPERVISORS } from 'client/constants'
export const STEP_ID = 'advanced'
const Content = ({ hypervisor }) => (
<FormWithSchema cy="attach-disk" id={STEP_ID} fields={FIELDS(hypervisor)} />
)
const Content = ({ hypervisor }) => {
const sections = useMemo(() => SECTIONS(hypervisor), [])
return (
<Box
display="grid"
gap="2em"
sx={{ gridTemplateColumns: { lg: '1fr 1fr', md: '1fr' } }}
>
{sections.map(({ id, legend, fields }) => (
<FormWithSchema
key={id}
rootProps={{ sx: id === 'general' && { gridColumn: '1 / -1' } }}
cy={id}
fields={fields}
legend={legend}
id={STEP_ID}
/>
))}
</Box>
)
}
/**
* Renders advanced options to disk.
*
* @param {object} props - Props
* @param {HYPERVISORS} props.hypervisor - Hypervisor
* @returns {Step} Advance options step
*/
const AdvancedOptions = ({ hypervisor } = {}) => ({
id: STEP_ID,
label: T.AdvancedOptions,

View File

@ -13,37 +13,83 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import * as yup from 'yup'
import { number, ObjectSchema } from 'yup'
import * as COMMON_FIELDS from 'client/components/Forms/Vm/AttachDiskForm/CommonFields'
import { INPUT_TYPES, HYPERVISORS } from 'client/constants'
import { getValidationFromFields } from 'client/utils'
import {
GENERAL_FIELDS,
VCENTER_FIELDS,
EDGE_CLUSTER_FIELDS,
THROTTLING_BYTES_FIELDS,
THROTTLING_IOPS_FIELDS,
} from 'client/components/Forms/Vm/AttachDiskForm/CommonFields'
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
import {
Field,
Section,
getValidationFromFields,
filterFieldsByHypervisor,
} from 'client/utils'
const { vcenter } = HYPERVISORS
/** @type {Field} Size field */
const SIZE = {
name: 'SIZE',
label: 'Size on instantiate',
tooltip: `
The size of the disk will be modified to match
this size when the template is instantiated`,
label: T.SizeOnInstantiate,
tooltip: T.SizeOnInstantiateConcept,
notOnHypervisors: [vcenter],
type: INPUT_TYPES.TEXT,
htmlType: 'number',
validation: yup
.number()
.typeError('Size value must be a number')
validation: number()
.notRequired()
.default(undefined),
.default(() => undefined),
}
export const FIELDS = (hypervisor) =>
[SIZE, ...Object.values(COMMON_FIELDS)]
.map((field) => (typeof field === 'function' ? field(hypervisor) : field))
.filter(
({ notOnHypervisors } = {}) => !notOnHypervisors?.includes?.(hypervisor)
)
/**
* @param {HYPERVISORS} hypervisor - Hypervisor
* @returns {Section[]} Sections
*/
const SECTIONS = (hypervisor) => [
{
id: 'general',
legend: T.General,
fields: filterFieldsByHypervisor([SIZE, ...GENERAL_FIELDS], hypervisor),
},
{
id: 'vcenter',
legend: 'vCenter',
fields: filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor),
},
{
id: 'edge-cluster',
legend: T.EdgeCluster,
fields: filterFieldsByHypervisor(EDGE_CLUSTER_FIELDS, hypervisor),
},
{
id: 'throttling-bytes',
legend: T.ThrottlingBytes,
fields: filterFieldsByHypervisor(THROTTLING_BYTES_FIELDS, hypervisor),
},
{
id: 'throttling-iops',
legend: T.ThrottlingIOPS,
fields: filterFieldsByHypervisor(THROTTLING_IOPS_FIELDS, hypervisor),
},
]
export const SCHEMA = (hypervisor) =>
yup.object(getValidationFromFields(FIELDS(hypervisor)))
/**
* @param {HYPERVISORS} hypervisor - Hypervisor
* @returns {Field[]} Advanced options fields
*/
const FIELDS = (hypervisor) =>
SECTIONS(hypervisor)
.map(({ fields }) => fields)
.flat()
/**
* @param {HYPERVISORS} hypervisor - Hypervisor
* @returns {ObjectSchema} Advanced options schema
*/
const SCHEMA = (hypervisor) => getValidationFromFields(FIELDS(hypervisor))
export { SECTIONS, FIELDS, SCHEMA }

View File

@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types'
import { useListForm } from 'client/hooks'
import { ImagesTable } from 'client/components/Tables'
import { SCHEMA } from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable/schema'
import { Step } from 'client/utils'
import { T } from 'client/constants'
export const STEP_ID = 'image'
@ -48,6 +48,11 @@ const Content = ({ data, setFormData }) => {
)
}
/**
* Renders datatable to select an image form pool.
*
* @returns {Step} Image step
*/
const ImageStep = () => ({
id: STEP_ID,
label: T.Image,

View File

@ -13,22 +13,50 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { Box } from '@mui/material'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import {
SCHEMA,
FIELDS,
SECTIONS,
} from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions/schema'
import { T } from 'client/constants'
import { Step } from 'client/utils'
import { T, HYPERVISORS } from 'client/constants'
export const STEP_ID = 'advanced'
const Content = ({ hypervisor }) => (
<FormWithSchema cy="attach-disk" fields={FIELDS(hypervisor)} id={STEP_ID} />
)
const Content = ({ hypervisor }) => {
const sections = useMemo(() => SECTIONS(hypervisor), [])
return (
<Box
display="grid"
gap="2em"
sx={{ gridTemplateColumns: { lg: '1fr 1fr', md: '1fr' } }}
>
{sections.map(({ id, legend, fields }) => (
<FormWithSchema
key={id}
rootProps={{ sx: id === 'general' && { gridColumn: '1 / -1' } }}
cy={id}
fields={fields}
legend={legend}
id={STEP_ID}
/>
))}
</Box>
)
}
/**
* Renders advanced options to volatile disk.
*
* @param {object} props - Props
* @param {HYPERVISORS} props.hypervisor - Hypervisor
* @returns {Step} Advance options step
*/
const AdvancedOptions = ({ hypervisor } = {}) => ({
id: STEP_ID,
label: T.AdvancedOptions,

View File

@ -13,24 +13,68 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import * as yup from 'yup'
import { ObjectSchema } from 'yup'
import {
TARGET,
READONLY,
CACHE,
IO,
DISCARD,
DEV_PREFIX,
VCENTER_DISK_TYPE,
GENERAL_FIELDS,
VCENTER_FIELDS,
EDGE_CLUSTER_FIELDS,
THROTTLING_BYTES_FIELDS,
THROTTLING_IOPS_FIELDS,
} from 'client/components/Forms/Vm/AttachDiskForm/CommonFields'
import { getValidationFromFields } from 'client/utils'
import { T, HYPERVISORS } from 'client/constants'
import {
Field,
Section,
getValidationFromFields,
filterFieldsByHypervisor,
} from 'client/utils'
export const FIELDS = (hypervisor) =>
[TARGET, READONLY, CACHE, IO, DISCARD, DEV_PREFIX, VCENTER_DISK_TYPE].filter(
({ notOnHypervisors } = {}) => !notOnHypervisors?.includes?.(hypervisor)
)
/**
* @param {HYPERVISORS} hypervisor - Hypervisor
* @returns {Section[]} Sections
*/
const SECTIONS = (hypervisor) => [
{
id: 'general',
legend: T.General,
fields: filterFieldsByHypervisor(GENERAL_FIELDS, hypervisor),
},
{
id: 'vcenter',
legend: 'vCenter',
fields: filterFieldsByHypervisor(VCENTER_FIELDS, hypervisor),
},
{
id: 'edge-cluster',
legend: T.EdgeCluster,
fields: filterFieldsByHypervisor(EDGE_CLUSTER_FIELDS, hypervisor),
},
{
id: 'throttling-bytes',
legend: T.ThrottlingBytes,
fields: filterFieldsByHypervisor(THROTTLING_BYTES_FIELDS, hypervisor),
},
{
id: 'throttling-iops',
legend: T.ThrottlingIOPS,
fields: filterFieldsByHypervisor(THROTTLING_IOPS_FIELDS, hypervisor),
},
]
export const SCHEMA = (hypervisor) =>
yup.object(getValidationFromFields(FIELDS(hypervisor)))
/**
* @param {HYPERVISORS} hypervisor - Hypervisor
* @returns {Field[]} Advanced options fields
*/
const FIELDS = (hypervisor) =>
SECTIONS(hypervisor)
.map(({ fields }) => fields)
.flat()
/**
* @param {HYPERVISORS} hypervisor - Hypervisor
* @returns {ObjectSchema} Advanced options schema
*/
const SCHEMA = (hypervisor) => getValidationFromFields(FIELDS(hypervisor))
export { SECTIONS, FIELDS, SCHEMA }

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
@ -21,14 +21,24 @@ import {
SCHEMA,
FIELDS,
} from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration/schema'
import { T } from 'client/constants'
import { Step } from 'client/utils'
import { T, HYPERVISORS } from 'client/constants'
export const STEP_ID = 'configuration'
const Content = ({ hypervisor }) => (
<FormWithSchema cy="attach-disk" fields={FIELDS(hypervisor)} id={STEP_ID} />
)
const Content = ({ hypervisor }) => {
const memoFields = useMemo(() => FIELDS(hypervisor), [])
return <FormWithSchema cy="attach-disk" fields={memoFields} id={STEP_ID} />
}
/**
* Renders configuration to volatile disk.
*
* @param {object} props - Props
* @param {HYPERVISORS} props.hypervisor - Hypervisor
* @returns {Step} Basic configuration step
*/
const BasicConfiguration = ({ hypervisor } = {}) => ({
id: STEP_ID,
label: T.Configuration,

View File

@ -13,26 +13,34 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import * as yup from 'yup'
import { number, string, object, ObjectSchema } from 'yup'
import { INPUT_TYPES, HYPERVISORS } from 'client/constants'
import { getValidationFromFields } from 'client/utils'
import { useGetSunstoneConfigQuery } from 'client/features/OneApi/system'
import {
Field,
getValidationFromFields,
filterFieldsByHypervisor,
arrayToOptions,
} from 'client/utils'
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
const { vcenter } = HYPERVISORS
/** @type {Field} Size field */
const SIZE = {
name: 'SIZE',
label: 'Size',
label: T.Size,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
validation: yup
.number()
.typeError('Size value must be a number')
.required('Size field is required')
.default(undefined),
validation: number()
.required()
.default(() => undefined),
}
/**
* @param {HYPERVISORS} hypervisor - hypervisor
* @returns {Field} Disk type field
*/
const TYPE = (hypervisor) => ({
name: 'TYPE',
label: 'Disk type',
@ -44,61 +52,60 @@ const TYPE = (hypervisor) => ({
{ text: 'FS', value: 'fs' },
{ text: 'Swap', value: 'swap' },
],
validation: yup.string().trim().notRequired().default('fs'),
validation: string().trim().notRequired().default('fs'),
})
const FORMAT = (hypervisor) => {
const typeFieldName = TYPE(hypervisor).name
/**
* @param {HYPERVISORS} hypervisor - hypervisor
* @returns {Field} Format field
*/
const FORMAT = (hypervisor) => ({
name: 'FORMAT',
label: T.Format,
type: INPUT_TYPES.SELECT,
dependOf: 'TYPE',
htmlType: (type) => type === 'swap' && INPUT_TYPES.HIDDEN,
values:
hypervisor === vcenter
? [{ text: 'Raw', value: 'raw' }]
: [
{ text: 'Raw', value: 'raw' },
{ text: 'qcow2', value: 'qcow2' },
],
validation: string()
.trim()
.when('TYPE', (type, schema) =>
type === 'swap' ? schema.notRequired() : schema.required()
)
.default('raw'),
})
return {
name: 'FORMAT',
label: 'Format',
type: INPUT_TYPES.SELECT,
dependOf: typeFieldName,
htmlType: (type) => (type === 'swap' ? INPUT_TYPES.HIDDEN : undefined),
values:
hypervisor === vcenter
? [{ text: 'Raw', value: 'raw' }]
: [
{ text: 'Raw', value: 'raw' },
{ text: 'qcow2', value: 'qcow2' },
],
validation: yup
.string()
.trim()
.when(typeFieldName, (type, schema) =>
type === 'swap'
? schema.notRequired()
: schema.required('Format field is required')
)
.default('raw'),
}
}
const FILESYSTEM = (hypervisor) => ({
/** @type {Field} Filesystem field */
const FILESYSTEM = {
name: 'FS',
label: 'Filesystem',
label: T.FileSystemType,
notOnHypervisors: [vcenter],
type: INPUT_TYPES.SELECT,
dependOf: TYPE(hypervisor).name,
htmlType: (type) => (type === 'swap' ? INPUT_TYPES.HIDDEN : undefined),
values: [
// TODO: sunstone-config => support_fs ???
{ text: '', value: '' },
{ text: 'ext4', value: 'ext4' },
{ text: 'ext3', value: 'ext3' },
{ text: 'ext2', value: 'ext2' },
{ text: 'xfs', value: 'xfs' },
],
validation: yup.string().trim().notRequired().default(undefined),
})
dependOf: 'TYPE',
htmlType: (type) => type === 'swap' && INPUT_TYPES.HIDDEN,
values: () => {
const { data: config } = useGetSunstoneConfigQuery()
return arrayToOptions(config?.supported_fs)
},
validation: string().trim().notRequired().default(undefined),
}
/**
* @param {HYPERVISORS} hypervisor - hypervisor
* @returns {Field[]} List of fields
*/
export const FIELDS = (hypervisor) =>
[SIZE, TYPE, FORMAT, FILESYSTEM]
.map((field) => (typeof field === 'function' ? field(hypervisor) : field))
.filter(
({ notOnHypervisors } = {}) => !notOnHypervisors?.includes?.(hypervisor)
)
filterFieldsByHypervisor([SIZE, TYPE, FORMAT, FILESYSTEM], hypervisor)
/**
* @param {HYPERVISORS} hypervisor - hypervisor
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = (hypervisor) =>
yup.object(getValidationFromFields(FIELDS(hypervisor)))
object(getValidationFromFields(FIELDS(hypervisor)))

View File

@ -13,31 +13,55 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { Box } from '@mui/material'
import {
SCHEMA,
FIELDS,
SECTIONS,
} from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/schema'
import { T } from 'client/constants'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { Step } from 'client/utils'
import { T, Nic, HYPERVISORS } from 'client/constants'
export const STEP_ID = 'advanced'
const Content = (props) => (
<FormWithSchema
cy="attach-nic-advanced"
id={STEP_ID}
fields={FIELDS(props)}
/>
)
const Content = (props) => {
const sections = useMemo(() => SECTIONS(props), [])
return (
<Box
display="grid"
gap="2em"
sx={{ gridTemplateColumns: { lg: '1fr 1fr', md: '1fr' } }}
>
{sections.map(({ id, legend, fields }) => (
<FormWithSchema
key={id}
rootProps={{ sx: id === 'general' && { gridColumn: '1 / -1' } }}
cy={id}
fields={fields}
legend={legend}
id={STEP_ID}
/>
))}
</Box>
)
}
/**
* Renders advanced options to nic.
*
* @param {object} props - Props
* @param {Nic[]} props.nics - Current nics
* @param {HYPERVISORS} props.hypervisor - Hypervisor
* @returns {Step} Advance options step
*/
const AdvancedOptions = (props) => ({
id: STEP_ID,
label: T.AdvancedOptions,
resolver: SCHEMA,
resolver: () => SCHEMA(props),
optionsValidate: { abortEarly: false },
content: () => Content(props),
})
@ -46,6 +70,7 @@ Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
nics: PropTypes.array,
hypervisor: PropTypes.string,
}
export default AdvancedOptions

View File

@ -13,82 +13,378 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { boolean, string, object, ObjectSchema } from 'yup'
import { boolean, number, string, ObjectSchema } from 'yup'
import { Field, getValidationFromFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
import {
Field,
Section,
filterFieldsByHypervisor,
filterFieldsByDriver,
getObjectSchemaFromFields,
} from 'client/utils'
import { T, INPUT_TYPES, HYPERVISORS, VN_DRIVERS, Nic } from 'client/constants'
/** @type {Field} RDP connection field */
const RDP_FIELD = {
name: 'RDP',
label: T.RdpConnection,
type: INPUT_TYPES.SWITCH,
validation: boolean().yesOrNo(),
grid: { md: 12 },
}
const { firecracker } = HYPERVISORS
const { ovswitch, vcenter } = VN_DRIVERS
/** @type {Field} SSH connection field */
const SSH_FIELD = {
name: 'SSH',
label: T.SshConnection,
type: INPUT_TYPES.SWITCH,
validation: boolean().yesOrNo(),
grid: { md: 12 },
}
const filterByHypAndDriver = (fields, { hypervisor, driver }) =>
filterFieldsByDriver(filterFieldsByHypervisor(fields, hypervisor), driver)
/**
* @param {object} currentFormData - Current form data
* @param {object[]} currentFormData.nics - Nics
* @returns {Field} Alias field
* @param {object} [data] - VM or VM Template data
* @param {Nic[]} [data.nics] - Current NICs
* @returns {Field[]} List of general fields
*/
const ALIAS_FIELD = ({ nics = [] }) => ({
name: 'PARENT',
label: T.AsAnAlias,
dependOf: 'NAME',
type: (name) => {
const hasAlias = nics?.some((nic) => nic.PARENT === name)
const GENERAL_FIELDS = ({ nics = [] } = {}) =>
[
{
name: 'RDP',
label: T.RdpConnection,
type: INPUT_TYPES.SWITCH,
validation: boolean().yesOrNo(),
grid: { sm: 6 },
},
{
name: 'SSH',
label: T.SshConnection,
type: INPUT_TYPES.SWITCH,
validation: boolean().yesOrNo(),
grid: { sm: 6 },
},
!!nics?.length && {
name: 'PARENT',
label: T.AsAnAlias,
dependOf: 'NAME',
type: (name) => {
const hasAlias = nics?.some((nic) => nic.PARENT === name)
return name && hasAlias ? INPUT_TYPES.HIDDEN : INPUT_TYPES.SELECT
return name && hasAlias ? INPUT_TYPES.HIDDEN : INPUT_TYPES.SELECT
},
values: (name) => [
{ text: '', value: '' },
...nics
.filter(({ PARENT }) => !PARENT) // filter nic alias
.filter(({ NAME }) => NAME !== name || !name) // filter it self
.map((nic) => {
const { NAME, IP = '', NETWORK = '', NIC_ID = '' } = nic
const text = [NAME ?? NIC_ID, NETWORK, IP]
.filter(Boolean)
.join(' - ')
return { text, value: NAME }
}),
],
validation: string()
.trim()
.notRequired()
.default(() => undefined),
grid: { sm: 6 },
},
{
name: 'EXTERNAL',
label: T.External,
tooltip: T.ExternalConcept,
type: INPUT_TYPES.SWITCH,
dependOf: 'PARENT',
htmlType: (parent) => !parent?.length && INPUT_TYPES.HIDDEN,
validation: boolean().yesOrNo(),
grid: { sm: 6 },
},
].filter(Boolean)
/** @type {Field[]} List of IPv4 fields */
const OVERRIDE_IPV4_FIELDS = [
{
name: 'IP',
label: T.IP,
tooltip: T.IPv4Concept,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.notRequired()
.default(() => undefined),
},
{
name: 'MAC',
label: T.MAC,
tooltip: T.MACConcept,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.notRequired()
.default(() => undefined),
},
{
name: 'NETWORK_MASK',
label: T.NetworkMask,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.notRequired()
.default(() => undefined),
fieldProps: { placeholder: '255.255.255.0' },
},
{
name: 'NETWORK_ADDRESS',
label: T.NetworkAddress,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.notRequired()
.default(() => undefined),
fieldProps: { placeholder: '192.168.1.0' },
},
{
name: 'GATEWAY',
label: T.Gateway,
tooltip: T.GatewayConcept,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.notRequired()
.default(() => undefined),
},
{
name: 'SEARCH_DOMAIN',
label: T.SearchDomainForDNSResolution,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.notRequired()
.default(() => undefined),
},
{
name: 'METHOD',
label: T.NetworkMethod,
tooltip: T.NetworkMethod4Concept,
type: INPUT_TYPES.SELECT,
values: [
{ text: 'static (Based on context)', value: 'static' },
{ text: 'dhcp (DHCPv4)', value: 'dhcp' },
{ text: 'skip (Do not configure IPv4)', value: 'skip' },
],
validation: string().trim().notRequired().default('static'),
},
values: (name) => [
{ text: '', value: '' },
...nics
.filter(({ PARENT }) => !PARENT) // filter nic alias
.filter(({ NAME }) => NAME !== name || !name) // filter it self
.map((nic) => {
const { NAME, IP = '', NETWORK = '', NIC_ID = '' } = nic
const text = [NAME ?? NIC_ID, NETWORK, IP].filter(Boolean).join(' - ')
return { text, value: NAME }
}),
],
validation: string()
.trim()
.notRequired()
.default(() => undefined),
})
/** @type {Field} External field */
const EXTERNAL_FIELD = {
name: 'EXTERNAL',
label: T.External,
tooltip: T.ExternalConcept,
type: INPUT_TYPES.SWITCH,
dependOf: 'PARENT',
htmlType: (parent) => !parent?.length && INPUT_TYPES.HIDDEN,
validation: boolean().yesOrNo(),
}
/**
* @param {object} [currentFormData] - Current form data
* @returns {Field[]} List of Graphics fields
*/
export const FIELDS = (currentFormData = {}) => [
RDP_FIELD,
SSH_FIELD,
ALIAS_FIELD(currentFormData),
EXTERNAL_FIELD,
]
/** @type {ObjectSchema} Advanced options schema */
export const SCHEMA = object(getValidationFromFields(FIELDS()))
/** @type {Field[]} List of IPv6 fields */
const OVERRIDE_IPV6_FIELDS = [
{
name: 'IP6',
label: T.IP,
tooltip: T.IPv6Concept,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.notRequired()
.default(() => undefined),
},
{
name: 'GATEWAY6',
label: T.Gateway,
tooltip: T.Gateway6Concept,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.notRequired()
.default(() => undefined),
},
{
name: 'IP6_METHOD',
label: T.NetworkMethod,
tooltip: T.NetworkMethod6Concept,
type: INPUT_TYPES.SELECT,
values: [
{ text: 'static (Based on context)', value: 'static' },
{ text: 'auto (SLAAC)', value: 'auto' },
{ text: 'dhcp (SLAAC and DHCPv6)', value: 'dhcp' },
{ text: 'disable (Do not use IPv6)', value: 'disable' },
{ text: 'skip (Do not configure IPv4)', value: 'skip' },
],
validation: string().trim().notRequired().default('static'),
},
]
/** @type {Field[]} List of Inbound traffic QoS fields */
const OVERRIDE_IN_QOS_FIELDS = [
{
name: 'INBOUND_AVG_BW',
label: T.AverageBandwidth,
tooltip: T.InboundAverageBandwidthConcept,
type: INPUT_TYPES.TEXT,
notOnHypervisors: [firecracker],
htmlType: 'number',
validation: number()
.notRequired()
.default(() => undefined),
},
{
name: 'INBOUND_PEAK_BW',
label: T.PeakBandwidth,
tooltip: T.InboundPeakBandwidthConcept,
type: INPUT_TYPES.TEXT,
notOnHypervisors: [firecracker],
htmlType: 'number',
validation: number()
.notRequired()
.default(() => undefined),
},
{
name: 'INBOUND_PEAK_KB',
label: T.PeakBurst,
tooltip: T.PeakBurstConcept,
type: INPUT_TYPES.TEXT,
notOnHypervisors: [firecracker],
notOnDrivers: [vcenter],
htmlType: 'number',
validation: number()
.notRequired()
.default(() => undefined),
},
]
/** @type {Field[]} List of Outbound traffic QoS fields */
const OVERRIDE_OUT_QOS_FIELDS = [
{
name: 'OUTBOUND_AVG_BW',
label: T.AverageBandwidth,
tooltip: T.OutboundAverageBandwidthConcept,
type: INPUT_TYPES.TEXT,
notOnHypervisors: [firecracker],
notOnDrivers: [ovswitch],
htmlType: 'number',
validation: number()
.notRequired()
.default(() => undefined),
},
{
name: 'OUTBOUND_PEAK_BW',
label: T.PeakBandwidth,
tooltip: T.OutboundPeakBandwidthConcept,
type: INPUT_TYPES.TEXT,
notOnHypervisors: [firecracker],
notOnDrivers: [ovswitch],
htmlType: 'number',
validation: number()
.notRequired()
.default(() => undefined),
},
{
name: 'OUTBOUND_PEAK_KB',
label: T.PeakBurst,
tooltip: T.PeakBurstConcept,
type: INPUT_TYPES.TEXT,
notOnHypervisors: [firecracker],
notOnDrivers: [ovswitch, vcenter],
htmlType: 'number',
validation: number()
.notRequired()
.default(() => undefined),
},
]
/** @type {Field[]} List of hardware fields */
const HARDWARE_FIELDS = [
{
name: 'MODEL',
label: T.HardwareModelToEmulate,
type: INPUT_TYPES.TEXT,
notOnHypervisors: [firecracker],
validation: string()
.trim()
.notRequired()
.default(() => undefined),
},
{
name: 'VIRTIO_QUEUES',
label: T.TransmissionQueue,
tooltip: T.OnlySupportedForVirtioDriver,
type: INPUT_TYPES.TEXT,
notOnHypervisors: [firecracker],
htmlType: 'number',
validation: number()
.notRequired()
.default(() => undefined),
},
]
/** @type {Field[]} List of guest option fields */
const GUEST_FIELDS = [
{
name: 'GUEST_MTU',
label: T.GuestMTU,
tooltip: T.GuestMTUConcept,
type: INPUT_TYPES.TEXT,
notOnHypervisors: [firecracker],
validation: string()
.trim()
.notRequired()
.default(() => undefined),
},
]
/**
* @param {object} data - VM or VM Template data
* @param {Nic[]} [data.nics] - Current nics on resource
* @param {VN_DRIVERS} [data.driver] - Virtual network driver
* @param {HYPERVISORS} [data.hypervisor] - VM Hypervisor
* @returns {Section[]} Sections
*/
const SECTIONS = ({ nics, driver, hypervisor = HYPERVISORS.kvm } = {}) => {
const filters = { driver, hypervisor }
return [
{
id: 'general',
legend: T.General,
fields: filterByHypAndDriver(GENERAL_FIELDS({ nics }), filters),
},
{
id: 'override-ipv4',
legend: T.OverrideNetworkValuesIPv4,
fields: filterByHypAndDriver(OVERRIDE_IPV4_FIELDS, filters),
},
{
id: 'override-ipv6',
legend: T.OverrideNetworkValuesIPv6,
fields: filterByHypAndDriver(OVERRIDE_IPV6_FIELDS, filters),
},
{
id: 'override-in-qos',
legend: T.OverrideNetworkInboundTrafficQos,
fields: filterByHypAndDriver(OVERRIDE_IN_QOS_FIELDS, filters),
},
{
id: 'override-out-qos',
legend: T.OverrideNetworkOutboundTrafficQos,
fields: filterByHypAndDriver(OVERRIDE_OUT_QOS_FIELDS, filters),
},
{
id: 'hardware',
legend: T.Hardware,
fields: filterByHypAndDriver(HARDWARE_FIELDS, filters),
},
{
id: 'guest',
legend: T.GuestOptions,
fields: filterByHypAndDriver(GUEST_FIELDS, filters),
},
]
}
/**
* @param {object} data - VM or VM Template data
* @returns {Field[]} Advanced options schema
*/
const FIELDS = (data) =>
SECTIONS(data)
.map(({ fields }) => fields)
.flat()
/**
* @param {object} data - VM or VM Template data
* @returns {ObjectSchema} Advanced options schema
*/
const SCHEMA = (data) => getObjectSchemaFromFields(FIELDS(data))
export { SECTIONS, FIELDS, SCHEMA }

View File

@ -38,7 +38,7 @@ const FixedLeases = ({ leases }) => {
return (
<>
<Stack spacing={2}>
<Stack spacing={1}>
{transformChartersToSchedActions(fixedLeases, true)?.map((action) => {
const { ACTION, TIME, PERIOD, WARNING, WARNING_PERIOD } = action

View File

@ -69,7 +69,7 @@ const BootItemDraggable = styled('div')(({ theme, disabled }) => ({
* @returns {string} Updated boot order after remove
*/
export const reorderBootAfterRemove = (id, list, currentBootOrder) => {
const type = String(id).toLowerCase().replace(/\d+/g, '') // nic | disk
const type = String(id).toLowerCase().replace(/\d+/g, '') // nic | nic_alias | disk
const getIndexFromId = (bootId) => `${bootId}`.toLowerCase().replace(type, '')
@ -129,9 +129,13 @@ const BootOrder = () => {
[]
)
const nics = useMemo(
() =>
getValues(`${EXTRA_ID}.${NIC_ID}`)?.map((nic, idx) => ({
const nics = useMemo(() => {
const nicId = `${EXTRA_ID}.${NIC_ID[0]}`
const nicAliasId = `${EXTRA_ID}.${NIC_ID[1]}`
const nicValues = getValues([nicId, nicAliasId]).flat()
return (
nicValues?.map((nic, idx) => ({
ID: `nic${idx}`,
NAME: (
<>
@ -139,9 +143,9 @@ const BootOrder = () => {
{[nic?.NAME, nic.NETWORK].filter(Boolean).join(': ')}
</>
),
})) ?? [],
[]
)
})) ?? []
)
}, [])
const enabledItems = [...disks, ...nics]
.filter((item) => bootOrder.includes(item.ID))
@ -230,5 +234,4 @@ const BootOrder = () => {
}
BootOrder.displayName = 'BootOrder'
export default BootOrder

View File

@ -47,7 +47,8 @@ const Booting = ({ hypervisor, ...props }) => {
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
>
{(!!props.data?.[STORAGE_ID]?.length ||
!!props.data?.[NIC_ID]?.length) && (
!!props.data?.[NIC_ID[0]]?.length ||
!!props.data?.[NIC_ID[1]]?.length) && (
<FormControl
component="fieldset"
sx={{ width: '100%', gridColumn: '1 / -1' }}

View File

@ -1,105 +0,0 @@
/* ------------------------------------------------------------------------- *
* 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 { memo, JSXElementConstructor } from 'react'
import PropTypes from 'prop-types'
import { Edit, Trash } from 'iconoir-react'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import { AttachNicForm } from 'client/components/Forms/Vm'
import { Translate } from 'client/components/HOC'
import { stringToBoolean } from 'client/models/Helper'
import { T } from 'client/constants'
/**
* @param {object} props - Props
* @param {number} props.index - Index in list
* @param {object} props.item - NIC
* @param {string} props.handleRemove - Remove function
* @param {string} props.handleUpdate - Update function
* @returns {JSXElementConstructor} - NIC card
*/
const NicItem = memo(({ item, nics, handleRemove, handleUpdate }) => {
const { id, NAME, RDP, SSH, NETWORK, PARENT, EXTERNAL } = item
const hasAlias = nics?.some((nic) => nic.PARENT === NAME)
return (
<SelectCard
key={id ?? NAME}
title={[NAME, NETWORK].filter(Boolean).join(' - ')}
subheader={
<>
{Object.entries({
RDP: stringToBoolean(RDP),
SSH: stringToBoolean(SSH),
EXTERNAL: stringToBoolean(EXTERNAL),
[`PARENT: ${PARENT}`]: PARENT,
})
.map(([k, v]) => (v ? `${k}` : ''))
.filter(Boolean)
.join(' | ')}
</>
}
action={
<>
{!hasAlias && (
<Action
data-cy={`remove-${NAME}`}
tooltip={<Translate word={T.Remove} />}
handleClick={handleRemove}
color="error"
icon={<Trash />}
/>
)}
<ButtonToTriggerForm
buttonProps={{
'data-cy': `edit-${NAME}`,
icon: <Edit />,
tooltip: <Translate word={T.Edit} />,
}}
options={[
{
dialogProps: {
title: (
<Translate
word={T.EditSomething}
values={[`${NAME} - ${NETWORK}`]}
/>
),
},
form: () => AttachNicForm({ nics }, item),
onSubmit: handleUpdate,
},
]}
/>
</>
}
/>
)
})
NicItem.propTypes = {
index: PropTypes.number,
item: PropTypes.object,
nics: PropTypes.array,
handleRemove: PropTypes.func,
handleUpdate: PropTypes.func,
}
NicItem.displayName = 'NicItem'
export default NicItem

View File

@ -18,9 +18,12 @@ import { Stack } from '@mui/material'
import { ServerConnection as NetworkIcon } from 'iconoir-react'
import { useFormContext, useFieldArray } from 'react-hook-form'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import { AttachNicForm } from 'client/components/Forms/Vm'
import { FormWithSchema } from 'client/components/Forms'
import NicCard from 'client/components/Cards/NicCard'
import {
AttachAction,
DetachAction,
} from 'client/components/Tabs/Vm/Network/Actions'
import {
STEP_ID as EXTRA_ID,
@ -33,58 +36,78 @@ import {
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting'
import { FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/networking/schema'
import { T } from 'client/constants'
import NicItem from './NicItem'
export const TAB_ID = 'NIC'
export const TAB_ID = ['NIC', 'NIC_ALIAS']
const mapNameFunction = mapNameByIndex('NIC')
const mapNicNameFunction = mapNameByIndex('NIC')
const mapAliasNameFunction = mapNameByIndex('NIC_ALIAS')
const Networking = () => {
const Networking = ({ hypervisor }) => {
const { setValue, getValues } = useFormContext()
const {
fields: nics,
replace,
update,
append,
fields: nics = [],
replace: replaceNic,
update: updateNic,
append: appendNic,
} = useFieldArray({
name: `${EXTRA_ID}.${TAB_ID}`,
name: `${EXTRA_ID}.${TAB_ID[0]}`,
})
const removeAndReorder = (nicName) => {
const updatedNics = nics
const {
fields: alias = [],
replace: replaceAlias,
update: updateAlias,
append: appendAlias,
} = useFieldArray({
name: `${EXTRA_ID}.${TAB_ID[1]}`,
})
const removeAndReorder = (nic) => {
const nicName = nic?.NAME
const isAlias = !!nic?.PARENT?.length
const list = isAlias ? alias : nics
const updatedList = list
.filter(({ NAME }) => NAME !== nicName)
.map(mapNameFunction)
.map(isAlias ? mapAliasNameFunction : mapNicNameFunction)
const currentBootOrder = getValues(BOOT_ORDER_NAME())
const updatedBootOrder = reorderBootAfterRemove(
nicName,
nics,
list,
currentBootOrder
)
replace(updatedNics)
isAlias ? replaceAlias(updatedList) : replaceNic(updatedList)
setValue(BOOT_ORDER_NAME(), updatedBootOrder)
}
const handleUpdate = (updatedNic, index) => {
update(index, mapNameFunction(updatedNic, index))
const handleUpdate = ({ NAME: _, ...updatedNic }, id) => {
const isAlias = !!updatedNic?.PARENT?.length
const index = isAlias
? alias.findIndex((nic) => nic.id === id)
: nics.findIndex((nic) => nic.id === id)
isAlias
? updateAlias(index, mapAliasNameFunction(updatedNic, index))
: updateNic(index, mapNicNameFunction(updatedNic, index))
}
const handleAppend = (newNic) => {
const isAlias = !!newNic?.PARENT?.length
isAlias
? appendAlias(mapAliasNameFunction(newNic, alias.length))
: appendNic(mapNicNameFunction(newNic, nics.length))
}
return (
<>
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'add-nic',
label: T.AttachNic,
variant: 'outlined',
}}
options={[
{
dialogProps: { title: T.AttachNic, dataCy: 'modal-attach-nic' },
form: () => AttachNicForm({ nics }),
onSubmit: (nic) => append(mapNameFunction(nic, nics.length)),
},
]}
<div>
<AttachAction
currentNics={nics}
hypervisor={hypervisor}
onSubmit={handleAppend}
/>
<Stack
pb="1em"
@ -94,19 +117,49 @@ const Networking = () => {
sx={{
gridTemplateColumns: {
sm: '1fr',
md: 'repeat(auto-fit, minmax(300px, 0.5fr))',
md: 'repeat(auto-fit, minmax(400px, 0.5fr))',
},
}}
>
{nics?.map(({ id, ...item }, index) => (
<NicItem
key={id ?? item?.NAME}
item={item}
nics={nics}
handleRemove={() => removeAndReorder(item?.NAME)}
handleUpdate={(updatedNic) => handleUpdate(updatedNic, index)}
/>
))}
{[...nics, ...alias]?.map(({ id, ...item }, index) => {
const hasAlias = alias?.some((nic) => nic.PARENT === item.NAME)
item.NIC_ID = index
return (
<NicCard
key={id ?? item?.NAME}
nic={item}
showParents
clipboardOnTags={false}
actions={
<>
{!hasAlias && (
<DetachAction
nic={item}
onSubmit={() => removeAndReorder(item)}
/>
)}
<AttachAction
nic={item}
hypervisor={hypervisor}
currentNics={nics}
onSubmit={(updatedNic) => {
const wasAlias = !!item?.PARENT?.length
const isAlias = !!updatedNic?.PARENT?.length
if (wasAlias === isAlias) {
return handleUpdate(updatedNic, id)
}
removeAndReorder(item)
handleAppend(updatedNic)
}}
/>
</>
}
/>
)
})}
</Stack>
<FormWithSchema
cy={`${EXTRA_ID}-network-options`}
@ -115,7 +168,7 @@ const Networking = () => {
legendTooltip={T.NetworkDefaultsConcept}
id={EXTRA_ID}
/>
</>
</div>
)
}
@ -132,7 +185,7 @@ const TAB = {
name: T.Network,
icon: NetworkIcon,
Content: Networking,
getError: (error) => !!error?.[TAB_ID],
getError: (error) => TAB_ID.some((id) => error?.[id]),
}
export default TAB

View File

@ -49,6 +49,9 @@ const SCHEMA = object({
NIC: array()
.ensure()
.transform((nics) => nics.map(mapNameByIndex('NIC'))),
NIC_ALIAS: array()
.ensure()
.transform((nics) => nics.map(mapNameByIndex('NIC_ALIAS'))),
}).concat(getObjectSchemaFromFields(FIELDS))
export { FIELDS, SCHEMA }

View File

@ -1,143 +0,0 @@
/* ------------------------------------------------------------------------- *
* 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 { memo, JSXElementConstructor } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { Edit, Trash } from 'iconoir-react'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import { ImageSteps, VolatileSteps } from 'client/components/Forms/Vm'
import { StatusCircle } from 'client/components/Status'
import { Translate } from 'client/components/HOC'
import { getState, getDiskType } from 'client/models/Image'
import { stringToBoolean } from 'client/models/Helper'
import { prettyBytes } from 'client/utils'
import { T } from 'client/constants'
/**
* The disk item will be included in the VM Template.
*
* @param {object} props - Props
* @param {number} props.index - Index in list
* @param {object} props.item - Disk
* @param {string} props.hypervisor - VM hypervisor
* @param {string} props.handleRemove - Remove function
* @param {string} props.handleUpdate - Update function
* @returns {JSXElementConstructor} - Disk card
*/
const DiskItem = memo(({ item, hypervisor, handleRemove, handleUpdate }) => {
const {
NAME,
TYPE,
IMAGE,
IMAGE_ID,
IMAGE_STATE,
ORIGINAL_SIZE,
SIZE = ORIGINAL_SIZE,
READONLY,
DATASTORE,
PERSISTENT,
} = item
const isVolatile = !IMAGE && !IMAGE_ID
const state = !isVolatile && getState({ STATE: IMAGE_STATE })
const type = isVolatile ? TYPE : getDiskType(item)
const originalSize = +ORIGINAL_SIZE ? prettyBytes(+ORIGINAL_SIZE, 'MB') : '-'
const size = prettyBytes(+SIZE, 'MB')
return (
<SelectCard
title={
isVolatile ? (
<>
{`${NAME} - `}
<Translate word={T.VolatileDisk} />
</>
) : (
<Stack
component="span"
direction="row"
alignItems="center"
gap="0.5em"
>
<StatusCircle color={state?.color} tooltip={state?.name} />
{`${NAME}: ${IMAGE}`}
</Stack>
)
}
subheader={
<>
{Object.entries({
[DATASTORE]: DATASTORE,
READONLY: stringToBoolean(READONLY),
PERSISTENT: stringToBoolean(PERSISTENT),
[isVolatile || ORIGINAL_SIZE === SIZE
? size
: `${originalSize}/${size}`]: true,
[type]: type,
})
.map(([k, v]) => (v ? `${k}` : ''))
.filter(Boolean)
.join(' | ')}
</>
}
action={
<>
<Action
data-cy={`remove-${NAME}`}
tooltip={<Translate word={T.Remove} />}
handleClick={handleRemove}
color="error"
icon={<Trash />}
/>
<ButtonToTriggerForm
buttonProps={{
'data-cy': `edit-${NAME}`,
icon: <Edit />,
tooltip: <Translate word={T.Edit} />,
}}
options={[
{
dialogProps: {
title: <Translate word={T.EditSomething} values={[NAME]} />,
},
form: () =>
isVolatile
? VolatileSteps({ hypervisor }, item)
: ImageSteps({ hypervisor }, item),
onSubmit: handleUpdate,
},
]}
/>
</>
}
/>
)
})
DiskItem.propTypes = {
index: PropTypes.number,
item: PropTypes.object,
hypervisor: PropTypes.string,
handleRemove: PropTypes.func,
handleUpdate: PropTypes.func,
}
DiskItem.displayName = 'DiskItem'
export default DiskItem

View File

@ -18,9 +18,12 @@ import { Stack } from '@mui/material'
import { Db as DatastoreIcon } from 'iconoir-react'
import { useFieldArray, useFormContext } from 'react-hook-form'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import { ImageSteps, VolatileSteps } from 'client/components/Forms/Vm'
import { FormWithSchema } from 'client/components/Forms'
import DiskCard from 'client/components/Cards/DiskCard'
import {
AttachAction,
DetachAction,
} from 'client/components/Tabs/Vm/Storage/Actions'
import {
STEP_ID as EXTRA_ID,
@ -32,8 +35,8 @@ import {
reorderBootAfterRemove,
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/booting'
import { FIELDS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/storage/schema'
import { getDiskName } from 'client/models/Image'
import { T } from 'client/constants'
import DiskItem from './DiskItem'
export const TAB_ID = 'DISK'
@ -70,33 +73,10 @@ const Storage = ({ hypervisor }) => {
}
return (
<>
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'add-disk',
label: T.AttachDisk,
variant: 'outlined',
}}
options={[
{
cy: 'attach-image',
name: T.Image,
dialogProps: { title: T.AttachImage, dataCy: 'modal-attach-image' },
form: () => ImageSteps({ hypervisor }),
onSubmit: (image) => append(mapNameFunction(image, disks.length)),
},
{
cy: 'attach-volatile',
name: T.Volatile,
dialogProps: {
title: T.AttachVolatile,
dataCy: 'modal-attach-volatile',
},
form: () => VolatileSteps({ hypervisor }),
onSubmit: (image) => append(mapNameFunction(image, disks.length)),
},
]}
<div>
<AttachAction
hypervisor={hypervisor}
onSubmit={(image) => append(mapNameFunction(image, disks.length))}
/>
<Stack
pb="1em"
@ -110,14 +90,30 @@ const Storage = ({ hypervisor }) => {
},
}}
>
{disks?.map(({ id, ...item }, index) => (
<DiskItem
key={id ?? item?.NAME}
item={item}
handleRemove={() => removeAndReorder(item?.NAME)}
handleUpdate={(updatedDisk) => handleUpdate(updatedDisk, index)}
/>
))}
{disks?.map(({ id, ...item }, index) => {
item.DISK_ID ??= index
return (
<DiskCard
key={id ?? item?.NAME}
disk={item}
actions={
<>
<DetachAction
disk={item}
name={getDiskName(item)}
onSubmit={() => removeAndReorder(item?.NAME)}
/>
<AttachAction
disk={item}
hypervisor={hypervisor}
onSubmit={(updatedDisk) => handleUpdate(updatedDisk, index)}
/>
</>
}
/>
)
})}
</Stack>
<FormWithSchema
cy={`${EXTRA_ID}-storage-options`}
@ -125,7 +121,7 @@ const Storage = ({ hypervisor }) => {
legend={T.StorageOptions}
id={EXTRA_ID}
/>
</>
</div>
)
}

View File

@ -13,18 +13,18 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { number } from 'yup'
import { Field } from 'client/utils'
import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
const commonValidation = number()
.positive()
.default(() => undefined)
/** @type {Field} Memory field */
const MEMORY = (hypervisor) => {
let validation = number()
.integer('Memory should be integer number')
.positive('Memory should be positive number')
.typeError('Memory must be a number')
.required('Memory field is required')
.default(() => undefined)
let validation = commonValidation.required()
if (hypervisor === HYPERVISORS.vcenter) {
validation = validation.isDivisibleBy(4)
@ -33,7 +33,7 @@ const MEMORY = (hypervisor) => {
return {
name: 'MEMORY',
label: T.Memory,
tooltip: 'Amount of RAM required for the VM.',
tooltip: T.MemoryConcept,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
validation,
@ -41,34 +41,25 @@ const MEMORY = (hypervisor) => {
}
}
/** @type {Field} Physical CPU field */
const PHYSICAL_CPU = {
name: 'CPU',
label: T.PhysicalCpu,
tooltip: `
Percentage of CPU divided by 100 required for
the Virtual Machine. Half a processor is written 0.5.`,
tooltip: T.PhysicalCpuConcept,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
validation: number()
.positive('CPU should be positive number')
.typeError('CPU must be a number')
.required('CPU field is required')
.default(() => undefined),
validation: commonValidation.required(),
grid: { md: 12 },
}
/** @type {Field} Virtual CPU field */
const VIRTUAL_CPU = {
name: 'VCPU',
label: T.VirtualCpu,
tooltip: `
Number of virtual cpus. This value is optional, the default
hypervisor behavior is used, usually one virtual CPU`,
tooltip: T.VirtualCpuConcept,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
validation: number()
.positive('Virtual CPU should be positive number')
.notRequired()
.default(() => undefined),
validation: commonValidation.notRequired(),
grid: { md: 12 },
}

View File

@ -36,7 +36,7 @@ const Content = () => {
const { view, getResourceView } = useAuth()
const { watch } = useFormContext()
const groups = useMemo(() => {
const sections = useMemo(() => {
const hypervisor = watch(`${TEMPLATE_ID}[0].TEMPLATE.HYPERVISOR`)
const dialog = getResourceView('VM-TEMPLATE')?.dialogs?.instantiate_dialog
const sectionsAvailable = getSectionsAvailable(dialog, hypervisor)
@ -46,7 +46,7 @@ const Content = () => {
return (
<div className={classes.root}>
{groups.map(({ id, legend, fields }) => (
{sections.map(({ id, legend, fields }) => (
<FormWithSchema
key={id}
className={classes[id]}

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import PropTypes from 'prop-types'
import { Trash } from 'iconoir-react'
import { Edit, Trash } from 'iconoir-react'
import {
useAttachNicMutation,
@ -28,43 +28,60 @@ import { jsonToXml } from 'client/models/Helper'
import { Tr, Translate } from 'client/components/HOC'
import { T } from 'client/constants'
const AttachAction = memo(({ vmId, currentNics }) => {
const [attachNic] = useAttachNicMutation()
const AttachAction = memo(
({ vmId, hypervisor, nic, currentNics, onSubmit, sx }) => {
const [attachNic] = useAttachNicMutation()
const handleAttachNic = async (formData) => {
const isAlias = !!formData?.PARENT?.length
const data = { [isAlias ? 'NIC_ALIAS' : 'NIC']: formData }
const handleAttachNic = async (formData) => {
if (onSubmit && typeof onSubmit === 'function') {
return await onSubmit(formData)
}
const template = jsonToXml(data)
await attachNic({ id: vmId, template })
const isAlias = !!formData?.PARENT?.length
const data = { [isAlias ? 'NIC_ALIAS' : 'NIC']: formData }
const template = jsonToXml(data)
await attachNic({ id: vmId, template })
}
return (
<ButtonToTriggerForm
buttonProps={
nic
? {
'data-cy': `edit-${nic.NIC_ID}`,
icon: <Edit />,
tooltip: Tr(T.Edit),
sx,
}
: {
color: 'secondary',
'data-cy': 'add-nic',
label: T.AttachNic,
variant: 'outlined',
sx,
}
}
options={[
{
dialogProps: { title: T.AttachNic, dataCy: 'modal-attach-nic' },
form: () => AttachNicForm({ hypervisor, nics: currentNics }, nic),
onSubmit: handleAttachNic,
},
]}
/>
)
}
)
return (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'attach-nic',
label: T.AttachNic,
variant: 'outlined',
}}
options={[
{
dialogProps: { title: T.AttachNic },
form: () => AttachNicForm({ nics: currentNics }),
onSubmit: handleAttachNic,
},
]}
/>
)
})
const DetachAction = memo(({ vmId, nic }) => {
const DetachAction = memo(({ vmId, nic, onSubmit, sx }) => {
const [detachNic] = useDetachNicMutation()
const { NIC_ID, PARENT } = nic
const isAlias = !!PARENT?.length
const handleDetach = async () => {
await detachNic({ id: vmId, nic: NIC_ID })
const handleDetachNic = onSubmit ?? detachNic
await handleDetachNic({ id: vmId, nic: NIC_ID })
}
return (
@ -73,6 +90,7 @@ const DetachAction = memo(({ vmId, nic }) => {
'data-cy': `detach-nic-${NIC_ID}`,
icon: <Trash />,
tooltip: Tr(T.Detach),
sx,
}}
options={[
{
@ -81,7 +99,7 @@ const DetachAction = memo(({ vmId, nic }) => {
title: (
<Translate
word={T.DetachSomething}
values={`${isAlias ? T.Alias : T.NIC} #${nic}`}
values={`${isAlias ? T.Alias : T.NIC} #${NIC_ID}`}
/>
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
@ -94,9 +112,12 @@ const DetachAction = memo(({ vmId, nic }) => {
})
const ActionPropTypes = {
vmId: PropTypes.string.isRequired,
currentNics: PropTypes.object,
vmId: PropTypes.string,
hypervisor: PropTypes.string,
currentNics: PropTypes.array,
nic: PropTypes.object,
onSubmit: PropTypes.func,
sx: PropTypes.object,
}
AttachAction.propTypes = ActionPropTypes

View File

@ -46,24 +46,24 @@ const { ATTACH_NIC, DETACH_NIC } = VM_ACTIONS
const VmNetworkTab = ({ tabProps: { actions } = {}, id }) => {
const { data: vm } = useGetVmQuery(id)
const [nics, actionsAvailable] = useMemo(() => {
const [nics, hypervisor, actionsAvailable] = useMemo(() => {
const groupedNics = getNics(vm, {
groupAlias: true,
securityGroupsFromTemplate: true,
})
const hypervisor = getHypervisor(vm)
const actionsByHypervisor = getActionsAvailable(actions, hypervisor)
const hyperV = getHypervisor(vm)
const actionsByHypervisor = getActionsAvailable(actions, hyperV)
const actionsByState = actionsByHypervisor.filter(
(action) => !isAvailableAction(action)(vm)
)
return [groupedNics, actionsByState]
return [groupedNics, hyperV, actionsByState]
}, [vm])
return (
<>
<div>
{actionsAvailable?.includes?.(ATTACH_NIC) && (
<AttachAction vmId={id} currentNics={nics} />
<AttachAction vmId={id} currentNics={nics} hypervisor={hypervisor} />
)}
<Stack direction="column" gap="1em" py="0.8em">
@ -75,15 +75,16 @@ const VmNetworkTab = ({ tabProps: { actions } = {}, id }) => {
<NicCard
key={key}
nic={nic}
extraActionProps={{ vmId: id }}
actions={[
actionsAvailable.includes(DETACH_NIC) && DetachAction,
].filter(Boolean)}
actions={
actionsAvailable.includes(DETACH_NIC) && (
<DetachAction nic={nic} vmId={id} />
)
}
/>
)
})}
</Stack>
</>
</div>
)
}

View File

@ -58,7 +58,7 @@ const VmSnapshotTab = ({ tabProps: { actions } = {}, id }) => {
}, [vm])
return (
<>
<div>
{actionsAvailable?.includes(SNAPSHOT_CREATE) && (
<CreateAction vmId={id} />
)}
@ -75,7 +75,7 @@ const VmSnapshotTab = ({ tabProps: { actions } = {}, id }) => {
/>
))}
</Stack>
</>
</div>
)
}

View File

@ -48,48 +48,86 @@ import { jsonToXml } from 'client/models/Helper'
import { Tr, Translate } from 'client/components/HOC'
import { T, VM_ACTIONS } from 'client/constants'
const AttachAction = memo(({ vmId, hypervisor }) => {
const AttachAction = memo(({ vmId, disk, hypervisor, onSubmit, sx }) => {
const [attachDisk] = useAttachDiskMutation()
const handleAttachDisk = async (formData) => {
if (onSubmit && typeof onSubmit === 'function') {
return await onSubmit(formData)
}
const template = jsonToXml({ DISK: formData })
await attachDisk({ id: vmId, template })
}
return (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'attach-disk',
label: T.AttachDisk,
variant: 'outlined',
}}
options={[
{
cy: 'attach-image',
name: T.Image,
dialogProps: { title: T.AttachImage },
form: () => ImageSteps({ hypervisor }),
onSubmit: handleAttachDisk,
},
{
cy: 'attach-volatile',
name: T.Volatile,
dialogProps: { title: T.AttachVolatile },
form: () => VolatileSteps({ hypervisor }),
onSubmit: handleAttachDisk,
},
]}
buttonProps={
disk
? {
'data-cy': `edit-${disk.DISK_ID}`,
icon: <Edit />,
tooltip: Tr(T.Edit),
sx,
}
: {
color: 'secondary',
'data-cy': 'add-disk',
label: T.AttachDisk,
variant: 'outlined',
sx,
}
}
options={
disk
? [
{
dialogProps: {
title: (
<Translate word={T.EditSomething} values={[disk?.NAME]} />
),
},
form: () =>
!disk?.IMAGE && !disk?.IMAGE_ID // is volatile
? VolatileSteps({ hypervisor }, disk)
: ImageSteps({ hypervisor }, disk),
onSubmit: handleAttachDisk,
},
]
: [
{
cy: 'attach-image',
name: T.Image,
dialogProps: {
title: T.AttachImage,
dataCy: 'modal-attach-image',
},
form: () => ImageSteps({ hypervisor }, disk),
onSubmit: handleAttachDisk,
},
{
cy: 'attach-volatile',
name: T.Volatile,
dialogProps: {
title: T.AttachVolatile,
dataCy: 'modal-attach-volatile',
},
form: () => VolatileSteps({ hypervisor }, disk),
onSubmit: handleAttachDisk,
},
]
}
/>
)
})
const DetachAction = memo(({ vmId, disk, name: imageName }) => {
const DetachAction = memo(({ vmId, disk, name: imageName, onSubmit, sx }) => {
const [detachDisk] = useDetachDiskMutation()
const { DISK_ID } = disk
const handleDetach = async () => {
await detachDisk({ id: vmId, disk: DISK_ID })
const handleDetachDisk = onSubmit ?? detachDisk
await handleDetachDisk({ id: vmId, disk: DISK_ID })
}
return (
@ -98,6 +136,7 @@ const DetachAction = memo(({ vmId, disk, name: imageName }) => {
'data-cy': `${VM_ACTIONS.DETACH_DISK}-${DISK_ID}`,
icon: <Trash />,
tooltip: Tr(T.Detach),
sx,
}}
options={[
{
@ -118,7 +157,7 @@ const DetachAction = memo(({ vmId, disk, name: imageName }) => {
)
})
const SaveAsAction = memo(({ vmId, disk, snapshot, name: imageName }) => {
const SaveAsAction = memo(({ vmId, disk, snapshot, name: imageName, sx }) => {
const [saveAsDisk] = useSaveAsDiskMutation()
const { DISK_ID: diskId } = disk
const { ID: snapshotId, NAME: snapshotName } = snapshot ?? {}
@ -138,6 +177,7 @@ const SaveAsAction = memo(({ vmId, disk, snapshot, name: imageName }) => {
'data-cy': `${VM_ACTIONS.DISK_SAVEAS}-${diskId}`,
icon: <SaveActionFloppy />,
tooltip: Tr(T.SaveAs),
sx,
}}
options={[
{
@ -163,7 +203,7 @@ const SaveAsAction = memo(({ vmId, disk, snapshot, name: imageName }) => {
)
})
const ResizeAction = memo(({ vmId, disk, name: imageName }) => {
const ResizeAction = memo(({ vmId, disk, name: imageName, sx }) => {
const [resizeDisk] = useResizeDiskMutation()
const { DISK_ID } = disk
@ -177,6 +217,7 @@ const ResizeAction = memo(({ vmId, disk, name: imageName }) => {
'data-cy': `${VM_ACTIONS.RESIZE_DISK}-${DISK_ID}`,
icon: <Expand />,
tooltip: Tr(T.Resize),
sx,
}}
options={[
{
@ -196,7 +237,7 @@ const ResizeAction = memo(({ vmId, disk, name: imageName }) => {
)
})
const SnapshotCreateAction = memo(({ vmId, disk, name: imageName }) => {
const SnapshotCreateAction = memo(({ vmId, disk, name: imageName, sx }) => {
const [createDiskSnapshot] = useCreateDiskSnapshotMutation()
const { DISK_ID } = disk
@ -210,6 +251,7 @@ const SnapshotCreateAction = memo(({ vmId, disk, name: imageName }) => {
'data-cy': `${VM_ACTIONS.SNAPSHOT_DISK_CREATE}-${DISK_ID}`,
icon: <Camera />,
tooltip: Tr(T.TakeSnapshot),
sx,
}}
options={[
{
@ -229,7 +271,7 @@ const SnapshotCreateAction = memo(({ vmId, disk, name: imageName }) => {
)
})
const SnapshotRenameAction = memo(({ vmId, disk, snapshot }) => {
const SnapshotRenameAction = memo(({ vmId, disk, snapshot, sx }) => {
const [renameDiskSnapshot] = useRenameDiskSnapshotMutation()
const { DISK_ID } = disk
const { ID, NAME = '' } = snapshot
@ -249,6 +291,7 @@ const SnapshotRenameAction = memo(({ vmId, disk, snapshot }) => {
'data-cy': `${VM_ACTIONS.SNAPSHOT_DISK_RENAME}-${DISK_ID}-${ID}`,
icon: <Edit />,
tooltip: Tr(T.Edit),
sx,
}}
options={[
{
@ -266,7 +309,7 @@ const SnapshotRenameAction = memo(({ vmId, disk, snapshot }) => {
)
})
const SnapshotRevertAction = memo(({ vmId, disk, snapshot }) => {
const SnapshotRevertAction = memo(({ vmId, disk, snapshot, sx }) => {
const [revertDiskSnapshot] = useRevertDiskSnapshotMutation()
const { DISK_ID } = disk
const { ID, NAME = T.Snapshot } = snapshot
@ -281,6 +324,7 @@ const SnapshotRevertAction = memo(({ vmId, disk, snapshot }) => {
'data-cy': `${VM_ACTIONS.SNAPSHOT_DISK_REVERT}-${DISK_ID}-${ID}`,
icon: <UndoAction />,
tooltip: Tr(T.Revert),
sx,
}}
options={[
{
@ -298,7 +342,7 @@ const SnapshotRevertAction = memo(({ vmId, disk, snapshot }) => {
)
})
const SnapshotDeleteAction = memo(({ vmId, disk, snapshot }) => {
const SnapshotDeleteAction = memo(({ vmId, disk, snapshot, sx }) => {
const [deleteDiskSnapshot] = useDeleteDiskSnapshotMutation()
const { DISK_ID } = disk
const { ID, NAME = T.Snapshot } = snapshot
@ -313,6 +357,7 @@ const SnapshotDeleteAction = memo(({ vmId, disk, snapshot }) => {
'data-cy': `${VM_ACTIONS.SNAPSHOT_DISK_DELETE}-${DISK_ID}-${ID}`,
icon: <Trash />,
tooltip: Tr(T.Delete),
sx,
}}
options={[
{
@ -331,11 +376,13 @@ const SnapshotDeleteAction = memo(({ vmId, disk, snapshot }) => {
})
const ActionPropTypes = {
vmId: PropTypes.string.isRequired,
vmId: PropTypes.string,
hypervisor: PropTypes.string,
disk: PropTypes.object,
snapshot: PropTypes.object,
name: PropTypes.string,
onSubmit: PropTypes.func,
sx: PropTypes.object,
}
AttachAction.propTypes = ActionPropTypes

View File

@ -18,6 +18,7 @@ import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { useGetVmQuery } from 'client/features/OneApi/vm'
import DiskCard from 'client/components/Cards/DiskCard'
import {
AttachAction,
SaveAsAction,
@ -28,13 +29,13 @@ import {
SnapshotRenameAction,
SnapshotDeleteAction,
} from 'client/components/Tabs/Vm/Storage/Actions'
import DiskCard from 'client/components/Cards/DiskCard'
import {
getDisks,
getHypervisor,
isAvailableAction,
} from 'client/models/VirtualMachine'
import { getDiskName } from 'client/models/Image'
import { getActionsAvailable } from 'client/models/Helper'
import { VM_ACTIONS } from 'client/constants'
@ -71,11 +72,8 @@ const VmStorageTab = ({ tabProps: { actions } = {}, id }) => {
return [getDisks(vm), hyperV, actionsByState]
}, [vm])
const filterByAvailable = (action, button) =>
actionsAvailable.includes(action) && button
return (
<>
<div>
{actionsAvailable?.includes?.(ATTACH_DISK) && (
<AttachAction vmId={id} hypervisor={hypervisor} />
)}
@ -83,31 +81,60 @@ const VmStorageTab = ({ tabProps: { actions } = {}, id }) => {
<Stack direction="column" gap="1em" py="0.8em">
{disks.map((disk) => {
const isImage = disk.IMAGE_ID !== undefined
const imageName = getDiskName(disk)
const diskActionProps = { vmId: id, disk, name: imageName }
return (
<DiskCard
key={disk.DISK_ID}
vmId={id}
disk={disk}
extraActionProps={{ vmId: id }}
extraSnapshotActionProps={{ disk, vmId: id }}
actions={[
isImage && filterByAvailable(DISK_SAVEAS, SaveAsAction),
filterByAvailable(SNAPSHOT_DISK_CREATE, SnapshotCreateAction),
filterByAvailable(RESIZE_DISK, ResizeAction),
filterByAvailable(DETACH_DISK, DetachAction),
].filter(Boolean)}
snapshotActions={[
isImage && filterByAvailable(DISK_SAVEAS, SaveAsAction),
filterByAvailable(SNAPSHOT_DISK_RENAME, SnapshotRenameAction),
filterByAvailable(SNAPSHOT_DISK_REVERT, SnapshotRevertAction),
filterByAvailable(SNAPSHOT_DISK_DELETE, SnapshotDeleteAction),
].filter(Boolean)}
actions={
<>
{isImage && actionsAvailable.includes(DISK_SAVEAS) && (
<SaveAsAction {...diskActionProps} />
)}
{actionsAvailable.includes(SNAPSHOT_DISK_CREATE) && (
<SnapshotCreateAction {...diskActionProps} />
)}
{actionsAvailable.includes(RESIZE_DISK) && (
<ResizeAction {...diskActionProps} />
)}
{actionsAvailable.includes(DETACH_DISK) && (
<DetachAction {...diskActionProps} />
)}
</>
}
snapshotActions={({ snapshot }) => (
<>
{isImage && actionsAvailable.includes(DISK_SAVEAS) && (
<SaveAsAction {...diskActionProps} snapshot={snapshot} />
)}
{actionsAvailable.includes(SNAPSHOT_DISK_RENAME) && (
<SnapshotRenameAction
{...diskActionProps}
snapshot={snapshot}
/>
)}
{actionsAvailable.includes(SNAPSHOT_DISK_REVERT) && (
<SnapshotRevertAction
{...diskActionProps}
snapshot={snapshot}
/>
)}
{actionsAvailable.includes(SNAPSHOT_DISK_DELETE) && (
<SnapshotDeleteAction
{...diskActionProps}
snapshot={snapshot}
/>
)}
</>
)}
/>
)
})}
</Stack>
</>
</div>
)
}

View File

@ -108,6 +108,21 @@ export const AR_TYPES = {
IP4_6_STATIC: 'IP4_6_STATIC',
}
/** @enum {string} Virtual Network Drivers */
export const VN_DRIVERS = {
dummy: 'dummy',
dot1Q: '802.1Q',
ebtables: 'ebtables',
fw: 'fw',
ovswitch: 'ovswitch',
vxlan: 'vxlan',
vcenter: 'vcenter',
ovswitch_vxlan: 'ovswitch_vxlan',
bridge: 'bridge',
elastic: 'elastic',
nodeport: 'nodeport',
}
/** @enum {string} Virtual network actions */
export const VN_ACTIONS = {
CREATE_DIALOG: 'create_dialog',

View File

@ -347,11 +347,11 @@ module.exports = {
Secondary: 'Secondary',
/* instances schema */
IP: 'IP',
DeployID: 'Deploy ID',
vCenterDeployment: 'vCenter Deployment',
Deployment: 'Deployment',
Monitoring: 'Monitoring',
EdgeCluster: 'Edge Cluster',
/* flow schema */
Strategy: 'Strategy',
@ -391,6 +391,10 @@ module.exports = {
SshConnection: 'SSH connection',
External: 'External',
ExternalConcept: 'The NIC will be attached as an external alias of the VM',
OverrideNetworkValuesIPv4: 'Override Network Values IPv4',
OverrideNetworkValuesIPv6: 'Override Network Values IPv6',
OverrideNetworkInboundTrafficQos: 'Override Network Inbound Traffic QoS',
OverrideNetworkOutboundTrafficQos: 'Override Network Outbound Traffic QoS',
/* VM Template schema */
/* VM Template schema - general */
@ -572,7 +576,7 @@ module.exports = {
Keymap: 'Keymap',
GenerateRandomPassword: 'Generate random password',
Command: 'Command',
Bus: 'Bus',
Bus: 'BUS',
/* VM Template schema - NUMA */
PinPolicy: 'Pin Policy',
PinPolicyConcept: 'Virtual CPU pinning preference: %s',
@ -590,6 +594,45 @@ module.exports = {
Number of virtual CPUs. This value is optional, the default
hypervisor behavior is used, usually one virtual CPU`,
/* Virtual Network schema - network */
IP: 'IP',
IPv4Concept: 'First IP in the range in dot notation',
IPv6Concept: 'First IP6 (full 128 bits) in the range',
MAC: 'MAC',
MACConcept: `
First MAC, if not provided it will be generated
using the IP and the MAC_PREFIX in oned.conf`,
NetworkAddress: 'Network address',
NetworkMask: 'Network mask',
Gateway: 'Gateway',
GatewayConcept: 'Default gateway for the network',
Gateway6Concept: 'IPv6 router for this network',
SearchDomainForDNSResolution: 'Search domains for DNS resolution',
NetworkMethod: 'Network method',
NetworkMethod4Concept: 'Sets IPv4 guest conf. method for NIC in this network',
NetworkMethod6Concept: 'Sets IPv6 guest conf. method for NIC in this network',
DNS: 'DNS',
DNSConcept: 'DNS servers, a space separated list of servers',
AverageBandwidth: 'Average bandwidth (KBytes/s)',
PeakBandwidth: 'Peak bandwidth (KBytes/s)',
PeakBurst: 'Peak burst (KBytes)',
InboundAverageBandwidthConcept:
'Average bitrate for the interface in kilobytes/second for inbound traffic',
InboundPeakBandwidthConcept:
'Maximum bitrate for the interface in kilobytes/second for inbound traffic',
OutboundAverageBandwidthConcept:
'Average bitrate for the interface in kilobytes/second for outbound traffic',
OutboundPeakBandwidthConcept:
'Maximum bitrate for the interface in kilobytes/second for outbound traffic',
PeakBurstConcept: 'Data that can be transmitted at peak speed in kilobytes',
Hardware: 'Hardware',
HardwareModelToEmulate: 'Hardware model to emulate',
TransmissionQueue: 'Transmission queue',
OnlySupportedForVirtioDriver: 'Only supported for virtio driver',
GuestOptions: 'Guest options',
GuestMTU: 'GuestMTU',
GuestMTUConcept: 'Sets the MTU for the NICs in this network',
/* security group schema */
TCP: 'TCP',
UDP: 'UDP',
@ -629,19 +672,53 @@ module.exports = {
DownloadAppToOpenNebula: 'Download App to OpenNebula',
ExportAppNameConcept:
'Name that the resource will get for description purposes',
ExportTemplateNameConcept:
'The following template will be created in OpenNebula and the previous images will be referenced in the disks',
ExportTemplateNameConcept: `
The following template will be created in OpenNebula
and the previous images will be referenced in the disks`,
ExportAssociateApp: 'Export associated VM templates/images',
ImportAssociateApp: 'Import associated VM templates/images',
/* Image schema */
/* Image - general */
Limit: 'Limit',
BasePath: 'Base path',
/* Image schema */
FileSystemType: 'Filesystem type',
Persistent: 'Persistent',
RunningVMs: 'Running VMs',
/* Disk - general */
DiskType: 'Disk type',
SizeOnInstantiate: 'Size on instantiate',
SizeOnInstantiateConcept: `
The size of the disk will be modified to match
this size when the template is instantiated`,
TargetDevice: 'Target device',
TargetDeviceConcept: `
Device to map image disk.
If set, it will overwrite the default device mapping`,
ReadOnly: 'Read-only',
BusAdapterController: 'Bus adapter controller',
DiskProvisioningType: 'Disk provisioning type',
Cache: 'Cache',
IoPolicy: 'IO Policy',
Discard: 'Discard',
IopsSize: 'Size of IOPS per second',
ThrottlingBytes: 'Throttling (Bytes/s)',
ThrottlingIOPS: 'Throttling (IOPS)',
TotalValue: 'Total value',
TotalMaximum: 'Total maximum',
TotalMaximumLength: 'Total maximum length',
ReadValue: 'Read value',
ReadMaximum: 'Read maximum',
ReadMaximumLength: 'Read maximum length',
WriteValue: 'Write value',
WriteMaximum: 'Write maximum',
WriteMaximumLength: 'Write maximum length',
SnapshotFrequency: 'Snapshot Frequency in seconds',
IoThreadId: 'IOTHREAD id',
IoThreadIdConcept: `
Iothread id used by this disk. Default is round robin.
Can be used only if IOTHREADS > 0. If this input is disabled
please first configure IOTHREADS value on OS & CPU -> Features`,
/* User inputs */
UserInputs: 'User Inputs',

View File

@ -73,8 +73,10 @@ const oneApi = createApi({
return { data: response.data ?? {} }
} catch (axiosError) {
const { message, data, status, statusText } = axiosError
const error = message ?? data?.message ?? statusText
const { message, data = {}, status, statusText } = axiosError
const { message: messageFromServer, data: errorFromOned } = data
const error = message ?? errorFromOned ?? messageFromServer ?? statusText
status === httpCodes.unauthorized.id
? dispatch(logout(T.SessionExpired))

View File

@ -20,6 +20,7 @@ import {
StateInfo,
Image,
} from 'client/constants'
import { prettyBytes } from 'client/utils'
/**
* Returns the image type.
@ -46,3 +47,16 @@ export const getState = ({ STATE } = {}) => IMAGE_STATES[+STATE]
*/
export const getDiskType = ({ DISK_TYPE } = {}) =>
isNaN(+DISK_TYPE) ? DISK_TYPE : DISK_TYPES[+DISK_TYPE]
/**
* Returns the disk name.
*
* @param {Image} image - Image
* @returns {string} - Disk name
*/
export const getDiskName = ({ IMAGE, SIZE, TYPE, FORMAT } = {}) => {
const size = +SIZE ? prettyBytes(+SIZE, 'MB') : '-'
const type = String(TYPE).toLowerCase()
return IMAGE ?? { fs: `${FORMAT} - ${size}`, swap: size }[type]
}

View File

@ -350,6 +350,13 @@ export default (appTheme, mode = SCHEMES.DARK) => {
},
},
},
MuiFormLabel: {
styleOverrides: {
root: {
padding: '0 2em 0 0',
},
},
},
MuiTextField: {
defaultProps: {
variant: 'outlined',

View File

@ -16,7 +16,8 @@
import DOMPurify from 'dompurify'
import { object, reach, ObjectSchema, BaseSchema } from 'yup'
import { isMergeableObject } from 'client/utils/merge'
import { HYPERVISORS } from 'client/constants'
import { Field } from 'client/utils/schema'
import { HYPERVISORS, VN_DRIVERS } from 'client/constants'
/**
* Simulate a delay in a function.
@ -198,9 +199,9 @@ export const getObjectSchemaFromFields = (fields) =>
}, object())
/**
* @param {Array} fields - Fields
* @param {Field[]} fields - Fields
* @param {HYPERVISORS} hypervisor - Hypervisor
* @returns {Array} Filtered fields
* @returns {Field[]} Filtered fields
*/
export const filterFieldsByHypervisor = (
fields,
@ -212,6 +213,18 @@ export const filterFieldsByHypervisor = (
({ notOnHypervisors } = {}) => !notOnHypervisors?.includes?.(hypervisor)
)
/**
* @param {Field[]} fields - Fields
* @param {VN_DRIVERS} driver - Driver
* @returns {Field[]} Filtered fields
*/
export const filterFieldsByDriver = (fields, driver = false) =>
fields
.map((field) => (typeof field === 'function' ? field(driver) : field))
.filter(
({ notOnDrivers } = {}) => !driver || !notOnDrivers?.includes?.(driver)
)
/**
* Filter an object list by property.
*

View File

@ -16,7 +16,7 @@
/* eslint-disable jsdoc/valid-types */
// eslint-disable-next-line no-unused-vars
import { JSXElementConstructor, SetStateAction } from 'react'
import { ReactElement, SetStateAction } from 'react'
import {
// eslint-disable-next-line no-unused-vars
GridProps,
@ -34,6 +34,10 @@ import { Row } from 'react-table'
import {
UserInputObject,
T,
// eslint-disable-next-line no-unused-vars
HYPERVISORS,
// eslint-disable-next-line no-unused-vars
VN_DRIVERS,
INPUT_TYPES,
USER_INPUT_TYPES,
} from 'client/constants'
@ -70,7 +74,7 @@ import {
/**
* @typedef {object} SelectOption - Option of select field
* @property {string|JSXElementConstructor} text - Text to display on select list
* @property {string|ReactElement} text - Text to display on select list
* @property {any} value - Value to option
*/
@ -104,12 +108,16 @@ import {
* - Default: { xs: 12, md: 6 }
* @property {BaseSchema|DependOfCallback} [validation]
* - Schema to validate the field value
* @property {HYPERVISORS[]|DependOfCallback} [notOnHypervisors]
* - Filters the field when the hypervisor is not include on list
* @property {VN_DRIVERS[]|DependOfCallback} [notOnDrivers]
* - Filters the field when the driver is not include on list
* @property {TextFieldProps|CheckboxProps|InputBaseComponentProps} [fieldProps]
* - Extra properties to material-ui field
* @property {function(string|number):any} [renderValue]
* - Render the current selected value inside selector input
* - **Only for select inputs.**
* @property {JSXElementConstructor} [Table]
* @property {ReactElement} [Table]
* - Table component. One of table defined in: `client/components/Tables`
* - **Only for table inputs.**
* @property {boolean|DependOfCallback} [singleSelect]
@ -148,7 +156,7 @@ import {
* @property {string} id - Id
* @property {string} label - Label
* @property {BaseSchema|function(object):BaseSchema} resolver - Schema
* @property {function(object, SetStateAction):JSXElementConstructor} content - Content
* @property {function(object, SetStateAction):ReactElement} content - Content
* @property {ValidateOptions|undefined} optionsValidate - Validate options
*/

View File

@ -32,6 +32,7 @@ const ALLOWED_KEYS_ONED_CONF = [
'DS_MAD_CONF',
'MARKET_MAD_CONF',
'VM_MAD',
'VN_MAD_CONF',
'IM_MAD',
'AUTH_MAD',
]