parent
3770194598
commit
152f68624c
@ -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',
|
||||||
|
@ -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
|
||||||
|
@ -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 =>
|
||||||
|
@ -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}>
|
|
||||||
{task.name_label} ({task.name_description && `${task.name_description} `}on{' '}
|
|
||||||
{host ? (
|
{host ? (
|
||||||
<Link to={`/hosts/${host.id}`}>{host.name_label}</Link>
|
<Link to={`/hosts/${host.id}`}>{host.name_label}</Link>
|
||||||
) : (
|
) : (
|
||||||
`unknown host − ${task.$host}`
|
`unknown host − ${task.$host}`
|
||||||
)})
|
)})
|
||||||
{' ' + Math.round(task.progress * 100)}%
|
{' ' + Math.round(task.progress * 100)}%
|
||||||
</Col>
|
</div>
|
||||||
<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>
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user