summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-09 15:17:20 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-09 15:17:20 +0000
commit3670ddd229b178c0a2e09a1466ddfd7fd2f7855d (patch)
tree9be2a8155e0b14fb9a07b6a1c8bcfa629af4a25c
parent0b4adad74b76b34855e9a6d943f9b9188c3914fa (diff)
downloadgitlab-ce-3670ddd229b178c0a2e09a1466ddfd7fd2f7855d.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/CODEOWNERS103
-rw-r--r--.gitlab/ci/package-and-test/main.gitlab-ci.yml4
-rw-r--r--.gitlab/ci/review-apps/main.gitlab-ci.yml5
-rw-r--r--.gitlab/issue_templates/Feature Flag Cleanup.md2
-rw-r--r--.rubocop.yml6
-rw-r--r--.rubocop_todo/rspec/avoid_conditional_statements.yml84
-rw-r--r--app/assets/images/vulnerability/secureflag-logo.svg25
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js6
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue8
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue1
-rw-r--r--app/assets/javascripts/issuable/popover/index.js1
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue10
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/translations.js2
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js5
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue14
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_discussion.vue19
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue16
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue14
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue49
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue18
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue22
-rw-r--r--app/assets/javascripts/work_items/graphql/project_work_items.query.graphql4
-rw-r--r--app/assets/javascripts/work_items/index.js1
-rw-r--r--app/helpers/projects/ml/experiments_helper.rb12
-rw-r--r--app/models/concerns/mentionable/reference_regexes.rb2
-rw-r--r--app/models/ml/candidate.rb6
-rw-r--r--app/models/service_desk/custom_email_verification.rb7
-rw-r--r--app/models/work_item.rb8
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml5
-rw-r--r--app/views/projects/branches/_commit.html.haml2
-rw-r--r--app/views/projects/issues/_work_item_links.html.haml2
-rw-r--r--app/views/projects/tags/_edit_release_button.html.haml4
-rw-r--r--app/views/projects/tags/_release_link.html.haml6
-rw-r--r--app/views/projects/tags/_tag.html.haml18
-rw-r--r--babel.config.js1
-rw-r--r--config/events/20230508093658_gcp_signup_offer_click_click.yml21
-rw-r--r--config/events/20230508093912_gcp_signup_offer_dismiss_dismiss.yml21
-rw-r--r--config/feature_flags/development/disable_follow_users.yml2
-rw-r--r--db/post_migrate/20230328030101_add_secureflag_training_provider.rb31
-rw-r--r--db/post_migrate/20230508093910_create_package_manager_name_index.rb23
-rw-r--r--db/schema_migrations/202303280301011
-rw-r--r--db/schema_migrations/202305080939101
-rw-r--r--db/structure.sql2
-rw-r--r--doc/api/graphql/reference/index.md59
-rw-r--r--doc/development/documentation/index.md14
-rw-r--r--doc/development/service_ping/metrics_lifecycle.md56
-rw-r--r--doc/user/markdown.md12
-rw-r--r--lib/banzai/filter/issuable_reference_expansion_filter.rb2
-rw-r--r--lib/banzai/filter/references/work_item_reference_filter.rb26
-rw-r--r--lib/banzai/issuable_extractor.rb4
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb20
-rw-r--r--lib/banzai/reference_parser/work_item_parser.rb17
-rw-r--r--lib/gitlab/ci/config/external/file/project.rb4
-rw-r--r--lib/gitlab/ci/yaml_processor.rb10
-rw-r--r--lib/gitlab/database_importers/security/training_providers/importer.rb9
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/regex.rb4
-rw-r--r--lib/gitlab/saas.rb4
-rw-r--r--lib/tasks/gitlab/tw/codeowners.rake5
-rw-r--r--locale/gitlab.pot38
-rw-r--r--rubocop/cop/rspec/avoid_conditional_statements.rb35
-rw-r--r--spec/factories/work_items.rb13
-rw-r--r--spec/features/boards/issue_ordering_spec.rb4
-rw-r--r--spec/features/markdown/markdown_spec.rb2
-rw-r--r--spec/features/markdown/metrics_spec.rb15
-rw-r--r--spec/features/merge_request/user_sees_discussions_navigation_spec.rb2
-rw-r--r--spec/features/projects/environments/environments_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb4
-rw-r--r--spec/features/tags/developer_deletes_tag_spec.rb22
-rw-r--r--spec/features/tags/maintainer_deletes_protected_tag_spec.rb7
-rw-r--r--spec/fixtures/markdown.md.erb13
-rw-r--r--spec/frontend/behaviors/markdown/render_gfm_spec.js26
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js58
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js8
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js23
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js4
-rw-r--r--spec/frontend/security_configuration/mock_data.js3
-rw-r--r--spec/frontend/work_items/components/notes/work_item_add_note_spec.js18
-rw-r--r--spec/frontend/work_items/components/notes/work_item_discussion_spec.js8
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js7
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_created_updated_spec.js5
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js10
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js12
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js5
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_spec.js5
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js12
-rw-r--r--spec/helpers/projects/ml/experiments_helper_spec.rb12
-rw-r--r--spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb89
-rw-r--r--spec/lib/banzai/filter/references/work_item_reference_filter_spec.rb314
-rw-r--r--spec/lib/banzai/issuable_extractor_spec.rb15
-rw-r--r--spec/lib/banzai/reference_parser/work_item_parser_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb28
-rw-r--r--spec/migrations/20230328030101_add_secureflag_training_provider_spec.rb25
-rw-r--r--spec/models/ml/candidate_spec.rb19
-rw-r--r--spec/models/work_item_spec.rb14
-rw-r--r--spec/rubocop/cop/rspec/avoid_conditional_statements_spec.rb42
-rw-r--r--spec/support/capybara_wait_for_all_requests.rb (renamed from spec/support/capybara_wait_for_all_requests_after_visit_page.rb)18
-rw-r--r--spec/support/finder_collection_allowlist.yml1
-rw-r--r--spec/support/helpers/markdown_feature.rb8
-rw-r--r--spec/support/matchers/markdown_matchers.rb11
-rw-r--r--spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb129
-rw-r--r--spec/support/shared_examples/security_training_providers_importer.rb4
-rw-r--r--spec/support_specs/capybara_wait_for_all_requests_after_page_visit_spec.rb23
-rw-r--r--spec/support_specs/capybara_wait_for_all_requests_spec.rb43
120 files changed, 1692 insertions, 442 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index 31ddb12c65b..72860a91241 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -411,7 +411,7 @@ lib/gitlab/checks/**
/doc/administration/environment_variables.md @axil
/doc/administration/external_pipeline_validation.md @drcatherinepope
/doc/administration/feature_flags.md @axil
-/doc/administration/file_hooks.md @ashrafkhamis
+/doc/administration/file_hooks.md @eread @ashrafkhamis
/doc/administration/geo/ @axil
/doc/administration/get_started.md @kpaizee
/doc/administration/git_protocol.md @aqualls
@@ -436,7 +436,7 @@ lib/gitlab/checks/**
/doc/administration/logs/index.md @msedlakjakubowski
/doc/administration/maintenance_mode/ @axil
/doc/administration/merge_request_diffs.md @aqualls
-/doc/administration/monitoring/github_imports.md @eread
+/doc/administration/monitoring/github_imports.md @eread @ashrafkhamis
/doc/administration/monitoring/gitlab_self_monitoring_project/ @msedlakjakubowski
/doc/administration/monitoring/index.md @msedlakjakubowski
/doc/administration/monitoring/ip_allowlist.md @jglassman1
@@ -480,7 +480,7 @@ lib/gitlab/checks/**
/doc/administration/smime_signing_email.md @axil
/doc/administration/snippets/ @aqualls
/doc/administration/static_objects_external_storage.md @ashrafkhamis
-/doc/administration/system_hooks.md @ashrafkhamis
+/doc/administration/system_hooks.md @eread @ashrafkhamis
/doc/administration/terraform_state.md @phillipwells
/doc/administration/timezone.md @axil
/doc/administration/troubleshooting/ @axil
@@ -490,7 +490,7 @@ lib/gitlab/checks/**
/doc/api/access_requests.md @jglassman1
/doc/api/admin_sidekiq_queues.md @axil
/doc/api/alert_management_alerts.md @msedlakjakubowski
-/doc/api/api_resources.md @ashrafkhamis
+/doc/api/api_resources.md @eread @ashrafkhamis
/doc/api/appearance.md @jglassman1
/doc/api/applications.md @jglassman1
/doc/api/audit_events.md @eread
@@ -498,7 +498,7 @@ lib/gitlab/checks/**
/doc/api/award_emoji.md @msedlakjakubowski
/doc/api/boards.md @msedlakjakubowski
/doc/api/branches.md @aqualls
-/doc/api/bulk_imports.md @eread
+/doc/api/bulk_imports.md @eread @ashrafkhamis
/doc/api/cluster_agents.md @phillipwells
/doc/api/commits.md @aqualls
/doc/api/container_registry.md @marcel.amirault
@@ -522,13 +522,14 @@ lib/gitlab/checks/**
/doc/api/features.md @phillipwells
/doc/api/freeze_periods.md @phillipwells
/doc/api/geo_nodes.md @axil
+/doc/api/geo_sites.md @axil
/doc/api/graphql/audit_report.md @eread
/doc/api/graphql/branch_rules.md @aqualls
/doc/api/graphql/custom_emoji.md @msedlakjakubowski
-/doc/api/graphql/getting_started.md @ashrafkhamis
-/doc/api/graphql/index.md @ashrafkhamis
-/doc/api/graphql/reference/ @ashrafkhamis
-/doc/api/graphql/removed_items.md @ashrafkhamis
+/doc/api/graphql/getting_started.md @eread @ashrafkhamis
+/doc/api/graphql/index.md @eread @ashrafkhamis
+/doc/api/graphql/reference/ @eread @ashrafkhamis
+/doc/api/graphql/removed_items.md @eread @ashrafkhamis
/doc/api/graphql/sample_issue_boards.md @msedlakjakubowski
/doc/api/graphql/users_example.md @jglassman1
/doc/api/group_access_tokens.md @jglassman1
@@ -537,22 +538,22 @@ lib/gitlab/checks/**
/doc/api/group_boards.md @msedlakjakubowski
/doc/api/group_clusters.md @phillipwells
/doc/api/group_epic_boards.md @msedlakjakubowski
-/doc/api/group_import_export.md @eread
+/doc/api/group_import_export.md @eread @ashrafkhamis
/doc/api/group_iterations.md @msedlakjakubowski
/doc/api/group_labels.md @msedlakjakubowski
/doc/api/group_level_variables.md @marcel.amirault
/doc/api/group_milestones.md @msedlakjakubowski
/doc/api/group_protected_branches.md @aqualls
/doc/api/group_protected_environments.md @phillipwells
-/doc/api/group_relations_export.md @eread
+/doc/api/group_relations_export.md @eread @ashrafkhamis
/doc/api/group_releases.md @phillipwells
/doc/api/group_repository_storage_moves.md @ashrafkhamis
/doc/api/groups.md @lciutacu
-/doc/api/import.md @eread
-/doc/api/index.md @ashrafkhamis
+/doc/api/import.md @eread @ashrafkhamis
+/doc/api/index.md @eread @ashrafkhamis
/doc/api/instance_clusters.md @phillipwells
/doc/api/instance_level_ci_variables.md @marcel.amirault
-/doc/api/integrations.md @ashrafkhamis
+/doc/api/integrations.md @eread @ashrafkhamis
/doc/api/issue_links.md @msedlakjakubowski
/doc/api/issues.md @msedlakjakubowski
/doc/api/issues_statistics.md @msedlakjakubowski
@@ -580,7 +581,7 @@ lib/gitlab/checks/**
/doc/api/notes.md @msedlakjakubowski
/doc/api/notification_settings.md @msedlakjakubowski
/doc/api/oauth2.md @jglassman1
-/doc/api/openapi/ @ashrafkhamis
+/doc/api/openapi/ @eread @ashrafkhamis
/doc/api/packages.md @marcel.amirault
/doc/api/packages/ @marcel.amirault
/doc/api/personal_access_tokens.md @eread
@@ -595,7 +596,7 @@ lib/gitlab/checks/**
/doc/api/project_clusters.md @phillipwells
/doc/api/project_import_export.md @aqualls
/doc/api/project_level_variables.md @marcel.amirault
-/doc/api/project_relations_export.md @eread
+/doc/api/project_relations_export.md @eread @ashrafkhamis
/doc/api/project_repository_storage_moves.md @eread
/doc/api/project_snippets.md @aqualls
/doc/api/project_statistics.md @aqualls
@@ -616,7 +617,7 @@ lib/gitlab/checks/**
/doc/api/resource_milestone_events.md @msedlakjakubowski
/doc/api/resource_state_events.md @msedlakjakubowski
/doc/api/resource_weight_events.md @msedlakjakubowski
-/doc/api/rest/ @ashrafkhamis
+/doc/api/rest/ @eread @ashrafkhamis
/doc/api/runners.md @fneill
/doc/api/saml.md @jglassman1
/doc/api/scim.md @jglassman1
@@ -629,7 +630,7 @@ lib/gitlab/checks/**
/doc/api/statistics.md @jglassman1
/doc/api/status_checks.md @eread
/doc/api/suggestions.md @aqualls
-/doc/api/system_hooks.md @ashrafkhamis
+/doc/api/system_hooks.md @eread @ashrafkhamis
/doc/api/tags.md @aqualls
/doc/api/templates/dockerfiles.md @aqualls
/doc/api/templates/gitignores.md @aqualls
@@ -649,7 +650,7 @@ lib/gitlab/checks/**
/doc/architecture/blueprints/database_scaling/ @aqualls
/doc/ci/ @drcatherinepope
/doc/ci/caching/ @marcel.amirault
-/doc/ci/chatops/ @eread
+/doc/ci/chatops/ @eread @ashrafkhamis
/doc/ci/cloud_deployment/ @phillipwells
/doc/ci/cloud_services/ @marcel.amirault
/doc/ci/directed_acyclic_graph/ @marcel.amirault
@@ -690,10 +691,10 @@ lib/gitlab/checks/**
/doc/development/backend/ @sselhorn
/doc/development/backend/create_source_code_be/ @aqualls
/doc/development/build_test_package.md @axil
-/doc/development/bulk_import.md @eread
+/doc/development/bulk_import.md @eread @ashrafkhamis
/doc/development/cached_queries.md @jglassman1
/doc/development/cascading_settings.md @jglassman1
-/doc/development/chatops_on_gitlabcom.md @eread
+/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
@@ -709,7 +710,7 @@ lib/gitlab/checks/**
/doc/development/distributed_tracing.md @msedlakjakubowski
/doc/development/distribution/ @axil
/doc/development/documentation/ @sselhorn
-/doc/development/export_csv.md @eread
+/doc/development/export_csv.md @eread @ashrafkhamis
/doc/development/fe_guide/customizable_dashboards.md @lciutacu
/doc/development/fe_guide/dark_mode.md @sselhorn
/doc/development/fe_guide/graphql.md @sselhorn
@@ -726,13 +727,13 @@ lib/gitlab/checks/**
/doc/development/gitaly.md @eread
/doc/development/gitlab_flavored_markdown/ @ashrafkhamis
/doc/development/gitlab_shell/ @aqualls
-/doc/development/graphql_guide/ @ashrafkhamis
+/doc/development/graphql_guide/ @eread @ashrafkhamis
/doc/development/graphql_guide/batchloader.md @aqualls
-/doc/development/i18n/ @eread
+/doc/development/i18n/ @eread @ashrafkhamis
/doc/development/image_scaling.md @lciutacu
-/doc/development/import_export.md @eread
+/doc/development/import_export.md @eread @ashrafkhamis
/doc/development/index.md @sselhorn
-/doc/development/integrations/ @ashrafkhamis
+/doc/development/integrations/ @eread @ashrafkhamis
/doc/development/integrations/secure.md @rdickenson
/doc/development/integrations/secure_partner_integration.md @rdickenson
/doc/development/internal_api/ @aqualls
@@ -778,21 +779,21 @@ lib/gitlab/checks/**
/doc/integration/advanced_search/ @ashrafkhamis
/doc/integration/akismet.md @phillipwells
/doc/integration/arkose.md @phillipwells
-/doc/integration/datadog.md @ashrafkhamis
-/doc/integration/external-issue-tracker.md @ashrafkhamis
+/doc/integration/datadog.md @eread @ashrafkhamis
+/doc/integration/external-issue-tracker.md @eread @ashrafkhamis
/doc/integration/gitpod.md @ashrafkhamis
/doc/integration/glab/ @aqualls
-/doc/integration/gmail_action_buttons_for_gitlab.md @ashrafkhamis
-/doc/integration/index.md @ashrafkhamis
-/doc/integration/jenkins.md @ashrafkhamis
-/doc/integration/jira/ @ashrafkhamis
+/doc/integration/gmail_action_buttons_for_gitlab.md @eread @ashrafkhamis
+/doc/integration/index.md @eread @ashrafkhamis
+/doc/integration/jenkins.md @eread @ashrafkhamis
+/doc/integration/jira/ @eread @ashrafkhamis
/doc/integration/mattermost/ @axil
/doc/integration/partner_marketplace.md @fneill
/doc/integration/recaptcha.md @phillipwells
/doc/integration/security_partners/ @rdickenson
-/doc/integration/slash_commands.md @ashrafkhamis
+/doc/integration/slash_commands.md @eread @ashrafkhamis
/doc/integration/sourcegraph.md @aqualls
-/doc/integration/trello_power_up.md @ashrafkhamis
+/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
@@ -811,21 +812,23 @@ lib/gitlab/checks/**
/doc/subscriptions/gitlab_dedicated/ @drcatherinepope
/doc/topics/authentication/ @jglassman1
/doc/topics/autodevops/ @phillipwells
-/doc/topics/awesome_co.md @sselhorn
+/doc/topics/data_seeder.md @sselhorn
/doc/topics/git/ @aqualls
/doc/topics/gitlab_flow.md @aqualls
/doc/topics/offline/ @axil
/doc/topics/plan_and_track.md @msedlakjakubowski
-/doc/topics/your_work.md @sselhorn
/doc/tutorials/ @kpaizee
-/doc/tutorials/create_compliance_pipeline.md @eread
-/doc/tutorials/fuzz_testing_tutorial.md @rdickenson
-/doc/tutorials/scan_result_policy.md @rdickenson
+/doc/tutorials/boards_for_teams/ @msedlakjakubowski
+/doc/tutorials/compliance_pipeline/ @eread
+/doc/tutorials/convert_personal_namespace_to_group/ @lciutacu
+/doc/tutorials/fuzz_testing/ @rdickenson
+/doc/tutorials/move_personal_project_to_group/ @lciutacu
+/doc/tutorials/scan_result_policy/ @rdickenson
/doc/update/ @axil
/doc/update/background_migrations.md @aqualls
/doc/user/admin_area/analytics/ @lciutacu
/doc/user/admin_area/credentials_inventory.md @jglassman1
-/doc/user/admin_area/custom_project_templates.md @eread
+/doc/user/admin_area/custom_project_templates.md @aqualls
/doc/user/admin_area/diff_limits.md @aqualls
/doc/user/admin_area/external_users.md @jglassman1
/doc/user/admin_area/geo_sites.md @axil
@@ -846,12 +849,12 @@ lib/gitlab/checks/**
/doc/user/admin_area/settings/files_api_rate_limits.md @aqualls
/doc/user/admin_area/settings/git_lfs_rate_limits.md @aqualls
/doc/user/admin_area/settings/gitaly_timeouts.md @eread
-/doc/user/admin_area/settings/import_export_rate_limits.md @eread
+/doc/user/admin_area/settings/import_export_rate_limits.md @eread @ashrafkhamis
/doc/user/admin_area/settings/incident_management_rate_limits.md @msedlakjakubowski
/doc/user/admin_area/settings/index.md @aqualls
/doc/user/admin_area/settings/instance_template_repository.md @aqualls
/doc/user/admin_area/settings/package_registry_rate_limits.md @marcel.amirault
-/doc/user/admin_area/settings/project_integration_management.md @ashrafkhamis
+/doc/user/admin_area/settings/project_integration_management.md @eread @ashrafkhamis
/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
@@ -881,10 +884,10 @@ lib/gitlab/checks/**
/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 @eread
+/doc/user/group/custom_project_templates.md @aqualls
/doc/user/group/devops_adoption/ @lciutacu
/doc/user/group/epics/ @msedlakjakubowski
-/doc/user/group/import/ @eread
+/doc/user/group/import/ @eread @ashrafkhamis
/doc/user/group/index.md @lciutacu
/doc/user/group/insights/ @lciutacu
/doc/user/group/issues_analytics/ @msedlakjakubowski
@@ -930,16 +933,16 @@ lib/gitlab/checks/**
/doc/user/project/file_lock.md @aqualls
/doc/user/project/git_attributes.md @aqualls
/doc/user/project/highlighting.md @aqualls
-/doc/user/project/import/ @eread
+/doc/user/project/import/ @eread @ashrafkhamis
/doc/user/project/import/jira.md @msedlakjakubowski
/doc/user/project/index.md @lciutacu
/doc/user/project/insights/ @lciutacu
-/doc/user/project/integrations/ @ashrafkhamis
+/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
+/doc/user/project/issues/csv_import.md @eread @ashrafkhamis
/doc/user/project/labels.md @msedlakjakubowski
/doc/user/project/members/ @lciutacu
/doc/user/project/merge_requests/ @aqualls
@@ -962,14 +965,13 @@ lib/gitlab/checks/**
/doc/user/project/repository/web_editor.md @ashrafkhamis
/doc/user/project/requirements/ @msedlakjakubowski
/doc/user/project/service_desk.md @msedlakjakubowski
-/doc/user/project/settings/import_export.md @eread
-/doc/user/project/settings/import_export_troubleshooting.md @eread
+/doc/user/project/settings/import_export.md @eread @ashrafkhamis
+/doc/user/project/settings/import_export_troubleshooting.md @eread @ashrafkhamis
/doc/user/project/settings/index.md @lciutacu
/doc/user/project/settings/project_access_tokens.md @jglassman1
/doc/user/project/system_notes.md @aqualls
/doc/user/project/time_tracking.md @msedlakjakubowski
/doc/user/project/web_ide/ @ashrafkhamis
-/doc/user/project/web_ide_beta/ @ashrafkhamis
/doc/user/project/working_with_projects.md @lciutacu
/doc/user/public_access.md @lciutacu
/doc/user/report_abuse.md @phillipwells
@@ -981,7 +983,6 @@ lib/gitlab/checks/**
/doc/user/tasks.md @msedlakjakubowski
/doc/user/todos.md @msedlakjakubowski
/doc/user/usage_quotas.md @fneill
-/doc/user/workspace/quick_start/ @ashrafkhamis
# End rake-managed-docs-block
[Authentication and Authorization] @gitlab-org/manage/authentication-and-authorization/approvers
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
index e61eb0b5ca6..a53c195e025 100644
--- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
@@ -132,11 +132,15 @@ trigger-omnibus-env:
echo "EE=$([[ $FOSS_ONLY == '1' ]] && echo 'false' || echo 'true')" >> $BUILD_ENV
target_branch_name="${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-${CI_COMMIT_REF_NAME}}"
echo "TRIGGER_BRANCH=$([[ "${target_branch_name}" =~ ^[0-9-]+-stable(-ee)?$ ]] && echo ${target_branch_name%-ee} || echo 'master')" >> $BUILD_ENV
+ - |
echo "Built environment file for omnibus build:"
cat $BUILD_ENV
artifacts:
+ expire_in: 3 days
reports:
dotenv: $BUILD_ENV
+ paths:
+ - $BUILD_ENV
trigger-omnibus-env-ce:
extends:
diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml
index 94492e93b75..aa61e67de08 100644
--- a/.gitlab/ci/review-apps/main.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml
@@ -90,9 +90,9 @@ review-build-cng:
strategy: depend
.review-workflow-base:
- extends:
- - .default-retry
image: ${REVIEW_APPS_IMAGE}
+ retry:
+ max: 2 # This is confusing but this means "3 runs at max"
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
@@ -126,6 +126,7 @@ review-deploy:
- *base-before_script
- !reference [".use-kube-context", before_script]
script:
+ - run_timed_command "retry delete_helm_release"
- run_timed_command "check_kube_domain"
- run_timed_command "download_chart"
- run_timed_command "deploy" || (display_deployment_debug && exit 1)
diff --git a/.gitlab/issue_templates/Feature Flag Cleanup.md b/.gitlab/issue_templates/Feature Flag Cleanup.md
index d32b0c874d4..f96165fd359 100644
--- a/.gitlab/issue_templates/Feature Flag Cleanup.md
+++ b/.gitlab/issue_templates/Feature Flag Cleanup.md
@@ -48,4 +48,4 @@ Are there any other stages or teams involved that need to be kept in the loop?
- [ ] Close this rollout issue.
-/label ~"feature flag" ~"type::feature" ~"feature::addition"
+/label ~"feature flag" ~"type::maintenance" ~"maintenance::removal"
diff --git a/.rubocop.yml b/.rubocop.yml
index b3f24e12c22..81ad4cd31f3 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -527,6 +527,12 @@ RSpec/AvoidTestProf:
- 'ee/spec/lib/gitlab/background_migration/**/*.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/**/*.rb'
+RSpec/AvoidConditionalStatements:
+ Enabled: true
+ Include:
+ - 'spec/features/**/*.rb'
+ - 'ee/spec/features/**/*.rb'
+
RSpec/FactoriesInMigrationSpecs:
Enabled: true
Include:
diff --git a/.rubocop_todo/rspec/avoid_conditional_statements.yml b/.rubocop_todo/rspec/avoid_conditional_statements.yml
new file mode 100644
index 00000000000..43ffaaa452a
--- /dev/null
+++ b/.rubocop_todo/rspec/avoid_conditional_statements.yml
@@ -0,0 +1,84 @@
+---
+RSpec/AvoidConditionalStatements:
+ Details: grace period
+ Exclude:
+ - 'ee/spec/features/admin/admin_settings_spec.rb'
+ - 'ee/spec/features/analytics/code_analytics_spec.rb'
+ - 'ee/spec/features/billings/billing_plans_spec.rb'
+ - 'ee/spec/features/boards/scoped_issue_board_spec.rb'
+ - 'ee/spec/features/boards/user_visits_board_spec.rb'
+ - 'ee/spec/features/ci_shared_runner_warnings_spec.rb'
+ - 'ee/spec/features/epic_boards/epic_boards_spec.rb'
+ - 'ee/spec/features/epics/epic_show_spec.rb'
+ - 'ee/spec/features/epics/gfm_autocomplete_spec.rb'
+ - 'ee/spec/features/group_protected_branches_spec.rb'
+ - 'ee/spec/features/groups/analytics/cycle_analytics/filters_and_data_spec.rb'
+ - 'ee/spec/features/groups/analytics/cycle_analytics/multiple_value_streams_spec.rb'
+ - 'ee/spec/features/groups/iterations/user_views_iteration_spec.rb'
+ - 'ee/spec/features/incidents/incident_details_spec.rb'
+ - 'ee/spec/features/issues/user_sees_empty_state_spec.rb'
+ - 'ee/spec/features/labels_hierarchy_spec.rb'
+ - 'ee/spec/features/profiles/usage_quotas_spec.rb'
+ - 'ee/spec/features/projects/analytics/visualization_designer_spec.rb'
+ - 'ee/spec/features/projects/licenses/maintainer_views_policies_spec.rb'
+ - 'ee/spec/features/projects/merge_requests/user_approves_merge_request_spec.rb'
+ - 'ee/spec/features/projects/settings/issues_settings_spec.rb'
+ - 'ee/spec/features/projects_spec.rb'
+ - 'ee/spec/features/registrations/email_confirmation_spec.rb'
+ - 'ee/spec/features/registrations/identity_verification_spec.rb'
+ - 'ee/spec/features/search/elastic/snippet_search_spec.rb'
+ - 'ee/spec/features/subscriptions/expiring_subscription_message_spec.rb'
+ - 'ee/spec/features/users/identity_verification_spec.rb'
+ - 'spec/features/admin/dashboard_spec.rb'
+ - 'spec/features/calendar_spec.rb'
+ - 'spec/features/groups/dependency_proxy_for_containers_spec.rb'
+ - 'spec/features/groups/empty_states_spec.rb'
+ - 'spec/features/groups/group_settings_spec.rb'
+ - 'spec/features/groups/members/sort_members_spec.rb'
+ - 'spec/features/groups/navbar_spec.rb'
+ - 'spec/features/issuables/issuable_list_spec.rb'
+ - 'spec/features/issuables/markdown_references/jira_spec.rb'
+ - 'spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb'
+ - 'spec/features/issues/user_bulk_edits_issues_labels_spec.rb'
+ - 'spec/features/issues/user_creates_branch_and_merge_request_spec.rb'
+ - 'spec/features/issues/user_edits_issue_spec.rb'
+ - 'spec/features/issues/user_interacts_with_awards_spec.rb'
+ - 'spec/features/labels_hierarchy_spec.rb'
+ - 'spec/features/markdown/keyboard_shortcuts_spec.rb'
+ - 'spec/features/merge_request/batch_comments_spec.rb'
+ - 'spec/features/merge_request/user_posts_diff_notes_spec.rb'
+ - 'spec/features/merge_request/user_reverts_merge_request_spec.rb'
+ - 'spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb'
+ - 'spec/features/merge_request/user_squashes_merge_request_spec.rb'
+ - 'spec/features/merge_request/user_suggests_changes_on_diff_spec.rb'
+ - 'spec/features/monitor_sidebar_link_spec.rb'
+ - 'spec/features/oauth_login_spec.rb'
+ - 'spec/features/participants_autocomplete_spec.rb'
+ - 'spec/features/profiles/user_edit_profile_spec.rb'
+ - 'spec/features/projects/blobs/edit_spec.rb'
+ - 'spec/features/projects/branches_spec.rb'
+ - 'spec/features/projects/commit/cherry_pick_spec.rb'
+ - 'spec/features/projects/commit/user_reverts_commit_spec.rb'
+ - 'spec/features/projects/compare_spec.rb'
+ - 'spec/features/projects/deploy_keys_spec.rb'
+ - 'spec/features/projects/environments/environment_spec.rb'
+ - 'spec/features/projects/files/template_selector_menu_spec.rb'
+ - 'spec/features/projects/integrations/user_activates_issue_tracker_spec.rb'
+ - 'spec/features/projects/integrations/user_activates_jira_spec.rb'
+ - 'spec/features/projects/labels/user_removes_labels_spec.rb'
+ - 'spec/features/projects/members/sorting_spec.rb'
+ - 'spec/features/projects/milestones/milestone_spec.rb'
+ - 'spec/features/projects/releases/user_views_releases_spec.rb'
+ - 'spec/features/projects/settings/project_settings_spec.rb'
+ - 'spec/features/projects/settings/repository_settings_spec.rb'
+ - 'spec/features/projects/settings/user_transfers_a_project_spec.rb'
+ - 'spec/features/projects/show/user_sees_git_instructions_spec.rb'
+ - 'spec/features/projects/tree/create_directory_spec.rb'
+ - 'spec/features/projects/tree/create_file_spec.rb'
+ - 'spec/features/projects_spec.rb'
+ - 'spec/features/search/user_uses_header_search_field_spec.rb'
+ - 'spec/features/snippets/explore_spec.rb'
+ - 'spec/features/tags/developer_creates_tag_spec.rb'
+ - 'spec/features/usage_stats_consent_spec.rb'
+ - 'spec/features/users/login_spec.rb'
+ - 'spec/features/users/overview_spec.rb'
diff --git a/app/assets/images/vulnerability/secureflag-logo.svg b/app/assets/images/vulnerability/secureflag-logo.svg
new file mode 100644
index 00000000000..621c56b9043
--- /dev/null
+++ b/app/assets/images/vulnerability/secureflag-logo.svg
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">
+ <defs>
+ <linearGradient id="paint1_linear_117_388" x1="8.32922" y1="0.701083" x2="25.6103" y2="8.8381"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(5.965987, 0, 0, 5.965987, -1.949537, -0.014549)">
+ <stop stop-color="#005AEC" />
+ <stop offset="1" stop-color="#12DFE7" />
+ </linearGradient>
+ <linearGradient id="paint2_linear_117_388" x1="3.30485" y1="11.2131" x2="20.4972" y2="19.4227"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(5.965987, 0, 0, 5.965987, -1.949537, -0.014549)">
+ <stop stop-color="#005AEC" />
+ <stop offset="1" stop-color="#12DFE7" />
+ </linearGradient>
+ </defs>
+ <g style="" transform="matrix(3.098345, 0, 0, 3.098345, 46.765705, 8.335629)"
+ bx:origin="0.503455 0.502894">
+ <path d="M 65.436 0.003 L 65.436 26.662 L 87.144 12.772 L 65.436 0.003 Z"
+ fill="url(#paint1_linear_117_388)" style="" />
+ <path
+ d="M 108.686 77.337 L 87.143 65.001 L 87.143 38.543 L 65.434 51.815 L 43.562 38.543 L 43.809 64.746 L 22.512 77.592 L 0.393 64.321 L 0.393 116.301 L 65.352 155.095 L 129.901 116.301 L 129.901 64.406 L 108.686 77.337 Z M 65.434 103.2 L 43.562 90.609 L 43.562 114.344 L 22.923 102.945 L 43.562 90.609 L 65.434 77.848 C 65.434 77.848 85.663 90.694 86.156 90.609 C 86.65 90.523 65.434 103.2 65.434 103.2 Z M 86.156 114.344 L 86.156 90.609 L 106.795 102.945 L 86.156 114.344 Z"
+ fill="url(#paint2_linear_117_388)" style="" />
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 04b3599ea8c..39a7a76e91f 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -39,7 +39,7 @@ export function renderGFM(element) {
'.js-render-mermaid',
'[lang="json"][data-lang-params="table"]',
'.gfm-project_member',
- '.gfm-issue, .gfm-merge_request',
+ '.gfm-issue, .gfm-work_item, .gfm-merge_request',
'.js-render-metrics',
'.js-render-observability',
].map((selector) => Array.from(element.querySelectorAll(selector)));
@@ -50,7 +50,9 @@ export function renderGFM(element) {
renderSandboxedMermaid(mermaidEls);
renderJSONTable(tableEls.map((e) => e.parentNode));
highlightCurrentUser(userEls);
- renderMetrics(metricsEls);
+ if (!window.gon?.features?.removeMonitorMetrics) {
+ renderMetrics(metricsEls);
+ }
renderObservability(observabilityEls);
initPopovers(popoverEls);
}
diff --git a/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue b/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue
index 2cff11c1aa1..087ddafb137 100644
--- a/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue
@@ -43,8 +43,12 @@ export default {
},
computed: {
shouldShowCreateRunnerWorkflow() {
- // create_runner_workflow_for_admin feature flag
- return this.newRunnerPath && this.glFeatures?.createRunnerWorkflowForAdmin;
+ // create_runner_workflow_for_admin or create_runner_workflow_for_namespace
+ return (
+ this.newRunnerPath &&
+ (this.glFeatures?.createRunnerWorkflowForAdmin ||
+ this.glFeatures?.createRunnerWorkflowForNamespace)
+ );
},
},
modalId: 'runners-empty-state-instructions-modal',
diff --git a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
index 9f3e6f247d7..a4ca44b8e5a 100644
--- a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
@@ -270,6 +270,7 @@ export default {
v-if="noRunnersFound"
:registration-token="registrationToken"
:is-search-filtered="isSearchFiltered"
+ :new-runner-path="newRunnerPath"
:svg-path="emptyStateSvgPath"
:filtered-svg-path="emptyStateFilteredSvgPath"
/>
diff --git a/app/assets/javascripts/issuable/popover/index.js b/app/assets/javascripts/issuable/popover/index.js
index de3c8160b7a..9430419685b 100644
--- a/app/assets/javascripts/issuable/popover/index.js
+++ b/app/assets/javascripts/issuable/popover/index.js
@@ -6,6 +6,7 @@ import MRPopover from './components/mr_popover.vue';
const componentsByReferenceType = {
issue: IssuePopover,
+ work_item: IssuePopover,
merge_request: MRPopover,
};
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
index 0d40a727029..25c06aa2f7f 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
@@ -86,6 +86,7 @@ export default {
{ key: 'user', label: this.$options.i18n.USER_LABEL },
...this.paramNames,
...this.metricNames,
+ { key: 'ci_job', label: this.$options.i18n.CI_JOB_LABEL },
{ key: 'artifact', label: this.$options.i18n.ARTIFACTS_LABEL },
];
},
@@ -226,6 +227,15 @@ export default {
<gl-link v-if="data.value" :href="data.value.path">@{{ data.value.username }}</gl-link>
<div v-else>{{ $options.i18n.NO_DATA_CONTENT }}</div>
</template>
+
+ <template #cell(ci_job)="data">
+ <gl-link v-if="data.value" :href="data.value.path" target="_blank">{{
+ data.value.name
+ }}</gl-link>
+ <div v-else class="gl-font-style-italic gl-text-gray-500">
+ {{ $options.i18n.NO_JOB }}
+ </div>
+ </template>
</gl-table-lite>
</div>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/translations.js b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/translations.js
index 604658ded3d..3af33f53fbd 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/translations.js
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/translations.js
@@ -3,12 +3,14 @@ import { s__ } from '~/locale';
export const ARTIFACTS_LABEL = s__('MlExperimentTracking|Artifacts');
export const DETAILS_LABEL = s__('MlExperimentTracking|Details');
export const USER_LABEL = s__('MlExperimentTracking|Author');
+export const CI_JOB_LABEL = s__('MlExperimentTracking|CI Job');
export const CREATED_AT_LABEL = s__('MlExperimentTracking|Created at');
export const NAME_LABEL = s__('MlExperimentTracking|Name');
export const NO_DATA_CONTENT = s__('MlExperimentTracking|-');
export const FILTER_CANDIDATES_LABEL = s__('MlExperimentTracking|Filter candidates');
export const NO_CANDIDATE_NAME = s__('MlExperimentTracking|No name');
export const NO_ARTIFACT = s__('MlExperimentTracking|No artifacts');
+export const NO_JOB = s__('MlExperimentTracking|-');
export const CREATE_NEW_LABEL = s__('MlExperimentTracking|Create new candidates');
export const EMPTY_STATE_DESCRIPTION_LABEL = s__(
'MlExperimentTracking|No candidates logged for the query. Create new candidates using the MLflow client.',
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index 3bf0401ef5e..1b86d7d0a2b 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -17,6 +17,7 @@ import {
import kontraLogo from 'images/vulnerability/kontra-logo.svg';
import scwLogo from 'images/vulnerability/scw-logo.svg';
+import secureflagLogo from 'images/vulnerability/secureflag-logo.svg';
import configureSastMutation from '../graphql/configure_sast.mutation.graphql';
import configureSastIacMutation from '../graphql/configure_iac.mutation.graphql';
import configureSecretDetectionMutation from '../graphql/configure_secret_detection.mutation.graphql';
@@ -313,6 +314,9 @@ export const TEMP_PROVIDER_LOGOS = {
[__('Secure Code Warrior')]: {
svg: scwLogo,
},
+ SecureFlag: {
+ svg: secureflagLogo,
+ },
};
// Use the `url` field from the GraphQL query once this issue is resolved
@@ -320,4 +324,5 @@ export const TEMP_PROVIDER_LOGOS = {
export const TEMP_PROVIDER_URLS = {
Kontra: 'https://application.security/',
[__('Secure Code Warrior')]: 'https://www.securecodewarrior.com/',
+ SecureFlag: 'https://www.secureflag.com/',
};
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index 32ac5daf5de..f9f4bf260a1 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -21,19 +21,16 @@ export default {
WorkItemCommentForm,
},
mixins: [Tracking.mixin()],
+ inject: ['fullPath'],
props: {
workItemId: {
type: String,
required: true,
},
- fullPath: {
+ workItemIid: {
type: String,
required: true,
},
- queryVariables: {
- type: Object,
- required: true,
- },
discussionId: {
type: String,
required: false,
@@ -85,13 +82,16 @@ export default {
workItem: {
query: workItemByIidQuery,
variables() {
- return this.queryVariables;
+ return {
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
+ };
},
update(data) {
return data.workspace.workItems.nodes[0];
},
skip() {
- return !this.queryVariables.iid;
+ return !this.workItemIid;
},
error() {
this.$emit('error', i18n.fetchError);
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
index e40c6296229..e98e03f76fd 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
@@ -18,16 +18,13 @@ export default {
DiscussionNotesRepliesWrapper,
WorkItemNoteReplying,
},
+ inject: ['fullPath'],
props: {
workItemId: {
type: String,
required: true,
},
- queryVariables: {
- type: Object,
- required: true,
- },
- fullPath: {
+ workItemIid: {
type: String,
required: true,
},
@@ -161,8 +158,7 @@ export default {
:assignees="assignees"
:can-set-work-item-metadata="canSetWorkItemMetadata"
:work-item-id="workItemId"
- :query-variables="queryVariables"
- :full-path="fullPath"
+ :work-item-iid="workItemIid"
@startReplying="showReplyForm"
@deleteNote="$emit('deleteNote', note)"
@reportAbuse="$emit('reportAbuse', note)"
@@ -192,9 +188,8 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:assignees="assignees"
:work-item-id="workItemId"
+ :work-item-iid="workItemIid"
:can-set-work-item-metadata="canSetWorkItemMetadata"
- :query-variables="queryVariables"
- :full-path="fullPath"
@startReplying="showReplyForm"
@deleteNote="$emit('deleteNote', note)"
@reportAbuse="$emit('reportAbuse', note)"
@@ -219,9 +214,8 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:assignees="assignees"
:work-item-id="workItemId"
+ :work-item-iid="workItemIid"
:can-set-work-item-metadata="canSetWorkItemMetadata"
- :query-variables="queryVariables"
- :full-path="fullPath"
@startReplying="showReplyForm"
@deleteNote="$emit('deleteNote', reply)"
@reportAbuse="$emit('reportAbuse', reply)"
@@ -233,9 +227,8 @@ export default {
v-if="shouldShowReplyForm"
:notes-form="false"
:autofocus="autofocus"
- :query-variables="queryVariables"
- :full-path="fullPath"
:work-item-id="workItemId"
+ :work-item-iid="workItemIid"
:discussion-id="discussionId"
:work-item-type="workItemType"
:sort-order="sortOrder"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index a6ab8a371de..75b0970a89e 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -32,16 +32,13 @@ export default {
EditedAt,
},
mixins: [Tracking.mixin()],
+ inject: ['fullPath'],
props: {
- fullPath: {
+ workItemId: {
type: String,
required: true,
},
- queryVariables: {
- type: Object,
- required: true,
- },
- workItemId: {
+ workItemIid: {
type: String,
required: true,
},
@@ -158,13 +155,16 @@ export default {
workItem: {
query: workItemByIidQuery,
variables() {
- return this.queryVariables;
+ return {
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
+ };
},
update(data) {
return data.workspace?.workItems?.nodes[0];
},
skip() {
- return !this.queryVariables.iid;
+ return !this.workItemIid;
},
error() {
this.$emit('error', i18n.fetchError);
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index 95527dda1d4..4e6583b65f8 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -54,6 +54,7 @@ export default {
GlIntersectionObserver,
},
mixins: [Tracking.mixin()],
+ inject: ['fullPath'],
props: {
workItemId: {
type: String,
@@ -81,10 +82,6 @@ export default {
required: false,
default: false,
},
- fullPath: {
- type: String,
- required: true,
- },
},
data() {
return {
diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
index 5c30e984f13..78a86aa49a4 100644
--- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue
+++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
@@ -10,17 +10,13 @@ export default {
GlSprintf,
TimeAgoTooltip,
},
+ inject: ['fullPath'],
props: {
workItemIid: {
type: String,
required: false,
default: null,
},
- fullPath: {
- type: String,
- required: false,
- default: null,
- },
},
computed: {
createdAt() {
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index 942f5d4a9f0..f3c94732aae 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -28,19 +28,16 @@ export default {
WorkItemDescriptionRendered,
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
+ inject: ['fullPath'],
props: {
workItemId: {
type: String,
required: true,
},
- fullPath: {
+ workItemIid: {
type: String,
required: true,
},
- queryVariables: {
- type: Object,
- required: true,
- },
},
markdownDocsPath: helpPagePath('user/project/quick_actions'),
quickActionsDocsPath: helpPagePath('user/project/quick_actions'),
@@ -64,13 +61,16 @@ export default {
workItem: {
query: workItemByIidQuery,
variables() {
- return this.queryVariables;
+ return {
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
+ };
},
update(data) {
return data.workspace.workItems.nodes[0];
},
skip() {
- return !this.queryVariables.iid;
+ return !this.workItemIid;
},
result() {
if (this.isEditing) {
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index ef99001c0e8..56802f76bba 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -140,7 +140,10 @@ export default {
workItem: {
query: workItemByIidQuery,
variables() {
- return this.queryVariables;
+ return {
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
+ };
},
skip() {
return !this.workItemIid;
@@ -314,12 +317,6 @@ export default {
workItemNotes() {
return this.isWidgetPresent(WIDGET_TYPE_NOTES);
},
- queryVariables() {
- return {
- fullPath: this.fullPath,
- iid: this.workItemIid,
- };
- },
children() {
return this.workItem ? findHierarchyWidgetChildren(this.workItem) : [];
},
@@ -398,10 +395,12 @@ export default {
this.toggleChildFromCache(child, child.id, client);
},
toggleChildFromCache(workItem, childId, store) {
- const sourceData = store.readQuery({
+ const query = {
query: workItemByIidQuery,
- variables: this.queryVariables,
- });
+ variables: { fullPath: this.fullPath, iid: this.workItemIid },
+ };
+
+ const sourceData = store.readQuery(query);
const newData = produce(sourceData, (draftState) => {
const { widgets } = draftState.workspace.workItems.nodes[0];
@@ -416,11 +415,7 @@ export default {
}
});
- store.writeQuery({
- query: workItemByIidQuery,
- variables: this.queryVariables,
- data: newData,
- });
+ store.writeQuery({ ...query, data: newData });
},
async updateWorkItem(workItem, childId, parentId) {
return this.$apollo.mutate({
@@ -603,7 +598,7 @@ export default {
:can-update="canUpdate"
@error="updateError = $event"
/>
- <work-item-created-updated :work-item-iid="workItemIid" :full-path="fullPath" />
+ <work-item-created-updated :work-item-iid="workItemIid" />
<work-item-state
:work-item="workItem"
:work-item-parent-id="workItemParentId"
@@ -618,15 +613,13 @@ export default {
:allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
:work-item-type="workItemType"
:can-invite-members="workItemAssignees.canInviteMembers"
- :full-path="fullPath"
@error="updateError = $event"
/>
<work-item-labels
v-if="workItemLabels"
- :work-item-id="workItem.id"
:can-update="canUpdate"
- :full-path="fullPath"
- :query-variables="queryVariables"
+ :work-item-id="workItem.id"
+ :work-item-iid="workItem.iid"
@error="updateError = $event"
/>
<work-item-due-date
@@ -644,7 +637,6 @@ export default {
:work-item-milestone="workItemMilestone.milestone"
:work-item-type="workItemType"
:can-update="canUpdate"
- :full-path="fullPath"
@error="updateError = $event"
/>
<work-item-weight
@@ -653,8 +645,8 @@ export default {
:can-update="canUpdate"
:weight="workItemWeight.weight"
:work-item-id="workItem.id"
+ :work-item-iid="workItem.iid"
:work-item-type="workItemType"
- :query-variables="queryVariables"
@error="updateError = $event"
/>
<work-item-progress
@@ -664,7 +656,6 @@ export default {
:progress="workItemProgress.progress"
:work-item-id="workItem.id"
:work-item-type="workItemType"
- :query-variables="queryVariables"
@error="updateError = $event"
/>
<work-item-iteration
@@ -673,9 +664,8 @@ export default {
:iteration="workItemIteration.iteration"
:can-update="canUpdate"
:work-item-id="workItem.id"
+ :work-item-iid="workItem.iid"
:work-item-type="workItemType"
- :query-variables="queryVariables"
- :full-path="fullPath"
@error="updateError = $event"
/>
<work-item-health-status
@@ -684,16 +674,14 @@ export default {
:health-status="workItemHealthStatus.healthStatus"
:can-update="canUpdate"
:work-item-id="workItem.id"
+ :work-item-iid="workItem.iid"
:work-item-type="workItemType"
- :query-variables="queryVariables"
- :full-path="fullPath"
@error="updateError = $event"
/>
<work-item-description
v-if="hasDescriptionWidget"
:work-item-id="workItem.id"
- :full-path="fullPath"
- :query-variables="queryVariables"
+ :work-item-iid="workItem.iid"
class="gl-pt-5"
@error="updateError = $event"
/>
@@ -705,7 +693,6 @@ export default {
:work-item-iid="workItemIid"
:children="children"
:can-update="canUpdate"
- :project-path="fullPath"
:confidential="workItem.confidential"
@addWorkItemChild="addChild"
@removeChild="removeChild"
@@ -715,8 +702,6 @@ export default {
v-if="workItemNotes"
:work-item-id="workItem.id"
:work-item-iid="workItem.iid"
- :query-variables="queryVariables"
- :full-path="fullPath"
:work-item-type="workItemType"
:is-modal="isModal"
:assignees="workItemAssignees && workItemAssignees.assignees.nodes"
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index 574ac5f0f5d..015c86ba043 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -43,21 +43,18 @@ export default {
LabelItem,
},
mixins: [Tracking.mixin()],
+ inject: ['fullPath'],
props: {
workItemId: {
type: String,
required: true,
},
- canUpdate: {
- type: Boolean,
- required: true,
- },
- fullPath: {
+ workItemIid: {
type: String,
required: true,
},
- queryVariables: {
- type: Object,
+ canUpdate: {
+ type: Boolean,
required: true,
},
},
@@ -76,13 +73,16 @@ export default {
workItem: {
query: workItemByIidQuery,
variables() {
- return this.queryVariables;
+ return {
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
+ };
},
update(data) {
return data.workspace.workItems.nodes[0];
},
skip() {
- return !this.queryVariables.iid;
+ return !this.workItemIid;
},
error() {
this.$emit('error', i18n.fetchError);
diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js
index 45dd4c00683..636c9357170 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/index.js
+++ b/app/assets/javascripts/work_items/components/work_item_links/index.js
@@ -9,11 +9,11 @@ export default function initWorkItemLinks() {
const workItemLinksRoot = document.querySelector('.js-work-item-links-root');
if (!workItemLinksRoot) {
- return;
+ return null;
}
const {
- projectPath,
+ fullPath,
wiHasIssueWeightsFeature,
wiHasIterationsFeature,
wiHasIssuableHealthStatusFeature,
@@ -22,8 +22,7 @@ export default function initWorkItemLinks() {
wiReportAbusePath,
} = workItemLinksRoot.dataset;
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el: workItemLinksRoot,
name: 'WorkItemLinksRoot',
apolloProvider,
@@ -31,8 +30,7 @@ export default function initWorkItemLinks() {
WorkItemLinks,
},
provide: {
- projectPath,
- fullPath: projectPath,
+ fullPath,
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
hasIterationsFeature: wiHasIterationsFeature,
hasIssuableHealthStatusFeature: wiHasIssuableHealthStatusFeature,
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
index 2811ebc4bb0..098917f2b56 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
@@ -19,6 +19,7 @@ export default {
WorkItemLinkChild,
},
mixins: [glFeatureFlagsMixin()],
+ inject: ['fullPath'],
props: {
workItemType: {
type: String,
@@ -43,10 +44,6 @@ export default {
required: false,
default: false,
},
- projectPath: {
- type: String,
- required: true,
- },
fetchByIid: {
type: Boolean,
required: false,
@@ -86,7 +83,7 @@ export default {
queryVariables() {
return this.fetchByIid
? {
- fullPath: this.projectPath,
+ fullPath: this.fullPath,
iid: this.workItemIid,
}
: {
@@ -98,7 +95,7 @@ export default {
addWorkItemQuery({ id, iid }) {
const variables = this.fetchByIid
? {
- fullPath: this.projectPath,
+ fullPath: this.fullPath,
iid,
}
: {
@@ -232,7 +229,6 @@ export default {
<work-item-link-child
v-for="child in children"
:key="child.id"
- :project-path="projectPath"
:can-update="canUpdate"
:issuable-gid="workItemId"
:child-item="child"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index 35430261e29..8152412a5c1 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -38,11 +38,8 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ inject: ['fullPath'],
props: {
- projectPath: {
- type: String,
- required: true,
- },
canUpdate: {
type: Boolean,
required: true,
@@ -121,9 +118,7 @@ export default {
return this.isItemOpen ? __('Created') : __('Closed');
},
childPath() {
- return `${gon?.relative_url_root || ''}/${this.projectPath}/-/work_items/${
- this.childItem.iid
- }`;
+ return `${gon?.relative_url_root || ''}/${this.fullPath}/-/work_items/${this.childItem.iid}`;
},
chevronType() {
return this.isExpanded ? 'chevron-down' : 'chevron-right';
@@ -353,7 +348,6 @@ export default {
</div>
<work-item-tree-children
v-if="isExpanded"
- :project-path="projectPath"
:can-update="canUpdate"
:work-item-id="issuableGid"
:work-item-type="workItemType"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index 7feef26f211..46c109f2d57 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -36,7 +36,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['projectPath', 'reportAbusePath'],
+ inject: ['fullPath', 'reportAbusePath'],
props: {
issuableId: {
type: Number,
@@ -226,7 +226,7 @@ export default {
this.$apollo.addSmartQuery('prefetchedWorkItem', {
query: workItemByIidQuery,
variables: {
- fullPath: this.projectPath,
+ fullPath: this.fullPath,
iid,
},
update(data) {
@@ -335,7 +335,6 @@ export default {
/>
<work-item-children-wrapper
:children="children"
- :project-path="projectPath"
:can-update="canUpdate"
:work-item-id="issuableGid"
@removeChild="removeChild"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index af475496075..51c83784d06 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -41,7 +41,7 @@ export default {
GlFormCheckbox,
GlTooltip,
},
- inject: ['projectPath', 'hasIterationsFeature'],
+ inject: ['fullPath', 'hasIterationsFeature'],
props: {
issuableGid: {
type: String,
@@ -88,7 +88,7 @@ export default {
query: projectWorkItemTypesQuery,
variables() {
return {
- fullPath: this.projectPath,
+ fullPath: this.fullPath,
};
},
update(data) {
@@ -99,7 +99,7 @@ export default {
query: projectWorkItemsQuery,
variables() {
return {
- projectPath: this.projectPath,
+ fullPath: this.fullPath,
searchTerm: this.search?.title || this.search,
types: [this.childrenType],
in: this.search ? 'TITLE' : undefined,
@@ -131,7 +131,7 @@ export default {
workItemInput() {
let workItemInput = {
title: this.search?.title || this.search,
- projectPath: this.projectPath,
+ projectPath: this.fullPath,
workItemTypeId: this.childWorkItemType,
hierarchyWidget: {
parentId: this.issuableGid,
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
index 4dcc4d51957..cbca78e4b14 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
@@ -23,6 +23,7 @@ export default {
WorkItemLinksForm,
WorkItemChildrenWrapper,
},
+ inject: ['fullPath'],
props: {
workItemType: {
type: String,
@@ -57,10 +58,6 @@ export default {
required: false,
default: false,
},
- projectPath: {
- type: String,
- required: true,
- },
},
data() {
return {
@@ -106,7 +103,7 @@ export default {
this.$apollo.addSmartQuery('prefetchedWorkItem', {
query: workItemByIidQuery,
variables: {
- fullPath: this.projectPath,
+ fullPath: this.fullPath,
iid,
},
update(data) {
@@ -164,7 +161,6 @@ export default {
/>
<work-item-children-wrapper
:children="children"
- :project-path="projectPath"
:can-update="canUpdate"
:work-item-id="workItemId"
:work-item-iid="workItemIid"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
index ba5c0794395..121c987da71 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
@@ -3,6 +3,7 @@ export default {
components: {
WorkItemLinkChild: () => import('./work_item_link_child.vue'),
},
+ inject: ['fullPath'],
props: {
workItemType: {
type: String,
@@ -22,10 +23,6 @@ export default {
required: false,
default: false,
},
- projectPath: {
- type: String,
- required: true,
- },
},
};
</script>
@@ -35,7 +32,6 @@ export default {
<work-item-link-child
v-for="child in children"
:key="child.id"
- :project-path="projectPath"
:can-update="canUpdate"
:issuable-gid="workItemId"
:child-item="child"
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
index 06051eee3a3..693397686d0 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -46,6 +46,7 @@ export default {
GlDropdownText,
},
mixins: [Tracking.mixin()],
+ inject: ['fullPath'],
props: {
workItemId: {
type: String,
@@ -66,10 +67,6 @@ export default {
required: false,
default: false,
},
- fullPath: {
- type: String,
- required: true,
- },
},
data() {
return {
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index 42afde76a00..092b90a5731 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -45,6 +45,7 @@ export default {
WorkItemNotesActivityHeader,
WorkItemHistoryOnlyFilterNote,
},
+ inject: ['fullPath'],
props: {
workItemId: {
type: String,
@@ -54,14 +55,6 @@ export default {
type: String,
required: true,
},
- queryVariables: {
- type: Object,
- required: true,
- },
- fullPath: {
- type: String,
- required: true,
- },
workItemType: {
type: String,
required: true,
@@ -122,9 +115,9 @@ export default {
},
workItemCommentFormProps() {
return {
- queryVariables: this.queryVariables,
fullPath: this.fullPath,
workItemId: this.workItemId,
+ workItemIid: this.workItemIid,
workItemType: this.workItemType,
sortOrder: this.sortOrder,
isNewDiscussion: true,
@@ -169,7 +162,8 @@ export default {
},
variables() {
return {
- ...this.queryVariables,
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
after: this.after,
pageSize: DEFAULT_PAGE_SIZE_NOTES,
};
@@ -179,7 +173,7 @@ export default {
return widgets?.find((widget) => widget.type === 'NOTES')?.discussions || [];
},
skip() {
- return !this.queryVariables.iid;
+ return !this.workItemIid;
},
error() {
this.$emit('error', i18n.fetchError);
@@ -264,7 +258,8 @@ export default {
await this.$apollo.queries.workItemNotes
.fetchMore({
variables: {
- ...this.queryVariables,
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
after: this.pageInfo?.endCursor,
},
})
@@ -359,9 +354,8 @@ export default {
<work-item-discussion
:key="getDiscussionKey(discussion)"
:discussion="discussion.notes.nodes"
- :query-variables="queryVariables"
- :full-path="fullPath"
:work-item-id="workItemId"
+ :work-item-iid="workItemIid"
:work-item-type="workItemType"
:is-modal="isModal"
:autocomplete-data-sources="autocompleteDataSources"
diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
index fce10f6f2a6..7d63af448d4 100644
--- a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
@@ -1,10 +1,10 @@
query projectWorkItems(
$searchTerm: String
- $projectPath: ID!
+ $fullPath: ID!
$types: [IssueType!]
$in: [IssuableSearchableField!]
) {
- workspace: project(fullPath: $projectPath) {
+ workspace: project(fullPath: $fullPath) {
id
workItems(search: $searchTerm, types: $types, in: $in) {
nodes {
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index eb37336bbf9..70bda7d3783 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -29,7 +29,6 @@ export const initWorkItemsRoot = () => {
apolloProvider,
provide: {
fullPath,
- projectPath: fullPath,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasOkrsFeature: parseBoolean(hasOkrsFeature),
issuesListPath,
diff --git a/app/helpers/projects/ml/experiments_helper.rb b/app/helpers/projects/ml/experiments_helper.rb
index bcf07df0e72..6e5e13ef0b6 100644
--- a/app/helpers/projects/ml/experiments_helper.rb
+++ b/app/helpers/projects/ml/experiments_helper.rb
@@ -40,6 +40,7 @@ module Projects
{
**candidate.params.to_h { |p| [p.name, p.value] },
**candidate.latest_metrics.to_h { |m| [m.name, number_with_precision(m.value, precision: 4)] },
+ ci_job: job_info(candidate),
artifact: link_to_artifact(candidate),
details: link_to_details(candidate),
name: candidate.name,
@@ -92,6 +93,17 @@ module Projects
project_ml_candidate_path(candidate.project, candidate.iid)
end
+ def job_info(candidate)
+ return unless candidate.from_ci?
+
+ build = candidate.ci_build
+
+ {
+ path: project_job_path(build.project, build),
+ name: build.name
+ }
+ end
+
def link_to_experiment(project, experiment)
project_ml_experiment_path(project, experiment.iid)
end
diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb
index 0265d609e19..0b6075fbeb8 100644
--- a/app/models/concerns/mentionable/reference_regexes.rb
+++ b/app/models/concerns/mentionable/reference_regexes.rb
@@ -20,7 +20,7 @@ module Mentionable
def self.default_pattern
strong_memoize(:default_pattern) do
issue_pattern = Issue.reference_pattern
- link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic, Vulnerability].map(&:link_reference_pattern).compact)
+ link_patterns = Regexp.union([Issue, WorkItem, Commit, MergeRequest, Epic, Vulnerability].map(&:link_reference_pattern).compact)
reference_pattern(link_patterns, issue_pattern)
end
end
diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb
index adf0c26bbc6..6f4728a1d98 100644
--- a/app/models/ml/candidate.rb
+++ b/app/models/ml/candidate.rb
@@ -29,7 +29,7 @@ module Ml
scope: :project,
init: AtomicInternalId.project_init(self, :internal_id)
- scope :including_relationships, -> { includes(:latest_metrics, :params, :user, :package, :project) }
+ scope :including_relationships, -> { includes(:latest_metrics, :params, :user, :package, :project, :ci_build) }
scope :by_name, ->(name) { where("ml_candidates.name LIKE ?", "%#{sanitize_sql_like(name)}%") } # rubocop:disable GitlabSecurity/SqlInjection
scope :order_by_metric, ->(metric, direction) do
@@ -69,6 +69,10 @@ module Ml
iid
end
+ def from_ci?
+ ci_build_id.present?
+ end
+
class << self
def with_project_id_and_eid(project_id, eid)
return unless project_id.present? && eid.present?
diff --git a/app/models/service_desk/custom_email_verification.rb b/app/models/service_desk/custom_email_verification.rb
index 69bd0f5e7f8..482a10447ed 100644
--- a/app/models/service_desk/custom_email_verification.rb
+++ b/app/models/service_desk/custom_email_verification.rb
@@ -77,6 +77,13 @@ module ServiceDesk
verification.error = error
verification.token = nil
end
+
+ # Supress warning:
+ # both enum and its state_machine have defined a different default for "state".
+ # State machine uses `nil` and the enum should use the same.
+ def owner_class_attribute_default
+ nil
+ end
end
# Needs to be below `state_machine` definition to suppress
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 1d96dd9faff..11b83528a79 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -32,6 +32,14 @@ class WorkItem < Issue
'issues.id'
end
+ # def reference_pattern
+ # # no-op: We currently only support link_reference_pattern parsing
+ # end
+
+ def link_reference_pattern
+ @link_reference_pattern ||= compose_link_reference_pattern('work_items', Gitlab::Regex.work_item)
+ end
+
def work_item_children_keyset_order
keyset_order = Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index af4c934fd72..40632e27fa7 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -2,9 +2,10 @@
= render Pajamas::AlertComponent.new(title: s_('ClusterIntegration|Did you know?'),
alert_options: { class: 'gcp-signup-offer',
- data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path }}) do |c|
+ data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path }},
+ close_button_options: { data: { track_action: 'click_dismiss', track_label: 'gcp_signup_offer_banner' }}) do |c|
= c.body do
= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
= c.actions do
- = render Pajamas::ButtonComponent.new(variant: :confirm, href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', button_options: { rel: 'noopener noreferrer' }) do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', button_options: { rel: 'noopener noreferrer', data: { track_action: 'click_button', track_label: 'gcp_signup_offer_banner' } }) do
= s_("ClusterIntegration|Apply for credit")
diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml
index e33e9509e3a..cfa0cf6d07b 100644
--- a/app/views/projects/branches/_commit.html.haml
+++ b/app/views/projects/branches/_commit.html.haml
@@ -6,4 +6,4 @@
%span.str-truncated
= link_to_markdown commit.title, project_commit_path(project, commit.id), class: "commit-row-message cgray"
&middot;
- #{time_ago_with_tooltip(commit.committed_date)}
+ %span.gl-text-secondary= time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/projects/issues/_work_item_links.html.haml b/app/views/projects/issues/_work_item_links.html.haml
index 911260308b4..981021c97e6 100644
--- a/app/views/projects/issues/_work_item_links.html.haml
+++ b/app/views/projects/issues/_work_item_links.html.haml
@@ -1,5 +1,5 @@
.js-work-item-links-root{ data: { issuable_id: @issue.id,
- project_path: @project.full_path,
+ full_path: @project.full_path,
wi: work_items_index_data(@project),
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
sign_in_path: new_session_path(:user, redirect_to_referer: 'yes') } }
diff --git a/app/views/projects/tags/_edit_release_button.html.haml b/app/views/projects/tags/_edit_release_button.html.haml
index 1c2626e5612..9a6c18df2ca 100644
--- a/app/views/projects/tags/_edit_release_button.html.haml
+++ b/app/views/projects/tags/_edit_release_button.html.haml
@@ -5,5 +5,5 @@
- if release
- release_btn_text = s_('TagsPage|Edit release')
- release_btn_path = edit_project_release_path(project, release)
-= link_to release_btn_path, class: css_classes, title: release_btn_text, data: { container: "body" } do
- = sprite_icon('pencil', css_class: 'gl-icon')
+= link_to release_btn_path, class: css_classes do
+ = release_btn_text
diff --git a/app/views/projects/tags/_release_link.html.haml b/app/views/projects/tags/_release_link.html.haml
index 6c79b13f438..9284204af77 100644
--- a/app/views/projects/tags/_release_link.html.haml
+++ b/app/views/projects/tags/_release_link.html.haml
@@ -1,5 +1,5 @@
- if can?(current_user, :read_release, release)
- .gl-text-secondary
- = sprite_icon("rocket", size: 12)
- = _("Release")
+ %span
+ = sprite_icon("rocket", size: 12, css_class: "gl-text-secondary")
+ = _("Release:")
= link_to release.name, project_release_path(project, release), class: "gl-text-blue-600!"
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index fcad8509a7d..cc49ff9e293 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -2,9 +2,9 @@
- release = @releases.find { |release| release.tag == tag.name }
- commit_status = @tag_pipeline_statuses[tag.name] unless @tag_pipeline_statuses.nil?
-%li.flex-row.js-tag-list{ class: "gl-white-space-normal! gl-align-items-flex-start!" }
+%li.gl-justify-content-space-between{ class: "gl-md-display-flex! gl-align-items-flex-start!", data: { testid: 'tag-row' } }
.row-main-content
- = sprite_icon('tag')
+ = sprite_icon('tag', css_class: "gl-text-secondary")
= link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name'
- if protected_tag?(@project, tag)
@@ -21,12 +21,13 @@
= render 'release_link', project: @project, release: release
- if tag.message.present?
- %pre.wrap
+ %pre.wrap.gl-mt-3.gl-max-w-80
= strip_signature(tag.message)
- .row-fixed-content.controls.flex-row
+ .row-fixed-content.flex-row
- if tag.has_signature?
- = render partial: 'projects/commit/signature', object: tag.signature
+ .gl-mr-3
+ = render partial: 'projects/commit/signature', object: tag.signature
- if commit_status
= render 'ci/status/icon', size: 24, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-5'
@@ -34,8 +35,11 @@
.gl-display-inline-flex.gl-vertical-align-middle.gl-mr-5
%svg.s24
- = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]
-
- if can?(current_user, :admin_tag, @project)
= render 'edit_release_button', tag: tag, project: @project, release: release, option_css_classes: 'gl-mr-3!'
+
+ .gl-mr-3
+ = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]
+
+ - if can?(current_user, :admin_tag, @project)
= render 'projects/buttons/remove_tag', project: @project, tag: tag
diff --git a/babel.config.js b/babel.config.js
index 6838df3ad84..ddb8b568bed 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -5,6 +5,7 @@ let presets = [
'@babel/preset-env',
{
useBuiltIns: 'usage',
+ bugfixes: true,
corejs: { version: coreJSVersion, proposals: true },
modules: false,
},
diff --git a/config/events/20230508093658_gcp_signup_offer_click_click.yml b/config/events/20230508093658_gcp_signup_offer_click_click.yml
new file mode 100644
index 00000000000..493a7733225
--- /dev/null
+++ b/config/events/20230508093658_gcp_signup_offer_click_click.yml
@@ -0,0 +1,21 @@
+---
+description: GCP offer banner Apply for credit is clicked
+category: gcp_signup_offer_click
+action: click
+label_description: 'gcp_signup_offer_banner'
+property_description:
+value_description:
+extra_properties:
+identifiers:
+product_section: ops
+product_stage: deploy
+product_group: group::environments
+milestone: '16.0'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119940
+distributions:
+ - ce
+ - ee
+tiers:
+ - free
+ - premium
+ - ultimate
diff --git a/config/events/20230508093912_gcp_signup_offer_dismiss_dismiss.yml b/config/events/20230508093912_gcp_signup_offer_dismiss_dismiss.yml
new file mode 100644
index 00000000000..f42cb7d8f2e
--- /dev/null
+++ b/config/events/20230508093912_gcp_signup_offer_dismiss_dismiss.yml
@@ -0,0 +1,21 @@
+---
+description: GCP offer banner is dismissed
+category: gcp_signup_offer_dismiss
+action: dismiss
+label_description: 'gcp_signup_offer_banner'
+property_description:
+value_description:
+extra_properties:
+identifiers:
+product_section: ops
+product_stage: deploy
+product_group: group::environments
+milestone: '16.0'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119940
+distributions:
+ - ce
+ - ee
+tiers:
+ - free
+ - premium
+ - ultimate
diff --git a/config/feature_flags/development/disable_follow_users.yml b/config/feature_flags/development/disable_follow_users.yml
index 9788ca520fd..ead3687f302 100644
--- a/config/feature_flags/development/disable_follow_users.yml
+++ b/config/feature_flags/development/disable_follow_users.yml
@@ -1,7 +1,7 @@
---
name: disable_follow_users
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116023
-rollout_issue_url:
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/408886
milestone: '16.0'
type: development
group: group::authentication and authorization
diff --git a/db/post_migrate/20230328030101_add_secureflag_training_provider.rb b/db/post_migrate/20230328030101_add_secureflag_training_provider.rb
new file mode 100644
index 00000000000..4b32570ea56
--- /dev/null
+++ b/db/post_migrate/20230328030101_add_secureflag_training_provider.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class AddSecureflagTrainingProvider < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ SECUREFLAG_DATA = {
+ name: 'SecureFlag',
+ description: "Get remediation advice with example code and recommended hands-on labs in a fully
+ interactive virtualised environment.",
+ url: "https://knowledge-base-api.secureflag.com/gitlab"
+ }
+
+ class TrainingProvider < MigrationRecord
+ self.table_name = 'security_training_providers'
+ end
+
+ def up
+ current_time = Time.current
+ timestamps = { created_at: current_time, updated_at: current_time }
+
+ TrainingProvider.reset_column_information
+ TrainingProvider.upsert(SECUREFLAG_DATA.merge(timestamps))
+ end
+
+ def down
+ TrainingProvider.reset_column_information
+ TrainingProvider.find_by(name: SECUREFLAG_DATA[:name])&.destroy
+ end
+end
diff --git a/db/post_migrate/20230508093910_create_package_manager_name_index.rb b/db/post_migrate/20230508093910_create_package_manager_name_index.rb
new file mode 100644
index 00000000000..e15f253a417
--- /dev/null
+++ b/db/post_migrate/20230508093910_create_package_manager_name_index.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class CreatePackageManagerNameIndex < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_on_sbom_sources_package_manager_name'
+
+ def up
+ return if index_exists_by_name?(:sbom_sources, INDEX_NAME)
+
+ disable_statement_timeout do
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY #{INDEX_NAME}
+ ON sbom_sources
+ USING BTREE ((source->'package_manager'->>'name'))
+ SQL
+ end
+ end
+
+ def down
+ remove_concurrent_index_by_name :sbom_sources, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230328030101 b/db/schema_migrations/20230328030101
new file mode 100644
index 00000000000..0b50a16a514
--- /dev/null
+++ b/db/schema_migrations/20230328030101
@@ -0,0 +1 @@
+eb05e37733efa95de5067d328a8e3dbe2fe696c95658bad5362893c04c8b89b6 \ No newline at end of file
diff --git a/db/schema_migrations/20230508093910 b/db/schema_migrations/20230508093910
new file mode 100644
index 00000000000..d9b056e68f9
--- /dev/null
+++ b/db/schema_migrations/20230508093910
@@ -0,0 +1 @@
+1e0b966332d5094050ea779ba6efefaa5c0c2a7d9f2ec05a1fa8a049bd6fcd84 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index c2e2ddd0e4d..cce1aac348a 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -31644,6 +31644,8 @@ CREATE INDEX index_on_projects_path ON projects USING btree (path);
CREATE INDEX index_on_routes_lower_path ON routes USING btree (lower((path)::text));
+CREATE INDEX index_on_sbom_sources_package_manager_name ON sbom_sources USING btree ((((source -> 'package_manager'::text) ->> 'name'::text)));
+
CREATE INDEX index_on_todos_user_project_target_and_state ON todos USING btree (user_id, project_id, target_type, target_id, id) WHERE ((state)::text = 'pending'::text);
CREATE INDEX index_on_users_lower_email ON users USING btree (lower((email)::text));
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index d37ee757c6b..d6187ea13ea 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -13184,7 +13184,7 @@ A software dependency used by a project.
| <a id="dependencyid"></a>`id` | [`GlobalID!`](#globalid) | ID of the dependency. |
| <a id="dependencylocation"></a>`location` | [`Location`](#location) | Information about where the dependency is located. |
| <a id="dependencyname"></a>`name` | [`String!`](#string) | Name of the dependency. |
-| <a id="dependencypackager"></a>`packager` | [`String`](#string) | Description of the tool used to manage the dependency. |
+| <a id="dependencypackager"></a>`packager` | [`PackageManager`](#packagemanager) | Description of the tool used to manage the dependency. |
| <a id="dependencyversion"></a>`version` | [`String`](#string) | Version of the dependency. |
### `DependencyProxyBlob`
@@ -15820,6 +15820,24 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
| <a id="groupvaluestreamanalyticsflowmetricsissuecountprojectids"></a>`projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. |
| <a id="groupvaluestreamanalyticsflowmetricsissuecountto"></a>`to` | [`Time!`](#time) | Before the date. |
+##### `GroupValueStreamAnalyticsFlowMetrics.issuesCompletedCount`
+
+Number of open issues closed (completed) in the given period. Maximum value is 10,001.
+
+Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
+| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
+| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountfrom"></a>`from` | [`Time!`](#time) | After the date. |
+| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountlabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue. |
+| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. |
+| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountprojectids"></a>`projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. |
+| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountto"></a>`to` | [`Time!`](#time) | Before the date. |
+
##### `GroupValueStreamAnalyticsFlowMetrics.leadTime`
Median time from when the issue was created to when it was closed.
@@ -19229,6 +19247,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="projectdependenciespackagemanagers"></a>`packageManagers` | [`[PackageManager!]`](#packagemanager) | Filter dependencies by package managers. |
| <a id="projectdependenciessort"></a>`sort` | [`DependencySort`](#dependencysort) | Sort dependencies by given criteria. |
##### `Project.deployment`
@@ -20460,6 +20479,23 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
| <a id="projectvaluestreamanalyticsflowmetricsissuecountmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. |
| <a id="projectvaluestreamanalyticsflowmetricsissuecountto"></a>`to` | [`Time!`](#time) | Before the date. |
+##### `ProjectValueStreamAnalyticsFlowMetrics.issuesCompletedCount`
+
+Number of open issues closed (completed) in the given period. Maximum value is 10,001.
+
+Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
+| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
+| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountfrom"></a>`from` | [`Time!`](#time) | After the date. |
+| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountlabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue. |
+| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. |
+| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountto"></a>`to` | [`Time!`](#time) | Before the date. |
+
##### `ProjectValueStreamAnalyticsFlowMetrics.leadTime`
Median time from when the issue was created to when it was closed.
@@ -24747,6 +24783,27 @@ Values for sorting group packages.
| <a id="packagegroupsortversion_asc"></a>`VERSION_ASC` | Ordered by version in ascending order. |
| <a id="packagegroupsortversion_desc"></a>`VERSION_DESC` | Ordered by version in descending order. |
+### `PackageManager`
+
+Values for package manager.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="packagemanagerbundler"></a>`BUNDLER` | Package manager: bundler. |
+| <a id="packagemanagercomposer"></a>`COMPOSER` | Package manager: composer. |
+| <a id="packagemanagerconan"></a>`CONAN` | Package manager: conan. |
+| <a id="packagemanagergo"></a>`GO` | Package manager: go. |
+| <a id="packagemanagergradle"></a>`GRADLE` | Package manager: gradle. |
+| <a id="packagemanagermaven"></a>`MAVEN` | Package manager: maven. |
+| <a id="packagemanagernpm"></a>`NPM` | Package manager: npm. |
+| <a id="packagemanagernuget"></a>`NUGET` | Package manager: nuget. |
+| <a id="packagemanagerpip"></a>`PIP` | Package manager: pip. |
+| <a id="packagemanagerpipenv"></a>`PIPENV` | Package manager: pipenv. |
+| <a id="packagemanagerpnpm"></a>`PNPM` | Package manager: pnpm. |
+| <a id="packagemanagersbt"></a>`SBT` | Package manager: sbt. |
+| <a id="packagemanagersetuptools"></a>`SETUPTOOLS` | Package manager: setuptools. |
+| <a id="packagemanageryarn"></a>`YARN` | Package manager: yarn. |
+
### `PackageSort`
Values for sorting package.
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index c9f31b36e3f..18d962451e4 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -151,6 +151,20 @@ change, you must update the `CODEOWNERS` file:
1. Add and commit all your changes and push your branch up to `origin`.
1. Create a merge request and assign it to a technical writing manager for review.
+When updating the `codeowners.rake` file:
+
+- To specify multiple writers for a single group, use a space between writer names:
+
+ ```plaintext
+ CodeOwnerRule.new('Group Name', '@writer1 @writer2'),
+ ```
+
+- For a group that does not have an assigned writer, include the group name in the file and comment out the line:
+
+ ```plaintext
+ # CodeOwnerRule.new('Group Name', ''),
+ ```
+
## Move, rename, or delete a page
See [redirects](redirects.md).
diff --git a/doc/development/service_ping/metrics_lifecycle.md b/doc/development/service_ping/metrics_lifecycle.md
index 3c51eefc4b4..318db6895fb 100644
--- a/doc/development/service_ping/metrics_lifecycle.md
+++ b/doc/development/service_ping/metrics_lifecycle.md
@@ -14,57 +14,21 @@ Follow the [Implement Service Ping](implement.md) guide.
## Change an existing metric
-See [this video tutorial](https://youtu.be/bYf3c01KCls) for help with the update of metric attributes.
-
-NOTE:
-The `key_path` attribute represents the location of the metric in Service Ping payload and must not be changed.
-
-Because we do not control when customers update their self-managed instances of GitLab,
-we **STRONGLY DISCOURAGE** changes to the logic used to calculate any metric.
-Any such changes lead to inconsistent reports from multiple GitLab instances.
-If there is a problem with an existing metric, it's best to deprecate the existing metric,
-and use it, side by side, with the desired new metric.
-
-If you do need to change a metric, please notify the Customer Success Ops team (`@csops-team`), Analytics Engineers (`@gitlab-data/analytics-engineers`), and Product Analysts (`@gitlab-data/product-analysts`) teams by `@` mentioning those groups in a comment on the MR.
-Many Service Ping metrics are relied upon for health score and XMAU reporting and
-unexpected changes to those metrics could break reporting.
-
-Example:
-Consider following change. Before GitLab 12.6, the `example_metric` was implemented as:
-
-```ruby
-{
- ...
- example_metric: distinct_count(Project, :creator_id)
-}
-```
-
-For GitLab 12.6, the metric was changed to filter out archived projects:
-
-```ruby
-{
- ...
- example_metric: distinct_count(Project.non_archived, :creator_id)
-}
-```
+WARNING:
+We want to **PREVENT** changes to the calculation logic or important attributes on any metric as this invalidates comparisons of the same metric across different versions of GitLab.
-In this scenario, all instances running up to GitLab 12.5 continue to report `example_metric`,
-including all archived projects, while all instances running GitLab 12.6 and higher filters
-out such projects. As Service Ping data is collected from all reporting instances, the
-resulting dataset includes mixed data, which distorts any following business analysis.
+If you change a metric, you have to consider that not all instances of GitLab are running on the newest version. Old instances will still report the old version of the metric.
+Additionally, a metric's reported numbers are primarily interesting compared to previously reported numbers.
+As a result, if you need to change one of the following parts of a metric, you need to add a new metric instead. It's your choice whether to keep the old metric alongside the new one or [remove it](#remove-a-metric).
-The correct approach is to add a new metric for GitLab 12.6 release with updated logic:
+- **calculation logic**: This means any changes that can produce a different value than the previous implementation
+- **YAML attributes**: The following attributes are directly used for analysis or calculation: `key_path`, `time_frame`, `value_type`, `data_source`.
-```ruby
-{
- ...
- example_metric_without_archived: distinct_count(Project.non_archived, :creator_id)
-}
-```
+If you change the `performance_indicator_type` attribute of a metric or think your case needs an exception from the outlined rules then please notify the Customer Success Ops team (`@csops-team`), Analytics Engineers (`@gitlab-data/analytics-engineers`), and Product Analysts (`@gitlab-data/product-analysts`) teams by `@` mentioning those groups in a comment on the merge request or issue.
-and update existing business analysis artefacts to use `example_metric_without_archived` instead of `example_metric`
+You can change any other attributes without impact to the calculation or analysis. See [this video tutorial](https://youtu.be/bYf3c01KCls) for help updating metric attributes.
-Currently, the [Metrics Dictionary](https://metrics.gitlab.com/) is built automatically once a day. When a change to a metric is made in a YAML file, you can see the change in the dictionary within 24 hours.
+Currently, the [Metrics Dictionary](https://metrics.gitlab.com/) is built automatically once a day. You can see the change in the dictionary within 24 hours when you change the metric's YAML file.
## Remove a metric
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index ec1ed05af93..104c633229a 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -669,19 +669,21 @@ In addition to this, links to some objects are also recognized and formatted. So
### Show the issue, merge request, or epic title in the reference
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15694) in GitLab 14.6.
+> - Support for issues, merge requests, and epics [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15694) in GitLab 14.6.
+> - Support for work items [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390854) in GitLab 16.0.
-To include the title in the rendered link of an issue, merge request, or epic, add a plus (`+`)
+To include the title in the rendered link of an issue, work item, merge request, or epic, add a plus (`+`)
at the end of the reference. For example, a reference like `#123+` is rendered as
`The issue title (#123)`.
URL references like `https://gitlab.com/gitlab-org/gitlab/-/issues/1234+` are also expanded.
-### Show the issue or merge request summary in the reference
+### Show the issue, work item or merge request summary in the reference
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/386937) in GitLab 15.10.
+> - Support for issues and merge requests [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/386937) in GitLab 15.10.
+> - Support for work items [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390854) in GitLab 16.0.
-To include an extended summary in the rendered link of an issue or merge request, add a `+s`
+To include an extended summary in the rendered link of an issue, work item, or merge request, add a `+s`
at the end of the reference. Summary includes information about **assignees**, **milestone**
and **health status** of referenced item.
diff --git a/lib/banzai/filter/issuable_reference_expansion_filter.rb b/lib/banzai/filter/issuable_reference_expansion_filter.rb
index 8fe1c90b314..ec7778a3630 100644
--- a/lib/banzai/filter/issuable_reference_expansion_filter.rb
+++ b/lib/banzai/filter/issuable_reference_expansion_filter.rb
@@ -90,7 +90,7 @@ module Banzai
end
def moved_issue?(issuable)
- issuable.instance_of?(Issue) && issuable.moved?
+ issuable.is_a?(Issue) && issuable.moved?
end
def should_expand?(node, issuable)
diff --git a/lib/banzai/filter/references/work_item_reference_filter.rb b/lib/banzai/filter/references/work_item_reference_filter.rb
new file mode 100644
index 00000000000..ed62b9a1be1
--- /dev/null
+++ b/lib/banzai/filter/references/work_item_reference_filter.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces work item references with links. References to
+ # work items that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ class WorkItemReferenceFilter < IssueReferenceFilter
+ self.reference_type = :work_item
+ self.object_class = WorkItem
+
+ def parent_records(parent, ids)
+ parent.work_items.where(iid: ids.to_a)
+ end
+
+ private
+
+ def additional_object_attributes(work_item)
+ { work_item_type: work_item.work_item_type.base_type }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb
index 34b6ca99e32..6428f71eb8f 100644
--- a/lib/banzai/issuable_extractor.rb
+++ b/lib/banzai/issuable_extractor.rb
@@ -12,6 +12,7 @@ module Banzai
attr_reader :context
ISSUE_REFERENCE_TYPE = '@data-reference-type="issue"'
+ WORK_ITEM_REFERENCE_TYPE = '@data-reference-type="work_item"'
MERGE_REQUEST_REFERENCE_TYPE = '@data-reference-type="merge_request"'
# context - An instance of Banzai::RenderContext.
@@ -41,6 +42,7 @@ module Banzai
def parsers
[
Banzai::ReferenceParser::IssueParser.new(context),
+ Banzai::ReferenceParser::WorkItemParser.new(context),
Banzai::ReferenceParser::MergeRequestParser.new(context)
]
end
@@ -53,7 +55,7 @@ module Banzai
end
def reference_types
- [ISSUE_REFERENCE_TYPE, MERGE_REQUEST_REFERENCE_TYPE]
+ [ISSUE_REFERENCE_TYPE, WORK_ITEM_REFERENCE_TYPE, MERGE_REQUEST_REFERENCE_TYPE]
end
end
end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 1fd565999f5..53f938c044f 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -58,6 +58,7 @@ module Banzai
Filter::References::ProjectReferenceFilter,
Filter::References::DesignReferenceFilter,
Filter::References::IssueReferenceFilter,
+ Filter::References::WorkItemReferenceFilter,
Filter::References::ExternalIssueReferenceFilter,
Filter::References::MergeRequestReferenceFilter,
Filter::References::SnippetReferenceFilter,
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index 1833d8239d6..d0e74044bba 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -57,7 +57,17 @@ module Banzai
end
def records_for_nodes(nodes)
- node_includes = [
+ @issues_for_nodes ||= grouped_objects_for_nodes(
+ nodes,
+ Issue.all.includes(node_includes),
+ self.class.data_attribute
+ )
+ end
+
+ private
+
+ def node_includes
+ includes = [
:work_item_type,
:namespace,
:author,
@@ -69,13 +79,9 @@ module Banzai
project: [:namespace, :project_feature, :route]
}
]
- node_includes << :milestone if context.options[:extended_preload]
+ includes << :milestone if context.options[:extended_preload]
- @issues_for_nodes ||= grouped_objects_for_nodes(
- nodes,
- Issue.all.includes(node_includes),
- self.class.data_attribute
- )
+ includes
end
end
end
diff --git a/lib/banzai/reference_parser/work_item_parser.rb b/lib/banzai/reference_parser/work_item_parser.rb
new file mode 100644
index 00000000000..1ce0b067687
--- /dev/null
+++ b/lib/banzai/reference_parser/work_item_parser.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Banzai
+ module ReferenceParser
+ class WorkItemParser < IssueParser
+ self.reference_type = :work_item
+
+ def records_for_nodes(nodes)
+ @work_items_for_nodes ||= grouped_objects_for_nodes(
+ nodes,
+ WorkItem.all.includes(node_includes),
+ self.class.data_attribute
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/external/file/project.rb b/lib/gitlab/ci/config/external/file/project.rb
index 16c5375b8bb..16a6bc8a692 100644
--- a/lib/gitlab/ci/config/external/file/project.rb
+++ b/lib/gitlab/ci/config/external/file/project.rb
@@ -89,7 +89,9 @@ module Gitlab
return if project.nil?
- # with `itself`, we are force-loading the project
+ # We are force-loading the project with the `itself` method
+ # because the `project` variable can be a `BatchLoader` object and we should not
+ # pass a `BatchLoader` object in the `for` method to prevent unwanted behaviors.
BatchLoader.for(project.itself)
.batch(key: context.user) do |projects, loader, args|
projects.uniq.each do |project|
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 0f9e7daf4b8..63242d60c85 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -104,8 +104,14 @@ module Gitlab
end
def validate_duplicate_needs!(name, needs)
- unless needs.uniq == needs
- error!("#{name} has duplicate entries in the needs section.")
+ duplicated_needs =
+ needs
+ .group_by { |need| need[:name] }
+ .select { |_, items| items.count > 1 }
+ .keys
+
+ unless duplicated_needs.empty?
+ error!("#{name} has the following needs duplicated: #{duplicated_needs.join(', ')}.")
end
end
diff --git a/lib/gitlab/database_importers/security/training_providers/importer.rb b/lib/gitlab/database_importers/security/training_providers/importer.rb
index aa6a9f29c6d..87bef6400fa 100644
--- a/lib/gitlab/database_importers/security/training_providers/importer.rb
+++ b/lib/gitlab/database_importers/security/training_providers/importer.rb
@@ -20,6 +20,13 @@ module Gitlab
url: "https://integration-api.securecodewarrior.com/api/v1/trial"
}.freeze
+ SECUREFLAG_DATA = {
+ name: 'SecureFlag',
+ description: "Get remediation advice with example code and recommended hands-on labs in a fully
+ interactive virtualised environment.",
+ url: "https://knowledge-base-api.secureflag.com/gitlab"
+ }.freeze
+
module Security
class TrainingProvider < ApplicationRecord
self.table_name = 'security_training_providers'
@@ -31,7 +38,7 @@ module Gitlab
timestamps = { created_at: current_time, updated_at: current_time }
Security::TrainingProvider.upsert_all(
- [KONTRA_DATA.merge(timestamps), SCW_DATA.merge(timestamps)],
+ [KONTRA_DATA.merge(timestamps), SCW_DATA.merge(timestamps), SECUREFLAG_DATA.merge(timestamps)],
unique_by: :index_security_training_providers_on_unique_name
)
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index af11bcf06b0..ef8cb38029a 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -68,6 +68,8 @@ module Gitlab
push_frontend_feature_flag(:vscode_web_ide, current_user)
push_frontend_feature_flag(:super_sidebar_peek, current_user)
push_frontend_feature_flag(:unbatch_graphql_queries)
+ # To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248
+ push_frontend_feature_flag(:remove_monitor_metrics)
end
# Exposes the state of a feature flag to the frontend code.
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 3640edbaa26..eb99805e2e8 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -594,6 +594,10 @@ module Gitlab
@issue ||= /(?<issue>\d+)(?<format>\+s{,1})?(?=\W|\z)/
end
+ def work_item
+ @work_item ||= /(?<work_item>\d+)(?<format>\+s{,1})?(?=\W|\z)/
+ end
+
def merge_request
@merge_request ||= /(?<merge_request>\d+)(?<format>\+s{,1})?/
end
diff --git a/lib/gitlab/saas.rb b/lib/gitlab/saas.rb
index 16a7a697e6a..722475ce61d 100644
--- a/lib/gitlab/saas.rb
+++ b/lib/gitlab/saas.rb
@@ -49,6 +49,10 @@ module Gitlab
"https://about.gitlab.com/pricing#faq"
end
+ def self.about_feature_comparison_url
+ "https://about.gitlab.com/pricing/gitlab-com/feature-comparison"
+ end
+
def self.doc_url
'https://docs.gitlab.com'
end
diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake
index 4d43dd2dd85..b4b34581f43 100644
--- a/lib/tasks/gitlab/tw/codeowners.rake
+++ b/lib/tasks/gitlab/tw/codeowners.rake
@@ -39,7 +39,7 @@ namespace :tw do
CodeOwnerRule.new('Development', '@sselhorn'),
CodeOwnerRule.new('Distribution', '@axil'),
CodeOwnerRule.new('Distribution (Charts)', '@axil'),
- CodeOwnerRule.new('Distribution (Omnibus)', '@axil'),
+ CodeOwnerRule.new('Distribution (Omnibus)', '@eread'),
CodeOwnerRule.new('Documentation Guidelines', '@sselhorn'),
CodeOwnerRule.new('Dynamic Analysis', '@rdickenson'),
CodeOwnerRule.new('IDE', '@ashrafkhamis'),
@@ -50,9 +50,8 @@ namespace :tw do
CodeOwnerRule.new('Gitaly', '@eread'),
CodeOwnerRule.new('GitLab Dedicated', '@drcatherinepope'),
CodeOwnerRule.new('Global Search', '@ashrafkhamis'),
- CodeOwnerRule.new('Import', '@eread'),
+ CodeOwnerRule.new('Import and Integrate', '@eread @ashrafkhamis'),
CodeOwnerRule.new('Infrastructure', '@sselhorn'),
- CodeOwnerRule.new('Integrations', '@ashrafkhamis'),
# CodeOwnerRule.new('Knowledge', ''),
# CodeOwnerRule.new('MLOps', '')
CodeOwnerRule.new('Observability', '@drcatherinepope'),
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 118c9e08558..70764610ff7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1319,6 +1319,12 @@ msgstr ""
msgid "'%{value}' days of inactivity must be greater than or equal to 90"
msgstr ""
+msgid "'projects' is not yet supported"
+msgstr ""
+
+msgid "'starterProjects' is not yet supported"
+msgstr ""
+
msgid "(%d closed)"
msgid_plural "(%d closed)"
msgstr[0] ""
@@ -5073,6 +5079,9 @@ msgstr ""
msgid "Analytics|Browser Family"
msgstr ""
+msgid "Analytics|Cancel"
+msgstr ""
+
msgid "Analytics|Choose a chart type on the right"
msgstr ""
@@ -5088,6 +5097,9 @@ msgstr ""
msgid "Analytics|Configure Dashboard Project"
msgstr ""
+msgid "Analytics|Create dashboard %{dashboardId}"
+msgstr ""
+
msgid "Analytics|Custom dashboards"
msgstr ""
@@ -5112,7 +5124,7 @@ msgstr ""
msgid "Analytics|Edit"
msgstr ""
-msgid "Analytics|Error while saving Dashboard!"
+msgid "Analytics|Error while saving dashboard"
msgstr ""
msgid "Analytics|Host"
@@ -5127,6 +5139,9 @@ msgstr ""
msgid "Analytics|New Analytics Visualization Title"
msgstr ""
+msgid "Analytics|New dashboard"
+msgstr ""
+
msgid "Analytics|No dashboard matches the specified URL path."
msgstr ""
@@ -9247,6 +9262,9 @@ msgstr ""
msgid "Checkout|Payment method"
msgstr ""
+msgid "Checkout|Pricing reflective of %{linkStart}limited-time offer%{linkEnd}."
+msgstr ""
+
msgid "Checkout|Purchase details"
msgstr ""
@@ -23674,7 +23692,7 @@ msgstr ""
msgid "Inherited:"
msgstr ""
-msgid "Inheriting from parent is not yet supported"
+msgid "Inheriting from 'parent' is not yet supported"
msgstr ""
msgid "Initial default branch name"
@@ -24770,6 +24788,9 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Issues Completed"
+msgstr ""
+
msgid "Issues Rate Limits"
msgstr ""
@@ -28753,6 +28774,9 @@ msgstr ""
msgid "MlExperimentTracking|Author"
msgstr ""
+msgid "MlExperimentTracking|CI Job"
+msgstr ""
+
msgid "MlExperimentTracking|Candidate removed"
msgstr ""
@@ -34009,9 +34033,6 @@ msgstr ""
msgid "ProductAnalytics|Back to dashboards"
msgstr ""
-msgid "ProductAnalytics|Cancel Edit"
-msgstr ""
-
msgid "ProductAnalytics|Click Events"
msgstr ""
@@ -37167,6 +37188,9 @@ msgstr ""
msgid "Release with tag \"%{tag}\" was not found"
msgstr ""
+msgid "Release:"
+msgstr ""
+
msgid "ReleaseAssetLinkType|Image"
msgstr ""
@@ -53300,7 +53324,9 @@ msgid "is too long (maximum is 1000 entries)"
msgstr ""
msgid "issue"
-msgstr ""
+msgid_plural "issues"
+msgstr[0] ""
+msgstr[1] ""
msgid "issues at risk"
msgstr ""
diff --git a/rubocop/cop/rspec/avoid_conditional_statements.rb b/rubocop/cop/rspec/avoid_conditional_statements.rb
new file mode 100644
index 00000000000..48c230a6a7a
--- /dev/null
+++ b/rubocop/cop/rspec/avoid_conditional_statements.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'rubocop-rspec'
+
+module RuboCop
+ module Cop
+ module RSpec
+ # This cop checks for the usage of conditional statements in specs.
+ #
+ # @example
+ #
+ # # bad
+ #
+ # page.has_css?('[data-testid="begin-commit-button"]') ? find('[data-testid="begin-commit-button"]').click : nil
+ #
+ # if page.has_css?('[data-testid="begin-commit-button"]')
+ # find('[data-testid="begin-commit-button"]').click
+ # end
+ #
+ # unless page.has_css?('[data-testid="begin-commit-button"]')
+ # find('[data-testid="begin-commit-button"]').click
+ # end
+ class AvoidConditionalStatements < RuboCop::Cop::Base
+ MESSAGE = "Don't use `%{conditional}` conditional statments in specs, it might create flakiness. " \
+ "See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109"
+
+ def on_if(node)
+ conditional = node.ternary? ? "#{node.condition.to_s.delete!("\n")} ? (ternary)" : node.keyword
+
+ add_offense(node, message: format(MESSAGE, conditional: conditional))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/factories/work_items.rb b/spec/factories/work_items.rb
index adf0c907adb..10764457d84 100644
--- a/spec/factories/work_items.rb
+++ b/spec/factories/work_items.rb
@@ -14,6 +14,19 @@ FactoryBot.define do
confidential { true }
end
+ trait :opened do
+ state_id { WorkItem.available_states[:opened] }
+ end
+
+ trait :locked do
+ discussion_locked { true }
+ end
+
+ trait :closed do
+ state_id { WorkItem.available_states[:closed] }
+ closed_at { Time.now }
+ end
+
trait :task do
issue_type { :task }
association :work_item_type, :default, :task
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index 8aecaab42c2..b6196fa6a1d 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -138,7 +138,7 @@ RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do
wait_for_requests
end
- it 'moves to end of list' do
+ it 'moves to end of list', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410100' do
expect(all('.board-card').first).to have_content(issue3.title)
page.within(find('.board:nth-child(2)')) do
@@ -151,7 +151,7 @@ RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do
expect(all('.board-card').last).to have_content(issue3.title)
end
- it 'moves to start of list' do
+ it 'moves to start of list', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410100' do
expect(all('.board-card').last).to have_content(issue1.title)
page.within(find('.board:nth-child(2)')) do
diff --git a/spec/features/markdown/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb
index 7a4c7529711..a31ad5a868e 100644
--- a/spec/features/markdown/markdown_spec.rb
+++ b/spec/features/markdown/markdown_spec.rb
@@ -250,6 +250,7 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures, feature_category: :team_p
aggregate_failures 'all reference filters' do
expect(doc).to reference_users
expect(doc).to reference_issues
+ expect(doc).to reference_work_items
expect(doc).to reference_merge_requests
expect(doc).to reference_snippets
expect(doc).to reference_commit_ranges
@@ -345,6 +346,7 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures, feature_category: :team_p
aggregate_failures 'all reference filters' do
expect(doc).to reference_users
expect(doc).to reference_issues
+ expect(doc).to reference_work_items
expect(doc).to reference_merge_requests
expect(doc).to reference_snippets
expect(doc).to reference_commit_ranges
diff --git a/spec/features/markdown/metrics_spec.rb b/spec/features/markdown/metrics_spec.rb
index 45b5d2f78e8..9f00bb99c0d 100644
--- a/spec/features/markdown/metrics_spec.rb
+++ b/spec/features/markdown/metrics_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_st
let(:metrics_url) { urls.metrics_project_environment_url(project, environment) }
before do
+ stub_feature_flags(remove_monitor_metrics: false)
clear_host_from_memoized_variables
stub_gitlab_domain
@@ -50,6 +51,20 @@ RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_st
.at_least(:once)
end
+ context 'with remove_monitor_metrics flag enabled' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'does not show embedded metrics' do
+ visit project_issue_path(project, issue)
+
+ expect(page).not_to have_css('div.prometheus-graph')
+ expect(page).not_to have_text('Memory Usage (Total)')
+ expect(page).not_to have_text('Core Usage (Total)')
+ end
+ end
+
context 'when dashboard params are in included the url' do
let(:metrics_url) { urls.metrics_project_environment_url(project, environment, **chart_params) }
diff --git a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
index 06276d2a933..e5352ad88ce 100644
--- a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe 'Merge request > User sees discussions navigation', :js, feature_
shared_examples 'a page with a thread navigation' do
context 'with active threads' do
- it 'navigates to the first thread' do
+ it 'navigates to the first thread', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410144' do
goto_next_thread
expect(page).to have_selector(first_discussion_selector, obscured: false)
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 3bb4e93dc64..038d58a20ab 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -370,7 +370,7 @@ RSpec.describe 'Environments page', :js, feature_category: :projects do
sha: project.commit.id)
end
- it 'does not show deployments' do
+ it 'does not show deployments', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/409990' do
visit_environments(project)
page.click_button _('Expand')
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index c0e41ba88aa..06d894fd5f1 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -302,7 +302,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do
click_button('manual build')
end
- it 'enqueues manual action job' do
+ it 'enqueues manual action job', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/409984' do
expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] .gl-dropdown-toggle:disabled')
end
end
@@ -369,7 +369,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do
wait_for_requests
end
- it 'enqueues the delayed job', :js do
+ it 'enqueues the delayed job', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410129' do
expect(delayed_job.reload).to be_pending
end
end
diff --git a/spec/features/tags/developer_deletes_tag_spec.rb b/spec/features/tags/developer_deletes_tag_spec.rb
index 76cf3aa691d..19feb5b21bc 100644
--- a/spec/features/tags/developer_deletes_tag_spec.rb
+++ b/spec/features/tags/developer_deletes_tag_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Developer deletes tag', :js, feature_category: :source_code_management do
+ include Spec::Support::Helpers::ModalHelpers
+
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: group) }
@@ -18,7 +20,7 @@ RSpec.describe 'Developer deletes tag', :js, feature_category: :source_code_mana
it 'deletes the tag' do
expect(page).to have_content 'v1.1.0'
- container = page.find('.content .flex-row', text: 'v1.1.0')
+ container = page.find('[data-testid="tag-row"]', text: 'v1.1.0')
delete_tag container
expect(page).not_to have_content 'v1.1.0'
@@ -28,7 +30,7 @@ RSpec.describe 'Developer deletes tag', :js, feature_category: :source_code_mana
it 'can not delete protected tags' do
expect(page).to have_content 'v1.1.1'
- container = page.find('.content .flex-row', text: 'v1.1.1')
+ container = page.find('[data-testid="tag-row"]', text: 'v1.1.1')
expect(container).to have_button('Only a project maintainer or owner can delete a protected tag',
disabled: true)
end
@@ -41,8 +43,7 @@ RSpec.describe 'Developer deletes tag', :js, feature_category: :source_code_mana
expect(page).to have_current_path(
project_tag_path(project, 'v1.0.0'), ignore_query: true)
- container = page.find('.nav-controls')
- delete_tag container
+ delete_tag
expect(page).to have_current_path(project_tags_path(project), ignore_query: true)
expect(page).not_to have_content 'v1.0.0'
@@ -58,17 +59,22 @@ RSpec.describe 'Developer deletes tag', :js, feature_category: :source_code_mana
end
it 'shows the error message' do
- container = page.find('.content .flex-row', text: 'v1.1.0')
+ container = page.find('[data-testid="tag-row"]', text: 'v1.1.0')
delete_tag container
expect(page).to have_content('Do not delete tags')
end
end
- def delete_tag(container)
- container.find('.js-delete-tag-button').click
+ def delete_tag(container = page.document)
+ within container do
+ click_button('Delete tag')
+ end
+
+ within_modal do
+ click_button('Yes, delete tag')
+ end
- page.within('.modal') { click_button('Yes, delete tag') }
wait_for_requests
end
end
diff --git a/spec/features/tags/maintainer_deletes_protected_tag_spec.rb b/spec/features/tags/maintainer_deletes_protected_tag_spec.rb
index ce518b962cd..67f6862502c 100644
--- a/spec/features/tags/maintainer_deletes_protected_tag_spec.rb
+++ b/spec/features/tags/maintainer_deletes_protected_tag_spec.rb
@@ -19,7 +19,10 @@ RSpec.describe 'Maintainer deletes protected tag', :js, feature_category: :sourc
it 'deletes the tag' do
expect(page).to have_content "#{tag_name} protected"
- page.find('.content .flex-row', text: tag_name).find('.js-delete-tag-button').click
+ page.within('[data-testid="tag-row"]', text: tag_name) do
+ click_button('Delete tag')
+ end
+
assert_modal_content(tag_name)
confirm_delete_tag(tag_name)
@@ -35,7 +38,7 @@ RSpec.describe 'Maintainer deletes protected tag', :js, feature_category: :sourc
it 'deletes the tag' do
expect(page).to have_current_path(project_tag_path(project, tag_name), ignore_query: true)
- page.find('.js-delete-tag-button').click
+ click_button('Delete tag')
assert_modal_content(tag_name)
confirm_delete_tag(tag_name)
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 26e5f110687..fa73cd53a66 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -194,6 +194,19 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Link to issue by reference: [Issue](<%= issue.to_reference %>)
- Link to issue by URL: [Issue](<%= urls.project_issue_url(issue.project, issue) %>)
+#### WorkItemReferenceFilter
+
+Note: work item references use `#`, which get built as an issue link.
+
+- Work item (counted as an issue reference): <%= work_item.to_reference %>
+- Work item in another project (counted as an issue reference): <%= xwork_item.to_reference(project) %>
+- Ignored in code: `<%= work_item.to_reference %>`
+- Ignored in links: [Link to <%= work_item.to_reference %>](#work_item-link)
+- Ignored when backslash escaped: \<%= work_item.to_reference %>
+- Work item by URL: <%= urls.project_work_item_url(work_item.project, work_item) %>
+- Link to work item by reference (counted as an issue reference): [Work item](<%= work_item.to_reference %>)
+- Link to work item by URL: [Work item](<%= urls.project_work_item_url(work_item.project, work_item) %>)
+
#### MergeRequestReferenceFilter
- Merge request: <%= merge_request.to_reference %>
diff --git a/spec/frontend/behaviors/markdown/render_gfm_spec.js b/spec/frontend/behaviors/markdown/render_gfm_spec.js
index 0bbb92282e5..220ad874b47 100644
--- a/spec/frontend/behaviors/markdown/render_gfm_spec.js
+++ b/spec/frontend/behaviors/markdown/render_gfm_spec.js
@@ -1,4 +1,7 @@
import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import renderMetrics from '~/behaviors/markdown/render_metrics';
+
+jest.mock('~/behaviors/markdown/render_metrics');
describe('renderGFM', () => {
it('handles a missing element', () => {
@@ -6,4 +9,27 @@ describe('renderGFM', () => {
renderGFM();
}).not.toThrow();
});
+
+ describe('remove_monitor_metrics flag', () => {
+ let metricsElement;
+
+ beforeEach(() => {
+ window.gon = { features: { removeMonitorMetrics: true } };
+ metricsElement = document.createElement('div');
+ metricsElement.setAttribute('class', '.js-render-metrics');
+ });
+
+ it('renders metrics when the flag is disabled', () => {
+ window.gon.features = { features: { removeMonitorMetrics: false } };
+ renderGFM(metricsElement);
+
+ expect(renderMetrics).toHaveBeenCalled();
+ });
+
+ it('does not render metrics when the flag is enabled', () => {
+ renderGFM(metricsElement);
+
+ expect(renderMetrics).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
index e4ca84853c3..f4e93e83ce8 100644
--- a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
@@ -61,44 +61,52 @@ describe('RunnerListEmptyState', () => {
expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
});
- describe('when create_runner_workflow_for_admin is enabled', () => {
- beforeEach(() => {
- createComponent({
- provide: {
- glFeatures: { createRunnerWorkflowForAdmin: true },
- },
+ describe.each([
+ { createRunnerWorkflowForAdmin: true },
+ { createRunnerWorkflowForNamespace: true },
+ ])('when %o', (glFeatures) => {
+ describe('when newRunnerPath is defined', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures,
+ },
+ });
});
- });
- it('shows a link to the new runner page', () => {
- expect(findLink().attributes('href')).toBe(newRunnerPath);
+ it('shows a link to the new runner page', () => {
+ expect(findLink().attributes('href')).toBe(newRunnerPath);
+ });
});
- });
- describe('when create_runner_workflow_for_admin is enabled and newRunnerPath not defined', () => {
- beforeEach(() => {
- createComponent({
- props: {
- newRunnerPath: null,
- },
- provide: {
- glFeatures: { createRunnerWorkflowForAdmin: true },
- },
+ describe('when newRunnerPath not defined', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ newRunnerPath: null,
+ },
+ provide: {
+ glFeatures,
+ },
+ });
});
- });
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
});
});
- describe('when create_runner_workflow_for_admin is disabled', () => {
+ describe.each([
+ { createRunnerWorkflowForAdmin: false },
+ { createRunnerWorkflowForNamespace: false },
+ ])('when %o', (glFeatures) => {
beforeEach(() => {
createComponent({
provide: {
- glFeatures: { createRunnerWorkflowForAdmin: false },
+ glFeatures,
},
});
});
diff --git a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
index 5b2ddeafe04..e750327294c 100644
--- a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
@@ -420,7 +420,13 @@ describe('GroupRunnersApp', () => {
});
it('shows an empty state', () => {
- expect(findRunnerListEmptyState().exists()).toBe(true);
+ expect(findRunnerListEmptyState().props()).toMatchObject({
+ isSearchFiltered: false,
+ newRunnerPath,
+ registrationToken: mockRegistrationToken,
+ svgPath: 'emptyStateSvgPath.svg',
+ filteredSvgPath: 'emptyStateFilteredSvgPath.svg',
+ });
});
});
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
index 5961babc5f8..2dd17888305 100644
--- a/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
@@ -43,6 +43,8 @@ describe('MlExperimentsShow', () => {
const hrefInRowAndColumn = (row, col) =>
findColumnInRow(row, col).findComponent(GlLink).attributes().href;
+ const linkTextInRowAndColumn = (row, col) =>
+ findColumnInRow(row, col).findComponent(GlLink).text();
describe('default inputs', () => {
beforeEach(() => {
@@ -242,6 +244,7 @@ describe('MlExperimentsShow', () => {
'Rmse',
'Auc',
'Mae',
+ 'CI Job',
'Artifacts',
];
@@ -264,6 +267,26 @@ describe('MlExperimentsShow', () => {
});
});
+ describe('CI Job column', () => {
+ const jobColumnIndex = -2;
+
+ it('has a link to the job', () => {
+ expect(hrefInRowAndColumn(firstCandidateIndex, jobColumnIndex)).toBe(
+ firstCandidate.ci_job.path,
+ );
+ });
+
+ it('shows the name of the job', () => {
+ expect(linkTextInRowAndColumn(firstCandidateIndex, jobColumnIndex)).toBe(
+ firstCandidate.ci_job.name,
+ );
+ });
+
+ it('shows empty state when there is no job', () => {
+ expect(findColumnInRow(secondCandidateIndex, jobColumnIndex).text()).toBe('-');
+ });
+ });
+
describe('User column', () => {
const userColumn = 2;
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js
index adfb3dbf773..4a606be8da6 100644
--- a/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js
@@ -15,6 +15,10 @@ export const MOCK_CANDIDATES = [
l1_ratio: 0.4,
details: 'link_to_candidate1',
artifact: 'link_to_artifact',
+ ci_job: {
+ path: 'link_to_job',
+ name: 'a job',
+ },
name: 'aCandidate',
created_at: '2023-01-05T14:07:02.975Z',
user: { username: 'root', path: '/root' },
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
index 3d4f01d0da1..df10d33e2f0 100644
--- a/spec/frontend/security_configuration/mock_data.js
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -9,10 +9,11 @@ import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
export const testProjectPath = 'foo/bar';
export const testProviderIds = [101, 102, 103];
-export const testProviderName = ['Kontra', 'Secure Code Warrior', 'Other Vendor'];
+export const testProviderName = ['Kontra', 'Secure Code Warrior', 'SecureFlag'];
export const testTrainingUrls = [
'https://www.vendornameone.com/url',
'https://www.vendornametwo.com/url',
+ 'https://www.vendornamethree.com/url',
];
const createSecurityTrainingProviders = ({ providerOverrides = {} }) => [
diff --git a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
index fbcf759d50f..739340f4936 100644
--- a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
@@ -36,8 +36,8 @@ describe('Work item add note', () => {
const createComponent = async ({
mutationHandler = mutationSuccessHandler,
canUpdate = true,
+ workItemIid = '1',
workItemResponse = workItemByIidResponseFactory({ canUpdate }),
- queryVariables = { iid: '1' },
signedIn = true,
isEditing = true,
workItemType = 'Task',
@@ -56,10 +56,12 @@ describe('Work item add note', () => {
const { id } = workItemQueryResponse.data.workItem;
wrapper = shallowMountExtended(WorkItemAddNote, {
apolloProvider,
+ provide: {
+ fullPath: 'test-project-path',
+ },
propsData: {
workItemId: id,
- fullPath: 'test-project-path',
- queryVariables,
+ workItemIid,
workItemType,
markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
autocompleteDataSources: {},
@@ -114,11 +116,7 @@ describe('Work item add note', () => {
});
it('emits `replied` event and hides form after successful mutation', async () => {
- await createComponent({
- isEditing: true,
- signedIn: true,
- queryVariables: { iid: '1' },
- });
+ await createComponent({ isEditing: true, signedIn: true });
findCommentForm().vm.$emit('submitForm', 'some text');
await waitForPromises();
@@ -226,8 +224,8 @@ describe('Work item add note', () => {
expect(workItemResponseHandler).toHaveBeenCalled();
});
- it('skips calling the work item query when missing queryVariables', async () => {
- await createComponent({ queryVariables: {}, isEditing: false });
+ it('skips calling the work item query when missing workItemIid', async () => {
+ await createComponent({ workItemIid: null, isEditing: false });
expect(workItemResponseHandler).not.toHaveBeenCalled();
});
diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
index 28c8ea13a23..fac5011b6af 100644
--- a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
@@ -28,16 +28,16 @@ describe('Work Item Discussion', () => {
const createComponent = ({
discussion = [mockWorkItemCommentNote],
workItemId = mockWorkItemId,
- queryVariables = { iid: '1' },
- fullPath = 'gitlab-org',
workItemType = 'Task',
} = {}) => {
wrapper = shallowMount(WorkItemDiscussion, {
+ provide: {
+ fullPath: 'gitlab-org',
+ },
propsData: {
discussion,
workItemId,
- queryVariables,
- fullPath,
+ workItemIid: '1',
workItemType,
markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
autocompleteDataSources: {},
diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js
index 956b52bb74c..f2cf5171cc1 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -69,19 +69,20 @@ describe('Work Item Note', () => {
workItemId = mockWorkItemId,
updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler,
assignees = mockAssignees,
- queryVariables = { iid: '1' },
} = {}) => {
wrapper = shallowMount(WorkItemNote, {
+ provide: {
+ fullPath: 'test-project-path',
+ },
propsData: {
workItemId,
+ workItemIid: '1',
note,
isFirstNote,
workItemType: 'Task',
markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
autocompleteDataSources: {},
assignees,
- queryVariables,
- fullPath: 'test-project-path',
},
apolloProvider: mockApollo([
[workItemByIidQuery, workItemResponseHandler],
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index 1e336a928a0..25b0b74c217 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -82,6 +82,9 @@ describe('WorkItemAssignees component', () => {
]);
wrapper = mountExtended(WorkItemAssignees, {
+ provide: {
+ fullPath: 'test-project-path',
+ },
propsData: {
assignees,
workItemId,
@@ -89,7 +92,6 @@ describe('WorkItemAssignees component', () => {
workItemType: TASK_TYPE_NAME,
canUpdate,
canInviteMembers,
- fullPath: 'test-project-path',
},
attachTo: document.body,
apolloProvider,
diff --git a/spec/frontend/work_items/components/work_item_created_updated_spec.js b/spec/frontend/work_items/components/work_item_created_updated_spec.js
index 2a5b2853b5e..68ede7d5bc0 100644
--- a/spec/frontend/work_items/components/work_item_created_updated_spec.js
+++ b/spec/frontend/work_items/components/work_item_created_updated_spec.js
@@ -29,7 +29,10 @@ describe('WorkItemCreatedUpdated component', () => {
wrapper = shallowMount(WorkItemCreatedUpdated, {
apolloProvider: createMockApollo([[workItemByIidQuery, successHandler]]),
- propsData: { workItemIid, fullPath: '/some/project' },
+ provide: {
+ fullPath: '/some/project',
+ },
+ propsData: { workItemIid },
stubs: {
GlAvatarLink,
GlSprintf,
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index 174dd520a61..b7877784a2d 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -58,7 +58,7 @@ describe('WorkItemDescription', () => {
canUpdate = true,
workItemResponse = workItemByIidResponseFactory({ canUpdate }),
isEditing = false,
- queryVariables = { iid: '1' },
+ workItemIid = '1',
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
@@ -71,10 +71,10 @@ describe('WorkItemDescription', () => {
]),
propsData: {
workItemId: id,
- fullPath: 'test-project-path',
- queryVariables,
+ workItemIid,
},
provide: {
+ fullPath: 'test-project-path',
glFeatures: {
workItemsMvc,
},
@@ -304,8 +304,8 @@ describe('WorkItemDescription', () => {
expect(workItemResponseHandler).toHaveBeenCalled();
});
- it('skips calling the work item query when missing queryVariables', async () => {
- await createComponent({ queryVariables: {} });
+ it('skips calling the work item query when missing workItemIid', async () => {
+ await createComponent({ workItemIid: null });
expect(workItemResponseHandler).not.toHaveBeenCalled();
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index e6f7793b43f..554c9a4f7b8 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -46,7 +46,7 @@ describe('WorkItemLabels component', () => {
workItemQueryHandler = workItemQuerySuccess,
searchQueryHandler = successSearchQueryHandler,
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
- queryVariables = { iid: '1' },
+ workItemIid = '1',
} = {}) => {
wrapper = mountExtended(WorkItemLabels, {
apolloProvider: createMockApollo([
@@ -55,11 +55,13 @@ describe('WorkItemLabels component', () => {
[updateWorkItemMutation, updateWorkItemMutationHandler],
[workItemLabelsSubscription, subscriptionHandler],
]),
+ provide: {
+ fullPath: 'test-project-path',
+ },
propsData: {
workItemId,
+ workItemIid,
canUpdate,
- fullPath: 'test-project-path',
- queryVariables,
},
attachTo: document.body,
});
@@ -263,8 +265,8 @@ describe('WorkItemLabels component', () => {
expect(workItemQuerySuccess).toHaveBeenCalled();
});
- it('skips calling the work item query when missing queryVariables', async () => {
- createComponent({ queryVariables: {} });
+ it('skips calling the work item query when missing workItemIid', async () => {
+ createComponent({ workItemIid: null });
await waitForPromises();
expect(workItemQuerySuccess).not.toHaveBeenCalled();
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
index ad95350fc67..08b9408c656 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
@@ -28,12 +28,14 @@ describe('WorkItemChildrenWrapper', () => {
} = {}) => {
wrapper = shallowMountExtended(WorkItemChildrenWrapper, {
apolloProvider: createMockApollo([[workItemByIidQuery, getWorkItemQueryHandler]]),
+ provide: {
+ fullPath: 'test/project',
+ },
propsData: {
workItemType,
workItemId: 'gid://gitlab/WorkItem/515',
confidential,
children,
- projectPath: 'test/project',
fetchByIid: true,
},
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
index adbb514d61a..bc429bfb037 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -52,7 +52,6 @@ describe('WorkItemLinkChild', () => {
Vue.use(VueApollo);
const createComponent = ({
- projectPath = 'gitlab-org/gitlab-test',
canUpdate = true,
issuableGid = WORK_ITEM_ID,
childItem = workItemTask,
@@ -71,8 +70,10 @@ describe('WorkItemLinkChild', () => {
[getWorkItemTreeQuery, getWorkItemTreeQueryHandler],
[updateWorkItemMutation, mutationChangeParentHandler],
]),
+ provide: {
+ fullPath: 'gitlab-org/gitlab-test',
+ },
propsData: {
- projectPath,
canUpdate,
issuableGid,
childItem,
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
index 6100bbea4a1..5f7f56d7063 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
@@ -61,7 +61,7 @@ describe('WorkItemLinksForm', () => {
formType,
},
provide: {
- projectPath: 'project/path',
+ fullPath: 'project/path',
hasIterationsFeature,
},
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index 0d726b0635a..cc3d2394231 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -76,7 +76,7 @@ describe('WorkItemLinks', () => {
};
},
provide: {
- projectPath: 'project/path',
+ fullPath: 'project/path',
hasIterationsFeature,
reportAbusePath: '/report/abuse/path',
},
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
index 710ed85bd97..06716584879 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
@@ -29,13 +29,15 @@ describe('WorkItemTree', () => {
canUpdate = true,
} = {}) => {
wrapper = shallowMountExtended(WorkItemTree, {
+ provide: {
+ fullPath: 'test/project',
+ },
propsData: {
workItemType,
parentWorkItemType,
workItemId: 'gid://gitlab/WorkItem/515',
confidential,
children,
- projectPath: 'test/project',
canUpdate,
},
});
diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js
index 95b0aee1315..c42c9a573e5 100644
--- a/spec/frontend/work_items/components/work_item_milestone_spec.js
+++ b/spec/frontend/work_items/components/work_item_milestone_spec.js
@@ -31,7 +31,6 @@ describe('WorkItemMilestone component', () => {
const workItemId = 'gid://gitlab/WorkItem/1';
const workItemType = 'Task';
- const fullPath = 'full-path';
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
@@ -67,12 +66,14 @@ describe('WorkItemMilestone component', () => {
[projectMilestonesQuery, searchQueryHandler],
[updateWorkItemMutation, mutationHandler],
]),
+ provide: {
+ fullPath: 'full-path',
+ },
propsData: {
canUpdate,
workItemMilestone: milestone,
workItemId,
workItemType,
- fullPath,
},
stubs: {
GlDropdown,
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
index be5861ba1e2..c2821cc99f9 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -97,11 +97,12 @@ describe('WorkItemNotes component', () => {
[workItemNoteUpdatedSubscription, notesUpdateSubscriptionHandler],
[workItemNoteDeletedSubscription, notesDeleteSubscriptionHandler],
]),
+ provide: {
+ fullPath: 'test-path',
+ },
propsData: {
workItemId,
workItemIid,
- queryVariables: { iid: '1' },
- fullPath: 'test-path',
workItemType: 'task',
reportAbusePath: '/report/abuse/path',
isModal,
@@ -139,6 +140,7 @@ describe('WorkItemNotes component', () => {
await waitForPromises();
expect(workItemNotesQueryHandler).toHaveBeenCalledWith({
after: undefined,
+ fullPath: 'test-path',
iid: '1',
pageSize: 30,
});
@@ -163,15 +165,17 @@ describe('WorkItemNotes component', () => {
it('fetch more notes should be called', async () => {
expect(workItemMoreNotesQueryHandler).toHaveBeenCalledWith({
- pageSize: DEFAULT_PAGE_SIZE_NOTES,
+ fullPath: 'test-path',
iid: '1',
+ pageSize: DEFAULT_PAGE_SIZE_NOTES,
});
await nextTick();
expect(workItemMoreNotesQueryHandler).toHaveBeenCalledWith({
- pageSize: DEFAULT_PAGE_SIZE_NOTES,
+ fullPath: 'test-path',
iid: '1',
+ pageSize: DEFAULT_PAGE_SIZE_NOTES,
after: mockMoreNotesWidgetResponse.discussions.pageInfo.endCursor,
});
});
diff --git a/spec/helpers/projects/ml/experiments_helper_spec.rb b/spec/helpers/projects/ml/experiments_helper_spec.rb
index 4296d6acdca..e0fbc8ac488 100644
--- a/spec/helpers/projects/ml/experiments_helper_spec.rb
+++ b/spec/helpers/projects/ml/experiments_helper_spec.rb
@@ -8,8 +8,16 @@ require 'mime/types'
RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do
let_it_be(:project) { create(:project, :private) }
let_it_be(:experiment) { create(:ml_experiments, user_id: project.creator, project: project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:build) { create(:ci_build, pipeline: pipeline) }
let_it_be(:candidate0) do
- create(:ml_candidates, :with_artifact, experiment: experiment, user: project.creator, project: project).tap do |c|
+ create(:ml_candidates,
+ :with_artifact,
+ experiment: experiment,
+ user: project.creator,
+ project: project,
+ ci_build: build
+ ).tap do |c|
c.params.build([{ name: 'param1', value: 'p1' }, { name: 'param2', value: 'p2' }])
c.metrics.create!(
[{ name: 'metric1', value: 0.1 }, { name: 'metric2', value: 0.2 }, { name: 'metric3', value: 0.3 }]
@@ -35,11 +43,13 @@ RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do
{ 'param1' => 'p1', 'param2' => 'p2', 'metric1' => '0.1000', 'metric2' => '0.2000', 'metric3' => '0.3000',
'artifact' => "/#{project.full_path}/-/packages/#{candidate0.artifact.id}",
'details' => "/#{project.full_path}/-/ml/candidates/#{candidate0.iid}",
+ 'ci_job' => { 'path' => "/#{project.full_path}/-/jobs/#{build.id}", 'name' => 'test' },
'name' => candidate0.name,
'created_at' => candidate0.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
'user' => { 'username' => candidate0.user.username, 'path' => "/#{candidate0.user.username}" } },
{ 'param2' => 'p3', 'param3' => 'p4', 'metric3' => '0.4000',
'artifact' => nil, 'details' => "/#{project.full_path}/-/ml/candidates/#{candidate1.iid}",
+ 'ci_job' => nil,
'name' => candidate1.name,
'created_at' => candidate1.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
'user' => { 'username' => candidate1.user.username, 'path' => "/#{candidate1.user.username}" } }
diff --git a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
index 0933f45e7c3..e14b1362687 100644
--- a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
+++ b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
@@ -21,6 +21,10 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor
create(:issue, state, attributes.merge(project: project))
end
+ def create_item(issuable_type, state, attributes = {})
+ create(issuable_type, state, attributes.merge(project: project))
+ end
+
def create_merge_request(state, attributes = {})
create(:merge_request, state, attributes.merge(source_project: project, target_project: project))
end
@@ -115,75 +119,88 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor
end
end
- context 'for issue references' do
- it 'ignores open issue references' do
- issue = create_issue(:opened)
- link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue')
+ shared_examples 'issue / work item references' do
+ it 'ignores open references' do
+ issuable = create_item(issuable_type, :opened)
+ link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type)
doc = filter(link, context)
- expect(doc.css('a').last.text).to eq(issue.to_reference)
+ expect(doc.css('a').last.text).to eq(issuable.to_reference)
end
- it 'appends state to closed issue references' do
- link = create_link(closed_issue.to_reference, issue: closed_issue.id, reference_type: 'issue')
+ it 'appends state to moved references' do
+ moved_issuable = create_item(issuable_type, :closed, project: project,
+ moved_to: create_item(issuable_type, :opened))
+ link = create_link(moved_issuable.to_reference, "#{issuable_type}": moved_issuable.id,
+ reference_type: issuable_type)
doc = filter(link, context)
- expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference} (closed)")
+ expect(doc.css('a').last.text).to eq("#{moved_issuable.to_reference} (moved)")
end
- it 'appends state to moved issue references' do
- moved_issue = create(:issue, :closed, project: project, moved_to: create_issue(:opened))
- link = create_link(moved_issue.to_reference, issue: moved_issue.id, reference_type: 'issue')
+ it 'appends state to closed references' do
+ issuable = create_item(issuable_type, :closed)
+ link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type)
doc = filter(link, context)
- expect(doc.css('a').last.text).to eq("#{moved_issue.to_reference} (moved)")
+ expect(doc.css('a').last.text).to eq("#{issuable.to_reference} (closed)")
end
it 'shows title for references with +' do
- issue = create_issue(:opened, title: 'Some issue')
- link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+')
+ issuable = create_item(issuable_type, :opened, title: 'Some issue')
+ link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type,
+ reference_format: '+')
doc = filter(link, context)
- expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference})")
+ expect(doc.css('a').last.text).to eq("#{issuable.title} (#{issuable.to_reference})")
end
it 'truncates long title for references with +' do
- issue = create_issue(:opened, title: 'Some issue ' * 10)
- link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+')
+ issuable = create_item(issuable_type, :opened, title: 'Some issue ' * 10)
+ link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type,
+ reference_format: '+')
doc = filter(link, context)
- expect(doc.css('a').last.text).to eq("#{issue.title.truncate(50)} (#{issue.to_reference})")
+ expect(doc.css('a').last.text).to eq("#{issuable.title.truncate(50)} (#{issuable.to_reference})")
end
it 'shows both title and state for closed references with +' do
- issue = create_issue(:closed, title: 'Some issue')
- link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+')
+ issuable = create_item(issuable_type, :closed, title: 'Some issue')
+ link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type,
+ reference_format: '+')
doc = filter(link, context)
- expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference} - closed)")
+ expect(doc.css('a').last.text).to eq("#{issuable.title} (#{issuable.to_reference} - closed)")
end
it 'shows title for references with +s' do
- issue = create_issue(:opened, title: 'Some issue')
- link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+s')
+ issuable = create_item(issuable_type, :opened, title: 'Some issue')
+ link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type,
+ reference_format: '+s')
doc = filter(link, context)
- expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference}) • Unassigned")
+ expect(doc.css('a').last.text).to eq("#{issuable.title} (#{issuable.to_reference}) • Unassigned")
end
context 'when extended summary props are present' do
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:assignees) { create_list(:user, 3) }
- let_it_be(:issue) { create_issue(:opened, title: 'Some issue', milestone: milestone, assignees: assignees) }
+ let_it_be(:issuable) do
+ create_item(issuable_type, :opened, title: 'Some issue', milestone: milestone,
+ assignees: assignees)
+ end
+
let_it_be(:link) do
- create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+s')
+ create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type,
+ reference_format: '+s')
end
it 'shows extended summary for references with +s' do
doc = filter(link, context)
expect(doc.css('a').last.text).to eq(
- "#{issue.title} (#{issue.to_reference}) • #{assignees[0].name}, #{assignees[1].name}+ • #{milestone.title}"
+ "#{issuable.title} (#{issuable.to_reference}) • #{assignees[0].name}, #{assignees[1].name}+ " \
+ "• #{milestone.title}"
)
end
@@ -192,8 +209,10 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor
let_it_be(:assignees2) { create_list(:user, 3) }
it 'does not have N+1 for extended summary', :use_sql_query_cache do
- issue2 = create_issue(:opened, title: 'Another issue', milestone: milestone2, assignees: assignees2)
- link2 = create_link(issue2.to_reference, issue: issue2.id, reference_type: 'issue', reference_format: '+s')
+ issuable2 = create_item(issuable_type, :opened, title: 'Another issue',
+ milestone: milestone2, assignees: assignees2)
+ link2 = create_link(issuable2.to_reference, "#{issuable_type}": issuable2.id,
+ reference_type: issuable_type, reference_format: '+s')
# warm up
filter(link, context)
@@ -212,6 +231,18 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor
end
end
+ context 'for work item references' do
+ let_it_be(:issuable_type) { :work_item }
+
+ it_behaves_like 'issue / work item references'
+ end
+
+ context 'for issue references' do
+ let_it_be(:issuable_type) { :issue }
+
+ it_behaves_like 'issue / work item references'
+ end
+
context 'for merge request references' do
it 'ignores open merge request references' do
merge_request = create_merge_request(:opened)
diff --git a/spec/lib/banzai/filter/references/work_item_reference_filter_spec.rb b/spec/lib/banzai/filter/references/work_item_reference_filter_spec.rb
new file mode 100644
index 00000000000..e59e53891bf
--- /dev/null
+++ b/spec/lib/banzai/filter/references/work_item_reference_filter_spec.rb
@@ -0,0 +1,314 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::References::WorkItemReferenceFilter, feature_category: :team_planning do
+ include FilterSpecHelper
+
+ let_it_be(:namespace) { create(:namespace, name: 'main-namespace') }
+ let_it_be(:project) { create(:project, :public, namespace: namespace, path: 'main-project') }
+ let_it_be(:cross_namespace) { create(:namespace, name: 'cross-namespace') }
+ let_it_be(:cross_project) { create(:project, :public, namespace: cross_namespace, path: 'cross-project') }
+ let_it_be(:work_item) { create(:work_item, project: project) }
+
+ def item_url(item)
+ work_item_path = "/#{item.project.namespace.path}/#{item.project.path}/-/work_items/#{item.iid}"
+
+ "http://#{Gitlab.config.gitlab.host}#{work_item_path}"
+ end
+
+ it 'subclasses from IssueReferenceFilter' do
+ expect(described_class.superclass).to eq Banzai::Filter::References::IssueReferenceFilter
+ end
+
+ shared_examples 'a reference with work item type information' do
+ it 'contains work-item-type as a data attribute' do
+ doc = reference_filter("Fixed #{reference}")
+
+ expect(doc.css('a').first.attr('data-work-item-type')).to eq('issue')
+ end
+ end
+
+ shared_examples 'a work item reference' do
+ it_behaves_like 'a reference containing an element node'
+
+ it_behaves_like 'a reference with work item type information'
+
+ it 'links to a valid reference' do
+ doc = reference_filter("Fixed #{written_reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq work_item_url
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{written_reference}.)")
+
+ expect(doc.text).to match(%r{^Fixed \(.*\.\)})
+ end
+
+ it 'includes a title attribute' do
+ doc = reference_filter("Issue #{written_reference}")
+
+ expect(doc.css('a').first.attr('title')).to eq work_item.title
+ end
+
+ it 'escapes the title attribute' do
+ work_item.update_attribute(:title, %("></a>whatever<a title="))
+
+ doc = reference_filter("Issue #{written_reference}")
+
+ expect(doc.text).not_to include 'whatever'
+ end
+
+ it 'renders non-HTML tooltips' do
+ doc = reference_filter("Issue #{written_reference}")
+
+ expect(doc.at_css('a')).not_to have_attribute('data-html')
+ end
+
+ it 'includes default classes' do
+ doc = reference_filter("Issue #{written_reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-work_item'
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Issue #{written_reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq cross_project.id.to_s
+ end
+
+ it 'includes a data-issue attribute' do
+ doc = reference_filter("See #{written_reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-work-item')
+ expect(link.attr('data-work-item')).to eq work_item.id.to_s
+ end
+
+ it 'includes data attributes for issuable popover' do
+ doc = reference_filter("See #{written_reference}")
+ link = doc.css('a').first
+
+ expect(link.attr('data-project-path')).to eq cross_project.full_path
+ expect(link.attr('data-iid')).to eq work_item.iid.to_s
+ end
+
+ it 'includes a data-original attribute' do
+ doc = reference_filter("See #{written_reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-original')
+ expect(link.attr('data-original')).to eq inner_text
+ end
+
+ it 'does not escape the data-original attribute' do
+ skip if written_reference.start_with?('<a')
+
+ inner_html = 'element <code>node</code> inside'
+ doc = reference_filter(%(<a href="#{written_reference}">#{inner_html}</a>))
+
+ expect(doc.children.first.attr('data-original')).to eq inner_html
+ end
+
+ it 'includes a data-reference-format attribute' do
+ skip if written_reference.start_with?('<a')
+
+ doc = reference_filter("Issue #{written_reference}+")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-reference-format')
+ expect(link.attr('data-reference-format')).to eq('+')
+ expect(link.attr('href')).to eq(work_item_url)
+ end
+
+ it 'includes a data-reference-format attribute for URL references' do
+ doc = reference_filter("Issue #{work_item_url}+")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-reference-format')
+ expect(link.attr('data-reference-format')).to eq('+')
+ expect(link.attr('href')).to eq(work_item_url)
+ end
+
+ it 'includes a data-reference-format attribute for extended summary URL references' do
+ doc = reference_filter("Issue #{work_item_url}+s")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-reference-format')
+ expect(link.attr('data-reference-format')).to eq('+s')
+ expect(link.attr('href')).to eq(work_item_url)
+ end
+
+ it 'does not process links containing issue numbers followed by text' do
+ href = "#{written_reference}st"
+ doc = reference_filter("<a href='#{href}'></a>")
+ link = doc.css('a').first.attr('href')
+
+ expect(link).to eq(href)
+ end
+ end
+
+ # Example:
+ # "See #1"
+ context 'when standard internal reference' do
+ it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do
+ doc = reference_filter("Fixed ##{work_item.iid}")
+
+ expect(doc.css('a')).to be_empty
+ end
+ end
+
+ # Example:
+ # "See cross-namespace/cross-project#1"
+ context 'when cross-project / cross-namespace complete reference' do
+ let_it_be(:work_item2) { create(:work_item, project: cross_project) }
+ let_it_be(:reference) { "#{cross_project.full_path}##{work_item2.iid}" }
+
+ it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a')).to be_empty
+ end
+ end
+
+ # Example:
+ # "See main-namespace/cross-project#1"
+ context 'when cross-project / same-namespace complete reference' do
+ let_it_be(:cross_project) { create(:project, :public, namespace: namespace, path: 'cross-project') }
+ let_it_be(:work_item) { create(:work_item, project: cross_project) }
+ let_it_be(:reference) { "#{cross_project.full_path}##{work_item.iid}" }
+
+ it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a')).to be_empty
+ end
+ end
+
+ # Example:
+ # "See cross-project#1"
+ context 'when cross-project / same-namespace shorthand reference' do
+ let_it_be(:cross_project) { create(:project, :public, namespace: namespace, path: 'cross-project') }
+ let_it_be(:work_item) { create(:work_item, project: cross_project) }
+ let_it_be(:reference) { "#{cross_project.path}##{work_item.iid}" }
+
+ it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a')).to be_empty
+ end
+ end
+
+ # Example:
+ # "See http://localhost/cross-namespace/cross-project/-/work_items/1"
+ context 'when cross-project URL reference' do
+ let_it_be(:work_item, reload: true) { create(:work_item, project: cross_project) }
+ let_it_be(:work_item_url) { item_url(work_item) }
+ let_it_be(:reference) { work_item_url }
+ let_it_be(:written_reference) { reference }
+ let_it_be(:inner_text) { written_reference }
+
+ it_behaves_like 'a work item reference'
+ end
+
+ # Example:
+ # "See http://localhost/cross-namespace/cross-project/-/work_items/1#note_123"
+ context 'when cross-project URL reference with comment anchor' do
+ let_it_be(:work_item) { create(:work_item, project: cross_project) }
+ let_it_be(:work_item_url) { item_url(work_item) }
+ let_it_be(:reference) { "#{work_item_url}#note_123" }
+
+ it_behaves_like 'a reference containing an element node'
+
+ it_behaves_like 'a reference with work item type information'
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq reference
+ end
+
+ it 'link with trailing slash' do
+ doc = reference_filter("Fixed (#{work_item_url}/.)")
+
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(work_item.to_reference(project))}</a>\.\)})
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(work_item.to_reference(project))} \(comment 123\)</a>\.\)})
+ end
+ end
+
+ # Example:
+ # 'See <a href="cross-namespace/cross-project#1">Reference</a>''
+ context 'when cross-project reference in link href' do
+ let_it_be(:work_item) { create(:work_item, project: cross_project) }
+ let_it_be(:reference) { work_item.to_reference(project) }
+ let_it_be(:reference_link) { %(<a href="#{reference}">Reference</a>) }
+ let_it_be(:work_item_url) { item_url(work_item) }
+
+ it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do
+ doc = reference_filter("See #{reference_link}")
+
+ expect(doc.css('a').first[:href]).to eq reference
+ expect(doc.css('a').first[:href]).not_to eq work_item_url
+ end
+ end
+
+ # Example:
+ # 'See <a href=\"http://localhost/cross-namespace/cross-project/-/work_items/1\">Reference</a>''
+ context 'when cross-project URL in link href' do
+ let_it_be(:work_item, reload: true) { create(:work_item, project: cross_project) }
+ let_it_be(:work_item_url) { item_url(work_item) }
+ let_it_be(:reference) { work_item_url }
+ let_it_be(:reference_link) { %(<a href="#{reference}">Reference</a>) }
+ let_it_be(:written_reference) { reference_link }
+ let_it_be(:inner_text) { 'Reference' }
+
+ it_behaves_like 'a work item reference'
+ end
+
+ context 'for group context' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:context) { { project: nil, group: group } }
+ let_it_be(:work_item_url) { item_url(work_item) }
+
+ it 'links to a valid reference for url cross-namespace' do
+ reference = "#{work_item_url}#note_123"
+
+ doc = reference_filter("See #{reference}", context)
+
+ link = doc.css('a').first
+ expect(link.attr('href')).to eq("#{work_item_url}#note_123")
+ expect(link.text).to include("#{project.full_path}##{work_item.iid}")
+ end
+
+ it 'links to a valid reference for cross-namespace in link href' do
+ reference = "#{work_item_url}#note_123"
+ reference_link = %(<a href="#{reference}">Reference</a>)
+
+ doc = reference_filter("See #{reference_link}", context)
+
+ link = doc.css('a').first
+ expect(link.attr('href')).to eq("#{work_item_url}#note_123")
+ expect(link.text).to include('Reference')
+ end
+ end
+
+ describe 'performance' do
+ let(:another_work_item) { create(:work_item, project: project) }
+
+ it 'does not have a N+1 query problem' do
+ single_reference = "Work item #{work_item.to_reference}"
+ multiple_references = "Work items #{work_item.to_reference} and #{another_work_item.to_reference}"
+
+ control_count = ActiveRecord::QueryRecorder.new { reference_filter(single_reference).to_html }.count
+
+ expect { reference_filter(multiple_references).to_html }.not_to exceed_query_limit(control_count)
+ end
+ end
+end
diff --git a/spec/lib/banzai/issuable_extractor_spec.rb b/spec/lib/banzai/issuable_extractor_spec.rb
index b2c869bd066..5bbd98592e7 100644
--- a/spec/lib/banzai/issuable_extractor_spec.rb
+++ b/spec/lib/banzai/issuable_extractor_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Banzai::IssuableExtractor, feature_category: :team_planning do
let(:user) { create(:user) }
let(:extractor) { described_class.new(Banzai::RenderContext.new(project, user)) }
let(:issue) { create(:issue, project: project) }
+ let(:work_item) { create(:work_item, project: project) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:issue_link) do
html_to_node(
@@ -14,6 +15,12 @@ RSpec.describe Banzai::IssuableExtractor, feature_category: :team_planning do
)
end
+ let(:work_item_link) do
+ html_to_node(
+ "<a href='' data-work-item='#{work_item.id}' data-reference-type='work_item' class='gfm'>text</a>"
+ )
+ end
+
let(:merge_request_link) do
html_to_node(
"<a href='' data-merge-request='#{merge_request.id}' data-reference-type='merge_request' class='gfm'>text</a>"
@@ -27,17 +34,17 @@ RSpec.describe Banzai::IssuableExtractor, feature_category: :team_planning do
end
it 'returns instances of issuables for nodes with references' do
- result = extractor.extract([issue_link, merge_request_link])
+ result = extractor.extract([issue_link, work_item_link, merge_request_link])
- expect(result).to eq(issue_link => issue, merge_request_link => merge_request)
+ expect(result).to eq(issue_link => issue, work_item_link => work_item, merge_request_link => merge_request)
end
describe 'caching', :request_store do
it 'saves records to cache' do
- extractor.extract([issue_link, merge_request_link])
+ extractor.extract([issue_link, work_item_link, merge_request_link])
second_call_queries = ActiveRecord::QueryRecorder.new do
- extractor.extract([issue_link, merge_request_link])
+ extractor.extract([issue_link, work_item_link, merge_request_link])
end.count
expect(second_call_queries).to eq 0
diff --git a/spec/lib/banzai/reference_parser/work_item_parser_spec.rb b/spec/lib/banzai/reference_parser/work_item_parser_spec.rb
new file mode 100644
index 00000000000..dbde01cc94f
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/work_item_parser_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::ReferenceParser::WorkItemParser, feature_category: :team_planning do
+ include ReferenceParserHelpers
+
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be_with_reload(:project) { create(:project, :public, group: group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:work_item) { create(:work_item, project: project) }
+ let_it_be(:link) { empty_html_link }
+
+ subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
+ describe '#records_for_nodes' do
+ it 'returns a Hash containing the work items for a list of nodes' do
+ link['data-work-item'] = work_item.id.to_s
+ nodes = [link]
+
+ expect(subject.records_for_nodes(nodes)).to eq({ link => work_item })
+ end
+ end
+
+ context 'when checking multiple work items on another project' do
+ let_it_be(:other_project) { create(:project, :public) }
+ let_it_be(:other_work_item) { create(:work_item, project: other_project) }
+ let_it_be(:control_links) do
+ [work_item_link(other_work_item)]
+ end
+
+ let_it_be(:actual_links) do
+ control_links + [work_item_link(create(:work_item, project: other_project))]
+ end
+
+ def work_item_link(work_item)
+ Nokogiri::HTML.fragment(%(<a data-work-item="#{work_item.id}"></a>)).children[0]
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'no N+1 queries'
+ end
+end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index d7dcfe64c74..a35dd968cd6 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -2414,9 +2414,33 @@ module Gitlab
end
context 'duplicate needs' do
- let(:needs) { %w(build1 build1) }
+ context 'when needs are specified in an array' do
+ let(:needs) { %w(build1 build1) }
- it_behaves_like 'returns errors', 'test1 has duplicate entries in the needs section.'
+ it_behaves_like 'returns errors', 'test1 has the following needs duplicated: build1.'
+ end
+
+ context 'when a job is specified multiple times' do
+ let(:needs) do
+ [
+ { job: "build2", artifacts: true, optional: false },
+ { job: "build2", artifacts: true, optional: false }
+ ]
+ end
+
+ it_behaves_like 'returns errors', 'test1 has the following needs duplicated: build2.'
+ end
+
+ context 'when job is specified multiple times with different attributes' do
+ let(:needs) do
+ [
+ { job: "build2", artifacts: false, optional: true },
+ { job: "build2", artifacts: true, optional: false }
+ ]
+ end
+
+ it_behaves_like 'returns errors', 'test1 has the following needs duplicated: build2.'
+ end
end
context 'needs and dependencies that are mismatching' do
diff --git a/spec/migrations/20230328030101_add_secureflag_training_provider_spec.rb b/spec/migrations/20230328030101_add_secureflag_training_provider_spec.rb
new file mode 100644
index 00000000000..774ea89937a
--- /dev/null
+++ b/spec/migrations/20230328030101_add_secureflag_training_provider_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddSecureflagTrainingProvider, :migration, feature_category: :vulnerability_management do
+ include MigrationHelpers::WorkItemTypesHelper
+
+ let!(:security_training_providers) { table(:security_training_providers) }
+
+ it 'adds additional provider' do
+ # Need to delete all as security training providers are seeded before entire test suite
+ security_training_providers.delete_all
+
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(security_training_providers.count).to eq(0)
+ }
+
+ migration.after -> {
+ expect(security_training_providers.count).to eq(1)
+ }
+ end
+ end
+end
diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb
index a39babd78b7..fa19b723ee2 100644
--- a/spec/models/ml/candidate_spec.rb
+++ b/spec/models/ml/candidate_spec.rb
@@ -157,6 +157,9 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d
expect(subject.association_cached?(:latest_metrics)).to be(true)
expect(subject.association_cached?(:params)).to be(true)
expect(subject.association_cached?(:user)).to be(true)
+ expect(subject.association_cached?(:project)).to be(true)
+ expect(subject.association_cached?(:package)).to be(true)
+ expect(subject.association_cached?(:ci_build)).to be(true)
end
end
@@ -188,6 +191,22 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d
end
end
+ describe 'from_ci?' do
+ subject { candidate }
+
+ it 'is false if candidate does not have ci_build_id' do
+ allow(candidate).to receive(:ci_build_id).and_return(nil)
+
+ is_expected.not_to be_from_ci
+ end
+
+ it 'is true if candidate does has ci_build_id' do
+ allow(candidate).to receive(:ci_build_id).and_return(1)
+
+ is_expected.to be_from_ci
+ end
+ end
+
describe '#order_by_metric' do
let_it_be(:auc_metrics) do
create(:ml_candidate_metrics, name: 'auc', value: 0.4, candidate: candidate)
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index 57193d4ac49..24920421ae6 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -329,6 +329,20 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
+ describe '#link_reference_pattern' do
+ let(:match_data) { described_class.link_reference_pattern.match(link_reference_url) }
+
+ context 'with work item url' do
+ let(:link_reference_url) { 'http://localhost/namespace/project/-/work_items/1' }
+
+ it 'matches with expected attributes' do
+ expect(match_data['namespace']).to eq('namespace')
+ expect(match_data['project']).to eq('project')
+ expect(match_data['work_item']).to eq('1')
+ end
+ end
+ end
+
context 'with hierarchy' do
let_it_be(:type1) { create(:work_item_type, namespace: reusable_project.namespace) }
let_it_be(:type2) { create(:work_item_type, namespace: reusable_project.namespace) }
diff --git a/spec/rubocop/cop/rspec/avoid_conditional_statements_spec.rb b/spec/rubocop/cop/rspec/avoid_conditional_statements_spec.rb
new file mode 100644
index 00000000000..d2f5e4aa619
--- /dev/null
+++ b/spec/rubocop/cop/rspec/avoid_conditional_statements_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../../rubocop/cop/rspec/avoid_conditional_statements'
+
+RSpec.describe RuboCop::Cop::RSpec::AvoidConditionalStatements, feature_category: :tooling do
+ context 'when using conditionals' do
+ it 'flags if conditional' do
+ expect_offense(<<~RUBY)
+ if page.has_css?('[data-testid="begin-commit-button"]')
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use `if` conditional statments in specs, it might create flakiness. See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109
+ find('[data-testid="begin-commit-button"]').click
+ end
+ RUBY
+ end
+
+ it 'flags unless conditional' do
+ expect_offense(<<~RUBY)
+ RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_ide do
+ it 'creates directory in current directory' do
+ unless page.has_css?('[data-testid="begin-commit-button"]')
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use `unless` conditional statments in specs, it might create flakiness. See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109
+ find('[data-testid="begin-commit-button"]').click
+ end
+ end
+ end
+ RUBY
+ end
+
+ it 'flags ternary operator' do
+ expect_offense(<<~RUBY)
+ RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_ide do
+ it 'creates directory in current directory' do
+ user.present ? user : nil
+ ^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use `(send (send nil :user) :present) ? (ternary)` conditional statments in specs, it might create flakiness. See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109
+ end
+ end
+ RUBY
+ end
+ end
+end
diff --git a/spec/support/capybara_wait_for_all_requests_after_visit_page.rb b/spec/support/capybara_wait_for_all_requests.rb
index f20e82e89a9..86f3e77cc19 100644
--- a/spec/support/capybara_wait_for_all_requests_after_visit_page.rb
+++ b/spec/support/capybara_wait_for_all_requests.rb
@@ -18,4 +18,22 @@ module Capybara
prepend WaitForAllRequestsAfterVisitPage
end
+
+ module Node
+ module Actions
+ include CapybaraHelpers
+ include WaitHelpers
+ include WaitForRequests
+
+ module WaitForAllRequestsAfterClickButton
+ def click_button(locator = nil, **options)
+ super
+
+ wait_for_all_requests
+ end
+ end
+
+ prepend WaitForAllRequestsAfterClickButton
+ end
+ end
end
diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml
index 0e94c5e348e..8fcb4ee7b9c 100644
--- a/spec/support/finder_collection_allowlist.yml
+++ b/spec/support/finder_collection_allowlist.yml
@@ -63,6 +63,7 @@
- Security::TrainingUrlsFinder
- Security::TrainingProviders::KontraUrlFinder
- Security::TrainingProviders::SecureCodeWarriorUrlFinder
+- Security::TrainingProviders::SecureFlagUrlFinder
- SentryIssueFinder
- ServerlessDomainFinder
- TagsFinder
diff --git a/spec/support/helpers/markdown_feature.rb b/spec/support/helpers/markdown_feature.rb
index 0cb2863dc2c..5d9ef557ae6 100644
--- a/spec/support/helpers/markdown_feature.rb
+++ b/spec/support/helpers/markdown_feature.rb
@@ -48,6 +48,10 @@ class MarkdownFeature
@issue ||= create(:issue, project: project)
end
+ def work_item
+ @issue ||= create(:work_item, project: project)
+ end
+
def merge_request
@merge_request ||= create(:merge_request, :simple, source_project: project)
end
@@ -106,6 +110,10 @@ class MarkdownFeature
@xissue ||= create(:issue, project: xproject)
end
+ def xwork_item
+ @xwork_item ||= create(:work_item, project: xproject)
+ end
+
def xmerge_request
@xmerge_request ||= create(:merge_request, :simple, source_project: xproject)
end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 575ae572f25..8fdece7b26d 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -115,7 +115,16 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-issue', count: 6)
+ expect(actual).to have_selector('a.gfm.gfm-issue', count: 9)
+ end
+ end
+
+ # WorkItemReferenceFilter
+ matcher :reference_work_items do
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_selector('a.gfm.gfm-work_item', count: 2)
end
end
diff --git a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
index cb74d0e8dca..9c096c5a158 100644
--- a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
+++ b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
@@ -498,3 +498,132 @@ RSpec.shared_examples 'value stream analytics flow metrics cycleTime examples' d
end
end
end
+
+RSpec.shared_examples 'value stream analytics flow metrics issuesCompleted examples' do
+ let_it_be(:milestone) { create(:milestone, group: group) }
+ let_it_be(:label) { create(:group_label, group: group) }
+
+ let_it_be(:author) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
+
+ # we don't care about opened date, only closed date.
+ let_it_be(:issue1) do
+ create(:issue, project: project1, author: author, created_at: 17.days.ago, closed_at: 12.days.ago)
+ end
+
+ let_it_be(:issue2) do
+ create(:issue, project: project2, author: author, created_at: 16.days.ago, closed_at: 13.days.ago)
+ end
+
+ let_it_be(:issue3) do
+ create(:labeled_issue,
+ project: project1,
+ labels: [label],
+ author: author,
+ milestone: milestone,
+ assignees: [assignee],
+ created_at: 14.days.ago,
+ closed_at: 11.days.ago)
+ end
+
+ let_it_be(:issue4) do
+ create(:labeled_issue,
+ project: project2,
+ labels: [label],
+ assignees: [assignee],
+ created_at: 20.days.ago,
+ closed_at: 15.days.ago)
+ end
+
+ before do
+ Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute
+ end
+
+ let(:query) do
+ <<~QUERY
+ query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) {
+ #{context}(fullPath: $path) {
+ flowMetrics {
+ issuesCompletedCount(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) {
+ value
+ unit
+ identifier
+ title
+ links {
+ label
+ url
+ }
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ let(:variables) do
+ {
+ path: full_path,
+ from: 21.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ subject(:result) do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ graphql_data.dig(context.to_s, 'flowMetrics', 'issuesCompletedCount')
+ end
+
+ it 'returns the correct value' do
+ expect(result).to match(a_hash_including({
+ 'identifier' => 'issues_completed',
+ 'unit' => n_('issue', 'issues', 4),
+ 'value' => 4,
+ 'title' => _('Issues Completed'),
+ 'links' => [
+ { 'label' => s_('ValueStreamAnalytics|Dashboard'), 'url' => match(/issues_analytics/) },
+ { 'label' => s_('ValueStreamAnalytics|Go to docs'), 'url' => match(/definitions/) }
+ ]
+ }))
+ end
+
+ context 'when the user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(result).to eq(nil)
+ end
+ end
+
+ context 'when outside of the date range' do
+ let(:variables) do
+ {
+ path: full_path,
+ from: 30.days.ago.iso8601,
+ to: 25.days.ago.iso8601
+ }
+ end
+
+ it 'returns 0 count' do
+ expect(result).to match(a_hash_including({ 'value' => 0.0 }))
+ end
+ end
+
+ context 'with all filters' do
+ let(:variables) do
+ {
+ path: full_path,
+ assigneeUsernames: [assignee.username],
+ labelNames: [label.title],
+ authorUsername: author.username,
+ milestoneTitle: milestone.title,
+ from: 20.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ it 'returns filtered count' do
+ expect(result).to match(a_hash_including({ 'value' => 1.0 }))
+ end
+ end
+end
diff --git a/spec/support/shared_examples/security_training_providers_importer.rb b/spec/support/shared_examples/security_training_providers_importer.rb
index 69d92964270..81b3d22ab23 100644
--- a/spec/support/shared_examples/security_training_providers_importer.rb
+++ b/spec/support/shared_examples/security_training_providers_importer.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples 'security training providers importer' do
end
it 'upserts security training providers' do
- expect { 2.times { subject } }.to change { security_training_providers.count }.from(0).to(2)
- expect(security_training_providers.all.map(&:name)).to match_array(['Kontra', 'Secure Code Warrior'])
+ expect { 3.times { subject } }.to change { security_training_providers.count }.from(0).to(3)
+ expect(security_training_providers.all.map(&:name)).to match_array(['Kontra', 'Secure Code Warrior', 'SecureFlag'])
end
end
diff --git a/spec/support_specs/capybara_wait_for_all_requests_after_page_visit_spec.rb b/spec/support_specs/capybara_wait_for_all_requests_after_page_visit_spec.rb
deleted file mode 100644
index aec67b66379..00000000000
--- a/spec/support_specs/capybara_wait_for_all_requests_after_page_visit_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'capybara'
-require 'support/capybara_wait_for_all_requests_after_visit_page'
-
-RSpec.describe Capybara::Session::WaitForAllRequestsAfterVisitPage, feature_category: :tooling do # rubocop:disable RSpec/FilePath
- let(:page_visitor) do
- Class.new do
- def visit(visit_uri)
- visit_uri
- end
-
- prepend Capybara::Session::WaitForAllRequestsAfterVisitPage
- end.new
- end
-
- it 'waits for all requests after a page visit' do
- expect(page_visitor).to receive(:wait_for_all_requests)
-
- page_visitor.visit('http://test.com')
- end
-end
diff --git a/spec/support_specs/capybara_wait_for_all_requests_spec.rb b/spec/support_specs/capybara_wait_for_all_requests_spec.rb
new file mode 100644
index 00000000000..fd105c3ab01
--- /dev/null
+++ b/spec/support_specs/capybara_wait_for_all_requests_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'capybara'
+require 'support/capybara_wait_for_all_requests'
+
+RSpec.describe 'capybara_wait_for_all_requests', feature_category: :tooling do # rubocop:disable RSpec/FilePath
+ context 'for Capybara::Session::WaitForAllRequestsAfterVisitPage' do
+ let(:page_visitor) do
+ Class.new do
+ def visit(visit_uri)
+ visit_uri
+ end
+
+ prepend Capybara::Session::WaitForAllRequestsAfterVisitPage
+ end.new
+ end
+
+ it 'waits for all requests after a page visit' do
+ expect(page_visitor).to receive(:wait_for_all_requests)
+
+ page_visitor.visit('http://test.com')
+ end
+ end
+
+ context 'for Capybara::Node::Actions::WaitForAllRequestsAfterClickButton' do
+ let(:node) do
+ Class.new do
+ def click_button(locator = nil, **_options)
+ locator
+ end
+
+ prepend Capybara::Node::Actions::WaitForAllRequestsAfterClickButton
+ end.new
+ end
+
+ it 'waits for all requests after a page visit' do
+ expect(node).to receive(:wait_for_all_requests)
+
+ node.click_button
+ end
+ end
+end