feat(xo-web/new-xosan): use SortedTable (#3691)

See #2416
This commit is contained in:
Rajaa.BARHTAOUI 2018-12-10 16:15:04 +01:00 committed by Pierre Donias
parent cc26e378e5
commit 48727740c4
5 changed files with 168 additions and 159 deletions

View File

@ -11,6 +11,7 @@
- [Servers] Auto-connect to ejected host [#2238](https://github.com/vatesfr/xen-orchestra/issues/2238) (PR [#3738](https://github.com/vatesfr/xen-orchestra/pull/3738))
- [Backup NG] Add "XOSAN" in excluded tags by default [#2128](https://github.com/vatesfr/xen-orchestra/issues/3563) (PR [#3559](https://github.com/vatesfr/xen-orchestra/pull/3563))
- [VM] add tooltip for VM status icon [#3749](https://github.com/vatesfr/xen-orchestra/issues/3749) (PR [#3765](https://github.com/vatesfr/xen-orchestra/pull/3765))
- [New XOSAN] Improve view and possibility to sort SRs by name/size/free space [#2416](https://github.com/vatesfr/xen-orchestra/issues/2416) (PR [#3691](https://github.com/vatesfr/xen-orchestra/pull/3691))
### Bug fixes

View File

@ -1947,7 +1947,7 @@ const messages = {
xosanRestartAgents: 'Restart toolstacks',
xosanMasterOffline: 'Pool master is not running',
xosanInstallPackTitle: 'Install XOSAN pack on {pool}',
xosanSelect2Srs: 'Select at least 2 SRs',
xosanSrOnSameHostMessage: 'Select no more than 1 SR per host',
xosanLayout: 'Layout',
xosanRedundancy: 'Redundancy',
xosanCapacity: 'Capacity',

View File

@ -71,7 +71,8 @@ export const Host = decorate([
pool: createGetObject(
createSelector(
getHost,
host => get(() => host.$pool)
(_, props) => props.pool,
(host, showPool) => showPool && get(() => host.$pool)
)
),
}
@ -94,11 +95,13 @@ Host.propTypes = {
id: PropTypes.string.isRequired,
link: PropTypes.bool,
newTab: PropTypes.bool,
pool: PropTypes.bool,
}
Host.defaultProps = {
link: false,
newTab: false,
pool: true,
}
// ===================================================================
@ -183,7 +186,8 @@ export const Sr = decorate([
const getContainer = createGetObject(
createSelector(
getSr,
sr => get(() => sr.$container)
(_, props) => props.container,
(sr, showContainer) => showContainer && get(() => sr.$container)
)
)
return (state, props) => ({
@ -217,6 +221,7 @@ export const Sr = decorate([
])
Sr.propTypes = {
container: PropTypes.bool,
id: PropTypes.string.isRequired,
link: PropTypes.bool,
newTab: PropTypes.bool,
@ -225,6 +230,7 @@ Sr.propTypes = {
}
Sr.defaultProps = {
container: true,
link: false,
newTab: false,
self: false,

View File

@ -71,11 +71,11 @@ class TableFilter extends Component {
this.props.onChange(event.target.value)
}
focus () {
focus() {
this.refs.filter.getWrappedInstance().focus()
}
render () {
render() {
const { props } = this
return (
@ -140,7 +140,7 @@ class ColumnHead extends Component {
props.sort(props.columnId)
}
render () {
render() {
const { name, sortIcon, textAlign } = this.props
if (!this.props.sort) {
@ -174,7 +174,7 @@ class Checkbox extends Component {
indeterminate: PropTypes.bool.isRequired,
}
componentDidUpdate () {
componentDidUpdate() {
const {
props: { indeterminate },
ref,
@ -189,7 +189,7 @@ class Checkbox extends Component {
this.componentDidUpdate()
}
render () {
render() {
const { indeterminate, ...props } = this.props
props.ref = this._ref
props.type = 'checkbox'
@ -295,6 +295,7 @@ export default class SortedTable extends Component {
groupedActions: actionsShape,
individualActions: actionsShape,
itemsPerPage: PropTypes.number,
onSelect: PropTypes.func,
paginationContainer: PropTypes.func,
rowAction: PropTypes.func,
rowLink: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
@ -316,7 +317,7 @@ export default class SortedTable extends Component {
router: routerShape,
}
constructor (props, context) {
constructor(props, context) {
super(props, context)
this._getUserData =
@ -505,7 +506,7 @@ export default class SortedTable extends Component {
)
}
componentDidMount () {
componentDidMount() {
this._checkUpdatePage()
// Force one Portal refresh.
@ -532,7 +533,7 @@ export default class SortedTable extends Component {
})
}
componentDidUpdate () {
componentDidUpdate() {
const { selectedItemsIds } = this.state
// Unselect items that are no longer visible
@ -567,7 +568,7 @@ export default class SortedTable extends Component {
}
// update state in the state and update the URL param
_setVisibleState (state) {
_setVisibleState(state) {
this.setState(state, this.props.stateUrlParam && this._saveUrlState)
}
@ -579,7 +580,7 @@ export default class SortedTable extends Component {
})
}
_checkUpdatePage () {
_checkUpdatePage() {
const { page } = this.state
if (page === 1) {
return
@ -597,15 +598,21 @@ export default class SortedTable extends Component {
}
}
_setPage (page) {
_setPage(page) {
this._setVisibleState({ page })
}
_setPage = this._setPage.bind(this)
_selectAllVisibleItems = event => {
const { checked } = event.target
const { onSelect } = this.props
if (onSelect !== undefined) {
onSelect(checked ? map(this._getVisibleItems(), 'id') : [])
}
this.setState({
all: false,
selectedItemsIds: event.target.checked
selectedItemsIds: checked
? this.state.selectedItemsIds.union(map(this._getVisibleItems(), 'id'))
: this.state.selectedItemsIds.clear(),
})
@ -626,34 +633,37 @@ export default class SortedTable extends Component {
}
}
_selectAll = () => this.setState({ all: true })
_selectAll = () => {
const { onSelect } = this.props
if (onSelect !== undefined) {
onSelect(map(this._getItems(), 'id'))
}
this.setState({ all: true })
}
_selectItem (current, selected, range = false) {
_selectItem(current, selected, range = false) {
const { onSelect } = this.props
const { all, selectedItemsIds } = this.state
const visibleItems = this._getVisibleItems()
const item = visibleItems[current]
let _selectedItemsIds
if (all) {
return this.setState({
all: false,
selectedItemsIds: new Set().withMutations(selectedItemsIds => {
forEach(visibleItems, item => {
selectedItemsIds.add(item.id)
})
selectedItemsIds.delete(item.id)
}),
_selectedItemsIds = new Set().withMutations(selectedItemsIds => {
forEach(visibleItems, item => {
selectedItemsIds.add(item.id)
})
selectedItemsIds.delete(item.id)
})
}
} else {
const method = (selected === undefined
? !selectedItemsIds.has(item.id)
: selected)
? 'add'
: 'delete'
const method = (selected === undefined
? !selectedItemsIds.has(item.id)
: selected)
? 'add'
: 'delete'
let previous
this.setState({
selectedItemsIds:
let previous
_selectedItemsIds =
range && (previous = this._previous) !== undefined
? selectedItemsIds.withMutations(selectedItemsIds => {
let i = previous
@ -666,10 +676,18 @@ export default class SortedTable extends Component {
selectedItemsIds[method](visibleItems[i].id)
}
})
: selectedItemsIds[method](item.id),
})
: selectedItemsIds[method](item.id)
this._previous = current
}
this._previous = current
if (onSelect !== undefined) {
onSelect(_selectedItemsIds.toArray())
}
this.setState({
all: false,
selectedItemsIds: _selectedItemsIds,
})
}
_onSelectItemCheckbox = event => {
@ -717,7 +735,7 @@ export default class SortedTable extends Component {
_renderItem = (item, i) => {
const { props, state } = this
const { actions, individualActions, rowAction, rowLink } = props
const { actions, individualActions, onSelect, rowAction, rowLink } = props
const userData = this._getUserData()
const hasGroupedActions = this._hasGroupedActions()
@ -741,7 +759,7 @@ export default class SortedTable extends Component {
const { id = i } = item
const selectionColumn = hasGroupedActions && (
const selectionColumn = (hasGroupedActions || onSelect !== undefined) && (
<td className='text-xs-center' onClick={this._toggleNestedCheckbox}>
<input
checked={state.all || state.selectedItemsIds.has(id)}
@ -790,13 +808,14 @@ export default class SortedTable extends Component {
)
}
render () {
render() {
const { props, state } = this
const {
actions,
filterContainer,
individualActions,
itemsPerPage,
onSelect,
paginationContainer,
shortcutsTarget,
} = props
@ -903,7 +922,7 @@ export default class SortedTable extends Component {
</th>
</tr>
<tr>
{hasGroupedActions && (
{(hasGroupedActions || onSelect !== undefined) && (
<th
className='text-xs-center'
onClick={this._toggleNestedCheckbox}

View File

@ -6,21 +6,14 @@ import Icon from 'icon'
import Link from 'link'
import React from 'react'
import SingleLineRow from 'single-line-row'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import { get } from '@xen-orchestra/defined'
import { Host, Sr } from 'render-xo-item'
import { Container, Col, Row } from 'grid'
import { Toggle, SizeInput } from 'form'
import { SelectPif, SelectPool } from 'select-objects'
import {
every,
filter,
find,
forEach,
groupBy,
isEmpty,
keys,
map,
pickBy,
} from 'lodash'
import { filter, forEach, groupBy, isEmpty, map, pickBy, some } from 'lodash'
import {
createFilter,
createGetObjectsOfType,
@ -62,6 +55,45 @@ const _findLatestTemplate = templates => {
const DEFAULT_BRICKSIZE = 100 * 1024 * 1024 * 1024 // 100 GiB
const DEFAULT_MEMORY = 2 * 1024 * 1024 * 1024 // 2 GiB
const XOSAN_SR_COLUMNS = [
{
itemRenderer: sr => (
<Sr id={sr.id} container={false} spaceLeft={false} link />
),
name: _('xosanName'),
sortCriteria: 'name_label',
},
{
itemRenderer: sr => <Host id={sr.$container} pool={false} link />,
name: _('xosanHost'),
sortCriteria: (sr, { hosts }) => hosts[sr.$container].name_label,
},
{
itemRenderer: sr => <span>{formatSize(sr.size)}</span>,
name: _('xosanSize'),
sortCriteria: 'size',
},
{
itemRenderer: sr =>
sr.size > 0 && (
<Tooltip
content={_('spaceLeftTooltip', {
used: String(Math.round((sr.physical_usage / sr.size) * 100)),
free: formatSize(sr.size - sr.physical_usage),
})}
>
<progress
className='progress'
max='100'
value={(sr.physical_usage / sr.size) * 100}
/>
</Tooltip>
),
name: _('xosanUsedSpace'),
sortCriteria: sr => sr.size - sr.physical_usage,
},
]
@addSubscriptions({
catalog: subscribeResourceCatalog,
})
@ -72,7 +104,7 @@ const DEFAULT_MEMORY = 2 * 1024 * 1024 * 1024 // 2 GiB
})
export default class NewXosan extends Component {
state = {
selectedSrs: {},
selectedSrs: [],
brickSize: DEFAULT_BRICKSIZE,
ipRange: '172.31.100.0',
memorySize: DEFAULT_MEMORY,
@ -116,13 +148,13 @@ export default class NewXosan extends Component {
needsUpdate: false,
pif: undefined,
pool,
selectedSrs: {},
selectedSrs: [],
})
return this._checkPacks(pool)
}
componentDidUpdate () {
componentDidUpdate() {
this._refreshSuggestions()
}
@ -131,13 +163,16 @@ export default class NewXosan extends Component {
() => this.state.selectedSrs,
() => this.state.brickSize,
() => this.state.customBrickSize,
async (selectedSrs, brickSize, customBrickSize) => {
() => this.state.srsOnSameHost,
async (selectedSrs, brickSize, customBrickSize, srsOnSameHost) => {
this.setState({
suggestion: 0,
suggestions: await computeXosanPossibleOptions(
keys(pickBy(selectedSrs)),
customBrickSize ? brickSize : undefined
),
suggestions: !srsOnSameHost
? await computeXosanPossibleOptions(
selectedSrs,
customBrickSize ? brickSize : undefined
)
: [],
})
}
)
@ -156,7 +191,7 @@ export default class NewXosan extends Component {
_getHosts = createSelector(
() => this.props.hosts,
this._getIsInPool,
(hosts, isInPool) => filter(hosts, isInPool)
(hosts, isInPool) => pickBy(hosts, isInPool)
)
// LVM SRs that are connected
@ -164,14 +199,17 @@ export default class NewXosan extends Component {
createSelector(
createFilter(
() => this.props.srs,
createSelector(this._getHosts, hosts => sr => {
let host
return (
sr.SR_type === 'lvm' &&
(host = find(hosts, { id: sr.$container })) !== undefined &&
host.power_state === 'Running'
)
})
createSelector(
this._getHosts,
hosts => sr => {
let host
return (
sr.SR_type === 'lvm' &&
(host = hosts[sr.$container]) !== undefined &&
host.power_state === 'Running'
)
}
)
),
this._getPbdsBySr,
(srs, pbdsBySr) =>
@ -195,10 +233,17 @@ export default class NewXosan extends Component {
this.setState({ brickSize })
}
_selectSr = async (event, sr) => {
const selectedSrs = { ...this.state.selectedSrs }
selectedSrs[sr.id] = event.target.checked
this.setState({ selectedSrs })
_selectSrs = selectedSrs => {
const { srs } = this.props
const found = {}
let container
this.setState({
selectedSrs,
srsOnSameHost: some(selectedSrs, srId => {
container = get(() => srs[srId].$container)
return found[container] || ((found[container] = true), false)
}),
})
}
_getPifPredicate = createSelector(
@ -208,7 +253,7 @@ export default class NewXosan extends Component {
_getNSelectedSrs = createSelector(
() => this.state.selectedSrs,
srs => filter(srs).length
srs => srs.length
)
_getLatestTemplate = createSelector(
@ -218,24 +263,14 @@ export default class NewXosan extends Component {
_findLatestTemplate
)
_getDisableSrCheckbox = createSelector(
() => this.state.selectedSrs,
this._getLvmSrs,
(selectedSrs, lvmsrs) => sr =>
!every(
keys(pickBy(selectedSrs)),
selectedSrId =>
selectedSrId === sr.id ||
find(lvmsrs, { id: selectedSrId }).$container !== sr.$container
)
)
_getDisableCreation = createSelector(
() => this.state.srsOnSameHost,
() => this.state.suggestion,
() => this.state.suggestions,
() => this.state.pif,
this._getNSelectedSrs,
(suggestion, suggestions, pif, nSelectedSrs) =>
(srsOnSameHost, suggestion, suggestions, pif, nSelectedSrs) =>
srsOnSameHost ||
!suggestions ||
!suggestions[suggestion] ||
!pif ||
@ -254,7 +289,7 @@ export default class NewXosan extends Component {
template: this._getLatestTemplate(),
pif: this.state.pif,
vlan: this.state.vlan || 0,
srs: keys(pickBy(this.state.selectedSrs)),
srs: this.state.selectedSrs,
glusterType: params.layout,
redundancy: params.redundancy,
brickSize: this.state.customBrickSize ? this.state.brickSize : undefined,
@ -265,7 +300,7 @@ export default class NewXosan extends Component {
this.props.onSrCreationStarted()
}
render () {
render() {
if (process.env.XOA_PLAN === 5) {
return (
<em>
@ -288,7 +323,7 @@ export default class NewXosan extends Component {
needsUpdate,
pif,
pool,
selectedSrs,
srsOnSameHost,
suggestion,
suggestions,
useVlan,
@ -307,12 +342,8 @@ export default class NewXosan extends Component {
)
}
const lvmsrs = this._getLvmSrs()
const hosts = this._getHosts()
const disableSrCheckbox = this._getDisableSrCheckbox()
const hostsNeedRestart =
pool !== undefined &&
pool != null &&
hostsNeedRestartByPool !== undefined &&
hostsNeedRestartByPool[pool.id]
const architecture = suggestions != null && suggestions[suggestion]
@ -375,69 +406,21 @@ export default class NewXosan extends Component {
[
<Row>
<Col>
<em>{_('xosanSelect2Srs')}</em>
<table className='table table-striped'>
<thead>
<tr>
<th />
<th>{_('xosanName')}</th>
<th>{_('xosanHost')}</th>
<th>{_('xosanSize')}</th>
<th>{_('xosanUsedSpace')}</th>
</tr>
</thead>
<tbody>
{map(lvmsrs, sr => {
const host = find(hosts, ['id', sr.$container])
return (
<tr key={sr.id}>
<td>
<input
checked={selectedSrs[sr.id] || false}
disabled={disableSrCheckbox(sr)}
onChange={event => this._selectSr(event, sr)}
type='checkbox'
/>
</td>
<td>
<Link to={`/srs/${sr.id}/general`}>
{sr.name_label}
</Link>
</td>
<td>
<Link to={`/hosts/${host.id}/general`}>
{host.name_label}
</Link>
</td>
<td>{formatSize(sr.size)}</td>
<td>
{sr.size > 0 && (
<Tooltip
content={_('spaceLeftTooltip', {
used: String(
Math.round(
(sr.physical_usage / sr.size) * 100
)
),
free: formatSize(
sr.size - sr.physical_usage
),
})}
>
<progress
className='progress'
max='100'
value={(sr.physical_usage / sr.size) * 100}
/>
</Tooltip>
)}
</td>
</tr>
)
})}
</tbody>
</table>
<SortedTable
collection={this._getLvmSrs()}
columns={XOSAN_SR_COLUMNS}
data-hosts={this._getHosts()}
onSelect={this._selectSrs}
/>
</Col>
</Row>,
<Row>
<Col>
{srsOnSameHost && (
<span className='text-danger'>
<Icon icon='alarm' /> {_('xosanSrOnSameHostMessage')}
</span>
)}
</Col>
</Row>,
<Row>