diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-13 21:11:25 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-13 21:11:25 +0000 |
commit | 5f36333180258e43c88b71047086150b6ca233a4 (patch) | |
tree | 62b80c1edc934309aca3f3eddde766a1c6d94c4f | |
parent | a5605d87fb839e0b1015ad9e736c44fbb2ada202 (diff) | |
download | gitlab-ce-5f36333180258e43c88b71047086150b6ca233a4.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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 Binary files differdeleted file mode 100644 index 30e932211cb..00000000000 --- a/doc/user/admin_area/img/abuse_reports_page.png +++ /dev/null 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 Binary files differnew file mode 100644 index 00000000000..bcb2aec9e64 --- /dev/null +++ b/doc/user/admin_area/img/abuse_reports_page_v13_11.png 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 Binary files differdeleted file mode 100644 index 3b23e74130a..00000000000 --- a/doc/user/analytics/img/code_review_analytics_v12_8.png +++ /dev/null diff --git a/doc/user/analytics/img/code_review_analytics_v13_11.png b/doc/user/analytics/img/code_review_analytics_v13_11.png Binary files differnew file mode 100644 index 00000000000..e337afa3ace --- /dev/null +++ b/doc/user/analytics/img/code_review_analytics_v13_11.png 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 Binary files differdeleted file mode 100644 index fccfa949779..00000000000 --- a/doc/user/analytics/img/issues_created_per_month_v12_8.png +++ /dev/null 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 Binary files differnew file mode 100644 index 00000000000..01ebde5a54d --- /dev/null +++ b/doc/user/analytics/img/issues_created_per_month_v13_11.png 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 Binary files differnew file mode 100644 index 00000000000..95e176b71b8 --- /dev/null +++ b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_11.png 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 Binary files differdeleted file mode 100644 index b2ac4f95e0d..00000000000 --- a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_6.png +++ /dev/null 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 Binary files differdeleted file mode 100644 index 0e338b99e4c..00000000000 --- a/doc/user/group/insights/img/insights_example_stacked_bar_chart.png +++ /dev/null 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 Binary files differnew file mode 100644 index 00000000000..1ef49191a13 --- /dev/null +++ b/doc/user/group/insights/img/insights_example_stacked_bar_chart_v13_11.png 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 Binary files differdeleted file mode 100644 index 9a6d6bae766..00000000000 --- a/doc/user/group/insights/img/insights_sidebar_link_v12_8.png +++ /dev/null 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 |