Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c0fe5a7e5b
commit
fb663cc988
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
23
Gemfile
23
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
107
Gemfile.lock
107
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)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,26 @@
|
|||
<script>
|
||||
import { GlEmptyState, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
|
||||
import { GlEmptyState, GlSprintf, GlLink, GlAlert, GlDrawer } from '@gitlab/ui';
|
||||
import CLUSTER_EMPTY_SVG from '@gitlab/svgs/dist/illustrations/empty-state/empty-state-clusters.svg?url';
|
||||
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
|
||||
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
|
||||
import { s__ } from '~/locale';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { k8sResourceType } from '~/environments/graphql/resolvers/kubernetes/constants';
|
||||
import { createK8sAccessConfiguration } from '~/environments/helpers/k8s_integration_helper';
|
||||
import {
|
||||
createK8sAccessConfiguration,
|
||||
fluxSyncStatus,
|
||||
} from '~/environments/helpers/k8s_integration_helper';
|
||||
import fluxKustomizationQuery from '~/environments/graphql/queries/flux_kustomization.query.graphql';
|
||||
import fluxHelmReleaseQueryStatus from '~/environments/graphql/queries/flux_helm_release_status.query.graphql';
|
||||
import fluxHelmReleaseQueryStatus from '~/environments/graphql/queries/flux_helm_release.query.graphql';
|
||||
import {
|
||||
CLUSTER_HEALTH_SUCCESS,
|
||||
CLUSTER_HEALTH_ERROR,
|
||||
HELM_RELEASES_RESOURCE_TYPE,
|
||||
KUSTOMIZATIONS_RESOURCE_TYPE,
|
||||
} from '~/environments/constants';
|
||||
import WorkloadDetails from '~/kubernetes_dashboard/components/workload_details.vue';
|
||||
import KubernetesStatusBar from './kubernetes_status_bar.vue';
|
||||
import KubernetesAgentInfo from './kubernetes_agent_info.vue';
|
||||
import KubernetesTabs from './kubernetes_tabs.vue';
|
||||
|
|
@ -25,9 +31,11 @@ export default {
|
|||
KubernetesStatusBar,
|
||||
KubernetesAgentInfo,
|
||||
KubernetesTabs,
|
||||
WorkloadDetails,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
GlAlert,
|
||||
GlDrawer,
|
||||
},
|
||||
inject: ['kasTunnelUrl'],
|
||||
props: {
|
||||
|
|
@ -69,7 +77,7 @@ export default {
|
|||
this.fluxApiError = err.message;
|
||||
},
|
||||
},
|
||||
fluxHelmReleaseStatus: {
|
||||
fluxHelmRelease: {
|
||||
query: fluxHelmReleaseQueryStatus,
|
||||
variables() {
|
||||
return {
|
||||
|
|
@ -94,6 +102,8 @@ export default {
|
|||
podsLoading: false,
|
||||
activeTab: k8sResourceType.k8sPods,
|
||||
fluxApiError: '',
|
||||
selectedItem: {},
|
||||
showDetailsDrawer: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -116,7 +126,13 @@ export default {
|
|||
return Object.values(this.failedState).some((item) => item);
|
||||
},
|
||||
fluxResourceStatus() {
|
||||
return this.fluxKustomization?.conditions || this.fluxHelmReleaseStatus?.conditions;
|
||||
return this.fluxKustomization?.conditions || this.fluxHelmRelease?.conditions;
|
||||
},
|
||||
drawerHeaderHeight() {
|
||||
return getContentWrapperHeight();
|
||||
},
|
||||
hasSelectedItem() {
|
||||
return Object.keys(this.selectedItem).length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -134,6 +150,37 @@ export default {
|
|||
...event,
|
||||
};
|
||||
},
|
||||
transformFluxResourceData(item) {
|
||||
return {
|
||||
name: item.metadata.name,
|
||||
status: fluxSyncStatus(item.status.conditions).status,
|
||||
labels: item.metadata.labels,
|
||||
annotations: item.metadata.annotations,
|
||||
kind: item.kind,
|
||||
spec: item.spec,
|
||||
fullStatus: item.status.conditions,
|
||||
};
|
||||
},
|
||||
showFluxResourceDetails() {
|
||||
const fluxResource = this.fluxKustomization || this.fluxHelmRelease;
|
||||
const fluxResourceTransformed = this.transformFluxResourceData(fluxResource);
|
||||
|
||||
this.openDetailsDrawer(fluxResourceTransformed);
|
||||
},
|
||||
openDetailsDrawer(item) {
|
||||
this.selectedItem = item;
|
||||
this.showDetailsDrawer = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.drawer?.$el?.querySelector('button')?.focus();
|
||||
});
|
||||
},
|
||||
closeDetailsDrawer() {
|
||||
this.showDetailsDrawer = false;
|
||||
this.selectedItem = {};
|
||||
this.$nextTick(() => {
|
||||
this.$refs.status_bar?.$refs?.flux_status_badge?.$el?.focus();
|
||||
});
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
emptyTitle: s__('Environment|No Kubernetes clusters configured'),
|
||||
|
|
@ -145,6 +192,7 @@ export default {
|
|||
learnMoreLink: helpPagePath('user/clusters/agent/index'),
|
||||
getStartedLink: helpPagePath('ci/environments/kubernetes_dashboard'),
|
||||
CLUSTER_EMPTY_SVG,
|
||||
DRAWER_Z_INDEX,
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
|
@ -154,6 +202,7 @@ export default {
|
|||
>
|
||||
<kubernetes-agent-info :cluster-agent="clusterAgent" class="gl-mb-2 gl-mr-5" />
|
||||
<kubernetes-status-bar
|
||||
ref="status_bar"
|
||||
:cluster-health-status="clusterHealthStatus"
|
||||
:configuration="k8sAccessConfiguration"
|
||||
:namespace="kubernetesNamespace"
|
||||
|
|
@ -163,6 +212,7 @@ export default {
|
|||
:flux-resource-status="fluxResourceStatus"
|
||||
:flux-api-error="fluxApiError"
|
||||
@error="handleError"
|
||||
@show-flux-resource-details="showFluxResourceDetails"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -178,7 +228,26 @@ export default {
|
|||
@cluster-error="handleError"
|
||||
@loading="podsLoading = $event"
|
||||
@update-failed-state="handleFailedState"
|
||||
@show-resource-details="openDetailsDrawer"
|
||||
@remove-selection="closeDetailsDrawer"
|
||||
/>
|
||||
|
||||
<gl-drawer
|
||||
ref="drawer"
|
||||
:open="showDetailsDrawer"
|
||||
:header-height="drawerHeaderHeight"
|
||||
:z-index="$options.DRAWER_Z_INDEX"
|
||||
@close="closeDetailsDrawer"
|
||||
>
|
||||
<template #title>
|
||||
<h2 class="gl-font-bold gl-m-0 gl-break-anywhere">
|
||||
{{ selectedItem.name }}
|
||||
</h2>
|
||||
</template>
|
||||
<template #default>
|
||||
<workload-details v-if="hasSelectedItem" :item="selectedItem" />
|
||||
</template>
|
||||
</gl-drawer>
|
||||
</div>
|
||||
<gl-empty-state
|
||||
v-else
|
||||
|
|
|
|||
|
|
@ -129,10 +129,10 @@ export default {
|
|||
return `${this.environmentName}-flux-sync-badge`;
|
||||
},
|
||||
syncStatusBadge() {
|
||||
if (!this.fluxResourceStatus.length && this.fluxApiError) {
|
||||
if (!this.fluxResourcePresent && this.fluxApiError) {
|
||||
return { ...SYNC_STATUS_BADGES.unavailable, popoverText: this.fluxApiError };
|
||||
}
|
||||
if (!this.fluxResourceStatus.length) {
|
||||
if (!this.fluxResourcePresent) {
|
||||
return SYNC_STATUS_BADGES.unavailable;
|
||||
}
|
||||
|
||||
|
|
@ -165,11 +165,22 @@ export default {
|
|||
isFluxConnectionStatus() {
|
||||
return Boolean(this.fluxConnectionParams.resourceType);
|
||||
},
|
||||
fluxResourcePresent() {
|
||||
return Boolean(this.fluxResourceStatus?.length);
|
||||
},
|
||||
fluxBadgeHref() {
|
||||
return this.fluxResourcePresent ? '#' : null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleError(error) {
|
||||
this.$emit('error', error);
|
||||
},
|
||||
toggleFluxResource() {
|
||||
if (!this.fluxResourcePresent) return;
|
||||
|
||||
this.$emit('show-flux-resource-details');
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
healthLabel: s__('Environment|Environment status'),
|
||||
|
|
@ -216,10 +227,13 @@ export default {
|
|||
<template v-else>
|
||||
<gl-badge
|
||||
:id="fluxBadgeId"
|
||||
ref="flux_status_badge"
|
||||
:icon="syncStatusBadge.icon"
|
||||
:variant="syncStatusBadge.variant"
|
||||
data-testid="sync-badge"
|
||||
tabindex="0"
|
||||
:href="fluxBadgeHref"
|
||||
@click.native="toggleFluxResource"
|
||||
>{{ syncStatusBadge.text }}
|
||||
</gl-badge>
|
||||
<gl-popover :target="fluxBadgeId" :title="syncStatusBadge.popoverTitle">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
<script>
|
||||
import { GlTabs, GlDrawer } from '@gitlab/ui';
|
||||
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
|
||||
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
|
||||
import { GlTabs } from '@gitlab/ui';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { k8sResourceType } from '~/environments/graphql/resolvers/kubernetes/constants';
|
||||
import WorkloadDetails from '~/kubernetes_dashboard/components/workload_details.vue';
|
||||
import KubernetesPods from './kubernetes_pods.vue';
|
||||
import KubernetesServices from './kubernetes_services.vue';
|
||||
import KubernetesSummary from './kubernetes_summary.vue';
|
||||
|
|
@ -18,8 +15,6 @@ export default {
|
|||
KubernetesPods,
|
||||
KubernetesServices,
|
||||
KubernetesSummary,
|
||||
GlDrawer,
|
||||
WorkloadDetails,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
|
|
@ -46,14 +41,9 @@ export default {
|
|||
activeTabIndex: this.glFeatures.k8sTreeView
|
||||
? tabsWithSummary.indexOf(this.value)
|
||||
: defaultTabs.indexOf(this.value),
|
||||
selectedItem: {},
|
||||
showDetailsDrawer: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
drawerHeaderHeight() {
|
||||
return getContentWrapperHeight();
|
||||
},
|
||||
renderTreeView() {
|
||||
return this.glFeatures.k8sTreeView;
|
||||
},
|
||||
|
|
@ -66,16 +56,6 @@ export default {
|
|||
this.$emit('input', this.tabs[newValue]);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showResourceDetails(item) {
|
||||
this.selectedItem = item;
|
||||
this.showDetailsDrawer = true;
|
||||
},
|
||||
closeDetailsDrawer() {
|
||||
this.showDetailsDrawer = false;
|
||||
},
|
||||
},
|
||||
DRAWER_Z_INDEX,
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
|
@ -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')"
|
||||
/>
|
||||
|
||||
<kubernetes-services
|
||||
:namespace="namespace"
|
||||
:configuration="configuration"
|
||||
@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')"
|
||||
/>
|
||||
</gl-tabs>
|
||||
|
||||
<gl-drawer
|
||||
:open="showDetailsDrawer"
|
||||
:header-height="drawerHeaderHeight"
|
||||
:z-index="$options.DRAWER_Z_INDEX"
|
||||
@close="closeDetailsDrawer"
|
||||
>
|
||||
<template #title>
|
||||
<h4 class="gl-font-bold gl-font-size-h2 gl-m-0 gl-break-anywhere">
|
||||
{{ selectedItem.name }}
|
||||
</h4>
|
||||
</template>
|
||||
<template #default>
|
||||
<workload-details :item="selectedItem" />
|
||||
</template>
|
||||
</gl-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
query getFluxHelmReleaseStatusQuery($configuration: LocalConfiguration, $fluxResourcePath: String) {
|
||||
fluxHelmReleaseStatus(configuration: $configuration, fluxResourcePath: $fluxResourcePath)
|
||||
@client {
|
||||
conditions {
|
||||
message
|
||||
reason
|
||||
status
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<script>
|
||||
import { GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { issuableTypeText } from '~/issues/constants';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBadge,
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -29,8 +28,12 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-badge v-gl-tooltip :title="title" variant="warning" class="gl-shrink-0">
|
||||
<gl-icon name="spam" />
|
||||
<span class="gl-sr-only">{{ __('Hidden') }}</span>
|
||||
</gl-badge>
|
||||
<gl-badge
|
||||
v-gl-tooltip
|
||||
icon="spam"
|
||||
:title="title"
|
||||
:aria-label="__('Hidden')"
|
||||
variant="warning"
|
||||
class="gl-shrink-0"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<script>
|
||||
import { GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { issuableTypeText } from '~/issues/constants';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBadge,
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -34,12 +33,11 @@ export default {
|
|||
<template>
|
||||
<gl-badge
|
||||
v-gl-tooltip
|
||||
icon="lock"
|
||||
:title="title"
|
||||
:aria-label="title"
|
||||
variant="warning"
|
||||
data-testid="locked-badge"
|
||||
class="gl-shrink-0"
|
||||
>
|
||||
<gl-icon name="lock" />
|
||||
<span class="gl-sr-only">{{ __('Locked') }}</span>
|
||||
</gl-badge>
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlBadge, GlIcon } from '@gitlab/ui';
|
||||
import { GlBadge } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import {
|
||||
STATUS_CLOSED,
|
||||
|
|
@ -68,7 +68,6 @@ const badgePropertiesMap = {
|
|||
export default {
|
||||
components: {
|
||||
GlBadge,
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
issuableType: {
|
||||
|
|
@ -93,10 +92,10 @@ export default {
|
|||
<template>
|
||||
<gl-badge
|
||||
:variant="badgeProperties.variant"
|
||||
:icon="badgeProperties.icon"
|
||||
:aria-label="badgeProperties.text"
|
||||
class="gl-shrink-0"
|
||||
>
|
||||
<gl-icon :name="badgeProperties.icon" />
|
||||
{{ badgeProperties.text }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ export default {
|
|||
this.loading = true;
|
||||
const oauthAuthorizeURL = await this.getOauthAuthorizeURL();
|
||||
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
window.open(oauthAuthorizeURL, I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, OAUTH_WINDOW_OPTIONS);
|
||||
} catch (e) {
|
||||
if (e.message) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { GlBadge, GlTruncate } from '@gitlab/ui';
|
|||
import { stringify } from 'yaml';
|
||||
import { s__ } from '~/locale';
|
||||
import PodLogsButton from '~/environments/environment_details/components/kubernetes/pod_logs_button.vue';
|
||||
import { WORKLOAD_STATUS_BADGE_VARIANTS } from '../constants';
|
||||
import { WORKLOAD_STATUS_BADGE_VARIANTS, STATUS_LABELS } from '../constants';
|
||||
import WorkloadDetailsItem from './workload_details_item.vue';
|
||||
|
||||
export default {
|
||||
|
|
@ -73,6 +73,7 @@ export default {
|
|||
containers: s__('KubernetesDashboard|Containers'),
|
||||
},
|
||||
WORKLOAD_STATUS_BADGE_VARIANTS,
|
||||
STATUS_LABELS,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -86,21 +87,21 @@ export default {
|
|||
</workload-details-item>
|
||||
<workload-details-item v-if="itemLabels.length" :label="$options.i18n.labels">
|
||||
<div class="gl-display-flex gl-flex-wrap gl-gap-2">
|
||||
<gl-badge v-for="label of itemLabels" :key="label" class="gl-max-w-full">
|
||||
<gl-badge v-for="label of itemLabels" :key="label" class="gl-max-w-full gl-w-auto">
|
||||
<gl-truncate :text="label" with-tooltip />
|
||||
</gl-badge>
|
||||
</div>
|
||||
</workload-details-item>
|
||||
<workload-details-item v-if="item.status && !item.fullStatus" :label="$options.i18n.status">
|
||||
<gl-badge :variant="$options.WORKLOAD_STATUS_BADGE_VARIANTS[item.status]">{{
|
||||
item.status
|
||||
$options.STATUS_LABELS[item.status]
|
||||
}}</gl-badge>
|
||||
</workload-details-item>
|
||||
<workload-details-item v-if="item.fullStatus" :label="$options.i18n.status" collapsible>
|
||||
<template v-if="item.status" #label>
|
||||
<span class="gl-mr-2 gl-font-bold">{{ $options.i18n.status }}</span>
|
||||
<gl-badge :variant="$options.WORKLOAD_STATUS_BADGE_VARIANTS[item.status]">{{
|
||||
item.status
|
||||
$options.STATUS_LABELS[item.status]
|
||||
}}</gl-badge>
|
||||
</template>
|
||||
<pre>{{ statusYaml }}</pre>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(() =>
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
<script>
|
||||
import { GlTabs, GlTab, GlBadge, GlSprintf } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { joinPaths, updateHistory, pathSegments } from '~/lib/utils/url_utility';
|
||||
import { PROJECT_DASHBOARD_TABS, CONTRIBUTED_TAB } from 'ee_else_ce/projects/your_work/constants';
|
||||
import {
|
||||
PROJECT_DASHBOARD_TABS,
|
||||
CONTRIBUTED_TAB,
|
||||
CUSTOM_DASHBOARD_ROUTE_NAMES,
|
||||
} from 'ee_else_ce/projects/your_work/constants';
|
||||
|
||||
export default {
|
||||
name: 'YourWorkProjectsApp',
|
||||
|
|
@ -18,7 +21,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
activeTabIndex: 0,
|
||||
activeTabIndex: this.initActiveTabIndex(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -26,21 +29,11 @@ export default {
|
|||
return PROJECT_DASHBOARD_TABS.map((tab) => ({ ...tab, count: 0 }));
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getTabFromUrl();
|
||||
},
|
||||
methods: {
|
||||
getTabFromUrl() {
|
||||
const tab = pathSegments(window.location)?.pop();
|
||||
const tabIndex = PROJECT_DASHBOARD_TABS.findIndex(({ value }) => value === tab);
|
||||
|
||||
this.activeTabIndex = tabIndex > 0 ? tabIndex : 0;
|
||||
},
|
||||
setTabInUrl() {
|
||||
const tab = PROJECT_DASHBOARD_TABS[this.activeTabIndex] || CONTRIBUTED_TAB;
|
||||
const url = joinPaths(gon.relative_url_root || '/', `/dashboard/projects/${tab.value}`);
|
||||
|
||||
updateHistory({ url, replace: true });
|
||||
initActiveTabIndex() {
|
||||
return CUSTOM_DASHBOARD_ROUTE_NAMES.includes(this.$route.name)
|
||||
? 0
|
||||
: PROJECT_DASHBOARD_TABS.findIndex((tab) => tab.value === this.$route.name);
|
||||
},
|
||||
onTabUpdate(index) {
|
||||
// This return will prevent us overwriting the root `/` and `/dashboard/projects` paths
|
||||
|
|
@ -48,7 +41,9 @@ export default {
|
|||
if (index === this.activeTabIndex) return;
|
||||
|
||||
this.activeTabIndex = index;
|
||||
this.setTabInUrl();
|
||||
|
||||
const tab = PROJECT_DASHBOARD_TABS[index] || CONTRIBUTED_TAB;
|
||||
this.$router.push({ name: tab.value });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,3 +21,17 @@ export const MEMBER_TAB = {
|
|||
};
|
||||
|
||||
export const PROJECT_DASHBOARD_TABS = [CONTRIBUTED_TAB, STARRED_TAB, PERSONAL_TAB, MEMBER_TAB];
|
||||
|
||||
export const BASE_ROUTE = '/dashboard/projects';
|
||||
|
||||
export const ROOT_ROUTE_NAME = 'root';
|
||||
|
||||
export const DASHBOARD_ROUTE_NAME = 'dashboard';
|
||||
|
||||
export const PROJECTS_DASHBOARD_ROUTE_NAME = 'projects-dashboard';
|
||||
|
||||
export const CUSTOM_DASHBOARD_ROUTE_NAMES = [
|
||||
ROOT_ROUTE_NAME,
|
||||
DASHBOARD_ROUTE_NAME,
|
||||
PROJECTS_DASHBOARD_ROUTE_NAME,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,20 @@
|
|||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import routes from './routes';
|
||||
import YourWorkProjectsApp from './components/app.vue';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
export const createRouter = () => {
|
||||
const router = new VueRouter({
|
||||
routes,
|
||||
mode: 'history',
|
||||
base: gon.relative_url_root || '/',
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
export const initYourWorkProjects = () => {
|
||||
const el = document.getElementById('js-your-work-projects-app');
|
||||
|
||||
|
|
@ -8,6 +22,7 @@ export const initYourWorkProjects = () => {
|
|||
|
||||
return new Vue({
|
||||
el,
|
||||
router: createRouter(),
|
||||
name: 'YourWorkProjectsRoot',
|
||||
render(createElement) {
|
||||
return createElement(YourWorkProjectsApp);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import {
|
||||
BASE_ROUTE,
|
||||
ROOT_ROUTE_NAME,
|
||||
DASHBOARD_ROUTE_NAME,
|
||||
PROJECTS_DASHBOARD_ROUTE_NAME,
|
||||
PROJECT_DASHBOARD_TABS,
|
||||
} from 'ee_else_ce/projects/your_work/constants';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: ROOT_ROUTE_NAME,
|
||||
path: '/',
|
||||
},
|
||||
{
|
||||
name: DASHBOARD_ROUTE_NAME,
|
||||
path: '/dashboard',
|
||||
},
|
||||
{
|
||||
name: PROJECTS_DASHBOARD_ROUTE_NAME,
|
||||
path: BASE_ROUTE,
|
||||
},
|
||||
...PROJECT_DASHBOARD_TABS.map(({ value }) => ({
|
||||
name: value,
|
||||
path: `${BASE_ROUTE}/${value}`,
|
||||
})),
|
||||
];
|
||||
|
|
@ -22,7 +22,7 @@ export default {
|
|||
<slot v-if="$scopedSlots.heading" name="heading"></slot>
|
||||
<template v-else>{{ heading }}</template>
|
||||
</h2>
|
||||
<p v-if="$scopedSlots.description || description" class="gl-text-secondary gl-m-0">
|
||||
<p v-if="$scopedSlots.description || description" class="gl-text-secondary gl-mb-3">
|
||||
<slot v-if="$scopedSlots.description" name="description"></slot>
|
||||
<template v-else>{{ description }}</template>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { __, n__, sprintf } from '~/locale';
|
|||
import IssuableAssignees from '~/issuable/components/issue_assignees.vue';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
|
||||
import WorkItemPrefetch from '~/work_items/components/work_item_prefetch.vue';
|
||||
import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants';
|
||||
import { isAssigneesWidget, isLabelsWidget } from '~/work_items/utils';
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ export default {
|
|||
GlSprintf,
|
||||
IssuableAssignees,
|
||||
WorkItemTypeIcon,
|
||||
WorkItemPrefetch,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -286,17 +288,38 @@ export default {
|
|||
:title="__('This issue is hidden because its author has been banned.')"
|
||||
:aria-label="__('Hidden')"
|
||||
/>
|
||||
<gl-link
|
||||
class="issue-title-text gl-font-base"
|
||||
dir="auto"
|
||||
:href="issuableLinkHref"
|
||||
data-testid="issuable-title-link"
|
||||
v-bind="issuableTitleProps"
|
||||
@click="handleIssuableItemClick"
|
||||
>
|
||||
{{ issuable.title }}
|
||||
<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
|
||||
</gl-link>
|
||||
<template v-if="preventRedirect">
|
||||
<work-item-prefetch :work-item-iid="issuableIid" data-testid="issuable-prefetch-trigger">
|
||||
<template #default="{ prefetchWorkItem, clearPrefetching }">
|
||||
<gl-link
|
||||
class="issue-title-text gl-font-base"
|
||||
dir="auto"
|
||||
:href="issuableLinkHref"
|
||||
data-testid="issuable-title-link"
|
||||
v-bind="issuableTitleProps"
|
||||
@click="handleIssuableItemClick"
|
||||
@mouseover.native="prefetchWorkItem(issuableIid)"
|
||||
@mouseout.native="clearPrefetching"
|
||||
>
|
||||
{{ issuable.title }}
|
||||
<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
|
||||
</gl-link>
|
||||
</template>
|
||||
</work-item-prefetch>
|
||||
</template>
|
||||
<template v-else>
|
||||
<gl-link
|
||||
class="issue-title-text gl-font-base"
|
||||
dir="auto"
|
||||
:href="issuableLinkHref"
|
||||
data-testid="issuable-title-link"
|
||||
v-bind="issuableTitleProps"
|
||||
@click="handleIssuableItemClick"
|
||||
>
|
||||
{{ issuable.title }}
|
||||
<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
|
||||
</gl-link>
|
||||
</template>
|
||||
<slot v-if="hasSlotContents('title-icons')" name="title-icons"></slot>
|
||||
<span
|
||||
v-if="taskStatus"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
<script>
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
|
||||
|
||||
export default {
|
||||
name: 'WorkItemPrefetch',
|
||||
inject: {
|
||||
fullPath: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
props: {
|
||||
workItemIid: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
skipQuery: true,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
workItem: {
|
||||
query() {
|
||||
return workItemByIidQuery;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
iid: this.workItemIid,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.fullPath || this.skipQuery;
|
||||
},
|
||||
update(data) {
|
||||
return data.workspace.workItem ?? {};
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
prefetchWorkItem() {
|
||||
this.prefetch = setTimeout(() => {
|
||||
this.skipQuery = false;
|
||||
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
},
|
||||
clearPrefetching() {
|
||||
if (this.prefetch) {
|
||||
clearTimeout(this.prefetch);
|
||||
this.prefetch = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return this.$scopedSlots.default({
|
||||
prefetchWorkItem: this.prefetchWorkItem,
|
||||
clearPrefetching: this.clearPrefetching,
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
<script>
|
||||
import { GlBadge, GlIcon } from '@gitlab/ui';
|
||||
import { GlBadge } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { STATE_OPEN } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBadge,
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
workItemState: {
|
||||
|
|
@ -32,8 +31,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-badge :variant="workItemStateVariant" class="gl-align-middle">
|
||||
<gl-icon :name="workItemStateIcon" :size="16" />
|
||||
<span class="gl-sr-only sm:gl-not-sr-only gl-ml-2">{{ stateText }}</span>
|
||||
<gl-badge :variant="workItemStateVariant" :icon="workItemStateIcon" class="gl-align-middle">
|
||||
{{ stateText }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)\/'
|
||||
|
|
@ -8644,6 +8644,30 @@ Input type: `SecurityPolicyProjectCreateInput`
|
|||
| <a id="mutationsecuritypolicyprojectcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationsecuritypolicyprojectcreateproject"></a>`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 |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationsecuritypolicyprojectcreateasyncclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationsecuritypolicyprojectcreateasyncfullpath"></a>`fullPath` | [`String!`](#string) | Full path of the project or group. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationsecuritypolicyprojectcreateasyncclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationsecuritypolicyprojectcreateasyncerrors"></a>`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.
|
|||
| <a id="policylicensescanningviolationlicense"></a>`license` | [`String!`](#string) | License name. |
|
||||
| <a id="policylicensescanningviolationurl"></a>`url` | [`String`](#string) | URL of the license. |
|
||||
|
||||
### `PolicyProjectCreated`
|
||||
|
||||
Response of security policy creation.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="policyprojectcreatederrormessage"></a>`errorMessage` | [`String`](#string) | Error message in case status is :error. |
|
||||
| <a id="policyprojectcreatedproject"></a>`project` | [`Project`](#project) | Security Policy Project that was created. |
|
||||
| <a id="policyprojectcreatedstatus"></a>`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.
|
|||
| <a id="pipelinestatusenumwaiting_for_callback"></a>`WAITING_FOR_CALLBACK` | Pipeline is waiting for an external action. |
|
||||
| <a id="pipelinestatusenumwaiting_for_resource"></a>`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 |
|
||||
| ----- | ----------- |
|
||||
| <a id="policyprojectcreatedstatuserror"></a>`ERROR` | Creating the security policy project faild. |
|
||||
| <a id="policyprojectcreatedstatussuccess"></a>`SUCCESS` | Creating the security policy project was successful. |
|
||||
|
||||
### `PolicyViolationErrorType`
|
||||
|
||||
| Value | Description |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
|
|
@ -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<br>Dependency Scanning<br>Fuzz Testing<br>SAST |
|
||||
| Operations | Deployments<br>Pipelines<br>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}**).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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?'))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:')}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<ExistingEvent>]
|
||||
# @return [Array<Hash>] hash (compact) has keys/values:
|
||||
# value: [Array<NewMetric>]
|
||||
# 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<String>] event names
|
||||
# @return [Array<NewMetric>]
|
||||
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<ExistingEvent>]
|
||||
# @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<ExistingEvent>]
|
||||
# @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<ExistingEvent>]
|
||||
# @return [Array<String>]
|
||||
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<NewMetric>]
|
||||
# @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)")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(...)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :with_reassign_to_user do
|
||||
with_placeholder_user
|
||||
reassign_to_user factory: :user
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
23
spec/fixtures/scripts/internal_events/events/secondary_event_with_additional_properties.yml
vendored
Normal file
23
spec/fixtures/scripts/internal_events/events/secondary_event_with_additional_properties.yml
vendored
Normal file
|
|
@ -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
|
||||
25
spec/fixtures/scripts/internal_events/metrics/total_multi_event_some_additional_props.yml
vendored
Normal file
25
spec/fixtures/scripts/internal_events/metrics/total_multi_event_some_additional_props.yml
vendored
Normal file
|
|
@ -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
|
||||
24
spec/fixtures/scripts/internal_events/metrics/total_multiple_events_with_rename.yml
vendored
Normal file
24
spec/fixtures/scripts/internal_events/metrics/total_multiple_events_with_rename.yml
vendored
Normal file
|
|
@ -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
|
||||
28
spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_additional_props.yml
vendored
Normal file
28
spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_additional_props.yml
vendored
Normal file
|
|
@ -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
|
||||
44
spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_all_additional_props.yml
vendored
Normal file
44
spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_all_additional_props.yml
vendored
Normal file
|
|
@ -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
|
||||
28
spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_additional_props.yml
vendored
Normal file
28
spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_additional_props.yml
vendored
Normal file
|
|
@ -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
|
||||
44
spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_all_additional_props.yml
vendored
Normal file
44
spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_all_additional_props.yml
vendored
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue