feat(xo-web): XO Tasks (#6861)
This commit is contained in:
parent
e48bfa2c88
commit
92fd92ae63
@ -25,6 +25,7 @@ const messages = {
|
||||
vmSrUsage: 'Storage: {used} used of {total} ({free} free)',
|
||||
|
||||
notDefined: 'Not defined',
|
||||
status: 'Status',
|
||||
statusConnecting: 'Connecting',
|
||||
statusDisconnected: 'Disconnected',
|
||||
statusLoading: 'Loading…',
|
||||
@ -449,6 +450,8 @@ const messages = {
|
||||
taskSkipped: 'Skipped',
|
||||
taskStarted: 'Started',
|
||||
taskInterrupted: 'Interrupted',
|
||||
taskEnded: 'Ended',
|
||||
taskAborted: 'Aborted',
|
||||
taskTransferredDataSize: 'Transfer size',
|
||||
taskTransferredDataSpeed: 'Transfer speed',
|
||||
taskMergedDataSize: 'Merge size',
|
||||
@ -1678,10 +1681,13 @@ const messages = {
|
||||
importToSr: 'To SR',
|
||||
|
||||
// ---- Tasks ---
|
||||
poolTasks: 'Pool tasks',
|
||||
xoTasks: 'XO tasks',
|
||||
cancelTask: 'Cancel',
|
||||
destroyTask: 'Destroy',
|
||||
cancelTasks: 'Cancel selected tasks',
|
||||
destroyTasks: 'Destroy selected tasks',
|
||||
object: 'Object',
|
||||
objects: 'Objects',
|
||||
pool: 'Pool',
|
||||
task: 'Task',
|
||||
|
@ -531,28 +531,28 @@ const xoItemToRender = {
|
||||
},
|
||||
|
||||
// XO objects.
|
||||
pool: ({ id }) => <Pool id={id} />,
|
||||
pool: props => <Pool {...props} />,
|
||||
|
||||
VDI: ({ id }) => <Vdi id={id} showSr />,
|
||||
'VDI-resourceSet': ({ id }) => <Vdi id={id} self showSr />,
|
||||
VDI: props => <Vdi {...props} showSr />,
|
||||
'VDI-resourceSet': props => <Vdi {...props} self showSr />,
|
||||
|
||||
// Pool objects.
|
||||
'VM-template': ({ id }) => <VmTemplate id={id} />,
|
||||
'VM-template-resourceSet': ({ id }) => <VmTemplate id={id} self />,
|
||||
host: ({ id, memoryFree }) => <Host id={id} memoryFree={memoryFree} />,
|
||||
network: ({ id }) => <Network id={id} />,
|
||||
'network-resourceSet': ({ id }) => <Network id={id} self />,
|
||||
'VM-template': props => <VmTemplate {...props} />,
|
||||
'VM-template-resourceSet': props => <VmTemplate {...props} self />,
|
||||
host: props => <Host {...props} />,
|
||||
network: props => <Network {...props} />,
|
||||
'network-resourceSet': props => <Network {...props} self />,
|
||||
|
||||
// SR.
|
||||
SR: ({ id }) => <Sr id={id} />,
|
||||
'SR-resourceSet': ({ id }) => <Sr id={id} self />,
|
||||
SR: props => <Sr {...props} />,
|
||||
'SR-resourceSet': props => <Sr {...props} self />,
|
||||
|
||||
// VM.
|
||||
VM: ({ id }) => <Vm id={id} />,
|
||||
'VM-snapshot': ({ id }) => <Vm id={id} />,
|
||||
'VM-controller': ({ id }) => (
|
||||
VM: props => <Vm {...props} />,
|
||||
'VM-snapshot': props => <Vm {...props} />,
|
||||
'VM-controller': props => (
|
||||
<span>
|
||||
<Icon icon='host' /> <Vm id={id} />
|
||||
<Icon icon='host' /> <Vm {...props} />
|
||||
</span>
|
||||
),
|
||||
|
||||
|
@ -802,7 +802,7 @@ class SortedTable extends Component {
|
||||
const userData = this._getUserData()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={props.className}>
|
||||
{shortcutsTarget !== undefined && (
|
||||
<Shortcuts
|
||||
handler={this._getShortcutsHandler()}
|
||||
|
30
packages/xo-web/src/common/task-status.js
Normal file
30
packages/xo-web/src/common/task-status.js
Normal file
@ -0,0 +1,30 @@
|
||||
export default {
|
||||
failure: {
|
||||
icon: 'halted',
|
||||
label: 'taskFailed',
|
||||
},
|
||||
skipped: {
|
||||
icon: 'skipped',
|
||||
label: 'taskSkipped',
|
||||
},
|
||||
success: {
|
||||
icon: 'running',
|
||||
label: 'taskSuccess',
|
||||
},
|
||||
pending: {
|
||||
icon: 'busy',
|
||||
label: 'taskStarted',
|
||||
},
|
||||
interrupted: {
|
||||
icon: 'halted',
|
||||
label: 'taskInterrupted',
|
||||
},
|
||||
aborted: {
|
||||
icon: 'skipped',
|
||||
label: 'taskAborted',
|
||||
},
|
||||
unknown: {
|
||||
icon: 'unknown',
|
||||
label: 'unknown',
|
||||
},
|
||||
}
|
@ -232,7 +232,7 @@ const createSubscription = (cb, { polling = 5e3 } = {}) => {
|
||||
|
||||
running = true
|
||||
_signIn
|
||||
.then(() => cb())
|
||||
.then(() => cb(cache))
|
||||
.then(
|
||||
result => {
|
||||
running = false
|
||||
@ -564,6 +564,31 @@ export const createSrUnhealthyVdiChainsLengthSubscription = sr => {
|
||||
|
||||
export const subscribeUserAuthTokens = createSubscription(() => _call('user.getAuthenticationTokens'))
|
||||
|
||||
export const subscribeXoTasks = createSubscription(async previousTasks => {
|
||||
let filter = ''
|
||||
// Deduplicate previous tasks and new tasks with a Map
|
||||
const tasks = new Map()
|
||||
if (previousTasks !== undefined) {
|
||||
let lastUpdate = 0
|
||||
previousTasks.forEach(task => {
|
||||
if (task.updatedAt > lastUpdate) {
|
||||
lastUpdate = task.updatedAt
|
||||
}
|
||||
tasks.set(task.id, task)
|
||||
})
|
||||
filter = `&filter=updatedAt%3A%3E${lastUpdate}`
|
||||
}
|
||||
|
||||
// Fetch new and updated tasks
|
||||
const response = await fetch('./rest/v0/tasks?fields=end,id,name,objectId,properties,start,status,updatedAt' + filter)
|
||||
for (const task of await response.json()) {
|
||||
tasks.set(task.id, task)
|
||||
}
|
||||
|
||||
// Sort dates by start time
|
||||
return Array.from(tasks.values()).sort(({ start: start1 }, { start: start2 }) => start1 - start2)
|
||||
})
|
||||
|
||||
// System ============================================================
|
||||
|
||||
export const apiMethods = _call('system.getMethodsInfo')
|
||||
|
@ -10,6 +10,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import SearchBar from 'search-bar'
|
||||
import Select from 'form/select'
|
||||
import TASK_STATUS from 'task-status'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions, connectStore, formatSize, formatSpeed } from 'utils'
|
||||
import { countBy, cloneDeep, filter, map } from 'lodash'
|
||||
@ -22,29 +23,6 @@ import BaseComponent from 'base-component'
|
||||
|
||||
const hasTaskFailed = ({ status }) => status !== 'success' && status !== 'pending'
|
||||
|
||||
const TASK_STATUS = {
|
||||
failure: {
|
||||
icon: 'halted',
|
||||
label: 'taskFailed',
|
||||
},
|
||||
skipped: {
|
||||
icon: 'skipped',
|
||||
label: 'taskSkipped',
|
||||
},
|
||||
success: {
|
||||
icon: 'running',
|
||||
label: 'taskSuccess',
|
||||
},
|
||||
pending: {
|
||||
icon: 'busy',
|
||||
label: 'taskStarted',
|
||||
},
|
||||
interrupted: {
|
||||
icon: 'halted',
|
||||
label: 'taskInterrupted',
|
||||
},
|
||||
}
|
||||
|
||||
const TaskStateInfos = ({ status }) => {
|
||||
const { icon, label } = TASK_STATUS[status]
|
||||
return (
|
||||
|
@ -4,8 +4,10 @@ import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import React from 'react'
|
||||
import renderXoItem, { Pool } from 'render-xo-item'
|
||||
import renderXoItem, { Pool, renderXoItemFromId } from 'render-xo-item'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TASK_STATUS from 'task-status'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions, connectStore, resolveIds } from 'utils'
|
||||
import { FormattedDate, FormattedRelative, injectIntl } from 'react-intl'
|
||||
import { SelectPool } from 'select-objects'
|
||||
@ -19,7 +21,7 @@ import {
|
||||
getResolvedPendingTasks,
|
||||
isAdmin,
|
||||
} from 'selectors'
|
||||
import { cancelTask, cancelTasks, destroyTask, destroyTasks, subscribePermissions } from 'xo'
|
||||
import { cancelTask, cancelTasks, destroyTask, destroyTasks, subscribePermissions, subscribeXoTasks } from 'xo'
|
||||
|
||||
import Page from '../page'
|
||||
|
||||
@ -151,6 +153,53 @@ const FINISHED_TASKS_COLUMNS = [
|
||||
},
|
||||
]
|
||||
|
||||
const XO_TASKS_COLUMNS = [
|
||||
{
|
||||
itemRenderer: task => task.name,
|
||||
name: _('name'),
|
||||
},
|
||||
{
|
||||
itemRenderer: task => (task.objectId === undefined ? null : renderXoItemFromId(task.objectId, { link: true })),
|
||||
name: _('object'),
|
||||
},
|
||||
{
|
||||
itemRenderer: task => {
|
||||
const progress = task.properties?.progress
|
||||
|
||||
return progress === undefined ? null : (
|
||||
<progress style={TASK_ITEM_STYLE} className='progress' value={progress} max='100' />
|
||||
)
|
||||
},
|
||||
name: _('progress'),
|
||||
sortCriteria: 'progress',
|
||||
},
|
||||
{
|
||||
default: true,
|
||||
itemRenderer: task => (task.start === undefined ? null : <FormattedRelative value={task.start} />),
|
||||
name: _('taskStarted'),
|
||||
sortCriteria: 'start',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
{
|
||||
itemRenderer: task => (task.end === undefined ? null : <FormattedRelative value={task.end} />),
|
||||
name: _('taskEnded'),
|
||||
sortCriteria: 'end',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
{
|
||||
itemRenderer: task => {
|
||||
const { icon, label } = TASK_STATUS[task.status] ?? TASK_STATUS.unknown
|
||||
return (
|
||||
<Tooltip content={_(label)}>
|
||||
<Icon icon={icon} />
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
name: _('status'),
|
||||
sortCriteria: 'status',
|
||||
},
|
||||
]
|
||||
|
||||
const isNotCancelable = task => !task.allowedOperations.includes('cancel')
|
||||
const isNotDestroyable = task => !task.allowedOperations.includes('destroy')
|
||||
|
||||
@ -190,6 +239,7 @@ const GROUPED_ACTIONS = [
|
||||
|
||||
@addSubscriptions({
|
||||
permissions: subscribePermissions,
|
||||
xoTasks: subscribeXoTasks,
|
||||
})
|
||||
@connectStore(() => {
|
||||
const getPools = createGetObjectsOfType('pool').pick(
|
||||
@ -235,8 +285,6 @@ export default class Tasks extends Component {
|
||||
|
||||
_getItemsPerPageContainer = () => this.state.itemsPerPageContainer
|
||||
|
||||
_setItemsPerPageContainer = itemsPerPageContainer => this.setState({ itemsPerPageContainer })
|
||||
|
||||
render() {
|
||||
const { props } = this
|
||||
const { intl, nResolvedTasks, pools } = props
|
||||
@ -244,16 +292,17 @@ export default class Tasks extends Component {
|
||||
|
||||
return (
|
||||
<Page header={HEADER} title={`(${nResolvedTasks}) ${formatMessage(messages.taskPage)}`}>
|
||||
<h2>{_('poolTasks')}</h2>
|
||||
<Container>
|
||||
<Row className='mb-1'>
|
||||
<Col mediumSize={7}>
|
||||
<SelectPool multi onChange={this.linkState('pools')} />
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<div ref={container => this.setState({ container })} />
|
||||
<div ref={container => this.setState({ filterContainer: container })} />
|
||||
</Col>
|
||||
<Col mediumSize={1}>
|
||||
<div ref={this._setItemsPerPageContainer} />
|
||||
<div ref={container => this.setState({ itemsPerPageContainer: container })} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
@ -262,9 +311,9 @@ export default class Tasks extends Component {
|
||||
collection={this._getTasks()}
|
||||
columns={COLUMNS}
|
||||
defaultFilter='filterOutShortTasks'
|
||||
filterContainer={() => this.state.container}
|
||||
filterContainer={() => this.state.filterContainer}
|
||||
filters={FILTERS}
|
||||
itemsPerPageContainer={this._getItemsPerPageContainer}
|
||||
itemsPerPageContainer={() => this.state.itemsPerPageContainer}
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
individualActions={INDIVIDUAL_ACTIONS}
|
||||
stateUrlParam='s'
|
||||
@ -276,6 +325,7 @@ export default class Tasks extends Component {
|
||||
<Col>
|
||||
<Collapse buttonText={_('previousTasks')}>
|
||||
<SortedTable
|
||||
className='mt-1'
|
||||
collection={this._getFinishedTasks()}
|
||||
columns={FINISHED_TASKS_COLUMNS}
|
||||
filters={FILTERS}
|
||||
@ -285,6 +335,14 @@ export default class Tasks extends Component {
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<h2 className='mt-2'>{_('xoTasks')}</h2>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<SortedTable collection={props.xoTasks} columns={XO_TASKS_COLUMNS} stateUrlParam='s_xo' />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user