diff options
59 files changed, 508 insertions, 231 deletions
diff --git a/.gitlab/ci/preflight.gitlab-ci.yml b/.gitlab/ci/preflight.gitlab-ci.yml index e477466e5f3..968402b2ea5 100644 --- a/.gitlab/ci/preflight.gitlab-ci.yml +++ b/.gitlab/ci/preflight.gitlab-ci.yml @@ -16,20 +16,23 @@ - !reference [.default-before_script, before_script] - cd qa && bundle install -rails-production-server-boot: +.rails-production-server-boot: extends: - .preflight-job-base - .default-before_script - .production - .ruby-cache - - .setup:rules:rails-production-server-boot + - .preflight:rules:rails-production-server-boot - .use-pg13 variables: BUNDLE_WITHOUT: "development:test" BUNDLE_WITH: "production" - needs: [] + +# Test the puma configuration present in `config/puma.rb.example` +rails-production-server-boot-puma-example: + extends: + - .rails-production-server-boot script: - - source scripts/utils.sh - cp config/puma.rb.example config/puma.rb - sed --in-place "s:/home/git/gitlab:${PWD}:" config/puma.rb - echo 'bind "tcp://127.0.0.1:3000"' >> config/puma.rb @@ -38,17 +41,30 @@ rails-production-server-boot: - retry_times_sleep 10 5 "curl http://127.0.0.1:3000" - kill $(jobs -p) +# Test the puma configuration present in +# https://gitlab.com/gitlab-org/build/CNG/-/raw/master/gitlab-webservice/configuration/puma.rb +rails-production-server-boot-puma-cng: + extends: + - .rails-production-server-boot + script: + - curl --silent https://gitlab.com/gitlab-org/build/CNG/-/raw/master/gitlab-webservice/configuration/puma.rb > config/puma.rb + - sed --in-place "s:/srv/gitlab:${PWD}:" config/puma.rb + - bundle exec puma --environment production --config config/puma.rb & + - sleep 40 # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114124#note_1309506358 + - retry_times_sleep 10 5 "curl http://127.0.0.1:8080" + - kill $(jobs -p) + no-ee-check: extends: - .preflight-job-base - - .setup:rules:no-ee-check + - .preflight:rules:no-ee-check script: - scripts/no-dir-check ee no-jh-check: extends: - .preflight-job-base - - .setup:rules:no-jh-check + - .preflight:rules:no-jh-check script: - scripts/no-dir-check jh diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 9390d89212c..a37d0e2be37 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -2477,25 +2477,6 @@ - <<: *if-default-refs changes: *code-backstage-patterns -.setup:rules:rails-production-server-boot: - rules: - - <<: *if-default-refs - changes: *code-patterns - -.setup:rules:no-ee-check: - rules: - - <<: *if-not-foss - when: never - - <<: *if-default-refs - changes: *code-backstage-patterns - -.setup:rules:no-jh-check: - rules: - - <<: *if-jh - when: never - - <<: *if-default-refs - changes: *code-backstage-patterns - .setup:rules:verify-ruby-3.0: rules: - <<: *if-merge-request-labels-run-in-ruby2 @@ -2527,6 +2508,29 @@ - "scripts/rspec_helpers.sh" ####################### +# Preflight rules # +####################### + +.preflight:rules:rails-production-server-boot: + rules: + - <<: *if-default-refs + changes: *code-patterns + +.preflight:rules:no-ee-check: + rules: + - <<: *if-not-foss + when: never + - <<: *if-default-refs + changes: *code-backstage-patterns + +.preflight:rules:no-jh-check: + rules: + - <<: *if-jh + when: never + - <<: *if-default-refs + changes: *code-backstage-patterns + +####################### # Test metadata rules # ####################### .test-metadata:rules:retrieve-tests-metadata: diff --git a/.rubocop_todo/style/mutable_constant.yml b/.rubocop_todo/style/mutable_constant.yml index 8e6a0a2335b..dd7fd259b2f 100644 --- a/.rubocop_todo/style/mutable_constant.yml +++ b/.rubocop_todo/style/mutable_constant.yml @@ -40,16 +40,6 @@ Style/MutableConstant: - 'lib/gitlab/sidekiq_signals.rb' - 'lib/gitlab/web_hooks/recursion_detection/uuid.rb' - 'lib/tasks/gitlab/backup.rake' - - 'rubocop/cop/background_migration/feature_category.rb' - - 'rubocop/cop/filename_length.rb' - - 'rubocop/cop/gitlab/event_store_subscriber.rb' - - 'rubocop/cop/graphql/descriptions.rb' - - 'rubocop/cop/graphql/enum_names.rb' - - 'rubocop/cop/migration/prevent_index_creation.rb' - - 'rubocop/cop/migration/versioned_migration_class.rb' - - 'rubocop/cop/migration/with_lock_retries_disallowed_method.rb' - - 'rubocop/cop/scalability/idempotent_worker.rb' - - 'rubocop/cop/sidekiq_load_balancing/worker_data_consistency.rb' - 'scripts/lib/glfm/constants.rb' - 'scripts/lint-docs-blueprints.rb' - 'scripts/perf/gc/collect_gc_stats.rb' @@ -287,7 +287,7 @@ gem 'circuitbox', '2.0.0' # Sanitize user input gem 'sanitize', '~> 6.0' -gem 'babosa', '~> 1.0.4' +gem 'babosa', '~> 2.0' # Sanitizes SVG input gem 'loofah', '~> 2.21.1' diff --git a/Gemfile.checksum b/Gemfile.checksum index f6f50484232..cc3ed31f5af 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -46,7 +46,7 @@ {"name":"axiom-types","version":"0.1.1","platform":"ruby","checksum":"c1ff113f3de516fa195b2db7e0a9a95fd1b08475a502ff660d04507a09980383"}, {"name":"azure-storage-blob","version":"2.0.3","platform":"ruby","checksum":"61b76118843c91776bd24bee22c74adafeb7c4bb3a858a325047dae3b59d0363"}, {"name":"azure-storage-common","version":"2.0.4","platform":"ruby","checksum":"608f4daab0e06b583b73dcffd3246ea39e78056de31630286b0cf97af7d6956b"}, -{"name":"babosa","version":"1.0.4","platform":"ruby","checksum":"18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99"}, +{"name":"babosa","version":"2.0.0","platform":"ruby","checksum":"a6218db8a4dc8fd99260dde8bc3d5fa1a0c52178196e236ebb31e41fbdcdb8a6"}, {"name":"backport","version":"1.2.0","platform":"ruby","checksum":"912c7dfdd9ee4625d013ddfccb6205c3f92da69a8990f65c440e40f5b2fc7f75"}, {"name":"base32","version":"0.3.2","platform":"ruby","checksum":"532e9b19c5dd1fce281df67fc93a803ebd5d26426a93f6dda6612769bc46fe2c"}, {"name":"batch-loader","version":"2.0.1","platform":"ruby","checksum":"93f711df78d316ee0440a7a45daba4f5418d0ee2b5f58f60c9ea038424e7a89d"}, diff --git a/Gemfile.lock b/Gemfile.lock index fe45768cdf9..3380ec995f6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -236,7 +236,7 @@ GEM faraday_middleware (~> 1.0, >= 1.0.0.rc1) net-http-persistent (~> 4.0) nokogiri (~> 1, >= 1.10.8) - babosa (1.0.4) + babosa (2.0.0) backport (1.2.0) base32 (0.3.2) batch-loader (2.0.1) @@ -1672,7 +1672,7 @@ DEPENDENCIES aws-sdk-core (~> 3.173.0) aws-sdk-s3 (~> 1.122.0) axe-core-rspec - babosa (~> 1.0.4) + babosa (~> 2.0) base32 (~> 0.3.0) batch-loader (~> 2.0.1) bcrypt (~> 3.1, >= 3.1.14) diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index 5fb83dfd1ab..2cb966cf1fd 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -1,6 +1,7 @@ <script> import { GlButton, + GlButtonGroup, GlFilteredSearchToken, GlTooltipDirective, GlDropdown, @@ -14,6 +15,7 @@ import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_st import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue'; import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import { createAlert, VARIANT_INFO } from '~/alert'; import { TYPENAME_USER } from '~/graphql_shared/constants'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; @@ -78,6 +80,9 @@ import { RELATIVE_POSITION_ASC, UPDATED_DESC, urlSortParams, + ISSUES_VIEW_TYPE_KEY, + ISSUES_LIST_VIEW_KEY, + ISSUES_GRID_VIEW_KEY, } from '../constants'; import eventHub from '../eventhub'; import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql'; @@ -116,11 +121,15 @@ const CrmOrganizationToken = () => export default { i18n, issuableListTabs, + ISSUES_VIEW_TYPE_KEY, + ISSUES_GRID_VIEW_KEY, + ISSUES_LIST_VIEW_KEY, components: { CsvImportExportButtons, EmptyStateWithAnyIssues, EmptyStateWithoutAnyIssues, GlButton, + GlButtonGroup, GlDropdown, GlDropdownDivider, GlDropdownItem, @@ -129,6 +138,7 @@ export default { IssueCardStatistics, IssueCardTimeInfo, NewResourceDropdown, + LocalStorageSync, }, directives: { GlTooltip: GlTooltipDirective, @@ -194,6 +204,7 @@ export default { sortKey: CREATED_DESC, state: STATUS_OPEN, pageSize: DEFAULT_PAGE_SIZE, + viewType: ISSUES_LIST_VIEW_KEY, }; }, apollo: { @@ -504,6 +515,12 @@ export default { }) ); }, + gridViewFeatureEnabled() { + return Boolean(this.glFeatures?.issuesGridView); + }, + isGridView() { + return this.viewType === ISSUES_GRID_VIEW_KEY; + }, }, watch: { $route(newValue, oldValue) { @@ -764,6 +781,15 @@ export default { this.sortKey = sortKey; this.state = state || STATUS_OPEN; }, + switchViewType(type) { + // Filter the wrong data from localStorage + if (type === ISSUES_GRID_VIEW_KEY) { + this.viewType = ISSUES_GRID_VIEW_KEY; + return; + } + // The default view is list view + this.viewType = ISSUES_LIST_VIEW_KEY; + }, }, }; </script> @@ -798,6 +824,7 @@ export default { :has-next-page="pageInfo.hasNextPage" :has-previous-page="pageInfo.hasPreviousPage" :show-filtered-search-friendly-text="hasOrFeature" + :is-grid-view="isGridView" show-work-item-type-icon @click-tab="handleClickTab" @dismiss-alert="handleDismissAlert" @@ -810,6 +837,30 @@ export default { @page-size-change="handlePageSizeChange" > <template #nav-actions> + <local-storage-sync + v-if="gridViewFeatureEnabled" + :value="viewType" + :storage-key="$options.ISSUES_VIEW_TYPE_KEY" + @input="switchViewType" + > + <gl-button-group> + <gl-button + :variant="isGridView ? 'default' : 'confirm'" + data-testid="list-view-type" + @click="switchViewType($options.ISSUES_LIST_VIEW_KEY)" + > + {{ $options.i18n.listLabel }} + </gl-button> + <gl-button + :variant="isGridView ? 'confirm' : 'default'" + data-testid="grid-view-type" + @click="switchViewType($options.ISSUES_GRID_VIEW_KEY)" + > + {{ $options.i18n.gridLabel }} + </gl-button> + </gl-button-group> + </local-storage-sync> + <gl-button v-if="canBulkUpdate" :disabled="isBulkEditButtonDisabled" diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js index 56d3a57457b..1a3d97277c7 100644 --- a/app/assets/javascripts/issues/list/constants.js +++ b/app/assets/javascripts/issues/list/constants.js @@ -75,6 +75,10 @@ export const NORMAL_FILTER = 'normalFilter'; export const SPECIAL_FILTER = 'specialFilter'; export const ALTERNATIVE_FILTER = 'alternativeFilter'; +export const ISSUES_VIEW_TYPE_KEY = 'issuesViewType'; +export const ISSUES_LIST_VIEW_KEY = 'List'; +export const ISSUES_GRID_VIEW_KEY = 'Grid'; + export const i18n = { actionsLabel: __('Actions'), calendarLabel: __('Subscribe to calendar'), @@ -116,6 +120,8 @@ export const i18n = { upvotes: __('Upvotes'), titles: __('Titles'), descriptions: __('Descriptions'), + listLabel: __('List'), + gridLabel: __('Grid'), }; export const urlSortParams = { diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_grid.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_grid.vue new file mode 100644 index 00000000000..0ada1d8a6ae --- /dev/null +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_grid.vue @@ -0,0 +1 @@ +<template><div></div></template> diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue index 95108933a0b..4023337a1cb 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue @@ -6,12 +6,14 @@ import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import issuableEventHub from '~/issues/list/eventhub'; import { DEFAULT_SKELETON_COUNT, PAGE_SIZE_STORAGE_KEY } from '../constants'; import IssuableBulkEditSidebar from './issuable_bulk_edit_sidebar.vue'; import IssuableItem from './issuable_item.vue'; import IssuableTabs from './issuable_tabs.vue'; +import IssuableGrid from './issuable_grid.vue'; const VueDraggable = () => import('vuedraggable'); @@ -30,12 +32,14 @@ export default { IssuableTabs, FilteredSearchBar, IssuableItem, + IssuableGrid, IssuableBulkEditSidebar, GlPagination, VueDraggable, PageSizeSelector, LocalStorageSync, }, + mixins: [glFeatureFlagMixin()], props: { namespace: { type: String, @@ -194,6 +198,11 @@ export default { required: false, default: false, }, + isGridView: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -229,6 +238,9 @@ export default { issuablesWrapper() { return this.isManualOrdering ? VueDraggable : 'ul'; }, + gridViewFeatureEnabled() { + return Boolean(this.glFeatures?.issuesGridView); + }, }, watch: { issuables(list) { @@ -342,7 +354,7 @@ export default { <template v-else> <component :is="issuablesWrapper" - v-if="issuables.length > 0" + v-if="issuables.length > 0 && !isGridView" class="content-list issuable-list issues-list" :class="{ 'manual-ordering': isManualOrdering }" v-bind="$options.vueDraggableAttributes" @@ -382,6 +394,9 @@ export default { </template> </issuable-item> </component> + <div v-else-if="issuables.length > 0 && isGridView"> + <issuable-grid /> + </div> <slot v-else name="empty-state"></slot> </template> diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index d2f65104d86..182d7aee060 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -36,6 +36,7 @@ class GroupsController < Groups::ApplicationController push_frontend_feature_flag(:or_issuable_queries, group) push_frontend_feature_flag(:frontend_caching, group) push_force_frontend_feature_flag(:work_items, group.work_items_feature_flag_enabled?) + push_frontend_feature_flag(:issues_grid_view) end before_action only: :merge_requests do diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 642d5943854..ac12dcb6f49 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -50,6 +50,7 @@ class Projects::IssuesController < Projects::ApplicationController push_force_frontend_feature_flag(:content_editor_on_issues, project&.content_editor_on_issues_feature_flag_enabled?) push_frontend_feature_flag(:service_desk_new_note_email_native_attachments, project) push_frontend_feature_flag(:saved_replies, current_user) + push_frontend_feature_flag(:issues_grid_view) end before_action only: [:index, :show] do diff --git a/app/graphql/mutations/environments/create.rb b/app/graphql/mutations/environments/create.rb index a6386e3c0b1..271585eb06c 100644 --- a/app/graphql/mutations/environments/create.rb +++ b/app/graphql/mutations/environments/create.rb @@ -30,6 +30,11 @@ module Mutations required: false, description: 'Tier of the environment.' + argument :cluster_agent_id, + ::Types::GlobalIDType[::Clusters::Agent], + required: false, + description: 'Cluster agent of the environment.' + field :environment, Types::EnvironmentType, null: true, @@ -38,6 +43,8 @@ module Mutations def resolve(project_path:, **kwargs) project = authorized_find!(project_path) + kwargs[:cluster_agent] = GitlabSchema.find_by_gid(kwargs.delete(:cluster_agent_id))&.sync + response = ::Environments::CreateService.new(project, current_user, kwargs).execute if response.success? diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 1d0ce594f63..e830594af11 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -4,7 +4,7 @@ module Awardable extend ActiveSupport::Concern included do - has_many :award_emoji, -> { includes(:user).order(:id) }, as: :awardable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :award_emoji, -> { includes(:user).order(:id) }, as: :awardable, inverse_of: :awardable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent if self < Participable # By default we always load award_emoji user association diff --git a/app/models/work_items/widgets/base.rb b/app/models/work_items/widgets/base.rb index b54b84f1e1b..a8b1b3f9a59 100644 --- a/app/models/work_items/widgets/base.rb +++ b/app/models/work_items/widgets/base.rb @@ -16,9 +16,13 @@ module WorkItems end def self.callback_class - Issuable::Callbacks.const_get(name.demodulize, false) + WorkItems::Callbacks.const_get(name.demodulize, false) rescue NameError - nil + begin + Issuable::Callbacks.const_get(name.demodulize, false) + rescue NameError + nil + end end def type diff --git a/app/services/environments/create_service.rb b/app/services/environments/create_service.rb index 81fedf4b6d8..760c8a6e306 100644 --- a/app/services/environments/create_service.rb +++ b/app/services/environments/create_service.rb @@ -2,6 +2,8 @@ module Environments class CreateService < BaseService + ALLOWED_ATTRIBUTES = %i[name external_url tier cluster_agent].freeze + def execute unless can?(current_user, :create_environment, project) return ServiceResponse.error( @@ -10,7 +12,13 @@ module Environments ) end - environment = project.environments.create(**params) + if unauthorized_cluster_agent? + return ServiceResponse.error( + message: _('Unauthorized to access the cluster agent in this project'), + payload: { environment: nil }) + end + + environment = project.environments.create(**params.slice(*ALLOWED_ATTRIBUTES)) if environment.persisted? ServiceResponse.success(payload: { environment: environment }) @@ -21,5 +29,16 @@ module Environments ) end end + + private + + def unauthorized_cluster_agent? + return false unless params[:cluster_agent] + + ::Clusters::Agents::Authorizations::UserAccess::Finder + .new(current_user, agent: params[:cluster_agent], project: project) + .execute + .empty? + end end end diff --git a/app/services/issuable/callbacks/base.rb b/app/services/issuable/callbacks/base.rb index 3fabce2c949..368dd76c16c 100644 --- a/app/services/issuable/callbacks/base.rb +++ b/app/services/issuable/callbacks/base.rb @@ -12,6 +12,7 @@ module Issuable end def after_initialize; end + def before_update; end def after_update_commit; end def after_save_commit; end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index e9312bd6b31..3b007d4dba7 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -314,16 +314,19 @@ class IssuableBaseService < ::BaseContainerService before_update(issuable) - # Do not touch when saving the issuable if only changes position within a list. We should call - # this method at this point to capture all possible changes. - should_touch = update_timestamp?(issuable) - - issuable.updated_by = current_user if should_touch # We have to perform this check before saving the issuable as Rails resets # the changed fields upon calling #save. update_project_counters = issuable.project && update_project_counter_caches?(issuable) issuable_saved = issuable.with_transaction_returning_status do + @callbacks.each(&:before_update) + + # Do not touch when saving the issuable if only changes position within a list. We should call + # this method at this point to capture all possible changes. + should_touch = update_timestamp?(issuable) + + issuable.updated_by = current_user if should_touch + transaction_update(issuable, { save_with_touch: should_touch }) end diff --git a/app/services/work_items/callbacks/award_emoji.rb b/app/services/work_items/callbacks/award_emoji.rb new file mode 100644 index 00000000000..6344813d4b9 --- /dev/null +++ b/app/services/work_items/callbacks/award_emoji.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module WorkItems + module Callbacks + class AwardEmoji < Base + def before_update + return unless params.present? && params.key?(:name) && params.key?(:action) + return unless has_permission?(:award_emoji) + + execute_emoji_service(params[:action], params[:name]) + end + + private + + def execute_emoji_service(action, name) + class_name = { + add: ::AwardEmojis::AddService, + remove: ::AwardEmojis::DestroyService + } + + raise_error(invalid_action_error(action)) unless class_name.key?(action) + + result = class_name[action].new(work_item, name, current_user).execute + + raise_error(result[:message]) if result[:status] == :error + end + + def invalid_action_error(key) + format(_("%{key} is not a valid action."), key: key) + end + end + end +end diff --git a/app/services/work_items/callbacks/base.rb b/app/services/work_items/callbacks/base.rb new file mode 100644 index 00000000000..c91e2b37d10 --- /dev/null +++ b/app/services/work_items/callbacks/base.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module WorkItems + module Callbacks + class Base < Issuable::Callbacks::Base + alias_method :work_item, :issuable + + def raise_error(message) + raise ::WorkItems::Widgets::BaseService::WidgetError, message + end + end + end +end diff --git a/app/services/work_items/widgets/award_emoji_service/update_service.rb b/app/services/work_items/widgets/award_emoji_service/update_service.rb deleted file mode 100644 index 7c58c0c9af9..00000000000 --- a/app/services/work_items/widgets/award_emoji_service/update_service.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module WorkItems - module Widgets - module AwardEmojiService - class UpdateService < WorkItems::Widgets::BaseService - def before_update_in_transaction(params:) - return unless params.present? && params.key?(:name) && params.key?(:action) - return unless has_permission?(:award_emoji) - - service_response!(service_result(params[:action], params[:name])) - end - - private - - def service_result(action, name) - class_name = { - add: ::AwardEmojis::AddService, - remove: ::AwardEmojis::DestroyService - } - - return invalid_action_error(action) unless class_name.key?(action) - - class_name[action].new(work_item, name, current_user).execute - end - - def invalid_action_error(key) - error(format(_("%{key} is not a valid action."), key: key)) - end - end - end - end -end diff --git a/config/feature_flags/development/issues_grid_view.yml b/config/feature_flags/development/issues_grid_view.yml new file mode 100644 index 00000000000..310b725d6c8 --- /dev/null +++ b/config/feature_flags/development/issues_grid_view.yml @@ -0,0 +1,8 @@ +--- +name: issues_grid_view +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113012 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393152 +milestone: '16.0' +type: development +group: group::project management +default_enabled: false
\ No newline at end of file diff --git a/doc/.vale/gitlab/Uppercase.yml b/doc/.vale/gitlab/Uppercase.yml index 1948842d026..4730184b950 100644 --- a/doc/.vale/gitlab/Uppercase.yml +++ b/doc/.vale/gitlab/Uppercase.yml @@ -57,6 +57,7 @@ exceptions: - DHCP - DML - DNS + - DSN - DOM - DORA - DSA diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 3523721c342..f8cab0c605e 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -36,7 +36,7 @@ The following metrics are available: | Metric | Type | Since | Description | Labels | | :--------------------------------------------------------------- | :---------- | ------: | :-------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------- | -| `gitlab_cache_misses_total` | Counter | 10.2 | Cache read miss | `controller`, `action`, `store` | +| `gitlab_cache_misses_total` | Counter | 10.2 | Cache read miss | `controller`, `action` | | `gitlab_cache_operation_duration_seconds` | Histogram | 10.2 | Cache access time | `operation`, `store` | | `gitlab_cache_operations_total` | Counter | 12.2 | Cache operations by controller or action | `controller`, `action`, `operation`, `store` | | `gitlab_cache_read_multikey_count` | Histogram | 15.7 | Count of keys in multi-key cache read operations | `controller`, `action`, `store` | @@ -63,8 +63,8 @@ The following metrics are available: | `gitlab_transaction_cache_<key>_duration_total` | Counter | 10.2 | Counter for total time (seconds) spent in Rails cache calls (per key) | | | `gitlab_transaction_cache_count_total` | Counter | 10.2 | Counter for total Rails cache calls (aggregate) | | | `gitlab_transaction_cache_duration_total` | Counter | 10.2 | Counter for total time (seconds) spent in Rails cache calls (aggregate) | | -| `gitlab_transaction_cache_read_hit_count_total` | Counter | 10.2 | Counter for cache hits for Rails cache calls | `controller`, `action`, `store` | -| `gitlab_transaction_cache_read_miss_count_total` | Counter | 10.2 | Counter for cache misses for Rails cache calls | `controller`, `action`, `store` | +| `gitlab_transaction_cache_read_hit_count_total` | Counter | 10.2 | Counter for cache hits for Rails cache calls | `controller`, `action` | +| `gitlab_transaction_cache_read_miss_count_total` | Counter | 10.2 | Counter for cache misses for Rails cache calls | `controller`, `action` | | `gitlab_transaction_duration_seconds` | Histogram | 10.2 | Duration for successful requests (`gitlab_transaction_*` metrics) | `controller`, `action` | | `gitlab_transaction_event_build_found_total` | Counter | 9.4 | Counter for build found for API /jobs/request | | | `gitlab_transaction_event_build_invalid_total` | Counter | 9.4 | Counter for build invalid due to concurrency conflict for API /jobs/request | | diff --git a/doc/api/environments.md b/doc/api/environments.md index 3cbb6076300..501fec70dd6 100644 --- a/doc/api/environments.md +++ b/doc/api/environments.md @@ -299,7 +299,7 @@ PUT /projects/:id/environments/:environments_id | `tier` | string | no | The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`. | ```shell -curl --request PUT --data "name=staging&external_url=https://staging.gitlab.example.com" \ +curl --request PUT --data "external_url=https://staging.gitlab.example.com" \ --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments/1" ``` diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 39ee028faa0..c27075a9a53 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -3077,6 +3077,7 @@ Input type: `EnvironmentCreateInput` | Name | Type | Description | | ---- | ---- | ----------- | | <a id="mutationenvironmentcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationenvironmentcreateclusteragentid"></a>`clusterAgentId` | [`ClustersAgentID`](#clustersagentid) | Cluster agent of the environment. | | <a id="mutationenvironmentcreateexternalurl"></a>`externalUrl` | [`String`](#string) | External URL of the environment. | | <a id="mutationenvironmentcreatename"></a>`name` | [`String!`](#string) | Name of the environment. | | <a id="mutationenvironmentcreateprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project. | diff --git a/doc/development/database/adding_database_indexes.md b/doc/development/database/adding_database_indexes.md index 7b29b1b14de..23a12413975 100644 --- a/doc/development/database/adding_database_indexes.md +++ b/doc/development/database/adding_database_indexes.md @@ -429,7 +429,7 @@ Use the asynchronous index helpers on your local environment to test changes for For very large tables, index destruction can be a challenge to manage. While `remove_concurrent_index` removes indexes in a way that does not block ordinary traffic, it can still be problematic if index destruction runs for -during `autovacuum`. Necessary database operations like `autovacuum` cannot run, and +many hours. Necessary database operations like `autovacuum` cannot run, and the deployment process on GitLab.com is blocked while waiting for index destruction to finish. diff --git a/doc/development/service_ping/implement.md b/doc/development/service_ping/implement.md index 2cabd6c40e1..0677e8febf3 100644 --- a/doc/development/service_ping/implement.md +++ b/doc/development/service_ping/implement.md @@ -339,8 +339,6 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd/) and [P Name format for Redis HLL events `{hll_counters}_<name>` - [See Metric name](metrics_dictionary.md#metric-name) for a complete guide on metric naming suggestion. - Example names: `users_creating_epics`, `users_triggering_security_scans`. - `aggregation`: may be set to a `:daily` or `:weekly` key. Defines how counting data is stored in Redis. diff --git a/doc/development/service_ping/metrics_dictionary.md b/doc/development/service_ping/metrics_dictionary.md index debe1cca37e..d53400276d0 100644 --- a/doc/development/service_ping/metrics_dictionary.md +++ b/doc/development/service_ping/metrics_dictionary.md @@ -32,7 +32,7 @@ Each metric is defined in a separate YAML file consisting of a number of fields: | Field | Required | Additional information | |---------------------|----------|----------------------------------------------------------------| | `key_path` | yes | JSON key path for the metric, location in Service Ping payload. | -| `name` | no | Metric name suggestion. Can replace the last part of `key_path`. | +| `name` (deprecated) | no | Metric name suggestion. Does not have any impact on the Service Ping payload, only serves as documentation. | | `description` | yes | | | `product_section` | yes | The [section](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/data/sections.yml). | | `product_stage` | yes | The [stage](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml) for the metric. | @@ -71,7 +71,11 @@ NOTE: We can't control what the metric's `key_path` is, because some of them are generated dynamically in `usage_data.rb`. For example, see [Redis HLL metrics](implement.md#redis-hll-counters). -### Metric name +### Metric name (deprecated) + +WARNING: +This feature was deprecated in GitLab 16.1 +and is planned for [removal](https://gitlab.com/gitlab-org/gitlab/-/issues/411602) in 16.2. To improve metric discoverability by a wider audience, each metric with instrumentation added at an appointed `key_path` receives a `name` attribute @@ -86,7 +90,11 @@ Metric name suggestions can contain two types of elements: For a metric name to be valid, it must not include any prompt, and fixed suggestions must not be changed. -#### Generate a metric name suggestion +#### Generate a metric name suggestion (deprecated) + +WARNING: +This feature was deprecated in GitLab 16.1 +and is planned for [removal](https://gitlab.com/gitlab-org/gitlab/-/issues/411602) in 16.2. The metric YAML generator can suggest a metric name for you. To generate a metric name suggestion, first instrument the metric at the provided `key_path`. @@ -149,7 +157,11 @@ We use the following categories to classify a metric: An aggregate metric is a metric that is the sum of two or more child metrics. Service Ping uses the data category of the aggregate metric to determine whether or not the data is included in the reported Service Ping payload. -### Metric name suggestion examples +### Metric name suggestion examples (deprecated) + +WARNING: +This feature was deprecated in GitLab 16.1 +and is planned for [removal](https://gitlab.com/gitlab-org/gitlab/-/issues/411602) in 16.2. #### Metric with `data_source: database` diff --git a/doc/development/service_ping/review_guidelines.md b/doc/development/service_ping/review_guidelines.md index feb85a491ad..71c16820e23 100644 --- a/doc/development/service_ping/review_guidelines.md +++ b/doc/development/service_ping/review_guidelines.md @@ -53,8 +53,6 @@ are regular backend changes. - Perform a first-pass review on the merge request and suggest improvements to the author. - Check the [metrics location](metrics_dictionary.md#metric-key_path) in the Service Ping JSON payload. -- Suggest that the author checks the [naming suggestion](metrics_dictionary.md#generate-a-metric-name-suggestion) while - generating the metric's YAML definition. - Add the `~database` label and ask for a [database review](../database_review.md) for metrics that are based on Database. - Add `~Data Warehouse::Impact Check` for any database metric that has a query change. Changes in queries can affect [data operations](https://about.gitlab.com/handbook/business-technology/data-team/how-we-work/triage/#gitlabcom-db-structure-changes). diff --git a/doc/development/work_items_widgets.md b/doc/development/work_items_widgets.md index bafceccdafe..6c453f49db1 100644 --- a/doc/development/work_items_widgets.md +++ b/doc/development/work_items_widgets.md @@ -146,18 +146,24 @@ You can update widgets using custom fine-grained mutations (for example, `WorkIt ### Widget callbacks When updating the widget together with the work item's mutation, backend code should be implemented using -callback classes that inherit from `Issuable::Callbacks::Base`. These classes have callback methods +callback classes that inherit from `WorkItems::Callbacks::Base`. These classes have callback methods that are named similar to ActiveRecord callbacks and behave similarly. -Callback classes with the same name as the widget are automatically used. For example, `Issuable::Callbacks::Milestone` -is called when the work item has the milestone widget. To use a different class, you can override the `callback_class` +Callback classes with the same name as the widget are automatically used. For example, `WorkItems::Callbacks::AwardEmoji` +is called when the work item has the `AwardEmoji` widget. To use a different class, you can override the `callback_class` class method. +When a callback class is also used for other issuables like merge requests or epics, define the class under `Issuable::Callbacks` +and add the class to the list in `IssuableBaseService#available_callbacks`. These are executed for both work item updates and +legacy issue, merge request, or epic updates. + #### Available callbacks - `after_initialize` is called after the work item is initialized by the `BuildService` and before the work item is saved by the `CreateService` and `UpdateService`. This callback runs outside the creation or update database transaction. +- `before_update` is called before the work item is saved by the `UpdateService`. This callback runs + within the update database transaction. - `after_update_commit` is called after the DB update transaction is committed by the `UpdateService`. - `after_save_commit` is called after the creation or DB update transaction is committed by the `CreateService` or `UpdateService`. diff --git a/doc/operations/error_tracking.md b/doc/operations/error_tracking.md index d38210dab8d..224f04ad24f 100644 --- a/doc/operations/error_tracking.md +++ b/doc/operations/error_tracking.md @@ -26,7 +26,7 @@ For error tracking to work, you need: Whatever backend you choose, the [error tracking UI](#error-tracking-list) is the same. -## Integrated error tracking +## Integrated error tracking **(FREE SAAS)** This guide provides you with basics of setting up error tracking for your project, using examples from different languages. @@ -42,64 +42,59 @@ According to the Sentry [data model](https://develop.sentry.dev/sdk/envelopes/#d - [User feedback](https://develop.sentry.dev/sdk/envelopes/#user-feedback) (also known as user report) - [Client report](https://develop.sentry.dev/sdk/client-reports/) -### Enable error tracking for your project +### Enable error tracking for a project Regardless of the programming language you use, you first need to enable error tracking for your GitLab project. -The `gitlab.com` instance is used in this guide. +The `GitLab.com` instance is used in this guide. -> This guide assumes that you already have a project for which you want to enable error tracking. Refer to [the GitLab documentation](../user/project/index.md) if you need to create a new one. +Prerequisites: -To enable error tracking: +- You have a project for which you want to enable error tracking. To learn how to create a new one, see [Create a project](../user/project/index.md). -1. In your project, go to **Settings > Monitor**. Expand the `Error Tracking` tab: +To enable error tracking with GitLab as the backend: - ![MonitorTabPreEnable](img/Monitor_tab-pre-enable.png) - -1. Enable Error Tracking with GitLab as backend: - - ![MonitorTabPostEnable](img/Monitor_tab-post-enable.png) - -1. Select `Save Changes`. +1. In your project, go to **Settings > Monitor**. +1. Expand **Error Tracking**. +1. Under **Enable error tracking**, select the **Active** checkbox. +1. Under **Error tracking backend**, select **GitLab**. +1. Select **Save changes**. -1. Copy the DSN string. You need it for configuring your SDK implementation. +1. Copy the Data Source Name (DSN) string. You need it for configuring your SDK implementation. ## Error tracking list -Once your application has emitted errors to the Error Tracking API through the Sentry SDK, +After your application has emitted errors to the Error Tracking API through the Sentry SDK, they should be available under the **Monitor > Error Tracking** tab/section. -![MonitorListErrors](img/Monitor-list_errors.png) +![MonitorListErrors](img/list_errors_v16_0.png) ## Error tracking details -![MonitorDetailErrors](img/Monitor-detail_errors.png) +In the Error Details view you can see more details of the exception, including number of occurrences, +users affected, first seen, and last seen dates. -The Error Details view allows you to see more details of the Exception, including number of occurrences, users affected, first seen, and last seen. +You can also review the stack trace. -You can also review the Stack trace. +![MonitorDetailErrors](img/detail_errors_v16_0.png) ## Emit errors ### Supported language SDKs & Sentry types -In the following table, you can see a list of all event types available through Sentry SDK, and whether they are currently supported by GitLab Error Tracking. +In the following table, you can see a list of all event types available through Sentry SDK, and whether they are supported by GitLab Error Tracking. -<!-- markdownlint-disable MD044 --> +| Language | Tested SDK client and version | Endpoint | Supported item types | +| -------- | ------------------------------- | ---------- | --------------------------------- | +| Go | `sentry-go/0.20.0` | `store` | `exception`, `message` | +| Java | `sentry.java:6.18.1` | `envelope` | `exception`, `message` | +| NodeJS | `sentry.javascript.node:7.38.0` | `envelope` | `exception`, `message` | +| PHP | `sentry.php/3.18.0` | `store` | `exception`, `message` | +| Python | `sentry.python/1.21.0` | `envelope` | `exception`, `message`, `session` | +| Ruby | `sentry.ruby:5.9.0` | `envelope` | `exception`, `message` | +| Rust | `sentry.rust/0.31.0` | `envelope` | `exception`, `message`, `session` | -| LANGUAGE | TESTED SDK CLIENT/VERSION | ENDPOINT | SUPPORTED ITEM TYPES | -| --- | --- | --- | --- | -| Go | sentry-go/0.20.0 | store | exception, message | -| Java | sentry.java:6.18.1 | envelope | exception, message | -| NodeJS | sentry.javascript.node:7.38.0 | envelope | exception, message | -| PHP | sentry.php/3.18.0 | store | exception, message | -| Python | sentry.python/1.21.0 | envelope | exception, message, session | -| Ruby | sentry.ruby:5.9.0 | envelope | exception, message | -| Rust | sentry.rust/0.31.0 | envelope | exception, message, session | - -<!-- markdownlint-enable --> - -For a detailed version of this matrix, see [this issue](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/1737). +For a detailed version of this table, see [this issue](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/1737). ## Usage examples @@ -112,34 +107,34 @@ see [Sentry SDK's documentation](https://docs.sentry.io/) specific to the used l ## Rotate generated DSN -The Sentry DSN (client key) is a secret and it should not be exposed to the public. +The Sentry Data Source Name, or DSN, (client key) is a secret and it should not be exposed to the public. In case of a leak, rotate the Sentry DSN by following these steps: -1. [Create an access token](/ee/user/profile/personal_access_tokens.md#create-a-personal-access-token) -by selecting your profile picture in GitLab.com. -Then select Preferences, and then Access Token. Make sure you add API scope. -1. Using the [error tracking API](/ee/api/error_tracking.md), -create a new Sentry DSN: +1. [Create an access token](../user/profile/personal_access_tokens.md#create-a-personal-access-token) + by selecting your profile picture in GitLab.com. + Then select Preferences, and then Access Token. Make sure you add API scope. +1. Using the [error tracking API](../api/error_tracking.md), + create a new Sentry DSN: -```shell -curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" ---header "Content-Type: application/json" \ - "https://gitlab.example.com/api/v4/projects/<your_project_number>/error_tracking/client_keys" -``` + ```shell + curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" + --header "Content-Type: application/json" \ + "https://gitlab.example.com/api/v4/projects/<your_project_number>/error_tracking/client_keys" + ``` -- Get the available client keys (Sentry DSNs). - Ensure that the newly created Sentry DSN is in place. - Then note down the key ID of the old client key: +1. Get the available client keys (Sentry DSNs). + Ensure that the newly created Sentry DSN is in place. + Then note down the key ID of the old client key: -```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<your_project_number>/error_tracking/client_keys" -``` + ```shell + curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<your_project_number>/error_tracking/client_keys" + ``` -- Delete the old client key. +1. Delete the old client key. -```shell -curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<your_project_number>/error_tracking/client_keys/<key_id>" -``` + ```shell + curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<your_project_number>/error_tracking/client_keys/<key_id>" + ``` ## Debug SDK issues @@ -158,51 +153,53 @@ so users can view a list of Sentry errors in GitLab. You can sign up to the cloud-hosted [Sentry](https://sentry.io) or deploy your own [on-premise instance](https://github.com/getsentry/onpremise/). -### Enabling Sentry +### Enable Sentry integration for a project + +GitLab provides a way to connect Sentry to your project. + +Prerequisites: -GitLab provides a way to connect Sentry to your project. You need at -least Maintainer [permissions](../user/permissions.md) to enable the Sentry integration. +- You must have at least the Developer role for the project. + +To enable the Sentry integration: 1. Sign up to Sentry.io or [deploy your own](#deploying-sentry) Sentry instance. -1. [Create](https://docs.sentry.io/product/sentry-basics/guides/integrate-frontend/create-new-project/) -a new Sentry project. For each GitLab project that you want to integrate, -you should create a new Sentry project. +1. [Create a new Sentry project](https://docs.sentry.io/product/sentry-basics/guides/integrate-frontend/create-new-project/). + For each GitLab project that you want to integrate, you should create a new Sentry project. 1. Find or generate a [Sentry auth token](https://docs.sentry.io/api/auth/#auth-tokens). For the SaaS version of Sentry, you can find or generate the auth token at [https://sentry.io/api/](https://sentry.io/api/). You should give the token at least the following scopes: `project:read`, `event:read`, and `event:write` (for resolving events). -1. In GitLab, enable error tracking: +1. In GitLab, enable and configure Error Tracking: 1. On the top bar, select **Main menu > Projects** and find your project. 1. On the left sidebar, select **Monitor > Error Tracking**. - 1. Select **Enable error tracking**. -1. In GitLab, ensure error tracking is active. - 1. On the left sidebar, select **Settings > Monitor**. - 1. Expand **Error Tracking**. - 1. Ensure the **Active** checkbox is selected. -1. In the **Sentry API URL** box, enter your Sentry hostname. For example, -enter `https://sentry.example.com`. -For the SaaS version of Sentry, the hostname is `https://sentry.io`. -1. In the **Auth Token** box, enter the token you previously generated. -1. To test the connection to Sentry and populate the **Project** dropdown list, -select **Connect**. -1. From the **Project** list, choose a Sentry project to link to your GitLab project. -1. Select **Save changes**. + 1. Under **Enable error tracking**, select the **Active** checkbox. + 1. Under **Error tracking backend**, select **Sentry**. + 1. Under **Sentry API URL**, enter your Sentry hostname. For example, + enter `https://sentry.example.com`. + For the SaaS version of Sentry, the hostname is `https://sentry.io`. + 1. Under **Auth Token**, enter the token you previously generated. + 1. To test the connection to Sentry and populate the **Project** dropdown list, + select **Connect**. + 1. From the **Project** list, choose a Sentry project to link to your GitLab project. + 1. Select **Save changes**. You can now visit **Monitor > Error Tracking** in your project's sidebar to [view a list](#error-tracking-list) of Sentry errors. -### Enabling GitLab issues links +### Sentry's GitLab integration You might also want to enable Sentry's GitLab integration by following the steps -in the [Sentry documentation](https://docs.sentry.io/product/integrations/gitlab/) +in the [Sentry documentation](https://docs.sentry.io/product/integrations/gitlab/). ### Enable GitLab Runner To configure GitLab Runner with Sentry, you must add the value for `sentry_dsn` -to your GitLab Runner's `config.toml` configuration file, as referenced in -[GitLab Runner Advanced Configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html). -While setting up Sentry, select **Go** if you're asked for the project type. +to your runner's `config.toml` configuration file, as referenced in +[Advanced configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html). + +If you're asked for the project type while setting up Sentry, select **Go**. If you see the following error in your GitLab Runner logs, then you should specify the deprecated diff --git a/doc/operations/img/Monitor-detail_errors.png b/doc/operations/img/Monitor-detail_errors.png Binary files differdeleted file mode 100644 index ac4234a3811..00000000000 --- a/doc/operations/img/Monitor-detail_errors.png +++ /dev/null diff --git a/doc/operations/img/Monitor-list_errors.png b/doc/operations/img/Monitor-list_errors.png Binary files differdeleted file mode 100644 index 0f1feca6329..00000000000 --- a/doc/operations/img/Monitor-list_errors.png +++ /dev/null diff --git a/doc/operations/img/Monitor_tab-post-enable.png b/doc/operations/img/Monitor_tab-post-enable.png Binary files differdeleted file mode 100644 index 91f0a0d8d27..00000000000 --- a/doc/operations/img/Monitor_tab-post-enable.png +++ /dev/null diff --git a/doc/operations/img/Monitor_tab-pre-enable.png b/doc/operations/img/Monitor_tab-pre-enable.png Binary files differdeleted file mode 100644 index 2984110a7d7..00000000000 --- a/doc/operations/img/Monitor_tab-pre-enable.png +++ /dev/null diff --git a/doc/operations/img/detail_errors_v16_0.png b/doc/operations/img/detail_errors_v16_0.png Binary files differnew file mode 100644 index 00000000000..3b63c565e56 --- /dev/null +++ b/doc/operations/img/detail_errors_v16_0.png diff --git a/doc/operations/img/list_errors_v16_0.png b/doc/operations/img/list_errors_v16_0.png Binary files differnew file mode 100644 index 00000000000..0ddf7f93616 --- /dev/null +++ b/doc/operations/img/list_errors_v16_0.png diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index a68ad604989..9b299b46f75 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -129,12 +129,14 @@ If you are running a self-managed instance of GitLab, ### Configure GitLab Pages in a Helm Chart (Kubernetes) instance To configure GitLab Pages on instances deployed via Helm chart (Kubernetes), use either: - -- [The `gitlab-pages` subchart](https://docs.gitlab.com/charts/charts/gitlab/gitlab-pages/). -- [An external GitLab Pages instance](https://docs.gitlab.com/charts/advanced/external-gitlab-pages/). + +- [The `gitlab-pages` subchart](https://docs.gitlab.com/charts/charts/gitlab/gitlab-pages/). +- [An external GitLab Pages instance](https://docs.gitlab.com/charts/advanced/external-gitlab-pages/). ## Security for GitLab Pages +### Namespaces that contain `.` + If your username is `example`, your GitLab Pages website is located at `example.gitlab.io`. GitLab allows usernames to contain a `.`, so a user named `bar.example` could create a GitLab Pages website `bar.example.gitlab.io` that effectively is a subdomain of your @@ -153,3 +155,9 @@ document.cookie = "key=value;domain=example.gitlab.io"; This issue doesn't affect users with a custom domain, or users who don't set any cookies manually with JavaScript. + +### Shared cookies + +By default, every project in a group shares the same domain, for example, `group.gitlab.io`. This means that cookies are also shared for all projects in a group. + +To ensure each project uses different cookies, enable the Pages [unique domains](introduction.md#enable-unique-domains) feature for your project. diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index 05d0b461fea..f7d273d198d 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -94,6 +94,25 @@ the group must be at the top level and not a subgroup. For [project websites](../../project/pages/getting_started_part_one.md#project-website-examples), you can create your project first and access it under `http(s)://namespace.example.io/projectname`. +## Enable unique domains + +> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9347) in GitLab 15.9 [with a flag](../../../administration/feature_flags.md) named `pages_unique_domain`. Disabled by default. +> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/388151) in GitLab 15.11. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, +ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `pages_unique_domain`. +On GitLab.com, by default this feature is available. + +By default, every project in a group shares the same domain, for example, `group.gitlab.io`. This means that cookies are also shared for all projects in a group. + +To ensure your project uses a unique Pages domain, enable the unique domains feature for the project: + +1. On the top bar, select **Main menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Pages**. +1. Select the **Use unique domain** checkbox. +1. Select **Save changes**. + ## Specific configuration options for Pages Learn how to set up GitLab CI/CD for specific use cases. diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb index 4639ea63622..b4e9e85a012 100644 --- a/lib/gitlab/metrics/subscribers/rails_cache.rb +++ b/lib/gitlab/metrics/subscribers/rails_cache.rb @@ -13,7 +13,7 @@ module Gitlab return unless current_transaction - labels = { store: extract_store_name(event) } + labels = { store: event.payload[:store].split('::').last } current_transaction.observe(:gitlab_cache_read_multikey_count, event.payload[:key].size, labels) do buckets [10, 50, 100, 1000] docstring 'Number of keys for mget in read_multi/fetch_multi' @@ -48,26 +48,23 @@ module Gitlab def cache_fetch_hit(event) return unless current_transaction - labels = { store: extract_store_name(event) } - current_transaction.increment(:gitlab_transaction_cache_read_hit_count_total, 1, labels) + current_transaction.increment(:gitlab_transaction_cache_read_hit_count_total, 1) end def cache_generate(event) return unless current_transaction - labels = { store: extract_store_name(event) } - - current_transaction.increment(:gitlab_cache_misses_total, 1, labels) do + current_transaction.increment(:gitlab_cache_misses_total, 1) do docstring 'Cache read miss' end - current_transaction.increment(:gitlab_transaction_cache_read_miss_count_total, 1, labels) + current_transaction.increment(:gitlab_transaction_cache_read_miss_count_total, 1) end def observe(key, event) return unless current_transaction - labels = { operation: key, store: extract_store_name(event) } + labels = { operation: key, store: event.payload[:store].split('::').last } current_transaction.increment(:gitlab_cache_operations_total, 1, labels) do docstring 'Cache operations' @@ -79,11 +76,6 @@ module Gitlab private - def extract_store_name(event) - # see payload documentation in https://guides.rubyonrails.org/active_support_instrumentation.html#active-support - event.payload[:store].to_s.split('::').last - end - def current_transaction ::Gitlab::Metrics::WebTransaction.current end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0576cb19803..b5e6e8e2cde 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -20933,6 +20933,9 @@ msgstr "" msgid "Gravatar enabled" msgstr "" +msgid "Grid" +msgstr "" + msgid "Group" msgstr "" diff --git a/rubocop/cop/background_migration/feature_category.rb b/rubocop/cop/background_migration/feature_category.rb index ec70b5baadf..c6611a65e49 100644 --- a/rubocop/cop/background_migration/feature_category.rb +++ b/rubocop/cop/background_migration/feature_category.rb @@ -17,7 +17,7 @@ module RuboCop "https://docs.gitlab.com/ee/development/feature_categorization/#batched-background-migrations" INVALID_FEATURE_CATEGORY_MSG = "'feature_category' is invalid. " \ - "List of valid ones can be found in #{FEATURE_CATEGORIES_FILE_PATH}" + "List of valid ones can be found in #{FEATURE_CATEGORIES_FILE_PATH}".freeze RESTRICT_ON_SEND = [:feature_category].freeze diff --git a/rubocop/cop/filename_length.rb b/rubocop/cop/filename_length.rb index 948f69cc88c..4144337f00f 100644 --- a/rubocop/cop/filename_length.rb +++ b/rubocop/cop/filename_length.rb @@ -7,8 +7,8 @@ module RuboCop FILEPATH_MAX_BYTES = 256 FILENAME_MAX_BYTES = 100 - MSG_FILEPATH_LEN = "This file path is too long. It should be #{FILEPATH_MAX_BYTES} or less" - MSG_FILENAME_LEN = "This file name is too long. It should be #{FILENAME_MAX_BYTES} or less" + MSG_FILEPATH_LEN = "This file path is too long. It should be #{FILEPATH_MAX_BYTES} or less".freeze + MSG_FILENAME_LEN = "This file name is too long. It should be #{FILENAME_MAX_BYTES} or less".freeze def on_new_investigation file_path = processed_source.file_path diff --git a/rubocop/cop/gitlab/event_store_subscriber.rb b/rubocop/cop/gitlab/event_store_subscriber.rb index 7e4cc3e66cc..04dc29ec400 100644 --- a/rubocop/cop/gitlab/event_store_subscriber.rb +++ b/rubocop/cop/gitlab/event_store_subscriber.rb @@ -32,8 +32,8 @@ module RuboCop # class EventStoreSubscriber < RuboCop::Cop::Base SUBSCRIBER_MODULE_NAME = 'Gitlab::EventStore::Subscriber' - FORBID_PERFORM_OVERRIDE = "Do not override `perform` in a `#{SUBSCRIBER_MODULE_NAME}`." - REQUIRE_HANDLE_EVENT = "A `#{SUBSCRIBER_MODULE_NAME}` must implement `#handle_event(event)`." + FORBID_PERFORM_OVERRIDE = "Do not override `perform` in a `#{SUBSCRIBER_MODULE_NAME}`.".freeze + REQUIRE_HANDLE_EVENT = "A `#{SUBSCRIBER_MODULE_NAME}` must implement `#handle_event(event)`.".freeze def_node_matcher :includes_subscriber?, <<~PATTERN (send nil? :include (const (const (const nil? :Gitlab) :EventStore) :Subscriber)) diff --git a/rubocop/cop/graphql/descriptions.rb b/rubocop/cop/graphql/descriptions.rb index 239f5b966a4..b096dfb148e 100644 --- a/rubocop/cop/graphql/descriptions.rb +++ b/rubocop/cop/graphql/descriptions.rb @@ -51,11 +51,12 @@ module RuboCop extend RuboCop::Cop::AutoCorrector MSG_STYLE_GUIDE_LINK = 'See the description style guide: https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#description-style-guide' - MSG_NO_DESCRIPTION = "Please add a `description` property. #{MSG_STYLE_GUIDE_LINK}" - MSG_NO_PERIOD = "`description` strings must end with a `.`. #{MSG_STYLE_GUIDE_LINK}" - MSG_BAD_START = "`description` strings should not start with \"A...\" or \"The...\". #{MSG_STYLE_GUIDE_LINK}" + MSG_NO_DESCRIPTION = "Please add a `description` property. #{MSG_STYLE_GUIDE_LINK}".freeze + MSG_NO_PERIOD = "`description` strings must end with a `.`. #{MSG_STYLE_GUIDE_LINK}".freeze + MSG_BAD_START = "`description` strings should not start with \"A...\" or \"The...\"."\ + " #{MSG_STYLE_GUIDE_LINK}".freeze MSG_CONTAINS_THIS = "`description` strings should not contain the demonstrative \"this\"."\ - " #{MSG_STYLE_GUIDE_LINK}" + " #{MSG_STYLE_GUIDE_LINK}".freeze def_node_matcher :graphql_describable?, <<~PATTERN (send nil? {:field :argument :value} ...) diff --git a/rubocop/cop/graphql/enum_names.rb b/rubocop/cop/graphql/enum_names.rb index 74847cb8d17..19ba426fdeb 100644 --- a/rubocop/cop/graphql/enum_names.rb +++ b/rubocop/cop/graphql/enum_names.rb @@ -34,9 +34,9 @@ module RuboCop module Graphql class EnumNames < RuboCop::Cop::Base SEE_SG_MSG = "See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#enums" - CLASS_NAME_SUFFIX_MSG = "Enum class names must end with `Enum`. #{SEE_SG_MSG}" - GRAPHQL_NAME_MISSING_MSG = "A `graphql_name` must be defined for a GraphQL enum. #{SEE_SG_MSG}" - GRAPHQL_NAME_WITH_ENUM_MSG = "The `graphql_name` must not contain the string \"Enum\". #{SEE_SG_MSG}" + CLASS_NAME_SUFFIX_MSG = "Enum class names must end with `Enum`. #{SEE_SG_MSG}".freeze + GRAPHQL_NAME_MISSING_MSG = "A `graphql_name` must be defined for a GraphQL enum. #{SEE_SG_MSG}".freeze + GRAPHQL_NAME_WITH_ENUM_MSG = "The `graphql_name` must not contain the string \"Enum\". #{SEE_SG_MSG}".freeze def_node_matcher :enum_subclass, <<~PATTERN (class $(const nil? _) (const {nil? cbase} /.*Enum$/) ...) diff --git a/rubocop/cop/migration/prevent_index_creation.rb b/rubocop/cop/migration/prevent_index_creation.rb index 185f36cb24b..4c39032eb0f 100644 --- a/rubocop/cop/migration/prevent_index_creation.rb +++ b/rubocop/cop/migration/prevent_index_creation.rb @@ -10,7 +10,7 @@ module RuboCop FORBIDDEN_TABLES = %i[ci_builds].freeze - MSG = "Adding new index to #{FORBIDDEN_TABLES.join(", ")} is forbidden, see https://gitlab.com/gitlab-org/gitlab/-/issues/332886" + MSG = "Adding new index to #{FORBIDDEN_TABLES.join(", ")} is forbidden, see https://gitlab.com/gitlab-org/gitlab/-/issues/332886".freeze def on_new_investigation super diff --git a/rubocop/cop/migration/versioned_migration_class.rb b/rubocop/cop/migration/versioned_migration_class.rb index 27f1c0e16a1..d078acedb37 100644 --- a/rubocop/cop/migration/versioned_migration_class.rb +++ b/rubocop/cop/migration/versioned_migration_class.rb @@ -12,10 +12,10 @@ module RuboCop DOC_LINK = "https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning" MSG_INHERIT = "Don't inherit from ActiveRecord::Migration or old versions of Gitlab::Database::Migration. " \ - "Use Gitlab::Database::Migration[2.1] instead. See #{DOC_LINK}." + "Use Gitlab::Database::Migration[2.1] instead. See #{DOC_LINK}.".freeze MSG_INCLUDE = "Don't include migration helper modules directly. " \ - "Inherit from Gitlab::Database::Migration[2.1] instead. See #{DOC_LINK}." + "Inherit from Gitlab::Database::Migration[2.1] instead. See #{DOC_LINK}.".freeze GITLAB_MIGRATION_CLASS = 'Gitlab::Database::Migration' ACTIVERECORD_MIGRATION_CLASS = 'ActiveRecord::Migration' diff --git a/rubocop/cop/migration/with_lock_retries_disallowed_method.rb b/rubocop/cop/migration/with_lock_retries_disallowed_method.rb index 5c96b38dcdc..1b0d5ed9324 100644 --- a/rubocop/cop/migration/with_lock_retries_disallowed_method.rb +++ b/rubocop/cop/migration/with_lock_retries_disallowed_method.rb @@ -31,7 +31,7 @@ module RuboCop create_trigger_to_sync_tables ].sort.freeze - MSG = "The method is not allowed to be called within the `with_lock_retries` block, the only allowed methods are: #{ALLOWED_MIGRATION_METHODS.join(', ')}" + MSG = "The method is not allowed to be called within the `with_lock_retries` block, the only allowed methods are: #{ALLOWED_MIGRATION_METHODS.join(', ')}".freeze MSG_ONLY_ONE_FK_ALLOWED = "Avoid adding more than one foreign key within the `with_lock_retries`. See https://docs.gitlab.com/ee/development/migration_style_guide.html#examples" def_node_matcher :send_node?, <<~PATTERN diff --git a/rubocop/cop/scalability/idempotent_worker.rb b/rubocop/cop/scalability/idempotent_worker.rb index 88b5d796def..fee9e952105 100644 --- a/rubocop/cop/scalability/idempotent_worker.rb +++ b/rubocop/cop/scalability/idempotent_worker.rb @@ -28,7 +28,7 @@ module RuboCop HELP_LINK = 'https://github.com/mperham/sidekiq/wiki/Best-Practices#2-make-your-job-idempotent-and-transactional' - MSG = <<~MSG + MSG = <<~MSG.freeze Avoid adding not idempotent workers. A worker is considered idempotent if: diff --git a/rubocop/cop/sidekiq_load_balancing/worker_data_consistency.rb b/rubocop/cop/sidekiq_load_balancing/worker_data_consistency.rb index badd81ff138..1d421a5d017 100644 --- a/rubocop/cop/sidekiq_load_balancing/worker_data_consistency.rb +++ b/rubocop/cop/sidekiq_load_balancing/worker_data_consistency.rb @@ -28,13 +28,13 @@ module RuboCop HELP_LINK = 'https://docs.gitlab.com/ee/development/sidekiq/worker_attributes.html#job-data-consistency-strategies' - MISSING_DATA_CONSISTENCY_MSG = <<~MSG + MISSING_DATA_CONSISTENCY_MSG = <<~MSG.freeze Should define data_consistency expectation. See #{HELP_LINK} for a more detailed explanation of these settings. MSG DISCOURAGE_ALWAYS_MSG = "Refrain from using `:always` if possible." \ - "See #{HELP_LINK} for a more detailed explanation of these settings." + "See #{HELP_LINK} for a more detailed explanation of these settings.".freeze def_node_search :application_worker?, <<~PATTERN `(send nil? :include (const nil? :ApplicationWorker)) diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index af24b547545..4f37717c933 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -11,7 +11,8 @@ import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_coun import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import { getIssuesCountsQueryResponse, @@ -34,6 +35,7 @@ import { issuableListTabs } from '~/vue_shared/issuable/list/constants'; import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue'; import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue'; import IssuesListApp from '~/issues/list/components/issues_list_app.vue'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue'; import { CREATED_DESC, @@ -135,6 +137,9 @@ describe('CE IssuesListApp component', () => { const findGlButton = () => wrapper.findComponent(GlButton); const findGlButtons = () => wrapper.findAllComponents(GlButton); const findIssuableList = () => wrapper.findComponent(IssuableList); + const findListViewTypeBtn = () => wrapper.findByTestId('list-view-type'); + const findGridtViewTypeBtn = () => wrapper.findByTestId('grid-view-type'); + const findViewTypeLocalStorageSync = () => wrapper.findAllComponents(LocalStorageSync).at(0); const findNewResourceDropdown = () => wrapper.findComponent(NewResourceDropdown); const findRssButton = () => wrapper.findByRole('menuitem', { name: IssuesListApp.i18n.rssLabel }); @@ -233,6 +238,7 @@ describe('CE IssuesListApp component', () => { hasPreviousPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasPreviousPage, hasNextPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasNextPage, }); + expect(findIssuableList().props('isGridView')).toBe(false); }); }); @@ -354,6 +360,37 @@ describe('CE IssuesListApp component', () => { }); }); + describe('header action buttons with the grid view enabled', () => { + beforeEach(() => { + wrapper = mountComponent({ + mountFn: shallowMountExtended, + provide: { + glFeatures: { + issuesGridView: true, + }, + }, + stubs: { + IssuableList: stubComponent(IssuableList, { + template: `<div><slot name="nav-actions" /></div>`, + }), + }, + }); + }); + + it('switch between list and grid', async () => { + findGridtViewTypeBtn().vm.$emit('click'); + await nextTick(); + + expect(findIssuableList().props('isGridView')).toBe(true); + expect(findViewTypeLocalStorageSync().props('value')).toBe('Grid'); + + findListViewTypeBtn().vm.$emit('click'); + await nextTick(); + expect(findIssuableList().props('isGridView')).toBe(false); + expect(findViewTypeLocalStorageSync().props('value')).toBe('List'); + }); + }); + describe('initial url params', () => { describe('page', () => { it('page_after is set from the url params', () => { diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js index ec975dfdcb5..68904603f40 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js @@ -7,6 +7,7 @@ import { TEST_HOST } from 'helpers/test_constants'; import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vue'; import IssuableListRoot from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; +import issuableGrid from '~/vue_shared/issuable/list/components/issuable_grid.vue'; import IssuableTabs from '~/vue_shared/issuable/list/components/issuable_tabs.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue'; @@ -43,6 +44,7 @@ describe('IssuableListRoot', () => { const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination); const findGlPagination = () => wrapper.findComponent(GlPagination); const findIssuableItem = () => wrapper.findComponent(IssuableItem); + const findIssuableGrid = () => wrapper.findComponent(issuableGrid); const findIssuableTabs = () => wrapper.findComponent(IssuableTabs); const findVueDraggable = () => wrapper.findComponent(VueDraggable); const findPageSizeSelector = () => wrapper.findComponent(PageSizeSelector); @@ -514,4 +516,18 @@ describe('IssuableListRoot', () => { expect(wrapper.emitted('page-size-change')).toEqual([[pageSize]]); }); }); + + describe('grid view issue', () => { + beforeEach(() => { + wrapper = createComponent({ + props: { + isGridView: true, + }, + }); + }); + + it('renders issuableGrid', () => { + expect(findIssuableGrid().exists()).toBe(true); + }); + }); }); diff --git a/spec/graphql/mutations/environments/create_spec.rb b/spec/graphql/mutations/environments/create_spec.rb index 8b40ecd6c81..c15f5cacade 100644 --- a/spec/graphql/mutations/environments/create_spec.rb +++ b/spec/graphql/mutations/environments/create_spec.rb @@ -12,10 +12,9 @@ RSpec.describe Mutations::Environments::Create, feature_category: :environment_m subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } describe '#resolve' do - subject { mutation.resolve(project_path: project.full_path, name: name, external_url: external_url) } + subject { mutation.resolve(project_path: project.full_path, **kwargs) } - let(:name) { 'production' } - let(:external_url) { 'https://gitlab.com/' } + let(:kwargs) { { name: 'production', external_url: 'https://gitlab.com/' } } context 'when service execution succeeded' do it 'returns no errors' do @@ -23,13 +22,13 @@ RSpec.describe Mutations::Environments::Create, feature_category: :environment_m end it 'creates the environment' do - expect(subject[:environment][:name]).to eq(name) - expect(subject[:environment][:external_url]).to eq(external_url) + expect(subject[:environment][:name]).to eq('production') + expect(subject[:environment][:external_url]).to eq('https://gitlab.com/') end end context 'when service cannot create the attribute' do - let(:external_url) { 'http://${URL}' } + let(:kwargs) { { name: 'production', external_url: 'http://${URL}' } } it 'returns an error' do expect(subject) @@ -40,6 +39,18 @@ RSpec.describe Mutations::Environments::Create, feature_category: :environment_m end end + context 'when setting cluster agent ID to the environment' do + let_it_be(:cluster_agent) { create(:cluster_agent, project: project) } + + let!(:authorization) { create(:agent_user_access_project_authorization, project: project, agent: cluster_agent) } + + let(:kwargs) { { name: 'production', cluster_agent_id: cluster_agent.to_global_id } } + + it 'sets the cluster agent to the environment' do + expect(subject[:environment].cluster_agent).to eq(cluster_agent) + end + end + context 'when user is reporter who does not have permission to access the environment' do let(:user) { reporter } diff --git a/spec/lib/gitlab/import/errors_spec.rb b/spec/lib/gitlab/import/errors_spec.rb index f89cb36bbb4..3b45af0618b 100644 --- a/spec/lib/gitlab/import/errors_spec.rb +++ b/spec/lib/gitlab/import/errors_spec.rb @@ -40,7 +40,6 @@ RSpec.describe Gitlab::Import::Errors, feature_category: :importers do "Author can't be blank", "Project does not match noteable project", "User can't be blank", - "Awardable can't be blank", "Name is not a valid emoji name" ) end diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb index 98f0fc5ea83..2d4c6d1cc56 100644 --- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb @@ -56,7 +56,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do it 'does not increment cache read miss total' do expect(transaction).not_to receive(:increment) - .with(:gitlab_cache_misses_total, 1, { store: store_label }) + .with(:gitlab_cache_misses_total, 1) subscriber.cache_read(event) end @@ -145,7 +145,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do it 'increments the cache_read_hit count' do expect(transaction).to receive(:increment) - .with(:gitlab_transaction_cache_read_hit_count_total, 1, { store: store_label }) + .with(:gitlab_transaction_cache_read_hit_count_total, 1) subscriber.cache_fetch_hit(event) end @@ -168,9 +168,9 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do end it 'increments the cache_fetch_miss count and cache_read_miss total' do - expect(transaction).to receive(:increment).with(:gitlab_cache_misses_total, 1, { store: store_label }) + expect(transaction).to receive(:increment).with(:gitlab_cache_misses_total, 1) expect(transaction).to receive(:increment) - .with(:gitlab_transaction_cache_read_miss_count_total, 1, { store: store_label }) + .with(:gitlab_transaction_cache_read_miss_count_total, 1) subscriber.cache_generate(event) end diff --git a/spec/services/environments/create_service_spec.rb b/spec/services/environments/create_service_spec.rb index e834e66dd9c..d7fdfd2a38e 100644 --- a/spec/services/environments/create_service_spec.rb +++ b/spec/services/environments/create_service_spec.rb @@ -29,6 +29,33 @@ RSpec.describe Environments::CreateService, feature_category: :environment_manag expect(response.payload[:environment].tier).to eq('production') end + context 'with a cluster agent' do + let_it_be(:agent_management_project) { create(:project) } + let_it_be(:cluster_agent) { create(:cluster_agent, project: agent_management_project) } + + let!(:authorization) { create(:agent_user_access_project_authorization, project: project, agent: cluster_agent) } + let(:params) { { name: 'production', cluster_agent: cluster_agent } } + + it 'returns successful response' do + response = subject + + expect(response).to be_success + expect(response.payload[:environment].cluster_agent).to eq(cluster_agent) + end + + context 'when user does not have permission to read the agent' do + let!(:authorization) { nil } + + it 'returns an error' do + response = subject + + expect(response).to be_error + expect(response.message).to eq('Unauthorized to access the cluster agent in this project') + expect(response.payload[:environment]).to be_nil + end + end + end + context 'when params contain invalid value' do let(:params) { { name: 'production', external_url: 'http://${URL}' } } @@ -45,6 +72,18 @@ RSpec.describe Environments::CreateService, feature_category: :environment_manag end end + context 'when disallowed parameter is passed' do + let(:params) { { name: 'production', slug: 'prod' } } + + it 'ignores the parameter' do + response = subject + + expect(response).to be_success + expect(response.payload[:environment].name).to eq('production') + expect(response.payload[:environment].slug).not_to eq('prod') + end + end + context 'when user is reporter' do let(:current_user) { reporter } diff --git a/spec/services/work_items/widgets/award_emoji_service/update_service_spec.rb b/spec/services/work_items/callbacks/award_emoji_spec.rb index 186e4d56cc4..831604d73b1 100644 --- a/spec/services/work_items/widgets/award_emoji_service/update_service_spec.rb +++ b/spec/services/work_items/callbacks/award_emoji_spec.rb @@ -2,27 +2,26 @@ require 'spec_helper' -RSpec.describe WorkItems::Widgets::AwardEmojiService::UpdateService, feature_category: :team_planning do +RSpec.describe WorkItems::Callbacks::AwardEmoji, feature_category: :team_planning do let_it_be(:reporter) { create(:user) } let_it_be(:unauthorized_user) { create(:user) } let_it_be(:project) { create(:project, :private) } let_it_be(:work_item) { create(:work_item, project: project) } let(:current_user) { reporter } - let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::AwardEmoji) } } before_all do project.add_reporter(reporter) end - describe '#before_update_in_transaction' do + describe '#before_update' do subject do - described_class.new(widget: widget, current_user: current_user) - .before_update_in_transaction(params: params) + described_class.new(issuable: work_item, current_user: current_user, params: params) + .before_update end shared_examples 'raises a WidgetError' do - it { expect { subject }.to raise_error(described_class::WidgetError, message) } + it { expect { subject }.to raise_error(::WorkItems::Widgets::BaseService::WidgetError, message) } end context 'when awarding an emoji' do |