feat(dashboard): allow the user in self service to see its quotas (#2268)
Fixes #1538
This commit is contained in:
parent
c89c7dab60
commit
add10ea556
@ -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 ---
|
||||
|
127
src/common/resource-set-quotas.js
Normal file
127
src/common/resource-set-quotas.js
Normal 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'>∞</p>
|
||||
)}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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'>∞</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'>∞</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'>∞</p>
|
||||
)}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<ResourceSetQuotas limits={limits} />
|
||||
</li>,
|
||||
<li key='actions' className='list-group-item text-xs-center'>
|
||||
<div className='btn-toolbar'>
|
||||
|
Loading…
Reference in New Issue
Block a user