Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-11-07 18:19:19 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-07 18:19:19 +0300
commitd4fcd1794ea9fc10d83cdc75490f76a418e59d52 (patch)
treeb072bfe2c59dc666ddaa28c11e0c04a7971014e0
parentdfa6eac07553d5a3f254ee904e4298bd666b410f (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab-ci.yml5
-rw-r--r--.gitlab/ci/rails/shared.gitlab-ci.yml4
-rw-r--r--.rubocop_todo/capybara/testid_finders.yml1
-rw-r--r--.rubocop_todo/layout/argument_alignment.yml1
-rw-r--r--.rubocop_todo/rspec/feature_category.yml3
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.checksum4
-rw-r--r--Gemfile.lock9
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js18
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue5
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue7
-rw-r--r--app/assets/javascripts/diffs/components/app.vue21
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue7
-rw-r--r--app/assets/javascripts/diffs/store/actions.js6
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js5
-rw-r--r--app/assets/javascripts/diffs/store/utils.js2
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue8
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue4
-rw-r--r--app/assets/javascripts/issuable/components/status_badge.vue10
-rw-r--r--app/assets/javascripts/issues/show/components/issue_header.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/sticky_header.vue12
-rw-r--r--app/assets/javascripts/lib/utils/color_utils.js2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js5
-rw-r--r--app/assets/javascripts/observability/client.js28
-rw-r--r--app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql5
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js1
-rw-r--r--app/assets/javascripts/profile/preferences/components/profile_preferences.vue6
-rw-r--r--app/assets/javascripts/projects/settings/init_access_dropdown.js1
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js14
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue11
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/index.vue128
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue1
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue13
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue5
-rw-r--r--app/assets/javascripts/work_items/constants.js6
-rw-r--r--app/assets/stylesheets/framework/files.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss9
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss4
-rw-r--r--app/assets/stylesheets/themes/theme_blue.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_gray.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_green.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_indigo.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_blue.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_gray.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_green.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_indigo.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_red.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_red.scss2
-rw-r--r--app/controllers/groups/settings/applications_controller.rb2
-rw-r--r--app/controllers/projects/artifacts_controller.rb6
-rw-r--r--app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb7
-rw-r--r--app/graphql/resolvers/issues/base_parent_resolver.rb9
-rw-r--r--app/graphql/resolvers/issues_resolver.rb7
-rw-r--r--app/graphql/types/issuable_state_enum.rb5
-rw-r--r--app/graphql/types/organizations/organization_user_badge_type.rb22
-rw-r--r--app/graphql/types/organizations/organization_user_type.rb4
-rw-r--r--app/graphql/types/user_interface.rb5
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/helpers/nav_helper.rb14
-rw-r--r--app/helpers/sorting_helper.rb15
-rw-r--r--app/helpers/users/callouts_helper.rb9
-rw-r--r--app/models/application_setting_implementation.rb1
-rw-r--r--app/models/ci/catalog/components_project.rb2
-rw-r--r--app/models/ci/catalog/resource.rb7
-rw-r--r--app/models/ci/catalog/resources/component.rb2
-rw-r--r--app/models/ci/catalog/resources/version.rb2
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/group.rb77
-rw-r--r--app/models/member.rb11
-rw-r--r--app/models/members/members/members_with_parents.rb105
-rw-r--r--app/models/namespace.rb8
-rw-r--r--app/models/project_feature_usage.rb13
-rw-r--r--app/models/users/callout.rb3
-rw-r--r--app/presenters/project_presenter.rb4
-rw-r--r--app/serializers/review_app_setup_entity.rb2
-rw-r--r--app/services/ci/catalog/resources/versions/create_service.rb111
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml3
-rw-r--r--app/views/layouts/application.html.haml4
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/layouts/devise_empty.html.haml4
-rw-r--r--app/views/layouts/fullscreen.html.haml4
-rw-r--r--app/views/layouts/signup_onboarding.html.haml4
-rw-r--r--app/views/layouts/terms.html.haml5
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml2
-rw-r--r--app/views/shared/_new_nav_for_everyone_announcement.html.haml18
-rw-r--r--app/workers/bulk_imports/entity_worker.rb16
-rw-r--r--config/feature_categories.yml1
-rw-r--r--config/feature_flags/development/ci_catalog_create_metadata.yml8
-rw-r--r--config/feature_flags/development/manage_project_access_tokens.yml2
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml4
-rw-r--r--config/metrics/settings/20210204124920_web_ide_clientside_preview_enabled.yml4
-rw-r--r--danger/documentation/Dangerfile7
-rw-r--r--data/deprecations/16-6-deprecation-legacy-geo-prometheus-metrics.yml22
-rw-r--r--db/click_house/main/20230707151359_create_ci_finished_builds.sql2
-rw-r--r--db/click_house/main/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.sql2
-rw-r--r--db/migrate/20231002162941_add_enable_artifact_external_redirect_warning_page_to_application_settings.rb10
-rw-r--r--db/migrate/20231024133234_add_source_package_name_to_sbom_component.rb28
-rw-r--r--db/post_migrate/20231102083539_backfill_p_ci_builds_pipeline_id.rb34
-rw-r--r--db/schema_migrations/202310021629411
-rw-r--r--db/schema_migrations/202310241332341
-rw-r--r--db/schema_migrations/202311020835391
-rw-r--r--db/structure.sql7
-rw-r--r--doc/administration/settings/continuous_integration.md16
-rw-r--r--doc/api/graphql/reference/index.md49
-rw-r--r--doc/api/settings.md1
-rw-r--r--doc/ci/components/index.md48
-rw-r--r--doc/ci/yaml/inputs.md8
-rw-r--r--doc/development/i18n/proofreader.md1
-rw-r--r--doc/subscriptions/self_managed/index.md29
-rw-r--r--doc/update/deprecations.md27
-rw-r--r--doc/user/group/index.md2
-rw-r--r--doc/user/product_analytics/index.md14
-rw-r--r--doc/user/project/members/index.md2
-rw-r--r--doc/user/project/members/share_project_with_groups.md2
-rw-r--r--gems/gitlab-http/Gemfile.lock1
-rw-r--r--gems/gitlab-http/README.md17
-rw-r--r--gems/gitlab-http/gitlab-http.gemspec1
-rw-r--r--gems/gitlab-http/lib/gitlab/http_v2/client.rb74
-rw-r--r--gems/gitlab-http/lib/gitlab/http_v2/lazy_response.rb48
-rw-r--r--gems/gitlab-http/spec/gitlab/http_v2_spec.rb97
-rw-r--r--lib/api/settings.rb1
-rw-r--r--lib/gitlab/background_migration/batched_migration_job.rb2
-rw-r--r--lib/gitlab/database/dynamic_model_helpers.rb3
-rw-r--r--lib/gitlab/database/migration_helpers.rb6
-rw-r--r--lib/gitlab/database/migrations/batched_background_migration_helpers.rb2
-rw-r--r--lib/gitlab/github_import/attachments_downloader.rb24
-rw-r--r--lib/gitlab/usage_data.rb1
-rw-r--r--locale/gitlab.pot40
-rw-r--r--package.json2
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--spec/controllers/groups/settings/applications_controller_spec.rb357
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb28
-rw-r--r--spec/features/boards/board_filters_spec.rb1
-rw-r--r--spec/features/boards/boards_spec.rb3
-rw-r--r--spec/features/boards/issue_ordering_spec.rb1
-rw-r--r--spec/features/boards/new_issue_spec.rb4
-rw-r--r--spec/features/boards/reload_boards_on_browser_back_spec.rb2
-rw-r--r--spec/features/boards/sidebar_labels_in_namespaces_spec.rb2
-rw-r--r--spec/features/boards/sidebar_labels_spec.rb1
-rw-r--r--spec/features/boards/sidebar_spec.rb1
-rw-r--r--spec/features/boards/user_adds_lists_to_board_spec.rb1
-rw-r--r--spec/features/boards/user_visits_board_spec.rb3
-rw-r--r--spec/features/groups/board_sidebar_spec.rb1
-rw-r--r--spec/features/groups/board_spec.rb4
-rw-r--r--spec/features/nav/new_nav_for_everyone_callout_spec.rb55
-rw-r--r--spec/features/nav/new_nav_toggle_spec.rb59
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js2
-rw-r--r--spec/frontend/diffs/components/app_spec.js6
-rw-r--r--spec/frontend/diffs/store/actions_spec.js2
-rw-r--r--spec/frontend/diffs/store/utils_spec.js2
-rw-r--r--spec/frontend/environments/environments_app_spec.js29
-rw-r--r--spec/frontend/environments/graphql/mock_data.js2
-rw-r--r--spec/frontend/issuable/components/status_badge_spec.js8
-rw-r--r--spec/frontend/issues/show/components/issue_header_spec.js6
-rw-r--r--spec/frontend/issues/show/components/sticky_header_spec.js12
-rw-r--r--spec/frontend/lib/utils/color_utils_spec.js10
-rw-r--r--spec/frontend/observability/client_spec.js37
-rw-r--r--spec/frontend/organizations/users/mock_data.js5
-rw-r--r--spec/frontend/protected_branches/protected_branch_edit_spec.js348
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js1
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/list_selector/index_spec.js114
-rw-r--r--spec/frontend/vue_shared/components/list_selector/mock_data.js40
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js1
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js2
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js43
-rw-r--r--spec/graphql/types/organizations/organization_user_badge_type_spec.rb10
-rw-r--r--spec/graphql/types/user_type_spec.rb1
-rw-r--r--spec/helpers/nav_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/batched_migration_job_spec.rb2
-rw-r--r--spec/lib/gitlab/database/dynamic_model_helpers_spec.rb67
-rw-r--r--spec/lib/gitlab/github_import/attachments_downloader_spec.rb17
-rw-r--r--spec/lib/gitlab/other_markup_spec.rb16
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb50
-rw-r--r--spec/models/ci/catalog/components_project_spec.rb6
-rw-r--r--spec/models/ci/catalog/resource_spec.rb30
-rw-r--r--spec/models/ci/catalog/resources/component_spec.rb17
-rw-r--r--spec/models/concerns/ci/partitionable/switch_spec.rb11
-rw-r--r--spec/models/group_spec.rb6
-rw-r--r--spec/models/member_spec.rb16
-rw-r--r--spec/models/members/members/members_with_parents_spec.rb92
-rw-r--r--spec/models/namespace_spec.rb42
-rw-r--r--spec/presenters/project_presenter_spec.rb22
-rw-r--r--spec/requests/api/graphql/organizations/organization_query_spec.rb7
-rw-r--r--spec/serializers/review_app_setup_entity_spec.rb4
-rw-r--r--spec/services/application_settings/update_service_spec.rb4
-rw-r--r--spec/services/ci/catalog/resources/versions/create_service_spec.rb180
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/views/themed_layout_examples.rb6
-rw-r--r--spec/tooling/danger/project_helper_spec.rb1
-rw-r--r--tooling/danger/project_helper.rb2
-rwxr-xr-xvendor/languages.yml2
-rw-r--r--yarn.lock28
210 files changed, 2699 insertions, 953 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7ef143d5525..9a24f58ad73 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -105,6 +105,11 @@ workflow:
<<: [*default-ruby-variables, *default-branch-pipeline-failure-variables]
GITLAB_DEPENDENCY_PROXY_ADDRESS: ""
PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_COMMIT_BRANCH branch pipeline (triggered by a project token)'
+ # For `$CI_DEFAULT_BRANCH` from wider community contributors, we don't want to run any pipelines on pushes,
+ # because normally we want to run merge request pipelines and scheduled pipelines, not for repository synchronization.
+ # This can avoid accidentally using up pipeline minutes quota while synchronizing the repository for wider community contributors.
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_NAMESPACE !~ /^gitlab(-org|-cn)?($|\/)/'
+ when: never
# For `$CI_DEFAULT_BRANCH` branch, create a pipeline (this includes on schedules, pushes, merges, etc.).
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
variables:
diff --git a/.gitlab/ci/rails/shared.gitlab-ci.yml b/.gitlab/ci/rails/shared.gitlab-ci.yml
index 6d6d99231ab..6046c672f7b 100644
--- a/.gitlab/ci/rails/shared.gitlab-ci.yml
+++ b/.gitlab/ci/rails/shared.gitlab-ci.yml
@@ -84,10 +84,10 @@ include:
- echo -e "\e[0Ksection_start:`date +%s`:report_results_section[collapsed=true]\r\e[0KReport results"
- |
if [ "$CREATE_RAILS_TEST_FAILURE_ISSUES" == "true" ]; then
- bundle exec relate-failure-issue --input-files "rspec/rspec-*.json" --system-log-files "log" --project "gitlab-org/gitlab" --token "${TEST_FAILURES_PROJECT_TOKEN}";
+ bundle exec relate-failure-issue --input-files "rspec/rspec-*.json" --system-log-files "log" --project "gitlab-org/gitlab" --token "${TEST_FAILURES_PROJECT_TOKEN}" --related-issues-file "rspec/${CI_JOB_ID}-failed-test-issues.json";
fi
if [ "$CREATE_RAILS_SLOW_TEST_ISSUES" == "true" ]; then
- bundle exec slow-test-issues --input-files "rspec/rspec-*.json" --project "gitlab-org/gitlab" --token "${TEST_FAILURES_PROJECT_TOKEN}";
+ bundle exec slow-test-issues --input-files "rspec/rspec-*.json" --project "gitlab-org/gitlab" --token "${TEST_FAILURES_PROJECT_TOKEN}" --related-issues-file "rspec/${CI_JOB_ID}-slow-test-issues.json";
fi
if [ "$ADD_SLOW_TEST_NOTE_TO_MERGE_REQUEST" == "true" ]; then
bundle exec slow-test-merge-request-report-note --input-files "rspec/rspec-*.json" --project "gitlab-org/gitlab" --merge_request_iid "$CI_MERGE_REQUEST_IID" --token "${TEST_SLOW_NOTE_PROJECT_TOKEN}";
diff --git a/.rubocop_todo/capybara/testid_finders.yml b/.rubocop_todo/capybara/testid_finders.yml
index 0238bfbc86a..ddaf1e27beb 100644
--- a/.rubocop_todo/capybara/testid_finders.yml
+++ b/.rubocop_todo/capybara/testid_finders.yml
@@ -60,7 +60,6 @@ Capybara/TestidFinders:
- 'spec/features/merge_request/user_views_open_merge_request_spec.rb'
- 'spec/features/milestone_spec.rb'
- 'spec/features/nav/new_nav_callout_spec.rb'
- - 'spec/features/nav/new_nav_toggle_spec.rb'
- 'spec/features/nav/pinned_nav_items_spec.rb'
- 'spec/features/populate_new_pipeline_vars_with_params_spec.rb'
- 'spec/features/profile_spec.rb'
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index e9a23d2ca7e..91c7f265f99 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -212,7 +212,6 @@ Layout/ArgumentAlignment:
- 'app/graphql/resolvers/group_releases_resolver.rb'
- 'app/graphql/resolvers/groups_resolver.rb'
- 'app/graphql/resolvers/incident_management/timeline_events_resolver.rb'
- - 'app/graphql/resolvers/issues/base_parent_resolver.rb'
- 'app/graphql/resolvers/issues/base_resolver.rb'
- 'app/graphql/resolvers/issues_resolver.rb'
- 'app/graphql/resolvers/labels_resolver.rb'
diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml
index 330cdc4e8e5..6a06ffef089 100644
--- a/.rubocop_todo/rspec/feature_category.yml
+++ b/.rubocop_todo/rspec/feature_category.yml
@@ -2846,7 +2846,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/background_migration/backfill_user_details_fields_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb'
- 'spec/lib/gitlab/background_migration/base_job_spec.rb'
- - 'spec/lib/gitlab/background_migration/batched_migration_job_spec.rb'
- 'spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb'
- 'spec/lib/gitlab/background_migration/batching_strategies/base_strategy_spec.rb'
- 'spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb'
@@ -3262,7 +3261,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb'
- 'spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb'
- 'spec/lib/gitlab/database/count_spec.rb'
- - 'spec/lib/gitlab/database/dynamic_model_helpers_spec.rb'
- 'spec/lib/gitlab/database/each_database_spec.rb'
- 'spec/lib/gitlab/database/grant_spec.rb'
- 'spec/lib/gitlab/database/load_balancing/configuration_spec.rb'
@@ -3285,7 +3283,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/database/migration_spec.rb'
- 'spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb'
- 'spec/lib/gitlab/database/migrations/base_background_runner_spec.rb'
- - 'spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb'
- 'spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb'
- 'spec/lib/gitlab/database/migrations/extension_helpers_spec.rb'
- 'spec/lib/gitlab/database/migrations/instrumentation_spec.rb'
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 2da0a2a4e91..f269bd38a5b 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-14.29.0
+14.30.0
diff --git a/Gemfile b/Gemfile
index d069c116a73..1536fa6bafe 100644
--- a/Gemfile
+++ b/Gemfile
@@ -63,7 +63,7 @@ gem 'marginalia', '~> 1.11.1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'declarative_policy', '~> 1.1.0' # rubocop:todo Gemfile/MissingFeatureCategory
# Authentication libraries
-gem 'devise', '~> 4.8.1' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'devise', '~> 4.9.3', feature_category: :system_access
gem 'devise-pbkdf2-encryptable', '~> 0.0.0', path: 'vendor/gems/devise-pbkdf2-encryptable' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'bcrypt', '~> 3.1', '>= 3.1.14' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'doorkeeper', '~> 5.6', '>= 5.6.6' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -208,7 +208,7 @@ gem 'deckar01-task_list', '2.3.3' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'gitlab-markup', '~> 1.9.0', require: 'github/markup' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'commonmarker', '~> 0.23.10' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'kramdown', '~> 2.3.1' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'RedCloth', '~> 4.3.2' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'RedCloth', '~> 4.3.3' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'org-ruby', '~> 0.9.12' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'creole', '~> 0.5.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'wikicloth', '0.8.1' # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 8b2da2ba7c8..59a1324ce23 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -1,6 +1,6 @@
[
{"name":"CFPropertyList","version":"3.0.5","platform":"ruby","checksum":"a78551cd4768d78ebca98488c27e33652ef818be64697a54676d34e6434674a4"},
-{"name":"RedCloth","version":"4.3.2","platform":"ruby","checksum":"1ee7bc55c8dcec92cf7741a2132a9a6cd19e4b884fbc1b3aca23e1a4fcd92d55"},
+{"name":"RedCloth","version":"4.3.3","platform":"ruby","checksum":"d941b8ac96e2730d2d9326d97dda9fcf64cb73532b3f902d91c18970c5f4632d"},
{"name":"acme-client","version":"2.0.11","platform":"ruby","checksum":"edf6da9f3c5dbe3ab0c6738eb3b97978b7a60e3500445480d2a72fcc610089de"},
{"name":"actioncable","version":"7.0.8","platform":"ruby","checksum":"1f504ddb4ab6a34f7c52e9df924441a403e9f358bace330c36dcca6358ecfb84"},
{"name":"actionmailbox","version":"7.0.8","platform":"ruby","checksum":"9420037b801e44aa4e36cf113f4bd6eb25c17eb1b84d9c8865e8abf8846c14e5"},
@@ -115,7 +115,7 @@
{"name":"devfile","version":"0.0.24.pre.alpha1","platform":"ruby","checksum":"72bbfc26edb519902d5c68e07188e0a3d699a1866392fa1497e5b7f3abb36600"},
{"name":"devfile","version":"0.0.24.pre.alpha1","platform":"x86_64-linux","checksum":"d121b1094aa3a24c29592a83c629ee640920e0196711dd06f27b6fa9b1ced609"},
{"name":"device_detector","version":"1.0.0","platform":"ruby","checksum":"b800fb3150b00c23e87b6768011808ac1771fffaae74c3238ebaf2b782947a7d"},
-{"name":"devise","version":"4.8.1","platform":"ruby","checksum":"fdd48bbe79a89e7c1152236a70479842ede48bea4fa7f4f2d8da1f872559803e"},
+{"name":"devise","version":"4.9.3","platform":"ruby","checksum":"480638d6c51b97f56da6e28d4f3e2a1b8e606681b316aa594b87c6ab94923488"},
{"name":"devise-two-factor","version":"4.1.1","platform":"ruby","checksum":"c95f5b07533e62217aaed3c386874d94e2d472fb5f2b6598afe8600fc17a8b95"},
{"name":"diff-lcs","version":"1.5.0","platform":"ruby","checksum":"49b934001c8c6aedb37ba19daec5c634da27b318a7a3c654ae979d6ba1929b67"},
{"name":"diff_match_patch","version":"0.1.0","platform":"ruby","checksum":"b36057bfcfeaedf19dcb7b2c28c19ee625bd6ec6d0d182717d3ef22b3879c40e"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 6f94a4f275e..55d6fe0dd0e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -28,6 +28,7 @@ PATH
specs:
gitlab-http (0.1.0)
activesupport (~> 7)
+ concurrent-ruby (~> 1.2)
httparty (~> 0.21.0)
ipaddress (~> 0.8.3)
nokogiri (~> 1.15.4)
@@ -161,7 +162,7 @@ GEM
specs:
CFPropertyList (3.0.5)
rexml
- RedCloth (4.3.2)
+ RedCloth (4.3.3)
acme-client (2.0.11)
faraday (>= 1.0, < 3.0.0)
faraday-retry (~> 1.0)
@@ -444,7 +445,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
devfile (0.0.24.pre.alpha1)
device_detector (1.0.0)
- devise (4.8.1)
+ devise (4.9.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@@ -1743,7 +1744,7 @@ PLATFORMS
DEPENDENCIES
CFPropertyList (~> 3.0.0)
- RedCloth (~> 4.3.2)
+ RedCloth (~> 4.3.3)
acme-client (~> 2.0)
activerecord-explain-analyze (~> 0.1)
activerecord-gitlab!
@@ -1799,7 +1800,7 @@ DEPENDENCIES
derailed_benchmarks
devfile (~> 0.0.24.pre.alpha1)
device_detector
- devise (~> 4.8.1)
+ devise (~> 4.9.3)
devise-pbkdf2-encryptable (~> 0.0.0)!
devise-two-factor (~> 4.1.1)
diff_match_patch (~> 0.1.0)
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
index 070ce38c8aa..d97f11a0acd 100644
--- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
+++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
@@ -72,22 +72,20 @@ export const fetchDrafts = ({ commit, getters, state, dispatch }) =>
}),
);
-export const publishSingleDraft = ({ commit, dispatch, getters }, draftId) => {
+export const publishSingleDraft = ({ commit, getters }, draftId) => {
commit(types.REQUEST_PUBLISH_DRAFT, draftId);
service
.publishDraft(getters.getNotesData.draftsPublishPath, draftId)
- .then(() => dispatch('updateDiscussionsAfterPublish'))
.then(() => commit(types.RECEIVE_PUBLISH_DRAFT_SUCCESS, draftId))
.catch(() => commit(types.RECEIVE_PUBLISH_DRAFT_ERROR, draftId));
};
-export const publishReview = ({ commit, dispatch, getters }, noteData = {}) => {
+export const publishReview = ({ commit, getters }, noteData = {}) => {
commit(types.REQUEST_PUBLISH_REVIEW);
return service
.publish(getters.getNotesData.draftsPublishPath, noteData)
- .then(() => dispatch('updateDiscussionsAfterPublish'))
.then(() => commit(types.RECEIVE_PUBLISH_REVIEW_SUCCESS))
.catch((e) => {
commit(types.RECEIVE_PUBLISH_REVIEW_ERROR);
@@ -96,18 +94,6 @@ export const publishReview = ({ commit, dispatch, getters }, noteData = {}) => {
});
};
-export const updateDiscussionsAfterPublish = async ({ dispatch, getters, rootGetters }) => {
- await dispatch(
- 'fetchDiscussions',
- { path: getters.getNotesData.discussionsPath },
- { root: true },
- );
-
- dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, {
- root: true,
- });
-};
-
export const updateDraft = (
{ commit, getters },
{ note, noteText, resolveDiscussion, position, flashContainer, callback, errorCallback },
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
index fce7aabf0cf..3da2f27c1b9 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
@@ -248,7 +248,6 @@ export default {
<template #downstream>
<linked-pipelines-column
v-if="showDownstreamPipelines"
- class="gl-mr-5"
:class="{ 'gl-sm-ml-3': isNewPipelineGraph }"
:config-paths="configPaths"
:linked-pipelines="downstreamPipelines"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
index 9bd0ec6d793..0d72373a0f5 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
@@ -11,7 +11,10 @@ export default {
};
</script>
<template>
- <div class="gl-display-flex" :class="{ 'gl-flex-wrap gl-w-full': isNewPipelineGraph }">
+ <div
+ class="gl-display-flex"
+ :class="{ 'gl-flex-wrap gl-sm-flex-nowrap gl-w-full': isNewPipelineGraph }"
+ >
<slot name="upstream"></slot>
<slot name="main"></slot>
<slot name="downstream"></slot>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
index 67918ea8d1a..c715d6af28a 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
@@ -74,7 +74,7 @@ export default {
left: 'gl-mx-6',
};
const positionValues = {
- right: 'gl-ml-5',
+ right: 'gl-mx-5',
left: 'gl-mx-4 gl-flex-basis-full',
};
const usePositionValues = this.isNewPipelineGraph ? positionValues : positionValuesOld;
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
index e144b9aab0c..01a9c6d030d 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
@@ -179,7 +179,7 @@ export default {
<template #stages>
<div
data-testid="stage-column-title"
- class="gl-display-flex gl-justify-content-space-between gl-relative"
+ class="stage-column-title gl-display-flex gl-justify-content-space-between gl-relative"
:class="titleClasses"
>
<span :title="name" class="gl-text-truncate gl-pr-3 gl-w-85p">
diff --git a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
index 5c1841615ab..dc4a2d91c84 100644
--- a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
+++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
@@ -403,12 +403,7 @@ export default {
{{ commitTitle }}
</h3>
<div>
- <ci-icon
- :status="detailedStatus"
- show-status-text
- :show-link="false"
- class="gl-display-inline-block gl-mb-3"
- />
+ <ci-icon :status="detailedStatus" show-status-text :show-link="false" class="gl-mb-3" />
<div class="gl-ml-2 gl-mb-3 gl-display-inline-block gl-h-6">
<gl-link
v-if="user"
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 7c3d6dc8c42..9971d3bf7f8 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -366,22 +366,11 @@ export default {
handleLocationHash();
this.autoScrolled = true;
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- this.unwatchDiscussions = this.$watch(
- () => `${this.flatBlobsList.length}:${this.$store.state.notes.discussions.length}`,
- () => {
- this.setDiscussions();
-
- if (this.$store.state.notes.doneFetchingBatchDiscussions) {
- this.unwatchDiscussions();
- }
- },
- );
-
- this.unwatchRetrievingBatches = this.$watch(
- () => `${this.retrievingBatches}:${this.$store.state.notes.discussions.length}`,
- () => {
- if (!this.retrievingBatches && this.$store.state.notes.discussions.length) {
- this.unwatchRetrievingBatches();
+ this.$watch(
+ () => this.$store.state.notes.discussions.length,
+ (newVal, prevVal) => {
+ if (newVal > prevVal) {
+ this.setDiscussions();
}
},
);
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index 8915f32eadf..556f72059c2 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -39,12 +39,6 @@ export default {
},
methods: {
...mapActions(['toggleDiscussion']),
- ...mapActions('diffs', ['removeDiscussionsFromDiff']),
- deleteNoteHandler(discussion) {
- if (discussion.notes.length <= 1) {
- this.removeDiscussionsFromDiff(discussion);
- }
- },
isExpanded(discussion) {
return this.shouldCollapseDiscussions ? discussion.expanded : true;
},
@@ -90,7 +84,6 @@ export default {
:line="line"
:help-page-path="helpPagePath"
:should-scroll-to-note="false"
- @noteDeleted="deleteNoteHandler"
>
<template v-if="renderAvatarBadge" #avatar-badge>
<design-note-pin
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index d86a88f97b8..756f76569dc 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -419,7 +419,11 @@ export const assignDiscussionsToDiff = (
};
export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
- const { file_hash: fileHash, line_code: lineCode, id } = removeDiscussion;
+ const {
+ diff_file: { file_hash: fileHash },
+ line_code: lineCode,
+ id,
+ } = removeDiscussion;
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode, id });
};
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 31369b169f5..a9a2c35faa4 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -198,9 +198,10 @@ export default {
return {
...line,
discussionsExpanded:
- line.discussions && line.discussions.length
+ line.discussionsExpanded ||
+ (line.discussions && line.discussions.length
? line.discussions.some((disc) => !disc.resolved) || isLineNoteTargeted
- : false,
+ : false),
};
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 15d2ab71bc8..fb467a606b9 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -338,7 +338,7 @@ function prepareLine(line, file) {
problems.brokenSymlink || problems.fileOnlyMoved || problems.brokenLineCode,
),
rich_text: cleanRichText(line.rich_text),
- discussionsExpanded: true,
+ discussionsExpanded: false,
discussions: [],
hasForm: false,
text: undefined,
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 795cbf5327a..fd5fcb12cc5 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -112,6 +112,9 @@ export default {
canSetupReviewApp() {
return this.environmentApp?.reviewApp?.canSetupReviewApp;
},
+ hasReviewApp() {
+ return this.environmentApp?.reviewApp?.hasReviewApp;
+ },
canCleanUpEnvs() {
return this.environmentApp?.canStopStaleEnvironments;
},
@@ -157,7 +160,10 @@ export default {
};
},
openReviewAppModal() {
- if (!this.canSetupReviewApp) {
+ // we don't show the Enable review apps button
+ // if a user cannot setup a review app or review
+ // apps are already configured
+ if (!this.canSetupReviewApp || this.hasReviewApp) {
return null;
}
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index 2ee7b604253..126a3a84d66 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -194,7 +194,7 @@ export default {
<div
class="item-attributes-area gl-display-flex gl-align-items-center gl-flex-wrap gl-gap-3"
>
- <span v-if="hasPipeline" class="mr-ci-status order-md-last">
+ <span v-if="hasPipeline" class="mr-ci-status order-md-last gl-md-ml-3 gl-mr-n2">
<a :href="pipelinePath">
<ci-icon :status="pipelineStatus" :title="pipelineStatusTooltip" />
</a>
@@ -203,7 +203,7 @@ export default {
<issue-milestone
v-if="hasMilestone"
:milestone="milestone"
- class="item-milestone gl-font-sm gl-display-flex gl-align-items-center order-md-first"
+ class="item-milestone gl-font-sm gl-display-flex gl-align-items-center order-md-first gl-ml-2"
/>
<!-- Flex order for slots is defined in the parent component: e.g. related_issues_block.vue -->
diff --git a/app/assets/javascripts/issuable/components/status_badge.vue b/app/assets/javascripts/issuable/components/status_badge.vue
index 949fb3c1ce5..35f6446d582 100644
--- a/app/assets/javascripts/issuable/components/status_badge.vue
+++ b/app/assets/javascripts/issuable/components/status_badge.vue
@@ -14,29 +14,29 @@ import {
const badgePropertiesMap = {
[TYPE_EPIC]: {
[STATUS_OPEN]: {
- icon: 'epic',
+ icon: 'issue-open-m',
text: __('Open'),
variant: 'success',
},
[STATUS_CLOSED]: {
- icon: 'epic-closed',
+ icon: 'issue-close',
text: __('Closed'),
variant: 'info',
},
},
[TYPE_ISSUE]: {
[STATUS_OPEN]: {
- icon: 'issues',
+ icon: 'issue-open-m',
text: __('Open'),
variant: 'success',
},
[STATUS_CLOSED]: {
- icon: 'issue-closed',
+ icon: 'issue-close',
text: __('Closed'),
variant: 'info',
},
[STATUS_LOCKED]: {
- icon: 'issues',
+ icon: 'issue-open-m',
text: __('Open'),
variant: 'success',
},
diff --git a/app/assets/javascripts/issues/show/components/issue_header.vue b/app/assets/javascripts/issues/show/components/issue_header.vue
index c205a6361c7..96eb8fbb3c7 100644
--- a/app/assets/javascripts/issues/show/components/issue_header.vue
+++ b/app/assets/javascripts/issues/show/components/issue_header.vue
@@ -82,7 +82,7 @@ export default {
return this.issuableState === STATUS_OPEN || this.issuableState === STATUS_REOPENED;
},
statusIcon() {
- return this.isOpen ? 'issues' : 'issue-closed';
+ return this.isOpen ? 'issue-open-m' : 'issue-close';
},
statusText() {
if (this.isOpen) {
diff --git a/app/assets/javascripts/issues/show/components/sticky_header.vue b/app/assets/javascripts/issues/show/components/sticky_header.vue
index d75f6c75ba5..18e37c4216c 100644
--- a/app/assets/javascripts/issues/show/components/sticky_header.vue
+++ b/app/assets/javascripts/issues/show/components/sticky_header.vue
@@ -2,12 +2,7 @@
import { GlBadge, GlIcon, GlIntersectionObserver, GlLink } from '@gitlab/ui';
import HiddenBadge from '~/issuable/components/hidden_badge.vue';
import LockedBadge from '~/issuable/components/locked_badge.vue';
-import {
- issuableStatusText,
- STATUS_CLOSED,
- TYPE_EPIC,
- WORKSPACE_PROJECT,
-} from '~/issues/constants';
+import { issuableStatusText, STATUS_CLOSED, WORKSPACE_PROJECT } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
export default {
@@ -60,10 +55,7 @@ export default {
return this.issuableStatus === STATUS_CLOSED;
},
statusIcon() {
- if (this.issuableType === TYPE_EPIC) {
- return this.isClosed ? 'epic-closed' : 'epic';
- }
- return this.isClosed ? 'issue-closed' : 'issues';
+ return this.isClosed ? 'issue-close' : 'issue-open-m';
},
statusText() {
return issuableStatusText[this.issuableStatus];
diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js
index a9f4257e28b..74c9f7de8c1 100644
--- a/app/assets/javascripts/lib/utils/color_utils.js
+++ b/app/assets/javascripts/lib/utils/color_utils.js
@@ -46,5 +46,5 @@ export function darkModeEnabled() {
if (isWebIde) {
return ideDarkThemes.includes(window.gon?.user_color_scheme);
}
- return document.body.classList.contains('gl-dark');
+ return document.documentElement.classList.contains('gl-dark');
}
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index e0b1f7a8c6a..493beb8cea9 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -290,9 +290,6 @@ export default {
parent: this.$el,
});
},
- deleteNoteHandler(note) {
- this.$emit('noteDeleted', this.discussion, note);
- },
onStartReplying(discussionId) {
if (this.discussion.id === discussionId) {
this.showReplyForm();
@@ -329,7 +326,6 @@ export default {
:is-overview-tab="isOverviewTab"
:should-scroll-to-note="shouldScrollToNote"
@startReplying="showReplyForm"
- @deleteNote="deleteNoteHandler"
>
<template #avatar-badge>
<slot name="avatar-badge"></slot>
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 966f4184780..a995b9fa214 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -318,11 +318,6 @@ export default {
const note = noteData;
const selectedDiscussion = state.discussions.find((disc) => disc.id === note.id);
note.expanded = true; // override expand flag to prevent collapse
- if (note.diff_file) {
- Object.assign(note, {
- file_hash: note.diff_file.file_hash,
- });
- }
Object.assign(selectedDiscussion, { ...note });
},
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index da8837c21da..54ca8311621 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -246,22 +246,18 @@ async function fetchOperations(operationsUrl, serviceName) {
}
}
-async function fetchMetrics() {
- // TODO replace mocks with API calls https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2469
- /* eslint-disable @gitlab/require-i18n-strings */
- return {
- metrics: [
- { name: 'metric A', description: 'a counter metric called A', type: 'COUNTER' },
- { name: 'metric B', description: 'a gauge metric called B', type: 'GAUGE' },
- { name: 'metric C', description: 'a histogram metric called C', type: 'HISTOGRAM' },
- {
- name: 'metric D',
- description: 'a exp histogram metric called D',
- type: 'EXPONENTIAL HISTOGRAM',
- },
- ],
- };
- /* eslint-enable @gitlab/require-i18n-strings */
+async function fetchMetrics(metricsUrl) {
+ try {
+ const { data } = await axios.get(metricsUrl, {
+ withCredentials: true,
+ });
+ if (!Array.isArray(data.metrics)) {
+ throw new Error('metrics are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+ return data;
+ } catch (e) {
+ return reportErrorAndThrow(e);
+ }
}
export function buildClient(options) {
diff --git a/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
index 7b37186ba1a..a0b2a639401 100644
--- a/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
+++ b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
@@ -3,7 +3,10 @@ query getOrganizationUsers($id: OrganizationsOrganizationID!) {
id
organizationUsers {
nodes {
- badges
+ badges {
+ text
+ variant
+ }
id
user {
id
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index c03c00c06aa..bba8e1f7ba5 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -23,6 +23,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-geo-migrate-hashed-storage-callout',
'.js-unlimited-members-during-trial-alert',
'.js-branch-rules-info-callout',
+ '.js-new-nav-for-everyone-callout',
'.js-namespace-over-storage-users-combined-alert',
];
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index aa30192b74b..2fc1f99c183 100644
--- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
+++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
@@ -5,9 +5,9 @@ import { INTEGRATION_VIEW_CONFIGS, i18n } from '../constants';
import IntegrationView from './integration_view.vue';
function updateClasses(bodyClasses = '', applicationTheme, layout) {
- // Remove body class for any previous theme, re-add current one
- document.body.classList.remove(...bodyClasses.split(' '));
- document.body.classList.add(applicationTheme);
+ // Remove documentElement class for any previous theme, re-add current one
+ document.documentElement.classList.remove(...bodyClasses.split(' '));
+ document.documentElement.classList.add(applicationTheme);
// Toggle container-fluid class
if (layout === 'fluid') {
diff --git a/app/assets/javascripts/projects/settings/init_access_dropdown.js b/app/assets/javascripts/projects/settings/init_access_dropdown.js
index 102b1846453..b02a33675ee 100644
--- a/app/assets/javascripts/projects/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/init_access_dropdown.js
@@ -22,6 +22,7 @@ export const initAccessDropdown = (el, options) => {
data() {
return { preselected };
},
+ disabled,
methods: {
setPreselectedItems(items) {
this.preselected = items;
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 29034b3bc0e..66da3de516a 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -6,6 +6,10 @@ import { initToggle } from '~/toggles';
import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
+const isDropdownDisabled = (dropdown) => {
+ return dropdown?.$options.disabled === '';
+};
+
export default class ProtectedBranchEdit {
constructor(options) {
this.hasLicense = options.hasLicense;
@@ -104,6 +108,9 @@ export default class ProtectedBranchEdit {
}
initSelectedItems(dropdown, accessLevel) {
+ if (isDropdownDisabled(dropdown)) {
+ return;
+ }
this.selectedItems[accessLevel] = dropdown.preselected.map((item) => {
if (item.type === LEVEL_TYPES.USER) return { id: item.id, user_id: item.user_id };
if (item.type === LEVEL_TYPES.ROLE) return { id: item.id, access_level: item.access_level };
@@ -183,7 +190,10 @@ export default class ProtectedBranchEdit {
};
});
- this.selectedItems[accessLevel] = itemsToAdd;
- this[`${accessLevel}_dropdown`]?.setPreselectedItems(itemsToAdd);
+ const dropdown = this[`${accessLevel}_dropdown`];
+ if (!isDropdownDisabled(dropdown)) {
+ this.selectedItems[accessLevel] = itemsToAdd;
+ dropdown?.setPreselectedItems(itemsToAdd);
+ }
}
}
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index 891e883b6c0..5712b716f48 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -8,7 +8,6 @@ import {
} from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, __, sprintf } from '~/locale';
-import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import Tracking from '~/tracking';
import PersistentUserCallout from '~/persistent_user_callout';
import { USER_MENU_TRACKING_DEFAULTS, DROPDOWN_Y_OFFSET, IMPERSONATING_OFFSET } from '../constants';
@@ -39,14 +38,13 @@ export default {
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
GlButton,
- NewNavToggle,
UserMenuProfileItem,
},
directives: {
SafeHtml,
},
mixins: [Tracking.mixin()],
- inject: ['toggleNewNavEndpoint', 'isImpersonating'],
+ inject: ['isImpersonating'],
props: {
data: {
required: true,
@@ -301,13 +299,6 @@ export default {
/>
</gl-disclosure-dropdown-group>
- <gl-disclosure-dropdown-group bordered>
- <template #group-label>
- <span class="gl-font-sm">{{ $options.i18n.newNavigation.sectionTitle }}</span>
- </template>
- <new-nav-toggle :endpoint="toggleNewNavEndpoint" enabled new-navigation />
- </gl-disclosure-dropdown-group>
-
<gl-disclosure-dropdown-group
v-if="data.can_sign_out"
bordered
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index d0d98ef3808..9e540175b48 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -66,13 +66,7 @@ export const initSuperSidebar = () => {
if (!el) return false;
- const {
- rootPath,
- sidebar,
- toggleNewNavEndpoint,
- forceDesktopExpandedSidebar,
- commandPalette,
- } = el.dataset;
+ const { rootPath, sidebar, forceDesktopExpandedSidebar, commandPalette } = el.dataset;
bindSuperSidebarCollapsedEvents(forceDesktopExpandedSidebar);
initSuperSidebarCollapsedState(parseBoolean(forceDesktopExpandedSidebar));
@@ -98,7 +92,6 @@ export const initSuperSidebar = () => {
name: 'SuperSidebarRoot',
provide: {
rootPath,
- toggleNewNavEndpoint,
isImpersonating,
...getTrialStatusWidgetData(sidebarData),
commandPaletteCommands,
diff --git a/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue b/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
index f62bfb551df..70daac311c7 100644
--- a/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlSearchBoxByType,
- GlSprintf,
-} from '@gitlab/ui';
+import { GlDisclosureDropdown, GlIcon, GlSearchBoxByType, GlSprintf } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { __, n__, s__, sprintf } from '~/locale';
@@ -16,12 +10,16 @@ export const i18n = {
searchFiles: __('Search files'),
};
+const variantCssColorMap = {
+ success: 'gl-text-green-500',
+ danger: 'gl-text-red-500',
+};
+
export default {
i18n,
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
+ GlDisclosureDropdown,
+ GlIcon,
GlSearchBoxByType,
GlSprintf,
},
@@ -54,6 +52,15 @@ export default {
? fuzzaldrinPlus.filter(this.files, this.search, { key: 'name' })
: this.files;
},
+ dropdownItems() {
+ return this.filteredFiles.map((file) => {
+ return {
+ ...file,
+ text: file.name || this.$options.i18n.noFileNameAvailable,
+ iconColor: variantCssColorMap[file.iconColor],
+ };
+ });
+ },
messageChanged() {
return sprintf(
n__(
@@ -64,21 +71,21 @@ export default {
{ count: this.changed },
);
},
-
- additionsText() {
- return n__('Diffs|%d addition', 'Diffs|%d additions', this.added);
- },
- deletionsText() {
- return n__('Diffs|%d deletion', 'Diffs|%d deletions', this.deleted);
- },
},
methods: {
- jumpToFile(fileHash) {
- window.location.hash = fileHash;
- },
focusInput() {
this.$refs.search.focusInput();
},
+ focusFirstItem() {
+ if (!this.filteredFiles.length) return;
+ this.$el.querySelector('.gl-new-dropdown-item:first-child').focus();
+ },
+ additionsText(numberOfChanges = this.added) {
+ return n__('Diffs|%d addition', 'Diffs|%d additions', numberOfChanges);
+ },
+ deletionsText(numberOfChanges = this.deleted) {
+ return n__('Diffs|%d deletion', 'Diffs|%d deletions', numberOfChanges);
+ },
},
};
</script>
@@ -87,15 +94,15 @@ export default {
<div>
<gl-sprintf :message="messageChanged">
<template #dropdown="{ content: dropdownText }">
- <gl-dropdown
+ <gl-disclosure-dropdown
+ :toggle-text="dropdownText"
+ :items="dropdownItems"
category="tertiary"
variant="confirm"
- :text="dropdownText"
data-testid="diff-stats-dropdown"
class="gl-vertical-align-baseline"
toggle-class="gl-px-0! gl-font-weight-bold!"
- menu-class="gl-w-auto!"
- no-flip
+ fluid-width
@shown="focusInput"
>
<template #header>
@@ -103,35 +110,38 @@ export default {
ref="search"
v-model.trim="search"
:placeholder="$options.i18n.searchFiles"
+ class="gl-mx-3 gl-my-4"
+ @keydown.down="focusFirstItem"
/>
+ <span v-if="!filteredFiles.length" class="gl-mx-3">
+ {{ $options.i18n.noFilesFound }}
+ </span>
</template>
- <gl-dropdown-item
- v-for="file in filteredFiles"
- :key="file.href"
- :icon-name="file.icon"
- :icon-color="file.iconColor"
- @click="jumpToFile(file.href)"
- >
- <div class="gl-display-flex">
- <span v-if="file.name" class="gl-font-weight-bold gl-mr-3 gl-text-truncate">{{
- file.name
- }}</span>
- <span v-else class="gl-mr-3 gl-font-weight-bold gl-font-style-italic gl-gray-400">{{
- $options.i18n.noFileNameAvailable
- }}</span>
- <span class="gl-ml-auto gl-white-space-nowrap">
- <span class="gl-text-green-600">+{{ file.added }}</span>
- <span class="gl-text-red-500">-{{ file.removed }}</span>
- </span>
+ <template #list-item="{ item }">
+ <div class="gl-display-flex gl-gap-3 gl-align-items-center">
+ <gl-icon :name="item.icon" :class="item.iconColor" />
+ <div class="gl-flex-grow-1">
+ <div class="gl-display-flex">
+ <span
+ class="gl-font-weight-bold gl-mr-3 gl-flex-grow-1"
+ :class="item.name ? 'gl-text-truncate' : 'gl-font-style-italic gl-gray-400'"
+ >{{ item.text }}</span
+ >
+ <span class="gl-ml-auto gl-white-space-nowrap" aria-hidden="true">
+ <span class="gl-text-green-600">+{{ item.added }}</span>
+ <span class="gl-text-red-500">-{{ item.removed }}</span>
+ </span>
+ <span class="gl-sr-only"
+ >{{ additionsText(item.added) }}, {{ deletionsText(item.removed) }}</span
+ >
+ </div>
+ <div class="gl-text-gray-700 gl-overflow-hidden gl-text-overflow-ellipsis">
+ {{ item.path }}
+ </div>
+ </div>
</div>
- <div class="gl-text-gray-700 gl-overflow-hidden gl-text-overflow-ellipsis">
- {{ file.path }}
- </div>
- </gl-dropdown-item>
- <gl-dropdown-text v-if="!filteredFiles.length">
- {{ $options.i18n.noFilesFound }}
- </gl-dropdown-text>
- </gl-dropdown>
+ </template>
+ </gl-disclosure-dropdown>
</template>
</gl-sprintf>
<span
@@ -140,12 +150,20 @@ export default {
>
<gl-sprintf :message="$options.i18n.messageAdditionsDeletions">
<template #additions>
- <span class="gl-text-green-600 gl-font-weight-bold">{{ additionsText }}</span>
+ <span class="gl-text-green-600 gl-font-weight-bold">{{ additionsText() }}</span>
</template>
<template #deletions>
- <span class="gl-text-red-500 gl-font-weight-bold">{{ deletionsText }}</span>
+ <span class="gl-text-red-500 gl-font-weight-bold">{{ deletionsText() }}</span>
</template>
</gl-sprintf>
</span>
</div>
</template>
+
+<style scoped>
+/* TODO: Use max-height prop when gitlab-ui got updated.
+See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2374 */
+::v-deep .gl-new-dropdown-inner {
+ max-height: 310px;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/constants.js b/app/assets/javascripts/vue_shared/components/list_selector/constants.js
index 2e58527a2ea..cff9c56a1c0 100644
--- a/app/assets/javascripts/vue_shared/components/list_selector/constants.js
+++ b/app/assets/javascripts/vue_shared/components/list_selector/constants.js
@@ -1,6 +1,6 @@
import { __ } from '~/locale';
export const CONFIG = {
- users: { title: __('Users'), icon: 'user', filterKey: 'username' },
+ users: { title: __('Users'), icon: 'user', filterKey: 'username', showNamespaceDropdown: true },
groups: { title: __('Groups'), icon: 'group', filterKey: 'name' },
};
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/index.vue b/app/assets/javascripts/vue_shared/components/list_selector/index.vue
index 6813d9ca077..b8480a0c496 100644
--- a/app/assets/javascripts/vue_shared/components/list_selector/index.vue
+++ b/app/assets/javascripts/vue_shared/components/list_selector/index.vue
@@ -1,13 +1,23 @@
<script>
import { GlCard, GlIcon, GlCollapsibleListbox, GlSearchBoxByType } from '@gitlab/ui';
-import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { createAlert } from '~/alert';
+import { __ } from '~/locale';
import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
+import Api from '~/api';
import UserItem from './user_item.vue';
import GroupItem from './group_item.vue';
import { CONFIG } from './constants';
+const I18N = {
+ allGroups: __('All groups'),
+ projectGroups: __('Project groups'),
+ apiErrorMessage: __('An error occurred while fetching. Please try again.'),
+};
+
export default {
name: 'ListSelector',
+ i18n: I18N,
components: {
GlCard,
GlIcon,
@@ -33,11 +43,16 @@ export default {
required: false,
default: null,
},
+ groupPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
searchValue: '',
- isProject: true, // TODO: implement a way to distinguish between project/group
+ isProjectNamespace: 'true',
selected: [],
items: [],
};
@@ -46,39 +61,44 @@ export default {
config() {
return CONFIG[this.type];
},
- searchItems() {
- return this.items;
- },
isUserVariant() {
return this.type === 'users';
},
component() {
return this.isUserVariant ? UserItem : GroupItem;
},
+ namespaceDropdownText() {
+ return parseBoolean(this.isProjectNamespace)
+ ? this.$options.i18n.projectGroups
+ : this.$options.i18n.allGroups;
+ },
},
methods: {
async handleSearchInput(search) {
this.$refs.results.open();
- if (this.isUserVariant) {
- this.items = await this.fetchUsersBySearchTerm(search);
- } else {
- this.items = await this.fetchGroupsBySearchTerm(search);
+
+ try {
+ if (this.isUserVariant) {
+ this.items = await this.fetchUsersBySearchTerm(search);
+ } else {
+ this.items = await this.fetchGroupsBySearchTerm(search);
+ }
+ } catch (e) {
+ createAlert({
+ message: this.$options.i18n.apiErrorMessage,
+ });
}
},
- fetchUsersBySearchTerm(search) {
- const namespace = this.isProject ? 'project' : 'group';
- return this.$apollo
- .query({
- query: usersAutocompleteQuery,
- variables: { fullPath: this.projectPath, search, isProject: this.isProject },
- })
- .then(({ data }) =>
- data[namespace]?.autocompleteUsers.map((user) => ({
- text: user.name,
- value: user.username,
- ...user,
- })),
- );
+ async fetchUsersBySearchTerm(search) {
+ let users = [];
+ if (parseBoolean(this.isProjectNamespace)) {
+ users = await Api.projectUsers(this.projectPath, search);
+ } else {
+ const groupMembers = await Api.groupMembers(this.groupPath, { query: search });
+ users = groupMembers?.data || [];
+ }
+
+ return users?.map((user) => ({ text: user.name, value: user.username, ...user }));
},
fetchGroupsBySearchTerm(search) {
return this.$apollo
@@ -95,7 +115,7 @@ export default {
);
},
getItemByKey(key) {
- return this.searchItems.find((item) => item[this.config.filterKey] === key);
+ return this.items.find((item) => item[this.config.filterKey] === key);
},
handleSelectItem(key) {
this.$emit('select', this.getItemByKey(key));
@@ -103,7 +123,15 @@ export default {
handleDeleteItem(key) {
this.$emit('delete', key);
},
+ handleSelectNamespace() {
+ this.items = [];
+ this.searchValue = '';
+ },
},
+ namespaceOptions: [
+ { text: I18N.projectGroups, value: 'true' },
+ { text: I18N.allGroups, value: 'false' },
+ ],
};
</script>
@@ -118,28 +146,38 @@ export default {
></template
>
- <gl-collapsible-listbox
- ref="results"
- v-model="selected"
- class="list-selector gl-mb-4 gl-display-block"
- :items="searchItems"
- multiple
- @shown="$refs.search.focusInput()"
- >
- <template #toggle>
- <gl-search-box-by-type
- ref="search"
- v-model="searchValue"
- autofocus
- debounce="500"
- @input="handleSearchInput"
- />
- </template>
+ <div class="gl-display-flex gl-gap-3" :class="{ 'gl-mb-4': selectedItems.length }">
+ <gl-collapsible-listbox
+ ref="results"
+ v-model="selected"
+ class="list-selector gl-display-block gl-flex-grow-1"
+ :items="items"
+ multiple
+ @shown="$refs.search.focusInput()"
+ >
+ <template #toggle>
+ <gl-search-box-by-type
+ ref="search"
+ v-model="searchValue"
+ autofocus
+ debounce="500"
+ @input="handleSearchInput"
+ />
+ </template>
+
+ <template #list-item="{ item }">
+ <component :is="component" :data="item" @select="handleSelectItem" />
+ </template>
+ </gl-collapsible-listbox>
- <template #list-item="{ item }">
- <component :is="component" :data="item" @select="handleSelectItem" />
- </template>
- </gl-collapsible-listbox>
+ <gl-collapsible-listbox
+ v-if="config.showNamespaceDropdown"
+ v-model="isProjectNamespace"
+ :toggle-text="namespaceDropdownText"
+ :items="$options.namespaceOptions"
+ @select="handleSelectNamespace"
+ />
+ </div>
<component
:is="component"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
index 45fde45f516..dae3ddfe016 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
@@ -74,6 +74,11 @@ export default {
required: false,
default: 0,
},
+ workspaceType: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
isUpdated() {
@@ -161,6 +166,7 @@ export default {
:issuable="issuable"
:status-icon="statusIcon"
:enable-edit="enableEdit"
+ :workspace-type="workspaceType"
@edit-issuable="$emit('edit-issuable', $event)"
>
<template #status-badge>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
index 3878c16c8d0..040f49c7c25 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
@@ -147,6 +147,7 @@ export default {
:description-help-path="descriptionHelpPath"
:task-list-update-path="taskListUpdatePath"
:task-list-lock-version="taskListLockVersion"
+ :workspace-type="workspaceType"
@edit-issuable="$emit('edit-issuable', $event)"
@task-list-update-success="$emit('task-list-update-success', $event)"
@task-list-update-failure="$emit('task-list-update-failure')"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
index 3aef7d141e0..5387e39e3eb 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
@@ -1,5 +1,6 @@
<script>
import { GlIcon, GlBadge, GlButton, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { STATUS_OPEN } from '~/issues/constants';
import { __ } from '~/locale';
@@ -13,6 +14,7 @@ export default {
GlBadge,
GlButton,
GlIntersectionObserver,
+ ConfidentialityBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -31,6 +33,11 @@ export default {
type: Boolean,
required: true,
},
+ workspaceType: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -89,6 +96,12 @@ export default {
<slot name="status-badge"></slot>
</span>
</gl-badge>
+ <confidentiality-badge
+ v-if="issuable.confidential"
+ class="gl-white-space-nowrap gl-mr-3 gl-align-self-center"
+ :issuable-type="issuable.type"
+ :workspace-type="workspaceType"
+ />
<p
class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0"
:title="issuable.title"
diff --git a/app/assets/javascripts/work_items/components/work_item_type_icon.vue b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
index 76a73093206..5426f3965b3 100644
--- a/app/assets/javascripts/work_items/components/work_item_type_icon.vue
+++ b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
@@ -36,11 +36,6 @@ export default {
return this.workItemType.toUpperCase().split(' ').join('_');
},
iconName() {
- // TODO Delete this conditional once we have an `issue-type-epic` icon
- if (this.workItemIconName === 'issue-type-epic') {
- return 'epic';
- }
-
return (
this.workItemIconName ||
WORK_ITEMS_TYPE_MAP[this.workItemTypeUppercase]?.icon ||
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 2f2401bd9b3..e2dbfeb55a5 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -35,6 +35,7 @@ export const WORK_ITEM_TYPE_ENUM_TEST_CASE = 'TEST_CASE';
export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
export const WORK_ITEM_TYPE_ENUM_OBJECTIVE = 'OBJECTIVE';
export const WORK_ITEM_TYPE_ENUM_KEY_RESULT = 'KEY_RESULT';
+export const WORK_ITEM_TYPE_ENUM_EPIC = 'EPIC';
export const WORK_ITEM_TYPE_VALUE_EPIC = 'Epic';
export const WORK_ITEM_TYPE_VALUE_INCIDENT = 'Incident';
@@ -185,6 +186,11 @@ export const WORK_ITEMS_TYPE_MAP = {
name: s__('WorkItem|Key result'),
value: WORK_ITEM_TYPE_VALUE_KEY_RESULT,
},
+ [WORK_ITEM_TYPE_ENUM_EPIC]: {
+ icon: `epic`,
+ name: s__('WorkItem|Epic'),
+ value: WORK_ITEM_TYPE_VALUE_EPIC,
+ },
};
export const WORK_ITEMS_TREE_TEXT_MAP = {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 613e504c771..eb627b036fe 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -247,6 +247,7 @@ span.idiff {
border-bottom: 1px solid $border-color;
padding: $gl-padding-8 $gl-padding;
margin: 0;
+ min-height: px-to-rem(42px);
border-radius: $border-radius-default $border-radius-default 0 0;
@include media-breakpoint-up(md) {
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index dcd8f90ab1c..aaec277cf08 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -169,6 +169,10 @@
}
}
+.stage-column-title .gl-ci-action-icon-container {
+ right: 11px;
+}
+
.split-report-section {
border-bottom: 1px solid var(--gray-50, $gray-50);
@@ -269,7 +273,12 @@
.stage-column,
.stage-column.is-stage-view {
+ min-width: 1px;
+
@media (min-width: $breakpoint-sm) {
+ min-width: inherit;
+ max-width: $gl-spacing-scale-48;
+
&:first-of-type {
margin-left: $gl-spacing-scale-6;
}
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index a4a2cd28d05..c0eced48171 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -105,7 +105,7 @@
--svg-status-bg: #{$white};
}
-body.gl-dark {
+:root.gl-dark {
// redefine some colors and values to prevent sourcegraph conflicts
color-scheme: dark;
--gray-10: #{$gray-10};
@@ -239,7 +239,7 @@ aside.right-sidebar:not(.right-sidebar-merge-requests) {
border-left-color: $gray-50;
}
-body.gl-dark {
+:root.gl-dark {
@include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-900, $white);
.terms {
diff --git a/app/assets/stylesheets/themes/theme_blue.scss b/app/assets/stylesheets/themes/theme_blue.scss
index 06f3e13e99e..749120a0ecb 100644
--- a/app/assets/stylesheets/themes/theme_blue.scss
+++ b/app/assets/stylesheets/themes/theme_blue.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-blue {
@include gitlab-theme(
$theme-blue-200,
diff --git a/app/assets/stylesheets/themes/theme_gray.scss b/app/assets/stylesheets/themes/theme_gray.scss
index 3112aaef227..70611e692cd 100644
--- a/app/assets/stylesheets/themes/theme_gray.scss
+++ b/app/assets/stylesheets/themes/theme_gray.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-gray {
@include gitlab-theme(
$gray-200,
diff --git a/app/assets/stylesheets/themes/theme_green.scss b/app/assets/stylesheets/themes/theme_green.scss
index c9ea1162206..ae969873692 100644
--- a/app/assets/stylesheets/themes/theme_green.scss
+++ b/app/assets/stylesheets/themes/theme_green.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-green {
@include gitlab-theme(
$theme-green-200,
diff --git a/app/assets/stylesheets/themes/theme_indigo.scss b/app/assets/stylesheets/themes/theme_indigo.scss
index 78ce96667d4..d7e8ddadf46 100644
--- a/app/assets/stylesheets/themes/theme_indigo.scss
+++ b/app/assets/stylesheets/themes/theme_indigo.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-indigo {
@include gitlab-theme(
$indigo-200,
diff --git a/app/assets/stylesheets/themes/theme_light_blue.scss b/app/assets/stylesheets/themes/theme_light_blue.scss
index 73fe072393f..430960f563f 100644
--- a/app/assets/stylesheets/themes/theme_light_blue.scss
+++ b/app/assets/stylesheets/themes/theme_light_blue.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-blue {
@include gitlab-theme(
$theme-light-blue-200,
diff --git a/app/assets/stylesheets/themes/theme_light_gray.scss b/app/assets/stylesheets/themes/theme_light_gray.scss
index e8357647f48..f63da3f22f1 100644
--- a/app/assets/stylesheets/themes/theme_light_gray.scss
+++ b/app/assets/stylesheets/themes/theme_light_gray.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-gray {
@include gitlab-theme(
$gray-500,
diff --git a/app/assets/stylesheets/themes/theme_light_green.scss b/app/assets/stylesheets/themes/theme_light_green.scss
index 6b058b2dd7b..05adc56c36a 100644
--- a/app/assets/stylesheets/themes/theme_light_green.scss
+++ b/app/assets/stylesheets/themes/theme_light_green.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-green {
@include gitlab-theme(
$theme-green-200,
diff --git a/app/assets/stylesheets/themes/theme_light_indigo.scss b/app/assets/stylesheets/themes/theme_light_indigo.scss
index ff12366466a..04bcfaf8366 100644
--- a/app/assets/stylesheets/themes/theme_light_indigo.scss
+++ b/app/assets/stylesheets/themes/theme_light_indigo.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-indigo {
@include gitlab-theme(
$indigo-200,
diff --git a/app/assets/stylesheets/themes/theme_light_red.scss b/app/assets/stylesheets/themes/theme_light_red.scss
index 3ae67309014..c4952b8e155 100644
--- a/app/assets/stylesheets/themes/theme_light_red.scss
+++ b/app/assets/stylesheets/themes/theme_light_red.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-red {
@include gitlab-theme(
$theme-light-red-200,
diff --git a/app/assets/stylesheets/themes/theme_red.scss b/app/assets/stylesheets/themes/theme_red.scss
index 82de30e8b0e..536963e12ef 100644
--- a/app/assets/stylesheets/themes/theme_red.scss
+++ b/app/assets/stylesheets/themes/theme_red.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-red {
@include gitlab-theme(
$theme-red-200,
diff --git a/app/controllers/groups/settings/applications_controller.rb b/app/controllers/groups/settings/applications_controller.rb
index 3ae1ae824a0..5aea078db17 100644
--- a/app/controllers/groups/settings/applications_controller.rb
+++ b/app/controllers/groups/settings/applications_controller.rb
@@ -5,7 +5,7 @@ module Groups
class ApplicationsController < Groups::ApplicationController
include OauthApplications
- prepend_before_action :authorize_admin_group!
+ before_action :authorize_admin_group!
before_action :set_application, only: [:show, :edit, :update, :renew, :destroy]
before_action :load_scopes, only: [:index, :create, :edit, :update]
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 2828d17c36f..85bdeb07b00 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -62,7 +62,11 @@ class Projects::ArtifactsController < Projects::ApplicationController
conditionally_expand_blob(blob)
if blob.external_link?(build)
- redirect_to external_file_project_job_artifacts_path(@project, @build, path: params[:path])
+ if Gitlab::CurrentSettings.enable_artifact_external_redirect_warning_page
+ redirect_to external_file_project_job_artifacts_path(@project, @build, path: params[:path])
+ else
+ redirect_to blob.external_url(build)
+ end
else
respond_to do |format|
format.html do
diff --git a/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb b/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb
index ecb105a64d0..1982b458143 100644
--- a/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb
+++ b/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb
@@ -17,7 +17,12 @@ module WorkItems
argument :state,
Types::IssuableStateEnum,
required: false,
- description: 'Current state of the work item.'
+ description: 'Current state of the work item.',
+ prepare: ->(state, _ctx) {
+ return state unless state == 'locked'
+
+ raise Gitlab::Graphql::Errors::ArgumentError, Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE
+ }
argument :types,
[Types::IssueTypeEnum],
as: :issue_types,
diff --git a/app/graphql/resolvers/issues/base_parent_resolver.rb b/app/graphql/resolvers/issues/base_parent_resolver.rb
index 6308e56f049..78ef4132baf 100644
--- a/app/graphql/resolvers/issues/base_parent_resolver.rb
+++ b/app/graphql/resolvers/issues/base_parent_resolver.rb
@@ -7,8 +7,13 @@ module Resolvers
include ::Issues::SortArguments
argument :state, Types::IssuableStateEnum,
- required: false,
- description: 'Current state of this issue.'
+ required: false,
+ description: 'Current state of this issue.',
+ prepare: ->(state, _ctx) {
+ return state unless state == 'locked'
+
+ raise Gitlab::Graphql::Errors::ArgumentError, Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE
+ }
# see app/graphql/types/issue_connection.rb
type 'Types::IssueConnection', null: true
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 34f14eee0e5..bc0e7334303 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -14,7 +14,12 @@ module Resolvers
description: 'Whether to include issues from archived projects. Defaults to `false`.'
argument :state, Types::IssuableStateEnum,
required: false,
- description: 'Current state of this issue.'
+ description: 'Current state of this issue.',
+ prepare: ->(state, _ctx) {
+ return state unless state == 'locked'
+
+ raise Gitlab::Graphql::Errors::ArgumentError, Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE
+ }
# see app/graphql/types/issue_connection.rb
type 'Types::IssueConnection', null: true
diff --git a/app/graphql/types/issuable_state_enum.rb b/app/graphql/types/issuable_state_enum.rb
index 5a1b11b3bdc..8e3ed1d4bc8 100644
--- a/app/graphql/types/issuable_state_enum.rb
+++ b/app/graphql/types/issuable_state_enum.rb
@@ -1,10 +1,15 @@
# frozen_string_literal: true
+# DO NOT use this ENUM with issues. We need to define a new enum in places where we
+# need to filter by state. locked is not a valid state filter for issues. More info in
+# https://gitlab.com/gitlab-org/gitlab/-/issues/420667#note_1605900474
module Types
class IssuableStateEnum < BaseEnum
graphql_name 'IssuableState'
description 'State of a GitLab issue or merge request'
+ INVALID_LOCKED_MESSAGE = 'locked is not a valid state filter for issues.'
+
value 'opened', description: 'In open state.'
value 'closed', description: 'In closed state.'
value 'locked', description: 'Discussion has been locked.'
diff --git a/app/graphql/types/organizations/organization_user_badge_type.rb b/app/graphql/types/organizations/organization_user_badge_type.rb
new file mode 100644
index 00000000000..f4e18676dd1
--- /dev/null
+++ b/app/graphql/types/organizations/organization_user_badge_type.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Types
+ module Organizations
+ # rubocop: disable Graphql/AuthorizeTypes -- Already authorized in parent OrganizationUserType.
+ class OrganizationUserBadgeType < BaseObject
+ graphql_name 'OrganizationUserBadge'
+ description 'An organization user badge.'
+
+ field :text,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Badge text.'
+
+ field :variant,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Badge variant.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/organizations/organization_user_type.rb b/app/graphql/types/organizations/organization_user_type.rb
index 41924586f38..ce036c7dd4a 100644
--- a/app/graphql/types/organizations/organization_user_type.rb
+++ b/app/graphql/types/organizations/organization_user_type.rb
@@ -13,7 +13,7 @@ module Types
alias_method :organization_user, :object
field :badges,
- [GraphQL::Types::String],
+ [::Types::Organizations::OrganizationUserBadgeType],
null: true,
description: 'Badges describing the user within the organization.',
alpha: { milestone: '16.4' }
@@ -29,7 +29,7 @@ module Types
alpha: { milestone: '16.4' }
def badges
- user_badges_in_admin_section(organization_user.user).pluck(:text) # rubocop:disable CodeReuse/ActiveRecord
+ user_badges_in_admin_section(organization_user.user)
end
end
end
diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb
index 47d486265b0..ca1b6eaa900 100644
--- a/app/graphql/types/user_interface.rb
+++ b/app/graphql/types/user_interface.rb
@@ -197,6 +197,11 @@ module Types
null: true,
description: 'Timestamp of when the user was created.'
+ field :last_activity_on,
+ type: Types::DateType,
+ null: true,
+ description: 'Date the user last performed any actions.'
+
field :pronouns,
type: ::GraphQL::Types::String,
null: true,
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 58648a82487..0c6ab41004a 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -488,6 +488,7 @@ module ApplicationSettingsHelper
:sidekiq_job_limiter_compression_threshold_bytes,
:sidekiq_job_limiter_limit_bytes,
:suggest_pipeline_enabled,
+ :enable_artifact_external_redirect_warning_page,
:search_rate_limit,
:search_rate_limit_unauthenticated,
:search_rate_limit_allowlist_raw,
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index f1e05b43cd3..0c61749701e 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -78,15 +78,11 @@ module NavHelper
%w[system_info background_migrations background_jobs health_check]
end
- def show_super_sidebar?(user = current_user)
- # The new sidebar is not enabled for anonymous use
- return true unless user
-
- # Users who get the new nav unless they explicitly
- # opt-out via the toggle
- return true if user.use_new_navigation.nil?
-
- !!user.use_new_navigation
+ def show_super_sidebar?(_user = current_user)
+ # The new navigation is now enabled for everyone.
+ # We are working on cleaning up the use of this helper and other related code.
+ # See https://gitlab.com/groups/gitlab-org/-/epics/11875
+ true
end
private
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 94445564c22..a1afb0493d5 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -263,21 +263,6 @@ module SortingHelper
sort_direction_button(url, reverse_sort, sort_value)
end
- def packages_sort_options_hash
- {
- sort_value_recently_created => sort_title_created_date,
- sort_value_oldest_created => sort_title_created_date,
- sort_value_name => sort_title_name,
- sort_value_name_desc => sort_title_name,
- sort_value_version_desc => sort_title_version,
- sort_value_version_asc => sort_title_version,
- sort_value_type_desc => sort_title_type,
- sort_value_type_asc => sort_title_type,
- sort_value_project_name_desc => sort_title_project_name,
- sort_value_project_name_asc => sort_title_project_name
- }
- end
-
def packages_reverse_sort_order_hash
{
sort_value_recently_created => sort_value_oldest_created,
diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb
index 8c26dd8ba9e..6f1d4db4349 100644
--- a/app/helpers/users/callouts_helper.rb
+++ b/app/helpers/users/callouts_helper.rb
@@ -15,6 +15,7 @@ module Users
REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze
WEB_HOOK_DISABLED = 'web_hook_disabled'
BRANCH_RULES_INFO_CALLOUT = 'branch_rules_info_callout'
+ NEW_NAV_FOR_EVERYONE_CALLOUT = 'new_nav_for_everyone_callout'
def show_gke_cluster_integration_callout?(project)
active_nav_link?(controller: sidebar_operations_paths) &&
@@ -74,6 +75,14 @@ module Users
!user_dismissed?(BRANCH_RULES_INFO_CALLOUT)
end
+ def show_new_nav_for_everyone_callout?
+ # The use_new_navigation user preference was controlled by the now removed "New navigation" toggle in the UI.
+ # We want to show this banner only to signed-in users who chose to disable the new nav (`false`).
+ # We don't want to show it for users who never touched the toggle and already had the new nav by default (`nil`)
+ user_had_new_nav_off = current_user && current_user.use_new_navigation == false
+ user_had_new_nav_off && !user_dismissed?(NEW_NAV_FOR_EVERYONE_CALLOUT)
+ end
+
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil, object: nil)
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 1bd15a56de5..a5ed402aa9a 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -57,6 +57,7 @@ module ApplicationSettingImplementation
default_artifacts_expire_in: '30 days',
default_branch_name: nil,
default_branch_protection: Settings.gitlab['default_branch_protection'],
+ default_branch_protection_defaults: Settings.gitlab['default_branch_protection_defaults'],
default_ci_config_path: nil,
default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_project_creation: Settings.gitlab['default_project_creation'],
diff --git a/app/models/ci/catalog/components_project.rb b/app/models/ci/catalog/components_project.rb
index 783a2d358c5..02593d41bc2 100644
--- a/app/models/ci/catalog/components_project.rb
+++ b/app/models/ci/catalog/components_project.rb
@@ -9,7 +9,7 @@ module Ci
TEMPLATE_FILE = 'template.yml'
TEMPLATES_DIR = 'templates'
- TEMPLATE_PATH_REGEX = '^templates\/\w+\-?\w+(?:\/template)?\.yml$'
+ TEMPLATE_PATH_REGEX = '^templates\/[\w-]+(?:\/template)?\.yml$'
COMPONENTS_LIMIT = 10
ComponentData = Struct.new(:content, :path, keyword_init: true)
diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb
index e3076e85d10..0da69e6ece0 100644
--- a/app/models/ci/catalog/resource.rb
+++ b/app/models/ci/catalog/resource.rb
@@ -13,7 +13,8 @@ module Ci
self.table_name = 'catalog_resources'
belongs_to :project
- has_many :components, class_name: 'Ci::Catalog::Resources::Component', inverse_of: :catalog_resource
+ has_many :components, class_name: 'Ci::Catalog::Resources::Component', foreign_key: :catalog_resource_id,
+ inverse_of: :catalog_resource
has_many :versions, class_name: 'Ci::Catalog::Resources::Version', inverse_of: :catalog_resource
scope :for_projects, ->(project_ids) { where(project_id: project_ids) }
@@ -44,6 +45,10 @@ module Ci
update!(state: :draft)
end
+ def publish!
+ update!(state: :published)
+ end
+
def sync_with_project!
sync_with_project
save!
diff --git a/app/models/ci/catalog/resources/component.rb b/app/models/ci/catalog/resources/component.rb
index 7b95c14ba7e..07d5404981b 100644
--- a/app/models/ci/catalog/resources/component.rb
+++ b/app/models/ci/catalog/resources/component.rb
@@ -6,6 +6,8 @@ module Ci
# This class represents a CI/CD Catalog resource component.
# The data will be used as metadata of a component.
class Component < ::ApplicationRecord
+ include BulkInsertSafe
+
self.table_name = 'catalog_resource_components'
belongs_to :project, inverse_of: :ci_components
diff --git a/app/models/ci/catalog/resources/version.rb b/app/models/ci/catalog/resources/version.rb
index 68f60e6a965..fae6d9846f9 100644
--- a/app/models/ci/catalog/resources/version.rb
+++ b/app/models/ci/catalog/resources/version.rb
@@ -6,6 +6,8 @@ module Ci
# This class represents a CI/CD Catalog resource version.
# Only versions which contain valid CI components are included in this table.
class Version < ::ApplicationRecord
+ include BulkInsertableAssociations
+
self.table_name = 'catalog_resource_versions'
belongs_to :release, inverse_of: :catalog_resource_version
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index ae970ca9e6b..f0093445ba8 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -15,7 +15,7 @@ class Deployment < ApplicationRecord
ARCHIVABLE_OFFSET = 50_000
- ignore_column :cluster_id, remove_with: '16.8', remove_after: '2023-12-22'
+ ignore_column :cluster_id, remove_with: '16.8', remove_after: '2023-12-21'
belongs_to :project, optional: false
belongs_to :environment, optional: false
diff --git a/app/models/group.rb b/app/models/group.rb
index 492f4195931..51c26767569 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -594,40 +594,13 @@ class Group < Namespace
end
def authorizable_members_with_parents
- source_ids =
- if has_parent?
- self_and_ancestors.reorder(nil).select(:id)
- else
- id
- end
-
- group_hierarchy_members = GroupMember.where(source_id: source_ids).select(*GroupMember.cached_column_list)
-
- GroupMember.from_union([group_hierarchy_members,
- members_from_self_and_ancestor_group_shares]).authorizable
+ Members::MembersWithParents.new(self).all_members.authorizable
end
def members_with_parents(only_active_users: true)
- # Avoids an unnecessary SELECT when the group has no parents
- source_ids =
- if has_parent?
- self_and_ancestors.reorder(nil).select(:id)
- else
- id
- end
-
- group_hierarchy_members = GroupMember.non_minimal_access
- .where(source_id: source_ids)
- .select(*GroupMember.cached_column_list)
-
- group_hierarchy_members = if only_active_users
- group_hierarchy_members.active_without_invites_and_requests
- else
- group_hierarchy_members.without_invites_and_requests
- end
-
- GroupMember.from_union([group_hierarchy_members,
- members_from_self_and_ancestor_group_shares])
+ Members::MembersWithParents
+ .new(self)
+ .members(active_users: only_active_users)
end
def members_from_self_and_ancestors_with_effective_access_level
@@ -988,48 +961,6 @@ class Group < Namespace
errors.add(:require_two_factor_authentication, _('is forbidden by a top-level group'))
end
- def members_from_self_and_ancestor_group_shares
- group_group_link_table = GroupGroupLink.arel_table
- group_member_table = GroupMember.arel_table
-
- source_ids =
- if has_parent?
- self_and_ancestors.reorder(nil).select(:id)
- else
- id
- end
-
- group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids)
- cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
- cte_alias = cte.table.alias(GroupGroupLink.table_name)
-
- # Instead of members.access_level, we need to maximize that access_level at
- # the respective group_group_links.group_access.
- member_columns = GroupMember.attribute_names.map do |column_name|
- if column_name == 'access_level'
- smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]], 'access_level')
- else
- group_member_table[column_name]
- end
- end
-
- GroupMember
- .with(cte.to_arel)
- .select(*member_columns)
- .from([group_member_table, cte.alias_to(group_group_link_table)])
- .where(group_member_table[:requested_at].eq(nil))
- .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
- .where(group_member_table[:source_type].eq('Namespace'))
- .where(group_member_table[:state].eq(::Member::STATE_ACTIVE))
- .non_minimal_access
- end
-
- def smallest_value_arel(args, column_alias)
- Arel::Nodes::As.new(
- Arel::Nodes::NamedFunction.new('LEAST', args),
- Arel::Nodes::SqlLiteral.new(column_alias))
- end
-
def runners_token_prefix
RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 77e283044ea..9690e16fd7d 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -135,11 +135,12 @@ class Member < ApplicationRecord
.reorder(nil)
end
- scope :without_invites_and_requests, -> do
- active_state
- .non_request
- .non_invite
- .non_minimal_access
+ scope :without_invites_and_requests, ->(minimal_access: false) do
+ result = active_state.non_request.non_invite
+
+ result = result.non_minimal_access unless minimal_access
+
+ result
end
scope :invite, -> { where.not(invite_token: nil) }
diff --git a/app/models/members/members/members_with_parents.rb b/app/models/members/members/members_with_parents.rb
new file mode 100644
index 00000000000..61ce99e1f3e
--- /dev/null
+++ b/app/models/members/members/members_with_parents.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module Members
+ class MembersWithParents
+ attr_reader :group
+
+ def initialize(group)
+ @group = group
+ end
+
+ # Returns all members for group and parents, with no filters
+ def all_members
+ GroupMember.from_union([
+ members_from_self_and_ancestors,
+ members_from_self_and_ancestor_group_shares
+ ])
+ end
+
+ # Returns members based on filter options:
+ #
+ # - `active_users`. DEPRECATED. If true, returns only members for active users
+ # - `minimal_access`. Used only in EE (GitLab Premium). If true, returns
+ # members which has minimal access. If false (default), does not return
+ # members with minimal access
+ #
+ # NOTE : this method does not return pending invites, nor requests.
+ def members(active_users: false, minimal_access: false)
+ raise ArgumentError, 'active_users: is deprecated' if active_users && minimal_access
+
+ group_hierarchy_members = members_from_self_and_ancestors
+
+ group_hierarchy_members =
+ if active_users
+ group_hierarchy_members.active_without_invites_and_requests
+ else
+ filter_invites_and_requests(group_hierarchy_members, minimal_access)
+ end
+
+ GroupMember.from_union([
+ group_hierarchy_members,
+ members_from_self_and_ancestor_group_shares
+ ])
+ end
+
+ private
+
+ # NOTE: minimal access is Premium, so in FOSS we will not include minimal access member
+ def filter_invites_and_requests(members, _minimal_access)
+ members.without_invites_and_requests(minimal_access: false)
+ end
+
+ def source_ids
+ # Avoids an unnecessary SELECT when the group has no parents
+ @source_ids ||=
+ if group.has_parent?
+ group.self_and_ancestors.reorder(nil).select(:id)
+ else
+ group.id
+ end
+ end
+
+ def members_from_self_and_ancestors
+ GroupMember
+ .with_source_id(source_ids)
+ .select(*GroupMember.cached_column_list)
+ end
+
+ def members_from_self_and_ancestor_group_shares
+ group_group_link_table = GroupGroupLink.arel_table
+ group_member_table = GroupMember.arel_table
+
+ group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids)
+ cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
+ cte_alias = cte.table.alias(GroupGroupLink.table_name)
+
+ # Instead of members.access_level, we need to maximize that access_level at
+ # the respective group_group_links.group_access.
+ member_columns = GroupMember.attribute_names.map do |column_name|
+ if column_name == 'access_level'
+ smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]], 'access_level')
+ else
+ group_member_table[column_name]
+ end
+ end
+
+ GroupMember
+ .with(cte.to_arel)
+ .select(*member_columns)
+ .from([group_member_table, cte.alias_to(group_group_link_table)])
+ .where(group_member_table[:requested_at].eq(nil))
+ .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
+ .where(group_member_table[:source_type].eq('Namespace'))
+ .where(group_member_table[:state].eq(::Member::STATE_ACTIVE))
+ .non_minimal_access
+ end
+
+ def smallest_value_arel(args, column_alias)
+ Arel::Nodes::As.new(
+ Arel::Nodes::NamedFunction.new('LEAST', args),
+ Arel::Nodes::SqlLiteral.new(column_alias))
+ end
+ end
+end
+
+Members::MembersWithParents.prepend_mod
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 5abda6196c1..1f2224bba09 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -301,6 +301,14 @@ class Namespace < ApplicationRecord
super || Gitlab::CurrentSettings.default_branch_protection
end
+ def default_branch_protection_settings
+ settings = default_branch_protection_defaults
+
+ return settings unless settings.blank?
+
+ Gitlab::CurrentSettings.default_branch_protection_defaults
+ end
+
def visibility_level_field
:visibility_level
end
diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb
index dba81a6cb60..5e47ec6310d 100644
--- a/app/models/project_feature_usage.rb
+++ b/app/models/project_feature_usage.rb
@@ -19,19 +19,6 @@ class ProjectFeatureUsage < ApplicationRecord
end
end
- def log_jira_dvcs_integration_usage(cloud: true)
- ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do
- integration_field = self.class.jira_dvcs_integration_field(cloud: cloud)
-
- # The feature usage is used only once later to query the feature usage in a
- # long date range. Therefore, we just need to update the timestamp once per
- # day
- break if persisted? && updated_today?(integration_field)
-
- persist_jira_dvcs_usage(integration_field)
- end
- end
-
private
def updated_today?(integration_field)
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index aad763555bf..a9880e56e8c 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -76,7 +76,8 @@ module Users
# 74 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132751
vsd_feedback_banner: 75, # EE-only
security_policy_protected_branch_modification: 76, # EE-only
- vulnerability_report_grouping: 77 # EE-only
+ vulnerability_report_grouping: 77, # EE-only
+ new_nav_for_everyone_callout: 78
}
validates :feature_name,
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 3e81418c757..c983d8623d2 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -436,6 +436,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
count_of_extra_topics_not_shown > 0
end
+ def has_review_app?
+ !project.environments_for_scope('review/*').empty?
+ end
+
def can_setup_review_app?
strong_memoize(:can_setup_review_app) do
(can_instantiate_cluster? && all_clusters_empty?) || cicd_missing?
diff --git a/app/serializers/review_app_setup_entity.rb b/app/serializers/review_app_setup_entity.rb
index 3a21fe24d9e..1fde31bc847 100644
--- a/app/serializers/review_app_setup_entity.rb
+++ b/app/serializers/review_app_setup_entity.rb
@@ -13,6 +13,8 @@ class ReviewAppSetupEntity < Grape::Entity
YAML.safe_load(File.read(Rails.root.join('lib', 'gitlab', 'ci', 'snippets', 'review_app_default.yml'))).to_s
end
+ expose :has_review_app?, as: :has_review_app
+
private
def current_user
diff --git a/app/services/ci/catalog/resources/versions/create_service.rb b/app/services/ci/catalog/resources/versions/create_service.rb
new file mode 100644
index 00000000000..863bad43271
--- /dev/null
+++ b/app/services/ci/catalog/resources/versions/create_service.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ module Versions
+ class CreateService
+ def initialize(release)
+ @project = release.project
+ @release = release
+ @errors = []
+ @version = nil
+ @components_project = Ci::Catalog::ComponentsProject.new(project)
+ end
+
+ def execute
+ build_catalog_resource_version
+ fetch_and_build_components if Feature.enabled?(:ci_catalog_create_metadata, project)
+ publish_catalog_resource!
+
+ if errors.empty?
+ ServiceResponse.success
+ else
+ ServiceResponse.error(message: errors.flatten.first(10).join(', '))
+ end
+ end
+
+ private
+
+ attr_reader :project, :errors, :release, :components_project
+
+ def build_catalog_resource_version
+ return error('Project is not a catalog resource') unless project.catalog_resource
+
+ @version = Ci::Catalog::Resources::Version.new(
+ release: release,
+ catalog_resource: project.catalog_resource,
+ project: project
+ )
+ end
+
+ def fetch_and_build_components
+ return if errors.present?
+
+ max_components = Ci::Catalog::ComponentsProject::COMPONENTS_LIMIT
+ component_paths = components_project.fetch_component_paths(release.sha, limit: max_components + 1)
+
+ if component_paths.size > max_components
+ return error("Release cannot contain more than #{max_components} components")
+ end
+
+ build_components(component_paths)
+ end
+
+ def build_components(component_paths)
+ paths_with_oids = component_paths.map { |path| [release.sha, path] }
+ blobs = project.repository.blobs_at(paths_with_oids)
+
+ blobs.each do |blob|
+ metadata = extract_metadata(blob)
+ build_catalog_resource_component(metadata)
+ end
+ rescue ::Gitlab::Config::Loader::FormatError => e
+ error(e)
+ end
+
+ def extract_metadata(blob)
+ {
+ name: components_project.extract_component_name(blob.path),
+ inputs: components_project.extract_inputs(blob.data),
+ path: blob.path
+ }
+ end
+
+ def build_catalog_resource_component(metadata)
+ return if errors.present?
+
+ component = @version.components.build(
+ name: metadata[:name],
+ project: @version.project,
+ inputs: metadata[:inputs],
+ catalog_resource: @version.catalog_resource,
+ path: metadata[:path],
+ created_at: Time.current
+ )
+
+ return if component.valid?
+
+ error("Build component error: #{component.errors.full_messages.join(', ')}")
+ end
+
+ def publish_catalog_resource!
+ return if errors.present?
+
+ ::Ci::Catalog::Resources::Version.transaction do
+ BulkInsertableAssociations.with_bulk_insert do
+ @version.save!
+ end
+
+ project.catalog_resource.publish!
+ end
+ end
+
+ def error(message)
+ errors << message
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 83f9ccdfad6..8092299fb61 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -59,6 +59,8 @@
.form-group
= f.gitlab_ui_checkbox_component :suggest_pipeline_enabled, s_('AdminSettings|Enable pipeline suggestion banner'), help_text: s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.')
#js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes }
+ .form-group
+ = f.gitlab_ui_checkbox_component :enable_artifact_external_redirect_warning_page, s_('AdminSettings|Enable the external redirect warning page for job artifacts'), help_text: s_('AdminSettings|Show a redirect page that warns you about user-generated content in GitLab Pages.')
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index c56678d730d..fe2c2e968e8 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -7,7 +7,7 @@
- sidebar_panel = super_sidebar_nav_panel(nav: nav, user: current_user, group: group, project: @project, current_ref: current_ref, ref_type: @ref_type, viewed_user: @user, organization: @organization)
- sidebar_data = super_sidebar_context(current_user, group: group, project: @project, panel: sidebar_panel, panel_type: nav).to_json
- %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_path, force_desktop_expanded_sidebar: @force_desktop_expanded_sidebar.to_s, command_palette: command_palette_data(project: @project).to_json } }
+ %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, force_desktop_expanded_sidebar: @force_desktop_expanded_sidebar.to_s, command_palette: command_palette_data(project: @project).to_json } }
- if display_whats_new?
#whats-new-app{ data: { version_digest: whats_new_version_digest } }
@@ -20,6 +20,7 @@
.mobile-overlay
= dispensable_render_if_exists 'layouts/header/verification_reminder'
.alert-wrapper.gl-force-block-formatting-context
+ = dispensable_render 'shared/new_nav_for_everyone_announcement'
= dispensable_render 'shared/outdated_browser'
= dispensable_render_if_exists "layouts/header/licensed_user_count_threshold"
= dispensable_render_if_exists "layouts/header/token_expiry_notification"
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 451c66b074b..5a66cc0ddb5 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,6 +1,6 @@
- page_classes = page_class << @html_class
-- page_classes = page_classes.flatten.compact
-- body_classes = [user_application_theme, user_tab_width, @body_class, client_class_list, *custom_diff_color_classes]
+- page_classes = [user_application_theme, page_classes.flatten.compact]
+- body_classes = [user_tab_width, @body_class, client_class_list, *custom_diff_color_classes]
!!! 5
%html{ lang: I18n.locale, class: page_classes }
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 4e9ae7c7fd8..366a51ef29e 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,9 +1,9 @@
- add_page_specific_style 'page_bundles/login'
- custom_text = custom_sign_in_description
!!! 5
-%html.html-devise-layout{ lang: I18n.locale }
+%html.html-devise-layout{ class: user_application_theme, lang: I18n.locale }
= render "layouts/head", { startup_filename: 'signin' }
- %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}", data: { page: body_data_page, qa_selector: 'login_page' } }
+ %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}", data: { page: body_data_page, qa_selector: 'login_page' } }
= header_message
= render "layouts/init_client_detection_flags"
- if Feature.enabled?(:restyle_login_page, @project)
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index 3e969b866a6..6816a64ac8f 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -1,8 +1,8 @@
- add_page_specific_style 'page_bundles/login'
!!! 5
-%html.html-devise-layout{ lang: I18n.locale }
+%html.html-devise-layout{ class: user_application_theme, lang: I18n.locale }
= render "layouts/head"
- %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}" }
+ %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}" }
= header_message
= render "layouts/init_client_detection_flags"
= render "layouts/header/empty"
diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml
index da192822902..f168c742085 100644
--- a/app/views/layouts/fullscreen.html.haml
+++ b/app/views/layouts/fullscreen.html.haml
@@ -1,8 +1,8 @@
- minimal = local_assigns.fetch(:minimal, false)
!!! 5
-%html{ lang: I18n.locale, class: page_class }
+%html{ class: [user_application_theme, page_class], lang: I18n.locale }
= render "layouts/head"
- %body{ class: "#{user_application_theme} #{user_tab_width} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
+ %body{ class: "#{user_tab_width} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
= render 'peek/bar'
= header_message
- unless minimal
diff --git a/app/views/layouts/signup_onboarding.html.haml b/app/views/layouts/signup_onboarding.html.haml
index d440b543662..c8e15896b97 100644
--- a/app/views/layouts/signup_onboarding.html.haml
+++ b/app/views/layouts/signup_onboarding.html.haml
@@ -1,9 +1,9 @@
- add_page_specific_style 'page_bundles/signup'
- add_page_specific_style 'page_bundles/login'
!!! 5
-%html.html-devise-layout{ lang: I18n.locale }
+%html.html-devise-layout{ class: user_application_theme, lang: I18n.locale }
= render "layouts/head"
- %body.signup-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}", data: { page: body_data_page } }
+ %body.signup-page.navless{ class: "#{system_message_class} #{client_class_list}", data: { page: body_data_page } }
= header_message
= render "layouts/init_client_detection_flags"
= render "layouts/header/logo_with_title"
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index 32f00a4c0c6..09b5407ecdb 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -2,11 +2,10 @@
- add_page_specific_style 'page_bundles/terms'
- @hide_top_bar = true
- @hide_top_bar_padding = true
-- body_classes = [user_application_theme]
-%html{ lang: I18n.locale, class: page_class }
+%html{ lang: I18n.locale, class: [user_application_theme, page_class] }
= render "layouts/head"
- %body{ class: body_classes, data: { page: body_data_page } }
+ %body{ data: { page: body_data_page } }
.layout-page.terms{ class: page_class }
.content-wrapper.gl-pb-5
.mobile-overlay
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
index e120975a8f9..19db01a2df1 100644
--- a/app/views/projects/artifacts/_tree_file.html.haml
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -1,6 +1,6 @@
- blob = file.blob
- external_link = blob.external_link?(@build)
-- if external_link
+- if external_link && Gitlab::CurrentSettings.enable_artifact_external_redirect_warning_page
- path_to_file = external_file_project_job_artifacts_path(@project, @build, path: file.path)
- else
- path_to_file = file_project_job_artifacts_path(@project, @build, path: file.path)
diff --git a/app/views/shared/_new_nav_for_everyone_announcement.html.haml b/app/views/shared/_new_nav_for_everyone_announcement.html.haml
new file mode 100644
index 00000000000..fa870249596
--- /dev/null
+++ b/app/views/shared/_new_nav_for_everyone_announcement.html.haml
@@ -0,0 +1,18 @@
+- return unless show_new_nav_for_everyone_callout?
+
+- blog_url = 'https://about.gitlab.com/blog/2023/08/15/navigation-research-blog-post/'
+- issues_url = 'https://about.gitlab.com/submit-feedback/#product-feedback'
+
+- blog_link_tags = tag_pair(link_to('', blog_url, rel: 'noopener noreferrer', target: '_blank'), :blog_link_start, :link_end)
+- issues_link_tags = tag_pair(link_to('', issues_url, rel: 'noopener noreferrer', target: '_blank'), :issues_link_start, :link_end)
+
+- welcome_text = safe_format(_('GitLab has redesigned the left sidebar to address customer feedback. View details in %{blog_link_start}this blog post%{link_end}. Here\'s how to %{issues_link_start}file an issue%{link_end} with the GitLab product team.'), blog_link_tags, issues_link_tags)
+
+= render Pajamas::AlertComponent.new(dismissible: true,
+ alert_options: { class: 'js-new-nav-for-everyone-callout', data: { feature_id: "new_nav_for_everyone_callout", dismiss_endpoint: callouts_path }}) do |c|
+ - c.with_body do
+ %p
+ = welcome_text
+ - c.with_actions do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: blog_url, target: '_blank', button_options: { class: 'gl-alert-action' }) do |c|
+ = _('Learn more')
diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb
index 1c426be4545..b35fa2ced3f 100644
--- a/app/workers/bulk_imports/entity_worker.rb
+++ b/app/workers/bulk_imports/entity_worker.rb
@@ -36,7 +36,12 @@ module BulkImports
def perform_failure(exception, entity_id)
@entity = ::BulkImports::Entity.find(entity_id)
- log_and_fail(exception)
+ Gitlab::ErrorTracking.track_exception(
+ exception,
+ log_params(message: "Request to export #{entity.source_type} failed")
+ )
+
+ entity.fail_op!
end
private
@@ -99,14 +104,5 @@ module BulkImports
defaults.merge(extra)
end
-
- def log_and_fail(exception)
- Gitlab::ErrorTracking.track_exception(
- exception,
- log_params(message: "Request to export #{entity.source_type} failed")
- )
-
- entity.fail_op!
- end
end
end
diff --git a/config/feature_categories.yml b/config/feature_categories.yml
index 0c36be00774..3eaf8b2b34d 100644
--- a/config/feature_categories.yml
+++ b/config/feature_categories.yml
@@ -93,6 +93,7 @@
- organization
- package_registry
- pages
+- permissions
- pipeline_composition
- portfolio_management
- product_analytics_data_management
diff --git a/config/feature_flags/development/ci_catalog_create_metadata.yml b/config/feature_flags/development/ci_catalog_create_metadata.yml
new file mode 100644
index 00000000000..a73f499554d
--- /dev/null
+++ b/config/feature_flags/development/ci_catalog_create_metadata.yml
@@ -0,0 +1,8 @@
+---
+name: ci_catalog_create_metadata
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134148
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430120
+milestone: '16.6'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/development/manage_project_access_tokens.yml b/config/feature_flags/development/manage_project_access_tokens.yml
index cf2dc9ab581..a6cf2cf4f9f 100644
--- a/config/feature_flags/development/manage_project_access_tokens.yml
+++ b/config/feature_flags/development/manage_project_access_tokens.yml
@@ -1,7 +1,7 @@
---
name: manage_project_access_tokens
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132342
-rollout_issue_url:
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430353
milestone: '16.5'
type: development
group: group::authorization
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 960caf1dc35..1e5fb17c971 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -183,6 +183,7 @@ Settings.gitlab['default_project_creation'] ||= ::Gitlab::Access::DEVELOPER_MAIN
Settings.gitlab['default_project_deletion_protection'] ||= false
Settings.gitlab['default_projects_limit'] ||= 100000
Settings.gitlab['default_branch_protection'] ||= 2
+Settings.gitlab['default_branch_protection_defaults'] ||= ::Gitlab::Access::BranchProtection.protected_fully
# `default_can_create_group` is deprecated since GitLab 15.5 in favour of the `can_create_group` column on `ApplicationSetting`.
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
diff --git a/config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml b/config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml
index 5bf8e1d6e78..c9c85bca415 100644
--- a/config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml
+++ b/config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml
@@ -6,7 +6,7 @@ product_section: dev
product_stage: manage
product_group: integrations
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
instrumentation_class: CountProjectsWithJiraDvcsIntegrationMetric
@@ -21,3 +21,5 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135755
+milestone_removed: "<16.6"
diff --git a/config/metrics/settings/20210204124920_web_ide_clientside_preview_enabled.yml b/config/metrics/settings/20210204124920_web_ide_clientside_preview_enabled.yml
index a206d8ecd7a..a04e8d82686 100644
--- a/config/metrics/settings/20210204124920_web_ide_clientside_preview_enabled.yml
+++ b/config/metrics/settings/20210204124920_web_ide_clientside_preview_enabled.yml
@@ -6,7 +6,7 @@ product_section: dev
product_stage: create
product_group: ide
value_type: boolean
-status: active
+status: removed
time_frame: none
data_source: database
distribution:
@@ -18,3 +18,5 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136114
+milestone_removed: "16.6"
diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile
index 150109eff51..78f8c87a528 100644
--- a/danger/documentation/Dangerfile
+++ b/danger/documentation/Dangerfile
@@ -19,6 +19,13 @@ MSG
docs_paths_to_review = helper.changes_by_category[:docs]
+# Some docs do not need a review from a technical writer
+SKIP_TW_REVIEW_PATHS = ['doc/solutions'].freeze
+
+docs_paths_to_review.delete_if do |item|
+ SKIP_TW_REVIEW_PATHS.any? { |skip_path| item.start_with?(skip_path) }
+end
+
# Documentation should be updated for feature::addition and feature::enhancement
if docs_paths_to_review.empty?
warn(DOCUMENTATION_UPDATE_MISSING) if feature_mr?
diff --git a/data/deprecations/16-6-deprecation-legacy-geo-prometheus-metrics.yml b/data/deprecations/16-6-deprecation-legacy-geo-prometheus-metrics.yml
new file mode 100644
index 00000000000..59db93293be
--- /dev/null
+++ b/data/deprecations/16-6-deprecation-legacy-geo-prometheus-metrics.yml
@@ -0,0 +1,22 @@
+- title: "Legacy Geo Prometheus metrics"
+ removal_milestone: "17.0"
+ announcement_milestone: "16.6"
+ breaking_change: true
+ reporter: sranasinghe
+ stage: enablement
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430192
+ body: |
+ Following the migration of projects to the [Geo self-service framework](https://docs.gitlab.com/ee/development/geo/framework.html) we have deprecated a number of [Prometheus](https://docs.gitlab.com/ee/administration/monitoring/prometheus/) metrics.
+ The following Geo-related Prometheus metrics are deprecated and will be removed in 17.0.
+ The table below lists the deprecated metrics and their respective replacements. The replacements are available in GitLab 16.3.0 and later.
+
+ | Deprecated metric | Replacement metric |
+ | ---------------------------------------- | ---------------------------------------------- |
+ | `geo_repositories_synced` | `geo_project_repositories_synced` |
+ | `geo_repositories_failed` | `geo_project_repositories_failed` |
+ | `geo_repositories_checksummed` | `geo_project_repositories_checksummed` |
+ | `geo_repositories_checksum_failed` | `geo_project_repositories_checksum_failed` |
+ | `geo_repositories_verified` | `geo_project_repositories_verified` |
+ | `geo_repositories_verification_failed` | `geo_project_repositories_verification_failed` |
+ | `geo_repositories_checksum_mismatch` | None available |
+ | `geo_repositories_retrying_verification` | None available |
diff --git a/db/click_house/main/20230707151359_create_ci_finished_builds.sql b/db/click_house/main/20230707151359_create_ci_finished_builds.sql
index 5c2cc0e8eb3..9fd17e1968f 100644
--- a/db/click_house/main/20230707151359_create_ci_finished_builds.sql
+++ b/db/click_house/main/20230707151359_create_ci_finished_builds.sql
@@ -30,4 +30,4 @@ CREATE TABLE ci_finished_builds
)
ENGINE = ReplacingMergeTree -- Using ReplacingMergeTree just in case we accidentally insert the same data twice
ORDER BY (status, runner_type, project_id, finished_at, id)
-PARTITION BY toYear(finished_at)
+PARTITION BY toYear(finished_at);
diff --git a/db/click_house/main/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.sql b/db/click_house/main/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.sql
index 56889ffc0d4..0b05c3a37f6 100644
--- a/db/click_house/main/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.sql
+++ b/db/click_house/main/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.sql
@@ -8,4 +8,4 @@ CREATE TABLE ci_finished_builds_aggregated_queueing_delay_percentiles
queueing_duration_quantile AggregateFunction(quantile, Int64)
)
ENGINE = AggregatingMergeTree()
-ORDER BY (started_at_bucket, status, runner_type)
+ORDER BY (started_at_bucket, status, runner_type);
diff --git a/db/migrate/20231002162941_add_enable_artifact_external_redirect_warning_page_to_application_settings.rb b/db/migrate/20231002162941_add_enable_artifact_external_redirect_warning_page_to_application_settings.rb
new file mode 100644
index 00000000000..06fc4b6b313
--- /dev/null
+++ b/db/migrate/20231002162941_add_enable_artifact_external_redirect_warning_page_to_application_settings.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddEnableArtifactExternalRedirectWarningPageToApplicationSettings < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def change
+ add_column(:application_settings, :enable_artifact_external_redirect_warning_page, :boolean, default: true,
+ null: false)
+ end
+end
diff --git a/db/migrate/20231024133234_add_source_package_name_to_sbom_component.rb b/db/migrate/20231024133234_add_source_package_name_to_sbom_component.rb
new file mode 100644
index 00000000000..41970429ca9
--- /dev/null
+++ b/db/migrate/20231024133234_add_source_package_name_to_sbom_component.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class AddSourcePackageNameToSbomComponent < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ INDEX = 'index_source_package_names_on_component_and_purl'
+
+ def up
+ with_lock_retries do
+ add_column :sbom_components, :source_package_name, :text, if_not_exists: true
+ end
+
+ add_text_limit :sbom_components, :source_package_name, 255
+ add_concurrent_index :sbom_components,
+ [:component_type, :source_package_name, :purl_type],
+ name: INDEX,
+ unique: true
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :sbom_components, :source_package_name, if_exists: true
+ end
+
+ remove_concurrent_index_by_name :sbom_components, name: INDEX
+ end
+end
diff --git a/db/post_migrate/20231102083539_backfill_p_ci_builds_pipeline_id.rb b/db/post_migrate/20231102083539_backfill_p_ci_builds_pipeline_id.rb
new file mode 100644
index 00000000000..feada383fe4
--- /dev/null
+++ b/db/post_migrate/20231102083539_backfill_p_ci_builds_pipeline_id.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class BackfillPCiBuildsPipelineId < Gitlab::Database::Migration[2.2]
+ restrict_gitlab_migration gitlab_schema: :gitlab_ci
+ milestone '16.6'
+
+ TABLE_NAME = :ci_builds
+ COLUMN_NAMES = %i[
+ auto_canceled_by_id
+ commit_id
+ erased_by_id
+ project_id
+ runner_id
+ trigger_request_id
+ upstream_pipeline_id
+ user_id
+ ]
+ SUB_BATCH_SIZE = 750
+ BATCH_SIZE = 75_000
+ PAUSE_MS = 0
+
+ def up
+ backfill_conversion_of_integer_to_bigint(
+ TABLE_NAME, COLUMN_NAMES,
+ sub_batch_size: SUB_BATCH_SIZE,
+ batch_size: BATCH_SIZE,
+ pause_ms: PAUSE_MS
+ )
+ end
+
+ def down
+ revert_backfill_conversion_of_integer_to_bigint(TABLE_NAME, COLUMN_NAMES)
+ end
+end
diff --git a/db/schema_migrations/20231002162941 b/db/schema_migrations/20231002162941
new file mode 100644
index 00000000000..a6842b3f677
--- /dev/null
+++ b/db/schema_migrations/20231002162941
@@ -0,0 +1 @@
+ddf75326b9bb04275bf48e9a2eb6c15af7a9ca6c00864a636d5e179c5881b20b \ No newline at end of file
diff --git a/db/schema_migrations/20231024133234 b/db/schema_migrations/20231024133234
new file mode 100644
index 00000000000..fb536f574d3
--- /dev/null
+++ b/db/schema_migrations/20231024133234
@@ -0,0 +1 @@
+0a92e23317e4fc12b9de9d15c0d3895afe211b543a0449834b9459616152680a \ No newline at end of file
diff --git a/db/schema_migrations/20231102083539 b/db/schema_migrations/20231102083539
new file mode 100644
index 00000000000..489269151bb
--- /dev/null
+++ b/db/schema_migrations/20231102083539
@@ -0,0 +1 @@
+1ac3716a5e014abe1828d648bd9f1014d770b40c4006944f341739728026fcd4 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 2de7aa4a6ab..bc15823a408 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11877,6 +11877,7 @@ CREATE TABLE application_settings (
project_jobs_api_rate_limit integer DEFAULT 600 NOT NULL,
math_rendering_limits_enabled boolean DEFAULT true NOT NULL,
service_access_tokens_expiration_enforced boolean DEFAULT true NOT NULL,
+ enable_artifact_external_redirect_warning_page boolean DEFAULT true NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
@@ -22824,7 +22825,9 @@ CREATE TABLE sbom_components (
component_type smallint NOT NULL,
name text NOT NULL,
purl_type smallint,
- CONSTRAINT check_91a8f6ad53 CHECK ((char_length(name) <= 255))
+ source_package_name text,
+ CONSTRAINT check_91a8f6ad53 CHECK ((char_length(name) <= 255)),
+ CONSTRAINT check_e2dcb53709 CHECK ((char_length(source_package_name) <= 255))
);
CREATE SEQUENCE sbom_components_id_seq
@@ -34396,6 +34399,8 @@ CREATE INDEX index_sop_schedules_on_sop_configuration_id ON security_orchestrati
CREATE INDEX index_sop_schedules_on_user_id ON security_orchestration_policy_rule_schedules USING btree (user_id);
+CREATE UNIQUE INDEX index_source_package_names_on_component_and_purl ON sbom_components USING btree (component_type, source_package_name, purl_type);
+
CREATE INDEX index_spam_logs_on_user_id ON spam_logs USING btree (user_id);
CREATE INDEX index_sprints_iterations_cadence_id ON sprints USING btree (iterations_cadence_id);
diff --git a/doc/administration/settings/continuous_integration.md b/doc/administration/settings/continuous_integration.md
index 841b6e644eb..0e2a512302d 100644
--- a/doc/administration/settings/continuous_integration.md
+++ b/doc/administration/settings/continuous_integration.md
@@ -266,6 +266,22 @@ To enable or disable the banner:
1. Select or clear the **Enable pipeline suggestion banner** checkbox.
1. Select **Save changes**.
+## Enable or disable the external redirect page for job artifacts
+
+By default, GitLab Pages shows an external redirect page when a user tries to view
+a job artifact served by GitLab Pages. This page warns about the potential for
+malicious user-generated content, as described in
+[issue 352611](https://gitlab.com/gitlab-org/gitlab/-/issues/352611).
+
+Self-managed administrators can disable the external redirect warning page,
+so you can view job artifact pages directly:
+
+1. On the left sidebar, select **Search or go to**.
+1. Select **Admin Area**.
+1. Select **Settings > CI/CD**.
+1. Expand **Continuous Integration and Deployment**.
+1. Deselect **Enable the external redirect page for job artifacts**.
+
## Required pipeline configuration **(ULTIMATE SELF)**
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/352316) from GitLab Premium to GitLab Ultimate in 15.0.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9ef51601468..6c22ffa5f22 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -7666,7 +7666,7 @@ Input type: `ValueStreamCreateInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationvaluestreamcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| <a id="mutationvaluestreamcreatename"></a>`name` | [`String!`](#string) | Value stream description. |
+| <a id="mutationvaluestreamcreatename"></a>`name` | [`String!`](#string) | Value stream name. |
| <a id="mutationvaluestreamcreatenamespacepath"></a>`namespacePath` | [`ID!`](#id) | Full path of the namespace(project or group) the value stream is created in. |
#### Fields
@@ -7702,6 +7702,32 @@ Input type: `ValueStreamDestroyInput`
| <a id="mutationvaluestreamdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationvaluestreamdestroyvaluestream"></a>`valueStream` | [`ValueStream`](#valuestream) | Value stream deleted after mutation. |
+### `Mutation.valueStreamUpdate`
+
+Updates a value stream.
+
+WARNING:
+**Introduced** in 16.6.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `ValueStreamUpdateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationvaluestreamupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationvaluestreamupdateid"></a>`id` | [`AnalyticsCycleAnalyticsValueStreamID!`](#analyticscycleanalyticsvaluestreamid) | Global ID of the value stream to update. |
+| <a id="mutationvaluestreamupdatename"></a>`name` | [`String!`](#string) | Value stream name. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationvaluestreamupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationvaluestreamupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationvaluestreamupdatevaluestream"></a>`valueStream` | [`ValueStream`](#valuestream) | Updated value stream. |
+
### `Mutation.vulnerabilitiesDismiss`
Input type: `VulnerabilitiesDismissInput`
@@ -14012,6 +14038,7 @@ Core representation of a GitLab user.
| <a id="autocompleteduserid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="autocompleteduseride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="autocompleteduserjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="autocompleteduserlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="autocompleteduserlinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="autocompleteduserlocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="autocompletedusername"></a>`name` | [`String!`](#string) | Human-readable name of the user. Returns `****` if the user is a project bot and the requester does not have permission to view the project. |
@@ -20447,6 +20474,7 @@ A user assigned to a merge request.
| <a id="mergerequestassigneeid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestassigneeide"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="mergerequestassigneejobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="mergerequestassigneelastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="mergerequestassigneelinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="mergerequestassigneelocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="mergerequestassigneemergerequestinteraction"></a>`mergeRequestInteraction` | [`UserMergeRequestInteraction`](#usermergerequestinteraction) | Details of this user's interactions with the merge request. |
@@ -20727,6 +20755,7 @@ The author of the merge request.
| <a id="mergerequestauthorid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestauthoride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="mergerequestauthorjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="mergerequestauthorlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="mergerequestauthorlinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="mergerequestauthorlocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="mergerequestauthormergerequestinteraction"></a>`mergeRequestInteraction` | [`UserMergeRequestInteraction`](#usermergerequestinteraction) | Details of this user's interactions with the merge request. |
@@ -21070,6 +21099,7 @@ A user participating in a merge request.
| <a id="mergerequestparticipantid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestparticipantide"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="mergerequestparticipantjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="mergerequestparticipantlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="mergerequestparticipantlinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="mergerequestparticipantlocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="mergerequestparticipantmergerequestinteraction"></a>`mergeRequestInteraction` | [`UserMergeRequestInteraction`](#usermergerequestinteraction) | Details of this user's interactions with the merge request. |
@@ -21386,6 +21416,7 @@ A user assigned to a merge request as a reviewer.
| <a id="mergerequestreviewerid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestrevieweride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="mergerequestreviewerjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="mergerequestreviewerlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="mergerequestreviewerlinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="mergerequestreviewerlocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="mergerequestreviewermergerequestinteraction"></a>`mergeRequestInteraction` | [`UserMergeRequestInteraction`](#usermergerequestinteraction) | Details of this user's interactions with the merge request. |
@@ -22099,10 +22130,21 @@ A user with access to the organization.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="organizationuserbadges"></a>`badges` **{warning-solid}** | [`[String!]`](#string) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Badges describing the user within the organization. |
+| <a id="organizationuserbadges"></a>`badges` **{warning-solid}** | [`[OrganizationUserBadge!]`](#organizationuserbadge) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Badges describing the user within the organization. |
| <a id="organizationuserid"></a>`id` **{warning-solid}** | [`ID!`](#id) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. ID of the organization user. |
| <a id="organizationuseruser"></a>`user` **{warning-solid}** | [`UserCore!`](#usercore) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. User that is associated with the organization. |
+### `OrganizationUserBadge`
+
+An organization user badge.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="organizationuserbadgetext"></a>`text` | [`String!`](#string) | Badge text. |
+| <a id="organizationuserbadgevariant"></a>`variant` | [`String!`](#string) | Badge variant. |
+
### `Package`
Represents a package with pipelines in the Package Registry.
@@ -26271,6 +26313,7 @@ Core representation of a GitLab user.
| <a id="usercoreid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="usercoreide"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="usercorejobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="usercorelastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="usercorelinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="usercorelocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="usercorename"></a>`name` | [`String!`](#string) | Human-readable name of the user. Returns `****` if the user is a project bot and the requester does not have permission to view the project. |
@@ -30097,6 +30140,7 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumnamespace_storage_limit_alert_error_threshold"></a>`NAMESPACE_STORAGE_LIMIT_ALERT_ERROR_THRESHOLD` | Callout feature name for namespace_storage_limit_alert_error_threshold. |
| <a id="usercalloutfeaturenameenumnamespace_storage_limit_alert_warning_threshold"></a>`NAMESPACE_STORAGE_LIMIT_ALERT_WARNING_THRESHOLD` | Callout feature name for namespace_storage_limit_alert_warning_threshold. |
| <a id="usercalloutfeaturenameenumnamespace_storage_pre_enforcement_banner"></a>`NAMESPACE_STORAGE_PRE_ENFORCEMENT_BANNER` | Callout feature name for namespace_storage_pre_enforcement_banner. |
+| <a id="usercalloutfeaturenameenumnew_nav_for_everyone_callout"></a>`NEW_NAV_FOR_EVERYONE_CALLOUT` | Callout feature name for new_nav_for_everyone_callout. |
| <a id="usercalloutfeaturenameenumnew_top_level_group_alert"></a>`NEW_TOP_LEVEL_GROUP_ALERT` | Callout feature name for new_top_level_group_alert. |
| <a id="usercalloutfeaturenameenumnew_user_signups_cap_reached"></a>`NEW_USER_SIGNUPS_CAP_REACHED` | Callout feature name for new_user_signups_cap_reached. |
| <a id="usercalloutfeaturenameenumpersonal_access_token_expiry"></a>`PERSONAL_ACCESS_TOKEN_EXPIRY` | Callout feature name for personal_access_token_expiry. |
@@ -31792,6 +31836,7 @@ Implementations:
| <a id="userid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="useride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="userjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="userlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="userlinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="userlocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="username"></a>`name` | [`String!`](#string) | Human-readable name of the user. Returns `****` if the user is a project bot and the requester does not have permission to view the project. |
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 670be23336c..0a74afe2abf 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -575,6 +575,7 @@ listed in the descriptions of the relevant settings.
| `spam_check_endpoint_url` | string | no | URL of the external Spamcheck service endpoint. Valid URI schemes are `grpc` or `tls`. Specifying `tls` forces communication to be encrypted.|
| `spam_check_api_key` | string | no | API key used by GitLab for accessing the Spam Check service endpoint. |
| `suggest_pipeline_enabled` | boolean | no | Enable pipeline suggestion banner. |
+| `enable_artifact_external_redirect_warning_page` | boolean | no | Show the external redirect page that warns you about user-generated content in GitLab Pages. |
| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to `0` for unlimited time. |
| `terms` | text | required by: `enforce_terms` | (**Required by:** `enforce_terms`) Markdown content for the ToS. |
| `throttle_authenticated_api_enabled` | boolean | no | (**If enabled, requires:** `throttle_authenticated_api_period_in_seconds` and `throttle_authenticated_api_requests_per_period`) Enable authenticated API request rate limit. Helps reduce request volume (for example, from crawlers or abusive bots). |
diff --git a/doc/ci/components/index.md b/doc/ci/components/index.md
index 9063b6d0378..338e4b2c205 100644
--- a/doc/ci/components/index.md
+++ b/doc/ci/components/index.md
@@ -29,6 +29,8 @@ A components repository is a GitLab project with a repository that hosts one or
If a component requires different versioning from other components, the component should be migrated to its own components repository.
+One component repository can have a maximum of 10 components.
+
## Create a components repository
To create a components repository, you must:
@@ -65,17 +67,17 @@ the file structure should be similar to:
```plaintext
├── templates/
-│ └── only_template.yml
+│ └── secret-detection.yml
├── README.md
└── .gitlab-ci.yml
```
-This example component could be referenced with a path similar to `gitlab.com/my-username/my-component/only_template@<version>`,
+This example component could be referenced with a path similar to `gitlab.com/my-namespace/my-project/secret-detection@<version>`,
if the project is:
- On GitLab.com
-- Named `my-component`
-- In a personal namespace named `my-username`
+- Named `my-project`
+- In a personal namespace or group named `my-namespace`
The templates directory and the suffix of the configuration file should be excluded from the referenced path.
@@ -85,26 +87,32 @@ If the project contains multiple components, then the file structure should be s
├── README.md
├── .gitlab-ci.yml
└── templates/
- └── all-scans.yml
+ ├── all-scans.yml
└── secret-detection.yml
```
These components would be referenced with these paths:
-- `gitlab.com/my-username/my-component/all-scans`
-- `gitlab.com/my-username/my-component/secret-detection`
+- `gitlab.com/my-namespace/my-project/all-scans@<version>`
+- `gitlab.com/my-namespace/my-project/secret-detection@<version>`
+
+You can also have components defined as a directory if you want to bundle together multiple related files.
+In this case GitLab expects a `template.yml` file to be present:
-You can omit the filename in the path if the configuration file is named `template.yml`.
-For example, the following component could be referenced with `gitlab.com/my-username/my-component/dast`:
+For example:
```plaintext
├── README.md
├── .gitlab-ci.yml
-├── templates/
-│ └── dast
-│ └── template.yml
+└── templates/
+ └── dast
+ ├── docs.md
+ ├── Dockerfile
+ └── template.yml
```
+In this example, the component could be referenced with `gitlab.com/my-namespace/my-project/dast@<version>`.
+
#### Component configurations saved in any directory (deprecated)
WARNING:
@@ -117,8 +125,8 @@ Components configurations can be saved through the following directory structure
components, each file must be in a separate subdirectory.
- `README.md`: A documentation file explaining the details of all the components in the repository.
-For example, if the project is on GitLab.com, named `my-component`, and in a personal
-namespace named `my-username`:
+For example, if the project is on GitLab.com, named `my-project`, and in a personal
+namespace named `my-namespace`:
- Containing a single component and a simple pipeline to test the component, then
the file structure might be:
@@ -132,7 +140,7 @@ namespace named `my-username`:
The `.gitlab-ci.yml` file is not required for a CI/CD component to work, but
[testing the component](#test-the-component) in a pipeline in the project is recommended.
- This component is referenced with the path `gitlab.com/my-username/my-component@<version>`.
+ This component is referenced with the path `gitlab.com/my-namespace/my-project@<version>`.
- Containing one default component and multiple sub-components, then the file structure
might be:
@@ -149,9 +157,9 @@ namespace named `my-username`:
These components are identified by these paths:
- - `gitlab.com/my-username/my-component`
- - `gitlab.com/my-username/my-component/unit`
- - `gitlab.com/my-username/my-component/integration`
+ - `gitlab.com/my-namespace/my-project`
+ - `gitlab.com/my-namespace/my-project/unit`
+ - `gitlab.com/my-namespace/my-project/integration`
It is possible to have a components repository with no default component, by having
no `template.yml` in the root directory.
@@ -205,7 +213,7 @@ For example:
```yaml
include:
- - component: gitlab.example.com/my-namespace/my-component@1.0
+ - component: gitlab.example.com/my-namespace/my-project@1.0
inputs:
stage: build
```
@@ -410,7 +418,7 @@ For example:
```yaml
include:
# include the component located in the current project from the current SHA
- - component: gitlab.com/$CI_PROJECT_PATH/my-component@$CI_COMMIT_SHA
+ - component: gitlab.com/$CI_PROJECT_PATH/my-project@$CI_COMMIT_SHA
inputs:
stage: build
diff --git a/doc/ci/yaml/inputs.md b/doc/ci/yaml/inputs.md
index 9e084cf0020..cf5040408a2 100644
--- a/doc/ci/yaml/inputs.md
+++ b/doc/ci/yaml/inputs.md
@@ -14,7 +14,8 @@ and subject to change without notice.
## Define input parameters with `spec:inputs`
-> `description` keyword [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/415637) in GitLab 16.5.
+> - `description` keyword [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/415637) in GitLab 16.5.
+> - `options` keyword [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393401) in GitLab 16.6.
Use `spec:inputs` to define input parameters for CI/CD configuration intended to be added
to a pipeline with `include`. Use [`include:inputs`](#set-input-parameter-values-with-includeinputs)
@@ -43,6 +44,8 @@ When using `spec:inputs`:
- Defined inputs are mandatory by default.
- Inputs can be made optional by specifying a `default`. Use `default: null` to have no default value.
+- Inputs can use `options` to specify a list of allowed values for an input. The limit is 50 options per input.
+- If an input uses both `default` and `options`, the default value must be one of the listed options. If not, the pipeline fails with a validation error.
- You can optionally use `description` to give a description to a specific input.
- A string containing an interpolation block must not exceed 1 MB.
- The string inside an interpolation block must not exceed 1 KB.
@@ -55,6 +58,7 @@ spec:
website:
user:
default: 'test-user'
+ options: ['test-user', 'admin-user']
flags:
default: null
description: 'Sample description of the `flags` input detail.'
@@ -66,7 +70,7 @@ spec:
In this example:
- `website` is mandatory and must be defined.
-- `user` is optional. If not defined, the value is `test-user`.
+- `user` is optional. If not defined, the value is `test-user`, which is one of the values specified in `options`.
- `flags` is optional. If not defined, it has no value. The optional description should give details about the input.
## Set input parameter values with `include:inputs`
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index cea59bae41b..f24ebacab18 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -140,7 +140,6 @@ are very appreciative of the work done by translators and proofreaders!
- Rıfat Ünalmış (Rifat Unalmis) - [GitLab](https://gitlab.com/runalmis), [Crowdin](https://crowdin.com/profile/runalmis)
- İsmail Arılık - [GitLab](https://gitlab.com/ismailarilik), [Crowdin](https://crowdin.com/profile/ismailarilik)
- Ukrainian
- - Volodymyr Sobotovych - [GitLab](https://gitlab.com/wheleph), [Crowdin](https://crowdin.com/profile/wheleph)
- Andrew Vityuk - [GitLab](https://gitlab.com/3_1_3_u), [Crowdin](https://crowdin.com/profile/andruwa13)
- Welsh
- Delyth Prys - [GitLab](https://gitlab.com/Delyth), [Crowdin](https://crowdin.com/profile/DelythPrys)
diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md
index 05d00323e2a..3d6e2b9af5f 100644
--- a/doc/subscriptions/self_managed/index.md
+++ b/doc/subscriptions/self_managed/index.md
@@ -34,7 +34,8 @@ Prorated charges are not possible without a quarterly usage report.
## View user totals
-You can view users for your license and determine if you've gone over your subscription.
+View the amount of users in your instance to determine if they exceed the amount
+paid for in your subscription.
1. On the left sidebar, select **Search or go to**.
1. Select **Admin Area**.
@@ -44,17 +45,19 @@ The lists of users are displayed.
### Billable users
-A _billable user_ counts against the number of subscription seats. Every user is considered a
-billable user, with the following exceptions:
-
-- [Deactivated users](../../administration/moderate_users.md#deactivate-a-user) and
- [blocked users](../../administration/moderate_users.md#block-a-user) don't count as billable users in the current subscription. When they are either deactivated or blocked they release a _billable user_ seat. However, they may
- count toward overages in the subscribed seat count.
-- Users who are [pending approval](../../administration/moderate_users.md#users-pending-approval).
-- Users with only the [Minimal Access role](../../user/permissions.md#users-with-minimal-access) on self-managed Ultimate subscriptions or any GitLab.com subscriptions.
-- Users with only the [Guest or Minimal Access roles on an Ultimate subscription](#free-guest-users).
-- Users without project or group memberships on an Ultimate subscription.
-- GitLab-created service accounts:
+Billable users count toward the number of subscription seats purchased in your subscription.
+
+A user is not counted as a billable user if:
+
+- They are [deactivated](../../administration/moderate_users.md#deactivate-a-user) or
+ [blocked](../../administration/moderate_users.md#block-a-user).
+ If the user occupied a seat prior to being deactivated or blocked,
+ the user is included in the number of [maximum users](#maximum-users).
+- They are [pending approval](../../administration/moderate_users.md#users-pending-approval).
+- They have only the [Minimal Access role](../../user/permissions.md#users-with-minimal-access) on self-managed Ultimate subscriptions or any GitLab.com subscriptions.
+- They have the [Guest or Minimal Access roles on an Ultimate subscription](#free-guest-users).
+- They have project or group memberships on an Ultimate subscription.
+- The account is a GitLab-created service account:
- [Ghost User](../../user/profile/account/delete_account.md#associated-records).
- Bots such as:
- [Support Bot](../../user/project/service_desk/configure.md#support-bot-user).
@@ -62,7 +65,7 @@ billable user, with the following exceptions:
- [Bot users for groups](../../user/group/settings/group_access_tokens.md#bot-users-for-groups).
- Other [internal users](../../development/internal_users.md#internal-users).
-**Billable users** as reported in the `/admin` section is updated once per day.
+The amount of **Billable users** is reported once a day in the Admin Area.
### Maximum users
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index c36203ee492..8c141559cd2 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -673,6 +673,33 @@ If you do access the internal container registry API and use the original tag de
<div class="deprecation breaking-change" data-milestone="17.0">
+### Legacy Geo Prometheus metrics
+
+<div class="deprecation-notes">
+- Announced in GitLab <span class="milestone">16.6</span>
+- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/430192).
+</div>
+
+Following the migration of projects to the [Geo self-service framework](https://docs.gitlab.com/ee/development/geo/framework.html) we have deprecated a number of [Prometheus](https://docs.gitlab.com/ee/administration/monitoring/prometheus/) metrics.
+The following Geo-related Prometheus metrics are deprecated and will be removed in 17.0.
+The table below lists the deprecated metrics and their respective replacements. The replacements are available in GitLab 16.3.0 and later.
+
+| Deprecated metric | Replacement metric |
+| ---------------------------------------- | ---------------------------------------------- |
+| `geo_repositories_synced` | `geo_project_repositories_synced` |
+| `geo_repositories_failed` | `geo_project_repositories_failed` |
+| `geo_repositories_checksummed` | `geo_project_repositories_checksummed` |
+| `geo_repositories_checksum_failed` | `geo_project_repositories_checksum_failed` |
+| `geo_repositories_verified` | `geo_project_repositories_verified` |
+| `geo_repositories_verification_failed` | `geo_project_repositories_verification_failed` |
+| `geo_repositories_checksum_mismatch` | None available |
+| `geo_repositories_retrying_verification` | None available |
+
+</div>
+
+<div class="deprecation breaking-change" data-milestone="17.0">
+
### Maintainer role providing the ability to change Package settings using GraphQL API
<div class="deprecation-notes">
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index ab8cba48dc4..1a4fa9df305 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -202,7 +202,7 @@ A table displays the member's:
NOTE:
The display of group members' **Source** might be inconsistent.
-For more information, see [issue 414557](https://gitlab.com/gitlab-org/gitlab/-/issues/414557).
+For more information, see [issue 23020](https://gitlab.com/gitlab-org/gitlab/-/issues/23020).
## Filter and sort members in a group
diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md
index 6f5f4ed4db8..e9528a11a3e 100644
--- a/doc/user/product_analytics/index.md
+++ b/doc/user/product_analytics/index.md
@@ -101,7 +101,19 @@ Prerequisites:
1. Expand **Configure** and enter the configuration values.
1. Select **Save changes**.
-## Instrument a GitLab project
+## Onboard a GitLab project
+
+Onboarding a GitLab project means preparing it to receive events that are used for product analytics.
+
+To onboard a project:
+
+1. On the left sidebar, select **Search or go to** and find your project.
+1. Select **Analyze > Analytics dashboards**.
+1. Under **Product analytics**, select **Set up**.
+1. Select **Set up product analytics**.
+Your instance is being created, and the project onboarded.
+
+## Instrument your application
To instrument code to collect data, use one or more of the existing SDKs:
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index 1fe6e3523b3..6df33a4fb06 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -190,7 +190,7 @@ To add a group to a project:
1. Select **Invite**.
The members of the group are not displayed on the **Members** tab.
-Private groups are masked from unauthenticated users.
+Private groups are masked from unauthorized users.
The **Members** tab shows:
- Members who are directly assigned to the project.
diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md
index 4474cb55929..94dbb922c0b 100644
--- a/doc/user/project/members/share_project_with_groups.md
+++ b/doc/user/project/members/share_project_with_groups.md
@@ -77,7 +77,7 @@ In addition:
- On the group's page, the project is listed on the **Shared projects** tab.
- On the project's **Members** page, the group is listed on the **Groups** tab.
- From [GitLab 16.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134623),
- the invited group's name and membership source will be hidden unless:
+ the invited group's name and membership source will be masked unless:
- the group is public, or
- the current user is a member of the group, or
- the current user is a member of the project.
diff --git a/gems/gitlab-http/Gemfile.lock b/gems/gitlab-http/Gemfile.lock
index 1023e12efd6..1f4910d1d57 100644
--- a/gems/gitlab-http/Gemfile.lock
+++ b/gems/gitlab-http/Gemfile.lock
@@ -20,6 +20,7 @@ PATH
specs:
gitlab-http (0.1.0)
activesupport (~> 7)
+ concurrent-ruby (~> 1.2)
httparty (~> 0.21.0)
ipaddress (~> 0.8.3)
nokogiri (~> 1.15.4)
diff --git a/gems/gitlab-http/README.md b/gems/gitlab-http/README.md
index 13ff330bb19..e717afbdb2c 100644
--- a/gems/gitlab-http/README.md
+++ b/gems/gitlab-http/README.md
@@ -24,16 +24,27 @@ end
### Actions
-Basic examples;
+Basic examples:
```ruby
Gitlab::HTTP_V2.post(uri, body: body)
Gitlab::HTTP_V2.try_get(uri, params)
-response = Gitlab::HTTP_V2.head(project_url, verify: true)
+response = Gitlab::HTTP_V2.head(project_url, verify: true) # returns an HTTParty::Response object
-Gitlab::HTTP_V2.post(path, base_uri: base_uri, **params)
+Gitlab::HTTP_V2.post(path, base_uri: base_uri, **params) # returns an HTTParty::Response object
+```
+
+Async usage examples:
+
+```ruby
+lazy_response = Gitlab::HTTP_V2.get(location, async: true)
+
+lazy_response.execute # starts the request and returns the same LazyResponse object
+lazy_response.wait # waits for the request to finish and returns the same LazyResponse object
+
+response = lazy_response.value # returns an HTTParty::Response object
```
## Development
diff --git a/gems/gitlab-http/gitlab-http.gemspec b/gems/gitlab-http/gitlab-http.gemspec
index 6146ba7f78b..0033f17447b 100644
--- a/gems/gitlab-http/gitlab-http.gemspec
+++ b/gems/gitlab-http/gitlab-http.gemspec
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]
spec.add_runtime_dependency 'activesupport', '~> 7'
+ spec.add_runtime_dependency 'concurrent-ruby', '~> 1.2'
spec.add_runtime_dependency 'httparty', '~> 0.21.0'
spec.add_runtime_dependency 'ipaddress', '~> 0.8.3'
spec.add_runtime_dependency 'nokogiri', '~> 1.15.4'
diff --git a/gems/gitlab-http/lib/gitlab/http_v2/client.rb b/gems/gitlab-http/lib/gitlab/http_v2/client.rb
index c10197e0385..52c9ab897f5 100644
--- a/gems/gitlab-http/lib/gitlab/http_v2/client.rb
+++ b/gems/gitlab-http/lib/gitlab/http_v2/client.rb
@@ -4,7 +4,8 @@ require 'httparty'
require 'net/http'
require 'active_support/all'
require_relative 'new_connection_adapter'
-require_relative "exceptions"
+require_relative 'exceptions'
+require_relative 'lazy_response'
module Gitlab
module HTTP_V2
@@ -45,9 +46,12 @@ module Gitlab
# TODO: This overwrites a method implemented by `HTTPParty`
# The calls to `get/...` will call this method instead of `httparty_perform_request`
def perform_request(http_method, path, options, &block)
+ raise_if_options_are_invalid(options)
raise_if_blocked_by_silent_mode(http_method) if options.delete(:silent_mode_enabled)
log_info = options.delete(:extra_log_info)
+ async = options.delete(:async)
+
options_with_timeouts =
if !options.has_key?(:timeout)
options.with_defaults(DEFAULT_TIMEOUT_OPTIONS)
@@ -57,29 +61,57 @@ module Gitlab
if options[:stream_body]
httparty_perform_request(http_method, path, options_with_timeouts, &block)
+ elsif async
+ async_perform_request(http_method, path, options, options_with_timeouts, log_info, &block)
else
- begin
- start_time = nil
- read_total_timeout = options.fetch(:timeout, DEFAULT_READ_TOTAL_TIMEOUT)
-
- httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
- start_time ||= system_monotonic_time
- elapsed = system_monotonic_time - start_time
-
- raise ReadTotalTimeout, "Request timed out after #{elapsed} seconds" if elapsed > read_total_timeout
-
- yield fragment if block
- end
- rescue HTTParty::RedirectionTooDeep
- raise RedirectionTooDeep
- rescue *HTTP_ERRORS => e
- extra_info = log_info || {}
- extra_info = log_info.call(e, path, options) if log_info.respond_to?(:call)
- configuration.log_exception(e, extra_info)
-
- raise e
+ sync_perform_request(http_method, path, options, options_with_timeouts, log_info, &block)
+ end
+ end
+
+ def async_perform_request(http_method, path, options, options_with_timeouts, log_info, &block)
+ start_time = nil
+ read_total_timeout = options.fetch(:timeout, DEFAULT_READ_TOTAL_TIMEOUT)
+
+ promise = Concurrent::Promise.new do
+ httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
+ start_time ||= system_monotonic_time
+ elapsed = system_monotonic_time - start_time
+
+ raise ReadTotalTimeout, "Request timed out after #{elapsed} seconds" if elapsed > read_total_timeout
+
+ yield fragment if block
end
end
+
+ LazyResponse.new(promise, path, options, log_info)
+ end
+
+ def sync_perform_request(http_method, path, options, options_with_timeouts, log_info, &block)
+ start_time = nil
+ read_total_timeout = options.fetch(:timeout, DEFAULT_READ_TOTAL_TIMEOUT)
+
+ httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
+ start_time ||= system_monotonic_time
+ elapsed = system_monotonic_time - start_time
+
+ raise ReadTotalTimeout, "Request timed out after #{elapsed} seconds" if elapsed > read_total_timeout
+
+ yield fragment if block
+ end
+ rescue HTTParty::RedirectionTooDeep
+ raise RedirectionTooDeep
+ rescue *HTTP_ERRORS => e
+ extra_info = log_info || {}
+ extra_info = log_info.call(e, path, options) if log_info.respond_to?(:call)
+ configuration.log_exception(e, extra_info)
+
+ raise e
+ end
+
+ def raise_if_options_are_invalid(options)
+ return unless options[:async] && (options[:stream_body] || options[:silent_mode_enabled])
+
+ raise ArgumentError, '`async` cannot be used with `stream_body` or `silent_mode_enabled`'
end
def raise_if_blocked_by_silent_mode(http_method)
diff --git a/gems/gitlab-http/lib/gitlab/http_v2/lazy_response.rb b/gems/gitlab-http/lib/gitlab/http_v2/lazy_response.rb
new file mode 100644
index 00000000000..65d1ab96644
--- /dev/null
+++ b/gems/gitlab-http/lib/gitlab/http_v2/lazy_response.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module HTTP_V2
+ class LazyResponse
+ NotExecutedError = Class.new(StandardError)
+
+ attr_reader :promise
+
+ delegate :state, to: :promise
+
+ def initialize(promise, path, options, log_info)
+ @promise = promise
+ @path = path
+ @options = options
+ @log_info = log_info
+ end
+
+ def execute
+ @promise.execute
+ self
+ end
+
+ def wait
+ @promise.wait
+ self
+ end
+
+ def value
+ raise NotExecutedError, '`execute` must be called before `value`' if @promise.unscheduled?
+
+ wait # wait for the promise to be completed
+
+ raise @promise.reason if @promise.rejected?
+
+ @promise.value
+ rescue HTTParty::RedirectionTooDeep
+ raise HTTP_V2::RedirectionTooDeep
+ rescue *HTTP_V2::HTTP_ERRORS => e
+ extra_info = @log_info || {}
+ extra_info = @log_info.call(e, @path, @options) if @log_info.respond_to?(:call)
+ Gitlab::HTTP_V2.configuration.log_exception(e, extra_info)
+
+ raise e
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-http/spec/gitlab/http_v2_spec.rb b/gems/gitlab-http/spec/gitlab/http_v2_spec.rb
index bfa1dcd2633..3151761d375 100644
--- a/gems/gitlab-http/spec/gitlab/http_v2_spec.rb
+++ b/gems/gitlab-http/spec/gitlab/http_v2_spec.rb
@@ -450,4 +450,101 @@ RSpec.describe Gitlab::HTTP_V2, feature_category: :shared do
end
end
end
+
+ context 'when options[:async] is true' do
+ context 'when it is a valid request' do
+ before do
+ stub_full_request('http://example.org', method: :any).to_return(status: 200, body: 'hello world')
+ end
+
+ it 'returns a LazyResponse' do
+ result = described_class.get('http://example.org', async: true)
+
+ expect(result).to be_a(Gitlab::HTTP_V2::LazyResponse)
+ expect(result.state).to eq(:unscheduled)
+
+ expect(result.execute).to be_a(Gitlab::HTTP_V2::LazyResponse)
+ expect(result.wait).to be_a(Gitlab::HTTP_V2::LazyResponse)
+
+ expect(result.value).to be_a(HTTParty::Response)
+ expect(result.value.body).to eq('hello world')
+ end
+ end
+
+ context 'when the URL is denied' do
+ let(:url) { 'http://localhost:3003' }
+ let(:error_class) { Gitlab::HTTP_V2::BlockedUrlError }
+ let(:opts) { {} }
+
+ let(:result) do
+ described_class.get(url, allow_local_requests: false, async: true, **opts)
+ end
+
+ it 'returns a LazyResponse with error value' do
+ expect(result).to be_a(Gitlab::HTTP_V2::LazyResponse)
+
+ expect { result.execute.value }.to raise_error(error_class)
+ end
+
+ it 'logs the exception' do
+ expect(described_class.configuration)
+ .to receive(:log_exception)
+ .with(instance_of(error_class), {})
+
+ expect { result.execute.value }.to raise_error(error_class)
+ end
+
+ context 'with extra_log_info as hash' do
+ let(:opts) { { extra_log_info: { a: :b } } }
+
+ it 'handles the request' do
+ expect(described_class.configuration)
+ .to receive(:log_exception)
+ .with(instance_of(error_class), { a: :b })
+
+ expect { result.execute.value }.to raise_error(error_class)
+ end
+ end
+
+ context 'with extra_log_info as proc' do
+ let(:extra_log_info) do
+ proc do |error, url, options|
+ { klass: error.class, url: url, options: options }
+ end
+ end
+
+ let(:opts) { { extra_log_info: extra_log_info } }
+
+ it 'handles the request' do
+ expect(described_class.configuration)
+ .to receive(:log_exception)
+ .with(instance_of(error_class), { url: url, klass: error_class, options: { allow_local_requests: false } })
+
+ expect { result.execute.value }.to raise_error(error_class)
+ end
+ end
+ end
+ end
+
+ context 'when options[:async] and options[:stream_body] are true' do
+ before do
+ stub_full_request('http://example.org', method: :any)
+ end
+
+ it 'raises an ArgumentError' do
+ expect { described_class.get('http://example.org', async: true, stream_body: true) }
+ .to raise_error(ArgumentError, '`async` cannot be used with `stream_body` or `silent_mode_enabled`')
+ end
+ end
+
+ context 'when options[:async] and options[:silent_mode_enabled] are true' do
+ before do
+ stub_full_request('http://example.org', method: :any)
+ end
+
+ it 'raises an ArgumentError' do
+ expect { described_class.get('http://example.org', async: true, silent_mode_enabled: true) }
+ .to raise_error(ArgumentError, '`async` cannot be used with `stream_body` or `silent_mode_enabled`')
+ end
+ end
end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 9120421fadf..7ad4ecd88b1 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -204,6 +204,7 @@ module API
optional :floc_enabled, type: Grape::API::Boolean, desc: 'Enable FloC (Federated Learning of Cohorts)'
optional :user_deactivation_emails_enabled, type: Boolean, desc: 'Send emails to users upon account deactivation'
optional :suggest_pipeline_enabled, type: Boolean, desc: 'Enable pipeline suggestion banner'
+ optional :enable_artifact_external_redirect_warning_page, type: Boolean, desc: 'Show the external redirect page that warns you about user-generated content in GitLab Pages'
optional :users_get_by_id_limit, type: Integer, desc: "Maximum number of calls to the /users/:id API per 10 minutes per user. Set to 0 for unlimited requests."
optional :runner_token_expiration_interval, type: Integer, desc: 'Token expiration interval for shared runners, in seconds'
optional :group_runner_token_expiration_interval, type: Integer, desc: 'Token expiration interval for group runners, in seconds'
diff --git a/lib/gitlab/background_migration/batched_migration_job.rb b/lib/gitlab/background_migration/batched_migration_job.rb
index 952e6d01f1a..9e9fc9b98b7 100644
--- a/lib/gitlab/background_migration/batched_migration_job.rb
+++ b/lib/gitlab/background_migration/batched_migration_job.rb
@@ -130,7 +130,7 @@ module Gitlab
end
def base_relation
- define_batchable_model(batch_table, connection: connection)
+ define_batchable_model(batch_table, connection: connection, primary_key: batch_column)
.where(batch_column => start_id..end_id)
end
diff --git a/lib/gitlab/database/dynamic_model_helpers.rb b/lib/gitlab/database/dynamic_model_helpers.rb
index 83edf77f37e..18854530278 100644
--- a/lib/gitlab/database/dynamic_model_helpers.rb
+++ b/lib/gitlab/database/dynamic_model_helpers.rb
@@ -5,7 +5,7 @@ module Gitlab
module DynamicModelHelpers
BATCH_SIZE = 1_000
- def define_batchable_model(table_name, connection:)
+ def define_batchable_model(table_name, connection:, primary_key: nil)
klass = Class.new(ActiveRecord::Base) do
include EachBatch
@@ -13,6 +13,7 @@ module Gitlab
self.inheritance_column = :_type_disabled
end
+ klass.primary_key = primary_key if connection.primary_keys(table_name).length > 1
klass.connection = connection
klass
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index efcceafda90..a57bce789c7 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -18,8 +18,8 @@ module Gitlab
include AsyncConstraints::MigrationHelpers
include WraparoundVacuumHelpers
- def define_batchable_model(table_name, connection: self.connection)
- super(table_name, connection: connection)
+ def define_batchable_model(table_name, connection: self.connection, primary_key: nil)
+ super(table_name, connection: connection, primary_key: primary_key)
end
def each_batch(table_name, connection: self.connection, **kwargs)
@@ -821,6 +821,7 @@ module Gitlab
primary_key: :id,
batch_size: 20_000,
sub_batch_size: 1000,
+ pause_ms: 100,
interval: 2.minutes
)
@@ -848,6 +849,7 @@ module Gitlab
conversions.keys,
conversions.values,
job_interval: interval,
+ pause_ms: pause_ms,
batch_size: batch_size,
sub_batch_size: sub_batch_size)
end
diff --git a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
index 64cde273a59..3d4ac113bf6 100644
--- a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
+++ b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
@@ -72,6 +72,7 @@ module Gitlab
batch_max_value: nil,
batch_class_name: BATCH_CLASS_NAME,
batch_size: BATCH_SIZE,
+ pause_ms: 100,
max_batch_size: nil,
sub_batch_size: SUB_BATCH_SIZE,
gitlab_schema: nil
@@ -105,6 +106,7 @@ module Gitlab
column_name: batch_column_name,
job_arguments: job_arguments,
interval: job_interval,
+ pause_ms: pause_ms,
min_value: batch_min_value,
max_value: batch_max_value,
batch_class_name: batch_class_name,
diff --git a/lib/gitlab/github_import/attachments_downloader.rb b/lib/gitlab/github_import/attachments_downloader.rb
index 4db55a6aabb..df9c6c8342d 100644
--- a/lib/gitlab/github_import/attachments_downloader.rb
+++ b/lib/gitlab/github_import/attachments_downloader.rb
@@ -29,8 +29,8 @@ module Gitlab
validate_content_length
validate_filepath
- redirection_url = get_assets_download_redirection_url
- file = download_from(redirection_url)
+ download_url = get_assets_download_redirection_url
+ file = download_from(download_url)
validate_symlink
file
@@ -60,16 +60,16 @@ module Gitlab
options[:follow_redirects] = false
response = Gitlab::HTTP.perform_request(Net::HTTP::Get, file_url, options)
- raise_error("expected a redirect response, got #{response.code}") unless response.redirection?
- redirection_url = response.headers[:location]
- filename = URI.parse(redirection_url).path
+ download_url = if response.redirection?
+ response.headers[:location]
+ else
+ file_url
+ end
- unless Gitlab::GithubImport::Markdown::Attachment::MEDIA_TYPES.any? { |type| filename.ends_with?(type) }
- raise UnsupportedAttachmentError
- end
+ file_type_valid?(URI.parse(download_url).path)
- redirection_url
+ download_url
end
def github_assets_url_regex
@@ -89,6 +89,12 @@ module Gitlab
File.join(dir, filename)
end
end
+
+ def file_type_valid?(file_url)
+ return if Gitlab::GithubImport::Markdown::Attachment::MEDIA_TYPES.any? { |type| file_url.ends_with?(type) }
+
+ raise UnsupportedAttachmentError
+ end
end
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index b2027791e9d..5f819f060e4 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -174,7 +174,6 @@ module Gitlab
prometheus_enabled: alt_usage_data(fallback: nil) { Gitlab::Prometheus::Internal.prometheus_enabled? },
prometheus_metrics_enabled: alt_usage_data(fallback: nil) { Gitlab::Metrics.prometheus_metrics_enabled? },
reply_by_email_enabled: alt_usage_data(fallback: nil) { Gitlab::Email::IncomingEmail.enabled? },
- web_ide_clientside_preview_enabled: alt_usage_data(fallback: nil) { false },
signup_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.allow_signup? },
grafana_link_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.grafana_enabled? },
gitpod_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.gitpod_enabled? }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 2185dc9b941..afdc179ece9 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3537,6 +3537,9 @@ msgstr ""
msgid "AdminSettings|Enable smartcn custom analyzer: Search"
msgstr ""
+msgid "AdminSettings|Enable the external redirect warning page for job artifacts"
+msgstr ""
+
msgid "AdminSettings|Enabled"
msgstr ""
@@ -3738,6 +3741,9 @@ msgstr ""
msgid "AdminSettings|Setting must be greater than 0."
msgstr ""
+msgid "AdminSettings|Show a redirect page that warns you about user-generated content in GitLab Pages."
+msgstr ""
+
msgid "AdminSettings|Size and domain settings for Pages static sites."
msgstr ""
@@ -4797,6 +4803,9 @@ msgstr ""
msgid "All environments"
msgstr ""
+msgid "All groups"
+msgstr ""
+
msgid "All groups and projects"
msgstr ""
@@ -5190,6 +5199,9 @@ msgstr ""
msgid "An error occurred while fetching this tab."
msgstr ""
+msgid "An error occurred while fetching. Please try again."
+msgstr ""
+
msgid "An error occurred while getting files for - %{branchId}"
msgstr ""
@@ -21929,6 +21941,9 @@ msgstr ""
msgid "GitLab group: %{source_link}"
msgstr ""
+msgid "GitLab has redesigned the left sidebar to address customer feedback. View details in %{blog_link_start}this blog post%{link_end}. Here's how to %{issues_link_start}file an issue%{link_end} with the GitLab product team."
+msgstr ""
+
msgid "GitLab informs you if a new version is available. %{link_start}What information does GitLab Inc. collect?%{link_end}"
msgstr ""
@@ -36282,6 +36297,9 @@ msgstr ""
msgid "ProductAnalytics|All Pages"
msgstr ""
+msgid "ProductAnalytics|All Returning Users Compared"
+msgstr ""
+
msgid "ProductAnalytics|All Sessions Compared"
msgstr ""
@@ -36309,6 +36327,9 @@ msgstr ""
msgid "ProductAnalytics|Compares all events against each other"
msgstr ""
+msgid "ProductAnalytics|Compares all returning users against each other"
+msgstr ""
+
msgid "ProductAnalytics|Compares all user sessions against each other"
msgstr ""
@@ -36351,7 +36372,7 @@ msgstr ""
msgid "ProductAnalytics|How many sessions a user has"
msgstr ""
-msgid "ProductAnalytics|How often sessions are repeated"
+msgid "ProductAnalytics|How often users returned compared to all sessions"
msgstr ""
msgid "ProductAnalytics|Instrument your application"
@@ -36372,6 +36393,9 @@ msgstr ""
msgid "ProductAnalytics|Measure all or specific Page Views"
msgstr ""
+msgid "ProductAnalytics|Measure all returning users"
+msgstr ""
+
msgid "ProductAnalytics|Measure all sessions"
msgstr ""
@@ -36387,10 +36411,13 @@ msgstr ""
msgid "ProductAnalytics|Page Views"
msgstr ""
+msgid "ProductAnalytics|Percentage of Users Returning"
+msgstr ""
+
msgid "ProductAnalytics|Product analytics onboarding"
msgstr ""
-msgid "ProductAnalytics|Repeat Visit Percentage"
+msgid "ProductAnalytics|Returning Users"
msgstr ""
msgid "ProductAnalytics|SDK application ID"
@@ -37071,6 +37098,9 @@ msgstr ""
msgid "Project export started. A download link will be sent by email and made available on this page."
msgstr ""
+msgid "Project groups"
+msgstr ""
+
msgid "Project has too many %{label_for_message} to search"
msgstr ""
@@ -43297,6 +43327,9 @@ msgstr ""
msgid "SecurityOrchestration|You already have the maximum %{maximumAllowed} %{policyType} policies."
msgstr ""
+msgid "SecurityOrchestration|You can't unprotect this branch because its protection is enforced by one or more %{security_policies_link_start}security policies%{security_policies_link_end}. %{learn_more_link_start}Learn more%{learn_more_link_end}."
+msgstr ""
+
msgid "SecurityOrchestration|You don't have any security policies yet"
msgstr ""
@@ -54361,6 +54394,9 @@ msgstr ""
msgid "WorkItem|Due date"
msgstr ""
+msgid "WorkItem|Epic"
+msgstr ""
+
msgid "WorkItem|Existing task"
msgstr ""
diff --git a/package.json b/package.json
index 83ffbb04323..ce3c2819411 100644
--- a/package.json
+++ b/package.json
@@ -254,7 +254,7 @@
"chalk": "^2.4.1",
"commander": "^2.20.3",
"custom-jquery-matchers": "^2.1.0",
- "eslint": "8.52.0",
+ "eslint": "8.53.0",
"eslint-import-resolver-jest": "3.0.2",
"eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-import": "^2.29.0",
diff --git a/qa/Gemfile b/qa/Gemfile
index 06de6004742..3b0e8fa888c 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -2,7 +2,7 @@
source 'https://rubygems.org'
-gem 'gitlab-qa', '~> 12', '>= 12.4.1', require: 'gitlab/qa'
+gem 'gitlab-qa', '~> 12', '>= 12.5.0', require: 'gitlab/qa'
gem 'gitlab_quality-test_tooling', '~> 0.9.3', require: false
gem 'gitlab-utils', path: '../gems/gitlab-utils'
gem 'activesupport', '~> 7.0.8' # This should stay in sync with the root's Gemfile
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 81602f9ecce..a1563a7351e 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -121,7 +121,7 @@ GEM
gitlab (4.19.0)
httparty (~> 0.20)
terminal-table (>= 1.5.1)
- gitlab-qa (12.4.1)
+ gitlab-qa (12.5.0)
activesupport (>= 6.1, < 7.1)
gitlab (~> 4.19)
http (~> 5.0)
@@ -351,7 +351,7 @@ DEPENDENCIES
faraday-retry (~> 2.2)
fog-core (= 2.1.0)
fog-google (~> 1.19)
- gitlab-qa (~> 12, >= 12.4.1)
+ gitlab-qa (~> 12, >= 12.5.0)
gitlab-utils!
gitlab_quality-test_tooling (~> 0.9.3)
influxdb-client (~> 2.9)
diff --git a/spec/controllers/groups/settings/applications_controller_spec.rb b/spec/controllers/groups/settings/applications_controller_spec.rb
index c398fd044c2..aa50ef9a92c 100644
--- a/spec/controllers/groups/settings/applications_controller_spec.rb
+++ b/spec/controllers/groups/settings/applications_controller_spec.rb
@@ -23,17 +23,55 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(response).to render_template :index
expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'renders the applications page' do
+ get :index, params: { group_id: group }
+
+ expect(response).to render_template :index
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
end
+ end
- it 'renders a 404' do
- get :index, params: { group_id: group }
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ before do
+ group.send("add_#{role}", user)
+ end
+
+ it 'renders a 404' do
+ get :index, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'renders the applications page' do
+ get :index, params: { group_id: group }
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to render_template :index
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
end
end
end
@@ -44,23 +82,61 @@ RSpec.describe Groups::Settings::ApplicationsController do
group.add_owner(user)
end
- it 'renders the application form' do
+ it 'renders the edit application page' do
get :edit, params: { group_id: group, id: application.id }
expect(response).to render_template :edit
expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'renders the edit application page' do
+ get :edit, params: { group_id: group, id: application.id }
+
+ expect(response).to render_template :edit
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
end
+ end
- it 'renders a 404' do
- get :edit, params: { group_id: group, id: application.id }
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ before do
+ group.send("add_#{role}", user)
+ end
+
+ it 'renders a 404' do
+ get :edit, params: { group_id: group, id: application.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
- expect(response).to have_gitlab_http_status(:not_found)
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'renders the edit application page' do
+ get :edit, params: { group_id: group, id: application.id }
+
+ expect(response).to render_template :edit
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
end
end
end
@@ -121,19 +197,71 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(response).to render_template :index
end
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'creates the application' do
+ create_params = attributes_for(:application, trusted: false, confidential: false, scopes: ['api'])
+
+ expect do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
+
+ application = Doorkeeper::Application.last
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
end
+ end
+
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ let(:create_params) { attributes_for(:application, trusted: true, confidential: false, scopes: ['api']) }
+
+ before do
+ group.send("add_#{role}", user)
+ end
+
+ it 'renders a 404' do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
- it 'renders a 404' do
- create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api'])
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'creates the application' do
+ create_params = attributes_for(:application, trusted: false, confidential: false, scopes: ['api'])
- post :create, params: { group_id: group, doorkeeper_application: create_params }
+ expect do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
- expect(response).to have_gitlab_http_status(:not_found)
+ application = Doorkeeper::Application.last
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
+ end
end
end
end
@@ -162,6 +290,26 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(json_response['secret']).not_to be_nil
end
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it { is_expected.to have_gitlab_http_status(:ok) }
+ it { expect { subject }.to change { application.reload.secret } }
+
+ it 'returns the secret in json format' do
+ subject
+
+ expect(json_response['secret']).not_to be_nil
+ end
+ end
+
context 'when renew fails' do
before do
allow_next_found_instance_of(Doorkeeper::Application) do |application|
@@ -174,21 +322,42 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
- end
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ let(:oauth_params) do
+ {
+ group_id: group,
+ id: application.id
+ }
+ end
- let(:oauth_params) do
- {
- group_id: group,
- id: application.id
- }
- end
+ before do
+ group.send("add_#{role}", user)
+ end
- it 'renders a 404' do
- put :renew, params: oauth_params
- expect(response).to have_gitlab_http_status(:not_found)
+ it 'renders a 404' do
+ put :renew, params: oauth_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'returns the secret in json format' do
+ put :renew, params: oauth_params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['secret']).not_to be_nil
+ end
+ end
end
end
end
@@ -230,19 +399,67 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(application).to be_confidential
end
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'updates the application' do
+ doorkeeper_params = { redirect_uri: 'http://example.com/', trusted: true, confidential: false }
+
+ patch :update, params: { group_id: group, id: application.id, doorkeeper_application: doorkeeper_params }
+
+ application.reload
+
+ expect(response).to redirect_to(group_settings_application_path(group, application))
+ expect(application)
+ .to have_attributes(redirect_uri: 'http://example.com/', trusted: false, confidential: false)
+ end
end
+ end
- it 'renders a 404' do
- doorkeeper_params = { redirect_uri: 'http://example.com/', trusted: true, confidential: false }
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ before do
+ group.send("add_#{role}", user)
+ end
- patch :update, params: { group_id: group, id: application.id, doorkeeper_application: doorkeeper_params }
+ it 'renders a 404' do
+ doorkeeper_params = { redirect_uri: 'http://example.com/', trusted: true, confidential: false }
+
+ patch :update, params: { group_id: group, id: application.id, doorkeeper_application: doorkeeper_params }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
- expect(response).to have_gitlab_http_status(:not_found)
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'updates the application' do
+ doorkeeper_params = { redirect_uri: 'http://example.com/', trusted: true, confidential: false }
+
+ patch :update, params: { group_id: group, id: application.id, doorkeeper_application: doorkeeper_params }
+
+ application.reload
+
+ expect(response).to redirect_to(group_settings_application_path(group, application))
+ expect(application)
+ .to have_attributes(redirect_uri: 'http://example.com/', trusted: false, confidential: false)
+ end
+ end
end
end
end
@@ -259,17 +476,55 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(Doorkeeper::Application.exists?(application.id)).to be_falsy
expect(response).to redirect_to(group_settings_applications_url(group))
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'deletes the application' do
+ delete :destroy, params: { group_id: group, id: application.id }
+
+ expect(Doorkeeper::Application.exists?(application.id)).to be_falsy
+ expect(response).to redirect_to(group_settings_applications_url(group))
+ end
end
+ end
- it 'renders a 404' do
- delete :destroy, params: { group_id: group, id: application.id }
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ before do
+ group.send("add_#{role}", user)
+ end
+
+ it 'renders a 404' do
+ delete :destroy, params: { group_id: group, id: application.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
- expect(response).to have_gitlab_http_status(:not_found)
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'deletes the application' do
+ delete :destroy, params: { group_id: group, id: application.id }
+
+ expect(Doorkeeper::Application.exists?(application.id)).to be_falsy
+ expect(response).to redirect_to(group_settings_applications_url(group))
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 31e6d6ae5e6..a0548e847a0 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -324,12 +324,32 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
end
context 'when the file exists' do
- it 'renders the file view' do
- path = 'ci_artifacts.txt'
+ context 'when the external redirect page is enabled' do
+ before do
+ stub_application_setting(enable_artifact_external_redirect_warning_page: true)
+ end
+
+ it 'redirects to the user-generated content warning page' do
+ path = 'ci_artifacts.txt'
- get :file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: path }
+ get :file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: path }
+
+ expect(response).to redirect_to(external_file_project_job_artifacts_path(project, job, path: path))
+ end
+ end
- expect(response).to redirect_to(external_file_project_job_artifacts_path(project, job, path: path))
+ context 'when the external redirect page is disabled' do
+ before do
+ stub_application_setting(enable_artifact_external_redirect_warning_page: false)
+ end
+
+ it 'renders the file view' do
+ path = 'ci_artifacts.txt'
+
+ get :file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: path }
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
end
end
end
diff --git a/spec/features/boards/board_filters_spec.rb b/spec/features/boards/board_filters_spec.rb
index 386bb196504..a6d5d4926ff 100644
--- a/spec/features/boards/board_filters_spec.rb
+++ b/spec/features/boards/board_filters_spec.rb
@@ -25,7 +25,6 @@ RSpec.describe 'Issue board filters', :js, feature_category: :team_planning do
let_it_be(:board) { create(:board, project: project) }
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 69019563e73..48b978f7245 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -34,7 +34,6 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
context 'signed in user' do
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
project.add_maintainer(user2)
@@ -518,7 +517,6 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
context 'signed out user' do
before do
- stub_feature_flags(apollo_boards: false)
visit project_board_path(project, board)
wait_for_requests
end
@@ -540,7 +538,6 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
let_it_be(:user_guest, reload: true) { create(:user) }
before do
- stub_feature_flags(apollo_boards: false)
project.add_guest(user_guest)
sign_in(user_guest)
visit project_board_path(project, board)
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index dd63fd8b80e..625a8ddad84 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -15,7 +15,6 @@ RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do
let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) }
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 8f9c197e6ba..1e44e1d35f9 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -13,10 +13,6 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
let(:board_list_header) { first('[data-testid="board-list-header"]') }
let(:project_select_dropdown) { find_by_testid('project-select-dropdown') }
- before do
- stub_feature_flags(apollo_boards: false)
- end
-
context 'authorized user' do
before do
project.add_maintainer(user)
diff --git a/spec/features/boards/reload_boards_on_browser_back_spec.rb b/spec/features/boards/reload_boards_on_browser_back_spec.rb
index 036daee7655..0ca680c5ed5 100644
--- a/spec/features/boards/reload_boards_on_browser_back_spec.rb
+++ b/spec/features/boards/reload_boards_on_browser_back_spec.rb
@@ -9,8 +9,6 @@ RSpec.describe 'Ensure Boards do not show stale data on browser back', :js, feat
context 'authorized user' do
before do
- stub_feature_flags(apollo_boards: false)
-
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
index 68c2b2587e7..da3dd6ba071 100644
--- a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
+++ b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
@@ -14,8 +14,6 @@ RSpec.describe 'Issue boards sidebar labels select', :js, feature_category: :tea
let_it_be(:group_board) { create(:board, group: group) }
before do
- stub_feature_flags(apollo_boards: false)
-
load_board group_board_path(group, group_board)
end
diff --git a/spec/features/boards/sidebar_labels_spec.rb b/spec/features/boards/sidebar_labels_spec.rb
index 460d0d232b3..0560cbbfae7 100644
--- a/spec/features/boards/sidebar_labels_spec.rb
+++ b/spec/features/boards/sidebar_labels_spec.rb
@@ -20,7 +20,6 @@ RSpec.describe 'Project issue boards sidebar labels', :js, feature_category: :te
let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 71cc9a28575..893f1c246a0 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -15,7 +15,6 @@ RSpec.describe 'Project issue boards sidebar', :js, feature_category: :team_plan
let_it_be(:issue, reload: true) { create(:issue, project: project, relative_position: 1) }
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/user_adds_lists_to_board_spec.rb b/spec/features/boards/user_adds_lists_to_board_spec.rb
index cc2afca7657..d202c2a1f7d 100644
--- a/spec/features/boards/user_adds_lists_to_board_spec.rb
+++ b/spec/features/boards/user_adds_lists_to_board_spec.rb
@@ -29,7 +29,6 @@ RSpec.describe 'User adds lists', :js, feature_category: :team_planning do
with_them do
before do
- stub_feature_flags(apollo_boards: false)
sign_in(user)
set_cookie('sidebar_collapsed', 'true')
diff --git a/spec/features/boards/user_visits_board_spec.rb b/spec/features/boards/user_visits_board_spec.rb
index 4741f58d883..cf8709b3a76 100644
--- a/spec/features/boards/user_visits_board_spec.rb
+++ b/spec/features/boards/user_visits_board_spec.rb
@@ -44,7 +44,6 @@ RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning
with_them do
before do
- stub_feature_flags(apollo_boards: false)
visit board_path
wait_for_requests
@@ -60,7 +59,6 @@ RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning
end
context "project boards" do
- stub_feature_flags(apollo_boards: false)
let_it_be(:board) { create_default(:board, project: project) }
let(:board_path) { project_boards_path(project, params) }
@@ -69,7 +67,6 @@ RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning
end
context "group boards" do
- stub_feature_flags(apollo_boards: false)
let_it_be(:board) { create_default(:board, group: group) }
let(:board_path) { group_boards_path(group, params) }
diff --git a/spec/features/groups/board_sidebar_spec.rb b/spec/features/groups/board_sidebar_spec.rb
index 6a1b7d20a25..3fe520ea2ea 100644
--- a/spec/features/groups/board_sidebar_spec.rb
+++ b/spec/features/groups/board_sidebar_spec.rb
@@ -19,7 +19,6 @@ RSpec.describe 'Group Issue Boards', :js, feature_category: :groups_and_projects
let(:card) { find('.board:nth-child(1)').first('.board-card') }
before do
- stub_feature_flags(apollo_boards: false)
sign_in(user)
visit group_board_path(group, board)
diff --git a/spec/features/groups/board_spec.rb b/spec/features/groups/board_spec.rb
index c2d6b80b4c0..e6dc6055e27 100644
--- a/spec/features/groups/board_spec.rb
+++ b/spec/features/groups/board_spec.rb
@@ -14,8 +14,6 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do
let_it_be(:project) { create(:project_empty_repo, group: group) }
before do
- stub_feature_flags(apollo_boards: false)
-
group.add_maintainer(user)
sign_in(user)
@@ -61,8 +59,6 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do
let_it_be(:issue2) { create(:issue, title: 'issue2', project: project2) }
before do
- stub_feature_flags(apollo_boards: false)
-
project1.add_guest(user)
project2.add_reporter(user)
diff --git a/spec/features/nav/new_nav_for_everyone_callout_spec.rb b/spec/features/nav/new_nav_for_everyone_callout_spec.rb
new file mode 100644
index 00000000000..ad0b57298d7
--- /dev/null
+++ b/spec/features/nav/new_nav_for_everyone_callout_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'new navigation for everyone callout', :js, feature_category: :navigation do
+ let_it_be(:callout_title) { _('GitLab has redesigned the left sidebar to address customer feedback') }
+
+ before do
+ sign_in(user)
+ visit root_path
+ end
+
+ context 'with new navigation previously toggled on' do
+ let_it_be(:user) { create(:user, use_new_navigation: true) }
+
+ it 'does not show the callout' do
+ expect(page).to have_css('[data-testid="super-sidebar"]')
+ expect(page).not_to have_content callout_title
+ end
+ end
+
+ context 'with new navigation previously toggled off' do
+ let_it_be(:user) { create(:user, use_new_navigation: false) }
+
+ it 'shows a callout about the new navigation now being active for everyone' do
+ expect(page).to have_css('[data-testid="super-sidebar"]')
+ expect(page).to have_content callout_title
+ end
+
+ context 'when user dismisses callout' do
+ it 'hides callout' do
+ expect(page).to have_content callout_title
+
+ page.within(find('[data-feature-id="new_nav_for_everyone_callout"]')) do
+ find_by_testid('close-icon').click
+ end
+
+ wait_for_requests
+
+ visit root_path
+
+ expect(page).not_to have_content callout_title
+ end
+ end
+ end
+
+ context 'with new navigation never toggled on or off' do
+ let_it_be(:user) { create(:user, use_new_navigation: nil) }
+
+ it 'does not show the callout' do
+ expect(page).to have_css('[data-testid="super-sidebar"]')
+ expect(page).not_to have_content callout_title
+ end
+ end
+end
diff --git a/spec/features/nav/new_nav_toggle_spec.rb b/spec/features/nav/new_nav_toggle_spec.rb
deleted file mode 100644
index 6872058be8e..00000000000
--- a/spec/features/nav/new_nav_toggle_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'new navigation toggle', :js, feature_category: :navigation do
- let_it_be(:user) { create(:user) }
-
- before do
- user.update!(use_new_navigation: user_preference)
- sign_in(user)
- visit explore_projects_path
- end
-
- context 'when user has new nav disabled' do
- let(:user_preference) { false }
-
- it 'allows to enable new nav', :aggregate_failures do
- within '.js-nav-user-dropdown' do
- find('a[data-toggle="dropdown"]').click
- expect(page).to have_content('Navigation redesign')
-
- toggle = page.find('.gl-toggle:not(.is-checked)')
- toggle.click # reloads the page
- end
-
- wait_for_requests
-
- expect(user.reload.use_new_navigation).to eq true
- end
-
- it 'shows the old navigation' do
- expect(page).to have_selector('.js-navbar')
- expect(page).not_to have_selector('[data-testid="super-sidebar"]')
- end
- end
-
- context 'when user has new nav enabled' do
- let(:user_preference) { true }
-
- it 'allows to disable new nav', :aggregate_failures do
- within '[data-testid="super-sidebar"] [data-testid="user-dropdown"]' do
- click_button "#{user.name} user’s menu"
- expect(page).to have_content('Navigation redesign')
-
- toggle = page.find('.gl-toggle.is-checked')
- toggle.click # reloads the page
- end
-
- wait_for_requests
-
- expect(user.reload.use_new_navigation).to eq false
- end
-
- it 'shows the new navigation' do
- expect(page).not_to have_selector('.js-navbar')
- expect(page).to have_selector('[data-testid="super-sidebar"]')
- end
- end
-end
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
index 824b2a296c6..3f8083aa37d 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
@@ -191,8 +191,6 @@ describe('Batch comments store actions', () => {
return actions.publishReview({ dispatch, commit, getters, rootGetters }).then(() => {
expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
expect(commit.mock.calls[1]).toEqual(['RECEIVE_PUBLISH_REVIEW_SUCCESS']);
-
- expect(dispatch.mock.calls[0]).toEqual(['updateDiscussionsAfterPublish']);
});
});
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 5299361a493..f09003edc0c 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -135,8 +135,6 @@ describe('diffs/components/app', () => {
jest.spyOn(wrapper.vm, 'fetchDiffFilesBatch').mockImplementation(fetchResolver);
jest.spyOn(wrapper.vm, 'fetchCoverageFiles').mockImplementation(fetchResolver);
jest.spyOn(wrapper.vm, 'setDiscussions').mockImplementation(() => {});
- jest.spyOn(wrapper.vm, 'unwatchDiscussions').mockImplementation(() => {});
- jest.spyOn(wrapper.vm, 'unwatchRetrievingBatches').mockImplementation(() => {});
store.state.diffs.retrievingBatches = true;
store.state.diffs.diffFiles = [];
return nextTick();
@@ -151,9 +149,7 @@ describe('diffs/components/app', () => {
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
expect(wrapper.vm.fetchCoverageFiles).toHaveBeenCalled();
- expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
expect(wrapper.vm.diffFilesLength).toBe(100);
- expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled();
});
it('calls batch methods if diffsBatchLoad is enabled, and latest version', async () => {
@@ -165,9 +161,7 @@ describe('diffs/components/app', () => {
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
expect(wrapper.vm.fetchCoverageFiles).toHaveBeenCalled();
- expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
expect(wrapper.vm.diffFilesLength).toBe(100);
- expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 18e81232b5c..51f8f04fc11 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -841,7 +841,7 @@ describe('DiffsStoreActions', () => {
};
const singleDiscussion = {
id: '1',
- file_hash: 'ABC',
+ diff_file: { file_hash: 'ABC' },
line_code: 'ABC_1_1',
};
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 720b72f4965..6331269d6e8 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -330,7 +330,7 @@ describe('DiffsStoreUtils', () => {
old_line: 5,
new_line: 5,
rich_text: '<p>rich</p>', // Note no leading space
- discussionsExpanded: true,
+ discussionsExpanded: false,
discussions: [],
hasForm: false,
text: undefined,
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index dc450eb2aa7..8c02a07994b 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -174,18 +174,25 @@ describe('~/environments/components/environments_app.vue', () => {
expect(button.exists()).toBe(true);
});
- it('should not show a button to open the review app modal if review apps are configured', async () => {
- await createWrapperWithMocked({
- environmentsApp: {
- ...resolvedEnvironmentsApp,
- reviewApp: { canSetupReviewApp: false },
- },
- folder: resolvedFolder,
- });
+ it.each`
+ canSetupReviewApp | hasReviewApp
+ ${false} | ${true}
+ ${true} | ${true}
+ `(
+ 'should not show button to open the review app modal',
+ async ({ canSetupReviewApp, hasReviewApp }) => {
+ await createWrapperWithMocked({
+ environmentsApp: {
+ ...resolvedEnvironmentsApp,
+ reviewApp: { canSetupReviewApp, hasReviewApp },
+ },
+ folder: resolvedFolder,
+ });
- const button = wrapper.findByRole('button', { name: s__('Environments|Enable review apps') });
- expect(button.exists()).toBe(false);
- });
+ const button = wrapper.findByRole('button', { name: s__('Environments|Enable review apps') });
+ expect(button.exists()).toBe(false);
+ },
+ );
it('should not show a button to clean up environments if the user has no permissions', async () => {
await createWrapperWithMocked({
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index fd97f19a6ab..b80b8508e8d 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -262,6 +262,7 @@ export const environmentsApp = {
review_app: {
can_setup_review_app: true,
all_clusters_empty: true,
+ has_review_app: false,
review_snippet:
'{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
},
@@ -471,6 +472,7 @@ export const resolvedEnvironmentsApp = {
reviewApp: {
canSetupReviewApp: true,
allClustersEmpty: true,
+ hasReviewApp: false,
reviewSnippet:
'{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
__typename: 'ReviewApp',
diff --git a/spec/frontend/issuable/components/status_badge_spec.js b/spec/frontend/issuable/components/status_badge_spec.js
index cdc848626c7..9ab5b4f7149 100644
--- a/spec/frontend/issuable/components/status_badge_spec.js
+++ b/spec/frontend/issuable/components/status_badge_spec.js
@@ -16,10 +16,10 @@ describe('StatusBadge component', () => {
${'merge_request'} | ${'Open'} | ${'opened'} | ${'success'} | ${'merge-request-open'}
${'merge_request'} | ${'Closed'} | ${'closed'} | ${'danger'} | ${'merge-request-close'}
${'merge_request'} | ${'Merged'} | ${'merged'} | ${'info'} | ${'merge'}
- ${'issue'} | ${'Open'} | ${'opened'} | ${'success'} | ${'issues'}
- ${'issue'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'issue-closed'}
- ${'epic'} | ${'Open'} | ${'opened'} | ${'success'} | ${'epic'}
- ${'epic'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'epic-closed'}
+ ${'issue'} | ${'Open'} | ${'opened'} | ${'success'} | ${'issue-open-m'}
+ ${'issue'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'issue-close'}
+ ${'epic'} | ${'Open'} | ${'opened'} | ${'success'} | ${'issue-open-m'}
+ ${'epic'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'issue-close'}
`(
'when issuableType=$issuableType and state=$state',
({ issuableType, badgeText, state, badgeVariant, badgeIcon }) => {
diff --git a/spec/frontend/issues/show/components/issue_header_spec.js b/spec/frontend/issues/show/components/issue_header_spec.js
index 6acc7004576..6c4e357d722 100644
--- a/spec/frontend/issues/show/components/issue_header_spec.js
+++ b/spec/frontend/issues/show/components/issue_header_spec.js
@@ -47,7 +47,7 @@ describe('IssueHeader component', () => {
issuableType: 'issue',
serviceDeskReplyTo: '',
showWorkItemTypeIcon: true,
- statusIcon: 'issues',
+ statusIcon: 'issue-open-m',
workspaceType: 'project',
});
});
@@ -63,7 +63,7 @@ describe('IssueHeader component', () => {
});
it('renders correct icon', () => {
- expect(findIssuableHeader().props('statusIcon')).toBe('issues');
+ expect(findIssuableHeader().props('statusIcon')).toBe('issue-open-m');
});
});
@@ -77,7 +77,7 @@ describe('IssueHeader component', () => {
});
it('renders correct icon', () => {
- expect(findIssuableHeader().props('statusIcon')).toBe('issue-closed');
+ expect(findIssuableHeader().props('statusIcon')).toBe('issue-close');
});
describe('when issue is marked as duplicate', () => {
diff --git a/spec/frontend/issues/show/components/sticky_header_spec.js b/spec/frontend/issues/show/components/sticky_header_spec.js
index a909084956f..43d96f398b6 100644
--- a/spec/frontend/issues/show/components/sticky_header_spec.js
+++ b/spec/frontend/issues/show/components/sticky_header_spec.js
@@ -36,12 +36,12 @@ describe('StickyHeader component', () => {
it.each`
issuableType | issuableStatus | statusIcon
- ${TYPE_INCIDENT} | ${STATUS_OPEN} | ${'issues'}
- ${TYPE_INCIDENT} | ${STATUS_CLOSED} | ${'issue-closed'}
- ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'}
- ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'}
- ${TYPE_EPIC} | ${STATUS_OPEN} | ${'epic'}
- ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'epic-closed'}
+ ${TYPE_INCIDENT} | ${STATUS_OPEN} | ${'issue-open-m'}
+ ${TYPE_INCIDENT} | ${STATUS_CLOSED} | ${'issue-close'}
+ ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issue-open-m'}
+ ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-close'}
+ ${TYPE_EPIC} | ${STATUS_OPEN} | ${'issue-open-m'}
+ ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'issue-close'}
`(
'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
({ issuableType, issuableStatus, statusIcon }) => {
diff --git a/spec/frontend/lib/utils/color_utils_spec.js b/spec/frontend/lib/utils/color_utils_spec.js
index 92ac66c19f0..aac50a2c850 100644
--- a/spec/frontend/lib/utils/color_utils_spec.js
+++ b/spec/frontend/lib/utils/color_utils_spec.js
@@ -18,15 +18,17 @@ describe('Color utils', () => {
describe('darkModeEnabled', () => {
it.each`
- page | bodyClass | ideTheme | expected
+ page | rootClass | ideTheme | expected
${'ide:index'} | ${'gl-dark'} | ${'monokai-light'} | ${false}
${'ide:index'} | ${'ui-light'} | ${'monokai'} | ${true}
${'groups:issues:index'} | ${'ui-light'} | ${'monokai'} | ${false}
${'groups:issues:index'} | ${'gl-dark'} | ${'monokai-light'} | ${true}
`(
- 'is $expected on $page with $bodyClass body class and $ideTheme IDE theme',
- ({ page, bodyClass, ideTheme, expected }) => {
- document.body.outerHTML = `<body class="${bodyClass}" data-page="${page}"></body>`;
+ 'is $expected on $page with $rootClass root class and $ideTheme IDE theme',
+ ({ page, rootClass, ideTheme, expected }) => {
+ document.documentElement.className = rootClass;
+ document.body.outerHTML = `<body data-page="${page}"></body>`;
+
window.gon = {
user_color_scheme: ideTheme,
};
diff --git a/spec/frontend/observability/client_spec.js b/spec/frontend/observability/client_spec.js
index 31998053742..e33cfc5b4a4 100644
--- a/spec/frontend/observability/client_spec.js
+++ b/spec/frontend/observability/client_spec.js
@@ -384,4 +384,41 @@ describe('buildClient', () => {
expectErrorToBeReported(new Error(e));
});
});
+
+ describe('fetchMetrics', () => {
+ const FETCHING_METRICS_ERROR = 'metrics are missing/invalid in the response';
+
+ it('fetches metrics from the metrics URL', async () => {
+ const mockResponse = {
+ metrics: [
+ { name: 'metric A', description: 'a counter metric called A', type: 'COUNTER' },
+ { name: 'metric B', description: 'a gauge metric called B', type: 'GAUGE' },
+ ],
+ };
+
+ axiosMock.onGet(metricsUrl).reply(200, mockResponse);
+
+ const result = await client.fetchMetrics();
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ expect(axios.get).toHaveBeenCalledWith(metricsUrl, {
+ withCredentials: true,
+ });
+ expect(result).toEqual(mockResponse);
+ });
+
+ it('rejects if metrics are missing', async () => {
+ axiosMock.onGet(metricsUrl).reply(200, {});
+
+ await expect(client.fetchMetrics()).rejects.toThrow(FETCHING_METRICS_ERROR);
+ expectErrorToBeReported(new Error(FETCHING_METRICS_ERROR));
+ });
+
+ it('rejects if metrics are invalid', async () => {
+ axiosMock.onGet(metricsUrl).reply(200, { traces: 'invalid' });
+
+ await expect(client.fetchMetrics()).rejects.toThrow(FETCHING_METRICS_ERROR);
+ expectErrorToBeReported(new Error(FETCHING_METRICS_ERROR));
+ });
+ });
});
diff --git a/spec/frontend/organizations/users/mock_data.js b/spec/frontend/organizations/users/mock_data.js
index 5503b063ad2..4f159c70c2c 100644
--- a/spec/frontend/organizations/users/mock_data.js
+++ b/spec/frontend/organizations/users/mock_data.js
@@ -12,7 +12,10 @@ export const MOCK_USERS = [
user: { id: 'gid://gitlab/User/2' },
},
{
- badges: ['Admin', "It's you!"],
+ badges: [
+ { text: 'Admin', variant: 'success' },
+ { text: "It's you!", variant: 'muted' },
+ ],
id: 'gid://gitlab/Organizations::OrganizationUser/1',
user: { id: 'gid://gitlab/User/1' },
},
diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js
index 6422856ba22..301b0e8e157 100644
--- a/spec/frontend/protected_branches/protected_branch_edit_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js
@@ -6,37 +6,96 @@ 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 ProtectedBranchEdit from '~/protected_branches/protected_branch_edit';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/alert');
const TEST_URL = `${TEST_HOST}/url`;
+
+const response = {
+ project_id: 2,
+ name: 'release/*',
+ id: 30,
+ created_at: '2023-09-21T03:06:27.532Z',
+ updated_at: '2023-10-31T21:37:50.126Z',
+ code_owner_approval_required: false,
+ allow_force_push: false,
+ namespace_id: null,
+ merge_access_levels: [
+ {
+ id: 37,
+ protected_branch_id: 30,
+ access_level: 40,
+ created_at: '2023-10-31T22:44:15.545Z',
+ updated_at: '2023-10-31T22:44:15.545Z',
+ user_id: null,
+ group_id: null,
+ },
+ ],
+ push_access_levels: [
+ {
+ id: 38,
+ protected_branch_id: 30,
+ access_level: 40,
+ created_at: '2023-10-31T22:43:53.105Z',
+ updated_at: '2023-10-31T22:43:53.105Z',
+ user_id: null,
+ group_id: null,
+ deploy_key_id: null,
+ },
+ ],
+};
+
+// Toggles
const FORCE_PUSH_TOGGLE_TESTID = 'force-push-toggle';
const CODE_OWNER_TOGGLE_TESTID = 'code-owner-toggle';
const IS_CHECKED_CLASS = 'is-checked';
const IS_DISABLED_CLASS = 'is-disabled';
const IS_LOADING_SELECTOR = '.toggle-loading';
+// Dropdowns
+const MERGE_DROPDOWN_TESTID = 'protected-branch-allowed-to-merge';
+const PUSH_DROPDOWN_TESTID = 'protected-branch-allowed-to-push';
+const INIT_MERGE_DATA_TESTID = 'js-allowed-to-merge';
+const INIT_PUSH_DATA_TESTID = 'js-allowed-to-push';
+
describe('ProtectedBranchEdit', () => {
let mock;
- beforeEach(() => {
- jest.spyOn(ProtectedBranchEdit.prototype, 'initDropdowns').mockImplementation();
-
- mock = new MockAdapter(axios);
- });
-
const findForcePushToggle = () =>
document.querySelector(`div[data-testid="${FORCE_PUSH_TOGGLE_TESTID}"] button`);
const findCodeOwnerToggle = () =>
document.querySelector(`div[data-testid="${CODE_OWNER_TOGGLE_TESTID}"] button`);
+ const findMergeDropdown = () =>
+ document.querySelector(`div[data-testid="${MERGE_DROPDOWN_TESTID}"]`);
+ const findPushDropdown = () =>
+ document.querySelector(`div[data-testid="${PUSH_DROPDOWN_TESTID}"]`);
const create = ({
forcePushToggleChecked = false,
codeOwnerToggleChecked = false,
+ mergeClass = INIT_MERGE_DATA_TESTID,
+ mergeDisabled = false,
+ mergePreselected = [],
+ pushClass = INIT_PUSH_DATA_TESTID,
+ pushDisabled = false,
+ pushPreselected = [],
hasLicense = true,
} = {}) => {
setHTMLFixture(`<div id="wrap" data-url="${TEST_URL}">
<span
+ class="${mergeClass}"
+ data-label="Dropdown allowed to merge"
+ data-disabled="${mergeDisabled}"
+ data-preselected-items='${mergePreselected}'
+ data-testid="${MERGE_DROPDOWN_TESTID}"></span>
+ <span
+ class="${pushClass}"
+ data-label="Dropdown allowed to push"
+ data-disabled="${pushDisabled}"
+ data-preselected-items='${pushPreselected}'
+ data-testid="${PUSH_DROPDOWN_TESTID}"></span>
+ <span
class="js-force-push-toggle"
data-label="Toggle allowed to force push"
data-is-checked="${forcePushToggleChecked}"
@@ -51,108 +110,261 @@ describe('ProtectedBranchEdit', () => {
return new ProtectedBranchEdit({ $wrap: $('#wrap'), hasLicense });
};
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
afterEach(() => {
mock.restore();
resetHTMLFixture();
});
- describe('when license supports code owner approvals', () => {
+ describe('dropdowns', () => {
+ const accessLevels = [
+ {
+ id: 40,
+ text: 'Maintainers',
+ before_divider: true,
+ },
+ {
+ id: 30,
+ text: 'Developers + Maintainers',
+ before_divider: true,
+ },
+ ];
+
beforeEach(() => {
- create();
- });
+ window.gon = {
+ current_project_id: 1,
+ merge_access_levels: { roles: accessLevels },
+ push_access_levels: { roles: accessLevels },
+ };
- it('instantiates the code owner toggle', () => {
- expect(findCodeOwnerToggle()).not.toBe(null);
+ jest.spyOn(ProtectedBranchEdit.prototype, 'initToggles').mockImplementation();
});
- });
- describe('when license does not support code owner approvals', () => {
- beforeEach(() => {
- create({ hasLicense: false });
- });
+ describe('rendering', () => {
+ describe('merge dropdown', () => {
+ it('builds the merge dropdown when it has the proper class', () => {
+ create();
+ expect(findMergeDropdown()).not.toBe(null);
+ });
- it('does not instantiate the code owner toggle', () => {
- expect(findCodeOwnerToggle()).toBe(null);
- });
- });
+ it('does not build the merge dropdown when it does not have the proper class', () => {
+ create({ mergeClass: 'invalid-class' });
+ expect(findMergeDropdown()).toBe(null);
+ });
+ });
- describe('when toggles are not available in the DOM on page load', () => {
- beforeEach(() => {
- create({ hasLicense: true });
- setHTMLFixture('');
- });
+ describe('push dropdown', () => {
+ it('builds the push dropdown when it has the proper class', () => {
+ create();
+ expect(findPushDropdown()).not.toBe(null);
+ });
- afterEach(() => {
- resetHTMLFixture();
+ it('does not build the push dropdown when it does not have the proper class', () => {
+ create({ pushClass: 'invalid-class' });
+ expect(findPushDropdown()).toBe(null);
+ });
+ });
});
- it('does not instantiate the force push toggle', () => {
- expect(findForcePushToggle()).toBe(null);
+ describe('preselected item', () => {
+ beforeEach(() => {
+ mock.onPatch(TEST_URL).reply(HTTP_STATUS_OK, response);
+ });
+
+ it('sets selected item on load', () => {
+ const preselected = [{ id: 38, access_level: 40, type: 'role' }];
+ const ProtectedBranchEditInstance = create({
+ pushPreselected: JSON.stringify(preselected),
+ });
+ const dropdown = ProtectedBranchEditInstance.push_access_levels_dropdown;
+ expect(dropdown.preselected).toEqual(preselected);
+ });
+
+ it('updates selected item on save for enabled dropdowns', async () => {
+ const selectedValue = [{ access_level: 40 }];
+ const ProtectedBranchEditInstance = create({});
+ const dropdown = ProtectedBranchEditInstance.push_access_levels_dropdown;
+ dropdown.$emit('select', selectedValue);
+ dropdown.$emit('hidden');
+ await waitForPromises();
+ expect(dropdown.preselected[0].id).toBe(response.push_access_levels[0].id);
+ });
+
+ it('does not update selected item on save for disabled dropdowns', async () => {
+ const selectedValue = [{ access_level: 40 }];
+ const ProtectedBranchEditInstance = create({ pushDisabled: '' });
+ const dropdown = ProtectedBranchEditInstance.push_access_levels_dropdown;
+ dropdown.$emit('select', selectedValue);
+ dropdown.$emit('hidden');
+ await waitForPromises();
+ expect(dropdown.preselected).toEqual([]);
+ });
});
- it('does not instantiate the code owner toggle', () => {
- expect(findCodeOwnerToggle()).toBe(null);
+ describe('on hidden', () => {
+ beforeEach(() => {
+ mock.onPatch(TEST_URL).reply(HTTP_STATUS_OK, {});
+ });
+
+ it('does not update permissions on hidden if there are no changes', () => {
+ const ProtectedBranchEditInstance = create();
+ const dropdown = ProtectedBranchEditInstance.merge_access_levels_dropdown;
+ dropdown.$emit('hidden');
+ expect(mock.history.patch).toHaveLength(0);
+ });
+
+ it('updates permissions on hidden for enabled dropdowns with changes', async () => {
+ const preselectedData = { id: 38, access_level: 40 };
+ const preselected = [{ ...preselectedData, type: 'role' }];
+ const selectedValue = [{ access_level: 30 }];
+ const ProtectedBranchEditInstance = create({
+ pushPreselected: JSON.stringify(preselected),
+ });
+ const dropdown = ProtectedBranchEditInstance.merge_access_levels_dropdown;
+ dropdown.$emit('select', selectedValue);
+ dropdown.$emit('hidden');
+ await waitForPromises();
+ expect(mock.history.patch).toHaveLength(1);
+ expect(mock.history.patch[0].data).toEqual(
+ JSON.stringify({
+ protected_branch: {
+ merge_access_levels_attributes: selectedValue,
+ push_access_levels_attributes: [preselectedData],
+ },
+ }),
+ );
+ });
+
+ it('does not update permissions on hidden for disabled dropdowns', async () => {
+ const preselected = [{ id: 38, access_level: 0, type: 'role' }];
+ const selectedValue = [{ access_level: 30 }];
+ const ProtectedBranchEditInstance = create({
+ mergeDisabled: '',
+ mergePreselected: JSON.stringify(preselected),
+ });
+ const dropdown = ProtectedBranchEditInstance.push_access_levels_dropdown;
+ dropdown.$emit('select', selectedValue);
+ dropdown.$emit('hidden');
+ await waitForPromises();
+ expect(mock.history.patch).toHaveLength(1);
+ expect(mock.history.patch[0].data).toEqual(
+ JSON.stringify({
+ protected_branch: {
+ merge_access_levels_attributes: [],
+ push_access_levels_attributes: selectedValue,
+ },
+ }),
+ );
+ });
});
});
- describe.each`
- description | checkedOption | patchParam | finder
- ${'force push'} | ${'forcePushToggleChecked'} | ${'allow_force_push'} | ${findForcePushToggle}
- ${'code owner'} | ${'codeOwnerToggleChecked'} | ${'code_owner_approval_required'} | ${findCodeOwnerToggle}
- `('when unchecked $description toggle button', ({ checkedOption, patchParam, finder }) => {
- let toggle;
-
+ describe('toggles', () => {
beforeEach(() => {
- create({ [checkedOption]: false });
+ jest.spyOn(ProtectedBranchEdit.prototype, 'initDropdowns').mockImplementation();
+ });
- toggle = finder();
+ describe('when license supports code owner approvals', () => {
+ beforeEach(() => {
+ create();
+ });
+
+ it('instantiates the code owner toggle', () => {
+ expect(findCodeOwnerToggle()).not.toBe(null);
+ });
});
- it('is not changed', () => {
- expect(toggle).not.toHaveClass(IS_CHECKED_CLASS);
- expect(toggle.querySelector(IS_LOADING_SELECTOR)).toBe(null);
- expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
+ describe('when license does not support code owner approvals', () => {
+ beforeEach(() => {
+ create({ hasLicense: false });
+ });
+
+ it('does not instantiate the code owner toggle', () => {
+ expect(findCodeOwnerToggle()).toBe(null);
+ });
});
- describe('when clicked', () => {
+ describe('when toggles are not available in the DOM on page load', () => {
beforeEach(() => {
- mock
- .onPatch(TEST_URL, { protected_branch: { [patchParam]: true } })
- .replyOnce(HTTP_STATUS_OK, {});
+ create({ hasLicense: true });
+ setHTMLFixture('');
});
- it('checks and disables button', async () => {
- await toggle.click();
+ afterEach(() => {
+ resetHTMLFixture();
+ });
- expect(toggle).toHaveClass(IS_CHECKED_CLASS);
- expect(toggle.querySelector(IS_LOADING_SELECTOR)).not.toBe(null);
- expect(toggle).toHaveClass(IS_DISABLED_CLASS);
+ it('does not instantiate the force push toggle', () => {
+ expect(findForcePushToggle()).toBe(null);
});
- it('sends update to BE', async () => {
- await toggle.click();
+ it('does not instantiate the code owner toggle', () => {
+ expect(findCodeOwnerToggle()).toBe(null);
+ });
+ });
- await axios.waitForAll();
+ describe.each`
+ description | checkedOption | patchParam | finder
+ ${'force push'} | ${'forcePushToggleChecked'} | ${'allow_force_push'} | ${findForcePushToggle}
+ ${'code owner'} | ${'codeOwnerToggleChecked'} | ${'code_owner_approval_required'} | ${findCodeOwnerToggle}
+ `('when unchecked $description toggle button', ({ checkedOption, patchParam, finder }) => {
+ let toggle;
- // Args are asserted in the `.onPatch` call
- expect(mock.history.patch).toHaveLength(1);
+ beforeEach(() => {
+ create({ [checkedOption]: false });
- expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
+ toggle = finder();
+ });
+
+ it('is not changed', () => {
+ expect(toggle).not.toHaveClass(IS_CHECKED_CLASS);
expect(toggle.querySelector(IS_LOADING_SELECTOR)).toBe(null);
- expect(createAlert).not.toHaveBeenCalled();
+ expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
});
- });
- describe('when clicked and BE error', () => {
- beforeEach(() => {
- mock.onPatch(TEST_URL).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- toggle.click();
+ describe('when clicked', () => {
+ beforeEach(() => {
+ mock
+ .onPatch(TEST_URL, { protected_branch: { [patchParam]: true } })
+ .replyOnce(HTTP_STATUS_OK, {});
+ });
+
+ it('checks and disables button', async () => {
+ await toggle.click();
+
+ expect(toggle).toHaveClass(IS_CHECKED_CLASS);
+ expect(toggle.querySelector(IS_LOADING_SELECTOR)).not.toBe(null);
+ expect(toggle).toHaveClass(IS_DISABLED_CLASS);
+ });
+
+ it('sends update to BE', async () => {
+ await toggle.click();
+
+ await axios.waitForAll();
+
+ // Args are asserted in the `.onPatch` call
+ expect(mock.history.patch).toHaveLength(1);
+
+ expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
+ expect(toggle.querySelector(IS_LOADING_SELECTOR)).toBe(null);
+ expect(createAlert).not.toHaveBeenCalled();
+ });
});
- it('alerts error', async () => {
- await axios.waitForAll();
+ describe('when clicked and BE error', () => {
+ beforeEach(() => {
+ mock.onPatch(TEST_URL).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ toggle.click();
+ });
+
+ it('alerts error', async () => {
+ await axios.waitForAll();
- expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index b58b65f09f5..27d65f27007 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -49,7 +49,6 @@ describe('UserBar component', () => {
sidebarData,
},
provide: {
- toggleNewNavEndpoint: '/-/profile/preferences',
isImpersonating: false,
...provideOverrides,
},
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index 79a31492f3f..45a60fce00a 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -3,8 +3,6 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import UserMenu from '~/super_sidebar/components/user_menu.vue';
import UserMenuProfileItem from '~/super_sidebar/components/user_menu_profile_item.vue';
-import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
-import invalidUrl from '~/lib/utils/invalid_url';
import { mockTracking } from 'helpers/tracking_helper';
import PersistentUserCallout from '~/persistent_user_callout';
import { userMenuMockData, userMenuMockStatus, userMenuMockPipelineMinutes } from '../mock_data';
@@ -14,7 +12,6 @@ describe('UserMenu component', () => {
let trackingSpy;
const GlEmoji = { template: '<img/>' };
- const toggleNewNavEndpoint = invalidUrl;
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const showDropdown = () => findDropdown().vm.$emit('shown');
@@ -34,7 +31,6 @@ describe('UserMenu component', () => {
...stubs,
},
provide: {
- toggleNewNavEndpoint,
isImpersonating: false,
...provide,
},
@@ -459,15 +455,6 @@ describe('UserMenu component', () => {
});
});
- describe('New navigation toggle item', () => {
- it('should render menu item with new navigation toggle', () => {
- createWrapper();
- const toggleItem = wrapper.findComponent(NewNavToggle);
- expect(toggleItem.exists()).toBe(true);
- expect(toggleItem.props('endpoint')).toBe(toggleNewNavEndpoint);
- });
- });
-
describe('Sign out group', () => {
const findSignOutGroup = () => wrapper.findByTestId('sign-out-group');
diff --git a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
index 40232eb367a..810269257b6 100644
--- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
@@ -1,15 +1,16 @@
import {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlSprintf,
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
GlSearchBoxByType,
+ GlIcon,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { nextTick } from 'vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import DiffStatsDropdown, { i18n } from '~/vue_shared/components/diff_stats_dropdown.vue';
+import { ARROW_DOWN_KEY } from '~/lib/utils/keys';
jest.mock('fuzzaldrin-plus', () => ({
filter: jest.fn().mockReturnValue([]),
@@ -42,7 +43,7 @@ describe('Diff Stats Dropdown', () => {
const focusInputMock = jest.fn();
const createComponent = ({ changed = 0, added = 0, deleted = 0, files = [] } = {}) => {
- wrapper = shallowMountExtended(DiffStatsDropdown, {
+ wrapper = mountExtended(DiffStatsDropdown, {
propsData: {
changed,
added,
@@ -51,7 +52,6 @@ describe('Diff Stats Dropdown', () => {
},
stubs: {
GlSprintf,
- GlDropdown,
GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
methods: { focusInput: focusInputMock },
}),
@@ -59,9 +59,8 @@ describe('Diff Stats Dropdown', () => {
});
};
- const findChanged = () => wrapper.findComponent(GlDropdown);
- const findChangedFiles = () => findChanged().findAllComponents(GlDropdownItem);
- const findNoFilesText = () => findChanged().findComponent(GlDropdownText);
+ const findChanged = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findChangedFiles = () => findChanged().findAllComponents(GlDisclosureDropdownItem);
const findCollapsed = () => wrapper.findByTestId('diff-stats-additions-deletions-expanded');
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
@@ -79,15 +78,14 @@ describe('Diff Stats Dropdown', () => {
const fileText = findChangedFiles().at(1).text();
expect(fileText).toContain(mockFiles[1].name);
expect(fileText).toContain(mockFiles[1].path);
- expect(fileData.props()).toMatchObject({
- iconName: mockFiles[1].icon,
- iconColor: mockFiles[1].iconColor,
- });
+ expect(fileData.findComponent(GlIcon).props('name')).toEqual(mockFiles[1].icon);
+ expect(fileData.findComponent(GlIcon).classes()).toContain('gl-text-red-500');
+ expect(fileData.find('a').attributes('href')).toEqual(mockFiles[1].href);
});
it('when no files changed', () => {
createComponent({ files: [] });
- expect(findNoFilesText().text()).toContain(i18n.noFilesFound);
+ expect(findChanged().text()).toContain(i18n.noFilesFound);
});
});
@@ -108,7 +106,7 @@ describe('Diff Stats Dropdown', () => {
});
it(`dropdown header should be '${expectedDropdownHeader}'`, () => {
- expect(findChanged().props('text')).toBe(expectedDropdownHeader);
+ expect(findChanged().props('toggleText')).toBe(expectedDropdownHeader);
});
it(`added and deleted count in collapsed section should be '${expectedAddedDeletedCollapsed}'`, () => {
@@ -137,27 +135,27 @@ describe('Diff Stats Dropdown', () => {
});
});
- describe('selecting file dropdown item', () => {
+ describe('on dropdown open', () => {
beforeEach(() => {
- createComponent({ files: mockFiles });
+ createComponent();
});
- it('updates the URL', () => {
- findChangedFiles().at(0).vm.$emit('click');
- expect(window.location.hash).toBe(mockFiles[0].href);
- findChangedFiles().at(1).vm.$emit('click');
- expect(window.location.hash).toBe(mockFiles[1].href);
+ it('should set the search input focus', () => {
+ findChanged().vm.$emit('shown');
+ expect(focusInputMock).toHaveBeenCalled();
});
});
- describe('on dropdown open', () => {
+ describe('keyboard nav', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ files: mockFiles });
});
- it('should set the search input focus', () => {
- findChanged().vm.$emit('shown');
- expect(focusInputMock).toHaveBeenCalled();
+ it('focuses the first item when pressing the down key within the search box', () => {
+ const spy = jest.spyOn(wrapper.vm, 'focusFirstItem');
+ findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ARROW_DOWN_KEY }));
+
+ expect(spy).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/vue_shared/components/list_selector/index_spec.js b/spec/frontend/vue_shared/components/list_selector/index_spec.js
index 4222a28afe8..11e64a91eb0 100644
--- a/spec/frontend/vue_shared/components/list_selector/index_spec.js
+++ b/spec/frontend/vue_shared/components/list_selector/index_spec.js
@@ -1,16 +1,18 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlCard, GlIcon, GlCollapsibleListbox, GlSearchBoxByType } from '@gitlab/ui';
+import Api from '~/api';
+import { createAlert } from '~/alert';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ListSelector from '~/vue_shared/components/list_selector/index.vue';
import UserItem from '~/vue_shared/components/list_selector/user_item.vue';
import GroupItem from '~/vue_shared/components/list_selector/group_item.vue';
-import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { USERS_RESPONSE_MOCK, GROUPS_RESPONSE_MOCK } from './mock_data';
+jest.mock('~/alert');
Vue.use(VueApollo);
describe('List Selector spec', () => {
@@ -20,6 +22,7 @@ describe('List Selector spec', () => {
const USERS_MOCK_PROPS = {
title: 'Users',
projectPath: 'some/project/path',
+ groupPath: 'some/group/path',
type: 'users',
};
@@ -29,15 +32,10 @@ describe('List Selector spec', () => {
type: 'groups',
};
- const usersAutocompleteQuerySuccess = jest.fn().mockResolvedValue(USERS_RESPONSE_MOCK);
const groupsAutocompleteQuerySuccess = jest.fn().mockResolvedValue(GROUPS_RESPONSE_MOCK);
- const createComponent = async (
- props,
- query = usersAutocompleteQuery,
- queryResponse = usersAutocompleteQuerySuccess,
- ) => {
- fakeApollo = createMockApollo([[query, queryResponse]]);
+ const createComponent = async (props) => {
+ fakeApollo = createMockApollo([[groupsAutocompleteQuery, groupsAutocompleteQuerySuccess]]);
wrapper = mountExtended(ListSelector, {
apolloProvider: fakeApollo,
@@ -52,12 +50,21 @@ describe('List Selector spec', () => {
const findCard = () => wrapper.findComponent(GlCard);
const findTitle = () => findCard().find('[data-testid="list-selector-title"]');
const findIcon = () => wrapper.findComponent(GlIcon);
- const findListBox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAllListBoxComponents = () => wrapper.findAllComponents(GlCollapsibleListbox);
+ const findSearchResultsDropdown = () => findAllListBoxComponents().at(0);
+ const findNamespaceDropdown = () => findAllListBoxComponents().at(1);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findAllUserComponents = () => wrapper.findAllComponents(UserItem);
const findAllGroupComponents = () => wrapper.findAllComponents(GroupItem);
+ beforeEach(() => {
+ jest.spyOn(Api, 'projectUsers').mockResolvedValue(USERS_RESPONSE_MOCK);
+ jest.spyOn(Api, 'groupMembers').mockResolvedValue({ data: USERS_RESPONSE_MOCK });
+ });
+
describe('Users type', () => {
+ const search = 'foo';
+
beforeEach(() => createComponent(USERS_MOCK_PROPS));
it('renders a Card component', () => {
@@ -77,47 +84,67 @@ describe('List Selector spec', () => {
expect(findSearchBox().exists()).toBe(true);
});
+ it('renders two namespace dropdown items', () => {
+ expect(findNamespaceDropdown().props('items').length).toBe(2);
+ });
+
it('does not call query when search box has not received an input', () => {
- expect(usersAutocompleteQuerySuccess).not.toHaveBeenCalled();
+ expect(Api.projectUsers).not.toHaveBeenCalled();
+ expect(Api.groupMembers).not.toHaveBeenCalled();
expect(findAllUserComponents().length).toBe(0);
});
- describe('searching', () => {
- const searchResponse = USERS_RESPONSE_MOCK.data.project.autocompleteUsers;
- const search = 'foo';
+ describe.each`
+ dropdownItemValue | apiMethod | apiParams | searchResponse
+ ${'false'} | ${'groupMembers'} | ${[USERS_MOCK_PROPS.groupPath, { query: search }]} | ${USERS_RESPONSE_MOCK}
+ ${'true'} | ${'projectUsers'} | ${[USERS_MOCK_PROPS.projectPath, search]} | ${USERS_RESPONSE_MOCK}
+ `(
+ 'searching based on namespace dropdown selection',
+ ({ dropdownItemValue, apiMethod, apiParams, searchResponse }) => {
+ const emitSearchInput = async () => {
+ findSearchBox().vm.$emit('input', search);
+ await waitForPromises();
+ };
+
+ beforeEach(async () => {
+ findNamespaceDropdown().vm.$emit('select', dropdownItemValue);
+ await emitSearchInput();
+ });
- const emitSearchInput = async () => {
- findSearchBox().vm.$emit('input', search);
- await waitForPromises();
- };
+ it('shows error alert when API fails', async () => {
+ jest.spyOn(Api, apiMethod).mockRejectedValueOnce();
+ await emitSearchInput();
- beforeEach(() => emitSearchInput());
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while fetching. Please try again.',
+ });
+ });
- it('calls query with correct variables when Search box receives an input', () => {
- expect(usersAutocompleteQuerySuccess).toHaveBeenCalledWith({
- fullPath: USERS_MOCK_PROPS.projectPath,
- isProject: true,
- search,
+ it('calls query with correct variables when Search box receives an input', () => {
+ expect(Api[apiMethod]).toHaveBeenCalledWith(...apiParams);
});
- });
- it('renders a List box component with the correct props', () => {
- expect(findListBox().props()).toMatchObject({ multiple: true, items: searchResponse });
- });
+ it('renders a List box component with the correct props', () => {
+ expect(findSearchResultsDropdown().props()).toMatchObject({
+ multiple: true,
+ items: searchResponse,
+ });
+ });
- it('renders a user component for each search result', () => {
- expect(findAllUserComponents().length).toBe(searchResponse.length);
- });
+ it('renders a user component for each search result', () => {
+ expect(findAllUserComponents().length).toBe(searchResponse.length);
+ });
- it('emits an event when a search result is selected', () => {
- const firstSearchResult = searchResponse[0];
- findAllUserComponents().at(0).vm.$emit('select', firstSearchResult.username);
+ it('emits an event when a search result is selected', () => {
+ const firstSearchResult = searchResponse[0];
+ findAllUserComponents().at(0).vm.$emit('select', firstSearchResult.username);
- expect(wrapper.emitted('select')).toEqual([
- [{ ...firstSearchResult, text: 'Administrator', value: 'root' }],
- ]);
- });
- });
+ expect(wrapper.emitted('select')).toEqual([
+ [{ ...firstSearchResult, text: 'Administrator', value: 'root' }],
+ ]);
+ });
+ },
+ );
describe('selected items', () => {
const selectedUser = { username: 'root' };
@@ -147,9 +174,7 @@ describe('List Selector spec', () => {
});
describe('Groups type', () => {
- beforeEach(() =>
- createComponent(GROUPS_MOCK_PROPS, groupsAutocompleteQuery, groupsAutocompleteQuerySuccess),
- );
+ beforeEach(() => createComponent(GROUPS_MOCK_PROPS));
it('renders a correct title', () => {
expect(findTitle().exists()).toBe(true);
@@ -182,8 +207,11 @@ describe('List Selector spec', () => {
});
});
- it('renders a List box component with the correct props', () => {
- expect(findListBox().props()).toMatchObject({ multiple: true, items: searchResponse });
+ it('renders a dropdown for the search results', () => {
+ expect(findSearchResultsDropdown().props()).toMatchObject({
+ multiple: true,
+ items: searchResponse,
+ });
});
it('renders a group component for each search result', () => {
diff --git a/spec/frontend/vue_shared/components/list_selector/mock_data.js b/spec/frontend/vue_shared/components/list_selector/mock_data.js
index 25ecac9632b..5b44a0c2a83 100644
--- a/spec/frontend/vue_shared/components/list_selector/mock_data.js
+++ b/spec/frontend/vue_shared/components/list_selector/mock_data.js
@@ -1,28 +1,20 @@
-export const USERS_RESPONSE_MOCK = {
- data: {
- project: {
- id: 'gid://gitlab/Project/20',
- autocompleteUsers: [
- {
- id: 'gid://gitlab/User/1',
- avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png',
- name: 'Administrator',
- username: 'root',
- __typename: 'AutocompletedUser',
- },
- {
- id: 'gid://gitlab/User/15',
- avatarUrl:
- 'https://www.gravatar.com/avatar/c4ab964b90c3049c47882b319d3c5cc0?s=80\u0026d=identicon',
- name: 'Corrine Rath',
- username: 'laronda.graham',
- __typename: 'AutocompletedUser',
- },
- ],
- __typename: 'Project',
- },
+export const USERS_RESPONSE_MOCK = [
+ {
+ id: 'gid://gitlab/User/1',
+ avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png',
+ name: 'Administrator',
+ username: 'root',
+ __typename: 'AutocompletedUser',
},
-};
+ {
+ id: 'gid://gitlab/User/15',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/c4ab964b90c3049c47882b319d3c5cc0?s=80\u0026d=identicon',
+ name: 'Corrine Rath',
+ username: 'laronda.graham',
+ __typename: 'AutocompletedUser',
+ },
+];
export const GROUPS_RESPONSE_MOCK = {
data: {
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
index 02e729a00bd..71ff5275063 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
@@ -133,6 +133,7 @@ describe('IssuableBody', () => {
issuable: issuableBodyProps.issuable,
statusIcon: issuableBodyProps.statusIcon,
enableEdit: issuableBodyProps.enableEdit,
+ workspaceType: issuableBodyProps.workspaceType,
});
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
index ad7afefff12..6d1d3773643 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
@@ -52,6 +52,7 @@ describe('IssuableShowRoot', () => {
descriptionPreviewPath,
descriptionHelpPath,
taskCompletionStatus,
+ workspaceType,
} = mockIssuableShowProps;
const { state, blocked, confidential, createdAt, author } = mockIssuable;
@@ -92,6 +93,7 @@ describe('IssuableShowRoot', () => {
editFormVisible,
descriptionPreviewPath,
descriptionHelpPath,
+ workspaceType,
});
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
index eefc9142064..0ea69bc27e5 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
@@ -4,6 +4,7 @@ import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import IssuableTitle from '~/vue_shared/issuable/show/components/issuable_title.vue';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
@@ -86,19 +87,39 @@ describe('IssuableTitle', () => {
expect(tooltip).toBeDefined();
});
- it('renders sticky header when `stickyTitleVisible` prop is true', async () => {
- wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
- await nextTick();
+ describe('sticky header', () => {
+ it('renders when `stickyTitleVisible` prop is true', async () => {
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
+ await nextTick();
- const stickyHeaderEl = findStickyHeader();
+ const stickyHeaderEl = findStickyHeader();
- expect(stickyHeaderEl.exists()).toBe(true);
- expect(stickyHeaderEl.findComponent(GlBadge).props('variant')).toBe('success');
- expect(stickyHeaderEl.findComponent(GlIcon).props('name')).toBe(
- issuableTitleProps.statusIcon,
- );
- expect(stickyHeaderEl.text()).toContain('Open');
- expect(stickyHeaderEl.text()).toContain(issuableTitleProps.issuable.title);
+ expect(stickyHeaderEl.exists()).toBe(true);
+ expect(stickyHeaderEl.findComponent(GlBadge).props('variant')).toBe('success');
+ expect(stickyHeaderEl.findComponent(GlIcon).props('name')).toBe(
+ issuableTitleProps.statusIcon,
+ );
+ expect(stickyHeaderEl.text()).toContain('Open');
+ expect(stickyHeaderEl.findComponent(ConfidentialityBadge).exists()).toBe(false);
+ expect(stickyHeaderEl.text()).toContain(issuableTitleProps.issuable.title);
+ });
+
+ it('renders ConfidentialityBadge when issuable is confidential', async () => {
+ wrapper = createComponent({
+ ...mockIssuableShowProps,
+ issuable: {
+ ...mockIssuable,
+ confidential: true,
+ },
+ });
+
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
+ await nextTick();
+
+ const stickyHeaderEl = findStickyHeader();
+
+ expect(stickyHeaderEl.findComponent(ConfidentialityBadge).exists()).toBe(true);
+ });
});
});
});
diff --git a/spec/graphql/types/organizations/organization_user_badge_type_spec.rb b/spec/graphql/types/organizations/organization_user_badge_type_spec.rb
new file mode 100644
index 00000000000..1ea9b3ad1df
--- /dev/null
+++ b/spec/graphql/types/organizations/organization_user_badge_type_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['OrganizationUserBadge'], feature_category: :cell do
+ let(:expected_fields) { %w[text variant] }
+
+ specify { expect(described_class.graphql_name).to eq('OrganizationUserBadge') }
+ specify { expect(described_class).to have_graphql_fields(*expected_fields) }
+end
diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb
index af0f8a86b6c..457127f5bed 100644
--- a/spec/graphql/types/user_type_spec.rb
+++ b/spec/graphql/types/user_type_spec.rb
@@ -55,6 +55,7 @@ RSpec.describe GitlabSchema.types['User'], feature_category: :user_profile do
organization
jobTitle
createdAt
+ lastActivityOn
pronouns
ide
]
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
index 3e0fc1ffcb7..9a0f72838fb 100644
--- a/spec/helpers/nav_helper_spec.rb
+++ b/spec/helpers/nav_helper_spec.rb
@@ -150,7 +150,7 @@ RSpec.describe NavHelper, feature_category: :navigation do
context 'when user has new nav disabled' do
let(:user_preference) { false }
- specify { expect(subject).to eq false }
+ specify { expect(subject).to eq true }
end
context 'when user has new nav enabled' do
diff --git a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
index 93a4d0ca602..da24e9b7978 100644
--- a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
+++ b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
+RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob, feature_category: :database do
let(:connection) { Gitlab::Database.database_base_models[:main].connection }
describe '.generic_instance' do
diff --git a/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb b/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
index fe423b3639b..7ab50d47408 100644
--- a/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
+++ b/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
@@ -2,28 +2,83 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::DynamicModelHelpers do
+RSpec.describe Gitlab::Database::DynamicModelHelpers, feature_category: :database do
let(:including_class) { Class.new.include(described_class) }
let(:table_name) { Project.table_name }
let(:connection) { Project.connection }
describe '#define_batchable_model' do
- subject { including_class.new.define_batchable_model(table_name, connection: connection) }
+ subject(:model) { including_class.new.define_batchable_model(table_name, connection: connection) }
it 'is an ActiveRecord model' do
- expect(subject.ancestors).to include(ActiveRecord::Base)
+ expect(model.ancestors).to include(ActiveRecord::Base)
end
it 'includes EachBatch' do
- expect(subject.included_modules).to include(EachBatch)
+ expect(model.included_modules).to include(EachBatch)
end
it 'has the correct table name' do
- expect(subject.table_name).to eq(table_name)
+ expect(model.table_name).to eq(table_name)
end
it 'has the inheritance type column disable' do
- expect(subject.inheritance_column).to eq('_type_disabled')
+ expect(model.inheritance_column).to eq('_type_disabled')
+ end
+
+ context 'for primary key' do
+ subject(:model) do
+ including_class.new.define_batchable_model(table_name, connection: connection, primary_key: primary_key)
+ end
+
+ context 'when table primary key is a single column' do
+ let(:primary_key) { nil }
+
+ context 'when primary key is nil' do
+ it 'does not change the primary key from :id' do
+ expect(model.primary_key).to eq('id')
+ end
+ end
+
+ context 'when primary key is not nil' do
+ let(:primary_key) { 'other_column' }
+
+ it 'does not change the primary key from :id' do
+ expect(model.primary_key).to eq('id')
+ end
+ end
+ end
+
+ context 'when table has composite primary key' do
+ let(:primary_key) { nil }
+ let(:table_name) { :_test_composite_primary_key }
+
+ before do
+ connection.execute(<<~SQL)
+ DROP TABLE IF EXISTS #{table_name};
+
+ CREATE TABLE #{table_name} (
+ id integer NOT NULL,
+ partition_id integer NOT NULL,
+ PRIMARY KEY (id, partition_id)
+ );
+ SQL
+ end
+
+ context 'when primary key is nil' do
+ it 'does not change the primary key from nil' do
+ expect(model.primary_key).to be_nil
+ end
+ end
+
+ context 'when primary key is not nil' do
+ let(:primary_key) { 'id' }
+
+ it 'changes the primary key' do
+ expect(model.primary_key).to eq('id')
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
index 72d8a9c0403..65c5a7daeb2 100644
--- a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
+++ b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
@@ -94,9 +94,9 @@ RSpec.describe Gitlab::GithubImport::AttachmentsDownloader, feature_category: :i
end
end
- context 'when attachment is behind a redirect' do
- let_it_be(:file_url) { "https://github.com/test/project/assets/142635249/4b9f9c90-f060-4845-97cf-b24c558bcb11" }
- let(:redirect_url) { "https://https://github-production-user-asset-6210df.s3.amazonaws.com/142635249/740edb05293e.jpg" }
+ context 'when attachment is behind a github asset endpoint' do
+ let(:file_url) { "https://github.com/test/project/assets/142635249/4b9f9c90-f060-4845-97cf-b24c558bcb11" }
+ let(:redirect_url) { "https://github-production-user-asset-6210df.s3.amazonaws.com/142635249/740edb05293e.jpg" }
let(:sample_response) do
instance_double(HTTParty::Response, redirection?: true, headers: { location: redirect_url })
end
@@ -115,6 +115,8 @@ RSpec.describe Gitlab::GithubImport::AttachmentsDownloader, feature_category: :i
end
context 'when url is not a redirection' do
+ let(:file_url) { "https://github.com/test/project/assets/142635249/4b9f9c90-f060-4845-97cf-b24c558bcb11.jpg" }
+
let(:sample_response) do
instance_double(HTTParty::Response, code: 200, redirection?: false)
end
@@ -125,8 +127,13 @@ RSpec.describe Gitlab::GithubImport::AttachmentsDownloader, feature_category: :i
.and_return sample_response
end
- it 'raises upon unsuccessful redirection' do
- expect { downloader.perform }.to raise_error("expected a redirect response, got #{sample_response.code}")
+ it 'queries with original file_url' do
+ expect(Gitlab::HTTP).to receive(:perform_request)
+ .with(Net::HTTP::Get, file_url, stream_body: true).and_yield(chunk_double)
+
+ file = downloader.perform
+
+ expect(File.exist?(file.path)).to eq(true)
end
end
diff --git a/spec/lib/gitlab/other_markup_spec.rb b/spec/lib/gitlab/other_markup_spec.rb
index 607d4cc9fd5..cdeaed6d368 100644
--- a/spec/lib/gitlab/other_markup_spec.rb
+++ b/spec/lib/gitlab/other_markup_spec.rb
@@ -111,6 +111,22 @@ RSpec.describe Gitlab::OtherMarkup, feature_category: :wiki do
end
end
+ context 'RedCloth markup' do
+ it 'renders textile correctly' do
+ test_text = '"This is *my* text."'
+ expected_res = "<p>&#8220;This is <strong>my</strong> text.&#8221;</p>"
+ expect(RedCloth.new(test_text).to_html).to eq(expected_res)
+ end
+
+ it 'protects against malicious backtracking' do
+ test_text = '<A' + ('A' * 54773)
+
+ expect do
+ Timeout.timeout(3.seconds) { RedCloth.new(test_text, [:sanitize_html]).to_html }
+ end.not_to raise_error
+ end
+ end
+
def render(...)
described_class.render(...)
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb
deleted file mode 100644
index a2d86fc5044..00000000000
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountProjectsWithJiraDvcsIntegrationMetric,
- feature_category: :integrations do
- describe 'metric value and query' do
- let_it_be_with_reload(:project_1) { create(:project) }
- let_it_be_with_reload(:project_2) { create(:project) }
- let_it_be_with_reload(:project_3) { create(:project) }
-
- before do
- project_1.feature_usage.log_jira_dvcs_integration_usage(cloud: false)
- project_2.feature_usage.log_jira_dvcs_integration_usage(cloud: false)
- project_3.feature_usage.log_jira_dvcs_integration_usage(cloud: true)
- end
-
- context 'when counting cloud integrations' do
- let(:expected_value) { 1 }
- let(:expected_query) do
- 'SELECT COUNT("project_feature_usages"."project_id") FROM "project_feature_usages" ' \
- 'WHERE "project_feature_usages"."jira_dvcs_cloud_last_sync_at" IS NOT NULL'
- end
-
- it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all', options: { cloud: true } }
- end
-
- context 'when counting non-cloud integrations' do
- let(:expected_value) { 2 }
- let(:expected_query) do
- 'SELECT COUNT("project_feature_usages"."project_id") FROM "project_feature_usages" ' \
- 'WHERE "project_feature_usages"."jira_dvcs_server_last_sync_at" IS NOT NULL'
- end
-
- it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all', options: { cloud: false } }
- end
- end
-
- it "raises an exception if option is not present" do
- expect do
- described_class.new(options: {}, time_frame: 'all')
- end.to raise_error(ArgumentError, %r{must be a boolean})
- end
-
- it "raises an exception if option has invalid value" do
- expect do
- described_class.new(options: { cloud: 'yes' }, time_frame: 'all')
- end.to raise_error(ArgumentError, %r{must be a boolean})
- end
-end
diff --git a/spec/models/ci/catalog/components_project_spec.rb b/spec/models/ci/catalog/components_project_spec.rb
index 4a7182a24d6..79e1a113e47 100644
--- a/spec/models/ci/catalog/components_project_spec.rb
+++ b/spec/models/ci/catalog/components_project_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe Ci::Catalog::ComponentsProject, feature_category: :pipeline_compo
it 'does not fetch more paths than the limit' do
paths = components_project.fetch_component_paths(project.default_branch, limit: 1)
- expect(paths.size).to be(1)
+ expect(paths.size).to eq(1)
end
end
@@ -53,7 +53,11 @@ RSpec.describe Ci::Catalog::ComponentsProject, feature_category: :pipeline_compo
context 'with valid component paths' do
where(:path, :name) do
'templates/secret-detection.yml' | 'secret-detection'
+ 'templates/secret_detection.yml' | 'secret_detection'
+ 'templates/secret_detection123.yml' | 'secret_detection123'
+ 'templates/secret-detection-123.yml' | 'secret-detection-123'
'templates/dast/template.yml' | 'dast'
+ 'templates/d-a-s_t/template.yml' | 'd-a-s_t'
'templates/template.yml' | 'template'
'templates/blank-yaml.yml' | 'blank-yaml'
end
diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb
index 9df6c9d663d..34268b92e1d 100644
--- a/spec/models/ci/catalog/resource_spec.rb
+++ b/spec/models/ci/catalog/resource_spec.rb
@@ -19,7 +19,13 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
let_it_be(:release3) { create(:release, project: project, released_at: tomorrow) }
it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:components).class_name('Ci::Catalog::Resources::Component') }
+
+ it do
+ is_expected.to(
+ have_many(:components).class_name('Ci::Catalog::Resources::Component').with_foreign_key(:catalog_resource_id)
+ )
+ end
+
it { is_expected.to have_many(:versions).class_name('Ci::Catalog::Resources::Version') }
it { is_expected.to delegate_method(:avatar_path).to(:project) }
@@ -122,6 +128,28 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
end
end
+ describe '#publish!' do
+ context 'when the catalog resource is in draft state' do
+ it 'updates the state of the catalog resource to published' do
+ expect(resource.state).to eq('draft')
+
+ resource.publish!
+
+ expect(resource.reload.state).to eq('published')
+ end
+ end
+
+ context 'when a catalog resource already has a published state' do
+ it 'leaves the state as published' do
+ resource.update!(state: 'published')
+
+ resource.publish!
+
+ expect(resource.state).to eq('published')
+ end
+ end
+ end
+
describe '#unpublish!' do
context 'when the catalog resource is in published state' do
it 'updates the state to draft' do
diff --git a/spec/models/ci/catalog/resources/component_spec.rb b/spec/models/ci/catalog/resources/component_spec.rb
index e8c92ce0788..2ee91175920 100644
--- a/spec/models/ci/catalog/resources/component_spec.rb
+++ b/spec/models/ci/catalog/resources/component_spec.rb
@@ -9,6 +9,23 @@ RSpec.describe Ci::Catalog::Resources::Component, type: :model, feature_category
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:version).class_name('Ci::Catalog::Resources::Version') }
+ it_behaves_like 'a BulkInsertSafe model', described_class do
+ let_it_be(:project) { create(:project, :readme, description: 'project description') }
+ let_it_be(:catalog_resource) { create(:ci_catalog_resource, project: project) }
+ let_it_be(:version) { create(:ci_catalog_resource_version, project: project) }
+
+ let(:valid_items_for_bulk_insertion) do
+ build_list(:ci_catalog_resource_component, 10) do |component|
+ component.catalog_resource = catalog_resource
+ component.version = version
+ component.project = project
+ component.created_at = Time.zone.now
+ end
+ end
+
+ let(:invalid_items_for_bulk_insertion) { [] }
+ end
+
describe 'validations' do
it { is_expected.to validate_presence_of(:catalog_resource) }
it { is_expected.to validate_presence_of(:project) }
diff --git a/spec/models/concerns/ci/partitionable/switch_spec.rb b/spec/models/concerns/ci/partitionable/switch_spec.rb
index 0041a33e50e..c6e2ed265bd 100644
--- a/spec/models/concerns/ci/partitionable/switch_spec.rb
+++ b/spec/models/concerns/ci/partitionable/switch_spec.rb
@@ -31,8 +31,6 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
end
before do
- allow(ActiveSupport::DescendantsTracker).to receive(:store_inherited)
-
create_tables(<<~SQL)
CREATE TABLE _test_ci_jobs_metadata(
id serial NOT NULL PRIMARY KEY,
@@ -78,6 +76,15 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
)
end
+ # the models defined here are leaked to other tests through
+ # `ActiveRecord::Base.descendants` and we need to counter the side effects
+ # from this. We redefine the method so that we don't check the FF existence
+ # outside of the examples here.
+ # `ActiveSupport::DescendantsTracker.clear` doesn't work with cached classes.
+ after do
+ model.define_singleton_method(:routing_table_enabled?) { false }
+ end
+
it { expect(model).not_to be_routing_class }
it { expect(partitioned_model).to be_routing_class }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 37b4a9011f4..2bca73545d0 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -1659,6 +1659,12 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
end
+ it 'returns true for a user in parent group' do
+ subgroup = create(:group, parent: group)
+
+ expect(subgroup.member?(user)).to be_truthy
+ end
+
context 'in shared group' do
let(:shared_group) { create(:group) }
let(:member_shared) { create(:user) }
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index fdd8a610fe4..b4941c71d6a 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -591,6 +591,22 @@ RSpec.describe Member, feature_category: :groups_and_projects do
it { is_expected.not_to include @member_with_minimal_access }
it { is_expected.not_to include awaiting_group_member }
it { is_expected.not_to include awaiting_project_member }
+
+ context 'when minimal_access is true' do
+ subject { described_class.without_invites_and_requests(minimal_access: true) }
+
+ it { is_expected.to include @owner }
+ it { is_expected.to include @maintainer }
+ it { is_expected.not_to include @invited_member }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.not_to include @requested_member }
+ it { is_expected.to include @accepted_request_member }
+ it { is_expected.to include @blocked_maintainer }
+ it { is_expected.to include @blocked_developer }
+ it { is_expected.to include @member_with_minimal_access }
+ it { is_expected.not_to include awaiting_group_member }
+ it { is_expected.not_to include awaiting_project_member }
+ end
end
describe '.connected_to_user' do
diff --git a/spec/models/members/members/members_with_parents_spec.rb b/spec/models/members/members/members_with_parents_spec.rb
new file mode 100644
index 00000000000..46c934c932f
--- /dev/null
+++ b/spec/models/members/members/members_with_parents_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Members::MembersWithParents, feature_category: :groups_and_projects do
+ let_it_be(:group) { create(:group, :nested) }
+ let_it_be(:maintainer) { group.parent.add_maintainer(create(:user)) }
+ let_it_be(:developer) { group.add_developer(create(:user)) }
+ let_it_be(:pending_maintainer) { create(:group_member, :awaiting, :maintainer, group: group.parent) }
+ let_it_be(:pending_developer) { create(:group_member, :awaiting, :developer, group: group) }
+ let_it_be(:invited_member) { create(:group_member, :invited, group: group) }
+ let_it_be(:inactive_developer) { group.add_developer(create(:user, :deactivated)) }
+ let_it_be(:minimal_access) { create(:group_member, :minimal_access, group: group) }
+
+ describe '#all_members' do
+ subject(:all_members) { described_class.new(group).all_members }
+
+ it 'returns all members for group and group parents' do
+ expect(all_members).to contain_exactly(
+ developer,
+ maintainer,
+ pending_maintainer,
+ pending_developer,
+ invited_member,
+ inactive_developer,
+ minimal_access
+ )
+ end
+ end
+
+ describe '#members' do
+ let(:arguments) { {} }
+
+ subject(:members) { described_class.new(group).members(**arguments) }
+
+ using Rspec::Parameterized::TableSyntax
+
+ where(:arguments, :expected_members) do
+ [
+ [
+ {},
+ lazy { [developer, maintainer, inactive_developer] }
+ ],
+ [
+ # minimal access is Premium, so in FOSS we will not include minimal access member
+ { minimal_access: true },
+ lazy { [developer, maintainer, inactive_developer] }
+ ],
+ [
+ { active_users: true },
+ lazy { [developer, maintainer] }
+ ]
+ ]
+ end
+
+ with_them do
+ it 'returns expected members' do
+ expect(members).to contain_exactly(*expected_members)
+ expect(members).not_to include(*(group.members - expected_members))
+ end
+ end
+
+ context 'when active_users: true and minimal_access: true' do
+ let(:arguments) { { active_users: true, minimal_access: true } }
+
+ it 'raises an error' do
+ expect { members }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'with group sharing' do
+ let_it_be(:shared_with_group) { create(:group) }
+
+ let_it_be(:shared_with_group_maintainer) do
+ shared_with_group.add_maintainer(create(:user))
+ end
+
+ let_it_be(:shared_with_group_developer) do
+ shared_with_group.add_developer(create(:user))
+ end
+
+ before do
+ create(:group_group_link, shared_group: group, shared_with_group: shared_with_group)
+ end
+
+ it 'returns shared with group members' do
+ expect(members).to(include(shared_with_group_maintainer))
+ expect(members).to(include(shared_with_group_developer))
+ end
+ end
+ end
+end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index a460e47a0e5..590abebb764 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -569,6 +569,48 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
end
end
end
+
+ describe "#default_branch_protection_settings" do
+ let(:default_branch_protection_defaults) { {} }
+ let(:namespace_setting) { create(:namespace_settings, default_branch_protection_defaults: default_branch_protection_defaults) }
+ let(:namespace) { create(:namespace, namespace_settings: namespace_setting) }
+ let(:group) { create(:group, namespace_settings: namespace_setting) }
+
+ before do
+ stub_application_setting(default_branch_protection_defaults: Gitlab::Access::BranchProtection.protected_against_developer_pushes)
+ end
+
+ context 'for a namespace' do
+ it 'returns the instance level setting' do
+ expected_settings = Gitlab::Access::BranchProtection.protected_against_developer_pushes.deep_stringify_keys
+ settings = namespace.default_branch_protection_settings.to_hash
+
+ expect(settings).to eq(expected_settings)
+ end
+ end
+
+ context 'for a group' do
+ context 'that has not altered the default value' do
+ it 'returns the instance level setting' do
+ expected_settings = Gitlab::Access::BranchProtection.protected_against_developer_pushes.deep_stringify_keys
+ settings = group.default_branch_protection_settings.to_hash
+
+ expect(settings).to eq(expected_settings)
+ end
+ end
+
+ context 'that has altered the default value' do
+ let(:default_branch_protection_defaults) { Gitlab::Access::BranchProtection.protected_fully.deep_stringify_keys }
+
+ it 'returns the group level setting' do
+ expected_settings = default_branch_protection_defaults
+ settings = group.default_branch_protection_settings.to_hash
+
+ expect(settings).to eq(expected_settings)
+ end
+ end
+ end
+ end
end
describe "Respond to" do
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 42c43a59fe2..48db41ea8e3 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -872,4 +872,26 @@ RSpec.describe ProjectPresenter do
end
end
end
+
+ describe '#has_review_app?' do
+ subject { presenter.has_review_app? }
+
+ let_it_be(:project) { create(:project, :repository) }
+
+ context 'when review apps exist' do
+ let!(:environment) do
+ create(:environment, :with_review_app, project: project)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when review apps do not exist' do
+ let!(:environment) do
+ create(:environment, project: project)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb
index d02158382eb..c243e0613ad 100644
--- a/spec/requests/api/graphql/organizations/organization_query_spec.rb
+++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb
@@ -79,7 +79,10 @@ RSpec.describe 'getting organization information', feature_category: :cell do
<<~FIELDS
organizationUsers {
nodes {
- badges
+ badges {
+ text
+ variant
+ }
id
user {
id
@@ -94,7 +97,7 @@ RSpec.describe 'getting organization information', feature_category: :cell do
organization_user_node = graphql_data_at(:organization, :organizationUsers, :nodes).first
expected_attributes = {
- "badges" => ["It's you!"],
+ "badges" => [{ "text" => "It's you!", "variant" => 'muted' }],
"id" => organization_user.to_global_id.to_s,
"user" => { "id" => user.to_global_id.to_s }
}
diff --git a/spec/serializers/review_app_setup_entity_spec.rb b/spec/serializers/review_app_setup_entity_spec.rb
index 9b068a2e9dd..9c6d54fd612 100644
--- a/spec/serializers/review_app_setup_entity_spec.rb
+++ b/spec/serializers/review_app_setup_entity_spec.rb
@@ -22,6 +22,10 @@ RSpec.describe ReviewAppSetupEntity do
expect(subject).to include(:can_setup_review_app)
end
+ it 'contains has_review_app' do
+ expect(subject).to include(:has_review_app)
+ end
+
context 'when the user can setup a review app' do
before do
allow(presenter).to receive(:can_setup_review_app?).and_return(true)
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index 1619e1db6c6..474d6ec4a9b 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -321,7 +321,9 @@ RSpec.describe ApplicationSettings::UpdateService, feature_category: :shared do
let(:params) { { default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE } }
it "updates default_branch_protection_defaults from the default_branch_protection param" do
- expect { subject.execute }.to change { application_settings.default_branch_protection_defaults }.from({}).to(expected)
+ default_value = ::Gitlab::Access::BranchProtection.protected_fully.deep_stringify_keys
+
+ expect { subject.execute }.to change { application_settings.default_branch_protection_defaults }.from(default_value).to(expected)
end
end
diff --git a/spec/services/ci/catalog/resources/versions/create_service_spec.rb b/spec/services/ci/catalog/resources/versions/create_service_spec.rb
new file mode 100644
index 00000000000..e614a74a4a1
--- /dev/null
+++ b/spec/services/ci/catalog/resources/versions/create_service_spec.rb
@@ -0,0 +1,180 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Resources::Versions::CreateService, feature_category: :pipeline_composition do
+ describe '#execute' do
+ let(:files) do
+ {
+ 'templates/secret-detection.yml' => "spec:\n inputs:\n website:\n---\nimage: alpine_1",
+ 'templates/dast/template.yml' => 'image: alpine_2',
+ 'templates/blank-yaml.yml' => '',
+ 'templates/dast/sub-folder/template.yml' => 'image: alpine_3',
+ 'templates/template.yml' => "spec:\n inputs:\n environment:\n---\nimage: alpine_6",
+ 'tests/test.yml' => 'image: alpine_7',
+ 'README.md' => 'Read me'
+ }
+ end
+
+ let(:project) do
+ create(
+ :project, :custom_repo,
+ description: 'Simple and Complex components',
+ files: files
+ )
+ end
+
+ let(:release) { create(:release, project: project, sha: project.repository.root_ref_sha) }
+ let!(:catalog_resource) { create(:ci_catalog_resource, project: project) }
+
+ context 'when the project is not a catalog resource' do
+ it 'does not create a version' do
+ project = create(:project, :repository)
+ release = create(:release, project: project, sha: project.repository.root_ref_sha)
+
+ response = described_class.new(release).execute
+
+ expect(response).to be_error
+ expect(response.message).to include('Project is not a catalog resource')
+ end
+ end
+
+ context 'when the catalog resource has different types of components and a release' do
+ it 'creates a version for the release' do
+ response = described_class.new(release).execute
+
+ expect(response).to be_success
+
+ version = Ci::Catalog::Resources::Version.last
+
+ expect(version.release).to eq(release)
+ expect(version.catalog_resource).to eq(catalog_resource)
+ expect(version.catalog_resource.project).to eq(project)
+ end
+
+ it 'marks the catalog resource as published' do
+ described_class.new(release).execute
+
+ expect(catalog_resource.reload.state).to eq('published')
+ end
+
+ context 'when the ci_catalog_create_metadata feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_catalog_create_metadata: false)
+ end
+
+ it 'does not create components' do
+ expect(Ci::Catalog::Resources::Component).not_to receive(:bulk_insert!).and_call_original
+ expect(project.ci_components.count).to eq(0)
+
+ response = described_class.new(release).execute
+
+ expect(response).to be_success
+ expect(project.ci_components.count).to eq(0)
+ end
+ end
+
+ context 'when the ci_catalog_create_metadata feature flag is enabled' do
+ context 'when there are more than 10 components' do
+ let(:files) do
+ {
+ 'templates/secret11.yml' => '',
+ 'templates/secret10.yml' => '',
+ 'templates/secret8.yml' => '',
+ 'templates/secret7.yml' => '',
+ 'templates/secret6.yml' => '',
+ 'templates/secret5.yml' => '',
+ 'templates/secret4.yml' => '',
+ 'templates/secret3.yml' => '',
+ 'templates/secret2.yml' => '',
+ 'templates/secret1.yml' => '',
+ 'templates/secret0.yml' => '',
+ 'README.md' => 'Read me'
+ }
+ end
+
+ it 'does not create components' do
+ response = described_class.new(release).execute
+
+ expect(response).to be_error
+ expect(response.message).to include('Release cannot contain more than 10 components')
+ expect(project.ci_components.count).to eq(0)
+ end
+ end
+
+ it 'bulk inserts all the components' do
+ expect(Ci::Catalog::Resources::Component).to receive(:bulk_insert!).and_call_original
+
+ described_class.new(release).execute
+ end
+
+ it 'creates components for the catalog resource' do
+ expect(project.ci_components.count).to eq(0)
+ response = described_class.new(release).execute
+
+ expect(response).to be_success
+
+ version = Ci::Catalog::Resources::Version.last
+
+ expect(project.ci_components.count).to eq(4)
+ expect(project.ci_components.first.name).to eq('blank-yaml')
+ expect(project.ci_components.first.project).to eq(version.project)
+ expect(project.ci_components.first.inputs).to eq({})
+ expect(project.ci_components.first.catalog_resource).to eq(version.catalog_resource)
+ expect(project.ci_components.first.version).to eq(version)
+ expect(project.ci_components.first.path).to eq('templates/blank-yaml.yml')
+ expect(project.ci_components.second.name).to eq('dast')
+ expect(project.ci_components.second.project).to eq(version.project)
+ expect(project.ci_components.second.inputs).to eq({})
+ expect(project.ci_components.second.catalog_resource).to eq(version.catalog_resource)
+ expect(project.ci_components.second.version).to eq(version)
+ expect(project.ci_components.second.path).to eq('templates/dast/template.yml')
+ expect(project.ci_components.third.name).to eq('secret-detection')
+ expect(project.ci_components.third.project).to eq(version.project)
+ expect(project.ci_components.third.inputs).to eq({ "website" => nil })
+ expect(project.ci_components.third.catalog_resource).to eq(version.catalog_resource)
+ expect(project.ci_components.third.version).to eq(version)
+ expect(project.ci_components.third.path).to eq('templates/secret-detection.yml')
+ expect(project.ci_components.fourth.name).to eq('template')
+ expect(project.ci_components.fourth.project).to eq(version.project)
+ expect(project.ci_components.fourth.inputs).to eq({ "environment" => nil })
+ expect(project.ci_components.fourth.catalog_resource).to eq(version.catalog_resource)
+ expect(project.ci_components.fourth.version).to eq(version)
+ expect(project.ci_components.fourth.path).to eq('templates/template.yml')
+ end
+ end
+ end
+
+ context 'with invalid data' do
+ let_it_be(:files) do
+ {
+ 'templates/secret-detection.yml' => 'some: invalid: syntax',
+ 'README.md' => 'Read me'
+ }
+ end
+
+ it 'returns an error' do
+ response = described_class.new(release).execute
+
+ expect(response).to be_error
+ expect(response.message).to include('mapping values are not allowed in this context')
+ end
+ end
+
+ context 'when one or more components are invalid' do
+ let_it_be(:files) do
+ {
+ 'templates/secret-detection.yml' => "spec:\n inputs:\n - website\n---\nimage: alpine_1",
+ 'README.md' => 'Read me'
+ }
+ end
+
+ it 'returns an error' do
+ response = described_class.new(release).execute
+
+ expect(response).to be_error
+ expect(response.message).to include('Inputs must be a valid json schema')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index dde9498eb2c..9bd10d56d8c 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -5,7 +5,6 @@ RSpec.shared_examples 'multiple issue boards' do
context 'authorized user' do
before do
- stub_feature_flags(apollo_boards: false)
parent.add_maintainer(user)
login_as(user)
@@ -124,7 +123,6 @@ RSpec.shared_examples 'multiple issue boards' do
context 'unauthorized user' do
before do
- stub_feature_flags(apollo_boards: false)
visit boards_path
wait_for_requests
end
diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
index c32e758d921..b653b4e265a 100644
--- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
@@ -50,6 +50,7 @@ RSpec.shared_examples "a user type with merge request interaction type" do
organization
jobTitle
createdAt
+ lastActivityOn
pronouns
ide
]
diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
index 06875440cca..90fa0c98376 100644
--- a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
@@ -26,6 +26,18 @@ RSpec.shared_examples 'graphql issue list request spec' do
issue_b.assignee_ids = another_user.id
end
+ context 'when filtering by state' do
+ context 'when filtering by locked state' do
+ let(:issue_filter_params) { { state: :locked } }
+
+ it 'returns an error message' do
+ post_query
+
+ expect_graphql_errors_to_include(Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE)
+ end
+ end
+ end
+
context 'when filtering by assignees' do
context 'when both assignee_username filters are provided' do
let(:issue_filter_params) do
diff --git a/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb
index a9c422c8f2d..82f98b883dc 100644
--- a/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb
@@ -46,6 +46,14 @@ RSpec.shared_examples 'graphql work item list request spec' do
expect(work_item_ids).to include(closed_work_item.to_global_id.to_s)
end
end
+
+ context 'when filtering by state locked' do
+ let(:item_filter_params) { { state: :locked } }
+
+ it 'return an error message' do
+ expect_graphql_errors_to_include(Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE)
+ end
+ end
end
context 'when filtering by type' do
diff --git a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb
index d749479544d..fa111ca5811 100644
--- a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb
@@ -5,7 +5,6 @@ RSpec.shared_examples 'multiple and scoped issue boards' do |route_definition|
context 'multiple issue boards' do
before do
- stub_feature_flags(apollo_boards: false)
board_parent.add_reporter(user)
stub_licensed_features(multiple_group_issue_boards: true)
end
diff --git a/spec/support/shared_examples/views/themed_layout_examples.rb b/spec/support/shared_examples/views/themed_layout_examples.rb
index 599fd141dd7..ffbc9026240 100644
--- a/spec/support/shared_examples/views/themed_layout_examples.rb
+++ b/spec/support/shared_examples/views/themed_layout_examples.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples "a layout which reflects the application theme setting" do
it 'renders with the default theme' do
render
- expect(rendered).to have_selector("body.#{default_theme_class}")
+ expect(rendered).to have_selector("html.#{default_theme_class}")
end
end
@@ -24,10 +24,10 @@ RSpec.shared_examples "a layout which reflects the application theme setting" do
render
if chosen_theme.css_class != default_theme_class
- expect(rendered).not_to have_selector("body.#{default_theme_class}")
+ expect(rendered).not_to have_selector("html.#{default_theme_class}")
end
- expect(rendered).to have_selector("body.#{chosen_theme.css_class}")
+ expect(rendered).to have_selector("html.#{chosen_theme.css_class}")
end
end
end
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index 00e528b0caf..2da90ddbd67 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -243,6 +243,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
[:analytics_instrumentation] | '+data-track-action' | ['components/welcome.vue']
[:analytics_instrumentation] | '+ data: { track_label:' | ['admin/groups/_form.html.haml']
[:analytics_instrumentation] | '+ Gitlab::Tracking.event' | ['dashboard/todos_controller.rb', 'admin/groups/_form.html.haml']
+ [:analytics_instrumentation] | '+ Gitlab::Tracking.event("c", "a")' | ['dashboard/todos_controller.rb', 'admin/groups/_form.html.haml']
[:database, :backend, :analytics_instrumentation] | '+ count(User.active)' | ['usage_data.rb', 'lib/gitlab/usage_data.rb', 'ee/lib/ee/gitlab/usage_data.rb']
[:database, :backend, :analytics_instrumentation] | '+ estimate_batch_distinct_count(User.active)' | ['usage_data.rb']
[:backend, :analytics_instrumentation] | '+ alt_usage_data(User.active)' | ['lib/gitlab/usage_data.rb']
diff --git a/tooling/danger/project_helper.rb b/tooling/danger/project_helper.rb
index d0cea5516ac..2b781b58a64 100644
--- a/tooling/danger/project_helper.rb
+++ b/tooling/danger/project_helper.rb
@@ -131,7 +131,7 @@ module Tooling
generator_templates/usage_metric_definition/metric_definition\.yml)\z}x => [:backend, :analytics_instrumentation],
%r{gitlab/usage_data(_spec)?\.rb} => [:analytics_instrumentation],
[%r{\.haml\z}, %r{data: \{ track}] => [:analytics_instrumentation],
- [%r{\.(rb|haml)\z}, %r{Gitlab::Tracking\.(event|enabled\?|options)$}] => [:analytics_instrumentation],
+ [%r{\.(rb|haml)\z}, %r{Gitlab::Tracking\.(event|enabled\?|options)}] => [:analytics_instrumentation],
[%r{\.(vue|js)\z}, %r{(Tracking.event|/\btrack\(/|data-track-action)}] => [:analytics_instrumentation],
%r{\A((ee|jh)/)?app/(?!assets|views)[^/]+} => :backend,
diff --git a/vendor/languages.yml b/vendor/languages.yml
index 0a6f78ebe5d..4b1d9ffdb3b 100755
--- a/vendor/languages.yml
+++ b/vendor/languages.yml
@@ -2750,7 +2750,7 @@ Kit:
language_id: 188
Kotlin:
type: programming
- color: "#F18E33"
+ color: "#7F52FF"
extensions:
- ".kt"
- ".ktm"
diff --git a/yarn.lock b/yarn.lock
index 36380d7144d..ee37317309a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1179,10 +1179,10 @@
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8"
integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==
-"@eslint/eslintrc@^2.1.2":
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396"
- integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==
+"@eslint/eslintrc@^2.1.3":
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.3.tgz#797470a75fe0fbd5a53350ee715e85e87baff22d"
+ integrity sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
@@ -1194,10 +1194,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
-"@eslint/js@8.52.0":
- version "8.52.0"
- resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.52.0.tgz#78fe5f117840f69dc4a353adf9b9cd926353378c"
- integrity sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==
+"@eslint/js@8.53.0":
+ version "8.53.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.53.0.tgz#bea56f2ed2b5baea164348ff4d5a879f6f81f20d"
+ integrity sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==
"@floating-ui/core@^1.2.6":
version "1.2.6"
@@ -6244,15 +6244,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
-eslint@8.52.0:
- version "8.52.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.52.0.tgz#d0cd4a1fac06427a61ef9242b9353f36ea7062fc"
- integrity sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==
+eslint@8.53.0:
+ version "8.53.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.53.0.tgz#14f2c8244298fcae1f46945459577413ba2697ce"
+ integrity sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.6.1"
- "@eslint/eslintrc" "^2.1.2"
- "@eslint/js" "8.52.0"
+ "@eslint/eslintrc" "^2.1.3"
+ "@eslint/js" "8.53.0"
"@humanwhocodes/config-array" "^0.11.13"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"