summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-03-01 18:07:43 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-03-01 18:07:43 +0000
commitad1e76fb4d1392c890c8b5e218a256a416d5a50b (patch)
tree51e5541bb1f1a799e288701bc1170a3b1a9a7393
parent8b1036168b0d395c379cbbaf457e256860147405 (diff)
downloadgitlab-ce-ad1e76fb4d1392c890c8b5e218a256a416d5a50b.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/database.gitlab-ci.yml7
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_observability.js2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue5
-rw-r--r--app/assets/javascripts/diffs/index.js2
-rw-r--r--app/assets/javascripts/diffs/store/actions.js2
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue41
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue18
-rw-r--r--app/graphql/mutations/issues/bulk_update.rb23
-rw-r--r--app/graphql/types/issuable_subscription_event_enum.rb11
-rw-r--r--app/helpers/merge_requests_helper.rb1
-rw-r--r--app/mailers/emails/issues.rb13
-rw-r--r--app/mailers/emails/shared.rb20
-rw-r--r--app/mailers/emails/work_items.rb4
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/models/group.rb1
-rw-r--r--app/models/members/member_role.rb11
-rw-r--r--app/services/clusters/agent_tokens/create_service.rb4
-rw-r--r--app/services/clusters/agent_tokens/revoke_service.rb13
-rw-r--r--app/services/clusters/agents/create_activity_event_service.rb4
-rw-r--r--app/services/work_items/export_csv_service.rb2
-rw-r--r--app/views/notify/_issuable_csv_export.html.haml6
-rw-r--r--app/views/notify/_issuable_csv_export.text.erb7
-rw-r--r--app/views/notify/export_work_items_csv_email.html.haml1
-rw-r--r--app/views/notify/export_work_items_csv_email.text.erb1
-rw-r--r--app/views/notify/issues_csv_email.text.erb6
-rw-r--r--app/views/notify/merge_requests_csv_email.text.erb6
-rw-r--r--app/views/projects/artifacts/browse.html.haml2
-rw-r--r--app/views/shared/icons/_mr_widget_empty_state.svg1
-rw-r--r--config/application.rb2
-rw-r--r--doc/api/graphql/reference/index.md17
-rw-r--r--doc/api/product_analytics.md3
-rw-r--r--doc/user/product_analytics/index.md4
-rw-r--r--lib/tasks/gitlab/tw/codeowners.rake3
-rw-r--r--locale/gitlab.pot13
-rw-r--r--spec/features/markdown/observability_spec.rb2
-rw-r--r--spec/frontend/behaviors/markdown/render_observability_spec.js4
-rw-r--r--spec/frontend/clusters/forms/components/integration_form_spec.js31
-rw-r--r--spec/frontend/diffs/components/app_spec.js1
-rw-r--r--spec/frontend/diffs/store/actions_spec.js4
-rw-r--r--spec/frontend/invite_members/components/invite_groups_modal_spec.js12
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js27
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js4
-rw-r--r--spec/mailers/emails/issues_spec.rb36
-rw-r--r--spec/mailers/emails/work_items_spec.rb17
-rw-r--r--spec/models/members/member_role_spec.rb24
-rw-r--r--spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb68
-rw-r--r--spec/services/clusters/agent_tokens/revoke_service_spec.rb32
-rw-r--r--spec/services/clusters/agents/create_activity_event_service_spec.rb13
-rw-r--r--spec/services/work_items/export_csv_service_spec.rb5
-rw-r--r--spec/support/shared_examples/mailers/export_csv_shared_examples.rb37
-rw-r--r--workhorse/go.mod2
-rw-r--r--workhorse/go.sum4
55 files changed, 402 insertions, 208 deletions
diff --git a/.gitlab/ci/database.gitlab-ci.yml b/.gitlab/ci/database.gitlab-ci.yml
index 6fd9ef62953..941cb9224fb 100644
--- a/.gitlab/ci/database.gitlab-ci.yml
+++ b/.gitlab/ci/database.gitlab-ci.yml
@@ -36,8 +36,8 @@ db:rollback:
- .db-job-base
- .rails:rules:db-rollback
script:
- - scripts/db_tasks db:migrate VERSION=20220502173045 # 14.10 (last 14.x version)
- - scripts/db_tasks db:migrate
+ - bundle exec rake db:migrate VERSION=20220502173045 # 14.10 (last 14.x version)
+ - bundle exec rake db:migrate
db:rollback single-db:
extends:
@@ -61,8 +61,7 @@ db:check-schema:
- .db-job-base
- .rails:rules:ee-mr-and-default-branch-only
script:
- - run_timed_command "bundle exec rake db:drop db:create"
- - run_timed_command "scripts/db_tasks db:migrate"
+ - run_timed_command "bundle exec rake db:drop db:create db:migrate"
db:check-schema-single-db:
extends:
diff --git a/app/assets/javascripts/behaviors/markdown/render_observability.js b/app/assets/javascripts/behaviors/markdown/render_observability.js
index 704d85cf22e..85aae6f53c4 100644
--- a/app/assets/javascripts/behaviors/markdown/render_observability.js
+++ b/app/assets/javascripts/behaviors/markdown/render_observability.js
@@ -3,7 +3,7 @@ import { darkModeEnabled } from '~/lib/utils/color_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
export function getFrameSrc(url) {
- return `${setUrlParams({ theme: darkModeEnabled() ? 'dark' : 'light' }, url)}&kiosk`;
+ return `${setUrlParams({ theme: darkModeEnabled() ? 'dark' : 'light' }, url)}&kiosk=inline-embed`;
}
const mountVueComponent = (element) => {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 2b5670bd9c7..9a6b36ad2e8 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -95,6 +95,10 @@ export default {
type: String,
required: true,
},
+ endpointDiffForPath: {
+ type: String,
+ required: true,
+ },
endpointCoverage: {
type: String,
required: false,
@@ -322,6 +326,7 @@ export default {
endpoint: this.endpoint,
endpointMetadata: this.endpointMetadata,
endpointBatch: this.endpointBatch,
+ endpointDiffForPath: this.endpointDiffForPath,
endpointCoverage: this.endpointCoverage,
endpointUpdateUser: this.endpointUpdateUser,
projectPath: this.projectPath,
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 7da5ef54b80..f6865056b05 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -25,6 +25,7 @@ export default function initDiffsApp(store = notesStore) {
endpoint: dataset.endpoint,
endpointMetadata: dataset.endpointMetadata || '',
endpointBatch: dataset.endpointBatch || '',
+ endpointDiffForPath: dataset.endpointDiffForPath || '',
endpointCoverage: dataset.endpointCoverage || '',
endpointCodequality: dataset.endpointCodequality || '',
endpointUpdateUser: dataset.updateCurrentUserPath,
@@ -86,6 +87,7 @@ export default function initDiffsApp(store = notesStore) {
endpoint: this.endpoint,
endpointMetadata: this.endpointMetadata,
endpointBatch: this.endpointBatch,
+ endpointDiffForPath: this.endpointDiffForPath,
endpointCoverage: this.endpointCoverage,
endpointCodequality: this.endpointCodequality,
endpointUpdateUser: this.endpointUpdateUser,
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index e06c15bc1b4..561e2a4f134 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -69,6 +69,7 @@ export const setBaseConfig = ({ commit }, options) => {
endpoint,
endpointMetadata,
endpointBatch,
+ endpointDiffForPath,
endpointCoverage,
endpointUpdateUser,
projectPath,
@@ -82,6 +83,7 @@ export const setBaseConfig = ({ commit }, options) => {
endpoint,
endpointMetadata,
endpointBatch,
+ endpointDiffForPath,
endpointCoverage,
endpointUpdateUser,
projectPath,
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 329db1fe2cf..593c28f20ec 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -16,6 +16,7 @@ export default () => ({
removedLines: null,
endpoint: '',
endpointUpdateUser: '',
+ endpointDiffForPath: '',
basePath: '',
commit: null,
startVersion: null, // Null unless a target diff is selected for comparison that is not the "base" diff
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index d2b798245fc..04f08e86955 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -33,6 +33,7 @@ export default {
endpoint,
endpointMetadata,
endpointBatch,
+ endpointDiffForPath,
endpointCoverage,
endpointUpdateUser,
projectPath,
@@ -46,6 +47,7 @@ export default {
endpoint,
endpointMetadata,
endpointBatch,
+ endpointDiffForPath,
endpointCoverage,
endpointUpdateUser,
projectPath,
diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
index 1e3b6093f0b..20dc32b3c9b 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -1,5 +1,14 @@
<script>
-import { GlFormGroup, GlFormSelect, GlModal, GlDatepicker, GlLink, GlSprintf } from '@gitlab/ui';
+import {
+ GlFormGroup,
+ GlFormSelect,
+ GlModal,
+ GlDatepicker,
+ GlLink,
+ GlSprintf,
+ GlButton,
+} from '@gitlab/ui';
+
import Tracking from '~/tracking';
import { sprintf } from '~/locale';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
@@ -33,6 +42,7 @@ export default {
GlLink,
GlModal,
GlSprintf,
+ GlButton,
ContentTransition,
},
mixins: [Tracking.mixin()],
@@ -246,13 +256,10 @@ export default {
data-qa-selector="invite_members_modal_content"
data-testid="invite-modal"
size="sm"
+ dialog-class="gl-mx-5"
:title="modalTitle"
:header-close-label="$options.HEADER_CLOSE_LABEL"
- :action-primary="actionPrimary"
- :action-cancel="actionCancel"
@shown="onShowModal"
- @primary="onSubmit"
- @cancel="onCancel"
@close="onClose"
@hidden="onReset"
>
@@ -330,5 +337,29 @@ export default {
<slot :name="key"></slot>
</template>
</content-transition>
+
+ <template #modal-footer>
+ <div
+ class="gl-m-0 gl-xs-w-full gl-display-flex gl-xs-flex-direction-column! gl-flex-direction-row-reverse"
+ >
+ <gl-button
+ class="gl-xs-w-full gl-xs-mb-3! gl-sm-ml-3!"
+ data-testid="invite-modal-submit"
+ v-bind="actionPrimary.attributes"
+ @click="onSubmit"
+ >
+ {{ actionPrimary.text }}
+ </gl-button>
+
+ <gl-button
+ class="gl-xs-w-full"
+ data-testid="invite-modal-cancel"
+ v-bind="actionCancel.attributes"
+ @click="onCancel"
+ >
+ {{ actionCancel.text }}
+ </gl-button>
+ </div>
+ </template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 850a4e2fd56..ef9fefeea70 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -1,7 +1,6 @@
<script>
import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
+import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/merge_requests.svg?url';
import api from '~/api';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -12,25 +11,19 @@ export default {
GlSprintf,
GlLink,
},
- directives: {
- SafeHtml,
- },
props: {
mr: {
type: Object,
required: true,
},
},
- data() {
- return { emptyStateSVG };
- },
methods: {
onClickNewFile() {
api.trackRedisHllUserEvent('i_code_review_widget_nothing_merge_click_new_file');
},
},
ciHelpPage: helpPagePath('ci/quick_start/index.html'),
- safeHtmlConfig: { ADD_TAGS: ['use'] },
+ EMPTY_STATE_SVG_URL,
};
</script>
@@ -38,9 +31,12 @@ export default {
<div class="mr-widget-body mr-widget-empty-state">
<div class="row">
<div
- class="artwork col-md-5 order-md-last col-12 text-center d-flex justify-content-center align-items-center"
+ class="col-md-5 order-md-last col-12 text-center d-flex justify-content-center align-items-center svg-content svg-250 pb-0"
>
- <span v-safe-html:[$options.safeHtmlConfig]="emptyStateSVG"></span>
+ <img
+ :alt="s__('mrWidgetNothingToMerge|This merge request contains no changes.')"
+ :src="$options.EMPTY_STATE_SVG_URL"
+ />
</div>
<div class="text col-md-7 order-md-first col-12">
<p class="highlight">
diff --git a/app/graphql/mutations/issues/bulk_update.rb b/app/graphql/mutations/issues/bulk_update.rb
index 3d80f119079..9c9dd3cf2fc 100644
--- a/app/graphql/mutations/issues/bulk_update.rb
+++ b/app/graphql/mutations/issues/bulk_update.rb
@@ -14,7 +14,8 @@ module Mutations
argument :parent_id, ::Types::GlobalIDType[::IssueParent],
required: true,
- description: 'Global ID of the parent that the bulk update will be scoped to . ' \
+ description: 'Global ID of the parent to which the bulk update will be scoped. ' \
+ 'The parent can be a project **(FREE)** or a group **(PREMIUM)**. ' \
'Example `IssueParentID` are `"gid://gitlab/Project/1"` and `"gid://gitlab/Group/1"`.'
argument :ids, [::Types::GlobalIDType[::Issue]],
@@ -31,6 +32,22 @@ module Mutations
required: false,
description: 'Global ID of the milestone that will be assigned to the issues.'
+ argument :state_event, Types::IssueStateEventEnum,
+ description: 'Close or reopen an issue.',
+ required: false
+
+ argument :add_label_ids, [::Types::GlobalIDType[::Label]],
+ description: 'Global ID array of the labels that will be added to the issues. ',
+ required: false
+
+ argument :remove_label_ids, [::Types::GlobalIDType[::Label]],
+ description: 'Global ID array of the labels that will be removed from the issues. ',
+ required: false
+
+ argument :subscription_event, Types::IssuableSubscriptionEventEnum,
+ description: 'Subscribe to or unsubscribe from issue notifications.',
+ required: false
+
field :updated_issue_count, GraphQL::Types::Int,
null: true,
description: 'Number of issues that were successfully updated.'
@@ -74,7 +91,7 @@ module Mutations
end
def prepared_params(attributes, ids)
- prepared = { issuable_ids: model_ids_from(ids).uniq }
+ prepared = attributes.except(*global_id_arguments).merge(issuable_ids: model_ids_from(ids).uniq)
global_id_arguments.each do |argument|
next unless attributes.key?(argument)
@@ -92,7 +109,7 @@ module Mutations
end
def global_id_arguments
- %i[assignee_ids milestone_id]
+ %i[assignee_ids milestone_id add_label_ids remove_label_ids]
end
def model_ids_from(attributes)
diff --git a/app/graphql/types/issuable_subscription_event_enum.rb b/app/graphql/types/issuable_subscription_event_enum.rb
new file mode 100644
index 00000000000..0f56fab8b46
--- /dev/null
+++ b/app/graphql/types/issuable_subscription_event_enum.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ class IssuableSubscriptionEventEnum < BaseEnum
+ graphql_name 'IssuableSubscriptionEvent'
+ description 'Values for subscribing and unsubscribing from issuables'
+
+ value 'SUBSCRIBE', 'Subscribe to an issuable.', value: 'subscribe'
+ value 'UNSUBSCRIBE', 'Unsubscribe from an issuable.', value: 'unsubscribe'
+ end
+end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index ec395baef9e..590659be0cb 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -185,6 +185,7 @@ module MergeRequestsHelper
endpoint_metadata: @endpoint_metadata_url,
endpoint_batch: diffs_batch_project_json_merge_request_path(project, merge_request, 'json', params),
endpoint_coverage: @coverage_path,
+ endpoint_diff_for_path: diff_for_path_namespace_project_merge_request_path(format: 'json', id: merge_request.iid, namespace_id: project.namespace.path, project_id: project.path),
help_page_path: help_page_path('user/project/merge_requests/reviews/suggestions.md'),
current_user_data: @current_user_data,
update_current_user_path: @update_current_user_path,
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 58843435fa0..e053fc0453c 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -100,18 +100,7 @@ module Emails
end
def issues_csv_email(user, project, csv_data, export_status)
- @project = project
- @count = export_status.fetch(:rows_expected)
- @written_count = export_status.fetch(:rows_written)
- @truncated = export_status.fetch(:truncated)
- @size_limit = ActiveSupport::NumberHelper
- .number_to_human_size(ExportCsv::BaseService::TARGET_FILESIZE)
-
- filename = "#{project.full_path.parameterize}_issues_#{Date.today.iso8601}.csv"
- attachments[filename] = { content: csv_data, mime_type: 'text/csv' }
- email_with_layout(
- to: user.notification_email_for(@project.group),
- subject: subject("Exported issues"))
+ csv_email(user, project, csv_data, export_status, 'issues')
end
private
diff --git a/app/mailers/emails/shared.rb b/app/mailers/emails/shared.rb
new file mode 100644
index 00000000000..09876c0960a
--- /dev/null
+++ b/app/mailers/emails/shared.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Emails
+ module Shared
+ def csv_email(user, project, csv_data, export_status, type)
+ @project = project
+ @count = export_status.fetch(:rows_expected)
+ @written_count = export_status.fetch(:rows_written)
+ @truncated = export_status.fetch(:truncated)
+ @size_limit = ActiveSupport::NumberHelper
+ .number_to_human_size(ExportCsv::BaseService::TARGET_FILESIZE)
+
+ filename = "#{project.full_path.parameterize}_#{type}_#{Date.today.iso8601}.csv"
+ attachments[filename] = { content: csv_data, mime_type: 'text/csv' }
+ email_with_layout(
+ to: user.notification_email_for(@project.group),
+ subject: subject("Exported #{type.humanize.downcase}"))
+ end
+ end
+end
diff --git a/app/mailers/emails/work_items.rb b/app/mailers/emails/work_items.rb
index fe669fdbedc..a243158d82f 100644
--- a/app/mailers/emails/work_items.rb
+++ b/app/mailers/emails/work_items.rb
@@ -11,5 +11,9 @@ module Emails
to: @user.notification_email_for(@project),
subject: subject('Imported work items'))
end
+
+ def export_work_items_csv_email(user, project, csv_data, export_status)
+ csv_email(user, project, csv_data, export_status, 'work_items')
+ end
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0465887cdc6..2d6b2a3099c 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -7,6 +7,7 @@ class Notify < ApplicationMailer
include ReminderEmailsHelper
include IssuablesHelper
+ include Emails::Shared
include Emails::Issues
include Emails::MergeRequests
include Emails::Notes
diff --git a/app/models/group.rb b/app/models/group.rb
index d325d1d351a..0dcc35b2a39 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -21,7 +21,6 @@ class Group < Namespace
include ChronicDurationAttribute
include RunnerTokenExpirationInterval
include Todoable
- include IssueParent
extend ::Gitlab::Utils::Override
diff --git a/app/models/members/member_role.rb b/app/models/members/member_role.rb
index 42ce228c318..77e97a35b6d 100644
--- a/app/models/members/member_role.rb
+++ b/app/models/members/member_role.rb
@@ -4,12 +4,15 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass
include IgnorableColumns
ignore_column :download_code, remove_with: '15.9', remove_after: '2023-01-22'
+ MAX_COUNT_PER_GROUP_HIERARCHY = 10
+
has_many :members
belongs_to :namespace
validates :namespace, presence: true
validates :base_access_level, presence: true
validate :belongs_to_top_level_namespace
+ validate :max_count_per_group_hierarchy, on: :create
validate :validate_namespace_locked, on: :update
validate :attributes_locked_after_member_associated, on: :update
@@ -25,6 +28,14 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass
errors.add(:namespace, s_("MemberRole|must be top-level namespace"))
end
+ def max_count_per_group_hierarchy
+ return unless namespace
+ return if namespace.member_roles.count < MAX_COUNT_PER_GROUP_HIERARCHY
+
+ errors.add(:namespace, s_("MemberRole|maximum number of Member Roles are already in use by the group hierarchy. "\
+ "Please delete an existing Member Role."))
+ end
+
def validate_namespace_locked
return unless namespace_id_changed?
diff --git a/app/services/clusters/agent_tokens/create_service.rb b/app/services/clusters/agent_tokens/create_service.rb
index e0c0b613adc..66a3cb04d98 100644
--- a/app/services/clusters/agent_tokens/create_service.rb
+++ b/app/services/clusters/agent_tokens/create_service.rb
@@ -19,7 +19,7 @@ module Clusters
token = ::Clusters::AgentToken.new(filtered_params.merge(agent_id: agent.id, created_by_user: current_user))
if token.save
- log_activity_event!(token)
+ log_activity_event(token)
ServiceResponse.success(payload: { secret: token.token, token: token })
else
@@ -37,7 +37,7 @@ module Clusters
params.slice(*ALLOWED_PARAMS)
end
- def log_activity_event!(token)
+ def log_activity_event(token)
Clusters::Agents::CreateActivityEventService.new(
token.agent,
kind: :token_created,
diff --git a/app/services/clusters/agent_tokens/revoke_service.rb b/app/services/clusters/agent_tokens/revoke_service.rb
index 247cedb8e38..5d89b405969 100644
--- a/app/services/clusters/agent_tokens/revoke_service.rb
+++ b/app/services/clusters/agent_tokens/revoke_service.rb
@@ -14,6 +14,8 @@ module Clusters
return error_no_permissions unless current_user.can?(:create_cluster, token.agent.project)
if token.update(status: token.class.statuses[:revoked])
+ log_activity_event(token)
+
ServiceResponse.success
else
ServiceResponse.error(message: token.errors.full_messages)
@@ -26,6 +28,17 @@ module Clusters
ServiceResponse.error(
message: s_('ClusterAgent|User has insufficient permissions to revoke the token for this project'))
end
+
+ def log_activity_event(token)
+ Clusters::Agents::CreateActivityEventService.new(
+ token.agent,
+ kind: :token_revoked,
+ level: :info,
+ recorded_at: token.updated_at,
+ user: current_user,
+ agent_token: token
+ ).execute
+ end
end
end
end
diff --git a/app/services/clusters/agents/create_activity_event_service.rb b/app/services/clusters/agents/create_activity_event_service.rb
index 886dddf1a52..87554f0e495 100644
--- a/app/services/clusters/agents/create_activity_event_service.rb
+++ b/app/services/clusters/agents/create_activity_event_service.rb
@@ -14,6 +14,10 @@ module Clusters
DeleteExpiredEventsWorker.perform_at(schedule_cleanup_at, agent.id)
ServiceResponse.success
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_exception(e, agent_id: agent.id)
+
+ ServiceResponse.error(message: e.message)
end
private
diff --git a/app/services/work_items/export_csv_service.rb b/app/services/work_items/export_csv_service.rb
index 9bef75e2c40..a715aab1b30 100644
--- a/app/services/work_items/export_csv_service.rb
+++ b/app/services/work_items/export_csv_service.rb
@@ -11,7 +11,7 @@ module WorkItems
end
def email(mail_to_user)
- # TODO - will be implemented as part of https://gitlab.com/gitlab-org/gitlab/-/issues/379082
+ Notify.export_work_items_csv_email(mail_to_user, resource_parent, csv_data, csv_builder.status).deliver_now
end
private
diff --git a/app/views/notify/_issuable_csv_export.html.haml b/app/views/notify/_issuable_csv_export.html.haml
index 3b1fe90eaee..f7c6168ecb6 100644
--- a/app/views/notify/_issuable_csv_export.html.haml
+++ b/app/views/notify/_issuable_csv_export.html.haml
@@ -1,6 +1,8 @@
+- type = type.to_s.humanize.downcase
+
%p{ style: 'font-size:18px; text-align:center; line-height:30px;' }
- project_link = link_to(@project.full_name, project_url(@project), style: "color:#3777b0; text-decoration:none; display:block;")
- = _('Your CSV export of %{count} from project %{project_link} has been added to this email as an attachment.').html_safe % { count: pluralize(@written_count, type.to_s.titleize.downcase), project_link: project_link }
+ = _('Your CSV export of %{count} from project %{project_link} has been added to this email as an attachment.').html_safe % { count: pluralize(@written_count, type.titleize.downcase), project_link: project_link }
- if @truncated
%p
- = _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{count} %{issuables} have been included. Consider re-exporting with a narrower selection of %{issuables}.') % { written_count: @written_count, count: @count, issuables: type.to_s.pluralize, size_limit: @size_limit }
+ = _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{count} %{issuables} have been included. Consider re-exporting with a narrower selection of %{issuables}.') % { written_count: @written_count, count: @count, issuables: type.pluralize, size_limit: @size_limit }
diff --git a/app/views/notify/_issuable_csv_export.text.erb b/app/views/notify/_issuable_csv_export.text.erb
new file mode 100644
index 00000000000..a6e908803f5
--- /dev/null
+++ b/app/views/notify/_issuable_csv_export.text.erb
@@ -0,0 +1,7 @@
+<% type = type.to_s.humanize.downcase %>
+
+<%= _('Your CSV export of %{exported_objects} from project %{project_name} (%{project_url}) has been added to this email as an attachment.') % { exported_objects: pluralize(@written_count, type), project_name: @project.full_name, project_url: project_url(@project) } %>
+
+<% if @truncated %>
+ <%= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{total_count} %{object_type} have been included. Consider re-exporting with a narrower selection of %{object_type}.') % { written_count: @written_count, total_count: @count, size_limit: @size_limit, object_type: type.pluralize } %>
+<% end %>
diff --git a/app/views/notify/export_work_items_csv_email.html.haml b/app/views/notify/export_work_items_csv_email.html.haml
new file mode 100644
index 00000000000..db842262049
--- /dev/null
+++ b/app/views/notify/export_work_items_csv_email.html.haml
@@ -0,0 +1 @@
+= render 'issuable_csv_export', type: :work_item
diff --git a/app/views/notify/export_work_items_csv_email.text.erb b/app/views/notify/export_work_items_csv_email.text.erb
new file mode 100644
index 00000000000..ec4aaa38886
--- /dev/null
+++ b/app/views/notify/export_work_items_csv_email.text.erb
@@ -0,0 +1 @@
+<%= render 'issuable_csv_export', type: :work_item %>
diff --git a/app/views/notify/issues_csv_email.text.erb b/app/views/notify/issues_csv_email.text.erb
index cf2910c4014..5b6c151e4ce 100644
--- a/app/views/notify/issues_csv_email.text.erb
+++ b/app/views/notify/issues_csv_email.text.erb
@@ -1,5 +1 @@
-<%= _('Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment.') % { written_count: pluralize(@written_count, 'issue'), project_name: @project.full_name, project_url: project_url(@project) } %>
-
-<% if @truncated %>
- <%= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, issues_count: @issues_count, size_limit: @size_limit } %>
-<% end %>
+<%= render 'issuable_csv_export', type: :issue %>
diff --git a/app/views/notify/merge_requests_csv_email.text.erb b/app/views/notify/merge_requests_csv_email.text.erb
index 78d11dde69f..c5dec164a4d 100644
--- a/app/views/notify/merge_requests_csv_email.text.erb
+++ b/app/views/notify/merge_requests_csv_email.text.erb
@@ -1,5 +1 @@
-<%= _('Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment.') % { written_count: pluralize(@written_count, 'merge request'), project_name: @project.full_name, project_url: project_url(@project) } %>
-
-<% if @truncated %>
- <%= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{merge_requests_count} merge requests have been included. Consider re-exporting with a narrower selection of merge requests.') % { written_count: @written_count, merge_requests_count: @merge_requests_count, size_limit: @size_limit} %>
-<% end %>
+<%= render 'issuable_csv_export', type: :merge_request %>
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index 3359ea5f63b..ccda06c7e4c 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -12,7 +12,7 @@
.nav-block
%ul.breadcrumb.repo-breadcrumb
%li.breadcrumb-item
- = link_to 'Artifacts', browse_project_job_artifacts_path(@project, @build)
+ = link_to _('Artifacts'), browse_project_job_artifacts_path(@project, @build)
- path_breadcrumbs do |title, path|
%li.breadcrumb-item
= link_to truncate(title, length: 40), browse_project_job_artifacts_path(@project, @build, path)
diff --git a/app/views/shared/icons/_mr_widget_empty_state.svg b/app/views/shared/icons/_mr_widget_empty_state.svg
deleted file mode 100644
index a75eee846c9..00000000000
--- a/app/views/shared/icons/_mr_widget_empty_state.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg width="256" height="146" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><mask id="d" x="0" y="0" width="178.7" height="115.4" fill="#FFF"><use xlink:href="#a"/></mask><mask id="e" x="0" y="0" width="43.1" height="36.4" fill="#FFF"><use xlink:href="#b"/></mask><mask id="f" x="0" y="0" width="43.1" height="36.4" fill="#FFF"><use xlink:href="#c"/></mask><path d="M8.8 31.5H33a10 10 0 0 0 10-10V10A10 10 0 0 0 33 0H10A10 10 0 0 0 0 10v11.6c0 1.2.2 2.4.7 3.5H0v7.5c0 4 2.4 5 5.3 2.2l3.5-3.3z" id="b"/><path d="M8.8 31.5H33a10 10 0 0 0 10-10V10A10 10 0 0 0 33 0H10A10 10 0 0 0 0 10v11.6c0 1.2.2 2.4.7 3.5H0v7.5c0 4 2.4 5 5.3 2.2l3.5-3.3z" id="c"/><rect id="a" width="178.7" height="115.4" rx="10"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 3.9)" fill="var(--gray-10, #f9f9f9)"><rect x="19.3" width="77.1" height="14.2" rx="7.1"/><rect y="28.4" width="84.9" height="14.2" rx="7.1"/><rect x="133.7" y="42.5" width="122.1" height="14.2" rx="7.1"/><rect x="82.9" y="127" width="101.6" height="14.2" rx="7.1"/><rect x="42.4" y="99.3" width="101.6" height="14.2" rx="7.1"/><rect x="19.9" y="70.9" width="225" height="14.2" rx="7.1"/><path d="M98.4 14.2h-85 13.9a7.1 7.1 0 0 1 7 7 7 7 0 0 1-7 7.2H13.5h84.9-23.5a7.1 7.1 0 0 1-7-7.1 7 7 0 0 1 7-7.1h23.5zm162 42.5H185h23.5a7.1 7.1 0 0 1 7 7.1 7 7 0 0 1-7 7.1H185h75.3-23.5a7.1 7.1 0 0 1-7-7 7 7 0 0 1 7-7.2h23.5zM103.5 85.1H28.3h23.4a7.1 7.1 0 0 1 7.1 7 7 7 0 0 1-7 7.2H28.2h75.2H80a7.1 7.1 0 0 1-7.1-7.1 7 7 0 0 1 7-7.1h23.5zm48.2 28.4H76.5h13.8a7.1 7.1 0 0 1 7 7 7 7 0 0 1-7 7.1H76.5h75.2-33a7.1 7.1 0 0 1-7.2-7 7 7 0 0 1 7.1-7.1h33.1z"/></g><g transform="translate(38.6 12.2)"><use stroke="var(--gray-200, #EEE)" mask="url(#d)" stroke-width="8" fill="var(--white, #fff)" xlink:href="#a"/><path fill="var(--gray-200, #EEE)" d="M2.6 18.7h174.2v2.6H2.6z"/><g fill="var(--gray-100, #EEE)"><g transform="translate(21.9 38.7)"><g fill="var(--dark-icon-color-purple-2, #B5A7DD)"><rect y=".6" width="3.9" height="1.3" rx=".6"/><rect y="7.7" width="3.9" height="1.3" rx=".6"/><rect y="14.8" width="3.9" height="1.3" rx=".6"/></g><rect x="9.6" width="9.6" height="2.6" rx="1.3"/><rect x="46.3" width="9.6" height="2.6" rx="1.3"/><rect x="25.1" y="14.2" width="9.6" height="2.6" rx="1.3"/><rect fill="#FC6D26" x="34.1" y="7.1" width="9.6" height="2.6" rx="1.3"/><rect fill="#FC6D26" opacity=".5" x="30.9" width="12.9" height="2.6" rx="1.3"/><rect x="9.6" y="14.2" width="12.9" height="2.6" rx="1.3"/><rect x="18.6" y="7.1" width="12.9" height="2.6" rx="1.3"/><rect fill="#FC6D26" x="21.9" width="6.4" height="2.6" rx="1.3"/><rect x="9.6" y="7.1" width="6.4" height="2.6" rx="1.3"/></g><g transform="translate(21.9 60)"><g fill="var(--dark-icon-color-purple-2, #B5A7DD)"><rect y=".6" width="3.9" height="1.3" rx=".6"/><rect y="7.7" width="3.9" height="1.3" rx=".6"/><rect y="14.8" width="3.9" height="1.3" rx=".6"/></g><rect fill="#FC6D26" x="9.6" width="9.6" height="2.6" rx="1.3"/><rect x="46.3" width="9.6" height="2.6" rx="1.3"/><rect fill="#FC6D26" opacity=".5" x="25.1" y="14.2" width="9.6" height="2.6" rx="1.3"/><rect x="34.1" y="7.1" width="9.6" height="2.6" rx="1.3"/><rect fill="#FC6D26" x="30.9" width="12.9" height="2.6" rx="1.3"/><rect fill="#FC6D26" x="9.6" y="14.2" width="12.9" height="2.6" rx="1.3"/><rect x="18.6" y="7.1" width="12.9" height="2.6" rx="1.3"/><rect fill="#FC6D26" opacity=".5" x="21.9" width="6.4" height="2.6" rx="1.3"/><rect x="9.6" y="7.1" width="6.4" height="2.6" rx="1.3"/></g><g transform="translate(21.9 81.2)"><g fill="var(--dark-icon-color-purple-2, #B5A7DD)"><rect y=".6" width="3.9" height="1.3" rx=".6"/><rect y="7.7" width="3.9" height="1.3" rx=".6"/><rect y="14.8" width="3.9" height="1.3" rx=".6"/></g><rect x="9.6" width="9.6" height="2.6" rx="1.3"/><rect x="46.3" width="9.6" height="2.6" rx="1.3"/><rect x="25.1" y="14.2" width="9.6" height="2.6" rx="1.3"/><rect x="34.1" y="7.1" width="9.6" height="2.6" rx="1.3"/><rect fill="#FC6D26" x="30.9" width="12.9" height="2.6" rx="1.3"/><rect x="9.6" y="14.2" width="12.9" height="2.6" rx="1.3"/><rect x="18.6" y="7.1" width="12.9" height="2.6" rx="1.3"/><rect fill="#FC6D26" opacity=".5" x="21.9" width="6.4" height="2.6" rx="1.3"/><rect x="9.6" y="7.1" width="6.4" height="2.6" rx="1.3"/></g><g transform="translate(101 38)"><g fill="var(--dark-icon-color-purple-3, #6B4FBB)"><rect opacity=".5" x="25.1" y="14.2" width="9.6" height="2.6" rx="1.3"/><rect x="34.1" y="7.1" width="9.6" height="2.6" rx="1.3"/><rect opacity=".5" x="30.9" width="12.9" height="2.6" rx="1.3"/><rect x="9.6" y="14.2" width="12.9" height="2.6" rx="1.3"/><rect x="21.9" width="6.4" height="2.6" rx="1.3"/><rect opacity=".5" x="25.1" y="35.5" width="9.6" height="2.6" rx="1.3"/><rect x="18.6" y="28.4" width="9.6" height="2.6" rx="1.3"/><rect x="30.9" y="21.3" width="12.9" height="2.6" rx="1.3"/><rect x="9.6" y="42.5" width="9.6" height="2.6" rx="1.3"/><rect opacity=".5" x="34.1" y="49.6" width="9.6" height="2.6" rx="1.3"/><rect x="18.6" y="49.6" width="12.9" height="2.6" rx="1.3"/></g><g fill="var(--dark-icon-color-orange-1, #FDE5D8)"><rect y=".6" width="3.9" height="1.3" rx=".6"/><rect y="7.7" width="3.9" height="1.3" rx=".6"/><rect y="14.8" width="3.9" height="1.3" rx=".6"/><rect y="21.9" width="3.9" height="1.3" rx=".6"/><rect y="29" width="3.9" height="1.3" rx=".6"/><rect y="36.1" width="3.9" height="1.3" rx=".6"/><rect y="43.2" width="3.9" height="1.3" rx=".6"/><rect y="50.3" width="3.9" height="1.3" rx=".6"/><rect y="57.4" width="3.9" height="1.3" rx=".6"/></g><rect x="9.6" width="9.6" height="2.6" rx="1.3"/><rect x="46.3" width="9.6" height="2.6" rx="1.3"/><rect x="18.6" y="7.1" width="12.9" height="2.6" rx="1.3"/><rect x="9.6" y="7.1" width="6.4" height="2.6" rx="1.3"/><rect x="9.6" y="21.3" width="9.6" height="2.6" rx="1.3"/><rect x="37.3" y="14.2" width="9.6" height="2.6" rx="1.3"/><rect x="9.6" y="35.5" width="12.9" height="2.6" rx="1.3"/><rect x="21.9" y="21.3" width="6.4" height="2.6" rx="1.3"/><rect x="9.6" y="28.4" width="6.4" height="2.6" rx="1.3"/><rect x="30.9" y="28.4" width="6.4" height="2.6" rx="1.3"/><rect x="39.9" y="28.4" width="6.4" height="2.6" rx="1.3"/><rect x="49.5" y="14.2" width="6.4" height="2.6" rx="1.3"/><rect x="25.1" y="56.7" width="9.6" height="2.6" rx="1.3"/><rect x="9.6" y="56.7" width="12.9" height="2.6" rx="1.3"/><rect x="21.9" y="42.5" width="6.4" height="2.6" rx="1.3"/><rect x="46.3" y="49.6" width="6.4" height="2.6" rx="1.3"/><rect x="9.6" y="49.6" width="6.4" height="2.6" rx="1.3"/></g></g></g><g transform="translate(196)"><use stroke="var(--dark-icon-color-orange-1, #FDE5D8)" mask="url(#e)" stroke-width="8" fill="var(--white, #fff)" xlink:href="#b"/><g fill="var(--dark-icon-color-orange-2, #FDB692)"><rect x="9" y="9" width="18.6" height="1.9" rx="1"/><rect x="9" y="14.8" width="25.1" height="1.9" rx="1"/><rect x="9" y="20.6" width="18.6" height="1.9" rx="1"/></g></g><g transform="translate(189 41.3)"><ellipse stroke="#FC6D26" stroke-width="3" fill="#fde5d8" cx="10.3" cy="9.7" rx="9.6" ry="9.7"/><path d="M0 9a8.4 8.4 0 0 0 8-4.3m1-4V0" stroke="#FC6D26" stroke-width="2"/><path d="M5 2a10.3 10.3 0 0 0 8.5 4.4c2.1 0 4-.6 5.7-1.7" stroke="#FC6D26" stroke-width="2"/><circle fill="#FC6D26" cx="6.8" cy="11.3" r="1"/><circle fill="#FC6D26" cx="13.8" cy="11.3" r="1"/></g><g transform="translate(47 96)"><ellipse stroke="var(--dark-icon-color-purple-3, #6B4FBB)" stroke-width="3" fill="#F4F1FA" cx="9.6" cy="10.3" rx="9.6" ry="9.7"/><path d="m12.9 4.5-1.7-2-1.6 2-1.6-2-1.6 2-1.6-2-1.6 2H1.5A9.6 9.6 0 0 1 9.6 0c3.5 0 6.5 1.8 8.2 4.5h-1.7l-1.6-2-1.6 2z" fill="var(--dark-icon-color-purple-3, #6B4FBB)"/><circle fill="var(--dark-icon-color-purple-3, #6B4FBB)" cx="6.1" cy="11.3" r="1"/><circle fill="var(--dark-icon-color-purple-3, #6B4FBB)" cx="13.2" cy="11.3" r="1"/></g><g transform="matrix(-1 0 0 1 56.6 54.8)" fill="var(--dark-icon-color-purple-2, #b5a8dd)"><use stroke="var(--dark-icon-color-purple-1, #E2DCF2)" mask="url(#f)" stroke-width="8" fill="var(--white, #fff)" xlink:href="#c"/><rect x="15.4" y="9" width="18.6" height="1.9" rx="1"/><rect x="21.9" y="14.8" width="12.2" height="1.9" rx="1"/><rect x="21.9" y="20.6" width="12.2" height="1.9" rx="1"/></g></g></svg>
diff --git a/config/application.rb b/config/application.rb
index a9008e387cf..94195be4481 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -405,7 +405,7 @@ module Gitlab
config.middleware.insert_before ActionDispatch::RemoteIp, ::Gitlab::Middleware::HandleIpSpoofAttackError
- config.middleware.insert_after ActionDispatch::ActionableExceptions, ::Gitlab::Middleware::HandleMalformedStrings
+ config.middleware.insert_after Rails::Rack::Logger, ::Gitlab::Middleware::HandleMalformedStrings
config.middleware.insert_after Rack::Sendfile, ::Gitlab::Middleware::RackMultipartTempfileFactory
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index ebeaca6b5b5..e0f34da8fbd 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -3673,12 +3673,18 @@ Input type: `IssuesBulkUpdateInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mutationissuesbulkupdateaddlabelids"></a>`addLabelIds` | [`[LabelID!]`](#labelid) | Global ID array of the labels that will be added to the issues. |
| <a id="mutationissuesbulkupdateassigneeids"></a>`assigneeIds` | [`[UserID!]`](#userid) | Global ID array of the users that will be assigned to the given issues. Existing assignees will be replaced with the ones on this list. |
| <a id="mutationissuesbulkupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationissuesbulkupdateepicid"></a>`epicId` | [`EpicID`](#epicid) | Global ID of the epic that will be assigned to the issues. |
+| <a id="mutationissuesbulkupdatehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Health status that will be assigned to the issues. |
| <a id="mutationissuesbulkupdateids"></a>`ids` | [`[IssueID!]!`](#issueid) | Global ID array of the issues that will be updated. IDs that the user can't update will be ignored. A max of 100 can be provided. |
| <a id="mutationissuesbulkupdateiterationid"></a>`iterationId` | [`IterationID`](#iterationid) | Global ID of the iteration that will be assigned to the issues. |
| <a id="mutationissuesbulkupdatemilestoneid"></a>`milestoneId` | [`MilestoneID`](#milestoneid) | Global ID of the milestone that will be assigned to the issues. |
-| <a id="mutationissuesbulkupdateparentid"></a>`parentId` | [`IssueParentID!`](#issueparentid) | Global ID of the parent that the bulk update will be scoped to . Example `IssueParentID` are `"gid://gitlab/Project/1"` and `"gid://gitlab/Group/1"`. |
+| <a id="mutationissuesbulkupdateparentid"></a>`parentId` | [`IssueParentID!`](#issueparentid) | Global ID of the parent to which the bulk update will be scoped. The parent can be a project **(FREE)** or a group **(PREMIUM)**. Example `IssueParentID` are `"gid://gitlab/Project/1"` and `"gid://gitlab/Group/1"`. |
+| <a id="mutationissuesbulkupdateremovelabelids"></a>`removeLabelIds` | [`[LabelID!]`](#labelid) | Global ID array of the labels that will be removed from the issues. |
+| <a id="mutationissuesbulkupdatestateevent"></a>`stateEvent` | [`IssueStateEvent`](#issuestateevent) | Close or reopen an issue. |
+| <a id="mutationissuesbulkupdatesubscriptionevent"></a>`subscriptionEvent` | [`IssuableSubscriptionEvent`](#issuablesubscriptionevent) | Subscribe to or unsubscribe from issue notifications. |
#### Fields
@@ -23035,6 +23041,15 @@ State of a GitLab issue or merge request.
| <a id="issuablestatelocked"></a>`locked` | Discussion has been locked. |
| <a id="issuablestateopened"></a>`opened` | In open state. |
+### `IssuableSubscriptionEvent`
+
+Values for subscribing and unsubscribing from issuables.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="issuablesubscriptioneventsubscribe"></a>`SUBSCRIBE` | Subscribe to an issuable. |
+| <a id="issuablesubscriptioneventunsubscribe"></a>`UNSUBSCRIBE` | Unsubscribe from an issuable. |
+
### `IssueCreationIterationWildcardId`
Iteration ID wildcard values for issue creation.
diff --git a/doc/api/product_analytics.md b/doc/api/product_analytics.md
index c687acdb5db..8eda24d1c65 100644
--- a/doc/api/product_analytics.md
+++ b/doc/api/product_analytics.md
@@ -6,7 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Product analytics API **(ULTIMATE)**
-> Introduced in GitLab 15.4 [with a flag](../administration/feature_flags.md) named `cube_api_proxy`. Disabled by default.
+> - Introduced in GitLab 15.4 [with a flag](../administration/feature_flags.md) named `cube_api_proxy`. Disabled by default.
+> - `cube_api_proxy` removed and replaced with `product_analytics_internal_preview` in GitLab 15.10.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `cube_api_proxy`.
diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md
index 51259f1677d..1a6ad4edf02 100644
--- a/doc/user/product_analytics/index.md
+++ b/doc/user/product_analytics/index.md
@@ -8,9 +8,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Introduced in GitLab 15.4 as an [Alpha](../../policy/alpha-beta-support.md#alpha-features) feature [with a flag](../../administration/feature_flags.md) named `cube_api_proxy`. Disabled by default.
> - `cube_api_proxy` revised to only reference the [Product Analytics API](../../api/product_analytics.md) in GitLab 15.6.
+> - `cube_api_proxy` removed and replaced with `product_analytics_internal_preview` in GitLab 15.10.
FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `cube_api_proxy`.
+On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `product_analytics_internal_preview`.
On GitLab.com, this feature is not available.
This feature is not ready for production use.
@@ -50,6 +51,7 @@ Product Analytics uses several tools:
> - Introduced in GitLab 15.6 behind the [feature flag](../../administration/feature_flags.md) named `cube_api_proxy`. Disabled by default.
> - Moved to be behind the [feature flag](../../administration/feature_flags.md) named `product_analytics_admin_settings` in GitLab 15.7. Disabled by default.
+> - `cube_api_proxy` removed and replaced with `product_analytics_internal_preview` in GitLab 15.10.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `product_analytics_admin_settings`.
diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake
index e6bbbc55cb1..742f13063c9 100644
--- a/lib/tasks/gitlab/tw/codeowners.rake
+++ b/lib/tasks/gitlab/tw/codeowners.rake
@@ -24,7 +24,6 @@ namespace :tw do
CodeOwnerRule.new('Certify', '@msedlakjakubowski'),
CodeOwnerRule.new('Code Review', '@aqualls'),
CodeOwnerRule.new('Compliance', '@eread'),
- CodeOwnerRule.new('Commerce Integrations', '@drcatherinepope'),
CodeOwnerRule.new('Composition Analysis', '@rdickenson'),
CodeOwnerRule.new('Configure', '@phillipwells'),
CodeOwnerRule.new('Container Registry', '@marcel.amirault'),
@@ -73,7 +72,7 @@ namespace :tw do
CodeOwnerRule.new('Static Analysis', '@rdickenson'),
CodeOwnerRule.new('Style Guide', '@sselhorn'),
CodeOwnerRule.new('Testing', '@eread'),
- CodeOwnerRule.new('Threat Insights', '@dianalogan'),
+ CodeOwnerRule.new('Threat Insights', '@rdickenson'),
CodeOwnerRule.new('Tutorials', '@kpaizee'),
CodeOwnerRule.new('Utilization', '@fneill'),
CodeOwnerRule.new('Vulnerability Research', '@dianalogan'),
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d6ad6c4ad20..266717412a6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -26447,6 +26447,9 @@ msgstr ""
msgid "MemberRole|cannot be deleted because it is already assigned to a user. Please disassociate the member role from all users before deletion."
msgstr ""
+msgid "MemberRole|maximum number of Member Roles are already in use by the group hierarchy. Please delete an existing Member Role."
+msgstr ""
+
msgid "MemberRole|must be top-level namespace"
msgstr ""
@@ -43961,13 +43964,7 @@ msgstr ""
msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{count} %{issuables} have been included. Consider re-exporting with a narrower selection of %{issuables}."
msgstr ""
-msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
-msgstr ""
-
-msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{merge_requests_count} merge requests have been included. Consider re-exporting with a narrower selection of merge requests."
-msgstr ""
-
-msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{requirements_count} requirements have been included. Consider re-exporting with a narrower selection of requirements."
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{total_count} %{object_type} have been included. Consider re-exporting with a narrower selection of %{object_type}."
msgstr ""
msgid "This block is self-referential"
@@ -49805,7 +49802,7 @@ msgstr ""
msgid "Your CSV export of %{count} from project %{project_link} has been added to this email as an attachment."
msgstr ""
-msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgid "Your CSV export of %{exported_objects} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
msgstr ""
msgid "Your CSV import for project"
diff --git a/spec/features/markdown/observability_spec.rb b/spec/features/markdown/observability_spec.rb
index e57bfafe05e..0c5aba5326f 100644
--- a/spec/features/markdown/observability_spec.rb
+++ b/spec/features/markdown/observability_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Observability rendering', :js, feature_category: :metrics do
let_it_be(:observable_url) { "https://observe.gitlab.com/#{group.id}/some-dashboard" }
let_it_be(:expected) do
- %(<iframe src="#{observable_url}?theme=light&amp;kiosk" frameborder="0")
+ %(<iframe src="#{observable_url}?theme=light&amp;kiosk=inline-embed" frameborder="0")
end
before do
diff --git a/spec/frontend/behaviors/markdown/render_observability_spec.js b/spec/frontend/behaviors/markdown/render_observability_spec.js
index c87d11742dc..7ca426d8cd6 100644
--- a/spec/frontend/behaviors/markdown/render_observability_spec.js
+++ b/spec/frontend/behaviors/markdown/render_observability_spec.js
@@ -3,7 +3,9 @@ import * as ColorUtils from '~/lib/utils/color_utils';
describe('Observability iframe renderer', () => {
const findObservabilityIframes = (theme = 'light') =>
- document.querySelectorAll(`iframe[src="https://observe.gitlab.com/?theme=${theme}&kiosk"]`);
+ document.querySelectorAll(
+ `iframe[src="https://observe.gitlab.com/?theme=${theme}&kiosk=inline-embed"]`,
+ );
const renderEmbeddedObservability = () => {
renderObservability([...document.querySelectorAll('.js-render-observability')]);
diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js
index b17886a5826..396f8215b9f 100644
--- a/spec/frontend/clusters/forms/components/integration_form_spec.js
+++ b/spec/frontend/clusters/forms/components/integration_form_spec.js
@@ -1,6 +1,6 @@
import { GlToggle, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
import IntegrationForm from '~/clusters/forms/components/integration_form.vue';
import { createStore } from '~/clusters/forms/stores/index';
@@ -27,17 +27,9 @@ describe('ClusterIntegrationForm', () => {
});
};
- const destroyWrapper = () => {
- wrapper.destroy();
- wrapper = null;
- };
-
const findSubmitButton = () => wrapper.findComponent(GlButton);
const findGlToggle = () => wrapper.findComponent(GlToggle);
-
- afterEach(() => {
- destroyWrapper();
- });
+ const findClusterEnvironmentScopeInput = () => wrapper.find('[id="cluster_environment_scope"]');
describe('rendering', () => {
beforeEach(() => createWrapper());
@@ -50,7 +42,9 @@ describe('ClusterIntegrationForm', () => {
});
it('sets the envScope to default', () => {
- expect(wrapper.find('[id="cluster_environment_scope"]').attributes('value')).toBe('*');
+ expect(findClusterEnvironmentScopeInput().attributes('value')).toBe(
+ defaultStoreValues.environmentScope,
+ );
});
it('sets the baseDomain to default', () => {
@@ -76,20 +70,15 @@ describe('ClusterIntegrationForm', () => {
beforeEach(() => createWrapper());
it('enables the submit button on changing toggle to different value', async () => {
- await nextTick();
- // setData is a bad approach because it changes the internal implementation which we should not touch
- // but our GlFormInput lacks the ability to set a new value.
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ toggleEnabled: !defaultStoreValues.enabled });
+ await findGlToggle().vm.$emit('change', false);
expect(findSubmitButton().props('disabled')).toBe(false);
});
it('enables the submit button on changing input values', async () => {
- await nextTick();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ envScope: `${defaultStoreValues.environmentScope}1` });
+ await findClusterEnvironmentScopeInput().vm.$emit(
+ 'input',
+ `${defaultStoreValues.environmentScope}1`,
+ );
expect(findSubmitButton().props('disabled')).toBe(false);
});
});
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 3d8cbaa6b89..14c3ac8b8fe 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -59,6 +59,7 @@ describe('diffs/components/app', () => {
endpoint: TEST_ENDPOINT,
endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`,
endpointBatch: `${TEST_HOST}/diff/endpointBatch`,
+ endpointDiffForPath: TEST_ENDPOINT,
endpointCoverage: `${TEST_HOST}/diff/endpointCoverage`,
endpointCodequality: '',
projectPath: 'namespace/project',
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index b5e773ecdab..b44b75382ad 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -69,6 +69,7 @@ describe('DiffsStoreActions', () => {
const endpoint = '/diffs/set/endpoint';
const endpointMetadata = '/diffs/set/endpoint/metadata';
const endpointBatch = '/diffs/set/endpoint/batch';
+ const endpointDiffForPath = '/diffs/set/endpoint/path';
const endpointCoverage = '/diffs/set/coverage_reports';
const projectPath = '/root/project';
const dismissEndpoint = '/-/user_callouts';
@@ -83,6 +84,7 @@ describe('DiffsStoreActions', () => {
{
endpoint,
endpointBatch,
+ endpointDiffForPath,
endpointMetadata,
endpointCoverage,
projectPath,
@@ -93,6 +95,7 @@ describe('DiffsStoreActions', () => {
{
endpoint: '',
endpointBatch: '',
+ endpointDiffForPath: '',
endpointMetadata: '',
endpointCoverage: '',
projectPath: '',
@@ -106,6 +109,7 @@ describe('DiffsStoreActions', () => {
endpoint,
endpointMetadata,
endpointBatch,
+ endpointDiffForPath,
endpointCoverage,
projectPath,
dismissEndpoint,
diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
index c2a55517405..0e17b6bc07f 100644
--- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
@@ -58,11 +58,13 @@ describe('InviteGroupsModal', () => {
findMembersFormGroup().attributes('invalid-feedback');
const findBase = () => wrapper.findComponent(InviteModalBase);
const triggerGroupSelect = (val) => findGroupSelect().vm.$emit('input', val);
- const emitEventFromModal = (eventName) => () =>
- findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
- const hideModal = emitEventFromModal('hidden');
- const clickInviteButton = emitEventFromModal('primary');
- const clickCancelButton = emitEventFromModal('cancel');
+ const hideModal = () => findModal().vm.$emit('hidden', { preventDefault: jest.fn() });
+
+ const emitClickFromModal = (testId) => () =>
+ wrapper.findByTestId(testId).vm.$emit('click', { preventDefault: jest.fn() });
+
+ const clickInviteButton = emitClickFromModal('invite-modal-submit');
+ const clickCancelButton = emitClickFromModal('invite-modal-cancel');
describe('displaying the correct introText and form group description', () => {
describe('when inviting to a project', () => {
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index 9687d528321..41ffe9b92c9 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -134,10 +134,15 @@ describe('InviteMembersModal', () => {
`${Object.keys(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]}: ${
Object.values(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]
}`;
- const emitEventFromModal = (eventName) => () =>
- findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
- const clickInviteButton = emitEventFromModal('primary');
- const clickCancelButton = emitEventFromModal('cancel');
+ const findActionButton = () => wrapper.findByTestId('invite-modal-submit');
+ const findCancelButton = () => wrapper.findByTestId('invite-modal-cancel');
+
+ const emitClickFromModal = (findButton) => () =>
+ findButton().vm.$emit('click', { preventDefault: jest.fn() });
+
+ const clickInviteButton = emitClickFromModal(findActionButton);
+ const clickCancelButton = emitClickFromModal(findCancelButton);
+
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
findMembersFormGroup().attributes('invalid-feedback');
@@ -368,13 +373,11 @@ describe('InviteMembersModal', () => {
it('tracks actions', async () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- const mockEvent = { preventDefault: jest.fn() };
-
await triggerOpenModal({ mode: 'celebrate', source: ON_CELEBRATION_TRACK_LABEL });
expectTracking('render', ON_CELEBRATION_TRACK_LABEL);
- findModal().vm.$emit('cancel', mockEvent);
+ clickCancelButton();
expectTracking('click_cancel', ON_CELEBRATION_TRACK_LABEL);
findModal().vm.$emit('close');
@@ -411,13 +414,11 @@ describe('InviteMembersModal', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- const mockEvent = { preventDefault: jest.fn() };
-
await triggerOpenModal(source);
expectTracking('render', label);
- findModal().vm.$emit('cancel', mockEvent);
+ clickCancelButton();
expectTracking('click_cancel', label);
findModal().vm.$emit('close');
@@ -734,7 +735,7 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
expect(findMembersSelect().props('exceptionState')).toBe(false);
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
});
it('clears the error when the modal is hidden', async () => {
@@ -746,7 +747,7 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
expect(findMembersSelect().props('exceptionState')).toBe(false);
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
findModal().vm.$emit('hidden');
@@ -768,7 +769,7 @@ describe('InviteMembersModal', () => {
expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError);
expect(membersFormGroupInvalidFeedback()).toBe('');
expect(findMembersSelect().props('exceptionState')).not.toBe(false);
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
});
it('displays all errors when there are multiple emails that return a restricted error message', async () => {
diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js
index f34f9902514..7e49977432d 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -66,8 +66,8 @@ describe('InviteModalBase', () => {
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const findDisabledInput = () => wrapper.findByTestId('disabled-input');
- const findCancelButton = () => wrapper.find('.js-modal-action-cancel');
- const findActionButton = () => wrapper.find('.js-modal-action-primary');
+ const findCancelButton = () => wrapper.findByTestId('invite-modal-cancel');
+ const findActionButton = () => wrapper.findByTestId('invite-modal-submit');
describe('rendering the modal', () => {
let trackingSpy;
@@ -88,20 +88,19 @@ describe('InviteModalBase', () => {
});
it('renders the Cancel button text correctly', () => {
- expect(wrapper.findComponent(GlModal).props('actionCancel')).toMatchObject({
- text: CANCEL_BUTTON_TEXT,
- });
+ expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
});
it('renders the Invite button correctly', () => {
- expect(wrapper.findComponent(GlModal).props('actionPrimary')).toMatchObject({
- text: INVITE_BUTTON_TEXT,
- attributes: {
- variant: 'confirm',
- disabled: false,
- loading: false,
- 'data-qa-selector': 'invite_button',
- },
+ const actionButton = findActionButton();
+
+ expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT);
+ expect(actionButton.attributes('data-qa-selector')).toBe('invite_button');
+
+ expect(actionButton.props()).toMatchObject({
+ variant: 'confirm',
+ disabled: false,
+ loading: false,
});
});
@@ -235,7 +234,7 @@ describe('InviteModalBase', () => {
},
});
- expect(wrapper.findComponent(GlModal).props('actionPrimary').attributes.loading).toBe(true);
+ expect(findActionButton().props('loading')).toBe(true);
});
it('with invalidFeedbackMessage, set members form group exception state', () => {
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
index dd3e55c82bb..e2640de1f94 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
@@ -31,10 +31,6 @@ describe('DropdownWidget component', () => {
},
});
- // We need to mock out `showDropdown` which
- // invokes `show` method of BDropdown used inside GlDropdown.
- // Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679
- jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
jest.spyOn(findDropdown().vm, 'hide').mockImplementation();
};
diff --git a/spec/mailers/emails/issues_spec.rb b/spec/mailers/emails/issues_spec.rb
index 21e07c0252d..b5f3972f38e 100644
--- a/spec/mailers/emails/issues_spec.rb
+++ b/spec/mailers/emails/issues_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'email_spec'
-RSpec.describe Emails::Issues do
+RSpec.describe Emails::Issues, feature_category: :team_planning do
include EmailSpec::Matchers
it 'adds email methods to Notify' do
@@ -54,38 +54,6 @@ RSpec.describe Emails::Issues do
subject { Notify.issues_csv_email(user, empty_project, "dummy content", export_status) }
- include_context 'gitlab email notification'
-
- it 'attachment has csv mime type' do
- expect(attachment.mime_type).to eq 'text/csv'
- end
-
- it 'generates a useful filename' do
- expect(attachment.filename).to include(Date.today.year.to_s)
- expect(attachment.filename).to include('issues')
- expect(attachment.filename).to include('myproject')
- expect(attachment.filename).to end_with('.csv')
- end
-
- it 'mentions number of issues and project name' do
- expect(subject).to have_content '3'
- expect(subject).to have_content empty_project.name
- end
-
- it "doesn't need to mention truncation by default" do
- expect(subject).not_to have_content 'truncated'
- end
-
- context 'when truncated' do
- let(:export_status) { { truncated: true, rows_expected: 12, rows_written: 10 } }
-
- it 'mentions that the csv has been truncated' do
- expect(subject).to have_content 'truncated'
- end
-
- it 'mentions the number of issues written and expected' do
- expect(subject).to have_content '10 of 12 issues'
- end
- end
+ it_behaves_like 'export csv email', 'issues'
end
end
diff --git a/spec/mailers/emails/work_items_spec.rb b/spec/mailers/emails/work_items_spec.rb
new file mode 100644
index 00000000000..eb2c751388d
--- /dev/null
+++ b/spec/mailers/emails/work_items_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'email_spec'
+
+RSpec.describe Emails::WorkItems, feature_category: :team_planning do
+ describe '#export_work_items_csv_email' do
+ let(:user) { build_stubbed(:user) }
+ let(:empty_project) { build_stubbed(:project, path: 'myproject') }
+ let(:export_status) { { truncated: false, rows_expected: 3, rows_written: 3 } }
+ let(:attachment) { subject.attachments.first }
+
+ subject { Notify.export_work_items_csv_email(user, empty_project, "dummy content", export_status) }
+
+ it_behaves_like 'export csv email', 'work_items'
+ end
+end
diff --git a/spec/models/members/member_role_spec.rb b/spec/models/members/member_role_spec.rb
index 1ee86376e02..69d164e1942 100644
--- a/spec/models/members/member_role_spec.rb
+++ b/spec/models/members/member_role_spec.rb
@@ -35,6 +35,30 @@ RSpec.describe MemberRole, feature_category: :system_access do
end
end
+ context 'for max_count_per_group_hierarchy' do
+ let_it_be(:group) { create(:group) }
+
+ subject(:member_role) { build(:member_role, namespace: group) }
+
+ context 'when number of member roles is below limit' do
+ it 'is valid' do
+ is_expected.to be_valid
+ end
+ end
+
+ context 'when number of member roles is above limit' do
+ before do
+ stub_const('MemberRole::MAX_COUNT_PER_GROUP_HIERARCHY', 1)
+ create(:member_role, namespace: group)
+ group.reload
+ end
+
+ it 'is invalid' do
+ is_expected.to be_invalid
+ end
+ end
+ end
+
context 'when for namespace' do
let_it_be(:root_group) { create(:group) }
diff --git a/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
index b9c83311908..b729585a89b 100644
--- a/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
@@ -8,7 +8,9 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
let_it_be(:developer) { create(:user) }
let_it_be(:group) { create(:group).tap { |group| group.add_developer(developer) } }
let_it_be(:project) { create(:project, group: group) }
- let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project) }
+ let_it_be(:label1) { create(:group_label, group: group) }
+ let_it_be(:label2) { create(:group_label, group: group) }
+ let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project, label_ids: [label1.id]) }
let_it_be(:milestone) { create(:milestone, group: group) }
let(:parent) { project }
@@ -21,10 +23,36 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
let(:additional_arguments) do
{
assignee_ids: [current_user.to_gid.to_s],
- milestone_id: milestone.to_gid.to_s
+ milestone_id: milestone.to_gid.to_s,
+ state_event: :CLOSE,
+ add_label_ids: [label2.to_gid.to_s],
+ remove_label_ids: [label1.to_gid.to_s],
+ subscription_event: :UNSUBSCRIBE
}
end
+ before_all do
+ updatable_issues.each { |i| i.subscribe(developer, project) }
+ end
+
+ context 'when Gitlab is FOSS only' do
+ unless Gitlab.ee?
+ context 'when parent is a group' do
+ let(:parent) { group }
+
+ it 'does not allow bulk updating issues at the group level' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including(
+ 'message' => match(/does not represent an instance of IssueParent/)
+ )
+ )
+ end
+ end
+ end
+ end
+
context 'when the `bulk_update_issues_mutation` feature flag is disabled' do
before do
stub_feature_flags(bulk_update_issues_mutation: false)
@@ -67,6 +95,11 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
updatable_issues.each(&:reload)
end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2)
.and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2))
+ .and(change { updatable_issues.map(&:state) }.from(['opened'] * 2).to(['closed'] * 2))
+ .and(change { updatable_issues.flat_map(&:label_ids) }.from([label1.id] * 2).to([label2.id] * 2))
+ .and(
+ change { updatable_issues.map { |i| i.subscribed?(developer, project) } }.from([true] * 2).to([false] * 2)
+ )
expect(mutation_response).to include(
'updatedIssueCount' => updatable_issues.count
@@ -88,37 +121,6 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
end
end
- context 'when scoping to a parent group' do
- let(:parent) { group }
-
- it 'updates all issues' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- updatable_issues.each(&:reload)
- end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2)
- .and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2))
-
- expect(mutation_response).to include(
- 'updatedIssueCount' => updatable_issues.count
- )
- end
-
- context 'when current user cannot read the specified group' do
- let(:parent) { create(:group, :private) }
-
- it 'returns a resource not found error' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(graphql_errors).to contain_exactly(
- hash_including(
- 'message' => "The resource that you are attempting to access does not exist or you don't have " \
- 'permission to perform this action'
- )
- )
- end
- end
- end
-
context 'when setting arguments to null or none' do
let(:additional_arguments) { { assignee_ids: [], milestone_id: nil } }
diff --git a/spec/services/clusters/agent_tokens/revoke_service_spec.rb b/spec/services/clusters/agent_tokens/revoke_service_spec.rb
index 24485a5f66d..9e511de0a13 100644
--- a/spec/services/clusters/agent_tokens/revoke_service_spec.rb
+++ b/spec/services/clusters/agent_tokens/revoke_service_spec.rb
@@ -4,6 +4,8 @@ require 'spec_helper'
RSpec.describe Clusters::AgentTokens::RevokeService, feature_category: :kubernetes_management do
describe '#execute' do
+ subject { described_class.new(token: agent_token, current_user: user).execute }
+
let(:agent) { create(:cluster_agent) }
let(:agent_token) { create(:cluster_agent_token, agent: agent) }
let(:project) { agent.project }
@@ -20,10 +22,24 @@ RSpec.describe Clusters::AgentTokens::RevokeService, feature_category: :kubernet
context 'when user revokes agent token' do
it 'succeeds' do
- described_class.new(token: agent_token, current_user: user).execute
+ subject
expect(agent_token.revoked?).to be true
end
+
+ it 'creates an activity event' do
+ expect { subject }.to change { ::Clusters::Agents::ActivityEvent.count }.by(1)
+
+ event = agent.activity_events.last
+
+ expect(event).to have_attributes(
+ kind: 'token_revoked',
+ level: 'info',
+ recorded_at: agent_token.reload.updated_at,
+ user: user,
+ agent_token: agent_token
+ )
+ end
end
context 'when there is a validation failure' do
@@ -32,24 +48,26 @@ RSpec.describe Clusters::AgentTokens::RevokeService, feature_category: :kubernet
end
it 'fails without raising an error', :aggregate_failures do
- result = described_class.new(token: agent_token, current_user: user).execute
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq(["Name can't be blank"])
+ end
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq(["Name can't be blank"])
+ it 'does not create an activity event' do
+ expect { subject }.not_to change { ::Clusters::Agents::ActivityEvent.count }
end
end
end
context 'when user is not authorized' do
- let(:unauthorized_user) { create(:user) }
+ let(:user) { create(:user) }
before do
- project.add_guest(unauthorized_user)
+ project.add_guest(user)
end
context 'when user attempts to revoke agent token' do
it 'fails' do
- described_class.new(token: agent_token, current_user: unauthorized_user).execute
+ subject
expect(agent_token.revoked?).to be false
end
diff --git a/spec/services/clusters/agents/create_activity_event_service_spec.rb b/spec/services/clusters/agents/create_activity_event_service_spec.rb
index 7a8f0e16d60..3da8ecddb8d 100644
--- a/spec/services/clusters/agents/create_activity_event_service_spec.rb
+++ b/spec/services/clusters/agents/create_activity_event_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Agents::CreateActivityEventService do
+RSpec.describe Clusters::Agents::CreateActivityEventService, feature_category: :kubernetes_management do
let_it_be(:agent) { create(:cluster_agent) }
let_it_be(:token) { create(:cluster_agent_token, agent: agent) }
let_it_be(:user) { create(:user) }
@@ -40,5 +40,16 @@ RSpec.describe Clusters::Agents::CreateActivityEventService do
subject
end
+
+ context 'when activity event creation fails' do
+ let(:params) { {} }
+
+ it 'tracks the exception without raising' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(instance_of(ActiveRecord::RecordInvalid), agent_id: agent.id)
+
+ subject
+ end
+ end
end
end
diff --git a/spec/services/work_items/export_csv_service_spec.rb b/spec/services/work_items/export_csv_service_spec.rb
index 0718d3b686a..7c22312ce1f 100644
--- a/spec/services/work_items/export_csv_service_spec.rb
+++ b/spec/services/work_items/export_csv_service_spec.rb
@@ -30,9 +30,8 @@ RSpec.describe WorkItems::ExportCsvService, :with_license, feature_category: :te
end
describe '#email' do
- # TODO - will be implemented as part of https://gitlab.com/gitlab-org/gitlab/-/issues/379082
- xit 'emails csv' do
- expect { subject.email(user) }.o change { ActionMailer::Base.deliveries.count }.from(0).to(1)
+ it 'emails csv' do
+ expect { subject.email(user) }.to change { ActionMailer::Base.deliveries.count }.from(0).to(1)
end
end
diff --git a/spec/support/shared_examples/mailers/export_csv_shared_examples.rb b/spec/support/shared_examples/mailers/export_csv_shared_examples.rb
new file mode 100644
index 00000000000..731d7c810f9
--- /dev/null
+++ b/spec/support/shared_examples/mailers/export_csv_shared_examples.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'export csv email' do |collection_type|
+ include_context 'gitlab email notification'
+
+ it 'attachment has csv mime type' do
+ expect(attachment.mime_type).to eq 'text/csv'
+ end
+
+ it 'generates a useful filename' do
+ expect(attachment.filename).to include(Date.today.year.to_s)
+ expect(attachment.filename).to include(collection_type)
+ expect(attachment.filename).to include('myproject')
+ expect(attachment.filename).to end_with('.csv')
+ end
+
+ it 'mentions number of objects and project name' do
+ expect(subject).to have_content '3'
+ expect(subject).to have_content empty_project.name
+ end
+
+ it "doesn't need to mention truncation by default" do
+ expect(subject).not_to have_content 'truncated'
+ end
+
+ context 'when truncated' do
+ let(:export_status) { { truncated: true, rows_expected: 12, rows_written: 10 } }
+
+ it 'mentions that the csv has been truncated' do
+ expect(subject).to have_content 'truncated'
+ end
+
+ it 'mentions the number of objects written and expected' do
+ expect(subject).to have_content "10 of 12 #{collection_type.humanize.downcase}"
+ end
+ end
+end
diff --git a/workhorse/go.mod b/workhorse/go.mod
index 9347a0ca300..02d7b04567d 100644
--- a/workhorse/go.mod
+++ b/workhorse/go.mod
@@ -32,7 +32,7 @@ require (
gocloud.dev v0.28.0
golang.org/x/image v0.5.0
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616
- golang.org/x/net v0.6.0
+ golang.org/x/net v0.7.0
golang.org/x/oauth2 v0.5.0
golang.org/x/tools v0.2.0
google.golang.org/grpc v1.53.0
diff --git a/workhorse/go.sum b/workhorse/go.sum
index 1efb189a752..05a120d1deb 100644
--- a/workhorse/go.sum
+++ b/workhorse/go.sum
@@ -2127,8 +2127,8 @@ golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfS
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=