1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-02-21 13:57:56 +03:00

F #5755: Implement overcommitment in FSunstone host tab (#2606)

This commit is contained in:
Jorge Miguel Lobo Escalona 2023-05-16 19:41:46 +02:00 committed by GitHub
parent 15c4626313
commit 5aa4b25902
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 231 additions and 35 deletions

View File

@ -13,15 +13,15 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo, useMemo, useState, createRef } from 'react'
import PropTypes from 'prop-types'
import { createRef, memo, useMemo, useState } from 'react'
import { Link as RouterLink } from 'react-router-dom'
import { Typography, Link, Stack } from '@mui/material'
import { InputAdornment, Link, Stack, Typography } from '@mui/material'
import { useDialog } from 'client/hooks'
import { DialogConfirmation } from 'client/components/Dialogs'
import { Actions, Inputs } from 'client/components/Tabs/Common/Attribute'
import { useDialog } from 'client/hooks'
import { Translate } from 'client/components/HOC'
import { T } from 'client/constants'
@ -30,7 +30,13 @@ const Column = (props) => (
<Stack
direction="row"
alignItems="center"
sx={{ '&:hover > .actions': { display: 'contents' } }}
sx={{
'&:hover > .actions': { display: 'contents' },
'&': { overflow: 'visible !important' },
'& .slider > span[data-index="0"][aria-hidden="true"]': {
left: '0px !important',
},
}}
{...props}
/>
)
@ -56,6 +62,12 @@ const Attribute = memo(
value,
valueInOptionList,
dataCy,
min,
max,
currentValue,
unit,
unitParser = false,
title = '',
}) => {
const numberOfParents = useMemo(() => path.split('.').length - 1, [path])
@ -126,6 +138,26 @@ const Attribute = memo(
ref={inputRef}
options={options}
/>
) : min && max ? (
<Inputs.SliderInput
name={name}
initialValue={currentValue}
ref={inputRef}
min={+min}
max={+max}
unitParser={unitParser}
{...(unit
? {
InputProps: {
endAdornment: (
<InputAdornment position="end">
{unit}
</InputAdornment>
),
},
}
: {})}
/>
) : (
<Inputs.Text name={name} initialValue={value} ref={inputRef} />
)}
@ -154,6 +186,7 @@ const Attribute = memo(
{value && canCopy && <Actions.Copy name={name} value={value} />}
{(value || numberOfParents > 0) && canEdit && (
<Actions.Edit
title={title || name}
name={name}
handleClick={handleActiveEditForm}
/>
@ -201,6 +234,12 @@ export const AttributePropTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
valueInOptionList: PropTypes.string,
dataCy: PropTypes.string,
min: PropTypes.string,
max: PropTypes.string,
currentValue: PropTypes.string,
unit: PropTypes.string,
unitParser: PropTypes.bool,
title: PropTypes.string,
}
Attribute.propTypes = AttributePropTypes

View File

@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Grid, Slider, TextField } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { prettyBytes } from 'client/utils'
import PropTypes from 'prop-types'
import {
forwardRef,
useState,
ForwardedRef,
JSXElementConstructor,
forwardRef,
useState,
} from 'react'
import PropTypes from 'prop-types'
import { TextField } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { Actions } from 'client/components/Tabs/Common/Attribute'
@ -45,6 +45,18 @@ const useStyles = makeStyles({
},
})
const useStylesInput = makeStyles((theme) => ({
input: (color) => {
const styles = {}
const backgroundColor = theme?.palette?.[color.inputColor]?.[100]
if (backgroundColor) {
styles.backgroundColor = backgroundColor
}
return styles
},
}))
const Select = forwardRef(
/**
* @param {InputProps} props - Props
@ -105,6 +117,71 @@ const Text = forwardRef(
}
)
const SliderInput = forwardRef(
/**
* @param {InputProps} props - Props
* @param {ForwardedRef} ref - Forward reference
* @returns {JSXElementConstructor} Text field
*/
({ name = '', initialValue = '', min, max, unitParser, ...props }, ref) => {
const [newValue, setNewValue] = useState(() => +initialValue)
const [inputColor, setInputColor] = useState()
const handleChange = (event) => {
const targetValue = +event.target.value
setNewValue(targetValue < min ? min : targetValue)
setInputColor(
targetValue > +initialValue
? 'success'
: targetValue < +initialValue
? 'error'
: ''
)
}
const classes = useStylesInput({ inputColor })
return (
<Grid container>
<Grid item xs={12}>
<Slider
className="slider"
color="secondary"
onChange={handleChange}
value={newValue}
marks={[
{
value: 0,
label: unitParser ? prettyBytes(0) : '0',
},
{
value: max,
label: unitParser ? prettyBytes(max) : max,
},
]}
min={min}
max={max}
{...props}
/>
</Grid>
<Grid item xs={12}>
<TextField
inputProps={{
'data-cy': Actions.getAttributeCy('text', name),
className: classes.input,
}}
type="number"
inputRef={ref}
onChange={handleChange}
value={newValue}
name={name}
{...props}
/>
</Grid>
</Grid>
)
}
)
const InputPropTypes = {
name: PropTypes.string,
initialValue: PropTypes.string,
@ -116,9 +193,26 @@ const InputPropTypes = {
),
}
const InputSlidePropTypes = {
name: PropTypes.string,
initialValue: PropTypes.string,
options: PropTypes.arrayOf(
PropTypes.shape({
text: PropTypes.string.isRequired,
value: PropTypes.string,
})
),
min: PropTypes.number,
max: PropTypes.number,
unitParser: PropTypes.bool,
inputProps: PropTypes.object,
}
Select.displayName = 'Select'
Select.propTypes = InputPropTypes
Text.displayName = 'Text'
Text.propTypes = InputPropTypes
SliderInput.displayName = 'SliderInput'
SliderInput.propTypes = InputSlidePropTypes
export { Select, Text, InputPropTypes }
export { InputPropTypes, Select, SliderInput, Text }

