mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-27 23:18:49 -06:00
Merge pull request #109 from owncloud/feature/batch-actions
This commit is contained in:
6
changelog/unreleased/add-enable-disable-ui.md
Normal file
6
changelog/unreleased/add-enable-disable-ui.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Enhancement: Add enable/disable capabilities to the WebUI
|
||||
|
||||
We've added batch actions into the accounts listing to provide options to enable and disable accounts.
|
||||
|
||||
https://github.com/owncloud/product/issues/118
|
||||
https://github.com/owncloud/ocis-accounts/pull/109
|
||||
23
go.mod
23
go.mod
@@ -5,40 +5,25 @@ go 1.13
|
||||
require (
|
||||
github.com/CiscoM31/godata v0.0.0-20191007193734-c2c4ebb1b415
|
||||
github.com/blevesearch/bleve v1.0.9
|
||||
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d // indirect
|
||||
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect
|
||||
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 // indirect
|
||||
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/go-test/deep v1.0.6 // indirect
|
||||
github.com/gofrs/uuid v3.3.0+incompatible
|
||||
github.com/gogo/protobuf v1.3.1 // indirect
|
||||
github.com/golang/protobuf v1.4.2
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/imdario/mergo v0.3.11 // indirect
|
||||
github.com/jmhodges/levigo v1.0.0 // indirect
|
||||
github.com/mennanov/fieldmask-utils v0.3.2
|
||||
github.com/micro/cli/v2 v2.1.2
|
||||
github.com/micro/go-micro/v2 v2.9.1
|
||||
github.com/oklog/run v1.1.0
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/onsi/ginkgo v1.10.1 // indirect
|
||||
github.com/onsi/gomega v1.7.0 // indirect
|
||||
github.com/owncloud/ocis v1.0.0-rc1 // indirect
|
||||
github.com/owncloud/ocis-pkg/v2 v2.4.1-0.20200902134813-1e87c6173ada
|
||||
github.com/owncloud/ocis-settings v0.3.2-0.20200828130413-0cc0f5bf26fe
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/owncloud/ocis-settings v0.3.2-0.20200903035407-ad5de8264f91
|
||||
github.com/restic/calens v0.2.0
|
||||
github.com/rs/zerolog v1.19.0
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect
|
||||
github.com/tredoe/osutil v1.0.5
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344
|
||||
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202
|
||||
google.golang.org/genproto v0.0.0-20200624020401-64a14ca9d1ad
|
||||
google.golang.org/protobuf v1.25.0
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +1,36 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="uk-container uk-padding">
|
||||
<h1>
|
||||
Accounts
|
||||
</h1>
|
||||
<h1 v-text="$gettext('Accounts')" />
|
||||
<oc-grid v-if="numberOfSelectedAccounts > 0" key="selected-accounts-info" gutter="small" class="uk-flex-middle">
|
||||
<span v-text="selectionInfoText" />
|
||||
<span>|</span>
|
||||
<div>
|
||||
<oc-button v-text="$gettext('Clear selection')" variation="raw" @click="RESET_ACCOUNTS_SELECTION" />
|
||||
</div>
|
||||
<div>
|
||||
<oc-action-drop class="accounts-actions-dropdown">
|
||||
<template v-slot:button>
|
||||
<span class="uk-margin-xsmall-right" v-text="$gettext('Actions')" />
|
||||
<oc-icon name="expand_more" />
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<oc-button
|
||||
v-for="(action, index) in actions"
|
||||
:key="action.label"
|
||||
:id="action.id"
|
||||
variation="raw"
|
||||
role="menuitem"
|
||||
:class="{ 'uk-margin-small-bottom': index + 1 !== actions.length }"
|
||||
class="uk-width-1-1 uk-flex-left"
|
||||
@click="action.handler"
|
||||
>
|
||||
{{ action.label }}
|
||||
</oc-button>
|
||||
</template>
|
||||
</oc-action-drop>
|
||||
</div>
|
||||
</oc-grid>
|
||||
<template v-if="isInitialized">
|
||||
<accounts-list :accounts="accounts" />
|
||||
</template>
|
||||
@@ -13,22 +40,77 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import { mapGetters, mapActions, mapState, mapMutations } from 'vuex'
|
||||
import AccountsList from './accounts/AccountsList.vue'
|
||||
export default {
|
||||
name: 'App',
|
||||
components: { AccountsList },
|
||||
computed: {
|
||||
...mapGetters('Accounts', ['isInitialized', 'getAccountsSorted']),
|
||||
...mapState('Accounts', ['selectedAccounts']),
|
||||
|
||||
accounts () {
|
||||
return this.getAccountsSorted
|
||||
},
|
||||
|
||||
numberOfSelectedAccounts () {
|
||||
return this.selectedAccounts.length
|
||||
},
|
||||
|
||||
selectionInfoText () {
|
||||
const translated = this.$ngettext('%{ amount } selected user', '%{ amount } selected users', this.numberOfSelectedAccounts)
|
||||
|
||||
return this.$gettextInterpolate(translated, { amount: this.numberOfSelectedAccounts })
|
||||
},
|
||||
|
||||
actions () {
|
||||
const actions = []
|
||||
const numberOfDisabledAccounts = this.selectedAccounts.filter(account => !account.accountEnabled).length
|
||||
const isAnyAccountDisabled = numberOfDisabledAccounts > 0
|
||||
const isAnyAccountEnabled = numberOfDisabledAccounts < this.numberOfSelectedAccounts
|
||||
|
||||
if (isAnyAccountDisabled) {
|
||||
actions.push({
|
||||
id: 'accounts-actions-dropdown-action-enable',
|
||||
label: this.$gettext('Enable'),
|
||||
handler: () => this.toggleAccountStatus(true)
|
||||
})
|
||||
}
|
||||
|
||||
if (isAnyAccountEnabled) {
|
||||
actions.push({
|
||||
id: 'accounts-actions-dropdown-action-disable',
|
||||
label: this.$gettext('Disable'),
|
||||
handler: () => this.toggleAccountStatus(false)
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('Accounts', ['initialize'])
|
||||
...mapActions('Accounts', ['initialize', 'toggleAccountStatus']),
|
||||
...mapMutations('Accounts', ['RESET_ACCOUNTS_SELECTION'])
|
||||
},
|
||||
created () {
|
||||
this.initialize()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.RESET_ACCOUNTS_SELECTION()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* TODO: After https://github.com/owncloud/owncloud-design-system/pull/418 gets merged
|
||||
there won't be an extra span and this won't be needed anymore */
|
||||
.accounts-selection-actions-btn > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* TODO: Adjust in ODS */
|
||||
.oc-dropdown-menu {
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
<oc-table middle divider>
|
||||
<oc-table-group>
|
||||
<oc-table-row>
|
||||
<oc-table-cell shrink type="head">
|
||||
<oc-checkbox
|
||||
:value="areAllAccountsSelected"
|
||||
:label="$gettext('Select all users')"
|
||||
hide-label
|
||||
@change="toggleSelectionAll"
|
||||
/>
|
||||
</oc-table-cell>
|
||||
<oc-table-cell shrink type="head" />
|
||||
<oc-table-cell type="head" v-text="$gettext('Username')" />
|
||||
<oc-table-cell type="head" v-text="$gettext('Display name')" />
|
||||
@@ -25,6 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import AccountsListRow from './AccountsListRow.vue'
|
||||
|
||||
export default {
|
||||
@@ -37,6 +46,12 @@ export default {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('Accounts', ['areAllAccountsSelected'])
|
||||
},
|
||||
methods: {
|
||||
...mapActions('Accounts', ['toggleSelectionAll'])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<oc-table-row>
|
||||
<oc-table-cell>
|
||||
<oc-checkbox :value="isAccountSelected" @change="TOGGLE_SELECTION_ACCOUNT(account)" :label="selectAccountLabel" hide-label />
|
||||
</oc-table-cell>
|
||||
<oc-table-cell>
|
||||
<avatar :user-name="account.displayName || account.onPremisesSamAccountName" :userid="account.id" :width="35" />
|
||||
</oc-table-cell>
|
||||
@@ -39,14 +42,28 @@
|
||||
<oc-table-cell v-text="account.uidNumber || '-'" />
|
||||
<oc-table-cell v-text="account.gidNumber || '-'" />
|
||||
<oc-table-cell class="uk-text-center">
|
||||
<oc-icon v-if="account.accountEnabled" name="ready" variation="success" :aria-label="$gettext('Account is enabled')" />
|
||||
<oc-icon v-else name="deprecated" variation="danger" :aria-label="$gettext('Account is disabled')" />
|
||||
<oc-icon
|
||||
v-if="account.accountEnabled"
|
||||
key="account-icon-enabled"
|
||||
name="ready"
|
||||
variation="success"
|
||||
:aria-label="$gettext('Account is enabled')"
|
||||
class="accounts-status-indicator-enabled"
|
||||
/>
|
||||
<oc-icon
|
||||
v-else
|
||||
name="deprecated"
|
||||
key="account-icon-disabled"
|
||||
variation="danger"
|
||||
:aria-label="$gettext('Account is disabled')"
|
||||
class="accounts-status-indicator-disabled"
|
||||
/>
|
||||
</oc-table-cell>
|
||||
</oc-table-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapState, mapActions } from 'vuex'
|
||||
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex'
|
||||
import { isObjectEmpty } from '../../helpers/utils'
|
||||
import { injectAuthToken } from '../../helpers/auth'
|
||||
// eslint-disable-next-line camelcase
|
||||
@@ -73,7 +90,17 @@ export default {
|
||||
|
||||
computed: {
|
||||
...mapGetters(['user', 'configuration']),
|
||||
...mapState('Accounts', ['roles'])
|
||||
...mapState('Accounts', ['roles', 'selectedAccounts']),
|
||||
|
||||
isAccountSelected () {
|
||||
return this.selectedAccounts.indexOf(this.account) > -1
|
||||
},
|
||||
|
||||
selectAccountLabel () {
|
||||
const translated = this.$gettext('Select %{ account }')
|
||||
|
||||
return this.$gettextInterpolate(translated, { account: this.account.displayName }, true)
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
@@ -82,6 +109,7 @@ export default {
|
||||
|
||||
methods: {
|
||||
...mapActions(['showMessage']),
|
||||
...mapMutations('Accounts', ['TOGGLE_SELECTION_ACCOUNT']),
|
||||
|
||||
async changeRole (roleId) {
|
||||
injectAuthToken(this.user.token)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { AccountsService_ListAccounts } from '../client/accounts'
|
||||
import { AccountsService_ListAccounts, AccountsService_UpdateAccount } from '../client/accounts'
|
||||
import { RoleService_ListRoles } from '../client/settings'
|
||||
/* eslint-enable camelcase */
|
||||
import { injectAuthToken } from '../helpers/auth'
|
||||
@@ -8,7 +8,8 @@ const state = {
|
||||
config: null,
|
||||
initialized: false,
|
||||
accounts: {},
|
||||
roles: null
|
||||
roles: null,
|
||||
selectedAccounts: []
|
||||
}
|
||||
|
||||
const getters = {
|
||||
@@ -21,7 +22,8 @@ const getters = {
|
||||
}
|
||||
return a1.onPremisesSamAccountName.localeCompare(a2.onPremisesSamAccountName)
|
||||
})
|
||||
}
|
||||
},
|
||||
areAllAccountsSelected: state => state.accounts.length === state.selectedAccounts.length
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
@@ -36,6 +38,24 @@ const mutations = {
|
||||
},
|
||||
SET_ROLES (state, roles) {
|
||||
state.roles = roles
|
||||
},
|
||||
TOGGLE_SELECTION_ACCOUNT (state, account) {
|
||||
const accountIndex = state.selectedAccounts.indexOf(account)
|
||||
|
||||
accountIndex > -1 ? state.selectedAccounts.splice(accountIndex, 1) : state.selectedAccounts.push(account)
|
||||
},
|
||||
SET_SELECTED_ACCOUNTS (state, accounts) {
|
||||
state.selectedAccounts = accounts
|
||||
},
|
||||
|
||||
UPDATE_ACCOUNT (state, updatedAccount) {
|
||||
const accountIndex = state.accounts.findIndex(account => account.id === updatedAccount.id)
|
||||
|
||||
state.accounts.splice(accountIndex, 1, updatedAccount)
|
||||
},
|
||||
|
||||
RESET_ACCOUNTS_SELECTION (state) {
|
||||
state.selectedAccounts = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +107,63 @@ const actions = {
|
||||
status: 'danger'
|
||||
}, { root: true })
|
||||
}
|
||||
},
|
||||
|
||||
toggleSelectionAll ({ commit, getters, state }) {
|
||||
getters.areAllAccountsSelected ? commit('RESET_ACCOUNTS_SELECTION') : commit('SET_SELECTED_ACCOUNTS', [...state.accounts])
|
||||
},
|
||||
|
||||
async toggleAccountStatus ({ commit, dispatch, state, rootGetters }, status) {
|
||||
const failedAccounts = []
|
||||
injectAuthToken(rootGetters.user.token)
|
||||
|
||||
for (const account of state.selectedAccounts) {
|
||||
if (account.accountEnabled === status) {
|
||||
continue
|
||||
}
|
||||
|
||||
const response = await AccountsService_UpdateAccount({
|
||||
$domain: rootGetters.configuration.server,
|
||||
body: {
|
||||
account: {
|
||||
id: account.id,
|
||||
accountEnabled: status
|
||||
},
|
||||
update_mask: {
|
||||
paths: ['AccountEnabled']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (response.status === 201) {
|
||||
commit('UPDATE_ACCOUNT', { ...account, accountEnabled: status })
|
||||
} else {
|
||||
failedAccounts.push({ account: account.diisplayName, statusText: response.statusText })
|
||||
}
|
||||
}
|
||||
|
||||
if (failedAccounts.length === 1) {
|
||||
const failedMessageTitle = status ? 'Failed to enable account.' : 'Failed to disable account.'
|
||||
|
||||
dispatch('showMessage', {
|
||||
title: failedMessageTitle,
|
||||
desc: failedAccounts[0].statusText,
|
||||
status: 'danger'
|
||||
}, { root: true })
|
||||
}
|
||||
|
||||
if (failedAccounts.length > 1) {
|
||||
const failedMessageTitle = status ? 'Failed to enable accounts.' : 'Failed to disable accounts.'
|
||||
const failedMessageDesc = status ? 'Could not enable multiple accounts.' : 'Could not disable multiple accounts.'
|
||||
|
||||
dispatch('showMessage', {
|
||||
title: failedMessageTitle,
|
||||
desc: failedMessageDesc,
|
||||
status: 'danger'
|
||||
}, { root: true })
|
||||
}
|
||||
|
||||
commit('RESET_ACCOUNTS_SELECTION')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,3 +32,30 @@ Feature: Accounts
|
||||
And user "Einstein" logs in using the webUI
|
||||
And the user browses to the accounts page
|
||||
Then the user should not be able to see the accounts list on the WebUI
|
||||
|
||||
# We want to separate this into own scenarios but because we do not have clean env for each scenario yet
|
||||
# we are resetting it manually by combining them into one
|
||||
Scenario: disable/enable account
|
||||
Given user "Moss" has logged in using the webUI
|
||||
When the user browses to the accounts page
|
||||
Then user "einstein" should be displayed in the accounts list on the WebUI
|
||||
When the user disables user "einstein" using the WebUI
|
||||
Then the status indicator of user "einstein" should be "disabled" on the WebUI
|
||||
# And user "einstein" should not be able to log in
|
||||
When the user enables user "einstein" using the WebUI
|
||||
Then the status indicator of user "einstein" should be "enabled" on the WebUI
|
||||
# And user "einstein" should be able to log in
|
||||
|
||||
Scenario: disable/enable multiple accounts
|
||||
Given user "Moss" has logged in using the webUI
|
||||
When the user browses to the accounts page
|
||||
Then user "einstein" should be displayed in the accounts list on the WebUI
|
||||
And user "marie" should be displayed in the accounts list on the WebUI
|
||||
When the user disables users "einstein,marie" using the WebUI
|
||||
Then the status indicator of users "einstein,marie" should be "disabled" on the WebUI
|
||||
# And user "einstein" should not be able to log in
|
||||
# And user "marie" should not be able to log in
|
||||
When the user enables users "einstein,marie" using the WebUI
|
||||
Then the status indicator of user "einstein,marie" should be "enabled" on the WebUI
|
||||
# And user "einstein" should be able to log in
|
||||
# And user "marie" should be able to log in
|
||||
|
||||
@@ -40,6 +40,42 @@ module.exports = {
|
||||
util.format(this.elements.currentRole.selector, role)
|
||||
|
||||
return this.useXpath().expect.element(roleSelector).to.be.visible
|
||||
},
|
||||
|
||||
toggleUserStatus: function (usernames, status) {
|
||||
usernames = usernames.split(',')
|
||||
const actionSelector = status === 'enabled' ? this.elements.enableAction : this.elements.disableAction
|
||||
|
||||
// Select users
|
||||
for (const username of usernames) {
|
||||
const checkboxSelector =
|
||||
util.format(this.elements.rowByUsername.selector, username) +
|
||||
this.elements.rowCheckbox.selector
|
||||
|
||||
this.useXpath().click(checkboxSelector)
|
||||
}
|
||||
|
||||
return this
|
||||
.waitForElementVisible('@actionsDropdownTrigger')
|
||||
.click('@actionsDropdownTrigger')
|
||||
.useCss()
|
||||
.waitForElementVisible(actionSelector)
|
||||
.click(actionSelector)
|
||||
.useXpath()
|
||||
},
|
||||
|
||||
checkUsersStatus: function (usernames, status) {
|
||||
usernames = usernames.split(',')
|
||||
|
||||
for (const username of usernames) {
|
||||
const indicatorSelector =
|
||||
util.format(this.elements.rowByUsername.selector, username) +
|
||||
util.format(this.elements.statusIndicator.selector, status)
|
||||
|
||||
this.useXpath().waitForElementVisible(indicatorSelector)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
},
|
||||
|
||||
@@ -75,6 +111,24 @@ module.exports = {
|
||||
loadingAccountsList: {
|
||||
selector: '//div[contains(@class, "oc-loader")]',
|
||||
locateStrategy: 'xpath'
|
||||
},
|
||||
rowCheckbox: {
|
||||
selector: '//input[@class="oc-checkbox"]',
|
||||
locateStrategy: 'xpath'
|
||||
},
|
||||
actionsDropdownTrigger: {
|
||||
selector: '//div[contains(@class, "accounts-actions-dropdown")]//button[normalize-space()="Actions"]',
|
||||
locateStrategy: 'xpath'
|
||||
},
|
||||
disableAction: {
|
||||
selector: '#accounts-actions-dropdown-action-disable'
|
||||
},
|
||||
enableAction: {
|
||||
selector: '#accounts-actions-dropdown-action-enable'
|
||||
},
|
||||
statusIndicator: {
|
||||
selector: '//span[contains(@class, "accounts-status-indicator-%s")]',
|
||||
locateStrategy: 'xpath'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +30,15 @@ Then('the user should not be able to see the accounts list on the WebUI', async
|
||||
.waitForElementVisible('@loadingAccountsList')
|
||||
.waitForElementNotPresent('@accountsListTable')
|
||||
})
|
||||
|
||||
When('the user disables user/users {string} using the WebUI', function (usernames) {
|
||||
return client.page.accountsPage().toggleUserStatus(usernames, 'disabled')
|
||||
})
|
||||
|
||||
When('the user enables user/users {string} using the WebUI', function (usernames) {
|
||||
return client.page.accountsPage().toggleUserStatus(usernames, 'enabled')
|
||||
})
|
||||
|
||||
Then('the status indicator of user/users {string} should be {string} on the WebUI', function (usernames, status) {
|
||||
return client.page.accountsPage().checkUsersStatus(usernames, status)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user