Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
836c276094
commit
f4f581c6d9
|
|
@ -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
|
||||
318
.eslintrc.yml
318
.eslintrc.yml
|
|
@ -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
|
||||
|
|
@ -473,7 +473,8 @@
|
|||
|
||||
# Code patterns + .ci-patterns
|
||||
.code-patterns: &code-patterns
|
||||
- ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
|
||||
- ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
|
||||
- "eslint.config.mjs"
|
||||
- ".browserslistrc"
|
||||
- ".stylelintrc"
|
||||
- "{,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-backstage-patterns: &code-backstage-patterns
|
||||
- ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
|
||||
- ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
|
||||
- "eslint.config.mjs"
|
||||
- ".browserslistrc"
|
||||
- ".stylelintrc"
|
||||
- "{,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-qa-patterns: &code-qa-patterns
|
||||
- ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
|
||||
- ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
|
||||
- "eslint.config.mjs"
|
||||
- ".browserslistrc"
|
||||
- ".stylelintrc"
|
||||
- "{,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-backstage-qa-patterns: &code-backstage-qa-patterns
|
||||
- ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
|
||||
- ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
|
||||
- "eslint.config.mjs"
|
||||
- ".browserslistrc"
|
||||
- ".stylelintrc"
|
||||
- "{,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"
|
||||
- "Dockerfile.assets"
|
||||
- "vendor/assets/**/*"
|
||||
- ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
|
||||
- ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
|
||||
- "eslint.config.mjs"
|
||||
- "*_VERSION"
|
||||
- "{,jh/}Gemfile{,.lock}"
|
||||
- "{,jh/}Gemfile.next{,.lock}"
|
||||
|
|
|
|||
|
|
@ -3,5 +3,4 @@
|
|||
Lint/Void:
|
||||
Details: grace period
|
||||
Exclude:
|
||||
- 'ee/lib/gitlab/llm/ai_message.rb'
|
||||
- 'spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb'
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ Rails/InverseOf:
|
|||
- 'ee/app/models/ee/service_desk_setting.rb'
|
||||
- 'ee/app/models/ee/user.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_log.rb'
|
||||
- 'ee/app/models/geo/job_artifact_registry.rb'
|
||||
|
|
|
|||
8
Gemfile
8
Gemfile
|
|
@ -387,9 +387,9 @@ gem 'gitlab-license', '~> 2.5', feature_category: :shared
|
|||
gem 'rack-attack', '~> 6.7.0' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
|
||||
# Sentry integration
|
||||
gem 'sentry-ruby', '~> 5.19.0', feature_category: :observability
|
||||
gem 'sentry-rails', '~> 5.19.0', feature_category: :observability
|
||||
gem 'sentry-sidekiq', '~> 5.19.0', feature_category: :observability
|
||||
gem 'sentry-ruby', '~> 5.21.0', feature_category: :observability
|
||||
gem 'sentry-rails', '~> 5.21.0', feature_category: :observability
|
||||
gem 'sentry-sidekiq', '~> 5.21.0', feature_category: :observability
|
||||
|
||||
# PostgreSQL query parsing
|
||||
#
|
||||
|
|
@ -582,7 +582,7 @@ group :test do
|
|||
|
||||
gem 'shoulda-matchers', '~> 5.1.0', require: false # 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 'concurrent-ruby', '~> 1.1' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'test-prof', '~> 1.4.0', feature_category: :tooling
|
||||
|
|
|
|||
|
|
@ -661,11 +661,11 @@
|
|||
{"name":"sawyer","version":"0.9.2","platform":"ruby","checksum":"fa3a72d62a4525517b18857ddb78926aab3424de0129be6772a8e2ba240e7aca"},
|
||||
{"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"},
|
||||
{"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":"sentry-rails","version":"5.19.0","platform":"ruby","checksum":"d4ad5323feea8e876f9feb2f50b126a3be3b4f6e137d37c360c31d52b6861995"},
|
||||
{"name":"sentry-ruby","version":"5.19.0","platform":"ruby","checksum":"0ddf89f246840a5c50df6c68b8eb59ad23ee4adb4a91187a414bb196cee1838b"},
|
||||
{"name":"sentry-sidekiq","version":"5.19.0","platform":"ruby","checksum":"1b16ec4b15b35dcbdd182494d612aae7ec5c923a9ed6814aed1b56103feecb80"},
|
||||
{"name":"sentry-rails","version":"5.21.0","platform":"ruby","checksum":"b5a943d199aff0d3cb94dbac4eb3e00622dd0c55fd1be0cffd43a7e09f0ad602"},
|
||||
{"name":"sentry-ruby","version":"5.21.0","platform":"ruby","checksum":"294e0dd59afce7e08ba22a4e880924345c75c3e858dc8ee23553716793f78629"},
|
||||
{"name":"sentry-sidekiq","version":"5.21.0","platform":"ruby","checksum":"6df54ec79238f69d9d4b7647bcd2a192a4702f3a39edffd63a455203430e90e2"},
|
||||
{"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"},
|
||||
{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"},
|
||||
{"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":"webauthn","version":"3.0.0","platform":"ruby","checksum":"3f77d422c2a8a4b31e56cf42f83414bd066e0506e9896936e1730262dc4a20e6"},
|
||||
{"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":"websocket","version":"1.2.10","platform":"ruby","checksum":"2cc1a4a79b6e63637b326b4273e46adcddf7871caa5dc5711f2ca4061a629fa8"},
|
||||
{"name":"websocket-driver","version":"0.7.6","platform":"java","checksum":"bc894b9e9d5aee55ac04b61003e1957c4ef411a5a048199587d0499785b505c3"},
|
||||
|
|
|
|||
22
Gemfile.lock
22
Gemfile.lock
|
|
@ -1694,7 +1694,7 @@ GEM
|
|||
seed-fu (2.3.7)
|
||||
activerecord (>= 3.1)
|
||||
activesupport (>= 3.1)
|
||||
selenium-webdriver (4.23.0)
|
||||
selenium-webdriver (4.25.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
|
|
@ -1705,14 +1705,14 @@ GEM
|
|||
pastel (~> 0.8.0)
|
||||
thor (~> 1.3)
|
||||
tty-command (~> 0.10.1)
|
||||
sentry-rails (5.19.0)
|
||||
sentry-rails (5.21.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.19.0)
|
||||
sentry-ruby (5.19.0)
|
||||
sentry-ruby (~> 5.21.0)
|
||||
sentry-ruby (5.21.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.19.0)
|
||||
sentry-ruby (~> 5.19.0)
|
||||
sentry-sidekiq (5.21.0)
|
||||
sentry-ruby (~> 5.21.0)
|
||||
sidekiq (>= 3.0)
|
||||
shellany (0.0.1)
|
||||
shoulda-matchers (5.1.0)
|
||||
|
|
@ -1930,7 +1930,7 @@ GEM
|
|||
activesupport
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
webmock (3.23.1)
|
||||
webmock (3.24.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
|
|
@ -2272,9 +2272,9 @@ DEPENDENCIES
|
|||
seed-fu (~> 2.3.7)
|
||||
selenium-webdriver (~> 4.21, >= 4.21.1)
|
||||
semver_dialects (~> 3.0)
|
||||
sentry-rails (~> 5.19.0)
|
||||
sentry-ruby (~> 5.19.0)
|
||||
sentry-sidekiq (~> 5.19.0)
|
||||
sentry-rails (~> 5.21.0)
|
||||
sentry-ruby (~> 5.21.0)
|
||||
sentry-sidekiq (~> 5.21.0)
|
||||
shoulda-matchers (~> 5.1.0)
|
||||
sidekiq!
|
||||
sidekiq-cron (~> 1.12.0)
|
||||
|
|
@ -2319,7 +2319,7 @@ DEPENDENCIES
|
|||
vmstat (~> 2.3.0)
|
||||
warning (~> 1.3.0)
|
||||
webauthn (~> 3.0)
|
||||
webmock (~> 3.23.0)
|
||||
webmock (~> 3.24.0)
|
||||
webrick (~> 1.8.1)
|
||||
wikicloth (= 0.8.1)
|
||||
yajl-ruby (~> 1.4.3)
|
||||
|
|
|
|||
|
|
@ -674,11 +674,11 @@
|
|||
{"name":"sawyer","version":"0.9.2","platform":"ruby","checksum":"fa3a72d62a4525517b18857ddb78926aab3424de0129be6772a8e2ba240e7aca"},
|
||||
{"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"},
|
||||
{"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":"sentry-rails","version":"5.19.0","platform":"ruby","checksum":"d4ad5323feea8e876f9feb2f50b126a3be3b4f6e137d37c360c31d52b6861995"},
|
||||
{"name":"sentry-ruby","version":"5.19.0","platform":"ruby","checksum":"0ddf89f246840a5c50df6c68b8eb59ad23ee4adb4a91187a414bb196cee1838b"},
|
||||
{"name":"sentry-sidekiq","version":"5.19.0","platform":"ruby","checksum":"1b16ec4b15b35dcbdd182494d612aae7ec5c923a9ed6814aed1b56103feecb80"},
|
||||
{"name":"sentry-rails","version":"5.21.0","platform":"ruby","checksum":"b5a943d199aff0d3cb94dbac4eb3e00622dd0c55fd1be0cffd43a7e09f0ad602"},
|
||||
{"name":"sentry-ruby","version":"5.21.0","platform":"ruby","checksum":"294e0dd59afce7e08ba22a4e880924345c75c3e858dc8ee23553716793f78629"},
|
||||
{"name":"sentry-sidekiq","version":"5.21.0","platform":"ruby","checksum":"6df54ec79238f69d9d4b7647bcd2a192a4702f3a39edffd63a455203430e90e2"},
|
||||
{"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"},
|
||||
{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"},
|
||||
{"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":"webauthn","version":"3.0.0","platform":"ruby","checksum":"3f77d422c2a8a4b31e56cf42f83414bd066e0506e9896936e1730262dc4a20e6"},
|
||||
{"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":"websocket","version":"1.2.10","platform":"ruby","checksum":"2cc1a4a79b6e63637b326b4273e46adcddf7871caa5dc5711f2ca4061a629fa8"},
|
||||
{"name":"websocket-driver","version":"0.7.6","platform":"java","checksum":"bc894b9e9d5aee55ac04b61003e1957c4ef411a5a048199587d0499785b505c3"},
|
||||
|
|
|
|||
|
|
@ -1720,7 +1720,7 @@ GEM
|
|||
seed-fu (2.3.7)
|
||||
activerecord (>= 3.1)
|
||||
activesupport (>= 3.1)
|
||||
selenium-webdriver (4.23.0)
|
||||
selenium-webdriver (4.25.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
|
|
@ -1731,14 +1731,14 @@ GEM
|
|||
pastel (~> 0.8.0)
|
||||
thor (~> 1.3)
|
||||
tty-command (~> 0.10.1)
|
||||
sentry-rails (5.19.0)
|
||||
sentry-rails (5.21.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.19.0)
|
||||
sentry-ruby (5.19.0)
|
||||
sentry-ruby (~> 5.21.0)
|
||||
sentry-ruby (5.21.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.19.0)
|
||||
sentry-ruby (~> 5.19.0)
|
||||
sentry-sidekiq (5.21.0)
|
||||
sentry-ruby (~> 5.21.0)
|
||||
sidekiq (>= 3.0)
|
||||
shellany (0.0.1)
|
||||
shoulda-matchers (5.1.0)
|
||||
|
|
@ -1957,7 +1957,7 @@ GEM
|
|||
activesupport
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
webmock (3.23.1)
|
||||
webmock (3.24.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
|
|
@ -2299,9 +2299,9 @@ DEPENDENCIES
|
|||
seed-fu (~> 2.3.7)
|
||||
selenium-webdriver (~> 4.21, >= 4.21.1)
|
||||
semver_dialects (~> 3.0)
|
||||
sentry-rails (~> 5.19.0)
|
||||
sentry-ruby (~> 5.19.0)
|
||||
sentry-sidekiq (~> 5.19.0)
|
||||
sentry-rails (~> 5.21.0)
|
||||
sentry-ruby (~> 5.21.0)
|
||||
sentry-sidekiq (~> 5.21.0)
|
||||
shoulda-matchers (~> 5.1.0)
|
||||
sidekiq!
|
||||
sidekiq-cron (~> 1.12.0)
|
||||
|
|
@ -2346,7 +2346,7 @@ DEPENDENCIES
|
|||
vmstat (~> 2.3.0)
|
||||
warning (~> 1.3.0)
|
||||
webauthn (~> 3.0)
|
||||
webmock (~> 3.23.0)
|
||||
webmock (~> 3.24.0)
|
||||
webrick (~> 1.8.1)
|
||||
wikicloth (= 0.8.1)
|
||||
yajl-ruby (~> 1.4.3)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlFormGroup } from '@gitlab/ui';
|
||||
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';
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
rules:
|
||||
# https://gitlab.com/gitlab-org/gitlab/issues/28716
|
||||
import/no-cycle: off
|
||||
|
|
@ -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
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
rules:
|
||||
# https://gitlab.com/gitlab-org/gitlab/issues/28719
|
||||
import/no-cycle: off
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
globals:
|
||||
AP: readonly
|
||||
rules:
|
||||
'@gitlab/require-i18n-strings': off
|
||||
'@gitlab/vue-require-i18n-strings': off
|
||||
|
|
@ -199,7 +199,7 @@ export default {
|
|||
}
|
||||
|
||||
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() {
|
||||
this.$nextTick(() => {
|
||||
|
|
@ -233,9 +233,15 @@ export default {
|
|||
const node = el.nodeType === Node.TEXT_NODE ? el.parentNode : el;
|
||||
return node.closest('.js-noteable-discussion');
|
||||
},
|
||||
async quoteReply() {
|
||||
async quoteReply(e) {
|
||||
const discussionEl = this.getDiscussionInSelection();
|
||||
const text = await CopyAsGFM.selectionToGfm();
|
||||
|
||||
// Prevent 'r' being written.
|
||||
if (e && typeof e.preventDefault === 'function') {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (!discussionEl) {
|
||||
this.replyInMainEditor(text);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
<script>
|
||||
import { GlAlert, GlButton } from '@gitlab/ui';
|
||||
import { s__, sprintf, n__ } from '~/locale';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import { n__ } from '~/locale';
|
||||
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 RegistryList from '~/packages_and_registries/shared/components/registry_list.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 {
|
||||
DELETE_PACKAGE_TRACKING_ACTION,
|
||||
DELETE_PACKAGES_TRACKING_ACTION,
|
||||
|
|
@ -30,9 +29,8 @@ const forwardingFieldToPackageTypeMapping = {
|
|||
export default {
|
||||
name: 'PackagesList',
|
||||
components: {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
DeleteModal,
|
||||
PackageErrorsCount,
|
||||
PackagesListLoader,
|
||||
PackagesListRow,
|
||||
RegistryList,
|
||||
|
|
@ -90,51 +88,6 @@ export default {
|
|||
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() {
|
||||
return Object.keys(this.groupSettings)
|
||||
.filter((field) => this.groupSettings[field])
|
||||
|
|
@ -183,15 +136,6 @@ export default {
|
|||
}
|
||||
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>
|
||||
|
|
@ -205,22 +149,11 @@ export default {
|
|||
</div>
|
||||
|
||||
<template v-else>
|
||||
<gl-alert
|
||||
v-if="showErrorPackageAlert"
|
||||
class="gl-mt-5"
|
||||
variant="danger"
|
||||
: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>
|
||||
<package-errors-count
|
||||
v-if="!hideErrorAlert"
|
||||
:error-packages="errorPackages"
|
||||
@confirm-delete="setItemsToBeDeleted"
|
||||
/>
|
||||
<registry-list
|
||||
data-testid="packages-table"
|
||||
:hidden-delete="!canDeletePackages"
|
||||
|
|
|
|||
|
|
@ -236,9 +236,6 @@ export default {
|
|||
displayWikiSpecificMarkdownHelp() {
|
||||
return !this.isContentEditorActive;
|
||||
},
|
||||
disableSubmitButton() {
|
||||
return !this.title;
|
||||
},
|
||||
drawioEnabled() {
|
||||
return typeof this.drawioUrl === 'string' && this.drawioUrl.length > 0;
|
||||
},
|
||||
|
|
@ -557,7 +554,6 @@ export default {
|
|||
variant="confirm"
|
||||
type="submit"
|
||||
data-testid="wiki-submit-button"
|
||||
:disabled="disableSubmitButton"
|
||||
>{{ submitButtonText }}</gl-button
|
||||
>
|
||||
<gl-button
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -10,8 +10,10 @@ import {
|
|||
GlTooltip,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { escapeRegExp } from 'lodash';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
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 RichTimestampTooltip from '../rich_timestamp_tooltip.vue';
|
||||
import WorkItemTypeIcon from '../work_item_type_icon.vue';
|
||||
|
|
@ -22,6 +24,7 @@ import {
|
|||
WIDGET_TYPE_ASSIGNEES,
|
||||
WIDGET_TYPE_LABELS,
|
||||
LINKED_CATEGORIES_MAP,
|
||||
INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
|
||||
} from '../../constants';
|
||||
import WorkItemRelationshipIcons from './work_item_relationship_icons.vue';
|
||||
|
||||
|
|
@ -50,6 +53,14 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: {
|
||||
preventRouterNav: {
|
||||
from: INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
|
||||
default: false,
|
||||
},
|
||||
isGroup: {},
|
||||
},
|
||||
props: {
|
||||
childItem: {
|
||||
type: Object,
|
||||
|
|
@ -137,11 +148,40 @@ export default {
|
|||
return item.linkType !== LINKED_CATEGORIES_MAP.RELATES_TO;
|
||||
});
|
||||
},
|
||||
issueAsWorkItem() {
|
||||
return (
|
||||
!this.isGroup &&
|
||||
this.glFeatures.workItemsViewPreference &&
|
||||
gon.current_user_use_work_items_view
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showScopedLabel(label) {
|
||||
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>
|
||||
|
|
@ -171,10 +211,10 @@ export default {
|
|||
/>
|
||||
</span>
|
||||
<gl-link
|
||||
:href="childItem.webUrl"
|
||||
:href="childItemWebUrl"
|
||||
:class="{ '!gl-text-secondary': !isChildItemOpen }"
|
||||
class="gl-hyphens-auto gl-break-words gl-font-semibold"
|
||||
@click.exact="$emit('click', $event)"
|
||||
@click.exact="handleTitleClick"
|
||||
@mouseover="$emit('mouseover')"
|
||||
@mouseout="$emit('mouseout')"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -864,6 +864,7 @@ export default {
|
|||
:can-update-children="canUpdateChildren"
|
||||
:confidential="workItem.confidential"
|
||||
:allowed-child-types="allowedChildTypes"
|
||||
:is-drawer="isDrawer"
|
||||
@show-modal="openInModal"
|
||||
@addChild="$emit('addChild')"
|
||||
@childrenLoaded="hasChildren = $event"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { GlAlert, GlModal } from '@gitlab/ui';
|
|||
import { s__ } from '~/locale';
|
||||
import { removeHierarchyChild } from '../graphql/cache_utils';
|
||||
import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
|
||||
import { INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION } from '../constants';
|
||||
|
||||
export default {
|
||||
WORK_ITEM_DETAIL_MODAL_ID: 'work-item-detail-modal',
|
||||
|
|
@ -15,6 +16,9 @@ export default {
|
|||
GlModal,
|
||||
WorkItemDetail: () => import('./work_item_detail.vue'),
|
||||
},
|
||||
provide: {
|
||||
[INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION]: true,
|
||||
},
|
||||
props: {
|
||||
parentId: {
|
||||
type: String,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
WORKITEM_TREE_SHOWLABELS_LOCALSTORAGEKEY,
|
||||
WORK_ITEM_TYPE_VALUE_EPIC,
|
||||
WIDGET_TYPE_HIERARCHY,
|
||||
INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
|
||||
} from '../../constants';
|
||||
import {
|
||||
findHierarchyWidgets,
|
||||
|
|
@ -48,6 +49,11 @@ export default {
|
|||
WorkItemRolledUpData,
|
||||
},
|
||||
inject: ['hasSubepicsFeature'],
|
||||
provide() {
|
||||
return {
|
||||
[INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION]: !this.isDrawer,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
fullPath: {
|
||||
type: String,
|
||||
|
|
@ -96,6 +102,11 @@ export default {
|
|||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
isDrawer: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -372,3 +372,7 @@ export const WORK_ITEM_BASE_ROUTE_MAP = {
|
|||
export const WORKITEM_LINKS_SHOWLABELS_LOCALSTORAGEKEY = 'workItemLinks.showLabels';
|
||||
export const WORKITEM_TREE_SHOWLABELS_LOCALSTORAGEKEY = 'workItemTree.showLabels';
|
||||
export const WORKITEM_RELATIONSHIPS_SHOWLABELS_LOCALSTORAGEKEY = 'workItemRelationships.showLabels';
|
||||
|
||||
export const INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION = Symbol(
|
||||
'injection:prevent-router-navigation',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -19,11 +19,14 @@ class ServiceDeskSetting < ApplicationRecord
|
|||
allow_blank: true,
|
||||
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,
|
||||
length: { maximum: 255 },
|
||||
uniqueness: 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,
|
||||
presence: true,
|
||||
|
|
|
|||
|
|
@ -1665,7 +1665,8 @@ class User < ApplicationRecord
|
|||
|
||||
counts = Organizations::OrganizationUser
|
||||
.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')
|
||||
|
||||
Organizations::Organization
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ module Packages
|
|||
module Conan
|
||||
class CreatePackageService < ::Packages::CreatePackageService
|
||||
def execute
|
||||
create_package!(:conan,
|
||||
created_package = create_package!(:conan,
|
||||
name: params[:package_name],
|
||||
version: params[:package_version],
|
||||
conan_metadatum_attributes: {
|
||||
|
|
@ -12,6 +12,10 @@ module Packages
|
|||
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
|
||||
|
|
|
|||
|
|
@ -39,6 +39,18 @@
|
|||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"^gitlab_secrets_manager$": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"^gcp_secret_manager$": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
@ -198,6 +210,11 @@
|
|||
"required": [
|
||||
"akeyless"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"gitlab_secrets_manager"
|
||||
]
|
||||
}
|
||||
],
|
||||
"additionalProperties": false
|
||||
|
|
|
|||
|
|
@ -9,15 +9,17 @@ const imagesPaths = [
|
|||
|
||||
async function getAllFiles(dir, prependPath = '') {
|
||||
const result = [];
|
||||
let files = []
|
||||
let files = [];
|
||||
try {
|
||||
files = await readdir(dir, { withFileTypes: true });
|
||||
} catch(e) {}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file.name);
|
||||
|
||||
if (file.isDirectory()) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const nestedFiles = await getAllFiles(filePath, `${prependPath}${file.name}/`);
|
||||
result.push(...nestedFiles);
|
||||
} else {
|
||||
|
|
@ -32,19 +34,26 @@ export async function ImagesPlugin() {
|
|||
return {
|
||||
name: 'vite-plugin-gitlab-images',
|
||||
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 mappings = [[CEpath, CEfiles], [EEpath, EEfiles], [JHpath, JHfiles]].reduce((acc, [filesPath, filenames]) => {
|
||||
filenames.forEach(filename => {
|
||||
const mappings = [
|
||||
[CEpath, CEfiles],
|
||||
[EEpath, EEfiles],
|
||||
[JHpath, JHfiles],
|
||||
].reduce((acc, [filesPath, filenames]) => {
|
||||
filenames.forEach((filename) => {
|
||||
acc[filename] = path.resolve(filesPath, filename);
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
const alias = Object.keys(mappings).map(mapping => {
|
||||
const alias = Object.keys(mappings).map((mapping) => {
|
||||
return {
|
||||
find: mapping,
|
||||
replacement: mappings[mapping],
|
||||
}
|
||||
};
|
||||
});
|
||||
return {
|
||||
resolve: {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,12 @@
|
|||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"number"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ class DropCiPipelinesConfig < Gitlab::Database::Migration[2.2]
|
|||
milestone '17.6'
|
||||
|
||||
def up
|
||||
execute(<<~SQL)
|
||||
ALTER TABLE p_ci_pipelines_config DETACH PARTITION ci_pipelines_config;
|
||||
drop_table(:ci_pipelines_config, if_exists: true)
|
||||
|
||||
execute(<<~SQL)
|
||||
CREATE TABLE IF NOT EXISTS #{fully_qualified_partition_name(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)}
|
||||
PARTITION OF p_ci_pipelines_config FOR VALUES IN (102);
|
||||
SQL
|
||||
|
||||
drop_table(:ci_pipelines_config, if_exists: true)
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
gitlab.my-company.com. CNAME tenant_name.gitlab-dedicated.com
|
||||
```
|
||||
- Access to your domain host's DNS settings.
|
||||
|
||||
- `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
|
||||
example.com. IN CAA 0 issue "pki.goog"
|
||||
example.com. IN CAA 0 issue "letsencrypt.org"
|
||||
```
|
||||
1. Sign in to your domain host's website.
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 105 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 364 KiB |
|
|
@ -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,
|
||||
where various AWS accounts managed by GitLab and customers are controlled by a Switchboard application.
|
||||
|
||||

|
||||

|
||||
|
||||
When managing GitLab Dedicated tenant instances:
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
- Contains the string `OOO`, `PTO`, `Parental Leave`, `Friends and Family`, or `Conference`.
|
||||
- Emoji is from one of these categories:
|
||||
- **On leave** - 🌴 `:palm_tree:`, 🏖️ `:beach:`, ⛱ `:beach_umbrella:`, 🏖 `:beach_with_umbrella:`, 🌞 `:sun_with_face:`, 🎡 `:ferris_wheel:`, 🏙 `:cityscape:`
|
||||
- **Out sick** - 🌡️ `:thermometer:`, 🤒 `:face_with_thermometer:`
|
||||
- **On leave** - 🌴 `palm_tree`, 🏖️ `beach`, ⛱ `beach_umbrella`, 🏖 `beach_with_umbrella`, 🌞 `sun_with_face`, 🎡 `ferris_wheel`, 🏙 `cityscape`
|
||||
- **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
|
||||
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
|
||||
as a Slack or [GitLab status](../user/profile/index.md#set-your-current-status):
|
||||
- 2️⃣ - `:two:`
|
||||
- 3️⃣ - `:three:`
|
||||
- 4️⃣ - `:four:`
|
||||
- 5️⃣ - `:five:`
|
||||
- 2️⃣ - `two`
|
||||
- 3️⃣ - `three`
|
||||
- 4️⃣ - `four`
|
||||
- 5️⃣ - `five`
|
||||
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -176,6 +176,14 @@ Whereas, this filter is even more restricted and only includes `pull_package` ev
|
|||
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.
|
||||
|
||||
## Aggregated metrics
|
||||
|
|
|
|||
|
|
@ -85,10 +85,7 @@ Tracking classes already have three built-in properties:
|
|||
- `value`(numeric)
|
||||
|
||||
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.
|
||||
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
|
||||
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
|
||||
[`create_ci_internal_pipeline.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/537ea367dab731e886e6040d8399c430fdb67ab7/config/events/create_ci_internal_pipeline.yml):
|
||||
|
||||
```ruby
|
||||
|
|
@ -127,9 +124,7 @@ track_internal_event(
|
|||
)
|
||||
```
|
||||
|
||||
Please add custom properties only in addition to the built-in properties.
|
||||
|
||||
Custom rules can not be used as [metric filters](metric_definition_guide.md#filters).
|
||||
Please add custom properties only in addition to the built-in properties. Additional properties can only have string or numeric values.
|
||||
|
||||
#### Controller and API helpers
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ You can migrate GitLab groups:
|
|||
- Between groups in the same GitLab instance.
|
||||
|
||||
WARNING:
|
||||
Migrating GitLab groups and projects by using direct transfer is [currently unavailable](https://status.gitlab.com). We don't have an
|
||||
estimated time for resolution. For more information, please [contact support](https://about.gitlab.com/support/).
|
||||
Migrating GitLab.com groups and projects by using direct transfer is [unavailable](https://status.gitlab.com).
|
||||
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
|
||||
can [transfer groups](../manage.md#transfer-a-group) if the groups are in the same GitLab instance. Transferring groups
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ DETAILS:
|
|||
Import your projects from Bitbucket Server to GitLab.
|
||||
|
||||
WARNING:
|
||||
Importing from Bitbucket Server to GitLab.com is [currently unavailable](https://status.gitlab.com). We don't have an
|
||||
estimated time for resolution. For more information, please [contact support](https://about.gitlab.com/support/).
|
||||
This unavailability doesn't affect [importing from Bitbucket Cloud](bitbucket.md).
|
||||
Importing from Bitbucket Server to GitLab.com is [unavailable](https://status.gitlab.com).
|
||||
For more information, contact [GitLab Support](https://about.gitlab.com/support/).
|
||||
[Importing from Bitbucket Cloud](bitbucket.md) is not affected.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ DETAILS:
|
|||
Import your projects from Gitea to GitLab.
|
||||
|
||||
WARNING:
|
||||
Importing from Gitea to GitLab.com is [currently unavailable](https://status.gitlab.com). We don't have an
|
||||
estimated time for resolution. For more information, please [contact support](https://about.gitlab.com/support/).
|
||||
Importing from Gitea to GitLab.com is [unavailable](https://status.gitlab.com).
|
||||
For more information, contact [GitLab Support](https://about.gitlab.com/support/).
|
||||
|
||||
The Gitea importer can import:
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
WARNING:
|
||||
Importing from GitHub to GitLab.com is [currently unavailable](https://status.gitlab.com). We don't have an
|
||||
estimated time for resolution. For more information, please [contact support](https://about.gitlab.com/support/).
|
||||
Importing from GitHub to GitLab.com is [unavailable](https://status.gitlab.com).
|
||||
For more information, contact [GitLab Support](https://about.gitlab.com/support/).
|
||||
|
||||
Imported issues, merge requests, comments, and events have an **Imported** badge in GitLab.
|
||||
|
||||
|
|
|
|||
|
|
@ -43,8 +43,11 @@ Generate a detailed description for an issue based on a short summary you provid
|
|||
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 have permission to view the issue.
|
||||
- You must have permission to create an issue.
|
||||
- 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:
|
||||
|
||||
|
|
@ -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
|
||||
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
|
||||
|
||||
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. 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
|
||||
|
||||
> - [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.
|
||||
> - [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
|
||||
moved to the target project and remain associated as child tasks on the moved issue.
|
||||
When you move an issue to another project, all its child tasks are also moved to the target project
|
||||
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
|
||||
copied to the target project.
|
||||
|
||||
|
|
@ -224,7 +213,31 @@ To do it:
|
|||
|
||||
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.
|
||||
|
||||
|
|
@ -261,6 +274,8 @@ To close an issue, you can either:
|
|||
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**.
|
||||
|
||||
You can also use the `/close` [quick action](../quick_actions.md) in a comment or description.
|
||||
|
||||
### Reopen a closed issue
|
||||
|
||||
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**.
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
NOTE:
|
||||
Promoting a confidential issue to an epic makes all information
|
||||
related to the issue public, as epics are public to group members.
|
||||
Promoting a confidential issue to an epic creates a
|
||||
[confidential epic](../../group/epics/manage_epics.md#make-an-epic-confidential), retaining
|
||||
confidentiality.
|
||||
|
||||
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.
|
||||
- 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. 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
|
||||
|
||||
|
|
@ -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. Select **Plan > Issues**.
|
||||
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. Select the text box again, and in the dropdown list that appears, select **Search Within**, and then either **Titles** or **Descriptions**.
|
||||
1. 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}**).
|
||||
|
||||
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`,
|
||||
GitLab can return results with `securing`, `secured`,
|
||||
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.
|
||||
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. 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.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ DETAILS:
|
|||
**Tier:** Free, Premium, Ultimate
|
||||
**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.
|
||||
|
||||
|
|
@ -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:
|
||||
|
||||
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. 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
|
||||
|
||||
|
|
@ -44,7 +50,7 @@ To view the milestone list:
|
|||
1. Select **Plan > Milestones**.
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -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),
|
||||
showing the progress of completing a milestone.
|
||||
|
||||

|
||||

|
||||
|
||||
#### 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.
|
||||
- The start date and due date.
|
||||
- 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 count of total, open, closed, and merged merge requests.
|
||||
- Links to associated releases.
|
||||
- The milestone's reference you can copy to your clipboard.
|
||||
|
||||

|
||||
|
||||
|
|
@ -185,7 +194,8 @@ To delete a 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
|
||||
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.
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -176,11 +176,17 @@ module API
|
|||
end
|
||||
|
||||
def find_or_create_package
|
||||
package || ::Packages::Conan::CreatePackageService.new(
|
||||
return package if package
|
||||
|
||||
service_response = ::Packages::Conan::CreatePackageService.new(
|
||||
project,
|
||||
current_user,
|
||||
params.merge(build: current_authenticated_job)
|
||||
).execute
|
||||
|
||||
bad_request!(service_response.message) if service_response.error?
|
||||
|
||||
service_response[:package]
|
||||
end
|
||||
|
||||
def track_push_package_event
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ module Gitlab
|
|||
GITLAB_HOSTED_RUNNER = 'gitlab-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,
|
||||
target_audience: target_audience).encoded
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ module Gitlab
|
|||
# incoming_email and service_desk_email.
|
||||
module CustomEmail
|
||||
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
|
||||
def reply_address(issue, reply_key)
|
||||
|
|
@ -53,7 +54,7 @@ module Gitlab
|
|||
|
||||
def find_service_desk_setting_from_reply_address(email, 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)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ module Gitlab
|
|||
|
||||
def events_worker_args(event_class, events)
|
||||
events
|
||||
.map { |event| event.data.deep_stringify_keys }
|
||||
.map { |event| event.data.deep_stringify_keys.to_h }
|
||||
.each_slice(group_size)
|
||||
.map { |events_data_group| [event_class.name, events_data_group] }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ module Gitlab
|
|||
Gitlab::Tracking::EventValidator.new(event_name, additional_properties, kwargs).validate!
|
||||
|
||||
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]
|
||||
kwargs[:namespace] ||= project.namespace if project
|
||||
|
||||
update_redis_values(event_name, additional_properties, kwargs)
|
||||
trigger_snowplow_event(event_name, category, additional_properties, extra, kwargs) if send_snowplow_event
|
||||
send_application_instrumentation_event(event_name, additional_properties, 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, base_additional_properties, kwargs) if send_snowplow_event
|
||||
|
||||
if Feature.enabled?(:early_access_program, kwargs[:user], type: :wip)
|
||||
create_early_access_program_event(event_name, category, additional_properties[:label], kwargs)
|
||||
|
|
@ -65,7 +65,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def custom_additional_properties(additional_properties)
|
||||
additional_properties.except(*base_additional_properties.keys)
|
||||
additional_properties.except(*base_additional_properties_keys)
|
||||
end
|
||||
|
||||
def update_total_counter(event_selection_rule)
|
||||
|
|
@ -160,8 +160,8 @@ module Gitlab
|
|||
end
|
||||
strong_memoize_attr :gitlab_sdk_client
|
||||
|
||||
def base_additional_properties
|
||||
Gitlab::Tracking::EventValidator::BASE_ADDITIONAL_PROPERTIES
|
||||
def base_additional_properties_keys
|
||||
Gitlab::Tracking::EventValidator::BASE_ADDITIONAL_PROPERTIES.keys
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -11,6 +11,7 @@ module Gitlab
|
|||
property: [String],
|
||||
value: [Integer, Float]
|
||||
}.freeze
|
||||
CUSTOM_PROPERTIES_CLASSES = [String, Integer, Float].freeze
|
||||
|
||||
def initialize(event_name, additional_properties, kwargs)
|
||||
@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
|
||||
custom_properties = additional_properties.except(*BASE_ADDITIONAL_PROPERTIES.keys)
|
||||
event_definition_attributes = Gitlab::Tracking::EventDefinition.find(event_name).to_h
|
||||
allowed_types = CUSTOM_PROPERTIES_CLASSES
|
||||
custom_properties.each_key do |key|
|
||||
unless event_definition_attributes[:additional_properties].include?(key)
|
||||
raise InvalidPropertyError, "Unknown additional property: #{key}"
|
||||
end
|
||||
|
||||
validate_property!(custom_properties, key, *allowed_types)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
- i_ci_secrets_management_gcp_secret_manager_build_created
|
||||
- i_ci_secrets_management_id_tokens_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_file_browser_setting
|
||||
- i_code_review_click_single_file_mode_setting
|
||||
|
|
|
|||
|
|
@ -5,12 +5,8 @@ module Gitlab
|
|||
module Email
|
||||
extend self
|
||||
|
||||
# Don't use Devise.email_regexp or URI::MailTo::EMAIL_REGEXP to be a bit more restrictive
|
||||
# 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 = %r{(?>[a-zA-Z0-9]+|[\-._!#$%&'*+\/=?^{|}~]+){1,255}@[\w\-.]{1,253}\.{1}[a-zA-Z]{2,63}}
|
||||
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
|
||||
# deform adds a fix number of * to ensure the address cannot be guessed. Also obfuscates TLD with **
|
||||
|
|
|
|||
|
|
@ -38691,9 +38691,6 @@ msgstr ""
|
|||
msgid "Package type must be Terraform Module"
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -38907,6 +38904,9 @@ msgstr ""
|
|||
msgid "PackageRegistry|Failed to load version data"
|
||||
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}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -39218,7 +39218,7 @@ msgstr ""
|
|||
msgid "PackageRegistry|There was an error publishing %{count} packages"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|There was an error publishing a %{packageName} package"
|
||||
msgid "PackageRegistry|There was an error publishing %{packageName}"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|This NuGet package has no dependencies."
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"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",
|
||||
"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}'",
|
||||
"preinternal:stylelint": "yarn run tailwindcss:build",
|
||||
"prejest": "yarn check-dependencies",
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
"@gitlab/fonts": "^1.3.0",
|
||||
"@gitlab/query-language": "^0.0.5-a-20241017",
|
||||
"@gitlab/svgs": "3.119.0",
|
||||
"@gitlab/ui": "97.3.0",
|
||||
"@gitlab/ui": "98.4.0",
|
||||
"@gitlab/web-ide": "^0.0.1-dev-20240909013227",
|
||||
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
|
||||
"@rails/actioncable": "7.0.8-4",
|
||||
|
|
@ -254,6 +254,8 @@
|
|||
"yaml": "^2.0.0-10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@gitlab/eslint-plugin": "20.4.1",
|
||||
"@gitlab/stylelint-config": "6.2.2",
|
||||
"@graphql-eslint/eslint-plugin": "3.20.1",
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ module Gitlab
|
|||
.deep_merge(license_values)
|
||||
.deep_merge(env_values)
|
||||
.deep_merge(configuration.values)
|
||||
.deep_merge(ResourcePresets.resource_values(ci ? ResourcePresets::HIGH : ResourcePresets::DEFAULT))
|
||||
.deep_stringify_keys
|
||||
.to_yaml
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -48,6 +48,7 @@ module Gitlab
|
|||
|
||||
create_cluster
|
||||
update_server_url
|
||||
install_metrics_server
|
||||
log("Cluster '#{name}' created", :success)
|
||||
rescue Helpers::Shell::CommandFailure
|
||||
# Exit cleanly without stacktrace if shell command fails
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ RSpec.describe Gitlab::Cng::Deployment::Installation, :aggregate_failures do
|
|||
)
|
||||
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
|
||||
{
|
||||
global: {
|
||||
|
|
@ -55,7 +61,7 @@ RSpec.describe Gitlab::Cng::Deployment::Installation, :aggregate_failures do
|
|||
license: { secret: "gitlab-license" }
|
||||
},
|
||||
**config_values
|
||||
}.deep_stringify_keys.to_yaml
|
||||
}.deep_merge(resources_values).deep_stringify_keys.to_yaml
|
||||
end
|
||||
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -84,11 +84,11 @@ RSpec.describe Gitlab::Cng::Kind::Cluster do
|
|||
|
||||
it "creates cluster with ci specific configuration", :aggregate_failures do
|
||||
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",
|
||||
"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",
|
||||
namespace: "kube-system",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|||
import Runtime from 'jest-runtime';
|
||||
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)), '../../');
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ async function getCurrentTopLevelPatterns() {
|
|||
|
||||
return serializeAliasedDependencyPatterns(dependencies)
|
||||
.concat(serializeAliasedDependencyPatterns(devDependencies))
|
||||
.filter(isAliasedDependency);
|
||||
.filter(dep => isAliasedDependency(dep));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -439,7 +439,6 @@ spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
|
|||
spec/frontend/vue_popovers_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/chronic_duration_input_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/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/runner_instructions_modal_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/source_viewer/components/chunk_spec.js
|
||||
spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
|
||||
|
|
|
|||
|
|
@ -59,11 +59,14 @@ export function webpackTailwindCompilerPlugin({ shouldWatch = true }) {
|
|||
}
|
||||
|
||||
if (wasScriptCalledDirectly()) {
|
||||
build().then(() => {
|
||||
console.log('Tailwind utils built successfully')
|
||||
}).catch(e => {
|
||||
console.warn('Building Tailwind utils produced an error')
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
build()
|
||||
// eslint-disable-next-line promise/always-return
|
||||
.then(() => {
|
||||
console.log('Tailwind utils built successfully');
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('Building Tailwind utils produced an error');
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -418,8 +418,15 @@ module InternalEventsCli
|
|||
q.convert ->(input) { input.split(',').map(&:to_i).uniq }
|
||||
q.validate %r{^(\d|\s|,)*$}
|
||||
q.messages[:valid?] = "Inputs for #{formatted_prop} must be numeric"
|
||||
else
|
||||
elsif property == 'property' || property == 'label'
|
||||
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
|
||||
|
||||
|
|
@ -431,6 +438,13 @@ module InternalEventsCli
|
|||
inputs.map { |input| { property => input } }.uniq
|
||||
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
|
||||
#
|
||||
# Gets all the permutations of the provided property values.
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ await Promise.all(
|
|||
|
||||
if (errors > 0) {
|
||||
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.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
extends:
|
||||
- 'plugin:@gitlab/jest'
|
||||
settings:
|
||||
import/core-modules:
|
||||
- '@pact-foundation/pact'
|
||||
- jest-pact
|
||||
|
|
@ -219,7 +219,7 @@ RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', feature_category: :tooling do
|
|||
Dir.glob('.github/*') +
|
||||
Dir.glob('.gitlab/{issue,merge_request}_templates/**/*') +
|
||||
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('{.git,.lefthook,.ruby-lsp}/**/*') +
|
||||
Dir.glob('{file_hooks,log}/**/*') +
|
||||
|
|
|
|||
29
spec/fixtures/scripts/internal_events/events/event_with_multiple_custom_properties.yml
vendored
Normal file
29
spec/fixtures/scripts/internal_events/events/event_with_multiple_custom_properties.yml
vendored
Normal 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
|
||||
38
spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_custom_key_filter.yml
vendored
Normal file
38
spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_custom_key_filter.yml
vendored
Normal 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
|
||||
38
spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_custom_key_filter.yml
vendored
Normal file
38
spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_custom_key_filter.yml
vendored
Normal 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
|
||||
|
|
@ -366,6 +366,38 @@
|
|||
- 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
|
||||
|
||||
- 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
|
||||
inputs:
|
||||
files:
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
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;
|
||||
|
||||
describe('vue_shared/components/chronic_duration_input', () => {
|
||||
describe('admin/application_settings/runner_token_expiration/components/chronic_duration_input', () => {
|
||||
let wrapper;
|
||||
let textElement;
|
||||
let textFormInput;
|
||||
let hiddenElement;
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -15,8 +17,10 @@ describe('vue_shared/components/chronic_duration_input', () => {
|
|||
});
|
||||
|
||||
const findComponents = () => {
|
||||
textElement = wrapper.find('input[type=text]').element;
|
||||
textElement = wrapper.findComponent(GlFormInput).element;
|
||||
hiddenElement = wrapper.find('input[type=hidden]').element;
|
||||
|
||||
textFormInput = wrapper.findComponent(GlFormInput);
|
||||
};
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
|
|
@ -44,8 +48,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
|
|||
const createAndDispatch = async (initialValue, humanReadableInput) => {
|
||||
createComponent({ value: initialValue });
|
||||
await nextTick();
|
||||
textElement.value = humanReadableInput;
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
textFormInput.vm.$emit('input', humanReadableInput);
|
||||
};
|
||||
|
||||
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 () => {
|
||||
textElement.value = '1m10s';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
textFormInput.vm.$emit('input', '1m10s');
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.emitted('valid')).toEqual([
|
||||
|
|
@ -126,8 +128,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
|
|||
expect(hiddenElement.validity.customError).toBe(false);
|
||||
expect(hiddenElement.validationMessage).toBe('');
|
||||
|
||||
textElement.value = '';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
textFormInput.vm.$emit('input', '');
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.emitted('valid')).toEqual([
|
||||
|
|
@ -144,8 +145,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
|
|||
});
|
||||
|
||||
it('emits invalid with user input', async () => {
|
||||
textElement.value = 'gobbledygook';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
textFormInput.vm.$emit('input', 'gobbledygook');
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.emitted('valid')).toEqual([
|
||||
|
|
@ -203,8 +203,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
|
|||
});
|
||||
|
||||
it('emits valid when input is integer', async () => {
|
||||
textElement.value = '2hr20min';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
textFormInput.vm.$emit('input', '2hr20min');
|
||||
await nextTick();
|
||||
|
||||
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 () => {
|
||||
textElement.value = '1.5s';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
textFormInput.vm.$emit('input', '1.5s');
|
||||
await nextTick();
|
||||
|
||||
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 () => {
|
||||
textElement.value = '2hr20min';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
textFormInput.vm.$emit('input', '2hr20min');
|
||||
await nextTick();
|
||||
|
||||
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 () => {
|
||||
textElement.value = '1.5s';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
textFormInput.vm.$emit('input', '1.5s');
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.emitted('change')).toBeUndefined();
|
||||
|
|
@ -310,8 +306,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
|
|||
});
|
||||
|
||||
it('passes updated prop via v-model', async () => {
|
||||
textElement.value = '2hr20min';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
textFormInput.vm.$emit('input', '2hr20min');
|
||||
await nextTick();
|
||||
|
||||
expect(textElement.value).toBe('2hr20min');
|
||||
|
|
@ -321,8 +316,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
|
|||
|
||||
describe('change', () => {
|
||||
it('passes user input to parent via v-model', async () => {
|
||||
textElement.value = '2hr20min';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
textFormInput.vm.$emit('input', '2hr20min');
|
||||
await nextTick();
|
||||
|
||||
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 () => {
|
||||
textElement.value = '1m10s';
|
||||
textElement.dispatchEvent(new Event('input'));
|
||||
textFormInput.vm.$emit('input', '1m10s');
|
||||
await nextTick();
|
||||
|
||||
const formData = new FormData(wrapper.find('[data-testid=myForm]').element);
|
||||
|
|
@ -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`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
import { GlAlert, GlButton } from '@gitlab/ui';
|
||||
import { nextTick } from 'vue';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
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 DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.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 {
|
||||
DELETE_PACKAGE_TRACKING_ACTION,
|
||||
DELETE_PACKAGES_TRACKING_ACTION,
|
||||
|
|
@ -52,8 +51,7 @@ describe('packages_list', () => {
|
|||
const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
|
||||
const findRegistryList = () => wrapper.findComponent(RegistryList);
|
||||
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
|
||||
const findErrorPackageAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findErrorAlertButton = () => findErrorPackageAlert().findComponent(GlButton);
|
||||
const findPackageErrorsCount = () => wrapper.findComponent(PackageErrorsCount);
|
||||
const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal);
|
||||
|
||||
const showMock = jest.fn();
|
||||
|
|
@ -138,8 +136,8 @@ describe('packages_list', () => {
|
|||
expect(findDeletePackagesModal().props('showRequestForwardingContent')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not have an error alert displayed', () => {
|
||||
expect(findErrorPackageAlert().exists()).toBe(false);
|
||||
it('renders PackageErrorsCount component', () => {
|
||||
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(() => {
|
||||
mountComponent({ props: { list: [firstPackage, 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 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('renders PackageErrorsCount component with props', () => {
|
||||
expect(findPackageErrorsCount().props('errorPackages')).toStrictEqual([errorPackage]);
|
||||
});
|
||||
|
||||
it('should display alert body with message set in `statusMessage`', () => {
|
||||
mountComponent({
|
||||
props: { list: [firstPackage, { ...errorPackage, statusMessage: 'custom error message' }] },
|
||||
});
|
||||
it('and PackageErrorsCount component emits `confirm-delete`, modal component is shown', async () => {
|
||||
findPackageErrorsCount().vm.$emit('confirm-delete', [errorPackage]);
|
||||
|
||||
expect(findErrorPackageAlert().exists()).toBe(true);
|
||||
expect(findErrorPackageAlert().props('title')).toBe(
|
||||
'There was an error publishing a error package package',
|
||||
);
|
||||
expect(findErrorPackageAlert().text()).toBe('custom error message');
|
||||
});
|
||||
expect(showMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
describe('`Delete this package` button', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({ props: { list: [firstPackage, errorPackage] }, stubs: { GlAlert } });
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
it('displays the button within the alert', () => {
|
||||
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]);
|
||||
});
|
||||
expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([errorPackage]);
|
||||
});
|
||||
|
||||
describe('when `hideErrorAlert` is true', () => {
|
||||
|
|
@ -339,43 +309,7 @@ describe('packages_list', () => {
|
|||
});
|
||||
|
||||
it('does not display alert message', () => {
|
||||
expect(findErrorPackageAlert().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`,
|
||||
);
|
||||
expect(findPackageErrorsCount().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ describe('WikiForm', () => {
|
|||
const findFormat = () => wrapper.find('#wiki_format');
|
||||
const findMessage = () => wrapper.find('#wiki_message');
|
||||
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
|
||||
const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
|
||||
const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button');
|
||||
|
||||
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', () => {
|
||||
it.each`
|
||||
persisted | redirectLink
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
@ -14,6 +14,7 @@ import WorkItemRelationshipIcons from '~/work_items/components/shared/work_item_
|
|||
|
||||
import {
|
||||
workItemTask,
|
||||
workItemEpic,
|
||||
workItemObjectiveWithChild,
|
||||
confidentialWorkItemTask,
|
||||
closedWorkItemTask,
|
||||
|
|
@ -32,6 +33,8 @@ describe('WorkItemLinkChildContents', () => {
|
|||
const mockAssignees = ASSIGNEES.assignees.nodes;
|
||||
const mockLabels = LABELS.labels.nodes;
|
||||
|
||||
const mockRouterPush = jest.fn();
|
||||
|
||||
const findStatusBadgeComponent = () =>
|
||||
wrapper.findByTestId('item-status-icon').findComponent(WorkItemStateBadge);
|
||||
const findConfidentialIconComponent = () => wrapper.findByTestId('confidential-icon');
|
||||
|
|
@ -48,13 +51,23 @@ describe('WorkItemLinkChildContents', () => {
|
|||
canUpdate = true,
|
||||
childItem = workItemTask,
|
||||
showLabels = true,
|
||||
workItemFullPath = 'test-project-path',
|
||||
isGroup = false,
|
||||
} = {}) => {
|
||||
wrapper = shallowMountExtended(WorkItemLinkChildContents, {
|
||||
propsData: {
|
||||
canUpdate,
|
||||
childItem,
|
||||
showLabels,
|
||||
workItemFullPath: 'test-project-path',
|
||||
workItemFullPath,
|
||||
},
|
||||
provide: {
|
||||
isGroup,
|
||||
},
|
||||
mocks: {
|
||||
$router: {
|
||||
push: mockRouterPush,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -129,6 +142,29 @@ describe('WorkItemLinkChildContents', () => {
|
|||
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -1898,14 +1898,15 @@ export const workItemEpic = {
|
|||
namespace: {
|
||||
__typename: 'Project',
|
||||
id: '1',
|
||||
fullPath: 'test-project-path',
|
||||
fullPath: 'gitlab-org/gitlab-test',
|
||||
name: 'Project name',
|
||||
},
|
||||
createdAt: '2022-08-03T12:41:54Z',
|
||||
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: [
|
||||
workItemObjectiveMetadataWidgets.ASSIGNEES,
|
||||
workItemObjectiveMetadataWidgets.LINKED_ITEMS,
|
||||
{
|
||||
type: 'HIERARCHY',
|
||||
hasChildren: false,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -219,6 +219,11 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
|
|||
name: event_name,
|
||||
time_framed: time_framed,
|
||||
filter: { label: 'label_name', value: 16.17 }
|
||||
),
|
||||
Gitlab::Usage::EventSelectionRule.new(
|
||||
name: event_name,
|
||||
time_framed: time_framed,
|
||||
filter: { custom: 'custom_property' }
|
||||
)
|
||||
]
|
||||
end
|
||||
|
|
@ -244,6 +249,27 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
|
|||
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
|
||||
let(:time_framed) { false }
|
||||
let(:redis_arguments) { %w[SOME_LEGACY_KEY ANOTHER_LEGACY_KEY A_THIRD_LEGACY_KEY] }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -51,11 +51,12 @@ RSpec.describe Gitlab::Tracking::EventValidator, feature_category: :service_ping
|
|||
end
|
||||
end
|
||||
|
||||
context 'when a base additional property is invalid' do
|
||||
context 'when an additional property is invalid' do
|
||||
[
|
||||
{ label: 123 },
|
||||
{ value: 'test_value' },
|
||||
{ property: true }
|
||||
{ property: true },
|
||||
{ lang: [1, 2] }
|
||||
].each do |invalid_property|
|
||||
context "when #{invalid_property.each_key.first} is invalid" do
|
||||
let(:additional_properties) { invalid_property }
|
||||
|
|
|
|||
|
|
@ -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**'
|
||||
'removed user@example.com, hello@example.com and bye@example.com' |
|
||||
'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
|
||||
|
||||
with_them do
|
||||
|
|
|
|||
|
|
@ -4866,42 +4866,41 @@ RSpec.describe User, feature_category: :user_profile do
|
|||
end
|
||||
|
||||
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
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'has owned organizations' do
|
||||
let(:organization) { create(:organization) }
|
||||
|
||||
before do
|
||||
organization.add_owner(user)
|
||||
let_it_be(:solo_owned_organizations) { create_list(:organization_owner, 2, user: user).map(&:organization) }
|
||||
let_it_be(:multi_owned_organization) do
|
||||
create(:organization, organization_users: [
|
||||
create(:organization_owner, user: user),
|
||||
create(:organization_owner, user: create(:user))
|
||||
])
|
||||
end
|
||||
|
||||
context 'not solo owner' do
|
||||
let_it_be(:user2) { create(:user) }
|
||||
|
||||
before do
|
||||
organization.add_owner(user2)
|
||||
end
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
it 'returns solo-owned organizations' do
|
||||
is_expected.to match_array(solo_owned_organizations)
|
||||
end
|
||||
|
||||
context 'solo owner' do
|
||||
it { is_expected.to include(organization) }
|
||||
it 'does not return multi owned organizations' do
|
||||
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
|
||||
|
||||
context 'solo owner with other members' do
|
||||
before do
|
||||
create(:organization_user, organization: organization)
|
||||
end
|
||||
|
||||
it { is_expected.to include(organization) }
|
||||
end
|
||||
it { is_expected.to include(organization) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,20 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
|
|||
subject(:service) { described_class.new(project, user, params) }
|
||||
|
||||
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
|
||||
let(:params) do
|
||||
|
|
@ -20,6 +33,8 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
|
|||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'returning a success service response'
|
||||
|
||||
it 'creates a new package' do
|
||||
expect(package).to be_valid
|
||||
expect(package.name).to eq(params[:package_name])
|
||||
|
|
@ -30,8 +45,14 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
context 'invalid params' do
|
||||
|
|
@ -44,13 +65,13 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
|
|||
}
|
||||
end
|
||||
|
||||
it 'fails' do
|
||||
expect { package }.to raise_error(ActiveRecord::RecordInvalid, /Conan metadatum package username is invalid/)
|
||||
end
|
||||
it_behaves_like 'returning an error service response and not creating conan package',
|
||||
message: 'Validation failed: Conan metadatum package username is invalid'
|
||||
end
|
||||
|
||||
context 'with existing recipe' do
|
||||
let_it_be(:existing_package) { create(:conan_package, project: project) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
package_name: existing_package.name,
|
||||
|
|
@ -60,9 +81,8 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
|
|||
}
|
||||
end
|
||||
|
||||
it 'does not create a conan package with same recipe' do
|
||||
expect { package }.to raise_error(ActiveRecord::RecordInvalid, /Package recipe already exists/)
|
||||
end
|
||||
it_behaves_like 'returning an error service response and not creating conan package',
|
||||
message: 'Validation failed: Package recipe already exists'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -31,13 +31,6 @@ RSpec.shared_examples 'User creates wiki page' do
|
|||
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
|
||||
page.within(".wiki-form") do
|
||||
fill_in(:wiki_content, with: "[link test](test)")
|
||||
|
|
|
|||
|
|
@ -127,13 +127,6 @@ RSpec.shared_examples 'User updates wiki page' do
|
|||
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
|
||||
find('#wiki_content').native.send_keys('')
|
||||
fill_in(:wiki_content, with: ':')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
96
yarn.lock
96
yarn.lock
|
|
@ -1273,11 +1273,31 @@
|
|||
minimatch "^3.1.2"
|
||||
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":
|
||||
version "8.57.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
|
||||
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":
|
||||
version "2.1.0"
|
||||
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"
|
||||
integrity sha512-Os/PF37pCY75uLA0dmGaZe13BmirzlWH+pFLinCAPRChEC7KhHCJtIy0efRAxzkA4uatmHpJHxftuTc7NeiSNQ==
|
||||
|
||||
"@gitlab/ui@97.3.0":
|
||||
version "97.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-97.3.0.tgz#8d3a59666e4d463032dd6e18ee38f14f7785fd06"
|
||||
integrity sha512-4TqMBdHspR9+y83LEqLs+87wUSqOZIOKm9UQr9xJQszKBLdhd6TlhgAR4tsErw35d6CmSuMc4jbuGzckkoDLKA==
|
||||
"@gitlab/ui@98.4.0":
|
||||
version "98.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-98.4.0.tgz#6f00322f1138abf894ccaed27f19609e9d6ab376"
|
||||
integrity sha512-M+00vM4h4wTRr87C8vGWJzoKGKBtKmlmmdQUzATDratdWbGDqHGRn8qIu/ETjugBPcmfnkRPgipEWhrCTkCoOg==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "1.4.3"
|
||||
echarts "^5.3.2"
|
||||
|
|
@ -4146,10 +4166,10 @@ acorn@^6.4.1:
|
|||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
|
||||
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:
|
||||
version "8.12.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
|
||||
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
|
||||
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.13.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3"
|
||||
integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==
|
||||
|
||||
agent-base@6:
|
||||
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"
|
||||
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:
|
||||
version "8.57.1"
|
||||
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"
|
||||
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:
|
||||
version "9.6.1"
|
||||
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:
|
||||
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:
|
||||
version "15.9.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932"
|
||||
integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==
|
||||
|
|
@ -13562,14 +13601,6 @@ source-map-support@0.5.13:
|
|||
buffer-from "^1.0.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:
|
||||
version "0.4.1"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
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:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
|
@ -13812,6 +13852,13 @@ strip-ansi@^5.2.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "7.1.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
|
@ -15554,6 +15601,15 @@ wrap-ansi@^6.2.0:
|
|||
string-width "^4.1.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:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
|
|
|||
Loading…
Reference in New Issue