diff --git a/changelog/unreleased/enhancement-user-role-filter b/changelog/unreleased/enhancement-user-role-filter
new file mode 100644
index 00000000000..860363d276f
--- /dev/null
+++ b/changelog/unreleased/enhancement-user-role-filter
@@ -0,0 +1,5 @@
+Enhancement: User role filter
+
+Users in the users list can now be filtered by their role assignments.
+
+https://github.com/owncloud/web/pull/8492
diff --git a/packages/web-app-admin-settings/src/views/Users.vue b/packages/web-app-admin-settings/src/views/Users.vue
index 54c91d212fa..d894cbe94fc 100644
--- a/packages/web-app-admin-settings/src/views/Users.vue
+++ b/packages/web-app-admin-settings/src/views/Users.vue
@@ -62,6 +62,7 @@
:allow-multiple="true"
display-name-attribute="displayName"
:filterable-attributes="['displayName']"
+ class="oc-mr-s"
@selection-change="filterGroups"
>
@@ -71,6 +72,23 @@
+
+
+
+
+
+
+
+
@@ -132,6 +150,7 @@ import { toRaw } from 'vue'
import { SpaceResource } from 'web-client/src'
import { useGettext } from 'vue3-gettext'
import { diff } from 'deep-object-diff'
+import { format } from 'util'
export default defineComponent({
name: 'UsersView',
@@ -169,7 +188,18 @@ export default defineComponent({
let loadResourcesEventToken
let userUpdatedEventToken
- const groupFilterParam = useRouteQuery('q_groups')
+ const filters = {
+ groups: {
+ param: useRouteQuery('q_groups'),
+ query: `memberOf/any(m:m/id eq '%s')`,
+ ids: ref([])
+ },
+ roles: {
+ param: useRouteQuery('q_roles'),
+ query: `appRoleAssignments/any(m:m/appRoleId eq '%s')`,
+ ids: ref([])
+ }
+ }
const loadGroupsTask = useTask(function* (signal) {
const groupsResponse = yield unref(graphClient).groups.listGroups()
@@ -181,14 +211,25 @@ export default defineComponent({
roles.value = applicationsResponse.data.value[0].appRoles
})
- const loadUsersTask = useTask(function* (signal, groupIds) {
- const groupFilter = groupIds?.map((id) => `memberOf/any(m:m/id eq '${id}')`).join(' and ')
- const usersResponse = yield unref(graphClient).users.listUsers('displayName', groupFilter)
+ const loadUsersTask = useTask(function* (signal) {
+ const filter = Object.values(filters)
+ .reduce((acc, f) => {
+ acc.push(
+ unref(f.ids)
+ .map((id) => format(f.query, id))
+ .join(' and ')
+ )
+ return acc
+ }, [])
+ .filter(Boolean)
+ .join(' and ')
+
+ const usersResponse = yield unref(graphClient).users.listUsers('displayName', filter)
users.value = usersResponse.data.value || []
})
- const loadResourcesTask = useTask(function* (signal, groupIds = null) {
- yield loadUsersTask.perform(groupIds)
+ const loadResourcesTask = useTask(function* (signal) {
+ yield loadUsersTask.perform()
yield loadGroupsTask.perform()
yield loadAppRolesTask.perform()
})
@@ -212,8 +253,12 @@ export default defineComponent({
})
const filterGroups = (groups) => {
- const groupIds = groups.map((g) => g.id)
- loadUsersTask.perform(groupIds)
+ filters.groups.ids.value = groups.map((g) => g.id)
+ loadUsersTask.perform()
+ }
+ const filterRoles = (roles) => {
+ filters.roles.ids.value = roles.map((r) => r.id)
+ loadUsersTask.perform()
}
const selectedPersonalDrives = ref([])
@@ -261,8 +306,11 @@ export default defineComponent({
})
onMounted(async () => {
- const groupFilterIds = queryItemAsString(unref(groupFilterParam))?.split('+')
- await loadResourcesTask.perform(groupFilterIds)
+ for (const f in filters) {
+ filters[f].ids.value = queryItemAsString(unref(filters[f].param))?.split('+') || []
+ }
+
+ await loadResourcesTask.perform()
loadResourcesEventToken = eventBus.subscribe('app.admin-settings.list.load', () => {
loadResourcesTask.perform()
selectedUsers.value = []
@@ -308,6 +356,7 @@ export default defineComponent({
createUserModalOpen,
batchActions,
filterGroups,
+ filterRoles,
quotaModalIsOpen,
closeQuotaModal,
spaceQuotaUpdated,
diff --git a/packages/web-app-admin-settings/tests/unit/views/Users.spec.ts b/packages/web-app-admin-settings/tests/unit/views/Users.spec.ts
index 8ee773617f0..a4bbb0b0525 100644
--- a/packages/web-app-admin-settings/tests/unit/views/Users.spec.ts
+++ b/packages/web-app-admin-settings/tests/unit/views/Users.spec.ts
@@ -86,7 +86,8 @@ const getDefaultGraphMock = () => {
}
const selectors = {
- itemFilterGroupsStub: 'item-filter-stub[filtername="groups"]'
+ itemFilterGroupsStub: 'item-filter-stub[filtername="groups"]',
+ itemFilterRolesStub: 'item-filter-stub[filtername="roles"]'
}
describe('Users view', () => {
@@ -334,36 +335,71 @@ describe('Users view', () => {
})
describe('filter', () => {
- it('does filter users by groups when the "selectionChange"-event is triggered', async () => {
- const graphMock = getDefaultGraphMock()
- const { wrapper } = getMountedWrapper({ mountType: mount, graph: graphMock })
- await wrapper.vm.loadResourcesTask.last
- expect(graphMock.users.listUsers).toHaveBeenCalledTimes(1)
- ;(wrapper.findComponent(selectors.itemFilterGroupsStub).vm as any).$emit(
- 'selectionChange',
- [{ id: '1' }]
- )
- await wrapper.vm.$nextTick()
- expect(graphMock.users.listUsers).toHaveBeenCalledTimes(2)
- expect(graphMock.users.listUsers).toHaveBeenNthCalledWith(
- 2,
- 'displayName',
- "memberOf/any(m:m/id eq '1')"
- )
+ describe('groups', () => {
+ it('does filter users by groups when the "selectionChange"-event is triggered', async () => {
+ const graphMock = getDefaultGraphMock()
+ const { wrapper } = getMountedWrapper({ mountType: mount, graph: graphMock })
+ await wrapper.vm.loadResourcesTask.last
+ expect(graphMock.users.listUsers).toHaveBeenCalledTimes(1)
+ ;(wrapper.findComponent(selectors.itemFilterGroupsStub).vm as any).$emit(
+ 'selectionChange',
+ [{ id: '1' }]
+ )
+ await wrapper.vm.$nextTick()
+ expect(graphMock.users.listUsers).toHaveBeenCalledTimes(2)
+ expect(graphMock.users.listUsers).toHaveBeenNthCalledWith(
+ 2,
+ 'displayName',
+ "memberOf/any(m:m/id eq '1')"
+ )
+ })
+ it('does filter initially if group ids are given via query param', async () => {
+ const groupIdsQueryParam = '1+2'
+ const graphMock = getDefaultGraphMock()
+ const { wrapper } = getMountedWrapper({
+ mountType: mount,
+ graph: graphMock,
+ groupFilterQuery: groupIdsQueryParam
+ })
+ await wrapper.vm.loadResourcesTask.last
+ expect(graphMock.users.listUsers).toHaveBeenCalledWith(
+ 'displayName',
+ "memberOf/any(m:m/id eq '1') and memberOf/any(m:m/id eq '2')"
+ )
+ })
})
- it('does filter initially if group ids are given via query param', async () => {
- const groupIdsQueryParam = '1+2'
- const graphMock = getDefaultGraphMock()
- const { wrapper } = getMountedWrapper({
- mountType: mount,
- graph: graphMock,
- queryItem: groupIdsQueryParam
+ describe('roles', () => {
+ it('does filter users by roles when the "selectionChange"-event is triggered', async () => {
+ const graphMock = getDefaultGraphMock()
+ const { wrapper } = getMountedWrapper({ mountType: mount, graph: graphMock })
+ await wrapper.vm.loadResourcesTask.last
+ expect(graphMock.users.listUsers).toHaveBeenCalledTimes(1)
+ ;(wrapper.findComponent(selectors.itemFilterRolesStub).vm as any).$emit(
+ 'selectionChange',
+ [{ id: '1' }]
+ )
+ await wrapper.vm.$nextTick()
+ expect(graphMock.users.listUsers).toHaveBeenCalledTimes(2)
+ expect(graphMock.users.listUsers).toHaveBeenNthCalledWith(
+ 2,
+ 'displayName',
+ "appRoleAssignments/any(m:m/appRoleId eq '1')"
+ )
+ })
+ it('does filter initially if group ids are given via query param', async () => {
+ const roleIdsQueryParam = '1+2'
+ const graphMock = getDefaultGraphMock()
+ const { wrapper } = getMountedWrapper({
+ mountType: mount,
+ graph: graphMock,
+ roleFilterQuery: roleIdsQueryParam
+ })
+ await wrapper.vm.loadResourcesTask.last
+ expect(graphMock.users.listUsers).toHaveBeenCalledWith(
+ 'displayName',
+ "appRoleAssignments/any(m:m/appRoleId eq '1') and appRoleAssignments/any(m:m/appRoleId eq '2')"
+ )
})
- await wrapper.vm.loadResourcesTask.last
- expect(graphMock.users.listUsers).toHaveBeenCalledWith(
- 'displayName',
- "memberOf/any(m:m/id eq '1') and memberOf/any(m:m/id eq '2')"
- )
})
})
})
@@ -371,9 +407,11 @@ describe('Users view', () => {
function getMountedWrapper({
mountType = shallowMount,
graph = getDefaultGraphMock(),
- queryItem = null
+ groupFilterQuery = null,
+ roleFilterQuery = null
} = {}) {
- jest.mocked(queryItemAsString).mockImplementation(() => queryItem)
+ jest.mocked(queryItemAsString).mockImplementationOnce(() => groupFilterQuery)
+ jest.mocked(queryItemAsString).mockImplementationOnce(() => roleFilterQuery)
const mocks = {
...defaultComponentMocks(),
...getActionMixinMocks({ actions: mixins })