feat(xo-web/home): don't use SortedTable for VMs (not) backed up (#5046)

Introduced by f736381933
This commit is contained in:
Rajaa.BARHTAOUI 2020-06-24 14:12:37 +02:00 committed by GitHub
parent 1bd504d67e
commit 52020abde8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 102 additions and 337 deletions

View File

@ -13,6 +13,7 @@
- [Home/Template] Ability to copy/clone VM templates [#4734](https://github.com/vatesfr/xen-orchestra/issues/4734) (PR [#5006](https://github.com/vatesfr/xen-orchestra/pull/5006))
- [VM/bulk copy] Add fast clone option (PR [#5006](https://github.com/vatesfr/xen-orchestra/pull/5006))
- [VM] Differentiate PV drivers detection from management agent detection [#4783](https://github.com/vatesfr/xen-orchestra/issues/4783) (PR [#5007](https://github.com/vatesfr/xen-orchestra/pull/5007))
- [Home/VM] Homogenize the list of backed up VMs with the normal list (PR [#5046](https://github.com/vatesfr/xen-orchestra/pull/5046)
### Bug fixes

View File

@ -262,7 +262,6 @@ 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',

View File

@ -248,8 +248,6 @@ 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(
@ -314,7 +312,6 @@ class SortedTable extends Component {
}
static defaultProps = {
displayFilter: true,
itemsPerPage: 10,
}
@ -816,7 +813,6 @@ class SortedTable extends Component {
const { props, state } = this
const {
actions,
displayFilter,
filterContainer,
individualActions,
itemsPerPage,
@ -849,7 +845,7 @@ class SortedTable extends Component {
/>
)
const filterInstance = displayFilter && (
const filterInstance = (
<TableFilter
filters={props.filters}
onChange={this._setFilter}
@ -990,14 +986,13 @@ class SortedTable extends Component {
))}
</Col>
<Col mediumSize={4}>
{displayFilter &&
(filterContainer ? (
<Portal container={() => filterContainer()}>
{filterInstance}
</Portal>
) : (
filterInstance
))}
{filterContainer ? (
<Portal container={() => filterContainer()}>
{filterInstance}
</Portal>
) : (
filterInstance
)}
</Col>
</SingleLineRow>
</Container>

View File

@ -1,236 +0,0 @@
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

View File

@ -21,9 +21,11 @@ import { Card, CardHeader, CardBlock } from 'card'
import {
ceil,
debounce,
differenceBy,
escapeRegExp,
filter,
find,
flatMap,
forEach,
identity,
includes,
@ -31,10 +33,12 @@ import {
keys,
map,
mapValues,
omit,
pick,
pickBy,
size,
some,
uniq,
} from 'lodash'
import {
addCustomFilter,
@ -56,11 +60,13 @@ import {
startVms,
stopHosts,
stopVms,
subscribeBackupNgJobs,
subscribeResourceSets,
subscribeServers,
suspendVms,
} from 'xo'
import { Container, Row, Col } from 'grid'
import { createPredicate } from 'value-matcher'
import {
SelectHost,
SelectPool,
@ -89,7 +95,6 @@ import {
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'
@ -459,6 +464,7 @@ const NoObjects = props =>
)
@addSubscriptions({
jobs: subscribeBackupNgJobs,
noResourceSets: cb => subscribeResourceSets(data => cb(isEmpty(data))),
})
@connectStore(() => {
@ -703,7 +709,26 @@ export default class Home extends Component {
})
_getFilteredItems = createSort(
createFilter(() => this.props.items, this._getFilterFunction),
createSelector(
createFilter(() => this.props.items, this._getFilterFunction),
() => this.props.location.query.backup,
() => this.props.jobs,
(filteredItems, backup, jobs) => {
if (backup === undefined) {
return filteredItems
}
const backedUpVms = uniq(
flatMap(jobs, job =>
filter(filteredItems, createPredicate(omit(job.vms, 'power_state')))
)
)
return backup === 'true'
? backedUpVms
: differenceBy(map(filteredItems), backedUpVms, 'id')
}
),
createSelector(
() => this.state.sortBy,
sortBy => [sortBy, 'name_label']
@ -915,14 +940,7 @@ export default class Home extends Component {
const customFilters = this._getCustomFilters()
const filteredItems = this._getFilteredItems()
const nItems = this._getNumberOfItems()
const {
isAdmin,
isPoolAdmin,
items,
location,
noResourceSets,
type,
} = this.props
const { isAdmin, isPoolAdmin, items, noResourceSets, type } = this.props
const {
homeItemsPerPage,
@ -943,10 +961,6 @@ 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}>
@ -1028,29 +1042,27 @@ export default class Home extends Component {
</Row>
<Row className={classNames(styles.itemRowHeader, 'mt-1')}>
<Col smallSize={6} mediumSize={2}>
{!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>
<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() ? (
@ -1210,7 +1222,6 @@ export default class Home extends Component {
)}
<DropdownButton
bsStyle='link'
disabled={disableHomeFeatures}
id='sort'
title={_('homeSortBy')}
>
@ -1237,11 +1248,9 @@ export default class Home extends Component {
)}
</Col>
<Col smallSize={6} mediumSize={2} className='text-xs-right'>
{!disableHomeFeatures && (
<Button onClick={this._expandAll}>
<Icon icon='nav' />
</Button>
)}{' '}
<Button onClick={this._expandAll}>
<Icon icon='nav' />
</Button>{' '}
<DropdownButton bsStyle='info' title={homeItemsPerPage}>
{ITEMS_PER_PAGE_OPTIONS.map(nItems => (
<MenuItem
@ -1318,55 +1327,52 @@ export default class Home extends Component {
name='Home'
targetNodeSelector='body'
/>
{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
<div>
{backup !== undefined && (
<h5>
{backup === 'true' ? _('backedUpVms') : _('notBackedUpVms')}
</h5>
)}
<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}
key={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>
onSelect={this.toggleState(`selectedItems.${item.id}`)}
selected={Boolean(selectedItems[item.id])}
/>
</div>
</Row>
))
)}
</div>
) : (
<BackedUpVms
itemsPerPage={homeItemsPerPage}
showBackedUpVms={backup === 'true'}
vms={filteredItems}
/>
)}
{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>
</Page>
)
}