diff options
59 files changed, 625 insertions, 506 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index acb80bd194b..dadff210e75 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -29,6 +29,7 @@ /doc/ci/test_cases/ @msedlakjakubowski /doc/development/ @marcia /doc/development/documentation/ @cnorris +/doc/development/i18n/ @ngaskill /doc/development/value_stream_analytics.md @msedlakjakubowski /doc/gitlab-basics/ @marcia /doc/install/ @axil @@ -57,7 +58,7 @@ /doc/user/profile/ @msedlakjakubowski /doc/user/project/ @aqualls @axil @eread @msedlakjakubowski @ngaskill /doc/user/project/clusters/ @ngaskill -/doc/user/project/import/ @msedlakjakubowski +/doc/user/project/import/ @ngaskill @msedlakjakubowski /doc/user/project/integrations/ @aqualls /doc/user/project/integrations/prometheus_library/ @ngaskill /doc/user/project/issues/ @msedlakjakubowski diff --git a/app/assets/javascripts/members/components/app.vue b/app/assets/javascripts/members/components/app.vue index 585fabdf3ff..a08518584f3 100644 --- a/app/assets/javascripts/members/components/app.vue +++ b/app/assets/javascripts/members/components/app.vue @@ -9,7 +9,17 @@ import MembersTable from './table/members_table.vue'; export default { name: 'MembersApp', components: { MembersTable, FilterSortContainer, GlAlert }, - inject: ['namespace'], + provide() { + return { + namespace: this.namespace, + }; + }, + props: { + namespace: { + type: String, + required: true, + }, + }, computed: { ...mapState({ showError(state) { diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js index 6c913af8a0f..2ed0958d1dc 100644 --- a/app/assets/javascripts/members/index.js +++ b/app/assets/javascripts/members/index.js @@ -2,20 +2,11 @@ import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; import Vuex from 'vuex'; import { parseDataAttributes } from '~/members/utils'; -import App from './components/app.vue'; +import MembersTabs from './components/members_tabs.vue'; +import { MEMBER_TYPES } from './constants'; import membersStore from './store'; -export const initMembersApp = ( - el, - { - namespace, - tableFields = [], - tableAttrs = {}, - tableSortableFields = [], - requestFormatter = () => {}, - filteredSearchBar = { show: false }, - }, -) => { +export const initMembersApp = (el, options) => { if (!el) { return () => {}; } @@ -25,29 +16,45 @@ export const initMembersApp = ( const { sourceId, canManageMembers, ...vuexStoreAttributes } = parseDataAttributes(el); - const store = new Vuex.Store({ - modules: { + const modules = Object.keys(MEMBER_TYPES).reduce((accumulator, namespace) => { + const namespacedOptions = options[namespace]; + + if (!namespacedOptions) { + return accumulator; + } + + const { + tableFields = [], + tableAttrs = {}, + tableSortableFields = [], + requestFormatter = () => {}, + filteredSearchBar = { show: false }, + } = namespacedOptions; + + return { + ...accumulator, [namespace]: membersStore({ - ...vuexStoreAttributes, + ...vuexStoreAttributes[namespace], tableFields, tableAttrs, tableSortableFields, requestFormatter, filteredSearchBar, }), - }, - }); + }; + }, {}); + + const store = new Vuex.Store({ modules }); return new Vue({ el, - components: { App }, + components: { MembersTabs }, store, provide: { - namespace, currentUserId: gon.current_user_id || null, sourceId, canManageMembers, }, - render: (createElement) => createElement('app'), + render: (createElement) => createElement('members-tabs'), }); }; diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js index b0a70055835..13656ee9b16 100644 --- a/app/assets/javascripts/pages/groups/group_members/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -29,46 +29,43 @@ function mountRemoveMemberModal() { const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions']; -initMembersApp(document.querySelector('.js-group-members-list'), { - namespace: MEMBER_TYPES.user, - tableFields: SHARED_FIELDS.concat(['source', 'granted']), - tableAttrs: { tr: { 'data-qa-selector': 'member_row' } }, - tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'], - requestFormatter: groupMemberRequestFormatter, - filteredSearchBar: { - show: true, - tokens: ['two_factor', 'with_inherited_permissions'], - searchParam: 'search', - placeholder: s__('Members|Filter members'), - recentSearchesStorageKey: 'group_members', +initMembersApp(document.querySelector('.js-group-members-list-app'), { + [MEMBER_TYPES.user]: { + tableFields: SHARED_FIELDS.concat(['source', 'granted']), + tableAttrs: { tr: { 'data-qa-selector': 'member_row' } }, + tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'], + requestFormatter: groupMemberRequestFormatter, + filteredSearchBar: { + show: true, + tokens: ['two_factor', 'with_inherited_permissions'], + searchParam: 'search', + placeholder: s__('Members|Filter members'), + recentSearchesStorageKey: 'group_members', + }, }, -}); - -initMembersApp(document.querySelector('.js-group-group-links-list'), { - namespace: MEMBER_TYPES.group, - tableFields: SHARED_FIELDS.concat('granted'), - tableAttrs: { - table: { 'data-qa-selector': 'groups_list' }, - tr: { 'data-qa-selector': 'group_row' }, + [MEMBER_TYPES.group]: { + tableFields: SHARED_FIELDS.concat('granted'), + tableAttrs: { + table: { 'data-qa-selector': 'groups_list' }, + tr: { 'data-qa-selector': 'group_row' }, + }, + requestFormatter: groupLinkRequestFormatter, }, - requestFormatter: groupLinkRequestFormatter, -}); -initMembersApp(document.querySelector('.js-group-invited-members-list'), { - namespace: MEMBER_TYPES.invite, - tableFields: SHARED_FIELDS.concat('invited'), - requestFormatter: groupMemberRequestFormatter, - filteredSearchBar: { - show: true, - tokens: [], - searchParam: 'search_invited', - placeholder: s__('Members|Search invited'), - recentSearchesStorageKey: 'group_invited_members', + [MEMBER_TYPES.invite]: { + tableFields: SHARED_FIELDS.concat('invited'), + requestFormatter: groupMemberRequestFormatter, + filteredSearchBar: { + show: true, + tokens: [], + searchParam: 'search_invited', + placeholder: s__('Members|Search invited'), + recentSearchesStorageKey: 'group_invited_members', + }, + }, + [MEMBER_TYPES.accessRequest]: { + tableFields: SHARED_FIELDS.concat('requested'), + requestFormatter: groupMemberRequestFormatter, }, -}); -initMembersApp(document.querySelector('.js-group-access-requests-list'), { - namespace: MEMBER_TYPES.accessRequest, - tableFields: SHARED_FIELDS.concat('requested'), - requestFormatter: groupMemberRequestFormatter, }); groupsSelect(); diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js index 471798d2931..177dc346c60 100644 --- a/app/assets/javascripts/pages/projects/project_members/index.js +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -42,46 +42,41 @@ initInviteMembersForm(); new UsersSelect(); // eslint-disable-line no-new const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions']; -initMembersApp(document.querySelector('.js-project-members-list'), { - namespace: MEMBER_TYPES.user, - tableFields: SHARED_FIELDS.concat(['source', 'granted']), - tableAttrs: { tr: { 'data-qa-selector': 'member_row' } }, - tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'], - requestFormatter: projectMemberRequestFormatter, - filteredSearchBar: { - show: true, - tokens: ['with_inherited_permissions'], - searchParam: 'search', - placeholder: s__('Members|Filter members'), - recentSearchesStorageKey: 'project_members', +initMembersApp(document.querySelector('.js-project-members-list-app'), { + [MEMBER_TYPES.user]: { + tableFields: SHARED_FIELDS.concat(['source', 'granted']), + tableAttrs: { tr: { 'data-qa-selector': 'member_row' } }, + tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'], + requestFormatter: projectMemberRequestFormatter, + filteredSearchBar: { + show: true, + tokens: ['with_inherited_permissions'], + searchParam: 'search', + placeholder: s__('Members|Filter members'), + recentSearchesStorageKey: 'project_members', + }, }, -}); - -initMembersApp(document.querySelector('.js-project-group-links-list'), { - namespace: MEMBER_TYPES.group, - tableFields: SHARED_FIELDS.concat('granted'), - tableAttrs: { - table: { 'data-qa-selector': 'groups_list' }, - tr: { 'data-qa-selector': 'group_row' }, + [MEMBER_TYPES.group]: { + tableFields: SHARED_FIELDS.concat('granted'), + tableAttrs: { + table: { 'data-qa-selector': 'groups_list' }, + tr: { 'data-qa-selector': 'group_row' }, + }, + requestFormatter: groupLinkRequestFormatter, + filteredSearchBar: { + show: true, + tokens: [], + searchParam: 'search_groups', + placeholder: s__('Members|Search groups'), + recentSearchesStorageKey: 'project_group_links', + }, }, - requestFormatter: groupLinkRequestFormatter, - filteredSearchBar: { - show: true, - tokens: [], - searchParam: 'search_groups', - placeholder: s__('Members|Search groups'), - recentSearchesStorageKey: 'project_group_links', + [MEMBER_TYPES.invite]: { + tableFields: SHARED_FIELDS.concat('invited'), + requestFormatter: projectMemberRequestFormatter, + }, + [MEMBER_TYPES.accessRequest]: { + tableFields: SHARED_FIELDS.concat('requested'), + requestFormatter: projectMemberRequestFormatter, }, -}); - -initMembersApp(document.querySelector('.js-project-invited-members-list'), { - namespace: MEMBER_TYPES.invite, - tableFields: SHARED_FIELDS.concat('invited'), - requestFormatter: projectMemberRequestFormatter, -}); - -initMembersApp(document.querySelector('.js-project-access-requests-list'), { - namespace: MEMBER_TYPES.accessRequest, - tableFields: SHARED_FIELDS.concat('requested'), - requestFormatter: projectMemberRequestFormatter, }); diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb index 79191616c8f..c4d920dc317 100644 --- a/app/helpers/groups/group_members_helper.rb +++ b/app/helpers/groups/group_members_helper.rb @@ -13,12 +13,15 @@ module Groups::GroupMembersHelper render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: group.access_level_roles, default_access_level: default_access_level end - def group_members_list_data_json(group, members, pagination = {}) - group_members_list_data(group, members, pagination).to_json - end - - def group_group_links_list_data_json(group) - group_group_links_list_data(group).to_json + def group_members_app_data_json(group, members:, invited:, access_requests:) + { + user: group_members_list_data(group, members, { param_name: :page, params: { invited_members_page: nil, search_invited: nil } }), + group: group_group_links_list_data(group), + invite: group_members_list_data(group, invited.nil? ? [] : invited, { param_name: :invited_members_page, params: { page: nil } }), + access_request: group_members_list_data(group, access_requests.nil? ? [] : access_requests), + source_id: group.id, + can_manage_members: can?(current_user, :admin_group_member, group) + }.to_json end private @@ -32,13 +35,11 @@ module Groups::GroupMembersHelper end # Overridden in `ee/app/helpers/ee/groups/group_members_helper.rb` - def group_members_list_data(group, members, pagination) + def group_members_list_data(group, members, pagination = {}) { members: group_members_serialized(group, members), pagination: members_pagination_data(members, pagination), - member_path: group_group_member_path(group, ':id'), - source_id: group.id, - can_manage_members: can?(current_user, :admin_group_member, group) + member_path: group_group_member_path(group, ':id') } end @@ -48,8 +49,7 @@ module Groups::GroupMembersHelper { members: group_group_links_serialized(group_links), pagination: members_pagination_data(group_links), - member_path: group_group_link_path(group, ':id'), - source_id: group.id + member_path: group_group_link_path(group, ':id') } end end diff --git a/app/helpers/projects/project_members_helper.rb b/app/helpers/projects/project_members_helper.rb index fa68bbad135..0871d5638b8 100644 --- a/app/helpers/projects/project_members_helper.rb +++ b/app/helpers/projects/project_members_helper.rb @@ -27,12 +27,15 @@ module Projects::ProjectMembersHelper project.group.has_owner?(current_user) end - def project_members_list_data_json(project, members, pagination = {}) - project_members_list_data(project, members, pagination).to_json - end - - def project_group_links_list_data_json(project, group_links) - project_group_links_list_data(project, group_links).to_json + def project_members_app_data_json(project, members:, group_links:, invited:, access_requests:) + { + user: project_members_list_data(project, members, { param_name: :page, params: { search_groups: nil } }), + group: project_group_links_list_data(project, group_links), + invite: project_members_list_data(project, invited.nil? ? [] : invited), + access_request: project_members_list_data(project, access_requests.nil? ? [] : access_requests), + source_id: project.id, + can_manage_members: can_manage_project_members?(project) + }.to_json end private @@ -45,13 +48,11 @@ module Projects::ProjectMembersHelper GroupLink::ProjectGroupLinkSerializer.new.represent(group_links, { current_user: current_user }) end - def project_members_list_data(project, members, pagination) + def project_members_list_data(project, members, pagination = {}) { members: project_members_serialized(project, members), pagination: members_pagination_data(members, pagination), - member_path: project_project_member_path(project, ':id'), - source_id: project.id, - can_manage_members: can_manage_project_members?(project) + member_path: project_project_member_path(project, ':id') } end @@ -59,9 +60,7 @@ module Projects::ProjectMembersHelper { members: project_group_links_serialized(group_links), pagination: members_pagination_data(group_links), - member_path: project_group_link_path(project, ':id'), - source_id: project.id, - can_manage_members: can_manage_project_members?(project) + member_path: project_group_link_path(project, ':id') } end end diff --git a/app/helpers/registrations_helper.rb b/app/helpers/registrations_helper.rb index 79f0a66f995..24131e32c6c 100644 --- a/app/helpers/registrations_helper.rb +++ b/app/helpers/registrations_helper.rb @@ -7,6 +7,16 @@ module RegistrationsHelper devise_mapping.omniauthable? && button_based_providers_enabled? end + + def signup_username_data_attributes + { + min_length: User::MIN_USERNAME_LENGTH, + min_length_message: s_('SignUp|Username is too short (minimum is %{min_length} characters).') % { min_length: User::MIN_USERNAME_LENGTH }, + max_length: User::MAX_USERNAME_LENGTH, + max_length_message: s_('SignUp|Username is too long (maximum is %{max_length} characters).') % { max_length: User::MAX_USERNAME_LENGTH }, + qa_selector: 'new_user_username_field' + } + end end RegistrationsHelper.prepend_mod_with('RegistrationsHelper') diff --git a/app/models/user.rb b/app/models/user.rb index 0eb58baae11..b059bd8dd13 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,6 +35,9 @@ class User < ApplicationRecord COUNT_CACHE_VALIDITY_PERIOD = 24.hours + MAX_USERNAME_LENGTH = 255 + MIN_USERNAME_LENGTH = 2 + add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) } add_authentication_token_field :feed_token add_authentication_token_field :static_object_token diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml index 00429f1acbc..4ec3fcde337 100644 --- a/app/views/devise/registrations/new.html.haml +++ b/app/views/devise/registrations/new.html.haml @@ -8,6 +8,5 @@ = render 'devise/shared/signup_box', url: registration_path(resource_name), button_text: _('Register'), - show_omniauth_providers: omniauth_enabled? && button_based_providers_enabled?, - suggestion_path: nil + show_omniauth_providers: omniauth_enabled? && button_based_providers_enabled? = render 'devise/shared/sign_in_link' diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 56f74916d8f..4c70d8bf6f6 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -1,6 +1,4 @@ - max_first_name_length = max_last_name_length = 127 -- max_username_length = 255 -- min_username_length = 2 - omniauth_providers_placement ||= :bottom .gl-mb-3.gl-p-4.gl-border-gray-100.gl-border-1.gl-border-solid.gl-rounded-base @@ -15,22 +13,49 @@ .name.form-row .col.form-group = f.label :first_name, _('First name'), for: 'new_user_first_name', class: 'label-bold' - = f.text_field :first_name, class: 'form-control gl-form-input top js-block-emoji js-validate-length', :data => { :max_length => max_first_name_length, :max_length_message => s_('SignUp|First name is too long (maximum is %{max_length} characters).') % { max_length: max_first_name_length }, :qa_selector => 'new_user_first_name_field' }, required: true, title: _('This field is required.') + = f.text_field :first_name, + class: 'form-control gl-form-input top js-block-emoji js-validate-length', + data: { max_length: max_first_name_length, + max_length_message: s_('SignUp|First name is too long (maximum is %{max_length} characters).') % { max_length: max_first_name_length }, + qa_selector: 'new_user_first_name_field' }, + required: true, + title: _('This field is required.') .col.form-group = f.label :last_name, _('Last name'), for: 'new_user_last_name', class: 'label-bold' - = f.text_field :last_name, class: 'form-control gl-form-input top js-block-emoji js-validate-length', :data => { :max_length => max_last_name_length, :max_length_message => s_('SignUp|Last name is too long (maximum is %{max_length} characters).') % { max_length: max_last_name_length }, :qa_selector => 'new_user_last_name_field' }, required: true, title: _('This field is required.') + = f.text_field :last_name, + class: 'form-control gl-form-input top js-block-emoji js-validate-length', + data: { max_length: max_last_name_length, + max_length_message: s_('SignUp|Last name is too long (maximum is %{max_length} characters).') % { max_length: max_last_name_length }, + qa_selector: 'new_user_last_name_field' }, + required: true, + title: _('This field is required.') .username.form-group = f.label :username, class: 'label-bold' - = f.text_field :username, class: 'form-control gl-form-input middle js-block-emoji js-validate-length js-validate-username', :data => { :api_path => suggestion_path, :min_length => min_username_length, :min_length_message => s_('SignUp|Username is too short (minimum is %{min_length} characters).') % { min_length: min_username_length }, :max_length => max_username_length, :max_length_message => s_('SignUp|Username is too long (maximum is %{max_length} characters).') % { max_length: max_username_length }, :qa_selector => 'new_user_username_field' }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _('Please create a username with only alphanumeric characters.') + = f.text_field :username, + class: 'form-control gl-form-input middle js-block-emoji js-validate-length js-validate-username', + data: signup_username_data_attributes, + pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, + required: true, + title: _('Please create a username with only alphanumeric characters.') %p.validation-error.gl-text-red-500.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Username is already taken.') %p.validation-success.gl-text-green-600.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Username is available.') %p.validation-pending.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Checking username availability...') .form-group = f.label :email, class: 'label-bold' - = f.email_field :email, value: @invite_email, class: 'form-control gl-form-input middle', data: { qa_selector: 'new_user_email_field' }, required: true, title: _('Please provide a valid email address.') + = f.email_field :email, + value: @invite_email, + class: 'form-control gl-form-input middle', + data: { qa_selector: 'new_user_email_field' }, + required: true, + title: _('Please provide a valid email address.') .form-group.gl-mb-5#password-strength = f.label :password, class: 'label-bold' - = f.password_field :password, class: 'form-control gl-form-input bottom', data: { qa_selector: 'new_user_password_field' }, required: true, pattern: ".{#{@minimum_password_length},}", title: s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length } + = f.password_field :password, + class: 'form-control gl-form-input bottom', + data: { qa_selector: 'new_user_password_field' }, + required: true, + pattern: ".{#{@minimum_password_length},}", + title: s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length } %p.gl-field-hint.text-secondary= s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length } %div - if show_recaptcha_sign_up? diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 45488791272..4e4ea4c2de7 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,8 +1,5 @@ - add_page_specific_style 'page_bundles/members' - page_title _('Group members') -- show_invited_members = can_manage_members? && @invited_members.load.any? -- show_access_requests = can_manage_members? && @requesters.load.any? -- invited_active = params[:search_invited].present? || params[:invited_members_page].present? .js-remove-member-modal .row.gl-mt-3 @@ -35,47 +32,9 @@ = render_if_exists 'groups/group_members/ldap_sync' - %ul.nav-links.mobile-separator.nav.nav-tabs - %li.nav-item - = link_to '#tab-members', class: ['nav-link', ('active' unless invited_active)], data: { toggle: 'tab' } do - %span - = _('Members') - %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @members.total_count - - if @group.shared_with_group_links.present? - %li.nav-item - = link_to '#tab-groups', class: ['nav-link'] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do - %span - = _('Groups') - %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @group.shared_with_group_links.count - - if show_invited_members - %li.nav-item - = link_to '#tab-invited-members', class: ['nav-link', ('active' if invited_active)], data: { toggle: 'tab' } do - %span - = _('Invited') - %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @invited_members.total_count - - if show_access_requests - %li.nav-item - = link_to '#tab-access-requests', class: 'nav-link', data: { toggle: 'tab' } do - %span - = _('Access requests') - %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @requesters.count - .tab-content - #tab-members.tab-pane{ class: ('active' unless invited_active) } - .js-group-members-list{ data: { members_data: group_members_list_data_json(@group, @members, { param_name: :page, params: { invited_members_page: nil, search_invited: nil } }) } } - .loading - .gl-spinner.gl-spinner-md - - if @group.shared_with_group_links.present? - #tab-groups.tab-pane - .js-group-group-links-list{ data: { members_data: group_group_links_list_data_json(@group) } } - .loading - .gl-spinner.gl-spinner-md - - if show_invited_members - #tab-invited-members.tab-pane{ class: ('active' if invited_active) } - .js-group-invited-members-list{ data: { members_data: group_members_list_data_json(@group, @invited_members, { param_name: :invited_members_page, params: { page: nil } }) } } - .loading - .gl-spinner.gl-spinner-md - - if show_access_requests - #tab-access-requests.tab-pane - .js-group-access-requests-list{ data: { members_data: group_members_list_data_json(@group, @requesters) } } - .loading - .gl-spinner.gl-spinner-md + .js-group-members-list-app{ data: { members_data: group_members_app_data_json(@group, + members: @members, + invited: @invited_members, + access_requests: @requesters) } } + .loading + .gl-spinner.gl-spinner-md diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 0fa9fb7079b..5d9aab17b15 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -56,47 +56,10 @@ .invite-member= render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project) - elsif @project.allowed_to_share_with_group? .invite-group= render 'shared/members/invite_group', access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, submit_url: project_group_links_path(@project), group_link_field: 'link_group_id', group_access_field: 'link_group_access' - %ul.nav-links.mobile-separator.nav.nav-tabs - %li.nav-item - = link_to '#tab-members', class: ['nav-link', ('active' unless groups_tab_active?)], data: { toggle: 'tab' } do - %span - = _('Members') - %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @project_members.total_count - - if show_groups?(@group_links) - %li.nav-item - = link_to '#tab-groups', class: ['nav-link', ('active' if groups_tab_active?)] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do - %span - = _('Groups') - %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @group_links.count - - if show_invited_members?(@project, @invited_members) - %li.nav-item - = link_to '#tab-invited-members', class: 'nav-link', data: { toggle: 'tab' } do - %span - = _('Invited') - %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @invited_members.count - - if show_access_requests?(@project, @requesters) - %li.nav-item - = link_to '#tab-access-requests', class: 'nav-link', data: { toggle: 'tab' } do - %span - = _('Access requests') - %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @requesters.count - .tab-content - #tab-members.tab-pane{ class: ('active' unless groups_tab_active?) } - .js-project-members-list{ data: { members_data: project_members_list_data_json(@project, @project_members, { param_name: :page, params: { search_groups: nil } }) } } - .loading - .gl-spinner.gl-spinner-md - - if show_groups?(@group_links) - #tab-groups.tab-pane{ class: ('active' if groups_tab_active?) } - .js-project-group-links-list{ data: { members_data: project_group_links_list_data_json(@project, @group_links) } } - .loading - .gl-spinner.gl-spinner-md - - if show_invited_members?(@project, @invited_members) - #tab-invited-members.tab-pane - .js-project-invited-members-list{ data: { members_data: project_members_list_data_json(@project, @invited_members) } } - .loading - .gl-spinner.gl-spinner-md - - if show_access_requests?(@project, @requesters) - #tab-access-requests.tab-pane - .js-project-access-requests-list{ data: { members_data: project_members_list_data_json(@project, @requesters) } } - .loading - .gl-spinner.gl-spinner-md + .js-project-members-list-app{ data: { members_data: project_members_app_data_json(@project, + members: @project_members, + group_links: @group_links, + invited: @invited_members, + access_requests: @requesters) } } + .loading + .gl-spinner.gl-spinner-md diff --git a/app/views/registrations/invites/new.html.haml b/app/views/registrations/invites/new.html.haml index 6e6ff7aaeee..0feae9b17e9 100644 --- a/app/views/registrations/invites/new.html.haml +++ b/app/views/registrations/invites/new.html.haml @@ -13,6 +13,5 @@ url: users_sign_up_invites_path, button_text: _('Continue'), show_omniauth_providers: social_signin_enabled?, - omniauth_providers_placement: :top, - suggestion_path: nil + omniauth_providers_placement: :top = render 'devise/shared/sign_in_link' diff --git a/changelogs/unreleased/324681-group-project-members-migrate-to-one-vue-app-and-gltabs-2.yml b/changelogs/unreleased/324681-group-project-members-migrate-to-one-vue-app-and-gltabs-2.yml new file mode 100644 index 00000000000..b3bf8effaec --- /dev/null +++ b/changelogs/unreleased/324681-group-project-members-migrate-to-one-vue-app-and-gltabs-2.yml @@ -0,0 +1,5 @@ +--- +title: Update group/project member tabs to comply with Pajamas design system +merge_request: +author: +type: other diff --git a/changelogs/unreleased/325982-feature-flag-enable-ci_external_validation_service.yml b/changelogs/unreleased/325982-feature-flag-enable-ci_external_validation_service.yml new file mode 100644 index 00000000000..69d36af1d0f --- /dev/null +++ b/changelogs/unreleased/325982-feature-flag-enable-ci_external_validation_service.yml @@ -0,0 +1,5 @@ +--- +title: Remove the feature flag for the external validation service +merge_request: 61351 +author: +type: other diff --git a/config/feature_flags/development/ci_external_validation_service.yml b/config/feature_flags/development/ci_external_validation_service.yml deleted file mode 100644 index 9df770d87e5..00000000000 --- a/config/feature_flags/development/ci_external_validation_service.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: ci_external_validation_service -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56856 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323935 -milestone: '13.11' -type: development -group: group::continuous integration -default_enabled: false diff --git a/danger/plugins/product_intelligence.rb b/danger/plugins/product_intelligence.rb new file mode 100644 index 00000000000..91551a8312f --- /dev/null +++ b/danger/plugins/product_intelligence.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative '../../tooling/danger/product_intelligence' + +module Danger + class ProductIntelligence < ::Danger::Plugin + # Put the helper code somewhere it can be tested + include Tooling::Danger::ProductIntelligence + end +end diff --git a/danger/product_intelligence/Dangerfile b/danger/product_intelligence/Dangerfile index 3867aed84d5..5469e8a87a5 100644 --- a/danger/product_intelligence/Dangerfile +++ b/danger/product_intelligence/Dangerfile @@ -18,82 +18,16 @@ UPDATE_DICTIONARY_MESSAGE = <<~MSG ``` MSG -all_changed_files = helper.all_changed_files +# exit if not matching files +matching_changed_files = product_intelligence.matching_changed_files +return unless matching_changed_files.any? -tracking_files = [ - 'lib/gitlab/tracking.rb', - 'spec/lib/gitlab/tracking_spec.rb', - 'app/helpers/tracking_helper.rb', - 'spec/helpers/tracking_helper_spec.rb', - 'app/assets/javascripts/tracking.js', - 'spec/frontend/tracking_spec.js', - 'generator_templates/usage_metric_definition/metric_definition.yml', - 'lib/generators/gitlab/usage_metric_definition_generator.rb', - 'lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb', - 'spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb', - 'spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb', - 'config/metrics/schema.json' -] +warn format(CHANGED_FILES_MESSAGE, changed_files: helper.markdown_list(matching_changed_files)) +fail format(UPDATE_DICTIONARY_MESSAGE) if product_intelligence.need_dictionary_changes? -tracking_changed_files = all_changed_files & tracking_files -usage_data_changed_files = all_changed_files.grep(%r{(usage_data)}) -dictionary_changed_file = all_changed_files.grep(%r{(doc/development/usage_ping/dictionary.md)}) -metrics_changed_files = all_changed_files.grep(%r{((ee/)?config/metrics/.*\.yml)}) +labels = product_intelligence.missing_labels +return unless labels.any? -def matching_files?(file, extension:, pattern:) - return unless file.end_with?(extension) - - helper.changed_lines(file).grep(pattern).any? -end - -js_patterns = Regexp.union( - 'Tracking.event', - /\btrack\(/, - 'data-track-event', - 'data-track-action' -) - -dictionary_pattern = Regexp.union( - 'key_path:', - 'description:', - 'product_section:', - 'product_stage:', - 'product_group:', - 'status:', - 'tier:' -) - -snowplow_changed_files = all_changed_files.select do |file| - matching_files?(file, extension: '.rb', pattern: %r{Gitlab::Tracking\.event}) || - matching_files?(file, extension: '.js', pattern: js_patterns) || - matching_files?(file, extension: '.vue', pattern: js_patterns) || - matching_files?(file, extension: '.haml', pattern: %r{data: \{ track}) -end - -required_dictionary_update_changed_files = metrics_changed_files.select do |file| - matching_files?(file, extension: '.yml', pattern: dictionary_pattern) -end - -matching_changed_files = usage_data_changed_files + - tracking_changed_files + - metrics_changed_files + - dictionary_changed_file + - snowplow_changed_files - -if matching_changed_files.any? - warn format(CHANGED_FILES_MESSAGE, changed_files: helper.markdown_list(matching_changed_files)) - - fail format(UPDATE_DICTIONARY_MESSAGE) if required_dictionary_update_changed_files.any? && dictionary_changed_file.empty? - - return unless helper.ci? - - labels = [] - labels << 'product intelligence' unless helper.mr_has_labels?('product intelligence') - labels << 'product intelligence::review pending' unless helper.mr_has_labels?(['product intelligence::approved', 'product intelligence::review pending']) - - if labels.any? - gitlab.api.update_merge_request(gitlab.mr_json['project_id'], +gitlab.api.update_merge_request(gitlab.mr_json['project_id'], gitlab.mr_json['iid'], add_labels: labels) - end -end diff --git a/doc/architecture/blueprints/ci_scale/ci_builds_cumulative_forecast.png b/doc/architecture/blueprints/ci_scale/ci_builds_cumulative_forecast.png Binary files differindex fa34c7d1c36..d1e7db30b11 100644 --- a/doc/architecture/blueprints/ci_scale/ci_builds_cumulative_forecast.png +++ b/doc/architecture/blueprints/ci_scale/ci_builds_cumulative_forecast.png diff --git a/doc/architecture/blueprints/ci_scale/ci_builds_daily_forecast.png b/doc/architecture/blueprints/ci_scale/ci_builds_daily_forecast.png Binary files differindex b73a592fa6b..15f250e6b0e 100644 --- a/doc/architecture/blueprints/ci_scale/ci_builds_daily_forecast.png +++ b/doc/architecture/blueprints/ci_scale/ci_builds_daily_forecast.png diff --git a/doc/development/cicd/index.md b/doc/development/cicd/index.md index 965ab3610dd..a596af3d25d 100644 --- a/doc/development/cicd/index.md +++ b/doc/development/cicd/index.md @@ -185,17 +185,3 @@ Watch a walkthrough of this feature in details in the video below. <figure class="video-container"> <iframe src="https://www.youtube.com/embed/NmdWRGT8kZg" frameborder="0" allowfullscreen="true"> </iframe> </figure> - -## External pipeline validation service - -The [external CI/CD pipeline validation service](../../administration/external_pipeline_validation.md) -is available for use on self-managed GitLab instances, but is not in use on GitLab.com. -It is configured with [environment variables](../../administration/environment_variables.md) -on the instance. - -To enable the feature on GitLab.com, enable the `ci_external_validation_service` -[feature flag](../feature_flags/index.md). The valid "Not accepted" response code -for GitLab.com is `406` only. - -For more details, see the linked issues and MRs in the -[feature flag rollout issue](https://gitlab.com/gitlab-org/gitlab/-/issues/325982). diff --git a/doc/development/testing_guide/end_to_end/img/gl-capybara_V13_12.png b/doc/development/testing_guide/end_to_end/img/gl-capybara_V13_12.png Binary files differindex 9ceccd39025..d5a2522ed82 100644 --- a/doc/development/testing_guide/end_to_end/img/gl-capybara_V13_12.png +++ b/doc/development/testing_guide/end_to_end/img/gl-capybara_V13_12.png diff --git a/doc/development/testing_guide/end_to_end/img/gl-chemlab_V13_12.png b/doc/development/testing_guide/end_to_end/img/gl-chemlab_V13_12.png Binary files differindex 489a043f52e..55eecaf8adf 100644 --- a/doc/development/testing_guide/end_to_end/img/gl-chemlab_V13_12.png +++ b/doc/development/testing_guide/end_to_end/img/gl-chemlab_V13_12.png diff --git a/doc/integration/jira/dvcs.md b/doc/integration/jira/dvcs.md index 89d1d70d6aa..dc23765337b 100644 --- a/doc/integration/jira/dvcs.md +++ b/doc/integration/jira/dvcs.md @@ -18,7 +18,7 @@ are accessible. - **Jira Cloud**: Your instance must be accessible through the internet. - **Jira Server**: Your network must allow access to your instance. -## Smart Commits +## Smart commits When connecting GitLab with Jira with DVCS, you can process your Jira issues using special commands, called @@ -74,6 +74,9 @@ your integration. 1. In the **Name** field, enter a descriptive name for the integration, such as `Jira`. 1. In the **Redirect URI** field, enter the URI appropriate for your version of GitLab, replacing `<gitlab.example.com>` with your GitLab instance domain: + - *For GitLab versions 13.0 and later* **and** *Jira versions 8.14 and later,* use the + generated `Redirect URL` from + [Linking GitLab accounts with Jira](https://confluence.atlassian.com/adminjiraserver/linking-gitlab-accounts-1027142272.html). - *For GitLab versions 11.3 and later,* use `https://<gitlab.example.com>/login/oauth/callback`. If you use GitLab.com, the URL is `https://gitlab.com/login/oauth/callback`. - *For GitLab versions 11.2 and earlier,* use @@ -99,13 +102,14 @@ it completes, refreshes every 60 minutes: - *For Jira Cloud,* go to **Settings (gear) > Products > DVCS accounts**. 1. To create a new integration, select the appropriate value for **Host**: - *For Jira versions 8.14 and later:* Select **GitLab** or - <!-- vale gitlab.Substitutions = NO --> - **GitLab Self-Hosted**. - <!-- vale gitlab.Substitutions = YES --> + **GitLab Self-Managed**. - *For Jira versions 8.13 and earlier:* Select **GitHub Enterprise**. 1. For **Team or User Account**, enter either: - - The relative path of a top-level GitLab group that you have access to. - - The relative path of your personal namespace. + - *For Jira versions 8.14 and later:* + - The relative path of a top-level GitLab group that you have access to. + - *For Jira versions 8.13 and earlier:* + - The relative path of a top-level GitLab group that you have access to. + - The relative path of your personal namespace. 1. In the **Host URL** field, enter the URI appropriate for your version of GitLab, replacing `<gitlab.example.com>` with your GitLab instance domain: diff --git a/doc/operations/metrics/dashboards/img/metrics_dashboard_template_selection_v13_3.png b/doc/operations/metrics/dashboards/img/metrics_dashboard_template_selection_v13_3.png Binary files differindex cad075ca421..1571ab9de90 100644 --- a/doc/operations/metrics/dashboards/img/metrics_dashboard_template_selection_v13_3.png +++ b/doc/operations/metrics/dashboards/img/metrics_dashboard_template_selection_v13_3.png diff --git a/doc/user/admin_area/analytics/img/instance_activity_pipelines_chart_v13_6_a.png b/doc/user/admin_area/analytics/img/instance_activity_pipelines_chart_v13_6_a.png Binary files differindex 210c5c2609a..bd02065556c 100644 --- a/doc/user/admin_area/analytics/img/instance_activity_pipelines_chart_v13_6_a.png +++ b/doc/user/admin_area/analytics/img/instance_activity_pipelines_chart_v13_6_a.png diff --git a/doc/user/admin_area/img/cohorts_v13_9_a.png b/doc/user/admin_area/img/cohorts_v13_9_a.png Binary files differindex a891b5b12c2..1a4590290b9 100644 --- a/doc/user/admin_area/img/cohorts_v13_9_a.png +++ b/doc/user/admin_area/img/cohorts_v13_9_a.png diff --git a/doc/user/application_security/security_dashboard/img/security_center_settings_v13_4.png b/doc/user/application_security/security_dashboard/img/security_center_settings_v13_4.png Binary files differindex 74592e2cea5..6578c0bf4cf 100644 --- a/doc/user/application_security/security_dashboard/img/security_center_settings_v13_4.png +++ b/doc/user/application_security/security_dashboard/img/security_center_settings_v13_4.png diff --git a/doc/user/application_security/threat_monitoring/img/threat_monitoring_policy_alert_list_v13_12.png b/doc/user/application_security/threat_monitoring/img/threat_monitoring_policy_alert_list_v13_12.png Binary files differindex 1f02fd30f8e..e165c7e6ceb 100644 --- a/doc/user/application_security/threat_monitoring/img/threat_monitoring_policy_alert_list_v13_12.png +++ b/doc/user/application_security/threat_monitoring/img/threat_monitoring_policy_alert_list_v13_12.png diff --git a/doc/user/application_security/vulnerability_report/index.md b/doc/user/application_security/vulnerability_report/index.md index 012992c8a72..f68fb0c5cbb 100644 --- a/doc/user/application_security/vulnerability_report/index.md +++ b/doc/user/application_security/vulnerability_report/index.md @@ -45,11 +45,11 @@ From the Vulnerability Report you can: You can filter the vulnerabilities table by: -| Filter | Available options | +| Filter | Available options | |:---------|:------------------| | Status | Detected, Confirmed, Dismissed, Resolved. | | Severity | Critical, High, Medium, Low, Info, Unknown. | -| Scanner | [Available scanners](../index.md#security-scanning-tools). | +| Scanner | For more details, see [Scanner filter](#scanner-filter). | | Project | For more details, see [Project filter](#project-filter). | | Activity | For more details, see [Activity filter](#activity-filter). | @@ -61,12 +61,27 @@ To filter the list of vulnerabilities: 1. Select values from the dropdown. 1. Repeat the above steps for each desired filter. -The vulnerability table is applied immediately. The vulnerability severity totals are also updated. +After each filter is selected: + +- The list of matching vulnerabilities is updated. +- The vulnerability severity totals are updated. The filters' criteria are combined to show only vulnerabilities matching all criteria. An exception to this behavior is the Activity filter. For more details about how it works, see [Activity filter](#activity-filter). +## Scanner filter + +The scanner filter allows you to focus on vulnerabilities detected by selected scanners. + +When using the scanner filter, you can choose: + +- **All scanners** (default). +- Individual GitLab-provided scanners. +- Any integrated 3rd-party scanner. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229661) in GitLab 13.12. + +For details of each of the available scanners, see [Security scanning tools](../index.md#security-scanning-tools). + ### Project filter The content of the Project filter depends on the current level: @@ -89,8 +104,8 @@ Selection behavior when using the Activity filter: | Activity selection | Results displayed | |:------------------------------------|:------------------| -| All | Vulnerabilities with any Activity status (same as ignoring this filter). Selecting this will deselect any other Activity filter options. | -| No activity | Only vulnerabilities without either an associated Issue or that are no longer detected. Selecting this will deselect any other Activity filter options. | +| All | Vulnerabilities with any Activity status (same as ignoring this filter). Selecting this deselects any other Activity filter options. | +| No activity | Only vulnerabilities without either an associated Issue or that are no longer detected. Selecting this deselects any other Activity filter options. | | With issues | Only vulnerabilities with one or more associated issues. Does not include vulnerabilities that also are no longer detected. | | No longer detected | Only vulnerabilities that are no longer detected in the latest pipeline scan of the `default` branch. Does not include vulnerabilities with one or more associated issues. | | With issues and No longer detected | Only vulnerabilities that have one or more associated issues and also are no longer detected in the latest pipeline scan of the `default` branch. | diff --git a/doc/user/group/issues_analytics/img/issues_created_per_month_v12_8_a.png b/doc/user/group/issues_analytics/img/issues_created_per_month_v12_8_a.png Binary files differindex 5994cbfa401..2e19aa38412 100644 --- a/doc/user/group/issues_analytics/img/issues_created_per_month_v12_8_a.png +++ b/doc/user/group/issues_analytics/img/issues_created_per_month_v12_8_a.png diff --git a/doc/user/group/value_stream_analytics/img/delete_value_stream_v13_12.png b/doc/user/group/value_stream_analytics/img/delete_value_stream_v13_12.png Binary files differindex c02f259ae8c..ef532986100 100644 --- a/doc/user/group/value_stream_analytics/img/delete_value_stream_v13_12.png +++ b/doc/user/group/value_stream_analytics/img/delete_value_stream_v13_12.png diff --git a/doc/user/group/value_stream_analytics/img/new_value_stream_v13_12.png b/doc/user/group/value_stream_analytics/img/new_value_stream_v13_12.png Binary files differindex b2b6ce04a14..d64ec31aabf 100644 --- a/doc/user/group/value_stream_analytics/img/new_value_stream_v13_12.png +++ b/doc/user/group/value_stream_analytics/img/new_value_stream_v13_12.png diff --git a/doc/user/group/value_stream_analytics/img/vsa_filter_bar_v13_12.png b/doc/user/group/value_stream_analytics/img/vsa_filter_bar_v13_12.png Binary files differindex ee0d007a778..834556df051 100644 --- a/doc/user/group/value_stream_analytics/img/vsa_filter_bar_v13_12.png +++ b/doc/user/group/value_stream_analytics/img/vsa_filter_bar_v13_12.png diff --git a/doc/user/group/value_stream_analytics/img/vsa_label_based_stage_v14_0.png b/doc/user/group/value_stream_analytics/img/vsa_label_based_stage_v14_0.png Binary files differindex 1f47670462c..648ab53dd12 100644 --- a/doc/user/group/value_stream_analytics/img/vsa_label_based_stage_v14_0.png +++ b/doc/user/group/value_stream_analytics/img/vsa_label_based_stage_v14_0.png diff --git a/doc/user/group/value_stream_analytics/img/vsa_overview_stage_v13_11.png b/doc/user/group/value_stream_analytics/img/vsa_overview_stage_v13_11.png Binary files differindex 7d47003972c..8d77c53db7f 100644 --- a/doc/user/group/value_stream_analytics/img/vsa_overview_stage_v13_11.png +++ b/doc/user/group/value_stream_analytics/img/vsa_overview_stage_v13_11.png diff --git a/doc/user/group/value_stream_analytics/img/vsa_time_metrics_v13_12.png b/doc/user/group/value_stream_analytics/img/vsa_time_metrics_v13_12.png Binary files differindex 63bae51afef..68d9741bed8 100644 --- a/doc/user/group/value_stream_analytics/img/vsa_time_metrics_v13_12.png +++ b/doc/user/group/value_stream_analytics/img/vsa_time_metrics_v13_12.png diff --git a/doc/user/profile/img/notification_global_settings_v13_12.png b/doc/user/profile/img/notification_global_settings_v13_12.png Binary files differindex 2989543c2d8..0998bb89778 100644 --- a/doc/user/profile/img/notification_global_settings_v13_12.png +++ b/doc/user/profile/img/notification_global_settings_v13_12.png diff --git a/lib/gitlab/ci/pipeline/chain/validate/external.rb b/lib/gitlab/ci/pipeline/chain/validate/external.rb index 539b44513f0..e8fe3b4f797 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/external.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/external.rb @@ -16,8 +16,6 @@ module Gitlab GENERAL_REJECTED_STATUS = (400..499).freeze def perform! - return unless enabled? - pipeline_authorized = validate_external log_message = pipeline_authorized ? 'authorized' : 'not authorized' @@ -32,12 +30,6 @@ module Gitlab private - def enabled? - return true unless Gitlab.com? - - ::Feature.enabled?(:ci_external_validation_service, project, default_enabled: :yaml) - end - def validate_external return true unless validation_service_url diff --git a/qa/qa/page/group/members.rb b/qa/qa/page/group/members.rb index 68a169d5a7f..b526a4488b2 100644 --- a/qa/qa/page/group/members.rb +++ b/qa/qa/page/group/members.rb @@ -26,7 +26,7 @@ module QA element :delete_member_button end - view 'app/views/groups/group_members/index.html.haml' do + view 'app/assets/javascripts/members/components/members_tabs.vue' do element :groups_list_tab end diff --git a/qa/qa/page/project/members.rb b/qa/qa/page/project/members.rb index 09264d95aed..eeb589d6ca8 100644 --- a/qa/qa/page/project/members.rb +++ b/qa/qa/page/project/members.rb @@ -6,7 +6,7 @@ module QA class Members < Page::Base include QA::Page::Component::InviteMembersModal - view 'app/views/projects/project_members/index.html.haml' do + view 'app/assets/javascripts/members/components/members_tabs.vue' do element :groups_list_tab end diff --git a/qa/qa/page/registration/sign_up.rb b/qa/qa/page/registration/sign_up.rb index 0fb4b466e62..6d1b9cb3615 100644 --- a/qa/qa/page/registration/sign_up.rb +++ b/qa/qa/page/registration/sign_up.rb @@ -7,12 +7,15 @@ module QA view 'app/views/devise/shared/_signup_box.html.haml' do element :new_user_first_name_field element :new_user_last_name_field - element :new_user_username_field element :new_user_email_field element :new_user_password_field element :new_user_register_button end + view 'app/helpers/registrations_helper.rb' do + element :new_user_username_field + end + view 'app/views/registrations/welcome/show.html.haml' do element :get_started_button end diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 5b52797d285..74c04ab2b7c 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -370,6 +370,7 @@ HELM_CMD=$(cat << EOF ${HELM_CMD} \ --version="${CI_PIPELINE_ID}-${CI_JOB_ID}" \ -f "${base_config_file}" \ + -v "${HELM_LOG_VERBOSITY:-1}" \ "${release}" "gitlab-${GITLAB_HELM_CHART_REF}" EOF ) diff --git a/spec/features/groups/members/tabs_spec.rb b/spec/features/groups/members/tabs_spec.rb index 2f95e9fa6d3..2e9f332c0d6 100644 --- a/spec/features/groups/members/tabs_spec.rb +++ b/spec/features/groups/members/tabs_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Groups > Members > Tabs' do +RSpec.describe 'Groups > Members > Tabs', :js do using RSpec::Parameterized::TableSyntax shared_examples 'active "Members" tab' do @@ -56,7 +56,7 @@ RSpec.describe 'Groups > Members > Tabs' do it_behaves_like 'active "Members" tab' end - context 'when searching "Invited"', :js do + context 'when searching "Invited"' do before do visit group_group_members_path(group) @@ -86,7 +86,7 @@ RSpec.describe 'Groups > Members > Tabs' do end end - context 'when using "Invited" pagination', :js do + context 'when using "Invited" pagination' do before do visit group_group_members_path(group) diff --git a/spec/features/projects/members/tabs_spec.rb b/spec/features/projects/members/tabs_spec.rb index 471be26e126..5611e7ee810 100644 --- a/spec/features/projects/members/tabs_spec.rb +++ b/spec/features/projects/members/tabs_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Projects > Members > Tabs' do +RSpec.describe 'Projects > Members > Tabs', :js do include Spec::Support::Helpers::Features::MembersHelpers using RSpec::Parameterized::TableSyntax @@ -44,7 +44,7 @@ RSpec.describe 'Projects > Members > Tabs' do end end - context 'when searching "Groups"', :js do + context 'when searching "Groups"' do before do click_link 'Groups' diff --git a/spec/frontend/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js index 05933e36b52..b9fdf8792fd 100644 --- a/spec/frontend/members/components/app_spec.js +++ b/spec/frontend/members/components/app_spec.js @@ -33,7 +33,7 @@ describe('MembersApp', () => { wrapper = shallowMount(MembersApp, { localVue, - provide: { + propsData: { namespace: MEMBER_TYPES.user, }, store, diff --git a/spec/frontend/members/components/members_tabs_spec.js b/spec/frontend/members/components/members_tabs_spec.js index 28614b52706..6f1a6d0c223 100644 --- a/spec/frontend/members/components/members_tabs_spec.js +++ b/spec/frontend/members/components/members_tabs_spec.js @@ -6,7 +6,7 @@ import MembersTabs from '~/members/components/members_tabs.vue'; import { MEMBER_TYPES } from '~/members/constants'; import { pagination } from '../mock_data'; -describe('MembersApp', () => { +describe('MembersTabs', () => { Vue.use(Vuex); let wrapper; @@ -111,10 +111,10 @@ describe('MembersApp', () => { const membersApps = wrapper.findAllComponents(MembersApp).wrappers; - expect(membersApps[0].attributes('namespace')).toBe(MEMBER_TYPES.user); - expect(membersApps[1].attributes('namespace')).toBe(MEMBER_TYPES.group); - expect(membersApps[2].attributes('namespace')).toBe(MEMBER_TYPES.invite); - expect(membersApps[3].attributes('namespace')).toBe(MEMBER_TYPES.accessRequest); + expect(membersApps[0].props('namespace')).toBe(MEMBER_TYPES.user); + expect(membersApps[1].props('namespace')).toBe(MEMBER_TYPES.group); + expect(membersApps[2].props('namespace')).toBe(MEMBER_TYPES.invite); + expect(membersApps[3].props('namespace')).toBe(MEMBER_TYPES.accessRequest); }); }); diff --git a/spec/frontend/members/index_spec.js b/spec/frontend/members/index_spec.js index b07534ae4ed..efabe54f238 100644 --- a/spec/frontend/members/index_spec.js +++ b/spec/frontend/members/index_spec.js @@ -1,5 +1,5 @@ import { createWrapper } from '@vue/test-utils'; -import MembersApp from '~/members/components/app.vue'; +import MembersTabs from '~/members/components/members_tabs.vue'; import { MEMBER_TYPES } from '~/members/constants'; import { initMembersApp } from '~/members/index'; import { members, pagination, dataAttribute } from './mock_data'; @@ -11,12 +11,13 @@ describe('initMembersApp', () => { const setup = () => { vm = initMembersApp(el, { - namespace: MEMBER_TYPES.user, - tableFields: ['account'], - tableAttrs: { table: { 'data-qa-selector': 'members_list' } }, - tableSortableFields: ['account'], - requestFormatter: () => ({}), - filteredSearchBar: { show: false }, + [MEMBER_TYPES.user]: { + tableFields: ['account'], + tableAttrs: { table: { 'data-qa-selector': 'members_list' } }, + tableSortableFields: ['account'], + requestFormatter: () => ({}), + filteredSearchBar: { show: false }, + }, }); wrapper = createWrapper(vm); }; @@ -35,10 +36,10 @@ describe('initMembersApp', () => { wrapper = null; }); - it('renders `MembersApp`', () => { + it('renders `MembersTabs`', () => { setup(); - expect(wrapper.find(MembersApp).exists()).toBe(true); + expect(wrapper.find(MembersTabs).exists()).toBe(true); }); it('parses and sets `members` in Vuex store', () => { diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js index d0a7c36349b..678cea87dba 100644 --- a/spec/frontend/members/mock_data.js +++ b/spec/frontend/members/mock_data.js @@ -1,3 +1,5 @@ +import { MEMBER_TYPES } from '~/members/constants'; + export const member = { requestedAt: null, canUpdate: false, @@ -97,10 +99,12 @@ export const pagination = { }; export const dataAttribute = JSON.stringify({ - members, - pagination: paginationData, + [MEMBER_TYPES.user]: { + members, + pagination: paginationData, + member_path: '/groups/foo-bar/-/group_members/:id', + ldap_override_path: '/groups/ldap-group/-/group_members/:id/override', + }, source_id: 234, can_manage_members: true, - member_path: '/groups/foo-bar/-/group_members/:id', - ldap_override_path: '/groups/ldap-group/-/group_members/:id/override', }); diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js index 72696979722..9740e1c2edb 100644 --- a/spec/frontend/members/utils_spec.js +++ b/spec/frontend/members/utils_spec.js @@ -1,4 +1,4 @@ -import { DEFAULT_SORT } from '~/members/constants'; +import { DEFAULT_SORT, MEMBER_TYPES } from '~/members/constants'; import { generateBadges, isGroup, @@ -268,11 +268,13 @@ describe('Members Utils', () => { it('correctly parses the data attribute', () => { expect(parseDataAttributes(el)).toMatchObject({ - members, - pagination, + [MEMBER_TYPES.user]: { + members, + pagination, + memberPath: '/groups/foo-bar/-/group_members/:id', + }, sourceId: 234, canManageMembers: true, - memberPath: '/groups/foo-bar/-/group_members/:id', }); }); }); diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb index c3f1509fbc8..b409bebaac3 100644 --- a/spec/helpers/groups/group_members_helper_spec.rb +++ b/spec/helpers/groups/group_members_helper_spec.rb @@ -23,58 +23,79 @@ RSpec.describe Groups::GroupMembersHelper do end end - describe '#group_members_list_data_json' do - let(:group_members) { create_list(:group_member, 2, group: group, created_by: current_user) } - - let(:pagination) { {} } - let(:collection) { group_members } - let(:presented_members) { present_members(collection) } + describe '#group_members_app_data_json' do + include_context 'group_group_link' - subject { Gitlab::Json.parse(helper.group_members_list_data_json(group, presented_members, pagination)) } + let(:members) { create_list(:group_member, 2, group: shared_group, created_by: current_user) } + let(:invited) { create_list(:group_member, 2, :invited, group: shared_group, created_by: current_user) } + let!(:access_requests) { create_list(:group_member, 2, :access_request, group: shared_group, created_by: current_user) } + + let(:members_collection) { members } + + subject do + Gitlab::Json.parse( + helper.group_members_app_data_json( + shared_group, + members: present_members(members_collection), + invited: present_members(invited), + access_requests: present_members(access_requests) + ) + ) + end - shared_examples 'members.json' do + shared_examples 'members.json' do |member_type| it 'returns `members` property that matches json schema' do - expect(subject['members'].to_json).to match_schema('members') + expect(subject[member_type]['members'].to_json).to match_schema('members') + end + + it 'sets `member_path` property' do + expect(subject[member_type]['member_path']).to eq('/groups/foo-bar/-/group_members/:id') end end before do - allow(helper).to receive(:group_group_member_path).with(group, ':id').and_return('/groups/foo-bar/-/group_members/:id') - allow(helper).to receive(:can?).with(current_user, :admin_group_member, group).and_return(true) + allow(helper).to receive(:group_group_member_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_members/:id') + allow(helper).to receive(:group_group_link_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id') + allow(helper).to receive(:can?).with(current_user, :admin_group_member, shared_group).and_return(true) end it 'returns expected json' do expected = { - member_path: '/groups/foo-bar/-/group_members/:id', - source_id: group.id, + source_id: shared_group.id, can_manage_members: true }.as_json expect(subject).to include(expected) end - context 'for a group member' do - it_behaves_like 'members.json' + context 'group members' do + it_behaves_like 'members.json', 'user' context 'with user status set' do let(:user) { create(:user) } let!(:status) { create(:user_status, user: user) } - let(:group_members) { [create(:group_member, group: group, user: user, created_by: current_user)] } + let(:members) { [create(:group_member, group: shared_group, user: user, created_by: current_user)] } - it_behaves_like 'members.json' + it_behaves_like 'members.json', 'user' end end - context 'for an invited group member' do - let(:group_members) { create_list(:group_member, 2, :invited, group: group, created_by: current_user) } + context 'invited group members' do + it_behaves_like 'members.json', 'invite' + end - it_behaves_like 'members.json' + context 'access requests' do + it_behaves_like 'members.json', 'access_request' end - context 'for an access request' do - let(:group_members) { create_list(:group_member, 2, :access_request, group: group, created_by: current_user) } + context 'group links' do + it 'sets `group.members` property that matches json schema' do + expect(subject['group']['members'].to_json).to match_schema('group_link/group_group_links') + end - it_behaves_like 'members.json' + it 'sets `member_path` property' do + expect(subject['group']['member_path']).to eq('/groups/foo-bar/-/group_links/:id') + end end context 'when pagination is not available' do @@ -87,13 +108,12 @@ RSpec.describe Groups::GroupMembersHelper do params: {} }.as_json - expect(subject['pagination']).to include(expected) + expect(subject['access_request']['pagination']).to include(expected) end end context 'when pagination is available' do - let(:collection) { Kaminari.paginate_array(group_members).page(1).per(1) } - let(:pagination) { { param_name: :page, params: { search_groups: nil } } } + let(:members_collection) { Kaminari.paginate_array(members).page(1).per(1) } it 'sets `pagination` attribute to expected json' do expected = { @@ -101,41 +121,11 @@ RSpec.describe Groups::GroupMembersHelper do per_page: 1, total_items: 2, param_name: :page, - params: { search_groups: nil } + params: { invited_members_page: nil, search_invited: nil } }.as_json - expect(subject['pagination']).to include(expected) + expect(subject['user']['pagination']).to include(expected) end end end - - describe '#group_group_links_list_data_json' do - include_context 'group_group_link' - - subject { Gitlab::Json.parse(helper.group_group_links_list_data_json(shared_group)) } - - before do - allow(helper).to receive(:group_group_link_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id') - end - - it 'returns expected json' do - expected = { - pagination: { - current_page: nil, - per_page: nil, - total_items: 1, - param_name: nil, - params: {} - }, - member_path: '/groups/foo-bar/-/group_links/:id', - source_id: shared_group.id - }.as_json - - expect(subject).to include(expected) - end - - it 'returns `members` property that matches json schema' do - expect(subject['members'].to_json).to match_schema('group_link/group_group_links') - end - end end diff --git a/spec/helpers/projects/project_members_helper_spec.rb b/spec/helpers/projects/project_members_helper_spec.rb index 90035f3e1c5..b180b5ec06f 100644 --- a/spec/helpers/projects/project_members_helper_spec.rb +++ b/spec/helpers/projects/project_members_helper_spec.rb @@ -147,16 +147,27 @@ RSpec.describe Projects::ProjectMembersHelper do end describe 'project members' do - let_it_be(:project_members) { create_list(:project_member, 2, project: project) } + let_it_be(:members) { create_list(:project_member, 2, project: project) } + let_it_be(:group_links) { create_list(:project_group_link, 1, project: project) } + let_it_be(:invited) { create_list(:project_member, 2, :invited, project: project) } + let_it_be(:access_requests) { create_list(:project_member, 2, :access_request, project: project) } - let(:collection) { project_members } - let(:presented_members) { present_members(collection) } + let(:members_collection) { members } - describe '#project_members_list_data_json' do + describe '#project_members_app_data_json' do let(:allow_admin_project) { true } - let(:pagination) { {} } - subject { Gitlab::Json.parse(helper.project_members_list_data_json(project, presented_members, pagination)) } + subject do + Gitlab::Json.parse( + helper.project_members_app_data_json( + project, + members: present_members(members_collection), + group_links: group_links, + invited: present_members(invited), + access_requests: present_members(access_requests) + ) + ) + end before do allow(helper).to receive(:project_project_member_path).with(project, ':id').and_return('/foo-bar/-/project_members/:id') @@ -164,7 +175,6 @@ RSpec.describe Projects::ProjectMembersHelper do it 'returns expected json' do expected = { - member_path: '/foo-bar/-/project_members/:id', source_id: project.id, can_manage_members: true }.as_json @@ -172,8 +182,12 @@ RSpec.describe Projects::ProjectMembersHelper do expect(subject).to include(expected) end - it 'returns `members` property that matches json schema' do - expect(subject['members'].to_json).to match_schema('members') + it 'sets `members` property that matches json schema' do + expect(subject['user']['members'].to_json).to match_schema('members') + end + + it 'sets `member_path` property' do + expect(subject['user']['member_path']).to eq('/foo-bar/-/project_members/:id') end context 'when pagination is not available' do @@ -186,13 +200,12 @@ RSpec.describe Projects::ProjectMembersHelper do params: {} }.as_json - expect(subject['pagination']).to include(expected) + expect(subject['invite']['pagination']).to include(expected) end end context 'when pagination is available' do - let(:collection) { Kaminari.paginate_array(project_members).page(1).per(1) } - let(:pagination) { { param_name: :page, params: { search_groups: nil } } } + let(:members_collection) { Kaminari.paginate_array(members).page(1).per(1) } it 'sets `pagination` attribute to expected json' do expected = { @@ -203,45 +216,9 @@ RSpec.describe Projects::ProjectMembersHelper do params: { search_groups: nil } }.as_json - expect(subject['pagination']).to match(expected) + expect(subject['user']['pagination']).to match(expected) end end end end - - describe 'project group links' do - let_it_be(:project_group_links) { create_list(:project_group_link, 1, project: project) } - - let(:allow_admin_project) { true } - - describe '#project_group_links_list_data_json' do - subject { Gitlab::Json.parse(helper.project_group_links_list_data_json(project, project_group_links)) } - - before do - allow(helper).to receive(:project_group_link_path).with(project, ':id').and_return('/foo-bar/-/group_links/:id') - allow(helper).to receive(:can?).with(current_user, :admin_project_member, project).and_return(true) - end - - it 'returns expected json' do - expected = { - pagination: { - current_page: nil, - per_page: nil, - total_items: 1, - param_name: nil, - params: {} - }, - member_path: '/foo-bar/-/group_links/:id', - source_id: project.id, - can_manage_members: true - }.as_json - - expect(subject).to include(expected) - end - - it 'returns `members` property that matches json schema' do - expect(subject['members'].to_json).to match_schema('group_link/project_group_links') - end - end - end end diff --git a/spec/helpers/registrations_helper_spec.rb b/spec/helpers/registrations_helper_spec.rb index 00d0a0850cd..fa647548b3c 100644 --- a/spec/helpers/registrations_helper_spec.rb +++ b/spec/helpers/registrations_helper_spec.rb @@ -26,4 +26,10 @@ RSpec.describe RegistrationsHelper do it { is_expected.to eq(result) } end end + + describe '#signup_username_data_attributes' do + it 'has expected attributes' do + expect(helper.signup_username_data_attributes.keys).to include(:min_length, :min_length_message, :max_length, :max_length_message, :qa_selector) + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb index e3061f8095b..58ad3d1b4d1 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb @@ -199,37 +199,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do end end - context 'when the feature flag is disabled' do - before do - stub_feature_flags(ci_external_validation_service: false) - stub_request(:post, validation_service_url) - end - - it 'does not drop the pipeline' do - perform! - - expect(pipeline.status).not_to eq('failed') - expect(pipeline.errors).to be_empty - end - - it 'does not break the chain' do - perform! - - expect(step.break?).to be false - end - - it 'does not make requests' do - perform! - - expect(WebMock).not_to have_requested(:post, validation_service_url) - end - end - context 'when not on .com' do let(:dot_com) { false } before do - stub_feature_flags(ci_external_validation_service: false) stub_request(:post, validation_service_url).to_return(status: 404, body: "{}") end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index cb34917f073..e52e26cf148 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -22,6 +22,8 @@ RSpec.describe User do describe 'constants' do it { expect(described_class::COUNT_CACHE_VALIDITY_PERIOD).to be_a(Integer) } + it { expect(described_class::MAX_USERNAME_LENGTH).to be_a(Integer) } + it { expect(described_class::MIN_USERNAME_LENGTH).to be_a(Integer) } end describe 'delegations' do diff --git a/spec/tooling/danger/product_intelligence_spec.rb b/spec/tooling/danger/product_intelligence_spec.rb new file mode 100644 index 00000000000..17ef67e64fe --- /dev/null +++ b/spec/tooling/danger/product_intelligence_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require 'gitlab-dangerfiles' +require 'gitlab/dangerfiles/spec_helper' + +require_relative '../../../tooling/danger/product_intelligence' +require_relative '../../../tooling/danger/project_helper' + +RSpec.describe Tooling::Danger::ProductIntelligence do + include_context "with dangerfile" + + subject(:product_intelligence) { fake_danger.new(helper: fake_helper) } + + let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) } + let(:changed_files) { ['metrics/counts_7d/test_metric.yml', 'doc/development/usage_ping/dictionary.md'] } + let(:changed_lines) { ['+tier: ee'] } + + before do + allow(fake_helper).to receive(:all_changed_files).and_return(changed_files) + allow(fake_helper).to receive(:changed_lines).and_return(changed_lines) + end + + describe '#need_dictionary_changes?' do + subject { product_intelligence.need_dictionary_changes? } + + context 'when changed files do not contain dictionary changes' do + let(:changed_files) { ['config/metrics/counts_7d/test_metric.yml'] } + + it { is_expected.to be true } + end + + context 'when changed files already contains dictionary changes' do + let(:changed_files) { ['doc/development/usage_ping/dictionary.md'] } + + it { is_expected.to be false } + end + end + + describe '#missing_labels' do + subject { product_intelligence.missing_labels } + + let(:ci_env) { true } + + before do + allow(fake_helper).to receive(:mr_has_labels?).and_return(false) + allow(fake_helper).to receive(:ci?).and_return(ci_env) + end + + context 'with ci? false' do + let(:ci_env) { false } + + it { is_expected.to be_empty } + end + + context 'with ci? true' do + let(:expected_labels) { ['product intelligence', 'product intelligence::review pending'] } + + it { is_expected.to match_array(expected_labels) } + end + + context 'with product intelligence label' do + let(:expected_labels) { ['product intelligence::review pending'] } + + before do + allow(fake_helper).to receive(:mr_has_labels?).with('product intelligence').and_return(true) + end + + it { is_expected.to match_array(expected_labels) } + end + + context 'with product intelligence::review pending' do + before do + allow(fake_helper).to receive(:mr_has_labels?).and_return(true) + end + + it { is_expected.to be_empty } + end + end + + describe '#matching_changed_files' do + subject { product_intelligence.matching_changed_files } + + let(:changed_files) do + [ + 'dashboard/todos_controller.rb', + 'components/welcome.vue', + 'admin/groups/_form.html.haml' + ] + end + + context 'with snowplow files changed' do + context 'when vue file changed' do + let(:changed_lines) { ['+data-track-event'] } + + it { is_expected.to match_array(['components/welcome.vue']) } + end + + context 'when haml file changed' do + let(:changed_lines) { ['+ data: { track_label:'] } + + it { is_expected.to match_array(['admin/groups/_form.html.haml']) } + end + + context 'when ruby file changed' do + let(:changed_lines) { ['+ Gitlab::Tracking.event'] } + let(:changed_files) { ['dashboard/todos_controller.rb', 'admin/groups/_form.html.haml'] } + + it { is_expected.to match_array(['dashboard/todos_controller.rb']) } + end + end + + context 'with dictionary file not changed' do + it { is_expected.to be_empty } + end + + context 'with metrics files changed' do + let(:changed_files) { ['config/metrics/counts_7d/test_metric.yml', 'ee/config/metrics/counts_7d/ee_metric.yml'] } + + it { is_expected.to match_array(changed_files) } + end + + context 'with metrics files not changed' do + it { is_expected.to be_empty } + end + + context 'with tracking files changed' do + let(:changed_files) do + [ + 'lib/gitlab/tracking.rb', + 'spec/lib/gitlab/tracking_spec.rb', + 'app/helpers/tracking_helper.rb' + ] + end + + it { is_expected.to match_array(changed_files) } + end + + context 'with usage_data files changed' do + let(:changed_files) do + [ + 'doc/api/usage_data.md', + 'ee/lib/ee/gitlab/usage_data.rb', + 'spec/lib/gitlab/usage_data_spec.rb' + ] + end + + it { is_expected.to match_array(changed_files) } + end + end +end diff --git a/spec/views/devise/shared/_signup_box.html.haml_spec.rb b/spec/views/devise/shared/_signup_box.html.haml_spec.rb index b73e32fa765..6efb2730964 100644 --- a/spec/views/devise/shared/_signup_box.html.haml_spec.rb +++ b/spec/views/devise/shared/_signup_box.html.haml_spec.rb @@ -9,7 +9,7 @@ RSpec.describe 'devise/shared/_signup_box' do allow(view).to receive(:url).and_return('_url_') allow(view).to receive(:terms_path).and_return('_terms_path_') allow(view).to receive(:button_text).and_return('_button_text_') - allow(view).to receive(:suggestion_path).and_return('_suggestion_path_') + allow(view).to receive(:signup_username_data_attributes).and_return({}) stub_template 'devise/shared/_error_messages.html.haml' => '' end diff --git a/tooling/danger/product_intelligence.rb b/tooling/danger/product_intelligence.rb new file mode 100644 index 00000000000..e05557b58dd --- /dev/null +++ b/tooling/danger/product_intelligence.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true +# rubocop:disable Style/SignalException + +module Tooling + module Danger + module ProductIntelligence + WORKFLOW_LABELS = [ + 'product intelligence::approved', + 'product intelligence::review pending' + ].freeze + + TRACKING_FILES = [ + 'lib/gitlab/tracking.rb', + 'spec/lib/gitlab/tracking_spec.rb', + 'app/helpers/tracking_helper.rb', + 'spec/helpers/tracking_helper_spec.rb', + 'app/assets/javascripts/tracking.js', + 'spec/frontend/tracking_spec.js', + 'generator_templates/usage_metric_definition/metric_definition.yml', + 'lib/generators/gitlab/usage_metric_definition_generator.rb', + 'lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb', + 'spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb', + 'spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb', + 'config/metrics/schema.json' + ].freeze + + def missing_labels + return [] unless helper.ci? + + labels = [] + labels << 'product intelligence' unless helper.mr_has_labels?('product intelligence') + labels << 'product intelligence::review pending' unless helper.mr_has_labels?(WORKFLOW_LABELS) + + labels + end + + def matching_changed_files + tracking_changed_files = all_changed_files & TRACKING_FILES + usage_data_changed_files = all_changed_files.grep(%r{(usage_data)}) + + usage_data_changed_files + tracking_changed_files + metrics_changed_files + dictionary_changed_file + snowplow_changed_files + end + + def need_dictionary_changes? + required_dictionary_update_changed_files.any? && dictionary_changed_file.empty? + end + + private + + def all_changed_files + helper.all_changed_files + end + + def dictionary_changed_file + all_changed_files.grep(%r{(doc/development/usage_ping/dictionary\.md)}) + end + + def metrics_changed_files + all_changed_files.grep(%r{((ee/)?config/metrics/.*\.yml)}) + end + + def matching_files?(file, extension:, pattern:) + return unless file.end_with?(extension) + + helper.changed_lines(file).grep(pattern).any? + end + + def snowplow_changed_files + js_patterns = Regexp.union( + 'Tracking.event', + /\btrack\(/, + 'data-track-event', + 'data-track-action' + ) + all_changed_files.select do |file| + matching_files?(file, extension: '.rb', pattern: %r{Gitlab::Tracking\.(event|enabled\?|snowplow_options)$}) || + matching_files?(file, extension: '.js', pattern: js_patterns) || + matching_files?(file, extension: '.vue', pattern: js_patterns) || + matching_files?(file, extension: '.haml', pattern: %r{data: \{ track}) + end + end + + def required_dictionary_update_changed_files + dictionary_pattern = Regexp.union( + 'key_path:', + 'description:', + 'product_section:', + 'product_stage:', + 'product_group:', + 'status:', + 'tier:' + ) + + metrics_changed_files.select do |file| + matching_files?(file, extension: '.yml', pattern: dictionary_pattern) + end + end + end + end +end |