From add10ea556a2fb43912a69152e38427f3d6d7463 Mon Sep 17 00:00:00 2001 From: badrAZ Date: Mon, 29 Jan 2018 10:44:43 +0100 Subject: [PATCH] feat(dashboard): allow the user in self service to see its quotas (#2268) Fixes #1538 --- src/common/intl/messages.js | 16 ++- src/common/resource-set-quotas.js | 127 +++++++++++++++++++++ src/xo-app/dashboard/overview/index.js | 128 +++++++++++++++------ src/xo-app/self/index.js | 152 ++++--------------------- 4 files changed, 257 insertions(+), 166 deletions(-) create mode 100644 src/common/resource-set-quotas.js diff --git a/src/common/intl/messages.js b/src/common/intl/messages.js index ed5dc80ce..fcf23ba71 100644 --- a/src/common/intl/messages.js +++ b/src/common/intl/messages.js @@ -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 --- diff --git a/src/common/resource-set-quotas.js b/src/common/resource-set-quotas.js new file mode 100644 index 000000000..b19da2c96 --- /dev/null +++ b/src/common/resource-set-quotas.js @@ -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: ( + + {_('cpuStatePanel')} + + ), + validFormat: true, + quota: cpus, + }, + { + header: ( + + {_('memoryStatePanel')} + + ), + validFormat: false, + quota: memory, + }, + { + header: ( + + {_('srUsageStatePanel')} + + ), + validFormat: false, + quota: disk, + }, + ] + return ( + + + {map(quotas, ({ header, validFormat, quota }, key) => ( + + + {header} + + {quota !== undefined ? ( +
+ +

+ {_('resourceSetQuota', { + total: validFormat + ? quota.total.toString() + : formatSize(quota.total), + usage: validFormat + ? quota.usage.toString() + : formatSize(quota.usage), + })} +

+
+ ) : ( +

+ )} +
+
+ + ))} +
+
+ ) + } +} diff --git a/src/xo-app/dashboard/overview/index.js b/src/xo-app/dashboard/overview/index.js index dce8216ae..391e21a73 100644 --- a/src/xo-app/dashboard/overview/index.js +++ b/src/xo-app/dashboard/overview/index.js @@ -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 ( @@ -186,14 +197,17 @@ export default class Overview extends Component {

@@ -214,7 +228,10 @@ export default class Overview extends Component {

- {_('ofUsage', { - total: `${props.hostMetrics.cpus} CPUs`, - usage: `${props.vmMetrics.vcpus} vCPUs`, + {_('ofCpusUsage', { + nCpus: props.hostMetrics.cpus, + nVcpus: props.vmMetrics.vcpus, })}

@@ -244,17 +261,16 @@ export default class Overview extends Component {

@@ -326,7 +342,11 @@ export default class Overview extends Component { - ) : ( - - - + ) + } +} + +// =================================================================== + +@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 {_('notEnoughPermissionsError')} + } + + return ( + + + {showResourceSets ? ( + map(props.resourceSets, resourceSet => ( + + + + {resourceSet.name} + + + + + + + )) + ) : ( + + )} + + ) } } diff --git a/src/xo-app/self/index.js b/src/xo-app/self/index.js index c56ab5b44..1377d7855 100644 --- a/src/xo-app/self/index.js +++ b/src/xo-app/self/index.js @@ -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 }) => (

@@ -117,14 +112,15 @@ const Hosts = propTypes({
-)) +) + +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 [
  • @@ -614,16 +611,16 @@ class ResourceSet extends Component {
  • {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 ( {renderXoItem({ name: resolvedIpPool && resolvedIpPool.name, type: 'ipPool', })} - {limits && ( + {ipPoolLimits && ( {' '} ({available}/{total}) @@ -635,112 +632,7 @@ class ResourceSet extends Component {
  • ),
  • - - - - - {_('resourceSetVcpus')} - - - {cpus ? ( -
    - -

    - {_('usedResource')} {cpus.total - cpus.available} ({_( - 'totalResource' - )}{' '} - {cpus.total}) -

    -
    - ) : ( -

    - )} -
    -
    - - - - - {_('resourceSetMemory')} - - - {memory ? ( -
    - -

    - {_('usedResource')}{' '} - {formatSize(memory.total - memory.available)} ({_( - 'totalResource' - )}{' '} - {formatSize(memory.total)}) -

    -
    - ) : ( -

    - )} -
    -
    - - - - - {_('resourceSetStorage')} - - - {disk ? ( -
    - -

    - {_('usedResource')}{' '} - {formatSize(disk.total - disk.available)} ({_( - 'totalResource' - )}{' '} - {formatSize(disk.total)}) -

    -
    - ) : ( -

    - )} -
    -
    - -
    +
  • ,