Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-10-25 18:15:49 +00:00
parent 836c276094
commit f4f581c6d9
97 changed files with 2093 additions and 955 deletions

View File

@ -1,13 +0,0 @@
/app/assets/javascripts/locale/**/app.js
/builds/
/coverage/
/coverage-frontend/
/node_modules/
/public/
/tmp/
/vendor/
/sitespeed-result/
/fixtures/**/*.graphql
# Storybook build artifacts
/storybook/public
spec/fixtures/**/*.graphql

View File

@ -1,318 +0,0 @@
extends:
- plugin:@gitlab/default
- plugin:@gitlab/i18n
- plugin:no-jquery/slim
- plugin:no-jquery/deprecated-3.4
- plugin:no-unsanitized/recommended-legacy
- ./tooling/eslint-config/conditionally_ignore.js
globals:
__webpack_public_path__: true
gl: false
gon: false
localStorage: false
IS_EE: false
plugins:
- no-jquery
- local-rules
settings:
import/resolver:
webpack:
config: './config/webpack.config.js'
rules:
import/no-commonjs: error
import/no-default-export: off
no-underscore-dangle:
- error
- allow:
- __
- _links
import/no-unresolved:
- error
- ignore:
# In FOSS, these import paths are rewritten using
# NormalModuleReplacementPlugin, which import/no-unresolved doesn't
# consider. See
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89831.
- '^(ee|jh)_component/'
lines-between-class-members: off
# all offenses of no-jquery/no-animate-toggle are false positives ( $toast.show() )
no-jquery/no-animate-toggle: off
no-jquery/no-event-shorthand: off
no-jquery/no-serialize: error
promise/always-return: off
promise/no-callback-in-promise: off
'@gitlab/no-global-event-off': error
'@gitlab/vue-no-new-non-primitive-in-template':
- error
- allowNames:
- 'class(es)?$'
- '^style$'
- '^to$'
- '^$'
- '^variables$'
- 'attrs?$'
'@gitlab/vue-no-undef-apollo-properties': error
'@gitlab/tailwind-no-interpolation': error
'@gitlab/vue-tailwind-no-interpolation': error
no-param-reassign:
- error
- props: true
ignorePropertyModificationsFor:
- acc
- accumulator
- el
- element
- state
ignorePropertyModificationsForRegex:
- '^draft'
import/order:
- error
- groups:
- builtin
- external
- internal
- parent
- sibling
- index
pathGroups:
- pattern: ~/**
group: internal
- pattern: emojis/**
group: internal
- pattern: '{ee_,jh_,}empty_states/**'
group: internal
- pattern: '{ee_,jh_,}icons/**'
group: internal
- pattern: '{ee_,jh_,}images/**'
group: internal
- pattern: vendor/**
group: internal
- pattern: shared_queries/**
group: internal
- pattern: '{ee_,}spec/**'
group: internal
- pattern: '{ee_,jh_,}jest/**'
group: internal
- pattern: '{ee_,jh_,any_}else_ce/**'
group: internal
- pattern: ee/**
group: internal
- pattern: '{ee_,jh_,}component/**'
group: internal
- pattern: jh_else_ee/**
group: internal
- pattern: jh/**
group: internal
- pattern: '{test_,}helpers/**'
group: internal
- pattern: test_fixtures/**
group: internal
alphabetize:
order: ignore
'no-restricted-syntax':
- error
- selector: ImportSpecifier[imported.name='GlSkeletonLoading']
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
- selector: ImportSpecifier[imported.name='GlSafeHtmlDirective']
message: 'Use directive at ~/vue_shared/directives/safe_html.js instead.'
- selector: Literal[value=/docs.gitlab.+\u002Fee/]
message: 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
- selector: TemplateElement[value.cooked=/docs.gitlab.+\u002Fee/]
message: 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
- selector: Literal[value=/(?=.*docs.gitlab.*)(?!.*\u002Fee\b.*)/]
message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`'
- selector: TemplateElement[value.cooked=/(?=.*docs.gitlab.*)(?!.*\u002Fee\b.*)/]
message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`'
- selector: Literal[value=/(?=.*about.gitlab.*)(?!.*\u002Fblog\b.*)/]
message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`'
- selector: TemplateElement[value.cooked=/(?=.*about.gitlab.*)(?!.*\u002Fblog\b.*)/]
message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`'
- selector: TemplateLiteral[expressions.0.name=DOCS_URL] > TemplateElement[value.cooked=/\u002Fjh|\u002Fee/]
message: '`/ee` or `/jh` path found in docs url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
# This can be removed once GitLab is on Vue 3
- selector: MemberExpression[object.type='ThisExpression'][property.name=/(\$delete|\$set)/]
message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
no-restricted-properties:
- error
- object: window
property: open
message: 'Use `visitUrl` in `jh_else_ce/lib/utils/url_utility` to avoid cross-site leaks.'
# This can be removed once GitLab is on Vue 3
- object: vm
property: $delete
message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
# This can be removed once GitLab is on Vue 3
- object: Vue
property: delete
message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
# This can be removed once GitLab is on Vue 3
- object: vm
property: $set
message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
# This can be removed once GitLab is on Vue 3
- object: Vue
property: set
message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
no-restricted-imports:
- error
- paths:
- name: mousetrap
message: 'Import { Mousetrap } from ~/lib/mousetrap instead.'
- name: vuex
message: 'See our documentation on "Migrating from VueX" for tips on how to avoid adding new VueX stores.'
- name: '@sentry/browser'
message: Use "import * as Sentry from '~/sentry/sentry_browser_wrapper';" instead
patterns:
- group: ['react', 'react-dom/*']
message: We do not allow usage of React in our codebase except for the graphql_explorer
unicorn/prefer-dom-node-dataset:
- error
no-unsanitized/method:
- error
- escape:
methods: ['sanitize']
no-unsanitized/property:
- error
- escape:
methods: ['sanitize']
# This rule will be enabled later.
unicorn/no-array-callback-reference: off
vue/no-undef-components:
- error
- ignorePatterns:
- '^router-link$'
- '^router-view$'
- '^gl-emoji$'
local-rules/require-valid-help-page-path: 'error'
local-rules/vue-require-valid-help-page-link-component: 'error'
overrides:
- files:
- '{,ee/,jh/}spec/frontend*/**/*'
rules:
'@gitlab/require-i18n-strings': off
'@gitlab/no-runtime-template-compiler': off
'@gitlab/tailwind-no-interpolation': off
'@gitlab/vue-tailwind-no-interpolation': off
'require-await': error
'import/no-dynamic-require': off
'no-import-assign': off
'no-restricted-syntax':
- error
- selector: CallExpression[callee.object.name=/(wrapper|vm)/][callee.property.name="setData"]
message: 'Avoid using "setData" on VTU wrapper'
- selector: MemberExpression[object.type!='ThisExpression'][property.type='Identifier'][property.name='$nextTick']
message: 'Using $nextTick from a component instance is discouraged. Import nextTick directly from the Vue package.'
- selector: Identifier[name='setImmediate']
message: 'Prefer explicit waitForPromises (or equivalent), or jest.runAllTimers (or equivalent) to vague setImmediate calls.'
- selector: ImportSpecifier[imported.name='GlSkeletonLoading']
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
- selector: CallExpression[arguments.length=1][arguments.0.type='Literal'] CallExpression[callee.property.name='toBe'] CallExpression[callee.property.name='attributes'][arguments.length=1][arguments.0.value='disabled']
message: Avoid asserting disabled attribute exact value, because Vue.js 2 and Vue.js 3 renders it differently. Use toBeDefined / toBeUndefined instead
- selector: MemberExpression[object.object.name='Vue'][object.property.name='config'][property.name='errorHandler']
message: 'Use setErrorHandler/resetVueErrorHandler from helpers/set_vue_error_handler.js instead.'
- selector: Literal[value=/docs.gitlab.+\u002Fee/]
message: 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
- selector: TemplateElement[value.cooked=/docs.gitlab.+\u002Fee/]
message: 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
- selector: Literal[value=/(?=.*docs.gitlab.*)(?!.*\u002Fee\b.*)/]
message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`'
- selector: TemplateElement[value.cooked=/(?=.*docs.gitlab.*)(?!.*\u002Fee\b.*)/]
message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`'
- selector: Literal[value=/(?=.*about.gitlab.*)(?!.*\u002Fblog\b.*)/]
message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`'
- selector: TemplateElement[value.cooked=/(?=.*about.gitlab.*)(?!.*\u002Fblog\b.*)/]
message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`'
- selector: TemplateLiteral[expressions.0.name=DOCS_URL] > TemplateElement[value.cooked=/\u002Fjh|\u002Fee/]
message: '`/ee` or `/jh` path found in docs url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
# This can be removed once GitLab is on Vue 3
- selector: CallExpression[callee.property.name=/(\$delete|\$set)/]
message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
no-restricted-properties:
- error
# This can be removed once GitLab is on Vue 3
- object: Vue
property: delete
message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
# This can be removed once GitLab is on Vue 3
- object: Vue
property: set
message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
no-unsanitized/method: off
no-unsanitized/property: off
local-rules/require-valid-help-page-path: off
local-rules/vue-require-valid-help-page-link-component: off
no-restricted-imports:
- error
- paths:
- name: mousetrap
message: 'Import { Mousetrap } from ~/lib/mousetrap instead.'
- name: vuex
message: 'See our documentation on "Migrating from VueX" for tips on how to avoid adding new VueX stores.'
- name: '@sentry/browser'
message: Use "import * as Sentry from '~/sentry/sentry_browser_wrapper';" instead
- name: ~/locale
importNames:
- __
- s__
message: 'Do not externalize strings in specs: https://docs.gitlab.com/ee/development/i18n/externalization.html#test-files-jest'
- files:
- 'config/**/*'
- 'scripts/**/*'
- '*.config.js'
- '*.config.*.js'
- 'jest_resolver.js'
rules:
'@gitlab/require-i18n-strings': off
import/no-extraneous-dependencies: off
import/no-commonjs: off
import/no-nodejs-modules: off
filenames/match-regex: off
no-console: off
- files:
- '*.stories.js'
rules:
filenames/match-regex: off
'@gitlab/require-i18n-strings': off
- files:
- '*.graphql'
plugins:
- '@graphql-eslint'
parserOptions:
parser: '@graphql-eslint/eslint-plugin'
operations: '{,ee/,jh/}app/**/*.graphql'
schema: './tmp/tests/graphql/gitlab_schema_apollo.graphql'
rules:
filenames/match-regex: off
spaced-comment: off
# TODO: We need a way to include this rule + support ee_else_ce fragments
#'@graphql-eslint/unique-fragment-name': error
# TODO: Uncomment these rules when then `schema` is available
#'@graphql-eslint/fragments-on-composite-type': error
#'@graphql-eslint/known-argument-names': error
#'@graphql-eslint/known-type-names': error
'@graphql-eslint/no-anonymous-operations': error
'@graphql-eslint/unique-operation-name': error
'@graphql-eslint/require-id-when-available': error
'@graphql-eslint/no-unused-variables': error
'@graphql-eslint/no-unused-fragments': error
'@graphql-eslint/no-duplicate-fields': error
- files:
- '{,ee/}spec/contracts/consumer/**/*'
rules:
'@gitlab/require-i18n-strings': off
- files:
- 'app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql'
- 'app/assets/javascripts/projects/settings/repository/branch_rules/graphql/mutations/create_branch_rule.mutation.graphql'
- 'app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql'
- 'ee/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql'
- 'ee/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql'
rules:
'@graphql-eslint/require-id-when-available': off
- files:
- '{,spec/}tooling/**/*'
rules:
'no-undef': off
'import/no-commonjs': off
'import/no-extraneous-dependencies': off
'no-restricted-syntax': off
'@gitlab/require-i18n-strings': off

View File

@ -473,7 +473,8 @@
# Code patterns + .ci-patterns # Code patterns + .ci-patterns
.code-patterns: &code-patterns .code-patterns: &code-patterns
- ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" - ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- "eslint.config.mjs"
- ".browserslistrc" - ".browserslistrc"
- ".stylelintrc" - ".stylelintrc"
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*" - "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*"
@ -501,7 +502,8 @@
# .code-patterns + .backstage-patterns # .code-patterns + .backstage-patterns
.code-backstage-patterns: &code-backstage-patterns .code-backstage-patterns: &code-backstage-patterns
- ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" - ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- "eslint.config.mjs"
- ".browserslistrc" - ".browserslistrc"
- ".stylelintrc" - ".stylelintrc"
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*" - "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*"
@ -536,7 +538,8 @@
# .code-patterns + .qa-patterns # .code-patterns + .qa-patterns
.code-qa-patterns: &code-qa-patterns .code-qa-patterns: &code-qa-patterns
- ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" - ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- "eslint.config.mjs"
- ".browserslistrc" - ".browserslistrc"
- ".stylelintrc" - ".stylelintrc"
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*" - "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*"
@ -566,7 +569,8 @@
# .code-patterns + .backstage-patterns + .qa-patterns # .code-patterns + .backstage-patterns + .qa-patterns
.code-backstage-qa-patterns: &code-backstage-qa-patterns .code-backstage-qa-patterns: &code-backstage-qa-patterns
- ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" - ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- "eslint.config.mjs"
- ".browserslistrc" - ".browserslistrc"
- ".stylelintrc" - ".stylelintrc"
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*" - "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*"
@ -614,7 +618,8 @@
- ".stylelintrc" - ".stylelintrc"
- "Dockerfile.assets" - "Dockerfile.assets"
- "vendor/assets/**/*" - "vendor/assets/**/*"
- ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" - ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- "eslint.config.mjs"
- "*_VERSION" - "*_VERSION"
- "{,jh/}Gemfile{,.lock}" - "{,jh/}Gemfile{,.lock}"
- "{,jh/}Gemfile.next{,.lock}" - "{,jh/}Gemfile.next{,.lock}"

View File

@ -3,5 +3,4 @@
Lint/Void: Lint/Void:
Details: grace period Details: grace period
Exclude: Exclude:
- 'ee/lib/gitlab/llm/ai_message.rb'
- 'spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb' - 'spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb'

View File

@ -61,7 +61,6 @@ Rails/InverseOf:
- 'ee/app/models/ee/service_desk_setting.rb' - 'ee/app/models/ee/service_desk_setting.rb'
- 'ee/app/models/ee/user.rb' - 'ee/app/models/ee/user.rb'
- 'ee/app/models/elastic/reindexing_subtask.rb' - 'ee/app/models/elastic/reindexing_subtask.rb'
- 'ee/app/models/elastic/reindexing_task.rb'
- 'ee/app/models/geo/event.rb' - 'ee/app/models/geo/event.rb'
- 'ee/app/models/geo/event_log.rb' - 'ee/app/models/geo/event_log.rb'
- 'ee/app/models/geo/job_artifact_registry.rb' - 'ee/app/models/geo/job_artifact_registry.rb'

View File

@ -387,9 +387,9 @@ gem 'gitlab-license', '~> 2.5', feature_category: :shared
gem 'rack-attack', '~> 6.7.0' # rubocop:todo Gemfile/MissingFeatureCategory gem 'rack-attack', '~> 6.7.0' # rubocop:todo Gemfile/MissingFeatureCategory
# Sentry integration # Sentry integration
gem 'sentry-ruby', '~> 5.19.0', feature_category: :observability gem 'sentry-ruby', '~> 5.21.0', feature_category: :observability
gem 'sentry-rails', '~> 5.19.0', feature_category: :observability gem 'sentry-rails', '~> 5.21.0', feature_category: :observability
gem 'sentry-sidekiq', '~> 5.19.0', feature_category: :observability gem 'sentry-sidekiq', '~> 5.21.0', feature_category: :observability
# PostgreSQL query parsing # PostgreSQL query parsing
# #
@ -582,7 +582,7 @@ group :test do
gem 'shoulda-matchers', '~> 5.1.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory gem 'shoulda-matchers', '~> 5.1.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
gem 'email_spec', '~> 2.2.0' # rubocop:todo Gemfile/MissingFeatureCategory gem 'email_spec', '~> 2.2.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'webmock', '~> 3.23.0', feature_category: :shared gem 'webmock', '~> 3.24.0', feature_category: :shared
gem 'rails-controller-testing' # rubocop:todo Gemfile/MissingFeatureCategory gem 'rails-controller-testing' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'concurrent-ruby', '~> 1.1' # rubocop:todo Gemfile/MissingFeatureCategory gem 'concurrent-ruby', '~> 1.1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'test-prof', '~> 1.4.0', feature_category: :tooling gem 'test-prof', '~> 1.4.0', feature_category: :tooling

View File

@ -661,11 +661,11 @@
{"name":"sawyer","version":"0.9.2","platform":"ruby","checksum":"fa3a72d62a4525517b18857ddb78926aab3424de0129be6772a8e2ba240e7aca"}, {"name":"sawyer","version":"0.9.2","platform":"ruby","checksum":"fa3a72d62a4525517b18857ddb78926aab3424de0129be6772a8e2ba240e7aca"},
{"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"}, {"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"},
{"name":"seed-fu","version":"2.3.7","platform":"ruby","checksum":"f19673443e9af799b730e3d4eca6a89b39e5a36825015dffd00d02ea3365cf74"}, {"name":"seed-fu","version":"2.3.7","platform":"ruby","checksum":"f19673443e9af799b730e3d4eca6a89b39e5a36825015dffd00d02ea3365cf74"},
{"name":"selenium-webdriver","version":"4.23.0","platform":"ruby","checksum":"490aeddee879cfea58a4db6628338d60a905bc56cd5e1a60dfbaa9090a19b801"}, {"name":"selenium-webdriver","version":"4.25.0","platform":"ruby","checksum":"7e11abf2b0fd56df61d98b6d59d621781cf103261d941df3510837547bd4a0d5"},
{"name":"semver_dialects","version":"3.4.3","platform":"ruby","checksum":"ae3ea99f7806693ab031df3121017c102f1a35f4fc2524674055cb446fb9cc82"}, {"name":"semver_dialects","version":"3.4.3","platform":"ruby","checksum":"ae3ea99f7806693ab031df3121017c102f1a35f4fc2524674055cb446fb9cc82"},
{"name":"sentry-rails","version":"5.19.0","platform":"ruby","checksum":"d4ad5323feea8e876f9feb2f50b126a3be3b4f6e137d37c360c31d52b6861995"}, {"name":"sentry-rails","version":"5.21.0","platform":"ruby","checksum":"b5a943d199aff0d3cb94dbac4eb3e00622dd0c55fd1be0cffd43a7e09f0ad602"},
{"name":"sentry-ruby","version":"5.19.0","platform":"ruby","checksum":"0ddf89f246840a5c50df6c68b8eb59ad23ee4adb4a91187a414bb196cee1838b"}, {"name":"sentry-ruby","version":"5.21.0","platform":"ruby","checksum":"294e0dd59afce7e08ba22a4e880924345c75c3e858dc8ee23553716793f78629"},
{"name":"sentry-sidekiq","version":"5.19.0","platform":"ruby","checksum":"1b16ec4b15b35dcbdd182494d612aae7ec5c923a9ed6814aed1b56103feecb80"}, {"name":"sentry-sidekiq","version":"5.21.0","platform":"ruby","checksum":"6df54ec79238f69d9d4b7647bcd2a192a4702f3a39edffd63a455203430e90e2"},
{"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"}, {"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"},
{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"}, {"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"},
{"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"}, {"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"},
@ -767,7 +767,7 @@
{"name":"warning","version":"1.3.0","platform":"ruby","checksum":"23695a5d8e50bd5c46068931b529bee0b28e4982cbcefbe77d867800dde8069e"}, {"name":"warning","version":"1.3.0","platform":"ruby","checksum":"23695a5d8e50bd5c46068931b529bee0b28e4982cbcefbe77d867800dde8069e"},
{"name":"webauthn","version":"3.0.0","platform":"ruby","checksum":"3f77d422c2a8a4b31e56cf42f83414bd066e0506e9896936e1730262dc4a20e6"}, {"name":"webauthn","version":"3.0.0","platform":"ruby","checksum":"3f77d422c2a8a4b31e56cf42f83414bd066e0506e9896936e1730262dc4a20e6"},
{"name":"webfinger","version":"2.1.3","platform":"ruby","checksum":"567a52bde77fb38ca6b67e55db755f988766ec4651c1d24916a65aa70540695c"}, {"name":"webfinger","version":"2.1.3","platform":"ruby","checksum":"567a52bde77fb38ca6b67e55db755f988766ec4651c1d24916a65aa70540695c"},
{"name":"webmock","version":"3.23.1","platform":"ruby","checksum":"0fa738c0767d1c4ec8cc57f6b21998f0c238c8a5b32450df1c847f2767140d95"}, {"name":"webmock","version":"3.24.0","platform":"ruby","checksum":"be01357f6fc773606337ca79f3ba332b7d52cbe5c27587671abc0572dbec7122"},
{"name":"webrick","version":"1.8.1","platform":"ruby","checksum":"19411ec6912911fd3df13559110127ea2badd0c035f7762873f58afc803e158f"}, {"name":"webrick","version":"1.8.1","platform":"ruby","checksum":"19411ec6912911fd3df13559110127ea2badd0c035f7762873f58afc803e158f"},
{"name":"websocket","version":"1.2.10","platform":"ruby","checksum":"2cc1a4a79b6e63637b326b4273e46adcddf7871caa5dc5711f2ca4061a629fa8"}, {"name":"websocket","version":"1.2.10","platform":"ruby","checksum":"2cc1a4a79b6e63637b326b4273e46adcddf7871caa5dc5711f2ca4061a629fa8"},
{"name":"websocket-driver","version":"0.7.6","platform":"java","checksum":"bc894b9e9d5aee55ac04b61003e1957c4ef411a5a048199587d0499785b505c3"}, {"name":"websocket-driver","version":"0.7.6","platform":"java","checksum":"bc894b9e9d5aee55ac04b61003e1957c4ef411a5a048199587d0499785b505c3"},

View File

@ -1694,7 +1694,7 @@ GEM
seed-fu (2.3.7) seed-fu (2.3.7)
activerecord (>= 3.1) activerecord (>= 3.1)
activesupport (>= 3.1) activesupport (>= 3.1)
selenium-webdriver (4.23.0) selenium-webdriver (4.25.0)
base64 (~> 0.2) base64 (~> 0.2)
logger (~> 1.4) logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
@ -1705,14 +1705,14 @@ GEM
pastel (~> 0.8.0) pastel (~> 0.8.0)
thor (~> 1.3) thor (~> 1.3)
tty-command (~> 0.10.1) tty-command (~> 0.10.1)
sentry-rails (5.19.0) sentry-rails (5.21.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.19.0) sentry-ruby (~> 5.21.0)
sentry-ruby (5.19.0) sentry-ruby (5.21.0)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.19.0) sentry-sidekiq (5.21.0)
sentry-ruby (~> 5.19.0) sentry-ruby (~> 5.21.0)
sidekiq (>= 3.0) sidekiq (>= 3.0)
shellany (0.0.1) shellany (0.0.1)
shoulda-matchers (5.1.0) shoulda-matchers (5.1.0)
@ -1930,7 +1930,7 @@ GEM
activesupport activesupport
faraday (~> 2.0) faraday (~> 2.0)
faraday-follow_redirects faraday-follow_redirects
webmock (3.23.1) webmock (3.24.0)
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
@ -2272,9 +2272,9 @@ DEPENDENCIES
seed-fu (~> 2.3.7) seed-fu (~> 2.3.7)
selenium-webdriver (~> 4.21, >= 4.21.1) selenium-webdriver (~> 4.21, >= 4.21.1)
semver_dialects (~> 3.0) semver_dialects (~> 3.0)
sentry-rails (~> 5.19.0) sentry-rails (~> 5.21.0)
sentry-ruby (~> 5.19.0) sentry-ruby (~> 5.21.0)
sentry-sidekiq (~> 5.19.0) sentry-sidekiq (~> 5.21.0)
shoulda-matchers (~> 5.1.0) shoulda-matchers (~> 5.1.0)
sidekiq! sidekiq!
sidekiq-cron (~> 1.12.0) sidekiq-cron (~> 1.12.0)
@ -2319,7 +2319,7 @@ DEPENDENCIES
vmstat (~> 2.3.0) vmstat (~> 2.3.0)
warning (~> 1.3.0) warning (~> 1.3.0)
webauthn (~> 3.0) webauthn (~> 3.0)
webmock (~> 3.23.0) webmock (~> 3.24.0)
webrick (~> 1.8.1) webrick (~> 1.8.1)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
yajl-ruby (~> 1.4.3) yajl-ruby (~> 1.4.3)

