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-05-30 18:09:38 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-30 18:09:38 +0300
commitaad78b57896307fff73539719d510a8c82be77f9 (patch)
tree22cf373de6921743c23dd694945f0a349880b4a2
parent96e4317f73c749841cc1d808b788d43aa3407a4a (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/CODEOWNERS72
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml25
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--.rubocop_todo/rails/lexically_scoped_action_filter.yml1
-rw-r--r--.rubocop_todo/rspec/context_wording.yml2
-rw-r--r--.rubocop_todo/rspec/missing_feature_category.yml2
-rw-r--r--.rubocop_todo/rspec/return_from_stub.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue5
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/constants.js1
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/index.js141
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/options.js142
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_details.vue13
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_managers_detail.vue127
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_managers_table.vue71
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_managers.query.graphql13
-rw-r--r--app/assets/javascripts/drawio/constants.js15
-rw-r--r--app/assets/javascripts/drawio/drawio_editor.js19
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue7
-rw-r--r--app/assets/javascripts/pages/shared/wikis/edit.js1
-rw-r--r--app/assets/javascripts/super_sidebar/components/context_switcher.vue1
-rw-r--r--app/assets/javascripts/super_sidebar/components/groups_list.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/items_list.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue11
-rw-r--r--app/assets/javascripts/super_sidebar/components/projects_list.vue4
-rw-r--r--app/controllers/concerns/integrations/actions.rb9
-rw-r--r--app/controllers/concerns/wiki_actions.rb3
-rw-r--r--app/controllers/groups/settings/integrations_controller.rb4
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb7
-rw-r--r--app/graphql/types/ci/catalog/resource_type.rb12
-rw-r--r--app/helpers/application_settings_helper.rb2
-rw-r--r--app/helpers/registrations_helper.rb3
-rw-r--r--app/models/application_setting.rb5
-rw-r--r--app/models/application_setting_implementation.rb2
-rw-r--r--app/models/ci/catalog/resource.rb2
-rw-r--r--app/views/admin/application_settings/_diagramsnet.html.haml25
-rw-r--r--app/views/admin/application_settings/general.html.haml1
-rw-r--r--app/views/projects/commits/_commits.html.haml2
-rw-r--r--app/workers/concerns/worker_attributes.rb19
-rw-r--r--app/workers/packages/cleanup/delete_orphaned_dependencies_worker.rb6
-rw-r--r--config/feature_flags/worker/defer_sidekiq_workers_on_database_health_signal.yml (renamed from config/feature_flags/development/packages_delete_orphaned_dependencies_worker.yml)12
-rw-r--r--db/migrate/20230329235300_add_diagramsnet_to_application_settings.rb12
-rw-r--r--db/migrate/20230406115900_add_diagramsnet_text_limit.rb13
-rw-r--r--db/migrate/20230519103034_truncate_schema_inconsistencies_table.rb13
-rw-r--r--db/migrate/20230519112106_add_diff_column_to_schema_inconsistencies.rb12
-rw-r--r--db/migrate/20230519135414_add_text_limit_for_diff.rb13
-rw-r--r--db/post_migrate/20230521521419_drop_merge_request_state_id_temp_index.rb16
-rw-r--r--db/schema_migrations/202303292353001
-rw-r--r--db/schema_migrations/202304061159001
-rw-r--r--db/schema_migrations/202305191030341
-rw-r--r--db/schema_migrations/202305191121061
-rw-r--r--db/schema_migrations/202305191354141
-rw-r--r--db/schema_migrations/202305215214191
-rw-r--r--db/structure.sql7
-rw-r--r--doc/administration/integration/diagrams_net.md53
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/api/settings.md6
-rw-r--r--doc/api/users.md11
-rw-r--r--doc/architecture/blueprints/runner_tokens/index.md2
-rw-r--r--doc/ci/examples/index.md2
-rw-r--r--doc/user/markdown.md5
-rw-r--r--doc/user/permissions.md1
-rw-r--r--lib/api/settings.rb4
-rw-r--r--lib/gitlab/database/load_balancing/host.rb81
-rw-r--r--lib/gitlab/database/schema_validation/schema_inconsistency.rb2
-rw-r--r--lib/gitlab/database/schema_validation/track_inconsistency.rb31
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/defer_jobs.rb1
-rw-r--r--lib/tasks/gitlab/tw/codeowners.rake6
-rw-r--r--locale/gitlab.pot33
-rw-r--r--scripts/utils.sh7
-rw-r--r--spec/controllers/admin/integrations_controller_spec.rb26
-rw-r--r--spec/controllers/groups/settings/integrations_controller_spec.rb12
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb34
-rw-r--r--spec/controllers/projects/merge_requests/creations_controller_spec.rb2
-rw-r--r--spec/factories/gitlab/database/background_migration/schema_inconsistencies.rb1
-rw-r--r--spec/features/commits/user_view_commits_spec.rb4
-rw-r--r--spec/features/merge_request/user_comments_on_merge_request_spec.rb3
-rw-r--r--spec/frontend/__helpers__/test_constants.js2
-rw-r--r--spec/frontend/boards/components/board_form_spec.js239
-rw-r--r--spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/index_spec.js27
-rw-r--r--spec/frontend/ci/pipeline_editor/mock_data.js36
-rw-r--r--spec/frontend/ci/pipeline_editor/options_spec.js27
-rw-r--r--spec/frontend/ci/runner/components/runner_details_spec.js26
-rw-r--r--spec/frontend/ci/runner/components/runner_details_tabs_spec.js12
-rw-r--r--spec/frontend/ci/runner/components/runner_managers_detail_spec.js188
-rw-r--r--spec/frontend/ci/runner/components/runner_managers_table_spec.js123
-rw-r--r--spec/frontend/ci/runner/mock_data.js2
-rw-r--r--spec/frontend/drawio/drawio_editor_spec.js12
-rw-r--r--spec/frontend/fixtures/runner.rb19
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js20
-rw-r--r--spec/graphql/types/ci/catalog/resource_type_spec.rb2
-rw-r--r--spec/helpers/ci/pipeline_editor_helper_spec.rb4
-rw-r--r--spec/helpers/registrations_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/database/load_balancing/host_spec.rb123
-rw-r--r--spec/lib/gitlab/database/schema_validation/schema_inconsistency_spec.rb1
-rw-r--r--spec/lib/gitlab/database/schema_validation/track_inconsistency_spec.rb36
-rw-r--r--spec/models/application_setting_spec.rb20
-rw-r--r--spec/models/ci/catalog/resource_spec.rb2
-rw-r--r--spec/requests/api/settings_spec.rb15
-rw-r--r--spec/requests/projects/wikis_controller_spec.rb32
-rw-r--r--spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb3
-rw-r--r--spec/workers/concerns/worker_attributes_spec.rb35
-rw-r--r--spec/workers/packages/cleanup/delete_orphaned_dependencies_worker_spec.rb10
-rw-r--r--vendor/gems/attr_encrypted/.gitlab-ci.yml2
108 files changed, 1743 insertions, 523 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index b6ab141a649..6a494d30c70 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -401,16 +401,15 @@ lib/gitlab/checks/**
/doc/administration/audit_reports.md @eread
/doc/administration/auditor_users.md @jglassman1
/doc/administration/auth/ @jglassman1
-/doc/administration/cicd.md @drcatherinepope
+/doc/administration/cicd.md @marcel.amirault
/doc/administration/clusters/ @phillipwells
/doc/administration/compliance.md @eread
/doc/administration/configure.md @axil
/doc/administration/consul.md @axil
-/doc/administration/dedicated/ @drcatherinepope
/doc/administration/docs_self_host.md @axil
/doc/administration/encrypted_configuration.md @axil
/doc/administration/environment_variables.md @axil
-/doc/administration/external_pipeline_validation.md @drcatherinepope
+/doc/administration/external_pipeline_validation.md @marcel.amirault
/doc/administration/feature_flags.md @axil
/doc/administration/file_hooks.md @eread @ashrafkhamis
/doc/administration/geo/ @axil
@@ -515,7 +514,6 @@ lib/gitlab/checks/**
/doc/api/epic_issues.md @msedlakjakubowski
/doc/api/epic_links.md @msedlakjakubowski
/doc/api/epics.md @msedlakjakubowski
-/doc/api/error_tracking.md @drcatherinepope
/doc/api/events.md @eread
/doc/api/feature_flag_user_lists.md @phillipwells
/doc/api/feature_flags.md @phillipwells
@@ -559,20 +557,19 @@ lib/gitlab/checks/**
/doc/api/issues_statistics.md @msedlakjakubowski
/doc/api/iterations.md @msedlakjakubowski
/doc/api/job_artifacts.md @marcel.amirault
-/doc/api/jobs.md @drcatherinepope
+/doc/api/jobs.md @marcel.amirault
/doc/api/keys.md @aqualls
/doc/api/labels.md @msedlakjakubowski
/doc/api/license.md @fneill
/doc/api/linked_epics.md @msedlakjakubowski
/doc/api/lint.md @marcel.amirault
-/doc/api/managed_licenses.md @fneill
/doc/api/markdown.md @msedlakjakubowski
/doc/api/member_roles.md @jglassman1
/doc/api/members.md @jglassman1
/doc/api/merge_request_approvals.md @aqualls
/doc/api/merge_request_context_commits.md @aqualls
/doc/api/merge_requests.md @aqualls
-/doc/api/merge_trains.md @drcatherinepope
+/doc/api/merge_trains.md @marcel.amirault
/doc/api/metadata.md @phillipwells
/doc/api/metrics_dashboard_annotations.md @msedlakjakubowski
/doc/api/metrics_user_starred_dashboards.md @msedlakjakubowski
@@ -585,9 +582,9 @@ lib/gitlab/checks/**
/doc/api/packages.md @marcel.amirault
/doc/api/packages/ @marcel.amirault
/doc/api/personal_access_tokens.md @eread
-/doc/api/pipeline_schedules.md @drcatherinepope
-/doc/api/pipeline_triggers.md @drcatherinepope
-/doc/api/pipelines.md @drcatherinepope
+/doc/api/pipeline_schedules.md @marcel.amirault
+/doc/api/pipeline_triggers.md @marcel.amirault
+/doc/api/pipelines.md @marcel.amirault
/doc/api/plan_limits.md @jglassman1
/doc/api/product_analytics.md @lciutacu
/doc/api/project_access_tokens.md @jglassman1
@@ -595,6 +592,7 @@ lib/gitlab/checks/**
/doc/api/project_badges.md @aqualls
/doc/api/project_clusters.md @phillipwells
/doc/api/project_import_export.md @aqualls
+/doc/api/project_job_token_scopes.md @marcel.amirault
/doc/api/project_level_variables.md @marcel.amirault
/doc/api/project_relations_export.md @eread @ashrafkhamis
/doc/api/project_repository_storage_moves.md @eread
@@ -622,6 +620,7 @@ lib/gitlab/checks/**
/doc/api/saml.md @jglassman1
/doc/api/scim.md @jglassman1
/doc/api/search.md @ashrafkhamis
+/doc/api/search_admin.md @ashrafkhamis
/doc/api/secure_files.md @marcel.amirault
/doc/api/settings.md @jglassman1
/doc/api/sidekiq_metrics.md @axil
@@ -641,50 +640,26 @@ lib/gitlab/checks/**
/doc/api/usage_data.md @lciutacu
/doc/api/users.md @jglassman1
/doc/api/version.md @phillipwells
-/doc/api/visual_review_discussions.md @drcatherinepope
+/doc/api/visual_review_discussions.md @marcel.amirault
/doc/api/vulnerabilities.md @rdickenson
/doc/api/vulnerability_exports.md @rdickenson
/doc/api/vulnerability_findings.md @rdickenson
/doc/architecture/blueprints/cells/ @lciutacu
/doc/architecture/blueprints/database/scalability/patterns/ @aqualls
/doc/architecture/blueprints/database_scaling/ @aqualls
-/doc/ci/ @drcatherinepope
-/doc/ci/caching/ @marcel.amirault
+/doc/ci/ @marcel.amirault
/doc/ci/chatops/ @eread @ashrafkhamis
/doc/ci/cloud_deployment/ @phillipwells
-/doc/ci/cloud_services/ @marcel.amirault
-/doc/ci/components/ @marcel.amirault
-/doc/ci/directed_acyclic_graph/ @marcel.amirault
/doc/ci/docker/using_docker_images.md @fneill
/doc/ci/environments/ @phillipwells
-/doc/ci/examples/authenticating-with-hashicorp-vault/ @marcel.amirault
/doc/ci/examples/deployment/ @phillipwells
-/doc/ci/examples/semantic-release.md @marcel.amirault
/doc/ci/interactive_web_terminal/ @fneill
-/doc/ci/introduction/ @marcel.amirault
-/doc/ci/jobs/ @marcel.amirault
/doc/ci/large_repositories/ @fneill
-/doc/ci/lint.md @marcel.amirault
-/doc/ci/migration/ @marcel.amirault
-/doc/ci/pipeline_editor/ @marcel.amirault
-/doc/ci/pipelines/downstream_pipelines.md @marcel.amirault
-/doc/ci/pipelines/index.md @marcel.amirault
-/doc/ci/pipelines/pipeline_architectures.md @marcel.amirault
-/doc/ci/pipelines/pipeline_artifacts.md @marcel.amirault
-/doc/ci/quick_start/ @marcel.amirault
/doc/ci/resource_groups/ @phillipwells
/doc/ci/runners/ @fneill
-/doc/ci/secrets/ @marcel.amirault
-/doc/ci/secure_files/ @marcel.amirault
/doc/ci/services/ @fneill
-/doc/ci/ssh_keys/ @marcel.amirault
/doc/ci/test_cases/ @msedlakjakubowski
/doc/ci/testing/code_quality.md @rdickenson
-/doc/ci/triggers/ @marcel.amirault
-/doc/ci/troubleshooting.md @marcel.amirault
-/doc/ci/variables/ @marcel.amirault
-/doc/ci/yaml/ @marcel.amirault
-/doc/ci/yaml/artifacts_reports.md @drcatherinepope
/doc/development/advanced_search.md @ashrafkhamis
/doc/development/application_limits.md @axil
/doc/development/audit_event_guide/ @eread
@@ -697,8 +672,6 @@ lib/gitlab/checks/**
/doc/development/cascading_settings.md @jglassman1
/doc/development/chatops_on_gitlabcom.md @eread @ashrafkhamis
/doc/development/cicd/ @marcel.amirault
-/doc/development/cicd/cicd_tables.md @drcatherinepope
-/doc/development/cicd/index.md @drcatherinepope
/doc/development/code_intelligence/ @aqualls
/doc/development/code_owners/ @aqualls
/doc/development/contributing/ @sselhorn
@@ -756,6 +729,7 @@ lib/gitlab/checks/**
/doc/development/policies.md @jglassman1
/doc/development/project_templates.md @aqualls
/doc/development/prometheus_metrics.md @msedlakjakubowski
+/doc/development/rails_endpoints/ @aqualls
/doc/development/real_time.md @jglassman1
/doc/development/rubocop_development_guide.md @sselhorn
/doc/development/search/ @ashrafkhamis
@@ -796,11 +770,9 @@ lib/gitlab/checks/**
/doc/integration/sourcegraph.md @aqualls
/doc/integration/trello_power_up.md @eread @ashrafkhamis
/doc/integration/vault.md @phillipwells
-/doc/operations/error_tracking.md @drcatherinepope
/doc/operations/feature_flags.md @phillipwells
/doc/operations/incident_management/ @msedlakjakubowski
/doc/operations/index.md @msedlakjakubowski
-/doc/operations/metrics/ @msedlakjakubowski
/doc/policy/ @axil
/doc/raketasks/ @axil
/doc/raketasks/generate_sample_prometheus_data.md @msedlakjakubowski
@@ -810,7 +782,6 @@ lib/gitlab/checks/**
/doc/security/ @jglassman1
/doc/security/email_verification.md @phillipwells
/doc/subscriptions/ @fneill
-/doc/subscriptions/gitlab_dedicated/ @drcatherinepope
/doc/topics/authentication/ @jglassman1
/doc/topics/autodevops/ @phillipwells
/doc/topics/data_seeder.md @sselhorn
@@ -821,6 +792,7 @@ lib/gitlab/checks/**
/doc/tutorials/ @kpaizee
/doc/tutorials/boards_for_teams/ @msedlakjakubowski
/doc/tutorials/compliance_pipeline/ @eread
+/doc/tutorials/configure_gitlab_runner_to_use_gke/ @fneill
/doc/tutorials/convert_personal_namespace_to_group/ @lciutacu
/doc/tutorials/fuzz_testing/ @rdickenson
/doc/tutorials/move_personal_project_to_group/ @lciutacu
@@ -843,7 +815,7 @@ lib/gitlab/checks/**
/doc/user/admin_area/reporting/spamcheck.md @axil
/doc/user/admin_area/review_abuse_reports.md @phillipwells
/doc/user/admin_area/settings/account_and_limit_settings.md @aqualls
-/doc/user/admin_area/settings/continuous_integration.md @drcatherinepope
+/doc/user/admin_area/settings/continuous_integration.md @marcel.amirault
/doc/user/admin_area/settings/deprecated_api_rate_limits.md @aqualls
/doc/user/admin_area/settings/email.md @msedlakjakubowski
/doc/user/admin_area/settings/external_authorization.md @jglassman1
@@ -859,7 +831,7 @@ lib/gitlab/checks/**
/doc/user/admin_area/settings/push_event_activities_limit.md @aqualls
/doc/user/admin_area/settings/rate_limit_on_issues_creation.md @msedlakjakubowski
/doc/user/admin_area/settings/rate_limit_on_notes_creation.md @msedlakjakubowski
-/doc/user/admin_area/settings/rate_limit_on_pipelines_creation.md @drcatherinepope
+/doc/user/admin_area/settings/rate_limit_on_pipelines_creation.md @marcel.amirault
/doc/user/admin_area/settings/rate_limit_on_projects_api.md @lciutacu
/doc/user/admin_area/settings/rate_limit_on_users_api.md @jglassman1
/doc/user/admin_area/settings/scim_setup.md @jglassman1
@@ -881,29 +853,23 @@ lib/gitlab/checks/**
/doc/user/discussions/ @aqualls
/doc/user/enterprise_user/ @jglassman1
/doc/user/feature_flags.md @sselhorn
-/doc/user/group/access_and_permissions.md @lciutacu
+/doc/user/group/ @lciutacu
/doc/user/group/clusters/ @phillipwells
/doc/user/group/compliance_frameworks.md @eread
-/doc/user/group/contribution_analytics/ @lciutacu
/doc/user/group/custom_project_templates.md @aqualls
-/doc/user/group/devops_adoption/ @lciutacu
/doc/user/group/epics/ @msedlakjakubowski
/doc/user/group/import/ @eread @ashrafkhamis
-/doc/user/group/index.md @lciutacu
-/doc/user/group/insights/ @lciutacu
/doc/user/group/issues_analytics/ @msedlakjakubowski
/doc/user/group/iterations/ @msedlakjakubowski
-/doc/user/group/manage.md @lciutacu
/doc/user/group/moderate_users.md @phillipwells
/doc/user/group/planning_hierarchy/ @msedlakjakubowski
/doc/user/group/reporting/ @phillipwells
-/doc/user/group/repositories_analytics/ @drcatherinepope
+/doc/user/group/repositories_analytics/ @marcel.amirault
/doc/user/group/roadmap/ @msedlakjakubowski
/doc/user/group/saml_sso/ @jglassman1
/doc/user/group/settings/ @jglassman1
-/doc/user/group/subgroups/ @lciutacu
-/doc/user/group/value_stream_analytics/ @lciutacu
/doc/user/infrastructure/ @phillipwells
+/doc/user/infrastructure/clusters/manage/management_project_applications/ @phillipwells
/doc/user/infrastructure/clusters/manage/management_project_applications/runner.md @fneill
/doc/user/markdown.md @msedlakjakubowski
/doc/user/namespace/ @lciutacu
@@ -939,8 +905,6 @@ lib/gitlab/checks/**
/doc/user/project/index.md @lciutacu
/doc/user/project/insights/ @lciutacu
/doc/user/project/integrations/ @eread @ashrafkhamis
-/doc/user/project/integrations/prometheus.md @msedlakjakubowski
-/doc/user/project/integrations/prometheus_library/ @msedlakjakubowski
/doc/user/project/issue_board.md @msedlakjakubowski
/doc/user/project/issues/ @msedlakjakubowski
/doc/user/project/issues/csv_import.md @eread @ashrafkhamis
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index fc98f771344..68bfa682d0f 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -108,30 +108,23 @@ start-review-app-pipeline:
- artifact: review-app-pipeline.yml
job: e2e-test-pipeline-generate
+include:
+ - remote: 'https://gitlab.com/gitlab-org/quality/pipeline-common/-/raw/6.4.0/ci/danger-review.yml'
+
danger-review:
extends:
- .default-retry
- .ruby-node-cache
- .review:rules:danger
- stage: test
- needs: []
+ image: "${DEFAULT_CI_IMAGE}"
before_script:
- source scripts/utils.sh
- bundle_install_script "--with danger"
- yarn_install_script
- script:
- # ${DANGER_DANGERFILE} is used by Jihulab for customizing danger support: https://jihulab.com/gitlab-cn/gitlab/-/blob/main-jh/jh/.gitlab-ci.yml
- - >
- if [ -z "$DANGER_GITLAB_API_TOKEN" ]; then
- run_timed_command danger_as_local
- else
- danger_id=$(echo -n ${DANGER_GITLAB_API_TOKEN} | md5sum | awk '{print $1}' | cut -c5-10)
- run_timed_command "bundle exec danger --fail-on-errors=true --verbose --danger_id=\"${danger_id}\" --dangerfile=\"${DANGER_DANGERFILE:-Dangerfile}\""
- fi
danger-review-local:
- extends:
- - danger-review
- - .review:rules:danger-local
- script:
- - run_timed_command danger_as_local
+ extends: danger-review
+ before_script:
+ - !reference ["danger-review", "before_script"]
+ # We unset DANGER_GITLAB_API_TOKEN so that Danger will run as local from `danger-review:script`
+ - unset DANGER_GITLAB_API_TOKEN
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 91c47aad503..16b3419ef02 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -38,7 +38,6 @@ Layout/LineLength:
- 'app/controllers/groups/group_members_controller.rb'
- 'app/controllers/groups/settings/applications_controller.rb'
- 'app/controllers/groups/settings/ci_cd_controller.rb'
- - 'app/controllers/groups/settings/integrations_controller.rb'
- 'app/controllers/groups/settings/repository_controller.rb'
- 'app/controllers/groups_controller.rb'
- 'app/controllers/import/base_controller.rb'
diff --git a/.rubocop_todo/rails/lexically_scoped_action_filter.yml b/.rubocop_todo/rails/lexically_scoped_action_filter.yml
index 1620fbd39b7..282b6ead10e 100644
--- a/.rubocop_todo/rails/lexically_scoped_action_filter.yml
+++ b/.rubocop_todo/rails/lexically_scoped_action_filter.yml
@@ -7,7 +7,6 @@ Rails/LexicallyScopedActionFilter:
- 'app/controllers/clusters/base_controller.rb'
- 'app/controllers/clusters/clusters_controller.rb'
- 'app/controllers/concerns/enforces_two_factor_authentication.rb'
- - 'app/controllers/concerns/integrations/actions.rb'
- 'app/controllers/concerns/oauth_applications.rb'
- 'app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb'
- 'app/controllers/confirmations_controller.rb'
diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml
index 515ac988cc2..54b40a045a7 100644
--- a/.rubocop_todo/rspec/context_wording.yml
+++ b/.rubocop_todo/rspec/context_wording.yml
@@ -972,7 +972,6 @@ RSpec/ContextWording:
- 'spec/config/object_store_settings_spec.rb'
- 'spec/controllers/admin/application_settings_controller_spec.rb'
- 'spec/controllers/admin/instance_review_controller_spec.rb'
- - 'spec/controllers/admin/integrations_controller_spec.rb'
- 'spec/controllers/admin/sessions_controller_spec.rb'
- 'spec/controllers/admin/users_controller_spec.rb'
- 'spec/controllers/application_controller_spec.rb'
@@ -1004,7 +1003,6 @@ RSpec/ContextWording:
- 'spec/controllers/groups/registry/repositories_controller_spec.rb'
- 'spec/controllers/groups/releases_controller_spec.rb'
- 'spec/controllers/groups/settings/ci_cd_controller_spec.rb'
- - 'spec/controllers/groups/settings/integrations_controller_spec.rb'
- 'spec/controllers/groups/settings/repository_controller_spec.rb'
- 'spec/controllers/groups_controller_spec.rb'
- 'spec/controllers/help_controller_spec.rb'
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml
index 992295c1bcb..b4533feec23 100644
--- a/.rubocop_todo/rspec/missing_feature_category.yml
+++ b/.rubocop_todo/rspec/missing_feature_category.yml
@@ -1638,7 +1638,6 @@ RSpec/MissingFeatureCategory:
- 'spec/controllers/admin/hooks_controller_spec.rb'
- 'spec/controllers/admin/identities_controller_spec.rb'
- 'spec/controllers/admin/impersonations_controller_spec.rb'
- - 'spec/controllers/admin/integrations_controller_spec.rb'
- 'spec/controllers/admin/jobs_controller_spec.rb'
- 'spec/controllers/admin/plan_limits_controller_spec.rb'
- 'spec/controllers/admin/projects_controller_spec.rb'
@@ -1701,7 +1700,6 @@ RSpec/MissingFeatureCategory:
- 'spec/controllers/groups/releases_controller_spec.rb'
- 'spec/controllers/groups/settings/applications_controller_spec.rb'
- 'spec/controllers/groups/settings/ci_cd_controller_spec.rb'
- - 'spec/controllers/groups/settings/integrations_controller_spec.rb'
- 'spec/controllers/groups/settings/repository_controller_spec.rb'
- 'spec/controllers/groups/shared_projects_controller_spec.rb'
- 'spec/controllers/groups/uploads_controller_spec.rb'
diff --git a/.rubocop_todo/rspec/return_from_stub.yml b/.rubocop_todo/rspec/return_from_stub.yml
index da4c41e2f6d..20e4112da62 100644
--- a/.rubocop_todo/rspec/return_from_stub.yml
+++ b/.rubocop_todo/rspec/return_from_stub.yml
@@ -79,7 +79,6 @@ RSpec/ReturnFromStub:
- 'spec/bin/feature_flag_spec.rb'
- 'spec/config/settings_spec.rb'
- 'spec/controllers/admin/application_settings_controller_spec.rb'
- - 'spec/controllers/admin/integrations_controller_spec.rb'
- 'spec/controllers/concerns/page_limiter_spec.rb'
- 'spec/controllers/concerns/send_file_upload_spec.rb'
- 'spec/controllers/concerns/spammable_actions/akismet_mark_as_spam_action_spec.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 44545bd2731..cb64edcf9aa 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-58851fe18b0291ed7b11c8821c7ae548c2a96337
+67a5d6727d2ee6b4c50fcee9c3b9be5a8b528e42
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
index eabf4749e9c..069704b7bf4 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
@@ -70,7 +70,10 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1">
+ <div
+ class="gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1 gl-sm-flex-direction-column"
+ >
+ <slot></slot>
<gl-button
:href="$options.TEMPLATE_REPOSITORY_URL"
size="small"
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
index 403793a255a..f8c8be76bd5 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -1,5 +1,6 @@
<script>
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
+import CiEditorHeader from 'ee_else_ce/ci/pipeline_editor/components/editor/ci_editor_header.vue';
import { s__, __ } from '~/locale';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
@@ -19,7 +20,6 @@ import {
} from '../constants';
import getAppStatus from '../graphql/queries/client/app_status.query.graphql';
import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue';
-import CiEditorHeader from './editor/ci_editor_header.vue';
import CiValidate from './validate/ci_validate.vue';
import TextEditor from './editor/text_editor.vue';
import EditorTab from './ui/editor_tab.vue';
diff --git a/app/assets/javascripts/ci/pipeline_editor/constants.js b/app/assets/javascripts/ci/pipeline_editor/constants.js
index 912e0fcbff9..756041315e4 100644
--- a/app/assets/javascripts/ci/pipeline_editor/constants.js
+++ b/app/assets/javascripts/ci/pipeline_editor/constants.js
@@ -65,6 +65,7 @@ export const CI_YAML_LINK = 'CI_YAML_LINK';
export const pipelineEditorTrackingOptions = {
label: 'pipeline_editor',
actions: {
+ browseCatalog: 'browse_catalog',
browseTemplates: 'browse_templates',
closeHelpDrawer: 'close_help_drawer',
commitCiConfig: 'commit_ci_config',
diff --git a/app/assets/javascripts/ci/pipeline_editor/index.js b/app/assets/javascripts/ci/pipeline_editor/index.js
index b8d6c27435d..bc20e478876 100644
--- a/app/assets/javascripts/ci/pipeline_editor/index.js
+++ b/app/assets/javascripts/ci/pipeline_editor/index.js
@@ -1,17 +1,6 @@
import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import { EDITOR_APP_STATUS_LOADING } from './constants';
-import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
-import getCurrentBranch from './graphql/queries/client/current_branch.query.graphql';
-import getAppStatus from './graphql/queries/client/app_status.query.graphql';
-import getLastCommitBranch from './graphql/queries/client/last_commit_branch.query.graphql';
-import getPipelineEtag from './graphql/queries/client/pipeline_etag.query.graphql';
-import { resolvers } from './graphql/resolvers';
-import typeDefs from './graphql/typedefs.graphql';
-import PipelineEditorApp from './pipeline_editor_app.vue';
+import { createAppOptions } from 'ee_else_ce/ci/pipeline_editor/options';
export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
const el = document.querySelector(selector);
@@ -20,129 +9,9 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
return null;
}
- const {
- // Add to apollo cache as it can be updated by future queries
- initialBranchName,
- pipelineEtag,
- // Add to provide/inject API for static values
- ciConfigPath,
- ciExamplesHelpPagePath,
- ciHelpPagePath,
- ciLintPath,
- ciTroubleshootingPath,
- defaultBranch,
- emptyStateIllustrationPath,
- helpPaths,
- includesHelpPagePath,
- lintHelpPagePath,
- needsHelpPagePath,
- newMergeRequestPath,
- pipelinePagePath,
- projectFullPath,
- projectPath,
- projectNamespace,
- simulatePipelineHelpPagePath,
- totalBranches,
- usesExternalConfig,
- validateTabIllustrationPath,
- ymlHelpPagePath,
- aiChatAvailable,
- } = el.dataset;
+ const options = createAppOptions(el);
- const configurationPaths = Object.fromEntries(
- Object.entries(CODE_SNIPPET_SOURCE_SETTINGS).map(([source, { datasetKey }]) => [
- source,
- el.dataset[datasetKey],
- ]),
- );
-
- Vue.use(VueApollo);
-
- const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers, {
- typeDefs,
- useGet: true,
- }),
- });
- const { cache } = apolloProvider.clients.defaultClient;
-
- cache.writeQuery({
- query: getAppStatus,
- data: {
- app: {
- __typename: 'PipelineEditorApp',
- status: EDITOR_APP_STATUS_LOADING,
- },
- },
- });
-
- cache.writeQuery({
- query: getCurrentBranch,
- data: {
- workBranches: {
- __typename: 'BranchList',
- current: {
- __typename: 'WorkBranch',
- name: initialBranchName || defaultBranch,
- },
- },
- },
- });
-
- cache.writeQuery({
- query: getLastCommitBranch,
- data: {
- workBranches: {
- __typename: 'BranchList',
- lastCommit: {
- __typename: 'WorkBranch',
- name: '',
- },
- },
- },
- });
-
- cache.writeQuery({
- query: getPipelineEtag,
- data: {
- etags: {
- __typename: 'EtagValues',
- pipeline: pipelineEtag,
- },
- },
- });
-
- return new Vue({
- el,
- apolloProvider,
- provide: {
- aiChatAvailable: parseBoolean(aiChatAvailable),
- ciConfigPath,
- ciExamplesHelpPagePath,
- ciHelpPagePath,
- ciLintPath,
- ciTroubleshootingPath,
- configurationPaths,
- dataMethod: 'graphql',
- defaultBranch,
- emptyStateIllustrationPath,
- helpPaths,
- includesHelpPagePath,
- lintHelpPagePath,
- needsHelpPagePath,
- newMergeRequestPath,
- pipelinePagePath,
- projectFullPath,
- projectPath,
- projectNamespace,
- simulatePipelineHelpPagePath,
- totalBranches: parseInt(totalBranches, 10),
- usesExternalConfig: parseBoolean(usesExternalConfig),
- validateTabIllustrationPath,
- ymlHelpPagePath,
- },
- render(h) {
- return h(PipelineEditorApp);
- },
- });
+ return new Vue(options);
};
+
+initPipelineEditor();
diff --git a/app/assets/javascripts/ci/pipeline_editor/options.js b/app/assets/javascripts/ci/pipeline_editor/options.js
new file mode 100644
index 00000000000..922c8eee8fc
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/options.js
@@ -0,0 +1,142 @@
+import Vue from 'vue';
+
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { EDITOR_APP_STATUS_LOADING } from './constants';
+import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
+import getCurrentBranch from './graphql/queries/client/current_branch.query.graphql';
+import getAppStatus from './graphql/queries/client/app_status.query.graphql';
+import getLastCommitBranch from './graphql/queries/client/last_commit_branch.query.graphql';
+import getPipelineEtag from './graphql/queries/client/pipeline_etag.query.graphql';
+import { resolvers } from './graphql/resolvers';
+import typeDefs from './graphql/typedefs.graphql';
+import PipelineEditorApp from './pipeline_editor_app.vue';
+
+export const createAppOptions = (el) => {
+ const {
+ // Add to apollo cache as it can be updated by future queries
+ initialBranchName,
+ pipelineEtag,
+ // Add to provide/inject API for static values
+ ciConfigPath,
+ ciExamplesHelpPagePath,
+ ciHelpPagePath,
+ ciLintPath,
+ ciTroubleshootingPath,
+ defaultBranch,
+ emptyStateIllustrationPath,
+ helpPaths,
+ includesHelpPagePath,
+ lintHelpPagePath,
+ needsHelpPagePath,
+ newMergeRequestPath,
+ pipelinePagePath,
+ projectFullPath,
+ projectPath,
+ projectNamespace,
+ simulatePipelineHelpPagePath,
+ totalBranches,
+ usesExternalConfig,
+ validateTabIllustrationPath,
+ ymlHelpPagePath,
+ aiChatAvailable,
+ } = el.dataset;
+
+ const configurationPaths = Object.fromEntries(
+ Object.entries(CODE_SNIPPET_SOURCE_SETTINGS).map(([source, { datasetKey }]) => [
+ source,
+ el.dataset[datasetKey],
+ ]),
+ );
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers, {
+ typeDefs,
+ useGet: true,
+ }),
+ });
+ const { cache } = apolloProvider.clients.defaultClient;
+
+ cache.writeQuery({
+ query: getAppStatus,
+ data: {
+ app: {
+ __typename: 'PipelineEditorApp',
+ status: EDITOR_APP_STATUS_LOADING,
+ },
+ },
+ });
+
+ cache.writeQuery({
+ query: getCurrentBranch,
+ data: {
+ workBranches: {
+ __typename: 'BranchList',
+ current: {
+ __typename: 'WorkBranch',
+ name: initialBranchName || defaultBranch,
+ },
+ },
+ },
+ });
+
+ cache.writeQuery({
+ query: getLastCommitBranch,
+ data: {
+ workBranches: {
+ __typename: 'BranchList',
+ lastCommit: {
+ __typename: 'WorkBranch',
+ name: '',
+ },
+ },
+ },
+ });
+
+ cache.writeQuery({
+ query: getPipelineEtag,
+ data: {
+ etags: {
+ __typename: 'EtagValues',
+ pipeline: pipelineEtag,
+ },
+ },
+ });
+
+ return {
+ el,
+ apolloProvider,
+ provide: {
+ aiChatAvailable: parseBoolean(aiChatAvailable),
+ ciConfigPath,
+ ciExamplesHelpPagePath,
+ ciHelpPagePath,
+ ciLintPath,
+ ciTroubleshootingPath,
+ configurationPaths,
+ dataMethod: 'graphql',
+ defaultBranch,
+ emptyStateIllustrationPath,
+ helpPaths,
+ includesHelpPagePath,
+ lintHelpPagePath,
+ needsHelpPagePath,
+ newMergeRequestPath,
+ pipelinePagePath,
+ projectFullPath,
+ projectPath,
+ projectNamespace,
+ simulatePipelineHelpPagePath,
+ totalBranches: parseInt(totalBranches, 10),
+ usesExternalConfig: parseBoolean(usesExternalConfig),
+ validateTabIllustrationPath,
+ ymlHelpPagePath,
+ },
+ render(h) {
+ return h(PipelineEditorApp);
+ },
+ };
+};
diff --git a/app/assets/javascripts/ci/runner/components/runner_details.vue b/app/assets/javascripts/ci/runner/components/runner_details.vue
index 0608d63897b..51c752f0dee 100644
--- a/app/assets/javascripts/ci/runner/components/runner_details.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_details.vue
@@ -1,7 +1,7 @@
<script>
-import { GlIcon, GlIntersperse, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlIntersperse, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__, formatNumber } from '~/locale';
+import { s__ } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
@@ -15,10 +15,10 @@ import RunnerDetail from './runner_detail.vue';
import RunnerGroups from './runner_groups.vue';
import RunnerProjects from './runner_projects.vue';
import RunnerTags from './runner_tags.vue';
+import RunnerManagersDetail from './runner_managers_detail.vue';
export default {
components: {
- GlIcon,
GlIntersperse,
GlLink,
GlSprintf,
@@ -33,6 +33,7 @@ export default {
RunnerUpgradeStatusAlert: () =>
import('ee_component/ci/runner/components/runner_upgrade_status_alert.vue'),
RunnerTags,
+ RunnerManagersDetail,
TimeAgo,
},
props: {
@@ -81,9 +82,6 @@ export default {
anchor: 'authentication-token-security',
});
},
- runnerManagersCount() {
- return formatNumber(this.runner?.managers?.count || 0);
- },
},
ACCESS_LEVEL_REF_PROTECTED,
RUNNER_MANAGERS_HELP_URL,
@@ -185,8 +183,7 @@ export default {
</help-popover>
</template>
<template #value>
- <gl-icon name="container-image" class="gl-text-secondary" />
- {{ runnerManagersCount }}
+ <runner-managers-detail :runner="runner" />
</template>
</runner-detail>
</dl>
diff --git a/app/assets/javascripts/ci/runner/components/runner_managers_detail.vue b/app/assets/javascripts/ci/runner/components/runner_managers_detail.vue
new file mode 100644
index 00000000000..a9df7f12955
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_managers_detail.vue
@@ -0,0 +1,127 @@
+<script>
+import { GlCollapse, GlButton, GlIcon, GlSkeletonLoader, GlTableLite } from '@gitlab/ui';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
+import { __, s__, formatNumber } from '~/locale';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { createAlert } from '~/alert';
+import runnerManagersQuery from '../graphql/show/runner_managers.query.graphql';
+import { I18N_FETCH_ERROR } from '../constants';
+import { captureException } from '../sentry_utils';
+import { tableField } from '../utils';
+
+export default {
+ name: 'RunnerManagersDetail',
+ components: {
+ GlCollapse,
+ GlButton,
+ GlIcon,
+ GlSkeletonLoader,
+ GlTableLite,
+ TimeAgo,
+ HelpPopover,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ validator: (runner) => {
+ return Boolean(runner?.id);
+ },
+ },
+ },
+ data() {
+ return {
+ skip: true,
+ expanded: false,
+ managers: [],
+ };
+ },
+ apollo: {
+ managers: {
+ query: runnerManagersQuery,
+ skip() {
+ return this.skip;
+ },
+ variables() {
+ return { runnerId: this.runner.id };
+ },
+ update({ runner }) {
+ return runner?.managers?.nodes || [];
+ },
+ error(error) {
+ createAlert({ message: I18N_FETCH_ERROR });
+ captureException({ error, component: this.$options.name });
+ },
+ },
+ },
+ computed: {
+ runnerManagersCount() {
+ return this.runner?.managers?.count || 0;
+ },
+ runnerManagersCountFormatted() {
+ return formatNumber(this.runnerManagersCount);
+ },
+ icon() {
+ return this.expanded ? 'chevron-down' : 'chevron-right';
+ },
+ text() {
+ return this.expanded ? __('Hide details') : __('Show details');
+ },
+ loading() {
+ return this.$apollo?.queries.managers.loading;
+ },
+ },
+ methods: {
+ fetchManagers() {
+ this.skip = false;
+ },
+ toggleExpanded() {
+ this.expanded = !this.expanded;
+ },
+ },
+ fields: [
+ tableField({ key: 'systemId', label: s__('Runners|System ID') }),
+ tableField({
+ key: 'contactedAt',
+ label: s__('Runners|Last contact'),
+ tdClass: ['gl-text-right'],
+ thClasses: ['gl-text-right'],
+ }),
+ ],
+};
+</script>
+
+<template>
+ <div>
+ <gl-icon name="container-image" class="gl-text-secondary" />
+ {{ runnerManagersCountFormatted }}
+ <gl-button
+ v-if="runnerManagersCount"
+ variant="link"
+ @mouseover.once="fetchManagers"
+ @focus.once="fetchManagers"
+ @click.once="fetchManagers"
+ @click="toggleExpanded"
+ >
+ <gl-icon :name="icon" /> {{ text }}
+ </gl-button>
+
+ <gl-collapse :visible="expanded" class="gl-mt-5">
+ <gl-skeleton-loader v-if="loading" />
+ <gl-table-lite v-else-if="managers.length" :fields="$options.fields" :items="managers">
+ <template #head(systemId)="{ label }">
+ {{ label }}
+ <help-popover>
+ {{ s__('Runners|The unique ID for each runner that uses this configuration.') }}
+ </help-popover>
+ </template>
+ <template #cell(contactedAt)="{ item = {} }">
+ <template v-if="item.contactedAt">
+ <time-ago :time="item.contactedAt" />
+ </template>
+ <template v-else>{{ s__('Runners|Never contacted') }}</template>
+ </template>
+ </gl-table-lite>
+ </gl-collapse>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_managers_table.vue b/app/assets/javascripts/ci/runner/components/runner_managers_table.vue
new file mode 100644
index 00000000000..71cf76aed46
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_managers_table.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlIntersperse, GlTableLite } from '@gitlab/ui';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
+import { s__ } from '~/locale';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { tableField } from '../utils';
+
+export default {
+ name: 'RunnerManagersTable',
+ components: {
+ GlTableLite,
+ TimeAgo,
+ HelpPopover,
+ GlIntersperse,
+ },
+ props: {
+ items: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ skip: true,
+ expanded: false,
+ managers: [],
+ };
+ },
+ fields: [
+ tableField({ key: 'systemId', label: s__('Runners|System ID') }),
+ tableField({ key: 'version', label: s__('Runners|Version') }),
+ tableField({ key: 'ipAddress', label: s__('Runners|IP Address') }),
+ tableField({ key: 'executorName', label: s__('Runners|Executor') }),
+ tableField({ key: 'architecturePlatform', label: s__('Runners|Arch/Platform') }),
+ tableField({
+ key: 'contactedAt',
+ label: s__('Runners|Last contact'),
+ tdClass: ['gl-text-right'],
+ thClasses: ['gl-text-right'],
+ }),
+ ],
+};
+</script>
+
+<template>
+ <gl-table-lite :fields="$options.fields" :items="items">
+ <template #head(systemId)="{ label }">
+ {{ label }}
+ <help-popover>
+ {{ s__('Runners|The unique ID for each runner that uses this configuration.') }}
+ </help-popover>
+ </template>
+ <template #cell(version)="{ item = {} }">
+ {{ item.version }}
+ <template v-if="item.revision">({{ item.revision }})</template>
+ </template>
+ <template #cell(architecturePlatform)="{ item = {} }">
+ <gl-intersperse separator="/">
+ <span v-if="item.architectureName">{{ item.architectureName }}</span>
+ <span v-if="item.platformName">{{ item.platformName }}</span>
+ </gl-intersperse>
+ </template>
+ <template #cell(contactedAt)="{ item = {} }">
+ <template v-if="item.contactedAt">
+ <time-ago :time="item.contactedAt" />
+ </template>
+ <template v-else>{{ s__('Runners|Never contacted') }}</template>
+ </template>
+ </gl-table-lite>
+</template>
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_managers.query.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_managers.query.graphql
new file mode 100644
index 00000000000..65fb5f91b60
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_managers.query.graphql
@@ -0,0 +1,13 @@
+query getRunnerManagers($runnerId: CiRunnerID!) {
+ runner(id: $runnerId) {
+ id
+ managers {
+ count
+ nodes {
+ id
+ systemId
+ contactedAt
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/drawio/constants.js b/app/assets/javascripts/drawio/constants.js
index 2e1e074db3b..2f1870087f9 100644
--- a/app/assets/javascripts/drawio/constants.js
+++ b/app/assets/javascripts/drawio/constants.js
@@ -1,9 +1,18 @@
/*
* TODO: Make this URL configurable
*/
-export const DRAWIO_EDITOR_URL =
- 'https://embed.diagrams.net/?ui=sketch&noSaveBtn=1&saveAndExit=1&keepmodified=1&spin=1&embed=1&libraries=1&configure=1&proto=json&toSvg=1'; // TODO Make it configurable
-
+export const DRAWIO_PARAMS = {
+ ui: 'sketch',
+ noSaveBtn: 1,
+ saveAndExit: 1,
+ keepmodified: 1,
+ spin: 1,
+ embed: 1,
+ libraries: 1,
+ configure: 1,
+ proto: 'json',
+ toSvg: 1,
+};
export const DRAWIO_FRAME_ID = 'drawio-frame';
export const DARK_BACKGROUND_COLOR = '#202020';
diff --git a/app/assets/javascripts/drawio/drawio_editor.js b/app/assets/javascripts/drawio/drawio_editor.js
index 38d1cadcc63..3c411d8093c 100644
--- a/app/assets/javascripts/drawio/drawio_editor.js
+++ b/app/assets/javascripts/drawio/drawio_editor.js
@@ -4,8 +4,8 @@ import { darkModeEnabled } from '~/lib/utils/color_utils';
import { __ } from '~/locale';
import { setAttributes } from '~/lib/utils/dom_utils';
import {
+ DRAWIO_PARAMS,
DARK_BACKGROUND_COLOR,
- DRAWIO_EDITOR_URL,
DRAWIO_FRAME_ID,
DIAGRAM_BACKGROUND_COLOR,
DRAWIO_IFRAME_TIMEOUT,
@@ -17,7 +17,7 @@ function updateDrawioEditorState(drawIOEditorState, data) {
}
function postMessageToDrawioEditor(drawIOEditorState, message) {
- const { origin } = new URL(DRAWIO_EDITOR_URL);
+ const { origin } = new URL(drawIOEditorState.drawioUrl);
drawIOEditorState.iframe.contentWindow.postMessage(JSON.stringify(message), origin);
}
@@ -222,7 +222,7 @@ function createEditorIFrame(drawIOEditorState) {
setAttributes(iframe, {
id: DRAWIO_FRAME_ID,
- src: DRAWIO_EDITOR_URL,
+ src: drawIOEditorState.drawioUrl,
class: 'drawio-editor',
});
@@ -256,7 +256,7 @@ function attachDrawioIFrameMessageListener(drawIOEditorState, editorFacade) {
});
}
-const createDrawioEditorState = ({ filename = null }) => ({
+const createDrawioEditorState = ({ filename = null, drawioUrl }) => ({
newDiagram: true,
filename,
diagramSvg: null,
@@ -266,10 +266,17 @@ const createDrawioEditorState = ({ filename = null }) => ({
initialized: false,
dark: darkModeEnabled(),
disposeEventListener: null,
+ drawioUrl,
});
-export function launchDrawioEditor({ editorFacade, filename }) {
- const drawIOEditorState = createDrawioEditorState({ filename });
+export function launchDrawioEditor({ editorFacade, filename, drawioUrl = gon.diagramsnet_url }) {
+ const url = new URL(drawioUrl);
+
+ for (const [key, value] of Object.entries(DRAWIO_PARAMS)) {
+ url.searchParams.set(key, value);
+ }
+
+ const drawIOEditorState = createDrawioEditorState({ filename, drawioUrl: url.href });
// The execution order of these two functions matter
attachDrawioIFrameMessageListener(drawIOEditorState, editorFacade);
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 3b38d715ea5..4f68c7984e8 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -107,7 +107,7 @@ export default {
MarkdownEditor,
},
mixins: [trackingMixin],
- inject: ['formatOptions', 'pageInfo'],
+ inject: ['formatOptions', 'pageInfo', 'drawioUrl'],
data() {
return {
editingMode: 'source',
@@ -183,6 +183,9 @@ export default {
disableSubmitButton() {
return this.noContent || !this.title;
},
+ drawioEnabled() {
+ return typeof this.drawioUrl === 'string' && this.drawioUrl.length > 0;
+ },
},
mounted() {
if (!this.commitMessage) this.updateCommitMessage();
@@ -356,7 +359,7 @@ export default {
:autofocus="pageInfo.persisted"
:enable-autocomplete="true"
:autocomplete-data-sources="autocompleteDataSources"
- :drawio-enabled="true"
+ :drawio-enabled="drawioEnabled"
@contentEditor="notifyContentEditorActive"
@markdownField="notifyContentEditorInactive"
@keydown.ctrl.enter="submitFormShortcut"
diff --git a/app/assets/javascripts/pages/shared/wikis/edit.js b/app/assets/javascripts/pages/shared/wikis/edit.js
index 02878633916..0044575de62 100644
--- a/app/assets/javascripts/pages/shared/wikis/edit.js
+++ b/app/assets/javascripts/pages/shared/wikis/edit.js
@@ -70,6 +70,7 @@ const createWikiFormApp = () => {
provide: {
formatOptions: JSON.parse(formatOptions),
pageInfo: convertObjectPropsToCamelCase(JSON.parse(pageInfo)),
+ drawioUrl: gon.diagramsnet_url,
},
render(createElement) {
return createElement(wikiForm);
diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher.vue b/app/assets/javascripts/super_sidebar/components/context_switcher.vue
index ad2111140a1..d1297b19579 100644
--- a/app/assets/javascripts/super_sidebar/components/context_switcher.vue
+++ b/app/assets/javascripts/super_sidebar/components/context_switcher.vue
@@ -194,6 +194,7 @@ export default {
:key="item.link"
:item="item"
:link-classes="{ [item.link_classes]: item.link_classes }"
+ is-subitem
/>
</ul>
</li>
diff --git a/app/assets/javascripts/super_sidebar/components/groups_list.vue b/app/assets/javascripts/super_sidebar/components/groups_list.vue
index 4fa15f1cd76..48becacebb7 100644
--- a/app/assets/javascripts/super_sidebar/components/groups_list.vue
+++ b/app/assets/javascripts/super_sidebar/components/groups_list.vue
@@ -64,7 +64,7 @@ export default {
:search-results="searchResults"
>
<template #view-all-items>
- <nav-item v-bind="viewAllProps" />
+ <nav-item v-bind="viewAllProps" is-subitem />
</template>
</search-results>
<frequent-items-list
@@ -75,7 +75,7 @@ export default {
:pristine-text="$options.i18n.pristineText"
>
<template #view-all-items>
- <nav-item v-bind="viewAllProps" />
+ <nav-item v-bind="viewAllProps" is-subitem />
</template>
</frequent-items-list>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/items_list.vue b/app/assets/javascripts/super_sidebar/components/items_list.vue
index 46f27dd7d06..7d5af883651 100644
--- a/app/assets/javascripts/super_sidebar/components/items_list.vue
+++ b/app/assets/javascripts/super_sidebar/components/items_list.vue
@@ -24,6 +24,7 @@ export default {
:key="item.id"
:item="item"
:link-classes="{ 'gl-py-2!': true }"
+ is-subitem
>
<template #icon>
<project-avatar
@@ -32,6 +33,7 @@ export default {
:project-avatar-url="item.avatar"
:size="24"
aria-hidden="true"
+ class="gl-mr-n2"
/>
</template>
<template #actions>
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
index ec1c4069b1a..90a040b97f7 100644
--- a/app/assets/javascripts/super_sidebar/components/nav_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -51,6 +51,11 @@ export default {
required: false,
default: () => ({}),
},
+ isSubitem: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
pillData() {
@@ -99,6 +104,7 @@ export default {
return {
'gl-py-2': this.isPinnable,
'gl-py-3': !this.isPinnable,
+ 'gl-mx-2': this.isSubitem,
[this.item.link_classes]: this.item.link_classes,
...this.linkClasses,
};
@@ -106,6 +112,9 @@ export default {
navItemLinkComponent() {
return this.item.to ? NavItemRouterLink : NavItemLink;
},
+ iconClasses() {
+ return this.isSubitem === true ? 'gl-ml-2 gl-mr-4' : 'gl-w-6 gl-mx-3';
+ },
},
};
</script>
@@ -128,7 +137,7 @@ export default {
style="width: 3px; border-radius: 3px; margin-right: 1px"
data-testid="active-indicator"
></div>
- <div class="gl-flex-shrink-0 gl-w-6 gl-mx-3">
+ <div :class="iconClasses" class="gl-flex-shrink-0">
<slot name="icon">
<gl-icon v-if="item.icon" :name="item.icon" class="gl-ml-2 item-icon" />
<gl-icon
diff --git a/app/assets/javascripts/super_sidebar/components/projects_list.vue b/app/assets/javascripts/super_sidebar/components/projects_list.vue
index 78860e35eb1..8d1a5c825b5 100644
--- a/app/assets/javascripts/super_sidebar/components/projects_list.vue
+++ b/app/assets/javascripts/super_sidebar/components/projects_list.vue
@@ -65,7 +65,7 @@ export default {
:search-results="searchResults"
>
<template #view-all-items>
- <nav-item v-bind="viewAllProps" />
+ <nav-item v-bind="viewAllProps" is-subitem />
</template>
</search-results>
<frequent-items-list
@@ -76,7 +76,7 @@ export default {
:pristine-text="$options.i18n.pristineText"
>
<template #view-all-items>
- <nav-item v-bind="viewAllProps" />
+ <nav-item v-bind="viewAllProps" is-subitem />
</template>
</frequent-items-list>
</template>
diff --git a/app/controllers/concerns/integrations/actions.rb b/app/controllers/concerns/integrations/actions.rb
index c0816c2fe9c..10e86bcc98d 100644
--- a/app/controllers/concerns/integrations/actions.rb
+++ b/app/controllers/concerns/integrations/actions.rb
@@ -7,7 +7,12 @@ module Integrations::Actions
include Integrations::Params
include IntegrationsHelper
+ # :overrides is defined in Admin:IntegrationsController
+ # rubocop:disable Rails/LexicallyScopedActionFilter
+ before_action :ensure_integration_enabled, only: [:edit, :update, :overrides, :test]
before_action :integration, only: [:edit, :update, :overrides, :test]
+ # rubocop:enable Rails/LexicallyScopedActionFilter
+
before_action :render_404, only: :edit, if: -> do
integration.to_param == 'prometheus' && Feature.enabled?(:remove_monitor_metrics)
end
@@ -58,6 +63,10 @@ module Integrations::Actions
@integration ||= find_or_initialize_non_project_specific_integration(params[:id])
end
+ def ensure_integration_enabled
+ render_404 unless integration
+ end
+
def success_message
if integration.active?
format(s_('Integrations|%{integration} settings saved and active.'), integration: integration.title)
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index 265cf2a7698..c606ccf4a07 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -13,9 +13,10 @@ module WikiActions
included do
content_security_policy do |p|
next if p.directives.blank?
+ next unless Gitlab::CurrentSettings.diagramsnet_enabled?
default_frame_src = p.directives['frame-src'] || p.directives['default-src']
- frame_src_values = Array.wrap(default_frame_src) | ['https://embed.diagrams.net'].compact
+ frame_src_values = Array.wrap(default_frame_src) | [Gitlab::CurrentSettings.diagramsnet_url].compact
p.frame_src(*frame_src_values)
end
diff --git a/app/controllers/groups/settings/integrations_controller.rb b/app/controllers/groups/settings/integrations_controller.rb
index 0a63c3d304b..59b24e8103d 100644
--- a/app/controllers/groups/settings/integrations_controller.rb
+++ b/app/controllers/groups/settings/integrations_controller.rb
@@ -12,7 +12,9 @@ module Groups
layout 'group_settings'
def index
- @integrations = Integration.find_or_initialize_all_non_project_specific(Integration.for_group(group)).sort_by(&:title)
+ @integrations = Integration
+ .find_or_initialize_all_non_project_specific(Integration.for_group(group))
+ .sort_by(&:title)
end
def edit
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 30d2925fa5b..ebb8d9a7c53 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -167,7 +167,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
accept_pending_invitations(user: user) if new_user
persist_accepted_terms_if_required(user) if new_user
- perform_registration_tasks(user, oauth['provider']) if intent_to_register?
+ perform_registration_tasks(user, oauth['provider']) if new_user
sign_in_and_redirect_or_verify_identity(user, auth_user, new_user)
end
else
@@ -249,11 +249,6 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
(request_params['remember_me'] == '1') if request_params.present?
end
- def intent_to_register?
- request_params = request.env['omniauth.params']
- (request_params['intent'] == 'register') if request_params.present?
- end
-
def store_redirect_fragment(redirect_fragment)
key = stored_location_key_for(:user)
location = session[key]
diff --git a/app/graphql/types/ci/catalog/resource_type.rb b/app/graphql/types/ci/catalog/resource_type.rb
index 7b3f746b666..7958a0ba558 100644
--- a/app/graphql/types/ci/catalog/resource_type.rb
+++ b/app/graphql/types/ci/catalog/resource_type.rb
@@ -29,9 +29,21 @@ module Types
resolver: Resolvers::ReleasesResolver,
alpha: { milestone: '16.1' }
+ field :star_count, GraphQL::Types::Int, null: false,
+ description: 'Number of times the catalog resource has been starred.',
+ alpha: { milestone: '16.1' }
+
+ field :forks_count, GraphQL::Types::Int, null: false, calls_gitaly: true,
+ description: 'Number of times the catalog resource has been forked.',
+ alpha: { milestone: '16.1' }
+
def web_path
::Gitlab::Routing.url_helpers.project_path(object.project)
end
+
+ def forks_count
+ BatchLoader::GraphQL.wrap(object.forks_count)
+ end
end
# rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index e163625f85f..1c988b9767f 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -337,6 +337,8 @@ module ApplicationSettingsHelper
:kroki_formats,
:plantuml_enabled,
:plantuml_url,
+ :diagramsnet_enabled,
+ :diagramsnet_url,
:polling_interval_multiplier,
:project_export_enabled,
:prometheus_metrics_enabled,
diff --git a/app/helpers/registrations_helper.rb b/app/helpers/registrations_helper.rb
index 6f6a440ab13..4acba9b68d7 100644
--- a/app/helpers/registrations_helper.rb
+++ b/app/helpers/registrations_helper.rb
@@ -15,8 +15,9 @@ module RegistrationsHelper
'devise/shared/signup_box'
end
+ # overridden in EE
def register_omniauth_params(_local_assigns)
- { intent: :register }
+ {}
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index aafce07dcdb..0e7fca65208 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -186,6 +186,11 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :sourcegraph_url, presence: true, if: :sourcegraph_enabled
+ validates :diagramsnet_url,
+ presence: true,
+ addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ enforce_sanitization: true }),
+ if: :diagramsnet_enabled
+
validates :gitpod_url,
presence: true,
addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ enforce_sanitization: true }),
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 81ce057fa22..ea07fe99c99 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -132,6 +132,8 @@ module ApplicationSettingImplementation
personal_access_token_prefix: 'glpat-',
plantuml_enabled: false,
plantuml_url: nil,
+ diagramsnet_enabled: true,
+ diagramsnet_url: 'https://embed.diagrams.net',
polling_interval_multiplier: 1,
productivity_analytics_start_date: Time.current,
project_download_export_limit: 1,
diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb
index f400b8fe046..dfdf6bca251 100644
--- a/app/models/ci/catalog/resource.rb
+++ b/app/models/ci/catalog/resource.rb
@@ -17,7 +17,7 @@ module Ci
scope :order_by_name_desc, -> { joins(:project).merge(Project.sorted_by_name_desc) }
scope :order_by_name_asc, -> { joins(:project).merge(Project.sorted_by_name_asc) }
- delegate :avatar_path, :description, :name, to: :project
+ delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project
def versions
project.releases.order_released_desc
diff --git a/app/views/admin/application_settings/_diagramsnet.html.haml b/app/views/admin/application_settings/_diagramsnet.html.haml
new file mode 100644
index 00000000000..e493110a9dc
--- /dev/null
+++ b/app/views/admin/application_settings/_diagramsnet.html.haml
@@ -0,0 +1,25 @@
+- expanded = integration_expanded?('diagramsnet_')
+%section.settings.as-diagramsnet.no-animate#js-diagramsnet-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = _('Diagrams.net')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Render diagrams in your documents using diagrams.net.')
+ = link_to _('Learn more.'), help_page_path('administration/integration/diagrams_net.md'), target: '_blank', rel: 'noopener noreferrer'
+ .settings-content
+ = gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-diagramsnet-settings'), html: { class: 'fieldset-form', id: 'diagramsnet-settings' } do |f|
+ = form_errors(@application_setting) if expanded
+
+ %fieldset
+ .form-group
+ = f.gitlab_ui_checkbox_component :diagramsnet_enabled,
+ _('Enable diagrams.net')
+ .form-group
+ = f.label :diagramsnet_url, _('Diagrams.net URL'), class: 'label-bold'
+ = f.text_field :diagramsnet_url, class: 'form-control gl-form-input', placeholder: 'https://embed.diagrams.net'
+ .form-text.text-muted
+ = _('The hostname of your diagrams.net server.')
+
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index e6c27c1bc84..3413774b361 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -94,6 +94,7 @@
= render 'admin/application_settings/kroki'
= render 'admin/application_settings/mailgun'
= render 'admin/application_settings/plantuml'
+= render 'admin/application_settings/diagramsnet'
= render 'admin/application_settings/sourcegraph'
= render_if_exists 'admin/application_settings/slack'
-# this partial is from JiHu, see details in https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/417
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index e60fa8f019d..5d693ac603d 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -10,8 +10,6 @@
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, daily_commits|
%li.js-commit-header.gl-mt-7.gl-pb-2.gl-border-b{ data: { day: day } }
%span.day.font-weight-bold= l(day, format: '%b %d, %Y')
- %span -
- %span.commits-count= n_("%d commit", "%d commits", daily_commits.size) % daily_commits.size
%li.commits-row{ data: { day: day } }
%ul.content-list.commit-list.flex-list
diff --git a/app/workers/concerns/worker_attributes.rb b/app/workers/concerns/worker_attributes.rb
index 1674ed1483a..41cdf43bbbb 100644
--- a/app/workers/concerns/worker_attributes.rb
+++ b/app/workers/concerns/worker_attributes.rb
@@ -33,6 +33,8 @@ module WorkerAttributes
security_scans: 2
}.stringify_keys.freeze
+ DEFAULT_DEFER_DELAY = 5.seconds
+
class_methods do
def feature_category(value, *extras)
set_class_attribute(:feature_category, value)
@@ -190,5 +192,22 @@ module WorkerAttributes
def big_payload?
!!get_class_attribute(:big_payload)
end
+
+ def defer_on_database_health_signal(gitlab_schema, delay_by = DEFAULT_DEFER_DELAY, tables = [])
+ set_class_attribute(
+ :database_health_check_attrs,
+ { gitlab_schema: gitlab_schema, delay_by: delay_by, tables: tables }
+ )
+ end
+
+ def defer_on_database_health_signal?
+ return false unless Feature.enabled?(:defer_sidekiq_workers_on_database_health_signal, type: :worker)
+
+ database_health_check_attrs.present?
+ end
+
+ def database_health_check_attrs
+ get_class_attribute(:database_health_check_attrs)
+ end
end
end
diff --git a/app/workers/packages/cleanup/delete_orphaned_dependencies_worker.rb b/app/workers/packages/cleanup/delete_orphaned_dependencies_worker.rb
index 0b3d3c98742..4ace9a0e42e 100644
--- a/app/workers/packages/cleanup/delete_orphaned_dependencies_worker.rb
+++ b/app/workers/packages/cleanup/delete_orphaned_dependencies_worker.rb
@@ -20,8 +20,6 @@ module Packages
REDIS_EXPIRATION_TIME = 2.hours.to_i
def perform
- return unless enabled?
-
start_time
dependency_id = last_processed_dependency_id
@@ -44,10 +42,6 @@ module Packages
private
- def enabled?
- Feature.enabled?(:packages_delete_orphaned_dependencies_worker)
- end
-
def start_time
@start_time ||= ::Gitlab::Metrics::System.monotonic_time
end
diff --git a/config/feature_flags/development/packages_delete_orphaned_dependencies_worker.yml b/config/feature_flags/worker/defer_sidekiq_workers_on_database_health_signal.yml
index 2966d6b9bc3..7e8575074c0 100644
--- a/config/feature_flags/development/packages_delete_orphaned_dependencies_worker.yml
+++ b/config/feature_flags/worker/defer_sidekiq_workers_on_database_health_signal.yml
@@ -1,8 +1,8 @@
---
-name: packages_delete_orphaned_dependencies_worker
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113076
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/409458
-milestone: '16.0'
-type: development
-group: group::package registry
+name: defer_sidekiq_workers_on_database_health_signal
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121261
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412990
+milestone: '16.1'
+type: worker
+group: group::database
default_enabled: false
diff --git a/db/migrate/20230329235300_add_diagramsnet_to_application_settings.rb b/db/migrate/20230329235300_add_diagramsnet_to_application_settings.rb
new file mode 100644
index 00000000000..9e351f190f4
--- /dev/null
+++ b/db/migrate/20230329235300_add_diagramsnet_to_application_settings.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class AddDiagramsnetToApplicationSettings < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # limit is added in 20230406115900_add_diagramsnet_text_limit.rb
+ def change
+ add_column :application_settings, :diagramsnet_enabled, :boolean, default: true, null: false
+ add_column :application_settings, :diagramsnet_url, :text, default: 'https://embed.diagrams.net'
+ end
+ # rubocop:enable Migration/AddLimitToTextColumns
+end
diff --git a/db/migrate/20230406115900_add_diagramsnet_text_limit.rb b/db/migrate/20230406115900_add_diagramsnet_text_limit.rb
new file mode 100644
index 00000000000..27155c70c56
--- /dev/null
+++ b/db/migrate/20230406115900_add_diagramsnet_text_limit.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddDiagramsnetTextLimit < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :application_settings, :diagramsnet_url, 2048
+ end
+
+ def down
+ remove_text_limit :application_settings, :diagramsnet_url
+ end
+end
diff --git a/db/migrate/20230519103034_truncate_schema_inconsistencies_table.rb b/db/migrate/20230519103034_truncate_schema_inconsistencies_table.rb
new file mode 100644
index 00000000000..ddf165ca991
--- /dev/null
+++ b/db/migrate/20230519103034_truncate_schema_inconsistencies_table.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class TruncateSchemaInconsistenciesTable < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ truncate_tables!('schema_inconsistencies')
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/migrate/20230519112106_add_diff_column_to_schema_inconsistencies.rb b/db/migrate/20230519112106_add_diff_column_to_schema_inconsistencies.rb
new file mode 100644
index 00000000000..7b42fc233ff
--- /dev/null
+++ b/db/migrate/20230519112106_add_diff_column_to_schema_inconsistencies.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class AddDiffColumnToSchemaInconsistencies < Gitlab::Database::Migration[2.1]
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # rubocop:disable Rails/NotNullColumn
+ # limit is added in 20230519135414
+ def change
+ add_column :schema_inconsistencies, :diff, :text, null: false
+ end
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Rails/NotNullColumn
+end
diff --git a/db/migrate/20230519135414_add_text_limit_for_diff.rb b/db/migrate/20230519135414_add_text_limit_for_diff.rb
new file mode 100644
index 00000000000..682e28297db
--- /dev/null
+++ b/db/migrate/20230519135414_add_text_limit_for_diff.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddTextLimitForDiff < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :schema_inconsistencies, :diff, 6144
+ end
+
+ def down
+ remove_text_limit :schema_inconsistencies, :diff
+ end
+end
diff --git a/db/post_migrate/20230521521419_drop_merge_request_state_id_temp_index.rb b/db/post_migrate/20230521521419_drop_merge_request_state_id_temp_index.rb
new file mode 100644
index 00000000000..3411b374d7e
--- /dev/null
+++ b/db/post_migrate/20230521521419_drop_merge_request_state_id_temp_index.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class DropMergeRequestStateIdTempIndex < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'merge_requests_state_id_temp_index'
+ INDEX_CONDITION = "state_id IN (2, 3)"
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index(:merge_requests, :id, where: INDEX_CONDITION, name: INDEX_NAME)
+ end
+
+ def down
+ add_concurrent_index(:merge_requests, :id, where: INDEX_CONDITION, name: INDEX_NAME)
+ end
+end
diff --git a/db/schema_migrations/20230329235300 b/db/schema_migrations/20230329235300
new file mode 100644
index 00000000000..0f3d4099553
--- /dev/null
+++ b/db/schema_migrations/20230329235300
@@ -0,0 +1 @@
+44df5e98715af0cf9f8920e8fc35754901d578ae5c1dcc5fa7a3fb9ee49f995b \ No newline at end of file
diff --git a/db/schema_migrations/20230406115900 b/db/schema_migrations/20230406115900
new file mode 100644
index 00000000000..38fa9134dac
--- /dev/null
+++ b/db/schema_migrations/20230406115900
@@ -0,0 +1 @@
+85cf98db148785c25a6ed472a300f5967aea916ef9b937d78bff90e33905886f \ No newline at end of file
diff --git a/db/schema_migrations/20230519103034 b/db/schema_migrations/20230519103034
new file mode 100644
index 00000000000..cfefd850e23
--- /dev/null
+++ b/db/schema_migrations/20230519103034
@@ -0,0 +1 @@
+99247380d6a83a3eb0d03270848d7a53276072633073343a5b3bda709b836694 \ No newline at end of file
diff --git a/db/schema_migrations/20230519112106 b/db/schema_migrations/20230519112106
new file mode 100644
index 00000000000..a8eb4476be6
--- /dev/null
+++ b/db/schema_migrations/20230519112106
@@ -0,0 +1 @@
+be307c86a15692e694a276b52274ae400a90f31dfb7fde6ace37683fba1ed4b2 \ No newline at end of file
diff --git a/db/schema_migrations/20230519135414 b/db/schema_migrations/20230519135414
new file mode 100644
index 00000000000..159130e2d44
--- /dev/null
+++ b/db/schema_migrations/20230519135414
@@ -0,0 +1 @@
+e0aaa0eb24b0e2be91593198366d85408ea5b88399c1a2bd4e41fa9acabeb227 \ No newline at end of file
diff --git a/db/schema_migrations/20230521521419 b/db/schema_migrations/20230521521419
new file mode 100644
index 00000000000..9591cbde586
--- /dev/null
+++ b/db/schema_migrations/20230521521419
@@ -0,0 +1 @@
+04dd0a9bdfe5890f8a44aa15bbbc295a25883bb27fbee0015937a1f6c432a231 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 123cf711672..a23d01c628e 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11837,6 +11837,8 @@ CREATE TABLE application_settings (
remember_me_enabled boolean DEFAULT true NOT NULL,
encrypted_anthropic_api_key bytea,
encrypted_anthropic_api_key_iv bytea,
+ diagramsnet_enabled boolean DEFAULT true NOT NULL,
+ diagramsnet_url text DEFAULT 'https://embed.diagrams.net'::text,
allow_account_deletion boolean DEFAULT true NOT NULL,
vertex_project text,
wiki_asciidoc_allow_uri_includes boolean DEFAULT false NOT NULL,
@@ -11856,6 +11858,7 @@ CREATE TABLE application_settings (
CONSTRAINT app_settings_registry_repair_worker_max_concurrency_positive CHECK ((container_registry_data_repair_detail_worker_max_concurrency >= 0)),
CONSTRAINT app_settings_yaml_max_depth_positive CHECK ((max_yaml_depth > 0)),
CONSTRAINT app_settings_yaml_max_size_positive CHECK ((max_yaml_size_bytes > 0)),
+ CONSTRAINT check_0542340619 CHECK ((char_length(diagramsnet_url) <= 2048)),
CONSTRAINT check_17d9558205 CHECK ((char_length((kroki_url)::text) <= 1024)),
CONSTRAINT check_2b820eaac3 CHECK ((char_length(database_grafana_tag) <= 255)),
CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
@@ -22256,6 +22259,8 @@ CREATE TABLE schema_inconsistencies (
object_name text NOT NULL,
table_name text NOT NULL,
valitador_name text NOT NULL,
+ diff text NOT NULL,
+ CONSTRAINT check_001d186ac7 CHECK ((char_length(diff) <= 6144)),
CONSTRAINT check_120b6c86d0 CHECK ((char_length(valitador_name) <= 63)),
CONSTRAINT check_a0411f31fd CHECK ((char_length(object_name) <= 63)),
CONSTRAINT check_d96408dfd2 CHECK ((char_length(table_name) <= 63))
@@ -33101,8 +33106,6 @@ CREATE UNIQUE INDEX merge_request_user_mentions_on_mr_id_and_note_id_index ON me
CREATE UNIQUE INDEX merge_request_user_mentions_on_mr_id_index ON merge_request_user_mentions USING btree (merge_request_id) WHERE (note_id IS NULL);
-CREATE INDEX merge_requests_state_id_temp_index ON merge_requests USING btree (id) WHERE (state_id = ANY (ARRAY[2, 3]));
-
CREATE INDEX migrate_index_users_for_active_billable_users ON users USING btree (id) WHERE (((state)::text = 'active'::text) AND ((user_type IS NULL) OR (user_type = 0) OR (user_type = ANY (ARRAY[0, 6, 4, 13]))) AND ((user_type IS NULL) OR (user_type = 0) OR (user_type = ANY (ARRAY[0, 4, 5]))));
CREATE INDEX note_mentions_temp_index ON notes USING btree (id, noteable_type) WHERE (note ~~ '%@%'::text);
diff --git a/doc/administration/integration/diagrams_net.md b/doc/administration/integration/diagrams_net.md
new file mode 100644
index 00000000000..fe5e730a064
--- /dev/null
+++ b/doc/administration/integration/diagrams_net.md
@@ -0,0 +1,53 @@
+---
+stage: Create
+group: Source Code
+info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments"
+type: reference, howto
+---
+
+# Diagrams.net **(FREE)**
+
+With the [diagrams.net](https://www.diagrams.net/) integration, you can create and embed SVG diagrams in wikis.
+The diagram editor is available in both the Markdown editor and the content editor.
+
+On GitLab.com, this integration is enabled for all SaaS users and does not require any additional configuration.
+
+On self-managed GitLab, you can choose to integrate with the free [diagrams.net](https://www.diagrams.net/)
+website, or use a self-managed diagrams.net site in offline environments.
+
+To set up the integration on a self-managed instance, you must:
+
+1. Choose to integrate with the free diagrams.net website or
+ [configure your diagrams.net server](#configure-your-diagramsnet-server).
+1. [Enable the integration](#enable-diagramsnet-integration).
+
+After completing the integration, the diagrams.net editor opens with the URL you provided.
+
+## Configure your diagrams.net server
+
+You can set up your own diagrams.net server to generate the diagrams.
+
+This is a required step for users on offline (or "air-gapped") self-managed GitLab installations.
+
+For example, to run a diagrams.net container in Docker, run the following command:
+
+```shell
+docker run -it --rm --name="draw" -p 8080:8080 -p 8443:8443 jgraph/drawio
+```
+
+Make note of the hostname of the server running the container, to be used as the diagrams.net URL
+when you enable the integration.
+
+For more information, see [Run your own diagrams.net server with Docker](https://www.diagrams.net/blog/diagrams-docker-app).
+
+## Enable Diagrams.net integration
+
+1. Sign in to GitLab as an [Administrator](../../user/permissions.md) user.
+1. On the top bar, select **Main menu > Admin**.
+1. On the left sidebar, select **Settings > General**.
+1. Expand **Diagrams.net**.
+1. Select the **Enable Diagrams.net** checkbox.
+1. Enter the Diagrams.net URL. To connect to:
+ - The free public instance: enter `https://embed.diagrams.net`.
+ - A self-managed diagrams.net instance: enter the URL you [configured earlier](#configure-your-diagramsnet-server).
+1. Select **Save changes**.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index a50de40069e..3f8a3b6cb83 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -12446,9 +12446,11 @@ Represents the total number of issues and their weights for a particular day.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cicatalogresourcedescription"></a>`description` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. Description of the catalog resource. |
+| <a id="cicatalogresourceforkscount"></a>`forksCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. Number of times the catalog resource has been forked. |
| <a id="cicatalogresourceicon"></a>`icon` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. Icon for the catalog resource. |
| <a id="cicatalogresourceid"></a>`id` **{warning-solid}** | [`ID!`](#id) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. ID of the catalog resource. |
| <a id="cicatalogresourcename"></a>`name` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. Name of the catalog resource. |
+| <a id="cicatalogresourcestarcount"></a>`starCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. Number of times the catalog resource has been starred. |
| <a id="cicatalogresourcewebpath"></a>`webPath` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. Web path of the catalog resource. |
#### Fields with arguments
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 559332858b3..231cff418e5 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -67,6 +67,8 @@ Example response:
"repository_storages_weighted": {"default": 100},
"plantuml_enabled": false,
"plantuml_url": null,
+ "diagramsnet_enabled": true,
+ "diagramsnet_url": "https://embed.diagrams.net",
"kroki_enabled": false,
"kroki_url": null,
"terminal_max_session_time": 0,
@@ -193,6 +195,8 @@ Example response:
"repository_storages": ["default"],
"plantuml_enabled": false,
"plantuml_url": null,
+ "diagramsnet_enabled": true,
+ "diagramsnet_url": "https://embed.diagrams.net",
"terminal_max_session_time": 0,
"polling_interval_multiplier": 1.0,
"rsa_key_restriction": 0,
@@ -325,6 +329,8 @@ listed in the descriptions of the relevant settings.
| `delayed_group_deletion` **(PREMIUM SELF)** | boolean | no | Enable delayed group deletion. Default is `true`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352959) in GitLab 15.0. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), disables and locks the group-level setting for delayed protect deletion when set to `false`. From [GitLab 15.11](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113332), with the `always_perform_delayed_deletion` feature flag enabled, this attribute has been removed. This attribute will be completely removed in GitLab 16.0. |
| `default_project_deletion_protection` **(PREMIUM SELF)** | boolean | no | Enable default project deletion protection so only administrators can delete projects. Default is `false`. |
| `deletion_adjourned_period` **(PREMIUM SELF)** | integer | no | The number of days to wait before deleting a project or group that is marked for deletion. Value must be between `1` and `90`. Defaults to `7`. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), a hook on `deletion_adjourned_period` sets the period to `1` on every update, and sets both `delayed_project_deletion` and `delayed_group_deletion` to `false` if the period is `0`. |
+| `diagramsnet_enabled` | boolean | no | (If enabled, requires `diagramsnet_url`) Enable [Diagrams.net integration](../administration/integration/diagrams_net.md). Default is `true`. |
+| `diagramsnet_url` | string | required by: `diagramsnet_enabled` | The Diagrams.net instance URL for integration. |
| `diff_max_patch_bytes` | integer | no | Maximum [diff patch size](../user/admin_area/diff_limits.md), in bytes. |
| `diff_max_files` | integer | no | Maximum [files in a diff](../user/admin_area/diff_limits.md). |
| `diff_max_lines` | integer | no | Maximum [lines in a diff](../user/admin_area/diff_limits.md). |
diff --git a/doc/api/users.md b/doc/api/users.md
index 4d6bdb26020..fc441ab1925 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -2204,12 +2204,15 @@ Returns:
- `403 Forbidden` if not authenticated as an administrator.
- `404 User Not Found` if user cannot be found.
-## Create a CI runner **(FREE SELF)**
+## Create a runner **(FREE SELF)**
-It creates a new runner, linked to the current user.
+Creates a runner linked to the current user.
-Requires administrator access or ownership of the target namespace or project. Token values are returned once. Make sure you save it because you can't access
-it again.
+Prerequisites:
+
+- You must be an administrator or have the Owner role of the target namespace or project.
+
+Be sure to copy or save the `token` in the response, the value cannot be retrieved again.
```plaintext
POST /user/runners
diff --git a/doc/architecture/blueprints/runner_tokens/index.md b/doc/architecture/blueprints/runner_tokens/index.md
index 1cc47c05d83..0dc592531a3 100644
--- a/doc/architecture/blueprints/runner_tokens/index.md
+++ b/doc/architecture/blueprints/runner_tokens/index.md
@@ -532,7 +532,7 @@ Yes, existing runners will continue to work as usual. This change only affects r
### Can runners still be created programmatically?
-A new [POST /user/runners REST API](../../../api/users.md#create-a-ci-runner) was introduced in
+A new [POST /user/runners REST API](../../../api/users.md#create-a-runner) was introduced in
GitLab 15.11, which allows a runner to be created in the context of an authenticated user. This should only be used in
scenarios where the runner configuration is dynamic, or not reusable. If the runner configuration is static, it is
preferable to reuse the authentication token of an existing runner.
diff --git a/doc/ci/examples/index.md b/doc/ci/examples/index.md
index f2871e50617..a020f673fd7 100644
--- a/doc/ci/examples/index.md
+++ b/doc/ci/examples/index.md
@@ -23,11 +23,9 @@ The following table lists examples with step-by-step tutorials that are containe
| Use case | Resource |
|-------------------------------|----------|
-| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](../testing/browser_performance_testing.md). |
| Deployment with Dpl | [Using `dpl` as deployment tool](deployment/index.md). |
| GitLab Pages | See the [GitLab Pages](../../user/project/pages/index.md) documentation for a complete example of deploying a static site. |
| End-to-end testing | [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md). |
-| Load performance testing | [Load Performance Testing with the k6 container](../testing/load_performance_testing.md). |
| Multi project pipeline | [Build, test deploy using multi project pipeline](https://gitlab.com/gitlab-examples/upstream-project). |
| npm with semantic-release | [Publish npm packages to the GitLab Package Registry using semantic-release](semantic-release.md). |
| PHP with Laravel, Envoy | [Test and deploy Laravel applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md). |
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index b8ed1c06324..2026a033150 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -567,13 +567,12 @@ This example links to `<wiki_root>/miscellaneous.md`:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322174) in GitLab 15.10.
-NOTE:
-Use of the diagrams.net editor is not available on offline environments.
-
In wikis, you can use the [diagrams.net](https://www.diagrams.net/) editor to create diagrams. You
can also edit diagrams created with the diagrams.net editor. The diagram editor is available in both
the Markdown editor and the content editor.
+For more information, see [Diagrams.net](../administration/integration/diagrams_net.md).
+
##### Markdown editor
To create a diagram in the Markdown editor:
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index eaeb2a500c2..fa8e788759d 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -463,6 +463,7 @@ To work around the issue, give these users the Guest role or higher to any proje
- [Confidential issues](project/issues/confidential_issues.md)
- [Container Registry permissions](packages/container_registry/index.md#container-registry-visibility-permissions)
- [Release permissions](project/releases/index.md#release-permissions)
+- [Read-only namespaces](../user/read_only_namespaces.md)
## Custom roles **(ULTIMATE)**
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 78be9ee75e9..3b553c4a66e 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -125,6 +125,10 @@ module API
given plantuml_enabled: ->(val) { val } do
requires :plantuml_url, type: String, desc: 'The PlantUML server URL'
end
+ optional :diagramsnet_enabled, type: Boolean, desc: 'Enable Diagrams.net'
+ given diagramsnet_enabled: ->(val) { val } do
+ requires :diagramsnet_url, type: String, desc: 'The Diagrams.net server URL'
+ end
optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
optional :project_export_enabled, type: Boolean, desc: 'Enable project export'
optional :prometheus_metrics_enabled, type: Boolean, desc: 'Enable Prometheus metrics'
diff --git a/lib/gitlab/database/load_balancing/host.rb b/lib/gitlab/database/load_balancing/host.rb
index bdbb80d6f31..f8ed5fcd4cc 100644
--- a/lib/gitlab/database/load_balancing/host.rb
+++ b/lib/gitlab/database/load_balancing/host.rb
@@ -16,6 +16,43 @@ module Gitlab
PG::Error
].freeze
+ # This query checks that the current user has permissions before we try and query logical replication status. We
+ # also only allow >= PG14 because these views are only accessible to superuser before PG14 even if the
+ # has_table_privilege says otherwise.
+ CAN_TRACK_LOGICAL_LSN_QUERY = <<~SQL.squish.freeze
+ SELECT
+ has_table_privilege('pg_replication_origin_status', 'select')
+ AND
+ has_function_privilege('pg_show_replication_origin_status()', 'execute')
+ AND current_setting('server_version_num', true)::int >= 140000
+ AS allowed
+ SQL
+
+ # The following is necessary to handle a mix of logical and physical replicas. We assume that if they have
+ # pg_replication_origin_status then they are a logical replica. In a logical replica we need to use
+ # `remote_lsn` rather than `pg_last_wal_replay_lsn` in order for our LSN to be comparable to the source
+ # cluster. This logic would be broken if we have 2 logical subscriptions or if we have a logical subscription
+ # in the source primary cluster. Read more at https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121621
+ LATEST_LSN_WITH_LOGICAL_QUERY = <<~SQL.squish.freeze
+ CASE
+ WHEN (SELECT TRUE FROM pg_replication_origin_status) THEN
+ (SELECT remote_lsn FROM pg_replication_origin_status)
+ WHEN pg_is_in_recovery() THEN
+ pg_last_wal_replay_lsn()
+ ELSE
+ pg_current_wal_insert_lsn()
+ END
+ SQL
+
+ LATEST_LSN_WITHOUT_LOGICAL_QUERY = <<~SQL.squish.freeze
+ CASE
+ WHEN pg_is_in_recovery() THEN
+ pg_last_wal_replay_lsn()
+ ELSE
+ pg_current_wal_insert_lsn()
+ END
+ SQL
+
# host - The address of the database.
# load_balancer - The LoadBalancer that manages this Host.
def initialize(host, load_balancer, port: nil)
@@ -30,6 +67,7 @@ module Gitlab
@online = true
@last_checked_at = Time.zone.now
+ # Randomly somewhere in between interval and 2*interval we'll refresh the status of the host
interval = load_balancer.configuration.replica_check_interval
@intervals = (interval..(interval * 2)).step(0.5).to_a
end
@@ -91,6 +129,7 @@ module Gitlab
end
def refresh_status
+ @latest_lsn_query = nil # Periodically clear the cached @latest_lsn_query value in case permissions change
@online = replica_is_up_to_date?
@last_checked_at = Time.zone.now
end
@@ -142,11 +181,11 @@ module Gitlab
# primary.
#
# This method will return nil if no lag size could be calculated.
- def replication_lag_size
- location = connection.quote(primary_write_location)
+ def replication_lag_size(location = primary_write_location)
+ location = connection.quote(location)
+
row = query_and_release(<<-SQL.squish)
- SELECT pg_wal_lsn_diff(#{location}, pg_last_wal_replay_lsn())::float
- AS diff
+ SELECT pg_wal_lsn_diff(#{location}, (#{latest_lsn_query}))::float AS diff
SQL
row['diff'].to_i if row.any?
@@ -173,22 +212,8 @@ module Gitlab
#
# location - The transaction write location as reported by a primary.
def caught_up?(location)
- string = connection.quote(location)
-
- # In case the host is a primary pg_last_wal_replay_lsn/pg_last_xlog_replay_location() returns
- # NULL. The recovery check ensures we treat the host as up-to-date in
- # such a case.
- query = <<-SQL.squish
- SELECT NOT pg_is_in_recovery()
- OR pg_wal_lsn_diff(pg_last_wal_replay_lsn(), #{string}) >= 0
- AS result
- SQL
-
- row = query_and_release(query)
-
- ::Gitlab::Utils.to_boolean(row['result'])
- rescue *CONNECTION_ERRORS
- false
+ lag = replication_lag_size(location)
+ lag.present? && lag.to_i <= 0
end
def query_and_release(sql)
@@ -198,6 +223,22 @@ module Gitlab
ensure
release_connection
end
+
+ private
+
+ def can_track_logical_lsn?
+ row = query_and_release(CAN_TRACK_LOGICAL_LSN_QUERY)
+
+ ::Gitlab::Utils.to_boolean(row['allowed'])
+ rescue *CONNECTION_ERRORS
+ false
+ end
+
+ # The LATEST_LSN_WITH_LOGICAL query requires permissions that may not be present in self-managed configurations.
+ # We fallback gracefully to the query that does not correctly handle logical replicas for such configurations.
+ def latest_lsn_query
+ @latest_lsn_query ||= can_track_logical_lsn? ? LATEST_LSN_WITH_LOGICAL_QUERY : LATEST_LSN_WITHOUT_LOGICAL_QUERY
+ end
end
end
end
diff --git a/lib/gitlab/database/schema_validation/schema_inconsistency.rb b/lib/gitlab/database/schema_validation/schema_inconsistency.rb
index b682b26775a..9f39db5b4c0 100644
--- a/lib/gitlab/database/schema_validation/schema_inconsistency.rb
+++ b/lib/gitlab/database/schema_validation/schema_inconsistency.rb
@@ -8,7 +8,7 @@ module Gitlab
belongs_to :issue
- validates :object_name, :valitador_name, :table_name, presence: true
+ validates :object_name, :valitador_name, :table_name, :diff, presence: true
scope :with_open_issues, -> { joins(:issue).where('issue.state_id': Issue.available_states[:opened]) }
end
diff --git a/lib/gitlab/database/schema_validation/track_inconsistency.rb b/lib/gitlab/database/schema_validation/track_inconsistency.rb
index 5e437e2f2ff..32118f1f60d 100644
--- a/lib/gitlab/database/schema_validation/track_inconsistency.rb
+++ b/lib/gitlab/database/schema_validation/track_inconsistency.rb
@@ -4,6 +4,8 @@ module Gitlab
module Database
module SchemaValidation
class TrackInconsistency
+ COLUMN_TEXT_LIMIT = 6144
+
def initialize(inconsistency, project, user)
@inconsistency = inconsistency
@project = project
@@ -12,7 +14,7 @@ module Gitlab
def execute
return unless Gitlab.com?
- return if inconsistency_record.present?
+ return refresh_issue if inconsistency_record.present?
result = ::Issues::CreateService.new(container: project, current_user: user, params: params,
spam_params: nil).execute
@@ -25,18 +27,19 @@ module Gitlab
attr_reader :inconsistency, :project, :user
def track_inconsistency(issue)
- schema_inconsistency_model.create(
+ schema_inconsistency_model.create!(
issue: issue,
object_name: inconsistency.object_name,
table_name: inconsistency.table_name,
- valitador_name: inconsistency.type
+ valitador_name: inconsistency.type,
+ diff: inconsistency_diff
)
end
def params
{
title: issue_title,
- description: issue_description,
+ description: description,
issue_type: 'issue',
labels: %w[database database-inconsistency-report]
}
@@ -46,7 +49,7 @@ module Gitlab
"New schema inconsistency: #{inconsistency.object_name}"
end
- def issue_description
+ def description
<<~MSG
We have detected a new schema inconsistency.
@@ -85,8 +88,24 @@ module Gitlab
Gitlab::Database::SchemaValidation::SchemaInconsistency
end
+ def refresh_issue
+ return if inconsistency_diff == inconsistency_record.diff # Nothing to refresh
+
+ note = ::Notes::CreateService.new(
+ inconsistency_record.issue.project,
+ user,
+ { noteable_type: 'Issue', noteable: inconsistency_record.issue, note: description }
+ ).execute
+
+ inconsistency_record.update!(diff: inconsistency_diff) if note.persisted?
+ end
+
+ def inconsistency_diff
+ @inconsistency_diff ||= inconsistency.diff.to_s.first(COLUMN_TEXT_LIMIT)
+ end
+
def inconsistency_record
- schema_inconsistency_model.with_open_issues.find_by(
+ @inconsistency_record ||= schema_inconsistency_model.with_open_issues.find_by(
object_name: inconsistency.object_name,
table_name: inconsistency.table_name,
valitador_name: inconsistency.type
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 904a2ccc79b..eef176687e7 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -51,6 +51,8 @@ module Gitlab
gon.dot_com = Gitlab.com?
gon.uf_error_prefix = ::Gitlab::Utils::ErrorMessage::UF_ERROR_PREFIX
+ gon.diagramsnet_url = Gitlab::CurrentSettings.diagramsnet_url if Gitlab::CurrentSettings.diagramsnet_enabled
+
if current_user
gon.current_user_id = current_user.id
gon.current_username = current_user.username
diff --git a/lib/gitlab/sidekiq_middleware/defer_jobs.rb b/lib/gitlab/sidekiq_middleware/defer_jobs.rb
index e91621f96ce..76b44046a81 100644
--- a/lib/gitlab/sidekiq_middleware/defer_jobs.rb
+++ b/lib/gitlab/sidekiq_middleware/defer_jobs.rb
@@ -4,6 +4,7 @@ module Gitlab
module SidekiqMiddleware
class DeferJobs
include Sidekiq::ServerMiddleware
+
DELAY = ENV.fetch("SIDEKIQ_DEFER_JOBS_DELAY", 5.minutes)
FEATURE_FLAG_PREFIX = "defer_sidekiq_jobs"
diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake
index b4b34581f43..3c1bcfdddfa 100644
--- a/lib/tasks/gitlab/tw/codeowners.rake
+++ b/lib/tasks/gitlab/tw/codeowners.rake
@@ -48,18 +48,18 @@ namespace :tw do
CodeOwnerRule.new('Fuzz Testing', '@rdickenson'),
CodeOwnerRule.new('Geo', '@axil'),
CodeOwnerRule.new('Gitaly', '@eread'),
- CodeOwnerRule.new('GitLab Dedicated', '@drcatherinepope'),
+ # CodeOwnerRule.new('GitLab Dedicated', ''),
CodeOwnerRule.new('Global Search', '@ashrafkhamis'),
CodeOwnerRule.new('Import and Integrate', '@eread @ashrafkhamis'),
CodeOwnerRule.new('Infrastructure', '@sselhorn'),
# CodeOwnerRule.new('Knowledge', ''),
# CodeOwnerRule.new('MLOps', '')
- CodeOwnerRule.new('Observability', '@drcatherinepope'),
+ # CodeOwnerRule.new('Observability', ''),
CodeOwnerRule.new('Optimize', '@lciutacu'),
CodeOwnerRule.new('Organization', '@lciutacu'),
CodeOwnerRule.new('Package Registry', '@marcel.amirault'),
CodeOwnerRule.new('Pipeline Authoring', '@marcel.amirault'),
- CodeOwnerRule.new('Pipeline Execution', '@drcatherinepope'),
+ CodeOwnerRule.new('Pipeline Execution', '@marcel.amirault'),
CodeOwnerRule.new('Pipeline Security', '@marcel.amirault'),
CodeOwnerRule.new('Product Analytics', '@lciutacu'),
CodeOwnerRule.new('Product Planning', '@msedlakjakubowski'),
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7ebf97b7f9d..d66a953275e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8278,6 +8278,9 @@ msgstr ""
msgid "BroadcastMessages|Your message here"
msgstr ""
+msgid "Browse CI/CD Catalog"
+msgstr ""
+
msgid "Browse Directory"
msgstr ""
@@ -15829,6 +15832,12 @@ msgstr ""
msgid "Diagram saved successfully."
msgstr ""
+msgid "Diagrams.net"
+msgstr ""
+
+msgid "Diagrams.net URL"
+msgstr ""
+
msgid "Did not delete the source branch."
msgstr ""
@@ -16797,6 +16806,9 @@ msgstr ""
msgid "Enable dashboard limits on namespaces"
msgstr ""
+msgid "Enable diagrams.net"
+msgstr ""
+
msgid "Enable email notification"
msgstr ""
@@ -33856,9 +33868,6 @@ msgstr ""
msgid "Please create an index before enabling indexing"
msgstr ""
-msgid "Please delete your current license if you want to downgrade to the free plan."
-msgstr ""
-
msgid "Please enable and migrate to hashed storage to avoid security issues and ensure data integrity. %{migrate_link}"
msgstr ""
@@ -38025,6 +38034,9 @@ msgstr ""
msgid "Render diagrams in your documents using PlantUML."
msgstr ""
+msgid "Render diagrams in your documents using diagrams.net."
+msgstr ""
+
msgid "Renew subscription"
msgstr ""
@@ -39058,6 +39070,9 @@ msgstr ""
msgid "Runners|An upgrade is recommended for this runner"
msgstr ""
+msgid "Runners|Arch/Platform"
+msgstr ""
+
msgid "Runners|Architecture"
msgstr ""
@@ -39626,6 +39641,9 @@ msgstr ""
msgid "Runners|Support for registration tokens is deprecated"
msgstr ""
+msgid "Runners|System ID"
+msgstr ""
+
msgid "Runners|Tags"
msgstr ""
@@ -39644,6 +39662,9 @@ msgstr ""
msgid "Runners|The runner will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?"
msgstr ""
+msgid "Runners|The unique ID for each runner that uses this configuration."
+msgstr ""
+
msgid "Runners|These runners are assigned to this project."
msgstr ""
@@ -45461,6 +45482,9 @@ msgstr ""
msgid "The hostname of your Snowplow collector."
msgstr ""
+msgid "The hostname of your diagrams.net server."
+msgstr ""
+
msgid "The import cannot be canceled because it is %{project_status}"
msgstr ""
@@ -46415,6 +46439,9 @@ msgstr ""
msgid "This incident is already escalated with '%{escalation_policy_name}'."
msgstr ""
+msgid "This instance is now read-only. Don't worry, your data is safe. To change to GitLab Free and restore write access to this instance, delete your expired license."
+msgstr ""
+
msgid "This invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}."
msgstr ""
diff --git a/scripts/utils.sh b/scripts/utils.sh
index 40cf6716528..edfcf0f2dac 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -292,13 +292,6 @@ function fail_pipeline_early() {
fi
}
-function danger_as_local() {
- # Force danger to skip CI source GitLab and fallback to "local only git repo".
- unset GITLAB_CI
- # We need to base SHA to help danger determine the base commit for this shallow clone.
- bundle exec danger dry_run --fail-on-errors=true --verbose --base="${CI_MERGE_REQUEST_DIFF_BASE_SHA}" --head="${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA:-$CI_COMMIT_SHA}" --dangerfile="${DANGER_DANGERFILE:-Dangerfile}"
-}
-
# We're inlining this function in `.gitlab/ci/package-and-test/main.gitlab-ci.yml` so make sure to reflect any changes there
function assets_image_tag() {
local cache_assets_hash_file="cached-assets-hash.txt"
diff --git a/spec/controllers/admin/integrations_controller_spec.rb b/spec/controllers/admin/integrations_controller_spec.rb
index 9e2a2900b33..6e3d277fbd5 100644
--- a/spec/controllers/admin/integrations_controller_spec.rb
+++ b/spec/controllers/admin/integrations_controller_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe Admin::IntegrationsController do
- let(:admin) { create(:admin) }
+RSpec.describe Admin::IntegrationsController, feature_category: :integrations do
+ let_it_be(:admin) { create(:admin) }
before do
stub_feature_flags(remove_monitor_metrics: false)
@@ -19,14 +19,16 @@ RSpec.describe Admin::IntegrationsController do
end
describe '#edit' do
- Integration.available_integration_names.each do |integration_name|
- context integration_name.to_s do
- it 'successfully displays the template' do
- get :edit, params: { id: integration_name }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:edit)
- end
+ where(:integration_name) do
+ Integration.available_integration_names - Integration.project_specific_integration_names
+ end
+
+ with_them do
+ it 'successfully displays the template' do
+ get :edit, params: { id: integration_name }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:edit)
end
end
@@ -51,7 +53,7 @@ RSpec.describe Admin::IntegrationsController do
put :update, params: { id: integration.class.to_param, service: params }
end
- context 'valid params' do
+ context 'with valid params' do
let(:params) { { url: 'https://jira.gitlab-example.com', password: 'password' } }
it 'updates the integration' do
@@ -64,7 +66,7 @@ RSpec.describe Admin::IntegrationsController do
end
end
- context 'invalid params' do
+ context 'with invalid params' do
let(:params) { { url: 'invalid', password: 'password' } }
it 'does not update the integration' do
diff --git a/spec/controllers/groups/settings/integrations_controller_spec.rb b/spec/controllers/groups/settings/integrations_controller_spec.rb
index 3ae43c8ab7c..e21010b76f7 100644
--- a/spec/controllers/groups/settings/integrations_controller_spec.rb
+++ b/spec/controllers/groups/settings/integrations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Settings::IntegrationsController do
+RSpec.describe Groups::Settings::IntegrationsController, feature_category: :integrations do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
@@ -52,7 +52,11 @@ RSpec.describe Groups::Settings::IntegrationsController do
describe '#edit' do
context 'when user is not owner' do
it 'renders not_found' do
- get :edit, params: { group_id: group, id: Integration.available_integration_names(include_project_specific: false).sample }
+ get :edit,
+ params: {
+ group_id: group,
+ id: Integration.available_integration_names(include_project_specific: false).sample
+ }
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -88,7 +92,7 @@ RSpec.describe Groups::Settings::IntegrationsController do
put :update, params: { group_id: group, id: integration.class.to_param, service: params }
end
- context 'valid params' do
+ context 'with valid params' do
let(:params) { { url: 'https://jira.gitlab-example.com', password: 'password' } }
it 'updates the integration' do
@@ -97,7 +101,7 @@ RSpec.describe Groups::Settings::IntegrationsController do
end
end
- context 'invalid params' do
+ context 'with invalid params' do
let(:params) { { url: 'invalid', password: 'password' } }
it 'does not update the integration' do
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index ebfa48870a9..2e9fc1cece5 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -229,39 +229,19 @@ RSpec.describe OmniauthCallbacksController, type: :controller, feature_category:
end
end
- context 'sign up' do
+ context 'for sign up' do
include_context 'sign_up'
- context 'when intent to register is added to omniauth params' do
- before do
- request.env['omniauth.params'] = { 'intent' => 'register' }
- end
-
- it 'is allowed' do
- post provider
-
- expect(request.env['warden']).to be_authenticated
- end
-
- it 'redirects to welcome path' do
- post provider
+ it 'is allowed' do
+ post provider
- expect(response).to redirect_to(users_sign_up_welcome_path)
- end
+ expect(request.env['warden']).to be_authenticated
end
- context 'when intent to register is not added to omniauth params' do
- it 'is allowed' do
- post provider
-
- expect(request.env['warden']).to be_authenticated
- end
-
- it 'redirects to root path' do
- post provider
+ it 'redirects to welcome path' do
+ post provider
- expect(response).to redirect_to(root_path)
- end
+ expect(response).to redirect_to(users_sign_up_welcome_path)
end
end
diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
index c6a4dcbfdf0..5cf9d7c3fa0 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -63,7 +63,6 @@ RSpec.describe Projects::MergeRequests::CreationsController, feature_category: :
expect(total).to be > 0
expect(assigns(:hidden_commit_count)).to be > 0
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to match %r(<span class="commits-count">2 commits</span>)
end
end
@@ -77,7 +76,6 @@ RSpec.describe Projects::MergeRequests::CreationsController, feature_category: :
expect(total).to be > 0
expect(assigns(:hidden_commit_count)).to eq(0)
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to match %r(<span class="commits-count">#{total} commits</span>)
end
end
end
diff --git a/spec/factories/gitlab/database/background_migration/schema_inconsistencies.rb b/spec/factories/gitlab/database/background_migration/schema_inconsistencies.rb
index b71b0971417..1d2c460144d 100644
--- a/spec/factories/gitlab/database/background_migration/schema_inconsistencies.rb
+++ b/spec/factories/gitlab/database/background_migration/schema_inconsistencies.rb
@@ -7,5 +7,6 @@ FactoryBot.define do
object_name { 'name' }
table_name { 'table' }
valitador_name { 'validator' }
+ diff { 'diff' }
end
end
diff --git a/spec/features/commits/user_view_commits_spec.rb b/spec/features/commits/user_view_commits_spec.rb
index b58d7cf3741..e13bd90ff04 100644
--- a/spec/features/commits/user_view_commits_spec.rb
+++ b/spec/features/commits/user_view_commits_spec.rb
@@ -7,10 +7,6 @@ RSpec.describe 'Commit > User view commits', feature_category: :source_code_mana
let_it_be(:group) { create(:group, :public) }
shared_examples 'can view commits' do
- it 'displays the correct number of commits per day in the header' do
- expect(first('.js-commit-header').find('.commits-count').text).to eq('1 commit')
- end
-
it 'lists the correct number of commits' do
expect(page).to have_selector('#commits-list > li:nth-child(2) > ul', count: 1)
end
diff --git a/spec/features/merge_request/user_comments_on_merge_request_spec.rb b/spec/features/merge_request/user_comments_on_merge_request_spec.rb
index e113e305af5..3aa2ce2a154 100644
--- a/spec/features/merge_request/user_comments_on_merge_request_spec.rb
+++ b/spec/features/merge_request/user_comments_on_merge_request_spec.rb
@@ -6,12 +6,15 @@ RSpec.describe 'User comments on a merge request', :js, feature_category: :code_
include RepoHelpers
let(:project) { create(:project, :repository) }
+ let(:diagramsnet_url) { 'https://embed.diagrams.net' }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
before do
project.add_maintainer(user)
sign_in(user)
+ allow(Gitlab::CurrentSettings).to receive(:diagramsnet_enabled).and_return(true)
+ allow(Gitlab::CurrentSettings).to receive(:diagramsnet_url).and_return(diagramsnet_url)
visit(merge_request_path(merge_request))
end
diff --git a/spec/frontend/__helpers__/test_constants.js b/spec/frontend/__helpers__/test_constants.js
index 628b9b054d3..b5a585811d1 100644
--- a/spec/frontend/__helpers__/test_constants.js
+++ b/spec/frontend/__helpers__/test_constants.js
@@ -1,5 +1,6 @@
const FIXTURES_PATH = `/fixtures`;
const TEST_HOST = 'http://test.host';
+const DRAWIO_ORIGIN = 'https://embed.diagrams.net';
const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`;
@@ -15,6 +16,7 @@ const DUMMY_IMAGE_BLOB_PATH = 'SpongeBlob.png';
module.exports = {
FIXTURES_PATH,
TEST_HOST,
+ DRAWIO_ORIGIN,
DUMMY_IMAGE_URL,
GREEN_BOX_IMAGE_URL,
RED_BOX_IMAGE_URL,
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index f340dfab359..5604c589e37 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -1,9 +1,11 @@
import { GlModal } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
+import VueApollo from 'vue-apollo';
import setWindowLocation from 'helpers/set_window_location_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import createApolloProvider from 'helpers/mock_apollo_helper';
import BoardForm from '~/boards/components/board_form.vue';
import { formType } from '~/boards/constants';
@@ -42,7 +44,7 @@ const defaultProps = {
describe('BoardForm', () => {
let wrapper;
- let mutate;
+ let requestHandlers;
const findModal = () => wrapper.findComponent(GlModal);
const findModalActionPrimary = () => findModal().props('actionPrimary');
@@ -61,8 +63,43 @@ describe('BoardForm', () => {
},
});
- const createComponent = (props, provide) => {
+ const defaultHandlers = {
+ createBoardMutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ createBoard: {
+ board: { id: '1' },
+ errors: [],
+ },
+ },
+ }),
+ destroyBoardMutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ destroyBoard: {
+ board: { id: '1' },
+ },
+ },
+ }),
+ updateBoardMutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' }, errors: [] },
+ },
+ }),
+ };
+
+ const createMockApolloProvider = (handlers = {}) => {
+ Vue.use(VueApollo);
+ requestHandlers = handlers;
+
+ return createApolloProvider([
+ [createBoardMutation, handlers.createBoardMutationHandler],
+ [destroyBoardMutation, handlers.destroyBoardMutationHandler],
+ [updateBoardMutation, handlers.updateBoardMutationHandler],
+ ]);
+ };
+
+ const createComponent = ({ props, provide, handlers = defaultHandlers } = {}) => {
wrapper = shallowMountExtended(BoardForm, {
+ apolloProvider: createMockApolloProvider(handlers),
propsData: { ...defaultProps, ...props },
provide: {
boardBaseUrl: 'root',
@@ -70,23 +107,16 @@ describe('BoardForm', () => {
isProjectBoard: false,
...provide,
},
- mocks: {
- $apollo: {
- mutate,
- },
- },
store,
attachTo: document.body,
});
};
- afterEach(() => {
- mutate = null;
- });
-
describe('when user can not admin the board', () => {
beforeEach(() => {
- createComponent({ currentPage: formType.new });
+ createComponent({
+ props: { currentPage: formType.new },
+ });
});
it('hides modal footer when user is not a board admin', () => {
@@ -104,7 +134,9 @@ describe('BoardForm', () => {
describe('when user can admin the board', () => {
beforeEach(() => {
- createComponent({ canAdminBoard: true, currentPage: formType.new });
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.new },
+ });
});
it('shows modal footer when user is a board admin', () => {
@@ -123,7 +155,9 @@ describe('BoardForm', () => {
describe('when creating a new board', () => {
describe('on non-scoped-board', () => {
beforeEach(() => {
- createComponent({ canAdminBoard: true, currentPage: formType.new });
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.new },
+ });
});
it('clears the form', () => {
@@ -155,36 +189,30 @@ describe('BoardForm', () => {
findInput().trigger('keyup.enter', { metaKey: true });
};
- beforeEach(() => {
- mutate = jest.fn().mockResolvedValue({
- data: {
- createBoard: { board: { id: 'gid://gitlab/Board/123', webPath: 'test-path' } },
- },
- });
- });
-
it('does not call API if board name is empty', async () => {
- createComponent({ canAdminBoard: true, currentPage: formType.new });
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.new },
+ });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
- expect(mutate).not.toHaveBeenCalled();
+ expect(requestHandlers.createBoardMutationHandler).not.toHaveBeenCalled();
});
it('calls a correct GraphQL mutation and sets board in state', async () => {
- createComponent({ canAdminBoard: true, currentPage: formType.new });
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.new },
+ });
+
fillForm();
await waitForPromises();
- expect(mutate).toHaveBeenCalledWith({
- mutation: createBoardMutation,
- variables: {
- input: expect.objectContaining({
- name: 'test',
- }),
- },
+ expect(requestHandlers.createBoardMutationHandler).toHaveBeenCalledWith({
+ input: expect.objectContaining({
+ name: 'test',
+ }),
});
await waitForPromises();
@@ -192,14 +220,19 @@ describe('BoardForm', () => {
});
it('sets error in state if GraphQL mutation fails', async () => {
- mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
- createComponent({ canAdminBoard: true, currentPage: formType.new });
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.new },
+ handlers: {
+ ...defaultHandlers,
+ createBoardMutationHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
+ },
+ });
fillForm();
await waitForPromises();
- expect(mutate).toHaveBeenCalled();
+ expect(requestHandlers.createBoardMutationHandler).toHaveBeenCalled();
await waitForPromises();
expect(setBoardMock).not.toHaveBeenCalled();
@@ -208,21 +241,19 @@ describe('BoardForm', () => {
describe('when Apollo boards FF is on', () => {
it('calls a correct GraphQL mutation and emits addBoard event when creating a board', async () => {
- createComponent(
- { canAdminBoard: true, currentPage: formType.new },
- { isApolloBoard: true },
- );
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.new },
+ provide: { isApolloBoard: true },
+ });
+
fillForm();
await waitForPromises();
- expect(mutate).toHaveBeenCalledWith({
- mutation: createBoardMutation,
- variables: {
- input: expect.objectContaining({
- name: 'test',
- }),
- },
+ expect(requestHandlers.createBoardMutationHandler).toHaveBeenCalledWith({
+ input: expect.objectContaining({
+ name: 'test',
+ }),
});
await waitForPromises();
@@ -235,7 +266,9 @@ describe('BoardForm', () => {
describe('when editing a board', () => {
describe('on non-scoped-board', () => {
beforeEach(() => {
- createComponent({ canAdminBoard: true, currentPage: formType.edit });
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.edit },
+ });
});
it('clears the form', () => {
@@ -261,25 +294,19 @@ describe('BoardForm', () => {
});
it('calls GraphQL mutation with correct parameters when issues are not grouped', async () => {
- mutate = jest.fn().mockResolvedValue({
- data: {
- updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } },
- },
- });
setWindowLocation('https://test/boards/1');
- createComponent({ canAdminBoard: true, currentPage: formType.edit });
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.edit },
+ });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
- expect(mutate).toHaveBeenCalledWith({
- mutation: updateBoardMutation,
- variables: {
- input: expect.objectContaining({
- id: currentBoard.id,
- }),
- },
+ expect(requestHandlers.updateBoardMutationHandler).toHaveBeenCalledWith({
+ input: expect.objectContaining({
+ id: currentBoard.id,
+ }),
});
await waitForPromises();
@@ -288,25 +315,19 @@ describe('BoardForm', () => {
});
it('calls GraphQL mutation with correct parameters when issues are grouped by epic', async () => {
- mutate = jest.fn().mockResolvedValue({
- data: {
- updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } },
- },
- });
setWindowLocation('https://test/boards/1?group_by=epic');
- createComponent({ canAdminBoard: true, currentPage: formType.edit });
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.edit },
+ });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
- expect(mutate).toHaveBeenCalledWith({
- mutation: updateBoardMutation,
- variables: {
- input: expect.objectContaining({
- id: currentBoard.id,
- }),
- },
+ expect(requestHandlers.updateBoardMutationHandler).toHaveBeenCalledWith({
+ input: expect.objectContaining({
+ id: currentBoard.id,
+ }),
});
await waitForPromises();
@@ -315,14 +336,19 @@ describe('BoardForm', () => {
});
it('sets error in state if GraphQL mutation fails', async () => {
- mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
- createComponent({ canAdminBoard: true, currentPage: formType.edit });
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.edit },
+ handlers: {
+ ...defaultHandlers,
+ updateBoardMutationHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
+ },
+ });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
- expect(mutate).toHaveBeenCalled();
+ expect(requestHandlers.updateBoardMutationHandler).toHaveBeenCalled();
await waitForPromises();
expect(setBoardMock).not.toHaveBeenCalled();
@@ -331,28 +357,20 @@ describe('BoardForm', () => {
describe('when Apollo boards FF is on', () => {
it('calls a correct GraphQL mutation and emits updateBoard event when updating a board', async () => {
- mutate = jest.fn().mockResolvedValue({
- data: {
- updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } },
- },
- });
setWindowLocation('https://test/boards/1');
- createComponent(
- { canAdminBoard: true, currentPage: formType.edit },
- { isApolloBoard: true },
- );
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.edit },
+ provide: { isApolloBoard: true },
+ });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
- expect(mutate).toHaveBeenCalledWith({
- mutation: updateBoardMutation,
- variables: {
- input: expect.objectContaining({
- id: currentBoard.id,
- }),
- },
+ expect(requestHandlers.updateBoardMutationHandler).toHaveBeenCalledWith({
+ input: expect.objectContaining({
+ id: currentBoard.id,
+ }),
});
await waitForPromises();
@@ -367,28 +385,30 @@ describe('BoardForm', () => {
describe('when deleting a board', () => {
it('passes correct primary action text and variant', () => {
- createComponent({ canAdminBoard: true, currentPage: formType.delete });
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.delete },
+ });
expect(findModalActionPrimary().text).toBe('Delete');
expect(findModalActionPrimary().attributes.variant).toBe('danger');
});
it('renders delete confirmation message', () => {
- createComponent({ canAdminBoard: true, currentPage: formType.delete });
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.delete },
+ });
expect(findDeleteConfirmation().exists()).toBe(true);
});
it('calls a correct GraphQL mutation and redirects to correct page after deleting board', async () => {
- mutate = jest.fn().mockResolvedValue({});
- createComponent({ canAdminBoard: true, currentPage: formType.delete });
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.delete },
+ });
findModal().vm.$emit('primary');
await waitForPromises();
- expect(mutate).toHaveBeenCalledWith({
- mutation: destroyBoardMutation,
- variables: {
- id: currentBoard.id,
- },
+ expect(requestHandlers.destroyBoardMutationHandler).toHaveBeenCalledWith({
+ id: currentBoard.id,
});
await waitForPromises();
@@ -396,19 +416,26 @@ describe('BoardForm', () => {
});
it('dispatches `setError` action when GraphQL mutation fails', async () => {
- mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
- createComponent({ canAdminBoard: true, currentPage: formType.delete });
- jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
+ createComponent({
+ props: { canAdminBoard: true, currentPage: formType.delete },
+ handlers: {
+ ...defaultHandlers,
+ destroyBoardMutationHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
+ },
+ });
+ jest.spyOn(store, 'dispatch').mockImplementation(() => {});
findModal().vm.$emit('primary');
await waitForPromises();
- expect(mutate).toHaveBeenCalled();
+ expect(requestHandlers.destroyBoardMutationHandler).toHaveBeenCalled();
await waitForPromises();
expect(visitUrl).not.toHaveBeenCalled();
- expect(wrapper.vm.setError).toHaveBeenCalled();
+ expect(store.dispatch).toHaveBeenCalledWith('setError', {
+ message: 'Failed to delete board. Please try again.',
+ });
});
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 471b033913b..870f9663290 100644
--- a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -1,5 +1,3 @@
-// TODO
-
import { GlAlert, GlBadge, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
diff --git a/spec/frontend/ci/pipeline_editor/index_spec.js b/spec/frontend/ci/pipeline_editor/index_spec.js
new file mode 100644
index 00000000000..530a441bde1
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/index_spec.js
@@ -0,0 +1,27 @@
+import { initPipelineEditor } from '~/ci/pipeline_editor';
+import * as optionsCE from '~/ci/pipeline_editor/options';
+
+describe('initPipelineEditor', () => {
+ let el;
+ const selector = 'SELECTOR';
+
+ beforeEach(() => {
+ jest.spyOn(optionsCE, 'createAppOptions').mockReturnValue({ option: 2 });
+
+ el = document.createElement('div');
+ el.id = selector;
+ document.body.appendChild(el);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(el);
+ });
+
+ it('returns null if there are no elements found', () => {
+ expect(initPipelineEditor()).toBeNull();
+ });
+
+ it('returns an object if there is an element found', () => {
+ expect(initPipelineEditor(`#${selector}`)).toMatchObject({});
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js
index dddf2dd7602..a3294cdc269 100644
--- a/spec/frontend/ci/pipeline_editor/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/mock_data.js
@@ -1,6 +1,42 @@
import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/ci/pipeline_editor/constants';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
+export const commonOptions = {
+ ciConfigPath: '/ci/config',
+ ciExamplesHelpPagePath: 'help/ci/examples',
+ ciHelpPagePath: 'help/ci/',
+ ciLintPath: 'ci/lint',
+ ciTroubleshootingPath: 'help/troubleshoot',
+ defaultBranch: 'main',
+ emptyStateIllustrationPath: 'illustrations/svg',
+ helpPaths: '/ads',
+ includesHelpPagePath: 'help/includes',
+ needsHelpPagePath: 'help/ci/needs',
+ newMergeRequestPath: 'merge_request/new',
+ pipelinePagePath: '/pipelines/1',
+ projectFullPath: 'root/my-project',
+ projectNamespace: 'root',
+ simulatePipelineHelpPagePath: 'help/ci/simulate',
+ totalBranches: '10',
+ usesExternalConfig: 'false',
+ validateTabIllustrationPath: 'illustrations/tab',
+ ymlHelpPagePath: 'help/ci/yml',
+ aiChatAvailable: 'true',
+};
+
+export const editorDatasetOptions = {
+ initialBranchName: 'production',
+ pipelineEtag: 'pipelineEtag',
+ ...commonOptions,
+};
+
+export const expectedInjectValues = {
+ ...commonOptions,
+ aiChatAvailable: true,
+ usesExternalConfig: false,
+ totalBranches: 10,
+};
+
export const mockProjectNamespace = 'user1';
export const mockProjectPath = 'project1';
export const mockProjectFullPath = `${mockProjectNamespace}/${mockProjectPath}`;
diff --git a/spec/frontend/ci/pipeline_editor/options_spec.js b/spec/frontend/ci/pipeline_editor/options_spec.js
new file mode 100644
index 00000000000..b8f4105c923
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/options_spec.js
@@ -0,0 +1,27 @@
+import { createAppOptions } from '~/ci/pipeline_editor/options';
+import { editorDatasetOptions, expectedInjectValues } from './mock_data';
+
+describe('createAppOptions', () => {
+ let el;
+
+ const createElement = () => {
+ el = document.createElement('div');
+
+ document.body.appendChild(el);
+ Object.entries(editorDatasetOptions).forEach(([k, v]) => {
+ el.dataset[k] = v;
+ });
+ };
+
+ afterEach(() => {
+ el = null;
+ });
+
+ it("extracts the properties from the element's dataset", () => {
+ createElement();
+ const options = createAppOptions(el);
+ Object.entries(expectedInjectValues).forEach(([key, value]) => {
+ expect(options.provide).toMatchObject({ [key]: value });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_details_spec.js b/spec/frontend/ci/runner/components/runner_details_spec.js
index 47e09a1a7fd..cc91340655b 100644
--- a/spec/frontend/ci/runner/components/runner_details_spec.js
+++ b/spec/frontend/ci/runner/components/runner_details_spec.js
@@ -1,5 +1,5 @@
import { GlSprintf, GlIntersperse } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { useFakeDate } from 'helpers/fake_date';
@@ -11,6 +11,7 @@ import RunnerDetail from '~/ci/runner/components/runner_detail.vue';
import RunnerGroups from '~/ci/runner/components/runner_groups.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
import RunnerTag from '~/ci/runner/components/runner_tag.vue';
+import RunnerManagersDetail from '~/ci/runner/components/runner_managers_detail.vue';
import { runnerData, runnerWithGroupData } from '../mock_data';
@@ -25,6 +26,8 @@ describe('RunnerDetails', () => {
useFakeDate(mockNow);
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
+ const findRunnerManagersDetail = () => wrapper.findComponent(RunnerManagersDetail);
+
const findDdContent = (label) => findDd(label, wrapper).text().replace(/\s+/g, ' ');
const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => {
@@ -63,7 +66,7 @@ describe('RunnerDetails', () => {
${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'}
${'Token expiry'} | ${{ tokenExpiresAt: mockOneHourAgo }} | ${'1 hour ago'}
${'Token expiry'} | ${{ tokenExpiresAt: null }} | ${'Never expires'}
- ${'Runners'} | ${{ managers: { count: 2 } }} | ${'2'}
+ ${'Runners'} | ${{ managers: { count: 2 } }} | ${`2 ${__('Show details')}`}
`('"$field" field', ({ field, runner, expectedValue }) => {
beforeEach(() => {
createComponent({
@@ -77,12 +80,13 @@ describe('RunnerDetails', () => {
GlIntersperse,
GlSprintf,
TimeAgo,
+ RunnerManagersDetail,
},
});
});
it(`displays expected value "${expectedValue}"`, () => {
- expect(findDd(field, wrapper).text()).toBe(expectedValue);
+ expect(findDdContent(field)).toBe(expectedValue);
});
});
@@ -113,24 +117,14 @@ describe('RunnerDetails', () => {
});
describe('"Runners" field', () => {
- it.each`
- count | expected
- ${0} | ${'0'}
- ${1} | ${'1'}
- ${1000} | ${'1,000'}
- `('displays runner managers count of $count', ({ count, expected }) => {
+ it('displays runner managers count of $count', () => {
createComponent({
props: {
- runner: {
- ...mockRunner,
- managers: {
- count,
- },
- },
+ runner: mockRunner,
},
});
- expect(findDdContent(s__('Runners|Runners'))).toBe(expected);
+ expect(findRunnerManagersDetail().props('runner')).toEqual(mockRunner);
});
});
diff --git a/spec/frontend/ci/runner/components/runner_details_tabs_spec.js b/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
index a59c5a21377..689d0575726 100644
--- a/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
@@ -16,9 +16,17 @@ import { runnerData } from '../mock_data';
// Vue Test Utils `stubs` option does not stub components mounted
// in <router-view>. Use mocking instead:
jest.mock('~/ci/runner/components/runner_jobs.vue', () => {
- const ActualRunnerJobs = jest.requireActual('~/ci/runner/components/runner_jobs.vue').default;
+ const { props } = jest.requireActual('~/ci/runner/components/runner_jobs.vue').default;
return {
- props: ActualRunnerJobs.props,
+ props,
+ render() {},
+ };
+});
+
+jest.mock('~/ci/runner/components/runner_managers_detail.vue', () => {
+ const { props } = jest.requireActual('~/ci/runner/components/runner_managers_detail.vue').default;
+ return {
+ props,
render() {},
};
});
diff --git a/spec/frontend/ci/runner/components/runner_managers_detail_spec.js b/spec/frontend/ci/runner/components/runner_managers_detail_spec.js
new file mode 100644
index 00000000000..b2551e67396
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_managers_detail_spec.js
@@ -0,0 +1,188 @@
+import { GlCollapse, GlSkeletonLoader, GlTableLite } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { __ } from '~/locale';
+import {
+ shallowMountExtended,
+ mountExtended,
+ extendedWrapper,
+} from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import RunnerManagersDetail from '~/ci/runner/components/runner_managers_detail.vue';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+
+import runnerManagersQuery from '~/ci/runner/graphql/show/runner_managers.query.graphql';
+import { runnerData, runnerManagersData } from '../mock_data';
+
+jest.mock('~/alert');
+jest.mock('~/ci/runner/sentry_utils');
+
+const mockRunner = runnerData.data.runner;
+const mockRunnerManagers = runnerManagersData.data.runner.managers.nodes;
+
+Vue.use(VueApollo);
+
+describe('RunnerJobs', () => {
+ let wrapper;
+ let mockRunnerManagersHandler;
+
+ const findShowDetails = () => wrapper.findByText(__('Show details'));
+ const findHideDetails = () => wrapper.findByText(__('Hide details'));
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+
+ const findCollapse = () => wrapper.findComponent(GlCollapse);
+ const findRows = () => wrapper.findAll('tbody tr');
+ const findCell = ({ field, i }) => extendedWrapper(findRows().at(i)).findByTestId(`td-${field}`);
+
+ const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(RunnerManagersDetail, {
+ apolloProvider: createMockApollo([[runnerManagersQuery, mockRunnerManagersHandler]]),
+ propsData: {
+ runner: mockRunner,
+ ...props,
+ },
+ stubs: {
+ GlTableLite,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockRunnerManagersHandler = jest.fn();
+ });
+
+ afterEach(() => {
+ mockRunnerManagersHandler.mockReset();
+ });
+
+ describe('Runners count', () => {
+ it.each`
+ count | expected
+ ${0} | ${'0'}
+ ${1} | ${'1'}
+ ${1000} | ${'1,000'}
+ `('displays runner managers count of $count', ({ count, expected }) => {
+ createComponent({
+ props: {
+ runner: {
+ ...mockRunner,
+ managers: {
+ count,
+ },
+ },
+ },
+ });
+
+ expect(wrapper.text()).toContain(expected);
+ });
+ });
+
+ describe('Expand and collapse', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows link to expand', () => {
+ expect(findShowDetails().exists()).toBe(true);
+ expect(findHideDetails().exists()).toBe(false);
+ });
+
+ it('is collapsed', () => {
+ expect(findCollapse().attributes('visible')).toBeUndefined();
+ });
+
+ describe('when expanded', () => {
+ beforeEach(() => {
+ findShowDetails().vm.$emit('click');
+ });
+
+ it('shows link to collapse', () => {
+ expect(findShowDetails().exists()).toBe(false);
+ expect(findHideDetails().exists()).toBe(true);
+ });
+
+ it('shows loading state', () => {
+ expect(findCollapse().attributes('visible')).toBe('true');
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('fetches data', () => {
+ expect(mockRunnerManagersHandler).toHaveBeenCalledTimes(1);
+ expect(mockRunnerManagersHandler).toHaveBeenCalledWith({
+ runnerId: mockRunner.id,
+ });
+ });
+ });
+ });
+
+ describe('Prefetches data upon user interation', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('does not fetch initially', () => {
+ expect(mockRunnerManagersHandler).not.toHaveBeenCalled();
+ });
+
+ describe.each(['focus', 'mouseover'])('fetches data after %s', (event) => {
+ beforeEach(() => {
+ findShowDetails().vm.$emit(event);
+ });
+
+ it('fetches data', () => {
+ expect(mockRunnerManagersHandler).toHaveBeenCalledTimes(1);
+ expect(mockRunnerManagersHandler).toHaveBeenCalledWith({
+ runnerId: mockRunner.id,
+ });
+ });
+
+ it('fetches data only once', async () => {
+ findShowDetails().vm.$emit(event);
+ await waitForPromises();
+
+ expect(mockRunnerManagersHandler).toHaveBeenCalledTimes(1);
+ expect(mockRunnerManagersHandler).toHaveBeenCalledWith({
+ runnerId: mockRunner.id,
+ });
+ });
+ });
+ });
+
+ describe('Shows data', () => {
+ beforeEach(async () => {
+ mockRunnerManagersHandler.mockResolvedValue(runnerManagersData);
+
+ createComponent({ mountFn: mountExtended });
+
+ await findShowDetails().trigger('click');
+ });
+
+ it('shows rows', () => {
+ expect(findCollapse().attributes('visible')).toBe('true');
+ expect(findRows()).toHaveLength(mockRunnerManagers.length);
+ });
+
+ it('shows system id', () => {
+ expect(findCell({ field: 'systemId', i: 0 }).text()).toBe(mockRunnerManagers[0].systemId);
+ expect(findCell({ field: 'systemId', i: 1 }).text()).toBe(mockRunnerManagers[1].systemId);
+ });
+
+ it('shows contacted at', () => {
+ expect(findCell({ field: 'contactedAt', i: 0 }).findComponent(TimeAgo).props('time')).toBe(
+ mockRunnerManagers[0].contactedAt,
+ );
+ expect(findCell({ field: 'contactedAt', i: 1 }).findComponent(TimeAgo).props('time')).toBe(
+ mockRunnerManagers[1].contactedAt,
+ );
+ });
+
+ it('collapses when clicked', async () => {
+ await findHideDetails().trigger('click');
+
+ expect(findCollapse().attributes('visible')).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_managers_table_spec.js b/spec/frontend/ci/runner/components/runner_managers_table_spec.js
new file mode 100644
index 00000000000..a0ebf3b2578
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_managers_table_spec.js
@@ -0,0 +1,123 @@
+import { GlTableLite } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { s__ } from '~/locale';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
+
+import RunnerManagersTable from '~/ci/runner/components/runner_managers_table.vue';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+
+import { runnerManagersData } from '../mock_data';
+
+jest.mock('~/alert');
+jest.mock('~/ci/runner/sentry_utils');
+
+const [runnerManager1, runnerManager2] = runnerManagersData.data.runner.managers.nodes;
+
+Vue.use(VueApollo);
+
+describe('RunnerJobs', () => {
+ let wrapper;
+
+ const findHeaders = () => wrapper.findAll('thead th');
+ const findRows = () => wrapper.findAll('tbody tr');
+ const findCell = ({ field, i }) => extendedWrapper(findRows().at(i)).findByTestId(`td-${field}`);
+ const findCellText = (opts) => findCell(opts).text().replace(/\s+/g, ' ');
+
+ const createComponent = ({ item } = {}) => {
+ wrapper = mountExtended(RunnerManagersTable, {
+ propsData: {
+ items: [{ ...runnerManager1, ...item }, runnerManager2],
+ },
+ stubs: {
+ GlTableLite,
+ },
+ });
+ };
+
+ it('shows headers', () => {
+ createComponent();
+ expect(findHeaders().wrappers.map((w) => w.text())).toEqual([
+ expect.stringContaining(s__('Runners|System ID')),
+ s__('Runners|Version'),
+ s__('Runners|IP Address'),
+ s__('Runners|Executor'),
+ s__('Runners|Arch/Platform'),
+ s__('Runners|Last contact'),
+ ]);
+ });
+
+ it('shows rows', () => {
+ createComponent();
+ expect(findRows()).toHaveLength(2);
+ });
+
+ it('shows system id', () => {
+ createComponent();
+ expect(findCellText({ field: 'systemId', i: 0 })).toBe(runnerManager1.systemId);
+ expect(findCellText({ field: 'systemId', i: 1 })).toBe(runnerManager2.systemId);
+ });
+
+ it('shows version', () => {
+ createComponent({
+ item: { version: '1.0' },
+ });
+
+ expect(findCellText({ field: 'version', i: 0 })).toBe('1.0');
+ });
+
+ it('shows version with revision', () => {
+ createComponent({
+ item: { version: '1.0', revision: '123456' },
+ });
+
+ expect(findCellText({ field: 'version', i: 0 })).toBe('1.0 (123456)');
+ });
+
+ it('shows ip address', () => {
+ createComponent({
+ item: { ipAddress: '127.0.0.1' },
+ });
+
+ expect(findCellText({ field: 'ipAddress', i: 0 })).toBe('127.0.0.1');
+ });
+
+ it('shows executor', () => {
+ createComponent({
+ item: { executorName: 'shell' },
+ });
+
+ expect(findCellText({ field: 'executorName', i: 0 })).toBe('shell');
+ });
+
+ it('shows architecture', () => {
+ createComponent({
+ item: { architectureName: 'x64' },
+ });
+
+ expect(findCellText({ field: 'architecturePlatform', i: 0 })).toBe('x64');
+ });
+
+ it('shows platform', () => {
+ createComponent({
+ item: { platformName: 'darwin' },
+ });
+
+ expect(findCellText({ field: 'architecturePlatform', i: 0 })).toBe('darwin');
+ });
+
+ it('shows architecture and platform', () => {
+ createComponent({
+ item: { architectureName: 'x64', platformName: 'darwin' },
+ });
+
+ expect(findCellText({ field: 'architecturePlatform', i: 0 })).toBe('x64/darwin');
+ });
+
+ it('shows contacted at', () => {
+ createComponent();
+ expect(findCell({ field: 'contactedAt', i: 0 }).findComponent(TimeAgo).props('time')).toBe(
+ runnerManager1.contactedAt,
+ );
+ });
+});
diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index 223a156795c..d72f93ad574 100644
--- a/spec/frontend/ci/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -18,6 +18,7 @@ import runnerData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphq
import runnerWithGroupData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.with_group.json';
import runnerProjectsData from 'test_fixtures/graphql/ci/runner/show/runner_projects.query.graphql.json';
import runnerJobsData from 'test_fixtures/graphql/ci/runner/show/runner_jobs.query.graphql.json';
+import runnerManagersData from 'test_fixtures/graphql/ci/runner/show/runner_managers.query.graphql.json';
// Edit runner queries
import runnerFormData from 'test_fixtures/graphql/ci/runner/edit/runner_form.query.graphql.json';
@@ -336,6 +337,7 @@ export {
runnerWithGroupData,
runnerProjectsData,
runnerJobsData,
+ runnerManagersData,
runnerFormData,
runnerCreateResult,
runnerForRegistration,
diff --git a/spec/frontend/drawio/drawio_editor_spec.js b/spec/frontend/drawio/drawio_editor_spec.js
index d7d75922e1e..4d93908b757 100644
--- a/spec/frontend/drawio/drawio_editor_spec.js
+++ b/spec/frontend/drawio/drawio_editor_spec.js
@@ -1,6 +1,5 @@
import { launchDrawioEditor } from '~/drawio/drawio_editor';
import {
- DRAWIO_EDITOR_URL,
DRAWIO_FRAME_ID,
DIAGRAM_BACKGROUND_COLOR,
DRAWIO_IFRAME_TIMEOUT,
@@ -8,6 +7,10 @@ import {
} from '~/drawio/constants';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
+const DRAWIO_EDITOR_URL =
+ 'https://embed.diagrams.net/?ui=sketch&noSaveBtn=1&saveAndExit=1&keepmodified=1&spin=1&embed=1&libraries=1&configure=1&proto=json&toSvg=1';
+const DRAWIO_EDITOR_ORIGIN = new URL(DRAWIO_EDITOR_URL).origin;
+
jest.mock('~/alert');
jest.useFakeTimers();
@@ -59,6 +62,7 @@ describe('drawio/drawio_editor', () => {
updateDiagram: jest.fn(),
};
drawioIFrameReceivedMessages = [];
+ gon.diagramsnet_url = DRAWIO_EDITOR_ORIGIN;
});
afterEach(() => {
@@ -356,7 +360,11 @@ describe('drawio/drawio_editor', () => {
const TEST_FILENAME = 'diagram.drawio.svg';
beforeEach(() => {
- launchDrawioEditor({ editorFacade, filename: TEST_FILENAME });
+ launchDrawioEditor({
+ editorFacade,
+ filename: TEST_FILENAME,
+ drawioUrl: DRAWIO_EDITOR_ORIGIN,
+ });
});
it('displays loading spinner in the draw.io editor', async () => {
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index 099df607487..a73a0dcbdd1 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -14,6 +14,9 @@ RSpec.describe 'Runner (JavaScript fixtures)', feature_category: :runner_fleet d
let_it_be(:project_2) { create(:project, :repository, :public) }
let_it_be(:runner) { create(:ci_runner, :instance, description: 'My Runner', creator: admin, version: '1.0.0') }
+ let_it_be(:runner_manager_1) { create(:ci_runner_machine, runner: runner, contacted_at: Time.current) }
+ let_it_be(:runner_manager_2) { create(:ci_runner_machine, runner: runner, contacted_at: Time.current) }
+
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], version: '2.0.0') }
let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], version: '2.0.0') }
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], version: '2.0.0') }
@@ -137,6 +140,22 @@ RSpec.describe 'Runner (JavaScript fixtures)', feature_category: :runner_fleet d
end
end
+ describe 'runner_managers.query.graphql', type: :request do
+ runner_managers_query = 'show/runner_managers.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{runner_managers_query}")
+ end
+
+ it "#{fixtures_path}#{runner_managers_query}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ runner_id: runner.to_global_id.to_s
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
describe 'runner_form.query.graphql', type: :request do
runner_jobs_query = 'edit/runner_form.query.graphql'
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index ddaa3df71e8..1a3eb86a00e 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -14,6 +14,7 @@ import {
WIKI_FORMAT_LABEL,
WIKI_FORMAT_UPDATED_ACTION,
} from '~/pages/shared/wikis/constants';
+import { DRAWIO_ORIGIN } from 'spec/test_constants';
jest.mock('~/emoji');
@@ -69,12 +70,12 @@ describe('WikiForm', () => {
AsciiDoc: 'asciidoc',
Org: 'org',
};
-
function createWrapper({
mountFn = shallowMount,
persisted = false,
pageInfo,
glFeatures = { wikiSwitchBetweenContentEditorRawMarkdown: false },
+ provide = { drawioUrl: null },
} = {}) {
wrapper = extendedWrapper(
mountFn(WikiForm, {
@@ -85,6 +86,7 @@ describe('WikiForm', () => {
...(persisted ? pageInfoPersisted : pageInfoNew),
...pageInfo,
},
+ ...provide,
},
stubs: {
GlAlert,
@@ -334,4 +336,20 @@ describe('WikiForm', () => {
});
});
});
+
+ describe('when drawioURL is provided', () => {
+ it('enables drawio editor in the Markdown Editor', () => {
+ createWrapper({ provide: { drawioUrl: DRAWIO_ORIGIN } });
+
+ expect(findMarkdownEditor().props().drawioEnabled).toBe(true);
+ });
+ });
+
+ describe('when drawioURL is empty', () => {
+ it('disables drawio editor in the Markdown Editor', () => {
+ createWrapper();
+
+ expect(findMarkdownEditor().props().drawioEnabled).toBe(false);
+ });
+ });
});
diff --git a/spec/graphql/types/ci/catalog/resource_type_spec.rb b/spec/graphql/types/ci/catalog/resource_type_spec.rb
index 773be2e5b56..cdc863e0d90 100644
--- a/spec/graphql/types/ci/catalog/resource_type_spec.rb
+++ b/spec/graphql/types/ci/catalog/resource_type_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe Types::Ci::Catalog::ResourceType, feature_category: :pipeline_com
icon
web_path
versions
+ star_count
+ forks_count
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb
index b45882d9888..f411f533b25 100644
--- a/spec/helpers/ci/pipeline_editor_helper_spec.rb
+++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe Ci::PipelineEditorHelper do
context 'with a project with commits' do
it 'returns pipeline editor data' do
- expect(pipeline_editor_data).to eq(default_helper_data.merge({
+ expect(pipeline_editor_data).to include(default_helper_data.merge({
"pipeline_etag" => graphql_etag_pipeline_sha_path(project.commit.sha),
"total-branches" => project.repository.branches.length
}))
@@ -84,7 +84,7 @@ RSpec.describe Ci::PipelineEditorHelper do
let(:project) { create(:project, :empty_repo) }
it 'returns pipeline editor data' do
- expect(pipeline_editor_data).to eq(default_helper_data.merge({
+ expect(pipeline_editor_data).to include(default_helper_data.merge({
"pipeline_etag" => '',
"total-branches" => 0
}))
diff --git a/spec/helpers/registrations_helper_spec.rb b/spec/helpers/registrations_helper_spec.rb
index a5ad80abd11..85cedd4aace 100644
--- a/spec/helpers/registrations_helper_spec.rb
+++ b/spec/helpers/registrations_helper_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe RegistrationsHelper, feature_category: :user_management do
it 'adds intent to register' do
allow(helper).to receive(:glm_tracking_params).and_return({})
- expect(helper.register_omniauth_params({})).to eq({ intent: :register })
+ expect(helper.register_omniauth_params({})).to eq({})
end
end
end
diff --git a/spec/lib/gitlab/database/load_balancing/host_spec.rb b/spec/lib/gitlab/database/load_balancing/host_spec.rb
index b040c7a76bd..caae06ce43a 100644
--- a/spec/lib/gitlab/database/load_balancing/host_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/host_spec.rb
@@ -195,6 +195,40 @@ RSpec.describe Gitlab::Database::LoadBalancing::Host do
expect(host).to be_online
end
+
+ it 'clears the cache for latest_lsn_query' do
+ allow(host).to receive(:replica_is_up_to_date?).and_return(true)
+
+ expect(host)
+ .to receive(:query_and_release)
+ .with(described_class::CAN_TRACK_LOGICAL_LSN_QUERY)
+ .twice
+ .and_return({ 'allowed' => 't' }, { 'allowed' => 'f' })
+
+ # Should receive LATEST_LSN_WITH_LOGICAL_QUERY twice even though we only
+ # return 't' once above
+ expect(host)
+ .to receive(:query_and_release)
+ .with(a_string_including(described_class::LATEST_LSN_WITH_LOGICAL_QUERY))
+ .twice
+ .and_call_original
+
+ host.replication_lag_size
+ host.replication_lag_size
+
+ # Clear the cache for latest_lsn_query
+ host.refresh_status
+
+ # Should recieve LATEST_LSN_WITHOUT_LOGICAL_QUERY since we received 'f'
+ # after clearing the cache
+ expect(host)
+ .to receive(:query_and_release)
+ .with(a_string_including(described_class::LATEST_LSN_WITHOUT_LOGICAL_QUERY))
+ .once
+ .and_call_original
+
+ host.replication_lag_size
+ end
end
describe '#check_replica_status?' do
@@ -289,6 +323,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::Host do
expect(host)
.to receive(:query_and_release)
+ .with(described_class::CAN_TRACK_LOGICAL_LSN_QUERY)
+ .and_call_original
+
+ expect(host)
+ .to receive(:query_and_release)
.and_return({ 'diff' => diff })
expect(host.data_is_recent_enough?).to eq(false)
@@ -325,6 +364,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::Host do
it 'returns nil when the database query returned no rows' do
expect(host)
.to receive(:query_and_release)
+ .with(described_class::CAN_TRACK_LOGICAL_LSN_QUERY)
+ .and_call_original
+
+ expect(host)
+ .to receive(:query_and_release)
.and_return({})
expect(host.replication_lag_size).to be_nil
@@ -339,6 +383,54 @@ RSpec.describe Gitlab::Database::LoadBalancing::Host do
expect(host.replication_lag_size).to be_nil
end
+
+ context 'when can_track_logical_lsn? is false' do
+ before do
+ allow(host).to receive(:can_track_logical_lsn?).and_return(false)
+ end
+
+ it 'uses LATEST_LSN_WITHOUT_LOGICAL_QUERY' do
+ expect(host)
+ .to receive(:query_and_release)
+ .with(a_string_including(described_class::LATEST_LSN_WITHOUT_LOGICAL_QUERY))
+ .and_call_original
+
+ expect(host.replication_lag_size('0/00000000')).to be_an_instance_of(Integer)
+ end
+ end
+
+ context 'when can_track_logical_lsn? is true' do
+ before do
+ allow(host).to receive(:can_track_logical_lsn?).and_return(true)
+ end
+
+ it 'uses LATEST_LSN_WITH_LOGICAL_QUERY' do
+ expect(host)
+ .to receive(:query_and_release)
+ .with(a_string_including(described_class::LATEST_LSN_WITH_LOGICAL_QUERY))
+ .and_call_original
+
+ expect(host.replication_lag_size('0/00000000')).to be_an_instance_of(Integer)
+ end
+ end
+
+ context 'when CAN_TRACK_LOGICAL_LSN_QUERY raises connection errors' do
+ before do
+ expect(host)
+ .to receive(:query_and_release)
+ .with(described_class::CAN_TRACK_LOGICAL_LSN_QUERY)
+ .and_raise(ActiveRecord::ConnectionNotEstablished)
+ end
+
+ it 'uses LATEST_LSN_WITHOUT_LOGICAL_QUERY' do
+ expect(host)
+ .to receive(:query_and_release)
+ .with(a_string_including(described_class::LATEST_LSN_WITHOUT_LOGICAL_QUERY))
+ .and_call_original
+
+ expect(host.replication_lag_size('0/00000000')).to be_an_instance_of(Integer)
+ end
+ end
end
describe '#primary_write_location' do
@@ -357,28 +449,41 @@ RSpec.describe Gitlab::Database::LoadBalancing::Host do
it 'returns true when a host has caught up' do
allow(host).to receive(:connection).and_return(connection)
- expect(connection).to receive(:select_all).and_return([{ 'result' => 't' }])
- expect(host.caught_up?('foo')).to eq(true)
- end
+ expect(connection)
+ .to receive(:select_all)
+ .with(described_class::CAN_TRACK_LOGICAL_LSN_QUERY)
+ .and_return([{ 'has_table_privilege' => 't' }])
- it 'returns true when a host has caught up' do
- allow(host).to receive(:connection).and_return(connection)
- expect(connection).to receive(:select_all).and_return([{ 'result' => true }])
+ expect(connection)
+ .to receive(:select_all)
+ .and_return([{ 'diff' => -1 }])
expect(host.caught_up?('foo')).to eq(true)
end
- it 'returns false when a host has not caught up' do
+ it 'returns false when diff query returns nothing' do
allow(host).to receive(:connection).and_return(connection)
- expect(connection).to receive(:select_all).and_return([{ 'result' => 'f' }])
+
+ expect(connection)
+ .to receive(:select_all)
+ .with(described_class::CAN_TRACK_LOGICAL_LSN_QUERY)
+ .and_return([{ 'has_table_privilege' => 't' }])
+
+ expect(connection).to receive(:select_all).and_return([])
expect(host.caught_up?('foo')).to eq(false)
end
it 'returns false when a host has not caught up' do
allow(host).to receive(:connection).and_return(connection)
- expect(connection).to receive(:select_all).and_return([{ 'result' => false }])
+
+ expect(connection)
+ .to receive(:select_all)
+ .with(described_class::CAN_TRACK_LOGICAL_LSN_QUERY)
+ .and_return([{ 'has_table_privilege' => 't' }])
+
+ expect(connection).to receive(:select_all).and_return([{ 'diff' => 123 }])
expect(host.caught_up?('foo')).to eq(false)
end
diff --git a/spec/lib/gitlab/database/schema_validation/schema_inconsistency_spec.rb b/spec/lib/gitlab/database/schema_validation/schema_inconsistency_spec.rb
index 653ca0a2b2b..fbaf8474f22 100644
--- a/spec/lib/gitlab/database/schema_validation/schema_inconsistency_spec.rb
+++ b/spec/lib/gitlab/database/schema_validation/schema_inconsistency_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe Gitlab::Database::SchemaValidation::SchemaInconsistency, type: :m
it { is_expected.to validate_presence_of(:object_name) }
it { is_expected.to validate_presence_of(:valitador_name) }
it { is_expected.to validate_presence_of(:table_name) }
+ it { is_expected.to validate_presence_of(:diff) }
end
describe 'scopes' do
diff --git a/spec/lib/gitlab/database/schema_validation/track_inconsistency_spec.rb b/spec/lib/gitlab/database/schema_validation/track_inconsistency_spec.rb
index bb83dfa796f..8348ad9a1bc 100644
--- a/spec/lib/gitlab/database/schema_validation/track_inconsistency_spec.rb
+++ b/spec/lib/gitlab/database/schema_validation/track_inconsistency_spec.rb
@@ -63,15 +63,49 @@ RSpec.describe Gitlab::Database::SchemaValidation::TrackInconsistency, feature_c
end
context 'when the schema inconsistency already exists' do
+ let(:diff) do
+ "-#{structure_sql_statement}\n" \
+ "+#{database_statement}\n"
+ end
+
let!(:schema_inconsistency) do
create(:schema_inconsistency, object_name: 'index_name', table_name: 'achievements',
- valitador_name: 'different_definition_indexes')
+ valitador_name: 'different_definition_indexes', diff: diff)
end
before do
project.add_developer(user)
end
+ context 'when the issue has the last schema inconsistency' do
+ it 'does not add a note' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ expect { execute }.not_to change { schema_inconsistency.issue.notes.count }
+ end
+ end
+
+ context 'when the issue is outdated' do
+ let!(:schema_inconsistency) do
+ create(:schema_inconsistency, object_name: 'index_name', table_name: 'achievements',
+ valitador_name: 'different_definition_indexes', diff: 'old_diff')
+ end
+
+ it 'adds a note' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ expect { execute }.to change { schema_inconsistency.issue.notes.count }.from(0).to(1)
+ end
+
+ it 'updates the diff' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ execute
+
+ expect(schema_inconsistency.reload.diff).to eq(diff)
+ end
+ end
+
context 'when the GitLab issue is open' do
it 'does not create a new schema inconsistency record' do
allow(Gitlab).to receive(:com?).and_return(true)
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 37f4bbf0f63..9ecb0c6f75b 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -1127,6 +1127,26 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
end
end
+ describe 'diagrams.net settings' do
+ context 'when diagrams.net is enabled' do
+ before do
+ setting.diagramsnet_enabled = true
+ end
+
+ it { is_expected.not_to allow_value(nil).for(:diagramsnet_url) }
+ it { is_expected.to allow_value("https://embed.diagrams.net").for(:diagramsnet_url) }
+ it { is_expected.not_to allow_value('not a URL').for(:diagramsnet_url) }
+ end
+
+ context 'when diagrams.net is not enabled' do
+ before do
+ setting.diagramsnet_enabled = false
+ end
+
+ it { is_expected.to allow_value(nil).for(:diagramsnet_url) }
+ end
+ end
+
context 'throttle_* settings' do
where(:throttle_setting) do
%i[
diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb
index cccd164d4a8..4c1ade5c308 100644
--- a/spec/models/ci/catalog/resource_spec.rb
+++ b/spec/models/ci/catalog/resource_spec.rb
@@ -19,6 +19,8 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
it { is_expected.to delegate_method(:avatar_path).to(:project) }
it { is_expected.to delegate_method(:description).to(:project) }
it { is_expected.to delegate_method(:name).to(:project) }
+ it { is_expected.to delegate_method(:star_count).to(:project) }
+ it { is_expected.to delegate_method(:forks_count).to(:project) }
describe '.for_projects' do
it 'returns catalog resources for the given project IDs' do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 26c9399a8e5..79e96d7ea3e 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -19,6 +19,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['password_authentication_enabled']).to be_truthy
expect(json_response['plantuml_enabled']).to be_falsey
expect(json_response['plantuml_url']).to be_nil
+ expect(json_response['diagramsnet_enabled']).to be_truthy
+ expect(json_response['diagramsnet_url']).to eq('https://embed.diagrams.net')
expect(json_response['default_ci_config_path']).to be_nil
expect(json_response['sourcegraph_enabled']).to be_falsey
expect(json_response['sourcegraph_url']).to be_nil
@@ -125,6 +127,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
repository_storages_weighted: { 'custom' => 100 },
plantuml_enabled: true,
plantuml_url: 'http://plantuml.example.com',
+ diagramsnet_enabled: false,
+ diagramsnet_url: nil,
sourcegraph_enabled: true,
sourcegraph_url: 'https://sourcegraph.com',
sourcegraph_public_only: false,
@@ -203,6 +207,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['repository_storages_weighted']).to eq({ 'custom' => 100 })
expect(json_response['plantuml_enabled']).to be_truthy
expect(json_response['plantuml_url']).to eq('http://plantuml.example.com')
+ expect(json_response['diagramsnet_enabled']).to be_falsey
+ expect(json_response['diagramsnet_url']).to be_nil
expect(json_response['sourcegraph_enabled']).to be_truthy
expect(json_response['sourcegraph_url']).to eq('https://sourcegraph.com')
expect(json_response['sourcegraph_public_only']).to eq(false)
@@ -553,6 +559,15 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
end
end
+ context "missing diagramsnet_url value when diagramsnet_enabled is true" do
+ it "returns a blank parameter error message" do
+ put api("/application/settings", admin), params: { diagramsnet_enabled: true }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('diagramsnet_url is missing')
+ end
+ end
+
context 'asset_proxy settings' do
it 'updates application settings' do
put api('/application/settings', admin),
diff --git a/spec/requests/projects/wikis_controller_spec.rb b/spec/requests/projects/wikis_controller_spec.rb
index 3c434b36b21..9f69faf499e 100644
--- a/spec/requests/projects/wikis_controller_spec.rb
+++ b/spec/requests/projects/wikis_controller_spec.rb
@@ -6,6 +6,8 @@ RSpec.describe Projects::WikisController, feature_category: :wiki do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
+ let_it_be(:diagramsnet_is_enabled) { false }
+ let_it_be(:diagramsnet_url) { 'https://url.diagrams.net' }
let_it_be(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let_it_be(:project_wiki) { create(:project_wiki, project: project, user: user) }
let_it_be(:wiki_page) do
@@ -18,6 +20,12 @@ RSpec.describe Projects::WikisController, feature_category: :wiki do
before do
sign_in(user)
+ allow(Gitlab::CurrentSettings)
+ .to receive(:diagramsnet_enabled?)
+ .and_return(diagramsnet_is_enabled)
+ allow(Gitlab::CurrentSettings)
+ .to receive(:diagramsnet_url)
+ .and_return(diagramsnet_url)
allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:content_security_policy_nonce).and_return(csp_nonce)
@@ -25,12 +33,26 @@ RSpec.describe Projects::WikisController, feature_category: :wiki do
end
shared_examples 'embed.diagrams.net frame-src directive' do
- it 'adds drawio frame-src directive to the Content Security Policy header' do
- frame_src = response.headers['Content-Security-Policy'].split(';')
- .map(&:strip)
- .find { |entry| entry.starts_with?('frame-src') }
+ context 'when diagrams.net disabled' do
+ it 'drawio frame-src directive to the Content Security Policy header' do
+ frame_src = response.headers['Content-Security-Policy'].split(';')
+ .map(&:strip)
+ .find { |entry| entry.starts_with?('frame-src') }
- expect(frame_src).to include('https://embed.diagrams.net')
+ expect(frame_src).not_to include(diagramsnet_url)
+ end
+ end
+
+ context 'when diagrams.net enabled' do
+ let(:diagramsnet_is_enabled) { true }
+
+ it 'drawio frame-src directive to the Content Security Policy header' do
+ frame_src = response.headers['Content-Security-Policy'].split(';')
+ .map(&:strip)
+ .find { |entry| entry.starts_with?('frame-src') }
+
+ expect(frame_src).to include(diagramsnet_url)
+ end
end
end
diff --git a/spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb b/spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb
index a8aed0c1f0b..106260e644f 100644
--- a/spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb
@@ -10,6 +10,16 @@ RSpec.shared_examples Integrations::Actions do
)
end
+ shared_examples 'unknown integration' do
+ let(:routing_params) do
+ super().merge(id: 'unknown_integration')
+ end
+
+ it 'returns 404 Not Found' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
describe 'GET #edit' do
before do
get :edit, params: routing_params
@@ -19,6 +29,8 @@ RSpec.shared_examples Integrations::Actions do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:integration)).to eq(integration)
end
+
+ it_behaves_like 'unknown integration'
end
describe 'PUT #update' do
@@ -55,5 +67,15 @@ RSpec.shared_examples Integrations::Actions do
expect(integration.reload).to have_attributes(params.merge(api_key: 'secret'))
end
end
+
+ it_behaves_like 'unknown integration'
+ end
+
+ describe 'PUT #test' do
+ before do
+ put :test, params: routing_params
+ end
+
+ it_behaves_like 'unknown integration'
end
end
diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
index 1877aa6490d..7725366d565 100644
--- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
@@ -6,9 +6,12 @@
RSpec.shared_examples 'User updates wiki page' do
include WikiHelpers
+ let(:diagramsnet_url) { 'https://embed.diagrams.net' }
before do
sign_in(user)
+ allow(Gitlab::CurrentSettings).to receive(:diagramsnet_enabled).and_return(true)
+ allow(Gitlab::CurrentSettings).to receive(:diagramsnet_url).and_return(diagramsnet_url)
end
context 'when wiki is empty', :js do
diff --git a/spec/workers/concerns/worker_attributes_spec.rb b/spec/workers/concerns/worker_attributes_spec.rb
index ac9d3fa824e..cee8e0fb0ad 100644
--- a/spec/workers/concerns/worker_attributes_spec.rb
+++ b/spec/workers/concerns/worker_attributes_spec.rb
@@ -37,6 +37,7 @@ RSpec.describe WorkerAttributes, feature_category: :shared do
:worker_has_external_dependencies? | :worker_has_external_dependencies! | false | [] | true
:idempotent? | :idempotent! | false | [] | true
:big_payload? | :big_payload! | false | [] | true
+ :database_health_check_attrs | :defer_on_database_health_signal | nil | [:gitlab_main, 1.minute, [:users]] | { gitlab_schema: :gitlab_main, delay_by: 1.minute, tables: [:users] }
end
# rubocop: enable Layout/LineLength
@@ -141,4 +142,38 @@ RSpec.describe WorkerAttributes, feature_category: :shared do
end
end
end
+
+ describe '#defer_on_database_health_signal?' do
+ subject(:defer_on_database_health_signal?) { worker.defer_on_database_health_signal? }
+
+ context 'when defer_on_database_health_signal is set' do
+ before do
+ worker.defer_on_database_health_signal(:gitlab_main, 1.minute, [:users])
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when defer_on_database_health_signal is not set' do
+ it { is_expected.to be(false) }
+ end
+
+ context 'when FF `defer_sidekiq_workers_on_database_health_signal` is disabled' do
+ before do
+ stub_feature_flags(defer_sidekiq_workers_on_database_health_signal: false)
+ end
+
+ context 'when defer_on_database_health_signal is set' do
+ before do
+ worker.defer_on_database_health_signal(:gitlab_main, 1.minute, [:users])
+ end
+
+ it { is_expected.to be(false) }
+ end
+
+ context 'when defer_on_database_health_signal is not set' do
+ it { is_expected.to be(false) }
+ end
+ end
+ end
end
diff --git a/spec/workers/packages/cleanup/delete_orphaned_dependencies_worker_spec.rb b/spec/workers/packages/cleanup/delete_orphaned_dependencies_worker_spec.rb
index ffa7767075e..73a28053cc5 100644
--- a/spec/workers/packages/cleanup/delete_orphaned_dependencies_worker_spec.rb
+++ b/spec/workers/packages/cleanup/delete_orphaned_dependencies_worker_spec.rb
@@ -104,15 +104,5 @@ RSpec.describe Packages::Cleanup::DeleteOrphanedDependenciesWorker, feature_cate
subject
end
end
-
- context 'when the FF is disabled' do
- before do
- stub_feature_flags(packages_delete_orphaned_dependencies_worker: false)
- end
-
- it 'does not execute the worker' do
- expect { subject }.not_to change { Packages::Dependency.count }
- end
- end
end
end
diff --git a/vendor/gems/attr_encrypted/.gitlab-ci.yml b/vendor/gems/attr_encrypted/.gitlab-ci.yml
index 42b2b9e1e01..7d954de43a0 100644
--- a/vendor/gems/attr_encrypted/.gitlab-ci.yml
+++ b/vendor/gems/attr_encrypted/.gitlab-ci.yml
@@ -23,4 +23,4 @@ rspec:
- bundle exec rake test
parallel:
matrix:
- - RUBY_VERSION: ["2.7", "3.0"]
+ - RUBY_VERSION: ["2.7", "3.0", "3.1"]