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

F OpenNebula/one#5422: Add information tab to vm detail

This commit is contained in:
Sergio Betanzos 2021-07-06 16:43:36 +02:00
parent d5f2a0e0a7
commit 70dd7af341
No known key found for this signature in database
GPG Key ID: E3E704F097737136
22 changed files with 607 additions and 116 deletions

View File

@ -7,7 +7,7 @@ import { useVmApi } from 'client/features/One'
import VmTabs from 'client/components/Tabs/Vm'
const VmDetail = React.memo(({ id, view = {} }) => {
const VmDetail = React.memo(({ id }) => {
const { getHooksSocket } = useSocket()
const { getVm } = useVmApi()
@ -27,21 +27,14 @@ const VmDetail = React.memo(({ id, view = {} }) => {
}
if (error) {
return <div>{error}</div>
return <div>{error || 'Error'}</div>
}
const tabs = Object.entries(view['info-tabs'] ?? {})
?.map(([tabName, { enabled } = {}]) => !!enabled && tabName)
?.filter(Boolean)
return (
<VmTabs data={data} tabs={tabs} view={view} />
)
return <VmTabs data={data} />
})
VmDetail.propTypes = {
id: PropTypes.string.isRequired,
view: PropTypes.object.isRequired
id: PropTypes.string.isRequired
}
VmDetail.displayName = 'VmDetail'

View File

@ -16,14 +16,10 @@ const INTERVAL_ON_FIRST_RENDER = 2_000
const VmsTable = () => {
const vms = useVm()
const { getVms } = useVmApi()
const { view, views, filterPool } = useAuth()
const viewSelected = React.useMemo(() => views[view], [view])
const resourceView = viewSelected?.find(({ resource_name: name }) => name === 'VM')
const { view, getResourceView, filterPool } = useAuth()
const columns = React.useMemo(() => createColumns({
filters: resourceView?.filters,
filters: getResourceView('VM')?.filters,
columns: VmColumns
}), [view])
@ -63,7 +59,7 @@ const VmsTable = () => {
isLoading={loading || reloading}
getRowId={row => String(row.ID)}
RowComponent={VmRow}
renderDetail={row => <VmDetail id={row.ID} view={resourceView} />}
renderDetail={row => <VmDetail id={row.ID} />}
/>
)
}

View File

@ -17,11 +17,7 @@ const Multiple = ({ tags, limitTags = 1 }) => {
))
return (
<div style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'end'
}}>
<>
{Tags}
{more > 0 && (
<Tooltip arrow
@ -34,7 +30,7 @@ const Multiple = ({ tags, limitTags = 1 }) => {
</span>
</Tooltip>
)}
</div>
</>
)
}

View File

@ -53,7 +53,11 @@ const Row = ({ original, value, ...props }) => {
</div>
</div>
<div className={classes.secondary}>
{!!IPS?.length && <Multiple tags={IPS.split(',')} />}
{!!IPS?.length && (
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'end' }}>
<Multiple tags={IPS.split(',')} />
</div>
)}
</div>
</div>
)

View File

@ -0,0 +1,58 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, List as MList, ListItem, Typography, Paper } from '@material-ui/core'
import { Tr } from 'client/components/HOC'
const useStyles = makeStyles(theme => ({
list: {
...theme.typography.body2,
'& > * > *': {
width: '50%'
}
},
title: {
fontWeight: theme.typography.fontWeightBold,
borderBottom: `1px solid ${theme.palette.divider}`
}
}))
const List = ({ title, list = [], ...props }) => {
const classes = useStyles()
return (
<Paper variant='outlined' {...props}>
<MList className={classes.list}>
{/* TITLE */}
{title && (
<ListItem className={classes.title}>
<Typography noWrap>{Tr(title)}</Typography>
</ListItem>
)}
{/* LIST */}
{list.map(({ key, value }, idx) => (
<ListItem key={`${key}-${idx}`}>
<Typography noWrap title={key}>{Tr(key)}</Typography>
<Typography noWrap title={value}>{value}</Typography>
</ListItem>
))}
</MList>
</Paper>
)
}
List.propTypes = {
title: PropTypes.string,
list: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
])
}))
}
List.displayName = 'List'
export default List