View File

@ -674,11 +674,11 @@
{"name":"sawyer","version":"0.9.2","platform":"ruby","checksum":"fa3a72d62a4525517b18857ddb78926aab3424de0129be6772a8e2ba240e7aca"}, {"name":"sawyer","version":"0.9.2","platform":"ruby","checksum":"fa3a72d62a4525517b18857ddb78926aab3424de0129be6772a8e2ba240e7aca"},
{"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"}, {"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"},
{"name":"seed-fu","version":"2.3.7","platform":"ruby","checksum":"f19673443e9af799b730e3d4eca6a89b39e5a36825015dffd00d02ea3365cf74"}, {"name":"seed-fu","version":"2.3.7","platform":"ruby","checksum":"f19673443e9af799b730e3d4eca6a89b39e5a36825015dffd00d02ea3365cf74"},
{"name":"selenium-webdriver","version":"4.23.0","platform":"ruby","checksum":"490aeddee879cfea58a4db6628338d60a905bc56cd5e1a60dfbaa9090a19b801"}, {"name":"selenium-webdriver","version":"4.25.0","platform":"ruby","checksum":"7e11abf2b0fd56df61d98b6d59d621781cf103261d941df3510837547bd4a0d5"},
{"name":"semver_dialects","version":"3.4.3","platform":"ruby","checksum":"ae3ea99f7806693ab031df3121017c102f1a35f4fc2524674055cb446fb9cc82"}, {"name":"semver_dialects","version":"3.4.3","platform":"ruby","checksum":"ae3ea99f7806693ab031df3121017c102f1a35f4fc2524674055cb446fb9cc82"},
{"name":"sentry-rails","version":"5.19.0","platform":"ruby","checksum":"d4ad5323feea8e876f9feb2f50b126a3be3b4f6e137d37c360c31d52b6861995"}, {"name":"sentry-rails","version":"5.21.0","platform":"ruby","checksum":"b5a943d199aff0d3cb94dbac4eb3e00622dd0c55fd1be0cffd43a7e09f0ad602"},
{"name":"sentry-ruby","version":"5.19.0","platform":"ruby","checksum":"0ddf89f246840a5c50df6c68b8eb59ad23ee4adb4a91187a414bb196cee1838b"}, {"name":"sentry-ruby","version":"5.21.0","platform":"ruby","checksum":"294e0dd59afce7e08ba22a4e880924345c75c3e858dc8ee23553716793f78629"},
{"name":"sentry-sidekiq","version":"5.19.0","platform":"ruby","checksum":"1b16ec4b15b35dcbdd182494d612aae7ec5c923a9ed6814aed1b56103feecb80"}, {"name":"sentry-sidekiq","version":"5.21.0","platform":"ruby","checksum":"6df54ec79238f69d9d4b7647bcd2a192a4702f3a39edffd63a455203430e90e2"},
{"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"}, {"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"},
{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"}, {"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"},
{"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"}, {"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"},
@ -782,7 +782,7 @@
{"name":"warning","version":"1.3.0","platform":"ruby","checksum":"23695a5d8e50bd5c46068931b529bee0b28e4982cbcefbe77d867800dde8069e"}, {"name":"warning","version":"1.3.0","platform":"ruby","checksum":"23695a5d8e50bd5c46068931b529bee0b28e4982cbcefbe77d867800dde8069e"},
{"name":"webauthn","version":"3.0.0","platform":"ruby","checksum":"3f77d422c2a8a4b31e56cf42f83414bd066e0506e9896936e1730262dc4a20e6"}, {"name":"webauthn","version":"3.0.0","platform":"ruby","checksum":"3f77d422c2a8a4b31e56cf42f83414bd066e0506e9896936e1730262dc4a20e6"},
{"name":"webfinger","version":"2.1.3","platform":"ruby","checksum":"567a52bde77fb38ca6b67e55db755f988766ec4651c1d24916a65aa70540695c"}, {"name":"webfinger","version":"2.1.3","platform":"ruby","checksum":"567a52bde77fb38ca6b67e55db755f988766ec4651c1d24916a65aa70540695c"},
{"name":"webmock","version":"3.23.1","platform":"ruby","checksum":"0fa738c0767d1c4ec8cc57f6b21998f0c238c8a5b32450df1c847f2767140d95"}, {"name":"webmock","version":"3.24.0","platform":"ruby","checksum":"be01357f6fc773606337ca79f3ba332b7d52cbe5c27587671abc0572dbec7122"},
{"name":"webrick","version":"1.8.1","platform":"ruby","checksum":"19411ec6912911fd3df13559110127ea2badd0c035f7762873f58afc803e158f"}, {"name":"webrick","version":"1.8.1","platform":"ruby","checksum":"19411ec6912911fd3df13559110127ea2badd0c035f7762873f58afc803e158f"},
{"name":"websocket","version":"1.2.10","platform":"ruby","checksum":"2cc1a4a79b6e63637b326b4273e46adcddf7871caa5dc5711f2ca4061a629fa8"}, {"name":"websocket","version":"1.2.10","platform":"ruby","checksum":"2cc1a4a79b6e63637b326b4273e46adcddf7871caa5dc5711f2ca4061a629fa8"},
{"name":"websocket-driver","version":"0.7.6","platform":"java","checksum":"bc894b9e9d5aee55ac04b61003e1957c4ef411a5a048199587d0499785b505c3"}, {"name":"websocket-driver","version":"0.7.6","platform":"java","checksum":"bc894b9e9d5aee55ac04b61003e1957c4ef411a5a048199587d0499785b505c3"},

View File

@ -1720,7 +1720,7 @@ GEM
seed-fu (2.3.7) seed-fu (2.3.7)
activerecord (>= 3.1) activerecord (>= 3.1)
activesupport (>= 3.1) activesupport (>= 3.1)
selenium-webdriver (4.23.0) selenium-webdriver (4.25.0)
base64 (~> 0.2) base64 (~> 0.2)
logger (~> 1.4) logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
@ -1731,14 +1731,14 @@ GEM
pastel (~> 0.8.0) pastel (~> 0.8.0)
thor (~> 1.3) thor (~> 1.3)
tty-command (~> 0.10.1) tty-command (~> 0.10.1)
sentry-rails (5.19.0) sentry-rails (5.21.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.19.0) sentry-ruby (~> 5.21.0)
sentry-ruby (5.19.0) sentry-ruby (5.21.0)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.19.0) sentry-sidekiq (5.21.0)
sentry-ruby (~> 5.19.0) sentry-ruby (~> 5.21.0)
sidekiq (>= 3.0) sidekiq (>= 3.0)
shellany (0.0.1) shellany (0.0.1)
shoulda-matchers (5.1.0) shoulda-matchers (5.1.0)
@ -1957,7 +1957,7 @@ GEM
activesupport activesupport
faraday (~> 2.0) faraday (~> 2.0)
faraday-follow_redirects faraday-follow_redirects
webmock (3.23.1) webmock (3.24.0)
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
@ -2299,9 +2299,9 @@ DEPENDENCIES
seed-fu (~> 2.3.7) seed-fu (~> 2.3.7)
selenium-webdriver (~> 4.21, >= 4.21.1) selenium-webdriver (~> 4.21, >= 4.21.1)
semver_dialects (~> 3.0) semver_dialects (~> 3.0)
sentry-rails (~> 5.19.0) sentry-rails (~> 5.21.0)
sentry-ruby (~> 5.19.0) sentry-ruby (~> 5.21.0)
sentry-sidekiq (~> 5.19.0) sentry-sidekiq (~> 5.21.0)
shoulda-matchers (~> 5.1.0) shoulda-matchers (~> 5.1.0)
sidekiq! sidekiq!
sidekiq-cron (~> 1.12.0) sidekiq-cron (~> 1.12.0)
@ -2346,7 +2346,7 @@ DEPENDENCIES
vmstat (~> 2.3.0) vmstat (~> 2.3.0)
warning (~> 1.3.0) warning (~> 1.3.0)
webauthn (~> 3.0) webauthn (~> 3.0)
webmock (~> 3.23.0) webmock (~> 3.24.0)
webrick (~> 1.8.1) webrick (~> 1.8.1)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
yajl-ruby (~> 1.4.3) yajl-ruby (~> 1.4.3)

View File

