feat(xo-web): XO Tasks (#6861)

This commit is contained in:
Pierre Donias 2023-05-30 09:20:51 +02:00 committed by GitHub
parent e48bfa2c88
commit 92fd92ae63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 144 additions and 47 deletions

View File

@ -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',

View File

@ -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>
),

View File

@ -802,7 +802,7 @@ class SortedTable extends Component {
const userData = this._getUserData()
return (
<div>
<div className={props.className}>
{shortcutsTarget !== undefined && (
<Shortcuts
handler={this._getShortcutsHandler()}

View 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',
},
}

View File

@ -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')

View File

@ -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 (

View File

@ -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>
)
}