View File

@ -13,25 +13,29 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { ReactElement } from 'react'
import { generatePath } from 'react-router'
import { useRenameHostMutation } from 'client/features/OneApi/host'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
import { StatusChip, LinearProgressWithLabel } from 'client/components/Status'
import { LinearProgressWithLabel, StatusChip } from 'client/components/Status'
import { List } from 'client/components/Tabs/Common'
import { getState, getDatastores, getAllocatedInfo } from 'client/models/Host'
import { getCapacityInfo } from 'client/models/Datastore'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
import {
useRenameHostMutation,
useUpdateHostMutation,
} from 'client/features/OneApi/host'
import { PATH } from 'client/apps/sunstone/routesOne'
import {
DS_THRESHOLD,
HOST_THRESHOLD,
Host,
T,
VM_ACTIONS,
Host,
HOST_THRESHOLD,
DS_THRESHOLD,
} from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
import { getCapacityInfo } from 'client/models/Datastore'
import { jsonToXml } from 'client/models/Helper'
import { getAllocatedInfo, getDatastores, getState } from 'client/models/Host'
/**
* Renders mainly information tab.
@ -43,17 +47,46 @@ import { PATH } from 'client/apps/sunstone/routesOne'
*/
const InformationPanel = ({ host = {}, actions }) => {
const [renameHost] = useRenameHostMutation()
const [updateHost] = useUpdateHostMutation()
const { data: datastores = [] } = useGetDatastoresQuery()
const { ID, NAME, IM_MAD, VM_MAD, CLUSTER_ID, CLUSTER } = host
const { name: stateName, color: stateColor } = getState(host)
const { percentCpuUsed, percentCpuLabel, percentMemUsed, percentMemLabel } =
getAllocatedInfo(host)
const {
percentCpuUsed,
percentCpuLabel,
percentMemUsed,
percentMemLabel,
maxCpu,
maxMem,
totalCpu,
totalMem,
} = getAllocatedInfo(host)
const handleRename = async (_, newName) => {
await renameHost({ id: ID, name: newName })
}
const handleOvercommitment = async (name, value) => {
let newTemplate
if (/memory/i.test(name)) {
newTemplate = {
RESERVED_MEM: value !== totalMem ? totalMem - value : '',
}
}
if (/cpu/i.test(name)) {
newTemplate = {
RESERVED_CPU: value !== totalCpu ? totalCpu - value : '',
}
}
newTemplate &&
(await updateHost({
id: ID,
template: jsonToXml(newTemplate),
replace: 1,
}))
}
const info = [
{ name: T.ID, value: ID, dataCy: 'id' },
{
@ -84,6 +117,8 @@ const InformationPanel = ({ host = {}, actions }) => {
const capacity = [
{
name: T.AllocatedCpu,
handleEdit: handleOvercommitment,
canEdit: true,
value: (
<LinearProgressWithLabel
value={percentCpuUsed}
@ -92,9 +127,15 @@ const InformationPanel = ({ host = {}, actions }) => {
low={HOST_THRESHOLD.CPU.low}
/>
),
min: '0',
max: `${totalCpu * 2}`,
currentValue: maxCpu,
title: T.Overcommitment,
},
{
name: T.AllocatedMemory,
handleEdit: handleOvercommitment,
canEdit: true,
value: (
<LinearProgressWithLabel
value={percentMemUsed}
@ -103,6 +144,12 @@ const InformationPanel = ({ host = {}, actions }) => {
low={HOST_THRESHOLD.MEMORY.low}
/>
),
min: '0',
max: `${totalMem * 2}`,
currentValue: maxMem,
unit: 'KB',
unitParser: true,
title: T.Overcommitment,
},
]

View File

@ -35,6 +35,17 @@ export const SERVER_CONFIG = (() => {
return config
})()
export const UNITS = {
KB: 'KB',
MB: 'MB',
GB: 'GB',
TB: 'TB',
PB: 'PB',
EB: 'EB',
ZB: 'ZB',
YB: 'YB',
}
// should be equal to the apps in src/server/utils/constants/defaults.js
export const _APPS = { sunstone: 'sunstone', provision: 'provision' }
export const APPS = Object.keys(_APPS)

View File

@ -53,16 +53,17 @@ export const getDatastores = (host) =>
* }} Allocated information object
*/
export const getAllocatedInfo = (host) => {
const { CPU_USAGE, TOTAL_CPU, MEM_USAGE, TOTAL_MEM } = host?.HOST_SHARE ?? {}
const { CPU_USAGE, TOTAL_CPU, MEM_USAGE, TOTAL_MEM, MAX_MEM, MAX_CPU } =
host?.HOST_SHARE ?? {}
const percentCpuUsed = (+CPU_USAGE * 100) / +TOTAL_CPU || 0
const percentCpuLabel = `${CPU_USAGE} / ${TOTAL_CPU}
const percentCpuUsed = (+CPU_USAGE * 100) / +MAX_CPU || 0
const percentCpuLabel = `${CPU_USAGE} / ${MAX_CPU}
(${Math.round(isFinite(percentCpuUsed) ? percentCpuUsed : '--')}%)`
const isMemUsageNegative = +MEM_USAGE < 0
const percentMemUsed = (+MEM_USAGE * 100) / +TOTAL_MEM || 0
const percentMemUsed = (+MEM_USAGE * 100) / +MAX_MEM || 0
const usedMemBytes = prettyBytes(Math.abs(+MEM_USAGE))
const totalMemBytes = prettyBytes(+TOTAL_MEM)
const totalMemBytes = prettyBytes(+MAX_MEM)
const percentMemLabel = `${
isMemUsageNegative ? '-' : ''
}${usedMemBytes} / ${totalMemBytes}
@ -73,6 +74,10 @@ export const getAllocatedInfo = (host) => {
percentCpuLabel,
percentMemUsed,
percentMemLabel,
totalCpu: TOTAL_CPU,
totalMem: TOTAL_MEM,
maxCpu: MAX_CPU,
maxMem: MAX_MEM,
}
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { HYPERVISORS, VN_DRIVERS } from 'client/constants'
import { HYPERVISORS, UNITS, VN_DRIVERS } from 'client/constants'
import { isMergeableObject } from 'client/utils/merge'
import { Field } from 'client/utils/schema'
import DOMPurify from 'dompurify'
@ -125,20 +125,20 @@ export const downloadFile = (file) => {
* - Number of digits after the decimal point. Must be in the range 0 - 20, inclusive
* @returns {string} Returns an string displaying sizes for humans.
*/
export const prettyBytes = (value, unit = 'KB', fractionDigits = 0) => {
const UNITS = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
export const prettyBytes = (value, unit = UNITS.KB, fractionDigits = 0) => {
const units = Object.values(UNITS)
let ensuredValue = +value
if (Math.abs(ensuredValue) === 0) return `${value} ${UNITS[0]}`
if (Math.abs(ensuredValue) === 0) return `${value} ${units[0]}`
let idxUnit = UNITS.indexOf(unit)
let idxUnit = units.indexOf(unit)
while (ensuredValue > 1024) {
ensuredValue /= 1024
idxUnit += 1
}
return `${ensuredValue.toFixed(fractionDigits)} ${UNITS[idxUnit]}`
return `${ensuredValue.toFixed(fractionDigits)} ${units[idxUnit]}`
}
/**