@ -1,7 +1,7 @@
<script> <script>
import { GlFormGroup } from '@gitlab/ui'; import { GlFormGroup } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import ChronicDurationInput from '~/vue_shared/components/chronic_duration_input.vue'; import ChronicDurationInput from '~/admin/application_settings/runner_token_expiration/components/chronic_duration_input.vue';
import ExpirationIntervalDescription from './expiration_interval_description.vue'; import ExpirationIntervalDescription from './expiration_interval_description.vue';
export default { export default {

View File

@ -1,3 +0,0 @@
rules:
# https://gitlab.com/gitlab-org/gitlab/issues/28716
import/no-cycle: off

View File

@ -1,5 +0,0 @@
rules:
# https://gitlab.com/gitlab-org/gitlab/issues/28717
import/no-cycle: off
# https://gitlab.com/gitlab-org/gitlab/issues/33024
promise/no-nesting: off

View File

@ -1,3 +0,0 @@
rules:
# https://gitlab.com/gitlab-org/gitlab/issues/28719
import/no-cycle: off

View File

@ -1,5 +0,0 @@
globals:
AP: readonly
rules:
'@gitlab/require-i18n-strings': off
'@gitlab/vue-require-i18n-strings': off

View File

@ -199,7 +199,7 @@ export default {
} }
eventHub.$on('notesApp.updateIssuableConfidentiality', this.setConfidentiality); eventHub.$on('notesApp.updateIssuableConfidentiality', this.setConfidentiality);
Mousetrap.bind(keysFor(ISSUABLE_COMMENT_OR_REPLY), this.quoteReply); Mousetrap.bind(keysFor(ISSUABLE_COMMENT_OR_REPLY), (e) => this.quoteReply(e));
}, },
updated() { updated() {
this.$nextTick(() => { this.$nextTick(() => {
@ -233,9 +233,15 @@ export default {
const node = el.nodeType === Node.TEXT_NODE ? el.parentNode : el; const node = el.nodeType === Node.TEXT_NODE ? el.parentNode : el;
return node.closest('.js-noteable-discussion'); return node.closest('.js-noteable-discussion');
}, },
async quoteReply() { async quoteReply(e) {
const discussionEl = this.getDiscussionInSelection(); const discussionEl = this.getDiscussionInSelection();
const text = await CopyAsGFM.selectionToGfm(); const text = await CopyAsGFM.selectionToGfm();
// Prevent 'r' being written.
if (e && typeof e.preventDefault === 'function') {
e.preventDefault();
}
if (!discussionEl) { if (!discussionEl) {
this.replyInMainEditor(text); this.replyInMainEditor(text);
} else { } else {

View File

@ -0,0 +1,90 @@
<script>
import { GlAlert, GlButton } from '@gitlab/ui';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
export default {
name: 'PackageErrorsCount',
components: {
GlAlert,
GlButton,
},
props: {
errorPackages: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
errorTitleAlert() {
if (this.singleErrorPackage) {
return sprintf(s__('PackageRegistry|There was an error publishing %{packageName}'), {
packageName: this.singleErrorPackage.name,
});
}
return sprintf(s__('PackageRegistry|There was an error publishing %{count} packages'), {
count: this.errorPackages.length,
});
},
errorMessageBodyAlert() {
if (this.singleErrorPackage) {
return this.singleErrorPackage.statusMessage || this.$options.i18n.errorMessageBodyAlert;
}
return sprintf(
s__(
'PackageRegistry|Failed to publish %{count} packages. Delete these packages and try again.',
),
{
count: this.errorPackages.length,
},
);
},
singleErrorPackage() {
if (this.errorPackages.length === 1) {
const [errorPackage] = this.errorPackages;
return errorPackage;
}
return null;
},
showErrorPackageAlert() {
return this.errorPackages.length > 0;
},
errorPackagesHref() {
// For reactivity we depend on showErrorPackageAlert so we update accordingly
if (!this.showErrorPackageAlert) {
return '';
}
const pageParams = { after: null, before: null };
return mergeUrlParams({ status: 'error', ...pageParams }, window.location.href);
},
},
methods: {
handleClick() {
this.$emit('confirm-delete', [this.singleErrorPackage]);
},
},
i18n: {
errorMessageBodyAlert: s__(
'PackageRegistry|There was a timeout and the package was not published. Delete this package and try again.',
),
},
};
</script>
<template>
<gl-alert v-if="showErrorPackageAlert" class="gl-mt-5" variant="danger" :title="errorTitleAlert">
{{ errorMessageBodyAlert }}
<template #actions>
<gl-button v-if="singleErrorPackage" variant="confirm" @click="handleClick">{{
s__('PackageRegistry|Delete this package')
}}</gl-button>
<gl-button v-else :href="errorPackagesHref" variant="confirm">{{
s__('PackageRegistry|Show packages with errors')
}}</gl-button>
</template>
</gl-alert>
</template>

View File

@ -1,11 +1,10 @@
<script> <script>
import { GlAlert, GlButton } from '@gitlab/ui'; import { n__ } from '~/locale';
import { s__, sprintf, n__ } from '~/locale';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue'; import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue'; import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import PackageErrorsCount from '~/packages_and_registries/package_registry/components/list/package_errors_count.vue';
import { import {
DELETE_PACKAGE_TRACKING_ACTION, DELETE_PACKAGE_TRACKING_ACTION,
DELETE_PACKAGES_TRACKING_ACTION, DELETE_PACKAGES_TRACKING_ACTION,
@ -30,9 +29,8 @@ const forwardingFieldToPackageTypeMapping = {
export default { export default {
name: 'PackagesList', name: 'PackagesList',
components: { components: {
GlAlert,
GlButton,
DeleteModal, DeleteModal,
PackageErrorsCount,
PackagesListLoader, PackagesListLoader,
PackagesListRow, PackagesListRow,
RegistryList, RegistryList,
@ -90,51 +88,6 @@ export default {
category, category,
}; };
}, },
errorTitleAlert() {
if (this.singleErrorPackage) {
return sprintf(
s__('PackageRegistry|There was an error publishing a %{packageName} package'),
{ packageName: this.singleErrorPackage.name },
);
}
return sprintf(s__('PackageRegistry|There was an error publishing %{count} packages'), {
count: this.errorPackages.length,
});
},
errorMessageBodyAlert() {
if (this.singleErrorPackage) {
return this.singleErrorPackage.statusMessage || this.$options.i18n.errorMessageBodyAlert;
}
return sprintf(
s__(
'PackageRegistry|%{count} packages were not published to the registry. Remove these packages and try again.',
),
{
count: this.errorPackages.length,
},
);
},
singleErrorPackage() {
if (this.errorPackages.length === 1) {
const [errorPackage] = this.errorPackages;
return errorPackage;
}
return null;
},
showErrorPackageAlert() {
return this.errorPackages.length > 0 && !this.hideErrorAlert;
},
errorPackagesHref() {
// For reactivity we depend on showErrorPackageAlert so we update accordingly
if (!this.showErrorPackageAlert) {
return '';
}
const pageParams = { after: null, before: null };
return mergeUrlParams({ status: 'error', ...pageParams }, window.location.href);
},
packageTypesWithForwardingEnabled() { packageTypesWithForwardingEnabled() {
return Object.keys(this.groupSettings) return Object.keys(this.groupSettings)
.filter((field) => this.groupSettings[field]) .filter((field) => this.groupSettings[field])
@ -183,15 +136,6 @@ export default {
} }
this.itemsToBeDeleted = []; this.itemsToBeDeleted = [];
}, },
showConfirmationModal() {
this.setItemsToBeDeleted([this.singleErrorPackage]);
},
},
i18n: {
errorMessageBodyAlert: s__(
'PackageRegistry|There was a timeout and the package was not published. Delete this package and try again.',
),
deleteThisPackage: s__('PackageRegistry|Delete this package'),
}, },
}; };
</script> </script>
@ -205,22 +149,11 @@ export default {
</div> </div>
<template v-else> <template v-else>
<gl-alert <package-errors-count
v-if="showErrorPackageAlert" v-if="!hideErrorAlert"
class="gl-mt-5" :error-packages="errorPackages"
variant="danger" @confirm-delete="setItemsToBeDeleted"
:title="errorTitleAlert" />
>
{{ errorMessageBodyAlert }}
<template #actions>
<gl-button v-if="singleErrorPackage" variant="confirm" @click="showConfirmationModal">{{
$options.i18n.deleteThisPackage
}}</gl-button>
<gl-button v-else :href="errorPackagesHref" variant="confirm">{{
s__('PackageRegistry|Show packages with errors')
}}</gl-button>
</template>
</gl-alert>
<registry-list <registry-list
data-testid="packages-table" data-testid="packages-table"
:hidden-delete="!canDeletePackages" :hidden-delete="!canDeletePackages"

View File

@ -236,9 +236,6 @@ export default {
displayWikiSpecificMarkdownHelp() { displayWikiSpecificMarkdownHelp() {
return !this.isContentEditorActive; return !this.isContentEditorActive;
}, },
disableSubmitButton() {
return !this.title;
},
drawioEnabled() { drawioEnabled() {
return typeof this.drawioUrl === 'string' && this.drawioUrl.length > 0; return typeof this.drawioUrl === 'string' && this.drawioUrl.length > 0;
}, },
@ -557,7 +554,6 @@ export default {
variant="confirm" variant="confirm"
type="submit" type="submit"
data-testid="wiki-submit-button" data-testid="wiki-submit-button"
:disabled="disableSubmitButton"
>{{ submitButtonText }}</gl-button >{{ submitButtonText }}</gl-button
> >
<gl-button <gl-button

View File

@ -1,36 +0,0 @@
<script>
/**
* Allows to toggle slots based on an array of slot names.
*/
export default {
name: 'SlotSwitch',
props: {
activeSlotNames: {
type: Array,
required: true,
},
tagName: {
type: String,
required: false,
default: 'div',
},
},
computed: {
allSlotNames() {
// eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots
return Object.keys(this.$slots);
},
},
};
</script>
<template>
<component :is="tagName">
<template v-for="slotName in allSlotNames">
<slot v-if="activeSlotNames.includes(slotName)" :name="slotName"></slot>
</template>
</component>
</template>

View File

@ -10,8 +10,10 @@ import {
GlTooltip, GlTooltip,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/work_item_link_child_metadata.vue'; import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/work_item_link_child_metadata.vue';
import RichTimestampTooltip from '../rich_timestamp_tooltip.vue'; import RichTimestampTooltip from '../rich_timestamp_tooltip.vue';
import WorkItemTypeIcon from '../work_item_type_icon.vue'; import WorkItemTypeIcon from '../work_item_type_icon.vue';
@ -22,6 +24,7 @@ import {
WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_LABELS, WIDGET_TYPE_LABELS,
LINKED_CATEGORIES_MAP, LINKED_CATEGORIES_MAP,
INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
} from '../../constants'; } from '../../constants';
import WorkItemRelationshipIcons from './work_item_relationship_icons.vue'; import WorkItemRelationshipIcons from './work_item_relationship_icons.vue';
@ -50,6 +53,14 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [glFeatureFlagMixin()],
inject: {
preventRouterNav: {
from: INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
default: false,
},
isGroup: {},
},
props: { props: {
childItem: { childItem: {
type: Object, type: Object,
@ -137,11 +148,40 @@ export default {
return item.linkType !== LINKED_CATEGORIES_MAP.RELATES_TO; return item.linkType !== LINKED_CATEGORIES_MAP.RELATES_TO;
}); });
}, },
issueAsWorkItem() {
return (
!this.isGroup &&
this.glFeatures.workItemsViewPreference &&
gon.current_user_use_work_items_view
);
},
}, },
methods: { methods: {
showScopedLabel(label) { showScopedLabel(label) {
return isScopedLabel(label) && this.allowsScopedLabels; return isScopedLabel(label) && this.allowsScopedLabels;
}, },
handleTitleClick(e) {
const workItem = this.childItem;
if (e.metaKey || e.ctrlKey) {
return;
}
const escapedFullPath = escapeRegExp(this.workItemFullPath);
// eslint-disable-next-line no-useless-escape
const regex = new RegExp(`groups\/${escapedFullPath}\/-\/(work_items|epics)\/\\d+`);
const isWorkItemPath = regex.test(workItem.webUrl);
if (!(isWorkItemPath || this.issueAsWorkItem) || this.preventRouterNav) {
this.$emit('click', e);
} else {
e.preventDefault();
this.$router.push({
name: 'workItem',
params: {
iid: workItem.iid,
},
});
}
},
}, },
}; };
</script> </script>
@ -171,10 +211,10 @@ export default {
/> />
</span> </span>
<gl-link <gl-link
:href="childItem.webUrl" :href="childItemWebUrl"
:class="{ '!gl-text-secondary': !isChildItemOpen }" :class="{ '!gl-text-secondary': !isChildItemOpen }"
class="gl-hyphens-auto gl-break-words gl-font-semibold" class="gl-hyphens-auto gl-break-words gl-font-semibold"
@click.exact="$emit('click', $event)" @click.exact="handleTitleClick"
@mouseover="$emit('mouseover')" @mouseover="$emit('mouseover')"
@mouseout="$emit('mouseout')" @mouseout="$emit('mouseout')"
> >

View File

@ -864,6 +864,7 @@ export default {
:can-update-children="canUpdateChildren" :can-update-children="canUpdateChildren"
:confidential="workItem.confidential" :confidential="workItem.confidential"
:allowed-child-types="allowedChildTypes" :allowed-child-types="allowedChildTypes"
:is-drawer="isDrawer"
@show-modal="openInModal" @show-modal="openInModal"
@addChild="$emit('addChild')" @addChild="$emit('addChild')"
@childrenLoaded="hasChildren = $event" @childrenLoaded="hasChildren = $event"

View File

@ -3,6 +3,7 @@ import { GlAlert, GlModal } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { removeHierarchyChild } from '../graphql/cache_utils'; import { removeHierarchyChild } from '../graphql/cache_utils';
import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql'; import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
import { INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION } from '../constants';
export default { export default {
WORK_ITEM_DETAIL_MODAL_ID: 'work-item-detail-modal', WORK_ITEM_DETAIL_MODAL_ID: 'work-item-detail-modal',
@ -15,6 +16,9 @@ export default {
GlModal, GlModal,
WorkItemDetail: () => import('./work_item_detail.vue'), WorkItemDetail: () => import('./work_item_detail.vue'),
}, },
provide: {
[INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION]: true,
},
props: { props: {
parentId: { parentId: {
type: String, type: String,

View File

@ -16,6 +16,7 @@ import {
WORKITEM_TREE_SHOWLABELS_LOCALSTORAGEKEY, WORKITEM_TREE_SHOWLABELS_LOCALSTORAGEKEY,
WORK_ITEM_TYPE_VALUE_EPIC, WORK_ITEM_TYPE_VALUE_EPIC,
WIDGET_TYPE_HIERARCHY, WIDGET_TYPE_HIERARCHY,
INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
} from '../../constants'; } from '../../constants';
import { import {
findHierarchyWidgets, findHierarchyWidgets,
@ -48,6 +49,11 @@ export default {
WorkItemRolledUpData, WorkItemRolledUpData,
}, },
inject: ['hasSubepicsFeature'], inject: ['hasSubepicsFeature'],
provide() {
return {
[INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION]: !this.isDrawer,
};
},
props: { props: {
fullPath: { fullPath: {
type: String, type: String,
@ -96,6 +102,11 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
isDrawer: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {

View File

@ -372,3 +372,7 @@ export const WORK_ITEM_BASE_ROUTE_MAP = {
export const WORKITEM_LINKS_SHOWLABELS_LOCALSTORAGEKEY = 'workItemLinks.showLabels'; export const WORKITEM_LINKS_SHOWLABELS_LOCALSTORAGEKEY = 'workItemLinks.showLabels';
export const WORKITEM_TREE_SHOWLABELS_LOCALSTORAGEKEY = 'workItemTree.showLabels'; export const WORKITEM_TREE_SHOWLABELS_LOCALSTORAGEKEY = 'workItemTree.showLabels';
export const WORKITEM_RELATIONSHIPS_SHOWLABELS_LOCALSTORAGEKEY = 'workItemRelationships.showLabels'; export const WORKITEM_RELATIONSHIPS_SHOWLABELS_LOCALSTORAGEKEY = 'workItemRelationships.showLabels';
export const INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION = Symbol(
'injection:prevent-router-navigation',
);

View File

@ -19,11 +19,14 @@ class ServiceDeskSetting < ApplicationRecord
allow_blank: true, allow_blank: true,
format: { with: /\A[a-z0-9_]+\z/, message: ->(setting, data) { _("can contain only lowercase letters, digits, and '_'.") } } format: { with: /\A[a-z0-9_]+\z/, message: ->(setting, data) { _("can contain only lowercase letters, digits, and '_'.") } }
# Don't use Devise.email_regexp or URI::MailTo::EMAIL_REGEXP to be a bit more restrictive
# on the format of an email. For example because we don't want to allow `+` and other
# subaddress delimeters in the local part.
validates :custom_email, validates :custom_email,
length: { maximum: 255 }, length: { maximum: 255 },
uniqueness: true, uniqueness: true,
allow_nil: true, allow_nil: true,
format: Gitlab::Utils::Email::EMAIL_REGEXP_WITH_ANCHORS format: Gitlab::Email::ServiceDesk::CustomEmail::EMAIL_REGEXP_WITH_ANCHORS
validates :custom_email_credential, validates :custom_email_credential,
presence: true, presence: true,

View File

@ -1665,7 +1665,8 @@ class User < ApplicationRecord
counts = Organizations::OrganizationUser counts = Organizations::OrganizationUser
.owners .owners
.joins('INNER JOIN ownerships ON ownerships.organization_id = organization_users.organization_id') .where('organization_users.organization_id = organizations.id')
.group(:organization_id)
.having('count(organization_users.user_id) = 1') .having('count(organization_users.user_id) = 1')
Organizations::Organization Organizations::Organization

View File

@ -4,7 +4,7 @@ module Packages
module Conan module Conan
class CreatePackageService < ::Packages::CreatePackageService class CreatePackageService < ::Packages::CreatePackageService
def execute def execute
create_package!(:conan, created_package = create_package!(:conan,
name: params[:package_name], name: params[:package_name],
version: params[:package_version], version: params[:package_version],
conan_metadatum_attributes: { conan_metadatum_attributes: {
@ -12,6 +12,10 @@ module Packages
package_channel: params[:package_channel] package_channel: params[:package_channel]
} }
) )
ServiceResponse.success(payload: { package: created_package })
rescue ActiveRecord::RecordInvalid => e
ServiceResponse.error(message: e.message, reason: :record_invalid)
end end
end end
end end

View File

@ -39,6 +39,18 @@
}, },
"additionalProperties": false "additionalProperties": false
}, },
"^gitlab_secrets_manager$": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
},
"additionalProperties": false
},
"^gcp_secret_manager$": { "^gcp_secret_manager$": {
"type": "object", "type": "object",
"required": [ "required": [
@ -198,6 +210,11 @@
"required": [ "required": [
"akeyless" "akeyless"
] ]
},
{
"required": [
"gitlab_secrets_manager"
]
} }
], ],
"additionalProperties": false "additionalProperties": false

View File

@ -9,15 +9,17 @@ const imagesPaths = [
async function getAllFiles(dir, prependPath = '') { async function getAllFiles(dir, prependPath = '') {
const result = []; const result = [];
let files = [] let files = [];
try { try {
files = await readdir(dir, { withFileTypes: true }); files = await readdir(dir, { withFileTypes: true });
} catch(e) {} // eslint-disable-next-line no-empty
} catch (e) {}
for (const file of files) { for (const file of files) {
const filePath = path.join(dir, file.name); const filePath = path.join(dir, file.name);
if (file.isDirectory()) { if (file.isDirectory()) {
// eslint-disable-next-line no-await-in-loop
const nestedFiles = await getAllFiles(filePath, `${prependPath}${file.name}/`); const nestedFiles = await getAllFiles(filePath, `${prependPath}${file.name}/`);
result.push(...nestedFiles); result.push(...nestedFiles);
} else { } else {
@ -32,19 +34,26 @@ export async function ImagesPlugin() {
return { return {
name: 'vite-plugin-gitlab-images', name: 'vite-plugin-gitlab-images',
async config() { async config() {
const [CEfiles, EEfiles, JHfiles] = await Promise.all(imagesPaths.map(async imagesPath => await getAllFiles(imagesPath))); const [CEfiles, EEfiles, JHfiles] = await Promise.all(
// eslint-disable-next-line no-return-await
imagesPaths.map(async (imagesPath) => await getAllFiles(imagesPath)),
);
const [CEpath, EEpath, JHpath] = imagesPaths; const [CEpath, EEpath, JHpath] = imagesPaths;
const mappings = [[CEpath, CEfiles], [EEpath, EEfiles], [JHpath, JHfiles]].reduce((acc, [filesPath, filenames]) => { const mappings = [
filenames.forEach(filename => { [CEpath, CEfiles],
[EEpath, EEfiles],
[JHpath, JHfiles],
].reduce((acc, [filesPath, filenames]) => {
filenames.forEach((filename) => {
acc[filename] = path.resolve(filesPath, filename); acc[filename] = path.resolve(filesPath, filename);
}); });
return acc; return acc;
}, {}); }, {});
const alias = Object.keys(mappings).map(mapping => { const alias = Object.keys(mappings).map((mapping) => {
return { return {
find: mapping, find: mapping,
replacement: mappings[mapping], replacement: mappings[mapping],
} };
}); });
return { return {
resolve: { resolve: {

View File

@ -44,7 +44,12 @@
"type": "number" "type": "number"
} }
}, },
"additionalProperties": false "additionalProperties": {
"type": [
"string",
"number"
]
}
} }
}, },
"additionalProperties": false "additionalProperties": false

View File

@ -4,9 +4,9 @@ class DropCiPipelinesConfig < Gitlab::Database::Migration[2.2]
milestone '17.6' milestone '17.6'
def up def up
execute(<<~SQL) drop_table(:ci_pipelines_config, if_exists: true)
ALTER TABLE p_ci_pipelines_config DETACH PARTITION ci_pipelines_config;
execute(<<~SQL)
CREATE TABLE IF NOT EXISTS #{fully_qualified_partition_name(100)} CREATE TABLE IF NOT EXISTS #{fully_qualified_partition_name(100)}
PARTITION OF p_ci_pipelines_config FOR VALUES IN (100); PARTITION OF p_ci_pipelines_config FOR VALUES IN (100);
@ -16,8 +16,6 @@ class DropCiPipelinesConfig < Gitlab::Database::Migration[2.2]
CREATE TABLE IF NOT EXISTS #{fully_qualified_partition_name(102)} CREATE TABLE IF NOT EXISTS #{fully_qualified_partition_name(102)}
PARTITION OF p_ci_pipelines_config FOR VALUES IN (102); PARTITION OF p_ci_pipelines_config FOR VALUES IN (102);
SQL SQL
drop_table(:ci_pipelines_config, if_exists: true)
end end
def down def down

View File

@ -104,34 +104,48 @@ Even if a change request meets the minimum lead time, it might not be applied du
### Bring your own domain (BYOD) ### Bring your own domain (BYOD)
You can add a [custom hostname](../../subscriptions/gitlab_dedicated/index.md#bring-your-own-domain) for your GitLab Dedicated instance. Optionally, you can also provide a custom hostname for the bundled container registry and KAS services. You can use a [custom hostname](../../subscriptions/gitlab_dedicated/index.md#bring-your-own-domain) to access your GitLab Dedicated instance. You can also provide a custom hostname for the bundled container registry and Kubernetes Agent Server (KAS) services.
Prerequisites: #### Let's Encrypt certificates
- Access to your domain's server control panel to set up DNS records. GitLab Dedicated integrates with [Let's Encrypt](https://letsencrypt.org/), a free, automated, and open source certificate authority. When you use a custom hostname, Let's Encrypt automatically issues and renews SSL/TLS certificates for your domain.
This integration uses the [`http-01` challenge](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) to obtain certificates through Let's Encrypt.
#### Set up DNS records #### Set up DNS records
Custom domains require a: To use a custom hostname with GitLab Dedicated, you must update your domain's DNS records.
- `CNAME` record: Add a `CNAME` record that points your custom hostname to `tenant_name.gitlab-dedicated.com`. Prerequisites:
```plaintext - Access to your domain host's DNS settings.
gitlab.my-company.com. CNAME tenant_name.gitlab-dedicated.com
```
- `CAA` record: If your domain has an existing `CAA` (Certification Authority Authorization) record, [add a `CAA` record for Let's Encrypt](https://letsencrypt.org/docs/caa/). This allows Let's Encrypt to also issue certificates for your domain. To set up DNS records for a custom hostname with GitLab Dedicated:
```plaintext 1. Sign in to your domain host's website.
example.com. IN CAA 0 issue "pki.goog"
example.com. IN CAA 0 issue "letsencrypt.org"
```
In this example, the `CAA` record defines Google Trust Services (`"pki.goog"`) and Let's Encrypt (`"letsencrypt.org"`) as certificate authorities that are allowed to issue certificates for your domain. 1. Go to the DNS settings.
#### Add a custom hostname 1. Add a `CNAME` record that points your custom hostname to your GitLab Dedicated tenant. For example:
To add a custom hostname after your instance is created, submit a [support ticket](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=4414917877650). ```plaintext
gitlab.my-company.com. CNAME my-tenant.gitlab-dedicated.com
```
1. Optional. If your domain has an existing `CAA` record, update it to include [Let's Encrypt](https://letsencrypt.org/docs/caa/) as a valid certificate authority. If your domain does not have any `CAA` records, you can skip this step. For example:
```plaintext
example.com. IN CAA 0 issue "pki.goog"
example.com. IN CAA 0 issue "letsencrypt.org"
```
In this example, the `CAA` record defines Google Trust Services (`pki.goog`) and Let's Encrypt (`letsencrypt.org`) as certificate authorities that are allowed to issue certificates for your domain.
1. Save your changes and wait for the DNS changes to propagate.
#### Add your custom hostname
To add a custom hostname to your existing GitLab Dedicated instance, submit a [support ticket](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=4414917877650).
### SMTP email service ### SMTP email service

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

View File

@ -25,7 +25,7 @@ This page collects a set of architectural documents and diagrams for GitLab Dedi
The following diagram shows a high-level overview of the architecture for GitLab Dedicated, The following diagram shows a high-level overview of the architecture for GitLab Dedicated,
where various AWS accounts managed by GitLab and customers are controlled by a Switchboard application. where various AWS accounts managed by GitLab and customers are controlled by a Switchboard application.
![Diagram of a high-level overview of the GitLab Dedicated architecture.](img/high_level_architecture_diagram_v17_0.png) ![Diagram of a high-level overview of the GitLab Dedicated architecture.](img/high_level_architecture_diagram_v18_0.png)
When managing GitLab Dedicated tenant instances: When managing GitLab Dedicated tenant instances:

View File

@ -119,16 +119,17 @@ page, with these behaviors:
- It doesn't pick people whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status): - It doesn't pick people whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status):
- Contains the string `OOO`, `PTO`, `Parental Leave`, `Friends and Family`, or `Conference`. - Contains the string `OOO`, `PTO`, `Parental Leave`, `Friends and Family`, or `Conference`.
- Emoji is from one of these categories: - Emoji is from one of these categories:
- **On leave** - 🌴 `:palm_tree:`, 🏖️ `:beach:`, ⛱ `:beach_umbrella:`, 🏖 `:beach_with_umbrella:`, 🌞 `:sun_with_face:`, 🎡 `:ferris_wheel:`, 🏙 `:cityscape:` - **On leave** - 🌴 `palm_tree`, 🏖️ `beach`, ⛱ `beach_umbrella`, 🏖 `beach_with_umbrella`, 🌞 `sun_with_face`, 🎡 `ferris_wheel`, 🏙 `cityscape`
- **Out sick** - 🌡️ `:thermometer:`, 🤒 `:face_with_thermometer:` - **Out sick** - 🌡️ `thermometer`, 🤒 `face_with_thermometer`
- Important: The status emojis are not detected when present on the free text input **status message**. They have to be set on your GitLab **status emoji** by clicking on the emoji selector beside the text input.
- It doesn't pick people who are already assigned a number of reviews that is equal to - It doesn't pick people who are already assigned a number of reviews that is equal to
or greater than their chosen "review limit". The review limit is the maximum number of or greater than their chosen "review limit". The review limit is the maximum number of
reviews people are ready to handle at a time. Set a review limit by using one of the following reviews people are ready to handle at a time. Set a review limit by using one of the following
as a Slack or [GitLab status](../user/profile/index.md#set-your-current-status): as a Slack or [GitLab status](../user/profile/index.md#set-your-current-status):
- 2⃣ - `:two:` - 2⃣ - `two`
- 3⃣ - `:three:` - 3⃣ - `three`
- 4⃣ - `:four:` - 4⃣ - `four`
- 5⃣ - `:five:` - 5⃣ - `five`
The minimum review limit is 2⃣. The reason for not being able to completely turn oneself off The minimum review limit is 2⃣. The reason for not being able to completely turn oneself off
for reviews has been discussed [in this issue](https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/377). for reviews has been discussed [in this issue](https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/377).

View File

@ -176,6 +176,14 @@ Whereas, this filter is even more restricted and only includes `pull_package` ev
property: deploy_token property: deploy_token
``` ```
Filters support also [custom additional properties](quick_start.md#additional-properties):
```yaml
- name: pull_package
filter:
custom_key: custom_value
```
Filters only support matching of exact values and not wildcards or regular expressions. Filters only support matching of exact values and not wildcards or regular expressions.
## Aggregated metrics ## Aggregated metrics

View File

@ -85,10 +85,7 @@ Tracking classes already have three built-in properties:
- `value`(numeric) - `value`(numeric)
The arbitrary naming and typing of the these three properties is due to constraints from the data extraction process. The arbitrary naming and typing of the these three properties is due to constraints from the data extraction process.
It's recommended to use these properties first, even if their name does not match with the data you want to track. It's recommended to use these properties first, even if their name does not match with the data you want to track. You can further describe what is the actual data being tracked by using the `description` property in the YAML definition of the event. For an example, see
This recommendation is particularly important if you want to leverage these attributes as
[metric filters](metric_definition_guide.md#filters). You can further describe what is the actual data being tracked
by using the `description` property in the YAML definition of the event. For an example, see
[`create_ci_internal_pipeline.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/537ea367dab731e886e6040d8399c430fdb67ab7/config/events/create_ci_internal_pipeline.yml): [`create_ci_internal_pipeline.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/537ea367dab731e886e6040d8399c430fdb67ab7/config/events/create_ci_internal_pipeline.yml):
```ruby ```ruby
@ -127,9 +124,7 @@ track_internal_event(
) )
``` ```
Please add custom properties only in addition to the built-in properties. Please add custom properties only in addition to the built-in properties. Additional properties can only have string or numeric values.
Custom rules can not be used as [metric filters](metric_definition_guide.md#filters).
#### Controller and API helpers #### Controller and API helpers

View File

@ -22,8 +22,8 @@ You can migrate GitLab groups:
- Between groups in the same GitLab instance. - Between groups in the same GitLab instance.
WARNING: WARNING:
Migrating GitLab groups and projects by using direct transfer is [currently unavailable](https://status.gitlab.com). We don't have an Migrating GitLab.com groups and projects by using direct transfer is [unavailable](https://status.gitlab.com).
estimated time for resolution. For more information, please [contact support](https://about.gitlab.com/support/). For more information, contact [GitLab Support](https://about.gitlab.com/support/).
Migration by direct transfer creates a new copy of the group. If you want to move groups instead of copying groups, you Migration by direct transfer creates a new copy of the group. If you want to move groups instead of copying groups, you
can [transfer groups](../manage.md#transfer-a-group) if the groups are in the same GitLab instance. Transferring groups can [transfer groups](../manage.md#transfer-a-group) if the groups are in the same GitLab instance. Transferring groups

View File

@ -18,9 +18,9 @@ DETAILS:
Import your projects from Bitbucket Server to GitLab. Import your projects from Bitbucket Server to GitLab.
WARNING: WARNING:
Importing from Bitbucket Server to GitLab.com is [currently unavailable](https://status.gitlab.com). We don't have an Importing from Bitbucket Server to GitLab.com is [unavailable](https://status.gitlab.com).
estimated time for resolution. For more information, please [contact support](https://about.gitlab.com/support/). For more information, contact [GitLab Support](https://about.gitlab.com/support/).
This unavailability doesn't affect [importing from Bitbucket Cloud](bitbucket.md). [Importing from Bitbucket Cloud](bitbucket.md) is not affected.
## Prerequisites ## Prerequisites

View File

@ -17,8 +17,8 @@ DETAILS:
Import your projects from Gitea to GitLab. Import your projects from Gitea to GitLab.
WARNING: WARNING:
Importing from Gitea to GitLab.com is [currently unavailable](https://status.gitlab.com). We don't have an Importing from Gitea to GitLab.com is [unavailable](https://status.gitlab.com).
estimated time for resolution. For more information, please [contact support](https://about.gitlab.com/support/). For more information, contact [GitLab Support](https://about.gitlab.com/support/).
The Gitea importer can import: The Gitea importer can import:

View File

@ -18,8 +18,8 @@ You can import your GitHub projects from either GitHub.com or GitHub Enterprise.
migrate or import any types of groups or organizations from GitHub to GitLab. migrate or import any types of groups or organizations from GitHub to GitLab.
WARNING: WARNING:
Importing from GitHub to GitLab.com is [currently unavailable](https://status.gitlab.com). We don't have an Importing from GitHub to GitLab.com is [unavailable](https://status.gitlab.com).
estimated time for resolution. For more information, please [contact support](https://about.gitlab.com/support/). For more information, contact [GitLab Support](https://about.gitlab.com/support/).
Imported issues, merge requests, comments, and events have an **Imported** badge in GitLab. Imported issues, merge requests, comments, and events have an **Imported** badge in GitLab.

View File

@ -43,8 +43,11 @@ Generate a detailed description for an issue based on a short summary you provid
Prerequisites: Prerequisites:
- You must belong to at least one group with the [experiment and beta features setting](../../gitlab_duo/turn_on_off.md#turn-on-beta-and-experimental-features) enabled. - You must belong to at least one group with the [experiment and beta features setting](../../gitlab_duo/turn_on_off.md#turn-on-beta-and-experimental-features) enabled.
- You must have permission to view the issue. - You must have permission to create an issue.
- Only available for the plain text editor. - Only available for the plain text editor.
- Only available when creating a new issue.
For a proposal to add support for generating descriptions when editing existing issues, see
[issue 474141](https://gitlab.com/gitlab-org/gitlab/-/issues/474141).
To generate an issue description: To generate an issue description:
@ -59,22 +62,6 @@ Provide feedback on this experimental feature in [issue 409844](https://gitlab.c
**Data usage**: When you use this feature, the text you enter is sent to **Data usage**: When you use this feature, the text you enter is sent to
the [large language model listed on the GitLab Duo page](../../gitlab_duo/index.md#issue-description-generation). the [large language model listed on the GitLab Duo page](../../gitlab_duo/index.md#issue-description-generation).
### Remove a task list item
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9.
Prerequisites:
- You must have at least the Reporter role for the project, or be the author or assignee of the issue.
In an issue description with task list items:
1. Hover over a task list item and select the options menu (**{ellipsis_v}**).
1. Select **Delete**.
The task list item is removed from the issue description.
Any nested task list items are moved up a nested level.
## Bulk edit issues from a project ## Bulk edit issues from a project
You can edit multiple issues at a time when you're in a project. You can edit multiple issues at a time when you're in a project.
@ -153,14 +140,16 @@ To move an issue:
1. Search for a project to move the issue to. 1. Search for a project to move the issue to.
1. Select **Move**. 1. Select **Move**.
You can also use the `/move` [quick action](../quick_actions.md) in a comment or description.
### Moving tasks when the parent issue is moved ### Moving tasks when the parent issue is moved
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371252) in GitLab 16.9 [with a flag](../../../administration/feature_flags.md) named `move_issue_children`. Disabled by default. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371252) in GitLab 16.9 [with a flag](../../../administration/feature_flags.md) named `move_issue_children`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/371252) in GitLab 16.11. > - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/371252) in GitLab 16.11.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/371252) in GitLab 17.3. Feature flag `move_issue_children` removed. > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/371252) in GitLab 17.3. Feature flag `move_issue_children` removed.
When you move an issue to another project, all its child tasks are also When you move an issue to another project, all its child tasks are also moved to the target project
moved to the target project and remain associated as child tasks on the moved issue. and remain as child tasks of the moved issue.
Each task is moved the same way as the parent, that is, it's closed in the original project and Each task is moved the same way as the parent, that is, it's closed in the original project and
copied to the target project. copied to the target project.
@ -224,7 +213,31 @@ To do it:
1. To exit the Rails console, enter `quit`. 1. To exit the Rails console, enter `quit`.
## Reorder list items in the issue description ## Description lists and task lists
When you use ordered lists, unordered lists, or task lists in issue descriptions, you can:
- Reorder list items with drag and drop
- Delete list items
- [Convert task list items to GitLab Tasks](../../tasks.md#from-a-task-list-item)
### Delete a task list item
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9.
Prerequisites:
- You must have at least the Reporter role for the project, or be the author or assignee of the issue.
In an issue description with task list items:
1. Hover over a task list item and select the options menu (**{ellipsis_v}**).
1. Select **Delete**.
The task list item is removed from the issue description.
Any nested task list items are moved up a nested level.
### Reorder list items in the issue description
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15260) in GitLab 15.0. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15260) in GitLab 15.0.
@ -261,6 +274,8 @@ To close an issue, you can either:
1. Select **Plan > Issues**, then select your issue to view it. 1. Select **Plan > Issues**, then select your issue to view it.
1. In the upper-right corner, select **Issue actions** (**{ellipsis_v}**) and then **Close issue**. 1. In the upper-right corner, select **Issue actions** (**{ellipsis_v}**) and then **Close issue**.
You can also use the `/close` [quick action](../quick_actions.md) in a comment or description.
### Reopen a closed issue ### Reopen a closed issue
Prerequisites: Prerequisites:
@ -270,6 +285,8 @@ Prerequisites:
To reopen a closed issue, in the upper-right corner, select **Issue actions** (**{ellipsis_v}**) and then **Reopen issue**. To reopen a closed issue, in the upper-right corner, select **Issue actions** (**{ellipsis_v}**) and then **Reopen issue**.
A reopened issue is no different from any other open issue. A reopened issue is no different from any other open issue.
You can also use the `/reopen` [quick action](../quick_actions.md) in a comment or description.
### Closing issues automatically ### Closing issues automatically
You can close issues automatically by using certain words, called a _closing pattern_, You can close issues automatically by using certain words, called a _closing pattern_,
@ -438,13 +455,12 @@ DETAILS:
You can promote an issue to an [epic](../../group/epics/index.md) in the immediate parent group. You can promote an issue to an [epic](../../group/epics/index.md) in the immediate parent group.
NOTE: Promoting a confidential issue to an epic creates a
Promoting a confidential issue to an epic makes all information [confidential epic](../../group/epics/manage_epics.md#make-an-epic-confidential), retaining
related to the issue public, as epics are public to group members. confidentiality.
When an issue is promoted to an epic: When an issue is promoted to an epic:
- If the issue was confidential, an additional warning is displayed first.
- An epic is created in the same group as the project of the issue. - An epic is created in the same group as the project of the issue.
- Subscribers of the issue are notified that the epic was created. - Subscribers of the issue are notified that the epic was created.
@ -494,7 +510,11 @@ To add an issue to an [iteration](../../group/iterations/index.md):
1. From the dropdown list, select the iteration to add this issue to. 1. From the dropdown list, select the iteration to add this issue to.
1. Select any area outside the dropdown list. 1. Select any area outside the dropdown list.
Alternatively, you can use the `/iteration` [quick action](../quick_actions.md#issues-merge-requests-and-epics). To add an issue to an iteration, you can also:
- Use the `/iteration` [quick action](../quick_actions.md#issues-merge-requests-and-epics)
- Drag an issue into an iteration list in a board
- Bulk edit issues from the issues list
## View all issues assigned to you ## View all issues assigned to you
@ -557,8 +577,7 @@ To filter the list issues for text in a title or description:
1. On the left sidebar, select **Search or go to** and find your project. 1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Plan > Issues**. 1. Select **Plan > Issues**.
1. Above the list of issues, in the **Search or filter results** text box, enter the searched phrase. 1. Above the list of issues, in the **Search or filter results** text box, enter the searched phrase.
1. In the dropdown list that appears, select **Search for this text**. 1. In the dropdown list that appears, select **Search within**, and then either **Titles** or **Descriptions**.
1. Select the text box again, and in the dropdown list that appears, select **Search Within**, and then either **Titles** or **Descriptions**.
1. Press <kbd>Enter</kbd> or select the search icon (**{search}**). 1. Press <kbd>Enter</kbd> or select the search icon (**{search}**).
Filtering issues uses [PostgreSQL full text search](https://www.postgresql.org/docs/current/textsearch-intro.html) Filtering issues uses [PostgreSQL full text search](https://www.postgresql.org/docs/current/textsearch-intro.html)
@ -567,7 +586,7 @@ to match meaningful and significant words to answer a query.
For example, if you search for `I am securing information for M&A`, For example, if you search for `I am securing information for M&A`,
GitLab can return results with `securing`, `secured`, GitLab can return results with `securing`, `secured`,
or `information` in the title or description. or `information` in the title or description.
However, GitLab won't match the sentence or the words `I`, `am` or `M&A` exactly, However, GitLab doesn't match the sentence or the words `I`, `am` or `M&A` exactly,
as they aren't deemed lexically meaningful or significant. as they aren't deemed lexically meaningful or significant.
It's a limitation of PostgreSQL full text search. It's a limitation of PostgreSQL full text search.
@ -591,7 +610,7 @@ You can use the OR operator (**is one of: `||`**) when you [filter the list of i
1. On the left sidebar, select **Search or go to** and find your project. 1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Plan > Issues**. 1. Select **Plan > Issues**.
1. In the **Search** box, type the issue ID. For example, enter filter `#10` to return only issue 10. 1. In the **Search** box, type `#` followed by the issue ID. For example, enter filter `#10` to return only issue 10.
![filter issues by specific ID](img/issue_search_by_id_v15_0.png) ![filter issues by specific ID](img/issue_search_by_id_v15_0.png)

View File

@ -10,7 +10,10 @@ DETAILS:
**Tier:** Free, Premium, Ultimate **Tier:** Free, Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated **Offering:** GitLab.com, Self-managed, GitLab Dedicated
Milestones in GitLab are a way to track issues and merge requests created to achieve a broader goal in a certain period of time. Milestones in GitLab are a way to track issues and merge requests created to achieve a broader goal
in a certain period of time, such as a program increment or upcoming release.
When you use milestones together with [iterations](../../group/iterations/index.md), you can track
work across two concurrent timeboxes with different start and end dates.
Milestones allow you to organize issues and merge requests into a cohesive group, with an optional start date and an optional due date. Milestones allow you to organize issues and merge requests into a cohesive group, with an optional start date and an optional due date.
@ -18,11 +21,14 @@ Milestones allow you to organize issues and merge requests into a cohesive group
Milestones can be used to track releases. To do so: Milestones can be used to track releases. To do so:
1. Set the milestone due date to represent the release date of your release and leave the milestone start date blank. 1. Set the milestone due date to represent the release date of your release.
If you do not have a defined start date for your release cycle, you can leave the milestone start
date blank.
1. Set the milestone title to the version of your release, such as `Version 9.4`. 1. Set the milestone title to the version of your release, such as `Version 9.4`.
1. Add an issue to your release by associating the desired milestone from the issue's right-hand sidebar. 1. Add issues to your release by selecting the milestone from the issue's right sidebar.
Additionally, you can integrate milestones with the [Releases feature](../releases/index.md#associate-milestones-with-a-release). Additionally, to automatically generate release evidence when you create your release, integrate
milestones with the [Releases feature](../releases/index.md#associate-milestones-with-a-release).
## Project milestones and group milestones ## Project milestones and group milestones
@ -44,7 +50,7 @@ To view the milestone list:
1. Select **Plan > Milestones**. 1. Select **Plan > Milestones**.
In a project, GitLab displays milestones that belong to the project. In a project, GitLab displays milestones that belong to the project.
In a group, GitLab displays milestones that belong to the group and all projects in the group. In a group, GitLab displays milestones that belong to the group and all projects and subgroups in the group.
### View milestones in a project with issues turned off ### View milestones in a project with issues turned off
@ -98,16 +104,19 @@ The tabs below the title and description show the following:
The milestone view contains a [burndown and burnup chart](burndown_and_burnup_charts.md), The milestone view contains a [burndown and burnup chart](burndown_and_burnup_charts.md),
showing the progress of completing a milestone. showing the progress of completing a milestone.
![burndown chart](img/burndown_and_burnup_charts_v15_3.png) ![burndown and burnup chart](img/burndown_and_burnup_charts_v15_3.png)
#### Milestone sidebar #### Milestone sidebar
The milestone sidebar on the milestone view shows the following: The sidebar on the milestone view shows the following:
- Percentage complete, which is calculated as number of closed issues divided by total number of issues. - Percentage complete, which is calculated as number of closed issues divided by total number of issues.
- The start date and due date. - The start date and due date.
- The total time spent on all issues and merge requests assigned to the milestone. - The total time spent on all issues and merge requests assigned to the milestone.
- The total issue weight of all issues assigned to the milestone. - The total issue weight of all issues assigned to the milestone.
- The count of total, open, closed, and merged merge requests.
- Links to associated releases.
- The milestone's reference you can copy to your clipboard.
![Project milestone page](img/milestones_project_milestone_page_sidebar_v13_11.png) ![Project milestone page](img/milestones_project_milestone_page_sidebar_v13_11.png)
@ -185,7 +194,8 @@ To delete a milestone:
## Promote a project milestone to a group milestone ## Promote a project milestone to a group milestone
If you are expanding the number of projects in a group, you might want to share the same milestones If you are expanding the number of projects in a group, you might want to share the same milestones
among this group's projects. You can also promote project milestones to group milestones to among this group's projects.
You can promote project milestones to the parent group to
make them available to other projects in the same group. make them available to other projects in the same group.
Promoting a milestone merges all project milestones across all projects in this group with the same Promoting a milestone merges all project milestones across all projects in this group with the same
@ -223,7 +233,11 @@ To assign or unassign a milestone:
You can select from both project and group milestones. You can select from both project and group milestones.
1. Select the milestone you want to assign. 1. Select the milestone you want to assign.
You can also use the `/assign` [quick action](../quick_actions.md) in a comment. To assign or unassign a milestone, you can also:
- Use the `/milestone` [quick action](../quick_actions.md) in a comment or description
- Drag an issue to a [milestone list](../issue_board.md#milestone-lists) in a board
- [Bulk edit issues](../issues/managing_issues.md#bulk-edit-issues-from-a-project) from the issues list
## Filter issues and merge requests by milestone ## Filter issues and merge requests by milestone

683
eslint.config.mjs Normal file
View File

@ -0,0 +1,683 @@
/* eslint-disable import/no-default-export */
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import localRules from 'eslint-plugin-local-rules';
import js from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
const compat = new FlatCompat({
baseDirectory: dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
const jestConfig = {
files: ['{,ee/}spec/frontend/**/*.js'],
settings: {
// We have to teach eslint-plugin-import what node modules we use
// otherwise there is an error when it tries to resolve them
'import/core-modules': ['events', 'fs', 'path'],
'import/resolver': {
jest: {
jestConfigFile: 'jest.config.js',
},
},
},
rules: {
'@gitlab/vtu-no-explicit-wrapper-destroy': 'error',
'jest/expect-expect': [
'off',
{
assertFunctionNames: ['expect*', 'assert*', 'testAction'],
},
],
'@gitlab/no-global-event-off': 'off',
'import/no-unresolved': [
'error',
// The test fixtures and graphql schema are dynamically generated in CI
// during the `frontend-fixtures` and `graphql-schema-dump` jobs.
// They may not be present during linting.
{
ignore: ['^test_fixtures/', 'tmp/tests/graphql/gitlab_schema.graphql'],
},
],
},
};
export default [
{
ignores: [
'app/assets/javascripts/locale/**/app.js',
'builds/',
'coverage/',
'coverage-frontend/',
'node_modules/',
'public/',
'tmp/',
'vendor/',
'sitespeed-result/',
'fixtures/**/*.graphql',
'storybook/public',
'spec/fixtures/**/*.graphql',
],
},
...compat.extends(
'plugin:@gitlab/default',
'plugin:@gitlab/i18n',
'plugin:no-jquery/slim',
'plugin:no-jquery/deprecated-3.4',
'plugin:no-unsanitized/recommended-legacy',
'./tooling/eslint-config/conditionally_ignore.js',
'plugin:@gitlab/jest',
),
...compat.plugins('no-jquery', '@graphql-eslint'),
{
files: ['**/*.{js,vue}'],
plugins: {
'local-rules': localRules,
},
languageOptions: {
globals: {
__webpack_public_path__: true,
gl: false,
gon: false,
localStorage: false,
IS_EE: false,
},
},
settings: {
'import/resolver': {
webpack: {
config: './config/webpack.config.js',
},
},
},
rules: {
'import/no-commonjs': 'error',
'import/no-default-export': 'off',
'no-underscore-dangle': [
'error',
{
allow: ['__', '_links'],
},
],
'import/no-unresolved': [
'error',
{
ignore: ['^(ee|jh)_component/'],
},
],
'lines-between-class-members': 'off',
'no-jquery/no-animate-toggle': 'off',
'no-jquery/no-event-shorthand': 'off',
'no-jquery/no-serialize': 'error',
'promise/always-return': 'off',
'promise/no-callback-in-promise': 'off',
'@gitlab/no-global-event-off': 'error',
'@gitlab/vue-no-new-non-primitive-in-template': [
'error',
{
allowNames: ['class(es)?$', '^style$', '^to$', '^$', '^variables$', 'attrs?$'],
},
],
'@gitlab/vue-no-undef-apollo-properties': 'error',
'@gitlab/tailwind-no-interpolation': 'error',
'@gitlab/vue-tailwind-no-interpolation': 'error',
'no-param-reassign': [
'error',
{
props: true,
ignorePropertyModificationsFor: ['acc', 'accumulator', 'el', 'element', 'state'],
ignorePropertyModificationsForRegex: ['^draft'],
},
],
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
pathGroups: [
{
pattern: '~/**',
group: 'internal',
},
{
pattern: 'emojis/**',
group: 'internal',
},
{
pattern: '{ee_,jh_,}empty_states/**',
group: 'internal',
},
{
pattern: '{ee_,jh_,}icons/**',
group: 'internal',
},
{
pattern: '{ee_,jh_,}images/**',
group: 'internal',
},
{
pattern: 'vendor/**',
group: 'internal',
},
{
pattern: 'shared_queries/**',
group: 'internal',
},
{
pattern: '{ee_,}spec/**',
group: 'internal',
},
{
pattern: '{ee_,jh_,}jest/**',
group: 'internal',
},
{
pattern: '{ee_,jh_,any_}else_ce/**',
group: 'internal',
},
{
pattern: 'ee/**',
group: 'internal',
},
{
pattern: '{ee_,jh_,}component/**',
group: 'internal',
},
{
pattern: 'jh_else_ee/**',
group: 'internal',
},
{
pattern: 'jh/**',
group: 'internal',
},
{
pattern: '{test_,}helpers/**',
group: 'internal',
},
{
pattern: 'test_fixtures/**',
group: 'internal',
},
],
alphabetize: {
order: 'ignore',
},
},
],
'no-restricted-syntax': [
'error',
{
selector: "ImportSpecifier[imported.name='GlSkeletonLoading']",
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.',
},
{
selector: "ImportSpecifier[imported.name='GlSafeHtmlDirective']",
message: 'Use directive at ~/vue_shared/directives/safe_html.js instead.',
},
{
selector: 'Literal[value=/docs.gitlab.+\\u002Fee/]',
message:
'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector: 'TemplateElement[value.cooked=/docs.gitlab.+\\u002Fee/]',
message:
'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector: 'Literal[value=/(?=.*docs.gitlab.*)(?!.*\\u002Fee\\b.*)/]',
message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector: 'TemplateElement[value.cooked=/(?=.*docs.gitlab.*)(?!.*\\u002Fee\\b.*)/]',
message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector: 'Literal[value=/(?=.*about.gitlab.*)(?!.*\\u002Fblog\\b.*)/]',
message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector: 'TemplateElement[value.cooked=/(?=.*about.gitlab.*)(?!.*\\u002Fblog\\b.*)/]',
message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector:
'TemplateLiteral[expressions.0.name=DOCS_URL] > TemplateElement[value.cooked=/\\u002Fjh|\\u002Fee/]',
message:
'`/ee` or `/jh` path found in docs url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector:
"MemberExpression[object.type='ThisExpression'][property.name=/(\\$delete|\\$set)/]",
message:
"Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
},
],
'no-restricted-properties': [
'error',
{
object: 'window',
property: 'open',
message:
'Use `visitUrl` in `jh_else_ce/lib/utils/url_utility` to avoid cross-site leaks.',
},
{
object: 'vm',
property: '$delete',
message:
"Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
},
{
object: 'Vue',
property: 'delete',
message:
"Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
},
{
object: 'vm',
property: '$set',
message:
"Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
},
{
object: 'Vue',
property: 'set',
message:
"Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
},
],
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'mousetrap',
message: 'Import { Mousetrap } from ~/lib/mousetrap instead.',
},
{
name: 'vuex',
message:
'See our documentation on "Migrating from VueX" for tips on how to avoid adding new VueX stores.',
},
{
name: '@sentry/browser',
message: 'Use "import * as Sentry from \'~/sentry/sentry_browser_wrapper\';" instead',
},
],
patterns: [
{
group: ['react', 'react-dom/*'],
message:
'We do not allow usage of React in our codebase except for the graphql_explorer',
},
],
},
],
'unicorn/prefer-dom-node-dataset': ['error'],
'no-unsanitized/method': [
'error',
{
escape: {
methods: ['sanitize'],
},
},
],
'no-unsanitized/property': [
'error',
{
escape: {
methods: ['sanitize'],
},
},
],
'unicorn/no-array-callback-reference': 'off',
'vue/no-undef-components': [
'error',
{
ignorePatterns: ['^router-link$', '^router-view$', '^gl-emoji$'],
},
],
'local-rules/require-valid-help-page-path': 'error',
'local-rules/vue-require-valid-help-page-link-component': 'error',
},
},
{
files: ['{,ee/,jh/}spec/frontend*/**/*'],
rules: {
'@gitlab/require-i18n-strings': 'off',
'@gitlab/no-runtime-template-compiler': 'off',
'@gitlab/tailwind-no-interpolation': 'off',
'@gitlab/vue-tailwind-no-interpolation': 'off',
'require-await': 'error',
'import/no-dynamic-require': 'off',
'no-import-assign': 'off',
'no-restricted-syntax': [
'error',
{
selector:
'CallExpression[callee.object.name=/(wrapper|vm)/][callee.property.name="setData"]',
message: 'Avoid using "setData" on VTU wrapper',
},
{
selector:
"MemberExpression[object.type!='ThisExpression'][property.type='Identifier'][property.name='$nextTick']",
message:
'Using $nextTick from a component instance is discouraged. Import nextTick directly from the Vue package.',
},
{
selector: "Identifier[name='setImmediate']",
message:
'Prefer explicit waitForPromises (or equivalent), or jest.runAllTimers (or equivalent) to vague setImmediate calls.',
},
{
selector: "ImportSpecifier[imported.name='GlSkeletonLoading']",
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.',
},
{
selector:
"CallExpression[arguments.length=1][arguments.0.type='Literal'] CallExpression[callee.property.name='toBe'] CallExpression[callee.property.name='attributes'][arguments.length=1][arguments.0.value='disabled']",
message:
'Avoid asserting disabled attribute exact value, because Vue.js 2 and Vue.js 3 renders it differently. Use toBeDefined / toBeUndefined instead',
},
{
selector:
"MemberExpression[object.object.name='Vue'][object.property.name='config'][property.name='errorHandler']",
message:
'Use setErrorHandler/resetVueErrorHandler from helpers/set_vue_error_handler.js instead.',
},
{
selector: 'Literal[value=/docs.gitlab.+\\u002Fee/]',
message:
'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector: 'TemplateElement[value.cooked=/docs.gitlab.+\\u002Fee/]',
message:
'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector: 'Literal[value=/(?=.*docs.gitlab.*)(?!.*\\u002Fee\\b.*)/]',
message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector: 'TemplateElement[value.cooked=/(?=.*docs.gitlab.*)(?!.*\\u002Fee\\b.*)/]',
message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector: 'Literal[value=/(?=.*about.gitlab.*)(?!.*\\u002Fblog\\b.*)/]',
message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector: 'TemplateElement[value.cooked=/(?=.*about.gitlab.*)(?!.*\\u002Fblog\\b.*)/]',
message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector:
'TemplateLiteral[expressions.0.name=DOCS_URL] > TemplateElement[value.cooked=/\\u002Fjh|\\u002Fee/]',
message:
'`/ee` or `/jh` path found in docs url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`',
},
{
selector: 'CallExpression[callee.property.name=/(\\$delete|\\$set)/]',
message:
"Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
},
],
'no-restricted-properties': [
'error',
{
object: 'Vue',
property: 'delete',
message:
"Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
},
{
object: 'Vue',
property: 'set',
message:
"Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
},
],
'no-unsanitized/method': 'off',
'no-unsanitized/property': 'off',
'local-rules/require-valid-help-page-path': 'off',
'local-rules/vue-require-valid-help-page-link-component': 'off',
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'mousetrap',
message: 'Import { Mousetrap } from ~/lib/mousetrap instead.',
},
{
name: 'vuex',
message:
'See our documentation on "Migrating from VueX" for tips on how to avoid adding new VueX stores.',
},
{
name: '@sentry/browser',
message: 'Use "import * as Sentry from \'~/sentry/sentry_browser_wrapper\';" instead',
},
{
name: '~/locale',
importNames: ['__', 's__'],
message:
'Do not externalize strings in specs: https://docs.gitlab.com/ee/development/i18n/externalization.html#test-files-jest',
},
],
},
],
},
},
{
files: [
'config/**/*',
'scripts/**/*',
'**/*.config.js',
'**/*.config.*.js',
'**/jest_resolver.js',
'eslint.config.mjs',
],
rules: {
'@gitlab/require-i18n-strings': 'off',
'import/no-extraneous-dependencies': 'off',
'import/no-commonjs': 'off',
'import/no-nodejs-modules': 'off',
'filenames/match-regex': 'off',
'no-console': 'off',
},
},
{
files: ['**/*.stories.js'],
rules: {
'filenames/match-regex': 'off',
'@gitlab/require-i18n-strings': 'off',
},
},
{
files: ['**/*.graphql'],
languageOptions: {
ecmaVersion: 5,
sourceType: 'script',
parserOptions: {
parser: '@graphql-eslint/eslint-plugin',
operations: '{,ee/,jh/}app/**/*.graphql',
schema: './tmp/tests/graphql/gitlab_schema_apollo.graphql',
},
},
rules: {
'filenames/match-regex': 'off',
'spaced-comment': 'off',
'@graphql-eslint/no-anonymous-operations': 'error',
'@graphql-eslint/unique-operation-name': 'error',
'@graphql-eslint/require-id-when-available': 'error',
'@graphql-eslint/no-unused-variables': 'error',
'@graphql-eslint/no-unused-fragments': 'error',
'@graphql-eslint/no-duplicate-fields': 'error',
},
},
{
files: [
'app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql',
'app/assets/javascripts/projects/settings/repository/branch_rules/graphql/mutations/create_branch_rule.mutation.graphql',
'app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql',
'ee/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql',
'ee/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql',
],
rules: {
'@graphql-eslint/require-id-when-available': 'off',
},
},
{
files: ['{,spec/}tooling/**/*'],
rules: {
'no-undef': 'off',
'import/no-commonjs': 'off',
'import/no-extraneous-dependencies': 'off',
'no-restricted-syntax': 'off',
'@gitlab/require-i18n-strings': 'off',
},
},
// JIRA subscriptions config
{
files: ['app/assets/javascripts/jira_connect/subscriptions/**/*.{js,vue}'],
languageOptions: {
globals: {
AP: 'readonly',
},
},
rules: {
'@gitlab/require-i18n-strings': 'off',
'@gitlab/vue-require-i18n-strings': 'off',
},
},
// Storybook config
{
files: ['storybook/**/*.{js,vue}'],
rules: {
'@gitlab/require-i18n-strings': 'off',
'import/no-extraneous-dependencies': 'off',
'import/no-commonjs': 'off',
'import/no-nodejs-modules': 'off',
'filenames/match-regex': 'off',
'no-console': 'off',
'import/no-unresolved': 'off',
},
},
// Circular dependencies overrides
{
files: [
// https://gitlab.com/gitlab-org/gitlab/issues/37987
'ee/app/assets/javascripts/vue_shared/**/*.{js,vue}',
// https://gitlab.com/gitlab-org/gitlab/issues/28716
'{,ee/}app/assets/javascripts/filtered_search/**/*.js',
// https://gitlab.com/gitlab-org/gitlab/issues/28719
'app/assets/javascripts/image_diff/**/*.js',
],
rules: {
'import/no-cycle': 'off',
},
},
// Web IDE config
{
files: ['app/assets/javascripts/ide/**/*.{js,vue}'],
rules: {
// https://gitlab.com/gitlab-org/gitlab/issues/28717
'import/no-cycle': 'off',
// https://gitlab.com/gitlab-org/gitlab/issues/33024
'promise/no-nesting': 'off',
},
},
// Jest config
jestConfig,
// Integration tests config
{
files: ['{,ee/}spec/frontend_integration/**/*.js'],
settings: {
...jestConfig.settings,
'import/resolver': {
jest: {
jestConfigFile: 'jest.config.integration.js',
},
},
},
rules: {
...jestConfig.rules,
'no-restricted-imports': ['error', 'fs'],
},
languageOptions: {
globals: {
mockServer: false,
},
},
},
// Consumer specs config
{
files: ['{,ee/}spec/contracts/consumer/**/*.js'],
settings: {
'import/core-modules': ['@pact-foundation/pact', 'jest-pact'],
},
rules: {
'@gitlab/require-i18n-strings': 'off',
},
},
];

View File

@ -176,11 +176,17 @@ module API
end end
def find_or_create_package def find_or_create_package
package || ::Packages::Conan::CreatePackageService.new( return package if package
service_response = ::Packages::Conan::CreatePackageService.new(
project, project,
current_user, current_user,
params.merge(build: current_authenticated_job) params.merge(build: current_authenticated_job)
).execute ).execute
bad_request!(service_response.message) if service_response.error?
service_response[:package]
end end
def track_push_package_event def track_push_package_event

View File

@ -8,7 +8,9 @@ module Gitlab
GITLAB_HOSTED_RUNNER = 'gitlab-hosted' GITLAB_HOSTED_RUNNER = 'gitlab-hosted'
SELF_HOSTED_RUNNER = 'self-hosted' SELF_HOSTED_RUNNER = 'self-hosted'
def self.for_build(build, aud:, sub_components: [:project_path, :ref_type, :ref], target_audience: nil) def self.for_build(
build, aud:, sub_components: [:project_path, :ref_type,
:ref], target_audience: nil)
new(build, ttl: build.metadata_timeout, aud: aud, sub_components: sub_components, new(build, ttl: build.metadata_timeout, aud: aud, sub_components: sub_components,
target_audience: target_audience).encoded target_audience: target_audience).encoded
end end

View File

@ -8,6 +8,7 @@ module Gitlab
# incoming_email and service_desk_email. # incoming_email and service_desk_email.
module CustomEmail module CustomEmail
REPLY_ADDRESS_KEY_REGEXP = /\+([0-9a-f]{32})@/ REPLY_ADDRESS_KEY_REGEXP = /\+([0-9a-f]{32})@/
EMAIL_REGEXP_WITH_ANCHORS = /\A(?>[a-zA-Z0-9]+|[\-._]+){1,255}@[\w\-.]{1,255}\.{1}[a-zA-Z]{2,63}\z/
class << self class << self
def reply_address(issue, reply_key) def reply_address(issue, reply_key)
@ -53,7 +54,7 @@ module Gitlab
def find_service_desk_setting_from_reply_address(email, key) def find_service_desk_setting_from_reply_address(email, key)
potential_custom_email = email.sub("+#{key}", '') potential_custom_email = email.sub("+#{key}", '')
return unless Gitlab::Utils::Email::EMAIL_REGEXP_WITH_ANCHORS.match?(potential_custom_email) return unless EMAIL_REGEXP_WITH_ANCHORS.match?(potential_custom_email)
ServiceDeskSetting.find_by_custom_email(potential_custom_email) ServiceDeskSetting.find_by_custom_email(potential_custom_email)
end end

View File

@ -68,7 +68,7 @@ module Gitlab
def events_worker_args(event_class, events) def events_worker_args(event_class, events)
events events
.map { |event| event.data.deep_stringify_keys } .map { |event| event.data.deep_stringify_keys.to_h }
.each_slice(group_size) .each_slice(group_size)
.map { |events_data_group| [event_class.name, events_data_group] } .map { |events_data_group| [event_class.name, events_data_group] }
end end

View File

@ -18,14 +18,14 @@ module Gitlab
Gitlab::Tracking::EventValidator.new(event_name, additional_properties, kwargs).validate! Gitlab::Tracking::EventValidator.new(event_name, additional_properties, kwargs).validate!
extra = custom_additional_properties(additional_properties) extra = custom_additional_properties(additional_properties)
additional_properties = additional_properties.slice(*base_additional_properties.keys) base_additional_properties = additional_properties.slice(*base_additional_properties_keys)
project = kwargs[:project] project = kwargs[:project]
kwargs[:namespace] ||= project.namespace if project kwargs[:namespace] ||= project.namespace if project
update_redis_values(event_name, additional_properties, kwargs) update_redis_values(event_name, additional_properties, kwargs)
trigger_snowplow_event(event_name, category, additional_properties, extra, kwargs) if send_snowplow_event trigger_snowplow_event(event_name, category, base_additional_properties, extra, kwargs) if send_snowplow_event
send_application_instrumentation_event(event_name, additional_properties, kwargs) if send_snowplow_event send_application_instrumentation_event(event_name, base_additional_properties, kwargs) if send_snowplow_event
if Feature.enabled?(:early_access_program, kwargs[:user], type: :wip) if Feature.enabled?(:early_access_program, kwargs[:user], type: :wip)
create_early_access_program_event(event_name, category, additional_properties[:label], kwargs) create_early_access_program_event(event_name, category, additional_properties[:label], kwargs)
@ -65,7 +65,7 @@ module Gitlab
end end
def custom_additional_properties(additional_properties) def custom_additional_properties(additional_properties)
additional_properties.except(*base_additional_properties.keys) additional_properties.except(*base_additional_properties_keys)
end end
def update_total_counter(event_selection_rule) def update_total_counter(event_selection_rule)
@ -160,8 +160,8 @@ module Gitlab
end end
strong_memoize_attr :gitlab_sdk_client strong_memoize_attr :gitlab_sdk_client
def base_additional_properties def base_additional_properties_keys
Gitlab::Tracking::EventValidator::BASE_ADDITIONAL_PROPERTIES Gitlab::Tracking::EventValidator::BASE_ADDITIONAL_PROPERTIES.keys
end end
end end
end end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
require 'gitlab/cells/topology_service'
module Gitlab
module TopologyServiceClient
class BaseService
def initialize
raise NotImplementedError unless enabled?
end
private
def client
@client ||= service_class.new(
topology_service_address,
service_credentials
)
end
def cell_name
@cell_name ||= Gitlab.config.cell.name
end
def service_credentials
# mTls will be implemented later in Phase 5: https://gitlab.com/groups/gitlab-org/-/epics/14281
:this_channel_is_insecure
end
def topology_service_address
Gitlab.config.topology_service.address
end
def enabled?
Gitlab.config.topology_service.respond_to?(:enabled) && Gitlab.config.topology_service.enabled &&
Gitlab.config.cell.respond_to?(:name) && cell_name.present?
end
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'gitlab/cells/topology_service'
module Gitlab
module TopologyServiceClient
class CellService < BaseService
def get_cell_info
response = client.get_cell(Gitlab::Cells::TopologyService::GetCellRequest.new(cell_name: cell_name))
response.cell_info
rescue GRPC::NotFound
Gitlab::AppLogger.error(message: "Cell '#{cell_name}' not found on Topology Service")
nil
end
private
def service_class
Gitlab::Cells::TopologyService::CellService::Stub
end
end
end
end

View File

@ -11,6 +11,7 @@ module Gitlab
property: [String], property: [String],
value: [Integer, Float] value: [Integer, Float]
}.freeze }.freeze
CUSTOM_PROPERTIES_CLASSES = [String, Integer, Float].freeze
def initialize(event_name, additional_properties, kwargs) def initialize(event_name, additional_properties, kwargs)
@event_name = event_name @event_name = event_name
@ -56,10 +57,13 @@ module Gitlab
# skip base properties validation. To be done in a separate MR as we have some non-compliant definitions # skip base properties validation. To be done in a separate MR as we have some non-compliant definitions
custom_properties = additional_properties.except(*BASE_ADDITIONAL_PROPERTIES.keys) custom_properties = additional_properties.except(*BASE_ADDITIONAL_PROPERTIES.keys)
event_definition_attributes = Gitlab::Tracking::EventDefinition.find(event_name).to_h event_definition_attributes = Gitlab::Tracking::EventDefinition.find(event_name).to_h
allowed_types = CUSTOM_PROPERTIES_CLASSES
custom_properties.each_key do |key| custom_properties.each_key do |key|
unless event_definition_attributes[:additional_properties].include?(key) unless event_definition_attributes[:additional_properties].include?(key)
raise InvalidPropertyError, "Unknown additional property: #{key}" raise InvalidPropertyError, "Unknown additional property: #{key}"
end end
validate_property!(custom_properties, key, *allowed_types)
end end
end end
end end

View File

@ -42,6 +42,7 @@
- i_ci_secrets_management_gcp_secret_manager_build_created - i_ci_secrets_management_gcp_secret_manager_build_created
- i_ci_secrets_management_id_tokens_build_created - i_ci_secrets_management_id_tokens_build_created
- i_ci_secrets_management_vault_build_created - i_ci_secrets_management_vault_build_created
- i_ci_secrets_management_gitlab_secrets_manager_build_created
- i_code_review_click_diff_view_setting - i_code_review_click_diff_view_setting
- i_code_review_click_file_browser_setting - i_code_review_click_file_browser_setting
- i_code_review_click_single_file_mode_setting - i_code_review_click_single_file_mode_setting

View File

@ -5,12 +5,8 @@ module Gitlab
module Email module Email
extend self extend self
# Don't use Devise.email_regexp or URI::MailTo::EMAIL_REGEXP to be a bit more restrictive EMAIL_REGEXP = %r{(?>[a-zA-Z0-9]+|[\-._!#$%&'*+\/=?^{|}~]+){1,255}@[\w\-.]{1,253}\.{1}[a-zA-Z]{2,63}}
# on the format of an email. Especially for custom email addresses which cannot contain a `+`
# in app/models/service_desk_setting.rb
EMAIL_REGEXP = /[\w\-._]+@[\w\-.]+\.{1}[a-zA-Z]{2,}/
EMAIL_REGEXP_WITH_CAPTURING_GROUP = /(#{EMAIL_REGEXP})/ EMAIL_REGEXP_WITH_CAPTURING_GROUP = /(#{EMAIL_REGEXP})/
EMAIL_REGEXP_WITH_ANCHORS = /\A#{EMAIL_REGEXP.source}\z/
# Replaces most visible characters with * to obfuscate an email address # Replaces most visible characters with * to obfuscate an email address
# deform adds a fix number of * to ensure the address cannot be guessed. Also obfuscates TLD with ** # deform adds a fix number of * to ensure the address cannot be guessed. Also obfuscates TLD with **

View File

@ -38691,9 +38691,6 @@ msgstr ""
msgid "Package type must be Terraform Module" msgid "Package type must be Terraform Module"
msgstr "" msgstr ""
msgid "PackageRegistry|%{count} packages were not published to the registry. Remove these packages and try again."
msgstr ""
msgid "PackageRegistry|%{linkStart}Wildcards%{linkEnd} such as `my-package-*` are supported." msgid "PackageRegistry|%{linkStart}Wildcards%{linkEnd} such as `my-package-*` are supported."
msgstr "" msgstr ""
@ -38907,6 +38904,9 @@ msgstr ""
msgid "PackageRegistry|Failed to load version data" msgid "PackageRegistry|Failed to load version data"
msgstr "" msgstr ""
msgid "PackageRegistry|Failed to publish %{count} packages. Delete these packages and try again."
msgstr ""
msgid "PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}" msgid "PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}"
msgstr "" msgstr ""
@ -39218,7 +39218,7 @@ msgstr ""
msgid "PackageRegistry|There was an error publishing %{count} packages" msgid "PackageRegistry|There was an error publishing %{count} packages"
msgstr "" msgstr ""
msgid "PackageRegistry|There was an error publishing a %{packageName} package" msgid "PackageRegistry|There was an error publishing %{packageName}"
msgstr "" msgstr ""
msgid "PackageRegistry|This NuGet package has no dependencies." msgid "PackageRegistry|This NuGet package has no dependencies."

View File

@ -7,7 +7,7 @@
"dev-server": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" node scripts/frontend/webpack_dev_server.js", "dev-server": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" node scripts/frontend/webpack_dev_server.js",
"file-coverage": "scripts/frontend/file_test_coverage.js", "file-coverage": "scripts/frontend/file_test_coverage.js",
"lint-docs": "scripts/lint-doc.sh", "lint-docs": "scripts/lint-doc.sh",
"internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue,.graphql", "internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives",
"internal:stylelint": "stylelint -q --rd '{ee/,}app/assets/{stylesheets/**/*.{css,scss},builds/tailwind.css}'", "internal:stylelint": "stylelint -q --rd '{ee/,}app/assets/{stylesheets/**/*.{css,scss},builds/tailwind.css}'",
"preinternal:stylelint": "yarn run tailwindcss:build", "preinternal:stylelint": "yarn run tailwindcss:build",
"prejest": "yarn check-dependencies", "prejest": "yarn check-dependencies",
@ -76,7 +76,7 @@
"@gitlab/fonts": "^1.3.0", "@gitlab/fonts": "^1.3.0",
"@gitlab/query-language": "^0.0.5-a-20241017", "@gitlab/query-language": "^0.0.5-a-20241017",
"@gitlab/svgs": "3.119.0", "@gitlab/svgs": "3.119.0",
"@gitlab/ui": "97.3.0", "@gitlab/ui": "98.4.0",
"@gitlab/web-ide": "^0.0.1-dev-20240909013227", "@gitlab/web-ide": "^0.0.1-dev-20240909013227",
"@mattiasbuelens/web-streams-adapter": "^0.1.0", "@mattiasbuelens/web-streams-adapter": "^0.1.0",
"@rails/actioncable": "7.0.8-4", "@rails/actioncable": "7.0.8-4",
@ -254,6 +254,8 @@
"yaml": "^2.0.0-10" "yaml": "^2.0.0-10"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.13.0",
"@gitlab/eslint-plugin": "20.4.1", "@gitlab/eslint-plugin": "20.4.1",
"@gitlab/stylelint-config": "6.2.2", "@gitlab/stylelint-config": "6.2.2",
"@graphql-eslint/eslint-plugin": "3.20.1", "@graphql-eslint/eslint-plugin": "3.20.1",

View File

@ -189,6 +189,7 @@ module Gitlab
.deep_merge(license_values) .deep_merge(license_values)
.deep_merge(env_values) .deep_merge(env_values)
.deep_merge(configuration.values) .deep_merge(configuration.values)
.deep_merge(ResourcePresets.resource_values(ci ? ResourcePresets::HIGH : ResourcePresets::DEFAULT))
.deep_stringify_keys .deep_stringify_keys
.to_yaml .to_yaml

View File

@ -0,0 +1,153 @@
# frozen_string_literal: true
module Gitlab
module Cng
module Deployment
# Kubernetes resource request/limit presets optimised for different usecases
#
class ResourcePresets
DEFAULT = "default"
HIGH = "high"
class << self
# Kubernetes resources values for given preset
#
# @param [String] preset_name
# @return [Hash]
def resource_values(preset_name)
presets.fetch(preset_name)
end
private
# Different resources presets and replicas count
#
# Prefer vertical scaling over hpa for test stability
# Waiting for new pods to scale will lead to test flakiness and makes log reading harder
#
# @return [Hash]
def presets
@presets ||= {
# Default preset for local deployments
DEFAULT => {
gitlab: {
webservice: {
workerProcesses: 2,
minReplicas: 1,
resources: resources("1500m", "3Gi")
},
sidekiq: {
concurrency: 20,
minReplicas: 1,
resources: resources("900m", "1.6Gi"),
hpa: {
cpu: { targetAverageValue: "800m" }
}
},
kas: {
minReplicas: 1,
resources: resources("10m", "45Mi")
},
gitlab_shell: {
minReplicas: 1,
resources: resources("80m", "16Mi")
},
gitaly: {
resources: resources("300m", "300Mi")
}
},
registry: {
resources: resources("40m", "20Mi"),
hpa: {
minReplicas: 1,
**cpu_utilization
}
},
minio: {
resources: resources("9m", "128Mi")
}
},
# This preset is optimised for running e2e tests in parallel
HIGH => {
gitlab: {
webservice: {
workerProcesses: 4,
minReplicas: 1,
resources: resources(3, "4.5Gi"),
hpa: cpu_utilization
},
sidekiq: {
concurrency: 30,
minReplicas: 1,
resources: resources("1200m", "2Gi"),
hpa: cpu_utilization
},
kas: {
minReplicas: 1,
resources: resources("40m", "64Mi"),
hpa: cpu_utilization
},
gitlab_shell: {
minReplicas: 1,
resources: resources("24m", "32Mi"),
hpa: cpu_utilization
},
gitaly: {
resources: resources("450m", "450Mi")
}
},
registry: {
resources: resources("50m", "32Mi"),
hpa: {
minReplicas: 1,
**cpu_utilization
}
},
minio: {
resources: resources("15m", "256Mi")
}
}
}
end
# Kubernetes resources configuration
#
# Set limits equal to requests by default for simplicity
#
# @param [<String, Integer>] cpu_r
# @param [String] memory_r
# @param [<String, Integer>] cpu_l
# @param [String] memory_l
# @return [Hash]
def resources(cpu_r, memory_r, cpu_l = nil, memory_l = nil)
cpu_l ||= cpu_r
memory_l ||= memory_r
{
requests: {
cpu: cpu_r,
memory: memory_r
},
limits: {
cpu: cpu_l,
memory: memory_l
}
}
end
# Common hpa cpu utilization config
#
# @return [Hash]
def cpu_utilization
@cpu_utilization ||= {
cpu: {
targetType: "Utilization",
targetAverageUtilization: 90
}
}
end
end
end
end
end
end

View File

@ -48,6 +48,7 @@ module Gitlab
create_cluster create_cluster
update_server_url update_server_url
install_metrics_server
log("Cluster '#{name}' created", :success) log("Cluster '#{name}' created", :success)
rescue Helpers::Shell::CommandFailure rescue Helpers::Shell::CommandFailure
# Exit cleanly without stacktrace if shell command fails # Exit cleanly without stacktrace if shell command fails

View File

@ -41,6 +41,12 @@ RSpec.describe Gitlab::Cng::Deployment::Installation, :aggregate_failures do
) )
end end
let(:resources_values) do
Gitlab::Cng::Deployment::ResourcePresets.resource_values(
ci ? Gitlab::Cng::Deployment::ResourcePresets::HIGH : Gitlab::Cng::Deployment::ResourcePresets::DEFAULT
)
end
let(:expected_values_yml) do let(:expected_values_yml) do
{ {
global: { global: {
@ -55,7 +61,7 @@ RSpec.describe Gitlab::Cng::Deployment::Installation, :aggregate_failures do
license: { secret: "gitlab-license" } license: { secret: "gitlab-license" }
}, },
**config_values **config_values
}.deep_stringify_keys.to_yaml }.deep_merge(resources_values).deep_stringify_keys.to_yaml
end end
before do before do

View File

@ -0,0 +1,154 @@
# frozen_string_literal: true
RSpec.describe Gitlab::Cng::Deployment::ResourcePresets do
it "returns default resources values preset" do
expect(described_class.resource_values(described_class::DEFAULT)).to eq({
gitlab: {
webservice: {
workerProcesses: 2,
minReplicas: 1,
resources: {
requests: { cpu: "1500m", memory: "3Gi" },
limits: { cpu: "1500m", memory: "3Gi" }
}
},
sidekiq: {
concurrency: 20,
minReplicas: 1,
resources: {
requests: { cpu: "900m", memory: "1.6Gi" },
limits: { cpu: "900m", memory: "1.6Gi" }
},
hpa: {
cpu: { targetAverageValue: "800m" }
}
},
kas: {
minReplicas: 1,
resources: {
requests: { cpu: "10m", memory: "45Mi" },
limits: { cpu: "10m", memory: "45Mi" }
}
},
gitlab_shell: {
minReplicas: 1,
resources: {
requests: { cpu: "80m", memory: "16Mi" },
limits: { cpu: "80m", memory: "16Mi" }
}
},
gitaly: {
resources: {
requests: { cpu: "300m", memory: "300Mi" },
limits: { cpu: "300m", memory: "300Mi" }
}
}
},
registry: {
resources: {
requests: { cpu: "40m", memory: "20Mi" },
limits: { cpu: "40m", memory: "20Mi" }
},
hpa: {
minReplicas: 1,
cpu: {
targetType: "Utilization",
targetAverageUtilization: 90
}
}
},
minio: {
resources: {
requests: { cpu: "9m", memory: "128Mi" },
limits: { cpu: "9m", memory: "128Mi" }
}
}
})
end
it "returns high resources values preset" do
expect(described_class.resource_values(described_class::HIGH)).to eq({
gitlab: {
webservice: {
workerProcesses: 4,
minReplicas: 1,
resources: {
requests: { cpu: 3, memory: "4.5Gi" },
limits: { cpu: 3, memory: "4.5Gi" }
},
hpa: {
cpu: {
targetType: "Utilization",
targetAverageUtilization: 90
}
}
},
sidekiq: {
concurrency: 30,
minReplicas: 1,
resources: {
requests: { cpu: "1200m", memory: "2Gi" },
limits: { cpu: "1200m", memory: "2Gi" }
},
hpa: {
cpu: {
targetType: "Utilization",
targetAverageUtilization: 90
}
}
},
kas: {
minReplicas: 1,
resources: {
requests: { cpu: "40m", memory: "64Mi" },
limits: { cpu: "40m", memory: "64Mi" }
},
hpa: {
cpu: {
targetType: "Utilization",
targetAverageUtilization: 90
}
}
},
gitlab_shell: {
minReplicas: 1,
resources: {
requests: { cpu: "24m", memory: "32Mi" },
limits: { cpu: "24m", memory: "32Mi" }
},
hpa: {
cpu: {
targetType: "Utilization",
targetAverageUtilization: 90
}
}
},
gitaly: {
resources: {
requests: { cpu: "450m", memory: "450Mi" },
limits: { cpu: "450m", memory: "450Mi" }
}
}
},
registry: {
resources: {
requests: { cpu: "50m", memory: "32Mi" },
limits: { cpu: "50m", memory: "32Mi" }
},
hpa: {
minReplicas: 1,
cpu: {
targetType: "Utilization",
targetAverageUtilization: 90
}
}
},
minio: {
resources: {
requests: { cpu: "15m", memory: "256Mi" },
limits: { cpu: "15m", memory: "256Mi" }
}
}
})
end
end

View File

@ -84,11 +84,11 @@ RSpec.describe Gitlab::Cng::Kind::Cluster do
it "creates cluster with ci specific configuration", :aggregate_failures do it "creates cluster with ci specific configuration", :aggregate_failures do
expect { cluster.create }.to output(/Cluster '#{name}' created/).to_stdout expect { cluster.create }.to output(/Cluster '#{name}' created/).to_stdout
expect(helm).not_to have_received(:add_helm_chart).with( expect(helm).to have_received(:add_helm_chart).with(
"metrics-server", "metrics-server",
"https://kubernetes-sigs.github.io/metrics-server/" "https://kubernetes-sigs.github.io/metrics-server/"
) )
expect(helm).not_to have_received(:upgrade).with( expect(helm).to have_received(:upgrade).with(
"metrics-server", "metrics-server",
"metrics-server/metrics-server", "metrics-server/metrics-server",
namespace: "kube-system", namespace: "kube-system",

View File

@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
import Runtime from 'jest-runtime'; import Runtime from 'jest-runtime';
import { readConfig } from 'jest-config'; import { readConfig } from 'jest-config';
import createJestConfig from '../../jest.config.base.js'; import createJestConfig from '../../jest.config.base';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../'); const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../');

View File

@ -31,7 +31,7 @@ async function getCurrentTopLevelPatterns() {
return serializeAliasedDependencyPatterns(dependencies) return serializeAliasedDependencyPatterns(dependencies)
.concat(serializeAliasedDependencyPatterns(devDependencies)) .concat(serializeAliasedDependencyPatterns(devDependencies))
.filter(isAliasedDependency); .filter(dep => isAliasedDependency(dep));
} catch { } catch {
return []; return [];
} }

View File

@ -439,7 +439,6 @@ spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
spec/frontend/vue_popovers_spec.js spec/frontend/vue_popovers_spec.js
spec/frontend/vue_shared/components/alert_details_table_spec.js spec/frontend/vue_shared/components/alert_details_table_spec.js
spec/frontend/vue_shared/components/badges/beta_badge_spec.js spec/frontend/vue_shared/components/badges/beta_badge_spec.js
spec/frontend/vue_shared/components/chronic_duration_input_spec.js
spec/frontend/vue_shared/components/color_picker/color_picker_spec.js spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
spec/frontend/vue_shared/components/confirm_modal_spec.js spec/frontend/vue_shared/components/confirm_modal_spec.js
@ -465,7 +464,6 @@ spec/frontend/vue_shared/components/registry/registry_search_spec.js
spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
spec/frontend/vue_shared/components/segmented_control_button_group_spec.js spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
spec/frontend/vue_shared/components/slot_switch_spec.js
spec/frontend/vue_shared/components/smart_virtual_list_spec.js spec/frontend/vue_shared/components/smart_virtual_list_spec.js
spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js

View File

@ -59,11 +59,14 @@ export function webpackTailwindCompilerPlugin({ shouldWatch = true }) {
} }
if (wasScriptCalledDirectly()) { if (wasScriptCalledDirectly()) {
build().then(() => { build()
console.log('Tailwind utils built successfully') // eslint-disable-next-line promise/always-return
}).catch(e => { .then(() => {
console.warn('Building Tailwind utils produced an error') console.log('Tailwind utils built successfully');
console.error(e); })
process.exitCode = 1; .catch((e) => {
}); console.warn('Building Tailwind utils produced an error');
console.error(e);
process.exitCode = 1;
});
} }

View File

@ -418,8 +418,15 @@ module InternalEventsCli
q.convert ->(input) { input.split(',').map(&:to_i).uniq } q.convert ->(input) { input.split(',').map(&:to_i).uniq }
q.validate %r{^(\d|\s|,)*$} q.validate %r{^(\d|\s|,)*$}
q.messages[:valid?] = "Inputs for #{formatted_prop} must be numeric" q.messages[:valid?] = "Inputs for #{formatted_prop} must be numeric"
else elsif property == 'property' || property == 'label'
q.convert ->(input) { input.split(',').map(&:strip).uniq } q.convert ->(input) { input.split(',').map(&:strip).uniq }
else
q.convert ->(input) do
input.split(',').map do |value|
val = value.strip
cast_if_numeric(val)
end.uniq
end
end end
end end
@ -431,6 +438,13 @@ module InternalEventsCli
inputs.map { |input| { property => input } }.uniq inputs.map { |input| { property => input } }.uniq
end end
def cast_if_numeric(text)
float = Float(text)
float % 1 == 0 ? float.to_i : float
rescue ArgumentError
text
end
# Helper for #prompt_for_event_filters # Helper for #prompt_for_event_filters
# #
# Gets all the permutations of the provided property values. # Gets all the permutations of the provided property values.

View File

@ -71,7 +71,6 @@ await Promise.all(
if (errors > 0) { if (errors > 0) {
console.log(`Total errors: ${errors}`); console.log(`Total errors: ${errors}`);
// eslint-disable-next-line no-restricted-syntax
console.log(`To fix these errors, see https://docs.gitlab.com/ee/development/documentation/testing/#mermaid-chart-linting.`); console.log(`To fix these errors, see https://docs.gitlab.com/ee/development/documentation/testing/#mermaid-chart-linting.`);
process.exit(1); process.exit(1);
} }

View File

@ -1,7 +0,0 @@
---
extends:
- 'plugin:@gitlab/jest'
settings:
import/core-modules:
- '@pact-foundation/pact'
- jest-pact

View File

@ -219,7 +219,7 @@ RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', feature_category: :tooling do
Dir.glob('.github/*') + Dir.glob('.github/*') +
Dir.glob('.gitlab/{issue,merge_request}_templates/**/*') + Dir.glob('.gitlab/{issue,merge_request}_templates/**/*') +
Dir.glob('.gitlab/*.toml') + Dir.glob('.gitlab/*.toml') +
Dir.glob('{,**/}.{DS_Store,eslintrc.yml,gitignore,gitkeep,keep}', File::FNM_DOTMATCH) + Dir.glob('{,**/}.{DS_Store,gitignore,gitkeep,keep}', File::FNM_DOTMATCH) +
Dir.glob('{,vendor/}gems/*/.*') + Dir.glob('{,vendor/}gems/*/.*') +
Dir.glob('{.git,.lefthook,.ruby-lsp}/**/*') + Dir.glob('{.git,.lefthook,.ruby-lsp}/**/*') +
Dir.glob('{file_hooks,log}/**/*') + Dir.glob('{file_hooks,log}/**/*') +

View File

@ -0,0 +1,29 @@
---
description: Engineer uses Internal Event CLI to define a new event
internal_events: true
action: internal_events_cli_used
identifiers:
- project
- namespace
- user
additional_properties:
label:
description: TODO
custom_key1:
description: The extra custom property name 1
custom_key2:
description: The extra custom property name 2
custom_key3:
description: The extra custom property name 3
custom_key4:
description: The extra custom property name 3
product_group: analytics_instrumentation
milestone: '16.6'
introduced_by_url: TODO
distributions:
- ce
- ee
tiers:
- free
- premium
- ultimate

View File

@ -0,0 +1,38 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_under_60s_monthly
description: Monthly count of unique users who tried and failed to define an internal event using the CLI
product_group: analytics_instrumentation
performance_indicator_type: []
value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
tiers:
- free
- premium
- ultimate
events:
- name: internal_events_cli_used
unique: user.id
filter:
label: failure
custom_key1: metrics
custom_key3: 30
custom_key4: 12.4
- name: internal_events_cli_used
unique: user.id
filter:
label: failure
custom_key1: metrics
custom_key3: 13
custom_key4: 12.4

View File

@ -0,0 +1,38 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_under_60s_weekly
description: Weekly count of unique users who tried and failed to define an internal event using the CLI
product_group: analytics_instrumentation
performance_indicator_type: []
value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 7d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
tiers:
- free
- premium
- ultimate
events:
- name: internal_events_cli_used
unique: user.id
filter:
label: failure
custom_key1: metrics
custom_key3: 30
custom_key4: 12.4
- name: internal_events_cli_used
unique: user.id
filter:
label: failure
custom_key1: metrics
custom_key3: 13
custom_key4: 12.4

View File

@ -366,6 +366,38 @@
- path: config/metrics/counts_7d/count_distinct_user_id_from_failed_usage_attempts_under_60s_weekly.yml - path: config/metrics/counts_7d/count_distinct_user_id_from_failed_usage_attempts_under_60s_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_all_additional_props.yml content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_all_additional_props.yml
- description: Create a weekly/monthly metric for a single event with custom additional properties filters
inputs:
files:
- path: config/events/internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/events/event_with_multiple_custom_properties.yml
keystrokes:
- "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time
- "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
- "internal_events_cli_used" # Filters to this event
- "\n" # Select: config/events/internal_events_cli_used.yml
- "\e[B\e[B\e[B" # Arrow down to: Weekly count of unique users where label/value is...
- "\n" # Select: Weekly count of unique users where label/value is...
- "failure\n" # Input value for "label" filter
- "metrics\n" # Input value for "custom_key1" filter
- "\n" # Skip "custom_key2" filter
- "30,13\n" # Input value for "custom_key3" filter
- "12.4\n" # Input value for "custom_key4" filter
- "who tried and failed to define an internal event using the CLI\n" # Input description
- "failed_usage_attempts_under_60s\n" # Input metric key path
- "\n" # Submit weekly description for monthly
- "\n" # Submit weekly name for monthly
- "1\n" # Enum-select: Copy & continue
- "y\n" # Create file
- "y\n" # Create file
- "5\n" # Exit
outputs:
files:
- path: config/metrics/counts_28d/count_distinct_user_id_from_failed_usage_attempts_under_60s_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_custom_key_filter.yml
- path: config/metrics/counts_7d/count_distinct_user_id_from_failed_usage_attempts_under_60s_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_custom_key_filter.yml
- description: Create a weekly/monthly metric for multiple events with and without additional properties - description: Create a weekly/monthly metric for multiple events with and without additional properties
inputs: inputs:
files: files:

View File

@ -1,29 +0,0 @@
---
extends:
- 'plugin:@gitlab/jest'
settings:
# We have to teach eslint-plugin-import what node modules we use
# otherwise there is an error when it tries to resolve them
import/core-modules:
- events
- fs
- path
import/resolver:
jest:
jestConfigFile: 'jest.config.js'
rules:
'@gitlab/vtu-no-explicit-wrapper-destroy': error
jest/expect-expect:
- off
- assertFunctionNames:
- 'expect*'
- 'assert*'
- 'testAction'
"@gitlab/no-global-event-off":
- off
import/no-unresolved:
- error
# The test fixtures and graphql schema are dynamically generated in CI
# during the `frontend-fixtures` and `graphql-schema-dump` jobs.
# They may not be present during linting.
- ignore: ['^test_fixtures\/', 'tmp/tests/graphql/gitlab_schema.graphql']

View File

@ -1,12 +1,14 @@
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import ChronicDurationInput from '~/vue_shared/components/chronic_duration_input.vue'; import { GlFormInput } from '@gitlab/ui';
import ChronicDurationInput from '~/admin/application_settings/runner_token_expiration/components/chronic_duration_input.vue';
const MOCK_VALUE = 2 * 3600 + 20 * 60; const MOCK_VALUE = 2 * 3600 + 20 * 60;
describe('vue_shared/components/chronic_duration_input', () => { describe('admin/application_settings/runner_token_expiration/components/chronic_duration_input', () => {
let wrapper; let wrapper;
let textElement; let textElement;
let textFormInput;
let hiddenElement; let hiddenElement;
afterEach(() => { afterEach(() => {
@ -15,8 +17,10 @@ describe('vue_shared/components/chronic_duration_input', () => {
}); });
const findComponents = () => { const findComponents = () => {
textElement = wrapper.find('input[type=text]').element; textElement = wrapper.findComponent(GlFormInput).element;
hiddenElement = wrapper.find('input[type=hidden]').element; hiddenElement = wrapper.find('input[type=hidden]').element;
textFormInput = wrapper.findComponent(GlFormInput);
}; };
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
@ -44,8 +48,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
const createAndDispatch = async (initialValue, humanReadableInput) => { const createAndDispatch = async (initialValue, humanReadableInput) => {
createComponent({ value: initialValue }); createComponent({ value: initialValue });
await nextTick(); await nextTick();
textElement.value = humanReadableInput; textFormInput.vm.$emit('input', humanReadableInput);
textElement.dispatchEvent(new Event('input'));
}; };
describe('when starting with no value and receiving human-readable input', () => { describe('when starting with no value and receiving human-readable input', () => {
@ -111,8 +114,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
}); });
it('emits valid with user input', async () => { it('emits valid with user input', async () => {
textElement.value = '1m10s'; textFormInput.vm.$emit('input', '1m10s');
textElement.dispatchEvent(new Event('input'));
await nextTick(); await nextTick();
expect(wrapper.emitted('valid')).toEqual([ expect(wrapper.emitted('valid')).toEqual([
@ -126,8 +128,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
expect(hiddenElement.validity.customError).toBe(false); expect(hiddenElement.validity.customError).toBe(false);
expect(hiddenElement.validationMessage).toBe(''); expect(hiddenElement.validationMessage).toBe('');
textElement.value = ''; textFormInput.vm.$emit('input', '');
textElement.dispatchEvent(new Event('input'));
await nextTick(); await nextTick();
expect(wrapper.emitted('valid')).toEqual([ expect(wrapper.emitted('valid')).toEqual([
@ -144,8 +145,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
}); });
it('emits invalid with user input', async () => { it('emits invalid with user input', async () => {
textElement.value = 'gobbledygook'; textFormInput.vm.$emit('input', 'gobbledygook');
textElement.dispatchEvent(new Event('input'));
await nextTick(); await nextTick();
expect(wrapper.emitted('valid')).toEqual([ expect(wrapper.emitted('valid')).toEqual([
@ -203,8 +203,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
}); });
it('emits valid when input is integer', async () => { it('emits valid when input is integer', async () => {
textElement.value = '2hr20min'; textFormInput.vm.$emit('input', '2hr20min');
textElement.dispatchEvent(new Event('input'));
await nextTick(); await nextTick();
expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]); expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]);
@ -221,8 +220,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
}); });
it('emits valid when input is decimal', async () => { it('emits valid when input is decimal', async () => {
textElement.value = '1.5s'; textFormInput.vm.$emit('input', '1.5s');
textElement.dispatchEvent(new Event('input'));
await nextTick(); await nextTick();
expect(wrapper.emitted('change')).toEqual([[1.5]]); expect(wrapper.emitted('change')).toEqual([[1.5]]);
@ -245,8 +243,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
}); });
it('emits valid when input is integer', async () => { it('emits valid when input is integer', async () => {
textElement.value = '2hr20min'; textFormInput.vm.$emit('input', '2hr20min');
textElement.dispatchEvent(new Event('input'));
await nextTick(); await nextTick();
expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]); expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]);
@ -263,8 +260,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
}); });
it('emits invalid when input is decimal', async () => { it('emits invalid when input is decimal', async () => {
textElement.value = '1.5s'; textFormInput.vm.$emit('input', '1.5s');
textElement.dispatchEvent(new Event('input'));
await nextTick(); await nextTick();
expect(wrapper.emitted('change')).toBeUndefined(); expect(wrapper.emitted('change')).toBeUndefined();
@ -310,8 +306,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
}); });
it('passes updated prop via v-model', async () => { it('passes updated prop via v-model', async () => {
textElement.value = '2hr20min'; textFormInput.vm.$emit('input', '2hr20min');
textElement.dispatchEvent(new Event('input'));
await nextTick(); await nextTick();
expect(textElement.value).toBe('2hr20min'); expect(textElement.value).toBe('2hr20min');
@ -321,8 +316,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
describe('change', () => { describe('change', () => {
it('passes user input to parent via v-model', async () => { it('passes user input to parent via v-model', async () => {
textElement.value = '2hr20min'; textFormInput.vm.$emit('input', '2hr20min');
textElement.dispatchEvent(new Event('input'));
await nextTick(); await nextTick();
expect(wrapper.findComponent(ChronicDurationInput).props('value')).toBe(MOCK_VALUE); expect(wrapper.findComponent(ChronicDurationInput).props('value')).toBe(MOCK_VALUE);
@ -369,8 +363,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
}); });
it('creates form data with user-specified value', async () => { it('creates form data with user-specified value', async () => {
textElement.value = '1m10s'; textFormInput.vm.$emit('input', '1m10s');
textElement.dispatchEvent(new Event('input'));
await nextTick(); await nextTick();
const formData = new FormData(wrapper.find('[data-testid=myForm]').element); const formData = new FormData(wrapper.find('[data-testid=myForm]').element);

View File

@ -0,0 +1,117 @@
import { GlAlert, GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'spec/test_constants';
import PackageErrorsCount from '~/packages_and_registries/package_registry/components/list/package_errors_count.vue';
import { packageData } from '../../mock_data';
describe('PackageErrorsCount', () => {
let wrapper;
const firstPackage = packageData();
const errorPackage = {
...packageData(),
id: 'gid://gitlab/Packages::Package/121',
status: 'ERROR',
name: 'error package',
};
const findErrorPackageAlert = () => wrapper.findComponent(GlAlert);
const findErrorAlertButton = () => findErrorPackageAlert().findComponent(GlButton);
const mountComponent = ({ props = {}, stubs = {} } = {}) => {
wrapper = shallowMountExtended(PackageErrorsCount, {
propsData: {
...props,
},
stubs: {
...stubs,
},
});
};
describe('when an error package is present', () => {
beforeEach(() => {
mountComponent({ props: { errorPackages: [errorPackage] } });
});
it('should display an alert with default body message', () => {
expect(findErrorPackageAlert().exists()).toBe(true);
expect(findErrorPackageAlert().props('title')).toBe(
'There was an error publishing error package',
);
expect(findErrorPackageAlert().text()).toBe(
'There was a timeout and the package was not published. Delete this package and try again.',
);
});
it('should display alert body with message set in `statusMessage`', () => {
mountComponent({
props: {
errorPackages: [{ ...errorPackage, statusMessage: 'custom error message' }],
},
});
expect(findErrorPackageAlert().exists()).toBe(true);
expect(findErrorPackageAlert().props('title')).toBe(
'There was an error publishing error package',
);
expect(findErrorPackageAlert().text()).toBe('custom error message');
});
describe('`Delete this package` button', () => {
beforeEach(() => {
mountComponent({
props: { errorPackages: [errorPackage] },
stubs: { GlAlert },
});
});
it('displays the button within the alert', () => {
expect(findErrorAlertButton().text()).toBe('Delete this package');
});
it('when clicked emits `confirm-delete` event', () => {
findErrorAlertButton().vm.$emit('click');
expect(wrapper.emitted('confirm-delete')[0][0]).toStrictEqual([errorPackage]);
});
});
});
describe('when multiple error packages are present', () => {
beforeEach(() => {
mountComponent({
props: { errorPackages: [{ ...firstPackage, status: errorPackage.status }, errorPackage] },
});
});
it('should display an alert with default body message', () => {
expect(findErrorPackageAlert().props('title')).toBe(
'There was an error publishing 2 packages',
);
expect(findErrorPackageAlert().text()).toBe(
'Failed to publish 2 packages. Delete these packages and try again.',
);
});
describe('`Show packages with errors` button', () => {
beforeEach(() => {
setWindowLocation(`${TEST_HOST}/foo?type=maven&after=1234`);
mountComponent({
props: {
errorPackages: [{ ...firstPackage, status: errorPackage.status }, errorPackage],
},
stubs: { GlAlert },
});
});
it('is shown with correct href within the alert', () => {
expect(findErrorAlertButton().text()).toBe('Show packages with errors');
expect(findErrorAlertButton().attributes('href')).toBe(
`${TEST_HOST}/foo?type=maven&status=error`,
);
});
});
});
});

View File

@ -1,13 +1,12 @@
import { GlAlert, GlButton } from '@gitlab/ui';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'spec/test_constants';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue'; import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue'; import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import setWindowLocation from 'helpers/set_window_location_helper'; import PackageErrorsCount from '~/packages_and_registries/package_registry/components/list/package_errors_count.vue';
import { import {
DELETE_PACKAGE_TRACKING_ACTION, DELETE_PACKAGE_TRACKING_ACTION,
DELETE_PACKAGES_TRACKING_ACTION, DELETE_PACKAGES_TRACKING_ACTION,
@ -52,8 +51,7 @@ describe('packages_list', () => {
const findEmptySlot = () => wrapper.findComponent(EmptySlotStub); const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
const findRegistryList = () => wrapper.findComponent(RegistryList); const findRegistryList = () => wrapper.findComponent(RegistryList);
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow); const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
const findErrorPackageAlert = () => wrapper.findComponent(GlAlert); const findPackageErrorsCount = () => wrapper.findComponent(PackageErrorsCount);
const findErrorAlertButton = () => findErrorPackageAlert().findComponent(GlButton);
const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal); const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal);
const showMock = jest.fn(); const showMock = jest.fn();
@ -138,8 +136,8 @@ describe('packages_list', () => {
expect(findDeletePackagesModal().props('showRequestForwardingContent')).toBe(false); expect(findDeletePackagesModal().props('showRequestForwardingContent')).toBe(false);
}); });
it('does not have an error alert displayed', () => { it('renders PackageErrorsCount component', () => {
expect(findErrorPackageAlert().exists()).toBe(false); expect(findPackageErrorsCount().props('errorPackages')).toEqual([]);
}); });
}); });
@ -284,51 +282,23 @@ describe('packages_list', () => {
}); });
}); });
describe('when an error package is present', () => { describe('when error packages are present', () => {
beforeEach(() => { beforeEach(() => {
mountComponent({ props: { list: [firstPackage, errorPackage] } }); mountComponent({ props: { list: [firstPackage, errorPackage] } });
}); });
it('should display an alert with default body message', () => { it('renders PackageErrorsCount component with props', () => {
expect(findErrorPackageAlert().exists()).toBe(true); expect(findPackageErrorsCount().props('errorPackages')).toStrictEqual([errorPackage]);
expect(findErrorPackageAlert().props('title')).toBe(
'There was an error publishing a error package package',
);
expect(findErrorPackageAlert().text()).toBe(
'There was a timeout and the package was not published. Delete this package and try again.',
);
}); });
it('should display alert body with message set in `statusMessage`', () => { it('and PackageErrorsCount component emits `confirm-delete`, modal component is shown', async () => {
mountComponent({ findPackageErrorsCount().vm.$emit('confirm-delete', [errorPackage]);
props: { list: [firstPackage, { ...errorPackage, statusMessage: 'custom error message' }] },
});
expect(findErrorPackageAlert().exists()).toBe(true); expect(showMock).toHaveBeenCalledTimes(1);
expect(findErrorPackageAlert().props('title')).toBe(
'There was an error publishing a error package package',
);
expect(findErrorPackageAlert().text()).toBe('custom error message');
});
describe('`Delete this package` button', () => { await nextTick();
beforeEach(() => {
mountComponent({ props: { list: [firstPackage, errorPackage] }, stubs: { GlAlert } });
});
it('displays the button within the alert', () => { expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([errorPackage]);
expect(findErrorAlertButton().text()).toBe('Delete this package');
});
it('should display the deletion modal when clicked on the `Delete this package` button', async () => {
findErrorAlertButton().vm.$emit('click');
await nextTick();
expect(showMock).toHaveBeenCalledTimes(1);
expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([errorPackage]);
});
}); });
describe('when `hideErrorAlert` is true', () => { describe('when `hideErrorAlert` is true', () => {
@ -339,43 +309,7 @@ describe('packages_list', () => {
}); });
it('does not display alert message', () => { it('does not display alert message', () => {
expect(findErrorPackageAlert().exists()).toBe(false); expect(findPackageErrorsCount().exists()).toBe(false);
});
});
});
describe('when multiple error packages are present', () => {
beforeEach(() => {
mountComponent({
props: { list: [{ ...firstPackage, status: errorPackage.status }, errorPackage] },
});
});
it('should display an alert with default body message', () => {
expect(findErrorPackageAlert().props('title')).toBe(
'There was an error publishing 2 packages',
);
expect(findErrorPackageAlert().text()).toBe(
'2 packages were not published to the registry. Remove these packages and try again.',
);
});
describe('`Show packages with errors` button', () => {
beforeEach(() => {
setWindowLocation(`${TEST_HOST}/foo?type=maven&after=1234`);
mountComponent({
props: {
list: [{ ...firstPackage, status: errorPackage.status }, errorPackage],
},
stubs: { GlAlert },
});
});
it('is shown with correct href within the alert', () => {
expect(findErrorAlertButton().text()).toBe('Show packages with errors');
expect(findErrorAlertButton().attributes('href')).toBe(
`${TEST_HOST}/foo?type=maven&status=error`,
);
}); });
}); });
}); });

View File

@ -34,7 +34,6 @@ describe('WikiForm', () => {
const findFormat = () => wrapper.find('#wiki_format'); const findFormat = () => wrapper.find('#wiki_format');
const findMessage = () => wrapper.find('#wiki_message'); const findMessage = () => wrapper.find('#wiki_message');
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor); const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button'); const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button');
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link'); const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
@ -384,36 +383,6 @@ describe('WikiForm', () => {
}); });
}); });
describe('submit button state', () => {
it.each`
title | content | buttonState | disabledAttr
${'something'} | ${'something'} | ${'enabled'} | ${false}
${''} | ${'something'} | ${'disabled'} | ${true}
${'something'} | ${''} | ${'disabled'} | ${false}
${''} | ${''} | ${'disabled'} | ${true}
`(
"when title='$title', content='$content', then the button is $buttonState'",
async ({ title, content, disabledAttr }) => {
createWrapper({ mountFn: mount });
await findTitle().setValue(title);
await findMarkdownEditor().vm.$emit('input', content);
expect(findSubmitButton().props().disabled).toBe(disabledAttr);
},
);
it.each`
persisted | buttonLabel
${true} | ${'Save changes'}
${false} | ${'Create page'}
`('when persisted=$persisted, label is set to $buttonLabel', ({ persisted, buttonLabel }) => {
createWrapper({ persisted });
expect(findSubmitButton().text()).toBe(buttonLabel);
});
});
describe('cancel button state', () => { describe('cancel button state', () => {
it.each` it.each`
persisted | redirectLink persisted | redirectLink

View File

@ -1,52 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import { assertProps } from 'helpers/assert_props';
import SlotSwitch from '~/vue_shared/components/slot_switch.vue';
describe('SlotSwitch', () => {
const slots = {
first: '<a>AGP</a>',
second: '<p>PCI</p>',
};
let wrapper;
const createComponent = (propsData) => {
wrapper = shallowMount(SlotSwitch, {
propsData,
slots,
});
};
const getChildrenHtml = () => wrapper.findAll('* *').wrappers.map((c) => c.html());
it('throws an error if activeSlotNames is missing', () => {
expect(() => assertProps(SlotSwitch, {})).toThrow(
'[Vue warn]: Missing required prop: "activeSlotNames"',
);
});
it('renders no slots if activeSlotNames is empty', () => {
createComponent({
activeSlotNames: [],
});
expect(getChildrenHtml().length).toBe(0);
});
it('renders one slot if activeSlotNames contains single slot name', () => {
createComponent({
activeSlotNames: ['first'],
});
expect(getChildrenHtml()).toEqual([slots.first]);
});
it('renders multiple slots if activeSlotNames contains multiple slot names', () => {
createComponent({
activeSlotNames: Object.keys(slots),
});
expect(getChildrenHtml()).toEqual(Object.values(slots));
});
});

View File

@ -14,6 +14,7 @@ import WorkItemRelationshipIcons from '~/work_items/components/shared/work_item_
import { import {
workItemTask, workItemTask,
workItemEpic,
workItemObjectiveWithChild, workItemObjectiveWithChild,
confidentialWorkItemTask, confidentialWorkItemTask,
closedWorkItemTask, closedWorkItemTask,
@ -32,6 +33,8 @@ describe('WorkItemLinkChildContents', () => {
const mockAssignees = ASSIGNEES.assignees.nodes; const mockAssignees = ASSIGNEES.assignees.nodes;
const mockLabels = LABELS.labels.nodes; const mockLabels = LABELS.labels.nodes;
const mockRouterPush = jest.fn();
const findStatusBadgeComponent = () => const findStatusBadgeComponent = () =>
wrapper.findByTestId('item-status-icon').findComponent(WorkItemStateBadge); wrapper.findByTestId('item-status-icon').findComponent(WorkItemStateBadge);
const findConfidentialIconComponent = () => wrapper.findByTestId('confidential-icon'); const findConfidentialIconComponent = () => wrapper.findByTestId('confidential-icon');
@ -48,13 +51,23 @@ describe('WorkItemLinkChildContents', () => {
canUpdate = true, canUpdate = true,
childItem = workItemTask, childItem = workItemTask,
showLabels = true, showLabels = true,
workItemFullPath = 'test-project-path',
isGroup = false,
} = {}) => { } = {}) => {
wrapper = shallowMountExtended(WorkItemLinkChildContents, { wrapper = shallowMountExtended(WorkItemLinkChildContents, {
propsData: { propsData: {
canUpdate, canUpdate,
childItem, childItem,
showLabels, showLabels,
workItemFullPath: 'test-project-path', workItemFullPath,
},
provide: {
isGroup,
},
mocks: {
$router: {
push: mockRouterPush,
},
}, },
}); });
}; };
@ -129,6 +142,29 @@ describe('WorkItemLinkChildContents', () => {
expect(wrapper.emitted('click')).toEqual([[eventObj]]); expect(wrapper.emitted('click')).toEqual([[eventObj]]);
}); });
describe('when the linked item can be navigated to via Vue Router', () => {
const preventDefault = jest.fn();
beforeEach(() => {
createComponent({
childItem: workItemEpic,
isGroup: true,
workItemFullPath: 'gitlab-org/gitlab-test',
});
findTitleEl().vm.$emit('click', { preventDefault });
});
it('pushes a new router state', () => {
expect(mockRouterPush).toHaveBeenCalled();
});
it('prevents the default event behaviour', () => {
expect(preventDefault).toHaveBeenCalled();
});
it('does not emit a click event', () => {
expect(wrapper.emitted('click')).not.toBeDefined();
});
});
}); });
describe('item metadata', () => { describe('item metadata', () => {

View File

@ -1898,14 +1898,15 @@ export const workItemEpic = {
namespace: { namespace: {
__typename: 'Project', __typename: 'Project',
id: '1', id: '1',
fullPath: 'test-project-path', fullPath: 'gitlab-org/gitlab-test',
name: 'Project name', name: 'Project name',
}, },
createdAt: '2022-08-03T12:41:54Z', createdAt: '2022-08-03T12:41:54Z',
closedAt: null, closedAt: null,
webUrl: '/gitlab-org/gitlab-test/-/work_items/4', webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/gitlab-test/-/work_items/4',
widgets: [ widgets: [
workItemObjectiveMetadataWidgets.ASSIGNEES, workItemObjectiveMetadataWidgets.ASSIGNEES,
workItemObjectiveMetadataWidgets.LINKED_ITEMS,
{ {
type: 'HIERARCHY', type: 'HIERARCHY',
hasChildren: false, hasChildren: false,

View File

@ -1,12 +0,0 @@
---
extends: ../frontend/.eslintrc.yml
settings:
import/resolver:
jest:
jestConfigFile: 'jest.config.integration.js'
rules:
no-restricted-imports:
- error
- fs
globals:
mockServer: false

View File

@ -219,6 +219,11 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
name: event_name, name: event_name,
time_framed: time_framed, time_framed: time_framed,
filter: { label: 'label_name', value: 16.17 } filter: { label: 'label_name', value: 16.17 }
),
Gitlab::Usage::EventSelectionRule.new(
name: event_name,
time_framed: time_framed,
filter: { custom: 'custom_property' }
) )
] ]
end end
@ -244,6 +249,27 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
end end
end end
context 'when event selection rule has a filter on a custom property' do
let(:custom_properties) { { custom: 'custom_property' } }
let(:redis_arguments) do
[
"filter:[custom:custom_property]-#{week_suffix}",
"#{event_name}-#{week_suffix}"
]
end
it 'updates the correct redis keys' do
described_class.track_event(
event_name,
additional_properties: custom_properties,
user: user,
project: project
)
expect_redis_tracking
end
end
context 'when redis key is overridden in total_counter_redis_key_overrides.yml' do context 'when redis key is overridden in total_counter_redis_key_overrides.yml' do
let(:time_framed) { false } let(:time_framed) { false }
let(:redis_arguments) { %w[SOME_LEGACY_KEY ANOTHER_LEGACY_KEY A_THIRD_LEGACY_KEY] } let(:redis_arguments) { %w[SOME_LEGACY_KEY ANOTHER_LEGACY_KEY A_THIRD_LEGACY_KEY] }

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::TopologyServiceClient::BaseService, feature_category: :cell do
subject(:base_service) { described_class.new }
describe '#initialize' do
context 'when topology service is disabled' do
it 'raises an error when topology service is not enabled' do
expect(Gitlab.config.topology_service).to receive(:enabled).and_return(false)
expect { base_service }.to raise_error(NotImplementedError)
end
it 'raises an error when no cell is configured' do
allow(Gitlab.config.topology_service).to receive(:enabled).and_return(true)
expect(Gitlab.config.cell).to receive(:name).once.and_return(nil)
expect { base_service }.to raise_error(NotImplementedError)
end
end
end
end

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::TopologyServiceClient::CellService, feature_category: :cell do
subject(:cell_service) { described_class.new }
let(:service_class) { Gitlab::Cells::TopologyService::CellService::Stub } # gRpc Service Class
describe '#get_cell_info' do
context 'when topology service is disabled' do
it 'raises an error when topology service is not enabled' do
expect(Gitlab.config.topology_service).to receive(:enabled).and_return(false)
expect { cell_service }.to raise_error(NotImplementedError)
end
it 'raises an error when no cell is configured' do
allow(Gitlab.config.topology_service).to receive(:enabled).and_return(true)
expect(Gitlab.config.cell).to receive(:name).once.and_return(nil)
expect { cell_service }.to raise_error(NotImplementedError)
end
end
context 'when topology service is enabled' do
before do
allow(Gitlab.config.topology_service).to receive(:enabled).once.and_return(true)
allow(Gitlab.config.cell).to receive(:name).once.and_return("cell-1")
end
let(:cell_info) do
Gitlab::Cells::TopologyService::CellInfo.new(
name: "cell-1",
address: "127.0.0.1:3000",
session_prefix: "cell-1-",
sequence_range: Gitlab::Cells::TopologyService::SequenceRange.new(minval: 1, maxval: 1000)
)
end
it 'returns the cell information' do
expect_next_instance_of(service_class) do |instance|
expect(instance).to receive(:get_cell).with(
Gitlab::Cells::TopologyService::GetCellRequest.new(cell_name: "cell-1")
).and_return(Gitlab::Cells::TopologyService::GetCellResponse.new(cell_info: cell_info))
end
expect(cell_service.get_cell_info).to eq(cell_info)
end
it 'returns nil if the cell is not found' do
expect_next_instance_of(service_class) do |instance|
expect(instance).to receive(:get_cell).with(
Gitlab::Cells::TopologyService::GetCellRequest.new(cell_name: "cell-1")
).and_raise(GRPC::NotFound)
end
expected_error = "Cell 'cell-1' not found on Topology Service"
expect(Gitlab::AppLogger).to receive(:error).with(hash_including(message: expected_error))
expect(cell_service.get_cell_info).to be_nil
end
end
end
end

View File

@ -51,11 +51,12 @@ RSpec.describe Gitlab::Tracking::EventValidator, feature_category: :service_ping
end end
end end
context 'when a base additional property is invalid' do context 'when an additional property is invalid' do
[ [
{ label: 123 }, { label: 123 },
{ value: 'test_value' }, { value: 'test_value' },
{ property: true } { property: true },
{ lang: [1, 2] }
].each do |invalid_property| ].each do |invalid_property|
context "when #{invalid_property.each_key.first} is invalid" do context "when #{invalid_property.each_key.first} is invalid" do
let(:additional_properties) { invalid_property } let(:additional_properties) { invalid_property }

View File

@ -61,6 +61,10 @@ RSpec.describe Gitlab::Utils::Email, feature_category: :service_desk do
'added user@example.com and hello@example.com' | 'added us*****@e*****.c** and he*****@e*****.c**' 'added user@example.com and hello@example.com' | 'added us*****@e*****.c** and he*****@e*****.c**'
'removed user@example.com, hello@example.com and bye@example.com' | 'removed user@example.com, hello@example.com and bye@example.com' |
'removed us*****@e*****.c**, he*****@e*****.c** and by*****@e*****.c**' 'removed us*****@e*****.c**, he*****@e*****.c** and by*****@e*****.c**'
'added user#@example.com, hello!@example.com and bye$@example.com' |
'added us*****@e*****.c**, he*****@e*****.c** and by*****@e*****.c**'
'added user_@example.com, hello}@example.com and !#$%&\'*+-/=?^_{|}~@example.com' |
'added us*****@e*****.c**, he*****@e*****.c** and !#*****@e*****.c**'
end end
with_them do with_them do

View File

@ -4866,42 +4866,41 @@ RSpec.describe User, feature_category: :user_profile do
end end
describe '#solo_owned_organizations' do describe '#solo_owned_organizations' do
let_it_be_with_refind(:user) { create(:user) } let_it_be(:user) { create(:user) }
subject(:solo_owned_organizations) { user.solo_owned_organizations } subject { user.solo_owned_organizations }
context 'no owned organizations' do context 'no owned organizations' do
it { is_expected.to be_empty } it { is_expected.to be_empty }
end end
context 'has owned organizations' do context 'has owned organizations' do
let(:organization) { create(:organization) } let_it_be(:solo_owned_organizations) { create_list(:organization_owner, 2, user: user).map(&:organization) }
let_it_be(:multi_owned_organization) do
before do create(:organization, organization_users: [
organization.add_owner(user) create(:organization_owner, user: user),
create(:organization_owner, user: create(:user))
])
end end
context 'not solo owner' do it 'returns solo-owned organizations' do
let_it_be(:user2) { create(:user) } is_expected.to match_array(solo_owned_organizations)
before do
organization.add_owner(user2)
end
it { is_expected.to be_empty }
end end
context 'solo owner' do it 'does not return multi owned organizations' do
it { is_expected.to include(organization) } is_expected.not_to include(multi_owned_organization)
end
end
context 'solo owner with other members' do
let_it_be(:organization) do
create(:organization, organization_users: [
create(:organization_owner, user: user),
create(:organization_user, user: create(:user))
])
end end
context 'solo owner with other members' do it { is_expected.to include(organization) }
before do
create(:organization_user, organization: organization)
end
it { is_expected.to include(organization) }
end
end end
end end

View File

@ -8,7 +8,20 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
subject(:service) { described_class.new(project, user, params) } subject(:service) { described_class.new(project, user, params) }
describe '#execute' do describe '#execute' do
subject(:package) { service.execute } let(:package) { service_response.payload.fetch(:package) }
subject(:service_response) { service.execute }
shared_examples 'returning an error service response and not creating conan package' do |message:|
it_behaves_like 'returning an error service response', message: message
it { is_expected.to have_attributes(reason: :record_invalid) }
it 'does not create a conan package' do
expect { service_response }
.to not_change { Packages::Package.conan.count }
.and not_change { Packages::PackageFile.count }
end
end
context 'valid params' do context 'valid params' do
let(:params) do let(:params) do
@ -20,6 +33,8 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
} }
end end
it_behaves_like 'returning a success service response'
it 'creates a new package' do it 'creates a new package' do
expect(package).to be_valid expect(package).to be_valid
expect(package.name).to eq(params[:package_name]) expect(package.name).to eq(params[:package_name])
@ -30,8 +45,14 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
end end
it_behaves_like 'assigns the package creator' it_behaves_like 'assigns the package creator'
it_behaves_like 'assigns build to package'
it_behaves_like 'assigns status to package' it_behaves_like 'assigns build to package' do
subject { super().payload.fetch(:package) }
end
it_behaves_like 'assigns status to package' do
subject { super().payload.fetch(:package) }
end
end end
context 'invalid params' do context 'invalid params' do
@ -44,13 +65,13 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
} }
end end
it 'fails' do it_behaves_like 'returning an error service response and not creating conan package',
expect { package }.to raise_error(ActiveRecord::RecordInvalid, /Conan metadatum package username is invalid/) message: 'Validation failed: Conan metadatum package username is invalid'
end
end end
context 'with existing recipe' do context 'with existing recipe' do
let_it_be(:existing_package) { create(:conan_package, project: project) } let_it_be(:existing_package) { create(:conan_package, project: project) }
let(:params) do let(:params) do
{ {
package_name: existing_package.name, package_name: existing_package.name,
@ -60,9 +81,8 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
} }
end end
it 'does not create a conan package with same recipe' do it_behaves_like 'returning an error service response and not creating conan package',
expect { package }.to raise_error(ActiveRecord::RecordInvalid, /Package recipe already exists/) message: 'Validation failed: Package recipe already exists'
end
end end
end end
end end

View File

@ -31,13 +31,6 @@ RSpec.shared_examples 'User creates wiki page' do
end end
end end
it "disables the submit button", :js do
page.within(".wiki-form") do
fill_in(:wiki_title, with: "")
expect(page).to have_button('Create page', disabled: true)
end
end
it "makes sure links to unknown pages work correctly", :js do it "makes sure links to unknown pages work correctly", :js do
page.within(".wiki-form") do page.within(".wiki-form") do
fill_in(:wiki_content, with: "[link test](test)") fill_in(:wiki_content, with: "[link test](test)")

View File

@ -127,13 +127,6 @@ RSpec.shared_examples 'User updates wiki page' do
end end
end end
it "disables the submit button", :js do
page.within(".wiki-form") do
fill_in(:wiki_title, with: "")
expect(page).to have_button('Save changes', disabled: true)
end
end
it 'shows the emoji autocompletion dropdown', :js do it 'shows the emoji autocompletion dropdown', :js do
find('#wiki_content').native.send_keys('') find('#wiki_content').native.send_keys('')
fill_in(:wiki_content, with: ':') fill_in(:wiki_content, with: ':')

View File

@ -1,8 +0,0 @@
rules:
'@gitlab/require-i18n-strings': off
import/no-extraneous-dependencies: off
import/no-commonjs: off
import/no-nodejs-modules: off
filenames/match-regex: off
no-console: off
import/no-unresolved: off

View File

@ -1273,11 +1273,31 @@
minimatch "^3.1.2" minimatch "^3.1.2"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@eslint/eslintrc@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6"
integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
espree "^10.0.1"
globals "^14.0.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.1.0"
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@8.57.1": "@eslint/js@8.57.1":
version "8.57.1" version "8.57.1"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
"@eslint/js@^9.13.0":
version "9.13.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.13.0.tgz#c5f89bcd57eb54d5d4fa8b77693e9c28dc97e547"
integrity sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==
"@fastify/busboy@^2.0.0": "@fastify/busboy@^2.0.0":
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff" resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff"
@ -1381,10 +1401,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.119.0.tgz#becbeea7e7ee241baecdad02b9ad04de7a8aed04" resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.119.0.tgz#becbeea7e7ee241baecdad02b9ad04de7a8aed04"
integrity sha512-Os/PF37pCY75uLA0dmGaZe13BmirzlWH+pFLinCAPRChEC7KhHCJtIy0efRAxzkA4uatmHpJHxftuTc7NeiSNQ== integrity sha512-Os/PF37pCY75uLA0dmGaZe13BmirzlWH+pFLinCAPRChEC7KhHCJtIy0efRAxzkA4uatmHpJHxftuTc7NeiSNQ==
"@gitlab/ui@97.3.0": "@gitlab/ui@98.4.0":
version "97.3.0" version "98.4.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-97.3.0.tgz#8d3a59666e4d463032dd6e18ee38f14f7785fd06" resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-98.4.0.tgz#6f00322f1138abf894ccaed27f19609e9d6ab376"
integrity sha512-4TqMBdHspR9+y83LEqLs+87wUSqOZIOKm9UQr9xJQszKBLdhd6TlhgAR4tsErw35d6CmSuMc4jbuGzckkoDLKA== integrity sha512-M+00vM4h4wTRr87C8vGWJzoKGKBtKmlmmdQUzATDratdWbGDqHGRn8qIu/ETjugBPcmfnkRPgipEWhrCTkCoOg==
dependencies: dependencies:
"@floating-ui/dom" "1.4.3" "@floating-ui/dom" "1.4.3"
echarts "^5.3.2" echarts "^5.3.2"
@ -4146,10 +4166,10 @@ acorn@^6.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.8.1, acorn@^8.9.0: acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.12.0, acorn@^8.8.1, acorn@^8.9.0:
version "8.12.1" version "8.13.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3"
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==
agent-base@6: agent-base@6:
version "6.0.2" version "6.0.2"
@ -7323,6 +7343,11 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint-visitor-keys@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz#1f785cc5e81eb7534523d85922248232077d2f8c"
integrity sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==
eslint@8.57.1: eslint@8.57.1:
version "8.57.1" version "8.57.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9"
@ -7367,6 +7392,15 @@ eslint@8.57.1:
strip-ansi "^6.0.1" strip-ansi "^6.0.1"
text-table "^0.2.0" text-table "^0.2.0"
espree@^10.0.1:
version "10.2.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-10.2.0.tgz#f4bcead9e05b0615c968e85f83816bc386a45df6"
integrity sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==
dependencies:
acorn "^8.12.0"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^4.1.0"
espree@^9.3.1, espree@^9.6.0, espree@^9.6.1: espree@^9.3.1, espree@^9.6.0, espree@^9.6.1:
version "9.6.1" version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
@ -8181,6 +8215,11 @@ globals@^13.19.0, globals@^13.24.0:
dependencies: dependencies:
type-fest "^0.20.2" type-fest "^0.20.2"
globals@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
globals@^15.7.0: globals@^15.7.0:
version "15.9.0" version "15.9.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-15.9.0.tgz#e9de01771091ffbc37db5714dab484f9f69ff399" resolved "https://registry.yarnpkg.com/globals/-/globals-15.9.0.tgz#e9de01771091ffbc37db5714dab484f9f69ff399"
@ -13554,7 +13593,7 @@ source-map-resolve@^0.5.0:
source-map-url "^0.4.0" source-map-url "^0.4.0"
urix "^0.1.0" urix "^0.1.0"
source-map-support@0.5.13: source-map-support@0.5.13, source-map-support@~0.5.12:
version "0.5.13" version "0.5.13"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932"
integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==
@ -13562,14 +13601,6 @@ source-map-support@0.5.13:
buffer-from "^1.0.0" buffer-from "^1.0.0"
source-map "^0.6.0" source-map "^0.6.0"
source-map-support@~0.5.12:
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map-url@^0.4.0: source-map-url@^0.4.0:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
@ -13745,7 +13776,16 @@ string-length@^4.0.1:
char-regex "^1.0.2" char-regex "^1.0.2"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: "string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -13798,7 +13838,7 @@ string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1:
dependencies: dependencies:
safe-buffer "~5.1.0" safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -13812,6 +13852,13 @@ strip-ansi@^5.2.0:
dependencies: dependencies:
ansi-regex "^4.1.0" ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1, strip-ansi@^7.1.0: strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@ -15536,7 +15583,7 @@ worker-loader@^3.0.8:
loader-utils "^2.0.0" loader-utils "^2.0.0"
schema-utils "^3.0.0" schema-utils "^3.0.0"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -15554,6 +15601,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0: wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"