diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 459afc7c31c..f4b001ed1ba 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -393,8 +393,7 @@ db:migrate-from-previous-major-version: - sed -i -e "s/gem 'google-protobuf', '~> 3.8.0'/gem 'google-protobuf', '~> 3.12'/" Gemfile - sed -i -e "s/gem 'nokogiri', '~> 1.10.5'/gem 'nokogiri', '~> 1.11.0'/" Gemfile - sed -i -e "s/gem 'mimemagic', '~> 0.3.2'/gem 'ruby-magic', '~> 0.4.0'/" Gemfile - - run_timed_command "gem install bundler:1.17.3" - - run_timed_command "bundle update google-protobuf nokogiri grpc mimemagic bootsnap" + - run_timed_command "bundle update --bundler google-protobuf nokogiri grpc mimemagic bootsnap" - SETUP_DB=false USE_BUNDLE_INSTALL=true bash scripts/prepare_build.sh - run_timed_command "bundle exec rake db:drop db:create db:structure:load db:migrate db:seed_fu" - git checkout -f $CI_COMMIT_SHA diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue index 3a3e5bdfa39..70e6cbe9aca 100644 --- a/app/assets/javascripts/content_editor/components/content_editor.vue +++ b/app/assets/javascripts/content_editor/components/content_editor.vue @@ -2,6 +2,7 @@ import { GlAlert } from '@gitlab/ui'; import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2'; import { ContentEditor } from '../services/content_editor'; +import FormattingBubbleMenu from './formatting_bubble_menu.vue'; import TopToolbar from './top_toolbar.vue'; export default { @@ -9,6 +10,7 @@ export default { GlAlert, TiptapEditorContent, TopToolbar, + FormattingBubbleMenu, }, provide() { return { @@ -44,6 +46,7 @@ export default { :class="{ 'is-focused': contentEditor.tiptapEditor.isFocused }" > + diff --git a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue new file mode 100644 index 00000000000..6c00480b87e --- /dev/null +++ b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue @@ -0,0 +1,67 @@ + + diff --git a/app/assets/javascripts/content_editor/components/toolbar_button.vue b/app/assets/javascripts/content_editor/components/toolbar_button.vue index 3445fe3c4ca..cdb877152d4 100644 --- a/app/assets/javascripts/content_editor/components/toolbar_button.vue +++ b/app/assets/javascripts/content_editor/components/toolbar_button.vue @@ -29,6 +29,21 @@ export default { required: false, default: '', }, + variant: { + type: String, + required: false, + default: 'default', + }, + category: { + type: String, + required: false, + default: 'tertiary', + }, + size: { + type: String, + required: false, + default: 'small', + }, }, data() { return { @@ -55,9 +70,9 @@ export default { -import Tracking from '~/tracking'; -import { CONTENT_EDITOR_TRACKING_LABEL, TOOLBAR_CONTROL_TRACKING_ACTION } from '../constants'; +import trackUIControl from '../services/track_ui_control'; import Divider from './divider.vue'; import ToolbarButton from './toolbar_button.vue'; import ToolbarImageButton from './toolbar_image_button.vue'; @@ -8,10 +7,6 @@ import ToolbarLinkButton from './toolbar_link_button.vue'; import ToolbarTableButton from './toolbar_table_button.vue'; import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue'; -const trackingMixin = Tracking.mixin({ - label: CONTENT_EDITOR_TRACKING_LABEL, -}); - export default { components: { ToolbarButton, @@ -21,13 +16,9 @@ export default { ToolbarImageButton, Divider, }, - mixins: [trackingMixin], methods: { - trackToolbarControlExecution({ contentType: property, value }) { - this.track(TOOLBAR_CONTROL_TRACKING_ACTION, { - property, - value, - }); + trackToolbarControlExecution({ contentType, value }) { + trackUIControl({ property: contentType, value }); }, }, }; @@ -45,6 +36,7 @@ export default { data-testid="bold" content-type="bold" icon-name="bold" + class="gl-mx-2" editor-command="toggleBold" :label="__('Bold text')" @execute="trackToolbarControlExecution" @@ -53,6 +45,7 @@ export default { data-testid="italic" content-type="italic" icon-name="italic" + class="gl-mx-2" editor-command="toggleItalic" :label="__('Italic text')" @execute="trackToolbarControlExecution" @@ -61,6 +54,7 @@ export default { data-testid="strike" content-type="strike" icon-name="strikethrough" + class="gl-mx-2" editor-command="toggleStrike" :label="__('Strikethrough')" @execute="trackToolbarControlExecution" @@ -69,6 +63,7 @@ export default { data-testid="code" content-type="code" icon-name="code" + class="gl-mx-2" editor-command="toggleCode" :label="__('Code')" @execute="trackToolbarControlExecution" @@ -84,6 +79,7 @@ export default { data-testid="blockquote" content-type="blockquote" icon-name="quote" + class="gl-mx-2" editor-command="toggleBlockquote" :label="__('Insert a quote')" @execute="trackToolbarControlExecution" @@ -92,6 +88,7 @@ export default { data-testid="code-block" content-type="codeBlock" icon-name="doc-code" + class="gl-mx-2" editor-command="toggleCodeBlock" :label="__('Insert a code block')" @execute="trackToolbarControlExecution" @@ -100,6 +97,7 @@ export default { data-testid="bullet-list" content-type="bulletList" icon-name="list-bulleted" + class="gl-mx-2" editor-command="toggleBulletList" :label="__('Add a bullet list')" @execute="trackToolbarControlExecution" @@ -108,6 +106,7 @@ export default { data-testid="ordered-list" content-type="orderedList" icon-name="list-numbered" + class="gl-mx-2" editor-command="toggleOrderedList" :label="__('Add a numbered list')" @execute="trackToolbarControlExecution" @@ -116,6 +115,7 @@ export default { data-testid="horizontal-rule" content-type="horizontalRule" icon-name="dash" + class="gl-mx-2" editor-command="setHorizontalRule" :label="__('Add a horizontal rule')" @execute="trackToolbarControlExecution" diff --git a/app/assets/javascripts/content_editor/constants.js b/app/assets/javascripts/content_editor/constants.js index 7a5f1d3ed1f..f277508f628 100644 --- a/app/assets/javascripts/content_editor/constants.js +++ b/app/assets/javascripts/content_editor/constants.js @@ -6,6 +6,7 @@ export const PROVIDE_SERIALIZER_OR_RENDERER_ERROR = s__( export const CONTENT_EDITOR_TRACKING_LABEL = 'content_editor'; export const TOOLBAR_CONTROL_TRACKING_ACTION = 'execute_toolbar_control'; +export const BUBBLE_MENU_TRACKING_ACTION = 'execute_bubble_menu_control'; export const KEYBOARD_SHORTCUT_TRACKING_ACTION = 'execute_keyboard_shortcut'; export const INPUT_RULE_TRACKING_ACTION = 'execute_input_rule'; @@ -40,3 +41,7 @@ export const TEXT_STYLE_DROPDOWN_ITEMS = [ label: __('Normal text'), }, ]; + +export const LOADING_CONTENT_EVENT = 'loadingContent'; +export const LOADING_SUCCESS_EVENT = 'loadingSuccess'; +export const LOADING_ERROR_EVENT = 'loadingError'; diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js index 29553f4c2ca..209160ad80c 100644 --- a/app/assets/javascripts/content_editor/services/content_editor.js +++ b/app/assets/javascripts/content_editor/services/content_editor.js @@ -1,8 +1,11 @@ +import eventHubFactory from '~/helpers/event_hub_factory'; +import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants'; /* eslint-disable no-underscore-dangle */ export class ContentEditor { constructor({ tiptapEditor, serializer }) { this._tiptapEditor = tiptapEditor; this._serializer = serializer; + this._eventHub = eventHubFactory(); } get tiptapEditor() { @@ -16,12 +19,41 @@ export class ContentEditor { return doc.childCount === 0 || (doc.childCount === 1 && doc.child(0).childCount === 0); } + once(type, handler) { + this._eventHub.$once(type, handler); + } + + on(type, handler) { + this._eventHub.$on(type, handler); + } + + emit(type, params = {}) { + this._eventHub.$emit(type, params); + } + + off(type, handler) { + this._eventHub.$off(type, handler); + } + + disposeAllEvents() { + this._eventHub.dispose(); + } + async setSerializedContent(serializedContent) { const { _tiptapEditor: editor, _serializer: serializer } = this; - editor.commands.setContent( - await serializer.deserialize({ schema: editor.schema, content: serializedContent }), - ); + try { + this._eventHub.$emit(LOADING_CONTENT_EVENT); + const document = await serializer.deserialize({ + schema: editor.schema, + content: serializedContent, + }); + editor.commands.setContent(document); + this._eventHub.$emit(LOADING_SUCCESS_EVENT); + } catch (e) { + this._eventHub.$emit(LOADING_ERROR_EVENT, e); + throw e; + } } getSerializedContent() { diff --git a/app/assets/javascripts/content_editor/services/track_ui_control.js b/app/assets/javascripts/content_editor/services/track_ui_control.js new file mode 100644 index 00000000000..61f130ea861 --- /dev/null +++ b/app/assets/javascripts/content_editor/services/track_ui_control.js @@ -0,0 +1,9 @@ +import Tracking from '~/tracking'; +import { CONTENT_EDITOR_TRACKING_LABEL, TOOLBAR_CONTROL_TRACKING_ACTION } from '../constants'; + +export default ({ action = TOOLBAR_CONTROL_TRACKING_ACTION, property, value } = {}) => + Tracking.event(undefined, action, { + label: CONTENT_EDITOR_TRACKING_LABEL, + property, + value, + }); diff --git a/app/controllers/groups/dependency_proxy/application_controller.rb b/app/controllers/groups/dependency_proxy/application_controller.rb index c6484ffb5f1..fd9db41f748 100644 --- a/app/controllers/groups/dependency_proxy/application_controller.rb +++ b/app/controllers/groups/dependency_proxy/application_controller.rb @@ -18,23 +18,14 @@ module Groups def authenticate_user_from_jwt_token! return unless dependency_proxy_for_private_groups? - if Feature.enabled?(:dependency_proxy_deploy_tokens) - authenticate_with_http_token do |token, _| - @authentication_result = EMPTY_AUTH_RESULT + authenticate_with_http_token do |token, _| + @authentication_result = EMPTY_AUTH_RESULT - found_user = user_from_token(token) - sign_in(found_user) if found_user.is_a?(User) - end - - request_bearer_token! unless authenticated_user - else - authenticate_with_http_token do |token, _| - user = user_from_token(token) - sign_in(user) if user - end - - request_bearer_token! unless current_user + found_user = user_from_token(token) + sign_in(found_user) if found_user.is_a?(User) end + + request_bearer_token! unless authenticated_user end private @@ -51,7 +42,6 @@ module Groups def user_from_token(token) token_payload = ::DependencyProxy::AuthTokenService.decoded_token_payload(token) - return User.find(token_payload['user_id']) unless Feature.enabled?(:dependency_proxy_deploy_tokens) if token_payload['user_id'] token_user = User.find(token_payload['user_id']) diff --git a/app/models/packages/npm.rb b/app/models/packages/npm.rb new file mode 100644 index 00000000000..e49199d911c --- /dev/null +++ b/app/models/packages/npm.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +module Packages + module Npm + # from "@scope/package-name" return "scope" or nil + def self.scope_of(package_name) + return unless package_name + return unless package_name.starts_with?('@') + return unless package_name.include?('/') + + package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first + end + end +end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index d2e4f46898c..010e1835520 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -62,7 +62,7 @@ class Packages::Package < ApplicationRecord validate :valid_conan_package_recipe, if: :conan? validate :valid_composer_global_name, if: :composer? - validate :package_already_taken, if: :npm? + validate :npm_package_already_taken, if: :npm? validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan? validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic? validates :name, format: { with: Gitlab::Regex.helm_package_regex }, if: :helm? @@ -320,14 +320,22 @@ class Packages::Package < ApplicationRecord end end - def package_already_taken + def npm_package_already_taken return unless project + return unless follows_npm_naming_convention? - if project.package_already_taken?(name) + if project.package_already_taken?(name, package_type: :npm) errors.add(:base, _('Package already exists')) end end + # https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention + def follows_npm_naming_convention? + return false unless project&.root_namespace&.path + + project.root_namespace.path == ::Packages::Npm.scope_of(name) + end + def unique_debian_package_name return unless debian_publication&.distribution diff --git a/app/models/project.rb b/app/models/project.rb index 0d32138b08c..e0b8698c30b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2566,12 +2566,15 @@ class Project < ApplicationRecord [project&.id, root_group&.id] end - def package_already_taken?(package_name) + def package_already_taken?(package_name, package_type:) namespace.root_ancestor.all_projects .joins(:packages) .where.not(id: id) - .merge(Packages::Package.default_scoped.with_name(package_name)) - .exists? + .merge( + Packages::Package.default_scoped + .with_name(package_name) + .with_package_type(package_type) + ).exists? end def default_branch_or_main diff --git a/config/feature_flags/development/dependency_proxy_deploy_tokens.yml b/config/feature_flags/development/dependency_proxy_deploy_tokens.yml deleted file mode 100644 index f3cb1fc2c18..00000000000 --- a/config/feature_flags/development/dependency_proxy_deploy_tokens.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: dependency_proxy_deploy_tokens -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64363 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334565 -milestone: '14.2' -type: development -group: group::package -default_enabled: false diff --git a/config/feature_flags/development/multiple_database_metrics.yml b/config/feature_flags/development/multiple_database_metrics.yml deleted file mode 100644 index 7a700982022..00000000000 --- a/config/feature_flags/development/multiple_database_metrics.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: multiple_database_metrics -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63490 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333227 -milestone: '14.1' -type: development -group: group::sharding -default_enabled: false diff --git a/config/metrics/counts_28d/20210216182136_i_testing_test_case_parsed_monthly.yml b/config/metrics/counts_28d/20210216182136_i_testing_test_case_parsed_monthly.yml index 23a7074e72a..97479d10ff2 100644 --- a/config/metrics/counts_28d/20210216182136_i_testing_test_case_parsed_monthly.yml +++ b/config/metrics/counts_28d/20210216182136_i_testing_test_case_parsed_monthly.yml @@ -11,6 +11,10 @@ value_type: number status: data_available time_frame: 28d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - i_testing_test_case_parsed distribution: - ce - ee diff --git a/config/metrics/counts_28d/20210216184140_testing_total_unique_counts_monthly.yml b/config/metrics/counts_28d/20210216184140_testing_total_unique_counts_monthly.yml index 3e6e920e3ec..b4b800d1e10 100644 --- a/config/metrics/counts_28d/20210216184140_testing_total_unique_counts_monthly.yml +++ b/config/metrics/counts_28d/20210216184140_testing_total_unique_counts_monthly.yml @@ -10,6 +10,20 @@ value_type: number status: removed time_frame: 28d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - i_testing_test_case_parsed + - i_testing_metrics_report_widget_total + - i_testing_group_code_coverage_visit_total + - i_testing_full_code_quality_report_total + - i_testing_web_performance_widget_total + - i_testing_group_code_coverage_project_click_total + - i_testing_load_performance_widget_total + - i_testing_metrics_report_artifact_uploaders + - i_testing_summary_widget_total + - users_expanding_testing_code_quality_report + - users_expanding_testing_accessibility_report distribution: - ce tier: diff --git a/config/metrics/counts_28d/20210409100451_users_expanding_testing_code_quality_report_monthly.yml b/config/metrics/counts_28d/20210409100451_users_expanding_testing_code_quality_report_monthly.yml index d993b1a1ce7..a86d69b33fb 100644 --- a/config/metrics/counts_28d/20210409100451_users_expanding_testing_code_quality_report_monthly.yml +++ b/config/metrics/counts_28d/20210409100451_users_expanding_testing_code_quality_report_monthly.yml @@ -12,6 +12,10 @@ milestone: '13.11' introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133 time_frame: 28d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - users_expanding_testing_code_quality_report distribution: - ce - ee diff --git a/config/metrics/counts_28d/20210409100628_users_expanding_testing_accessibility_report_monthly.yml b/config/metrics/counts_28d/20210409100628_users_expanding_testing_accessibility_report_monthly.yml index 01d6512d124..5c8f72e96b9 100644 --- a/config/metrics/counts_28d/20210409100628_users_expanding_testing_accessibility_report_monthly.yml +++ b/config/metrics/counts_28d/20210409100628_users_expanding_testing_accessibility_report_monthly.yml @@ -12,6 +12,10 @@ milestone: '13.11' introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133 time_frame: 28d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - users_expanding_testing_accessibility_report distribution: - ce - ee diff --git a/config/metrics/counts_28d/20210413205507_i_testing_summary_widget_total_monthly.yml b/config/metrics/counts_28d/20210413205507_i_testing_summary_widget_total_monthly.yml index 8e2da49198b..aeaf9615ec4 100644 --- a/config/metrics/counts_28d/20210413205507_i_testing_summary_widget_total_monthly.yml +++ b/config/metrics/counts_28d/20210413205507_i_testing_summary_widget_total_monthly.yml @@ -12,6 +12,10 @@ milestone: "13.11" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59316 time_frame: 28d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - i_testing_summary_widget_total distribution: - ee - ce diff --git a/config/metrics/counts_7d/20210216182134_i_testing_test_case_parsed_weekly.yml b/config/metrics/counts_7d/20210216182134_i_testing_test_case_parsed_weekly.yml index c7ce2c729e1..6764ce3be94 100644 --- a/config/metrics/counts_7d/20210216182134_i_testing_test_case_parsed_weekly.yml +++ b/config/metrics/counts_7d/20210216182134_i_testing_test_case_parsed_weekly.yml @@ -11,6 +11,10 @@ value_type: number status: data_available time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - i_testing_test_case_parsed distribution: - ee - ce diff --git a/config/metrics/counts_7d/20210409100451_users_expanding_testing_code_quality_report_weekly.yml b/config/metrics/counts_7d/20210409100451_users_expanding_testing_code_quality_report_weekly.yml index 11aad6e8af1..13cc4ae2498 100644 --- a/config/metrics/counts_7d/20210409100451_users_expanding_testing_code_quality_report_weekly.yml +++ b/config/metrics/counts_7d/20210409100451_users_expanding_testing_code_quality_report_weekly.yml @@ -12,6 +12,10 @@ milestone: '13.11' introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133 time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - users_expanding_testing_code_quality_report distribution: - ce - ee diff --git a/config/metrics/counts_7d/20210409100628_users_expanding_testing_accessibility_report_weekly.yml b/config/metrics/counts_7d/20210409100628_users_expanding_testing_accessibility_report_weekly.yml index b6929fd771f..e59e3e0bd7c 100644 --- a/config/metrics/counts_7d/20210409100628_users_expanding_testing_accessibility_report_weekly.yml +++ b/config/metrics/counts_7d/20210409100628_users_expanding_testing_accessibility_report_weekly.yml @@ -12,6 +12,10 @@ milestone: '13.11' introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133 time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - users_expanding_testing_accessibility_report distribution: - ce - ee diff --git a/config/metrics/counts_7d/20210413205507_i_testing_summary_widget_total_weekly.yml b/config/metrics/counts_7d/20210413205507_i_testing_summary_widget_total_weekly.yml index 44597d4910b..847f615e166 100644 --- a/config/metrics/counts_7d/20210413205507_i_testing_summary_widget_total_weekly.yml +++ b/config/metrics/counts_7d/20210413205507_i_testing_summary_widget_total_weekly.yml @@ -12,6 +12,10 @@ milestone: "13.11" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59316 time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - i_testing_summary_widget_total distribution: - ee - ce diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md index 7440c5a95d9..6d9418133d8 100644 --- a/doc/administration/monitoring/performance/performance_bar.md +++ b/doc/administration/monitoring/performance/performance_bar.md @@ -31,9 +31,9 @@ From left to right, the performance bar displays: was sent to the read/write primary server. "Replica" means it was sent to a read-only replica. - **Config name**: shows up only when the - `multiple_database_metrics` feature flag is enabled. This is used to - distinguish between different databases configured for different GitLab - features. The name shown is the same name used to configure database + `GITLAB_MULTIPLE_DATABASE_METRICS` environment variable is set. This is + used to distinguish between different databases configured for different + GitLab features. The name shown is the same name used to configure database connections in GitLab. - **Gitaly calls**: the time taken (in milliseconds) and the total number of [Gitaly](../../gitaly/index.md) calls. Click to display a modal window with more diff --git a/lib/api/helpers/packages/npm.rb b/lib/api/helpers/packages/npm.rb index 2d556f889bf..ce5db52fdbc 100644 --- a/lib/api/helpers/packages/npm.rb +++ b/lib/api/helpers/packages/npm.rb @@ -49,28 +49,20 @@ module API when :project params[:id] when :instance - namespace_path = namespace_path_from_package_name + package_name = params[:package_name] + namespace_path = ::Packages::Npm.scope_of(package_name) next unless namespace_path namespace = Namespace.top_most .by_path(namespace_path) next unless namespace - finder = ::Packages::Npm::PackageFinder.new(params[:package_name], namespace: namespace) + finder = ::Packages::Npm::PackageFinder.new(package_name, namespace: namespace) finder.last&.project_id end end end - - # from "@scope/package-name" return "scope" or nil - def namespace_path_from_package_name - package_name = params[:package_name] - return unless package_name.starts_with?('@') - return unless package_name.include?('/') - - package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first - end end end end diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml index 48e877684f6..43ecc4b96d5 100644 --- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml @@ -27,7 +27,7 @@ code_quality: } - docker pull --quiet "$CODE_QUALITY_IMAGE" - | - docker run \ + docker run --rm \ $(propagate_env_vars \ SOURCE_CODE \ TIMEOUT_SECONDS \ diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 70e2af69c56..1b25d3c2090 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -163,6 +163,10 @@ module Gitlab end end + def self.db_config_names + ::ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).map(&:name) + end + def self.db_config_name(ar_connection) if ar_connection.respond_to?(:pool) && ar_connection.pool.respond_to?(:db_config) && diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index 1318d3831a4..99b488a4aa7 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -42,8 +42,13 @@ module Gitlab tracking_category: 'Growth::Conversion::Experiment::ContactSalesInApp', use_backwards_compatible_subject_index: true }, - remove_known_trial_form_fields: { - tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFields' + remove_known_trial_form_fields_welcoming: { + tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFieldsWelcoming', + rollout_strategy: :user + }, + remove_known_trial_form_fields_noneditable: { + tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFieldsNoneditable', + rollout_strategy: :user }, invite_members_new_dropdown: { tracking_category: 'Growth::Expansion::Experiment::InviteMembersNewDropdown' diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb index 8815088d23c..6a7a7032665 100644 --- a/lib/gitlab/git/commit_stats.rb +++ b/lib/gitlab/git/commit_stats.rb @@ -14,20 +14,22 @@ module Gitlab # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/323 def initialize(repo, commit) @id = commit.id - @additions = 0 - @deletions = 0 - @total = 0 - wrapped_gitaly_errors do - gitaly_stats(repo, commit) - end + additions, deletions = fetch_stats(repo, commit) + + @additions = additions.to_i + @deletions = deletions.to_i + @total = @additions + @deletions end - def gitaly_stats(repo, commit) - stats = repo.gitaly_commit_client.commit_stats(@id) - @additions = stats.additions - @deletions = stats.deletions - @total = @additions + @deletions + def fetch_stats(repo, commit) + Rails.cache.fetch("commit_stats:#{repo.gl_project_path}:#{@id}") do + stats = wrapped_gitaly_errors do + repo.gitaly_commit_client.commit_stats(@id) + end + + [stats.additions, stats.deletions] + end end end end diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb index 03c8e8b4cfe..5856031aa4f 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -8,17 +8,15 @@ module Gitlab attach_to :active_record IGNORABLE_SQL = %w{BEGIN COMMIT}.freeze - DB_COUNTERS = %i{db_count db_write_count db_cached_count}.freeze + DB_COUNTERS = %i{count write_count cached_count}.freeze SQL_COMMANDS_WITH_COMMENTS_REGEX = %r{\A(/\*.*\*/\s)?((?!(.*[^\w'"](DELETE|UPDATE|INSERT INTO)[^\w'"])))(WITH.*)?(SELECT)((?!(FOR UPDATE|FOR SHARE)).)*$}i.freeze SQL_DURATION_BUCKET = [0.05, 0.1, 0.25].freeze TRANSACTION_DURATION_BUCKET = [0.1, 0.25, 1].freeze - DB_LOAD_BALANCING_COUNTERS = %i{ - db_replica_count db_replica_cached_count db_replica_wal_count db_replica_wal_cached_count - db_primary_count db_primary_cached_count db_primary_wal_count db_primary_wal_cached_count - }.freeze - DB_LOAD_BALANCING_DURATIONS = %i{db_primary_duration_s db_replica_duration_s}.freeze + DB_LOAD_BALANCING_ROLES = %i{replica primary}.freeze + DB_LOAD_BALANCING_COUNTERS = %i{count cached_count wal_count wal_cached_count}.freeze + DB_LOAD_BALANCING_DURATIONS = %i{duration_s}.freeze SQL_WAL_LOCATION_REGEX = /(pg_current_wal_insert_lsn\(\)::text|pg_last_wal_replay_lsn\(\)::text)/.freeze @@ -40,9 +38,10 @@ module Gitlab payload = event.payload return if ignored_query?(payload) - increment(:db_count) - increment(:db_cached_count) if cached_query?(payload) - increment(:db_write_count) unless select_sql_command?(payload) + db_config_name = db_config_name(event.payload) + increment(:count, db_config_name: db_config_name) + increment(:cached_count, db_config_name: db_config_name) if cached_query?(payload) + increment(:write_count, db_config_name: db_config_name) unless select_sql_command?(payload) observe(:gitlab_sql_duration_seconds, event) do buckets SQL_DURATION_BUCKET @@ -61,24 +60,17 @@ module Gitlab return {} unless Gitlab::SafeRequestStore.active? {}.tap do |payload| - DB_COUNTERS.each do |counter| - payload[counter] = Gitlab::SafeRequestStore[counter].to_i + db_counter_keys.each do |key| + payload[key] = Gitlab::SafeRequestStore[key].to_i end if ::Gitlab::SafeRequestStore.active? && ::Gitlab::Database::LoadBalancing.enable? - DB_LOAD_BALANCING_COUNTERS.each do |counter| + load_balancing_metric_counter_keys.each do |counter| payload[counter] = ::Gitlab::SafeRequestStore[counter].to_i end - DB_LOAD_BALANCING_DURATIONS.each do |duration| - payload[duration] = ::Gitlab::SafeRequestStore[duration].to_f.round(3) - end - if Feature.enabled?(:multiple_database_metrics, default_enabled: :yaml) - ::Gitlab::SafeRequestStore[:duration_by_database]&.each do |name, duration_by_role| - duration_by_role.each do |db_role, duration| - payload[:"db_#{db_role}_#{name}_duration_s"] = duration.to_f.round(3) - end - end + load_balancing_metric_duration_keys.each do |duration| + payload[duration] = ::Gitlab::SafeRequestStore[duration].to_f.round(3) end end end @@ -92,12 +84,15 @@ module Gitlab def increment_db_role_counters(db_role, payload) cached = cached_query?(payload) - increment("db_#{db_role}_count".to_sym) - increment("db_#{db_role}_cached_count".to_sym) if cached + + db_config_name = db_config_name(payload) + + increment(:count, db_role: db_role, db_config_name: db_config_name) + increment(:cached_count, db_role: db_role, db_config_name: db_config_name) if cached if wal_command?(payload) - increment("db_#{db_role}_wal_count".to_sym) - increment("db_#{db_role}_wal_cached_count".to_sym) if cached + increment(:wal_count, db_role: db_role, db_config_name: db_config_name) + increment(:wal_cached_count, db_role: db_role, db_config_name: db_config_name) if cached end end @@ -109,15 +104,13 @@ module Gitlab return unless ::Gitlab::SafeRequestStore.active? duration = event.duration / 1000.0 - duration_key = "db_#{db_role}_duration_s".to_sym + duration_key = compose_metric_key(:duration_s, db_role) ::Gitlab::SafeRequestStore[duration_key] = (::Gitlab::SafeRequestStore[duration_key].presence || 0) + duration # Per database metrics - name = ::Gitlab::Database.db_config_name(event.payload[:connection]) - ::Gitlab::SafeRequestStore[:duration_by_database] ||= {} - ::Gitlab::SafeRequestStore[:duration_by_database][name] ||= {} - ::Gitlab::SafeRequestStore[:duration_by_database][name][db_role] ||= 0 - ::Gitlab::SafeRequestStore[:duration_by_database][name][db_role] += duration + db_config_name = db_config_name(event.payload) + duration_key = compose_metric_key(:duration_s, db_role, db_config_name) + ::Gitlab::SafeRequestStore[duration_key] = (::Gitlab::SafeRequestStore[duration_key].presence || 0) + duration end def ignored_query?(payload) @@ -132,10 +125,25 @@ module Gitlab payload[:sql].match(SQL_COMMANDS_WITH_COMMENTS_REGEX) end - def increment(counter) - current_transaction&.increment("gitlab_transaction_#{counter}_total".to_sym, 1) + def increment(counter, db_config_name:, db_role: nil) + log_key = compose_metric_key(counter, db_role) - Gitlab::SafeRequestStore[counter] = Gitlab::SafeRequestStore[counter].to_i + 1 + prometheus_key = if db_role + :"gitlab_transaction_db_#{db_role}_#{counter}_total" + else + :"gitlab_transaction_db_#{counter}_total" + end + + current_transaction&.increment(prometheus_key, 1) + Gitlab::SafeRequestStore[log_key] = Gitlab::SafeRequestStore[log_key].to_i + 1 + + # To avoid confusing log keys we only log the db_config_name metrics + # when we are also logging the db_role. Otherwise it will be hard to + # tell if the log key is referring to a db_role OR a db_config_name. + if db_role.present? && db_config_name.present? + log_key = compose_metric_key(counter, db_role, db_config_name) + Gitlab::SafeRequestStore[log_key] = Gitlab::SafeRequestStore[log_key].to_i + 1 + end end def observe(histogram, event, &block) @@ -145,6 +153,45 @@ module Gitlab def current_transaction ::Gitlab::Metrics::WebTransaction.current || ::Gitlab::Metrics::BackgroundTransaction.current end + + def db_config_name(payload) + ::Gitlab::Database.db_config_name(payload[:connection]) + end + + def self.db_counter_keys + DB_COUNTERS.map { |c| compose_metric_key(c) } + end + + def self.load_balancing_metric_counter_keys + load_balancing_metric_keys(DB_LOAD_BALANCING_COUNTERS) + end + + def self.load_balancing_metric_duration_keys + load_balancing_metric_keys(DB_LOAD_BALANCING_DURATIONS) + end + + def self.load_balancing_metric_keys(metrics) + [].tap do |counters| + DB_LOAD_BALANCING_ROLES.each do |role| + metrics.each do |metric| + counters << compose_metric_key(metric, role) + next unless ENV['GITLAB_MULTIPLE_DATABASE_METRICS'] + + ::Gitlab::Database.db_config_names.each do |config_name| + counters << compose_metric_key(metric, role, config_name) + end + end + end + end + end + + def compose_metric_key(metric, db_role = nil, db_config_name = nil) + self.class.compose_metric_key(metric, db_role, db_config_name) + end + + def self.compose_metric_key(metric, db_role = nil, db_config_name = nil) + [:db, db_role, db_config_name, metric].compact.join("_").to_sym + end end end end diff --git a/lib/peek/views/active_record.rb b/lib/peek/views/active_record.rb index 6d2c9a86c62..a3fe206c86f 100644 --- a/lib/peek/views/active_record.rb +++ b/lib/peek/views/active_record.rb @@ -81,7 +81,7 @@ module Peek end def format_call_details(call) - if Feature.enabled?(:multiple_database_metrics, default_enabled: :yaml) + if ENV['GITLAB_MULTIPLE_DATABASE_METRICS'] super else super.except(:db_config_name) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 339e6d192ca..84373a7d8df 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16337,6 +16337,9 @@ msgstr "" msgid "Hi %{username}!" msgstr "" +msgid "Hi%{salutation}, your GitLab Ultimate trial lasts for 30 days, but you can keep your free GitLab account forever. We just need some additional information about %{company} to activate your trial." +msgstr "" + msgid "Hide" msgstr "" @@ -38248,7 +38251,7 @@ msgstr "" msgid "Your GPG keys (%{count})" msgstr "" -msgid "Your GitLab Ultimate trial will last 30 days after which point you can keep your free GitLab account forever. We just need some additional information to activate your trial." +msgid "Your GitLab Ultimate trial lasts for 30 days, but you can keep your free GitLab account forever. We just need some additional information to activate your trial." msgstr "" msgid "Your GitLab account has been locked due to an excessive amount of unsuccessful sign in attempts. Your account will automatically unlock in %{duration} or you may click the link below to unlock now." @@ -40195,5 +40198,8 @@ msgstr "" msgid "yaml invalid" msgstr "" +msgid "your company" +msgstr "" + msgid "your settings" msgstr "" diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb index 0f0fb781512..00c3472bf55 100644 --- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb +++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb @@ -65,82 +65,39 @@ RSpec.describe Groups::DependencyProxyForContainersController do it { is_expected.to have_gitlab_http_status(:not_found) } end - context 'deploy tokens with dependency_proxy_deploy_tokens disabled' do - before do - stub_feature_flags(dependency_proxy_deploy_tokens: false) - end + context 'with deploy token from a different group,' do + let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) } - context 'with deploy token from a different group,' do - let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) } - - it { is_expected.to have_gitlab_http_status(:not_found) } - end - - context 'with revoked deploy token' do - let_it_be(:user) { create(:deploy_token, :revoked, :group, :dependency_proxy_scopes) } - let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } - - it { is_expected.to have_gitlab_http_status(:not_found) } - end - - context 'with expired deploy token' do - let_it_be(:user) { create(:deploy_token, :expired, :group, :dependency_proxy_scopes) } - let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } - - it { is_expected.to have_gitlab_http_status(:not_found) } - end - - context 'with deploy token with insufficient scopes' do - let_it_be(:user) { create(:deploy_token, :group) } - let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } - - it { is_expected.to have_gitlab_http_status(:not_found) } - end - - context 'when a group is not found' do - before do - expect(Group).to receive(:find_by_full_path).and_return(nil) - end - - it { is_expected.to have_gitlab_http_status(:not_found) } - end + it { is_expected.to have_gitlab_http_status(:not_found) } end - context 'deploy tokens with dependency_proxy_deploy_tokens enabled' do - context 'with deploy token from a different group,' do - let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) } + context 'with revoked deploy token' do + let_it_be(:user) { create(:deploy_token, :revoked, :group, :dependency_proxy_scopes) } + let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } - it { is_expected.to have_gitlab_http_status(:not_found) } + it { is_expected.to have_gitlab_http_status(:unauthorized) } + end + + context 'with expired deploy token' do + let_it_be(:user) { create(:deploy_token, :expired, :group, :dependency_proxy_scopes) } + let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } + + it { is_expected.to have_gitlab_http_status(:unauthorized) } + end + + context 'with deploy token with insufficient scopes' do + let_it_be(:user) { create(:deploy_token, :group) } + let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'when a group is not found' do + before do + expect(Group).to receive(:find_by_full_path).and_return(nil) end - context 'with revoked deploy token' do - let_it_be(:user) { create(:deploy_token, :revoked, :group, :dependency_proxy_scopes) } - let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } - - it { is_expected.to have_gitlab_http_status(:unauthorized) } - end - - context 'with expired deploy token' do - let_it_be(:user) { create(:deploy_token, :expired, :group, :dependency_proxy_scopes) } - let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } - - it { is_expected.to have_gitlab_http_status(:unauthorized) } - end - - context 'with deploy token with insufficient scopes' do - let_it_be(:user) { create(:deploy_token, :group) } - let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } - - it { is_expected.to have_gitlab_http_status(:not_found) } - end - - context 'when a group is not found' do - before do - expect(Group).to receive(:find_by_full_path).and_return(nil) - end - - it { is_expected.to have_gitlab_http_status(:not_found) } - end + it { is_expected.to have_gitlab_http_status(:not_found) } end context 'when user is not found' do @@ -274,25 +231,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do it_behaves_like 'returning response status', :success it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest_from_cache' end - - context 'with dependency_proxy_deploy_tokens feature flag disabled' do - before do - stub_feature_flags(dependency_proxy_deploy_tokens: false) - end - - it_behaves_like 'a successful manifest pull' - end - end - - context 'a valid deploy token with dependency_proxy_deploy_tokens feature flag disabled' do - let_it_be(:user) { create(:deploy_token, :dependency_proxy_scopes, :group) } - let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } - - before do - stub_feature_flags(dependency_proxy_deploy_tokens: false) - end - - it { is_expected.to have_gitlab_http_status(:not_found) } end context 'a valid deploy token' do @@ -395,25 +333,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do it_behaves_like 'returning response status', :success it_behaves_like 'a package tracking event', described_class.name, 'pull_blob_from_cache' end - - context 'with dependency_proxy_deploy_tokens feature flag disabled' do - before do - stub_feature_flags(dependency_proxy_deploy_tokens: false) - end - - it_behaves_like 'a successful blob pull' - end - end - - context 'a valid deploy token with dependency_proxy_deploy_tokens feature flag disabled' do - let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) } - let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } - - before do - stub_feature_flags(dependency_proxy_deploy_tokens: false) - end - - it { is_expected.to have_gitlab_http_status(:not_found) } end context 'a valid deploy token' do diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap index 35c02911e27..e508cddd6f9 100644 --- a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap +++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`content_editor/components/toolbar_button displays tertiary, small button with a provided label and icon 1`] = ` -" +" diff --git a/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js b/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js new file mode 100644 index 00000000000..e44a7fa4ddb --- /dev/null +++ b/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js @@ -0,0 +1,80 @@ +import { BubbleMenu } from '@tiptap/vue-2'; +import { mockTracking } from 'helpers/tracking_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import FormattingBubbleMenu from '~/content_editor/components/formatting_bubble_menu.vue'; + +import { + BUBBLE_MENU_TRACKING_ACTION, + CONTENT_EDITOR_TRACKING_LABEL, +} from '~/content_editor/constants'; +import { createTestEditor } from '../test_utils'; + +describe('content_editor/components/top_toolbar', () => { + let wrapper; + let trackingSpy; + let tiptapEditor; + + const buildEditor = () => { + tiptapEditor = createTestEditor(); + + jest.spyOn(tiptapEditor, 'isActive'); + }; + + const buildWrapper = () => { + wrapper = shallowMountExtended(FormattingBubbleMenu, { + provide: { + tiptapEditor, + }, + }); + }; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, null, jest.spyOn); + buildEditor(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders bubble menu component', () => { + buildWrapper(); + const bubbleMenu = wrapper.findComponent(BubbleMenu); + + expect(bubbleMenu.props().editor).toBe(tiptapEditor); + expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base']); + }); + + describe.each` + testId | controlProps + ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold', size: 'medium', category: 'primary' }} + ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic', size: 'medium', category: 'primary' }} + ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike', size: 'medium', category: 'primary' }} + ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode', size: 'medium', category: 'primary' }} + `('given a $testId toolbar control', ({ testId, controlProps }) => { + beforeEach(() => { + buildWrapper(); + }); + + it('renders the toolbar control with the provided properties', () => { + expect(wrapper.findByTestId(testId).exists()).toBe(true); + + Object.keys(controlProps).forEach((propName) => { + expect(wrapper.findByTestId(testId).props(propName)).toBe(controlProps[propName]); + }); + }); + + it('tracks the execution of toolbar controls', () => { + const eventData = { contentType: 'italic', value: 1 }; + const { contentType, value } = eventData; + + wrapper.findByTestId(testId).vm.$emit('execute', eventData); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, BUBBLE_MENU_TRACKING_ACTION, { + label: CONTENT_EDITOR_TRACKING_LABEL, + property: contentType, + value, + }); + }); + }); +}); diff --git a/spec/frontend/content_editor/components/toolbar_button_spec.js b/spec/frontend/content_editor/components/toolbar_button_spec.js index da32f308481..60263c46bdd 100644 --- a/spec/frontend/content_editor/components/toolbar_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_button_spec.js @@ -50,6 +50,24 @@ describe('content_editor/components/toolbar_button', () => { expect(findButton().html()).toMatchSnapshot(); }); + it('allows customizing the variant, category, size of the button', () => { + const variant = 'danger'; + const category = 'secondary'; + const size = 'medium'; + + buildWrapper({ + variant, + category, + size, + }); + + expect(findButton().props()).toMatchObject({ + variant, + category, + size, + }); + }); + it.each` editorState | outcomeDescription | outcome ${{ isActive: true, isFocused: true }} | ${'button is active'} | ${true} diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js index 18faf1930e7..a5df3d73289 100644 --- a/spec/frontend/content_editor/components/top_toolbar_spec.js +++ b/spec/frontend/content_editor/components/top_toolbar_spec.js @@ -1,6 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; import { mockTracking } from 'helpers/tracking_helper'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import TopToolbar from '~/content_editor/components/top_toolbar.vue'; import { TOOLBAR_CONTROL_TRACKING_ACTION, @@ -12,7 +11,7 @@ describe('content_editor/components/top_toolbar', () => { let trackingSpy; const buildWrapper = () => { - wrapper = extendedWrapper(shallowMount(TopToolbar)); + wrapper = shallowMountExtended(TopToolbar); }; beforeEach(() => { @@ -43,17 +42,17 @@ describe('content_editor/components/top_toolbar', () => { }); it('renders the toolbar control with the provided properties', () => { - expect(wrapper.findByTestId(testId).props()).toEqual({ - ...controlProps, + expect(wrapper.findByTestId(testId).exists()).toBe(true); + + Object.keys(controlProps).forEach((propName) => { + expect(wrapper.findByTestId(testId).props(propName)).toBe(controlProps[propName]); }); }); - it.each` - eventData - ${{ contentType: 'bold' }} - ${{ contentType: 'blockquote', value: 1 }} - `('tracks the execution of toolbar controls', ({ eventData }) => { + it('tracks the execution of toolbar controls', () => { + const eventData = { contentType: 'blockquote', value: 1 }; const { contentType, value } = eventData; + wrapper.findByTestId(testId).vm.$emit('execute', eventData); expect(trackingSpy).toHaveBeenCalledWith(undefined, TOOLBAR_CONTROL_TRACKING_ACTION, { diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js new file mode 100644 index 00000000000..8580d3249b9 --- /dev/null +++ b/spec/frontend/content_editor/services/content_editor_spec.js @@ -0,0 +1,56 @@ +import { + LOADING_CONTENT_EVENT, + LOADING_SUCCESS_EVENT, + LOADING_ERROR_EVENT, +} from '~/content_editor/constants'; +import { ContentEditor } from '~/content_editor/services/content_editor'; + +import { createTestEditor } from '../test_utils'; + +describe('content_editor/services/content_editor', () => { + let contentEditor; + let serializer; + + beforeEach(() => { + const tiptapEditor = createTestEditor(); + serializer = { deserialize: jest.fn() }; + contentEditor = new ContentEditor({ tiptapEditor, serializer }); + }); + + describe('when setSerializedContent succeeds', () => { + beforeEach(() => { + serializer.deserialize.mockResolvedValueOnce(''); + }); + + it('emits loadingContent and loadingSuccess event', () => { + let loadingContentEmitted = false; + + contentEditor.on(LOADING_CONTENT_EVENT, () => { + loadingContentEmitted = true; + }); + contentEditor.on(LOADING_SUCCESS_EVENT, () => { + expect(loadingContentEmitted).toBe(true); + }); + + contentEditor.setSerializedContent('**bold text**'); + }); + }); + + describe('when setSerializedContent fails', () => { + const error = 'error'; + + beforeEach(() => { + serializer.deserialize.mockRejectedValueOnce(error); + }); + + it('emits loadingError event', async () => { + contentEditor.on(LOADING_ERROR_EVENT, (e) => { + expect(e).toBe('error'); + }); + + await expect(() => contentEditor.setSerializedContent('**bold text**')).rejects.toEqual( + error, + ); + }); + }); +}); diff --git a/spec/lib/gitlab/git/commit_stats_spec.rb b/spec/lib/gitlab/git/commit_stats_spec.rb new file mode 100644 index 00000000000..29d3909efec --- /dev/null +++ b/spec/lib/gitlab/git/commit_stats_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Gitlab::Git::CommitStats, :seed_helper do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } + let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) } + + def verify_stats! + stats = described_class.new(repository, commit) + + expect(stats).to have_attributes( + additions: eq(11), + deletions: eq(6), + total: eq(17) + ) + end + + it 'returns commit stats and caches them', :use_clean_rails_redis_caching do + expect(repository.gitaly_commit_client).to receive(:commit_stats).with(commit.id).and_call_original + + verify_stats! + + expect(Rails.cache.fetch("commit_stats:group/project:#{commit.id}")).to eq([11, 6]) + + expect(repository.gitaly_commit_client).not_to receive(:commit_stats) + + verify_stats! + end +end diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index 4f2d84028cf..a98038cd3f8 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -294,47 +294,38 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do context 'when load balancing is enabled', :db_load_balancing do let(:db_config_name) { ::Gitlab::Database.db_config_name(ApplicationRecord.connection) } + let(:expected_db_payload_defaults) do + metrics = + ::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_counter_keys + + ::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_duration_keys + + ::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_keys + + [:db_duration_s] + + metrics.each_with_object({}) do |key, result| + result[key.to_s] = 0 + end + end + let(:expected_end_payload_with_db) do - expected_end_payload.merge( + expected_end_payload.merge(expected_db_payload_defaults).merge( 'db_duration_s' => a_value >= 0.1, 'db_count' => a_value >= 1, - 'db_cached_count' => 0, - 'db_write_count' => 0, - 'db_replica_count' => 0, - 'db_replica_cached_count' => 0, - 'db_replica_wal_count' => 0, + "db_replica_#{db_config_name}_count" => 0, 'db_replica_duration_s' => a_value >= 0, 'db_primary_count' => a_value >= 1, - 'db_primary_cached_count' => 0, - 'db_primary_wal_count' => 0, + "db_primary_#{db_config_name}_count" => a_value >= 1, 'db_primary_duration_s' => a_value > 0, - "db_primary_#{db_config_name}_duration_s" => a_value > 0, - 'db_primary_wal_cached_count' => 0, - 'db_replica_wal_cached_count' => 0 + "db_primary_#{db_config_name}_duration_s" => a_value > 0 ) end let(:end_payload) do - start_payload.merge( + start_payload.merge(expected_db_payload_defaults).merge( 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec', 'job_status' => 'done', 'duration_s' => 0.0, 'completed_at' => timestamp.to_f, - 'cpu_s' => 1.111112, - 'db_duration_s' => 0.0, - 'db_cached_count' => 0, - 'db_count' => 0, - 'db_write_count' => 0, - 'db_replica_count' => 0, - 'db_replica_cached_count' => 0, - 'db_replica_wal_count' => 0, - 'db_replica_duration_s' => 0, - 'db_primary_count' => 0, - 'db_primary_cached_count' => 0, - 'db_primary_wal_count' => 0, - 'db_primary_wal_cached_count' => 0, - 'db_replica_wal_cached_count' => 0, - 'db_primary_duration_s' => 0 + 'cpu_s' => 1.111112 ) end diff --git a/spec/lib/gitlab/usage_data_metrics_spec.rb b/spec/lib/gitlab/usage_data_metrics_spec.rb index 59ceaeaf6de..476a6e99cfd 100644 --- a/spec/lib/gitlab/usage_data_metrics_spec.rb +++ b/spec/lib/gitlab/usage_data_metrics_spec.rb @@ -66,6 +66,16 @@ RSpec.describe Gitlab::UsageDataMetrics do ) end + it 'includes testing monthly and weekly keys' do + expect(subject[:redis_hll_counters][:testing]).to include( + :i_testing_test_case_parsed_monthly, :i_testing_test_case_parsed_weekly, + :users_expanding_testing_code_quality_report_monthly, :users_expanding_testing_code_quality_report_weekly, + :users_expanding_testing_accessibility_report_monthly, :users_expanding_testing_accessibility_report_weekly, + :i_testing_summary_widget_total_monthly, :i_testing_summary_widget_total_weekly, + :testing_total_unique_counts_monthly + ) + end + it 'includes source_code monthly and weekly keys' do expect(subject[:redis_hll_counters][:source_code].keys).to contain_exactly(*[ :wiki_action_monthly, :wiki_action_weekly, diff --git a/spec/lib/peek/views/active_record_spec.rb b/spec/lib/peek/views/active_record_spec.rb index b9d977c38e4..6d50922904e 100644 --- a/spec/lib/peek/views/active_record_spec.rb +++ b/spec/lib/peek/views/active_record_spec.rb @@ -109,9 +109,9 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do ) end - context 'when the multiple_database_metrics feature flag is disabled' do + context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do before do - stub_feature_flags(multiple_database_metrics: false) + stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil) end it 'does not include db_config_name field' do @@ -191,9 +191,9 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do ) end - context 'when the multiple_database_metrics feature flag is disabled' do + context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do before do - stub_feature_flags(multiple_database_metrics: false) + stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil) end it 'does not include db_config_name field' do diff --git a/spec/models/packages/npm_spec.rb b/spec/models/packages/npm_spec.rb new file mode 100644 index 00000000000..fa4adadfe06 --- /dev/null +++ b/spec/models/packages/npm_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Npm do + using RSpec::Parameterized::TableSyntax + + describe '.scope_of' do + subject { described_class.scope_of(package_name) } + + where(:package_name, :expected_result) do + nil | nil + 'test' | nil + '@test' | nil + 'test/package' | nil + '@/package' | nil + '@test/package' | 'test' + '@test/' | nil + end + + with_them do + it { is_expected.to eq(expected_result) } + end + end +end diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index 449e30f9fb7..285c39e7aaf 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -418,7 +418,7 @@ RSpec.describe Packages::Package, type: :model do end end - describe '#package_already_taken' do + describe '#npm_package_already_taken' do context 'maven package' do let!(:package) { create(:maven_package) } @@ -428,6 +428,43 @@ RSpec.describe Packages::Package, type: :model do expect(new_package).to be_valid end end + + context 'npm package' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, namespace: group) } + let_it_be(:second_project) { create(:project, namespace: group)} + + let(:package) { build(:npm_package, project: project, name: name) } + let(:second_package) { build(:npm_package, project: second_project, name: name, version: '5.0.0') } + + context 'following the naming convention' do + let(:name) { "@#{group.path}/test" } + + it 'will allow the first package' do + expect(package).to be_valid + end + + it 'will not allow npm package with duplicate name' do + package.save! + + expect(second_package).not_to be_valid + end + end + + context 'not following the naming convention' do + let(:name) { '@foobar/test' } + + it 'will allow the first package' do + expect(package).to be_valid + end + + it 'will allow npm package with duplicate name' do + package.save! + + expect(second_package).to be_valid + end + end + end end context "recipe uniqueness for conan packages" do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 72c45de402e..048ac7ab6d5 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -6839,29 +6839,36 @@ RSpec.describe Project, factory_default: :keep do end describe '#package_already_taken?' do - let(:namespace) { create(:namespace) } - let(:project) { create(:project, :public, namespace: namespace) } - let!(:package) { create(:npm_package, project: project, name: "@#{namespace.path}/foo") } + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, :public, namespace: namespace) } + let_it_be(:package) { create(:npm_package, project: project, name: "@#{namespace.path}/foo") } context 'no package exists with the same name' do it 'returns false' do - result = project.package_already_taken?("@#{namespace.path}/bar") + result = project.package_already_taken?("@#{namespace.path}/bar", package_type: :npm) expect(result).to be false end it 'returns false if it is the project that the package belongs to' do - result = project.package_already_taken?("@#{namespace.path}/foo") + result = project.package_already_taken?("@#{namespace.path}/foo", package_type: :npm) expect(result).to be false end end context 'a package already exists with the same name' do - let(:alt_project) { create(:project, :public, namespace: namespace) } + let_it_be(:alt_project) { create(:project, :public, namespace: namespace) } it 'returns true' do - result = alt_project.package_already_taken?("@#{namespace.path}/foo") + result = alt_project.package_already_taken?(package.name, package_type: :npm) expect(result).to be true end + + context 'for a different package type' do + it 'returns false' do + result = alt_project.package_already_taken?(package.name, package_type: :nuget) + expect(result).to be false + end + end end end diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index 8230061546f..fa7bb911af8 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -228,6 +228,31 @@ RSpec.describe API::NpmProjectPackages do it_behaves_like 'handling upload with different authentications' end + + context 'with an existing package' do + let_it_be(:second_project) { create(:project, namespace: namespace) } + + context 'following the naming convention' do + let_it_be(:second_package) { create(:npm_package, project: second_project, name: "@#{group.path}/test") } + + let(:package_name) { "@#{group.path}/test" } + + it_behaves_like 'handling invalid record with 400 error' + end + + context 'not following the naming convention' do + let_it_be(:second_package) { create(:npm_package, project: second_project, name: "@any_scope/test") } + + let(:package_name) { "@any_scope/test" } + + it "uploads the package" do + expect { upload_package_with_token(package_name, params) } + .to change { project.packages.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + end + end + end end context 'package creation fails' do diff --git a/spec/support/enable_multiple_database_metrics_by_default.rb b/spec/support/enable_multiple_database_metrics_by_default.rb new file mode 100644 index 00000000000..6eeb4acd3d6 --- /dev/null +++ b/spec/support/enable_multiple_database_metrics_by_default.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.before do + # Enable this by default in all tests so it behaves like a FF + stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', '1') + end +end diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb index 8b21ae1101e..2df953638ee 100644 --- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb +++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb @@ -1,70 +1,80 @@ # frozen_string_literal: true RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| + let(:db_config_name) { ::Gitlab::Database.db_config_name(ApplicationRecord.connection) } + + let(:expected_payload_defaults) do + metrics = + ::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_counter_keys + + ::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_duration_keys + + ::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_keys + + metrics.each_with_object({}) do |key, result| + result[key] = 0 + end + end + it 'prevents db counters from leaking to the next transaction' do 2.times do Gitlab::WithRequestStore.with_request_store do subscriber.sql(event) - connection = event.payload[:connection] - if db_role == :primary - expected = { - db_count: record_query ? 1 : 0, - db_write_count: record_write_query ? 1 : 0, - db_cached_count: record_cached_query ? 1 : 0, - db_primary_cached_count: record_cached_query ? 1 : 0, - db_primary_count: record_query ? 1 : 0, - db_primary_duration_s: record_query ? 0.002 : 0, - db_replica_cached_count: 0, - db_replica_count: 0, - db_replica_duration_s: 0.0, - db_primary_wal_count: record_wal_query ? 1 : 0, - db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, - db_replica_wal_cached_count: 0, - db_replica_wal_count: 0 - } - expected[:"db_primary_#{::Gitlab::Database.db_config_name(connection)}_duration_s"] = 0.002 if record_query - elsif db_role == :replica - expected = { - db_count: record_query ? 1 : 0, - db_write_count: record_write_query ? 1 : 0, - db_cached_count: record_cached_query ? 1 : 0, - db_primary_cached_count: 0, - db_primary_count: 0, - db_primary_duration_s: 0.0, - db_replica_cached_count: record_cached_query ? 1 : 0, - db_replica_count: record_query ? 1 : 0, - db_replica_duration_s: record_query ? 0.002 : 0, - db_replica_wal_count: record_wal_query ? 1 : 0, - db_replica_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, - db_primary_wal_cached_count: 0, - db_primary_wal_count: 0 - } - expected[:"db_replica_#{::Gitlab::Database.db_config_name(connection)}_duration_s"] = 0.002 if record_query - else - expected = { - db_count: record_query ? 1 : 0, - db_write_count: record_write_query ? 1 : 0, - db_cached_count: record_cached_query ? 1 : 0 - } - end + expected = if db_role == :primary + expected_payload_defaults.merge({ + db_count: record_query ? 1 : 0, + db_write_count: record_write_query ? 1 : 0, + db_cached_count: record_cached_query ? 1 : 0, + db_primary_cached_count: record_cached_query ? 1 : 0, + "db_primary_#{db_config_name}_cached_count": record_cached_query ? 1 : 0, + db_primary_count: record_query ? 1 : 0, + "db_primary_#{db_config_name}_count": record_query ? 1 : 0, + db_primary_duration_s: record_query ? 0.002 : 0, + "db_primary_#{db_config_name}_duration_s": record_query ? 0.002 : 0, + db_primary_wal_count: record_wal_query ? 1 : 0, + "db_primary_#{db_config_name}_wal_count": record_wal_query ? 1 : 0, + db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, + "db_primary_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0 + }) + elsif db_role == :replica + expected_payload_defaults.merge({ + db_count: record_query ? 1 : 0, + db_write_count: record_write_query ? 1 : 0, + db_cached_count: record_cached_query ? 1 : 0, + db_replica_cached_count: record_cached_query ? 1 : 0, + "db_replica_#{db_config_name}_cached_count": record_cached_query ? 1 : 0, + db_replica_count: record_query ? 1 : 0, + "db_replica_#{db_config_name}_count": record_query ? 1 : 0, + db_replica_duration_s: record_query ? 0.002 : 0, + "db_replica_#{db_config_name}_duration_s": record_query ? 0.002 : 0, + db_replica_wal_count: record_wal_query ? 1 : 0, + "db_replica_#{db_config_name}_wal_count": record_wal_query ? 1 : 0, + db_replica_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, + "db_replica_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0 + }) + else + { + db_count: record_query ? 1 : 0, + db_write_count: record_write_query ? 1 : 0, + db_cached_count: record_cached_query ? 1 : 0 + } + end expect(described_class.db_counter_payload).to eq(expected) end end end - context 'when multiple_database_metrics is disabled' do + context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do before do - stub_feature_flags(multiple_database_metrics: false) + stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil) end it 'does not include per database metrics' do Gitlab::WithRequestStore.with_request_store do subscriber.sql(event) - connection = event.payload[:connection] - expect(described_class.db_counter_payload).not_to include(:"db_replica_#{::Gitlab::Database.db_config_name(connection)}_duration_s") + expect(described_class.db_counter_payload).not_to include(:"db_replica_#{db_config_name}_duration_s") + expect(described_class.db_counter_payload).not_to include(:"db_replica_#{db_config_name}_count") end end end