mirror of
https://github.com/OpenNebula/one.git
synced 2025-02-21 13:57:56 +03:00
parent
15c4626313
commit
5aa4b25902
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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]}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user