feat(tasks): use SortedTable (#2532)

See #2416
This commit is contained in:
Rajaa.BARHTAOUI 2018-01-31 13:54:03 +01:00 committed by Pierre Donias
parent 3770194598
commit 152f68624c
4 changed files with 155 additions and 69 deletions

View File

@ -1094,6 +1094,13 @@ const messages = {
// ---- Tasks --- // ---- Tasks ---
noTasks: 'No pending tasks', noTasks: 'No pending tasks',
xsTasks: 'Currently, there are not any pending XenServer tasks', xsTasks: 'Currently, there are not any pending XenServer tasks',
cancelTask: 'Cancel',
destroyTask: 'Destroy',
cancelTasks: 'Cancel selected tasks',
destroyTasks: 'Destroy selected tasks',
pool: 'Pool',
task: 'Task',
progress: 'Progress',
// ---- Backup views --- // ---- Backup views ---
backupSchedules: 'Schedules', backupSchedules: 'Schedules',
@ -1263,6 +1270,12 @@ const messages = {
trialReadyModal: 'Ready for trial?', trialReadyModal: 'Ready for trial?',
trialReadyModalText: trialReadyModalText:
'During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!', 'During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!',
cancelTasksModalTitle: 'Cancel task{nTasks, plural, one {} other {s}}',
cancelTasksModalMessage:
'Are you sure you want to cancel {nTasks, number} task{nTasks, plural, one {} other {s}}?',
destroyTasksModalTitle: 'Destroy task{nTasks, plural, one {} other {s}}',
destroyTasksModalMessage:
'Are you sure you want to destroy {nTasks, number} task{nTasks, plural, one {} other {s}}?',
// ----- Servers ----- // ----- Servers -----
serverLabel: 'Label', serverLabel: 'Label',

View File

@ -706,7 +706,7 @@ export default class SortedTable extends Component {
const displayPagination = const displayPagination =
paginationContainer === undefined && itemsPerPage < nAllItems paginationContainer === undefined && itemsPerPage < nAllItems
const displayFilter = filterContainer === undefined && nAllItems !== 0 const displayFilter = nAllItems !== 0
const paginationInstance = displayPagination && ( const paginationInstance = displayPagination && (
<Pagination <Pagination

View File

@ -1464,9 +1464,33 @@ export const removeTag = (object, tag) =>
export const cancelTask = task => _call('task.cancel', { id: resolveId(task) }) export const cancelTask = task => _call('task.cancel', { id: resolveId(task) })
export const cancelTasks = tasks =>
confirm({
title: _('cancelTasksModalTitle', { nTasks: tasks.length }),
body: _('cancelTasksModalMessage', { nTasks: tasks.length }),
}).then(
() =>
Promise.all(
map(tasks, task => _call('task.cancel', { id: resolveId(task) }))
),
noop
)
export const destroyTask = task => export const destroyTask = task =>
_call('task.destroy', { id: resolveId(task) }) _call('task.destroy', { id: resolveId(task) })
export const destroyTasks = tasks =>
confirm({
title: _('destroyTasksModalTitle', { nTasks: tasks.length }),
body: _('destroyTasksModalMessage', { nTasks: tasks.length }),
}).then(
() =>
Promise.all(
map(tasks, task => _call('task.destroy', { id: resolveId(task) }))
),
noop
)
// Jobs ------------------------------------------------------------- // Jobs -------------------------------------------------------------
export const createJob = job => export const createJob = job =>

View File

@ -1,24 +1,22 @@
import _, { messages } from 'intl' import _, { messages } from 'intl'
import ActionRowButton from 'action-row-button'
import ButtonGroup from 'button-group'
import CenterPanel from 'center-panel' import CenterPanel from 'center-panel'
import Component from 'base-component' import Component from 'base-component'
import Icon from 'icon' import Icon from 'icon'
import Link from 'link' import Link from 'link'
import React from 'react' import React from 'react'
import SingleLineRow from 'single-line-row' import SortedTable from 'sorted-table'
import { injectIntl } from 'react-intl' import { injectIntl } from 'react-intl'
import { SelectPool } from 'select-objects' import { SelectPool } from 'select-objects'
import { connectStore, resolveIds } from 'utils'
import { Card, CardBlock, CardHeader } from 'card' import { Card, CardBlock, CardHeader } from 'card'
import { connectStore, resolveId, resolveIds } from 'utils'
import { Col, Container, Row } from 'grid' import { Col, Container, Row } from 'grid'
import { includes, isEmpty, keys, map } from 'lodash' import { flatMap, flatten, isEmpty, keys, toArray } from 'lodash'
import { import {
createGetObject, createGetObject,
createGetObjectsOfType, createGetObjectsOfType,
createSelector, createSelector,
} from 'selectors' } from 'selectors'
import { cancelTask, destroyTask } from 'xo' import { cancelTask, cancelTasks, destroyTask, destroyTasks } from 'xo'
import Page from '../page' import Page from '../page'
@ -38,44 +36,93 @@ const TASK_ITEM_STYLE = {
// Remove all margin, otherwise it breaks vertical alignment. // Remove all margin, otherwise it breaks vertical alignment.
margin: 0, margin: 0,
} }
@connectStore(() => ({
host: createGetObject((_, props) => props.item.$host),
}))
export class TaskItem extends Component {
render () {
const { host, item: task } = this.props
export const TaskItem = connectStore(() => ({ return (
host: createGetObject((_, props) => props.task.$host), <div>
}))(({ task, host }) => ( {task.name_label} ({task.name_description &&
<SingleLineRow className='mb-1'> `${task.name_description} `}on{' '}
<Col mediumSize={6}> {host ? (
{task.name_label} ({task.name_description && `${task.name_description} `}on{' '} <Link to={`/hosts/${host.id}`}>{host.name_label}</Link>
{host ? ( ) : (
<Link to={`/hosts/${host.id}`}>{host.name_label}</Link> `unknown host ${task.$host}`
) : ( )})
`unknown host ${task.$host}` {' ' + Math.round(task.progress * 100)}%
)}) </div>
{' ' + Math.round(task.progress * 100)}% )
</Col> }
<Col mediumSize={4}> }
const COLUMNS = [
{
default: true,
itemRenderer: (task, userData) => {
const pool = userData.pools[task.$poolId]
return (
pool !== undefined && (
<Link to={`/pools/${pool.id}`}>{pool.name_label}</Link>
)
)
},
name: _('pool'),
sortCriteria: (task, userData) => {
const pool = userData.pools[task.$poolId]
return pool !== undefined && pool.name_label
},
},
{
component: TaskItem,
name: _('task'),
sortCriteria: 'name_label',
},
{
itemRenderer: task => (
<progress <progress
style={TASK_ITEM_STYLE} style={TASK_ITEM_STYLE}
className='progress' className='progress'
value={task.progress * 100} value={task.progress * 100}
max='100' max='100'
/> />
</Col> ),
<Col mediumSize={2}> name: _('progress'),
<ButtonGroup> sortCriteria: 'progress',
<ActionRowButton },
handler={cancelTask} ]
handlerParam={task}
icon='task-cancel' const INDIVIDUAL_ACTIONS = [
/> {
<ActionRowButton handler: cancelTask,
handler={destroyTask} icon: 'task-cancel',
handlerParam={task} label: _('cancelTask'),
icon='task-destroy' level: 'danger',
/> },
</ButtonGroup> {
</Col> handler: destroyTask,
</SingleLineRow> icon: 'task-destroy',
)) label: _('destroyTask'),
level: 'danger',
},
]
const GROUPED_ACTIONS = [
{
handler: cancelTasks,
icon: 'task-cancel',
label: _('cancelTasks'),
level: 'danger',
},
{
handler: destroyTasks,
icon: 'task-destroy',
label: _('destroyTasks'),
level: 'danger',
},
]
@connectStore(() => { @connectStore(() => {
const getPendingTasks = createGetObjectsOfType('task').filter([ const getPendingTasks = createGetObjectsOfType('task').filter([
@ -86,9 +133,9 @@ export const TaskItem = connectStore(() => ({
const getPendingTasksByPool = getPendingTasks.sort().groupBy('$pool') const getPendingTasksByPool = getPendingTasks.sort().groupBy('$pool')
const getPools = createGetObjectsOfType('pool') const getPools = createGetObjectsOfType('pool').pick(
.pick(createSelector(getPendingTasksByPool, keys)) createSelector(getPendingTasksByPool, keys)
.sort() )
return { return {
nTasks: getNPendingTasks, nTasks: getNPendingTasks,
@ -98,13 +145,18 @@ export const TaskItem = connectStore(() => ({
}) })
@injectIntl @injectIntl
export default class Tasks extends Component { export default class Tasks extends Component {
_showPoolTasks = pool => _getTasks = createSelector(
isEmpty(this.state.pools) || createSelector(() => this.state.pools, resolveIds),
includes(resolveIds(this.state.pools), resolveId(pool)) () => this.props.pendingTasksByPool,
(poolIds, pendingTasksByPool) =>
isEmpty(poolIds)
? flatten(toArray(pendingTasksByPool))
: flatMap(poolIds, poolId => pendingTasksByPool[poolId] || [])
)
render () { render () {
const { props, state } = this const { props } = this
const { intl, nTasks, pendingTasksByPool } = props const { intl, nTasks, pendingTasksByPool, pools } = props
if (isEmpty(pendingTasksByPool)) { if (isEmpty(pendingTasksByPool)) {
return ( return (
@ -126,6 +178,7 @@ export default class Tasks extends Component {
} }
const { formatMessage } = intl const { formatMessage } = intl
return ( return (
<Page <Page
header={HEADER} header={HEADER}
@ -133,30 +186,26 @@ export default class Tasks extends Component {
> >
<Container> <Container>
<Row className='mb-1'> <Row className='mb-1'>
<SelectPool <Col mediumSize={8}>
multi <SelectPool multi onChange={this.linkState('pools')} />
value={state.pools} </Col>
onChange={this.linkState('pools')} <Col mediumSize={4}>
/> <div ref={container => this.setState({ container })} />
</Col>
</Row>
<Row>
<Col>
<SortedTable
collection={this._getTasks()}
columns={COLUMNS}
filterContainer={() => this.state.container}
groupedActions={GROUPED_ACTIONS}
individualActions={INDIVIDUAL_ACTIONS}
stateUrlParam='s'
userData={{ pools }}
/>
</Col>
</Row> </Row>
{map(
props.pools,
pool =>
this._showPoolTasks(pool) && (
<Row key={pool.id}>
<Card>
<CardHeader key={pool.id}>
<Link to={`/pools/${pool.id}`}>{pool.name_label}</Link>
</CardHeader>
<CardBlock>
{map(pendingTasksByPool[pool.id], task => (
<TaskItem key={task.id} task={task} />
))}
</CardBlock>
</Card>
</Row>
)
)}
</Container> </Container>
</Page> </Page>
) )