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" > + + + + @@ -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 })