View File

@ -0,0 +1,51 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, List, ListItem, Typography, Paper, Divider } from '@material-ui/core'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
const useStyles = makeStyles(theme => ({
list: {
'& p': {
...theme.typography.body2,
width: '50%'
}
},
title: {
fontWeight: theme.typography.fontWeightBold
}
}))
const Ownership = React.memo(({ userName, groupName }) => {
const classes = useStyles()
return (
<Paper variant='outlined'>
<List className={classes.list}>
<ListItem className={classes.title}>
<Typography noWrap>{Tr(T.Ownership)}</Typography>
</ListItem>
<Divider />
<ListItem>
<Typography noWrap>{Tr(T.Owner)}</Typography>
<Typography noWrap>{userName}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Group)}</Typography>
<Typography>{groupName}</Typography>
</ListItem>
</List>
</Paper>
)
})
Ownership.propTypes = {
userName: PropTypes.string.isRequired,
groupName: PropTypes.string.isRequired
}
Ownership.displayName = 'Ownership'
export default Ownership

View File

@ -0,0 +1,167 @@
import React, { memo, useState } from 'react'
import PropTypes from 'prop-types'
import clsx from 'clsx'
import { List, ListItem, Typography, Grid, Paper, Divider } from '@material-ui/core'
import {
Check as CheckIcon,
Square as BlankSquareIcon,
EyeEmpty as EyeIcon
} from 'iconoir-react'
import { useProviderApi } from 'client/features/One'
import { Action } from 'client/components/Cards/SelectCard'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
import useStyles from 'client/containers/Providers/Sections/styles'
const Info = memo(({ fetchProps }) => {
const classes = useStyles()
const { getProviderConnection } = useProviderApi()
const [showConnection, setShowConnection] = useState(undefined)
const { ID, NAME, GNAME, UNAME, PERMISSIONS, TEMPLATE } = fetchProps?.data
const {
connection,
description,
provider: providerName,
registration_time: time
} = TEMPLATE?.PROVISION_BODY
const hasConnection = connection && Object.keys(connection).length > 0
const isChecked = checked =>
checked === '1' ? <CheckIcon /> : <BlankSquareIcon />
const ConnectionButton = () => (
<Action
icon={<EyeIcon />}
cy='provider-connection'
handleClick={() => getProviderConnection(ID).then(setShowConnection)}
/>
)
return (
<Grid container spacing={1}>
<Grid item xs={12} md={6}>
<Paper variant="outlined" className={classes.marginBottom}>
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Information)}</Typography>
</ListItem>
<Divider />
<ListItem>
<Typography>{'ID'}</Typography>
<Typography>{ID}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Name)}</Typography>
<Typography data-cy="provider-name">{NAME}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Description)}</Typography>
<Typography data-cy="provider-description" noWrap>{description}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Provider)}</Typography>
<Typography data-cy="provider-type">{providerName}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.RegistrationTime)}</Typography>
<Typography>
{new Date(time * 1000).toLocaleString()}
</Typography>
</ListItem>
</List>
</Paper>
{hasConnection && (
<Paper variant="outlined">
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Credentials)}</Typography>
<span className={classes.alignToRight}>
{!showConnection && <ConnectionButton />}
</span>
</ListItem>
<Divider />
{Object.entries(connection)?.map(([key, value]) =>
typeof value === 'string' && (
<ListItem key={key}>
<Typography>{key}</Typography>
<Typography data-cy={`provider-${key}`}>
{showConnection?.[key] ?? value}
</Typography>
</ListItem>
))}
</List>
</Paper>
)}
</Grid>
<Grid item xs={12} md={6}>
<Paper variant="outlined" className={classes.marginBottom}>
<List className={clsx(classes.list, 'w-25')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Permissions)}</Typography>
<Typography>{Tr(T.Use)}</Typography>
<Typography>{Tr(T.Manage)}</Typography>
<Typography>{Tr(T.Admin)}</Typography>
</ListItem>
<Divider />
<ListItem>
<Typography>{Tr(T.Owner)}</Typography>
<Typography>{isChecked(PERMISSIONS.OWNER_U)}</Typography>
<Typography>{isChecked(PERMISSIONS.OWNER_M)}</Typography>
<Typography>{isChecked(PERMISSIONS.OWNER_A)}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Group)}</Typography>
<Typography>{isChecked(PERMISSIONS.GROUP_U)}</Typography>
<Typography>{isChecked(PERMISSIONS.GROUP_M)}</Typography>
<Typography>{isChecked(PERMISSIONS.GROUP_A)}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Other)}</Typography>
<Typography>{isChecked(PERMISSIONS.OTHER_U)}</Typography>
<Typography>{isChecked(PERMISSIONS.OTHER_M)}</Typography>
<Typography>{isChecked(PERMISSIONS.OTHER_A)}</Typography>
</ListItem>
</List>
</Paper>
<Paper variant="outlined">
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Ownership)}</Typography>
</ListItem>
<Divider />
<ListItem>
<Typography>{Tr(T.Owner)}</Typography>
<Typography>{UNAME}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Group)}</Typography>
<Typography>{GNAME}</Typography>
</ListItem>
</List>
</Paper>
</Grid>
</Grid>
)
})
Info.propTypes = {
fetchProps: PropTypes.shape({
data: PropTypes.object.isRequired
}).isRequired
}
Info.defaultProps = {
fetchProps: {
data: {}
}
}
Info.displayName = 'Info'
export default Info

