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>
import { GlFilteredSearchToken } from '@gitlab/ui';
import { mapState } from 'vuex';
import {
getParameterByName,
......@@ -7,46 +6,24 @@ import {
queryToObject,
redirectTo,
} from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import {
SEARCH_TOKEN_TYPE,
SORT_QUERY_PARAM_NAME,
ACTIVE_TAB_QUERY_PARAM_NAME,
} from '~/members/constants';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
AVAILABLE_FILTERED_SEARCH_TOKENS,
} from 'ee_else_ce/members/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
export default {
name: 'MembersFilteredSearchBar',
components: { FilteredSearchBar },
availableTokens: [
{
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',
},
{
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'],
availableTokens: AVAILABLE_FILTERED_SEARCH_TOKENS,
inject: {
namespace: {},
sourceId: {},
canManageMembers: {},
canFilterByEnterprise: { default: false },
},
data() {
return {
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_SOURCE = 'source';
......@@ -82,6 +85,38 @@ export const DEFAULT_SORT = {
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 MEMBER_TYPES = {
......
......@@ -18,6 +18,7 @@ export const initMembersApp = (el, options) => {
sourceId,
canManageMembers,
canExportMembers,
canFilterByEnterprise,
exportCsvPath,
...vuexStoreAttributes
} = parseDataAttributes(el);
......@@ -60,6 +61,7 @@ export const initMembersApp = (el, options) => {
currentUserId: gon.current_user_id || null,
sourceId,
canManageMembers,
canFilterByEnterprise,
canExportMembers,
exportCsvPath,
},
......
......@@ -21,7 +21,7 @@ initMembersApp(document.querySelector('.js-group-members-list-app'), {
requestFormatter: groupMemberRequestFormatter,
filteredSearchBar: {
show: true,
tokens: ['two_factor', 'with_inherited_permissions'],
tokens: ['two_factor', 'with_inherited_permissions', 'enterprise'],
searchParam: 'search',
placeholder: s__('Members|Filter members'),
recentSearchesStorageKey: 'group_members',
......
......@@ -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 of the group and its subgroups, select **Membership = Inherited**.
- 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
......
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 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 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 { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import {
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';
jest.mock('~/lib/utils/url_utility', () => {
......@@ -32,7 +34,7 @@ describe('MembersFilteredSearchBar', () => {
state: {
filteredSearchBar: {
show: true,
tokens: ['two_factor'],
tokens: [FILTERED_SEARCH_TOKEN_TWO_FACTOR.type],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
......@@ -70,21 +72,7 @@ describe('MembersFilteredSearchBar', () => {
it('includes tokens set in `filteredSearchBar.tokens`', () => {
createComponent();
expect(findFilteredSearchBar().props('tokens')).toEqual([
{
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',
},
]);
expect(findFilteredSearchBar().props('tokens')).toEqual([FILTERED_SEARCH_TOKEN_TWO_FACTOR]);
});
describe('when `canManageMembers` is false', () => {
......@@ -93,7 +81,10 @@ describe('MembersFilteredSearchBar', () => {
state: {
filteredSearchBar: {
show: true,
tokens: ['two_factor', 'with_inherited_permissions'],
tokens: [
FILTERED_SEARCH_TOKEN_TWO_FACTOR.type,
FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS.type,
],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
......@@ -105,18 +96,7 @@ describe('MembersFilteredSearchBar', () => {
});
expect(findFilteredSearchBar().props('tokens')).toEqual([
{
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' },
],
},
FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS,
]);
});
});
......@@ -134,7 +114,7 @@ describe('MembersFilteredSearchBar', () => {
expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([
{
type: 'two_factor',
type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type,
value: {
data: 'enabled',
operator: '=',
......@@ -183,7 +163,7 @@ describe('MembersFilteredSearchBar', () => {
createComponent();
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');
......@@ -193,7 +173,7 @@ describe('MembersFilteredSearchBar', () => {
createComponent();
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' } },
]);
......@@ -206,7 +186,7 @@ describe('MembersFilteredSearchBar', () => {
createComponent();
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' } },
]);
......@@ -221,7 +201,7 @@ describe('MembersFilteredSearchBar', () => {
createComponent();
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' } },
]);
......
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