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)) - [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)) - [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)) - [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 ### Bug fixes

View File

@ -1,4 +1,5 @@
import add from 'lodash/add' import add from 'lodash/add'
import defined from '@xen-orchestra/defined'
import { check as checkPermissions } from 'xo-acl-resolver' import { check as checkPermissions } from 'xo-acl-resolver'
import { createSelector as create } from 'reselect' import { createSelector as create } from 'reselect'
import { import {
@ -6,6 +7,7 @@ import {
filter, filter,
find, find,
forEach, forEach,
forOwn,
groupBy, groupBy,
identity, identity,
isArrayLike, isArrayLike,
@ -588,3 +590,60 @@ export const createGetHostState = getHost =>
(powerState, enabled, operations) => (powerState, enabled, operations) =>
powerState !== 'Running' ? powerState : !isEmpty(operations) ? 'Busy' : !enabled ? 'Disabled' : 'Running' 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, createGetObjectsOfType,
createSelector, createSelector,
getIsPoolAdmin, getIsPoolAdmin,
getResolvedPendingTasks,
getStatus, getStatus,
getUser, getUser,
getXoaState, getXoaState,
@ -44,18 +45,19 @@ const returnTrue = () => true
@connectStore( @connectStore(
() => { () => {
const getHosts = createGetObjectsOfType('host') const getHosts = createGetObjectsOfType('host')
return { return (state, props) => ({
hosts: getHosts, hosts: getHosts(state, props),
isAdmin, isAdmin: isAdmin(state, props),
isPoolAdmin: getIsPoolAdmin, isPoolAdmin: getIsPoolAdmin(state, props),
nHosts: getHosts.count(), nHosts: getHosts.count()(state, props),
nTasks: createGetObjectsOfType('task').count([task => task.status === 'pending']), // true: useResourceSet to bypass permissions
pools: createGetObjectsOfType('pool'), nResolvedTasks: getResolvedPendingTasks(state, props, true).length,
srs: createGetObjectsOfType('SR'), pools: createGetObjectsOfType('pool')(state, props),
status: getStatus, srs: createGetObjectsOfType('SR')(state, props),
user: getUser, status: getStatus(state, props),
xoaState: getXoaState, user: getUser(state, props),
} xoaState: getXoaState(state, props),
})
}, },
{ {
withRef: true, withRef: true,
@ -207,7 +209,7 @@ export default class Menu extends Component {
} }
render() { 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 noOperatablePools = this._getNoOperatablePools()
const noResourceSets = this._getNoResourceSets() const noResourceSets = this._getNoResourceSets()
const noNotifications = this._getNoNotifications() const noNotifications = this._getNoNotifications()
@ -470,11 +472,11 @@ export default class Menu extends Component {
], ],
}, },
isAdmin && { to: '/about', icon: 'menu-about', label: 'aboutPage' }, isAdmin && { to: '/about', icon: 'menu-about', label: 'aboutPage' },
!noOperatablePools && { {
to: '/tasks', to: '/tasks',
icon: 'task', icon: 'task',
label: 'taskMenu', label: 'taskMenu',
pill: nTasks, pill: nResolvedTasks,
}, },
isAdmin && { to: '/xosan', icon: 'menu-xosan', label: 'xosan' }, isAdmin && { to: '/xosan', icon: 'menu-xosan', label: 'xosan' },
!noOperatablePools && { !noOperatablePools && {

View File

@ -1,19 +1,25 @@
import _, { messages } from 'intl' import _, { messages } from 'intl'
import Collapse from 'collapse' import Collapse from 'collapse'
import Component from 'base-component' import Component from 'base-component'
import defined from '@xen-orchestra/defined'
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 renderXoItem, { Pool } from 'render-xo-item' import renderXoItem, { Pool } from 'render-xo-item'
import SortedTable from 'sorted-table' import SortedTable from 'sorted-table'
import { addSubscriptions, connectStore, resolveIds } from 'utils'
import { FormattedDate, FormattedRelative, injectIntl } from 'react-intl' import { FormattedDate, FormattedRelative, injectIntl } from 'react-intl'
import { SelectPool } from 'select-objects' import { SelectPool } from 'select-objects'
import { connectStore, resolveIds } from 'utils'
import { Col, Container, Row } from 'grid' import { Col, Container, Row } from 'grid'
import { differenceBy, flatMap, flatten, forOwn, groupBy, isEmpty, keys, map, some, toArray } from 'lodash' import { differenceBy, flatMap, groupBy, isEmpty, keys, map, some } from 'lodash'
import { createFilter, createGetObject, createGetObjectsOfType, createSelector } from 'selectors' import {
import { cancelTask, cancelTasks, destroyTask, destroyTasks } from 'xo' createFilter,
createGetObject,
createGetObjectsOfType,
createSelector,
getResolvedPendingTasks,
isAdmin,
} from 'selectors'
import { cancelTask, cancelTasks, destroyTask, destroyTasks, subscribePermissions } from 'xo'
import Page from '../page' import Page from '../page'
@ -182,66 +188,25 @@ const GROUPED_ACTIONS = [
}, },
] ]
@connectStore(() => { @addSubscriptions({
const getPendingTasks = createGetObjectsOfType('task').filter([task => task.status === 'pending']) permissions: subscribePermissions,
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)
}) })
} @connectStore(() => {
const getResolvedPendingTasksByPool = createSelector(getResolvedPendingTasks, resolvedPendingTasks =>
forOwn(pools, resolveLinkedObjects) groupBy(resolvedPendingTasks, '$pool')
forOwn(hosts, resolveLinkedObjects)
forOwn(srs, resolveLinkedObjects)
forOwn(vdis, resolveLinkedObjects)
forOwn(vms, resolveLinkedObjects)
forOwn(networks, resolveLinkedObjects)
return linkedObjectsByTaskRefOrId
}
) )
const getPendingTasksByPool = createSelector( const getPools = createGetObjectsOfType('pool').pick(createSelector(getResolvedPendingTasksByPool, keys))
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))
return (state, props) => {
// true: useResourceSet to bypass permissions
const resolvedPendingTasksByPool = getResolvedPendingTasks(state, props, true)
return { return {
nTasks: getNPendingTasks, isAdmin: isAdmin(state, props),
pendingTasksByPool: getPendingTasksByPool, nResolvedTasks: resolvedPendingTasksByPool.length,
pools: getPools, pools: getPools(state, props, true),
resolvedPendingTasksByPool,
}
} }
}) })
@injectIntl @injectIntl
@ -251,11 +216,7 @@ export default class Tasks extends Component {
} }
componentWillReceiveProps(props) { componentWillReceiveProps(props) {
const finishedTasks = differenceBy( const finishedTasks = differenceBy(this.props.resolvedPendingTasksByPool, props.resolvedPendingTasksByPool, 'id')
flatten(toArray(this.props.pendingTasksByPool)),
flatten(toArray(props.pendingTasksByPool)),
'id'
)
if (!isEmpty(finishedTasks)) { if (!isEmpty(finishedTasks)) {
this.setState({ this.setState({
finishedTasks: finishedTasks finishedTasks: finishedTasks
@ -267,11 +228,11 @@ export default class Tasks extends Component {
_getTasks = createSelector( _getTasks = createSelector(
createSelector(() => this.state.pools, resolveIds), createSelector(() => this.state.pools, resolveIds),
() => this.props.pendingTasksByPool, () => this.props.resolvedPendingTasksByPool,
(poolIds, pendingTasksByPool) => (poolIds, resolvedPendingTasksByPool) =>
isEmpty(poolIds) isEmpty(poolIds)
? flatten(toArray(pendingTasksByPool)) ? resolvedPendingTasksByPool
: flatMap(poolIds, poolId => pendingTasksByPool[poolId] || []) : flatMap(poolIds, poolId => resolvedPendingTasksByPool[poolId] || [])
) )
_getFinishedTasks = createFilter( _getFinishedTasks = createFilter(
@ -288,11 +249,11 @@ export default class Tasks extends Component {
render() { render() {
const { props } = this const { props } = this
const { intl, nTasks, pools } = props const { intl, nResolvedTasks, pools } = props
const { formatMessage } = intl const { formatMessage } = intl
return ( return (
<Page header={HEADER} title={`(${nTasks}) ${formatMessage(messages.taskPage)}`}> <Page header={HEADER} title={`(${nResolvedTasks}) ${formatMessage(messages.taskPage)}`}>
<Container> <Container>
<Row className='mb-1'> <Row className='mb-1'>
<Col mediumSize={7}> <Col mediumSize={7}>

View File

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