feat(xo-web): display attached VDI snapshots in Health (#2684)
Fixes #2634
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
isEmpty,
|
||||
isFunction,
|
||||
map,
|
||||
startsWith,
|
||||
} from 'lodash'
|
||||
|
||||
import ActionRowButton from '../action-row-button'
|
||||
@@ -33,6 +34,7 @@ import Tooltip from '../tooltip'
|
||||
import { BlockLink } from '../link'
|
||||
import { Container, Col } from '../grid'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
createCounter,
|
||||
createFilter,
|
||||
createPager,
|
||||
@@ -287,10 +289,13 @@ const URL_STATE_RE = /^(?:(\d+)(?:_(\d+)(_desc)?)?-)?(.*)$/
|
||||
paginationContainer: propTypes.func,
|
||||
rowAction: propTypes.func,
|
||||
rowLink: propTypes.oneOfType([propTypes.func, propTypes.string]),
|
||||
rowTransform: propTypes.func,
|
||||
// DOM node selector like body or .my-class
|
||||
// The shortcuts will be enabled when the node is focused
|
||||
shortcutsTarget: propTypes.string,
|
||||
stateUrlParam: propTypes.string,
|
||||
|
||||
// @deprecated, use `data-${key}` instead
|
||||
userData: propTypes.any,
|
||||
},
|
||||
{
|
||||
@@ -305,6 +310,20 @@ export default class SortedTable extends Component {
|
||||
constructor (props, context) {
|
||||
super(props, context)
|
||||
|
||||
this._getUserData =
|
||||
'userData' in props
|
||||
? () => this.props.userData
|
||||
: createCollectionWrapper(() => {
|
||||
const { props } = this
|
||||
const userData = {}
|
||||
Object.keys(props).forEach(key => {
|
||||
if (startsWith(key, 'data-')) {
|
||||
userData[key.slice(5)] = props[key]
|
||||
}
|
||||
})
|
||||
return userData
|
||||
})
|
||||
|
||||
let selectedColumn = props.defaultColumn
|
||||
if (selectedColumn == null) {
|
||||
selectedColumn = findIndex(props.columns, 'default')
|
||||
@@ -350,17 +369,27 @@ export default class SortedTable extends Component {
|
||||
this._getSelectedColumn = () =>
|
||||
this.props.columns[this.state.selectedColumn]
|
||||
|
||||
this._getTotalNumberOfItems = createCounter(() => this.props.collection)
|
||||
let getAllItems = () => this.props.collection
|
||||
if ('rowTransform' in props) {
|
||||
getAllItems = createSelector(
|
||||
getAllItems,
|
||||
this._getUserData,
|
||||
() => this.props.rowTransform,
|
||||
(items, userData, rowTransform) =>
|
||||
map(items, item => rowTransform(item, userData))
|
||||
)
|
||||
}
|
||||
this._getTotalNumberOfItems = createCounter(getAllItems)
|
||||
|
||||
const createMatcher = str => CM.parse(str).createPredicate()
|
||||
this._getItems = createSort(
|
||||
createFilter(
|
||||
() => this.props.collection,
|
||||
getAllItems,
|
||||
createSelector(() => this.state.filter, createMatcher)
|
||||
),
|
||||
createSelector(
|
||||
() => this._getSelectedColumn().sortCriteria,
|
||||
() => this.props.userData,
|
||||
this._getUserData,
|
||||
(sortCriteria, userData) =>
|
||||
typeof sortCriteria === 'function'
|
||||
? object => sortCriteria(object, userData)
|
||||
@@ -396,7 +425,7 @@ export default class SortedTable extends Component {
|
||||
() => this.state.highlighted,
|
||||
() => this.props.rowLink,
|
||||
() => this.props.rowAction,
|
||||
() => this.props.userData,
|
||||
this._getUserData,
|
||||
(
|
||||
visibleItems,
|
||||
hasGroupedActions,
|
||||
@@ -643,7 +672,8 @@ export default class SortedTable extends Component {
|
||||
|
||||
_renderItem = (item, i) => {
|
||||
const { props, state } = this
|
||||
const { actions, individualActions, rowAction, rowLink, userData } = props
|
||||
const { actions, individualActions, rowAction, rowLink } = props
|
||||
const userData = this._getUserData()
|
||||
|
||||
const hasGroupedActions = this._hasGroupedActions()
|
||||
const hasIndividualActions =
|
||||
@@ -736,7 +766,6 @@ export default class SortedTable extends Component {
|
||||
itemsPerPage,
|
||||
paginationContainer,
|
||||
shortcutsTarget,
|
||||
userData,
|
||||
} = props
|
||||
const { all } = state
|
||||
const groupedActions = this._getGroupedActions()
|
||||
@@ -773,6 +802,8 @@ export default class SortedTable extends Component {
|
||||
/>
|
||||
)
|
||||
|
||||
const userData = this._getUserData()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{shortcutsTarget !== undefined && (
|
||||
|
||||
@@ -12,7 +12,7 @@ import Upgrade from 'xoa-upgrade'
|
||||
import xml2js from 'xml2js'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { confirm } from 'modal'
|
||||
import { connectStore, formatSize, mapPlus, noop, resolveIds } from 'utils'
|
||||
import { connectStore, formatSize, noop, resolveIds } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { flatten, get, includes, isEmpty, map, mapValues } from 'lodash'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
@@ -174,51 +174,102 @@ const ORPHANED_VDI_COLUMNS = [
|
||||
},
|
||||
]
|
||||
|
||||
const CONTROL_DOMAIN_VDI_COLUMNS = [
|
||||
const AttachedVdisTable = [
|
||||
connectStore({
|
||||
pools: createGetObjectsOfType('pool'),
|
||||
srs: createGetObjectsOfType('SR'),
|
||||
vbds: createGetObjectsOfType('VBD').pick(
|
||||
createSelector(
|
||||
createFilter(
|
||||
createGetObjectsOfType('VM-controller'),
|
||||
(_, props) => props.poolPredicate
|
||||
),
|
||||
createCollectionWrapper(vmControllers =>
|
||||
flatten(map(vmControllers, '$VBDs'))
|
||||
)
|
||||
)
|
||||
),
|
||||
vdis: createGetObjectsOfType('VDI'),
|
||||
vdiSnapshots: createGetObjectsOfType('VDI-snapshot'),
|
||||
}),
|
||||
({ columns, rowTransform }) => ({ pools, srs, vbds, vdis, vdiSnapshots }) => (
|
||||
<NoObjects
|
||||
collection={vbds}
|
||||
columns={columns}
|
||||
component={SortedTable}
|
||||
data-pools={pools}
|
||||
data-srs={srs}
|
||||
data-vdis={vdis}
|
||||
data-vdiSnapshots={vdiSnapshots}
|
||||
emptyMessage={_('noControlDomainVdis')}
|
||||
rowTransform={rowTransform}
|
||||
/>
|
||||
),
|
||||
{
|
||||
name: _('vdiNameLabel'),
|
||||
itemRenderer: vdi => vdi && vdi.name_label,
|
||||
sortCriteria: vdi => vdi && vdi.name_label,
|
||||
columns: [
|
||||
{
|
||||
name: _('vdiNameLabel'),
|
||||
itemRenderer: ({ vdi }) => (
|
||||
<span>
|
||||
{vdi.name_label}
|
||||
{vdi.type === 'VDI-snapshot' && [
|
||||
' ',
|
||||
<Icon icon='vm-snapshot' key='1' />,
|
||||
]}
|
||||
</span>
|
||||
),
|
||||
sortCriteria: ({ vdi }) => vdi.name_label,
|
||||
},
|
||||
{
|
||||
name: _('vdiNameDescription'),
|
||||
itemRenderer: ({ vdi }) => vdi.name_description,
|
||||
sortCriteria: ({ vdi }) => vdi.name_description,
|
||||
},
|
||||
{
|
||||
name: _('vdiPool'),
|
||||
itemRenderer: ({ pool }) =>
|
||||
pool === undefined ? null : (
|
||||
<Link to={`pools/${pool.id}`}>{pool.name_label}</Link>
|
||||
),
|
||||
sortCriteria: ({ pool }) => pool != null && pool.name_label,
|
||||
},
|
||||
{
|
||||
name: _('vdiSize'),
|
||||
itemRenderer: ({ vdi }) => formatSize(vdi.size),
|
||||
sortCriteria: ({ vdi }) => vdi.size,
|
||||
},
|
||||
{
|
||||
name: _('vdiSr'),
|
||||
itemRenderer: ({ sr }) =>
|
||||
sr === undefined ? null : (
|
||||
<Link to={`srs/${sr.id}`}>{sr.name_label}</Link>
|
||||
),
|
||||
sortCriteria: ({ sr }) => sr != null && sr.name_label,
|
||||
},
|
||||
{
|
||||
name: _('vdiAction'),
|
||||
itemRenderer: ({ vbd }) => (
|
||||
<ActionRowButton
|
||||
btnStyle='danger'
|
||||
handler={deleteVbd}
|
||||
handlerParam={vbd}
|
||||
icon='delete'
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
rowTransform: (vbd, { pools, srs, vdis, vdiSnapshots }) => {
|
||||
const vdi = vdis[vbd.VDI] || vdiSnapshots[vbd.VDI]
|
||||
|
||||
return {
|
||||
vbd,
|
||||
vdi,
|
||||
sr: srs[vdi.$SR],
|
||||
pool: pools[vbd.$poolId],
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _('vdiNameDescription'),
|
||||
itemRenderer: vdi => vdi && vdi.name_description,
|
||||
sortCriteria: vdi => vdi && vdi.name_description,
|
||||
},
|
||||
{
|
||||
name: _('vdiPool'),
|
||||
itemRenderer: vdi =>
|
||||
vdi &&
|
||||
vdi.pool && (
|
||||
<Link to={`pools/${vdi.pool.id}`}>{vdi.pool.name_label}</Link>
|
||||
),
|
||||
sortCriteria: vdi => vdi && vdi.pool && vdi.pool.name_label,
|
||||
},
|
||||
{
|
||||
name: _('vdiSize'),
|
||||
itemRenderer: vdi => vdi && formatSize(vdi.size),
|
||||
sortCriteria: vdi => vdi && vdi.size,
|
||||
},
|
||||
{
|
||||
name: _('vdiSr'),
|
||||
itemRenderer: vdi =>
|
||||
vdi && vdi.sr && <Link to={`srs/${vdi.sr.id}`}>{vdi.sr.name_label}</Link>,
|
||||
sortCriteria: vdi => vdi && vdi.sr && vdi.sr.name_label,
|
||||
},
|
||||
{
|
||||
name: _('vdiAction'),
|
||||
itemRenderer: vdi =>
|
||||
vdi &&
|
||||
vdi.vbd && (
|
||||
<ActionRowButton
|
||||
btnStyle='danger'
|
||||
handler={deleteVbd}
|
||||
handlerParam={vdi.vbd}
|
||||
icon='delete'
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
].reduceRight((value, decorator) => decorator(value))
|
||||
|
||||
const VM_COLUMNS = [
|
||||
{
|
||||
@@ -334,37 +385,6 @@ const ALARM_COLUMNS = [
|
||||
const getOrphanVdiSnapshots = createGetObjectsOfType('VDI-snapshot')
|
||||
.filter([_ => !_.$snapshot_of && _.$VBDs.length === 0])
|
||||
.sort()
|
||||
const getControlDomainVbds = createGetObjectsOfType('VBD')
|
||||
.pick(
|
||||
createSelector(
|
||||
createGetObjectsOfType('VM-controller'),
|
||||
createCollectionWrapper(vmControllers =>
|
||||
flatten(map(vmControllers, '$VBDs'))
|
||||
)
|
||||
)
|
||||
)
|
||||
.sort()
|
||||
const getControlDomainVdis = createSelector(
|
||||
getControlDomainVbds,
|
||||
createGetObjectsOfType('VDI'),
|
||||
createGetObjectsOfType('pool'),
|
||||
createGetObjectsOfType('SR'),
|
||||
(vbds, vdis, pools, srs) =>
|
||||
mapPlus(vbds, (vbd, push) => {
|
||||
const vdi = vdis[vbd.VDI]
|
||||
|
||||
if (vdi == null) {
|
||||
return
|
||||
}
|
||||
|
||||
push({
|
||||
...vdi,
|
||||
pool: pools[vbd.$pool],
|
||||
sr: srs[vdi.$SR],
|
||||
vbd,
|
||||
})
|
||||
})
|
||||
)
|
||||
const getOrphanVmSnapshots = createGetObjectsOfType('VM-snapshot')
|
||||
.filter([snapshot => !snapshot.$snapshot_of])
|
||||
.sort()
|
||||
@@ -379,7 +399,6 @@ const ALARM_COLUMNS = [
|
||||
return {
|
||||
areObjectsFetched,
|
||||
alertMessages: getAlertMessages,
|
||||
controlDomainVdis: getControlDomainVdis,
|
||||
userSrs: getUserSrs,
|
||||
vdiOrphaned: getOrphanVdiSnapshots,
|
||||
vdiSr: getVdiSrs,
|
||||
@@ -464,11 +483,6 @@ export default class Health extends Component {
|
||||
this._getPoolPredicate
|
||||
)
|
||||
|
||||
_getControlDomainVdis = createFilter(
|
||||
() => this.props.controlDomainVdis,
|
||||
this._getPoolPredicate
|
||||
)
|
||||
|
||||
_getVmOrphaned = createFilter(
|
||||
() => this.props.vmOrphaned,
|
||||
this._getPoolPredicate
|
||||
@@ -569,16 +583,7 @@ export default class Health extends Component {
|
||||
<Icon icon='disk' /> {_('vdisOnControlDomain')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
collection={
|
||||
props.areObjectsFetched
|
||||
? this._getControlDomainVdis()
|
||||
: null
|
||||
}
|
||||
columns={CONTROL_DOMAIN_VDI_COLUMNS}
|
||||
component={SortedTable}
|
||||
emptyMessage={_('noControlDomainVdis')}
|
||||
/>
|
||||
<AttachedVdisTable poolPredicate={this._getPoolPredicate()} />
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
Reference in New Issue
Block a user