Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/CODEOWNERS3
-rw-r--r--app/assets/javascripts/members/components/app.vue12
-rw-r--r--app/assets/javascripts/members/index.js47
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js71
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js73
-rw-r--r--app/helpers/groups/group_members_helper.rb24
-rw-r--r--app/helpers/projects/project_members_helper.rb25
-rw-r--r--app/helpers/registrations_helper.rb10
-rw-r--r--app/models/user.rb3
-rw-r--r--app/views/devise/registrations/new.html.haml3
-rw-r--r--app/views/devise/shared/_signup_box.html.haml39
-rw-r--r--app/views/groups/group_members/index.html.haml53
-rw-r--r--app/views/projects/project_members/index.html.haml51
-rw-r--r--app/views/registrations/invites/new.html.haml3
-rw-r--r--changelogs/unreleased/324681-group-project-members-migrate-to-one-vue-app-and-gltabs-2.yml5
-rw-r--r--changelogs/unreleased/325982-feature-flag-enable-ci_external_validation_service.yml5
-rw-r--r--config/feature_flags/development/ci_external_validation_service.yml8
-rw-r--r--danger/plugins/product_intelligence.rb10
-rw-r--r--danger/product_intelligence/Dangerfile82
-rw-r--r--doc/architecture/blueprints/ci_scale/ci_builds_cumulative_forecast.pngbin36221 -> 19274 bytes
-rw-r--r--doc/architecture/blueprints/ci_scale/ci_builds_daily_forecast.pngbin29472 -> 11550 bytes
-rw-r--r--doc/development/cicd/index.md14
-rw-r--r--doc/development/testing_guide/end_to_end/img/gl-capybara_V13_12.pngbin19201 -> 6777 bytes
-rw-r--r--doc/development/testing_guide/end_to_end/img/gl-chemlab_V13_12.pngbin17753 -> 6580 bytes
-rw-r--r--doc/integration/jira/dvcs.md16
-rw-r--r--doc/operations/metrics/dashboards/img/metrics_dashboard_template_selection_v13_3.pngbin31905 -> 9033 bytes
-rw-r--r--doc/user/admin_area/analytics/img/instance_activity_pipelines_chart_v13_6_a.pngbin92540 -> 30831 bytes
-rw-r--r--doc/user/admin_area/img/cohorts_v13_9_a.pngbin122096 -> 35297 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/security_center_settings_v13_4.pngbin69604 -> 23188 bytes
-rw-r--r--doc/user/application_security/threat_monitoring/img/threat_monitoring_policy_alert_list_v13_12.pngbin59862 -> 22929 bytes
-rw-r--r--doc/user/application_security/vulnerability_report/index.md25
-rw-r--r--doc/user/group/issues_analytics/img/issues_created_per_month_v12_8_a.pngbin54332 -> 16244 bytes
-rw-r--r--doc/user/group/value_stream_analytics/img/delete_value_stream_v13_12.pngbin57007 -> 20164 bytes
-rw-r--r--doc/user/group/value_stream_analytics/img/new_value_stream_v13_12.pngbin55481 -> 19993 bytes
-rw-r--r--doc/user/group/value_stream_analytics/img/vsa_filter_bar_v13_12.pngbin125695 -> 36706 bytes
-rw-r--r--doc/user/group/value_stream_analytics/img/vsa_label_based_stage_v14_0.pngbin65149 -> 19873 bytes
-rw-r--r--doc/user/group/value_stream_analytics/img/vsa_overview_stage_v13_11.pngbin60960 -> 21148 bytes
-rw-r--r--doc/user/group/value_stream_analytics/img/vsa_time_metrics_v13_12.pngbin51065 -> 18354 bytes
-rw-r--r--doc/user/profile/img/notification_global_settings_v13_12.pngbin20731 -> 9268 bytes
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/external.rb8
-rw-r--r--qa/qa/page/group/members.rb2
-rw-r--r--qa/qa/page/project/members.rb2
-rw-r--r--qa/qa/page/registration/sign_up.rb5
-rwxr-xr-xscripts/review_apps/review-apps.sh1
-rw-r--r--spec/features/groups/members/tabs_spec.rb6
-rw-r--r--spec/features/projects/members/tabs_spec.rb4
-rw-r--r--spec/frontend/members/components/app_spec.js2
-rw-r--r--spec/frontend/members/components/members_tabs_spec.js10
-rw-r--r--spec/frontend/members/index_spec.js19
-rw-r--r--spec/frontend/members/mock_data.js12
-rw-r--r--spec/frontend/members/utils_spec.js10
-rw-r--r--spec/helpers/groups/group_members_helper_spec.rb106
-rw-r--r--spec/helpers/projects/project_members_helper_spec.rb75
-rw-r--r--spec/helpers/registrations_helper_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb27
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/tooling/danger/product_intelligence_spec.rb150
-rw-r--r--spec/views/devise/shared/_signup_box.html.haml_spec.rb2
-rw-r--r--tooling/danger/product_intelligence.rb100
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
index 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
Binary files differ
diff --git a/doc/architecture/blueprints/ci_scale/ci_builds_daily_forecast.png b/doc/architecture/blueprints/ci_scale/ci_builds_daily_forecast.png
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
diff --git a/doc/user/admin_area/img/cohorts_v13_9_a.png b/doc/user/admin_area/img/cohorts_v13_9_a.png
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
diff --git a/doc/user/profile/img/notification_global_settings_v13_12.png b/doc/user/profile/img/notification_global_settings_v13_12.png
index 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
Binary files differ
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