View File

@ -0,0 +1,112 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, List, ListItem, Typography, Paper } from '@material-ui/core'
import { Check as CheckIcon, Square as BlankSquareIcon } from 'iconoir-react'
import { useVmApi } from 'client/features/One'
import { Action } from 'client/components/Cards/SelectCard'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
import * as Helper from 'client/models/Helper'
const CATEGORIES = [
{ title: T.Owner, category: 'owner' },
{ title: T.Group, category: 'group' },
{ title: T.Other, category: 'other' }
]
const useStyles = makeStyles(theme => ({
list: {
...theme.typography.body2,
'& > * > *': {
width: '25%'
}
},
title: {
fontWeight: theme.typography.fontWeightBold,
borderBottom: `1px solid ${theme.palette.divider}`
}
}))
const Permissions = React.memo(({
id,
OWNER_U, OWNER_M, OWNER_A,
GROUP_U, GROUP_M, GROUP_A,
OTHER_U, OTHER_M, OTHER_A
}) => {
const classes = useStyles()
const { changePermissions } = useVmApi()
const [permissions, setPermissions] = React.useState(() => ({
ownerUse: OWNER_U,
ownerManage: OWNER_M,
ownerAdmin: OWNER_A,
groupUse: GROUP_U,
groupManage: GROUP_M,
groupAdmin: GROUP_A,
otherUse: OTHER_U,
otherManage: OTHER_M,
otherAdmin: OTHER_A
}))
const handleChange = async (name, value) => {
const newPermission = { [name]: Helper.stringToBoolean(value) ? '0' : '1' }
const response = await changePermissions(id, newPermission)
String(response?.data) === String(id) &&
setPermissions(prev => ({ ...prev, ...newPermission }))
}
return (
<Paper variant='outlined'>
<List className={classes.list}>
<ListItem className={classes.title}>
<Typography noWrap>{Tr(T.Permissions)}</Typography>
<Typography noWrap>{Tr(T.Use)}</Typography>
<Typography noWrap>{Tr(T.Manage)}</Typography>
<Typography noWrap>{Tr(T.Admin)}</Typography>
</ListItem>
{CATEGORIES.map(({ title, category }) => (
<ListItem key={category}>
{/* TITLE */}
<Typography noWrap>{Tr(title)}</Typography>
{/* PERMISSIONS */}
{Object.entries(permissions)
.filter(([key, _]) => key.toLowerCase().includes(category))
.map(([key, permission]) => (
<span key={key}>
<Action
cy={`permission-${key}`}
disabled={permission === undefined}
icon={+permission ? <CheckIcon /> : <BlankSquareIcon />}
handleClick={() => handleChange(key, permission)}
/>
</span>
))
}
</ListItem>
))}
</List>
</Paper>
)
})
Permissions.propTypes = {
id: PropTypes.string.isRequired,
OWNER_U: PropTypes.string.isRequired,
OWNER_M: PropTypes.string.isRequired,
OWNER_A: PropTypes.string.isRequired,
GROUP_U: PropTypes.string.isRequired,
GROUP_M: PropTypes.string.isRequired,
GROUP_A: PropTypes.string.isRequired,
OTHER_U: PropTypes.string.isRequired,
OTHER_M: PropTypes.string.isRequired,
OTHER_A: PropTypes.string.isRequired
}
Permissions.displayName = 'Permissions'
export default Permissions

