feat(xo-web/home): ability to list VMs which are (not) backed up (#4974)
Fixes #4777
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
- [Audit] Record failed connection attempts [#4844](https://github.com/vatesfr/xen-orchestra/issues/4844) (PR [#4900](https://github.com/vatesfr/xen-orchestra/pull/4900))
|
||||
- [XO config export] Ability to encrypt the exported file (PR [#4997](https://github.com/vatesfr/xen-orchestra/pull/4997))
|
||||
- [SDN Controller] Ability to choose host as preferred center at private network creation [#4991](https://github.com/vatesfr/xen-orchestra/issues/4991) (PR [#5000](https://github.com/vatesfr/xen-orchestra/pull/5000))
|
||||
- [Home/VM] Ability to list VMs which are (not) backed up [#4777](https://github.com/vatesfr/xen-orchestra/issues/4777) (PR [#4974](https://github.com/vatesfr/xen-orchestra/pull/4974))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
||||
@@ -194,6 +194,9 @@ const messages = {
|
||||
editUserProfile: 'Edit my settings {username}',
|
||||
|
||||
// ----- Home view ------
|
||||
allVms: 'All VMs',
|
||||
backedUpVms: 'Backed up VMs',
|
||||
notBackedUpVms: 'Not backed up VMs',
|
||||
homeFetchingData: 'Fetching data…',
|
||||
homeWelcome: 'Welcome to Xen Orchestra!',
|
||||
homeWelcomeText: 'Add your XenServer hosts or pools',
|
||||
@@ -257,6 +260,7 @@ const messages = {
|
||||
warningHostTimeTooltip:
|
||||
'Host time and XOA time are not consistent with each other',
|
||||
selectExistingTags: 'Select from existing tags',
|
||||
description: 'Description',
|
||||
|
||||
// ----- Home snapshots -----
|
||||
snapshotVmsName: 'Name',
|
||||
|
||||
@@ -248,6 +248,8 @@ class SortedTable extends Component {
|
||||
static propTypes = {
|
||||
defaultColumn: PropTypes.number,
|
||||
defaultFilter: PropTypes.string,
|
||||
// To not duplicate filter on the home page.
|
||||
displayFilter: PropTypes.bool,
|
||||
collection: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||
.isRequired,
|
||||
columns: PropTypes.arrayOf(
|
||||
@@ -288,6 +290,10 @@ class SortedTable extends Component {
|
||||
individualLabel: PropTypes.node,
|
||||
label: PropTypes.node.isRequired,
|
||||
level: PropTypes.oneOf(['primary', 'warning', 'danger']),
|
||||
redirectOnSuccess: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.string,
|
||||
]),
|
||||
})
|
||||
),
|
||||
groupedActions: actionsShape,
|
||||
@@ -308,6 +314,7 @@ class SortedTable extends Component {
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
displayFilter: true,
|
||||
itemsPerPage: 10,
|
||||
}
|
||||
|
||||
@@ -718,6 +725,7 @@ class SortedTable extends Component {
|
||||
icon: a.icon,
|
||||
label: a.individualLabel !== undefined ? a.individualLabel : a.label,
|
||||
level: a.level,
|
||||
redirectOnSuccess: a.redirectOnSuccess,
|
||||
}))
|
||||
|
||||
return sortBy(
|
||||
@@ -808,6 +816,7 @@ class SortedTable extends Component {
|
||||
const { props, state } = this
|
||||
const {
|
||||
actions,
|
||||
displayFilter,
|
||||
filterContainer,
|
||||
individualActions,
|
||||
itemsPerPage,
|
||||
@@ -840,7 +849,7 @@ class SortedTable extends Component {
|
||||
/>
|
||||
)
|
||||
|
||||
const filterInstance = (
|
||||
const filterInstance = displayFilter && (
|
||||
<TableFilter
|
||||
filters={props.filters}
|
||||
onChange={this._setFilter}
|
||||
@@ -981,13 +990,14 @@ class SortedTable extends Component {
|
||||
))}
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
{filterContainer ? (
|
||||
<Portal container={() => filterContainer()}>
|
||||
{filterInstance}
|
||||
</Portal>
|
||||
) : (
|
||||
filterInstance
|
||||
)}
|
||||
{displayFilter &&
|
||||
(filterContainer ? (
|
||||
<Portal container={() => filterContainer()}>
|
||||
{filterInstance}
|
||||
</Portal>
|
||||
) : (
|
||||
filterInstance
|
||||
))}
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>
|
||||
|
||||
@@ -613,6 +613,11 @@ export const createCompare = criterias => (...items) => {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const createCompareContainers = poolId =>
|
||||
createCompare([c => c.$pool === poolId, c => c.type === 'pool'])
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const hasLicenseRestrictions = host => {
|
||||
const licenseType = host.license_params.sku_type
|
||||
return (
|
||||
|
||||
@@ -4,14 +4,12 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import { Container, Col } from 'grid'
|
||||
import { createCompare } from 'utils'
|
||||
import { createCompare, createCompareContainers } from 'utils'
|
||||
import { createSelector } from 'selectors'
|
||||
import { SelectSr } from 'select-objects'
|
||||
|
||||
import { isSrShared } from '../'
|
||||
|
||||
const createCompareContainers = poolId =>
|
||||
createCompare([_ => _.$pool === poolId, _ => _.type === 'pool'])
|
||||
const compareSrs = createCompare([isSrShared])
|
||||
|
||||
export default class MigrateVdiModalBody extends Component {
|
||||
|
||||
236
packages/xo-web/src/xo-app/home/backed-up-vms.js
Normal file
236
packages/xo-web/src/xo-app/home/backed-up-vms.js
Normal file
@@ -0,0 +1,236 @@
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions, connectStore, createCompareContainers } from 'utils'
|
||||
import {
|
||||
copyVms,
|
||||
deleteVms,
|
||||
editVm,
|
||||
migrateVm,
|
||||
migrateVms,
|
||||
pauseVms,
|
||||
restartVms,
|
||||
snapshotVms,
|
||||
startVms,
|
||||
stopVms,
|
||||
subscribeBackupNgJobs,
|
||||
suspendVms,
|
||||
} from 'xo'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { createPredicate } from 'value-matcher'
|
||||
import {
|
||||
difference,
|
||||
filter,
|
||||
flatMap,
|
||||
isEmpty,
|
||||
map,
|
||||
omit,
|
||||
some,
|
||||
uniq,
|
||||
} from 'lodash'
|
||||
import { Host, Pool } from 'render-xo-item'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { Text, XoSelect } from 'editable'
|
||||
|
||||
const getVmUrl = ({ id }) => `vms/${id}/general`
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: _('name'),
|
||||
itemRenderer: vm => {
|
||||
const operations = vm.current_operations
|
||||
const state = isEmpty(operations) ? vm.power_state : 'Busy'
|
||||
return (
|
||||
<span style={{ whiteSpace: 'nowrap' }}>
|
||||
<Tooltip
|
||||
content={
|
||||
<span>
|
||||
{_(`powerState${state}`)}
|
||||
{state === 'Busy' && (
|
||||
<span> ({Object.values(operations)[0]})</span>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Icon icon={state.toLowerCase()} />
|
||||
</Tooltip>
|
||||
<Text
|
||||
value={vm.name_label}
|
||||
onChange={value => editVm(vm, { name_label: value })}
|
||||
placeholder={_('vmHomeNamePlaceholder')}
|
||||
useLongClick
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
sortCriteria: 'name_label',
|
||||
},
|
||||
{
|
||||
name: _('description'),
|
||||
itemRenderer: vm => (
|
||||
<Text
|
||||
value={vm.name_description}
|
||||
onChange={value => editVm(vm, { name_description: value })}
|
||||
placeholder={_('vmHomeDescriptionPlaceholder')}
|
||||
useLongClick
|
||||
/>
|
||||
),
|
||||
sortCriteria: 'name_description',
|
||||
},
|
||||
{
|
||||
name: _('containersTabName'),
|
||||
itemRenderer: (vm, { pools, hosts }) => {
|
||||
let container
|
||||
return vm.power_state === 'Running' &&
|
||||
(container = hosts[vm.$container]) !== undefined ? (
|
||||
<XoSelect
|
||||
compareContainers={createCompareContainers(vm.$pool)}
|
||||
labelProp='name_label'
|
||||
onChange={host => migrateVm(vm, host)}
|
||||
placeholder={_('homeMigrateTo')}
|
||||
useLongClick
|
||||
value={container}
|
||||
xoType='host'
|
||||
>
|
||||
<Host id={container.id} link />
|
||||
</XoSelect>
|
||||
) : (
|
||||
(container = pools[vm.$container]) !== undefined && (
|
||||
<Pool id={container.id} link />
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const ACTIONS = [
|
||||
{
|
||||
disabled: vms => some(vms, { power_state: 'Running' }),
|
||||
handler: startVms,
|
||||
icon: 'vm-start',
|
||||
label: _('startVmLabel'),
|
||||
},
|
||||
{
|
||||
disabled: vms => some(vms, { power_state: 'Halted' }),
|
||||
handler: vms => stopVms(vms),
|
||||
icon: 'vm-stop',
|
||||
label: _('stopVmLabel'),
|
||||
},
|
||||
{
|
||||
handler: migrateVms,
|
||||
icon: 'vm-migrate',
|
||||
label: _('migrateVmLabel'),
|
||||
},
|
||||
{
|
||||
handler: snapshotVms,
|
||||
icon: 'vm-snapshot',
|
||||
label: _('snapshotVmLabel'),
|
||||
},
|
||||
{
|
||||
handler: copyVms,
|
||||
icon: 'vm-copy',
|
||||
label: _('copyVmLabel'),
|
||||
},
|
||||
{
|
||||
handler: (vms, { setHomeVmIdsSelection }) => {
|
||||
setHomeVmIdsSelection(map(vms, 'id'))
|
||||
},
|
||||
icon: 'backup',
|
||||
label: _('backupLabel'),
|
||||
redirectOnSuccess: '/backup/new/vms',
|
||||
},
|
||||
{
|
||||
handler: deleteVms,
|
||||
icon: 'vm-delete',
|
||||
label: _('vmRemoveButton'),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const GROUPED_ACTIONS = [
|
||||
{
|
||||
disabled: vms => some(vms, _ => _.power_state !== 'Running'),
|
||||
handler: vms => restartVms(vms),
|
||||
icon: 'vm-reboot',
|
||||
label: _('rebootVmLabel'),
|
||||
},
|
||||
{
|
||||
disabled: vms => some(vms, _ => _.power_state !== 'Running'),
|
||||
handler: pauseVms,
|
||||
icon: 'vm-pause',
|
||||
label: _('pauseVmLabel'),
|
||||
},
|
||||
{
|
||||
disabled: vms => some(vms, _ => _.power_state !== 'Running'),
|
||||
handler: suspendVms,
|
||||
icon: 'vm-suspend',
|
||||
label: _('suspendVmLabel'),
|
||||
},
|
||||
]
|
||||
|
||||
const BackedUpVms = decorate([
|
||||
addSubscriptions({
|
||||
jobs: subscribeBackupNgJobs,
|
||||
}),
|
||||
connectStore(() => ({
|
||||
hosts: createGetObjectsOfType('host'),
|
||||
pools: createGetObjectsOfType('pool'),
|
||||
})),
|
||||
provideState({
|
||||
computed: {
|
||||
backedUpVms: (_, { showBackedUpVms, jobs, vms }) =>
|
||||
uniq(
|
||||
flatMap(jobs, job =>
|
||||
filter(vms, createPredicate(omit(job.vms, 'power_state')))
|
||||
)
|
||||
),
|
||||
collection: (state, { showBackedUpVms }) =>
|
||||
showBackedUpVms ? state.backedUpVms : state.notBackedUpVms,
|
||||
notBackedUpVms: ({ backedUpVms }, { vms }) =>
|
||||
difference(vms, backedUpVms),
|
||||
title: (state, { showBackedUpVms }) =>
|
||||
showBackedUpVms ? _('backedUpVms') : _('notBackedUpVms'),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({
|
||||
hosts,
|
||||
itemsPerPage,
|
||||
pools,
|
||||
setHomeVmIdsSelection,
|
||||
state: { collection, title },
|
||||
}) => (
|
||||
<div>
|
||||
<h5>{title}</h5>
|
||||
<SortedTable
|
||||
actions={ACTIONS}
|
||||
collection={collection}
|
||||
columns={COLUMNS}
|
||||
data-hosts={hosts}
|
||||
data-pools={pools}
|
||||
data-setHomeVmIdsSelection={setHomeVmIdsSelection}
|
||||
displayFilter={false}
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
itemsPerPage={itemsPerPage}
|
||||
rowLink={getVmUrl}
|
||||
shortcutsTarget='body'
|
||||
stateUrlParam='s_backup'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
])
|
||||
|
||||
BackedUpVms.propTypes = {
|
||||
showBackedUpVms: PropTypes.bool,
|
||||
vms: PropTypes.arrayOf(PropTypes.object),
|
||||
}
|
||||
|
||||
BackedUpVms.defaultProps = {
|
||||
showBackedUpVms: true,
|
||||
}
|
||||
|
||||
export default BackedUpVms
|
||||
@@ -86,8 +86,10 @@ import {
|
||||
OverlayTrigger,
|
||||
Popover,
|
||||
} from 'react-bootstrap-4/lib'
|
||||
import { Select } from 'form'
|
||||
|
||||
import styles from './index.css'
|
||||
import BackedUpVms from './backed-up-vms'
|
||||
import HostItem from './host-item'
|
||||
import PoolItem from './pool-item'
|
||||
import VmItem from './vm-item'
|
||||
@@ -308,6 +310,12 @@ const TYPES = {
|
||||
|
||||
const DEFAULT_TYPE = 'VM'
|
||||
|
||||
const BACKUP_FILTERS = [
|
||||
{ value: 'all', label: _('allVms') },
|
||||
{ value: 'backedUpVms', label: _('backedUpVms') },
|
||||
{ value: 'notBackedUpVms', label: _('notBackedUpVms') },
|
||||
]
|
||||
|
||||
@connectStore(() => {
|
||||
const noServersConnected = invoke(
|
||||
createGetObjectsOfType('host'),
|
||||
@@ -544,7 +552,14 @@ export default class Home extends Component {
|
||||
const { pathname, query } = this.props.location
|
||||
this.context.router.push({
|
||||
pathname,
|
||||
query: { ...query, t: type, s: undefined, p: 1 },
|
||||
query: {
|
||||
...query,
|
||||
backup: undefined,
|
||||
p: 1,
|
||||
s: undefined,
|
||||
s_backup: undefined,
|
||||
t: type,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -843,7 +858,7 @@ export default class Home extends Component {
|
||||
: items.length - 1,
|
||||
})
|
||||
break
|
||||
case 'SELECT':
|
||||
case 'SELECT': {
|
||||
const itemId = items[this.state.highlighted].id
|
||||
this.setState({
|
||||
selectedItems: {
|
||||
@@ -852,24 +867,57 @@ export default class Home extends Component {
|
||||
},
|
||||
})
|
||||
break
|
||||
case 'JUMP_INTO':
|
||||
}
|
||||
case 'JUMP_INTO': {
|
||||
const item = items[this.state.highlighted]
|
||||
if (includes(['VM', 'host', 'pool', 'SR'], item && item.type)) {
|
||||
this.context.router.push({
|
||||
pathname: `${item.type.toLowerCase()}s/${item.id}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Header --------------------------------------------------------------------
|
||||
|
||||
_getBackupFilter = createSelector(
|
||||
() => this.props.location.query.backup,
|
||||
backup =>
|
||||
backup === undefined
|
||||
? 'all'
|
||||
: backup === 'true'
|
||||
? 'backedUpVms'
|
||||
: 'notBackedUpVms'
|
||||
)
|
||||
|
||||
_setBackupFilter = backupFilter => {
|
||||
const { pathname, query } = this.props.location
|
||||
const isAll = backupFilter === 'all'
|
||||
this.context.router.push({
|
||||
pathname,
|
||||
query: {
|
||||
...query,
|
||||
backup: isAll ? undefined : backupFilter === 'backedUpVms',
|
||||
p: isAll ? 1 : undefined,
|
||||
s_backup: undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
_renderHeader() {
|
||||
const customFilters = this._getCustomFilters()
|
||||
const filteredItems = this._getFilteredItems()
|
||||
const nItems = this._getNumberOfItems()
|
||||
const { isAdmin, isPoolAdmin, items, noResourceSets, type } = this.props
|
||||
const {
|
||||
isAdmin,
|
||||
isPoolAdmin,
|
||||
items,
|
||||
location,
|
||||
noResourceSets,
|
||||
type,
|
||||
} = this.props
|
||||
|
||||
const {
|
||||
homeItemsPerPage,
|
||||
@@ -890,6 +938,10 @@ export default class Home extends Component {
|
||||
showResourceSetsSelector,
|
||||
} = options
|
||||
|
||||
// Disable all the features that are already handled by the SortedTable
|
||||
// or irrelevant with the SortedTable.
|
||||
const disableHomeFeatures = location.query.backup !== undefined
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row className={styles.itemRowHeader}>
|
||||
@@ -971,25 +1023,29 @@ export default class Home extends Component {
|
||||
</Row>
|
||||
<Row className={classNames(styles.itemRowHeader, 'mt-1')}>
|
||||
<Col smallSize={6} mediumSize={2}>
|
||||
<input
|
||||
checked={this._getIsAllSelected()}
|
||||
onChange={this._toggleMaster}
|
||||
ref='masterCheckbox'
|
||||
type='checkbox'
|
||||
/>{' '}
|
||||
<span className='text-muted'>
|
||||
{this._getNumberOfSelectedItems()
|
||||
? _('homeSelectedItems', {
|
||||
icon: <Icon icon={type.toLowerCase()} />,
|
||||
selected: this._getNumberOfSelectedItems(),
|
||||
total: nItems,
|
||||
})
|
||||
: _('homeDisplayedItems', {
|
||||
displayed: filteredItems.length,
|
||||
icon: <Icon icon={type.toLowerCase()} />,
|
||||
total: nItems,
|
||||
})}
|
||||
</span>
|
||||
{!disableHomeFeatures && (
|
||||
<span>
|
||||
<input
|
||||
checked={this._getIsAllSelected()}
|
||||
onChange={this._toggleMaster}
|
||||
ref='masterCheckbox'
|
||||
type='checkbox'
|
||||
/>{' '}
|
||||
<span className='text-muted'>
|
||||
{this._getNumberOfSelectedItems()
|
||||
? _('homeSelectedItems', {
|
||||
icon: <Icon icon={type.toLowerCase()} />,
|
||||
selected: this._getNumberOfSelectedItems(),
|
||||
total: nItems,
|
||||
})
|
||||
: _('homeDisplayedItems', {
|
||||
displayed: filteredItems.length,
|
||||
icon: <Icon icon={type.toLowerCase()} />,
|
||||
total: nItems,
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={8} className='text-xs-right hidden-sm-down'>
|
||||
{this._getNumberOfSelectedItems() ? (
|
||||
@@ -1033,6 +1089,33 @@ export default class Home extends Component {
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{type === 'VM' && (
|
||||
<OverlayTrigger
|
||||
trigger='click'
|
||||
rootClose
|
||||
placement='bottom'
|
||||
overlay={
|
||||
<Popover
|
||||
className={styles.selectObject}
|
||||
id='backupPopover'
|
||||
>
|
||||
<Select
|
||||
autoFocus
|
||||
onChange={this._setBackupFilter}
|
||||
openOnFocus
|
||||
options={BACKUP_FILTERS}
|
||||
required
|
||||
simpleValue
|
||||
value={this._getBackupFilter()}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<Button btnStyle='link'>
|
||||
<Icon icon='backup' /> {_('backup')}
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{showPoolsSelector && (
|
||||
<OverlayTrigger
|
||||
trigger='click'
|
||||
@@ -1122,6 +1205,7 @@ export default class Home extends Component {
|
||||
)}
|
||||
<DropdownButton
|
||||
bsStyle='link'
|
||||
disabled={disableHomeFeatures}
|
||||
id='sort'
|
||||
title={_('homeSortBy')}
|
||||
>
|
||||
@@ -1148,9 +1232,11 @@ export default class Home extends Component {
|
||||
)}
|
||||
</Col>
|
||||
<Col smallSize={6} mediumSize={2} className='text-xs-right'>
|
||||
<Button onClick={this._expandAll}>
|
||||
<Icon icon='nav' />
|
||||
</Button>{' '}
|
||||
{!disableHomeFeatures && (
|
||||
<Button onClick={this._expandAll}>
|
||||
<Icon icon='nav' />
|
||||
</Button>
|
||||
)}{' '}
|
||||
<DropdownButton bsStyle='info' title={homeItemsPerPage}>
|
||||
{ITEMS_PER_PAGE_OPTIONS.map(nItems => (
|
||||
<MenuItem
|
||||
@@ -1174,7 +1260,11 @@ export default class Home extends Component {
|
||||
areObjectsFetched,
|
||||
isAdmin,
|
||||
isPoolAdmin,
|
||||
location: {
|
||||
query: { backup },
|
||||
},
|
||||
noResourceSets,
|
||||
type,
|
||||
} = this.props
|
||||
|
||||
if (!areObjectsFetched) {
|
||||
@@ -1201,8 +1291,13 @@ export default class Home extends Component {
|
||||
|
||||
const filteredItems = this._getFilteredItems()
|
||||
const visibleItems = this._getVisibleItems()
|
||||
const { Item } = OPTIONS[this.props.type]
|
||||
const { expandAll, highlighted, selectedItems } = this.state
|
||||
const { Item } = OPTIONS[type]
|
||||
const {
|
||||
expandAll,
|
||||
highlighted,
|
||||
homeItemsPerPage,
|
||||
selectedItems,
|
||||
} = this.state
|
||||
|
||||
// Necessary because indeterminate cannot be used as an attribute
|
||||
if (this.refs.masterCheckbox) {
|
||||
@@ -1218,49 +1313,55 @@ export default class Home extends Component {
|
||||
name='Home'
|
||||
targetNodeSelector='body'
|
||||
/>
|
||||
<div>
|
||||
<div className={styles.itemContainer}>
|
||||
{isEmpty(filteredItems) ? (
|
||||
<p className='text-xs-center mt-1'>
|
||||
<a className='btn btn-link' onClick={this._clearFilter}>
|
||||
<Icon icon='info' /> {_('homeNoMatches')}
|
||||
</a>
|
||||
</p>
|
||||
) : (
|
||||
map(visibleItems, (item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={
|
||||
highlighted === index ? styles.highlight : undefined
|
||||
}
|
||||
>
|
||||
<Item
|
||||
expandAll={expandAll}
|
||||
item={item}
|
||||
{backup === undefined ? (
|
||||
<div>
|
||||
<div className={styles.itemContainer}>
|
||||
{isEmpty(filteredItems) ? (
|
||||
<p className='text-xs-center mt-1'>
|
||||
<a className='btn btn-link' onClick={this._clearFilter}>
|
||||
<Icon icon='info' /> {_('homeNoMatches')}
|
||||
</a>
|
||||
</p>
|
||||
) : (
|
||||
map(visibleItems, (item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onSelect={this.toggleState(`selectedItems.${item.id}`)}
|
||||
selected={Boolean(selectedItems[item.id])}
|
||||
/>
|
||||
className={
|
||||
highlighted === index ? styles.highlight : undefined
|
||||
}
|
||||
>
|
||||
<Item
|
||||
expandAll={expandAll}
|
||||
item={item}
|
||||
key={item.id}
|
||||
onSelect={this.toggleState(`selectedItems.${item.id}`)}
|
||||
selected={Boolean(selectedItems[item.id])}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{filteredItems.length > homeItemsPerPage && (
|
||||
<Row>
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
<div style={{ margin: 'auto' }}>
|
||||
<Pagination
|
||||
onChange={this._onPageSelection}
|
||||
pages={ceil(filteredItems.length / homeItemsPerPage)}
|
||||
value={this._getPage()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
{filteredItems.length > this.state.homeItemsPerPage && (
|
||||
<Row>
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
<div style={{ margin: 'auto' }}>
|
||||
<Pagination
|
||||
onChange={this._onPageSelection}
|
||||
pages={ceil(
|
||||
filteredItems.length / this.state.homeItemsPerPage
|
||||
)}
|
||||
value={this._getPage()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<BackedUpVms
|
||||
itemsPerPage={homeItemsPerPage}
|
||||
showBackedUpVms={backup === 'true'}
|
||||
vms={filteredItems}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
createCompare,
|
||||
createCompareContainers,
|
||||
formatSize,
|
||||
noop,
|
||||
resolveResourceSet,
|
||||
@@ -66,8 +67,6 @@ import {
|
||||
subscribeResourceSets,
|
||||
} from 'xo'
|
||||
|
||||
const createCompareContainers = poolId =>
|
||||
createCompare([c => c.$pool === poolId, c => c.type === 'pool'])
|
||||
const compareSrs = createCompare([isSrShared])
|
||||
|
||||
class VdiSr extends Component {
|
||||
|
||||
Reference in New Issue
Block a user