feat(dashboard): allow the user in self service to see its quotas (#2268)

Fixes #1538
This commit is contained in:
badrAZ 2018-01-29 10:44:43 +01:00 committed by Pierre Donias
parent c89c7dab60
commit add10ea556
4 changed files with 257 additions and 166 deletions

View File

@ -904,12 +904,22 @@ const messages = {
hostPanel: 'Host{hosts, plural, one {} other {s}}', hostPanel: 'Host{hosts, plural, one {} other {s}}',
vmPanel: 'VM{vms, plural, one {} other {s}}', vmPanel: 'VM{vms, plural, one {} other {s}}',
memoryStatePanel: 'RAM Usage:', memoryStatePanel: 'RAM Usage:',
usedMemory: 'Used Memory',
totalMemory: 'Total Memory',
totalCpus: 'CPUs Total',
usedVCpus: 'Used vCPUs',
usedSpace: 'Used Space',
totalSpace: 'Total Space',
cpuStatePanel: 'CPUs Usage', cpuStatePanel: 'CPUs Usage',
vmStatePanel: 'VMs Power state', vmStatePanel: 'VMs Power state',
vmStateHalted: 'Halted',
vmStateOther: 'Other',
vmStateRunning: 'Running',
taskStatePanel: 'Pending tasks', taskStatePanel: 'Pending tasks',
usersStatePanel: 'Users', usersStatePanel: 'Users',
srStatePanel: 'Storage state', srStatePanel: 'Storage state',
ofUsage: '{usage} (of {total})', ofUsage: '{usage} (of {total})',
ofCpusUsage: '{nVcpus, number} vCPU{nVcpus, plural, one {} other {s}} (of {nCpus, number} CPU{nCpus, plural, one {} other {s}})',
noSrs: 'No storage', noSrs: 'No storage',
srName: 'Name', srName: 'Name',
srPool: 'Pool', srPool: 'Pool',
@ -921,6 +931,7 @@ const messages = {
srFree: 'free', srFree: 'free',
srUsageStatePanel: 'Storage Usage', srUsageStatePanel: 'Storage Usage',
srTopUsageStatePanel: 'Top 5 SR Usage (in %)', srTopUsageStatePanel: 'Top 5 SR Usage (in %)',
notEnoughPermissionsError: 'Not enough permissions!',
vmsStates: '{running, number} running ({halted, number} halted)', vmsStates: '{running, number} running ({halted, number} halted)',
dashboardStatsButtonRemoveAll: 'Clear selection', dashboardStatsButtonRemoveAll: 'Clear selection',
dashboardStatsButtonAddAllHost: 'Add all hosts', dashboardStatsButtonAddAllHost: 'Add all hosts',
@ -1048,9 +1059,10 @@ const messages = {
ipPool: 'IP pool', ipPool: 'IP pool',
quantity: 'Quantity', quantity: 'Quantity',
noResourceSetLimits: 'No limits.', noResourceSetLimits: 'No limits.',
totalResource: 'Total:',
remainingResource: 'Remaining:', remainingResource: 'Remaining:',
usedResource: 'Used:', usedResourceLabel: 'Used',
availableResourceLabel: 'Available',
resourceSetQuota: 'Used: {usage} (Total: {total})',
resourceSetNew: 'New', resourceSetNew: 'New',
// ---- VM import --- // ---- VM import ---

View File

@ -0,0 +1,127 @@
import _, { messages } from 'intl'
import ChartistGraph from 'react-chartist'
import PropTypes from 'prop-types'
import React from 'react'
import { Card, CardBlock, CardHeader } from 'card'
import { Container, Row, Col } from 'grid'
import { forEach, map } from 'lodash'
import { injectIntl } from 'react-intl'
import Component from './base-component'
import Icon from './icon'
import { createSelector } from './selectors'
import { formatSize } from './utils'
// ===================================================================
const RESOURCES = ['disk', 'memory', 'cpus']
// ===================================================================
@injectIntl
export default class ResourceSetQuotas extends Component {
static propTypes = {
limits: PropTypes.object.isRequired,
}
_getQuotas = createSelector(
() => this.props.limits,
limits => {
const quotas = {}
forEach(RESOURCES, resource => {
if (limits[resource] != null) {
const { available, total } = limits[resource]
quotas[resource] = {
available,
total,
usage: total - available,
}
}
})
return quotas
}
)
render () {
const { intl: { formatMessage } } = this.props
const labels = [
formatMessage(messages.availableResourceLabel),
formatMessage(messages.usedResourceLabel),
]
const { cpus, disk, memory } = this._getQuotas()
const quotas = [
{
header: (
<span>
<Icon icon='cpu' /> {_('cpuStatePanel')}
</span>
),
validFormat: true,
quota: cpus,
},
{
header: (
<span>
<Icon icon='memory' /> {_('memoryStatePanel')}
</span>
),
validFormat: false,
quota: memory,
},
{
header: (
<span>
<Icon icon='disk' /> {_('srUsageStatePanel')}
</span>
),
validFormat: false,
quota: disk,
},
]
return (
<Container>
<Row>
{map(quotas, ({ header, validFormat, quota }, key) => (
<Col key={key} mediumSize={4}>
<Card>
<CardHeader>{header}</CardHeader>
<CardBlock className='text-center'>
{quota !== undefined ? (
<div>
<ChartistGraph
data={{
labels,
series: [quota.available, quota.usage],
}}
options={{
donut: true,
donutWidth: 40,
showLabel: false,
}}
type='Pie'
/>
<p className='text-xs-center'>
{_('resourceSetQuota', {
total: validFormat
? quota.total.toString()
: formatSize(quota.total),
usage: validFormat
? quota.usage.toString()
: formatSize(quota.usage),
})}
</p>
</div>
) : (
<p className='text-xs-center display-1'>&infin;</p>
)}
</CardBlock>
</Card>
</Col>
))}
</Row>
</Container>
)
}
}

View File

@ -1,18 +1,18 @@
import _ from 'intl' import _, { messages } from 'intl'
import ButtonGroup from 'button-group' import ButtonGroup from 'button-group'
import ChartistGraph from 'react-chartist' import ChartistGraph from 'react-chartist'
import Component from 'base-component' import Component from 'base-component'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import propTypes from 'prop-types-decorator'
import Link, { BlockLink } from 'link'
import map from 'lodash/map'
import HostsPatchesTable from 'hosts-patches-table' import HostsPatchesTable from 'hosts-patches-table'
import Icon from 'icon'
import Link, { BlockLink } from 'link'
import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import size from 'lodash/size' import ResourceSetQuotas from 'resource-set-quotas'
import Upgrade from 'xoa-upgrade' import Upgrade from 'xoa-upgrade'
import { Card, CardBlock, CardHeader } from 'card' import { Card, CardBlock, CardHeader } from 'card'
import { Container, Row, Col } from 'grid' import { Container, Row, Col } from 'grid'
import { forEach, isEmpty, map, size } from 'lodash'
import { injectIntl } from 'react-intl'
import { import {
createCollectionWrapper, createCollectionWrapper,
createCounter, createCounter,
@ -22,17 +22,27 @@ import {
createTop, createTop,
isAdmin, isAdmin,
} from 'selectors' } from 'selectors'
import { connectStore, formatSize } from 'utils' import { addSubscriptions, connectStore, formatSize } from 'utils'
import { isSrWritable, subscribeUsers } from 'xo' import {
isSrWritable,
subscribePermissions,
subscribeResourceSets,
subscribeUsers,
} from 'xo'
import styles from './index.css' import styles from './index.css'
// =================================================================== // ===================================================================
@propTypes({ const PIE_GRAPH_OPTIONS = { donut: true, donutWidth: 40, showLabel: false }
hosts: propTypes.object.isRequired,
}) // ===================================================================
class PatchesCard extends Component { class PatchesCard extends Component {
static propTypes = {
hosts: PropTypes.object.isRequired,
}
_getContainer = () => this.refs.container _getContainer = () => this.refs.container
render () { render () {
@ -55,8 +65,6 @@ class PatchesCard extends Component {
} }
} }
// ===================================================================
@connectStore(() => { @connectStore(() => {
const getHosts = createGetObjectsOfType('host') const getHosts = createGetObjectsOfType('host')
const getVms = createGetObjectsOfType('VM') const getVms = createGetObjectsOfType('VM')
@ -111,7 +119,6 @@ class PatchesCard extends Component {
return { return {
hostMetrics: getHostMetrics, hostMetrics: getHostMetrics,
hosts: getHosts, hosts: getHosts,
isAdmin,
nAlarmMessages: getNumberOfAlarmMessages, nAlarmMessages: getNumberOfAlarmMessages,
nHosts: getNumberOfHosts, nHosts: getNumberOfHosts,
nPools: getNumberOfPools, nPools: getNumberOfPools,
@ -126,18 +133,22 @@ class PatchesCard extends Component {
vmMetrics: getVmMetrics, vmMetrics: getVmMetrics,
} }
}) })
export default class Overview extends Component { @injectIntl
class DefaultCard extends Component {
componentWillMount () { componentWillMount () {
this.componentWillUnmount = subscribeUsers(users => { this.componentWillUnmount = subscribeUsers(users => {
this.setState({ users }) this.setState({ users })
}) })
} }
render () { render () {
const { props, state } = this const { props, state } = this
const users = state && state.users const users = state && state.users
const nUsers = size(users) const nUsers = size(users)
return process.env.XOA_PLAN > 2 ? ( const { formatMessage } = props.intl
return (
<Container> <Container>
<Row> <Row>
<Col mediumSize={4}> <Col mediumSize={4}>
@ -186,14 +197,17 @@ export default class Overview extends Component {
<CardBlock className='dashboardItem'> <CardBlock className='dashboardItem'>
<ChartistGraph <ChartistGraph
data={{ data={{
labels: ['Used Memory', 'Total Memory'], labels: [
formatMessage(messages.usedMemory),
formatMessage(messages.totalMemory),
],
series: [ series: [
props.hostMetrics.memoryUsage, props.hostMetrics.memoryUsage,
props.hostMetrics.memoryTotal - props.hostMetrics.memoryTotal -
props.hostMetrics.memoryUsage, props.hostMetrics.memoryUsage,
], ],
}} }}
options={{ donut: true, donutWidth: 40, showLabel: false }} options={PIE_GRAPH_OPTIONS}
type='Pie' type='Pie'
/> />
<p className='text-xs-center'> <p className='text-xs-center'>
@ -214,7 +228,10 @@ export default class Overview extends Component {
<div className='ct-chart dashboardItem'> <div className='ct-chart dashboardItem'>
<ChartistGraph <ChartistGraph
data={{ data={{
labels: ['vCPUs', 'CPUs'], labels: [
formatMessage(messages.usedVCpus),
formatMessage(messages.totalCpus),
],
series: [props.vmMetrics.vcpus, props.hostMetrics.cpus], series: [props.vmMetrics.vcpus, props.hostMetrics.cpus],
}} }}
options={{ options={{
@ -225,9 +242,9 @@ export default class Overview extends Component {
type='Bar' type='Bar'
/> />
<p className='text-xs-center'> <p className='text-xs-center'>
{_('ofUsage', { {_('ofCpusUsage', {
total: `${props.hostMetrics.cpus} CPUs`, nCpus: props.hostMetrics.cpus,
usage: `${props.vmMetrics.vcpus} vCPUs`, nVcpus: props.vmMetrics.vcpus,
})} })}
</p> </p>
</div> </div>
@ -244,17 +261,16 @@ export default class Overview extends Component {
<BlockLink to='/dashboard/health'> <BlockLink to='/dashboard/health'>
<ChartistGraph <ChartistGraph
data={{ data={{
labels: ['Used Space', 'Total Space'], labels: [
formatMessage(messages.usedSpace),
formatMessage(messages.totalSpace),
],
series: [ series: [
props.srMetrics.srUsage, props.srMetrics.srUsage,
props.srMetrics.srTotal - props.srMetrics.srUsage, props.srMetrics.srTotal - props.srMetrics.srUsage,
], ],
}} }}
options={{ options={PIE_GRAPH_OPTIONS}
donut: true,
donutWidth: 40,
showLabel: false,
}}
type='Pie' type='Pie'
/> />
<p className='text-xs-center'> <p className='text-xs-center'>
@ -326,7 +342,11 @@ export default class Overview extends Component {
<BlockLink to='/home?t=VM'> <BlockLink to='/home?t=VM'>
<ChartistGraph <ChartistGraph
data={{ data={{
labels: ['Running', 'Halted', 'Other'], labels: [
formatMessage(messages.vmStateRunning),
formatMessage(messages.vmStateHalted),
formatMessage(messages.vmStateOther),
],
series: [ series: [
props.vmMetrics.running, props.vmMetrics.running,
props.vmMetrics.halted, props.vmMetrics.halted,
@ -381,10 +401,50 @@ export default class Overview extends Component {
</Col> </Col>
</Row> </Row>
</Container> </Container>
) : ( )
<Container> }
<Upgrade place='dashboard' available={3} /> }
</Container>
// ===================================================================
@addSubscriptions({
resourceSets: subscribeResourceSets,
permissions: subscribePermissions,
})
@connectStore({
isAdmin,
})
export default class Overview extends Component {
render () {
const { props } = this
const showResourceSets = !isEmpty(props.resourceSets) && !props.isAdmin
const authorized = !isEmpty(props.permissions) || props.isAdmin
if (!authorized && !showResourceSets) {
return <em>{_('notEnoughPermissionsError')}</em>
}
return (
<Upgrade place='dashboard' required={3}>
<Container>
{showResourceSets ? (
map(props.resourceSets, resourceSet => (
<Row key={resourceSet.id}>
<Card>
<CardHeader>
<Icon icon='menu-self-service' /> {resourceSet.name}
</CardHeader>
<CardBlock>
<ResourceSetQuotas limits={resourceSet.limits} />
</CardBlock>
</Card>
</Row>
))
) : (
<DefaultCard isAdmin={props.isAdmin} />
)}
</Container>
</Upgrade>
) )
} }
} }

View File

@ -1,6 +1,5 @@
import _ from 'intl' import _ from 'intl'
import ActionButton from 'action-button' import ActionButton from 'action-button'
import ChartistGraph from 'react-chartist'
import Collapse from 'collapse' import Collapse from 'collapse'
import Component from 'base-component' import Component from 'base-component'
import defined from 'xo-defined' import defined from 'xo-defined'
@ -15,13 +14,15 @@ import isEmpty from 'lodash/isEmpty'
import keys from 'lodash/keys' import keys from 'lodash/keys'
import map from 'lodash/map' import map from 'lodash/map'
import mapKeys from 'lodash/mapKeys' import mapKeys from 'lodash/mapKeys'
import propTypes from 'prop-types-decorator' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import remove from 'lodash/remove' import remove from 'lodash/remove'
import renderXoItem from 'render-xo-item' import renderXoItem from 'render-xo-item'
import ResourceSetQuotas from 'resource-set-quotas'
import Upgrade from 'xoa-upgrade' import Upgrade from 'xoa-upgrade'
import { Container, Row, Col } from 'grid' import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType, createSelector } from 'selectors' import { createGetObjectsOfType, createSelector } from 'selectors'
import { injectIntl } from 'react-intl'
import { SizeInput } from 'form' import { SizeInput } from 'form'
import { import {
@ -36,13 +37,10 @@ import {
import { import {
addSubscriptions, addSubscriptions,
connectStore, connectStore,
formatSize,
resolveIds, resolveIds,
resolveResourceSets, resolveResourceSets,
} from 'utils' } from 'utils'
import { Card, CardBlock, CardHeader } from 'card'
import { import {
SelectIpPool, SelectIpPool,
SelectNetwork, SelectNetwork,
@ -72,10 +70,7 @@ const HEADER = (
// =================================================================== // ===================================================================
const Hosts = propTypes({ const Hosts = ({ eligibleHosts, excludedHosts }) => (
eligibleHosts: propTypes.array.isRequired,
excludedHosts: propTypes.array.isRequired,
})(({ eligibleHosts, excludedHosts }) => (
<div> <div>
<Row> <Row>
<Col mediumSize={6}> <Col mediumSize={6}>
@ -117,14 +112,15 @@ const Hosts = propTypes({
</Col> </Col>
</Row> </Row>
</div> </div>
)) )
Hosts.propTypes = {
eligibleHosts: PropTypes.array.isRequired,
excludedHosts: PropTypes.array.isRequired,
}
// =================================================================== // ===================================================================
@propTypes({
onSave: propTypes.func,
resourceSet: propTypes.object,
})
@connectStore(() => { @connectStore(() => {
const getHosts = createGetObjectsOfType('host').sort() const getHosts = createGetObjectsOfType('host').sort()
const getHostsByPool = getHosts.groupBy('$pool') const getHostsByPool = getHosts.groupBy('$pool')
@ -135,6 +131,11 @@ const Hosts = propTypes({
} }
}) })
export class Edit extends Component { export class Edit extends Component {
static propTypes = {
onSave: PropTypes.func,
resourceSet: PropTypes.object,
}
constructor (props) { constructor (props) {
super(props) super(props)
@ -588,16 +589,12 @@ export class Edit extends Component {
@addSubscriptions({ @addSubscriptions({
ipPools: subscribeIpPools, ipPools: subscribeIpPools,
}) })
@injectIntl
class ResourceSet extends Component { class ResourceSet extends Component {
_renderDisplay = () => { _renderDisplay = () => {
const { resourceSet } = this.props const { resourceSet } = this.props
const resolvedIpPools = mapKeys(this.props.ipPools, 'id') const resolvedIpPools = mapKeys(this.props.ipPools, 'id')
const { const { limits, ipPools, subjects, objectsByType } = resourceSet
limits: { cpus, disk, memory } = {},
ipPools,
subjects,
objectsByType,
} = resourceSet
return [ return [
<li key='subjects' className='list-group-item'> <li key='subjects' className='list-group-item'>
@ -614,16 +611,16 @@ class ResourceSet extends Component {
<li key='ipPools' className='list-group-item'> <li key='ipPools' className='list-group-item'>
{map(ipPools, pool => { {map(ipPools, pool => {
const resolvedIpPool = resolvedIpPools[pool] const resolvedIpPool = resolvedIpPools[pool]
const limits = get(resourceSet, `limits[ipPool:${pool}]`) const ipPoolLimits = limits && get(limits, `[ipPool:${pool}]`)
const available = limits && limits.available const available = ipPoolLimits && ipPoolLimits.available
const total = limits && limits.total const total = ipPoolLimits && ipPoolLimits.total
return ( return (
<span className='mr-1'> <span className='mr-1'>
{renderXoItem({ {renderXoItem({
name: resolvedIpPool && resolvedIpPool.name, name: resolvedIpPool && resolvedIpPool.name,
type: 'ipPool', type: 'ipPool',
})} })}
{limits && ( {ipPoolLimits && (
<span> <span>
{' '} {' '}
({available}/{total}) ({available}/{total})
@ -635,112 +632,7 @@ class ResourceSet extends Component {
</li> </li>
), ),
<li key='graphs' className='list-group-item'> <li key='graphs' className='list-group-item'>
<Row> <ResourceSetQuotas limits={limits} />
<Col mediumSize={4}>
<Card>
<CardHeader>
<Icon icon='cpu' /> {_('resourceSetVcpus')}
</CardHeader>
<CardBlock className='text-center'>
{cpus ? (
<div>
<ChartistGraph
data={{
labels: ['Available', 'Used'],
series: [cpus.available, cpus.total - cpus.available],
}}
options={{
donut: true,
donutWidth: 40,
showLabel: false,
}}
type='Pie'
/>
<p className='text-xs-center'>
{_('usedResource')} {cpus.total - cpus.available} ({_(
'totalResource'
)}{' '}
{cpus.total})
</p>
</div>
) : (
<p className='text-xs-center display-1'>&infin;</p>
)}
</CardBlock>
</Card>
</Col>
<Col mediumSize={4}>
<Card>
<CardHeader>
<Icon icon='memory' /> {_('resourceSetMemory')}
</CardHeader>
<CardBlock className='text-center'>
{memory ? (
<div>
<ChartistGraph
data={{
labels: ['Available', 'Used'],
series: [
memory.available,
memory.total - memory.available,
],
}}
options={{
donut: true,
donutWidth: 40,
showLabel: false,
}}
type='Pie'
/>
<p className='text-xs-center'>
{_('usedResource')}{' '}
{formatSize(memory.total - memory.available)} ({_(
'totalResource'
)}{' '}
{formatSize(memory.total)})
</p>
</div>
) : (
<p className='text-xs-center display-1'>&infin;</p>
)}
</CardBlock>
</Card>
</Col>
<Col mediumSize={4}>
<Card>
<CardHeader>
<Icon icon='disk' /> {_('resourceSetStorage')}
</CardHeader>
<CardBlock>
{disk ? (
<div>
<ChartistGraph
data={{
labels: ['Available', 'Used'],
series: [disk.available, disk.total - disk.available],
}}
options={{
donut: true,
donutWidth: 40,
showLabel: false,
}}
type='Pie'
/>
<p className='text-xs-center'>
{_('usedResource')}{' '}
{formatSize(disk.total - disk.available)} ({_(
'totalResource'
)}{' '}
{formatSize(disk.total)})
</p>
</div>
) : (
<p className='text-xs-center display-1'>&infin;</p>
)}
</CardBlock>
</Card>
</Col>
</Row>
</li>, </li>,
<li key='actions' className='list-group-item text-xs-center'> <li key='actions' className='list-group-item text-xs-center'>
<div className='btn-toolbar'> <div className='btn-toolbar'>