View File

@ -0,0 +1,9 @@
import List from 'client/components/Tabs/Common/List'
import Permissions from 'client/components/Tabs/Common/Permissions'
import Ownership from 'client/components/Tabs/Common/Ownership'
export {
List,
Permissions,
Ownership
}

View File

@ -1,50 +1,61 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import loadable from '@loadable/component'
import { useAuth } from 'client/features/Auth'
import Tabs from 'client/components/Tabs'
import { stringToCamelCase, stringToCamelSpace } from 'client/utils'
const stringToCamelCase = s =>
s.replace(
/([-_][a-z])/ig,
$1 => $1.toUpperCase()
.replace('-', '')
.replace('_', '')
)
const Capacity = loadable(() => import('client/components/Tabs/Vm/capacity'))
const Configuration = loadable(() => import('client/components/Tabs/Vm/configuration'))
const Info = loadable(() => import('client/components/Tabs/Vm/info'))
const Log = loadable(() => import('client/components/Tabs/Vm/log'))
const Network = loadable(() => import('client/components/Tabs/Vm/network'))
const Placement = loadable(() => import('client/components/Tabs/Vm/placement'))
const SchedActions = loadable(() => import('client/components/Tabs/Vm/schedActions'))
const Snapshot = loadable(() => import('client/components/Tabs/Vm/snapshot'))
const Storage = loadable(() => import('client/components/Tabs/Vm/storage'))
const stringToCamelSpace = s => s.replace(/([a-z])([A-Z])/g, '$1 $2')
const loadTab = tabName => ({
capacity: Capacity,
configuration: Configuration,
info: Info,
log: Log,
network: Network,
placement: Placement,
schedActions: SchedActions,
snapshot: Snapshot,
storage: Storage
}[tabName])
const VmTabs = ({ data, tabs }) => {
const [renderTabs, setTabs] = React.useState(() => [])
const VmTabs = ({ data }) => {
const [tabsAvailable, setTabs] = React.useState(() => [])
const { view, getResourceView } = useAuth()
React.useEffect(() => {
const loadTab = async tabKey => {
try {
const camelCaseKey = stringToCamelCase(tabKey)
const infoTabs = getResourceView('VM')?.['info-tabs'] ?? {}
// dynamic import => client/components/Tabs/Vm
const tabComponent = await import(`./${camelCaseKey}`)
const tabs = Object.entries(infoTabs)
?.map(([tabName, { enabled } = {}]) => !!enabled && tabName)
?.filter(Boolean)
setTabs(prev => prev.concat([{
name: stringToCamelSpace(camelCaseKey),
renderContent: tabComponent.default(data)
}]))
} catch (error) {}
}
setTabs(() => tabs.map(tabName => {
const nameSanitize = stringToCamelCase(tabName)
const TabContent = loadTab(nameSanitize)
// reset
setTabs([])
return TabContent && {
name: stringToCamelSpace(nameSanitize),
renderContent: props => TabContent.render({ ...props })
}
}).filter(Boolean))
}, [view])
tabs?.forEach(loadTab)
}, [tabs?.length])
return <Tabs tabs={renderTabs} />
return <Tabs tabs={tabsAvailable} data={data} />
}
VmTabs.propTypes = {
data: PropTypes.object.isRequired,
tabs: PropTypes.arrayOf(
PropTypes.string
).isRequired
data: PropTypes.object.isRequired
}
VmTabs.displayName = 'VmTabs'

View File

