feat(xo-web/tasks): show tasks for Self Service users (#6217)
See zammad#5436
This commit is contained in:
parent
c7df11cc6f
commit
dae37c6a50
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
)
|
||||
|
@ -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 && {
|
||||
|
@ -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 = [
|
||||
},
|
||||
]
|
||||
|
||||
@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)
|
||||
@addSubscriptions({
|
||||
permissions: subscribePermissions,
|
||||
})
|
||||
}
|
||||
|
||||
forOwn(pools, resolveLinkedObjects)
|
||||
forOwn(hosts, resolveLinkedObjects)
|
||||
forOwn(srs, resolveLinkedObjects)
|
||||
forOwn(vdis, resolveLinkedObjects)
|
||||
forOwn(vms, resolveLinkedObjects)
|
||||
forOwn(networks, resolveLinkedObjects)
|
||||
|
||||
return linkedObjectsByTaskRefOrId
|
||||
}
|
||||
@connectStore(() => {
|
||||
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}>
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user