feat(xo-web/vm/advanced): ACL management from VM view (#3774)
Fixes #3040
This commit is contained in:
parent
100dd38c33
commit
1af42617c2
@ -6,6 +6,7 @@
|
||||
|
||||
- [Backup NG] Restore logs moved to restore tab [#3772](https://github.com/vatesfr/xen-orchestra/issues/3772) (PR [#3802](https://github.com/vatesfr/xen-orchestra/pull/3802))
|
||||
- [Remotes] New SMB implementation that provides better stability and performance [#2257](https://github.com/vatesfr/xen-orchestra/issues/2257) (PR [#3708](https://github.com/vatesfr/xen-orchestra/pull/3708))
|
||||
- [VM/advanced] ACL management from VM view [#3040](https://github.com/vatesfr/xen-orchestra/issues/3727) (PR [#3040](https://github.com/vatesfr/xen-orchestra/pull/3774))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
@ -1078,6 +1078,12 @@ const messages = {
|
||||
vmVgpuNone: 'None',
|
||||
vmAddVgpu: 'Add vGPU',
|
||||
vmSelectVgpuType: 'Select vGPU type',
|
||||
vmAcls: 'ACLs',
|
||||
vmAddAcls: 'Add ACLs',
|
||||
addAclsErrorTitle: 'Failed to add ACL(s)',
|
||||
addAclsErrorMessage: 'User(s)/group(s) and role are required.',
|
||||
removeAcl: 'Delete',
|
||||
moreAcls: '{nAcls, number} more…',
|
||||
|
||||
// ----- VM placeholders -----
|
||||
|
||||
|
@ -129,6 +129,7 @@ const AclTable = decorate([
|
||||
actions={ACL_ACTIONS}
|
||||
collection={state.acls}
|
||||
columns={ACL_COLUMNS}
|
||||
stateUrlParam='s'
|
||||
/>
|
||||
),
|
||||
])
|
||||
|
@ -1,28 +1,50 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import decorate from 'apply-decorators'
|
||||
import defined from '@xen-orchestra/defined'
|
||||
import getEventValue from 'get-event-value'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import React from 'react'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { assign, every, find, includes, isEmpty, map, uniq } from 'lodash'
|
||||
import { error } from 'notification'
|
||||
import { confirm } from 'modal'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { Number, Size, Text, XoSelect } from 'editable'
|
||||
import { Select, Toggle } from 'form'
|
||||
import { SelectResourceSet, SelectVgpuType } from 'select-objects'
|
||||
import {
|
||||
SelectResourceSet,
|
||||
SelectRole,
|
||||
SelectSubject,
|
||||
SelectVgpuType,
|
||||
} from 'select-objects'
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
formatSize,
|
||||
getCoresPerSocketPossibilities,
|
||||
getVirtualizationModeLabel,
|
||||
noop,
|
||||
osFamily,
|
||||
} from 'utils'
|
||||
import {
|
||||
assign,
|
||||
every,
|
||||
filter,
|
||||
find,
|
||||
includes,
|
||||
isEmpty,
|
||||
keyBy,
|
||||
map,
|
||||
some,
|
||||
uniq,
|
||||
} from 'lodash'
|
||||
import {
|
||||
addAcl,
|
||||
changeVirtualizationMode,
|
||||
cloneVm,
|
||||
convertVmToTemplate,
|
||||
@ -34,11 +56,15 @@ import {
|
||||
isVmRunning,
|
||||
pauseVm,
|
||||
recoveryStartVm,
|
||||
removeAcl,
|
||||
restartVm,
|
||||
shareVm,
|
||||
startVm,
|
||||
stopVm,
|
||||
subscribeAcls,
|
||||
subscribeGroups,
|
||||
subscribeResourceSets,
|
||||
subscribeUsers,
|
||||
suspendVm,
|
||||
XEN_DEFAULT_CPU_CAP,
|
||||
XEN_DEFAULT_CPU_WEIGHT,
|
||||
@ -49,6 +75,11 @@ import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
|
||||
// Button's height = react-select's height(36 px) + react-select's border-width(1 px) * 2
|
||||
// https://github.com/JedWatson/react-select/blob/916ab0e62fc7394be8e24f22251c399a68de8b1c/less/select.less#L21, L22
|
||||
const SHARE_BUTTON_STYLE = { height: '38px' }
|
||||
const LEVELS = {
|
||||
admin: 'danger',
|
||||
operator: 'primary',
|
||||
viewer: 'success',
|
||||
}
|
||||
|
||||
const forceReboot = vm => restartVm(vm, true)
|
||||
const forceShutdown = vm => stopVm(vm, true)
|
||||
@ -288,6 +319,138 @@ class CoresPerSocket extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
class AddAclsModal extends Component {
|
||||
get value() {
|
||||
return this.state
|
||||
}
|
||||
|
||||
_getPredicate = createSelector(
|
||||
() => this.props.acls,
|
||||
() => this.props.vm,
|
||||
(acls, object) => ({ id: subject, permission }) =>
|
||||
permission !== 'admin' && !some(acls, { object, subject })
|
||||
)
|
||||
|
||||
render() {
|
||||
const { action, subjects } = this.state
|
||||
return (
|
||||
<form>
|
||||
<div className='form-group'>
|
||||
<SelectSubject
|
||||
multi
|
||||
onChange={this.linkState('subjects')}
|
||||
predicate={this._getPredicate()}
|
||||
value={subjects}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<SelectRole onChange={this.linkState('action')} value={action} />
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Acls = decorate([
|
||||
addSubscriptions({
|
||||
acls: subscribeAcls,
|
||||
groups: cb => subscribeGroups(groups => cb(keyBy(groups, 'id'))),
|
||||
users: cb => subscribeUsers(users => cb(keyBy(users, 'id'))),
|
||||
}),
|
||||
provideState({
|
||||
effects: {
|
||||
addAcls: () => (state, { acls, vm }) =>
|
||||
confirm({
|
||||
title: _('vmAddAcls'),
|
||||
body: <AddAclsModal acls={acls} vm={vm} />,
|
||||
})
|
||||
.then(({ action, subjects }) => {
|
||||
if (action == null || isEmpty(subjects)) {
|
||||
return error(_('addAclsErrorTitle'), _('addAclsErrorMessage'))
|
||||
}
|
||||
|
||||
return (
|
||||
Promise.all(
|
||||
map(subjects, subject =>
|
||||
addAcl({ subject, object: vm, action })
|
||||
)
|
||||
),
|
||||
noop
|
||||
)
|
||||
})
|
||||
.catch(
|
||||
err =>
|
||||
err && error(_('addAclsErrorTitle'), err.message || String(err))
|
||||
),
|
||||
removeAcl: (_, { currentTarget: { dataset } }) => (_, { vm: object }) =>
|
||||
removeAcl({
|
||||
action: dataset.action,
|
||||
object,
|
||||
subject: dataset.subject,
|
||||
}),
|
||||
},
|
||||
computed: {
|
||||
rawAcls: (_, { acls, vm }) => filter(acls, { object: vm }),
|
||||
resolvedAcls: ({ rawAcls }, { users, groups }) => {
|
||||
if (users === undefined && groups === undefined) {
|
||||
return []
|
||||
}
|
||||
return rawAcls.map(({ subject, ...acl }) => ({
|
||||
...acl,
|
||||
subject:
|
||||
(users !== undefined && users[subject]) ||
|
||||
(groups !== undefined && groups[subject]),
|
||||
}))
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state: { resolvedAcls }, effects, vm }) => (
|
||||
<Container>
|
||||
{resolvedAcls.slice(0, 5).map(({ subject, action }) => (
|
||||
<Row key={`${subject.id}.${action}`}>
|
||||
<Col>
|
||||
<span>{renderXoItem(subject)}</span>{' '}
|
||||
<span className={`tag tag-pill tag-${LEVELS[action]}`}>
|
||||
{action}
|
||||
</span>{' '}
|
||||
<Tooltip content={_('removeAcl')}>
|
||||
<a
|
||||
data-action={action}
|
||||
data-subject={subject.id}
|
||||
onClick={effects.removeAcl}
|
||||
role='button'
|
||||
>
|
||||
<Icon icon='remove' />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
{resolvedAcls.length > 5 && (
|
||||
<Row>
|
||||
<Col>
|
||||
<Link to={`settings/acls?s=object:${vm}`}>
|
||||
{_('moreAcls', { nAcls: resolvedAcls.length - 5 })}
|
||||
</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
<Col>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={effects.addAcls}
|
||||
icon='add'
|
||||
size='small'
|
||||
tooltip={_('vmAddAcls')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
),
|
||||
])
|
||||
|
||||
const NIC_TYPE_OPTIONS = [
|
||||
{
|
||||
label: 'Realtek RTL819',
|
||||
@ -777,6 +940,14 @@ export default class TabAdvanced extends Component {
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
{isAdmin && (
|
||||
<tr>
|
||||
<th>{_('vmAcls')}</th>
|
||||
<td>
|
||||
<Acls vm={vm.id} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</Col>
|
||||
|
Loading…
Reference in New Issue
Block a user