summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-13 21:11:25 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-13 21:11:25 +0000
commit5f36333180258e43c88b71047086150b6ca233a4 (patch)
tree62b80c1edc934309aca3f3eddde766a1c6d94c4f
parenta5605d87fb839e0b1015ad9e736c44fbb2ada202 (diff)
downloadgitlab-ce-5f36333180258e43c88b71047086150b6ca233a4.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml13
-rw-r--r--.rubocop_manual_todo.yml59
-rw-r--r--.rubocop_todo.yml7
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue4
-rw-r--r--app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue7
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue7
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_member_button.vue7
-rw-r--r--app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue7
-rw-r--r--app/assets/javascripts/members/components/app.vue14
-rw-r--r--app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue10
-rw-r--r--app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue8
-rw-r--r--app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue10
-rw-r--r--app/assets/javascripts/members/components/modals/leave_modal.vue7
-rw-r--r--app/assets/javascripts/members/components/modals/remove_group_link_modal.vue19
-rw-r--r--app/assets/javascripts/members/components/table/expiration_datepicker.vue7
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue14
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue7
-rw-r--r--app/assets/javascripts/members/index.js24
-rw-r--r--app/assets/javascripts/members/store/index.js1
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js5
-rw-r--r--app/assets/javascripts/performance_bar/stores/performance_bar_store.js13
-rw-r--r--app/assets/javascripts/pipelines/components/graph/constants.js3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue23
-rw-r--r--app/views/devise/shared/_sign_in_link.html.haml4
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml2
-rw-r--r--app/views/shared/_file_picker_button.html.haml2
-rw-r--r--changelogs/unreleased/322043-insert-plan-trial.yml5
-rw-r--r--changelogs/unreleased/Externalise-strings-in-shared-_sign_in_link-html-haml.yml5
-rw-r--r--changelogs/unreleased/btn-default-filepicker.yml5
-rw-r--r--changelogs/unreleased/issue-220040-fix-rails-savebang-email-handlers.yml5
-rw-r--r--changelogs/unreleased/limit-graphql-requests-in-performance-bar.yml5
-rw-r--r--db/post_migrate/20210329102724_add_new_trail_plans.rb41
-rw-r--r--db/schema_migrations/202103291027241
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/user/admin_area/abuse_reports.md2
-rw-r--r--doc/user/admin_area/img/abuse_reports_page.pngbin86180 -> 0 bytes
-rw-r--r--doc/user/admin_area/img/abuse_reports_page_v13_11.pngbin0 -> 77994 bytes
-rw-r--r--doc/user/analytics/code_review_analytics.md4
-rw-r--r--doc/user/analytics/img/code_review_analytics_v12_8.pngbin40082 -> 0 bytes
-rw-r--r--doc/user/analytics/img/code_review_analytics_v13_11.pngbin0 -> 107184 bytes
-rw-r--r--doc/user/analytics/img/issues_created_per_month_v12_8.pngbin26718 -> 0 bytes
-rw-r--r--doc/user/analytics/img/issues_created_per_month_v13_11.pngbin0 -> 57729 bytes
-rw-r--r--doc/user/analytics/issue_analytics.md4
-rw-r--r--doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_11.pngbin0 -> 45002 bytes
-rw-r--r--doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_6.pngbin64273 -> 0 bytes
-rw-r--r--doc/user/compliance/compliance_dashboard/index.md2
-rw-r--r--doc/user/group/insights/img/insights_example_stacked_bar_chart.pngbin40798 -> 0 bytes
-rw-r--r--doc/user/group/insights/img/insights_example_stacked_bar_chart_v13_11.pngbin0 -> 85296 bytes
-rw-r--r--doc/user/group/insights/img/insights_sidebar_link_v12_8.pngbin7773 -> 0 bytes
-rw-r--r--doc/user/group/insights/index.md6
-rw-r--r--doc/user/packages/container_registry/index.md2
-rw-r--r--doc/user/packages/index.md2
-rw-r--r--doc/user/packages/rubygems_registry/index.md2
-rw-r--r--doc/user/packages/workflows/project_registry.md2
-rw-r--r--doc/user/permissions.md2
-rw-r--r--locale/gitlab.pot3
-rw-r--r--rubocop/cop/style/regexp_literal_mixed_preserve.rb33
-rw-r--r--spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js15
-rw-r--r--spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js11
-rw-r--r--spec/frontend/members/components/action_buttons/remove_member_button_spec.js15
-rw-r--r--spec/frontend/members/components/action_buttons/resend_invite_button_spec.js15
-rw-r--r--spec/frontend/members/components/app_spec.js25
-rw-r--r--spec/frontend/members/components/filter_sort/filter_sort_container_spec.js27
-rw-r--r--spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js23
-rw-r--r--spec/frontend/members/components/filter_sort/sort_dropdown_spec.js25
-rw-r--r--spec/frontend/members/components/modals/leave_modal_spec.js16
-rw-r--r--spec/frontend/members/components/modals/remove_group_link_modal_spec.js22
-rw-r--r--spec/frontend/members/components/table/expiration_datepicker_spec.js10
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js21
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js10
-rw-r--r--spec/frontend/members/index_spec.js20
-rw-r--r--spec/frontend/performance_bar/stores/performance_bar_store_spec.js40
-rw-r--r--spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb2
-rw-r--r--spec/migrations/add_new_trail_plans_spec.rb95
-rw-r--r--spec/rubocop/cop/style/regexp_literal_mixed_preserve_spec.rb131
78 files changed, 818 insertions, 138 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 89a9cbc3744..a19cba5eb46 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -644,3 +644,16 @@ Cop/UserAdmin:
Performance/OpenStruct:
Exclude:
- 'ee/spec/**/*.rb'
+
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/327495
+Style/RegexpLiteral:
+ Enabled: false
+
+Style/RegexpLiteralMixedPreserve:
+ Enabled: true
+ SupportedStyles:
+ - slashes
+ - percent_r
+ - mixed
+ - mixed_preserve
+ EnforcedStyle: mixed_preserve
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index 1073f6a04d1..786f81936e5 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -176,8 +176,6 @@ Rails/SaveBang:
- 'spec/lib/gitlab/database/custom_structure_spec.rb'
- 'spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb'
- 'spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb'
- - 'spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
- - 'spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb'
- 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
- 'spec/lib/gitlab/git_access_spec.rb'
- 'spec/lib/gitlab/import_export/avatar_saver_spec.rb'
@@ -3332,3 +3330,60 @@ Gitlab/FeatureAvailableUsage:
- 'ee/spec/models/project_spec.rb'
- 'lib/api/helpers/related_resources_helpers.rb'
- 'spec/models/concerns/featurable_spec.rb'
+
+# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/327490
+Style/RegexpLiteralMixedPreserve:
+ Exclude:
+ - 'app/controllers/projects/repositories_controller.rb'
+ - 'app/helpers/ci/variables_helper.rb'
+ - 'app/models/alert_management/alert.rb'
+ - 'app/models/application_setting.rb'
+ - 'app/models/blob_viewer/go_mod.rb'
+ - 'app/models/concerns/ci/maskable.rb'
+ - 'app/models/operations/feature_flag.rb'
+ - 'app/models/packages/go/module.rb'
+ - 'app/models/project_services/chat_message/base_message.rb'
+ - 'app/services/packages/conan/search_service.rb'
+ - 'app/services/projects/update_remote_mirror_service.rb'
+ - 'config/initializers/rspec_profiling.rb'
+ - 'ee/app/models/status_page/project_setting.rb'
+ - 'ee/app/presenters/vulnerability_presenter.rb'
+ - 'ee/lib/api/geo_nodes.rb'
+ - 'ee/lib/gitlab/vulnerabilities/standard_vulnerability.rb'
+ - 'ee/spec/controllers/concerns/ee/routable_actions/sso_enforcement_redirect_spec.rb'
+ - 'ee/spec/controllers/concerns/routable_actions_spec.rb'
+ - 'ee/spec/controllers/groups/groups_controller_spec.rb'
+ - 'ee/spec/features/groups/saml_enforcement_spec.rb'
+ - 'ee/spec/features/markdown/metrics_spec.rb'
+ - 'ee/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb'
+ - 'ee/spec/models/project_services/jira_service_spec.rb'
+ - 'ee/spec/services/jira/requests/issues/list_service_spec.rb'
+ - 'lib/api/invitations.rb'
+ - 'lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb'
+ - 'lib/gitlab/metrics/requests_rack_middleware.rb'
+ - 'lib/gitlab/metrics/subscribers/active_record.rb'
+ - 'lib/gitlab/regex.rb'
+ - 'lib/gitlab/utils.rb'
+ - 'lib/product_analytics/tracker.rb'
+ - 'qa/qa/page/project/settings/advanced.rb'
+ - 'qa/spec/service/docker_run/gitlab_runner_spec.rb'
+ - 'rubocop/cop/gitlab/duplicate_spec_location.rb'
+ - 'spec/features/clusters/cluster_health_dashboard_spec.rb'
+ - 'spec/features/markdown/metrics_spec.rb'
+ - 'spec/features/search/user_searches_for_code_spec.rb'
+ - 'spec/features/snippets/embedded_snippet_spec.rb'
+ - 'spec/helpers/diff_helper_spec.rb'
+ - 'spec/helpers/releases_helper_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/test_case_spec.rb'
+ - 'spec/lib/gitlab/consul/internal_spec.rb'
+ - 'spec/lib/gitlab/import_export/shared_spec.rb'
+ - 'spec/lib/gitlab/utils/usage_data_spec.rb'
+ - 'spec/presenters/ci/build_runner_presenter_spec.rb'
+ - 'spec/requests/api/projects_spec.rb'
+ - 'spec/services/jira/requests/projects/list_service_spec.rb'
+ - 'spec/support/capybara.rb'
+ - 'spec/support/helpers/grafana_api_helpers.rb'
+ - 'spec/support/helpers/query_recorder.rb'
+ - 'spec/support/helpers/require_migration.rb'
+ - 'spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb'
+ - 'spec/views/layouts/_head.html.haml_spec.rb'
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index f06496d4519..cfcf990029d 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -922,13 +922,6 @@ Style/RedundantRegexpEscape:
Style/RedundantSelf:
Enabled: false
-# Offense count: 213
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
-# SupportedStyles: slashes, percent_r, mixed
-Style/RegexpLiteral:
- Enabled: false
-
# Offense count: 53
# Cop supports --auto-correct.
Style/RescueModifier:
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index f7cfe80df5c..829a9d64cb7 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -87,7 +87,7 @@ export default {
@input="searchMergeRequests"
@removeToken="setSearchType(null)"
/>
- <gl-icon :size="18" name="search" class="ml-3 input-icon" use-deprecated-sizes />
+ <gl-icon :size="16" name="search" class="ml-3 input-icon" />
</label>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon
@@ -105,7 +105,7 @@ export default {
@click.stop="setSearchType(searchType)"
>
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
- <gl-icon :size="18" name="search" use-deprecated-sizes />
+ <gl-icon :size="16" name="search" />
</span>
<span>{{ searchType.label }}</span>
</button>
diff --git a/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
index 83f266779f2..00973100e15 100644
--- a/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
@@ -12,6 +12,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ inject: ['namespace'],
props: {
memberId: {
type: Number,
@@ -19,7 +20,11 @@ export default {
},
},
computed: {
- ...mapState(['memberPath']),
+ ...mapState({
+ memberPath(state) {
+ return state[this.namespace].memberPath;
+ },
+ }),
approvePath() {
return this.memberPath.replace(/:id$/, `${this.memberId}/approve_access_request`);
},
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
index 3b87c29c1bc..fef7940eaa2 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
@@ -12,6 +12,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ inject: ['namespace'],
props: {
groupLink: {
type: Object,
@@ -19,7 +20,11 @@ export default {
},
},
methods: {
- ...mapActions(['showRemoveGroupLinkModal']),
+ ...mapActions({
+ showRemoveGroupLinkModal(dispatch, payload) {
+ return dispatch(`${this.namespace}/showRemoveGroupLinkModal`, payload);
+ },
+ }),
},
};
</script>
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index 9954da3e0d4..fc5fcdc52c5 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
@@ -8,6 +8,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ inject: ['namespace'],
props: {
memberId: {
type: Number,
@@ -43,7 +44,11 @@ export default {
},
},
computed: {
- ...mapState(['memberPath']),
+ ...mapState({
+ memberPath(state) {
+ return state[this.namespace].memberPath;
+ },
+ }),
computedMemberPath() {
return this.memberPath.replace(':id', this.memberId);
},
diff --git a/app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue b/app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue
index 261a6279920..2173974c6f4 100644
--- a/app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue
@@ -12,6 +12,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ inject: ['namespace'],
props: {
memberId: {
type: Number,
@@ -19,7 +20,11 @@ export default {
},
},
computed: {
- ...mapState(['memberPath']),
+ ...mapState({
+ memberPath(state) {
+ return state[this.namespace].memberPath;
+ },
+ }),
resendPath() {
return this.memberPath.replace(/:id$/, `${this.memberId}/resend_invite`);
},
diff --git a/app/assets/javascripts/members/components/app.vue b/app/assets/javascripts/members/components/app.vue
index 27fceb7374e..585fabdf3ff 100644
--- a/app/assets/javascripts/members/components/app.vue
+++ b/app/assets/javascripts/members/components/app.vue
@@ -9,8 +9,16 @@ import MembersTable from './table/members_table.vue';
export default {
name: 'MembersApp',
components: { MembersTable, FilterSortContainer, GlAlert },
+ inject: ['namespace'],
computed: {
- ...mapState(['showError', 'errorMessage']),
+ ...mapState({
+ showError(state) {
+ return state[this.namespace].showError;
+ },
+ errorMessage(state) {
+ return state[this.namespace].errorMessage;
+ },
+ }),
},
watch: {
showError(value) {
@@ -23,7 +31,9 @@ export default {
},
methods: {
...mapMutations({
- hideError: HIDE_ERROR,
+ hideError(commit) {
+ return commit(`${this.namespace}/${HIDE_ERROR}`);
+ },
}),
},
};
diff --git a/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
index 812a8626949..419b7b83c0f 100644
--- a/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
+++ b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
@@ -6,8 +6,16 @@ import SortDropdown from './sort_dropdown.vue';
export default {
name: 'FilterSortContainer',
components: { MembersFilteredSearchBar, SortDropdown },
+ inject: ['namespace'],
computed: {
- ...mapState(['filteredSearchBar', 'tableSortableFields']),
+ ...mapState({
+ filteredSearchBar(state) {
+ return state[this.namespace].filteredSearchBar;
+ },
+ tableSortableFields(state) {
+ return state[this.namespace].tableSortableFields;
+ },
+ }),
showContainer() {
return this.filteredSearchBar.show || this.showSortDropdown;
},
diff --git a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
index 9e58c7022b8..cc97d235a9c 100644
--- a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
+++ b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
@@ -37,14 +37,18 @@ export default {
],
},
],
- inject: ['sourceId', 'canManageMembers'],
+ inject: ['namespace', 'sourceId', 'canManageMembers'],
data() {
return {
initialFilterValue: [],
};
},
computed: {
- ...mapState(['filteredSearchBar']),
+ ...mapState({
+ filteredSearchBar(state) {
+ return state[this.namespace].filteredSearchBar;
+ },
+ }),
tokens() {
return this.$options.availableTokens.filter((token) => {
if (
diff --git a/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
index 9fa8772faf4..ce28283ccdf 100644
--- a/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
+++ b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
@@ -8,8 +8,16 @@ import { parseSortParam, buildSortHref } from '~/members/utils';
export default {
name: 'SortDropdown',
components: { GlSorting, GlSortingItem },
+ inject: ['namespace'],
computed: {
- ...mapState(['tableSortableFields', 'filteredSearchBar']),
+ ...mapState({
+ tableSortableFields(state) {
+ return state[this.namespace].tableSortableFields;
+ },
+ filteredSearchBar(state) {
+ return state[this.namespace].filteredSearchBar;
+ },
+ }),
sort() {
return parseSortParam(this.tableSortableFields);
},
diff --git a/app/assets/javascripts/members/components/modals/leave_modal.vue b/app/assets/javascripts/members/components/modals/leave_modal.vue
index a0f978d85cc..b4cbef13b75 100644
--- a/app/assets/javascripts/members/components/modals/leave_modal.vue
+++ b/app/assets/javascripts/members/components/modals/leave_modal.vue
@@ -23,6 +23,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ inject: ['namespace'],
props: {
member: {
type: Object,
@@ -30,7 +31,11 @@ export default {
},
},
computed: {
- ...mapState(['memberPath']),
+ ...mapState({
+ memberPath(state) {
+ return state[this.namespace].memberPath;
+ },
+ }),
leavePath() {
return this.memberPath.replace(/:id$/, 'leave');
},
diff --git a/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
index 1ba6bf9aba6..b179ced46e1 100644
--- a/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
@@ -22,8 +22,19 @@ export default {
},
modalId: REMOVE_GROUP_LINK_MODAL_ID,
components: { GlModal, GlSprintf, GlForm },
+ inject: ['namespace'],
computed: {
- ...mapState(['memberPath', 'groupLinkToRemove', 'removeGroupLinkModalVisible']),
+ ...mapState({
+ memberPath(state) {
+ return state[this.namespace].memberPath;
+ },
+ groupLinkToRemove(state) {
+ return state[this.namespace].groupLinkToRemove;
+ },
+ removeGroupLinkModalVisible(state) {
+ return state[this.namespace].removeGroupLinkModalVisible;
+ },
+ }),
groupLinkPath() {
return this.memberPath.replace(/:id$/, this.groupLinkToRemove?.id);
},
@@ -35,7 +46,11 @@ export default {
},
},
methods: {
- ...mapActions(['hideRemoveGroupLinkModal']),
+ ...mapActions({
+ hideRemoveGroupLinkModal(dispatch) {
+ return dispatch(`${this.namespace}/hideRemoveGroupLinkModal`);
+ },
+ }),
handlePrimary() {
this.$refs.form.$el.submit();
},
diff --git a/app/assets/javascripts/members/components/table/expiration_datepicker.vue b/app/assets/javascripts/members/components/table/expiration_datepicker.vue
index 0a8af81c1d1..9f6e8979102 100644
--- a/app/assets/javascripts/members/components/table/expiration_datepicker.vue
+++ b/app/assets/javascripts/members/components/table/expiration_datepicker.vue
@@ -7,6 +7,7 @@ import { s__ } from '~/locale';
export default {
name: 'ExpirationDatepicker',
components: { GlDatepicker },
+ inject: ['namespace'],
props: {
member: {
type: Object,
@@ -46,7 +47,11 @@ export default {
}
},
methods: {
- ...mapActions(['updateMemberExpiration']),
+ ...mapActions({
+ updateMemberExpiration(dispatch, payload) {
+ return dispatch(`${this.namespace}/updateMemberExpiration`, payload);
+ },
+ }),
handleInput(date) {
this.busy = true;
this.updateMemberExpiration({
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 5db29951d94..236aeaef418 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -31,9 +31,19 @@ export default {
LdapOverrideConfirmationModal: () =>
import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
},
- inject: ['currentUserId'],
+ inject: ['namespace', 'currentUserId'],
computed: {
- ...mapState(['members', 'tableFields', 'tableAttrs']),
+ ...mapState({
+ members(state) {
+ return state[this.namespace].members;
+ },
+ tableFields(state) {
+ return state[this.namespace].tableFields;
+ },
+ tableAttrs(state) {
+ return state[this.namespace].tableAttrs;
+ },
+ }),
filteredFields() {
return FIELDS.filter(
(field) => this.tableFields.includes(field.key) && this.showField(field),
diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
index 8ad45ab6920..f84ded427cd 100644
--- a/app/assets/javascripts/members/components/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/role_dropdown.vue
@@ -11,6 +11,7 @@ export default {
GlDropdownItem,
LdapDropdownItem: () => import('ee_component/members/components/ldap/ldap_dropdown_item.vue'),
},
+ inject: ['namespace'],
props: {
member: {
type: Object,
@@ -44,7 +45,11 @@ export default {
}
},
methods: {
- ...mapActions(['updateMemberRole']),
+ ...mapActions({
+ updateMemberRole(dispatch, payload) {
+ return dispatch(`${this.namespace}/updateMemberRole`, payload);
+ },
+ }),
handleSelect(value, name) {
if (value === this.member.accessLevel.integerValue) {
return;
diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js
index 2f3589bbf6a..6376b3fa75a 100644
--- a/app/assets/javascripts/members/index.js
+++ b/app/assets/javascripts/members/index.js
@@ -8,6 +8,7 @@ import membersStore from './store';
export const initMembersApp = (
el,
{
+ namespace,
tableFields = [],
tableAttrs = {},
tableSortableFields = [],
@@ -24,22 +25,25 @@ export const initMembersApp = (
const { sourceId, canManageMembers, ...vuexStoreAttributes } = parseDataAttributes(el);
- const store = new Vuex.Store(
- membersStore({
- ...vuexStoreAttributes,
- tableFields,
- tableAttrs,
- tableSortableFields,
- requestFormatter,
- filteredSearchBar,
- }),
- );
+ const store = new Vuex.Store({
+ modules: {
+ [namespace]: membersStore({
+ ...vuexStoreAttributes,
+ tableFields,
+ tableAttrs,
+ tableSortableFields,
+ requestFormatter,
+ filteredSearchBar,
+ }),
+ },
+ });
return new Vue({
el,
components: { App },
store,
provide: {
+ namespace,
currentUserId: gon.current_user_id || null,
sourceId,
canManageMembers,
diff --git a/app/assets/javascripts/members/store/index.js b/app/assets/javascripts/members/store/index.js
index 45f4eefffc9..6c371887a3f 100644
--- a/app/assets/javascripts/members/store/index.js
+++ b/app/assets/javascripts/members/store/index.js
@@ -3,6 +3,7 @@ import mutations from 'ee_else_ce/members/store/mutations';
import createState from 'ee_else_ce/members/store/state';
export default (initialState) => ({
+ namespaced: true,
state: createState(initialState),
actions,
mutations,
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index ab70fa572ba..b0a70055835 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -8,6 +8,7 @@ import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigg
import { s__ } from '~/locale';
import memberExpirationDate from '~/member_expiration_date';
import { initMembersApp } from '~/members';
+import { MEMBER_TYPES } from '~/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils';
import UsersSelect from '~/users_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
@@ -29,6 +30,7 @@ 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'],
@@ -43,6 +45,7 @@ initMembersApp(document.querySelector('.js-group-members-list'), {
});
initMembersApp(document.querySelector('.js-group-group-links-list'), {
+ namespace: MEMBER_TYPES.group,
tableFields: SHARED_FIELDS.concat('granted'),
tableAttrs: {
table: { 'data-qa-selector': 'groups_list' },
@@ -51,6 +54,7 @@ initMembersApp(document.querySelector('.js-group-group-links-list'), {
requestFormatter: groupLinkRequestFormatter,
});
initMembersApp(document.querySelector('.js-group-invited-members-list'), {
+ namespace: MEMBER_TYPES.invite,
tableFields: SHARED_FIELDS.concat('invited'),
requestFormatter: groupMemberRequestFormatter,
filteredSearchBar: {
@@ -62,6 +66,7 @@ initMembersApp(document.querySelector('.js-group-invited-members-list'), {
},
});
initMembersApp(document.querySelector('.js-group-access-requests-list'), {
+ namespace: MEMBER_TYPES.accessRequest,
tableFields: SHARED_FIELDS.concat('requested'),
requestFormatter: groupMemberRequestFormatter,
});
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index 4aea5614bfb..471798d2931 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -7,6 +7,7 @@ import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigg
import { s__ } from '~/locale';
import memberExpirationDate from '~/member_expiration_date';
import { initMembersApp } from '~/members';
+import { MEMBER_TYPES } from '~/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils';
import { projectMemberRequestFormatter } from '~/projects/members/utils';
import UsersSelect from '~/users_select';
@@ -42,6 +43,7 @@ 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'],
@@ -56,6 +58,7 @@ initMembersApp(document.querySelector('.js-project-members-list'), {
});
initMembersApp(document.querySelector('.js-project-group-links-list'), {
+ namespace: MEMBER_TYPES.group,
tableFields: SHARED_FIELDS.concat('granted'),
tableAttrs: {
table: { 'data-qa-selector': 'groups_list' },
@@ -72,11 +75,13 @@ initMembersApp(document.querySelector('.js-project-group-links-list'), {
});
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/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
index 9d12d228d35..51a8eb5ca69 100644
--- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
+++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
@@ -47,10 +47,15 @@ export default class PerformanceBarStore {
}
canTrackRequest(requestUrl) {
- return (
- requestUrl.endsWith('/api/graphql') ||
- this.requests.filter((request) => request.url === requestUrl).length < 2
- );
+ // We want to store at most 2 unique requests per URL, as additional
+ // requests to the same URL probably aren't very interesting.
+ //
+ // GraphQL requests are the exception: because all GraphQL requests
+ // go to the same URL, we set a higher limit of 10 to allow
+ // capturing different queries a page may make.
+ const requestsLimit = requestUrl.endsWith('/api/graphql') ? 10 : 2;
+
+ return this.requests.filter((request) => request.url === requestUrl).length < requestsLimit;
}
static truncateUrl(requestUrl) {
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js
index 216b6c6737c..dd9cdae518f 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/pipelines/components/graph/constants.js
@@ -15,4 +15,7 @@ export const STAGE_VIEW = 'stage';
export const LAYER_VIEW = 'layer';
export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
+export const SINGLE_JOB = 'single_job';
+export const JOB_DROPDOWN = 'job_dropdown';
+
export const IID_FAILURE = 'missing_iid';
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index 78fee6a75a8..6451605a222 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -1,5 +1,6 @@
<script>
import { reportToSentry } from '../../utils';
+import { JOB_DROPDOWN, SINGLE_JOB } from './constants';
import JobItem from './job_item.vue';
/**
@@ -28,6 +29,10 @@ export default {
default: '',
},
},
+ jobItemTypes: {
+ jobDropdown: JOB_DROPDOWN,
+ singleJob: SINGLE_JOB,
+ },
computed: {
computedJobId() {
return this.pipelineId > -1 ? `${this.group.name}-${this.pipelineId}` : '';
@@ -57,11 +62,10 @@ export default {
>
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
<job-item
- :dropdown-length="group.size"
+ :type="$options.jobItemTypes.jobDropdown"
:group-tooltip="tooltipText"
:job="group"
:stage-name="stageName"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
<div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4">{{ group.size }}</div>
@@ -75,6 +79,7 @@ export default {
<job-item
:dropdown-length="group.size"
:job="job"
+ :type="$options.jobItemTypes.singleJob"
css-class-job-name="mini-pipeline-graph-dropdown-item"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 0f95037597f..6584d89d87c 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -8,7 +8,7 @@ import { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue';
import JobNameComponent from '../jobs_shared/job_name_component.vue';
import { accessValue } from './accessors';
-import { REST } from './constants';
+import { REST, SINGLE_JOB } from './constants';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@@ -97,6 +97,11 @@ export default {
required: false,
default: '',
},
+ type: {
+ type: String,
+ required: false,
+ default: SINGLE_JOB,
+ },
},
computed: {
boundary() {
@@ -111,6 +116,9 @@ export default {
hasDetails() {
return accessValue(this.dataMethod, 'hasDetails', this.status);
},
+ isSingleItem() {
+ return this.type === SINGLE_JOB;
+ },
nameComponent() {
return this.hasDetails ? 'gl-link' : 'div';
},
@@ -177,6 +185,17 @@ export default {
hideTooltips() {
this.$root.$emit(BV_HIDE_TOOLTIP);
},
+ jobItemClick(evt) {
+ if (this.isSingleItem) {
+ /*
+ This is so the jobDropdown still toggles. Issue to refactor:
+ https://gitlab.com/gitlab-org/gitlab/-/issues/267117
+ */
+ evt.stopPropagation();
+ }
+
+ this.hideTooltips();
+ },
pipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
},
@@ -201,7 +220,7 @@ export default {
:href="detailsPath"
class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none gl-w-full"
:data-testid="testId"
- @click.stop="hideTooltips"
+ @click="jobItemClick"
@mouseout="hideTooltips"
>
<div class="ci-job-name-component gl-display-flex gl-align-items-center">
diff --git a/app/views/devise/shared/_sign_in_link.html.haml b/app/views/devise/shared/_sign_in_link.html.haml
index 41a67485f14..0a48c342502 100644
--- a/app/views/devise/shared/_sign_in_link.html.haml
+++ b/app/views/devise/shared/_sign_in_link.html.haml
@@ -1,6 +1,6 @@
%p.text-center
%span.light
- Already have login and password?
+ = _('Already have login and password?')
- path_params = { redirect_to_referer: 'yes' }
- path_params[:invite_email] = @invite_email if @invite_email.present?
- = link_to "Sign in", new_session_path(:user, path_params)
+ = link_to _('Sign in'), new_session_path(:user, path_params)
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 40faf91eadf..90b79fddff1 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,4 +1,4 @@
-- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
+- current_route_path = request.fullpath.match(%r{-/tree/[^/]+/(.+$)}).to_a[1]
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
- @content_class = "limit-container-width" unless fluid_layout
- @skip_current_level_breadcrumb = true
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 355277b7d41..2d0c4cc20a0 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -1,4 +1,4 @@
-- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
+- current_route_path = request.fullpath.match(%r{-/tree/[^/]+/(.+$)}).to_a[1]
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
- add_page_startup_graphql_call('repository/permissions', { projectPath: @project.full_path })
- add_page_startup_graphql_call('repository/files', { nextPageCursor: "", pageSize: 100, projectPath: @project.full_path, ref: current_ref, path: current_route_path || "/"})
diff --git a/app/views/shared/_file_picker_button.html.haml b/app/views/shared/_file_picker_button.html.haml
index a30c144c38f..1d688e7f4b0 100644
--- a/app/views/shared/_file_picker_button.html.haml
+++ b/app/views/shared/_file_picker_button.html.haml
@@ -1,7 +1,7 @@
- classes = local_assigns.fetch(:classes, '')
%span.js-filepicker
- %button.gl-button.btn.js-filepicker-button{ type: 'button', class: classes }= _("Choose file…")
+ %button.gl-button.btn.btn-default.js-filepicker-button{ type: 'button', class: classes }= _("Choose file…")
%span.file_name.js-filepicker-filename= _("No file chosen.")
= f.file_field field, class: "js-filepicker-input hidden"
- if help_text.present?
diff --git a/changelogs/unreleased/322043-insert-plan-trial.yml b/changelogs/unreleased/322043-insert-plan-trial.yml
new file mode 100644
index 00000000000..a65860a5f85
--- /dev/null
+++ b/changelogs/unreleased/322043-insert-plan-trial.yml
@@ -0,0 +1,5 @@
+---
+title: Add a migration to insert trail plans within SAAS for Ultimate and Premium plans
+merge_request: 57814
+author:
+type: added
diff --git a/changelogs/unreleased/Externalise-strings-in-shared-_sign_in_link-html-haml.yml b/changelogs/unreleased/Externalise-strings-in-shared-_sign_in_link-html-haml.yml
new file mode 100644
index 00000000000..0b84279fe8a
--- /dev/null
+++ b/changelogs/unreleased/Externalise-strings-in-shared-_sign_in_link-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings in shared/_sign_in_link.html.haml
+merge_request: 58283
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/btn-default-filepicker.yml b/changelogs/unreleased/btn-default-filepicker.yml
new file mode 100644
index 00000000000..17becdea9da
--- /dev/null
+++ b/changelogs/unreleased/btn-default-filepicker.yml
@@ -0,0 +1,5 @@
+---
+title: Add btn-default class for file picker button
+merge_request: 58238
+author: Yogi (@yo)
+type: changed
diff --git a/changelogs/unreleased/issue-220040-fix-rails-savebang-email-handlers.yml b/changelogs/unreleased/issue-220040-fix-rails-savebang-email-handlers.yml
new file mode 100644
index 00000000000..168811e7b19
--- /dev/null
+++ b/changelogs/unreleased/issue-220040-fix-rails-savebang-email-handlers.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Rails/SaveBang Rubocop offenses for email handlers
+merge_request: 58095
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/changelogs/unreleased/limit-graphql-requests-in-performance-bar.yml b/changelogs/unreleased/limit-graphql-requests-in-performance-bar.yml
new file mode 100644
index 00000000000..98bff51a350
--- /dev/null
+++ b/changelogs/unreleased/limit-graphql-requests-in-performance-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Limit number of GraphQL requests tracked in performance bar to 10
+merge_request: 59158
+author:
+type: performance
diff --git a/db/post_migrate/20210329102724_add_new_trail_plans.rb b/db/post_migrate/20210329102724_add_new_trail_plans.rb
new file mode 100644
index 00000000000..b142f6385f7
--- /dev/null
+++ b/db/post_migrate/20210329102724_add_new_trail_plans.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+class AddNewTrailPlans < ActiveRecord::Migration[6.0]
+ class Plan < ActiveRecord::Base
+ self.inheritance_column = :_type_disabled
+
+ has_one :limits, class_name: 'PlanLimits'
+
+ def actual_limits
+ self.limits || self.build_limits
+ end
+ end
+
+ class PlanLimits < ActiveRecord::Base
+ self.inheritance_column = :_type_disabled
+
+ belongs_to :plan
+ end
+
+ def create_plan_limits(plan_limit_name, plan)
+ plan_limit = Plan.find_or_initialize_by(name: plan_limit_name).actual_limits.dup
+ plan_limit.plan = plan
+ plan_limit.save!
+ end
+
+ def up
+ return unless Gitlab.dev_env_or_com?
+
+ ultimate_trial = Plan.create!(name: 'ultimate_trial', title: 'Ultimate Trial')
+ premium_trial = Plan.create!(name: 'premium_trial', title: 'Premium Trial')
+
+ create_plan_limits('gold', ultimate_trial)
+ create_plan_limits('silver', premium_trial)
+ end
+
+ def down
+ return unless Gitlab.dev_env_or_com?
+
+ Plan.where(name: %w(ultimate_trial premium_trial)).delete_all
+ end
+end
diff --git a/db/schema_migrations/20210329102724 b/db/schema_migrations/20210329102724
new file mode 100644
index 00000000000..b2fdccf2bb8
--- /dev/null
+++ b/db/schema_migrations/20210329102724
@@ -0,0 +1 @@
+b40c702ea6b2120da6fe11b213064a7a124dbc86bfb2d6785bfd2274c44f1e22 \ No newline at end of file
diff --git a/doc/ci/README.md b/doc/ci/README.md
index b1bcb578daf..b0ebbf920f9 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -99,7 +99,7 @@ GitLab CI/CD uses a number of concepts to describe and run your build and deploy
| [Cache dependencies](caching/index.md) | Cache your dependencies for a faster execution. |
| [GitLab Runner](https://docs.gitlab.com/runner/) | Configure your own runners to execute your scripts. |
| [Pipeline efficiency](pipelines/pipeline_efficiency.md) | Configure your pipelines to run quickly and efficiently. |
-| [Test cases](test_cases/index.md) | Configure your pipelines to run quickly and efficiently. |
+| [Test cases](test_cases/index.md) | Configure your pipelines to run quickly and efficiently. <!--- this seems to be a duplicate description ---> |
## Configuration
diff --git a/doc/user/admin_area/abuse_reports.md b/doc/user/admin_area/abuse_reports.md
index 653c67ed197..85ad0667322 100644
--- a/doc/user/admin_area/abuse_reports.md
+++ b/doc/user/admin_area/abuse_reports.md
@@ -45,7 +45,7 @@ There are 3 ways to resolve an abuse report, with a button for each method:
The following is an example of the **Abuse Reports** page:
-![abuse-reports-page-image](img/abuse_reports_page.png)
+![abuse-reports-page-image](img/abuse_reports_page_v13_11.png)
### Blocking users
diff --git a/doc/user/admin_area/img/abuse_reports_page.png b/doc/user/admin_area/img/abuse_reports_page.png
deleted file mode 100644
index 30e932211cb..00000000000
--- a/doc/user/admin_area/img/abuse_reports_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/admin_area/img/abuse_reports_page_v13_11.png b/doc/user/admin_area/img/abuse_reports_page_v13_11.png
new file mode 100644
index 00000000000..bcb2aec9e64
--- /dev/null
+++ b/doc/user/admin_area/img/abuse_reports_page_v13_11.png
Binary files differ
diff --git a/doc/user/analytics/code_review_analytics.md b/doc/user/analytics/code_review_analytics.md
index 19016c3aa26..fe091fc9899 100644
--- a/doc/user/analytics/code_review_analytics.md
+++ b/doc/user/analytics/code_review_analytics.md
@@ -24,11 +24,11 @@ Initially, no data appears. Data is populated as users comment on open merge req
Code Review Analytics is available to users with Reporter access and above, and displays a table of open merge requests that have at least one non-author comment. The review time is measured from the time the first non-author comment was submitted.
-To access Code Review Analytics, from your project's menu, go to **Project Analytics > Code Review**.
+To access Code Review Analytics, from your project's menu, go to **Analytics > Code Review**.
You can filter the list of merge requests by milestone and label.
-![Code Review Analytics](img/code_review_analytics_v12_8.png "List of code reviews; oldest review first.")
+![Code Review Analytics](img/code_review_analytics_v13_11.png "List of code reviews; oldest review first.")
The table is sorted by:
diff --git a/doc/user/analytics/img/code_review_analytics_v12_8.png b/doc/user/analytics/img/code_review_analytics_v12_8.png
deleted file mode 100644
index 3b23e74130a..00000000000
--- a/doc/user/analytics/img/code_review_analytics_v12_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/analytics/img/code_review_analytics_v13_11.png b/doc/user/analytics/img/code_review_analytics_v13_11.png
new file mode 100644
index 00000000000..e337afa3ace
--- /dev/null
+++ b/doc/user/analytics/img/code_review_analytics_v13_11.png
Binary files differ
diff --git a/doc/user/analytics/img/issues_created_per_month_v12_8.png b/doc/user/analytics/img/issues_created_per_month_v12_8.png
deleted file mode 100644
index fccfa949779..00000000000
--- a/doc/user/analytics/img/issues_created_per_month_v12_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/analytics/img/issues_created_per_month_v13_11.png b/doc/user/analytics/img/issues_created_per_month_v13_11.png
new file mode 100644
index 00000000000..01ebde5a54d
--- /dev/null
+++ b/doc/user/analytics/img/issues_created_per_month_v13_11.png
Binary files differ
diff --git a/doc/user/analytics/issue_analytics.md b/doc/user/analytics/issue_analytics.md
index 6c6911ff4f4..b77a25a9d62 100644
--- a/doc/user/analytics/issue_analytics.md
+++ b/doc/user/analytics/issue_analytics.md
@@ -13,7 +13,7 @@ Issue Analytics is a bar graph which illustrates the number of issues created ea
The default time span is 13 months, which includes the current month, and the 12 months
prior.
-To access the chart, navigate to your project sidebar and select **{chart}** **Analytics > Issue Analytics**.
+To access the chart, navigate to your project sidebar and select **Analytics > Issue**.
Hover over each bar to see the total number of issues.
@@ -31,7 +31,7 @@ You can change the total number of months displayed by setting a URL parameter.
For example, `https://gitlab.com/groups/gitlab-org/-/issues_analytics?months_back=15`
shows a total of 15 months for the chart in the GitLab.org group.
-![Issues created per month](img/issues_created_per_month_v12_8.png)
+![Issues created per month](img/issues_created_per_month_v13_11.png)
## Drill into the information
diff --git a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_11.png b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_11.png
new file mode 100644
index 00000000000..95e176b71b8
--- /dev/null
+++ b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_11.png
Binary files differ
diff --git a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_6.png b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_6.png
deleted file mode 100644
index b2ac4f95e0d..00000000000
--- a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_6.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/compliance_dashboard/index.md b/doc/user/compliance/compliance_dashboard/index.md
index f1dfc431f25..8f620fe41bb 100644
--- a/doc/user/compliance/compliance_dashboard/index.md
+++ b/doc/user/compliance/compliance_dashboard/index.md
@@ -17,7 +17,7 @@ for merging into production.
To access the Compliance Dashboard for a group, navigate to **{shield}** **Security & Compliance > Compliance** on the group's menu.
-![Compliance Dashboard](img/compliance_dashboard_v13_6.png)
+![Compliance Dashboard](img/compliance_dashboard_v13_11.png)
NOTE:
The Compliance Dashboard shows only the latest MR on each project.
diff --git a/doc/user/group/insights/img/insights_example_stacked_bar_chart.png b/doc/user/group/insights/img/insights_example_stacked_bar_chart.png
deleted file mode 100644
index 0e338b99e4c..00000000000
--- a/doc/user/group/insights/img/insights_example_stacked_bar_chart.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/insights/img/insights_example_stacked_bar_chart_v13_11.png b/doc/user/group/insights/img/insights_example_stacked_bar_chart_v13_11.png
new file mode 100644
index 00000000000..1ef49191a13
--- /dev/null
+++ b/doc/user/group/insights/img/insights_example_stacked_bar_chart_v13_11.png
Binary files differ
diff --git a/doc/user/group/insights/img/insights_sidebar_link_v12_8.png b/doc/user/group/insights/img/insights_sidebar_link_v12_8.png
deleted file mode 100644
index 9a6d6bae766..00000000000
--- a/doc/user/group/insights/img/insights_sidebar_link_v12_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/insights/index.md b/doc/user/group/insights/index.md
index b559e6806e9..4975b27a66d 100644
--- a/doc/user/group/insights/index.md
+++ b/doc/user/group/insights/index.md
@@ -13,14 +13,12 @@ Configure the Insights that matter for your groups to explore data such as
triage hygiene, issues created/closed per a given period, average time for merge
requests to be merged and much more.
-![Insights example stacked bar chart](img/insights_example_stacked_bar_chart.png)
+![Insights example stacked bar chart](img/insights_example_stacked_bar_chart_v13_11.png)
## View your group's Insights
You can access your group's Insights by clicking the **Analytics > Insights**
-link in the left sidebar:
-
-![Insights sidebar link](img/insights_sidebar_link_v12_8.png)
+link in the left sidebar.
## Configure your Insights
diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md
index 5e9d6d8eda1..102defacffa 100644
--- a/doc/user/packages/container_registry/index.md
+++ b/doc/user/packages/container_registry/index.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# GitLab Container Registry
+# GitLab Container Registry **(FREE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4040) in GitLab 8.8.
> - Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker
diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md
index 591bdca9353..74072aa95e1 100644
--- a/doc/user/packages/index.md
+++ b/doc/user/packages/index.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Packages & Registries
+# Packages and Registries **(FREE)**
The GitLab [Package Registry](package_registry/index.md) acts as a private or public registry
for a variety of common package managers. You can publish and share
diff --git a/doc/user/packages/rubygems_registry/index.md b/doc/user/packages/rubygems_registry/index.md
index 2a94d2a3ccf..aa50bc6c2bc 100644
--- a/doc/user/packages/rubygems_registry/index.md
+++ b/doc/user/packages/rubygems_registry/index.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Ruby gems in the Package Registry
+# Ruby gems in the Package Registry **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/803) in [GitLab Free](https://about.gitlab.com/pricing/) 13.10.
diff --git a/doc/user/packages/workflows/project_registry.md b/doc/user/packages/workflows/project_registry.md
index c63c2cc9989..3e1c1e7f2ad 100644
--- a/doc/user/packages/workflows/project_registry.md
+++ b/doc/user/packages/workflows/project_registry.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Store all of your packages in one GitLab project
+# Store all of your packages in one GitLab project **(FREE)**
You can store all of your packages in one project's Package Registry. Rather than using
a GitLab repository to store code, you can use the repository to store all your packages.
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index b35aafd6a7e..0b6094b69af 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -333,7 +333,7 @@ project and should only have access to that project.
External users:
-- Cannot create projects (including forks), groups, or snippets.
+- Can only create projects (including forks), subgroups, and snippets within the top-level group to which they belong.
- Can only access public projects and projects to which they are explicitly granted access,
thus hiding all other internal or private ones from them (like being
logged out).
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8371a6f2812..cbcd2b4ed36 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3266,6 +3266,9 @@ msgstr ""
msgid "Already blocked"
msgstr ""
+msgid "Already have login and password?"
+msgstr ""
+
msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
msgstr ""
diff --git a/rubocop/cop/style/regexp_literal_mixed_preserve.rb b/rubocop/cop/style/regexp_literal_mixed_preserve.rb
new file mode 100644
index 00000000000..4dc38d82f45
--- /dev/null
+++ b/rubocop/cop/style/regexp_literal_mixed_preserve.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Style
+ # This cop is based on `Style/RegexpLiteral` but adds a new
+ # `EnforcedStyle` option `mixed_preserve`.
+ #
+ # This cop will be removed once the upstream PR is merged and RuboCop upgraded.
+ #
+ # See https://github.com/rubocop/rubocop/pull/9688
+ class RegexpLiteralMixedPreserve < RuboCop::Cop::Style::RegexpLiteral
+ module Patch
+ private
+
+ def allowed_slash_literal?(node)
+ super || allowed_mixed_preserve?(node)
+ end
+
+ def allowed_percent_r_literal?(node)
+ super || allowed_mixed_preserve?(node)
+ end
+
+ def allowed_mixed_preserve?(node)
+ style == :mixed_preserve && !contains_disallowed_slash?(node)
+ end
+ end
+
+ prepend Patch
+ end
+ end
+ end
+end
diff --git a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
index f77d41a642e..936715e7723 100644
--- a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ApproveAccessRequestButton from '~/members/components/action_buttons/approve_access_request_button.vue';
+import { MEMBER_TYPES } from '~/members/constants';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -14,9 +15,14 @@ describe('ApproveAccessRequestButton', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
- state: {
- memberPath: '/groups/foo-bar/-/group_members/:id',
- ...state,
+ modules: {
+ [MEMBER_TYPES.accessRequest]: {
+ namespaced: true,
+ state: {
+ memberPath: '/groups/foo-bar/-/group_members/:id',
+ ...state,
+ },
+ },
},
});
};
@@ -25,6 +31,9 @@ describe('ApproveAccessRequestButton', () => {
wrapper = shallowMount(ApproveAccessRequestButton, {
localVue,
store: createStore(state),
+ provide: {
+ namespace: MEMBER_TYPES.accessRequest,
+ },
propsData: {
memberId: 1,
...propsData,
diff --git a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
index f6e342898cb..f91aef131a1 100644
--- a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
@@ -3,6 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RemoveGroupLinkButton from '~/members/components/action_buttons/remove_group_link_button.vue';
+import { MEMBER_TYPES } from '~/members/constants';
import { group } from '../../mock_data';
const localVue = createLocalVue();
@@ -17,7 +18,12 @@ describe('RemoveGroupLinkButton', () => {
const createStore = () => {
return new Vuex.Store({
- actions,
+ modules: {
+ [MEMBER_TYPES.group]: {
+ namespaced: true,
+ actions,
+ },
+ },
});
};
@@ -25,6 +31,9 @@ describe('RemoveGroupLinkButton', () => {
wrapper = mount(RemoveGroupLinkButton, {
localVue,
store: createStore(),
+ provide: {
+ namespace: MEMBER_TYPES.group,
+ },
propsData: {
groupLink: group,
},
diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
index 952ce679a2f..bac3cd1029d 100644
--- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
@@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
+import { MEMBER_TYPES } from '~/members/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -11,9 +12,14 @@ describe('RemoveMemberButton', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
- state: {
- memberPath: '/groups/foo-bar/-/group_members/:id',
- ...state,
+ modules: {
+ [MEMBER_TYPES.user]: {
+ namespaced: true,
+ state: {
+ memberPath: '/groups/foo-bar/-/group_members/:id',
+ ...state,
+ },
+ },
},
});
};
@@ -22,6 +28,9 @@ describe('RemoveMemberButton', () => {
wrapper = shallowMount(RemoveMemberButton, {
localVue,
store: createStore(state),
+ provide: {
+ namespace: MEMBER_TYPES.user,
+ },
propsData: {
memberId: 1,
memberType: 'GroupMember',
diff --git a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
index 49b6979f954..547e067450c 100644
--- a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ResendInviteButton from '~/members/components/action_buttons/resend_invite_button.vue';
+import { MEMBER_TYPES } from '~/members/constants';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -14,9 +15,14 @@ describe('ResendInviteButton', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
- state: {
- memberPath: '/groups/foo-bar/-/group_members/:id',
- ...state,
+ modules: {
+ [MEMBER_TYPES.invite]: {
+ namespaced: true,
+ state: {
+ memberPath: '/groups/foo-bar/-/group_members/:id',
+ ...state,
+ },
+ },
},
});
};
@@ -25,6 +31,9 @@ describe('ResendInviteButton', () => {
wrapper = shallowMount(ResendInviteButton, {
localVue,
store: createStore(state),
+ provide: {
+ namespace: MEMBER_TYPES.invite,
+ },
propsData: {
memberId: 1,
...propsData,
diff --git a/spec/frontend/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js
index a1329c3ee9f..05933e36b52 100644
--- a/spec/frontend/members/components/app_spec.js
+++ b/spec/frontend/members/components/app_spec.js
@@ -5,6 +5,7 @@ import Vuex from 'vuex';
import * as commonUtils from '~/lib/utils/common_utils';
import MembersApp from '~/members/components/app.vue';
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
+import { MEMBER_TYPES } from '~/members/constants';
import { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_types';
import mutations from '~/members/store/mutations';
@@ -17,16 +18,24 @@ describe('MembersApp', () => {
const createComponent = (state = {}, options = {}) => {
store = new Vuex.Store({
- state: {
- showError: true,
- errorMessage: 'Something went wrong, please try again.',
- ...state,
+ modules: {
+ [MEMBER_TYPES.user]: {
+ namespaced: true,
+ state: {
+ showError: true,
+ errorMessage: 'Something went wrong, please try again.',
+ ...state,
+ },
+ mutations,
+ },
},
- mutations,
});
wrapper = shallowMount(MembersApp, {
localVue,
+ provide: {
+ namespace: MEMBER_TYPES.user,
+ },
store,
...options,
});
@@ -48,7 +57,9 @@ describe('MembersApp', () => {
it('renders and scrolls to error alert', async () => {
createComponent({ showError: false, errorMessage: '' });
- store.commit(RECEIVE_MEMBER_ROLE_ERROR, { error: new Error('Network Error') });
+ store.commit(`${MEMBER_TYPES.user}/${RECEIVE_MEMBER_ROLE_ERROR}`, {
+ error: new Error('Network Error'),
+ });
await nextTick();
@@ -66,7 +77,7 @@ describe('MembersApp', () => {
it('does not render and scroll to error alert', async () => {
createComponent();
- store.commit(HIDE_ERROR);
+ store.commit(`${MEMBER_TYPES.user}/${HIDE_ERROR}`);
await nextTick();
diff --git a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
index 0d9f9acbbeb..16ac52737bc 100644
--- a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
+++ b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
@@ -3,6 +3,7 @@ import Vuex from 'vuex';
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
+import { MEMBER_TYPES } from '~/members/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -12,22 +13,30 @@ describe('FilterSortContainer', () => {
const createComponent = (state) => {
const store = new Vuex.Store({
- state: {
- filteredSearchBar: {
- show: true,
- tokens: ['two_factor'],
- searchParam: 'search',
- placeholder: 'Filter members',
- recentSearchesStorageKey: 'group_members',
+ modules: {
+ [MEMBER_TYPES.user]: {
+ namespaced: true,
+ state: {
+ filteredSearchBar: {
+ show: true,
+ tokens: ['two_factor'],
+ searchParam: 'search',
+ placeholder: 'Filter members',
+ recentSearchesStorageKey: 'group_members',
+ },
+ tableSortableFields: ['account'],
+ ...state,
+ },
},
- tableSortableFields: ['account'],
- ...state,
},
});
wrapper = shallowMount(FilterSortContainer, {
localVue,
store,
+ provide: {
+ namespace: MEMBER_TYPES.user,
+ },
});
};
diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
index 326a29747ef..af5434f7068 100644
--- a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
+++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
@@ -2,6 +2,7 @@ import { GlFilteredSearchToken } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
+import { MEMBER_TYPES } from '~/members/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
const localVue = createLocalVue();
@@ -12,15 +13,20 @@ describe('MembersFilteredSearchBar', () => {
const createComponent = ({ state = {}, provide = {} } = {}) => {
const store = new Vuex.Store({
- state: {
- filteredSearchBar: {
- show: true,
- tokens: ['two_factor'],
- searchParam: 'search',
- placeholder: 'Filter members',
- recentSearchesStorageKey: 'group_members',
+ modules: {
+ [MEMBER_TYPES.user]: {
+ namespaced: true,
+ state: {
+ filteredSearchBar: {
+ show: true,
+ tokens: ['two_factor'],
+ searchParam: 'search',
+ placeholder: 'Filter members',
+ recentSearchesStorageKey: 'group_members',
+ },
+ ...state,
+ },
},
- ...state,
},
});
@@ -29,6 +35,7 @@ describe('MembersFilteredSearchBar', () => {
provide: {
sourceId: 1,
canManageMembers: true,
+ namespace: MEMBER_TYPES.user,
...provide,
},
store,
diff --git a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
index 390e12bc0e5..4b335755980 100644
--- a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
+++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
@@ -3,6 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import * as urlUtilities from '~/lib/utils/url_utility';
import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
+import { MEMBER_TYPES } from '~/members/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -14,16 +15,21 @@ describe('SortDropdown', () => {
const createComponent = (state) => {
const store = new Vuex.Store({
- state: {
- tableSortableFields: ['account', 'granted', 'expires', 'maxRole', 'lastSignIn'],
- filteredSearchBar: {
- show: true,
- tokens: ['two_factor'],
- searchParam: 'search',
- placeholder: 'Filter members',
- recentSearchesStorageKey: 'group_members',
+ modules: {
+ [MEMBER_TYPES.user]: {
+ namespaced: true,
+ state: {
+ tableSortableFields: ['account', 'granted', 'expires', 'maxRole', 'lastSignIn'],
+ filteredSearchBar: {
+ show: true,
+ tokens: ['two_factor'],
+ searchParam: 'search',
+ placeholder: 'Filter members',
+ recentSearchesStorageKey: 'group_members',
+ },
+ ...state,
+ },
},
- ...state,
},
});
@@ -31,6 +37,7 @@ describe('SortDropdown', () => {
localVue,
provide: {
sourceId: 1,
+ namespace: MEMBER_TYPES.user,
},
store,
});
diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js
index 2d52911572f..824e169f096 100644
--- a/spec/frontend/members/components/modals/leave_modal_spec.js
+++ b/spec/frontend/members/components/modals/leave_modal_spec.js
@@ -4,7 +4,7 @@ import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import LeaveModal from '~/members/components/modals/leave_modal.vue';
-import { LEAVE_MODAL_ID } from '~/members/constants';
+import { LEAVE_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
import { member } from '../../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -17,9 +17,14 @@ describe('LeaveModal', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
- state: {
- memberPath: '/groups/foo-bar/-/group_members/:id',
- ...state,
+ modules: {
+ [MEMBER_TYPES.user]: {
+ namespaced: true,
+ state: {
+ memberPath: '/groups/foo-bar/-/group_members/:id',
+ ...state,
+ },
+ },
},
});
};
@@ -28,6 +33,9 @@ describe('LeaveModal', () => {
wrapper = mount(LeaveModal, {
localVue,
store: createStore(state),
+ provide: {
+ namespace: MEMBER_TYPES.user,
+ },
propsData: {
member,
...propsData,
diff --git a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
index 62df912c1a2..01279581c55 100644
--- a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
@@ -4,7 +4,7 @@ import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import RemoveGroupLinkModal from '~/members/components/modals/remove_group_link_modal.vue';
-import { REMOVE_GROUP_LINK_MODAL_ID } from '~/members/constants';
+import { REMOVE_GROUP_LINK_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
import { group } from '../../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -21,13 +21,18 @@ describe('RemoveGroupLinkModal', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
- state: {
- memberPath: '/groups/foo-bar/-/group_links/:id',
- groupLinkToRemove: group,
- removeGroupLinkModalVisible: true,
- ...state,
+ modules: {
+ [MEMBER_TYPES.group]: {
+ namespaced: true,
+ state: {
+ memberPath: '/groups/foo-bar/-/group_links/:id',
+ groupLinkToRemove: group,
+ removeGroupLinkModalVisible: true,
+ ...state,
+ },
+ actions,
+ },
},
- actions,
});
};
@@ -35,6 +40,9 @@ describe('RemoveGroupLinkModal', () => {
wrapper = mount(RemoveGroupLinkModal, {
localVue,
store: createStore(state),
+ provide: {
+ namespace: MEMBER_TYPES.group,
+ },
attrs: {
static: true,
},
diff --git a/spec/frontend/members/components/table/expiration_datepicker_spec.js b/spec/frontend/members/components/table/expiration_datepicker_spec.js
index d26172b4ed1..3c4a9ba37ff 100644
--- a/spec/frontend/members/components/table/expiration_datepicker_spec.js
+++ b/spec/frontend/members/components/table/expiration_datepicker_spec.js
@@ -5,6 +5,7 @@ import Vuex from 'vuex';
import { useFakeDate } from 'helpers/fake_date';
import waitForPromises from 'helpers/wait_for_promises';
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
+import { MEMBER_TYPES } from '~/members/constants';
import { member } from '../../mock_data';
const localVue = createLocalVue();
@@ -31,7 +32,11 @@ describe('ExpirationDatepicker', () => {
),
};
- return new Vuex.Store({ actions });
+ return new Vuex.Store({
+ modules: {
+ [MEMBER_TYPES.user]: { namespaced: true, actions },
+ },
+ });
};
const createComponent = (propsData = {}) => {
@@ -41,6 +46,9 @@ describe('ExpirationDatepicker', () => {
permissions: { canUpdate: true },
...propsData,
},
+ provide: {
+ namespace: MEMBER_TYPES.user,
+ },
localVue,
store: createStore(),
mocks: {
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 0395dc39880..5cf1f40a8f4 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -14,6 +14,7 @@ import MemberAvatar from '~/members/components/table/member_avatar.vue';
import MemberSource from '~/members/components/table/member_source.vue';
import MembersTable from '~/members/components/table/members_table.vue';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
+import { MEMBER_TYPES } from '~/members/constants';
import * as initUserPopovers from '~/user_popovers';
import { member as memberMock, directMember, invite, accessRequest } from '../../mock_data';
@@ -25,14 +26,19 @@ describe('MembersTable', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
- state: {
- members: [],
- tableFields: [],
- tableAttrs: {
- table: { 'data-qa-selector': 'members_list' },
- tr: { 'data-qa-selector': 'member_row' },
+ modules: {
+ [MEMBER_TYPES.user]: {
+ namespaced: true,
+ state: {
+ members: [],
+ tableFields: [],
+ tableAttrs: {
+ table: { 'data-qa-selector': 'members_list' },
+ tr: { 'data-qa-selector': 'member_row' },
+ },
+ ...state,
+ },
},
- ...state,
},
});
};
@@ -44,6 +50,7 @@ describe('MembersTable', () => {
provide: {
sourceId: 1,
currentUserId: 1,
+ namespace: MEMBER_TYPES.user,
...provide,
},
stubs: [
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index aa280599061..c8b6bead450 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -7,6 +7,7 @@ import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import { BV_DROPDOWN_SHOW } from '~/lib/utils/constants';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
+import { MEMBER_TYPES } from '~/members/constants';
import { member } from '../../mock_data';
const localVue = createLocalVue();
@@ -24,11 +25,18 @@ describe('RoleDropdown', () => {
updateMemberRole: jest.fn(() => Promise.resolve()),
};
- return new Vuex.Store({ actions });
+ return new Vuex.Store({
+ modules: {
+ [MEMBER_TYPES.user]: { namespaced: true, actions },
+ },
+ });
};
const createComponent = (propsData = {}) => {
wrapper = mount(RoleDropdown, {
+ provide: {
+ namespace: MEMBER_TYPES.user,
+ },
propsData: {
member,
permissions: {},
diff --git a/spec/frontend/members/index_spec.js b/spec/frontend/members/index_spec.js
index f40c08401d4..8b645d9b059 100644
--- a/spec/frontend/members/index_spec.js
+++ b/spec/frontend/members/index_spec.js
@@ -1,5 +1,6 @@
import { createWrapper } from '@vue/test-utils';
import MembersApp from '~/members/components/app.vue';
+import { MEMBER_TYPES } from '~/members/constants';
import { initMembersApp } from '~/members/index';
import { membersJsonString, members } from './mock_data';
@@ -10,6 +11,7 @@ describe('initMembersApp', () => {
const setup = () => {
vm = initMembersApp(el, {
+ namespace: MEMBER_TYPES.user,
tableFields: ['account'],
tableAttrs: { table: { 'data-qa-selector': 'members_list' } },
tableSortableFields: ['account'],
@@ -45,42 +47,46 @@ describe('initMembersApp', () => {
it('parses and sets `members` in Vuex store', () => {
setup();
- expect(vm.$store.state.members).toEqual(members);
+ expect(vm.$store.state[MEMBER_TYPES.user].members).toEqual(members);
});
it('sets `tableFields` in Vuex store', () => {
setup();
- expect(vm.$store.state.tableFields).toEqual(['account']);
+ expect(vm.$store.state[MEMBER_TYPES.user].tableFields).toEqual(['account']);
});
it('sets `tableAttrs` in Vuex store', () => {
setup();
- expect(vm.$store.state.tableAttrs).toEqual({ table: { 'data-qa-selector': 'members_list' } });
+ expect(vm.$store.state[MEMBER_TYPES.user].tableAttrs).toEqual({
+ table: { 'data-qa-selector': 'members_list' },
+ });
});
it('sets `tableSortableFields` in Vuex store', () => {
setup();
- expect(vm.$store.state.tableSortableFields).toEqual(['account']);
+ expect(vm.$store.state[MEMBER_TYPES.user].tableSortableFields).toEqual(['account']);
});
it('sets `requestFormatter` in Vuex store', () => {
setup();
- expect(vm.$store.state.requestFormatter()).toEqual({});
+ expect(vm.$store.state[MEMBER_TYPES.user].requestFormatter()).toEqual({});
});
it('sets `filteredSearchBar` in Vuex store', () => {
setup();
- expect(vm.$store.state.filteredSearchBar).toEqual({ show: false });
+ expect(vm.$store.state[MEMBER_TYPES.user].filteredSearchBar).toEqual({ show: false });
});
it('sets `memberPath` in Vuex store', () => {
setup();
- expect(vm.$store.state.memberPath).toBe('/groups/foo-bar/-/group_members/:id');
+ expect(vm.$store.state[MEMBER_TYPES.user].memberPath).toBe(
+ '/groups/foo-bar/-/group_members/:id',
+ );
});
});
diff --git a/spec/frontend/performance_bar/stores/performance_bar_store_spec.js b/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
index 94dc1237cb0..b7324ba2f6e 100644
--- a/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
+++ b/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
@@ -59,4 +59,44 @@ describe('PerformanceBarStore', () => {
expect(store.findRequest('id').details.test.calls).toEqual(123);
});
});
+
+ describe('canTrackRequest', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new PerformanceBarStore();
+ });
+
+ it('limits to 10 requests for GraphQL', () => {
+ expect(store.canTrackRequest('https://gitlab.com/api/graphql')).toBe(true);
+
+ store.addRequest('0', 'https://gitlab.com/api/graphql');
+ store.addRequest('1', 'https://gitlab.com/api/graphql');
+ store.addRequest('2', 'https://gitlab.com/api/graphql');
+ store.addRequest('3', 'https://gitlab.com/api/graphql');
+ store.addRequest('4', 'https://gitlab.com/api/graphql');
+ store.addRequest('5', 'https://gitlab.com/api/graphql');
+ store.addRequest('6', 'https://gitlab.com/api/graphql');
+ store.addRequest('7', 'https://gitlab.com/api/graphql');
+ store.addRequest('8', 'https://gitlab.com/api/graphql');
+
+ expect(store.canTrackRequest('https://gitlab.com/api/graphql')).toBe(true);
+
+ store.addRequest('9', 'https://gitlab.com/api/graphql');
+
+ expect(store.canTrackRequest('https://gitlab.com/api/graphql')).toBe(false);
+ });
+
+ it('limits to 2 requests for all other URLs', () => {
+ expect(store.canTrackRequest('https://gitlab.com/api/v4/users/1')).toBe(true);
+
+ store.addRequest('a', 'https://gitlab.com/api/v4/users/1');
+
+ expect(store.canTrackRequest('https://gitlab.com/api/v4/users/1')).toBe(true);
+
+ store.addRequest('b', 'https://gitlab.com/api/v4/users/1');
+
+ expect(store.canTrackRequest('https://gitlab.com/api/v4/users/1')).toBe(false);
+ });
+ });
});
diff --git a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
index 13ad9ddd8ef..2c1badbd113 100644
--- a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe Gitlab::Email::Handler::UnsubscribeHandler do
context 'when the noteable could not be found' do
before do
- noteable.destroy
+ noteable.destroy!
end
it 'raises a NoteableNotFoundError' do
diff --git a/spec/migrations/add_new_trail_plans_spec.rb b/spec/migrations/add_new_trail_plans_spec.rb
new file mode 100644
index 00000000000..8ba6da11ad1
--- /dev/null
+++ b/spec/migrations/add_new_trail_plans_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddNewTrailPlans, :migration do
+ describe '#up' do
+ before do
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return true
+ end
+
+ it 'creates 2 entries within the plans table' do
+ expect { migrate! }.to change { AddNewTrailPlans::Plan.count }.by 2
+ expect(AddNewTrailPlans::Plan.last(2).pluck(:name)).to match_array(%w(ultimate_trial premium_trial))
+ end
+
+ it 'creates 2 entries for plan limits' do
+ expect { migrate! }.to change { AddNewTrailPlans::PlanLimits.count }.by 2
+ end
+
+ context 'when the plan limits for gold and silver exists' do
+ before do
+ table(:plans).create!(id: 1, name: 'gold', title: 'Gold')
+ table(:plan_limits).create!(id: 1, plan_id: 1, storage_size_limit: 2000)
+ table(:plans).create!(id: 2, name: 'silver', title: 'Silver')
+ table(:plan_limits).create!(id: 2, plan_id: 2, storage_size_limit: 1000)
+ end
+
+ it 'duplicates the gold and silvers plan limits entries' do
+ migrate!
+
+ ultimate_plan_limits = AddNewTrailPlans::Plan.find_by(name: 'ultimate_trial').limits
+ expect(ultimate_plan_limits.storage_size_limit).to be 2000
+
+ premium_plan_limits = AddNewTrailPlans::Plan.find_by(name: 'premium_trial').limits
+ expect(premium_plan_limits.storage_size_limit).to be 1000
+ end
+ end
+
+ context 'when the instance is not SaaS' do
+ before do
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return false
+ end
+
+ it 'does not create plans and plan limits and returns' do
+ expect { migrate! }.not_to change { AddNewTrailPlans::Plan.count }
+ expect { migrate! }.not_to change { AddNewTrailPlans::Plan.count }
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ table(:plans).create!(id: 3, name: 'other')
+ table(:plan_limits).create!(plan_id: 3)
+ end
+
+ context 'when the instance is SaaS' do
+ before do
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return true
+ end
+
+ it 'removes the newly added ultimate and premium trial entries' do
+ migrate!
+
+ expect { described_class.new.down }.to change { AddNewTrailPlans::Plan.count }.by(-2)
+ expect(AddNewTrailPlans::Plan.find_by(name: 'premium_trial')).to be_nil
+ expect(AddNewTrailPlans::Plan.find_by(name: 'ultimate_trial')).to be_nil
+
+ other_plan = AddNewTrailPlans::Plan.find_by(name: 'other')
+ expect(other_plan).to be_persisted
+ expect(AddNewTrailPlans::PlanLimits.count).to eq(1)
+ expect(AddNewTrailPlans::PlanLimits.first.plan_id).to eq(other_plan.id)
+ end
+ end
+
+ context 'when the instance is not SaaS' do
+ before do
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return false
+ table(:plans).create!(id: 1, name: 'ultimate_trial', title: 'Ultimate Trial')
+ table(:plans).create!(id: 2, name: 'premium_trial', title: 'Premium Trial')
+ table(:plan_limits).create!(id: 1, plan_id: 1)
+ table(:plan_limits).create!(id: 2, plan_id: 2)
+ end
+
+ it 'does not delete plans and plan limits and returns' do
+ migrate!
+
+ expect { described_class.new.down }.not_to change { AddNewTrailPlans::Plan.count }
+ expect(AddNewTrailPlans::PlanLimits.count).to eq(3)
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/style/regexp_literal_mixed_preserve_spec.rb b/spec/rubocop/cop/style/regexp_literal_mixed_preserve_spec.rb
new file mode 100644
index 00000000000..384a834a512
--- /dev/null
+++ b/spec/rubocop/cop/style/regexp_literal_mixed_preserve_spec.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require_relative '../../../../rubocop/cop/style/regexp_literal_mixed_preserve'
+
+# This spec contains only relevant examples.
+#
+# See also https://github.com/rubocop/rubocop/pull/9688
+RSpec.describe RuboCop::Cop::Style::RegexpLiteralMixedPreserve, :config do
+ let(:config) do
+ supported_styles = { 'SupportedStyles' => %w[slashes percent_r mixed mixed_preserve] }
+ RuboCop::Config.new('Style/PercentLiteralDelimiters' =>
+ percent_literal_delimiters_config,
+ 'Style/RegexpLiteralMixedPreserve' =>
+ cop_config.merge(supported_styles))
+ end
+
+ let(:percent_literal_delimiters_config) { { 'PreferredDelimiters' => { '%r' => '{}' } } }
+
+ context 'when EnforcedStyle is set to mixed_preserve' do
+ let(:cop_config) { { 'EnforcedStyle' => 'mixed_preserve' } }
+
+ describe 'a single-line `//` regex without slashes' do
+ it 'is accepted' do
+ expect_no_offenses('foo = /a/')
+ end
+ end
+
+ describe 'a single-line `//` regex with slashes' do
+ it 'registers an offense and corrects' do
+ expect_offense(<<~'RUBY')
+ foo = /home\//
+ ^^^^^^^^ Use `%r` around regular expression.
+ RUBY
+
+ expect_correction(<<~'RUBY')
+ foo = %r{home/}
+ RUBY
+ end
+
+ describe 'when configured to allow inner slashes' do
+ before do
+ cop_config['AllowInnerSlashes'] = true
+ end
+
+ it 'is accepted' do
+ expect_no_offenses('foo = /home\\//')
+ end
+ end
+ end
+
+ describe 'a multi-line `//` regex without slashes' do
+ it 'is accepted' do
+ expect_no_offenses(<<~'RUBY')
+ foo = /
+ foo
+ bar
+ /x
+ RUBY
+ end
+ end
+
+ describe 'a multi-line `//` regex with slashes' do
+ it 'registers an offense and corrects' do
+ expect_offense(<<~'RUBY')
+ foo = /
+ ^ Use `%r` around regular expression.
+ https?:\/\/
+ example\.com
+ /x
+ RUBY
+
+ expect_correction(<<~'RUBY')
+ foo = %r{
+ https?://
+ example\.com
+ }x
+ RUBY
+ end
+ end
+
+ describe 'a single-line %r regex without slashes' do
+ it 'is accepted' do
+ expect_no_offenses(<<~RUBY)
+ foo = %r{a}
+ RUBY
+ end
+ end
+
+ describe 'a single-line %r regex with slashes' do
+ it 'is accepted' do
+ expect_no_offenses('foo = %r{home/}')
+ end
+
+ describe 'when configured to allow inner slashes' do
+ before do
+ cop_config['AllowInnerSlashes'] = true
+ end
+
+ it 'is accepted' do
+ expect_no_offenses(<<~RUBY)
+ foo = %r{home/}
+ RUBY
+ end
+ end
+ end
+
+ describe 'a multi-line %r regex without slashes' do
+ it 'is accepted' do
+ expect_no_offenses(<<~RUBY)
+ foo = %r{
+ foo
+ bar
+ }x
+ RUBY
+ end
+ end
+
+ describe 'a multi-line %r regex with slashes' do
+ it 'is accepted' do
+ expect_no_offenses(<<~RUBY)
+ foo = %r{
+ https?://
+ example\.com
+ }x
+ RUBY
+ end
+ end
+ end
+end