Commit 4f1b22da authored by peterhegman's avatar peterhegman

Add `Enterprise` filter to members search bar

Allow filtering by users that have the Enterprise badge (provisioned
by SAML or SCIM)

Changelog: added
EE: true
parent 1bb6a4ab
<script> <script>
import { GlFilteredSearchToken } from '@gitlab/ui';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { import {
getParameterByName, getParameterByName,
...@@ -7,46 +6,24 @@ import { ...@@ -7,46 +6,24 @@ import {
queryToObject, queryToObject,
redirectTo, redirectTo,
} from '~/lib/utils/url_utility'; } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { import {
SEARCH_TOKEN_TYPE, SEARCH_TOKEN_TYPE,
SORT_QUERY_PARAM_NAME, SORT_QUERY_PARAM_NAME,
ACTIVE_TAB_QUERY_PARAM_NAME, ACTIVE_TAB_QUERY_PARAM_NAME,
} from '~/members/constants'; AVAILABLE_FILTERED_SEARCH_TOKENS,
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; } from 'ee_else_ce/members/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
export default { export default {
name: 'MembersFilteredSearchBar', name: 'MembersFilteredSearchBar',
components: { FilteredSearchBar }, components: { FilteredSearchBar },
availableTokens: [ availableTokens: AVAILABLE_FILTERED_SEARCH_TOKENS,
{ inject: {
type: 'two_factor', namespace: {},
icon: 'lock', sourceId: {},
title: s__('Members|2FA'), canManageMembers: {},
token: GlFilteredSearchToken, canFilterByEnterprise: { default: false },
unique: true, },
operators: OPERATOR_IS_ONLY,
options: [
{ value: 'enabled', title: s__('Members|Enabled') },
{ value: 'disabled', title: s__('Members|Disabled') },
],
requiredPermissions: 'canManageMembers',
},
{
type: 'with_inherited_permissions',
icon: 'group',
title: s__('Members|Membership'),
token: GlFilteredSearchToken,
unique: true,
operators: OPERATOR_IS_ONLY,
options: [
{ value: 'exclude', title: s__('Members|Direct') },
{ value: 'only', title: s__('Members|Inherited') },
],
},
],
inject: ['namespace', 'sourceId', 'canManageMembers'],
data() { data() {
return { return {
initialFilterValue: [], initialFilterValue: [],
......
import { __ } from '~/locale'; import { GlFilteredSearchToken } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
export const FIELD_KEY_ACCOUNT = 'account'; export const FIELD_KEY_ACCOUNT = 'account';
export const FIELD_KEY_SOURCE = 'source'; export const FIELD_KEY_SOURCE = 'source';
...@@ -82,6 +85,38 @@ export const DEFAULT_SORT = { ...@@ -82,6 +85,38 @@ export const DEFAULT_SORT = {
sortDesc: false, sortDesc: false,
}; };
export const FILTERED_SEARCH_TOKEN_TWO_FACTOR = {
type: 'two_factor',
icon: 'lock',
title: s__('Members|2FA'),
token: GlFilteredSearchToken,
unique: true,
operators: OPERATOR_IS_ONLY,
options: [
{ value: 'enabled', title: s__('Members|Enabled') },
{ value: 'disabled', title: s__('Members|Disabled') },
],
requiredPermissions: 'canManageMembers',
};
export const FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS = {
type: 'with_inherited_permissions',
icon: 'group',
title: s__('Members|Membership'),
token: GlFilteredSearchToken,
unique: true,
operators: OPERATOR_IS_ONLY,
options: [
{ value: 'exclude', title: s__('Members|Direct') },
{ value: 'only', title: s__('Members|Inherited') },
],
};
export const AVAILABLE_FILTERED_SEARCH_TOKENS = [
FILTERED_SEARCH_TOKEN_TWO_FACTOR,
FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS,
];
export const AVATAR_SIZE = 48; export const AVATAR_SIZE = 48;
export const MEMBER_TYPES = { export const MEMBER_TYPES = {
......
...@@ -18,6 +18,7 @@ export const initMembersApp = (el, options) => { ...@@ -18,6 +18,7 @@ export const initMembersApp = (el, options) => {
sourceId, sourceId,
canManageMembers, canManageMembers,
canExportMembers, canExportMembers,
canFilterByEnterprise,
exportCsvPath, exportCsvPath,
...vuexStoreAttributes ...vuexStoreAttributes
} = parseDataAttributes(el); } = parseDataAttributes(el);
...@@ -60,6 +61,7 @@ export const initMembersApp = (el, options) => { ...@@ -60,6 +61,7 @@ export const initMembersApp = (el, options) => {
currentUserId: gon.current_user_id || null, currentUserId: gon.current_user_id || null,
sourceId, sourceId,
canManageMembers, canManageMembers,
canFilterByEnterprise,
canExportMembers, canExportMembers,
exportCsvPath, exportCsvPath,
}, },
......
...@@ -21,7 +21,7 @@ initMembersApp(document.querySelector('.js-group-members-list-app'), { ...@@ -21,7 +21,7 @@ initMembersApp(document.querySelector('.js-group-members-list-app'), {
requestFormatter: groupMemberRequestFormatter, requestFormatter: groupMemberRequestFormatter,
filteredSearchBar: { filteredSearchBar: {
show: true, show: true,
tokens: ['two_factor', 'with_inherited_permissions'], tokens: ['two_factor', 'with_inherited_permissions', 'enterprise'],
searchParam: 'search', searchParam: 'search',
placeholder: s__('Members|Filter members'), placeholder: s__('Members|Filter members'),
recentSearchesStorageKey: 'group_members', recentSearchesStorageKey: 'group_members',
......
...@@ -172,6 +172,7 @@ Filter a group to find members. By default, all members in the group and subgrou ...@@ -172,6 +172,7 @@ Filter a group to find members. By default, all members in the group and subgrou
- To view members in the group only, select **Membership = Direct**. - To view members in the group only, select **Membership = Direct**.
- To view members of the group and its subgroups, select **Membership = Inherited**. - To view members of the group and its subgroups, select **Membership = Inherited**.
- To view members with two-factor authentication enabled or disabled, select **2FA = Enabled** or **Disabled**. - To view members with two-factor authentication enabled or disabled, select **2FA = Enabled** or **Disabled**.
- [In GitLab 14.0 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/349887), to view GitLab users created by [SAML SSO](saml_sso/index.md) or [SCIM provisioning](saml_sso/scim_setup.md) select **Enterprise = true**.
### Search a group ### Search a group
......
import { GlFilteredSearchToken } from '@gitlab/ui';
import { __ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import { AVAILABLE_FILTERED_SEARCH_TOKENS as AVAILABLE_FILTERED_SEARCH_TOKENS_CE } from '~/members/constants';
// eslint-disable-next-line import/export
export * from '~/members/constants';
export const LDAP_OVERRIDE_CONFIRMATION_MODAL_ID = 'ldap-override-confirmation-modal'; export const LDAP_OVERRIDE_CONFIRMATION_MODAL_ID = 'ldap-override-confirmation-modal';
export const FILTERED_SEARCH_TOKEN_ENTERPRISE = {
type: 'enterprise',
icon: 'work',
title: __('Enterprise'),
token: GlFilteredSearchToken,
unique: true,
operators: OPERATOR_IS_ONLY,
options: [
{ value: true, title: __('Yes') },
{ value: false, title: __('No') },
],
requiredPermissions: 'canFilterByEnterprise',
};
// eslint-disable-next-line import/export
export const AVAILABLE_FILTERED_SEARCH_TOKENS = [
...AVAILABLE_FILTERED_SEARCH_TOKENS_CE,
FILTERED_SEARCH_TOKEN_ENTERPRISE,
];
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
import { redirectTo } from '~/lib/utils/url_utility';
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { FILTERED_SEARCH_TOKEN_ENTERPRISE } from 'ee/members/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
jest.mock('~/lib/utils/url_utility', () => {
const urlUtility = jest.requireActual('~/lib/utils/url_utility');
return {
__esModule: true,
...urlUtility,
redirectTo: jest.fn(),
};
});
Vue.use(Vuex);
describe('MembersFilteredSearchBar', () => {
let wrapper;
const createComponent = ({ state = {}, provide = {} } = {}) => {
const store = new Vuex.Store({
modules: {
[MEMBER_TYPES.user]: {
namespaced: true,
state: {
filteredSearchBar: {
show: true,
tokens: ['enterprise'],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
},
...state,
},
},
},
});
wrapper = shallowMount(MembersFilteredSearchBar, {
provide: {
sourceId: 1,
canManageMembers: true,
canFilterByEnterprise: true,
namespace: MEMBER_TYPES.user,
...provide,
},
store,
});
};
const findFilteredSearchBar = () => wrapper.find(FilteredSearchBar);
describe('when `canFilterByEnterprise` is `true`', () => {
it('includes `enterprise` token in `filteredSearchBar.tokens`', () => {
createComponent();
expect(findFilteredSearchBar().props('tokens')).toEqual([FILTERED_SEARCH_TOKEN_ENTERPRISE]);
});
});
describe('when `canFilterByEnterprise` is `false`', () => {
it('does not include `enterprise` token in `filteredSearchBar.tokens`', () => {
createComponent({ provide: { canFilterByEnterprise: false } });
expect(findFilteredSearchBar().props('tokens')).toEqual([]);
});
});
describe('when filtered search bar is submitted with `enterprise = true` filter', () => {
beforeEach(() => {
setWindowLocation('https://localhost');
});
it('adds correct `?enterprise=true` query param', () => {
createComponent();
findFilteredSearchBar().vm.$emit('onFilter', [
{ type: FILTERED_SEARCH_TOKEN_ENTERPRISE.type, value: { data: true, operator: '=' } },
]);
expect(redirectTo).toHaveBeenCalledWith('https://localhost/?enterprise=true');
});
});
});
import { GlFilteredSearchToken } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper'; import setWindowLocation from 'helpers/set_window_location_helper';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue'; import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
import { MEMBER_TYPES } from '~/members/constants'; import {
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; MEMBER_TYPES,
FILTERED_SEARCH_TOKEN_TWO_FACTOR,
FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS,
} from '~/members/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
jest.mock('~/lib/utils/url_utility', () => { jest.mock('~/lib/utils/url_utility', () => {
...@@ -32,7 +34,7 @@ describe('MembersFilteredSearchBar', () => { ...@@ -32,7 +34,7 @@ describe('MembersFilteredSearchBar', () => {
state: { state: {
filteredSearchBar: { filteredSearchBar: {
show: true, show: true,
tokens: ['two_factor'], tokens: [FILTERED_SEARCH_TOKEN_TWO_FACTOR.type],
searchParam: 'search', searchParam: 'search',
placeholder: 'Filter members', placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members', recentSearchesStorageKey: 'group_members',
...@@ -70,21 +72,7 @@ describe('MembersFilteredSearchBar', () => { ...@@ -70,21 +72,7 @@ describe('MembersFilteredSearchBar', () => {
it('includes tokens set in `filteredSearchBar.tokens`', () => { it('includes tokens set in `filteredSearchBar.tokens`', () => {
createComponent(); createComponent();
expect(findFilteredSearchBar().props('tokens')).toEqual([ expect(findFilteredSearchBar().props('tokens')).toEqual([FILTERED_SEARCH_TOKEN_TWO_FACTOR]);
{
type: 'two_factor',
icon: 'lock',
title: '2FA',
token: GlFilteredSearchToken,
unique: true,
operators: OPERATOR_IS_ONLY,
options: [
{ value: 'enabled', title: 'Enabled' },
{ value: 'disabled', title: 'Disabled' },
],
requiredPermissions: 'canManageMembers',
},
]);
}); });
describe('when `canManageMembers` is false', () => { describe('when `canManageMembers` is false', () => {
...@@ -93,7 +81,10 @@ describe('MembersFilteredSearchBar', () => { ...@@ -93,7 +81,10 @@ describe('MembersFilteredSearchBar', () => {
state: { state: {
filteredSearchBar: { filteredSearchBar: {
show: true, show: true,
tokens: ['two_factor', 'with_inherited_permissions'], tokens: [
FILTERED_SEARCH_TOKEN_TWO_FACTOR.type,
FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS.type,
],
searchParam: 'search', searchParam: 'search',
placeholder: 'Filter members', placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members', recentSearchesStorageKey: 'group_members',
...@@ -105,18 +96,7 @@ describe('MembersFilteredSearchBar', () => { ...@@ -105,18 +96,7 @@ describe('MembersFilteredSearchBar', () => {
}); });
expect(findFilteredSearchBar().props('tokens')).toEqual([ expect(findFilteredSearchBar().props('tokens')).toEqual([
{ FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS,
type: 'with_inherited_permissions',
icon: 'group',
title: 'Membership',
token: GlFilteredSearchToken,
unique: true,
operators: OPERATOR_IS_ONLY,
options: [
{ value: 'exclude', title: 'Direct' },
{ value: 'only', title: 'Inherited' },
],
},
]); ]);
}); });
}); });
...@@ -134,7 +114,7 @@ describe('MembersFilteredSearchBar', () => { ...@@ -134,7 +114,7 @@ describe('MembersFilteredSearchBar', () => {
expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([ expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([
{ {
type: 'two_factor', type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type,
value: { value: {
data: 'enabled', data: 'enabled',
operator: '=', operator: '=',
...@@ -183,7 +163,7 @@ describe('MembersFilteredSearchBar', () => { ...@@ -183,7 +163,7 @@ describe('MembersFilteredSearchBar', () => {
createComponent(); createComponent();
findFilteredSearchBar().vm.$emit('onFilter', [ findFilteredSearchBar().vm.$emit('onFilter', [
{ type: 'two_factor', value: { data: 'enabled', operator: '=' } }, { type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type, value: { data: 'enabled', operator: '=' } },
]); ]);
expect(redirectTo).toHaveBeenCalledWith('https://localhost/?two_factor=enabled'); expect(redirectTo).toHaveBeenCalledWith('https://localhost/?two_factor=enabled');
...@@ -193,7 +173,7 @@ describe('MembersFilteredSearchBar', () => { ...@@ -193,7 +173,7 @@ describe('MembersFilteredSearchBar', () => {
createComponent(); createComponent();
findFilteredSearchBar().vm.$emit('onFilter', [ findFilteredSearchBar().vm.$emit('onFilter', [
{ type: 'two_factor', value: { data: 'enabled', operator: '=' } }, { type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type, value: { data: 'enabled', operator: '=' } },
{ type: 'filtered-search-term', value: { data: 'foobar' } }, { type: 'filtered-search-term', value: { data: 'foobar' } },
]); ]);
...@@ -206,7 +186,7 @@ describe('MembersFilteredSearchBar', () => { ...@@ -206,7 +186,7 @@ describe('MembersFilteredSearchBar', () => {
createComponent(); createComponent();
findFilteredSearchBar().vm.$emit('onFilter', [ findFilteredSearchBar().vm.$emit('onFilter', [
{ type: 'two_factor', value: { data: 'enabled', operator: '=' } }, { type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type, value: { data: 'enabled', operator: '=' } },
{ type: 'filtered-search-term', value: { data: 'foo bar baz' } }, { type: 'filtered-search-term', value: { data: 'foo bar baz' } },
]); ]);
...@@ -221,7 +201,7 @@ describe('MembersFilteredSearchBar', () => { ...@@ -221,7 +201,7 @@ describe('MembersFilteredSearchBar', () => {
createComponent(); createComponent();
findFilteredSearchBar().vm.$emit('onFilter', [ findFilteredSearchBar().vm.$emit('onFilter', [
{ type: 'two_factor', value: { data: 'enabled', operator: '=' } }, { type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type, value: { data: 'enabled', operator: '=' } },
{ type: 'filtered-search-term', value: { data: 'foobar' } }, { type: 'filtered-search-term', value: { data: 'foobar' } },
]); ]);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment