summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-03 06:09:47 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-03 06:09:47 +0000
commit9214e550c07793a8deb6d5cd5bb136d0d010a7ca (patch)
treebf094d583e9f57e2816a6f272bcbff302e264efe
parente1e9056d03fec6d72771c7a4ba3fc1174b5ac009 (diff)
downloadgitlab-ce-9214e550c07793a8deb6d5cd5bb136d0d010a7ca.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/filtered_search/recent_searches_storage_keys.js2
-rw-r--r--app/assets/javascripts/groups/members/components/app.vue6
-rw-r--r--app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue27
-rw-r--r--app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue18
-rw-r--r--app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue132
-rw-r--r--app/assets/javascripts/members/constants.js4
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js6
-rw-r--r--app/controllers/groups/group_members_controller.rb4
-rw-r--r--app/views/groups/group_members/index.html.haml53
-rw-r--r--changelogs/unreleased/242028-migrate-bs-callout-to-glalert-in-app-assets-javascripts-jobs-compo.yml5
-rw-r--r--config/feature_flags/development/group_members_filtered_search.yml8
-rw-r--r--doc/user/project/integrations/webhooks.md4
-rw-r--r--locale/gitlab.pot18
-rw-r--r--spec/features/groups/members/filter_members_spec.rb26
-rw-r--r--spec/features/groups/members/search_members_spec.rb7
-rw-r--r--spec/features/groups/members/sort_members_spec.rb2
-rw-r--r--spec/features/groups/members/tabs_spec.rb14
-rw-r--r--spec/frontend/groups/members/components/app_spec.js23
-rw-r--r--spec/frontend/jobs/components/unmet_prerequisites_block_spec.js34
-rw-r--r--spec/frontend/members/components/filter_sort/filter_sort_container_spec.js55
-rw-r--r--spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js176
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb1
22 files changed, 549 insertions, 76 deletions
diff --git a/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js
index 7e9b809e9b2..54d49821d92 100644
--- a/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js
+++ b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js
@@ -1,4 +1,6 @@
export default {
issues: 'issue-recent-searches',
merge_requests: 'merge-request-recent-searches',
+ group_members: 'group-members-recent-searches',
+ group_invited_members: 'group-invited-members-recent-searches',
};
diff --git a/app/assets/javascripts/groups/members/components/app.vue b/app/assets/javascripts/groups/members/components/app.vue
index 8f1bb6e8094..f6f3a955813 100644
--- a/app/assets/javascripts/groups/members/components/app.vue
+++ b/app/assets/javascripts/groups/members/components/app.vue
@@ -2,12 +2,15 @@
import { mapState, mapMutations } from 'vuex';
import { GlAlert } from '@gitlab/ui';
import MembersTable from '~/members/components/table/members_table.vue';
+import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
import { HIDE_ERROR } from '~/members/store/mutation_types';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'GroupMembersApp',
- components: { MembersTable, GlAlert },
+ components: { MembersTable, FilterSortContainer, GlAlert },
+ mixins: [glFeatureFlagsMixin()],
computed: {
...mapState(['showError', 'errorMessage']),
},
@@ -33,6 +36,7 @@ export default {
<gl-alert v-if="showError" ref="errorAlert" variant="danger" @dismiss="hideError">{{
errorMessage
}}</gl-alert>
+ <filter-sort-container v-if="glFeatures.groupMembersFilteredSearch" />
<members-table />
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue b/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue
index 633561c879e..c9747ca9f02 100644
--- a/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue
+++ b/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue
@@ -1,11 +1,19 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlLink, GlAlert } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
/**
* Renders Unmet Prerequisites block for job's view.
*/
export default {
+ i18n: {
+ failMessage: s__(
+ 'Job|This job failed because the necessary resources were not successfully created.',
+ ),
+ moreInformation: __('More information'),
+ },
components: {
GlLink,
+ GlAlert,
},
props: {
helpPath: {
@@ -16,15 +24,10 @@ export default {
};
</script>
<template>
- <div class="bs-callout bs-callout-danger">
- <p class="js-failed-unmet-prerequisites gl-mb-0">
- {{
- s__(`Job|This job failed because the necessary resources were not successfully created.`)
- }}
-
- <gl-link :href="helpPath" class="js-help-path">
- <strong> {{ __('More information') }} </strong>
- </gl-link>
- </p>
- </div>
+ <gl-alert variant="danger" class="gl-mt-3" :dismissible="false">
+ {{ $options.i18n.failMessage }}
+ <gl-link :href="helpPath">
+ {{ $options.i18n.moreInformation }}
+ </gl-link>
+ </gl-alert>
</template>
diff --git a/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
new file mode 100644
index 00000000000..f2acc3215cd
--- /dev/null
+++ b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
@@ -0,0 +1,18 @@
+<script>
+import { mapState } from 'vuex';
+import MembersFilteredSearchBar from './members_filtered_search_bar.vue';
+
+export default {
+ name: 'FilterSortContainer',
+ components: { MembersFilteredSearchBar },
+ computed: {
+ ...mapState(['filteredSearchBar']),
+ },
+};
+</script>
+
+<template>
+ <div v-if="filteredSearchBar.show" class="gl-bg-gray-10 gl-p-5">
+ <members-filtered-search-bar />
+ </div>
+</template>
diff --git a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
new file mode 100644
index 00000000000..c1df0b94234
--- /dev/null
+++ b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
@@ -0,0 +1,132 @@
+<script>
+import { mapState } from 'vuex';
+import { GlFilteredSearchToken } from '@gitlab/ui';
+import { setUrlParams, queryToObject } from '~/lib/utils/url_utility';
+import { getParameterByName } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import { SEARCH_TOKEN_TYPE, SORT_PARAM } from '~/members/constants';
+
+export default {
+ name: 'MembersFilteredSearchBar',
+ components: { FilteredSearchBar },
+ availableTokens: [
+ {
+ type: 'two_factor',
+ icon: 'lock',
+ title: s__('Members|2FA'),
+ token: GlFilteredSearchToken,
+ unique: true,
+ operators: [{ value: '=', description: 'is' }],
+ 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: [{ value: '=', description: 'is' }],
+ options: [
+ { value: 'exclude', title: s__('Members|Direct') },
+ { value: 'only', title: s__('Members|Inherited') },
+ ],
+ },
+ ],
+ data() {
+ return {
+ initialFilterValue: [],
+ };
+ },
+ computed: {
+ ...mapState(['sourceId', 'filteredSearchBar', 'canManageMembers']),
+ tokens() {
+ return this.$options.availableTokens.filter(token => {
+ if (
+ Object.prototype.hasOwnProperty.call(token, 'requiredPermissions') &&
+ !this[token.requiredPermissions]
+ ) {
+ return false;
+ }
+
+ return this.filteredSearchBar.tokens?.includes(token.type);
+ });
+ },
+ },
+ created() {
+ const query = queryToObject(window.location.search);
+
+ const tokens = this.tokens
+ .filter(token => query[token.type])
+ .map(token => ({
+ type: token.type,
+ value: {
+ data: query[token.type],
+ operator: '=',
+ },
+ }));
+
+ if (query[this.filteredSearchBar.searchParam]) {
+ tokens.push({
+ type: SEARCH_TOKEN_TYPE,
+ value: {
+ data: query[this.filteredSearchBar.searchParam],
+ },
+ });
+ }
+
+ this.initialFilterValue = tokens;
+ },
+ methods: {
+ handleFilter(tokens) {
+ const params = tokens.reduce((accumulator, token) => {
+ const { type, value } = token;
+
+ if (!type || !value) {
+ return accumulator;
+ }
+
+ if (type === SEARCH_TOKEN_TYPE) {
+ if (value.data !== '') {
+ return {
+ ...accumulator,
+ [this.filteredSearchBar.searchParam]: value.data,
+ };
+ }
+ } else {
+ return {
+ ...accumulator,
+ [type]: value.data,
+ };
+ }
+
+ return accumulator;
+ }, {});
+
+ const sortParam = getParameterByName(SORT_PARAM);
+
+ window.location.href = setUrlParams(
+ { ...params, ...(sortParam && { sort: sortParam }) },
+ window.location.href,
+ true,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <filtered-search-bar
+ :namespace="sourceId.toString()"
+ :tokens="tokens"
+ :recent-searches-storage-key="filteredSearchBar.recentSearchesStorageKey"
+ :search-input-placeholder="filteredSearchBar.placeholder"
+ :initial-filter-value="initialFilterValue"
+ data-testid="members-filtered-search-bar"
+ @onFilter="handleFilter"
+ />
+</template>
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index 5885420a122..a23e9b942ef 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -69,3 +69,7 @@ export const DAYS_TO_EXPIRE_SOON = 7;
export const LEAVE_MODAL_ID = 'member-leave-modal';
export const REMOVE_GROUP_LINK_MODAL_ID = 'remove-group-link-modal-id';
+
+export const SEARCH_TOKEN_TYPE = 'filtered-search-term';
+
+export const SORT_PARAM = 'sort';
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index fbb960a7ceb..d3900b84fa7 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -6,7 +6,7 @@ import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
import { initGroupMembersApp } from '~/groups/members';
import { memberRequestFormatter, groupLinkRequestFormatter } from '~/groups/members/utils';
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
@@ -33,7 +33,7 @@ initGroupMembersApp(document.querySelector('.js-group-members-list'), {
show: true,
tokens: ['two_factor', 'with_inherited_permissions'],
searchParam: 'search',
- placeholder: __('Members|Filter members'),
+ placeholder: s__('Members|Filter members'),
recentSearchesStorageKey: 'group_members',
},
});
@@ -52,7 +52,7 @@ initGroupMembersApp(document.querySelector('.js-group-invited-members-list'), {
show: true,
tokens: [],
searchParam: 'search_invited',
- placeholder: __('Members|Search invited'),
+ placeholder: s__('Members|Search invited'),
recentSearchesStorageKey: 'group_invited_members',
},
});
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 5df7ff0632a..8f836010d70 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -14,6 +14,10 @@ class Groups::GroupMembersController < Groups::ApplicationController
# Authorize
before_action :authorize_admin_group_member!, except: admin_not_required_endpoints
+ before_action do
+ push_frontend_feature_flag(:group_members_filtered_search, @group)
+ end
+
skip_before_action :check_two_factor_requirement, only: :leave
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite,
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 2a87b42ef13..a08212f151c 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -4,6 +4,7 @@
- show_access_requests = can_manage_members && @requesters.exists?
- invited_active = params[:search_invited].present? || params[:invited_members_page].present?
- vue_members_list_enabled = Feature.enabled?(:vue_group_members_list, @group, default_enabled: true)
+- filtered_search_enabled = Feature.enabled?(:group_members_filtered_search, @group)
- current_user_is_group_owner = @group && @group.has_owner?(current_user)
- form_item_label_css_class = 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center'
@@ -54,20 +55,21 @@
.tab-content
#tab-members.tab-pane{ class: ('active' unless invited_active) }
.card.card-without-border
- = render 'groups/group_members/tab_pane/header' do
- = render 'groups/group_members/tab_pane/title' do
- = html_escape(_('Members with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- = form_tag group_group_members_path(@group), method: :get, class: 'user-search-form gl-display-flex gl-md-align-items-center gl-flex-wrap gl-flex-direction-column gl-md-flex-direction-row gl-mx-n3 gl-my-n3', data: { testid: 'user-search-form' } do
- .gl-px-3.gl-py-2
- .search-control-wrap.gl-relative
- = render 'shared/members/search_field'
- - if can_manage_members
+ - unless filtered_search_enabled
+ = render 'groups/group_members/tab_pane/header' do
+ = render 'groups/group_members/tab_pane/title' do
+ = html_escape(_('Members with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = form_tag group_group_members_path(@group), method: :get, class: 'user-search-form gl-display-flex gl-md-align-items-center gl-flex-wrap gl-flex-direction-column gl-md-flex-direction-row gl-mx-n3 gl-my-n3', data: { testid: 'user-search-form' } do
+ .gl-px-3.gl-py-2
+ .search-control-wrap.gl-relative
+ = render 'shared/members/search_field'
+ - if can_manage_members
+ = render 'groups/group_members/tab_pane/form_item' do
+ = label_tag '2fa', _('2FA'), class: form_item_label_css_class
+ = render 'shared/members/filter_2fa_dropdown'
= render 'groups/group_members/tab_pane/form_item' do
- = label_tag '2fa', _('2FA'), class: form_item_label_css_class
- = render 'shared/members/filter_2fa_dropdown'
- = render 'groups/group_members/tab_pane/form_item' do
- = label_tag :sort_by, _('Sort by'), class: form_item_label_css_class
- = render 'shared/members/sort_dropdown'
+ = label_tag :sort_by, _('Sort by'), class: form_item_label_css_class
+ = render 'shared/members/sort_dropdown'
- if vue_members_list_enabled
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members) }
.loading
@@ -83,9 +85,10 @@
- if @group.shared_with_group_links.any?
#tab-groups.tab-pane
.card.card-without-border
- = render 'groups/group_members/tab_pane/header' do
- = render 'groups/group_members/tab_pane/title' do
- = html_escape(_('Groups with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ - unless filtered_search_enabled
+ = render 'groups/group_members/tab_pane/header' do
+ = render 'groups/group_members/tab_pane/title' do
+ = html_escape(_('Groups with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- if vue_members_list_enabled
.js-group-linked-list{ data: linked_groups_list_data_attributes(@group) }
.loading
@@ -97,11 +100,12 @@
- if show_invited_members
#tab-invited-members.tab-pane{ class: ('active' if invited_active) }
.card.card-without-border
- = render 'groups/group_members/tab_pane/header' do
- = render 'groups/group_members/tab_pane/title' do
- = html_escape(_('Members invited to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- = form_tag group_group_members_path(@group), method: :get, class: 'user-search-form', data: { testid: 'user-search-form' } do
- = render 'shared/members/search_field', name: 'search_invited'
+ - unless filtered_search_enabled
+ = render 'groups/group_members/tab_pane/header' do
+ = render 'groups/group_members/tab_pane/title' do
+ = html_escape(_('Members invited to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = form_tag group_group_members_path(@group), method: :get, class: 'user-search-form', data: { testid: 'user-search-form' } do
+ = render 'shared/members/search_field', name: 'search_invited'
- if vue_members_list_enabled
.js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) }
.loading
@@ -117,9 +121,10 @@
- if show_access_requests
#tab-access-requests.tab-pane
.card.card-without-border
- = render 'groups/group_members/tab_pane/header' do
- = render 'groups/group_members/tab_pane/title' do
- = html_escape(_('Users requesting access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ - unless filtered_search_enabled
+ = render 'groups/group_members/tab_pane/header' do
+ = render 'groups/group_members/tab_pane/title' do
+ = html_escape(_('Users requesting access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- if vue_members_list_enabled
.js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) }
.loading
diff --git a/changelogs/unreleased/242028-migrate-bs-callout-to-glalert-in-app-assets-javascripts-jobs-compo.yml b/changelogs/unreleased/242028-migrate-bs-callout-to-glalert-in-app-assets-javascripts-jobs-compo.yml
new file mode 100644
index 00000000000..8dd080ac936
--- /dev/null
+++ b/changelogs/unreleased/242028-migrate-bs-callout-to-glalert-in-app-assets-javascripts-jobs-compo.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate bs-callout to GlAlert in …/unmet_prerequisites_block.vue
+merge_request: 48398
+author:
+type: other
diff --git a/config/feature_flags/development/group_members_filtered_search.yml b/config/feature_flags/development/group_members_filtered_search.yml
new file mode 100644
index 00000000000..ea1a5b6a74f
--- /dev/null
+++ b/config/feature_flags/development/group_members_filtered_search.yml
@@ -0,0 +1,8 @@
+---
+name: group_members_filtered_search
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48272
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/289911
+milestone: '13.7'
+type: development
+group: group::access
+default_enabled: false
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 2aca15e04b9..4fc55d5afd9 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -68,7 +68,9 @@ If you are writing your own endpoint (web server) to receive
GitLab webhooks, keep in mind the following things:
- Your endpoint should send its HTTP response as fast as possible. If
- you wait too long, GitLab may decide the hook failed and retry it.
+ you wait too long (by default, a timeout of 10 seconds), GitLab may decide
+ the hook failed and retry it. You can configure this timeout with
+ `gitlab_rails['webhook_timeout']`.
- Your endpoint should ALWAYS return a valid HTTP response. If you do
not do this then GitLab thinks the hook failed and retries it.
Most HTTP libraries take care of this for you automatically but if
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 19d91b563c3..15bbf479050 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16864,6 +16864,9 @@ msgstr ""
msgid "Members|%{userName} is currently an LDAP user. Editing their permissions will override the settings from the LDAP group sync."
msgstr ""
+msgid "Members|2FA"
+msgstr ""
+
msgid "Members|An error occurred while trying to enable LDAP override, please try again."
msgstr ""
@@ -16897,9 +16900,18 @@ msgstr ""
msgid "Members|Are you sure you want to withdraw your access request for \"%{source}\""
msgstr ""
+msgid "Members|Direct"
+msgstr ""
+
+msgid "Members|Disabled"
+msgstr ""
+
msgid "Members|Edit permissions"
msgstr ""
+msgid "Members|Enabled"
+msgstr ""
+
msgid "Members|Expiration date removed successfully."
msgstr ""
@@ -16912,12 +16924,18 @@ msgstr ""
msgid "Members|Filter members"
msgstr ""
+msgid "Members|Inherited"
+msgstr ""
+
msgid "Members|LDAP override enabled."
msgstr ""
msgid "Members|Leave \"%{source}\""
msgstr ""
+msgid "Members|Membership"
+msgstr ""
+
msgid "Members|No expiration set"
msgstr ""
diff --git a/spec/features/groups/members/filter_members_spec.rb b/spec/features/groups/members/filter_members_spec.rb
index b6d33b3f4aa..917b35659a6 100644
--- a/spec/features/groups/members/filter_members_spec.rb
+++ b/spec/features/groups/members/filter_members_spec.rb
@@ -11,8 +11,7 @@ RSpec.describe 'Groups > Members > Filter members', :js do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
- two_factor_auth_dropdown_toggle_selector = '[data-testid="member-filter-2fa-dropdown"] [data-testid="dropdown-toggle"]'
- active_inherited_members_filter_selector = '[data-testid="filter-members-with-inherited-permissions"] a.is-active'
+ filtered_search_bar_selector = '[data-testid="members-filtered-search-bar"]'
before do
group.add_owner(user)
@@ -27,7 +26,6 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(0)).to include(user.name)
expect(member(1)).to include(user_with_2fa.name)
- expect(page).to have_css(two_factor_auth_dropdown_toggle_selector, text: 'Everyone')
end
it 'shows only 2FA members' do
@@ -35,7 +33,10 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(0)).to include(user_with_2fa.name)
expect(all_rows.size).to eq(1)
- expect(page).to have_css(two_factor_auth_dropdown_toggle_selector, text: 'Enabled')
+
+ within filtered_search_bar_selector do
+ expect(page).to have_content '2FA = Enabled'
+ end
end
it 'shows only non 2FA members' do
@@ -43,7 +44,10 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(0)).to include(user.name)
expect(all_rows.size).to eq(1)
- expect(page).to have_css(two_factor_auth_dropdown_toggle_selector, text: 'Disabled')
+
+ within filtered_search_bar_selector do
+ expect(page).to have_content '2FA = Disabled'
+ end
end
it 'shows inherited members by default' do
@@ -53,15 +57,16 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(1)).to include(user_with_2fa.name)
expect(member(2)).to include(nested_group_user.name)
expect(all_rows.size).to eq(3)
-
- expect(page).to have_css(active_inherited_members_filter_selector, text: 'Show all members', visible: false)
end
it 'shows only group members' do
visit_members_list(nested_group, with_inherited_permissions: 'exclude')
expect(member(0)).to include(nested_group_user.name)
expect(all_rows.size).to eq(1)
- expect(page).to have_css(active_inherited_members_filter_selector, text: 'Show only direct members', visible: false)
+
+ within filtered_search_bar_selector do
+ expect(page).to have_content 'Membership = Direct'
+ end
end
it 'shows only inherited members' do
@@ -69,7 +74,10 @@ RSpec.describe 'Groups > Members > Filter members', :js do
expect(member(0)).to include(user.name)
expect(member(1)).to include(user_with_2fa.name)
expect(all_rows.size).to eq(2)
- expect(page).to have_css(active_inherited_members_filter_selector, text: 'Show only inherited members', visible: false)
+
+ within filtered_search_bar_selector do
+ expect(page).to have_content 'Membership = Inherited'
+ end
end
def visit_members_list(group, options = {})
diff --git a/spec/features/groups/members/search_members_spec.rb b/spec/features/groups/members/search_members_spec.rb
index 0b2d2fd478d..fe5fed307d7 100644
--- a/spec/features/groups/members/search_members_spec.rb
+++ b/spec/features/groups/members/search_members_spec.rb
@@ -21,9 +21,10 @@ RSpec.describe 'Search group member', :js do
end
it 'renders member users' do
- page.within '[data-testid="user-search-form"]' do
- fill_in 'search', with: member.name
- find('[data-testid="user-search-submit"]').click
+ page.within '[data-testid="members-filtered-search-bar"]' do
+ find_field('Filter members').click
+ find('input').native.send_keys(member.name)
+ click_button 'Search'
end
expect(members_table).to have_content(member.name)
diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb
index f03cc36df18..74c736f6e2c 100644
--- a/spec/features/groups/members/sort_members_spec.rb
+++ b/spec/features/groups/members/sort_members_spec.rb
@@ -12,6 +12,8 @@ RSpec.describe 'Groups > Members > Sort members', :js do
dropdown_toggle_selector = '[data-testid="user-sort-dropdown"] [data-testid="dropdown-toggle"]'
before do
+ stub_feature_flags(group_members_filtered_search: false)
+
create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago)
create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago)
diff --git a/spec/features/groups/members/tabs_spec.rb b/spec/features/groups/members/tabs_spec.rb
index fa77d1a2ff8..2f95e9fa6d3 100644
--- a/spec/features/groups/members/tabs_spec.rb
+++ b/spec/features/groups/members/tabs_spec.rb
@@ -62,9 +62,10 @@ RSpec.describe 'Groups > Members > Tabs' do
click_link 'Invited'
- page.within '[data-testid="user-search-form"]' do
- fill_in 'search_invited', with: 'email'
- find('button[type="submit"]').click
+ page.within '[data-testid="members-filtered-search-bar"]' do
+ find_field('Search invited').click
+ find('input').native.send_keys('email')
+ click_button 'Search'
end
end
@@ -74,9 +75,10 @@ RSpec.describe 'Groups > Members > Tabs' do
before do
click_link 'Members'
- page.within '[data-testid="user-search-form"]' do
- fill_in 'search', with: 'test'
- find('button[type="submit"]').click
+ page.within '[data-testid="members-filtered-search-bar"]' do
+ find_field('Filter members').click
+ find('input').native.send_keys('test')
+ click_button 'Search'
end
end
diff --git a/spec/frontend/groups/members/components/app_spec.js b/spec/frontend/groups/members/components/app_spec.js
index 0db3fc18c34..208e2fc35b6 100644
--- a/spec/frontend/groups/members/components/app_spec.js
+++ b/spec/frontend/groups/members/components/app_spec.js
@@ -3,6 +3,7 @@ import { nextTick } from 'vue';
import Vuex from 'vuex';
import { GlAlert } from '@gitlab/ui';
import App from '~/groups/members/components/app.vue';
+import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import * as commonUtils from '~/lib/utils/common_utils';
import { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_types';
import mutations from '~/members/store/mutations';
@@ -14,7 +15,7 @@ describe('GroupMembersApp', () => {
let wrapper;
let store;
- const createComponent = (state = {}) => {
+ const createComponent = (state = {}, options = {}) => {
store = new Vuex.Store({
state: {
showError: true,
@@ -27,10 +28,12 @@ describe('GroupMembersApp', () => {
wrapper = shallowMount(App, {
localVue,
store,
+ ...options,
});
};
const findAlert = () => wrapper.find(GlAlert);
+ const findFilterSortContainer = () => wrapper.find(FilterSortContainer);
beforeEach(() => {
commonUtils.scrollToElement = jest.fn();
@@ -83,4 +86,22 @@ describe('GroupMembersApp', () => {
expect(findAlert().exists()).toBe(false);
});
});
+
+ describe.each`
+ featureFlagValue | exists
+ ${true} | ${true}
+ ${false} | ${false}
+ `(
+ 'when `group_members_filtered_search` feature flag is $featureFlagValue',
+ ({ featureFlagValue, exists }) => {
+ it(`${exists ? 'renders' : 'does not render'} FilterSortContainer`, () => {
+ createComponent(
+ {},
+ { provide: { glFeatures: { groupMembersFilteredSearch: featureFlagValue } } },
+ );
+
+ expect(findFilterSortContainer().exists()).toBe(exists);
+ });
+ },
+ );
});
diff --git a/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js b/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js
index 68fcb321214..9092d3f8163 100644
--- a/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js
+++ b/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js
@@ -1,37 +1,41 @@
-import Vue from 'vue';
-import component from '~/jobs/components/unmet_prerequisites_block.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlLink } from '@gitlab/ui';
+import UnmetPrerequisitesBlock from '~/jobs/components/unmet_prerequisites_block.vue';
describe('Unmet Prerequisites Block Job component', () => {
- const Component = Vue.extend(component);
- let vm;
+ let wrapper;
const helpPath = '/user/project/clusters/index.html#troubleshooting-failed-deployment-jobs';
- beforeEach(() => {
- vm = mountComponent(Component, {
- hasNoRunnersForProject: true,
- helpPath,
+ const createComponent = () => {
+ wrapper = shallowMount(UnmetPrerequisitesBlock, {
+ propsData: {
+ helpPath,
+ },
});
+ };
+
+ beforeEach(() => {
+ createComponent();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders an alert with the correct message', () => {
- const container = vm.$el.querySelector('.js-failed-unmet-prerequisites');
+ const container = wrapper.find(GlAlert);
const alertMessage =
'This job failed because the necessary resources were not successfully created.';
expect(container).not.toBeNull();
- expect(container.innerHTML).toContain(alertMessage);
+ expect(container.text()).toContain(alertMessage);
});
it('renders link to help page', () => {
- const helpLink = vm.$el.querySelector('.js-help-path');
+ const helpLink = wrapper.find(GlLink);
expect(helpLink).not.toBeNull();
- expect(helpLink.innerHTML).toContain('More information');
- expect(helpLink.getAttribute('href')).toEqual(helpPath);
+ expect(helpLink.text()).toContain('More information');
+ expect(helpLink.attributes().href).toEqual(helpPath);
});
});
diff --git a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
new file mode 100644
index 00000000000..4abf9f50959
--- /dev/null
+++ b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
@@ -0,0 +1,55 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
+import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('FilterSortContainer', () => {
+ let wrapper;
+
+ const createComponent = state => {
+ const store = new Vuex.Store({
+ state: {
+ filteredSearchBar: {
+ show: true,
+ tokens: ['two_factor'],
+ searchParam: 'search',
+ placeholder: 'Filter members',
+ recentSearchesStorageKey: 'group_members',
+ },
+ ...state,
+ },
+ });
+
+ wrapper = shallowMount(FilterSortContainer, {
+ localVue,
+ store,
+ });
+ };
+
+ describe('when `filteredSearchBar.show` is `false`', () => {
+ it('renders nothing', () => {
+ createComponent({
+ filteredSearchBar: {
+ show: false,
+ },
+ });
+
+ expect(wrapper.html()).toBe('');
+ });
+ });
+
+ describe('when `filteredSearchBar.show` is `true`', () => {
+ it('renders `MembersFilteredSearchBar`', () => {
+ createComponent({
+ filteredSearchBar: {
+ show: true,
+ },
+ });
+
+ expect(wrapper.find(MembersFilteredSearchBar).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
new file mode 100644
index 00000000000..ca885000c2f
--- /dev/null
+++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
@@ -0,0 +1,176 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { GlFilteredSearchToken } from '@gitlab/ui';
+import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('MembersFilteredSearchBar', () => {
+ let wrapper;
+
+ const createComponent = state => {
+ const store = new Vuex.Store({
+ state: {
+ sourceId: 1,
+ filteredSearchBar: {
+ show: true,
+ tokens: ['two_factor'],
+ searchParam: 'search',
+ placeholder: 'Filter members',
+ recentSearchesStorageKey: 'group_members',
+ },
+ canManageMembers: true,
+ ...state,
+ },
+ });
+
+ wrapper = shallowMount(MembersFilteredSearchBar, {
+ localVue,
+ store,
+ });
+ };
+
+ const findFilteredSearchBar = () => wrapper.find(FilteredSearchBar);
+
+ it('passes correct props to `FilteredSearchBar` component', () => {
+ createComponent();
+
+ expect(findFilteredSearchBar().props()).toMatchObject({
+ namespace: '1',
+ recentSearchesStorageKey: 'group_members',
+ searchInputPlaceholder: 'Filter members',
+ });
+ });
+
+ describe('filtering tokens', () => {
+ 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: [{ value: '=', description: 'is' }],
+ options: [
+ { value: 'enabled', title: 'Enabled' },
+ { value: 'disabled', title: 'Disabled' },
+ ],
+ requiredPermissions: 'canManageMembers',
+ },
+ ]);
+ });
+
+ describe('when `canManageMembers` is false', () => {
+ it('excludes 2FA token', () => {
+ createComponent({
+ filteredSearchBar: {
+ show: true,
+ tokens: ['two_factor', 'with_inherited_permissions'],
+ searchParam: 'search',
+ placeholder: 'Filter members',
+ recentSearchesStorageKey: 'group_members',
+ },
+ canManageMembers: false,
+ });
+
+ expect(findFilteredSearchBar().props('tokens')).toEqual([
+ {
+ type: 'with_inherited_permissions',
+ icon: 'group',
+ title: 'Membership',
+ token: GlFilteredSearchToken,
+ unique: true,
+ operators: [{ value: '=', description: 'is' }],
+ options: [{ value: 'exclude', title: 'Direct' }, { value: 'only', title: 'Inherited' }],
+ },
+ ]);
+ });
+ });
+ });
+
+ describe('when filters are set via query params', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL('https://localhost');
+ });
+
+ it('parses and passes tokens to `FilteredSearchBar` component as `initialFilterValue` prop', () => {
+ window.location.search = '?two_factor=enabled&token_not_available=foobar';
+
+ createComponent();
+
+ expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([
+ {
+ type: 'two_factor',
+ value: {
+ data: 'enabled',
+ operator: '=',
+ },
+ },
+ ]);
+ });
+
+ it('parses and passes search param to `FilteredSearchBar` component as `initialFilterValue` prop', () => {
+ window.location.search = '?search=foobar';
+
+ createComponent();
+
+ expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([
+ {
+ type: 'filtered-search-term',
+ value: {
+ data: 'foobar',
+ },
+ },
+ ]);
+ });
+ });
+
+ describe('when filter bar is submitted', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL('https://localhost');
+ });
+
+ it('adds correct filter query params', () => {
+ createComponent();
+
+ findFilteredSearchBar().vm.$emit('onFilter', [
+ { type: 'two_factor', value: { data: 'enabled', operator: '=' } },
+ ]);
+
+ expect(window.location.href).toBe('https://localhost/?two_factor=enabled');
+ });
+
+ it('adds search query param', () => {
+ createComponent();
+
+ findFilteredSearchBar().vm.$emit('onFilter', [
+ { type: 'two_factor', value: { data: 'enabled', operator: '=' } },
+ { type: 'filtered-search-term', value: { data: 'foobar' } },
+ ]);
+
+ expect(window.location.href).toBe('https://localhost/?two_factor=enabled&search=foobar');
+ });
+
+ it('adds sort query param', () => {
+ window.location.search = '?sort=name_asc';
+
+ createComponent();
+
+ findFilteredSearchBar().vm.$emit('onFilter', [
+ { type: 'two_factor', value: { data: 'enabled', operator: '=' } },
+ { type: 'filtered-search-term', value: { data: 'foobar' } },
+ ]);
+
+ expect(window.location.href).toBe(
+ 'https://localhost/?two_factor=enabled&search=foobar&sort=name_asc',
+ );
+ });
+ });
+});
diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
index 724d6db2705..1dbaace1c89 100644
--- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
+++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
@@ -50,7 +50,6 @@ RSpec.shared_examples 'Maintainer manages access requests' do
def expect_visible_access_request(entity, user)
if has_tabs
expect(page).to have_content "Access requests 1"
- expect(page).to have_content "Users requesting access to #{entity.name}"
else
expect(page).to have_content "Users requesting access to #{entity.name} 1"
end