diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-03-01 18:07:43 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-03-01 18:07:43 +0000 |
commit | ad1e76fb4d1392c890c8b5e218a256a416d5a50b (patch) | |
tree | 51e5541bb1f1a799e288701bc1170a3b1a9a7393 | |
parent | 8b1036168b0d395c379cbbaf457e256860147405 (diff) | |
download | gitlab-ce-ad1e76fb4d1392c890c8b5e218a256a416d5a50b.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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&kiosk" frameborder="0") + %(<iframe src="#{observable_url}?theme=light&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= |