@ -1,11 +1,15 @@
import * as React from 'react'
import { StatusBadge } from 'client/components/Status'
import { StatusChip } from 'client/components/Status'
import { List, Permissions, Ownership } from 'client/components/Tabs/Common'
import Multiple from 'client/components/Tables/Vms/multiple'
import * as VirtualMachine from 'client/models/VirtualMachine'
import * as Helper from 'client/models/Helper'
import { T } from 'client/constants'
const VmInfoTab = data => {
const { ID, NAME, UNAME, GNAME, RESCHED, STIME, ETIME, LOCK, DEPLOY_ID } = data
const { ID, NAME, UNAME, GNAME, RESCHED, STIME, ETIME, LOCK, DEPLOY_ID, PERMISSIONS } = data
const { name: stateName, color: stateColor } = VirtualMachine.getState(data)
@ -14,30 +18,57 @@ const VmInfoTab = data => {
const ips = VirtualMachine.getIps(data)
const info = [
{ key: [T.ID], value: ID },
{ key: [T.Name], value: NAME },
{
key: [T.State],
value: <StatusChip text={stateName} stateColor={stateColor} />
},
{
key: [T.Reschedule],
value: Helper.booleanToString(+RESCHED)
},
{
key: [T.Locked],
value: Helper.levelLockToString(LOCK?.LOCKED)
},
{
key: [T.IP],
value: ips?.length ? <Multiple tags={ips} /> : '--'
},
{
key: [T.StartTime],
value: Helper.timeToString(STIME)
},
{
key: [T.EndTime],
value: Helper.timeToString(ETIME)
},
{
key: [T.Host],
value: hostId ? `#${hostId} ${hostname}` : ''
},
{
key: [T.Cluster],
value: clusterId ? `#${clusterId} ${clusterName}` : ''
},
{
key: [T.DeployID],
value: DEPLOY_ID
}
]
return (
<div>
<p>
<StatusBadge
title={stateName}
stateColor={stateColor}
customTransform='translate(150%, 50%)'
/>
<span style={{ marginLeft: 20 }}>
{`#${ID} - ${NAME}`}
</span>
</p>
<div>
<p>Owner: {UNAME}</p>
<p>Group: {GNAME}</p>
<p>Reschedule: {Helper.booleanToString(+RESCHED)}</p>
<p>Locked: {Helper.levelLockToString(LOCK?.LOCKED)}</p>
<p>IP: {ips.join(', ') || '--'}</p>
<p>Start time: {Helper.timeToString(STIME)}</p>
<p>End time: {Helper.timeToString(ETIME)}</p>
<p>Host: {hostId ? `#${hostId} ${hostname}` : ''}</p>
<p>Cluster: {clusterId ? `#${clusterId} ${clusterName}` : ''}</p>
<p>Deploy ID: {DEPLOY_ID}</p>
</div>
<div style={{
display: 'grid',
gap: '1em',
gridTemplateColumns: 'repeat(auto-fit, minmax(480px, 1fr))',
padding: '1em'
}}>
<List title={T.Information} list={info} style={{ gridRow: 'span 3' }} />
<Permissions id={ID} {...PERMISSIONS} />
<Ownership userName={UNAME} groupName={GNAME} />
</div>
)
}

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import { Tabs as MTabs, Tab as MTab } from '@material-ui/core'
const Content = ({ name, renderContent: Content, hidden }) => (
const Content = ({ name, renderContent: Content, hidden, data }) => (
<div key={`tab-${name}`}
style={{
padding: 2,
@ -12,17 +12,17 @@ const Content = ({ name, renderContent: Content, hidden }) => (
display: hidden ? 'none' : 'block'
}}
>
{typeof Content === 'function' ? <Content /> : Content}
{typeof Content === 'function' ? <Content {...data} /> : Content}
</div>
)
const Tabs = ({ tabs = [], renderHiddenTabs = false }) => {
const Tabs = ({ tabs = [], renderHiddenTabs = false, data }) => {
const [tabSelected, setTab] = useState(0)
const renderTabs = useMemo(() => (
<MTabs
value={tabSelected}
variant="scrollable"
variant='scrollable'
scrollButtons='auto'
onChange={(_, tab) => setTab(tab)}
>
@ -52,7 +52,7 @@ const Tabs = ({ tabs = [], renderHiddenTabs = false }) => {
{renderHiddenTabs ? (
renderAllHiddenTabContents
) : (
<Content {...tabs.find(({ value }, idx) => (value ?? idx) === tabSelected)} />
<Content data={data} {...tabs.find(({ value }, idx) => (value ?? idx) === tabSelected)} />
)}
</>
)
@ -61,13 +61,20 @@ const Tabs = ({ tabs = [], renderHiddenTabs = false }) => {
Tabs.displayName = 'Tabs'
Content.displayName = 'Content'
Tabs.propTypes = {
tabs: PropTypes.array,
renderHiddenTabs: PropTypes.bool,
data: PropTypes.object
}
Content.propTypes = {
name: PropTypes.string,
renderContent: PropTypes.oneOfType([
PropTypes.object,
PropTypes.func
]),
hidden: PropTypes.bool
hidden: PropTypes.bool,
data: PropTypes.object
}
export default Tabs

View File

@ -145,11 +145,19 @@ module.exports = {
Information: 'Information',
/* general schema */
ID: 'ID',
Name: 'Name',
State: 'State',
Description: 'Description',
RegistrationTime: 'Registration time',
StartTime: 'Start time',
EndTime: 'End time',
Locked: 'Locked',
/* instances schema */
IP: 'IP',
Reschedule: 'Reschedule',
DeployID: 'Deploy ID',
/* flow schema */
Strategy: 'Strategy',

View File

@ -9,7 +9,7 @@ export const useAuth = () => {
const auth = useSelector(state => state.auth, shallowEqual)
const groups = useSelector(state => state.one.groups, shallowEqual)
const { user, jwt } = auth
const { user, jwt, view, views } = auth
const userGroups = [user?.GROUPS?.ID]
.flat()
@ -18,7 +18,10 @@ export const useAuth = () => {
const isLogged = !!jwt && !!userGroups?.length
return { ...auth, groups: userGroups, isLogged }
const getResourceView = resourceName => views?.[view]
?.find(({ resource_name: name }) => name === resourceName)
return { ...auth, groups: userGroups, isLogged, getResourceView }
}
export const useAuthApi = () => {

View File

@ -23,7 +23,7 @@ export const createAction = (type, service, wrapResult) =>
status === httpCodes.unauthorized.id && dispatch(logout(T.SessionExpired))
return rejectWithValue(message, data?.message ?? statusText)
return rejectWithValue(message ?? data?.data ?? data?.message ?? statusText)
}
}, {
condition: (_, { getState }) => !getState().one.requests[type]

View File

@ -15,7 +15,7 @@ export const getVms = createAction(
)
export const terminateVm = createAction(
'provider/delete',
'vm/delete',
payload => vmService.actionVm({
...payload,
action: {
@ -24,3 +24,5 @@ export const terminateVm = createAction(
}
})
)
export const changePermissions = createAction('vm/chmod', vmService.changePermissions)

View File

@ -19,6 +19,7 @@ export const useVmApi = () => {
return {
getVm: id => unwrapDispatch(actions.getVm({ id })),
getVms: options => unwrapDispatch(actions.getVms(options)),
terminateVm: id => unwrapDispatch(actions.terminateVm({ id }))
terminateVm: id => unwrapDispatch(actions.terminateVm({ id })),
changePermissions: (id, data) => unwrapDispatch(actions.changePermissions({ id, data }))
}
}

View File

@ -22,17 +22,30 @@ export const vmService = ({
const command = { name, ...Commands[name] }
return poolRequest(data, command, 'VM')
},
actionVm: async ({ action, id }) => {
actionVm: async ({ id, action }) => {
const name = Actions.VM_ACTION
const { url, options } = requestParams(
{ action, id },
{ id, action },
{ name, ...Commands[name] }
)
const res = await RestClient.put(url, options)
const res = await RestClient.put(url, options?.data)
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data?.VM ?? {}
},
changePermissions: async ({ id, data }) => {
const name = Actions.VM_CHMOD
const { url, options } = requestParams(
{ id, ...data },
{ name, ...Commands[name] }
)
const res = await RestClient.put(url, options?.data)
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
return res
}
})

View File

@ -18,3 +18,23 @@ export const levelLockToString = level => ({
3: 'Admin',
4: 'All'
}[level] || '-')
export const permissionsToOctal = permissions => {
const {
OWNER_U, OWNER_M, OWNER_A,
GROUP_U, GROUP_M, GROUP_A,
OTHER_U, OTHER_M, OTHER_A
} = permissions
const getCategoryValue = ([u, m, a]) => (
(stringToBoolean(u) ? 4 : 0) +
(stringToBoolean(m) ? 2 : 0) +
(stringToBoolean(a) ? 1 : 0)
)
return [
[OWNER_U, OWNER_M, OWNER_A],
[GROUP_U, GROUP_M, GROUP_A],
[OTHER_U, OTHER_M, OTHER_A]
].map(getCategoryValue).join('')
}

View File

@ -7,12 +7,14 @@ export const getAllocatedInfo = ({ HOST_SHARE = {} } = {}) => {
const { CPU_USAGE, TOTAL_CPU, MEM_USAGE, TOTAL_MEM } = HOST_SHARE
const percentCpuUsed = +CPU_USAGE * 100 / +TOTAL_CPU || 0
const percentCpuLabel = `${CPU_USAGE} / ${TOTAL_CPU} (${Math.round(percentCpuUsed)}%)`
const percentCpuLabel = `${CPU_USAGE} / ${TOTAL_CPU}
(${Math.round(isFinite(percentCpuUsed) ? percentCpuUsed : '--')}%)`
const percentMemUsed = +MEM_USAGE * 100 / +TOTAL_MEM || 0
const usedMemBytes = prettyBytes(+MEM_USAGE)
const totalMemBytes = prettyBytes(+TOTAL_MEM)
const percentMemLabel = `${usedMemBytes} / ${totalMemBytes} (${Math.round(percentMemUsed)}%)`
const percentMemLabel = `${usedMemBytes} / ${totalMemBytes}
(${Math.round(isFinite(percentMemUsed) ? percentMemUsed : '--')}%)`
return {
percentCpuUsed,

View File

@ -43,6 +43,13 @@ export const addOpacityToColor = (color, opacity) => {
export const capitalize = ([firstLetter, ...restOfWord]) =>
firstLetter.toUpperCase() + restOfWord.join('')
export const stringToCamelCase = s => s.replace(
/([-_\s][a-z])/ig,
$1 => $1.toUpperCase().replace(/[-_\s]/g, '')
)
export const stringToCamelSpace = s => s.replace(/([a-z])([A-Z])/g, '$1 $2')
export const getValidationFromFields = fields =>
fields.reduce(
(schema, field) => ({

View File

@ -155,7 +155,7 @@ module.exports = {
from: postBody,
default: 0
},
livemigration: {
liveMigration: {
from: postBody,
default: false
},
@ -357,39 +357,39 @@ module.exports = {
from: resource,
default: 0
},
user_use: {
ownerUse: {
from: postBody,
default: -1
},
user_manage: {
ownerManage: {
from: postBody,
default: -1
},
user_admin: {
ownerAdmin: {
from: postBody,
default: -1
},
group_use: {
groupUse: {
from: postBody,
default: -1
},
group_manage: {
groupManage: {
from: postBody,
default: -1
},
group_admin: {
groupAdmin: {
from: postBody,
default: -1
},
other_use: {
otherUse: {
from: postBody,
default: -1
},
other_manage: {
otherManage: {
from: postBody,
default: -1
},
other_admin: {
otherAdmin: {
from: postBody,
default: -1
}
@ -601,7 +601,7 @@ module.exports = {
from: query,
default: -2
},
filterbykey: {
filterByKey: {
from: query,
default: ''
}
@ -669,19 +669,19 @@ module.exports = {
from: query,
default: -2
},
start_month: {
startMonth: {
filter: query,
default: -1
},
start_year: {
startYear: {
filter: query,
default: -1
},
end_month: {
endMonth: {
filter: query,
default: -1
},
end_year: {
endYear: {
filter: query,
default: -1
}
@ -691,19 +691,19 @@ module.exports = {
// inspected
httpMethod: GET,
params: {
start_month: {
startMonth: {
filter: query,
default: -1
},
start_year: {
startYear: {
filter: query,
default: -1
},
end_month: {
endMonth: {
filter: query,
default: -1
},
end_year: {
endYear: {
filter: query,
default: -1
}