Skip to content

Commit

Permalink
feat(xo-web/tasks): show tasks for Self Service users (#6217)
Browse files Browse the repository at this point in the history
See zammad#5436
  • Loading branch information
Rajaa-BARHTAOUI committed Jun 28, 2022
1 parent c7df11c commit dae37c6
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 226 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Expand Up @@ -10,6 +10,7 @@
- [Backup] Merge delta backups without copying data when using VHD directories on NFS/SMB/local remote(https://github.com/vatesfr/xen-orchestra/pull/6271))
- [Proxies] Ability to copy the proxy access URL (PR [#6287](https://github.com/vatesfr/xen-orchestra/pull/6287))
- [User] User tokens management through XO interface (PR [#6276](https://github.com/vatesfr/xen-orchestra/pull/6276))
- [Tasks, VM/General] Self Service users: show tasks related to their pools, hosts, SRs, networks and VMs (PR [#6217](https://github.com/vatesfr/xen-orchestra/pull/6217))

### Bug fixes

Expand Down
59 changes: 59 additions & 0 deletions packages/xo-web/src/common/selectors.js
@@ -1,11 +1,13 @@
import add from 'lodash/add'
import defined from '@xen-orchestra/defined'
import { check as checkPermissions } from 'xo-acl-resolver'
import { createSelector as create } from 'reselect'
import {
difference,
filter,
find,
forEach,
forOwn,
groupBy,
identity,
isArrayLike,
Expand Down Expand Up @@ -588,3 +590,60 @@ export const createGetHostState = getHost =>
(powerState, enabled, operations) =>
powerState !== 'Running' ? powerState : !isEmpty(operations) ? 'Busy' : !enabled ? 'Disabled' : 'Running'
)

const taskPredicate = obj => !isEmpty(obj.current_operations)
const getLinkedObjectsByTaskRefOrId = create(
createGetObjectsOfType('pool').filter([taskPredicate]),
createGetObjectsOfType('host').filter([taskPredicate]),
createGetObjectsOfType('SR').filter([taskPredicate]),
createGetObjectsOfType('VDI').filter([taskPredicate]),
createGetObjectsOfType('VM').filter([taskPredicate]),
createGetObjectsOfType('network').filter([taskPredicate]),
getCheckPermissions,
(pools, hosts, srs, vdis, vms, networks, check) => {
const linkedObjectsByTaskRefOrId = {}
const resolveLinkedObjects = obj => {
if (!check(obj.id, 'view')) {
return
}

Object.keys(obj.current_operations).forEach(task => {
if (linkedObjectsByTaskRefOrId[task] === undefined) {
linkedObjectsByTaskRefOrId[task] = []
}
linkedObjectsByTaskRefOrId[task].push(obj)
})
}

forOwn(pools, resolveLinkedObjects)
forOwn(hosts, resolveLinkedObjects)
forOwn(srs, resolveLinkedObjects)
forOwn(vdis, resolveLinkedObjects)
forOwn(vms, resolveLinkedObjects)
forOwn(networks, resolveLinkedObjects)

return linkedObjectsByTaskRefOrId
}
)

export const getResolvedPendingTasks = create(
createGetObjectsOfType('task').filter([task => task.status === 'pending']),
getLinkedObjectsByTaskRefOrId,
(tasks, linkedObjectsByTaskRefOrId) => {
const resolvedTasks = []
forEach(tasks, task => {
const objects = [
...defined(linkedObjectsByTaskRefOrId[task.xapiRef], []),
// for VMs, the current_operations prop is
// { taskId → operation } map instead of { taskRef → operation } map
...defined(linkedObjectsByTaskRefOrId[task.id], []),
]
objects.length > 0 &&
resolvedTasks.push({
...task,
objects,
})
})
return resolvedTasks
}
)
32 changes: 17 additions & 15 deletions packages/xo-web/src/xo-app/menu/index.js
Expand Up @@ -26,6 +26,7 @@ import {
createGetObjectsOfType,
createSelector,
getIsPoolAdmin,
getResolvedPendingTasks,
getStatus,
getUser,
getXoaState,
Expand All @@ -44,18 +45,19 @@ const returnTrue = () => true
@connectStore(
() => {
const getHosts = createGetObjectsOfType('host')
return {
hosts: getHosts,
isAdmin,
isPoolAdmin: getIsPoolAdmin,
nHosts: getHosts.count(),
nTasks: createGetObjectsOfType('task').count([task => task.status === 'pending']),
pools: createGetObjectsOfType('pool'),
srs: createGetObjectsOfType('SR'),
status: getStatus,
user: getUser,
xoaState: getXoaState,
}
return (state, props) => ({
hosts: getHosts(state, props),
isAdmin: isAdmin(state, props),
isPoolAdmin: getIsPoolAdmin(state, props),
nHosts: getHosts.count()(state, props),
// true: useResourceSet to bypass permissions
nResolvedTasks: getResolvedPendingTasks(state, props, true).length,
pools: createGetObjectsOfType('pool')(state, props),
srs: createGetObjectsOfType('SR')(state, props),
status: getStatus(state, props),
user: getUser(state, props),
xoaState: getXoaState(state, props),
})
},
{
withRef: true,
Expand Down Expand Up @@ -207,7 +209,7 @@ export default class Menu extends Component {
}

render() {
const { isAdmin, isPoolAdmin, nTasks, state, status, user, pools, nHosts, srs, xoaState } = this.props
const { isAdmin, isPoolAdmin, nResolvedTasks, state, status, user, pools, nHosts, srs, xoaState } = this.props
const noOperatablePools = this._getNoOperatablePools()
const noResourceSets = this._getNoResourceSets()
const noNotifications = this._getNoNotifications()
Expand Down Expand Up @@ -470,11 +472,11 @@ export default class Menu extends Component {
],
},
isAdmin && { to: '/about', icon: 'menu-about', label: 'aboutPage' },
!noOperatablePools && {
{
to: '/tasks',
icon: 'task',
label: 'taskMenu',
pill: nTasks,
pill: nResolvedTasks,
},
isAdmin && { to: '/xosan', icon: 'menu-xosan', label: 'xosan' },
!noOperatablePools && {
Expand Down
105 changes: 33 additions & 72 deletions packages/xo-web/src/xo-app/tasks/index.js
@@ -1,19 +1,25 @@
import _, { messages } from 'intl'
import Collapse from 'collapse'
import Component from 'base-component'
import defined from '@xen-orchestra/defined'
import Icon from 'icon'
import Link from 'link'
import React from 'react'
import renderXoItem, { Pool } from 'render-xo-item'
import SortedTable from 'sorted-table'
import { addSubscriptions, connectStore, resolveIds } from 'utils'
import { FormattedDate, FormattedRelative, injectIntl } from 'react-intl'
import { SelectPool } from 'select-objects'
import { connectStore, resolveIds } from 'utils'
import { Col, Container, Row } from 'grid'
import { differenceBy, flatMap, flatten, forOwn, groupBy, isEmpty, keys, map, some, toArray } from 'lodash'
import { createFilter, createGetObject, createGetObjectsOfType, createSelector } from 'selectors'
import { cancelTask, cancelTasks, destroyTask, destroyTasks } from 'xo'
import { differenceBy, flatMap, groupBy, isEmpty, keys, map, some } from 'lodash'
import {
createFilter,
createGetObject,
createGetObjectsOfType,
createSelector,
getResolvedPendingTasks,
isAdmin,
} from 'selectors'
import { cancelTask, cancelTasks, destroyTask, destroyTasks, subscribePermissions } from 'xo'

import Page from '../page'

Expand Down Expand Up @@ -182,66 +188,25 @@ const GROUPED_ACTIONS = [
},
]

@addSubscriptions({
permissions: subscribePermissions,
})
@connectStore(() => {
const getPendingTasks = createGetObjectsOfType('task').filter([task => task.status === 'pending'])

const getNPendingTasks = getPendingTasks.count()

const predicate = obj => !isEmpty(obj.current_operations)

const getLinkedObjectsByTaskRefOrId = createSelector(
createGetObjectsOfType('pool').filter([predicate]),
createGetObjectsOfType('host').filter([predicate]),
createGetObjectsOfType('SR').filter([predicate]),
createGetObjectsOfType('VDI').filter([predicate]),
createGetObjectsOfType('VM').filter([predicate]),
createGetObjectsOfType('network').filter([predicate]),
(pools, hosts, srs, vdis, vms, networks) => {
const linkedObjectsByTaskRefOrId = {}
const resolveLinkedObjects = obj => {
Object.keys(obj.current_operations).forEach(task => {
if (linkedObjectsByTaskRefOrId[task] === undefined) {
linkedObjectsByTaskRefOrId[task] = []
}
linkedObjectsByTaskRefOrId[task].push(obj)
})
}

forOwn(pools, resolveLinkedObjects)
forOwn(hosts, resolveLinkedObjects)
forOwn(srs, resolveLinkedObjects)
forOwn(vdis, resolveLinkedObjects)
forOwn(vms, resolveLinkedObjects)
forOwn(networks, resolveLinkedObjects)

return linkedObjectsByTaskRefOrId
}
)

const getPendingTasksByPool = createSelector(
getPendingTasks,
getLinkedObjectsByTaskRefOrId,
(tasks, linkedObjectsByTaskRefOrId) =>
groupBy(
map(tasks, task => ({
...task,
objects: [
...defined(linkedObjectsByTaskRefOrId[task.xapiRef], []),
// for VMs, the current_operations prop is
// { taskId → operation } map instead of { taskRef → operation } map
...defined(linkedObjectsByTaskRefOrId[task.id], []),
],
})),
'$pool'
)
const getResolvedPendingTasksByPool = createSelector(getResolvedPendingTasks, resolvedPendingTasks =>
groupBy(resolvedPendingTasks, '$pool')
)

const getPools = createGetObjectsOfType('pool').pick(createSelector(getPendingTasksByPool, keys))
const getPools = createGetObjectsOfType('pool').pick(createSelector(getResolvedPendingTasksByPool, keys))

return {
nTasks: getNPendingTasks,
pendingTasksByPool: getPendingTasksByPool,
pools: getPools,
return (state, props) => {
// true: useResourceSet to bypass permissions
const resolvedPendingTasksByPool = getResolvedPendingTasks(state, props, true)
return {
isAdmin: isAdmin(state, props),
nResolvedTasks: resolvedPendingTasksByPool.length,
pools: getPools(state, props, true),
resolvedPendingTasksByPool,
}
}
})
@injectIntl
Expand All @@ -251,11 +216,7 @@ export default class Tasks extends Component {
}

componentWillReceiveProps(props) {
const finishedTasks = differenceBy(
flatten(toArray(this.props.pendingTasksByPool)),
flatten(toArray(props.pendingTasksByPool)),
'id'
)
const finishedTasks = differenceBy(this.props.resolvedPendingTasksByPool, props.resolvedPendingTasksByPool, 'id')
if (!isEmpty(finishedTasks)) {
this.setState({
finishedTasks: finishedTasks
Expand All @@ -267,11 +228,11 @@ export default class Tasks extends Component {

_getTasks = createSelector(
createSelector(() => this.state.pools, resolveIds),
() => this.props.pendingTasksByPool,
(poolIds, pendingTasksByPool) =>
() => this.props.resolvedPendingTasksByPool,
(poolIds, resolvedPendingTasksByPool) =>
isEmpty(poolIds)
? flatten(toArray(pendingTasksByPool))
: flatMap(poolIds, poolId => pendingTasksByPool[poolId] || [])
? resolvedPendingTasksByPool
: flatMap(poolIds, poolId => resolvedPendingTasksByPool[poolId] || [])
)

_getFinishedTasks = createFilter(
Expand All @@ -288,11 +249,11 @@ export default class Tasks extends Component {

render() {
const { props } = this
const { intl, nTasks, pools } = props
const { intl, nResolvedTasks, pools } = props
const { formatMessage } = intl

return (
<Page header={HEADER} title={`(${nTasks}) ${formatMessage(messages.taskPage)}`}>
<Page header={HEADER} title={`(${nResolvedTasks}) ${formatMessage(messages.taskPage)}`}>
<Container>
<Row className='mb-1'>
<Col mediumSize={7}>
Expand Down

0 comments on commit dae37c6

Please sign in to comment.