diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-22 06:10:55 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-22 06:10:55 +0300 |
commit | e0b6b475f203ebbe63a903328369c1363a747498 (patch) | |
tree | db5dd8ef36a7a5c81c39d94a53dd4d3ad39151fc | |
parent | 981548e28502956e47ac43c978cc36908636c265 (diff) |
Add latest changes from gitlab-org/gitlab@master
26 files changed, 511 insertions, 439 deletions
diff --git a/app/assets/javascripts/groups/components/group_name_and_path.vue b/app/assets/javascripts/groups/components/group_name_and_path.vue index 9a1ea2f1812..5f997ecc7ba 100644 --- a/app/assets/javascripts/groups/components/group_name_and_path.vue +++ b/app/assets/javascripts/groups/components/group_name_and_path.vue @@ -59,7 +59,7 @@ export default { learnMore: s__('Groups|Learn more'), }, inputSize: { md: 'lg' }, - changingGroupPathHelpPagePath: helpPagePath('user/group/index', { + changingGroupPathHelpPagePath: helpPagePath('user/group/manage', { anchor: 'change-a-groups-path', }), mattermostDataBindName: 'create_chat_team', diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 8bc78170872..5316fb9b84f 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -519,6 +519,7 @@ export default { <work-item-tree v-if="workItemType === $options.WORK_ITEM_TYPE_VALUE_OBJECTIVE" :work-item-type="workItemType" + :work-item-id="workItem.id" /> <gl-empty-state v-if="error" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue index 095ea86e0d8..22bbfd36a20 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue @@ -9,7 +9,16 @@ import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_ty import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql'; import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql'; import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql'; -import { FORM_TYPES, TASK_TYPE_NAME } from '../../constants'; +import { + FORM_TYPES, + WORK_ITEMS_TYPE_MAP, + WORK_ITEM_TYPE_ENUM_TASK, + I18N_WORK_ITEM_CREATE_BUTTON_LABEL, + I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, + I18N_WORK_ITEM_ADD_BUTTON_LABEL, + I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL, + sprintfWorkItem, +} from '../../constants'; export default { components: { @@ -52,6 +61,11 @@ export default { type: String, required: true, }, + childrenType: { + type: String, + required: false, + default: WORK_ITEM_TYPE_ENUM_TASK, + }, }, apollo: { workItemTypes: { @@ -71,7 +85,7 @@ export default { return { projectPath: this.projectPath, searchTerm: this.search?.title || this.search, - types: ['TASK'], + types: [this.childrenType], in: this.search ? 'TITLE' : undefined, }; }, @@ -79,7 +93,9 @@ export default { return !this.searchStarted; }, update(data) { - return data.workspace.workItems.nodes.filter((wi) => !this.childrenIds.includes(wi.id)); + return data.workspace.workItems.nodes.filter( + (wi) => !this.childrenIds.includes(wi.id) && this.issuableGid !== wi.id, + ); }, }, }, @@ -99,7 +115,7 @@ export default { let workItemInput = { title: this.search?.title || this.search, projectPath: this.projectPath, - workItemTypeId: this.taskWorkItemType, + workItemTypeId: this.childWorkItemType, hierarchyWidget: { parentId: this.issuableGid, }, @@ -122,19 +138,22 @@ export default { isCreateForm() { return this.formType === FORM_TYPES.create; }, + childrenTypeName() { + return WORK_ITEMS_TYPE_MAP[this.childrenType]?.name; + }, addOrCreateButtonLabel() { if (this.isCreateForm) { - return this.$options.i18n.createChildOptionLabel; + return sprintfWorkItem(I18N_WORK_ITEM_CREATE_BUTTON_LABEL, this.childrenTypeName); } else if (this.workItemsToAdd.length > 1) { - return this.$options.i18n.addTasksButtonLabel; + return sprintfWorkItem(I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL, this.childrenTypeName); } - return this.$options.i18n.addTaskButtonLabel; + return sprintfWorkItem(I18N_WORK_ITEM_ADD_BUTTON_LABEL, this.childrenTypeName); }, addOrCreateMethod() { return this.isCreateForm ? this.createChild : this.addChild; }, - taskWorkItemType() { - return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id; + childWorkItemType() { + return this.workItemTypes.find((type) => type.name === this.childrenTypeName)?.id; }, parentIterationId() { return this.parentIteration?.id; @@ -154,6 +173,9 @@ export default { isLoading() { return this.$apollo.queries.availableWorkItems.loading; }, + addInputPlaceholder() { + return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName); + }, }, created() { this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); @@ -253,17 +275,13 @@ export default { }, i18n: { inputLabel: __('Title'), - addTaskButtonLabel: s__('WorkItem|Add task'), - addTasksButtonLabel: s__('WorkItem|Add tasks'), addChildErrorMessage: s__( 'WorkItem|Something went wrong when trying to add a child. Please try again.', ), - createChildOptionLabel: s__('WorkItem|Create task'), createChildErrorMessage: s__( 'WorkItem|Something went wrong when trying to create a child. Please try again.', ), createPlaceholder: s__('WorkItem|Add a title'), - addPlaceholder: s__('WorkItem|Search existing tasks'), fieldValidationMessage: __('Maximum of 255 characters'), }, }; @@ -296,7 +314,7 @@ export default { v-model="workItemsToAdd" :dropdown-items="availableWorkItems" :loading="isLoading" - :placeholder="$options.i18n.addPlaceholder" + :placeholder="addInputPlaceholder" menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!" class="gl-mb-4" data-testid="work-item-token-select-input" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue index 49430cc4064..9c09ee3a66a 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue @@ -2,26 +2,42 @@ import { GlButton } from '@gitlab/ui'; import { s__ } from '~/locale'; -import { WORK_ITEMS_TREE_TEXT_MAP } from '../../constants'; +import { + FORM_TYPES, + WORK_ITEMS_TREE_TEXT_MAP, + WORK_ITEM_TYPE_ENUM_OBJECTIVE, + WORK_ITEM_TYPE_ENUM_KEY_RESULT, +} from '../../constants'; import OkrActionsSplitButton from './okr_actions_split_button.vue'; +import WorkItemLinksForm from './work_item_links_form.vue'; export default { + FORM_TYPES, WORK_ITEMS_TREE_TEXT_MAP, + WORK_ITEM_TYPE_ENUM_OBJECTIVE, + WORK_ITEM_TYPE_ENUM_KEY_RESULT, components: { GlButton, OkrActionsSplitButton, + WorkItemLinksForm, }, props: { workItemType: { type: String, required: true, }, + workItemId: { + type: String, + required: true, + }, }, data() { return { isShownAddForm: false, isOpen: true, error: null, + formType: null, + childType: null, }; }, computed: { @@ -36,9 +52,11 @@ export default { toggle() { this.isOpen = !this.isOpen; }, - showAddForm() { + showAddForm(formType, childType) { this.isOpen = true; this.isShownAddForm = true; + this.formType = formType; + this.childType = childType; this.$nextTick(() => { this.$refs.wiLinksForm.$refs.wiTitleInput?.$el.focus(); }); @@ -64,7 +82,20 @@ export default { {{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].title }} </h5> </div> - <okr-actions-split-button /> + <okr-actions-split-button + @showCreateObjectiveForm=" + showAddForm($options.FORM_TYPES.create, $options.WORK_ITEM_TYPE_ENUM_OBJECTIVE) + " + @showAddObjectiveForm=" + showAddForm($options.FORM_TYPES.add, $options.WORK_ITEM_TYPE_ENUM_OBJECTIVE) + " + @showCreateKeyResultForm=" + showAddForm($options.FORM_TYPES.create, $options.WORK_ITEM_TYPE_ENUM_KEY_RESULT) + " + @showAddKeyResultForm=" + showAddForm($options.FORM_TYPES.add, $options.WORK_ITEM_TYPE_ENUM_KEY_RESULT) + " + /> <div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-100 gl-pl-3 gl-ml-3"> <gl-button category="tertiary" @@ -87,6 +118,15 @@ export default { {{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].empty }} </p> </div> + <work-item-links-form + v-if="isShownAddForm" + ref="wiLinksForm" + data-testid="add-tree-form" + :issuable-gid="workItemId" + :form-type="formType" + :children-type="childType" + @cancel="hideAddForm" + /> </div> </div> </template> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 582532b51fd..cdb69586969 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -28,6 +28,7 @@ export const WORK_ITEM_TYPE_ENUM_TASK = 'TASK'; export const WORK_ITEM_TYPE_ENUM_TEST_CASE = 'TEST_CASE'; export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS'; export const WORK_ITEM_TYPE_ENUM_OBJECTIVE = 'OBJECTIVE'; +export const WORK_ITEM_TYPE_ENUM_KEY_RESULT = 'KEY_RESULT'; export const WORK_ITEM_TYPE_VALUE_OBJECTIVE = 'Objective'; @@ -64,6 +65,13 @@ export const I18N_WORK_ITEM_FETCH_ITERATIONS_ERROR = s__( 'WorkItem|Something went wrong when fetching iterations. Please try again.', ); +export const I18N_WORK_ITEM_CREATE_BUTTON_LABEL = s__('WorkItem|Create %{workItemType}'); +export const I18N_WORK_ITEM_ADD_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}'); +export const I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}s'); +export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__( + 'WorkItem|Search existing %{workItemType}s', +); + export const sprintfWorkItem = (msg, workItemTypeArg) => { const workItemType = workItemTypeArg || s__('WorkItem|Work item'); return capitalizeFirstCharacter( @@ -107,6 +115,10 @@ export const WORK_ITEMS_TYPE_MAP = { icon: `issue-type-issue`, name: s__('WorkItem|Objective'), }, + [WORK_ITEM_TYPE_ENUM_KEY_RESULT]: { + icon: `issue-type-issue`, + name: s__('WorkItem|Key result'), + }, }; export const WORK_ITEMS_TREE_TEXT_MAP = { diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js index 4fbcdfe2b96..e4d37382309 100644 --- a/app/assets/javascripts/work_items/index.js +++ b/app/assets/javascripts/work_items/index.js @@ -15,6 +15,7 @@ export const initWorkItemsRoot = () => { apolloProvider, provide: { fullPath, + projectPath: fullPath, hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature), issuesListPath, hasIterationsFeature: parseBoolean(hasIterationsFeature), diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 75193309a4e..1d05cee02d4 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -49,6 +49,14 @@ class Import::BitbucketController < Import::BaseController namespace_path = params[:new_namespace].presence || repo_owner target_namespace = find_or_create_namespace(namespace_path, current_user) + Gitlab::Tracking.event( + self.class.name, + 'create', + label: 'import_access_level', + user: current_user, + extra: { user_role: user_role(current_user, target_namespace), import_type: 'bitbucket' } + ) + if current_user.can?(:create_projects, target_namespace) # The token in a session can be expired, we need to get most recent one because # Bitbucket::Connection class refreshes it. @@ -89,6 +97,21 @@ class Import::BitbucketController < Import::BaseController private + def user_role(user, namespace) + if current_user.id == namespace&.owner_id + Gitlab::Access.options_with_owner.key(Gitlab::Access::OWNER) + else + access_level = current_user&.group_members&.find_by(source_id: namespace&.id)&.access_level + + case access_level + when nil + 'Not a member' + else + Gitlab::Access.human_access(access_level) + end + end + end + def oauth_client @oauth_client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options) end diff --git a/app/services/bulk_imports/create_service.rb b/app/services/bulk_imports/create_service.rb index d3c6dcca588..0403069e5d4 100644 --- a/app/services/bulk_imports/create_service.rb +++ b/app/services/bulk_imports/create_service.rb @@ -62,6 +62,8 @@ module BulkImports bulk_import.create_configuration!(credentials.slice(:url, :access_token)) Array.wrap(params).each do |entity| + track_access_level(entity) + BulkImports::Entity.create!( bulk_import: bulk_import, source_type: entity[:source_type], @@ -75,6 +77,34 @@ module BulkImports end end + def track_access_level(entity) + Gitlab::Tracking.event( + self.class.name, + 'create', + label: 'import_access_level', + user: current_user, + extra: { user_role: user_role(entity[:destination_namespace]), import_type: 'bulk_import_group' } + ) + end + + def user_role(destination_namespace) + namespace = Namespace.find_by_full_path(destination_namespace) + # if there is no parent namespace we assume user will be group creator/owner + return owner_role unless destination_namespace + return owner_role unless namespace + return owner_role unless namespace.group_namespace? # user namespace + + membership = current_user.group_members.find_by(source_id: namespace.id) # rubocop:disable CodeReuse/ActiveRecord + + return 'Not a member' unless membership + + Gitlab::Access.human_access(membership.access_level) + end + + def owner_role + Gitlab::Access.human_access(Gitlab::Access::OWNER) + end + def client @client ||= BulkImports::Clients::HTTP.new( url: @credentials[:url], diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb index 4092ded67bc..ac181245986 100644 --- a/app/services/groups/import_export/import_service.rb +++ b/app/services/groups/import_export/import_service.rb @@ -8,6 +8,7 @@ module Groups def initialize(group:, user:) @group = group @current_user = user + @user_role = user_role @shared = Gitlab::ImportExport::Shared.new(@group) @logger = Gitlab::Import::Logger.build end @@ -31,6 +32,14 @@ module Groups if valid_user_permissions? && import_file && restorers.all?(&:restore) notify_success + Gitlab::Tracking.event( + self.class.name, + 'create', + label: 'import_access_level', + user: current_user, + extra: { user_role: user_role, import_type: 'import_group_from_file' } + ) + group else notify_error! @@ -43,6 +52,15 @@ module Groups private + def user_role + # rubocop:disable CodeReuse/ActiveRecord, Style/MultilineTernaryOperator + access_level = group.parent ? + current_user&.group_members&.find_by(source_id: group.parent&.id)&.access_level : + Gitlab::Access::OWNER + Gitlab::Access.human_access(access_level) + # rubocop:enable CodeReuse/ActiveRecord, Style/MultilineTernaryOperator + end + def import_file @import_file ||= Gitlab::ImportExport::FileImporter.import( importable: group, diff --git a/app/services/import/base_service.rb b/app/services/import/base_service.rb index ab3e9c7abba..6b5adcbc39e 100644 --- a/app/services/import/base_service.rb +++ b/app/services/import/base_service.rb @@ -35,5 +35,30 @@ module Import def success(project) super().merge(project: project, status: :success) end + + def track_access_level(import_type) + Gitlab::Tracking.event( + self.class.name, + 'create', + label: 'import_access_level', + user: current_user, + extra: { user_role: user_role, import_type: import_type } + ) + end + + def user_role + if current_user.id == target_namespace.owner_id + 'Owner' + else + access_level = current_user&.group_members&.find_by(source_id: target_namespace.id)&.access_level + + case access_level + when nil + 'Not a member' + else + Gitlab::Access.human_access(access_level) + end + end + end end end diff --git a/app/services/import/bitbucket_server_service.rb b/app/services/import/bitbucket_server_service.rb index 20f6c987c92..f7f17f1e53e 100644 --- a/app/services/import/bitbucket_server_service.rb +++ b/app/services/import/bitbucket_server_service.rb @@ -19,6 +19,8 @@ module Import project = create_project(credentials) + track_access_level('bitbucket') + if project.persisted? success(project) elsif project.errors[:import_source_disabled].present? diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb index a60963e28c7..2378a4b11b1 100644 --- a/app/services/import/github_service.rb +++ b/app/services/import/github_service.rb @@ -13,6 +13,7 @@ module Import return context_error if context_error project = create_project(access_params, provider) + track_access_level('github') if project.persisted? store_import_settings(project) diff --git a/app/views/admin/application_settings/_kroki.html.haml b/app/views/admin/application_settings/_kroki.html.haml index 4f5a313d7b7..f1f6dd34401 100644 --- a/app/views/admin/application_settings/_kroki.html.haml +++ b/app/views/admin/application_settings/_kroki.html.haml @@ -24,7 +24,7 @@ - install_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: install_link_url } = html_escape(_('Use the public cloud instance URL (%{kroki_public_url}) or %{install_link_start}install Kroki%{install_link_end} on your own infrastructure and use your own instance URL.')) % { kroki_public_url: '<code>https://kroki.io</code>'.html_safe, install_link_start: install_link_start, install_link_end: '</a>'.html_safe } .form-group - = f.label :kroki_formats, 'Additional diagram formats', class: 'label-bold' + = f.label :kroki_formats, _('Additional diagram formats'), class: 'label-bold' .form-text.text-muted - container_link_url = 'https://docs.kroki.io/kroki/setup/install/#images' - container_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: container_link_url } diff --git a/config/feature_flags/development/graphql_keyset_pagination_without_next_page_query.yml b/config/feature_flags/development/graphql_keyset_pagination_without_next_page_query.yml deleted file mode 100644 index 7b4c884a82f..00000000000 --- a/config/feature_flags/development/graphql_keyset_pagination_without_next_page_query.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: graphql_keyset_pagination_without_next_page_query -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97509 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/373792 -milestone: '15.4' -type: development -group: group::optimize -default_enabled: true diff --git a/doc/user/project/merge_requests/reviews/index.md b/doc/user/project/merge_requests/reviews/index.md index 4c503211513..3a1e0099f0f 100644 --- a/doc/user/project/merge_requests/reviews/index.md +++ b/doc/user/project/merge_requests/reviews/index.md @@ -71,6 +71,50 @@ if you [approve a merge request](../approvals/index.md#approve-a-merge-request) are shown in the reviewer list, a green check mark **{check-circle-filled}** displays next to your name. +### Download merge request changes as a diff + +To download the changes included in a merge request as a diff: + +1. On the top bar, select **Main menu > Projects** and find your project. +1. On the left sidebar, select **Merge requests**. +1. On the top right, select **Code > Plain diff**. + +If you know the URL of the merge request, you can also download the diff from +the command line by appending `.diff` to the URL. This example downloads the diff +for merge request `000000`: + +```plaintext +https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.diff +``` + +To download and apply the diff in a one-line CLI command: + +```shell +curl "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.diff" | git apply +``` + +### Download merge request changes as a patch file + +To download the changes included in a merge request as a patch file: + +1. On the top bar, select **Main menu > Projects** and find your project. +1. On the left sidebar, select **Merge requests**. +1. On the top right, select **Code > Email patches**. + +If you know the URL of the merge request, you can also download the patch from +the command line by appending `.patch` to the URL. This example downloads the patch +file for merge request `000000`: + +```plaintext +https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.patch +``` + +To download and apply the patch in a one-line CLI command using [`git am`](https://git-scm.com/docs/git-am): + +```shell +curl "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.patch" | git am +``` + ### Submit a review You can submit your completed review in multiple ways: diff --git a/lib/gitlab/graphql/pagination/keyset/connection.rb b/lib/gitlab/graphql/pagination/keyset/connection.rb index eca4d42fb9a..208ca5f2d24 100644 --- a/lib/gitlab/graphql/pagination/keyset/connection.rb +++ b/lib/gitlab/graphql/pagination/keyset/connection.rb @@ -59,16 +59,7 @@ module Gitlab if before true elsif first - if Feature.enabled?(:graphql_keyset_pagination_without_next_page_query) - limited_nodes.size > limit_value - else - case sliced_nodes - when Array - sliced_nodes.size > limit_value - else - sliced_nodes.limit(1).offset(limit_value).exists? # rubocop: disable CodeReuse/ActiveRecord - end - end + limited_nodes.size > limit_value else false end @@ -126,15 +117,9 @@ module Gitlab @has_previous_page = paginated_nodes.count > limit_value @has_previous_page ? paginated_nodes.last(limit_value) : paginated_nodes elsif loaded?(sliced_nodes) - if Feature.enabled?(:graphql_keyset_pagination_without_next_page_query) - sliced_nodes.take(limit_value + 1) # rubocop: disable CodeReuse/ActiveRecord - else - sliced_nodes.take(limit_value) # rubocop: disable CodeReuse/ActiveRecord - end - elsif Feature.enabled?(:graphql_keyset_pagination_without_next_page_query) - sliced_nodes.limit(limit_value + 1).to_a + sliced_nodes.take(limit_value + 1) # rubocop: disable CodeReuse/ActiveRecord else - sliced_nodes.limit(limit_value) + sliced_nodes.limit(limit_value + 1).to_a end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c96477f64f8..55858daee9d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2464,6 +2464,9 @@ msgstr "" msgid "Adding new applications is disabled in your GitLab instance. Please contact your GitLab administrator to get the permission" msgstr "" +msgid "Additional diagram formats" +msgstr "" + msgid "Additional minutes" msgstr "" @@ -46324,6 +46327,12 @@ msgstr "" msgid "WorkItem|Add" msgstr "" +msgid "WorkItem|Add %{workItemType}" +msgstr "" + +msgid "WorkItem|Add %{workItemType}s" +msgstr "" + msgid "WorkItem|Add a title" msgstr "" @@ -46339,12 +46348,6 @@ msgstr "" msgid "WorkItem|Add start date" msgstr "" -msgid "WorkItem|Add task" -msgstr "" - -msgid "WorkItem|Add tasks" -msgstr "" - msgid "WorkItem|Add to iteration" msgstr "" @@ -46377,6 +46380,9 @@ msgstr "" msgid "WorkItem|Collapse tasks" msgstr "" +msgid "WorkItem|Create %{workItemType}" +msgstr "" + msgid "WorkItem|Create objective" msgstr "" @@ -46413,6 +46419,9 @@ msgstr "" msgid "WorkItem|Iteration" msgstr "" +msgid "WorkItem|Key result" +msgstr "" + msgid "WorkItem|Learn about tasks." msgstr "" @@ -46458,7 +46467,7 @@ msgstr "" msgid "WorkItem|Requirements" msgstr "" -msgid "WorkItem|Search existing tasks" +msgid "WorkItem|Search existing %{workItemType}s" msgstr "" msgid "WorkItem|Select type" diff --git a/qa/README.md b/qa/README.md index 564beb4c6e8..a0560e1f965 100644 --- a/qa/README.md +++ b/qa/README.md @@ -241,9 +241,8 @@ feature flag ([via the API](https://docs.gitlab.com/ee/api/features.html)) if no run all the tests in the `Test::Instance::All` scenario, and then enable the feature flag again if it was enabled earlier. -Note: the QA framework doesn't currently allow you to easily toggle a feature -flag during a single test, [as you can in unit tests](https://docs.gitlab.com/ee/development/feature_flags/index.html), -but [that capability is planned](https://gitlab.com/gitlab-org/quality/team-tasks/issues/77). +Note: You can also [toggle feature +flags in the tests themselves](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/feature_flags.html). Note also that the `--` separator isn't used because `--enable-feature` and `--disable-feature` are QA framework options, not `rspec` options. diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index e73e61b6ec5..35f712dc50d 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -185,6 +185,14 @@ RSpec.describe Import::BitbucketController do post :create, format: :json + expect_snowplow_event( + category: 'Import::BitbucketController', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Owner', import_type: 'bitbucket' } + ) + expect(response).to have_gitlab_http_status(:ok) end @@ -297,6 +305,14 @@ RSpec.describe Import::BitbucketController do .to receive(:new).and_return(double(execute: project)) expect { post :create, format: :json }.not_to change(Namespace, :count) + + expect_snowplow_event( + category: 'Import::BitbucketController', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Owner', import_type: 'bitbucket' } + ) end it "takes the current user's namespace" do @@ -417,6 +433,14 @@ RSpec.describe Import::BitbucketController do post :create, params: { target_namespace: other_namespace.name }, format: :json expect(response).to have_gitlab_http_status(:unprocessable_entity) + + expect_snowplow_event( + category: 'Import::BitbucketController', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Not a member', import_type: 'bitbucket' } + ) end end end diff --git a/spec/frontend/groups/components/group_name_and_path_spec.js b/spec/frontend/groups/components/group_name_and_path_spec.js index 823d2ed286a..9965b608f27 100644 --- a/spec/frontend/groups/components/group_name_and_path_spec.js +++ b/spec/frontend/groups/components/group_name_and_path_spec.js @@ -398,7 +398,7 @@ describe('GroupNameAndPath', () => { expect(findAlert().exists()).toBe(true); expect(findAlert().findByRole('link', { name: 'Learn more' }).attributes('href')).toBe( - helpPagePath('user/group/index', { + helpPagePath('user/group/manage', { anchor: 'change-a-groups-path', }), ); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js index 0c5f2af1209..9c1e9ccb6e8 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js @@ -1,7 +1,13 @@ import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue'; +import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue'; import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue'; +import { + FORM_TYPES, + WORK_ITEM_TYPE_ENUM_OBJECTIVE, + WORK_ITEM_TYPE_ENUM_KEY_RESULT, +} from '~/work_items/constants'; describe('WorkItemTree', () => { let wrapper; @@ -10,10 +16,11 @@ describe('WorkItemTree', () => { const findTreeBody = () => wrapper.findByTestId('tree-body'); const findEmptyState = () => wrapper.findByTestId('tree-empty'); const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton); + const findForm = () => wrapper.findComponent(WorkItemLinksForm); const createComponent = () => { wrapper = shallowMountExtended(WorkItemTree, { - propsData: { workItemType: 'Objective' }, + propsData: { workItemType: 'Objective', workItemId: 'gid://gitlab/WorkItem/515' }, }); }; @@ -42,4 +49,26 @@ describe('WorkItemTree', () => { it('displays empty state if there are no children', () => { expect(findEmptyState().exists()).toBe(true); }); + + it('does not display form by default', () => { + expect(findForm().exists()).toBe(false); + }); + + it.each` + option | event | formType | childType + ${'New objective'} | ${'showCreateObjectiveForm'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE} + ${'Existing objective'} | ${'showAddObjectiveForm'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE} + ${'New key result'} | ${'showCreateKeyResultForm'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT} + ${'Existing key result'} | ${'showAddKeyResultForm'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT} + `( + 'when selecting $option from split button, renders the form passing $formType and $childType', + async ({ event, formType, childType }) => { + findToggleFormSplitButton().vm.$emit(event); + await nextTick(); + + expect(findForm().exists()).toBe(true); + expect(findForm().props('formType')).toBe(formType); + expect(findForm().props('childrenType')).toBe(childType); + }, + ); }); diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb index 1124868bdae..773df9b20ee 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb @@ -50,17 +50,16 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do end before do - stub_feature_flags(graphql_keyset_pagination_without_next_page_query: false) allow(GitlabSchema).to receive(:default_max_page_size).and_return(2) end - it 'invokes an extra query for the next page check' do + it 'invokes no an extra query for the next page check' do arguments[:first] = 1 subject.nodes count = ActiveRecord::QueryRecorder.new { subject.has_next_page }.count - expect(count).to eq(1) + expect(count).to eq(0) end context 'when the relation is loaded' do @@ -438,382 +437,4 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do end end end - - # duplicated tests, remove with the removal of the graphql_keyset_pagination_without_next_page_query FF - context 'when the graphql_keyset_pagination_without_next_page_query is on' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - - before do - stub_feature_flags(graphql_keyset_pagination_without_next_page_query: true) - end - - it 'does not invoke an extra query for the next page check' do - arguments[:first] = 1 - - subject.nodes - - count = ActiveRecord::QueryRecorder.new { subject.has_next_page }.count - expect(count).to eq(0) - end - - it_behaves_like 'a connection with collection methods' - - it_behaves_like 'a redactable connection' do - let_it_be(:projects) { create_list(:project, 2) } - let(:unwanted) { projects.second } - end - - describe '#cursor_for' do - let(:project) { create(:project) } - let(:cursor) { connection.cursor_for(project) } - - it 'returns an encoded ID' do - expect(decoded_cursor(cursor)).to eq('id' => project.id.to_s) - end - - context 'when an order is specified' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - - it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('id' => project.id.to_s) - end - end - - context 'when multiple orders are specified' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_updated_at, column_order_created_at, column_order_id])) } - - it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) - end - end - end - - describe '#sliced_nodes' do - let(:projects) { create_list(:project, 4) } - - context 'when before is passed' do - let(:arguments) { { before: encoded_cursor(projects[1]) } } - - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(projects.first) - end - - context 'when the sort order is descending' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) } - - it 'returns the correct nodes' do - expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) - end - end - end - - context 'when after is passed' do - let(:arguments) { { after: encoded_cursor(projects[1]) } } - - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) - end - - context 'when the sort order is descending' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) } - - it 'returns the correct nodes' do - expect(subject.sliced_nodes).to contain_exactly(projects.first) - end - end - end - - context 'when both before and after are passed' do - let(:arguments) do - { - after: encoded_cursor(projects[1]), - before: encoded_cursor(projects[3]) - } - end - - it 'returns the expected set' do - expect(subject.sliced_nodes).to contain_exactly(projects[2]) - end - end - - shared_examples 'nodes are in ascending order' do - context 'when no cursor is passed' do - let(:arguments) { {} } - - it 'returns projects in ascending order' do - expect(subject.sliced_nodes).to eq(ascending_nodes) - end - end - - context 'when before cursor value is not NULL' do - let(:arguments) { { before: encoded_cursor(ascending_nodes[2]) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes.first(2)) - end - end - - context 'when after cursor value is not NULL' do - let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes.last(3)) - end - end - - context 'when before and after cursor' do - let(:arguments) { { before: encoded_cursor(ascending_nodes.last), after: encoded_cursor(ascending_nodes.first) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes[1..3]) - end - end - end - - shared_examples 'nodes are in descending order' do - context 'when no cursor is passed' do - let(:arguments) { {} } - - it 'only returns projects in descending order' do - expect(subject.sliced_nodes).to eq(descending_nodes) - end - end - - context 'when before cursor value is not NULL' do - let(:arguments) { { before: encoded_cursor(descending_nodes[2]) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes.first(2)) - end - end - - context 'when after cursor value is not NULL' do - let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes.last(3)) - end - end - - context 'when before and after cursor' do - let(:arguments) { { before: encoded_cursor(descending_nodes.last), after: encoded_cursor(descending_nodes.first) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes[1..3]) - end - end - end - - context 'when multiple orders with nil values are defined' do - let_it_be(:project1) { create(:project, last_repository_check_at: 10.days.ago) } # Asc: project5 Desc: project3 - let_it_be(:project2) { create(:project, last_repository_check_at: nil) } # Asc: project1 Desc: project1 - let_it_be(:project3) { create(:project, last_repository_check_at: 5.days.ago) } # Asc: project3 Desc: project5 - let_it_be(:project4) { create(:project, last_repository_check_at: nil) } # Asc: project2 Desc: project2 - let_it_be(:project5) { create(:project, last_repository_check_at: 20.days.ago) } # Asc: project4 Desc: project4 - - context 'when ascending' do - let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo, column_order_id]) } - let_it_be(:nodes) { Project.order(order) } - let_it_be(:ascending_nodes) { [project5, project1, project3, project2, project4] } - - it_behaves_like 'nodes are in ascending order' - - context 'when before cursor value is NULL' do - let(:arguments) { { before: encoded_cursor(project4) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq([project5, project1, project3, project2]) - end - end - - context 'when after cursor value is NULL' do - let(:arguments) { { after: encoded_cursor(project2) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq([project4]) - end - end - end - - context 'when descending' do - let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo_desc, column_order_id]) } - let_it_be(:nodes) { Project.order(order) } - let_it_be(:descending_nodes) { [project3, project1, project5, project2, project4] } - - it_behaves_like 'nodes are in descending order' - - context 'when before cursor value is NULL' do - let(:arguments) { { before: encoded_cursor(project4) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq([project3, project1, project5, project2]) - end - end - - context 'when after cursor value is NULL' do - let(:arguments) { { after: encoded_cursor(project2) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq([project4]) - end - end - end - end - - context 'when ordering by similarity' do - let_it_be(:project1) { create(:project, name: 'test') } - let_it_be(:project2) { create(:project, name: 'testing') } - let_it_be(:project3) { create(:project, name: 'tests') } - let_it_be(:project4) { create(:project, name: 'testing stuff') } - let_it_be(:project5) { create(:project, name: 'test') } - - let_it_be(:nodes) do - # Note: sorted_by_similarity_desc scope internally supports the generic keyset order. - Project.sorted_by_similarity_desc('test', include_in_select: true) - end - - let_it_be(:descending_nodes) { nodes.to_a } - - it_behaves_like 'nodes are in descending order' - end - - context 'when an invalid cursor is provided' do - let(:arguments) { { before: Base64Bp.urlsafe_encode64('invalidcursor', padding: false) } } - - it 'raises an error' do - expect { subject.sliced_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) - end - end - end - - describe '#nodes' do - let_it_be(:all_nodes) { create_list(:project, 5) } - - let(:paged_nodes) { subject.nodes } - - it_behaves_like 'connection with paged nodes' do - let(:paged_nodes_size) { 3 } - end - - context 'when both are passed' do - let(:arguments) { { first: 2, last: 2 } } - - it 'raises an error' do - expect { paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) - end - end - - context 'when primary key is not in original order' do - let(:nodes) { Project.order(last_repository_check_at: :desc) } - - it 'is added to end' do - sliced = subject.sliced_nodes - - order_sql = sliced.order_values.last.to_sql - - expect(order_sql).to end_with(Project.arel_table[:id].desc.to_sql) - end - end - - context 'when there is no primary key' do - before do - stub_const('NoPrimaryKey', Class.new(ActiveRecord::Base)) - NoPrimaryKey.class_eval do - self.table_name = 'no_primary_key' - self.primary_key = nil - end - end - - let(:nodes) { NoPrimaryKey.all } - - it 'raises an error' do - expect(NoPrimaryKey.primary_key).to be_nil - expect { subject.sliced_nodes }.to raise_error(ArgumentError, 'Relation must have a primary key') - end - end - end - - describe '#has_previous_page and #has_next_page' do - # using a list of 5 items with a max_page of 3 - let_it_be(:project_list) { create_list(:project, 5) } - let_it_be(:nodes) { Project.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - - context 'when default query' do - let(:arguments) { {} } - - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - end - end - - context 'when before is first item' do - let(:arguments) { { before: encoded_cursor(project_list.first) } } - - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - end - end - - describe 'using `before`' do - context 'when before is the last item' do - let(:arguments) { { before: encoded_cursor(project_list.last) } } - - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - end - end - - context 'when before and last specified' do - let(:arguments) { { before: encoded_cursor(project_list.last), last: 2 } } - - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy - end - end - - context 'when before and last does request all remaining nodes' do - let(:arguments) { { before: encoded_cursor(project_list[1]), last: 3 } } - - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - expect(subject.nodes).to eq [project_list[0]] - end - end - end - - describe 'using `after`' do - context 'when after is the first item' do - let(:arguments) { { after: encoded_cursor(project_list.first) } } - - it 'has a previous, and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy - end - end - - context 'when after and first specified' do - let(:arguments) { { after: encoded_cursor(project_list.first), first: 2 } } - - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy - end - end - - context 'when before and last does request all remaining nodes' do - let(:arguments) { { after: encoded_cursor(project_list[2]), last: 3 } } - - it 'has a previous but no next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_falsey - end - end - end - end - end end diff --git a/spec/services/bulk_imports/create_service_spec.rb b/spec/services/bulk_imports/create_service_spec.rb index bf174f5d5a2..cb7d51ce7b4 100644 --- a/spec/services/bulk_imports/create_service_spec.rb +++ b/spec/services/bulk_imports/create_service_spec.rb @@ -5,6 +5,8 @@ require 'spec_helper' RSpec.describe BulkImports::CreateService do let(:user) { create(:user) } let(:credentials) { { url: 'http://gitlab.example', access_token: 'token' } } + let(:destination_group) { create(:group, path: 'destination1') } + let_it_be(:parent_group) { create(:group, path: 'parent-group') } let(:params) do [ { @@ -43,6 +45,7 @@ RSpec.describe BulkImports::CreateService do end it 'creates bulk import' do + parent_group.add_owner(user) expect { subject.execute }.to change { BulkImport.count }.by(1) last_bulk_import = BulkImport.last @@ -50,11 +53,20 @@ RSpec.describe BulkImports::CreateService do expect(last_bulk_import.user).to eq(user) expect(last_bulk_import.source_version).to eq(source_version.to_s) expect(last_bulk_import.user).to eq(user) + expect_snowplow_event( category: 'BulkImports::CreateService', action: 'create', label: 'bulk_import_group' ) + + expect_snowplow_event( + category: 'BulkImports::CreateService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Owner', import_type: 'bulk_import_group' } + ) end it 'creates bulk import entities' do @@ -87,5 +99,109 @@ RSpec.describe BulkImports::CreateService do expect(result).to be_error expect(result.message).to eq("Validation failed: Source full path can't be blank") end + + describe '#user-role' do + context 'when there is a parent_namespace and the user is a member' do + let(:group2) { create(:group, path: 'destination200', source_id: parent_group.id ) } + let(:params) do + [ + { + source_type: 'group_entity', + source_full_path: 'full/path/to/group1', + destination_slug: 'destination200', + destination_namespace: 'parent-group' + } + ] + end + + it 'defines access_level from parent namespace membership' do + parent_group.add_guest(user) + subject.execute + + expect_snowplow_event( + category: 'BulkImports::CreateService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Guest', import_type: 'bulk_import_group' } + ) + end + end + + context 'when there is a parent_namespace and the user is not a member' do + let(:params) do + [ + { + source_type: 'group_entity', + source_full_path: 'full/path/to/group1', + destination_slug: 'destination-group-1', + destination_namespace: 'parent-group' + } + ] + end + + it 'defines access_level as not a member' do + subject.execute + expect_snowplow_event( + category: 'BulkImports::CreateService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Not a member', import_type: 'bulk_import_group' } + ) + end + end + + context 'when there is a destination_namespace but no parent_namespace' do + let(:params) do + [ + { + source_type: 'group_entity', + source_full_path: 'full/path/to/group1', + destination_slug: 'destination-group-1', + destination_namespace: 'destination1' + } + ] + end + + it 'defines access_level from destination_namespace' do + destination_group.add_developer(user) + subject.execute + + expect_snowplow_event( + category: 'BulkImports::CreateService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Developer', import_type: 'bulk_import_group' } + ) + end + end + + context 'when there is no destination_namespace or parent_namespace' do + let(:params) do + [ + { + source_type: 'group_entity', + source_full_path: 'full/path/to/group1', + destination_slug: 'destinationational mcdestiny', + destination_namespace: 'destinational-mcdestiny' + } + ] + end + + it 'defines access_level as owner' do + subject.execute + + expect_snowplow_event( + category: 'BulkImports::CreateService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Owner', import_type: 'bulk_import_group' } + ) + end + end + end end end diff --git a/spec/services/groups/import_export/import_service_spec.rb b/spec/services/groups/import_export/import_service_spec.rb index 66b50704939..d41acbcc2de 100644 --- a/spec/services/groups/import_export/import_service_spec.rb +++ b/spec/services/groups/import_export/import_service_spec.rb @@ -148,6 +148,14 @@ RSpec.describe Groups::ImportExport::ImportService do action: 'create', label: 'import_group_from_file' ) + + expect_snowplow_event( + category: 'Groups::ImportExport::ImportService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Owner', import_type: 'import_group_from_file' } + ) end it 'removes import file' do @@ -235,6 +243,14 @@ RSpec.describe Groups::ImportExport::ImportService do ) service.execute + + expect_snowplow_event( + category: 'Groups::ImportExport::ImportService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Owner', import_type: 'import_group_from_file' } + ) end end end @@ -275,6 +291,14 @@ RSpec.describe Groups::ImportExport::ImportService do action: 'create', label: 'import_group_from_file' ) + + expect_snowplow_event( + category: 'Groups::ImportExport::ImportService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Owner', import_type: 'import_group_from_file' } + ) end it 'removes import file' do @@ -352,6 +376,24 @@ RSpec.describe Groups::ImportExport::ImportService do expect(service.execute).to be_truthy end + it 'tracks the event' do + service.execute + + expect_snowplow_event( + category: 'Groups::ImportExport::ImportService', + action: 'create', + label: 'import_group_from_file' + ) + + expect_snowplow_event( + category: 'Groups::ImportExport::ImportService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Owner', import_type: 'import_group_from_file' } + ) + end + it 'logs the import success' do allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger) diff --git a/spec/services/import/bitbucket_server_service_spec.rb b/spec/services/import/bitbucket_server_service_spec.rb index 0b9fe10e95a..555812ca9cf 100644 --- a/spec/services/import/bitbucket_server_service_spec.rb +++ b/spec/services/import/bitbucket_server_service_spec.rb @@ -31,6 +31,25 @@ RSpec.describe Import::BitbucketServerService do allow(subject).to receive(:authorized?).and_return(true) end + context 'execute' do + before do + allow(subject).to receive(:authorized?).and_return(true) + allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(repo)) + end + + it 'tracks an access level event' do + subject.execute(credentials) + + expect_snowplow_event( + category: 'Import::BitbucketServerService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { import_type: 'bitbucket', user_role: 'Owner' } + ) + end + end + context 'when no repo is found' do before do allow(subject).to receive(:authorized?).and_return(true) diff --git a/spec/services/import/github_service_spec.rb b/spec/services/import/github_service_spec.rb index 38d84009f08..d1b372c5e87 100644 --- a/spec/services/import/github_service_spec.rb +++ b/spec/services/import/github_service_spec.rb @@ -82,9 +82,16 @@ RSpec.describe Import::GithubService do end context 'when there is no repository size limit defined' do - it 'skips the check and succeeds' do + it 'skips the check, succeeds, and tracks an access level' do expect(subject.execute(access_params, :github)).to include(status: :success) expect(settings).to have_received(:write).with(nil) + expect_snowplow_event( + category: 'Import::GithubService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { import_type: 'github', user_role: 'Owner' } + ) end end @@ -98,6 +105,13 @@ RSpec.describe Import::GithubService do it 'succeeds when the repository is smaller than the limit' do expect(subject.execute(access_params, :github)).to include(status: :success) expect(settings).to have_received(:write).with(nil) + expect_snowplow_event( + category: 'Import::GithubService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { import_type: 'github', user_role: 'Not a member' } + ) end it 'returns error when the repository is larger than the limit' do @@ -118,6 +132,13 @@ RSpec.describe Import::GithubService do it 'succeeds when the repository is smaller than the limit' do expect(subject.execute(access_params, :github)).to include(status: :success) expect(settings).to have_received(:write).with(nil) + expect_snowplow_event( + category: 'Import::GithubService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { import_type: 'github', user_role: 'Owner' } + ) end it 'returns error when the repository is larger than the limit' do |