mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-22 18:50:08 +03:00
(cherry picked from commit d078518b15ba499527afea83e9f9b65f4e062fce)
This commit is contained in:
parent
80fc9b2566
commit
941d4f33ae
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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),
|
||||
},
|
||||
]
|
||||
|
@ -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,
|
||||
|
@ -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 }
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 }
|
||||
|
@ -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,
|
||||
|
@ -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)))
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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' }}
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 },
|
||||
}
|
||||
|
||||
|
@ -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]}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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))
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -350,6 +350,13 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiFormLabel: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: '0 2em 0 0',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
defaultProps: {
|
||||
variant: 'outlined',
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -32,6 +32,7 @@ const ALLOWED_KEYS_ONED_CONF = [
|
||||
'DS_MAD_CONF',
|
||||
'MARKET_MAD_CONF',
|
||||
'VM_MAD',
|
||||
'VN_MAD_CONF',
|
||||
'IM_MAD',
|
||||
'AUTH_MAD',
|
||||
]
|
||||
|
Loading…
x
Reference in New Issue
Block a user