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}}',
vmPanel: 'VM{vms, plural, one {} other {s}}',
memoryStatePanel: 'RAM Usage:',
usedMemory: 'Used Memory',
totalMemory: 'Total Memory',
totalCpus: 'CPUs Total',
usedVCpus: 'Used vCPUs',
usedSpace: 'Used Space',
totalSpace: 'Total Space',
cpuStatePanel: 'CPUs Usage',
vmStatePanel: 'VMs Power state',
vmStateHalted: 'Halted',
vmStateOther: 'Other',
vmStateRunning: 'Running',
taskStatePanel: 'Pending tasks',
usersStatePanel: 'Users',
srStatePanel: 'Storage state',
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',
srName: 'Name',
srPool: 'Pool',
@ -921,6 +931,7 @@ const messages = {
srFree: 'free',
srUsageStatePanel: 'Storage Usage',
srTopUsageStatePanel: 'Top 5 SR Usage (in %)',
notEnoughPermissionsError: 'Not enough permissions!',
vmsStates: '{running, number} running ({halted, number} halted)',
dashboardStatsButtonRemoveAll: 'Clear selection',
dashboardStatsButtonAddAllHost: 'Add all hosts',
@ -1048,9 +1059,10 @@ const messages = {
ipPool: 'IP pool',
quantity: 'Quantity',
noResourceSetLimits: 'No limits.',
totalResource: 'Total:',
remainingResource: 'Remaining:',
usedResource: 'Used:',
usedResourceLabel: 'Used',
availableResourceLabel: 'Available',
resourceSetQuota: 'Used: {usage} (Total: {total})',
resourceSetNew: 'New',
// ---- 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 ChartistGraph from 'react-chartist'
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 Icon from 'icon'
import Link, { BlockLink } from 'link'
import PropTypes from 'prop-types'
import React from 'react'
import size from 'lodash/size'
import ResourceSetQuotas from 'resource-set-quotas'
import Upgrade from 'xoa-upgrade'
import { Card, CardBlock, CardHeader } from 'card'
import { Container, Row, Col } from 'grid'
import { forEach, isEmpty, map, size } from 'lodash'
import { injectIntl } from 'react-intl'
import {
createCollectionWrapper,
createCounter,
@ -22,17 +22,27 @@ import {
createTop,
isAdmin,
} from 'selectors'
import { connectStore, formatSize } from 'utils'
import { isSrWritable, subscribeUsers } from 'xo'
import { addSubscriptions, connectStore, formatSize } from 'utils'
import {
isSrWritable,
subscribePermissions,
subscribeResourceSets,
subscribeUsers,
} from 'xo'
import styles from './index.css'
// ===================================================================
@propTypes({
hosts: propTypes.object.isRequired,
})
const PIE_GRAPH_OPTIONS = { donut: true, donutWidth: 40, showLabel: false }
// ===================================================================
class PatchesCard extends Component {
static propTypes = {
hosts: PropTypes.object.isRequired,
}
_getContainer = () => this.refs.container
render () {
@ -55,8 +65,6 @@ class PatchesCard extends Component {
}
}
// ===================================================================
@connectStore(() => {
const getHosts = createGetObjectsOfType('host')
const getVms = createGetObjectsOfType('VM')
@ -111,7 +119,6 @@ class PatchesCard extends Component {
return {
hostMetrics: getHostMetrics,
hosts: getHosts,
isAdmin,
nAlarmMessages: getNumberOfAlarmMessages,
nHosts: getNumberOfHosts,
nPools: getNumberOfPools,
@ -126,18 +133,22 @@ class PatchesCard extends Component {
vmMetrics: getVmMetrics,
}
})
export default class Overview extends Component {
@injectIntl
class DefaultCard extends Component {
componentWillMount () {
this.componentWillUnmount = subscribeUsers(users => {
this.setState({ users })
})
}
render () {
const { props, state } = this
const users = state && state.users
const nUsers = size(users)
return process.env.XOA_PLAN > 2 ? (
const { formatMessage } = props.intl
return (
<Container>
<Row>
<Col mediumSize={4}>
@ -186,14 +197,17 @@ export default class Overview extends Component {
<CardBlock className='dashboardItem'>
<ChartistGraph
data={{
labels: ['Used Memory', 'Total Memory'],
labels: [
formatMessage(messages.usedMemory),
formatMessage(messages.totalMemory),
],
series: [
props.hostMetrics.memoryUsage,
props.hostMetrics.memoryTotal -
props.hostMetrics.memoryUsage,
],
}}
options={{ donut: true, donutWidth: 40, showLabel: false }}
options={PIE_GRAPH_OPTIONS}
type='Pie'
/>
<p className='text-xs-center'>
@ -214,7 +228,10 @@ export default class Overview extends Component {
<div className='ct-chart dashboardItem'>
<ChartistGraph
data={{
labels: ['vCPUs', 'CPUs'],
labels: [
formatMessage(messages.usedVCpus),
formatMessage(messages.totalCpus),
],
series: [props.vmMetrics.vcpus, props.hostMetrics.cpus],
}}
options={{
@ -225,9 +242,9 @@ export default class Overview extends Component {
type='Bar'
/>
<p className='text-xs-center'>
{_('ofUsage', {
total: `${props.hostMetrics.cpus} CPUs`,
usage: `${props.vmMetrics.vcpus} vCPUs`,
{_('ofCpusUsage', {
nCpus: props.hostMetrics.cpus,
nVcpus: props.vmMetrics.vcpus,
})}
</p>
</div>
@ -244,17 +261,16 @@ export default class Overview extends Component {
<BlockLink to='/dashboard/health'>
<ChartistGraph
data={{
labels: ['Used Space', 'Total Space'],
labels: [
formatMessage(messages.usedSpace),
formatMessage(messages.totalSpace),
],
series: [
props.srMetrics.srUsage,
props.srMetrics.srTotal - props.srMetrics.srUsage,
],
}}
options={{
donut: true,
donutWidth: 40,
showLabel: false,
}}
options={PIE_GRAPH_OPTIONS}
type='Pie'
/>
<p className='text-xs-center'>
@ -326,7 +342,11 @@ export default class Overview extends Component {
<BlockLink to='/home?t=VM'>
<ChartistGraph
data={{
labels: ['Running', 'Halted', 'Other'],
labels: [
formatMessage(messages.vmStateRunning),
formatMessage(messages.vmStateHalted),
formatMessage(messages.vmStateOther),
],
series: [
props.vmMetrics.running,
props.vmMetrics.halted,
@ -381,10 +401,50 @@ export default class Overview extends Component {
</Col>
</Row>
</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 ActionButton from 'action-button'
import ChartistGraph from 'react-chartist'
import Collapse from 'collapse'
import Component from 'base-component'
import defined from 'xo-defined'
@ -15,13 +14,15 @@ import isEmpty from 'lodash/isEmpty'
import keys from 'lodash/keys'
import map from 'lodash/map'
import mapKeys from 'lodash/mapKeys'
import propTypes from 'prop-types-decorator'
import PropTypes from 'prop-types'
import React from 'react'
import remove from 'lodash/remove'
import renderXoItem from 'render-xo-item'
import ResourceSetQuotas from 'resource-set-quotas'
import Upgrade from 'xoa-upgrade'
import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { injectIntl } from 'react-intl'
import { SizeInput } from 'form'
import {
@ -36,13 +37,10 @@ import {
import {
addSubscriptions,
connectStore,
formatSize,
resolveIds,
resolveResourceSets,
} from 'utils'
import { Card, CardBlock, CardHeader } from 'card'
import {
SelectIpPool,
SelectNetwork,
@ -72,10 +70,7 @@ const HEADER = (
// ===================================================================
const Hosts = propTypes({
eligibleHosts: propTypes.array.isRequired,
excludedHosts: propTypes.array.isRequired,
})(({ eligibleHosts, excludedHosts }) => (
const Hosts = ({ eligibleHosts, excludedHosts }) => (
<div>
<Row>
<Col mediumSize={6}>
@ -117,14 +112,15 @@ const Hosts = propTypes({
</Col>
</Row>
</div>
))
)
Hosts.propTypes = {
eligibleHosts: PropTypes.array.isRequired,
excludedHosts: PropTypes.array.isRequired,
}
// ===================================================================
@propTypes({
onSave: propTypes.func,
resourceSet: propTypes.object,
})
@connectStore(() => {
const getHosts = createGetObjectsOfType('host').sort()
const getHostsByPool = getHosts.groupBy('$pool')
@ -135,6 +131,11 @@ const Hosts = propTypes({
}
})
export class Edit extends Component {
static propTypes = {
onSave: PropTypes.func,
resourceSet: PropTypes.object,
}
constructor (props) {
super(props)
@ -588,16 +589,12 @@ export class Edit extends Component {
@addSubscriptions({
ipPools: subscribeIpPools,
})
@injectIntl
class ResourceSet extends Component {
_renderDisplay = () => {
const { resourceSet } = this.props
const resolvedIpPools = mapKeys(this.props.ipPools, 'id')
const {
limits: { cpus, disk, memory } = {},
ipPools,
subjects,
objectsByType,
} = resourceSet
const { limits, ipPools, subjects, objectsByType } = resourceSet
return [
<li key='subjects' className='list-group-item'>
@ -614,16 +611,16 @@ class ResourceSet extends Component {
<li key='ipPools' className='list-group-item'>
{map(ipPools, pool => {
const resolvedIpPool = resolvedIpPools[pool]
const limits = get(resourceSet, `limits[ipPool:${pool}]`)
const available = limits && limits.available
const total = limits && limits.total
const ipPoolLimits = limits && get(limits, `[ipPool:${pool}]`)
const available = ipPoolLimits && ipPoolLimits.available
const total = ipPoolLimits && ipPoolLimits.total
return (
<span className='mr-1'>
{renderXoItem({
name: resolvedIpPool && resolvedIpPool.name,
type: 'ipPool',
})}
{limits && (
{ipPoolLimits && (
<span>
{' '}
({available}/{total})
@ -635,112 +632,7 @@ class ResourceSet extends Component {
</li>
),
<li key='graphs' className='list-group-item'>
<Row>
<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>
<ResourceSetQuotas limits={limits} />
</li>,
<li key='actions' className='list-group-item text-xs-center'>
<div className='btn-toolbar'>