feat(xo-web/tasks): show tasks for Self Service users (#6217)

See zammad#5436
This commit is contained in:
Rajaa.BARHTAOUI 2022-06-28 18:35:58 +02:00 committed by GitHub
parent c7df11cc6f
commit dae37c6a50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 269 additions and 232 deletions

View File

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

View File

@ -1,4 +1,5 @@
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 {
@ -6,6 +7,7 @@ import {
filter,
find,
forEach,
forOwn,
groupBy,
identity,
isArrayLike,
@ -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
}
)

View File

@ -26,6 +26,7 @@ import {
createGetObjectsOfType,
createSelector,
getIsPoolAdmin,
getResolvedPendingTasks,
getStatus,
getUser,
getXoaState,
@ -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,
@ -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()
@ -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 && {

View File

@ -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'
@ -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 getResolvedPendingTasksByPool = createSelector(getResolvedPendingTasks, resolvedPendingTasks =>
groupBy(resolvedPendingTasks, '$pool')
)
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 getPools = createGetObjectsOfType('pool').pick(createSelector(getPendingTasksByPool, keys))
const getPools = createGetObjectsOfType('pool').pick(createSelector(getResolvedPendingTasksByPool, keys))
return (state, props) => {
// true: useResourceSet to bypass permissions
const resolvedPendingTasksByPool = getResolvedPendingTasks(state, props, true)
return {
nTasks: getNPendingTasks,
pendingTasksByPool: getPendingTasksByPool,
pools: getPools,
isAdmin: isAdmin(state, props),
nResolvedTasks: resolvedPendingTasksByPool.length,
pools: getPools(state, props, true),
resolvedPendingTasksByPool,
}
}
})
@injectIntl
@ -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
@ -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(
@ -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}>

View File

@ -1,5 +1,6 @@
import _ from 'intl'
import Copiable from 'copiable'
import decorate from 'apply-decorators'
import defined, { get } from '@xen-orchestra/defined'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
@ -14,14 +15,15 @@ import { FormattedRelative, FormattedDate } from 'react-intl'
import { Container, Row, Col } from 'grid'
import { Number, Size } from 'editable'
import {
createCollectionWrapper,
createFinder,
createGetObjectsOfType,
createGetVmLastShutdownTime,
createSelector,
getResolvedPendingTasks,
} from 'selectors'
import { connectStore, formatSizeShort, getVirtualizationModeLabel, osFamily } from 'utils'
import { CpuSparkLines, MemorySparkLines, NetworkSparkLines, XvdSparkLines } from 'xo-sparklines'
import { injectState, provideState } from 'reaclette'
const GuestToolsDetection = ({ vm }) => {
if (vm.power_state !== 'Running' || vm.pvDriversDetected === undefined) {
@ -78,7 +80,8 @@ const GuestToolsDetection = ({ vm }) => {
)
}
export default connectStore(() => {
const GeneralTab = decorate([
connectStore(() => {
const getVgpus = createGetObjectsOfType('vgpu')
.pick((_, { vm }) => vm.$VGPUs)
.sort()
@ -89,16 +92,24 @@ export default connectStore(() => {
createSelector(getVgpus, vgpus => map(vgpus, 'vgpuType'))
)
return {
lastShutdownTime: createGetVmLastShutdownTime(),
tasks: createGetObjectsOfType('task')
.pick(createSelector((_, { vm }) => vm.current_operations, createCollectionWrapper(Object.keys)))
.filter({ status: 'pending' })
.sort(),
vgpu: getAttachedVgpu,
vgpuTypes: getVgpuTypes,
}
})(({ lastShutdownTime, statsOverview, tasks, vgpu, vgpuTypes, vm, vmTotalDiskSpace }) => {
return (state, props) => ({
lastShutdownTime: createGetVmLastShutdownTime()(state, props),
// true: useResourceSet to bypass permissions
resolvedPendingTasks: getResolvedPendingTasks(state, props, true),
vgpu: getAttachedVgpu(state, props),
vgpuTypes: getVgpuTypes(state, props),
})
}),
provideState({
computed: {
vmResolvedPendingTasks: (_, { resolvedPendingTasks, vm }) => {
const vmTaskIds = Object.keys(vm.current_operations)
return resolvedPendingTasks.filter(task => vmTaskIds.includes(task.id))
},
},
}),
injectState,
({ state: { vmResolvedPendingTasks }, lastShutdownTime, statsOverview, vgpu, vgpuTypes, vm, vmTotalDiskSpace }) => {
const {
CPUs: cpus,
id,
@ -212,11 +223,11 @@ export default connectStore(() => {
</h2>
</Col>
</Row>
{isEmpty(tasks) ? null : (
{isEmpty(vmResolvedPendingTasks) ? null : (
<Row className='text-xs-center'>
<Col>
<h4>{_('vmCurrentStatus')}</h4>
{map(tasks, task => (
{map(vmResolvedPendingTasks, task => (
<p>
<strong>{task.name_label}</strong>
{task.progress > 0 && <span>: {Math.round(task.progress * 100)}%</span>}
@ -227,4 +238,7 @@ export default connectStore(() => {
)}
</Container>
)
})
},
])
export default GeneralTab