diff --git a/.eslintrc.yml b/.eslintrc.yml
index 8e979adc785..a7f498a0d7c 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -126,6 +126,11 @@ rules:
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`'
+ no-restricted-properties:
+ - error
+ - object: window
+ property: open
+ message: 'Use `visitUrl` in `jh_else_ce/lib/utils/url_utility` to avoid cross-site leaks.'
no-restricted-imports:
- error
- paths:
@@ -191,6 +196,7 @@ overrides:
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`'
+ no-restricted-properties: off
no-unsanitized/method: off
no-unsanitized/property: off
local-rules/require-valid-help-page-path: off
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1362df6a7b7..e6397c9e92a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -219,7 +219,7 @@ variables:
DOCS_REVIEW_APPS_DOMAIN: "docs.gitlab-review.app"
DOCS_GITLAB_REPO_SUFFIX: "ee"
- REVIEW_APPS_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}:gcloud-383-kubectl-1.27-helm-3.9"
+ REVIEW_APPS_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}:gcloud-383-kubectl-1.28-helm-3.9"
REVIEW_APPS_DOMAIN: "gitlab-review.app"
REVIEW_APPS_GCP_PROJECT: "gitlab-review-apps"
REVIEW_APPS_GCP_REGION: "us-central1"
diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml
index e8ec0781bc3..8bcb93b252e 100644
--- a/.rubocop_todo/rspec/context_wording.yml
+++ b/.rubocop_todo/rspec/context_wording.yml
@@ -369,7 +369,6 @@ RSpec/ContextWording:
- 'ee/spec/lib/gitlab/insights/finders/issuable_finder_spec.rb'
- 'ee/spec/lib/gitlab/insights/project_insights_config_spec.rb'
- 'ee/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb'
- - 'ee/spec/lib/gitlab/search/aggregation_parser_spec.rb'
- 'ee/spec/lib/gitlab/sitemaps/url_extractor_spec.rb'
- 'ee/spec/lib/gitlab/slash_commands/presenters/issue_show_spec.rb'
- 'ee/spec/lib/gitlab/status_page/filter/image_filter_spec.rb'
diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml
index 12d4765463c..797bfaba4a9 100644
--- a/.rubocop_todo/rspec/feature_category.yml
+++ b/.rubocop_todo/rspec/feature_category.yml
@@ -678,7 +678,6 @@ RSpec/FeatureCategory:
- 'ee/spec/lib/gitlab/reference_extractor_spec.rb'
- 'ee/spec/lib/gitlab/regex_spec.rb'
- 'ee/spec/lib/gitlab/return_to_location_spec.rb'
- - 'ee/spec/lib/gitlab/search/aggregation_parser_spec.rb'
- 'ee/spec/lib/gitlab/search/aggregation_spec.rb'
- 'ee/spec/lib/gitlab/search/client_spec.rb'
- 'ee/spec/lib/gitlab/search/recent_epics_spec.rb'
diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml
index a4d20610cc2..0a511b5b3db 100644
--- a/.rubocop_todo/rspec/named_subject.yml
+++ b/.rubocop_todo/rspec/named_subject.yml
@@ -457,7 +457,6 @@ RSpec/NamedSubject:
- 'ee/spec/lib/gitlab/patch/draw_route_spec.rb'
- 'ee/spec/lib/gitlab/proxy_spec.rb'
- 'ee/spec/lib/gitlab/reference_extractor_spec.rb'
- - 'ee/spec/lib/gitlab/search/aggregation_parser_spec.rb'
- 'ee/spec/lib/gitlab/search/aggregation_spec.rb'
- 'ee/spec/lib/gitlab/search/client_spec.rb'
- 'ee/spec/lib/gitlab/search_context/builder_spec.rb'
diff --git a/Gemfile b/Gemfile
index e1ce9541b0e..d6fcde7004b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -59,7 +59,8 @@ gem 'neighbor', '~> 0.3.2', feature_category: :duo_chat
gem 'rugged', '~> 1.6' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'faraday', '~> 1.10.3' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'faraday', '~> 2', feature_category: :shared
+gem 'faraday-retry', '~> 2', feature_category: :shared
gem 'marginalia', '~> 1.11.1' # rubocop:todo Gemfile/MissingFeatureCategory
# Authorization
@@ -116,7 +117,7 @@ gem 'attr_encrypted', '~> 3.2.4', path: 'vendor/gems/attr_encrypted' # rubocop:t
gem 'validates_hostname', '~> 1.0.13' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'rubyzip', '~> 2.3.2', require: 'zip' # rubocop:todo Gemfile/MissingFeatureCategory
# GitLab Pages letsencrypt support
-gem 'acme-client', '~> 2.0' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'acme-client', '~> 2.0.18' # rubocop:todo Gemfile/MissingFeatureCategory
# Browser detection
gem 'browser', '~> 5.3.1' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -146,7 +147,7 @@ gem 'graphql', '~> 2.3.5', feature_category: :api
gem 'graphql-docs', '~> 5.0.0', group: [:development, :test], feature_category: :api
gem 'graphiql-rails', '~> 1.10', feature_category: :api
gem 'apollo_upload_server', '~> 2.1.6', feature_category: :api
-gem 'graphlient', '~> 0.6.0', feature_category: :importers # Used by BulkImport feature (group::import)
+gem 'graphlient', '~> 0.8.0', feature_category: :importers # Used by BulkImport feature (group::import)
# Generate Fake data
gem 'ffaker', '~> 2.23' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -177,7 +178,7 @@ gem 'fog-local', '~> 0.8' # rubocop:todo Gemfile/MissingFeatureCategory
# We may want to update this dependency if this is ever addressed upstream, e.g. via
# https://github.com/aliyun/aliyun-oss-ruby-sdk/pull/93
gem 'fog-aliyun', '~> 0.4' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'gitlab-fog-azure-rm', '~> 1.9.1', require: 'fog/azurerm' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'gitlab-fog-azure-rm', '~> 2.0.1', require: 'fog/azurerm', feature_category: :shared
# for Google storage
@@ -205,14 +206,16 @@ gem 'google-cloud-compute-v1', '~> 2.6.0', feature_category: :shared
gem 'seed-fu', '~> 2.3.7' # rubocop:todo Gemfile/MissingFeatureCategory
# Search
-gem 'elasticsearch-model', '~> 7.2' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentation' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'elasticsearch-api', '7.13.3' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'elasticsearch-model', '~> 7.2', feature_category: :global_search
+gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentation', feature_category: :global_search
+gem 'elasticsearch-api', '7.17.11', feature_category: :global_search
gem 'aws-sdk-core', '~> 3.200.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-cloudformation', '~> 1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-s3', '~> 1.155.0' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'faraday_middleware-aws-sigv4', '~>0.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'typhoeus', '~> 1.4.0' # Used with Elasticsearch to support http keep-alive connections # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'faraday-typhoeus', '~> 1.1', feature_category: :global_search
+gem 'faraday_middleware-aws-sigv4', '~> 1.0.1', feature_category: :global_search
+# Used with Elasticsearch to support http keep-alive connections
+gem 'typhoeus', '~> 1.4.0', feature_category: :global_search
# Markdown and HTML processing
gem 'html-pipeline', '~> 2.14.3', feature_category: :team_planning
@@ -569,6 +572,8 @@ group :test do
end
gem 'octokit', '~> 9.0', feature_category: :importers
+# Needed by octokit: https://github.com/octokit/octokit.rb/pull/1688
+gem 'faraday-multipart', '~> 1.0', feature_category: :importers
gem 'gitlab-mail_room', '~> 0.0.24', require: 'mail_room', feature_category: :shared
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 47d1c7e0e8e..3d55ff7cb69 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -1,7 +1,7 @@
[
{"name":"CFPropertyList","version":"3.0.5","platform":"ruby","checksum":"a78551cd4768d78ebca98488c27e33652ef818be64697a54676d34e6434674a4"},
{"name":"RedCloth","version":"4.3.3","platform":"ruby","checksum":"d941b8ac96e2730d2d9326d97dda9fcf64cb73532b3f902d91c18970c5f4632d"},
-{"name":"acme-client","version":"2.0.11","platform":"ruby","checksum":"edf6da9f3c5dbe3ab0c6738eb3b97978b7a60e3500445480d2a72fcc610089de"},
+{"name":"acme-client","version":"2.0.18","platform":"ruby","checksum":"3feab341926ffc16eb65babe51ba4dad8180c13e21e774871344e0b3502ef275"},
{"name":"actioncable","version":"7.0.8.4","platform":"ruby","checksum":"7997fbc32d49c2cffbfe050540a316c96d287b9ef09fa6fe659821373dc186b0"},
{"name":"actionmailbox","version":"7.0.8.4","platform":"ruby","checksum":"3326fd92baabc3ff9d6e0931dc455769e9571604202af49b2d4cb84cf0062074"},
{"name":"actionmailer","version":"7.0.8.4","platform":"ruby","checksum":"f9f3a782b6cc9568fb1e61395f13ca4cf79afaa1fca85a1d314aef63cd3abac1"},
@@ -44,8 +44,6 @@
{"name":"axe-core-api","version":"4.9.1","platform":"ruby","checksum":"9ea7ac16bfee1cb3545345d210878aa8cccfb41b493e00fe1faab79af4d9fed8"},
{"name":"axe-core-rspec","version":"4.9.1","platform":"ruby","checksum":"31ef067bee36d6efb3f156a83aa2fb6ac721270a53fb9473f0268e325a3e6efd"},
{"name":"axiom-types","version":"0.1.1","platform":"ruby","checksum":"c1ff113f3de516fa195b2db7e0a9a95fd1b08475a502ff660d04507a09980383"},
-{"name":"azure-storage-blob","version":"2.0.3","platform":"ruby","checksum":"61b76118843c91776bd24bee22c74adafeb7c4bb3a858a325047dae3b59d0363"},
-{"name":"azure-storage-common","version":"2.0.4","platform":"ruby","checksum":"608f4daab0e06b583b73dcffd3246ea39e78056de31630286b0cf97af7d6956b"},
{"name":"babosa","version":"2.0.0","platform":"ruby","checksum":"a6218db8a4dc8fd99260dde8bc3d5fa1a0c52178196e236ebb31e41fbdcdb8a6"},
{"name":"backport","version":"1.2.0","platform":"ruby","checksum":"912c7dfdd9ee4625d013ddfccb6205c3f92da69a8990f65c440e40f5b2fc7f75"},
{"name":"base32","version":"0.3.2","platform":"ruby","checksum":"532e9b19c5dd1fce281df67fc93a803ebd5d26426a93f6dda6612769bc46fe2c"},
@@ -143,11 +141,11 @@
{"name":"ecma-re-validator","version":"0.3.0","platform":"ruby","checksum":"66a95bd8c2b0641baf1fbf9bd355a0dcf13c82c6883f6f496a722420a8b6e0d7"},
{"name":"ed25519","version":"1.3.0","platform":"java","checksum":"8e5d2f8a5325c7a463d61d1a48406ce54074c610f3dccd889e6532c9527a3894"},
{"name":"ed25519","version":"1.3.0","platform":"ruby","checksum":"514a5584f84d39daac568a17ec93a4e7261e140c52c562ed8c382c18456e627d"},
-{"name":"elasticsearch","version":"7.13.3","platform":"ruby","checksum":"58b1ad787fafd41836388176dc09e914b2f6e0b257e73b8a51a704ba6bf75b41"},
-{"name":"elasticsearch-api","version":"7.13.3","platform":"ruby","checksum":"888f35c64c896db7909f1a56f6c383c45ad6b73c3231649b9c989e39b3d2ba2e"},
-{"name":"elasticsearch-model","version":"7.2.0","platform":"ruby","checksum":"2cc1810a45619223c43eff78c6112988f12d475d201523243007dccc6ef96cc7"},
+{"name":"elasticsearch","version":"7.17.11","platform":"ruby","checksum":"ed080f085d939f21d07f424ebcea95326e4bdb5f770a8f33aac699374f2ffc86"},
+{"name":"elasticsearch-api","version":"7.17.11","platform":"ruby","checksum":"fed8f7b64493c97cf3984a33396a798204b54b8e1b01c5b6c099fa3fd4209107"},
+{"name":"elasticsearch-model","version":"7.2.1","platform":"ruby","checksum":"8b5c4b57664bb29f4854fa39603b5ccecfbf9b22fee87bcd16917321dae6a20b"},
{"name":"elasticsearch-rails","version":"7.2.1","platform":"ruby","checksum":"0750dc0e956358d9a3a0912a8186c266ef19f8de0b178c61996ed1a6998156e4"},
-{"name":"elasticsearch-transport","version":"7.13.3","platform":"ruby","checksum":"ab8d0226652fb5b32923f172c1abfbc7464058b7de2d9dde3215c88d518c8e2e"},
+{"name":"elasticsearch-transport","version":"7.17.11","platform":"ruby","checksum":"d18057d5295e4c39fe80084ede9e00e9c0e0d74580348985f8677b2fb7f70f03"},
{"name":"email_reply_trimmer","version":"0.1.6","platform":"ruby","checksum":"9fede222ce660993e4e2e3dad282535ceb7914e246eb8302c19aa9e021f7326e"},
{"name":"email_spec","version":"2.2.0","platform":"ruby","checksum":"60b7980580a835e7f676db60667f17a2d60e8e0e39c26d81cfc231805c544d79"},
{"name":"encryptor","version":"3.0.0","platform":"ruby","checksum":"abf23f94ab4d864b8cea85b43f3432044a60001982cda7c33c1cd90da8db1969"},
@@ -162,20 +160,15 @@
{"name":"extended-markdown-filter","version":"0.7.0","platform":"ruby","checksum":"c8eeef7409fbae18c6b407cd3e4eeb5d25c35cb08fe1ac06f375df3db2d4f138"},
{"name":"factory_bot","version":"6.4.5","platform":"ruby","checksum":"d71dd29bc95f0ec2bf27e3dd9b1b4d557bd534caca744663cb7db4bacf3198be"},
{"name":"factory_bot_rails","version":"6.4.3","platform":"ruby","checksum":"ea73ceac1c0ff3dc11fff390bf2ea8a2604066525ed8ecd3b3bc2c267226dcc8"},
-{"name":"faraday","version":"1.10.3","platform":"ruby","checksum":"20f52e9f73231e5f3d43fb645901573ce2b75f0bd01ea52a2772133d0106e6b0"},
-{"name":"faraday-em_http","version":"1.0.0","platform":"ruby","checksum":"7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689"},
-{"name":"faraday-em_synchrony","version":"1.0.0","platform":"ruby","checksum":"460dad1c30cc692d6e77d4c391ccadb4eca4854b315632cd7e560f74275cf9ed"},
-{"name":"faraday-excon","version":"1.1.0","platform":"ruby","checksum":"b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940"},
+{"name":"faraday","version":"2.10.0","platform":"ruby","checksum":"1a3e6c02acc511fc334d799521f1013e449bde38aa2dceb3af71e8030519bda9"},
+{"name":"faraday-follow_redirects","version":"0.3.0","platform":"ruby","checksum":"d92d975635e2c7fe525dd494fcd4b9bb7f0a4a0ec0d5f4c15c729530fdb807f9"},
{"name":"faraday-http-cache","version":"2.5.0","platform":"ruby","checksum":"64b7366d66e508e1c3dd855ebb20ce9da429330e412a23d9ebbc0a7a7b227463"},
-{"name":"faraday-httpclient","version":"1.0.1","platform":"ruby","checksum":"4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b"},
{"name":"faraday-multipart","version":"1.0.4","platform":"ruby","checksum":"9012021ab57790f7d712f590b48d5f948b19b43cfa11ca83e6459f06090b0725"},
-{"name":"faraday-net_http","version":"1.0.1","platform":"ruby","checksum":"3245ce406ebb77b40e17a77bfa66191dda04be2fd4e13a78d8a4305854d328ba"},
-{"name":"faraday-net_http_persistent","version":"1.2.0","platform":"ruby","checksum":"0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335"},
-{"name":"faraday-patron","version":"1.0.0","platform":"ruby","checksum":"dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7"},
-{"name":"faraday-rack","version":"1.0.0","platform":"ruby","checksum":"ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0"},
-{"name":"faraday-retry","version":"1.0.3","platform":"ruby","checksum":"add154f4f399243cbe070806ed41b96906942e7f5259bb1fe6daf2ec8f497194"},
-{"name":"faraday_middleware","version":"1.2.0","platform":"ruby","checksum":"ded15d574d50e92bd04448d5566913af5cb1a01b2fa311ceecc2464fa0ab88af"},
-{"name":"faraday_middleware-aws-sigv4","version":"0.3.0","platform":"ruby","checksum":"744654bd5b15539a54aed39b806e2dfb45aa47708fa1e6f6766fedcda6c262be"},
+{"name":"faraday-net_http","version":"3.1.0","platform":"ruby","checksum":"1627be414960d0131691190ff524506ba6607402a50fb6eccda9e64ca60f859f"},
+{"name":"faraday-net_http_persistent","version":"2.1.0","platform":"ruby","checksum":"b41720b13f56dae77114d9de54baef2d76d0b06ab40d695b2a98e254b56ade0b"},
+{"name":"faraday-retry","version":"2.2.1","platform":"ruby","checksum":"4146fed14549c0580bf14591fca419a40717de0dd24f267a8ec2d9a728677608"},
+{"name":"faraday-typhoeus","version":"1.1.0","platform":"ruby","checksum":"24c6147c213818dde3ebc50ae47ab92f9a7e554903aa362707126f749c6890e7"},
+{"name":"faraday_middleware-aws-sigv4","version":"1.0.1","platform":"ruby","checksum":"a001ea4f687ca1c60bad8f2a627196905ce3dbf285e461dc153240e92eaabe8f"},
{"name":"fast_blank","version":"1.0.1","platform":"java","checksum":"90d82106b0e4aa19ac24ba1604c79a0c5a4c471601e800c9b2b072938a6d9a92"},
{"name":"fast_blank","version":"1.0.1","platform":"ruby","checksum":"269fc30414fed4e6403bc4a49081e1ea539f8b9226e59276ed1efaefabaa17ea"},
{"name":"fast_gettext","version":"2.3.0","platform":"ruby","checksum":"0253e26423ccab68061c42387827e3b99243a1b15ad614df1c800ba870d64f84"},
@@ -219,7 +212,7 @@
{"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
{"name":"gitlab-dangerfiles","version":"4.8.0","platform":"ruby","checksum":"b327d079552ec974a63bf34d749a0308425af6ebf51d01064f1a6ff216a523db"},
{"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},
-{"name":"gitlab-fog-azure-rm","version":"1.9.1","platform":"ruby","checksum":"026b8e188ac4183c1bf1b1909b0489da0ffad453996a6e744e0eba67dc284f37"},
+{"name":"gitlab-fog-azure-rm","version":"2.0.1","platform":"ruby","checksum":"61cc049fa98cc61bc735c14ea01c1e4179e8439ad84ba496c8b5939810aa7925"},
{"name":"gitlab-glfm-markdown","version":"0.0.17","platform":"aarch64-linux","checksum":"81ccfd91c7a1da4b165e700f1a6fbb15cf20ffd283ec8c6e05d5e2078a569717"},
{"name":"gitlab-glfm-markdown","version":"0.0.17","platform":"arm64-darwin","checksum":"2f9da51bb0e57ca431fe957e384c385c4380127a9a22ff3cbf7e0c67efb35897"},
{"name":"gitlab-glfm-markdown","version":"0.0.17","platform":"ruby","checksum":"f379545fc53a71c31525025fdb422f46081133af5cced3130ce680b155c2aa69"},
@@ -282,7 +275,7 @@
{"name":"grape-swagger-entity","version":"0.5.4","platform":"ruby","checksum":"34c1644de6523c64cee922988bad3d1057634224f26dd48b9b5c1f90709bb571"},
{"name":"grape_logging","version":"1.8.4","platform":"ruby","checksum":"efcc3e322dbd5d620a68f078733b7db043cf12680144cd03c982f14115c792d1"},
{"name":"graphiql-rails","version":"1.10.0","platform":"ruby","checksum":"b557f989a737c8b9e985142609bec52fb1e9393a701eb50e02a7c14422891040"},
-{"name":"graphlient","version":"0.6.0","platform":"ruby","checksum":"b8d8664b4c8ec215012cbe3cca918a045b0a206d709712d68b6db51fd215c5c0"},
+{"name":"graphlient","version":"0.8.0","platform":"ruby","checksum":"98c408da1d083454e9f5e274f3b0b6261e2a0c2b5f2ed7b3ef9441d46f8e7cb1"},
{"name":"graphlyte","version":"1.0.0","platform":"ruby","checksum":"b5af4ab67dde6e961f00ea1c18f159f73b52ed11395bb4ece297fe628fa1804d"},
{"name":"graphql","version":"2.3.5","platform":"ruby","checksum":"9c367835f86541660d24c3d81632267ecee553d304577aaee070f8ac05860af1"},
{"name":"graphql-client","version":"0.23.0","platform":"ruby","checksum":"f238b8e451676baad06bd15f95396e018192243dcf12c4e6d13fb41d9a2babc1"},
@@ -607,7 +600,7 @@
{"name":"rubocop-rspec","version":"2.27.1","platform":"ruby","checksum":"2f27ce04700be75db65afe83d7993a36e0fafd07ec062222f4b3cc10137a7a9e"},
{"name":"ruby-fogbugz","version":"0.3.0","platform":"ruby","checksum":"5e04cde474648f498a71cf1e1a7ab42c66b953862fbe224f793ec0a7a1d5f657"},
{"name":"ruby-lsp","version":"0.17.4","platform":"ruby","checksum":"49ea1d6a49f5cfb79296fcb96c1988768737c2325270f6dce5aec64a1840e213"},
-{"name":"ruby-lsp-rails","version":"0.3.7","platform":"ruby","checksum":"392c831644a72a058f02d15ec48ff80e7e6dde14049d70d791ce798b3cdc9bf0"},
+{"name":"ruby-lsp-rails","version":"0.3.8","platform":"ruby","checksum":"847d3ac0a131a794831ceb3ad780ab3e00ddb4e91ab8e965017c817a6068a975"},
{"name":"ruby-lsp-rspec","version":"0.1.12","platform":"ruby","checksum":"34fe775e27dc4c2f31df901f3d44ee885ed0806b05ba9be0ea564682dd4811e5"},
{"name":"ruby-magic","version":"0.6.0","platform":"ruby","checksum":"7b2138877b7d23aff812c95564eba6473b74b815ef85beb0eb792e729a2b6101"},
{"name":"ruby-openai","version":"3.7.0","platform":"ruby","checksum":"fb735d4c055e282ade264cab9864944c05a8a10e0cddd45a0551e8a9851b1850"},
@@ -653,7 +646,7 @@
{"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.21.1","platform":"ruby","checksum":"c30b64014532fc5156c60797985f839f36adbe60ff4653e7112b008dc1c83263"},
+{"name":"selenium-webdriver","version":"4.23.0","platform":"ruby","checksum":"490aeddee879cfea58a4db6628338d60a905bc56cd5e1a60dfbaa9090a19b801"},
{"name":"semver_dialects","version":"3.4.0","platform":"ruby","checksum":"9625fd343cd47335961ccd71249ad905b1a7c03c514a031b35540bc2946eab59"},
{"name":"sentry-rails","version":"5.17.3","platform":"ruby","checksum":"017771c42d739c0ad2213a581ca9d005cf543227bc13662cd1ca9909f2429459"},
{"name":"sentry-ruby","version":"5.17.3","platform":"ruby","checksum":"61791a4b0bb0f95cd87aceeaa1efa6d4ab34d64236c9d5df820478adfe2fbbfc"},
@@ -706,7 +699,7 @@
{"name":"term-ansicolor","version":"1.7.1","platform":"ruby","checksum":"92339ffec77c4bddc786a29385c91601dd52fc68feda23609bba0491229b05f7"},
{"name":"terminal-table","version":"3.0.2","platform":"ruby","checksum":"f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91"},
{"name":"terser","version":"1.0.2","platform":"ruby","checksum":"80c2e0bc7e2db4e12e8529658f9e0820e13d685ae67d745bf981f269743bb28e"},
-{"name":"test-prof","version":"1.3.3","platform":"ruby","checksum":"0f03ee4f84c22a8cf6caaa8f8f93987a15b31b789b596d0cf8cf8a8a8fc19667"},
+{"name":"test-prof","version":"1.3.3.1","platform":"ruby","checksum":"0eb5dfdd6c2f70a8f1402683793e60b3ff63582a31690dfd8ef0aefd2c99b153"},
{"name":"test_file_finder","version":"0.3.1","platform":"ruby","checksum":"83fb0588a06b2784b51892910b9bfd06609f8d31f2d851a98d976f644d177199"},
{"name":"text","version":"1.3.1","platform":"ruby","checksum":"2fbbbc82c1ce79c4195b13018a87cbb00d762bda39241bb3cdc32792759dd3f4"},
{"name":"thor","version":"1.3.1","platform":"ruby","checksum":"fa7e3471d4f6a27138e3d9c9b0d4daac9c3d7383927667ae83e9ab42ae7401ef"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 0e77381d5ad..12f9c12e397 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -201,9 +201,9 @@ GEM
CFPropertyList (3.0.5)
rexml
RedCloth (4.3.3)
- acme-client (2.0.11)
+ acme-client (2.0.18)
faraday (>= 1.0, < 3.0.0)
- faraday-retry (~> 1.0)
+ faraday-retry (>= 1.0, < 3.0.0)
actioncable (7.0.8.4)
actionpack (= 7.0.8.4)
activesupport (= 7.0.8.4)
@@ -339,14 +339,6 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
- azure-storage-blob (2.0.3)
- azure-storage-common (~> 2.0)
- nokogiri (~> 1, >= 1.10.8)
- azure-storage-common (2.0.4)
- faraday (~> 1.0)
- faraday_middleware (~> 1.0, >= 1.0.0.rc1)
- net-http-persistent (~> 4.0)
- nokogiri (~> 1, >= 1.10.8)
babosa (2.0.0)
backport (1.2.0)
base32 (0.3.2)
@@ -543,18 +535,19 @@ GEM
ecma-re-validator (0.3.0)
regexp_parser (~> 2.0)
ed25519 (1.3.0)
- elasticsearch (7.13.3)
- elasticsearch-api (= 7.13.3)
- elasticsearch-transport (= 7.13.3)
- elasticsearch-api (7.13.3)
+ elasticsearch (7.17.11)
+ elasticsearch-api (= 7.17.11)
+ elasticsearch-transport (= 7.17.11)
+ elasticsearch-api (7.17.11)
multi_json
- elasticsearch-model (7.2.0)
+ elasticsearch-model (7.2.1)
activesupport (> 3)
elasticsearch (~> 7)
hashie
elasticsearch-rails (7.2.1)
- elasticsearch-transport (7.13.3)
- faraday (~> 1)
+ elasticsearch-transport (7.17.11)
+ base64
+ faraday (>= 1, < 3)
multi_json
email_reply_trimmer (0.1.6)
email_spec (2.2.0)
@@ -580,36 +573,28 @@ GEM
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
- faraday (1.10.3)
- faraday-em_http (~> 1.0)
- faraday-em_synchrony (~> 1.0)
- faraday-excon (~> 1.1)
- faraday-httpclient (~> 1.0)
- faraday-multipart (~> 1.0)
- faraday-net_http (~> 1.0)
- faraday-net_http_persistent (~> 1.0)
- faraday-patron (~> 1.0)
- faraday-rack (~> 1.0)
- faraday-retry (~> 1.0)
- ruby2_keywords (>= 0.0.4)
- faraday-em_http (1.0.0)
- faraday-em_synchrony (1.0.0)
- faraday-excon (1.1.0)
+ faraday (2.10.0)
+ faraday-net_http (>= 2.0, < 3.2)
+ logger
+ faraday-follow_redirects (0.3.0)
+ faraday (>= 1, < 3)
faraday-http-cache (2.5.0)
faraday (>= 0.8)
- faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
- faraday-net_http (1.0.1)
- faraday-net_http_persistent (1.2.0)
- faraday-patron (1.0.0)
- faraday-rack (1.0.0)
- faraday-retry (1.0.3)
- faraday_middleware (1.2.0)
- faraday (~> 1.0)
- faraday_middleware-aws-sigv4 (0.3.0)
+ faraday-net_http (3.1.0)
+ net-http
+ faraday-net_http_persistent (2.1.0)
+ faraday (~> 2.5)
+ net-http-persistent (~> 4.0)
+ faraday-retry (2.2.1)
+ faraday (~> 2.0)
+ faraday-typhoeus (1.1.0)
+ faraday (~> 2.0)
+ typhoeus (~> 1.4)
+ faraday_middleware-aws-sigv4 (1.0.1)
aws-sigv4 (~> 1.0)
- faraday (>= 0.15)
+ faraday (>= 2.0, < 3)
fast_blank (1.0.1)
fast_gettext (2.3.0)
ffaker (2.23.0)
@@ -717,12 +702,15 @@ GEM
gitlab-experiment (0.9.1)
activesupport (>= 3.0)
request_store (>= 1.0)
- gitlab-fog-azure-rm (1.9.1)
- azure-storage-blob (~> 2.0)
- azure-storage-common (~> 2.0)
+ gitlab-fog-azure-rm (2.0.1)
+ faraday (~> 2.0)
+ faraday-follow_redirects (~> 0.3.0)
+ faraday-net_http_persistent (~> 2.0)
fog-core (~> 2.1)
fog-json (~> 1.2)
mime-types
+ net-http-persistent (~> 4.0)
+ nokogiri (~> 1, >= 1.10.8)
gitlab-glfm-markdown (0.0.17)
rb_sys (= 0.9.94)
gitlab-labkit (0.36.1)
@@ -889,9 +877,8 @@ GEM
rack
graphiql-rails (1.10.0)
railties
- graphlient (0.6.0)
- faraday (>= 1.0)
- faraday_middleware
+ graphlient (0.8.0)
+ faraday (~> 2.0)
graphql-client
graphlyte (1.0.0)
graphql (2.3.5)
@@ -1634,8 +1621,8 @@ GEM
prism (>= 0.29.0, < 0.31)
rbs (>= 3, < 4)
sorbet-runtime (>= 0.5.10782)
- ruby-lsp-rails (0.3.7)
- ruby-lsp (>= 0.17.0, < 0.18.0)
+ ruby-lsp-rails (0.3.8)
+ ruby-lsp (>= 0.17.2, < 0.18.0)
ruby-lsp-rspec (0.1.12)
ruby-lsp (~> 0.17.0)
ruby-magic (0.6.0)
@@ -1671,8 +1658,9 @@ GEM
seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
- selenium-webdriver (4.21.1)
+ selenium-webdriver (4.23.0)
base64 (~> 0.2)
+ logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
@@ -1790,7 +1778,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
terser (1.0.2)
execjs (>= 0.3.0, < 3)
- test-prof (1.3.3)
+ test-prof (1.3.3.1)
test_file_finder (0.3.1)
faraday (>= 1.0, < 3.0, != 2.0.0)
text (1.3.1)
@@ -1936,7 +1924,7 @@ PLATFORMS
DEPENDENCIES
CFPropertyList (~> 3.0.0)
RedCloth (~> 4.3.3)
- acme-client (~> 2.0)
+ acme-client (~> 2.0.18)
activerecord-explain-analyze (~> 0.1)
activerecord-gitlab!
acts-as-taggable-on (~> 10.0)
@@ -2003,15 +1991,18 @@ DEPENDENCIES
doorkeeper-openid_connect (~> 1.8, >= 1.8.7)
duo_api (~> 1.3)
ed25519 (~> 1.3.0)
- elasticsearch-api (= 7.13.3)
+ elasticsearch-api (= 7.17.11)
elasticsearch-model (~> 7.2)
elasticsearch-rails (~> 7.2)
email_reply_trimmer (~> 0.1)
email_spec (~> 2.2.0)
error_tracking_open_api!
factory_bot_rails (~> 6.4.3)
- faraday (~> 1.10.3)
- faraday_middleware-aws-sigv4 (~> 0.3.0)
+ faraday (~> 2)
+ faraday-multipart (~> 1.0)
+ faraday-retry (~> 2)
+ faraday-typhoeus (~> 1.1)
+ faraday_middleware-aws-sigv4 (~> 1.0.1)
fast_blank (~> 1.0.1)
ffaker (~> 2.23)
flipper (~> 0.26.2)
@@ -2032,7 +2023,7 @@ DEPENDENCIES
gitlab-chronic (~> 0.10.5)
gitlab-dangerfiles (~> 4.8.0)
gitlab-experiment (~> 0.9.1)
- gitlab-fog-azure-rm (~> 1.9.1)
+ gitlab-fog-azure-rm (~> 2.0.1)
gitlab-glfm-markdown (~> 0.0.17)
gitlab-housekeeper!
gitlab-http!
@@ -2078,7 +2069,7 @@ DEPENDENCIES
grape-swagger-entity (~> 0.5.1)
grape_logging (~> 1.8, >= 1.8.4)
graphiql-rails (~> 1.10)
- graphlient (~> 0.6.0)
+ graphlient (~> 0.8.0)
graphlyte (~> 1.0.0)
graphql (~> 2.3.5)
graphql-docs (~> 5.0.0)
diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue
index 401263c3b46..c2449aa9631 100644
--- a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue
+++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue
@@ -1,20 +1,26 @@
@@ -154,6 +202,7 @@ export default {
>
@@ -178,7 +228,26 @@ export default {
@cluster-error="handleError"
@loading="podsLoading = $event"
@update-failed-state="handleFailedState"
+ @show-resource-details="openDetailsDrawer"
+ @remove-selection="closeDetailsDrawer"
/>
+
+
+
+
+ {{ selectedItem.name }}
+
+
+
+
+
+
{{ syncStatusBadge.text }}
diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_tabs.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_tabs.vue
index 1883bc965b1..3acdb8747b2 100644
--- a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_tabs.vue
+++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_tabs.vue
@@ -1,10 +1,7 @@
@@ -94,33 +74,17 @@ export default {
@loading="$emit('loading', $event)"
@update-failed-state="$emit('update-failed-state', $event)"
@cluster-error="$emit('cluster-error', $event)"
- @show-resource-details="showResourceDetails"
- @remove-selection="closeDetailsDrawer"
+ @show-resource-details="$emit('show-resource-details', $event)"
+ @remove-selection="$emit('remove-selection')"
/>
-
-
-
-
- {{ selectedItem.name }}
-
-
-
-
-
-
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
index 112d3bd0b51..e3bcf9f8254 100644
--- a/app/assets/javascripts/environments/graphql/client.js
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -12,7 +12,7 @@ import k8sServicesQuery from './queries/k8s_services.query.graphql';
import k8sDeploymentsQuery from './queries/k8s_deployments.query.graphql';
import k8sNamespacesQuery from './queries/k8s_namespaces.query.graphql';
import fluxKustomizationQuery from './queries/flux_kustomization.query.graphql';
-import fluxHelmReleaseStatusQuery from './queries/flux_helm_release_status.query.graphql';
+import fluxHelmReleaseQuery from './queries/flux_helm_release.query.graphql';
import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql';
import { connectionStatus } from './resolvers/kubernetes/constants';
@@ -132,10 +132,8 @@ export const apolloProvider = (endpoint) => {
cache.writeQuery({
query: fluxKustomizationQuery,
data: {
+ ...k8sData,
kind: '',
- metadata: {
- name: '',
- },
conditions: {
message: '',
reason: '',
@@ -146,8 +144,10 @@ export const apolloProvider = (endpoint) => {
},
});
cache.writeQuery({
- query: fluxHelmReleaseStatusQuery,
+ query: fluxHelmReleaseQuery,
data: {
+ ...k8sData,
+ kind: '',
conditions: {
message: '',
reason: '',
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_helm_release.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_helm_release.query.graphql
new file mode 100644
index 00000000000..86c03096c68
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/flux_helm_release.query.graphql
@@ -0,0 +1,14 @@
+#import "~/kubernetes_dashboard/graphql/queries/workload_item.fragment.graphql"
+
+query getFluxHelmReleaseQuery($configuration: LocalConfiguration, $fluxResourcePath: String) {
+ fluxHelmRelease(configuration: $configuration, fluxResourcePath: $fluxResourcePath) @client {
+ ...WorkloadItem
+ kind
+ conditions {
+ message
+ reason
+ status
+ type
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
deleted file mode 100644
index 6cac95a7536..00000000000
--- a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
+++ /dev/null
@@ -1,11 +0,0 @@
-query getFluxHelmReleaseStatusQuery($configuration: LocalConfiguration, $fluxResourcePath: String) {
- fluxHelmReleaseStatus(configuration: $configuration, fluxResourcePath: $fluxResourcePath)
- @client {
- conditions {
- message
- reason
- status
- type
- }
- }
-}
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_kustomization.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_kustomization.query.graphql
index 882da0fb11f..5593245fa42 100644
--- a/app/assets/javascripts/environments/graphql/queries/flux_kustomization.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/flux_kustomization.query.graphql
@@ -1,9 +1,9 @@
+#import "~/kubernetes_dashboard/graphql/queries/workload_item.fragment.graphql"
+
query getFluxKustomizationQuery($configuration: LocalConfiguration, $fluxResourcePath: String) {
fluxKustomization(configuration: $configuration, fluxResourcePath: $fluxResourcePath) @client {
+ ...WorkloadItem
kind
- metadata {
- name
- }
conditions {
message
reason
diff --git a/app/assets/javascripts/environments/graphql/resolvers/flux.js b/app/assets/javascripts/environments/graphql/resolvers/flux.js
index 789bb9d127d..8dedb275ac3 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/flux.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/flux.js
@@ -7,12 +7,12 @@ import {
import { updateConnectionStatus } from '~/environments/graphql/resolvers/kubernetes/k8s_connection_status';
import { connectionStatus } from '~/environments/graphql/resolvers/kubernetes/constants';
import fluxKustomizationQuery from '../queries/flux_kustomization.query.graphql';
-import fluxHelmReleaseStatusQuery from '../queries/flux_helm_release_status.query.graphql';
+import fluxHelmReleaseQuery from '../queries/flux_helm_release.query.graphql';
const helmReleasesApiVersion = 'helm.toolkit.fluxcd.io/v2beta1';
const kustomizationsApiVersion = 'kustomize.toolkit.fluxcd.io/v1';
-const helmReleaseField = 'fluxHelmReleaseStatus';
+const helmReleaseField = 'fluxHelmRelease';
const kustomizationField = 'fluxKustomization';
const handleClusterError = (err) => {
@@ -29,19 +29,25 @@ export const buildFluxResourceWatchPath = ({ namespace, apiVersion, resourceType
};
const mapFluxItems = (fluxItem, resourceType) => {
- if (resourceType === KUSTOMIZATIONS_RESOURCE_TYPE) {
- return {
- kind: fluxItem?.kind || '',
- metadata: {
- name: fluxItem?.metadata?.name || '',
- },
- conditions: fluxItem?.status?.conditions || [],
- inventory: fluxItem?.status?.inventory?.entries || [],
- };
- }
- return {
- conditions: fluxItem?.status?.conditions || [],
+ const metadata = {
+ ...fluxItem.metadata,
+ annotations: fluxItem.metadata?.annotations || {},
+ labels: fluxItem.metadata?.labels || {},
};
+
+ const result = {
+ kind: fluxItem?.kind || '',
+ status: fluxItem.status || {},
+ spec: fluxItem.spec || {},
+ metadata,
+ conditions: fluxItem.status?.conditions || [],
+ __typename: 'LocalWorkloadItem',
+ };
+
+ if (resourceType === KUSTOMIZATIONS_RESOURCE_TYPE) {
+ result.inventory = fluxItem.status?.inventory?.entries || [];
+ }
+ return result;
};
const watchFluxResource = ({
@@ -146,7 +152,7 @@ export const watchFluxKustomization = ({ configuration, client, fluxResourcePath
};
export const watchFluxHelmRelease = ({ configuration, client, fluxResourcePath }) => {
- const query = fluxHelmReleaseStatusQuery;
+ const query = fluxHelmReleaseQuery;
const variables = { configuration, fluxResourcePath };
const field = helmReleaseField;
const resourceType = HELM_RELEASES_RESOURCE_TYPE;
@@ -189,9 +195,9 @@ export default {
client,
});
},
- fluxHelmReleaseStatus(_, { configuration, fluxResourcePath }, { client }) {
+ fluxHelmRelease(_, { configuration, fluxResourcePath }, { client }) {
return getFluxResource({
- query: fluxHelmReleaseStatusQuery,
+ query: fluxHelmReleaseQuery,
variables: { configuration, fluxResourcePath },
field: helmReleaseField,
resourceType: HELM_RELEASES_RESOURCE_TYPE,
diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql
index e2e6b7a678c..b07edbea7ba 100644
--- a/app/assets/javascripts/environments/graphql/typedefs.graphql
+++ b/app/assets/javascripts/environments/graphql/typedefs.graphql
@@ -70,10 +70,6 @@ type LocalK8sNamespaces {
metadata: k8sNamespaceMetadata
}
-type LocalFluxResourceMetadata {
- name: String
-}
-
type LocalFluxResourceConditions {
message: String
reason: String
@@ -87,11 +83,17 @@ type LocalFluxResourceInventoryItem {
type LocalFluxKustomization {
kind: String
- metadata: LocalFluxResourceMetadata
+ metadata: LocalWorkloadMetadata
+ status: JSON
+ spec: JSON
conditions: [LocalFluxResourceConditions]
inventory: [LocalFluxResourceInventoryItem]
}
-type LocalFluxHelmReleaseStatus {
+type LocalFluxHelmRelease {
+ kind: String
+ metadata: LocalWorkloadMetadata
+ status: JSON
+ spec: JSON
conditions: [LocalFluxResourceConditions]
}
@@ -132,10 +134,7 @@ extend type Query {
configuration: LocalConfiguration
fluxResourcePath: String
): LocalFluxKustomization
- fluxHelmReleaseStatus(
- configuration: LocalConfiguration
- fluxResourcePath: String
- ): LocalFluxHelmReleaseStatus
+ fluxHelmRelease(configuration: LocalConfiguration, fluxResourcePath: String): LocalFluxHelmRelease
k8sDeployments(configuration: LocalConfiguration, namespace: String): [LocalK8sDeployment]
k8sLogs(configuration: LocalConfiguration, namespace: String, podName: String): [K8sLogsData]
}
diff --git a/app/assets/javascripts/issuable/components/hidden_badge.vue b/app/assets/javascripts/issuable/components/hidden_badge.vue
index 7b0b6265bc7..67070f33251 100644
--- a/app/assets/javascripts/issuable/components/hidden_badge.vue
+++ b/app/assets/javascripts/issuable/components/hidden_badge.vue
@@ -1,12 +1,11 @@
-
-
- {{ __('Hidden') }}
-
+
diff --git a/app/assets/javascripts/issuable/components/locked_badge.vue b/app/assets/javascripts/issuable/components/locked_badge.vue
index 03d5cc95a38..e35faf2fdcb 100644
--- a/app/assets/javascripts/issuable/components/locked_badge.vue
+++ b/app/assets/javascripts/issuable/components/locked_badge.vue
@@ -1,12 +1,11 @@
@@ -86,21 +87,21 @@ export default {
-
+
{{
- item.status
+ $options.STATUS_LABELS[item.status]
}}
{{ $options.i18n.status }}
{{
- item.status
+ $options.STATUS_LABELS[item.status]
}}
{{ statusYaml }}
diff --git a/app/assets/javascripts/kubernetes_dashboard/constants.js b/app/assets/javascripts/kubernetes_dashboard/constants.js
index 1eee081a204..6137da26ef6 100644
--- a/app/assets/javascripts/kubernetes_dashboard/constants.js
+++ b/app/assets/javascripts/kubernetes_dashboard/constants.js
@@ -7,6 +7,11 @@ export const STATUS_FAILED = 'Failed';
export const STATUS_READY = 'Ready';
export const STATUS_COMPLETED = 'Completed';
export const STATUS_SUSPENDED = 'Suspended';
+export const STATUS_RECONCILED = 'reconciled';
+export const STATUS_RECONCILING = 'reconciling';
+export const STATUS_STALLED = 'stalled';
+export const STATUS_UNKNOWN = 'unknown';
+export const STATUS_UNAVAILABLE = 'unavailable';
export const STATUS_LABELS = {
[STATUS_RUNNING]: s__('KubernetesDashboard|Running'),
@@ -16,6 +21,12 @@ export const STATUS_LABELS = {
[STATUS_READY]: s__('KubernetesDashboard|Ready'),
[STATUS_COMPLETED]: s__('KubernetesDashboard|Completed'),
[STATUS_SUSPENDED]: s__('KubernetesDashboard|Suspended'),
+ [STATUS_RECONCILED]: s__('Environment|Reconciled'),
+ [STATUS_RECONCILING]: s__('Environment|Reconciling'),
+ [STATUS_STALLED]: s__('Environment|Stalled'),
+ [STATUS_UNKNOWN]: s__('Environment|Unknown'),
+ [STATUS_UNAVAILABLE]: s__('Environment|Unavailable'),
+ failed: s__('KubernetesDashboard|Failed'),
};
export const WORKLOAD_STATUS_BADGE_VARIANTS = {
@@ -26,6 +37,12 @@ export const WORKLOAD_STATUS_BADGE_VARIANTS = {
[STATUS_READY]: 'success',
[STATUS_COMPLETED]: 'success',
[STATUS_SUSPENDED]: 'neutral',
+ [STATUS_RECONCILED]: 'success',
+ [STATUS_RECONCILING]: 'info',
+ [STATUS_STALLED]: 'warning',
+ [STATUS_UNKNOWN]: 'neutral',
+ [STATUS_UNAVAILABLE]: 'neutral',
+ failed: 'danger',
};
export const PAGE_SIZE = 20;
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index faa5a89b483..c4bbc4a728e 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -743,8 +743,10 @@ export function visitUrl(destination, openWindow = false) {
if (isExternal(url)) {
const target = openWindow ? '_blank' : '_self';
// Sets window.opener to null and avoids leaking referrer information.
+ // eslint-disable-next-line no-restricted-properties
window.open(url, target, 'noreferrer');
} else if (openWindow) {
+ // eslint-disable-next-line no-restricted-properties
window.open(url);
} else {
window.location.assign(url);
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index abc72018c82..a28f34d74c7 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -5,7 +5,7 @@ import { createAlert } from '~/alert';
import dateFormat from '~/lib/dateformat';
import axios from '~/lib/utils/axios_utils';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
-import { formatDate } from '~/lib/utils/datetime/date_format_utility';
+import { localeDateFormat } from '~/lib/utils/datetime/locale_dateformat';
import { n__, s__, __ } from '~/locale';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
@@ -289,7 +289,7 @@ export default class ActivityCalendar {
.querySelector(this.activitiesContainer)
.querySelectorAll('.js-localtime')
.forEach((el) => {
- el.setAttribute('title', formatDate(el.dataset.datetime));
+ el.setAttribute('title', localeDateFormat.asDateTimeFull.format(el.dataset.datetime));
});
})
.catch(() =>
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
index 71dc8c3d020..9a3f951791d 100644
--- a/app/assets/javascripts/persistent_user_callout.js
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -2,6 +2,7 @@ import { createAlert } from '~/alert';
import axios from './lib/utils/axios_utils';
import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
+import { visitUrl } from './lib/utils/url_utility';
const DEFERRED_LINK_CLASS = 'deferred-link';
@@ -69,7 +70,7 @@ export default class PersistentUserCallout {
if (deferredLinkOptions) {
const { href, target } = deferredLinkOptions;
- window.open(href, target);
+ visitUrl(href, target === '_blank');
}
})
.catch(() => {
diff --git a/app/assets/javascripts/projects/your_work/components/app.vue b/app/assets/javascripts/projects/your_work/components/app.vue
index 4ad6fa994ca..88bfb4b37b4 100644
--- a/app/assets/javascripts/projects/your_work/components/app.vue
+++ b/app/assets/javascripts/projects/your_work/components/app.vue
@@ -1,8 +1,11 @@
diff --git a/app/assets/javascripts/work_items/components/work_item_state_badge.vue b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
index 4924d6f80be..cab76be11ce 100644
--- a/app/assets/javascripts/work_items/components/work_item_state_badge.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
@@ -1,12 +1,11 @@
-
-
- {{ stateText }}
+
+ {{ stateText }}
diff --git a/app/components/layouts/settings_section_component.haml b/app/components/layouts/settings_section_component.haml
index 2e03a11cc24..8872473d482 100644
--- a/app/components/layouts/settings_section_component.haml
+++ b/app/components/layouts/settings_section_component.haml
@@ -4,7 +4,7 @@
%h2.gl-heading-2{ class: '!gl-mb-2' }
= heading || @heading
- if description || @description
- %p.gl-text-secondary.gl-m-0
+ %p.gl-text-secondary.gl-mb-3
= description || @description
%div{ data: { testid: 'settings-section-body' } }
= body
diff --git a/app/graphql/mutations/clusters/agents/create.rb b/app/graphql/mutations/clusters/agents/create.rb
index 0f1dbcff555..9fecf6d55f2 100644
--- a/app/graphql/mutations/clusters/agents/create.rb
+++ b/app/graphql/mutations/clusters/agents/create.rb
@@ -25,7 +25,7 @@ module Mutations
def resolve(project_path:, name:)
project = authorized_find!(project_path)
- result = ::Clusters::Agents::CreateService.new(project, current_user).execute(name: name)
+ result = ::Clusters::Agents::CreateService.new(project, current_user, { name: name }).execute
{
cluster_agent: result[:cluster_agent],
diff --git a/app/graphql/mutations/clusters/agents/delete.rb b/app/graphql/mutations/clusters/agents/delete.rb
index c8df31e02b4..fed96bc86fc 100644
--- a/app/graphql/mutations/clusters/agents/delete.rb
+++ b/app/graphql/mutations/clusters/agents/delete.rb
@@ -17,8 +17,8 @@ module Mutations
def resolve(id:)
cluster_agent = authorized_find!(id: id)
result = ::Clusters::Agents::DeleteService
- .new(container: cluster_agent.project, current_user: current_user)
- .execute(cluster_agent)
+ .new(container: cluster_agent.project, current_user: current_user, params: { cluster_agent: cluster_agent })
+ .execute
{
errors: Array.wrap(result.message)
diff --git a/app/models/group.rb b/app/models/group.rb
index ccaab2f6e38..c7d51341897 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -972,6 +972,9 @@ class Group < Namespace
# NOTE: We still want to keep this after removing `Namespace#feature_available?`.
override :feature_available?
def feature_available?(feature, user = nil)
+ # when we check the :issues feature at group level we need to check the `epics` license feature instead
+ feature = feature == :issues ? :epics : feature
+
if ::Groups::FeatureSetting.available_features.include?(feature)
group_feature.feature_available?(feature, user) # rubocop:disable Gitlab/FeatureAvailableUsage
else
diff --git a/app/models/import/source_user.rb b/app/models/import/source_user.rb
index 590c94939aa..2b7db6421e3 100644
--- a/app/models/import/source_user.rb
+++ b/app/models/import/source_user.rb
@@ -92,6 +92,14 @@ module Import
end
end
+ def accepted_reassign_to_user
+ reassign_to_user if accepted_status?
+ end
+
+ def accepted_status?
+ reassignment_in_progress? || completed? || failed?
+ end
+
def reassignable_status?
pending_reassignment? || rejected?
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 4cc87a4912a..410e4fe343c 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -769,6 +769,12 @@ class Issue < ApplicationRecord
project_id.blank?
end
+ def autoclose_by_merged_closing_merge_request?
+ return false if group_level?
+
+ project.autoclose_referenced_issues
+ end
+
private
def project_level_readable_by?(user)
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 17ecd7aa8f1..5a449a976b8 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -14,6 +14,10 @@ class SystemNoteMetadata < ApplicationRecord
moved merge
label milestone
relate unrelate
+ unrelate_from_parent
+ unrelate_from_child
+ relate_to_parent
+ relate_to_child
cloned
].freeze
diff --git a/app/models/work_items/widgets/development.rb b/app/models/work_items/widgets/development.rb
index 81e8b871ada..65dd8a0d0f0 100644
--- a/app/models/work_items/widgets/development.rb
+++ b/app/models/work_items/widgets/development.rb
@@ -8,18 +8,10 @@ module WorkItems
end
def will_auto_close_by_merge_request
- return false unless work_item.opened? && autoclose_referenced_issues_enabled?
+ return false unless work_item.opened? && work_item.autoclose_by_merged_closing_merge_request?
work_item.merge_requests_closing_issues.with_opened_merge_request.exists?
end
-
- private
-
- def autoclose_referenced_issues_enabled?
- return true if work_item.project.nil?
-
- work_item.project.autoclose_referenced_issues
- end
end
end
end
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index a0fb80297bf..f1ec9b155c1 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -65,6 +65,10 @@ class IssuePolicy < IssuablePolicy
prevent :read_issue
end
+ rule { can?(:read_issue) & notes_widget_enabled }.policy do
+ enable :read_note
+ end
+
rule { ~can?(:read_issue) }.policy do
prevent :create_note
prevent :read_note
diff --git a/app/services/clusters/agents/create_service.rb b/app/services/clusters/agents/create_service.rb
index 568f168d63b..052f4caae5f 100644
--- a/app/services/clusters/agents/create_service.rb
+++ b/app/services/clusters/agents/create_service.rb
@@ -3,15 +3,15 @@
module Clusters
module Agents
class CreateService < BaseService
- def execute(name:)
+ def execute
return error_no_permissions unless cluster_agent_permissions?
- agent = ::Clusters::Agent.new(name: name, project: project, created_by_user: current_user)
+ agent = ::Clusters::Agent.new(name: params[:name], project: project, created_by_user: current_user)
if agent.save
- success.merge(cluster_agent: agent)
+ ServiceResponse.new(status: :success, payload: { cluster_agent: agent }, reason: :created)
else
- error(agent.errors.full_messages)
+ ServiceResponse.error(message: agent.errors.full_messages)
end
end
@@ -22,8 +22,12 @@ module Clusters
end
def error_no_permissions
- error(s_('ClusterAgent|You have insufficient permissions to create a cluster agent for this project'))
+ ServiceResponse.error(
+ message: s_('ClusterAgent|You have insufficient permissions to create a cluster agent for this project')
+ )
end
end
end
end
+
+Clusters::Agents::CreateService.prepend_mod
diff --git a/app/services/clusters/agents/delete_service.rb b/app/services/clusters/agents/delete_service.rb
index 2132dffa606..bf085455587 100644
--- a/app/services/clusters/agents/delete_service.rb
+++ b/app/services/clusters/agents/delete_service.rb
@@ -3,7 +3,9 @@
module Clusters
module Agents
class DeleteService < ::BaseContainerService
- def execute(cluster_agent)
+ def execute
+ cluster_agent = params[:cluster_agent]
+
return error_no_permissions unless current_user.can?(:admin_cluster, cluster_agent)
if cluster_agent.destroy
@@ -21,3 +23,5 @@ module Clusters
end
end
end
+
+Clusters::Agents::DeleteService.prepend_mod
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index b75c3a88dd1..b00f6ccea36 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -60,7 +60,7 @@ module MergeRequests
# as the worker only supports finding an Issue. We are also only experiencing
# SQL timeouts when closing an Issue.
if issue.is_a?(Issue)
- next if issue.project.present? && !issue.project.autoclose_referenced_issues
+ next unless issue.autoclose_by_merged_closing_merge_request?
MergeRequests::CloseIssueWorker.perform_async(
project.id,
diff --git a/bin/pngquant b/bin/pngquant
index a614814a8a3..d2e6660d570 100755
--- a/bin/pngquant
+++ b/bin/pngquant
@@ -45,16 +45,16 @@ when 'lint'
uncompressed_files = Parallel.map(files) do |file|
is_uncompressed, _ = Tooling::Image.compress_image(file, true)
if is_uncompressed
- puts "Uncompressed file detected: ".color(:red) + file
+ puts Rainbow("Uncompressed file detected: ").red + file
file
end
end.compact
if uncompressed_files.empty?
- puts "All documentation images are optimally compressed!".color(:green)
+ puts Rainbow("All documentation images are optimally compressed!").green
else
warn(
- "The #{uncompressed_files.size} image(s) above have not been optimally compressed using pngquant.".color(:red),
+ Rainbow("The #{uncompressed_files.size} image(s) above have not been optimally compressed using pngquant.").red,
'Please run "bin/pngquant compress" and commit the result.'
)
abort
diff --git a/config/initializers/elastic_client_setup.rb b/config/initializers/elastic_client_setup.rb
index 5fe26d7bd92..f046ff80ac5 100644
--- a/config/initializers/elastic_client_setup.rb
+++ b/config/initializers/elastic_client_setup.rb
@@ -5,6 +5,18 @@
require 'gitlab/current_settings'
Gitlab.ee do
+ require 'elasticsearch'
+
+ module Elasticsearch
+ class Client
+ # https://github.com/elastic/elasticsearch-ruby/commit/51edf86470dad9d0701fcbac69dae5b89227bc02 introduced
+ # a verification step on the Elasticsearch version. Disable this check to maintain compatibility with OpenSearch.
+ def verify_elasticsearch
+ @verified = true
+ end
+ end
+ end
+
require 'elasticsearch/model'
### Monkey patches
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 7849507b10c..c4f8ae50ced 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -705,6 +705,8 @@
- 1
- - search_elastic_default_branch_changed
- 1
+- - search_elastic_delete
+ - 1
- - search_elastic_group_association_deletion
- 1
- - search_elastic_trigger_indexing
@@ -729,6 +731,8 @@
- 1
- - search_zoekt_project_transfer
- 1
+- - security_create_security_policy_project
+ - 1
- - security_delete_orchestration_configuration
- 1
- - security_generate_policy_violation_comment
diff --git a/doc/.vale/gitlab/InternalLinksCode.yml b/doc/.vale/gitlab/InternalLinksCode.yml
new file mode 100644
index 00000000000..17efd26e700
--- /dev/null
+++ b/doc/.vale/gitlab/InternalLinksCode.yml
@@ -0,0 +1,12 @@
+# Error: gitlab.InternalLinksCode
+#
+# Checks that internal links don't link to files outside the docs directory
+#
+# For a list of all options, see https://vale.sh/docs/topics/styles/
+extends: existence
+message: "Use full URLs for files outside the docs directory."
+link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#links
+level: error
+scope: raw
+raw:
+ - '\[[^\]]*\]\([\.\/]*(ee|app|bin|config|db|data|fixtures|lib|locale|qa|scripts|spec)\/'
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index eadd055e207..36300dbe099 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -8644,6 +8644,30 @@ Input type: `SecurityPolicyProjectCreateInput`
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| `project` | [`Project`](#project) | Security Policy Project that was created. |
+### `Mutation.securityPolicyProjectCreateAsync`
+
+**Status:** Alpha. Creates and assigns a security policy project for the given project or group (`full_path`) async.
+
+DETAILS:
+**Introduced** in GitLab 17.3.
+**Status**: Experiment.
+
+Input type: `SecurityPolicyProjectCreateAsyncInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `fullPath` | [`String!`](#string) | Full path of the project or group. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
### `Mutation.securityPolicyProjectUnassign`
Unassigns the security policy project for the given project (`full_path`).
@@ -27990,6 +28014,18 @@ Represents policy violation for `license_scanning` report_type.
| `license` | [`String!`](#string) | License name. |
| `url` | [`String`](#string) | URL of the license. |
+### `PolicyProjectCreated`
+
+Response of security policy creation.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `errorMessage` | [`String`](#string) | Error message in case status is :error. |
+| `project` | [`Project`](#project) | Security Policy Project that was created. |
+| `status` | [`PolicyProjectCreatedStatus`](#policyprojectcreatedstatus) | Status of the creation of the security policy project. |
+
### `PolicyScanFindingViolation`
Represents policy violation for `scan_finding` report_type.
@@ -36004,6 +36040,15 @@ Pipeline security report finding sort values.
| `WAITING_FOR_CALLBACK` | Pipeline is waiting for an external action. |
| `WAITING_FOR_RESOURCE` | A resource (for example, a runner) that the pipeline requires to run is unavailable. |
+### `PolicyProjectCreatedStatus`
+
+Types of security policy project created status.
+
+| Value | Description |
+| ----- | ----------- |
+| `ERROR` | Creating the security policy project faild. |
+| `SUCCESS` | Creating the security policy project was successful. |
+
### `PolicyViolationErrorType`
| Value | Description |
diff --git a/doc/development/advanced_search.md b/doc/development/advanced_search.md
index 59ff3d441a5..33e81eb343c 100644
--- a/doc/development/advanced_search.md
+++ b/doc/development/advanced_search.md
@@ -349,14 +349,14 @@ After indexing is done, the index is ready for search.
### Adding a new scope to search service
-Search data is available in [`SearchController`](../../app/controllers/search_controller.rb) and
-[Search API](../../lib/api/search.rb). Both use the [`SearchService`](../../app/services/search_service.rb) to return results.
+Search data is available in [`SearchController`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/controllers/search_controller.rb) and
+[Search API](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/api/search.rb). Both use the [`SearchService`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/search_service.rb) to return results.
The `SearchService` can be used to return results outside of the `SearchController` and `Search API`.
#### Search scopes
-The `SearchService` exposes searching at [global](../../app/services/search/global_service.rb),
-[group](../../app/services/search/group_service.rb), and [project](../../app/services/search/project_service.rb) levels.
+The `SearchService` exposes searching at [global](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/search/global_service.rb),
+[group](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/search/group_service.rb), and [project](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/search/project_service.rb) levels.
New scopes must be added to the following constants:
@@ -366,7 +366,7 @@ New scopes must be added to the following constants:
NOTE:
Global search can be disabled for a scope. Create an ops feature flag named `global_search_SCOPE_tab` that defaults to `true`
-and add it to the `global_search_enabled_for_scope?` method in [`SearchService`](../../app/services/search_service.rb).
+and add it to the `global_search_enabled_for_scope?` method in [`SearchService`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/search_service.rb).
#### Results classes
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index 725ad64b967..607b0cebe62 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -809,21 +809,24 @@ However, you should avoid putting too many links on any page. Too many links can
- Consider using [Related topics](../topic_types/index.md#related-topics) to reduce links that interrupt the flow of a task.
- Try to avoid anchor links to sections on the same page. Let users rely on the right navigation instead.
-### Links within the same repository
+### Links in the same repository
-To link to another page in the same repository,
-use a relative file path. For example, `../user/gitlab_com/index.md`.
+To link to another documentation (`.md`) file in the same repository:
-Use inline link Markdown markup `[Text](https://example.com)`,
-rather than reference-style links, like `[Text][identifier]`.
+- Use an inline link with a relative file path. For example, `[GitLab.com settings](../user/gitlab_com/index.md)`.
+- Put the entire link on a single line, even if the link is very long. ([Vale](../testing/vale.md) rule: [`SubstitutionWarning.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/MultiLineLinks.yml)).
-Put the entire link on a single line so that [linters](../testing/index.md) can find it.
+To link to a file outside of the documentation files, for example to link from development
+documentation to a specific code file, you can:
+
+- Use a full URL. For example: ``[`app/views/help/show.html.haml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/views/help/show.html.haml)``
+- (Optional) Use a full URL with a specific ref. For example: ``[`app/views/help/show.html.haml`](https://gitlab.com/gitlab-org/gitlab/-/blob/6d01aa9f1cfcbdfa88edf9d003bd073f1a6fff1d/app/views/help/show.html.haml)``
### Links in separate repositories
-To link to a page in a different repository, use an absolute URL.
+To link to a page in a different repository, use a full URL.
For example, to link from a page in the GitLab repository to the Charts repository,
-use a URL like `https://docs.gitlab.com/charts/`.
+use a URL like `[GitLab Charts documentation](https://docs.gitlab.com/charts/)`.
### Anchor links
@@ -851,7 +854,7 @@ any related links, search these directories:
- `ee/app/views/*`
If you do not fix these links, the [`ui-docs-lint` job](../testing/index.md#tests-in-ui-docs-links-lint)
-in your merge request fails.
+in your merge request might fail.
### Text for links
diff --git a/doc/solutions/cloud/aws/gitlab_aws_integration.md b/doc/solutions/cloud/aws/gitlab_aws_integration.md
index 86c6bbe8969..994a374efac 100644
--- a/doc/solutions/cloud/aws/gitlab_aws_integration.md
+++ b/doc/solutions/cloud/aws/gitlab_aws_integration.md
@@ -83,7 +83,7 @@ Documentation and References:
- **Amazon SageMaker Notebooks** [allow Git repositories to be specified by the Git clone URL](https://docs.aws.amazon.com/sagemaker/latest/dg/nbi-git-resource.html) and configuration of a secret - so GitLab is configurable. ([12/28/2023](https://aws.amazon.com/about-aws/whats-new/2023/12/codepipeline-gitlab-self-managed/)) `[AWS Configuration]`
- **AWS Amplify** - [uses a Git integration mechanism designed by the AWS Amplify team](https://docs.aws.amazon.com/amplify/latest/userguide/getting-started.html). `[AWS Built]`
-- **AWS Glue Notebook Jobs** support for GitLab respository URL with Personal Access Token (PAT) authentication at the "job" level. ([10/03/2022](https://aws.amazon.com/about-aws/whats-new/2022/10/aws-glue-git-integration/)) [AWS Docs about configuring GitLab](https://docs.aws.amazon.com/glue/latest/dg/edit-job-add-source-control-integration.html) `[AWS Configuration]`
+- **AWS Glue Notebook Jobs** support for GitLab repository URL with Personal Access Token (PAT) authentication at the "job" level. ([10/03/2022](https://aws.amazon.com/about-aws/whats-new/2022/10/aws-glue-git-integration/)) [AWS Docs about configuring GitLab](https://docs.aws.amazon.com/glue/latest/dg/edit-job-add-source-control-integration.html) `[AWS Configuration]`
#### Other SCM Integration Options
diff --git a/doc/user/analytics/analytics_dashboards.md b/doc/user/analytics/analytics_dashboards.md
index 860e4ca85d7..570ef72faee 100644
--- a/doc/user/analytics/analytics_dashboards.md
+++ b/doc/user/analytics/analytics_dashboards.md
@@ -169,7 +169,6 @@ To change the location of project dashboards:
to create the project to store your dashboard files.
1. On the left sidebar, select **Search or go to** and find the analytics project.
1. Select **Settings > Analytics**.
-1. Select **Expand** to see custom dashboard projects.
1. In the **Analytics Dashboards** section, select your dashboard files project.
1. Select **Save changes**.
diff --git a/doc/user/application_security/vulnerabilities/index.md b/doc/user/application_security/vulnerabilities/index.md
index 75179765b11..d73778a5a35 100644
--- a/doc/user/application_security/vulnerabilities/index.md
+++ b/doc/user/application_security/vulnerabilities/index.md
@@ -97,8 +97,7 @@ DETAILS:
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10779) in GitLab 16.7 as an [experiment](../../../policy/experiment-beta-support.md#experiment) on GitLab.com.
Use GitLab Duo Vulnerability resolution to automatically create a merge request that
-resolves the vulnerability. By default, it is powered by Anthropic's [`claude-3-haiku`](https://docs.anthropic.com/en/docs/about-claude/models#claude-3-a-new-generation-of-ai)
-model.
+resolves the vulnerability. By default, it is powered by Anthropic's [`claude-3.5-sonnet`](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-5-sonnet) model.
We cannot guarantee that the large language model produces results that are correct. Use the
explanation with caution.
diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md
index 11593e806a6..2668478660f 100644
--- a/doc/user/compliance/audit_event_types.md
+++ b/doc/user/compliance/audit_event_types.md
@@ -234,6 +234,10 @@ Audit event types belong to the following product categories.
| Name | Description | Saved to database | Streamed | Introduced in | Scope |
|:------------|:------------|:------------------|:---------|:--------------|:--------------|
+| [`cluster_agent_create_failed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/159593) | Event triggered when a user attempts to create a cluster agent but it failed| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.3](https://gitlab.com/gitlab-org/gitlab/-/issues/462749) | Project |
+| [`cluster_agent_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/159593) | Event triggered when a user creates a cluster agent| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.3](https://gitlab.com/gitlab-org/gitlab/-/issues/462749) | Project |
+| [`cluster_agent_delete_failed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/159593) | Event triggered when a user attempts to delete a cluster agent but it failed| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.3](https://gitlab.com/gitlab-org/gitlab/-/issues/462749) | Project |
+| [`cluster_agent_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/159593) | Event triggered when a user deletes a cluster agent| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.3](https://gitlab.com/gitlab-org/gitlab/-/issues/462749) | Project |
| [`cluster_agent_token_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112036) | Event triggered when a user creates a cluster agent token| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.10](https://gitlab.com/gitlab-org/gitlab/-/issues/382133) | Project |
| [`cluster_agent_token_revoked`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112036) | Event triggered when a user revokes a cluster agent token| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.10](https://gitlab.com/gitlab-org/gitlab/-/issues/382133) | Project |
diff --git a/doc/user/gitlab_duo/index.md b/doc/user/gitlab_duo/index.md
index b373536f079..93c141926c9 100644
--- a/doc/user/gitlab_duo/index.md
+++ b/doc/user/gitlab_duo/index.md
@@ -211,7 +211,7 @@ DETAILS:
**Status:** Experiment
- Help resolve a vulnerability by generating a merge request that addresses it.
-- LLM: Anthropic's [`claude-3-haiku`](https://docs.anthropic.com/en/docs/about-claude/models#claude-3-a-new-generation-of-ai).
+- LLM: Anthropic's [`claude-3.5-sonnet`](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-5-sonnet).
- [View documentation](../application_security/vulnerabilities/index.md#vulnerability-resolution).
### Product Analytics
diff --git a/doc/user/group/devops_adoption/img/group_devops_adoption_v14_2.png b/doc/user/group/devops_adoption/img/group_devops_adoption_v14_2.png
deleted file mode 100644
index 21e38907a10..00000000000
Binary files a/doc/user/group/devops_adoption/img/group_devops_adoption_v14_2.png and /dev/null differ
diff --git a/doc/user/group/devops_adoption/index.md b/doc/user/group/devops_adoption/index.md
index 07ca44d3069..dfb166b61ca 100644
--- a/doc/user/group/devops_adoption/index.md
+++ b/doc/user/group/devops_adoption/index.md
@@ -4,7 +4,7 @@ group: Optimize
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-# Group DevOps Adoption
+# DevOps adoption by group
DETAILS:
**Tier:** Ultimate
@@ -12,32 +12,20 @@ DETAILS:
> - [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/367093) to the [Registration Features Program](../../../administration/settings/usage_statistics.md#registration-features-program) in GitLab 16.6.
-DevOps Adoption shows you how groups in your organization adopt and use the most essential features of GitLab.
+DevOps adoption shows you how groups in your organization adopt and use GitLab features.
+This information is available for groups and [instances](../../../administration/analytics/dev_ops_reports.md).
-You can use Group DevOps Adoption to:
+Use DevOps adoption for groups to:
-- Identify specific subgroups that are lagging in their adoption of GitLab features, so you can guide them on
+- Identify subgroups that are lagging in their adoption of GitLab features, so you can guide them on
their DevOps journey.
- Find subgroups that have adopted certain features, and provide guidance to other subgroups on
how to use those features.
- Verify if you are getting the return on investment that you expected from GitLab.
-
+## Feature adoption
-## View DevOps Adoption
-
-Prerequisites:
-
-- You must have at least the Reporter role for the group.
-
-To view DevOps Adoption:
-
-1. On the left sidebar, select **Search or go to** and find your group.
-1. Select **Analyze > DevOps adoption**
-
-## DevOps Adoption categories
-
-DevOps Adoption shows feature adoption for development, security, and operations.
+DevOps adoption shows feature adoption for development, security, and operations.
| Category | Feature |
|-------------|---------|
@@ -45,48 +33,69 @@ DevOps Adoption shows feature adoption for development, security, and operations
| Security | DAST
Dependency Scanning
Fuzz Testing
SAST |
| Operations | Deployments
Pipelines
Runners |
-## Feature adoption
+A feature shows as **adopted** when a group or subgroup has used the feature in a project in the last full calendar month.
+For example, if an issue was created in a project in a group, the group has adopted issues in that time.
-DevOps Adoption shows feature adoption data for groups and subgroups for the previous calendar month.
+The DevOps adoption report excludes:
-A feature shows as **adopted** when a group has used the feature in a project during the time period.
-This includes projects in any subgroups of the group. For example, if an issue was created in a project in a group, the group has adopted issues in that time.
-
-### Exceptions to feature adoption data
-
-When GitLab measures DevOps Adoption, some common DevOps information is not included:
-
-- Dormant projects. It doesn't matter how many projects in the group use a feature. Even if you have many dormant projects, it doesn't lower the adoption.
+- Dormant projects. The number of projects that use a feature is not considered. Having many dormant projects doesn't lower the adoption.
- New GitLab features. Adoption is the total number of features adopted, not the percent of features.
-## When DevOps Adoption data is gathered
+## Data processing
-A weekly task processes data for DevOps Adoption. This task is disabled until you access
-DevOps Adoption for a group for the first time.
+A weekly task processes data for DevOps adoption.
+This task is disabled until you access DevOps adoption for a group for the first time.
-The data processing task updates the data on the first day of each month. If the monthly update
-fails, the task tries daily until it succeeds.
+The data processing task updates the data on the first day of each month.
+If the monthly update fails, the task tries daily until it succeeds.
-DevOps Adoption data may take up to a minute to appear while GitLab processes the group's data.
+DevOps adoption data may take up to a minute to appear while GitLab processes the group's data.
-## View feature adoption over time
+## View DevOps adoption for groups
-The **Adoption over time** chart shows the total number of adopted features from the previous
-twelve months. The chart only shows data from when you enabled DevOps Adoption for the group.
+Prerequisites:
-To view feature adoption over time:
+- You must have at least the Reporter role for the group.
+
+To view DevOps adoption:
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Analyze > DevOps adoption**.
-1. Select the **Overview** tab.
-Tooltips display information about the features tracked for individual months.
+The **Overview** tab displays the:
-## Add or remove a subgroup
+- Total number of features adopted.
+- Features adopted in each category.
+- Number of features adopted in each category by month in the **Adoption over time** chart.
+The chart shows only data from the date you enabled DevOps adoption for the group.
+- Number of features adopted in each category by subgroup in the **Adoption by subgroup** table.
-To add or remove a subgroup from the DevOps Adoption report:
+The **Dev**, **Sec**, and **Ops** tabs display the features adopted in development, security, and operations by subgroup.
-1. Select **Add or remove subgroups**.
-1. Select the subgroup you want to add or remove and select **Save changes**.
+## Add a subgroup to DevOps adoption
-It may take up to a minute for subgroup data to appear while GitLab collects the data.
+Prerequisites:
+
+- You must have at least the Reporter role for the group.
+
+To add a subgroup to the DevOps adoption report:
+
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Analyze > DevOps adoption**.
+1. From the **Add or remove subgroups** dropdown list, select the subgroup you want to add.
+
+## Remove a subgroup from DevOps adoption
+
+Prerequisites:
+
+- You must have at least the Reporter role for the group.
+
+To remove a subgroup from the DevOps adoption report:
+
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Analyze > DevOps adoption**.
+1. Either:
+
+- From the **Add or remove subgroups** dropdown list, clear the subgroup you want to remove.
+- From the **Adoption by subgroup** table, in the row of the group you want to remove, select
+**Remove Group from the table** (**{remove}**).
diff --git a/keeps/mark_old_advanced_search_migrations_as_obsolete.rb b/keeps/mark_old_advanced_search_migrations_as_obsolete.rb
index 1fa35e3b70e..1a2a3c5bf87 100644
--- a/keeps/mark_old_advanced_search_migrations_as_obsolete.rb
+++ b/keeps/mark_old_advanced_search_migrations_as_obsolete.rb
@@ -120,7 +120,7 @@ module Keeps
you must check the diff and pipeline failures to confirm if there are any issues.
It is the responsibility of the assignee (picked from ~"group::global search") to push those changes to this branch.
- [Read more](https://docs.gitlab.com/ee/development/search/advanced_search_migration_styleguide.html#deleting-advanced-search-migrations-in-a-major-version-upgrade)
+ [Read more](https://docs.gitlab.com/ee/development/search/advanced_search_migration_styleguide.html#cleaning-up-advanced-search-migrations)
about the process for marking Advanced search migrations as obsolete.
All Advanced search migrations must have had at least one
diff --git a/lib/api/clusters/agents.rb b/lib/api/clusters/agents.rb
index 02469fbad21..aed4ad87fe6 100644
--- a/lib/api/clusters/agents.rb
+++ b/lib/api/clusters/agents.rb
@@ -57,7 +57,7 @@ module API
params = declared_params(include_missing: false)
- result = ::Clusters::Agents::CreateService.new(user_project, current_user).execute(name: params[:name])
+ result = ::Clusters::Agents::CreateService.new(user_project, current_user, { name: params[:name] }).execute
bad_request!(result[:message]) if result[:status] == :error
@@ -76,7 +76,11 @@ module API
agent = ::Clusters::AgentsFinder.new(user_project, current_user).find(params[:agent_id])
- destroy_conditionally!(agent)
+ destroy_conditionally!(agent) do |agent|
+ ::Clusters::Agents::DeleteService
+ .new(container: agent.project, current_user: current_user, params: { cluster_agent: agent })
+ .execute
+ end
end
end
end
diff --git a/lib/container_registry/base_client.rb b/lib/container_registry/base_client.rb
index bd30b31722f..b556379228d 100644
--- a/lib/container_registry/base_client.rb
+++ b/lib/container_registry/base_client.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
require 'faraday'
-require 'faraday_middleware'
+require 'faraday/follow_redirects'
+require 'faraday/retry'
require 'digest'
module ContainerRegistry
@@ -15,7 +16,7 @@ module ContainerRegistry
ACCEPTED_TYPES = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE].freeze
ACCEPTED_TYPES_RAW = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE, DOCKER_DISTRIBUTION_MANIFEST_LIST_V2_TYPE, OCI_DISTRIBUTION_INDEX_TYPE].freeze
- RETRY_EXCEPTIONS = [Faraday::Request::Retry::DEFAULT_EXCEPTIONS, Faraday::ConnectionFailed].flatten.freeze
+ RETRY_EXCEPTIONS = [Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS, Faraday::ConnectionFailed].flatten.freeze
RETRY_OPTIONS = {
max: 1,
interval: 5,
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index f27bc43eee3..4dd262a6bf7 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -20,7 +20,7 @@ module ContainerRegistry
limit: 3,
cookies: [],
callback: ->(response_env, request_env) do
- request_env.request_headers.delete(::FaradayMiddleware::FollowRedirects::AUTH_HEADER)
+ request_env.request_headers.delete(::Faraday::FollowRedirects::Middleware::AUTH_HEADER)
redirect_to = request_env.url
unless redirect_to.scheme.in?(ALLOWED_REDIRECT_SCHEMES)
@@ -161,7 +161,7 @@ module ContainerRegistry
@faraday_blob ||= faraday_base do |conn|
initialize_connection(conn, @options)
- conn.use ::FaradayMiddleware::FollowRedirects, REDIRECT_OPTIONS
+ conn.use ::Faraday::FollowRedirects::Middleware, REDIRECT_OPTIONS
end
end
end
diff --git a/lib/gitlab/ci/config/external/file/component.rb b/lib/gitlab/ci/config/external/file/component.rb
index 026e7f6757d..b34c303785b 100644
--- a/lib/gitlab/ci/config/external/file/component.rb
+++ b/lib/gitlab/ci/config/external/file/component.rb
@@ -62,6 +62,13 @@ module Gitlab
attr_reader :path, :version
+ def content_result
+ context.logger.instrument(:config_component_fetch_content_hash) do
+ super
+ end
+ end
+ strong_memoize_attr :content_result
+
def component_result
::Ci::Components::FetchService.new(
address: location,
diff --git a/lib/gitlab/import/source_user_mapper.rb b/lib/gitlab/import/source_user_mapper.rb
index b4a041bdf96..dc23f890ce8 100644
--- a/lib/gitlab/import/source_user_mapper.rb
+++ b/lib/gitlab/import/source_user_mapper.rb
@@ -43,7 +43,7 @@ module Gitlab
return unless source_user
- source_user.reassign_to_user || source_user.placeholder_user
+ source_user.accepted_reassign_to_user || source_user.placeholder_user
end
def create_source_user_mapping
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 355a8808afa..cd08db48356 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -113,12 +113,12 @@ module Gitlab
end
# highlighting is only performed by Elasticsearch backed results
- def highlight_map(_scope)
+ def highlight_map(*)
{}
end
# aggregations are only performed by Elasticsearch backed results
- def aggregations(_scope)
+ def aggregations(*)
[]
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c220b16739f..1ddcf7dd8c2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -20774,9 +20774,15 @@ msgstr ""
msgid "Environment|Unauthorized to access %{resourceType} from this environment."
msgstr ""
+msgid "Environment|Unavailable"
+msgstr ""
+
msgid "Environment|Unhealthy"
msgstr ""
+msgid "Environment|Unknown"
+msgstr ""
+
msgid "Environment|You don't have permission to view all the namespaces in the cluster. If a namespace is not shown, you can still enter its name to select it."
msgstr ""
@@ -23723,6 +23729,9 @@ msgstr ""
msgid "GitLab Duo Pro seats"
msgstr ""
+msgid "GitLab Duo didn't find any reviewable files. Code Review request skipped."
+msgstr ""
+
msgid "GitLab Enterprise Edition"
msgstr ""
@@ -25450,9 +25459,6 @@ msgstr ""
msgid "GroupSettings|An experiment is a feature that is in the process of being developed. It is not production-ready. We encourage users to try experimental features and provide feedback. %{link_start}Learn more%{link_end}."
msgstr ""
-msgid "GroupSettings|Analytics"
-msgstr ""
-
msgid "GroupSettings|Analytics Dashboards"
msgstr ""
@@ -25495,9 +25501,6 @@ msgstr ""
msgid "GroupSettings|Choose the merge request checks for projects in this group. This setting overrides the same settings configured on each project in this group."
msgstr ""
-msgid "GroupSettings|Configure analytics features for this group."
-msgstr ""
-
msgid "GroupSettings|Configure limits on the number of repositories users can download, clone, or fork in a given time."
msgstr ""
@@ -41646,7 +41649,7 @@ msgstr ""
msgid "ProjectSettings|Build, test, and deploy your changes. Does not apply to project integrations."
msgstr ""
-msgid "ProjectSettings|By default the current project is used."
+msgid "ProjectSettings|Change the location of dashboards?"
msgstr ""
msgid "ProjectSettings|Checkbox is visible and selected by default."
@@ -41691,9 +41694,6 @@ msgstr ""
msgid "ProjectSettings|Cube API URL"
msgstr ""
-msgid "ProjectSettings|Custom dashboard projects"
-msgstr ""
-
msgid "ProjectSettings|Data sources"
msgstr ""
@@ -42003,7 +42003,7 @@ msgstr ""
msgid "ProjectSettings|Storage name:"
msgstr ""
-msgid "ProjectSettings|Store configuration files for custom dashboards and visualizations."
+msgid "ProjectSettings|Store configuration files for custom dashboards and visualizations. By default the current project is used."
msgstr ""
msgid "ProjectSettings|Submit changes to be merged upstream."
diff --git a/qa/gems/chemlab-gitlab/lib/gitlab/page/admin/subscription.rb b/qa/gems/chemlab-gitlab/lib/gitlab/page/admin/subscription.rb
deleted file mode 100644
index fe61111ebe9..00000000000
--- a/qa/gems/chemlab-gitlab/lib/gitlab/page/admin/subscription.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Page
- module Admin
- class Subscription < Chemlab::Page
- path '/admin/subscription'
-
- div :subscription_details
- text_field :activation_code
- button :activate
- label :terms_of_services, text: /I agree that/
- button :remove_license
- button :confirm_remove_license
- td :plan
- td :name
- td :company
- td :email
- h2 :users_in_subscription
- table :subscription_history
-
- div :no_valid_license_alert, text: /no longer has a valid license/
- h3 :no_active_subscription_title, text: /do not have an active subscription/
-
- def accept_terms
- terms_of_services_element.click # workaround for hidden checkbox
- end
-
- def remove_license_file
- remove_license
- confirm_remove_license
- end
-
- # Checks if a subscription record exists in subscription history table
- #
- # @param plan [Hash] Name of the plan
- # @option plan [Hash] Support::Helpers::FREE
- # @option plan [Hash] Support::Helpers::PREMIUM_SELF_MANAGED
- # @option plan [Hash] Support::Helpers::ULTIMATE_SELF_MANAGED
- # @param users_in_license [Integer] Number of users in license
- # @param license_type [Hash] Type of the license
- # @option license_type [String] 'license file'
- # @option license_type [String] 'cloud license'
- # @return [Boolean] True if record exists, false if not
- def has_subscription_record?(plan, users_in_license, license_type)
- # find any records that have a matching plan and seats and type
- subscription_history_element.hashes.any? do |record|
- record['Plan'] == plan[:name].capitalize && record['Seats'] == users_in_license.to_s && \
- record['Type'].strip.downcase == license_type
- end
- end
- end
- end
- end
-end
diff --git a/qa/gems/chemlab-gitlab/lib/gitlab/page/admin/subscription.stub.rb b/qa/gems/chemlab-gitlab/lib/gitlab/page/admin/subscription.stub.rb
deleted file mode 100644
index 9ed127f9281..00000000000
--- a/qa/gems/chemlab-gitlab/lib/gitlab/page/admin/subscription.stub.rb
+++ /dev/null
@@ -1,355 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Page
- module Admin
- module Subscription
- # @note Defined as +div :subscription_details+
- # @return [String] The text content or value of +subscription_details+
- def subscription_details
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.subscription_details_element).to exist
- # end
- # @return [Watir::Div] The raw +Div+ element
- def subscription_details_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_subscription_details
- # end
- # @return [Boolean] true if the +subscription_details+ element is present on the page
- def subscription_details?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +text_field :activation_code+
- # @return [String] The text content or value of +activation_code+
- def activation_code
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # Set the value of activation_code
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # subscription.activation_code = 'value'
- # end
- # @param value [String] The value to set.
- def activation_code=(value)
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.activation_code_element).to exist
- # end
- # @return [Watir::TextField] The raw +TextField+ element
- def activation_code_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_activation_code
- # end
- # @return [Boolean] true if the +activation_code+ element is present on the page
- def activation_code?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +button :activate+
- # Clicks +activate+
- def activate
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.activate_element).to exist
- # end
- # @return [Watir::Button] The raw +Button+ element
- def activate_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_activate
- # end
- # @return [Boolean] true if the +activate+ element is present on the page
- def activate?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +label :terms_of_services+
- # @return [String] The text content or value of +terms_of_services+
- def terms_of_services
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.terms_of_services_element).to exist
- # end
- # @return [Watir::Label] The raw +Label+ element
- def terms_of_services_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_terms_of_services
- # end
- # @return [Boolean] true if the +terms_of_services+ element is present on the page
- def terms_of_services?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +button :remove_license+
- # Clicks +remove_license+
- def remove_license
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.remove_license_element).to exist
- # end
- # @return [Watir::Button] The raw +Button+ element
- def remove_license_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_remove_license
- # end
- # @return [Boolean] true if the +remove_license+ element is present on the page
- def remove_license?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +button :confirm_remove_license+
- # Clicks +confirm_remove_license+
- def confirm_remove_license
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.confirm_remove_license_element).to exist
- # end
- # @return [Watir::Button] The raw +Button+ element
- def confirm_remove_license_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_confirm_remove_license
- # end
- # @return [Boolean] true if the +confirm_remove_license+ element is present on the page
- def confirm_remove_license?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +td :plan+
- # @return [String] The text content or value of +plan+
- def plan
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.plan_element).to exist
- # end
- # @return [Watir::Td] The raw +Td+ element
- def plan_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_plan
- # end
- # @return [Boolean] true if the +plan+ element is present on the page
- def plan?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +td :name+
- # @return [String] The text content or value of +name+
- def name
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.name_element).to exist
- # end
- # @return [Watir::Td] The raw +Td+ element
- def name_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_name
- # end
- # @return [Boolean] true if the +name+ element is present on the page
- def name?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +td :company+
- # @return [String] The text content or value of +company+
- def company
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.company_element).to exist
- # end
- # @return [Watir::Td] The raw +Td+ element
- def company_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_company
- # end
- # @return [Boolean] true if the +company+ element is present on the page
- def company?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +td :email+
- # @return [String] The text content or value of +email+
- def email
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.email_element).to exist
- # end
- # @return [Watir::Td] The raw +Td+ element
- def email_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_email
- # end
- # @return [Boolean] true if the +email+ element is present on the page
- def email?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +h2 :users_in_subscription+
- # @return [String] The text content or value of +users_in_subscription+
- def users_in_subscription
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.users_in_subscription_element).to exist
- # end
- # @return [Watir::H2] The raw +H2+ element
- def users_in_subscription_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_users_in_subscription
- # end
- # @return [Boolean] true if the +users_in_subscription+ element is present on the page
- def users_in_subscription?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +table :subscription_history+
- # @return [String] The text content or value of +subscription_history+
- def subscription_history
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.subscription_history_element).to exist
- # end
- # @return [Watir::Table] The raw +Table+ element
- def subscription_history_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_subscription_history
- # end
- # @return [Boolean] true if the +subscription_history+ element is present on the page
- def subscription_history?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +div :no_valid_license_alert+
- # @return [String] The text content or value of +no_valid_license_alert+
- def no_valid_license_alert
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.no_valid_license_alert_element).to exist
- # end
- # @return [Watir::Div] The raw +Div+ element
- def no_valid_license_alert_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_no_valid_license_alert
- # end
- # @return [Boolean] true if the +no_valid_license_alert+ element is present on the page
- def no_valid_license_alert?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +h3 :no_active_subscription_title+
- # @return [String] The text content or value of +no_active_subscription_title+
- def no_active_subscription_title
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.no_active_subscription_title_element).to exist
- # end
- # @return [Watir::H3] The raw +H3+ element
- def no_active_subscription_title_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_no_active_subscription_title
- # end
- # @return [Boolean] true if the +no_active_subscription_title+ element is present on the page
- def no_active_subscription_title?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
- end
- end
- end
-end
diff --git a/scripts/internal_events/cli.rb b/scripts/internal_events/cli.rb
index 13ba1a626e8..01ea1984d10 100755
--- a/scripts/internal_events/cli.rb
+++ b/scripts/internal_events/cli.rb
@@ -102,7 +102,7 @@ class Cli
def proceed_to_metric_definition
new_page!
- cli.say format_info("Amazing! The next step is adding a new metric! (~8 min)\n")
+ cli.say format_info("Amazing! The next step is adding a new metric! (~8-15 min)\n")
return not_ready_error('New Metric') unless cli.yes?(format_prompt('Ready to start?'))
diff --git a/scripts/internal_events/cli/event.rb b/scripts/internal_events/cli/event.rb
index 3d8dca1ea2d..4fe54d5932d 100755
--- a/scripts/internal_events/cli/event.rb
+++ b/scripts/internal_events/cli/event.rb
@@ -23,7 +23,15 @@ module InternalEventsCli
introduced_by_url: 'TODO'
}.freeze
- ExistingEvent = Struct.new(*NEW_EVENT_FIELDS, :file_path, keyword_init: true)
+ ExistingEvent = Struct.new(*NEW_EVENT_FIELDS, :file_path, keyword_init: true) do
+ def identifiers
+ self[:identifiers] || []
+ end
+
+ def available_filters
+ additional_properties&.keys || []
+ end
+ end
NewEvent = Struct.new(*NEW_EVENT_FIELDS, keyword_init: true) do
def formatted_output
diff --git a/scripts/internal_events/cli/helpers.rb b/scripts/internal_events/cli/helpers.rb
index 95672325652..89645ca2dd3 100755
--- a/scripts/internal_events/cli/helpers.rb
+++ b/scripts/internal_events/cli/helpers.rb
@@ -17,6 +17,7 @@ module InternalEventsCli
include MetricOptions
MILESTONE = File.read('VERSION').strip.match(/(\d+\.\d+)/).captures.first
+ NAME_REGEX = /\A[a-z0-9_]+\z/
def new_page!(page = nil, total = nil, steps = [])
cli.say TTY::Cursor.clear_screen
diff --git a/scripts/internal_events/cli/helpers/cli_inputs.rb b/scripts/internal_events/cli/helpers/cli_inputs.rb
index 62a6618d4a2..0de1da95979 100755
--- a/scripts/internal_events/cli/helpers/cli_inputs.rb
+++ b/scripts/internal_events/cli/helpers/cli_inputs.rb
@@ -17,14 +17,41 @@ module InternalEventsCli
end
end
- def prompt_for_text(message, value = nil)
- help_message = "(enter to #{value ? 'submit' : 'skip'})"
+ # Prompts the user to input text. Prefer this over calling cli#ask directly (so styling is consistent).
+ #
+ #
+ # @return [String, nil] user-provided text
+ # @param message [String] a single line prompt/question or last line of a prompt
+ # @param value [String, nil] prepopulated as the answer which user can accept/modify
+ # @option multiline [Boolean] indicates that any help text or prompt prefix will be printed on another line
+ # before calling #prompt_for_text --> ex) see MetricDefiner#prompt_for_description
+ # @yield [TTY::Prompt::Question]
+ # @see https://github.com/piotrmurach/tty-prompt?tab=readme-ov-file#21-ask
+ def prompt_for_text(message, value = nil, multiline: false, **opts)
+ prompt = message.dup # mutable for concat in #ask callback
- cli.ask(
- "#{message} #{format_help(help_message)}",
- value: value || '',
- **input_opts
- )
+ options = { **input_opts, **opts }
+ value ||= options.delete(:value)
+ options.delete(:prefix) if multiline
+
+ cli.ask(prompt, **options) do |q|
+ q.value(value) if value
+
+ yield q if block_given?
+
+ if multiline
+ # wrap error messages so they render nicely with prompt
+ q.messages.each do |key, error|
+ closing_text = "\n#{format_error('<<|')}" if error.lines.length > 1
+
+ q.messages[key] = [error, closing_text, "\n\n\n"].join('')
+ end
+ else
+ # append help text only if this line includes the formatted 'prompt' prefix,
+ # otherwise depend on the caller to print the help text if needed
+ prompt.concat(" #{q.required ? input_required_text : input_optional_text(value)}")
+ end
+ end
end
def input_opts
@@ -35,12 +62,20 @@ module InternalEventsCli
{ prefix: format_prompt('Yes/No: ') }
end
+ # Provide to cli#select as kwargs for consistent style/ux
def select_opts
- { prefix: format_prompt('Select one: '), cycle: true, show_help: :always }
+ {
+ prefix: format_prompt('Select one: '),
+ cycle: true,
+ show_help: :always,
+ # Strip colors so #format_selection is applied uniformly
+ active_color: ->(choice) { format_selection(clear_format(choice)) }
+ }
end
+ # Provide to cli#multiselect as kwargs for consistent style/ux
def multiselect_opts
- { prefix: format_prompt('Select multiple: '), cycle: true, show_help: :always, min: 1 }
+ { **select_opts, prefix: format_prompt('Select multiple: '), min: 1 }
end
# Accepts a number of lines occupied by text, so remaining
@@ -52,10 +87,18 @@ module InternalEventsCli
}
end
+ # Help text to use with required, multiline cli#ask prompts.
+ # Otherwise, prefer #prompt_for_text.
def input_required_text
format_help("(leave blank for help)")
end
+ # Help text to use with optional, multiline cli#ask prompts.
+ # Otherwise, prefer #prompt_for_text.
+ def input_optional_text(value)
+ format_help("(enter to #{value ? 'submit' : 'skip'})")
+ end
+
def disableable_option(value:, disabled:, name: nil)
should_disable = yield
name ||= value
diff --git a/scripts/internal_events/cli/helpers/files.rb b/scripts/internal_events/cli/helpers/files.rb
index 0d27ddd82a3..f567823ead5 100755
--- a/scripts/internal_events/cli/helpers/files.rb
+++ b/scripts/internal_events/cli/helpers/files.rb
@@ -4,6 +4,8 @@
module InternalEventsCli
module Helpers
module Files
+ MAX_FILENAME_LENGTH = 100
+
def prompt_to_save_file(filepath, content)
cli.say <<~TEXT.chomp
#{format_info('Preparing to generate definition with these attributes:')}
diff --git a/scripts/internal_events/cli/helpers/formatting.rb b/scripts/internal_events/cli/helpers/formatting.rb
index c026c99dd9a..75397416a8c 100755
--- a/scripts/internal_events/cli/helpers/formatting.rb
+++ b/scripts/internal_events/cli/helpers/formatting.rb
@@ -31,10 +31,33 @@ module InternalEventsCli
pastel.red(string)
end
+ # Strips all existing color/text style
+ def clear_format(string)
+ pastel.strip(string)
+ end
+
def format_heading(string)
[divider, pastel.cyan(string), divider].join("\n")
end
+ # Used for grouping prompts that occur on the same screen
+ # or as part of the same step of a flow.
+ #
+ # Counter is exluded if total is 1.
+ # The subject's formatting is extended to the counter.
+ #
+ # @return [String] ex) -- EATING COOKIES (2/3): Chocolate Chip --
+ # @param subject [String] describes task generically ex) EATING COOKIES
+ # @param item [String] describes specific context ex) Chocolate Chip
+ # @param count [Integer] ex) 2
+ # @param total [Integer] ex) 3
+ def format_subheader(subject, item, count, total)
+ formatting_end = "\e[0m"
+ suffix = formatting_end if subject[-formatting_end.length..] == formatting_end
+
+ "-- #{[subject.chomp(formatting_end), counter(count, total)].compact.join(' ')}:#{suffix} #{item} --"
+ end
+
def format_prefix(prefix, string)
string.lines.map { |line| line.prepend(prefix) }.join
end
@@ -59,8 +82,11 @@ module InternalEventsCli
"#{status}\n|==#{complete}>#{incomplete}|\n"
end
+ # Formats a counter if there's anything to count
+ #
+ # @return [String, nil] ex) "(3/4)""
def counter(idx, total)
- format_prompt("(#{idx + 1}/#{total})") if total > 1
+ "(#{idx + 1}/#{total})" if total > 1
end
private
diff --git a/scripts/internal_events/cli/helpers/metric_options.rb b/scripts/internal_events/cli/helpers/metric_options.rb
index 711458839e3..ccdb6eb1ebb 100755
--- a/scripts/internal_events/cli/helpers/metric_options.rb
+++ b/scripts/internal_events/cli/helpers/metric_options.rb
@@ -11,24 +11,38 @@ module InternalEventsCli
nil => "%s occurrences"
}.freeze
+ # Creates a list of metrics to be used as options in a
+ # select/multiselect menu; existing metrics and metrics for
+ # unavailable identifiers are marked as disabled
+ #
+ # @param events [Array]
+ # @return [Array] hash (compact) has keys/values:
+ # value: [Array]
+ # name: [String] formatted description of the metrics
+ # disabled: [String] reason metrics are disabled
def get_metric_options(events)
actions = events.map(&:action)
options = get_all_metric_options(actions)
identifiers = get_identifiers_for_events(events)
metric_name = format_metric_name_for_events(events)
+ filter_name = format_filter_options_for_events(events)
+
+ options.reject!(&:filters_expected?) unless filter_name
options = options.group_by do |metric|
[
metric.identifier.value,
conflicting_metric_exists?(metric),
+ metric.filters_expected?,
metric.time_frame.value == 'all'
]
end
- options.map do |(identifier, defined, _), metrics|
+ options.map do |(identifier, defined, filtered, _), metrics|
format_metric_option(
identifier,
metric_name,
+ (filter_name if filtered),
metrics,
defined: defined,
supported: [*identifiers, nil].include?(identifier)
@@ -38,50 +52,103 @@ module InternalEventsCli
private
+ # Lists all potential metrics supported in service ping,
+ # ordered by: identifier > filters > time_frame
+ #
+ # @param actions [Array] event names
+ # @return [Array]
def get_all_metric_options(actions)
[
Metric.new(actions: actions, time_frame: '28d', identifier: 'user'),
Metric.new(actions: actions, time_frame: '7d', identifier: 'user'),
+ Metric.new(actions: actions, time_frame: '28d', identifier: 'user', filters: []),
+ Metric.new(actions: actions, time_frame: '7d', identifier: 'user', filters: []),
Metric.new(actions: actions, time_frame: '28d', identifier: 'project'),
Metric.new(actions: actions, time_frame: '7d', identifier: 'project'),
+ Metric.new(actions: actions, time_frame: '28d', identifier: 'project', filters: []),
+ Metric.new(actions: actions, time_frame: '7d', identifier: 'project', filters: []),
Metric.new(actions: actions, time_frame: '28d', identifier: 'namespace'),
Metric.new(actions: actions, time_frame: '7d', identifier: 'namespace'),
+ Metric.new(actions: actions, time_frame: '28d', identifier: 'namespace', filters: []),
+ Metric.new(actions: actions, time_frame: '7d', identifier: 'namespace', filters: []),
Metric.new(actions: actions, time_frame: '28d'),
Metric.new(actions: actions, time_frame: '7d'),
- Metric.new(actions: actions, time_frame: 'all')
+ Metric.new(actions: actions, time_frame: '28d', filters: []),
+ Metric.new(actions: actions, time_frame: '7d', filters: []),
+ Metric.new(actions: actions, time_frame: 'all'),
+ Metric.new(actions: actions, time_frame: 'all', filters: [])
]
end
+ # Very brief summary of the provided events to use in a basic
+ # description of the metric; does not account for filters
+ #
+ # @param events [Array]
+ # @return [String]
def format_metric_name_for_events(events)
return events.first.action if events.length == 1
"any of #{events.length} events"
end
- # Get only the identifiers in common for all events
- def get_identifiers_for_events(events)
- events.map(&:identifiers).reduce(&:&) || []
+ # Formats the list of the additional properties available
+ # across any of the events
+ #
+ # @param events [Array]
+ # @return [String] ex) "label/property"
+ def format_filter_options_for_events(events)
+ available_filters = events.flat_map(&:available_filters).uniq
+
+ available_filters.join('/') if available_filters.any?
end
+ # Get only the identifiers in common for all events
+ #
+ # @param events [Array]
+ # @return [Array]
+ def get_identifiers_for_events(events)
+ events.map(&:identifiers).reduce(&:&)
+ end
+
+ # Checks if there's an existing metric which has the same
+ # properties as the new one
+ #
+ # @param new_metric [NewMetric]
+ # @return [Boolean]
def conflicting_metric_exists?(new_metric)
+ # metrics with filters are conflict-free until new filters are defined
+ return false if new_metric.filters_expected?
+
cli.global.metrics.any? do |existing_metric|
existing_metric.actions == new_metric.actions &&
existing_metric.time_frame == new_metric.time_frame.value &&
- existing_metric.identifier == new_metric.identifier.value
+ existing_metric.identifier == new_metric.identifier.value &&
+ !existing_metric.filtered?
end
end
- def format_metric_option(identifier, event_name, metrics, defined:, supported:)
+ # Formats & assembles a single select/multiselect menu item,
+ #
+ # @param identifier [String] user/project/namespace (must support unique metrics)
+ # @param event_name [String]
+ # @param filter_name [String]
+ # @param metrics [Array]
+ # @option defined [Boolean]
+ # @option supported [Boolean]
+ # @return [Hash] see #get_metric_options for format
+ def format_metric_option(identifier, event_name, filter_name, metrics, defined:, supported:)
time_frame = metrics.map { |metric| metric.time_frame.description }.join('/')
unique_by = "unique #{identifier}s " if identifier
event_phrase = EVENT_PHRASES[identifier] % event_name
+ filter_phrase = " where filtered" if filter_name
if supported && !defined
+ filter_phrase = " #{format_info('where')} #{filter_name} is..." if filter_name
time_frame = format_info(time_frame)
unique_by = format_info(unique_by)
end
- name = "#{time_frame} count of #{unique_by}[#{event_phrase}]"
+ name = "#{time_frame} count of #{unique_by}[#{event_phrase}]#{filter_phrase}"
if supported && defined
disabled = format_warning("(already defined)")
diff --git a/scripts/internal_events/cli/metric.rb b/scripts/internal_events/cli/metric.rb
index 937a3701d19..0dd2c70c28c 100755
--- a/scripts/internal_events/cli/metric.rb
+++ b/scripts/internal_events/cli/metric.rb
@@ -48,12 +48,22 @@ module InternalEventsCli
events&.map { |event| event['name'] } # rubocop:disable Rails/Pluck -- not rails
end
+ def filters
+ events&.map do |event|
+ [event['name'], event['filter'] || {}]
+ end
+ end
+
+ def filtered?
+ !!filters&.any? { |(_action, filter)| filter&.any? }
+ end
+
def time_frame
self[:time_frame] || 'all'
end
end
- NewMetric = Struct.new(*NEW_METRIC_FIELDS, :identifier, :actions, :key, keyword_init: true) do
+ NewMetric = Struct.new(*NEW_METRIC_FIELDS, :identifier, :actions, :key, :filters, keyword_init: true) do
def formatted_output
METRIC_DEFAULTS
.merge(to_h.compact)
@@ -101,13 +111,26 @@ module InternalEventsCli
Metric::Key.new(self[:key] || actions, time_frame, identifier)
end
- def events
- self[:events] || actions.map { |action| event_params(action) }
+ def filters
+ Metric::Filters.new(self[:filters])
end
- def event_params(action)
+ # Returns value for the `events` key in the metric definition.
+ # Requires #actions or #filters to be set by the caller first.
+ #
+ # @return [Hash]
+ def events
+ if filters.assigned?
+ self[:filters].map { |(action, filter)| event_params(action, filter) }
+ else
+ actions.map { |action| event_params(action) }
+ end
+ end
+
+ def event_params(action, filter = nil)
params = { 'name' => action }
params['unique'] = "#{identifier.value}.id" if identifier.value
+ params['filter'] = filter if filter&.any?
params
end
@@ -116,20 +139,39 @@ module InternalEventsCli
self[:actions] || []
end
+ # How to interpretting different values for filters:
+ # nil --> not expected, assigned or filtered
+ # (metric not initialized with filters)
+ # [] --> both expected and filtered
+ # (metric initialized with filters, but not yet assigned by user)
+ # [['event', {}]] --> not expected, assigned or filtered
+ # (filters were expected, but then skipped by user)
+ # [['event', { 'label' => 'a' }]] --> both assigned and filtered
+ # (filters exist for any event; user is done assigning)
+ def filtered?
+ filters.assigned? || filters.expected?
+ end
+
+ def filters_expected?
+ filters.expected?
+ end
+
def description_prefix
[time_frame.description, identifier.description].join(' ')
end
+ # Provides simplified but technically accurate description
def technical_description
- simple_event_list = actions.join(' or ')
+ event_name = actions.first if events.length == 1 && !filtered?
+ event_name ||= 'the selected events'
- case identifier
+ case identifier.value
when 'user'
- "#{description_prefix} who triggered #{simple_event_list}"
+ "#{description_prefix} who triggered #{event_name}"
when 'project', 'namespace'
- "#{description_prefix} where #{simple_event_list} occurred"
+ "#{description_prefix} where #{event_name} occurred"
else
- "#{description_prefix} #{simple_event_list} occurrences"
+ "#{description_prefix} #{event_name} occurrences"
end
end
@@ -181,11 +223,13 @@ module InternalEventsCli
end
Key = Struct.new(:events, :time_frame, :identifier) do
- def value
+ # @param name_to_display [String] return the key with the
+ # provided name instead of a list of event names
+ def value(name_to_display = nil)
[
'count',
identifier&.key_path,
- name_for_events,
+ name_to_display || name_for_events,
time_frame&.key_path
].compact.join('_')
end
@@ -194,7 +238,13 @@ module InternalEventsCli
"#{prefix}.#{value}"
end
+ # Refers to the middle portion of a metric's `key_path`
+ # pertaining to the relevent events; This does not include
+ # identifier/time_frame/etc
def name_for_events
+ # user may have defined a different name for events
+ return events unless events.respond_to?(:join)
+
events.join('_and_')
end
@@ -207,6 +257,28 @@ module InternalEventsCli
end
end
+ Filters = Struct.new(:filters) do
+ def expected?
+ filters == []
+ end
+
+ def assigned?
+ !!filters&.any? { |(_action, filter)| filter.any? }
+ end
+
+ def descriptions
+ Array(filters).filter_map do |(action, filter)|
+ next action if filter.none?
+
+ "#{action}(#{describe_filter(filter)})"
+ end.sort_by(&:length)
+ end
+
+ def describe_filter(filter)
+ filter.map { |k, v| "#{k}=#{v}" }.join(',')
+ end
+ end
+
def self.parse(**args)
ExistingMetric.new(**args)
end
diff --git a/scripts/internal_events/cli/metric_definer.rb b/scripts/internal_events/cli/metric_definer.rb
index 42e5790d9a2..7993ad28d4d 100755
--- a/scripts/internal_events/cli/metric_definer.rb
+++ b/scripts/internal_events/cli/metric_definer.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require_relative './helpers'
+require_relative './text'
module InternalEventsCli
class MetricDefiner
@@ -20,12 +21,28 @@ module InternalEventsCli
'Save files'
].freeze
+ NAME_REQUIREMENT_REASONS = {
+ filters: {
+ text: 'Metrics using filters are too complex for default naming.',
+ help: Text::METRIC_NAME_FILTER_HELP
+ },
+ length: {
+ text: 'The default filename will be too long.',
+ help: Text::METRIC_NAME_LENGTH_HELP
+ },
+ conflict: {
+ text: 'The default key path is already in use.',
+ help: Text::METRIC_NAME_CONFLICT_HELP
+ }
+ }.freeze
+
attr_reader :cli
def initialize(cli, starting_event = nil)
@cli = cli
@selected_event_paths = Array(starting_event)
@metrics = []
+ @selected_filters = {}
end
def run
@@ -35,10 +52,11 @@ module InternalEventsCli
return unless @selected_event_paths.any?
prompt_for_metrics
+ prompt_for_event_filters
return unless @metrics.any?
- prompt_for_description
+ prompt_for_descriptions
defaults = prompt_for_copying_event_properties
prompt_for_product_group(defaults)
prompt_for_url(defaults)
@@ -49,6 +67,8 @@ module InternalEventsCli
private
+ # ----- Memoization Helpers -----------------
+
def events
@events ||= events_by_filepath(@selected_event_paths)
end
@@ -57,6 +77,8 @@ module InternalEventsCli
@selected_events ||= events.values_at(*@selected_event_paths)
end
+ # ----- Prompts -----------------------------
+
def prompt_for_metric_type
return if @selected_event_paths.any?
@@ -112,8 +134,15 @@ module InternalEventsCli
end
new_page!(3, 9, STEPS)
+ cli.say format_info('SELECTED EVENTS')
+ cli.say selected_events_filter_options.join
+ cli.say "\n"
- @metrics = cli.select('Which metrics do you want to add?', eligible_metrics, **select_opts)
+ @metrics = cli.select(
+ 'Which metrics do you want to add?',
+ eligible_metrics,
+ **select_opts,
+ per_page: 20)
assign_shared_attrs(:actions, :milestone) do
{
@@ -123,50 +152,70 @@ module InternalEventsCli
end
end
- def prompt_for_description
- new_page!(4, 9, STEPS)
+ def prompt_for_event_filters
+ return if @metrics.none?(&:filters_expected?)
- cli.say Text::METRIC_DESCRIPTION_INTRO
- cli.say selected_event_descriptions.join('')
+ event_count = selected_events.length
+ previous_inputs = {
+ 'label' => nil,
+ 'property' => nil,
+ 'value' => nil
+ }
- base_description = nil
+ event_filters = selected_events.dup.flat_map.with_index do |event, idx|
+ print_event_filter_header(event, idx, event_count)
- @metrics.each_with_index do |metric, idx|
- multiline_prompt = [
- counter(idx, @metrics.length),
- format_prompt("Complete the text:"),
- "How would you describe this metric to a non-technical person?",
- input_required_text,
- "\n\n Technical description: #{metric.technical_description}"
- ].compact.join(' ')
+ next if deselect_nonfilterable_event?(event) # prompts user
- last_line_of_prompt = "\n Finish the description: #{format_info("#{metric.description_prefix}...")}"
-
- cli.say("\n")
- cli.say(multiline_prompt)
-
- description_help_message = [
- Text::METRIC_DESCRIPTION_HELP,
- multiline_prompt,
- "\n\n"
- ].join("\n")
-
- # Reassign base_description so the next metric's default value is their own input
- base_description = cli.ask(last_line_of_prompt, value: base_description.to_s) do |q|
- q.required true
- q.modify :trim
- q.messages[:required?] = description_help_message
+ filter_values = event.additional_properties&.filter_map do |property, _|
+ prompt_for_property_filter(
+ event.action,
+ property,
+ previous_inputs[property]
+ )
end
- cli.say("\n") # looks like multiline input, but isn't. Spacer improves clarity.
+ previous_inputs.merge!(@selected_filters[event.action] || {})
- metric.description = "#{metric.description_prefix} #{base_description}"
- end
+ find_filter_permutations(event.action, filter_values)
+ end.compact
+
+ bulk_assign(filters: event_filters)
end
- def selected_event_descriptions
- @selected_event_descriptions ||= selected_events.map do |event|
- " #{event.action} - #{format_selection(event.description)}\n"
+ def prompt_for_descriptions
+ default_description = nil
+ default_key = nil
+
+ separate_page_per_metric = @metrics.any? do |metric|
+ name_requirement_reason(metric)
+ end
+
+ @metrics.each_with_index do |metric, idx|
+ if idx == 0 || separate_page_per_metric
+ new_page!(4, 9, STEPS)
+
+ cli.say Text::METRIC_DESCRIPTION_INTRO
+ cli.say selected_event_descriptions.join
+ end
+
+ cli.say "\n"
+ cli.say format_prompt(format_subheader(
+ 'DESCRIBING METRIC',
+ metric.technical_description,
+ idx,
+ @metrics.length
+ ))
+
+ prompt_for_description(metric, default_description).tap do |description|
+ default_description = description
+ metric.description = "#{metric.description_prefix} #{description}"
+ end
+
+ prompt_for_metric_name(metric, default_key)&.tap do |key|
+ default_key = key
+ metric.key = key
+ end
end
end
@@ -198,26 +247,6 @@ module InternalEventsCli
shared_values
end
- def collect_values_for_shared_event_properties
- fields = Hash.new { |h, k| h[k] = [] }
-
- selected_events.each do |event|
- fields[:introduced_by_url] << event.introduced_by_url
- fields[:product_group] << event.product_group
- fields[:stage] << find_stage(event.product_group)
- fields[:section] << find_section(event.product_group)
- fields[:distribution] << event.distributions&.sort
- fields[:tier] << event.tiers&.sort
- end
-
- # Keep event values if every selected event is the same
- fields.each_with_object({}) do |(attr, values), defaults|
- next unless values.compact.uniq.length == 1
-
- defaults[attr] ||= values.first
- end
- end
-
def prompt_for_product_group(defaults)
assign_shared_attr(:product_group) do
new_page!(6, 9, STEPS)
@@ -231,7 +260,7 @@ module InternalEventsCli
new_page!(7, 9, STEPS)
prompt_for_text(
- "Which MR URL introduced the metric?",
+ 'Which MR URL introduced the metric?',
defaults[:introduced_by_url]
)
end
@@ -257,7 +286,8 @@ module InternalEventsCli
@metrics.map.with_index do |metric, idx|
new_page!(9, 9, STEPS) # Repeat the same step number but increment metric counter
- cli.say format_prompt("SAVING FILE #{counter(idx, @metrics.length)}: #{metric.technical_description}\n")
+ cli.say format_prompt(format_subheader('SAVING FILE', metric.description, idx, @metrics.length))
+ cli.say "\n"
prompt_to_save_file(metric.file_path, metric.formatted_output)
end
@@ -304,6 +334,193 @@ module InternalEventsCli
end
end
+ # ----- Prompt-specific Helpers -------------
+
+ # Helper for #prompt_for_metrics
+ def selected_events_filter_options
+ filterable_events_selected = selected_events.any? { |event| event.additional_properties&.any? }
+
+ selected_events.map do |event|
+ filters = event.additional_properties&.keys
+ filter_phrase = if filters
+ " (filterable by #{filters&.join(', ')})"
+ elsif filterable_events_selected
+ ' -- not filterable'
+ end
+
+ " - #{event.action}#{format_help(filter_phrase)}\n"
+ end
+ end
+
+ # Helper for #prompt_for_event_filters
+ def print_event_filter_header(event, idx, total)
+ cli.say "\n"
+ cli.say format_info(format_subheader('SETTING EVENT FILTERS', event.action, idx, total))
+
+ return unless event.additional_properties&.any?
+
+ event_filter_options = event.additional_properties.map do |property, attrs|
+ " #{property}: #{attrs['description']}\n"
+ end
+
+ cli.say event_filter_options.join
+ end
+
+ # Helper for #prompt_for_event_filters
+ def deselect_nonfilterable_event?(event)
+ cli.say "\n"
+
+ return false if event.additional_properties&.any?
+ return false if cli.yes?("This event is not filterable. Should it be included in the metric?", **yes_no_opts)
+
+ selected_events.delete(event)
+ bulk_assign(actions: selected_events.map(&:action).sort)
+
+ true
+ end
+
+ # Helper for #prompt_for_event_filters
+ def prompt_for_property_filter(action, property, default)
+ formatted_prop = format_info(property)
+ prompt = "Count where #{formatted_prop} equals any of (comma-sep):"
+
+ inputs = prompt_for_text(prompt, default, **input_opts) do |q|
+ if property == 'value'
+ 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
+ q.convert ->(input) { input.split(',').map(&:strip).uniq }
+ end
+ end
+
+ return unless inputs&.any?
+
+ @selected_filters[action] ||= {}
+ @selected_filters[action][property] = inputs.join(',')
+
+ inputs.map { |input| { property => input } }.uniq
+ end
+
+ # Helper for #prompt_for_event_filters
+ #
+ # Gets all the permutations of the provided property values.
+ # @param filters [Array] ex) [{ 'label' => 'red' }, { 'label' => 'blue' }, { value => 16 }]
+ # @return ex) [{ 'label' => 'red', value => 16 }, { 'label' => 'blue', value => 16 }]
+ def find_filter_permutations(action, filters)
+ # Define a filter for all events, regardless of the available props so NewMetric#events is correct
+ return [[action, {}]] unless filters&.any?
+
+ # Uses proc syntax to avoid spliting & type-checking `filters`
+ :product.to_proc.call(*filters).map do |filter|
+ [action, filter.reduce(&:merge)]
+ end
+ end
+
+ # Helper for #prompt_for_descriptions
+ def selected_event_descriptions
+ selected_events.map do |event|
+ filters = @selected_filters[event.action]
+
+ if filters&.any?
+ filter_phrase = filters.map { |k, v| "#{k}=#{v}" }.join(' ')
+ filter_phrase = format_help("(#{filter_phrase})")
+ end
+
+ " #{event.action}#{filter_phrase} - #{format_selection(event.description)}\n"
+ end
+ end
+
+ # Helper for #prompt_for_descriptions
+ def prompt_for_description(metric, default)
+ description_start = format_info("#{metric.description_prefix}...")
+
+ cli.say <<~TEXT
+
+ #{input_opts[:prefix]} How would you describe this metric to a non-technical person? #{input_required_text}
+
+ TEXT
+
+ prompt_for_text(" Finish the description: #{description_start}", default, multiline: true) do |q|
+ q.required true
+ q.messages[:required?] = Text::METRIC_DESCRIPTION_HELP
+ end
+ end
+
+ # Helper for #prompt_for_descriptions
+ def prompt_for_metric_name(metric, default)
+ name_reason = name_requirement_reason(metric)
+ default_name = metric.key.value
+ display_name = metric.key.value("\e[0m[REPLACE ME]\e[36m")
+ empty_name = metric.key.value('')
+
+ return unless name_reason
+
+ cli.say <<~TEXT
+
+ #{input_opts[:prefix]} #{name_reason[:text]} How should we refererence this metric? #{input_required_text}
+
+ ID: #{format_info(display_name)}
+ Filename: #{format_info(display_name)}#{format_info('.yml')}
+
+ TEXT
+
+ max_length = MAX_FILENAME_LENGTH - "#{empty_name}.yml".length
+ help_tokens = { name: default_name, count: max_length }
+
+ prompt_for_text(' Replace with: ', default, multiline: true) do |q|
+ q.required true
+ q.messages[:required?] = name_reason[:help] % help_tokens
+ q.messages[:valid?] = Text::METRIC_NAME_ERROR % help_tokens
+ q.validate ->(input) do
+ input.length <= max_length &&
+ input.match?(NAME_REGEX) &&
+ !conflicting_key_path?(metric.key.value(input))
+ end
+ end
+ end
+
+ # Helper for #prompt_for_descriptions
+ def name_requirement_reason(metric)
+ if metric.filters.assigned?
+ NAME_REQUIREMENT_REASONS[:filters]
+ elsif metric.file_name.length > MAX_FILENAME_LENGTH
+ NAME_REQUIREMENT_REASONS[:length]
+ elsif conflicting_key_path?(metric.key_path)
+ NAME_REQUIREMENT_REASONS[:conflict]
+ end
+ end
+
+ # Helper for #prompt_for_descriptions
+ def conflicting_key_path?(key_path)
+ cli.global.metrics.any? do |existing_metric|
+ existing_metric.key_path == key_path
+ end
+ end
+
+ # Helper for #prompt_for_copying_event_properties
+ def collect_values_for_shared_event_properties
+ fields = Hash.new { |h, k| h[k] = [] }
+
+ selected_events.each do |event|
+ fields[:introduced_by_url] << event.introduced_by_url
+ fields[:product_group] << event.product_group
+ fields[:stage] << find_stage(event.product_group)
+ fields[:section] << find_section(event.product_group)
+ fields[:distribution] << event.distributions&.sort
+ fields[:tier] << event.tiers&.sort
+ end
+
+ # Keep event values if every selected event is the same
+ fields.each_with_object({}) do |(attr, values), defaults|
+ next unless values.compact.uniq.length == 1
+
+ defaults[attr] ||= values.first
+ end
+ end
+
+ # ----- Shared Helpers ----------------------
+
def assign_shared_attrs(...)
metric = @metrics.first
attrs = metric.to_h.slice(...)
diff --git a/scripts/internal_events/cli/text.rb b/scripts/internal_events/cli/text.rb
index e3807ae354f..5190f02ca3e 100755
--- a/scripts/internal_events/cli/text.rb
+++ b/scripts/internal_events/cli/text.rb
@@ -218,7 +218,7 @@ module InternalEventsCli
#{format_info('SELECTED EVENT(S):')}
TEXT
- METRIC_DESCRIPTION_HELP = <<~TEXT.freeze
+ METRIC_DESCRIPTION_HELP = <<~TEXT.chomp.freeze
#{format_warning('Required. 10+ words likely, but length may vary.')}
An event description can often be rearranged to work as a metric description.
@@ -229,5 +229,31 @@ module InternalEventsCli
Look at the event descriptions above to get ideas!
TEXT
+
+ METRIC_NAME_FILTER_HELP = <<~TEXT.freeze
+ #{format_warning('Required. Max %{count} characters. Only lowercase/numbers/underscores allowed.')}
+
+ Metrics with filters must manually define this portion of their key path.
+
+ Auto-generated key paths for metrics filters results in long & confusing naming. By defining them manually, clarity and discoverability should be better.
+ TEXT
+
+ METRIC_NAME_CONFLICT_HELP = <<~TEXT.freeze
+ #{format_warning('Required. Max %{count} characters. Only lowercase/numbers/underscores allowed.')}
+
+ Conflict! A metric with the same name already exists: %{name}
+ TEXT
+
+ METRIC_NAME_LENGTH_HELP = <<~TEXT.freeze
+ #{format_warning('Required. Max %{count} characters. Only lowercase/numbers/underscores allowed.')}
+
+ Filenames cannot exceed 100 characters. The key path (ID) is not restricted, but keeping them aligned is recommended.
+
+ If needed, you can modify the key path and filename further after saving.
+ TEXT
+
+ METRIC_NAME_ERROR = <<~TEXT.freeze
+ #{format_warning('Input is invalid. Max %{count} characters. Only lowercase/numbers/underscores allowed. Ensure this key path (ID) is not already in use.')}
+ TEXT
end
end
diff --git a/spec/factories/import_source_users.rb b/spec/factories/import_source_users.rb
index a44b0abf9d4..7361c794354 100644
--- a/spec/factories/import_source_users.rb
+++ b/spec/factories/import_source_users.rb
@@ -14,6 +14,7 @@ FactoryBot.define do
end
trait :with_reassign_to_user do
+ with_placeholder_user
reassign_to_user factory: :user
end
diff --git a/spec/features/projects/work_items/work_item_children_spec.rb b/spec/features/projects/work_items/work_item_children_spec.rb
index 71f1ceca902..52de3bcd3f7 100644
--- a/spec/features/projects/work_items/work_item_children_spec.rb
+++ b/spec/features/projects/work_items/work_item_children_spec.rb
@@ -59,6 +59,8 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
end
it 'adds a new child task', :aggregate_failures do
+ allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(104)
+
within_testid('work-item-links') do
click_button 'Add'
click_button 'New task'
@@ -77,6 +79,7 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
end
it 'removes a child task and undoing', :aggregate_failures do
+ allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(104)
within_testid('work-item-links') do
click_button 'Add'
click_button 'New task'
diff --git a/spec/fixtures/scripts/internal_events/events/secondary_event_with_additional_properties.yml b/spec/fixtures/scripts/internal_events/events/secondary_event_with_additional_properties.yml
new file mode 100644
index 00000000000..c5dc95994f6
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/events/secondary_event_with_additional_properties.yml
@@ -0,0 +1,23 @@
+---
+description: Internal Event CLI is opened
+internal_events: true
+action: internal_events_cli_opened
+identifiers:
+- project
+- namespace
+- user
+additional_properties:
+ label:
+ description: TODO
+ value:
+ description: Time the CLI ran before closing (seconds)
+product_group: analytics_instrumentation
+milestone: '16.6'
+introduced_by_url: TODO
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/spec/fixtures/scripts/internal_events/metrics/total_multi_event_some_additional_props.yml b/spec/fixtures/scripts/internal_events/metrics/total_multi_event_some_additional_props.yml
new file mode 100644
index 00000000000..33700efec1e
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/total_multi_event_some_additional_props.yml
@@ -0,0 +1,25 @@
+---
+key_path: counts.count_total_failed_usage_attempts
+description: Total count of when someone 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: all
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: internal_events_cli_closed
+- name: internal_events_cli_used
+ filter:
+ label: failure
+ property: events
diff --git a/spec/fixtures/scripts/internal_events/metrics/total_multiple_events_with_rename.yml b/spec/fixtures/scripts/internal_events/metrics/total_multiple_events_with_rename.yml
new file mode 100644
index 00000000000..79aec02e830
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/total_multiple_events_with_rename.yml
@@ -0,0 +1,24 @@
+---
+key_path: counts.count_total_cli_interactions
+description: Total count of CLI interations
+product_group: analytics_instrumentation
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: all
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: internal_events_cli_closed
+- name: internal_events_cli_opened
+- name: internal_events_cli_used
+- name: random_name
diff --git a/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_additional_props.yml b/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_additional_props.yml
new file mode 100644
index 00000000000..58fa9a5a2a3
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_additional_props.yml
@@ -0,0 +1,28 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_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
+events:
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: failure
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: incomplete
diff --git a/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_all_additional_props.yml b/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_all_additional_props.yml
new file mode 100644
index 00000000000..d03ece44b06
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_all_additional_props.yml
@@ -0,0 +1,44 @@
+---
+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
+events:
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: failure
+ property: metrics
+ value: 60
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: failure
+ property: events
+ value: 60
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: incomplete
+ property: metrics
+ value: 60
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: incomplete
+ property: events
+ value: 60
diff --git a/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_additional_props.yml b/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_additional_props.yml
new file mode 100644
index 00000000000..4503f0fe882
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_additional_props.yml
@@ -0,0 +1,28 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_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
+events:
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: failure
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: incomplete
diff --git a/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_all_additional_props.yml b/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_all_additional_props.yml
new file mode 100644
index 00000000000..47c9415c3fc
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_all_additional_props.yml
@@ -0,0 +1,44 @@
+---
+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
+events:
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: failure
+ property: metrics
+ value: 60
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: failure
+ property: events
+ value: 60
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: incomplete
+ property: metrics
+ value: 60
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: incomplete
+ property: events
+ value: 60
diff --git a/spec/fixtures/scripts/internal_events/new_metrics.yml b/spec/fixtures/scripts/internal_events/new_metrics.yml
index 1b56109c874..d9e33987476 100644
--- a/spec/fixtures/scripts/internal_events/new_metrics.yml
+++ b/spec/fixtures/scripts/internal_events/new_metrics.yml
@@ -304,3 +304,128 @@
- "1\n" # Select: Copy & continue
- "n\n" # Don't overwrite file
- "5\n" # Exit
+
+- description: Create weekly/monthly metrics for a single event, filtering on multiple values for one property and skipping another property completely
+ inputs:
+ files:
+ - path: config/events/internal_events_cli_used.yml
+ content: spec/fixtures/scripts/internal_events/events/event_with_additional_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" # Arrow down to: Weekly count of unique users where label/value is...
+ - "\n" # Select: Weekly count of unique users where label/value is...
+ - "failure, incomplete\n" # Input multiple values for "label" filter
+ - "\n" # Skip "value" filter
+ - "who tried and failed to define an internal event using the CLI\n" # Input description
+ - "failed_usage_attempts\n" # Input metric key path
+ - "\n" # Submit monthly description for weekly
+ - "\n" # Submit monthly name for weekly
+ - "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_monthly.yml
+ content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_additional_props.yml
+ - path: config/metrics/counts_7d/count_distinct_user_id_from_failed_usage_attempts_weekly.yml
+ content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_additional_props.yml
+
+- description: Create a weekly/monthly metric for a single event with all additional properties
+ inputs:
+ files:
+ - path: config/events/internal_events_cli_used.yml
+ content: spec/fixtures/scripts/internal_events/events/event_with_all_additional_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" # Arrow down to: Weekly count of unique users where label/value is...
+ - "\n" # Select: Weekly count of unique users where label/value is...
+ - "failure, incomplete\n" # Input multiple values for "label" filter
+ - "metrics, events\n" # Input multiple values for "property" filter
+ - "bad input, 16\n" # Fail "value" validation
+ - "60\n" # Input valid "value"
+ - "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_all_additional_props.yml
+ - path: config/metrics/counts_7d/count_distinct_user_id_from_failed_usage_attempts_under_60s_weekly.yml
+ content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_all_additional_props.yml
+
+- description: Create a weekly/monthly metric for multiple events with and without additional properties
+ inputs:
+ files:
+ - path: config/events/internal_events_cli_used.yml
+ content: spec/fixtures/scripts/internal_events/events/event_with_all_additional_properties.yml
+ - path: config/events/internal_events_cli_closed.yml
+ content: spec/fixtures/scripts/internal_events/events/secondary_event_with_identifiers.yml
+ - path: config/events/internal_events_cli_opened.yml
+ content: spec/fixtures/scripts/internal_events/events/ee_event_without_identifiers.yml
+ keystrokes:
+ - "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+ - "2\n" # Enum-select: Multiple events -- count occurrences of several separate events or interactions
+ - 'internal_events_cli' # Filters to the relevant events
+ - ' ' # Multi-select: internal_events_cli_closed
+ - "\e[B " # Multi-select: Arrow down and select internal_events_cli_opened
+ - "\e[B " # Multi-select: Arrow down and select internal_events_cli_used
+ - "\n" # Submit selections
+ - "\e[A\n" # Multi-select: Arrow up and select Total count of events where label/property/value is...
+ - "\n" # Select Yes: include internal_events_cli_closed in the metric anyways (no additional properties)
+ - "n\n" # Select No: remove internal_events_cli_opened from the metric (no additional properties)
+ - "failure\n" # Input value for "label" filter
+ - "events\n" # Input value for "property" filter
+ - "\n" # Skip "value" filter
+ - "when someone tried and failed to define an internal event using the CLI\n" # Input description
+ - "failed_usage_attempts\n" # Input metric key path
+ - "\n" # Submit description
+ - "1\n" # Enum-select: Copy & continue
+ - "y\n" # Create file
+ - "5\n" # Exit
+ outputs:
+ files:
+ - path: config/metrics/counts_all/count_total_failed_usage_attempts.yml
+ content: spec/fixtures/scripts/internal_events/metrics/total_multi_event_some_additional_props.yml
+
+- description: Creates metric under a user-provided name if the default is too long
+ inputs:
+ files:
+ - path: config/events/internal_events_cli_used.yml
+ content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+ - path: config/events/internal_events_cli_closed.yml
+ content: spec/fixtures/scripts/internal_events/events/secondary_event_with_identifiers.yml
+ - path: config/events/internal_events_cli_opened.yml
+ content: spec/fixtures/scripts/internal_events/events/secondary_event_with_additional_properties.yml
+ - path: config/events/random_name.yml
+ content: spec/fixtures/scripts/internal_events/events/keyboard_smashed_event.yml
+ keystrokes:
+ - "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+ - "2\n" # Enum-select: Multiple events -- count occurrences of several separate events or interactions
+ - "_events_cli" # Filter list to just our CLI events
+ - "\e[B \e[B \e[B " # Select all 3 internal_events_cli events
+ - "\u007F\u007F\u007F\u007F\u007F\u007F\u007F\u007F\u007F\u007F\u007F" # Clear filter
+ - "random_name " # Filter list to the last event & select
+ - "\n" # Submit selections
+ - "\e[A\e[A\n" # Arrow up & Select: Total count of events (no filters)
+ - "CLI interations\n" # Input description
+ - "cli_interactions\n" # Input new metric name
+ - "1\n" # Enum-select: Copy & continue
+ - "instrumentation\n" # Select the analytics instrumentation group
+ - "y\n" # Create file
+ - "5\n" # Exit
+ outputs:
+ files:
+ - path: config/metrics/counts_all/count_total_cli_interactions.yml
+ content: spec/fixtures/scripts/internal_events/metrics/total_multiple_events_with_rename.yml
diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js
index 03c1fd24bb5..bf8002430ec 100644
--- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js
+++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js
@@ -1,14 +1,16 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlEmptyState, GlAlert } from '@gitlab/ui';
+import { GlEmptyState, GlAlert, GlDrawer } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
+import WorkloadDetails from '~/kubernetes_dashboard/components/workload_details.vue';
import KubernetesOverview from '~/environments/environment_details/components/kubernetes/kubernetes_overview.vue';
import KubernetesStatusBar from '~/environments/environment_details/components/kubernetes/kubernetes_status_bar.vue';
import KubernetesAgentInfo from '~/environments/environment_details/components/kubernetes/kubernetes_agent_info.vue';
import KubernetesTabs from '~/environments/environment_details/components/kubernetes/kubernetes_tabs.vue';
import { k8sResourceType } from '~/environments/graphql/resolvers/kubernetes/constants';
+import { mockPodsTableItems } from 'jest/kubernetes_dashboard/graphql/mock_data';
import { agent, kubernetesNamespace } from '../../../graphql/mock_data';
import { mockKasTunnelUrl, fluxResourceStatus, fluxKustomization } from '../../../mock_data';
@@ -40,13 +42,13 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
'kustomize.toolkit.fluxcd.io/v1/namespaces/my-namespace/kustomizations/app';
const fluxKustomizationQuery = jest.fn().mockReturnValue({});
- const fluxHelmReleaseStatusQuery = jest.fn().mockReturnValue({});
+ const fluxHelmReleaseQuery = jest.fn().mockReturnValue({});
const createApolloProvider = () => {
const mockResolvers = {
Query: {
fluxKustomization: fluxKustomizationQuery,
- fluxHelmReleaseStatus: fluxHelmReleaseStatusQuery,
+ fluxHelmRelease: fluxHelmReleaseQuery,
},
};
@@ -74,6 +76,8 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
const findKubernetesTabs = () => wrapper.findComponent(KubernetesTabs);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findWorkloadDetails = () => wrapper.findComponent(WorkloadDetails);
describe('when the agent data is present', () => {
it('renders kubernetes agent info', () => {
@@ -150,7 +154,7 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
it("doesn't request Kustomizations and HelmReleases", () => {
expect(fluxKustomizationQuery).not.toHaveBeenCalled();
- expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
+ expect(fluxHelmReleaseQuery).not.toHaveBeenCalled();
});
it('provides empty `fluxResourceStatus` to KubernetesStatusBar', () => {
@@ -181,7 +185,7 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
});
it("doesn't request HelmRelease resource status", () => {
- expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
+ expect(fluxHelmReleaseQuery).not.toHaveBeenCalled();
});
});
@@ -194,7 +198,7 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
});
it('requests the HelmRelease resource status', () => {
- expect(fluxHelmReleaseStatusQuery).toHaveBeenCalledWith(
+ expect(fluxHelmReleaseQuery).toHaveBeenCalledWith(
{},
expect.objectContaining({
configuration,
@@ -215,7 +219,7 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
const mockResolvers = {
Query: {
fluxKustomization: jest.fn().mockReturnValue(fluxKustomization),
- fluxHelmReleaseStatus: fluxHelmReleaseStatusQuery,
+ fluxHelmRelease: fluxHelmReleaseQuery,
},
};
@@ -245,7 +249,7 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
const mockResolvers = {
Query: {
fluxKustomization: jest.fn().mockRejectedValueOnce(error),
- fluxHelmReleaseStatus: jest.fn().mockRejectedValueOnce(error),
+ fluxHelmRelease: jest.fn().mockRejectedValueOnce(error),
},
};
@@ -268,6 +272,90 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
});
});
+ describe('resource details drawer', () => {
+ it('is closed by default', () => {
+ wrapper = createWrapper();
+
+ expect(findDrawer().props('open')).toBe(false);
+ });
+
+ describe('when receives show-resource-details event from the tabs', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ findKubernetesTabs().vm.$emit('show-resource-details', mockPodsTableItems[0]);
+ });
+
+ it('opens the drawer', () => {
+ expect(findDrawer().props('open')).toBe(true);
+ });
+
+ it('provides the resource details to the drawer', () => {
+ expect(findWorkloadDetails().props('item')).toEqual(mockPodsTableItems[0]);
+ });
+
+ it('renders a title with the selected item name', () => {
+ expect(findDrawer().text()).toContain(mockPodsTableItems[0].name);
+ });
+
+ it('is closed when clicked on a cross button', async () => {
+ expect(findDrawer().props('open')).toBe(true);
+
+ await findDrawer().vm.$emit('close');
+ expect(findDrawer().props('open')).toBe(false);
+ });
+
+ it('is closed on remove-selection event', async () => {
+ expect(findDrawer().props('open')).toBe(true);
+
+ await findKubernetesTabs().vm.$emit('remove-selection');
+ expect(findDrawer().props('open')).toBe(false);
+ });
+ });
+
+ describe('when receives show-flux-resource-details event from the status bar', () => {
+ beforeEach(async () => {
+ const createApolloProviderWithKustomizations = () => {
+ const mockResolvers = {
+ Query: {
+ fluxKustomization: jest.fn().mockReturnValue(fluxKustomization),
+ fluxHelmRelease: fluxHelmReleaseQuery,
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+ wrapper = createWrapper({
+ fluxResourcePath: kustomizationResourcePath,
+ apolloProvider: createApolloProviderWithKustomizations(),
+ });
+ await waitForPromises();
+
+ findKubernetesStatusBar().vm.$emit('show-flux-resource-details', fluxKustomization);
+ });
+
+ it('opens the drawer when gets selected item', () => {
+ expect(findDrawer().props('open')).toBe(true);
+ });
+
+ it('provides the resource details to the drawer', () => {
+ const selectedItem = {
+ name: fluxKustomization.metadata.name,
+ status: 'reconciled',
+ labels: fluxKustomization.metadata.labels,
+ annotations: fluxKustomization.metadata.annotations,
+ kind: fluxKustomization.kind,
+ spec: fluxKustomization.spec,
+ fullStatus: fluxKustomization.status.conditions,
+ };
+ expect(findWorkloadDetails().props('item')).toEqual(selectedItem);
+ });
+
+ it('renders a title with the selected item name', () => {
+ expect(findDrawer().text()).toContain(fluxKustomization.metadata.name);
+ });
+ });
+ });
+
describe('on child component error', () => {
beforeEach(() => {
wrapper = createWrapper();
diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_status_bar_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_status_bar_spec.js
index 034fc6c6111..95e595e00f8 100644
--- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_status_bar_spec.js
+++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_status_bar_spec.js
@@ -165,6 +165,10 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_st
it('renders sync status as Unavailable', () => {
expect(findSyncBadge().text()).toBe('Unavailable');
});
+
+ it('renders a non-clickable badge', () => {
+ expect(findSyncBadge().attributes('href')).toBeUndefined();
+ });
});
describe('when flux status data is provided', () => {
@@ -197,28 +201,45 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_st
},
);
- describe('when Flux API errored', () => {
- const fluxApiError = 'Error from the cluster_client API';
-
- beforeEach(() => {
- createWrapper({ fluxApiError });
+ it('renders a clickable badge', () => {
+ createWrapper({
+ fluxResourceStatus: [{ status: 'True', type: 'Ready' }],
});
- it('renders sync badge as unavailable', () => {
- const badge = SYNC_STATUS_BADGES.unavailable;
+ expect(findSyncBadge().attributes('href')).toBe('#');
+ });
- expect(findSyncBadge().text()).toBe(badge.text);
- expect(findSyncBadge().props()).toMatchObject({
- icon: badge.icon,
- variant: badge.variant,
- });
+ it('emits `show-flux-resource-details` event when badge is clicked', () => {
+ createWrapper({
+ fluxResourceStatus: [{ status: 'True', type: 'Ready' }],
});
- it('renders popover with an API error message', () => {
- expect(findPopover().text()).toBe(fluxApiError);
- expect(findPopover().props('title')).toBe('Flux sync status is unavailable');
+ findSyncBadge().trigger('click');
+ expect(wrapper.emitted('show-flux-resource-details')).toBeDefined();
+ });
+ });
+
+ describe('when Flux API errored', () => {
+ const fluxApiError = 'Error from the cluster_client API';
+
+ beforeEach(() => {
+ createWrapper({ fluxApiError });
+ });
+
+ it('renders sync badge as unavailable', () => {
+ const badge = SYNC_STATUS_BADGES.unavailable;
+
+ expect(findSyncBadge().text()).toBe(badge.text);
+ expect(findSyncBadge().props()).toMatchObject({
+ icon: badge.icon,
+ variant: badge.variant,
});
});
+
+ it('renders popover with an API error message', () => {
+ expect(findPopover().text()).toBe(fluxApiError);
+ expect(findPopover().props('title')).toBe('Flux sync status is unavailable');
+ });
});
});
});
diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_tabs_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_tabs_spec.js
index 68f47326fc6..06b5be0e60c 100644
--- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_tabs_spec.js
+++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_tabs_spec.js
@@ -1,11 +1,10 @@
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
-import { GlTabs, GlDrawer } from '@gitlab/ui';
+import { GlTabs } from '@gitlab/ui';
import KubernetesTabs from '~/environments/environment_details/components/kubernetes/kubernetes_tabs.vue';
import KubernetesPods from '~/environments/environment_details/components/kubernetes/kubernetes_pods.vue';
import KubernetesServices from '~/environments/environment_details/components/kubernetes/kubernetes_services.vue';
import KubernetesSummary from '~/environments/environment_details/components/kubernetes/kubernetes_summary.vue';
-import WorkloadDetails from '~/kubernetes_dashboard/components/workload_details.vue';
import { k8sResourceType } from '~/environments/graphql/resolvers/kubernetes/constants';
import { mockKasTunnelUrl, fluxKustomization } from 'jest/environments/mock_data';
import { mockPodsTableItems } from 'jest/kubernetes_dashboard/graphql/mock_data';
@@ -25,8 +24,6 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ta
const findKubernetesPods = () => wrapper.findComponent(KubernetesPods);
const findKubernetesServices = () => wrapper.findComponent(KubernetesServices);
const findKubernetesSummary = () => wrapper.findComponent(KubernetesSummary);
- const findDrawer = () => wrapper.findComponent(GlDrawer);
- const findWorkloadDetails = () => wrapper.findComponent(WorkloadDetails);
const createWrapper = ({
activeTab = k8sResourceType.k8sPods,
@@ -37,7 +34,6 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ta
glFeatures: { k8sTreeView: k8sTreeViewEnabled },
},
propsData: { configuration, namespace, fluxKustomization, value: activeTab },
- stubs: { GlDrawer },
});
};
@@ -147,52 +143,22 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ta
expect(wrapper.emitted('update-failed-state')).toEqual([[eventData]]);
});
+ it('emits show-resource-details event when gets it from the component', () => {
+ findKubernetesPods().vm.$emit('show-resource-details', mockPodsTableItems[0]);
+
+ expect(wrapper.emitted('show-resource-details')).toEqual([[mockPodsTableItems[0]]]);
+ });
+
+ it('emits remove-selection event when gets it from the component', () => {
+ findKubernetesPods().vm.$emit('remove-selection');
+
+ expect(wrapper.emitted('remove-selection')).toBeDefined();
+ });
+
it('emits a cluster error event when gets it from the component', () => {
const errorMessage = 'Error from the cluster_client API';
findKubernetesPods().vm.$emit('cluster-error', errorMessage);
expect(wrapper.emitted('cluster-error')).toEqual([[errorMessage]]);
});
});
-
- describe('resource details drawer', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('is closed by default', () => {
- expect(findDrawer().props('open')).toBe(false);
- });
-
- describe('when receives show-resource-details event', () => {
- beforeEach(() => {
- findKubernetesPods().vm.$emit('show-resource-details', mockPodsTableItems[0]);
- });
-
- it('opens the drawer', () => {
- expect(findDrawer().props('open')).toBe(true);
- });
-
- it('provides the resource details to the drawer', () => {
- expect(findWorkloadDetails().props('item')).toEqual(mockPodsTableItems[0]);
- });
-
- it('renders a title with the selected item name', () => {
- expect(findDrawer().text()).toContain(mockPodsTableItems[0].name);
- });
-
- it('is closed when clicked on a cross button', async () => {
- expect(findDrawer().props('open')).toBe(true);
-
- await findDrawer().vm.$emit('close');
- expect(findDrawer().props('open')).toBe(false);
- });
-
- it('is closed on remove-selection event', async () => {
- expect(findDrawer().props('open')).toBe(true);
-
- await findKubernetesPods().vm.$emit('remove-selection');
- expect(findDrawer().props('open')).toBe(false);
- });
- });
- });
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index 2db73a31230..110d959988b 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -816,19 +816,38 @@ export const k8sNamespacesMock = [
];
const fluxResourceStatusMock = [{ status: 'True', type: 'Ready', message: '', reason: '' }];
+const fluxResourceMetadataMock = {
+ name: 'custom-resource',
+ namespace: 'custom-namespace',
+ annotations: {},
+ labels: {},
+};
export const fluxKustomizationMock = {
kind: 'Kustomization',
- metadata: { name: 'custom-resource', namespace: 'custom-namespace' },
+ metadata: fluxResourceMetadataMock,
status: { conditions: fluxResourceStatusMock, inventory: { entries: [{ id: 'test_resource' }] } },
};
+export const fluxHelmReleaseMock = {
+ kind: 'HelmRelease',
+ metadata: fluxResourceMetadataMock,
+ status: { conditions: fluxResourceStatusMock },
+};
export const fluxKustomizationMapped = {
kind: 'Kustomization',
- metadata: { name: 'custom-resource' },
+ metadata: fluxResourceMetadataMock,
+ spec: {},
+ status: fluxKustomizationMock.status,
conditions: fluxResourceStatusMock,
inventory: [{ id: 'test_resource' }],
+ __typename: 'LocalWorkloadItem',
};
export const fluxHelmReleaseMapped = {
+ kind: 'HelmRelease',
+ metadata: fluxResourceMetadataMock,
+ spec: {},
+ status: { conditions: fluxResourceStatusMock },
conditions: fluxResourceStatusMock,
+ __typename: 'LocalWorkloadItem',
};
export const fluxResourcePathMock = 'kustomize.toolkit.fluxcd.io/v1/path/to/flux/resource';
diff --git a/spec/frontend/environments/graphql/resolvers/flux_spec.js b/spec/frontend/environments/graphql/resolvers/flux_spec.js
index dc2e0f14853..3a3b275c587 100644
--- a/spec/frontend/environments/graphql/resolvers/flux_spec.js
+++ b/spec/frontend/environments/graphql/resolvers/flux_spec.js
@@ -10,6 +10,7 @@ import {
} from '~/environments/graphql/resolvers/kubernetes/constants';
import {
fluxKustomizationMock,
+ fluxHelmReleaseMock,
fluxKustomizationMapped,
fluxHelmReleaseMapped,
} from '../mock_data';
@@ -144,7 +145,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
});
- describe('fluxHelmReleaseStatus', () => {
+ describe('fluxHelmRelease', () => {
const client = { writeQuery: jest.fn() };
const fluxResourcePath =
'helm.toolkit.fluxcd.io/v2beta1/namespaces/my-namespace/helmreleases/app';
@@ -156,7 +157,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
if (eventName === 'data') {
- callback([fluxKustomizationMock]);
+ callback([fluxHelmReleaseMock]);
}
});
const resourceName = 'custom-resource';
@@ -174,11 +175,11 @@ describe('~/frontend/environments/graphql/resolvers', () => {
.onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
.reply(HTTP_STATUS_OK, {
apiVersion,
- ...fluxKustomizationMock,
+ ...fluxHelmReleaseMock,
});
});
it('should watch HelmRelease by the metadata name from the cluster_client library when the data is present', async () => {
- await mockResolvers.Query.fluxHelmReleaseStatus(
+ await mockResolvers.Query.fluxHelmRelease(
null,
{
configuration,
@@ -197,7 +198,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
it('should return data when received from the library', async () => {
- const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus(
+ const fluxHelmRelease = await mockResolvers.Query.fluxHelmRelease(
null,
{
configuration,
@@ -206,7 +207,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
{ client },
);
- expect(fluxHelmReleaseStatus).toEqual(fluxHelmReleaseMapped);
+ expect(fluxHelmRelease).toEqual(fluxHelmReleaseMapped);
});
});
@@ -215,7 +216,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
.onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
.reply(HTTP_STATUS_OK, {});
- await mockResolvers.Query.fluxHelmReleaseStatus(
+ await mockResolvers.Query.fluxHelmRelease(
null,
{
configuration,
@@ -233,7 +234,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
.onGet(endpoint, { withCredentials: true, headers: configuration.base })
.reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
- const fluxHelmReleasesError = mockResolvers.Query.fluxHelmReleaseStatus(
+ const fluxHelmReleasesError = mockResolvers.Query.fluxHelmRelease(
null,
{
configuration,
diff --git a/spec/frontend/environments/mock_data.js b/spec/frontend/environments/mock_data.js
index 2917eb8b7b9..d2a3c88f292 100644
--- a/spec/frontend/environments/mock_data.js
+++ b/spec/frontend/environments/mock_data.js
@@ -318,12 +318,21 @@ const mockKasTunnelUrl = 'https://kas.gitlab.com/k8s-proxy';
const fluxResourceStatus = [{ status: 'True', type: 'Ready', message: '', reason: '' }];
const fluxKustomization = {
kind: 'Kustomization',
- metadata: { name: 'my-kustomization' },
+ status: { conditions: fluxResourceStatus },
+ spec: {},
+ metadata: {
+ name: 'my-kustomization',
+ namespace: 'my-namespace',
+ creationTimestamp: '',
+ labels: {},
+ annotations: {},
+ },
conditions: fluxResourceStatus,
inventory: [
{ id: 'flux-system_notification-controller_apps_Deployment' },
{ id: 'flux-system_source-controller_apps_Deployment' },
],
+ __typename: 'LocalWorkloadItem',
};
const k8sDeploymentsMock = [
diff --git a/spec/frontend/issuable/components/hidden_badge_spec.js b/spec/frontend/issuable/components/hidden_badge_spec.js
index db2248bb2d2..02ffac66f42 100644
--- a/spec/frontend/issuable/components/hidden_badge_spec.js
+++ b/spec/frontend/issuable/components/hidden_badge_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge, GlIcon } from '@gitlab/ui';
+import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import HiddenBadge from '~/issuable/components/hidden_badge.vue';
@@ -18,19 +18,18 @@ describe('HiddenBadge component', () => {
};
const findBadge = () => wrapper.findComponent(GlBadge);
- const findIcon = () => wrapper.findComponent(GlIcon);
beforeEach(() => {
mountComponent();
});
it('renders warning badge', () => {
- expect(findBadge().text()).toBe('Hidden');
+ expect(findBadge().attributes('aria-label')).toBe('Hidden');
expect(findBadge().props('variant')).toEqual('warning');
});
it('renders spam icon', () => {
- expect(findIcon().props('name')).toBe('spam');
+ expect(findBadge().props('icon')).toBe('spam');
});
it('has tooltip', () => {
diff --git a/spec/frontend/issuable/components/locked_badge_spec.js b/spec/frontend/issuable/components/locked_badge_spec.js
index 46143d16712..8d90435c4f1 100644
--- a/spec/frontend/issuable/components/locked_badge_spec.js
+++ b/spec/frontend/issuable/components/locked_badge_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge, GlIcon } from '@gitlab/ui';
+import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import LockedBadge from '~/issuable/components/locked_badge.vue';
@@ -18,19 +18,20 @@ describe('LockedBadge component', () => {
};
const findBadge = () => wrapper.findComponent(GlBadge);
- const findIcon = () => wrapper.findComponent(GlIcon);
beforeEach(() => {
mountComponent();
});
it('renders warning badge', () => {
- expect(findBadge().text()).toBe('Locked');
+ expect(findBadge().attributes('aria-label')).toBe(
+ 'The discussion in this issue is locked. Only project members can comment.',
+ );
expect(findBadge().props('variant')).toEqual('warning');
});
it('renders lock icon', () => {
- expect(findIcon().props('name')).toBe('lock');
+ expect(findBadge().props('icon')).toBe('lock');
});
it('has tooltip', () => {
diff --git a/spec/frontend/issuable/components/status_badge_spec.js b/spec/frontend/issuable/components/status_badge_spec.js
index 21fa5fbc208..7959b528aea 100644
--- a/spec/frontend/issuable/components/status_badge_spec.js
+++ b/spec/frontend/issuable/components/status_badge_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge, GlIcon } from '@gitlab/ui';
+import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import StatusBadge from '~/issuable/components/status_badge.vue';
@@ -36,7 +36,7 @@ describe('StatusBadge component', () => {
});
it(`sets badge icon as '${badgeIcon}'`, () => {
- expect(findBadge().findComponent(GlIcon).props('name')).toBe(badgeIcon);
+ expect(findBadge().props('icon')).toBe(badgeIcon);
});
},
);
diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js
index 376575a8acb..4c933e3712a 100644
--- a/spec/frontend/persistent_user_callout_spec.js
+++ b/spec/frontend/persistent_user_callout_spec.js
@@ -5,8 +5,10 @@ import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PersistentUserCallout from '~/persistent_user_callout';
+import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility');
describe('PersistentUserCallout', () => {
const dismissEndpoint = '/dismiss';
@@ -121,7 +123,6 @@ describe('PersistentUserCallout', () => {
let normalLink;
let mockAxios;
let persistentUserCallout;
- let windowSpy;
beforeEach(() => {
const fixture = createDeferredLinkFixture();
@@ -132,7 +133,6 @@ describe('PersistentUserCallout', () => {
mockAxios = new MockAdapter(axios);
persistentUserCallout = new PersistentUserCallout(container);
jest.spyOn(persistentUserCallout.container, 'remove').mockImplementation(() => {});
- windowSpy = jest.spyOn(window, 'open').mockImplementation(() => {});
});
afterEach(() => {
@@ -140,14 +140,14 @@ describe('PersistentUserCallout', () => {
});
it('defers loading of a link until callout is dismissed', async () => {
- const { href, target } = deferredLink;
+ const { href } = deferredLink;
mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_OK);
deferredLink.click();
await waitForPromises();
- expect(windowSpy).toHaveBeenCalledWith(href, target);
+ expect(visitUrl).toHaveBeenCalledWith(href, true);
expect(persistentUserCallout.container.remove).toHaveBeenCalled();
expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName }));
});
@@ -157,7 +157,7 @@ describe('PersistentUserCallout', () => {
await waitForPromises();
- expect(windowSpy).not.toHaveBeenCalled();
+ expect(visitUrl).not.toHaveBeenCalled();
expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
});
@@ -168,7 +168,7 @@ describe('PersistentUserCallout', () => {
await waitForPromises();
- expect(windowSpy).not.toHaveBeenCalled();
+ expect(visitUrl).not.toHaveBeenCalled();
expect(persistentUserCallout.container.remove).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/projects/your_work/components/app_spec.js b/spec/frontend/projects/your_work/components/app_spec.js
index a60b9971087..91934cfacea 100644
--- a/spec/frontend/projects/your_work/components/app_spec.js
+++ b/spec/frontend/projects/your_work/components/app_spec.js
@@ -1,11 +1,13 @@
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueRouter from 'vue-router';
import { GlTabs } from '@gitlab/ui';
-import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { updateHistory } from '~/lib/utils/url_utility';
import YourWorkProjectsApp from '~/projects/your_work/components/app.vue';
-import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import { createRouter } from '~/projects/your_work';
import {
+ ROOT_ROUTE_NAME,
+ DASHBOARD_ROUTE_NAME,
+ PROJECTS_DASHBOARD_ROUTE_NAME,
PROJECT_DASHBOARD_TABS,
CONTRIBUTED_TAB,
STARRED_TAB,
@@ -13,16 +15,23 @@ import {
MEMBER_TAB,
} from 'ee_else_ce/projects/your_work/constants';
-jest.mock('~/lib/utils/url_utility', () => ({
- ...jest.requireActual('~/lib/utils/url_utility'),
- updateHistory: jest.fn(),
-}));
+Vue.use(VueRouter);
+
+const defaultRoute = {
+ name: ROOT_ROUTE_NAME,
+};
describe('YourWorkProjectsApp', () => {
let wrapper;
+ let router;
- const createComponent = () => {
- wrapper = mountExtended(YourWorkProjectsApp);
+ const createComponent = ({ route = defaultRoute } = {}) => {
+ router = createRouter();
+ router.push(route);
+
+ wrapper = mountExtended(YourWorkProjectsApp, {
+ router,
+ });
};
const findPageTitle = () => wrapper.find('h1');
@@ -30,6 +39,10 @@ describe('YourWorkProjectsApp', () => {
const findAllTabTitles = () => wrapper.findAllByTestId('projects-dashboard-tab-title');
const findActiveTab = () => wrapper.find('.tab-pane.active');
+ afterEach(() => {
+ router = null;
+ });
+
describe('template', () => {
beforeEach(() => {
createComponent();
@@ -52,22 +65,17 @@ describe('YourWorkProjectsApp', () => {
});
describe.each`
- path | expectedTab
- ${'/'} | ${CONTRIBUTED_TAB}
- ${'/dashboard'} | ${CONTRIBUTED_TAB}
- ${'/dashboard/projects'} | ${CONTRIBUTED_TAB}
- ${'/dashboard/projects/contributed'} | ${CONTRIBUTED_TAB}
- ${'/dashboard/projects/starred'} | ${STARRED_TAB}
- ${'/dashboard/projects/personal'} | ${PERSONAL_TAB}
- ${'/dashboard/projects/member'} | ${MEMBER_TAB}
- ${'/dashboard/projects/fake'} | ${CONTRIBUTED_TAB}
- `('onMount when path is $path', ({ path, expectedTab }) => {
- useMockLocationHelper();
+ name | expectedTab
+ ${ROOT_ROUTE_NAME} | ${CONTRIBUTED_TAB}
+ ${DASHBOARD_ROUTE_NAME} | ${CONTRIBUTED_TAB}
+ ${PROJECTS_DASHBOARD_ROUTE_NAME} | ${CONTRIBUTED_TAB}
+ ${CONTRIBUTED_TAB.value} | ${CONTRIBUTED_TAB}
+ ${STARRED_TAB.value} | ${STARRED_TAB}
+ ${PERSONAL_TAB.value} | ${PERSONAL_TAB}
+ ${MEMBER_TAB.value} | ${MEMBER_TAB}
+ `('onMount when route name is $name', ({ name, expectedTab }) => {
beforeEach(() => {
- delete window.location;
- window.location = new URL(`${TEST_HOST}/${path}`);
-
- createComponent();
+ createComponent({ route: { name } });
});
it('initializes to the correct tab', () => {
@@ -79,48 +87,45 @@ describe('YourWorkProjectsApp', () => {
describe('when tab is already active', () => {
beforeEach(() => {
createComponent();
+ router.push = jest.fn();
});
- it('does not update the url path', async () => {
+ it('does not push new route', async () => {
findGlTabs().vm.$emit('input', 0);
await nextTick();
- expect(updateHistory).not.toHaveBeenCalled();
+ expect(router.push).not.toHaveBeenCalled();
});
});
describe('when tab is a valid tab', () => {
beforeEach(() => {
createComponent();
+ router.push = jest.fn();
});
- it('updates the url path correctly', async () => {
+ it('pushes new route correctly', async () => {
findGlTabs().vm.$emit('input', 2);
await nextTick();
- expect(updateHistory).toHaveBeenCalledWith({
- url: `/dashboard/projects/${PROJECT_DASHBOARD_TABS[2].value}`,
- replace: true,
- });
+ expect(router.push).toHaveBeenCalledWith({ name: PROJECT_DASHBOARD_TABS[2].value });
});
});
describe('when tab is an invalid tab', () => {
beforeEach(() => {
createComponent();
+ router.push = jest.fn();
});
- it('update the url path with the default Contributed tab', async () => {
+ it('pushes new route with default Contributed tab', async () => {
findGlTabs().vm.$emit('input', 100);
await nextTick();
- expect(updateHistory).toHaveBeenCalledWith({
- url: `/dashboard/projects/${CONTRIBUTED_TAB.value}`,
- replace: true,
- });
+ expect(router.push).toHaveBeenCalledWith({ name: CONTRIBUTED_TAB.value });
});
});
@@ -128,17 +133,16 @@ describe('YourWorkProjectsApp', () => {
beforeEach(() => {
gon.relative_url_root = '/gitlab';
createComponent();
+ router.push = jest.fn();
});
- it('update the url path correctly with relative url', async () => {
+ it('pushes new route correctly and respects relative url', async () => {
findGlTabs().vm.$emit('input', 3);
await nextTick();
- expect(updateHistory).toHaveBeenCalledWith({
- url: `/gitlab/dashboard/projects/${PROJECT_DASHBOARD_TABS[3].value}`,
- replace: true,
- });
+ expect(router.options.base).toBe('/gitlab');
+ expect(router.push).toHaveBeenCalledWith({ name: PROJECT_DASHBOARD_TABS[3].value });
});
});
});
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index 67991e40fa7..ae7f4d5ee06 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -51,6 +51,7 @@ describe('IssuableItem', () => {
const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const findIssuableTitleLink = () => wrapper.findComponentByTestId('issuable-title-link');
const findIssuableItemWrapper = () => wrapper.findByTestId('issuable-item-wrapper');
+ const findIssuablePrefetchTrigger = () => wrapper.findByTestId('issuable-prefetch-trigger');
const findStatusEl = () => wrapper.findByTestId('issuable-status');
describe('computed', () => {
@@ -397,6 +398,12 @@ describe('IssuableItem', () => {
expect(referenceEl.text()).toBe(`#${mockIssuable.iid}`);
});
+ it('does not enable item prefetching by default', () => {
+ wrapper = createComponent();
+
+ expect(findIssuablePrefetchTrigger().exists()).toBe(false);
+ });
+
it('renders issuable reference via slot', () => {
wrapper = createComponent({
issuableSymbol: '#',
@@ -651,5 +658,13 @@ describe('IssuableItem', () => {
expect(findIssuableItemWrapper().classes('gl-bg-blue-50')).toBe(true);
});
+
+ it('enables item prefetching', () => {
+ wrapper = createComponent({
+ preventRedirect: true,
+ });
+
+ expect(findIssuablePrefetchTrigger().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_prefetch_spec.js b/spec/frontend/work_items/components/work_item_prefetch_spec.js
new file mode 100644
index 00000000000..511a4fc9d7b
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_prefetch_spec.js
@@ -0,0 +1,69 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import WorkItemPrefetch from '~/work_items/components/work_item_prefetch.vue';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { workItemByIidResponseFactory } from '../mock_data';
+
+jest.mock('~/lib/utils/common_utils');
+
+describe('WorkItemPrefetch component', () => {
+ let wrapper;
+
+ const getWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
+ const findPrefetchTrigger = () => wrapper.findByTestId('prefetch-trigger');
+
+ Vue.use(VueApollo);
+
+ const createComponent = () => {
+ const mockApollo = createMockApollo([[workItemByIidQuery, getWorkItemQueryHandler]]);
+
+ wrapper = shallowMountExtended(WorkItemPrefetch, {
+ apolloProvider: mockApollo,
+ provide: {
+ fullPath: 'group/project',
+ },
+ propsData: {
+ workItemIid: '1',
+ },
+ scopedSlots: {
+ default: `
+
+
+ Hover item
+
+
+ `,
+ },
+ });
+ };
+
+ it('triggers prefetching on hover', async () => {
+ createComponent();
+
+ await findPrefetchTrigger().trigger('mouseover');
+
+ await waitForPromises();
+ await jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+
+ expect(getWorkItemQueryHandler).toHaveBeenCalled();
+ });
+
+ it('clears prefetching on mouseleave', async () => {
+ createComponent();
+
+ await findPrefetchTrigger().trigger('mouseover');
+ await findPrefetchTrigger().trigger('mouseleave');
+
+ expect(getWorkItemQueryHandler).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_state_badge_spec.js b/spec/frontend/work_items/components/work_item_state_badge_spec.js
index 248f16a4081..888d712cc5a 100644
--- a/spec/frontend/work_items/components/work_item_state_badge_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_badge_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge, GlIcon } from '@gitlab/ui';
+import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants';
import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue';
@@ -14,7 +14,6 @@ describe('WorkItemStateBadge', () => {
});
};
const findStatusBadge = () => wrapper.findComponent(GlBadge);
- const findStatusBadgeIcon = () => wrapper.findComponent(GlIcon);
it.each`
state | icon | stateText | variant
@@ -25,7 +24,7 @@ describe('WorkItemStateBadge', () => {
({ state, icon, stateText, variant }) => {
createComponent({ workItemState: state });
- expect(findStatusBadgeIcon().props('name')).toBe(icon);
+ expect(findStatusBadge().props('icon')).toBe(icon);
expect(findStatusBadge().props('variant')).toBe(variant);
expect(findStatusBadge().text()).toBe(stateText);
},
diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb
index f82d4a85c46..c6c763dd283 100644
--- a/spec/lib/container_registry/client_spec.rb
+++ b/spec/lib/container_registry/client_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe ContainerRegistry::Client, feature_category: :container_registry
it 'handles network timeouts' do
actual_retries = 0
retry_options_with_block = retry_options.merge(
- retry_block: ->(_, _, _, _) { actual_retries += 1 }
+ retry_block: ->(*) { actual_retries += 1 }
)
stub_const('ContainerRegistry::BaseClient::RETRY_OPTIONS', retry_options_with_block)
diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb
index ec5d38fd6b9..fea3603c167 100644
--- a/spec/lib/gitlab/ci/config/external/file/component_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb
@@ -210,4 +210,26 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category:
end
end
end
+
+ describe '#load_and_validate_expanded_hash!' do
+ let(:logger) { instance_double(::Gitlab::Ci::Pipeline::Logger, :instrument) }
+
+ let(:context_params) do
+ {
+ project: context_project,
+ sha: 'context_sha',
+ user: user,
+ variables: project_variables,
+ logger: logger
+ }
+ end
+
+ it 'tracks the content load time' do
+ expect(logger).to receive(:instrument).once.ordered.with(:config_component_fetch_content_hash).and_yield
+ expect(logger).to receive(:instrument).once.ordered.with(:config_file_fetch_content_hash).and_yield
+ expect(logger).to receive(:instrument).once.ordered.with(:config_file_expand_content_includes).and_yield
+
+ external_resource.load_and_validate_expanded_hash!
+ end
+ end
end
diff --git a/spec/lib/gitlab/import/source_user_mapper_spec.rb b/spec/lib/gitlab/import/source_user_mapper_spec.rb
index 92d714e852f..8cd657e0307 100644
--- a/spec/lib/gitlab/import/source_user_mapper_spec.rb
+++ b/spec/lib/gitlab/import/source_user_mapper_spec.rb
@@ -6,11 +6,11 @@ RSpec.describe Gitlab::Import::SourceUserMapper, feature_category: :importers do
describe '#find_or_create_internal_user' do
let_it_be(:namespace) { create(:namespace) }
- let(:import_type) { 'github' }
- let(:source_hostname) { 'github.com' }
- let(:source_name) { 'Pry Contributor' }
- let(:source_username) { 'a_pry_contributor' }
- let(:source_user_identifier) { '123456' }
+ let_it_be(:import_type) { 'github' }
+ let_it_be(:source_hostname) { 'github.com' }
+ let_it_be(:source_name) { 'Pry Contributor' }
+ let_it_be(:source_username) { 'a_pry_contributor' }
+ let_it_be(:source_user_identifier) { '123456' }
subject(:find_or_create_internal_user) do
described_class.new(
@@ -63,8 +63,8 @@ RSpec.describe Gitlab::Import::SourceUserMapper, feature_category: :importers do
end
context 'when the placeholder user limit has not been reached' do
- let!(:import_source_user_from_another_import) { create(:import_source_user) }
- let!(:different_source_user_from_same_import) do
+ let_it_be(:import_source_user_from_another_import) { create(:import_source_user) }
+ let_it_be(:different_source_user_from_same_import) do
create(:import_source_user,
namespace_id: namespace.id,
import_type: import_type,
@@ -86,7 +86,7 @@ RSpec.describe Gitlab::Import::SourceUserMapper, feature_category: :importers do
end
context 'when retried and another placeholder user was made while waiting' do
- let!(:existing_import_source_user) do
+ let_it_be(:existing_import_source_user) do
create(
:import_source_user,
:with_placeholder_user,
@@ -113,7 +113,7 @@ RSpec.describe Gitlab::Import::SourceUserMapper, feature_category: :importers do
context 'and an import source user exists for current import source' do
context 'and the source user maps to a placeholder user' do
- let!(:existing_import_source_user) do
+ let_it_be(:existing_import_source_user) do
create(
:import_source_user,
:with_placeholder_user,
@@ -131,7 +131,7 @@ RSpec.describe Gitlab::Import::SourceUserMapper, feature_category: :importers do
end
context 'and the source_user maps to a reassigned user' do
- let!(:existing_import_source_user) do
+ let_it_be(:existing_import_source_user) do
create(
:import_source_user,
:with_reassign_to_user,
@@ -141,11 +141,31 @@ RSpec.describe Gitlab::Import::SourceUserMapper, feature_category: :importers do
source_user_identifier: '123456')
end
- it 'returns the existing placeholder user' do
- expect(find_or_create_internal_user).to eq(existing_import_source_user.reassign_to_user)
+ before do
+ allow_next_found_instance_of(Import::SourceUser) do |source_user|
+ allow(source_user).to receive(:accepted_status?).and_return(accepted)
+ end
end
- it_behaves_like 'it does not create an import_source_user or placeholder user'
+ context 'when reassigned user has accepted the mapping' do
+ let(:accepted) { true }
+
+ it_behaves_like 'it does not create an import_source_user or placeholder user'
+
+ it 'returns the existing reassign to user' do
+ expect(find_or_create_internal_user).to eq(existing_import_source_user.reassign_to_user)
+ end
+ end
+
+ context 'when reassigned user has not accepted the mapping' do
+ let(:accepted) { false }
+
+ it_behaves_like 'it does not create an import_source_user or placeholder user'
+
+ it 'returns the existing placeholder user' do
+ expect(find_or_create_internal_user).to eq(existing_import_source_user.placeholder_user)
+ end
+ end
end
end
end
diff --git a/spec/models/import/source_user_spec.rb b/spec/models/import/source_user_spec.rb
index 253cac69812..6aa858fd051 100644
--- a/spec/models/import/source_user_spec.rb
+++ b/spec/models/import/source_user_spec.rb
@@ -208,19 +208,41 @@ RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do
end
end
+ describe '#accepted_reassign_to_user' do
+ let_it_be(:source_user) { build(:import_source_user, :with_reassign_to_user) }
+
+ subject(:accepted_reassign_to_user) { source_user.accepted_reassign_to_user }
+
+ before do
+ allow(source_user).to receive(:accepted_status?).and_return(accepted)
+ end
+
+ context 'when accepted' do
+ let(:accepted) { true }
+
+ it { is_expected.to eq(source_user.reassign_to_user) }
+ end
+
+ context 'when not accepted' do
+ let(:accepted) { false }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
describe '#reassignable_status?' do
reassignable_statuses = [:pending_reassignment, :rejected]
all_states = described_class.state_machines[:status].states
all_states.reject { |state| reassignable_statuses.include?(state.name) }.each do |state|
it "returns false for #{state.name}" do
- expect(described_class.new(status: state.value).reassignable_status?).to eq(false)
+ expect(described_class.new(status: state.value)).not_to be_reassignable_status
end
end
all_states.select { |state| reassignable_statuses.include?(state.name) }.each do |state|
it "returns true for #{state.name}" do
- expect(described_class.new(status: state.value).reassignable_status?).to eq(true)
+ expect(described_class.new(status: state.value)).to be_reassignable_status
end
end
end
@@ -231,13 +253,30 @@ RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do
all_states.reject { |state| cancelable_statuses.include?(state.name) }.each do |state|
it "returns false for #{state.name}" do
- expect(described_class.new(status: state.value).cancelable_status?).to eq(false)
+ expect(described_class.new(status: state.value)).not_to be_cancelable_status
end
end
all_states.select { |state| cancelable_statuses.include?(state.name) }.each do |state|
it "returns true for #{state.name}" do
- expect(described_class.new(status: state.value).cancelable_status?).to eq(true)
+ expect(described_class.new(status: state.value)).to be_cancelable_status
+ end
+ end
+ end
+
+ describe '#accepted_status?' do
+ accepted_statuses = [:reassignment_in_progress, :completed, :failed]
+ all_states = described_class.state_machines[:status].states
+
+ all_states.reject { |state| accepted_statuses.include?(state.name) }.each do |state|
+ it "returns false for #{state.name}" do
+ expect(described_class.new(status: state.value)).not_to be_accepted_status
+ end
+ end
+
+ all_states.select { |state| accepted_statuses.include?(state.name) }.each do |state|
+ it "returns true for #{state.name}" do
+ expect(described_class.new(status: state.value)).to be_accepted_status
end
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index a7d0ce0beab..1178475bdaa 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -1030,6 +1030,32 @@ RSpec.describe Issue, feature_category: :team_planning do
end
end
+ describe '#autoclose_by_merged_closing_merge_request?' do
+ subject { issue.autoclose_by_merged_closing_merge_request? }
+
+ context 'when issue belongs to a group' do
+ let(:issue) { build_stubbed(:issue, :group_level, namespace: build_stubbed(:group)) }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when issue belongs to a project' do
+ let(:issue) { build_stubbed(:issue, project: reusable_project) }
+
+ context 'when autoclose_referenced_issues is enabled for the project' do
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when autoclose_referenced_issues is disabled for the project' do
+ before do
+ issue.project.update!(autoclose_referenced_issues: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
describe '#suggested_branch_name' do
let(:repository) { double }
@@ -1589,12 +1615,12 @@ RSpec.describe Issue, feature_category: :team_planning do
end
describe '#publicly_visible?' do
- let(:project) { build(:project, project_visiblity) }
+ let(:project) { build(:project, project_visibility) }
let(:issue) { build(:issue, confidential: confidential, project: project) }
subject { issue.send(:publicly_visible?) }
- where(:project_visiblity, :confidential, :expected_value) do
+ where(:project_visibility, :confidential, :expected_value) do
:public | false | true
:public | true | false
:internal | false | false
@@ -1606,6 +1632,28 @@ RSpec.describe Issue, feature_category: :team_planning do
with_them do
it { is_expected.to eq(expected_value) }
end
+
+ context 'with group level issues' do
+ let(:group) { build(:group, group_visibility) }
+ let(:issue) { build(:issue, :group_level, confidential: confidential, namespace: group) }
+
+ before do
+ stub_licensed_features(epics: false)
+ end
+
+ where(:group_visibility, :confidential, :expected_value) do
+ :public | false | false
+ :public | true | false
+ :internal | false | false
+ :internal | true | false
+ :private | false | false
+ :private | true | false
+ end
+
+ with_them do
+ it { is_expected.to eq(expected_value) }
+ end
+ end
end
describe '#allow_possible_spam?' do
diff --git a/spec/models/work_items/widgets/development_spec.rb b/spec/models/work_items/widgets/development_spec.rb
index 48ec2d465c6..290020d9ca2 100644
--- a/spec/models/work_items/widgets/development_spec.rb
+++ b/spec/models/work_items/widgets/development_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe WorkItems::Widgets::Development, feature_category: :team_planning
context 'when work item exists at the group level' do
let_it_be_with_reload(:work_item) { create(:work_item, :group_level, namespace: group) }
- it_behaves_like 'will_auto_close_by_merge_request field spec', true
+ it_behaves_like 'will_auto_close_by_merge_request field spec', false
end
end
end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 51b28ffd625..fec75ca364a 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -34,25 +34,25 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
it 'allows support_bot to read issues, create and set metadata on new issues' do
- expect(permissions(support_bot, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
- expect(permissions(support_bot, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(support_bot, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(support_bot, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
expect(permissions(support_bot, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
end
end
shared_examples 'support bot with service desk disabled' do
it 'does not allow support_bot to read issues, create and set metadata on new issues' do
- expect(permissions(support_bot, issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
- expect(permissions(support_bot, issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(support_bot, issue)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(support_bot, issue_no_assignee)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
expect(permissions(support_bot, new_issue)).to be_disallowed(:create_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
end
end
shared_examples 'alert bot' do
it 'allows alert_bot to read and set metadata on issues' do
- expect(permissions(alert_bot, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(alert_bot, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(alert_bot, new_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
+ expect(permissions(alert_bot, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
+ expect(permissions(alert_bot, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
+ expect(permissions(alert_bot, new_issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
end
end
@@ -92,49 +92,49 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
it 'allows guests to read issues' do
- expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid, :admin_issue_relation, :admin_issue_link)
+ expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :admin_issue_relation, :admin_issue_link)
expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :mark_note_as_internal)
- expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :admin_issue_relation)
+ expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :admin_issue_relation)
expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(guest, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality)
end
it 'allows reporters to read, update, admin and create confidential notes' do
- expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
- expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
expect(permissions(reporter, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality, :mark_note_as_internal, :admin_issue_relation)
end
it 'allows reporters from group links to read, update, and admin issues' do
- expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
expect(permissions(reporter_from_group_link, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
end
it 'allows issue authors to read and update their issues' do
- expect(permissions(author, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue_relation)
+ expect(permissions(author, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue_relation)
expect(permissions(author, issue)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :admin_issue_relation)
+ expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :admin_issue_relation)
expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(author, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
end
it 'allows issue assignees to read and update their issues' do
- expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue_relation)
+ expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue_relation)
expect(permissions(assignee, issue)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
+ expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid)
expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(assignee, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
end
it 'does not allow non-members to read, update or create issues' do
- expect(permissions(non_member, issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
- expect(permissions(non_member, issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(non_member, issue)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(non_member, issue_no_assignee)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
expect(permissions(non_member, new_issue)).to be_disallowed(:create_issue, :set_issue_metadata, :set_confidentiality)
end
@@ -147,50 +147,50 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
it 'does not allow non-members to read confidential issues' do
- expect(permissions(non_member, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation, :award_emoji, :admin_issue_link)
- expect(permissions(non_member, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji, :admin_issue_link)
+ expect(permissions(non_member, confidential_issue)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation, :award_emoji, :admin_issue_link)
+ expect(permissions(non_member, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji, :admin_issue_link)
end
it 'does not allow guests to read confidential issues' do
- expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation, :award_emoji, :admin_issue_link)
- expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji, :admin_issue_link)
+ expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation, :award_emoji, :admin_issue_link)
+ expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji, :admin_issue_link)
end
it 'allows reporters to read, update, and admin confidential issues' do
- expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
- expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
+ expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
end
it 'allows reporters from group links to read, update, and admin confidential issues' do
- expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
- expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
+ expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
end
it 'allows issue authors to read and update their confidential issues' do
- expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue_relation, :award_emoji)
+ expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue_relation, :award_emoji)
expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation, :award_emoji)
- expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality, :award_emoji)
+ expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation, :award_emoji)
+ expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:admin_issue, :read_note, :set_issue_metadata, :set_confidentiality, :award_emoji)
end
it 'does not allow issue author to read or update confidential issue moved to an private project' do
confidential_issue.project = create(:project, :private)
- expect(permissions(author, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
+ expect(permissions(author, confidential_issue)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
end
it 'allows issue assignees to read and update their confidential issues' do
- expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :award_emoji)
+ expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :award_emoji)
expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
+ expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
end
it 'does not allow issue assignees to read or update confidential issue moved to an private project' do
confidential_issue.project = create(:project, :private)
- expect(permissions(assignee, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
+ expect(permissions(assignee, confidential_issue)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
end
end
end
@@ -215,7 +215,7 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
it 'does not allow anonymous user to create todos' do
- expect(permissions(nil, issue)).to be_allowed(:read_issue)
+ expect(permissions(nil, issue)).to be_allowed(:read_issue, :read_note)
expect(permissions(nil, issue)).to be_disallowed(:create_todo, :update_subscription, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
expect(permissions(nil, new_issue)).to be_disallowed(:create_issue, :set_issue_metadata, :set_confidentiality)
end
@@ -234,42 +234,42 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
it 'allows guests to read issues' do
- expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid, :create_todo, :update_subscription, :admin_issue_relation)
+ expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :create_todo, :update_subscription, :admin_issue_relation)
expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :admin_issue_relation)
+ expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :admin_issue_relation)
expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(guest, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :admin_issue_relation)
+ expect(permissions(guest, issue_locked)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :admin_issue_relation)
expect(permissions(guest, issue_locked)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(guest, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
end
it 'allows reporters to read, update, reopen, and admin issues' do
- expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
- expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
- expect(permissions(reporter, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(reporter, issue_locked)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
expect(permissions(reporter, issue_locked)).to be_disallowed(:reopen_issue)
expect(permissions(reporter, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
end
it 'allows reporters from group links to read, update, reopen and admin issues' do
- expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:reopen_issue)
- expect(permissions(reporter_from_group_link, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(reporter_from_group_link, issue_locked)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
expect(permissions(reporter_from_group_link, issue_locked)).to be_disallowed(:reopen_issue)
expect(permissions(reporter, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
end
it 'allows issue authors to read, reopen and update their issues' do
- expect(permissions(author, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :reopen_issue)
+ expect(permissions(author, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :reopen_issue)
expect(permissions(author, issue)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
+ expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid)
expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(author, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
+ expect(permissions(author, issue_locked)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue)
expect(permissions(author, issue_locked)).to be_disallowed(:admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(author, new_issue)).to be_allowed(:create_issue)
@@ -277,25 +277,25 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
it 'allows issue assignees to read, reopen and update their issues' do
- expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :reopen_issue)
+ expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :reopen_issue)
expect(permissions(assignee, issue)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
+ expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid)
expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(assignee, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
+ expect(permissions(assignee, issue_locked)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue)
expect(permissions(assignee, issue_locked)).to be_disallowed(:admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality)
end
it 'allows non-members to read and create issues' do
- expect(permissions(non_member, issue)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(non_member, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
+ expect(permissions(non_member, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid)
+ expect(permissions(non_member, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid)
expect(permissions(non_member, new_issue)).to be_allowed(:create_issue)
end
it 'allows non-members to read issues' do
- expect(permissions(non_member, issue)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(non_member, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
+ expect(permissions(non_member, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid)
+ expect(permissions(non_member, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid)
end
it 'does not allow non-members to update, admin or set metadata except for set confidential flag' do
@@ -310,10 +310,10 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
it 'allows support_bot to read issues' do
# support_bot is still allowed read access in public projects through :public_access permission,
# see project_policy public_access rules policy (rule { can?(:public_access) }.policy {...})
- expect(permissions(support_bot, issue)).to be_allowed(:read_issue, :read_issue_iid)
+ expect(permissions(support_bot, issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid)
expect(permissions(support_bot, issue)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(support_bot, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
+ expect(permissions(support_bot, issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid)
expect(permissions(support_bot, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(support_bot, new_issue)).to be_disallowed(:create_issue, :set_issue_metadata, :set_confidentiality)
@@ -383,7 +383,7 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
it 'does not allow non-members to update or create issues' do
- expect(permissions(non_member, issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(non_member, issue)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
expect(permissions(non_member, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
expect(permissions(non_member, new_issue)).to be_disallowed(:create_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
end
@@ -398,32 +398,32 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
it 'does not allow guests to read confidential issues' do
- expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation)
- expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
+ expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation)
+ expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
end
it 'allows reporters to read, update, and admin confidential issues' do
- expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation)
- expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
+ expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
end
it 'allows reporter from group links to read, update, and admin confidential issues' do
- expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation)
- expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
+ expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
end
it 'allows issue authors to read and update their confidential issues' do
- expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
+ expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue)
expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
+ expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
end
it 'allows issue assignees to read and update their confidential issues' do
- expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
+ expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_note, :read_issue_iid, :update_issue)
expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
+ expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_note, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
end
it 'allows admins to read confidential issues' do
diff --git a/spec/policies/work_item_policy_spec.rb b/spec/policies/work_item_policy_spec.rb
index 8f144a1ac4a..163bc512b52 100644
--- a/spec/policies/work_item_policy_spec.rb
+++ b/spec/policies/work_item_policy_spec.rb
@@ -342,16 +342,44 @@ RSpec.describe WorkItemPolicy, feature_category: :team_planning do
let_it_be(:public_group_member) { create(:user, reporter_of: public_group) }
let(:work_item_subject) { public_group_work_item }
+ context 'when user is anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:read_work_item, :read_issue, :read_note) }
+ end
+
context 'when user is not a member of the group' do
let(:current_user) { non_member_user }
- it { is_expected.not_to be_allowed(:read_note) }
+ it { is_expected.to be_allowed(:read_work_item, :read_issue, :read_note) }
end
context 'when user is a member of the group' do
let(:current_user) { public_group_member }
- it { is_expected.to be_allowed(:read_note) }
+ it { is_expected.to be_allowed(:read_work_item, :read_issue, :read_note) }
+ end
+
+ context 'when work item is confidential' do
+ let(:work_item_subject) { create(:work_item, :group_level, :confidential, namespace: public_group) }
+
+ context 'when user is anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.not_to be_allowed(:read_work_item, :read_issue, :read_note) }
+ end
+
+ context 'when user is not a member of the group' do
+ let(:current_user) { non_member_user }
+
+ it { is_expected.not_to be_allowed(:read_work_item, :read_issue, :read_note) }
+ end
+
+ context 'when user is a member of the group' do
+ let(:current_user) { public_group_member }
+
+ it { is_expected.to be_allowed(:read_work_item, :read_issue, :read_note) }
+ end
end
end
diff --git a/spec/scripts/internal_events/cli_spec.rb b/spec/scripts/internal_events/cli_spec.rb
index 76dcff12697..fa88baa4767 100644
--- a/spec/scripts/internal_events/cli_spec.rb
+++ b/spec/scripts/internal_events/cli_spec.rb
@@ -1173,7 +1173,7 @@ RSpec.describe Cli, feature_category: :service_ping do
with_cli_thread do
expect { plain_last_lines(30) }
- .to eventually_include_cli_text("Amazing! The next step is adding a new metric! (~8 min)")
+ .to eventually_include_cli_text("Amazing! The next step is adding a new metric! (~8-15 min)")
end
end
end
diff --git a/spec/services/clusters/agents/create_service_spec.rb b/spec/services/clusters/agents/create_service_spec.rb
index 85607fcdf3a..f07f7ef42fb 100644
--- a/spec/services/clusters/agents/create_service_spec.rb
+++ b/spec/services/clusters/agents/create_service_spec.rb
@@ -3,18 +3,19 @@
require 'spec_helper'
RSpec.describe Clusters::Agents::CreateService, feature_category: :deployment_management do
- subject(:service) { described_class.new(project, user) }
+ subject(:service) { described_class.new(project, user, { name: name }) }
+ let(:name) { 'some-agent' }
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
describe '#execute' do
context 'without user permissions' do
it 'returns errors when user does not have permissions' do
- expect(service.execute(name: 'missing-permissions')).to eq({
- status: :error,
- message: 'You have insufficient permissions to create a cluster agent for this project'
- })
+ response = service.execute
+
+ expect(response.status).to eq(:error)
+ expect(response.message).to eq('You have insufficient permissions to create a cluster agent for this project')
end
end
@@ -24,28 +25,34 @@ RSpec.describe Clusters::Agents::CreateService, feature_category: :deployment_ma
end
it 'creates a new clusters_agent' do
- expect { service.execute(name: 'with-user') }.to change { ::Clusters::Agent.count }.by(1)
+ expect { service.execute }.to change { ::Clusters::Agent.count }.by(1)
end
it 'returns success status', :aggregate_failures do
- result = service.execute(name: 'success')
+ response = service.execute
- expect(result[:status]).to eq(:success)
- expect(result[:message]).to be_nil
+ expect(response.status).to eq(:success)
+ expect(response.message).to be_nil
end
it 'returns agent values', :aggregate_failures do
- new_agent = service.execute(name: 'new-agent')[:cluster_agent]
+ new_agent = service.execute[:cluster_agent]
- expect(new_agent.name).to eq('new-agent')
+ expect(new_agent.name).to eq(name)
expect(new_agent.created_by_user).to eq(user)
end
- it 'generates an error message when name is invalid' do
- expect(service.execute(name: '@bad_agent_name!')).to eq({
- status: :error,
- message: ["Name can contain only lowercase letters, digits, and '-', but cannot start or end with '-'"]
- })
+ context 'with invalid name' do
+ let(:name) { '@bad_agent_name!' }
+
+ it 'generates an error message' do
+ response = service.execute
+
+ expect(response.status).to eq(:error)
+ expect(response.message).to eq(
+ ["Name can contain only lowercase letters, digits, and '-', but cannot start or end with '-'"]
+ )
+ end
end
end
end
diff --git a/spec/services/clusters/agents/delete_service_spec.rb b/spec/services/clusters/agents/delete_service_spec.rb
index febbb7ba5c8..6ed9fe67aef 100644
--- a/spec/services/clusters/agents/delete_service_spec.rb
+++ b/spec/services/clusters/agents/delete_service_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
RSpec.describe Clusters::Agents::DeleteService, feature_category: :deployment_management do
- subject(:service) { described_class.new(container: project, current_user: user) }
+ subject(:service) do
+ described_class.new(container: project, current_user: user, params: { cluster_agent: cluster_agent })
+ end
let(:cluster_agent) { create(:cluster_agent) }
let(:project) { cluster_agent.project }
@@ -12,7 +14,7 @@ RSpec.describe Clusters::Agents::DeleteService, feature_category: :deployment_ma
describe '#execute' do
context 'without user permissions' do
it 'fails to delete when the user has no permissions', :aggregate_failures do
- response = service.execute(cluster_agent)
+ response = service.execute
expect(response.status).to eq(:error)
expect(response.message).to eq('You have insufficient permissions to delete this cluster agent')
@@ -27,7 +29,7 @@ RSpec.describe Clusters::Agents::DeleteService, feature_category: :deployment_ma
end
it 'deletes a cluster agent', :aggregate_failures do
- expect { service.execute(cluster_agent) }.to change { ::Clusters::Agent.count }.by(-1)
+ expect { service.execute }.to change { ::Clusters::Agent.count }.by(-1)
expect { cluster_agent.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 8b09776705f..7f6ee1019ec 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -304,7 +304,7 @@ RSpec.describe MergeRequests::MergeService, feature_category: :code_review_workf
)
end
- it 'only closes issues where the setting is enabled or belong to a group' do
+ it 'only closes project issues where the setting is enabled' do
merge_request.cache_merge_request_closes_issues!
expect do
@@ -314,7 +314,7 @@ RSpec.describe MergeRequests::MergeService, feature_category: :code_review_workf
).and(
not_change { no_close_issue.reload.opened? }.from(true)
).and(
- change { group_issue.reload.closed? }.from(false).to(true)
+ not_change { group_issue.reload.opened? }.from(true)
)
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 120e58b879e..872b96c3fa3 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -1365,7 +1365,6 @@
- './ee/spec/lib/gitlab/reference_extractor_spec.rb'
- './ee/spec/lib/gitlab/regex_spec.rb'
- './ee/spec/lib/gitlab/return_to_location_spec.rb'
-- './ee/spec/lib/gitlab/search/aggregation_parser_spec.rb'
- './ee/spec/lib/gitlab/search/aggregation_spec.rb'
- './ee/spec/lib/gitlab/search_context/builder_spec.rb'
- './ee/spec/lib/gitlab/search/recent_epics_spec.rb'
diff --git a/spec/support/shared_examples/models/concerns/participable_shared_examples.rb b/spec/support/shared_examples/models/concerns/participable_shared_examples.rb
index f772cfc6bbd..200391e63e4 100644
--- a/spec/support/shared_examples/models/concerns/participable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/participable_shared_examples.rb
@@ -16,8 +16,7 @@ RSpec.shared_examples 'visible participants for issuable with read ability' do |
source = ability_source == :participable_source ? participable_source : instance
allow(instance).to receive(:bar).and_return(participable_source)
-
- expect(Ability).to receive(:allowed?).with(anything, ability_name, source)
+ allow(Ability).to receive(:allowed?).with(anything, ability_name, source)
expect(instance.visible_participants(user1)).to be_empty
end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 58c54ab9212..80df0e0bc9a 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -439,6 +439,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'RunPipelineScheduleWorker' => 3,
'ScanSecurityReportSecretsWorker' => 17,
'Search::ElasticGroupAssociationDeletionWorker' => 3,
+ 'Search::Elastic::DeleteWorker' => 3,
'Security::StoreScansWorker' => 3,
'Security::TrackSecureScansWorker' => 1,
'ServiceDeskEmailReceiverWorker' => 3,
diff --git a/tooling/merge_request.rb b/tooling/merge_request.rb
index d0f32a611aa..7fcf46fa10e 100644
--- a/tooling/merge_request.rb
+++ b/tooling/merge_request.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'faraday'
-require 'faraday_middleware'
module Tooling
class MergeRequest