feat(xo-web/home): don't use SortedTable for VMs (not) backed up (#5046)
Introduced by f736381933
This commit is contained in:
parent
1bd504d67e
commit
52020abde8
@ -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
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user