diff --git a/src/common/intl/messages.js b/src/common/intl/messages.js index 6dd7a57db..91f62f841 100644 --- a/src/common/intl/messages.js +++ b/src/common/intl/messages.js @@ -1094,6 +1094,13 @@ const messages = { // ---- Tasks --- noTasks: 'No pending 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 --- backupSchedules: 'Schedules', @@ -1263,6 +1270,12 @@ const messages = { trialReadyModal: 'Ready for trial?', trialReadyModalText: '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 ----- serverLabel: 'Label', diff --git a/src/common/sorted-table/index.js b/src/common/sorted-table/index.js index 63f0005df..1a2e1b6b6 100644 --- a/src/common/sorted-table/index.js +++ b/src/common/sorted-table/index.js @@ -706,7 +706,7 @@ export default class SortedTable extends Component { const displayPagination = paginationContainer === undefined && itemsPerPage < nAllItems - const displayFilter = filterContainer === undefined && nAllItems !== 0 + const displayFilter = nAllItems !== 0 const paginationInstance = displayPagination && ( 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 => _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 ------------------------------------------------------------- export const createJob = job => diff --git a/src/xo-app/tasks/index.js b/src/xo-app/tasks/index.js index 8fc33f5b8..290d873b0 100644 --- a/src/xo-app/tasks/index.js +++ b/src/xo-app/tasks/index.js @@ -1,24 +1,22 @@ import _, { messages } from 'intl' -import ActionRowButton from 'action-row-button' -import ButtonGroup from 'button-group' import CenterPanel from 'center-panel' import Component from 'base-component' import Icon from 'icon' import Link from 'link' import React from 'react' -import SingleLineRow from 'single-line-row' +import SortedTable from 'sorted-table' import { injectIntl } from 'react-intl' import { SelectPool } from 'select-objects' +import { connectStore, resolveIds } from 'utils' import { Card, CardBlock, CardHeader } from 'card' -import { connectStore, resolveId, resolveIds } from 'utils' import { Col, Container, Row } from 'grid' -import { includes, isEmpty, keys, map } from 'lodash' +import { flatMap, flatten, isEmpty, keys, toArray } from 'lodash' import { createGetObject, createGetObjectsOfType, createSelector, } from 'selectors' -import { cancelTask, destroyTask } from 'xo' +import { cancelTask, cancelTasks, destroyTask, destroyTasks } from 'xo' import Page from '../page' @@ -38,44 +36,93 @@ const TASK_ITEM_STYLE = { // Remove all margin, otherwise it breaks vertical alignment. 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(() => ({ - host: createGetObject((_, props) => props.task.$host), -}))(({ task, host }) => ( - - - {task.name_label} ({task.name_description && `${task.name_description} `}on{' '} - {host ? ( - {host.name_label} - ) : ( - `unknown host − ${task.$host}` - )}) - {' ' + Math.round(task.progress * 100)}% - - + return ( +
+ {task.name_label} ({task.name_description && + `${task.name_description} `}on{' '} + {host ? ( + {host.name_label} + ) : ( + `unknown host − ${task.$host}` + )}) + {' ' + Math.round(task.progress * 100)}% +
+ ) + } +} + +const COLUMNS = [ + { + default: true, + itemRenderer: (task, userData) => { + const pool = userData.pools[task.$poolId] + return ( + pool !== undefined && ( + {pool.name_label} + ) + ) + }, + 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 => ( - - - - - - - -
-)) + ), + name: _('progress'), + sortCriteria: 'progress', + }, +] + +const INDIVIDUAL_ACTIONS = [ + { + handler: cancelTask, + icon: 'task-cancel', + label: _('cancelTask'), + level: 'danger', + }, + { + handler: destroyTask, + 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(() => { const getPendingTasks = createGetObjectsOfType('task').filter([ @@ -86,9 +133,9 @@ export const TaskItem = connectStore(() => ({ const getPendingTasksByPool = getPendingTasks.sort().groupBy('$pool') - const getPools = createGetObjectsOfType('pool') - .pick(createSelector(getPendingTasksByPool, keys)) - .sort() + const getPools = createGetObjectsOfType('pool').pick( + createSelector(getPendingTasksByPool, keys) + ) return { nTasks: getNPendingTasks, @@ -98,13 +145,18 @@ export const TaskItem = connectStore(() => ({ }) @injectIntl export default class Tasks extends Component { - _showPoolTasks = pool => - isEmpty(this.state.pools) || - includes(resolveIds(this.state.pools), resolveId(pool)) + _getTasks = createSelector( + createSelector(() => this.state.pools, resolveIds), + () => this.props.pendingTasksByPool, + (poolIds, pendingTasksByPool) => + isEmpty(poolIds) + ? flatten(toArray(pendingTasksByPool)) + : flatMap(poolIds, poolId => pendingTasksByPool[poolId] || []) + ) render () { - const { props, state } = this - const { intl, nTasks, pendingTasksByPool } = props + const { props } = this + const { intl, nTasks, pendingTasksByPool, pools } = props if (isEmpty(pendingTasksByPool)) { return ( @@ -126,6 +178,7 @@ export default class Tasks extends Component { } const { formatMessage } = intl + return ( - + + + + +
this.setState({ container })} /> + + + + + this.state.container} + groupedActions={GROUPED_ACTIONS} + individualActions={INDIVIDUAL_ACTIONS} + stateUrlParam='s' + userData={{ pools }} + /> + - {map( - props.pools, - pool => - this._showPoolTasks(pool) && ( - - - - {pool.name_label} - - - {map(pendingTasksByPool[pool.id], task => ( - - ))} - - - - ) - )} )