summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-05-05 15:08:47 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-05-05 15:08:47 +0000
commitdad16033c2b7cfd54ffe20ca5cc1d844e9e41be6 (patch)
tree8010601f9b7066e07166d997624b723ea4c3f816
parent3c86701bc89302550abb9bbaa060132fdcd52480 (diff)
downloadgitlab-ce-dad16033c2b7cfd54ffe20ca5cc1d844e9e41be6.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/CODEOWNERS4
-rw-r--r--.rubocop_todo/layout/hash_alignment.yml1
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--.rubocop_todo/rails/time_zone.yml2
-rw-r--r--.rubocop_todo/rspec/verified_doubles.yml2
-rw-r--r--.rubocop_todo/style/percent_literal_delimiters.yml1
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue13
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/link.vue18
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/media.vue288
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue3
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/media.vue51
-rw-r--r--app/assets/javascripts/content_editor/extensions/image.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/playable.js6
-rw-r--r--app/assets/javascripts/content_editor/services/asset_resolver.js13
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js28
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js11
-rw-r--r--app/assets/javascripts/content_editor/services/upload_helpers.js4
-rw-r--r--app/assets/javascripts/content_editor/services/utils.js2
-rw-r--r--app/assets/javascripts/pages/projects/services/edit/index.js3
-rw-r--r--app/assets/javascripts/prometheus_alerts/components/reset_key.vue149
-rw-r--r--app/assets/javascripts/prometheus_alerts/index.js28
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue104
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue111
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue2
-rw-r--r--app/assets/stylesheets/components/content_editor.scss6
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb2
-rw-r--r--app/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder.rb62
-rw-r--r--app/finders/personal_access_tokens_finder.rb2
-rw-r--r--app/helpers/personal_access_tokens_helper.rb7
-rw-r--r--app/models/application_setting.rb1
-rw-r--r--app/models/personal_access_token.rb4
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml1
-rw-r--r--app/views/admin/application_settings/_note_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_users_api_limits.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_external_alerts.html.haml8
-rw-r--r--app/views/projects/services/prometheus/_show.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_top.html.haml9
-rw-r--r--app/views/shared/access_tokens/_table.html.haml15
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/metrics/counts_all/20210216175446_network_policy_forwards.yml4
-rw-r--r--config/metrics/counts_all/20210216175448_network_policy_drops.yml4
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--data/deprecations/14-7-deprecate-static-site-editor.yml4
-rw-r--r--db/migrate/20220502125053_recreate_index_for_project_group_link_with_group_id_and_project_id.rb18
-rw-r--r--db/migrate/20220502150408_add_slack_integrations_bot_columns.rb11
-rw-r--r--db/migrate/20220502152633_add_slack_integrations_bot_user_id_text_limit.rb13
-rw-r--r--db/migrate/20220503073401_recreate_index_for_group_group_link_with_both_group_ids.rb18
-rw-r--r--db/schema_migrations/202205021250531
-rw-r--r--db/schema_migrations/202205021504081
-rw-r--r--db/schema_migrations/202205021526331
-rw-r--r--db/schema_migrations/202205030734011
-rw-r--r--db/structure.sql10
-rw-r--r--doc/administration/index.md4
-rw-r--r--doc/administration/object_storage.md3
-rw-r--r--doc/administration/pseudonymizer.md123
-rw-r--r--doc/administration/reference_architectures/10k_users.md1
-rw-r--r--doc/administration/reference_architectures/25k_users.md1
-rw-r--r--doc/administration/reference_architectures/2k_users.md1
-rw-r--r--doc/administration/reference_architectures/3k_users.md1
-rw-r--r--doc/administration/reference_architectures/50k_users.md1
-rw-r--r--doc/administration/reference_architectures/5k_users.md1
-rw-r--r--doc/administration/terraform_state.md2
-rw-r--r--doc/api/settings.md1
-rw-r--r--doc/ci/variables/index.md4
-rw-r--r--doc/update/deprecations.md4
-rw-r--r--doc/user/admin_area/credentials_inventory.md13
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md16
-rw-r--r--doc/user/admin_area/settings/index.md2
-rw-r--r--doc/user/discussions/img/confidential_comments_v13_9.pngbin8311 -> 0 bytes
-rw-r--r--doc/user/discussions/img/confidential_comments_v15_0.pngbin0 -> 12775 bytes
-rw-r--r--doc/user/discussions/index.md4
-rw-r--r--doc/user/group/index.md16
-rw-r--r--doc/user/profile/personal_access_tokens.md2
-rw-r--r--doc/user/project/integrations/gitlab_slack_application.md22
-rw-r--r--doc/user/project/static_site_editor/img/edit_this_page_button_v12_10.pngbin28949 -> 0 bytes
-rw-r--r--doc/user/project/static_site_editor/img/front_matter_ui_v13_4.pngbin36431 -> 0 bytes
-rw-r--r--doc/user/project/static_site_editor/img/wysiwyg_editor_v13_3.pngbin57177 -> 0 bytes
-rw-r--r--doc/user/project/static_site_editor/index.md264
-rw-r--r--locale/gitlab.pot118
-rw-r--r--qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb5
-rw-r--r--spec/db/schema_spec.rb2
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb6
-rw-r--r--spec/features/merge_request/user_accepts_merge_request_spec.rb14
-rw-r--r--spec/features/merge_request/user_assigns_themselves_spec.rb6
-rw-r--r--spec/features/merge_request/user_awards_emoji_spec.rb4
-rw-r--r--spec/features/merge_request/user_customizes_merge_commit_message_spec.rb8
-rw-r--r--spec/features/merge_request/user_merges_immediately_spec.rb10
-rw-r--r--spec/features/merge_request/user_merges_merge_request_spec.rb1
-rw-r--r--spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb13
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb8
-rw-r--r--spec/features/merge_request/user_squashes_merge_request_spec.rb2
-rw-r--r--spec/features/projects/integrations/prometheus_external_alerts_spec.rb34
-rw-r--r--spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb73
-rw-r--r--spec/finders/personal_access_tokens_finder_spec.rb18
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/link_spec.js22
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/media_spec.js234
-rw-r--r--spec/frontend/content_editor/components/wrappers/media_spec.js69
-rw-r--r--spec/frontend/content_editor/extensions/attachment_spec.js32
-rw-r--r--spec/frontend/content_editor/services/asset_resolver_spec.js23
-rw-r--r--spec/frontend/content_editor/services/content_editor_spec.js2
-rw-r--r--spec/frontend/content_editor/test_constants.js25
-rw-r--r--spec/frontend/prometheus_alerts/components/reset_key_spec.js99
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js82
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js82
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js3
-rw-r--r--spec/graphql/mutations/container_repositories/destroy_spec.rb2
-rw-r--r--spec/graphql/mutations/container_repositories/destroy_tags_spec.rb8
-rw-r--r--spec/graphql/mutations/customer_relations/contacts/create_spec.rb6
-rw-r--r--spec/graphql/mutations/discussions/toggle_resolve_spec.rb8
-rw-r--r--spec/graphql/mutations/environments/canary_ingress/update_spec.rb2
-rw-r--r--spec/graphql/mutations/release_asset_links/delete_spec.rb4
-rw-r--r--spec/graphql/mutations/release_asset_links/update_spec.rb4
-rw-r--r--spec/graphql/mutations/timelogs/delete_spec.rb6
-rw-r--r--spec/graphql/resolvers/concerns/resolves_ids_spec.rb14
-rw-r--r--spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/design_management/design_resolver_spec.rb4
-rw-r--r--spec/graphql/resolvers/design_management/designs_resolver_spec.rb4
-rw-r--r--spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/timelog_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/work_item_resolver_spec.rb2
-rw-r--r--spec/graphql/types/terraform/state_version_type_spec.rb4
-rw-r--r--spec/models/personal_access_token_spec.rb8
-rw-r--r--spec/requests/api/graphql/boards/board_lists_query_spec.rb6
-rw-r--r--spec/requests/api/graphql/ci/job_spec.rb15
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb8
-rw-r--r--spec/requests/api/graphql/current_user_todos_spec.rb8
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb4
-rw-r--r--spec/requests/api/graphql/group/group_members_spec.rb8
-rw-r--r--spec/requests/api/graphql/group/merge_requests_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/milestones_spec.rb8
-rw-r--r--spec/requests/api/graphql/issue/issue_spec.rb5
-rw-r--r--spec/requests/api/graphql/merge_request/merge_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb22
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb8
-rw-r--r--spec/requests/api/graphql/mutations/todos/restore_many_spec.rb8
-rw-r--r--spec/requests/api/graphql/packages/conan_spec.rb21
-rw-r--r--spec/requests/api/graphql/packages/maven_spec.rb8
-rw-r--r--spec/requests/api/graphql/packages/nuget_spec.rb17
-rw-r--r--spec/requests/api/graphql/packages/pypi_spec.rb5
-rw-r--r--spec/requests/api/graphql/project/alert_management/integrations_spec.rb57
-rw-r--r--spec/requests/api/graphql/project/cluster_agents_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/version_spec.rb29
-rw-r--r--spec/requests/api/graphql/project/issue/designs/designs_spec.rb14
-rw-r--r--spec/requests/api/graphql/project/issue/designs/notes_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue_spec.rb7
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb14
-rw-r--r--spec/requests/api/graphql/project/milestones_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/pipeline_spec.rb19
-rw-r--r--spec/requests/api/graphql/project/project_members_spec.rb9
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb40
-rw-r--r--spec/requests/api/graphql/project/terraform/state_spec.rb18
-rw-r--r--spec/requests/api/graphql/project/terraform/states_spec.rb17
-rw-r--r--spec/requests/api/graphql/query_spec.rb16
-rw-r--r--spec/requests/api/graphql/user/starred_projects_query_spec.rb18
-rw-r--r--spec/requests/api/graphql/user_query_spec.rb72
-rw-r--r--spec/requests/api/graphql/users_spec.rb24
-rw-r--r--spec/spec_helper.rb3
-rw-r--r--spec/support/graphql/arguments.rb4
-rw-r--r--spec/support/helpers/graphql_helpers.rb94
-rw-r--r--spec/support/shared_contexts/graphql/requests/packages_shared_context.rb2
-rw-r--r--spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb10
-rw-r--r--spec/support_specs/helpers/graphql_helpers_spec.rb75
-rw-r--r--spec/views/shared/access_tokens/_table.html.haml_spec.rb41
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb1
173 files changed, 1758 insertions, 1676 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index 3e57949eeda..1a4d182c3d1 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -118,14 +118,10 @@ Dangerfile @gl-quality/eng-prod
/spec/models/clusters/applications/cilium_spec.rb @gitlab-org/protect/container-security-backend
/ee/app/controllers/projects/security/network_policies_controller.rb @gitlab-org/protect/container-security-backend
/ee/spec/controllers/projects/security/network_policies_controller_spec.rb @gitlab-org/protect/container-security-backend
-/ee/app/workers/network_policy_metrics_worker.rb @gitlab-org/protect/container-security-backend
-/ee/spec/workers/network_policy_metrics_worker_spec.rb @gitlab-org/protect/container-security-backend
/ee/app/services/network_policies/** @gitlab-org/protect/container-security-backend
/ee/spec/services/network_policies/** @gitlab-org/protect/container-security-backend
/ee/app/services/security/orchestration/** @gitlab-org/protect/container-security-backend
/ee/spec/services/security/orchestration/** @gitlab-org/protect/container-security-backend
-/ee/lib/gitlab/usage_data_counters/network_policy_counter.rb @gitlab-org/protect/container-security-backend
-/ee/spec/lib/gitlab/usage_data_counters/network_policy_counter_spec.rb @gitlab-org/protect/container-security-backend
^[Code Owners]
/ee/lib/gitlab/code_owners.rb @reprazent @kerrizor @garyh
diff --git a/.rubocop_todo/layout/hash_alignment.yml b/.rubocop_todo/layout/hash_alignment.yml
index aafacd5f265..b13a561958e 100644
--- a/.rubocop_todo/layout/hash_alignment.yml
+++ b/.rubocop_todo/layout/hash_alignment.yml
@@ -383,7 +383,6 @@ Layout/HashAlignment:
- 'ee/lib/gitlab/elastic/helper.rb'
- 'ee/lib/gitlab/elastic/indexer.rb'
- 'ee/lib/gitlab/geo/replication/base_transfer.rb'
- - 'ee/lib/gitlab/prometheus/queries/packet_flow_query.rb'
- 'ee/spec/controllers/ee/projects/variables_controller_spec.rb'
- 'ee/spec/controllers/groups/epic_boards_controller_spec.rb'
- 'ee/spec/controllers/groups/issues_controller_spec.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index d46c7a6da62..f80c3f20437 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -4424,7 +4424,6 @@ Layout/LineLength:
- 'spec/features/projects/files/user_browses_files_spec.rb'
- 'spec/features/projects/files/user_edits_files_spec.rb'
- 'spec/features/projects/infrastructure_registry_spec.rb'
- - 'spec/features/projects/integrations/prometheus_external_alerts_spec.rb'
- 'spec/features/projects/integrations/user_activates_issue_tracker_spec.rb'
- 'spec/features/projects/integrations/user_activates_jira_spec.rb'
- 'spec/features/projects/integrations/user_uses_inherited_settings_spec.rb'
diff --git a/.rubocop_todo/rails/time_zone.yml b/.rubocop_todo/rails/time_zone.yml
index 86d0632ac47..ff97dfeb444 100644
--- a/.rubocop_todo/rails/time_zone.yml
+++ b/.rubocop_todo/rails/time_zone.yml
@@ -13,7 +13,6 @@ Rails/TimeZone:
- ee/lib/gitlab/geo/log_cursor/logger.rb
- ee/lib/gitlab/geo/oauth/login_state.rb
- ee/lib/gitlab/prometheus/queries/cluster_query.rb
- - ee/lib/gitlab/prometheus/queries/packet_flow_query.rb
- ee/spec/lib/ee/gitlab/checks/push_rules/commit_check_spec.rb
- ee/spec/lib/ee/gitlab/ci/pipeline/quota/job_activity_spec.rb
- ee/spec/lib/gitlab/analytics/cycle_analytics/data_collector_spec.rb
@@ -44,7 +43,6 @@ Rails/TimeZone:
- ee/spec/lib/gitlab/git_access_spec.rb
- ee/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb
- ee/spec/lib/gitlab/prometheus/queries/cluster_query_spec.rb
- - ee/spec/lib/gitlab/prometheus/queries/packet_flow_query_spec.rb
- lib/api/helpers.rb
- lib/api/sidekiq_metrics.rb
- lib/backup/manager.rb
diff --git a/.rubocop_todo/rspec/verified_doubles.yml b/.rubocop_todo/rspec/verified_doubles.yml
index e272fbc555c..70fb8414f55 100644
--- a/.rubocop_todo/rspec/verified_doubles.yml
+++ b/.rubocop_todo/rspec/verified_doubles.yml
@@ -90,8 +90,6 @@ RSpec/VerifiedDoubles:
- ee/spec/lib/gitlab/middleware/ip_restrictor_spec.rb
- ee/spec/lib/gitlab/patch/legacy_database_config_spec.rb
- ee/spec/lib/gitlab/prometheus/queries/cluster_query_spec.rb
- - ee/spec/lib/gitlab/prometheus/queries/packet_flow_metrics_query_spec.rb
- - ee/spec/lib/gitlab/prometheus/queries/packet_flow_query_spec.rb
- ee/spec/lib/gitlab/subscription_portal/clients/rest_spec.rb
- ee/spec/lib/sidebars/groups/menus/analytics_menu_spec.rb
- ee/spec/lib/system_check/app/elasticsearch_check_spec.rb
diff --git a/.rubocop_todo/style/percent_literal_delimiters.yml b/.rubocop_todo/style/percent_literal_delimiters.yml
index 4673f0d71d2..bf50c4c1922 100644
--- a/.rubocop_todo/style/percent_literal_delimiters.yml
+++ b/.rubocop_todo/style/percent_literal_delimiters.yml
@@ -324,7 +324,6 @@ Style/PercentLiteralDelimiters:
- 'ee/lib/gitlab/ci/parsers/security/formatters/dast.rb'
- 'ee/lib/gitlab/geo.rb'
- 'ee/lib/gitlab/geo/replicator.rb'
- - 'ee/lib/gitlab/prometheus/queries/packet_flow_query.rb'
- 'ee/lib/gitlab/usage/metrics/instrumentations/license_metric.rb'
- 'ee/lib/tasks/gitlab/elastic/test.rake'
- 'ee/spec/config/metrics/every_metric_definition_spec.rb'
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue b/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue
index 46c15de6b2c..e35fbf14de5 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue
@@ -3,6 +3,9 @@ import { GlButtonGroup } from '@gitlab/ui';
import { BubbleMenu } from '@tiptap/vue-2';
import { BUBBLE_MENU_TRACKING_ACTION } from '../../constants';
import trackUIControl from '../../services/track_ui_control';
+import Image from '../../extensions/image';
+import Audio from '../../extensions/audio';
+import Video from '../../extensions/video';
import Code from '../../extensions/code';
import CodeBlockHighlight from '../../extensions/code_block_highlight';
import Diagram from '../../extensions/diagram';
@@ -24,7 +27,15 @@ export default {
shouldShow: ({ editor, from, to }) => {
if (from === to) return false;
- const exclude = [Code.name, CodeBlockHighlight.name, Diagram.name, Frontmatter.name];
+ const exclude = [
+ Code.name,
+ CodeBlockHighlight.name,
+ Diagram.name,
+ Frontmatter.name,
+ Image.name,
+ Audio.name,
+ Video.name,
+ ];
return !exclude.some((type) => editor.isActive(type));
},
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/link.vue b/app/assets/javascripts/content_editor/components/bubble_menus/link.vue
index 2f446832516..abd225c0b1a 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/link.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/link.vue
@@ -26,7 +26,7 @@ export default {
directives: {
GlTooltip,
},
- inject: ['tiptapEditor'],
+ inject: ['tiptapEditor', 'contentEditor'],
data() {
return {
linkHref: undefined,
@@ -57,9 +57,11 @@ export default {
this.isEditing = true;
},
- endEditingLink() {
+ async endEditingLink() {
this.isEditing = false;
+ this.linkHref = await this.contentEditor.resolveLink(this.linkCanonicalSrc);
+
if (!this.linkCanonicalSrc && !this.linkHref) {
this.removeLink();
}
@@ -70,7 +72,7 @@ export default {
this.updateLinkToState();
},
- saveEditedLink() {
+ async saveEditedLink() {
if (!this.linkCanonicalSrc) {
this.removeLink();
} else {
@@ -166,12 +168,12 @@ export default {
@click="removeLink"
/>
</gl-button-group>
- <gl-form v-else class="bubble-menu-form gl-p-4 gl-w-100" @submit="saveEditedLink">
- <gl-form-group data-testid="link-href-group" :label="__('URL')" label-for="link-href">
- <gl-form-input id="link-href" v-model="linkCanonicalSrc" />
+ <gl-form v-else class="bubble-menu-form gl-p-4 gl-w-100" @submit.prevent="saveEditedLink">
+ <gl-form-group :label="__('URL')" label-for="link-href">
+ <gl-form-input id="link-href" v-model="linkCanonicalSrc" data-testid="link-href" />
</gl-form-group>
- <gl-form-group data-testid="link-title-group" :label="__('Title')" label-for="link-title">
- <gl-form-input id="link-title" v-model="linkTitle" />
+ <gl-form-group :label="__('Title')" label-for="link-title">
+ <gl-form-input id="link-title" v-model="linkTitle" data-testid="link-title" />
</gl-form-group>
<div class="gl-display-flex gl-justify-content-end">
<gl-button class="gl-mr-3" data-testid="cancel-link" @click="cancelEditingLink">
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/media.vue b/app/assets/javascripts/content_editor/components/bubble_menus/media.vue
new file mode 100644
index 00000000000..d1bc5c83948
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/media.vue
@@ -0,0 +1,288 @@
+<script>
+import {
+ GlLink,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlLoadingIcon,
+ GlButton,
+ GlButtonGroup,
+ GlTooltipDirective as GlTooltip,
+} from '@gitlab/ui';
+import { BubbleMenu } from '@tiptap/vue-2';
+import { __ } from '~/locale';
+import Audio from '../../extensions/audio';
+import Image from '../../extensions/image';
+import Video from '../../extensions/video';
+import EditorStateObserver from '../editor_state_observer.vue';
+import { acceptedMimes } from '../../services/upload_helpers';
+
+const MEDIA_TYPES = [Audio.name, Image.name, Video.name];
+
+export default {
+ i18n: {
+ copySourceLabels: {
+ [Audio.name]: __('Copy audio URL'),
+ [Image.name]: __('Copy image URL'),
+ [Video.name]: __('Copy video URL'),
+ },
+ editLabels: {
+ [Audio.name]: __('Edit audio description'),
+ [Image.name]: __('Edit image description'),
+ [Video.name]: __('Edit video description'),
+ },
+ replaceLabels: {
+ [Audio.name]: __('Replace audio'),
+ [Image.name]: __('Replace image'),
+ [Video.name]: __('Replace video'),
+ },
+ deleteLabels: {
+ [Audio.name]: __('Delete audio'),
+ [Image.name]: __('Delete image'),
+ [Video.name]: __('Delete video'),
+ },
+ },
+ components: {
+ BubbleMenu,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlLink,
+ GlLoadingIcon,
+ GlButton,
+ GlButtonGroup,
+ EditorStateObserver,
+ },
+ directives: {
+ GlTooltip,
+ },
+ inject: ['tiptapEditor', 'contentEditor'],
+ data() {
+ return {
+ mediaType: undefined,
+ mediaSrc: undefined,
+ mediaCanonicalSrc: undefined,
+ mediaAlt: undefined,
+ mediaTitle: undefined,
+
+ isEditing: false,
+ isUpdating: false,
+ isUploading: false,
+ };
+ },
+ computed: {
+ copySourceLabel() {
+ return this.$options.i18n.copySourceLabels[this.mediaType];
+ },
+ editLabel() {
+ return this.$options.i18n.editLabels[this.mediaType];
+ },
+ replaceLabel() {
+ return this.$options.i18n.replaceLabels[this.mediaType];
+ },
+ deleteLabel() {
+ return this.$options.i18n.deleteLabels[this.mediaType];
+ },
+ showProgressIndicator() {
+ return this.isUploading || this.isUpdating;
+ },
+ },
+ methods: {
+ shouldShow() {
+ const shouldShow = MEDIA_TYPES.some((type) => this.tiptapEditor.isActive(type));
+
+ if (!shouldShow) this.isEditing = false;
+
+ return shouldShow;
+ },
+
+ startEditingMedia() {
+ this.isEditing = true;
+ },
+
+ endEditingMedia() {
+ this.isEditing = false;
+
+ this.updateMediaInfoToState();
+ },
+
+ cancelEditingMedia() {
+ this.endEditingMedia();
+ this.updateMediaInfoToState();
+ },
+
+ async saveEditedMedia() {
+ this.isUpdating = true;
+
+ this.mediaSrc = await this.contentEditor.resolveLink(this.mediaCanonicalSrc);
+
+ const position = this.tiptapEditor.state.selection.from;
+
+ this.tiptapEditor
+ .chain()
+ .focus()
+ .updateAttributes(this.mediaType, {
+ src: this.mediaSrc,
+ alt: this.mediaAlt,
+ canonicalSrc: this.mediaCanonicalSrc,
+ title: this.mediaTitle,
+ })
+ .run();
+
+ this.tiptapEditor.commands.setNodeSelection(position);
+
+ this.endEditingMedia();
+
+ this.isUpdating = false;
+ },
+
+ async updateMediaInfoToState() {
+ this.mediaType = MEDIA_TYPES.find((type) => this.tiptapEditor.isActive(type));
+
+ if (!this.mediaType) return;
+
+ this.isUpdating = true;
+
+ const { src, title, alt, canonicalSrc, uploading } = this.tiptapEditor.getAttributes(
+ this.mediaType,
+ );
+
+ this.mediaTitle = title;
+ this.mediaAlt = alt;
+ this.mediaCanonicalSrc = canonicalSrc || src;
+ this.isUploading = uploading;
+ this.mediaSrc = await this.contentEditor.resolveLink(this.mediaCanonicalSrc);
+
+ this.isUpdating = false;
+ },
+
+ replaceMedia() {
+ this.$refs.fileSelector.click();
+ },
+
+ onFileSelect(e) {
+ this.tiptapEditor
+ .chain()
+ .focus()
+ .deleteSelection()
+ .uploadAttachment({
+ file: e.target.files[0],
+ })
+ .run();
+
+ this.$refs.fileSelector.value = '';
+ },
+
+ copyMediaSrc() {
+ navigator.clipboard.writeText(this.mediaCanonicalSrc);
+ },
+
+ deleteMedia() {
+ this.tiptapEditor.chain().focus().deleteSelection().run();
+ },
+ },
+
+ acceptedMimes,
+};
+</script>
+<template>
+ <bubble-menu
+ data-testid="media-bubble-menu"
+ class="gl-shadow gl-rounded-base gl-bg-white"
+ :editor="tiptapEditor"
+ plugin-key="bubbleMenuMedia"
+ :should-show="() => shouldShow()"
+ >
+ <editor-state-observer @transaction="updateMediaInfoToState">
+ <gl-button-group v-if="!isEditing" class="gl-display-flex gl-align-items-center">
+ <gl-loading-icon v-if="showProgressIndicator" class="gl-pl-4 gl-pr-3" />
+ <input
+ ref="fileSelector"
+ type="file"
+ name="content_editor_image"
+ :accept="$options.acceptedMimes[mediaType]"
+ class="gl-display-none"
+ data-qa-selector="file_upload_field"
+ @change="onFileSelect"
+ />
+
+ <gl-link
+ v-if="!showProgressIndicator"
+ v-gl-tooltip
+ :href="mediaSrc"
+ :aria-label="mediaCanonicalSrc"
+ :title="mediaCanonicalSrc"
+ target="_blank"
+ class="gl-px-3 gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis"
+ >
+ {{ mediaCanonicalSrc }}
+ </gl-link>
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="copy-media-src"
+ :aria-label="copySourceLabel"
+ :title="copySourceLabel"
+ icon="copy-to-clipboard"
+ @click="copyMediaSrc"
+ />
+ <gl-button
+ v-if="!showProgressIndicator"
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="edit-media"
+ :aria-label="editLabel"
+ :title="editLabel"
+ icon="pencil"
+ @click="startEditingMedia"
+ />
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="replace-media"
+ :aria-label="replaceLabel"
+ :title="replaceLabel"
+ icon="upload"
+ @click="replaceMedia"
+ />
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="delete-media"
+ :aria-label="deleteLabel"
+ :title="deleteLabel"
+ icon="remove"
+ @click="deleteMedia"
+ />
+ </gl-button-group>
+ <gl-form v-else class="bubble-menu-form gl-p-4 gl-w-100" @submit.prevent="saveEditedMedia">
+ <gl-form-group :label="__('URL')" label-for="media-src">
+ <gl-form-input id="media-src" v-model="mediaCanonicalSrc" data-testid="media-src" />
+ </gl-form-group>
+ <gl-form-group :label="__('Description (alt text)')" label-for="media-alt">
+ <gl-form-input id="media-alt" v-model="mediaAlt" data-testid="media-alt" />
+ </gl-form-group>
+ <gl-form-group :label="__('Title')" label-for="media-title">
+ <gl-form-input id="media-title" v-model="mediaTitle" data-testid="media-title" />
+ </gl-form-group>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button
+ class="gl-mr-3"
+ data-testid="cancel-editing-media"
+ @click="cancelEditingMedia"
+ >{{ __('Cancel') }}</gl-button
+ >
+ <gl-button variant="confirm" type="submit">{{ __('Apply') }}</gl-button>
+ </div>
+ </gl-form>
+ </editor-state-observer>
+ </bubble-menu>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index a3247298b19..74ae37b6d06 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -7,6 +7,7 @@ import EditorStateObserver from './editor_state_observer.vue';
import FormattingBubbleMenu from './bubble_menus/formatting.vue';
import CodeBlockBubbleMenu from './bubble_menus/code_block.vue';
import LinkBubbleMenu from './bubble_menus/link.vue';
+import MediaBubbleMenu from './bubble_menus/media.vue';
import TopToolbar from './top_toolbar.vue';
import LoadingIndicator from './loading_indicator.vue';
@@ -20,6 +21,7 @@ export default {
FormattingBubbleMenu,
CodeBlockBubbleMenu,
LinkBubbleMenu,
+ MediaBubbleMenu,
EditorStateObserver,
},
props: {
@@ -95,6 +97,7 @@ export default {
<formatting-bubble-menu />
<code-block-bubble-menu />
<link-bubble-menu />
+ <media-bubble-menu />
<tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
<loading-indicator />
</div>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/media.vue b/app/assets/javascripts/content_editor/components/wrappers/media.vue
deleted file mode 100644
index 37119bdd066..00000000000
--- a/app/assets/javascripts/content_editor/components/wrappers/media.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { NodeViewWrapper } from '@tiptap/vue-2';
-
-const tagNameMap = {
- image: 'img',
- video: 'video',
- audio: 'audio',
-};
-
-export default {
- name: 'MediaWrapper',
- components: {
- NodeViewWrapper,
- GlLoadingIcon,
- },
- props: {
- node: {
- type: Object,
- required: true,
- },
- },
- computed: {
- tagName() {
- return tagNameMap[this.node.type.name] || 'img';
- },
- },
-};
-</script>
-<template>
- <node-view-wrapper class="gl-display-inline-block">
- <span class="gl-relative" :class="{ [`media-container ${tagName}-container`]: true }">
- <gl-loading-icon v-if="node.attrs.uploading" class="gl-absolute gl-left-50p gl-top-half" />
- <component
- :is="tagName"
- data-testid="media"
- :class="{
- 'gl-max-w-full gl-h-auto': tagName !== 'audio',
- 'gl-opacity-5': node.attrs.uploading,
- }"
- :title="node.attrs.title || node.attrs.alt"
- :alt="node.attrs.alt"
- :src="node.attrs.src"
- controls="true"
- />
- <a v-if="tagName !== 'img'" :href="node.attrs.canonicalSrc || node.attrs.src" @click.prevent>
- {{ node.attrs.title || node.attrs.alt }}
- </a>
- </span>
- </node-view-wrapper>
-</template>
diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js
index 311db8151cb..25f976f524f 100644
--- a/app/assets/javascripts/content_editor/extensions/image.js
+++ b/app/assets/javascripts/content_editor/extensions/image.js
@@ -1,6 +1,4 @@
import { Image } from '@tiptap/extension-image';
-import { VueNodeViewRenderer } from '@tiptap/vue-2';
-import MediaWrapper from '../components/wrappers/media.vue';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
const resolveImageEl = (element) =>
@@ -77,7 +75,4 @@ export default Image.extend({
},
];
},
- addNodeView() {
- return VueNodeViewRenderer(MediaWrapper);
- },
});
diff --git a/app/assets/javascripts/content_editor/extensions/playable.js b/app/assets/javascripts/content_editor/extensions/playable.js
index 2c5269377c5..ed343d8acf8 100644
--- a/app/assets/javascripts/content_editor/extensions/playable.js
+++ b/app/assets/javascripts/content_editor/extensions/playable.js
@@ -1,8 +1,6 @@
/* eslint-disable @gitlab/require-i18n-strings */
import { Node } from '@tiptap/core';
-import { VueNodeViewRenderer } from '@tiptap/vue-2';
-import MediaWrapper from '../components/wrappers/media.vue';
const queryPlayableElement = (element, mediaType) => element.querySelector(mediaType);
@@ -68,8 +66,4 @@ export default Node.create({
['a', { href: node.attrs.src }, node.attrs.title || node.attrs.alt || ''],
];
},
-
- addNodeView() {
- return VueNodeViewRenderer(MediaWrapper);
- },
});
diff --git a/app/assets/javascripts/content_editor/services/asset_resolver.js b/app/assets/javascripts/content_editor/services/asset_resolver.js
new file mode 100644
index 00000000000..942457b9664
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/asset_resolver.js
@@ -0,0 +1,13 @@
+import { memoize } from 'lodash';
+
+export default ({ renderMarkdown }) => ({
+ resolveUrl: memoize(async (canonicalSrc) => {
+ const html = await renderMarkdown(`[link](${canonicalSrc})`);
+ if (!html) return canonicalSrc;
+
+ const parser = new DOMParser();
+ const { body } = parser.parseFromString(html, 'text/html');
+
+ return body.querySelector('a').getAttribute('href');
+ }),
+});
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index 21843c482a8..b993851a92f 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -3,12 +3,13 @@ import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } fro
/* eslint-disable no-underscore-dangle */
export class ContentEditor {
- constructor({ tiptapEditor, serializer, deserializer, eventHub, languageLoader }) {
+ constructor({ tiptapEditor, serializer, deserializer, assetResolver, eventHub, languageLoader }) {
this._tiptapEditor = tiptapEditor;
this._serializer = serializer;
this._deserializer = deserializer;
this._eventHub = eventHub;
this._languageLoader = languageLoader;
+ this._assetResolver = assetResolver;
}
get tiptapEditor() {
@@ -34,22 +35,27 @@ export class ContentEditor {
this._eventHub.dispose();
}
+ deserialize(serializedContent) {
+ const { _tiptapEditor: editor, _deserializer: deserializer } = this;
+
+ return deserializer.deserialize({
+ schema: editor.schema,
+ content: serializedContent,
+ });
+ }
+
+ resolveAssetUrl(canonicalSrc) {
+ return this._assetResolver.resolveUrl(canonicalSrc);
+ }
+
async setSerializedContent(serializedContent) {
- const {
- _tiptapEditor: editor,
- _deserializer: deserializer,
- _eventHub: eventHub,
- _languageLoader: languageLoader,
- } = this;
+ const { _tiptapEditor: editor, _eventHub: eventHub, _languageLoader: languageLoader } = this;
const { doc, tr } = editor.state;
const selection = TextSelection.create(doc, 0, doc.content.size);
try {
eventHub.$emit(LOADING_CONTENT_EVENT);
- const result = await deserializer.deserialize({
- schema: editor.schema,
- content: serializedContent,
- });
+ const result = await this.deserialize(serializedContent);
if (Object.keys(result).length !== 0) {
const { document, languages } = result;
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index 28041504e3c..adb1398b2c4 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -60,6 +60,7 @@ import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
import createGlApiMarkdownDeserializer from './gl_api_markdown_deserializer';
import createRemarkMarkdownDeserializer from './remark_markdown_deserializer';
+import createAssetResolver from './asset_resolver';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
import languageLoader from './code_block_language_loader';
@@ -152,6 +153,14 @@ export const createContentEditor = ({
: createGlApiMarkdownDeserializer({
render: renderMarkdown,
});
+ const assetResolver = createAssetResolver({ renderMarkdown });
- return new ContentEditor({ tiptapEditor, serializer, eventHub, deserializer, languageLoader });
+ return new ContentEditor({
+ tiptapEditor,
+ serializer,
+ eventHub,
+ deserializer,
+ languageLoader,
+ assetResolver,
+ });
};
diff --git a/app/assets/javascripts/content_editor/services/upload_helpers.js b/app/assets/javascripts/content_editor/services/upload_helpers.js
index ed2c4b39131..09f0738b51b 100644
--- a/app/assets/javascripts/content_editor/services/upload_helpers.js
+++ b/app/assets/javascripts/content_editor/services/upload_helpers.js
@@ -70,6 +70,8 @@ const uploadContent = async ({ type, editor, file, uploadsPath, renderMarkdown,
const position = state.selection.from - 1;
const { tr } = state;
+ editor.commands.setNodeSelection(position);
+
try {
const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
@@ -81,6 +83,8 @@ const uploadContent = async ({ type, editor, file, uploadsPath, renderMarkdown,
canonicalSrc,
}),
);
+
+ editor.commands.setNodeSelection(position);
} catch (e) {
editor.commands.deleteRange({ from: position, to: position + 1 });
eventHub.$emit('alert', {
diff --git a/app/assets/javascripts/content_editor/services/utils.js b/app/assets/javascripts/content_editor/services/utils.js
index b3856b0dd74..e352fa8a9db 100644
--- a/app/assets/javascripts/content_editor/services/utils.js
+++ b/app/assets/javascripts/content_editor/services/utils.js
@@ -15,7 +15,7 @@ export const hasSelection = (tiptapEditor) => {
* @returns {string}
*/
export const extractFilename = (src) => {
- return src.replace(/^.*\/|\..+?$/g, '');
+ return src.replace(/^.*\/|\.[^.]+?$/g, '');
};
export const readFileAsDataURL = (file) => {
diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js
index 2048d3dfc37..64df0d07d74 100644
--- a/app/assets/javascripts/pages/projects/services/edit/index.js
+++ b/app/assets/javascripts/pages/projects/services/edit/index.js
@@ -1,5 +1,4 @@
import initIntegrationSettingsForm from '~/integrations/edit';
-import PrometheusAlerts from '~/prometheus_alerts';
import CustomMetrics from '~/prometheus_metrics/custom_metrics';
initIntegrationSettingsForm();
@@ -10,5 +9,3 @@ if (prometheusSettingsWrapper) {
const customMetrics = new CustomMetrics(prometheusSettingsSelector);
customMetrics.init();
}
-
-PrometheusAlerts();
diff --git a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
deleted file mode 100644
index befbca48736..00000000000
--- a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
+++ /dev/null
@@ -1,149 +0,0 @@
-<script>
-import {
- GlButton,
- GlFormGroup,
- GlFormInput,
- GlModal,
- GlModalDirective,
- GlSprintf,
- GlLink,
-} from '@gitlab/ui';
-import createFlash from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-
-export default {
- copyToClipboard: __('Copy'),
- components: {
- GlButton,
- GlFormGroup,
- GlFormInput,
- GlModal,
- ClipboardButton,
- GlSprintf,
- GlLink,
- },
- directives: {
- 'gl-modal': GlModalDirective,
- },
- props: {
- initialAuthorizationKey: {
- type: String,
- required: false,
- default: '',
- },
- changeKeyUrl: {
- type: String,
- required: true,
- },
- notifyUrl: {
- type: String,
- required: true,
- },
- learnMoreUrl: {
- type: String,
- required: true,
- },
- disabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- authorizationKey: this.initialAuthorizationKey,
- };
- },
- methods: {
- resetKey() {
- axios
- .post(this.changeKeyUrl)
- .then((res) => {
- this.authorizationKey = res.data.token;
- })
- .catch(() => {
- createFlash({
- message: __('Failed to reset key. Please try again.'),
- });
- });
- },
- },
-};
-</script>
-
-<template>
- <div class="row py-4 border-top js-prometheus-alerts">
- <div class="col-lg-3">
- <h4 class="mt-0">
- {{ __('Alerts') }}
- </h4>
- <p>
- {{ __('Receive alerts from manually configured Prometheus servers.') }}
- </p>
- </div>
- <div class="col-lg-9">
- <gl-sprintf
- :message="
- __(
- 'To receive alerts from manually configured Prometheus services, add the following URL and Authorization key to your Prometheus webhook config file. Learn more about %{linkStart}configuring Prometheus%{linkEnd} to send alerts to GitLab.',
- )
- "
- >
- <template #link="{ content }">
- <gl-link :href="learnMoreUrl" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- <gl-form-group :label="__('URL')" label-for="notify-url" label-class="label-bold">
- <div class="input-group">
- <gl-form-input id="notify-url" :readonly="true" :value="notifyUrl" />
- <span class="input-group-append">
- <clipboard-button
- :text="notifyUrl"
- :title="$options.copyToClipboard"
- :disabled="disabled"
- />
- </span>
- </div>
- </gl-form-group>
- <gl-form-group
- :label="__('Authorization key')"
- label-for="authorization-key"
- label-class="label-bold"
- >
- <div class="input-group">
- <gl-form-input id="authorization-key" :readonly="true" :value="authorizationKey" />
- <span class="input-group-append">
- <clipboard-button
- :text="authorizationKey"
- :title="$options.copyToClipboard"
- :disabled="disabled"
- />
- </span>
- </div>
- </gl-form-group>
- <template v-if="authorizationKey.length > 0">
- <gl-modal
- modal-id="authKeyModal"
- :title="__('Reset authorization key?')"
- :ok-title="__('Reset authorization key')"
- ok-variant="danger"
- @ok="resetKey"
- >
- {{
- __(
- 'Resetting the authorization key will invalidate the previous key. Existing alert configurations will need to be updated with the new key.',
- )
- }}
- </gl-modal>
- <gl-button v-gl-modal.authKeyModal class="js-reset-auth-key" :disabled="disabled">{{
- __('Reset key')
- }}</gl-button>
- </template>
- <gl-button v-else :disabled="disabled" class="js-reset-auth-key" @click="resetKey">{{
- __('Generate key')
- }}</gl-button>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/prometheus_alerts/index.js b/app/assets/javascripts/prometheus_alerts/index.js
deleted file mode 100644
index 7efe6ed186b..00000000000
--- a/app/assets/javascripts/prometheus_alerts/index.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import Vue from 'vue';
-import ResetKey from './components/reset_key.vue';
-
-export default () => {
- const el = document.querySelector('#js-settings-prometheus-alerts');
-
- if (!el) {
- return;
- }
-
- const { authorizationKey, changeKeyUrl, notifyUrl, learnMoreUrl, disabled } = el.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- render(createElement) {
- return createElement(ResetKey, {
- props: {
- initialAuthorizationKey: authorizationKey,
- changeKeyUrl,
- notifyUrl,
- learnMoreUrl,
- disabled,
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index accc9926a57..c2bb635e056 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -38,7 +38,7 @@ import {
} from '../runner_search_utils';
import { captureException } from '../sentry_utils';
-const runnersCountSmartQuery = {
+const countSmartQuery = () => ({
query: runnersAdminCountQuery,
fetchPolicy: fetchPolicies.NETWORK_ONLY,
update(data) {
@@ -47,6 +47,39 @@ const runnersCountSmartQuery = {
error(error) {
this.reportToSentry(error);
},
+});
+
+const tabCountSmartQuery = ({ type }) => {
+ return {
+ ...countSmartQuery(),
+ variables() {
+ return {
+ ...this.countVariables,
+ type,
+ };
+ },
+ };
+};
+
+const statusCountSmartQuery = ({ status, name }) => {
+ return {
+ ...countSmartQuery(),
+ skip() {
+ // skip if filtering by status and not using _this_ status as filter
+ if (this.countVariables.status && this.countVariables.status !== status) {
+ // reset count for given status
+ this[name] = null;
+ return true;
+ }
+ return false;
+ },
+ variables() {
+ return {
+ ...this.countVariables,
+ status,
+ };
+ },
+ };
};
export default {
@@ -101,65 +134,30 @@ export default {
this.reportToSentry(error);
},
},
+
+ // Tabs counts
allRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: null,
- };
- },
+ ...tabCountSmartQuery({ type: null }),
},
instanceRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: INSTANCE_TYPE,
- };
- },
+ ...tabCountSmartQuery({ type: INSTANCE_TYPE }),
},
groupRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: GROUP_TYPE,
- };
- },
+ ...tabCountSmartQuery({ type: GROUP_TYPE }),
},
projectRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: PROJECT_TYPE,
- };
- },
+ ...tabCountSmartQuery({ type: PROJECT_TYPE }),
},
+
+ // Runner stats
onlineRunnersTotal: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- status: STATUS_ONLINE,
- };
- },
+ ...statusCountSmartQuery({ status: STATUS_ONLINE, name: 'onlineRunnersTotal' }),
},
offlineRunnersTotal: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- status: STATUS_OFFLINE,
- };
- },
+ ...statusCountSmartQuery({ status: STATUS_OFFLINE, name: 'offlineRunnersTotal' }),
},
staleRunnersTotal: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- status: STATUS_STALE,
- };
- },
+ ...statusCountSmartQuery({ status: STATUS_STALE, name: 'staleRunnersTotal' }),
},
},
computed: {
@@ -263,12 +261,6 @@ export default {
</script>
<template>
<div>
- <runner-stats
- :online-runners-count="onlineRunnersTotal"
- :offline-runners-count="offlineRunnersTotal"
- :stale-runners-count="staleRunnersTotal"
- />
-
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
>
@@ -300,6 +292,12 @@ export default {
:namespace="$options.filteredSearchNamespace"
/>
+ <runner-stats
+ :online-runners-count="onlineRunnersTotal"
+ :offline-runners-count="offlineRunnersTotal"
+ :stale-runners-count="staleRunnersTotal"
+ />
+
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
</div>
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index b299d7c40fe..b5bd4b111fd 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -34,7 +34,7 @@ import {
} from '../runner_search_utils';
import { captureException } from '../sentry_utils';
-const runnersCountSmartQuery = {
+const countSmartQuery = () => ({
query: groupRunnersCountQuery,
fetchPolicy: fetchPolicies.NETWORK_ONLY,
update(data) {
@@ -43,6 +43,39 @@ const runnersCountSmartQuery = {
error(error) {
this.reportToSentry(error);
},
+});
+
+const tabCountSmartQuery = ({ type }) => {
+ return {
+ ...countSmartQuery(),
+ variables() {
+ return {
+ ...this.countVariables,
+ type,
+ };
+ },
+ };
+};
+
+const statusCountSmartQuery = ({ status, name }) => {
+ return {
+ ...countSmartQuery(),
+ skip() {
+ // skip if filtering by status and not using _this_ status as filter
+ if (this.countVariables.status && this.countVariables.status !== status) {
+ // reset count for given status
+ this[name] = null;
+ return true;
+ }
+ return false;
+ },
+ variables() {
+ return {
+ ...this.countVariables,
+ status,
+ };
+ },
+ };
};
export default {
@@ -116,59 +149,27 @@ export default {
this.reportToSentry(error);
},
},
- onlineRunnersTotal: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- groupFullPath: this.groupFullPath,
- status: STATUS_ONLINE,
- };
- },
- },
- offlineRunnersTotal: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- groupFullPath: this.groupFullPath,
- status: STATUS_OFFLINE,
- };
- },
- },
- staleRunnersTotal: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- groupFullPath: this.groupFullPath,
- status: STATUS_STALE,
- };
- },
- },
+
+ // Tabs counts
allRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: null,
- };
- },
+ ...tabCountSmartQuery({ type: null }),
},
groupRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: GROUP_TYPE,
- };
- },
+ ...tabCountSmartQuery({ type: GROUP_TYPE }),
},
projectRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: PROJECT_TYPE,
- };
- },
+ ...tabCountSmartQuery({ type: PROJECT_TYPE }),
+ },
+
+ // Runner status summary
+ onlineRunnersTotal: {
+ ...statusCountSmartQuery({ status: STATUS_ONLINE, name: 'onlineRunnersTotal' }),
+ },
+ offlineRunnersTotal: {
+ ...statusCountSmartQuery({ status: STATUS_OFFLINE, name: 'offlineRunnersTotal' }),
+ },
+ staleRunnersTotal: {
+ ...statusCountSmartQuery({ status: STATUS_STALE, name: 'staleRunnersTotal' }),
},
},
computed: {
@@ -263,12 +264,6 @@ export default {
<template>
<div>
- <runner-stats
- :online-runners-count="onlineRunnersTotal"
- :offline-runners-count="offlineRunnersTotal"
- :stale-runners-count="staleRunnersTotal"
- />
-
<div class="gl-display-flex gl-align-items-center">
<runner-type-tabs
v-model="search"
@@ -298,6 +293,12 @@ export default {
:namespace="filteredSearchNamespace"
/>
+ <runner-stats
+ :online-runners-count="onlineRunnersTotal"
+ :offline-runners-count="offlineRunnersTotal"
+ :stale-runners-count="staleRunnersTotal"
+ />
+
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index f350572a1f9..f12345426d4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -323,7 +323,7 @@ export default {
restructuredWidgetShowMergeButtons() {
if (this.glFeatures.restructuredMrWidget) {
return (
- this.isMergeAllowed &&
+ (this.isMergeAllowed || this.isAutoMergeAvailable) &&
this.state.userPermissions.canMerge &&
!this.mr.mergeOngoing &&
!this.mr.autoMergeEnabled
@@ -443,6 +443,8 @@ export default {
if (this.glFeatures.mergeRequestWidgetGraphql) {
this.updateGraphqlState();
}
+
+ this.isMakingRequest = false;
})
.catch(() => {
this.isMakingRequest = false;
@@ -521,6 +523,7 @@ export default {
<template>
<div
+ data-testid="ready_to_merge_state"
:class="{
'gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7 gl-rounded-bottom-left-base gl-rounded-bottom-right-base':
glFeatures.restructuredMrWidget,
@@ -633,6 +636,7 @@ export default {
glFeatures.restructuredMrWidget && (shouldShowSquashEdit || shouldShowMergeEdit)
"
v-model="editCommitMessage"
+ data-testid="widget_edit_commit_message"
class="gl-display-flex gl-align-items-center"
>
{{ __('Edit commit message') }}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 765bd146a03..1f309a19b14 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -373,8 +373,6 @@ export default {
<suggestions
v-if="hasSuggestion"
:note-html="markdownPreview"
- :from-line="lineNumber"
- :from-content="lineContent"
:line-type="lineType"
:disabled="true"
:suggestions="suggestions"
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index 7c64b0a6c86..870ed50c6eb 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -13,6 +13,11 @@
}
}
+ img.ProseMirror-selectednode {
+ outline: 3px solid rgba($blue-400, 0.48);
+ outline-offset: -3px;
+ }
+
ul[data-type='taskList'] {
list-style: none;
padding: 0;
@@ -125,4 +130,3 @@
.bubble-menu-form {
width: 320px;
}
-
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 8dc9697c56d..ad2e384077a 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -63,5 +63,3 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
finder(state: 'active', sort: 'expires_at_asc').execute
end
end
-
-Profiles::PersonalAccessTokensController.prepend_mod_with('Profiles::PersonalAccessTokensController')
diff --git a/app/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder.rb b/app/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder.rb
new file mode 100644
index 00000000000..909a896d77c
--- /dev/null
+++ b/app/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+# Groups::ProjectsRequiringAuthorizationsRefresh::OnDirectMembershipFinder
+#
+# Given a group, this finder can be used to obtain a list of Project IDs of projects
+# that requires their `project_authorizations` records to be refreshed in the event where
+# a member has been added/removed/updated in the group.
+
+module Groups
+ module ProjectsRequiringAuthorizationsRefresh
+ class OnDirectMembershipFinder
+ def initialize(group)
+ @group = group
+ end
+
+ def execute
+ project_ids = Set.new
+
+ project_ids.merge(ids_of_projects_in_hierarchy_and_project_shares(@group))
+ project_ids.merge(ids_of_projects_in_hierarchy_and_project_shares_of_shared_groups(@group))
+
+ project_ids.to_a
+ end
+
+ private
+
+ def ids_of_projects_in_hierarchy_and_project_shares(group)
+ project_ids = Set.new
+
+ ids_of_projects_in_hierarchy = group.all_projects.pluck(:id) # rubocop: disable CodeReuse/ActiveRecord
+ ids_of_projects_in_project_shares = ids_of_projects_shared_with_self_and_descendant_groups(group)
+
+ project_ids.merge(ids_of_projects_in_hierarchy)
+ project_ids.merge(ids_of_projects_in_project_shares)
+
+ project_ids
+ end
+
+ def ids_of_projects_shared_with_self_and_descendant_groups(group, batch_size: 50)
+ project_ids = Set.new
+
+ group.self_and_descendants_ids.each_slice(batch_size) do |group_ids|
+ project_ids.merge(ProjectGroupLink.in_group(group_ids).pluck(:project_id)) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ project_ids
+ end
+
+ def ids_of_projects_in_hierarchy_and_project_shares_of_shared_groups(group, batch_size: 10)
+ project_ids = Set.new
+
+ group.shared_groups.each_batch(of: batch_size) do |shared_groups_batch|
+ shared_groups_batch.each do |shared_group|
+ project_ids.merge(ids_of_projects_in_hierarchy_and_project_shares(shared_group))
+ end
+ end
+
+ project_ids
+ end
+ end
+ end
+end
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index be266045951..7d356c1014c 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -79,8 +79,6 @@ class PersonalAccessTokensFinder
tokens.active
when 'inactive'
tokens.inactive
- when 'active_or_expired'
- tokens.not_revoked.expired.or(tokens.active)
else
tokens
end
diff --git a/app/helpers/personal_access_tokens_helper.rb b/app/helpers/personal_access_tokens_helper.rb
deleted file mode 100644
index 5cc8d21096f..00000000000
--- a/app/helpers/personal_access_tokens_helper.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-module PersonalAccessTokensHelper
- def personal_access_token_expiration_enforced?
- false
- end
-end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index b2943d61216..a49658ce7e0 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -13,6 +13,7 @@ class ApplicationSetting < ApplicationRecord
ignore_column %i[max_package_files_for_package_destruction], remove_with: '14.9', remove_after: '2022-03-22'
ignore_column :user_email_lookup_limit, remove_with: '15.0', remove_after: '2022-04-18'
ignore_column :pseudonymizer_enabled, remove_with: '15.1', remove_after: '2022-06-22'
+ ignore_column :enforce_pat_expiration, remove_with: '15.2', remove_after: '2022-07-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 021ff789b13..68ba3d6eab4 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -49,10 +49,6 @@ class PersonalAccessToken < ApplicationRecord
!revoked? && !expired?
end
- def expired_but_not_enforced?
- false
- end
-
def self.redis_getdel(user_id)
Gitlab::Redis::SharedState.with do |redis|
redis_key = redis_shared_state_key(user_id)
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index a0fa69c54c5..d55efbaf701 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -29,7 +29,6 @@
= render_if_exists 'admin/application_settings/git_two_factor_session_expiry', form: f
= render_if_exists 'admin/application_settings/personal_access_token_expiration_policy', form: f
- = render_if_exists 'admin/application_settings/enforce_pat_expiration', form: f
= render_if_exists 'admin/application_settings/ssh_key_expiration_policy', form: f
= render_if_exists 'admin/application_settings/enforce_ssh_key_expiration', form: f
diff --git a/app/views/admin/application_settings/_note_limits.html.haml b/app/views/admin/application_settings/_note_limits.html.haml
index d4ae0d3944c..40760b3c45e 100644
--- a/app/views/admin/application_settings/_note_limits.html.haml
+++ b/app/views/admin/application_settings/_note_limits.html.haml
@@ -9,7 +9,7 @@
= f.label :notes_create_limit_allowlist, _('Users to exclude from the rate limit'), class: 'label-bold'
= f.text_area :notes_create_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5, aria: { describedBy: 'note-create-limits-allowlist-field-description' }
.form-text.text-muted{ id: 'note-create-limits-allowlist-field-description' }
- = _('List of users allowed to exceed the rate limit.')
+ = _('List of users who are allowed to exceed the rate limit. Example: username1, username2')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_users_api_limits.html.haml b/app/views/admin/application_settings/_users_api_limits.html.haml
index 9b3502b3cfd..3918c76b12c 100644
--- a/app/views/admin/application_settings/_users_api_limits.html.haml
+++ b/app/views/admin/application_settings/_users_api_limits.html.haml
@@ -9,6 +9,6 @@
= f.label :users_get_by_id_limit_allowlist_raw, _('Users to exclude from the rate limit'), class: 'label-bold'
= f.text_area :users_get_by_id_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5, aria: { describedBy: 'users-api-limit-users-allowlist-field-description' }
.form-text.text-muted{ id: 'users-api-limit-users-allowlist-field-description' }
- = _('List of users allowed to exceed the rate limit.')
+ = _('List of users who are allowed to exceed the rate limit. Example: username1, username2')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/projects/services/prometheus/_external_alerts.html.haml b/app/views/projects/services/prometheus/_external_alerts.html.haml
deleted file mode 100644
index 168b4853a9a..00000000000
--- a/app/views/projects/services/prometheus/_external_alerts.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-- return unless can?(current_user, :read_prometheus_alerts, @project)
-- return unless integration.manual_configuration?
-
-- notify_url = notify_project_prometheus_alerts_url(@project, format: :json)
-- authorization_key = @project.alerting_setting.try(:token)
-- learn_more_url = help_page_path('operations/metrics/alerts.md', anchor: 'external-prometheus-instances')
-
-#js-settings-prometheus-alerts{ data: { notify_url: notify_url, authorization_key: authorization_key, change_key_url: reset_alerting_token_project_settings_operations_path(@project), learn_more_url: learn_more_url, disabled: true } }
diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml
index 3350ac8a6c5..c80dc46bdb5 100644
--- a/app/views/projects/services/prometheus/_show.html.haml
+++ b/app/views/projects/services/prometheus/_show.html.haml
@@ -5,5 +5,3 @@
.row.gl-mb-3.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring
= render 'projects/services/prometheus/metrics', project: @project, integration: integration
-
-= render 'projects/services/prometheus/external_alerts', project: @project, integration: integration
diff --git a/app/views/projects/services/prometheus/_top.html.haml b/app/views/projects/services/prometheus/_top.html.haml
deleted file mode 100644
index 52b29ea2e8f..00000000000
--- a/app/views/projects/services/prometheus/_top.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- return unless integration.manual_configuration?
-
-.row
- .col-lg-12
- = render Pajamas::AlertComponent.new(dismissible: false) do
- .gl-alert-body
- = s_('AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated.')
- .gl-alert-actions
- = link_to _('Visit settings page'), project_settings_operations_path(@project, anchor: 'js-alert-management-settings'), class: 'gl-button btn gl-alert-action btn-info'
diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml
index 7f7dafbe5b0..5ca9cf8d9a4 100644
--- a/app/views/shared/access_tokens/_table.html.haml
+++ b/app/views/shared/access_tokens/_table.html.haml
@@ -1,23 +1,16 @@
- no_active_tokens_message = local_assigns.fetch(:no_active_tokens_message, _('This user has no active %{type}.') % { type: type_plural })
- impersonation = local_assigns.fetch(:impersonation, false)
- resource = local_assigns.fetch(:resource, false)
-- personal = !impersonation && !resource
%hr
%h5
= _('Active %{type} (%{token_length})') % { type: type_plural, token_length: active_tokens.length }
-- if personal && !personal_access_token_expiration_enforced?
- %p.profile-settings-content
- = _("Personal access tokens are not revoked upon expiration.")
- if impersonation
%p.profile-settings-content
= _("To see all the user's personal access tokens you must impersonate them first.")
-- if personal
- = render_if_exists 'profiles/personal_access_tokens/token_expiry_notification', active_tokens: active_tokens
-
- if active_tokens.present?
.table-responsive
%table.table.active-tokens
@@ -46,12 +39,8 @@
%span.token-never-used-label= _('Never')
%td
- if token.expires?
- - if token.expired? || token.expired_but_not_enforced?
- %span{ class: 'text-danger has-tooltip', title: _('Token valid until revoked') }
- = _('Expired')
- - else
- %span{ class: ('text-warning' if token.expires_soon?) }
- = time_ago_with_tooltip(token.expires_at)
+ %span{ class: ('text-warning' if token.expires_soon?) }
+ = time_ago_with_tooltip(token.expires_at)
- else
%span.token-never-expires-label= _('Never')
- if resource
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 65b33d8444b..81edeca4032 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -733,9 +733,6 @@ Gitlab.ee do
Settings.cron_jobs['users_create_statistics_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['users_create_statistics_worker']['cron'] ||= '2 15 * * *'
Settings.cron_jobs['users_create_statistics_worker']['job_class'] = 'Users::CreateStatisticsWorker'
- Settings.cron_jobs['network_policy_metrics_worker'] ||= Settingslogic.new({})
- Settings.cron_jobs['network_policy_metrics_worker']['cron'] ||= '0 3 * * 0'
- Settings.cron_jobs['network_policy_metrics_worker']['job_class'] = 'NetworkPolicyMetricsWorker'
Settings.cron_jobs['iterations_update_status_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['iterations_update_status_worker']['cron'] ||= '5 0 * * *'
Settings.cron_jobs['iterations_update_status_worker']['job_class'] = 'IterationsUpdateStatusWorker'
diff --git a/config/metrics/counts_all/20210216175446_network_policy_forwards.yml b/config/metrics/counts_all/20210216175446_network_policy_forwards.yml
index ef2b37bb001..64d7a9434be 100644
--- a/config/metrics/counts_all/20210216175446_network_policy_forwards.yml
+++ b/config/metrics/counts_all/20210216175446_network_policy_forwards.yml
@@ -8,7 +8,9 @@ product_stage: protect
product_group: group::container security
product_category: container_network_security
value_type: number
-status: active
+status: removed
+removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86351
+milestone_removed: 15.0
time_frame: all
data_source: redis
distribution:
diff --git a/config/metrics/counts_all/20210216175448_network_policy_drops.yml b/config/metrics/counts_all/20210216175448_network_policy_drops.yml
index 933363c0a14..d3a874253f9 100644
--- a/config/metrics/counts_all/20210216175448_network_policy_drops.yml
+++ b/config/metrics/counts_all/20210216175448_network_policy_drops.yml
@@ -8,7 +8,9 @@ product_stage: protect
product_group: group::container security
product_category: container_network_security
value_type: number
-status: active
+status: removed
+removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86351
+milestone_removed: "15.0"
time_frame: all
data_source: redis
distribution:
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 9750c9dd14e..7be1f7f32c9 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -421,6 +421,8 @@
- 1
- - security_scans
- 2
+- - security_sync_scan_policies
+ - 1
- - self_monitoring_project_create
- 2
- - self_monitoring_project_delete
diff --git a/data/deprecations/14-7-deprecate-static-site-editor.yml b/data/deprecations/14-7-deprecate-static-site-editor.yml
index 18f2ec26645..0960bffe4cf 100644
--- a/data/deprecations/14-7-deprecate-static-site-editor.yml
+++ b/data/deprecations/14-7-deprecate-static-site-editor.yml
@@ -4,7 +4,9 @@
removal_milestone: "15.0" # The milestone when this feature is planned to be removed
removal_date: "2022-05-22" # This should almost always be the 22nd of a month (YYYY-MM-22), the date of the milestone release when this feature is planned to be removed.
body: | # Do not modify this line, instead modify the lines below.
- The Static Site Editor will no longer be available starting in GitLab 15.0. Improvements to the Markdown editing experience across GitLab will deliver smiliar benefit but with a wider reach. Incoming requests to the Static Site Editor will be redirected to the Web IDE. Current users of the Static Site Editor can view the [documentation](https://docs.gitlab.com/ee/user/project/static_site_editor/) for more information, including how to remove the configuration files from existing projects.
+ The Static Site Editor will no longer be available starting in GitLab 15.0. Improvements to the Markdown editing experience across GitLab will deliver smiliar benefit but with a wider reach. Incoming requests to the Static Site Editor will be redirected to the [Web IDE](https://docs.gitlab.com/ee/user/project/web_ide/index.html).
+
+ Current users of the Static Site Editor can view the [documentation](https://docs.gitlab.com/ee/user/project/static_site_editor/) for more information, including how to remove the configuration files from existing projects.
# The following items are not published on the docs page, but may be used in the future.
stage: Create # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
tiers: [Free, Premium, Ultimate] # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
diff --git a/db/migrate/20220502125053_recreate_index_for_project_group_link_with_group_id_and_project_id.rb b/db/migrate/20220502125053_recreate_index_for_project_group_link_with_group_id_and_project_id.rb
new file mode 100644
index 00000000000..1d9a18b7b23
--- /dev/null
+++ b/db/migrate/20220502125053_recreate_index_for_project_group_link_with_group_id_and_project_id.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class RecreateIndexForProjectGroupLinkWithGroupIdAndProjectId < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ OLD_INDEX_NAME = 'index_project_group_links_on_group_id'
+ NEW_INDEX_NAME = 'index_project_group_links_on_group_id_and_project_id'
+
+ def up
+ add_concurrent_index :project_group_links, [:group_id, :project_id], name: NEW_INDEX_NAME
+ remove_concurrent_index_by_name :project_group_links, OLD_INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :project_group_links, [:group_id], name: OLD_INDEX_NAME
+ remove_concurrent_index_by_name :project_group_links, NEW_INDEX_NAME
+ end
+end
diff --git a/db/migrate/20220502150408_add_slack_integrations_bot_columns.rb b/db/migrate/20220502150408_add_slack_integrations_bot_columns.rb
new file mode 100644
index 00000000000..cb5b201e71e
--- /dev/null
+++ b/db/migrate/20220502150408_add_slack_integrations_bot_columns.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddSlackIntegrationsBotColumns < Gitlab::Database::Migration[2.0]
+ def change
+ change_table :slack_integrations do |t|
+ t.column :bot_user_id, :text
+ t.column :encrypted_bot_access_token, :binary
+ t.column :encrypted_bot_access_token_iv, :binary
+ end
+ end
+end
diff --git a/db/migrate/20220502152633_add_slack_integrations_bot_user_id_text_limit.rb b/db/migrate/20220502152633_add_slack_integrations_bot_user_id_text_limit.rb
new file mode 100644
index 00000000000..649d6ccf9d4
--- /dev/null
+++ b/db/migrate/20220502152633_add_slack_integrations_bot_user_id_text_limit.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddSlackIntegrationsBotUserIdTextLimit < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :slack_integrations, :bot_user_id, 255
+ end
+
+ def down
+ remove_text_limit :slack_integrations, :bot_user_id
+ end
+end
diff --git a/db/migrate/20220503073401_recreate_index_for_group_group_link_with_both_group_ids.rb b/db/migrate/20220503073401_recreate_index_for_group_group_link_with_both_group_ids.rb
new file mode 100644
index 00000000000..214e9c5e0a7
--- /dev/null
+++ b/db/migrate/20220503073401_recreate_index_for_group_group_link_with_both_group_ids.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class RecreateIndexForGroupGroupLinkWithBothGroupIds < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ OLD_INDEX_NAME = 'index_group_group_links_on_shared_with_group_id'
+ NEW_INDEX_NAME = 'index_group_group_links_on_shared_with_group_and_shared_group'
+
+ def up
+ add_concurrent_index :group_group_links, [:shared_with_group_id, :shared_group_id], name: NEW_INDEX_NAME
+ remove_concurrent_index_by_name :group_group_links, OLD_INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :group_group_links, [:shared_with_group_id], name: OLD_INDEX_NAME
+ remove_concurrent_index_by_name :group_group_links, NEW_INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20220502125053 b/db/schema_migrations/20220502125053
new file mode 100644
index 00000000000..9b026e23576
--- /dev/null
+++ b/db/schema_migrations/20220502125053
@@ -0,0 +1 @@
+b87e7b69f4d88a5620180648568c499e6e86fe001a8cfd235eebf050d5cdc9a1 \ No newline at end of file
diff --git a/db/schema_migrations/20220502150408 b/db/schema_migrations/20220502150408
new file mode 100644
index 00000000000..2bab54bbe7d
--- /dev/null
+++ b/db/schema_migrations/20220502150408
@@ -0,0 +1 @@
+a730ff7969895be95e92fff5bb9b468ed407bd65bccb9daf40f892e18b4d18b6 \ No newline at end of file
diff --git a/db/schema_migrations/20220502152633 b/db/schema_migrations/20220502152633
new file mode 100644
index 00000000000..b5dd2256ac8
--- /dev/null
+++ b/db/schema_migrations/20220502152633
@@ -0,0 +1 @@
+f8f34dc48e55723d868d1a247a92731ed1f1d5d185791c3202d0ed2cdedb41d3 \ No newline at end of file
diff --git a/db/schema_migrations/20220503073401 b/db/schema_migrations/20220503073401
new file mode 100644
index 00000000000..bccca17138b
--- /dev/null
+++ b/db/schema_migrations/20220503073401
@@ -0,0 +1 @@
+3e05b07c5a3a0912884e0bdda08e0f4ef93ce95b6e3f5deb30b10eca74c6ea79 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index becf04c5a02..0d494106594 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -20602,7 +20602,11 @@ CREATE TABLE slack_integrations (
alias character varying NOT NULL,
user_id character varying NOT NULL,
created_at timestamp without time zone NOT NULL,
- updated_at timestamp without time zone NOT NULL
+ updated_at timestamp without time zone NOT NULL,
+ bot_user_id text,
+ encrypted_bot_access_token bytea,
+ encrypted_bot_access_token_iv bytea,
+ CONSTRAINT check_bc553aea8a CHECK ((char_length(bot_user_id) <= 255))
);
CREATE SEQUENCE slack_integrations_id_seq
@@ -27886,7 +27890,7 @@ CREATE UNIQUE INDEX index_group_deploy_tokens_on_group_and_deploy_token_ids ON g
CREATE UNIQUE INDEX index_group_group_links_on_shared_group_and_shared_with_group ON group_group_links USING btree (shared_group_id, shared_with_group_id);
-CREATE INDEX index_group_group_links_on_shared_with_group_id ON group_group_links USING btree (shared_with_group_id);
+CREATE INDEX index_group_group_links_on_shared_with_group_and_shared_group ON group_group_links USING btree (shared_with_group_id, shared_group_id);
CREATE INDEX index_group_import_states_on_group_id ON group_import_states USING btree (group_id);
@@ -28768,7 +28772,7 @@ COMMENT ON INDEX index_project_features_on_project_id_include_container_registry
CREATE INDEX index_project_features_on_project_id_ral_20 ON project_features USING btree (project_id) WHERE (repository_access_level = 20);
-CREATE INDEX index_project_group_links_on_group_id ON project_group_links USING btree (group_id);
+CREATE INDEX index_project_group_links_on_group_id_and_project_id ON project_group_links USING btree (group_id, project_id);
CREATE INDEX index_project_group_links_on_project_id ON project_group_links USING btree (project_id);
diff --git a/doc/administration/index.md b/doc/administration/index.md
index b094bd59e58..1d8dcd34d68 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -205,10 +205,6 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [GitLab performance monitoring with Grafana](monitoring/performance/grafana_configuration.md): Configure GitLab to visualize time series metrics through graphs and dashboards.
- [Performance Bar](monitoring/performance/performance_bar.md): Get performance information for the current page.
-## Analytics
-
-- [Pseudonymizer](pseudonymizer.md): Export data from a GitLab database to CSV files in a secure way.
-
## Troubleshooting
- [Debugging tips](troubleshooting/debug.md): Tips to debug problems when things go wrong.
diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md
index 561108eef57..0560a8813df 100644
--- a/doc/administration/object_storage.md
+++ b/doc/administration/object_storage.md
@@ -536,7 +536,6 @@ supported by consolidated configuration form, refer to the following guides:
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| **{dotted-circle}** No |
| [Packages](packages/index.md#using-object-storage) (optional feature) | **{check-circle}** Yes |
| [Dependency Proxy](packages/dependency_proxy.md#using-object-storage) (optional feature) | **{check-circle}** Yes |
-| [Pseudonymizer](pseudonymizer.md) (optional feature) | **{dotted-circle}** No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | **{dotted-circle}** No |
| [Terraform state files](terraform_state.md#using-object-storage) | **{check-circle}** Yes |
| [Pages content](pages/index.md#using-object-storage) | **{check-circle}** Yes |
@@ -570,7 +569,7 @@ There are plans to [enable the use of a single bucket](https://gitlab.com/gitlab
in the future.
Helm-based installs require separate buckets to
-[handle backup restorations](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-pseudonymizer).
+[handle backup restorations](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-terraform-state-dependency-proxy).
### S3 API compatibility issues
diff --git a/doc/administration/pseudonymizer.md b/doc/administration/pseudonymizer.md
index 24d9792dcb0..ad4cfd11474 100644
--- a/doc/administration/pseudonymizer.md
+++ b/doc/administration/pseudonymizer.md
@@ -2,127 +2,14 @@
stage: Enablement
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+remove_date: '2022-08-22'
+redirect_to: 'index.md'
---
-# Pseudonymizer (DEPRECATED) **(ULTIMATE)**
+# Pseudonymizer (removed) **(ULTIMATE)**
-> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/219952) in GitLab 14.7.
+> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/219952) in
+> GitLab 14.7 and removed in 15.0.
WARNING:
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/219952) in GitLab 14.7.
-
-Your GitLab database contains sensitive information. To protect sensitive information
-when you run analytics on your database, you can use the Pseudonymizer service, which:
-
-1. Uses `HMAC(SHA256)` to mutate fields containing sensitive information.
-1. Preserves references (referential integrity) between fields.
-1. Exports your GitLab data, scrubbed of sensitive material.
-
-WARNING:
-If the source data is available, users can compare and correlate the scrubbed data
-with the original.
-
-To generate a pseudonymized data set:
-
-1. [Configure Pseudonymizer](#configure-pseudonymizer) fields and output location.
-1. [Enable Pseudonymizer data collection](#enable-pseudonymizer-data-collection).
-1. Optional. [Generate a data set manually](#generate-data-set-manually).
-
-## Configure Pseudonymizer
-
-To use the Pseudonymizer, configure both the fields you want to anonymize, and the location to
-store the scrubbed data:
-
-1. **Create a manifest file**: This file describes the fields to include or pseudonymize.
- - **Default manifest** - GitLab provides a default manifest in your GitLab installation
- ([example `manifest.yml` file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/pseudonymizer.yml)).
- To use the example manifest file, use the `config/pseudonymizer.yml` relative path
- when you configure connection parameters.
- - **Custom manifest** - To use a custom manifest file, use the absolute path to
- the file when you configure the connection parameters.
-1. **Configure connection parameters**: In the configuration method appropriate for
- your version of GitLab, specify the [object storage](object_storage.md)
- connection parameters (`pseudonymizer.upload.connection`).
-
-**For Omnibus installations:**
-
-1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
- the values you want:
-
- ```ruby
- gitlab_rails['pseudonymizer_manifest'] = 'config/pseudonymizer.yml'
- gitlab_rails['pseudonymizer_upload_remote_directory'] = 'gitlab-elt' # bucket name
- gitlab_rails['pseudonymizer_upload_connection'] = {
- 'provider' => 'AWS',
- 'region' => 'eu-central-1',
- 'aws_access_key_id' => 'AWS_ACCESS_KEY_ID',
- 'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY'
- }
- ```
-
- If you are using AWS IAM profiles, omit the AWS access key and secret access key/value pairs.
-
- ```ruby
- gitlab_rails['pseudonymizer_upload_connection'] = {
- 'provider' => 'AWS',
- 'region' => 'eu-central-1',
- 'use_iam_profile' => true
- }
- ```
-
-1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure)
- for the changes to take effect.
-
----
-
-**For installations from source:**
-
-1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
- lines:
-
- ```yaml
- pseudonymizer:
- manifest: config/pseudonymizer.yml
- upload:
- remote_directory: 'gitlab-elt' # bucket name
- connection:
- provider: AWS
- aws_access_key_id: AWS_ACCESS_KEY_ID
- aws_secret_access_key: AWS_SECRET_ACCESS_KEY
- region: eu-central-1
- ```
-
-1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source)
- for the changes to take effect.
-
-## Enable Pseudonymizer data collection
-
-To enable data collection:
-
-1. On the top bar, select **Menu > Admin**.
-1. On the left sidebar, select **Settings > Metrics and Profiling**, then expand
- **Pseudonymizer data collection**.
-1. Select **Enable Pseudonymizer data collection**.
-1. Select **Save changes**.
-
-## Generate data set manually
-
-You can also run the Pseudonymizer manually:
-
-1. Set these environment variables:
- - `PSEUDONYMIZER_OUTPUT_DIR` - Where to store the output CSV files. Defaults to `/tmp`.
- These commands produce CSV files that can be quite large. Make sure the directory
- can store a file at least 10% of the size of your database.
- - `PSEUDONYMIZER_BATCH` - The batch size when querying the database. Defaults to `100000`.
-1. Run the command appropriate for your application:
- - **Omnibus GitLab**:
- `sudo gitlab-rake gitlab:db:pseudonymizer`
- - **Installations from source**:
- `sudo -u git -H bundle exec rake gitlab:db:pseudonymizer RAILS_ENV=production`
-
-After you run the command, upload the output CSV files to your configured object
-storage. After the upload completes, delete the output file from the local disk.
-
-## Related topics
-
-- [Using object storage with GitLab](object_storage.md).
diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md
index e305c34fd5f..47eb149eb2d 100644
--- a/doc/administration/reference_architectures/10k_users.md
+++ b/doc/administration/reference_architectures/10k_users.md
@@ -2202,7 +2202,6 @@ on what features you intend to use:
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No |
| [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes |
| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes |
-| [Pseudonymizer](../pseudonymizer.md) (optional feature) | No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No |
| [Terraform state files](../terraform_state.md#using-object-storage) | Yes |
diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md
index bd09edbebfc..225cf4b9c71 100644
--- a/doc/administration/reference_architectures/25k_users.md
+++ b/doc/administration/reference_architectures/25k_users.md
@@ -2206,7 +2206,6 @@ on what features you intend to use:
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No |
| [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes |
| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes |
-| [Pseudonymizer](../pseudonymizer.md) (optional feature) | No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No |
| [Terraform state files](../terraform_state.md#using-object-storage) | Yes |
diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md
index bb6a9049f68..be089203f4f 100644
--- a/doc/administration/reference_architectures/2k_users.md
+++ b/doc/administration/reference_architectures/2k_users.md
@@ -924,7 +924,6 @@ on what features you intend to use:
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No |
| [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes |
| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes |
-| [Pseudonymizer](../pseudonymizer.md) (optional feature) | No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No |
| [Terraform state files](../terraform_state.md#using-object-storage) | Yes |
diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md
index a55e1c53ae5..d1ee13b1940 100644
--- a/doc/administration/reference_architectures/3k_users.md
+++ b/doc/administration/reference_architectures/3k_users.md
@@ -2141,7 +2141,6 @@ on what features you intend to use:
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No |
| [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes |
| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes |
-| [Pseudonymizer](../pseudonymizer.md) (optional feature) | No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No |
| [Terraform state files](../terraform_state.md#using-object-storage) | Yes |
diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md
index 66a8e9e67d4..7fb3f158848 100644
--- a/doc/administration/reference_architectures/50k_users.md
+++ b/doc/administration/reference_architectures/50k_users.md
@@ -2222,7 +2222,6 @@ on what features you intend to use:
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No |
| [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes |
| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes |
-| [Pseudonymizer](../pseudonymizer.md) (optional feature) | No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No |
| [Terraform state files](../terraform_state.md#using-object-storage) | Yes |
diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md
index c3c4f1311f2..91a630a7eb0 100644
--- a/doc/administration/reference_architectures/5k_users.md
+++ b/doc/administration/reference_architectures/5k_users.md
@@ -2141,7 +2141,6 @@ on what features you intend to use:
| [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No |
| [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes |
| [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes |
-| [Pseudonymizer](../pseudonymizer.md) (optional feature) | No |
| [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No |
| [Terraform state files](../terraform_state.md#using-object-storage) | Yes |
diff --git a/doc/administration/terraform_state.md b/doc/administration/terraform_state.md
index 5e8f7cb18bb..c2fbcbdfc55 100644
--- a/doc/administration/terraform_state.md
+++ b/doc/administration/terraform_state.md
@@ -18,7 +18,7 @@ The storage location of these files defaults to:
These locations can be configured using the options described below.
-Use [external object storage](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-pseudonymizer-terraform-state-dependency-proxy) configuration for [GitLab Helm chart](https://docs.gitlab.com/charts/) installations.
+Use [external object storage](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-terraform-state-dependency-proxy) configuration for [GitLab Helm chart](https://docs.gitlab.com/charts/) installations.
## Disabling Terraform state
diff --git a/doc/api/settings.md b/doc/api/settings.md
index f8df0220240..cf20cd279fc 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -392,7 +392,6 @@ listed in the descriptions of the relevant settings.
| `project_export_enabled` | boolean | no | Enable project export. |
| `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. |
| `protected_ci_variables` | boolean | no | CI/CD variables are protected by default. |
-| `pseudonymizer_enabled` **(PREMIUM)** | boolean | no | When enabled, GitLab runs a background job that produces pseudonymized CSVs of the GitLab database to upload to your configured object storage directory.
| `push_event_activities_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push events are created. [Bulk push events are created](../user/admin_area/settings/push_event_activities_limit.md) if it surpasses that value. |
| `push_event_hooks_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether webhooks and services fire or not. Webhooks and services aren't submitted if it surpasses that value. |
| `rate_limiting_response_text` | string | no | When rate limiting is enabled via the `throttle_*` settings, send this plain text response when a rate limit is exceeded. 'Retry later' is sent if this is blank. |
diff --git a/doc/ci/variables/index.md b/doc/ci/variables/index.md
index bebe9f9d20a..0bdfab97b9a 100644
--- a/doc/ci/variables/index.md
+++ b/doc/ci/variables/index.md
@@ -848,8 +848,8 @@ if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then
++ CI_SERVER_PROTOCOL=https
++ export CI_SERVER_NAME=GitLab
++ CI_SERVER_NAME=GitLab
-++ export GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
-++ GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
+++ export GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
+++ GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
++ export CI_PROJECT_ID=17893
++ CI_PROJECT_ID=17893
++ export CI_PROJECT_NAME=ci-debug-trace
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index a24b5ea8e3b..87954330878 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -1154,7 +1154,9 @@ to serve the Sidekiq metrics, similar to the way Sidekiq will behave in 15.0.
### Static Site Editor
-The Static Site Editor will no longer be available starting in GitLab 15.0. Improvements to the Markdown editing experience across GitLab will deliver smiliar benefit but with a wider reach. Incoming requests to the Static Site Editor will be redirected to the Web IDE. Current users of the Static Site Editor can view the [documentation](https://docs.gitlab.com/ee/user/project/static_site_editor/) for more information, including how to remove the configuration files from existing projects.
+The Static Site Editor will no longer be available starting in GitLab 15.0. Improvements to the Markdown editing experience across GitLab will deliver smiliar benefit but with a wider reach. Incoming requests to the Static Site Editor will be redirected to the [Web IDE](https://docs.gitlab.com/ee/user/project/web_ide/index.html).
+
+Current users of the Static Site Editor can view the [documentation](https://docs.gitlab.com/ee/user/project/static_site_editor/) for more information, including how to remove the configuration files from existing projects.
**Planned removal milestone: 15.0 (2022-05-22)**
diff --git a/doc/user/admin_area/credentials_inventory.md b/doc/user/admin_area/credentials_inventory.md
index bcf15192ef0..4308b45df78 100644
--- a/doc/user/admin_area/credentials_inventory.md
+++ b/doc/user/admin_area/credentials_inventory.md
@@ -40,14 +40,11 @@ To access the Credentials inventory:
If you see a **Revoke** button, you can revoke that user's PAT. Whether you see a **Revoke** button depends on the token state, and if an expiration date has been set. For more information, see the following table:
-| Token state | [Token expiration enforced?](settings/account_and_limit_settings.md#allow-expired-access-tokens-to-be-used-deprecated) | Show Revoke button? | Comments |
-|-------------|------------------------|--------------------|----------------------------------------------------------------------------|
-| Active | Yes | Yes | Allows administrators to revoke the PAT, such as for a compromised account |
-| Active | No | Yes | Allows administrators to revoke the PAT, such as for a compromised account |
-| Expired | Yes | No | PAT expires automatically |
-| Expired | No | Yes | The administrator may revoke the PAT to prevent indefinite use |
-| Revoked | Yes | No | Not applicable; token is already revoked |
-| Revoked | No | No | Not applicable; token is already revoked |
+| Token state | Show Revoke button? | Comments |
+|-------------|---------------------|----------------------------------------------------------------------------|
+| Active | Yes | Allows administrators to revoke the PAT, such as for a compromised account |
+| Expired | No | Not applicable; token is already expired |
+| Revoked | No | Not applicable; token is already revoked |
When a PAT is revoked from the credentials inventory, the instance notifies the user by email.
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
index 04952b6dcdf..df2ea82a556 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -285,23 +285,17 @@ Once a lifetime for access tokens is set, GitLab:
allowed lifetime. Three hours is given to allow administrators to change the allowed lifetime,
or remove it, before revocation takes place.
-## Allow expired access tokens to be used (DEPRECATED) **(ULTIMATE SELF)**
+<!-- start_remove The following content will be removed on remove_date: '2022-08-22' -->
+## Allow expired access tokens to be used (removed) **(ULTIMATE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214723) in GitLab 13.1.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/296881) in GitLab 13.9.
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/351962) in GitLab 14.8.
+> - [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/351962) in GitLab 15.0.
-WARNING:
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/351962) in GitLab 14.8.
-
-By default, expired personal access tokens (PATs) **are not usable**.
-
-To allow the use of expired PATs:
-
-1. On the top bar, select **Menu > Admin**.
-1. On the left sidebar, select **Settings > General**.
-1. Expand the **Account and limit** section.
-1. Uncheck the **Enforce access token expiration** checkbox.
+This feature was [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/351962) in GitLab 15.0.
+<!-- end_remove -->
## Disable user profile name changes **(PREMIUM SELF)**
diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md
index 052b6e26c07..9c6aa22e63c 100644
--- a/doc/user/admin_area/settings/index.md
+++ b/doc/user/admin_area/settings/index.md
@@ -112,8 +112,6 @@ The **Metrics and profiling** settings contain:
- [Self monitoring](../../../administration/monitoring/gitlab_self_monitoring_project/index.md#create-the-self-monitoring-project) -
Enable or disable instance self monitoring.
- [Usage statistics](usage_statistics.md) - Enable or disable version check and Service Ping.
-- [Pseudonymizer data collection](../../../administration/pseudonymizer.md) -
- Enable or disable the Pseudonymizer data collection.
### Network
diff --git a/doc/user/discussions/img/confidential_comments_v13_9.png b/doc/user/discussions/img/confidential_comments_v13_9.png
deleted file mode 100644
index b5be5a622a9..00000000000
--- a/doc/user/discussions/img/confidential_comments_v13_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/discussions/img/confidential_comments_v15_0.png b/doc/user/discussions/img/confidential_comments_v15_0.png
new file mode 100644
index 00000000000..36b7b466b4e
--- /dev/null
+++ b/doc/user/discussions/img/confidential_comments_v15_0.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 8537856ef25..1cce69415d7 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -161,7 +161,7 @@ ask an administrator to [enable the feature flag](../../administration/feature_f
On GitLab.com, this feature is not available.
You should not use this feature for production environments.
-You can make a comment **in an issue or an epic** confidential, so that it is visible only to you (the commenting user) and
+You can make a comment **in an issue or an epic** confidential. It's then visible only to you (the commenting user) and
the project members who have at least the Reporter role.
Keep in mind:
@@ -183,7 +183,7 @@ To mark a comment as confidential:
1. Below the comment, select the **Make this comment confidential** checkbox.
1. Select **Comment**.
-![Confidential comments](img/confidential_comments_v13_9.png)
+![Confidential comments](img/confidential_comments_v15_0.png)
You can also make an [entire issue confidential](../project/issues/confidential_issues.md).
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 960a9309966..7cbb0c08720 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -456,18 +456,17 @@ To restore a group that is marked for deletion:
## Prevent group sharing outside the group hierarchy
-This setting is only available on top-level groups. It affects all subgroups and projects.
+You can configure a top-level group so its subgroups and projects
+cannot invite other groups outside of the top-level group's hierarchy.
+This option is only available for top-level groups.
-When checked, any group in the top-level group hierarchy can only invite other groups from within the top-level
-group's hierarchy.
-
-For example, with this setup:
+For example, in the following group and project hierarchy:
- **Animals > Dogs > Dog Project**
- **Animals > Cats**
- **Plants > Trees**
-If you select this setting in the **Animals** group:
+If you prevent group sharing outside the hierarchy for the **Animals** group:
- **Dogs** can invite the group **Cats**.
- **Dogs** cannot invite the group **Trees**.
@@ -476,8 +475,9 @@ If you select this setting in the **Animals** group:
To prevent sharing outside of the group's hierarchy:
-1. Go to the group's **Settings > General** page.
-1. Expand the **Permissions and group features** section.
+1. On the top bar, select **Menu > Groups** and find your group.
+1. On the left sidebar, select **Settings > General**.
+1. Expand **Permissions and group features**.
1. Select **Prevent members from sending invitations to groups outside of `<group_name>` and its subgroups**.
1. Select **Save changes**.
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index 46dca48d3b8..09e71ce9133 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -110,8 +110,6 @@ Personal access tokens expire on the date you define, at midnight UTC.
- GitLab runs a check at 02:00 AM UTC every day to identify personal access tokens that expire on the current date. The owners of these tokens are notified by email.
- In GitLab Ultimate, administrators can
[limit the lifetime of access tokens](../admin_area/settings/account_and_limit_settings.md#limit-the-lifetime-of-access-tokens).
-- In GitLab Ultimate, administrators can choose whether or not to
- [enforce access token expiration](../admin_area/settings/account_and_limit_settings.md#allow-expired-access-tokens-to-be-used-deprecated).
## Create a personal access token programmatically **(FREE SELF)**
diff --git a/doc/user/project/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md
index 2dae02dc093..dc56c2669f8 100644
--- a/doc/user/project/integrations/gitlab_slack_application.md
+++ b/doc/user/project/integrations/gitlab_slack_application.md
@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
NOTE:
The GitLab Slack application is only configurable for GitLab.com. It will **not**
work for on-premises installations where you can configure the
-[Slack slash commands](slack_slash_commands.md) service instead. We're planning
+[Slack slash commands](slack_slash_commands.md) integration instead. We're planning
to make this configurable for all GitLab installations, but there's
no ETA - see [#28164](https://gitlab.com/gitlab-org/gitlab/-/issues/28164).
@@ -31,17 +31,21 @@ Alternatively, you can configure the Slack application with a project's
integration settings.
Keep in mind that you must have the appropriate permissions for your Slack
-team to be able to install a new application, read more in Slack's
+workspace to be able to install a new application. Read more in Slack's
documentation on [Adding an app to your workspace](https://slack.com/help/articles/202035138-Add-apps-to-your-Slack-workspace).
-To enable the GitLab service for your Slack team:
+To enable the GitLab integration for your Slack workspace:
1. Go to your project's **Settings > Integration > Slack application** (only
visible on GitLab.com).
-1. Select **Add to Slack**.
+1. Select **Install Slack app**.
+1. Select **Allow** on Slack's confirmation screen.
That's all! You can now start using the Slack slash commands.
+You can also select **Reinstall Slack app** to update the app in your Slack workspace
+to the latest version. See the [Version history](#version-history) for details.
+
## Create a project alias for Slack
To create a project alias on GitLab.com for Slack integration:
@@ -62,7 +66,7 @@ GitLab error: project or alias not found
## Usage
-After confirming the installation, you, and everyone else in your Slack team,
+After confirming the installation, you, and everyone else in your Slack workspace,
can use all the [slash commands](../../../integration/slash_commands.md).
When you perform your first slash command, you are asked to authorize your
@@ -78,3 +82,11 @@ project, you would do:
```plaintext
/gitlab gitlab-org/gitlab issue show 1001
```
+
+## Version history
+
+### 15.0+
+
+In GitLab 15.0 the Slack app is updated to [Slack's new granular permissions app model](https://medium.com/slack-developer-blog/more-precision-less-restrictions-a3550006f9c3).
+
+There is no change in functionality. A reinstall is not required but recommended.
diff --git a/doc/user/project/static_site_editor/img/edit_this_page_button_v12_10.png b/doc/user/project/static_site_editor/img/edit_this_page_button_v12_10.png
deleted file mode 100644
index 380d96f1db9..00000000000
--- a/doc/user/project/static_site_editor/img/edit_this_page_button_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/static_site_editor/img/front_matter_ui_v13_4.png b/doc/user/project/static_site_editor/img/front_matter_ui_v13_4.png
deleted file mode 100644
index 89864858ed3..00000000000
--- a/doc/user/project/static_site_editor/img/front_matter_ui_v13_4.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/static_site_editor/img/wysiwyg_editor_v13_3.png b/doc/user/project/static_site_editor/img/wysiwyg_editor_v13_3.png
deleted file mode 100644
index 52776c6a290..00000000000
--- a/doc/user/project/static_site_editor/img/wysiwyg_editor_v13_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/static_site_editor/index.md b/doc/user/project/static_site_editor/index.md
index 220623d0372..343482757f5 100644
--- a/doc/user/project/static_site_editor/index.md
+++ b/doc/user/project/static_site_editor/index.md
@@ -2,35 +2,15 @@
stage: Create
group: Editor
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments"
-type: reference, how-to
-description: "The static site editor enables users to edit content on static websites without prior knowledge of the underlying templating language, site architecture or Git commands."
+remove_date: '2022-08-03'
+redirect_to: '../web_ide/index.md'
---
-# Static Site Editor **(FREE)**
+# Static Site Editor (removed) **(FREE)**
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28758) in GitLab 12.10.
-> - WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214559) in GitLab 13.0.
-> - Non-Markdown content blocks not editable on the WYSIWYG mode [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216836) in GitLab 13.3.
-> - Formatting Markdown [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49052) in GitLab 13.7.
-> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77246) in GitLab 14.7.
-
-WARNING:
-This feature is in its end-of-life process. It is
-[deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77246)
-in GitLab 14.7, and is planned for
-[removal](https://gitlab.com/groups/gitlab-org/-/epics/7351) in GitLab 14.10.
-Users should instead use the [Web Editor](../repository/web_editor.md) or [Web IDE](../web_ide/index.md). [Removal instructions](#remove-the-static-site-editor) for existing projects are included on this page.
-
-Static Site Editor (SSE) enables users to edit content on static websites without
-prior knowledge of the underlying templating language, site architecture, or
-Git commands. A contributor to your project can quickly edit a Markdown page
-and submit the changes for review. For example:
-
-- Non-technical collaborators can edit a page directly from the browser.
- They don't need to know Git and the details of your project to contribute.
-- Recently hired team members can quickly edit content.
-- Temporary collaborators can jump from project to project and quickly edit pages instead
- of having to clone or fork every single project they need to submit changes to.
+This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77246) in GitLab 14.7
+and [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/352505) in 15.0.
+Use the [Web Editor](../repository/web_editor.md) or [Web IDE](../web_ide/index.md) instead.
## Remove the Static Site Editor
@@ -68,235 +48,3 @@ from an existing project, remove links that point back to the editor:
`/helpers/custom_helpers.rb` entirely.
1. Clean up any extraneous configuration files.
1. Commit and push your changes.
-
-## Requirements
-
-- In order use the Static Site Editor feature, your project needs to be
- pre-configured with the [Static Site Editor Middleman template](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman).
-- You need to be logged into GitLab and be a member of the
- project (with Developer or higher permission levels).
-
-## How it works
-
-The Static Site Editor is in an early stage of development and only supports
-Middleman sites for now. You have to use a specific site template to start
-using it. The project template is configured to deploy a [Middleman](https://middlemanapp.com/)
-static website with [GitLab Pages](../pages/index.md).
-
-Once your website is up and running, an **Edit this page** button displays on
-the bottom-left corner of its pages:
-
-![Edit this page button](img/edit_this_page_button_v12_10.png)
-
-When you click it, GitLab opens up an editor window from which the content
-can be directly edited. When you're ready, you can submit your changes in a
-click of a button:
-
-![Static Site Editor](img/wysiwyg_editor_v13_3.png)
-
-When an editor submits their changes, these are the following actions that GitLab
-performs automatically in the background:
-
-1. Creates a new branch.
-1. Commits their changes.
- 1. Fixes formatting according to the [Handbook Markdown Style Guide](https://about.gitlab.com/handbook/markdown-guide/)
- style guide and add them through another commit.
-1. Opens a merge request against the default branch.
-
-The editor can then navigate to the merge request to assign it to a colleague for review.
-
-## Set up your project
-
-First, set up the project. Once done, you can use the Static Site Editor to
-[edit your content](#edit-content).
-
-1. To get started, create a new project from the [Static Site Editor - Middleman](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman)
- template. You can either [fork it](../repository/forking_workflow.md#creating-a-fork)
- or [create a new project from a template](../working_with_projects.md#create-a-project-from-a-built-in-template).
-1. Edit the [`data/config.yml`](#static-site-generator-configuration) configuration file
- to replace `<username>` and `<project-name>` with the proper values for
- your project's path.
-1. Optional. Edit the [`.gitlab/static-site-editor.yml`](#static-site-editor-configuration-file) file
- to customize the behavior of the Static Site Editor.
-1. When you submit your changes, GitLab triggers a CI/CD pipeline to deploy your project with GitLab Pages.
-1. When the pipeline finishes, from your project's left-side menu, go to **Settings > Pages** to find the URL of your new website.
-1. Visit your website and look at the bottom-left corner of the screen to see the new **Edit this page** button.
-
-Anyone satisfying the [requirements](#requirements) can edit the
-content of the pages without prior knowledge of Git or of your site's
-codebase.
-
-## Edit content
-
-> - Support for modifying the default merge request title and description [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216861) in GitLab 13.5.
-> - Support for selecting a merge request template [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/263252) in GitLab 13.6.
-
-After setting up your project, you can start editing content directly from the Static Site Editor.
-
-To edit a file:
-
-1. Visit the page you want to edit.
-1. Select **Edit this page**.
-1. The file is opened in the Static Site Editor in **WYSIWYG** mode. If you
- wish to edit the raw Markdown instead, you can toggle the **Markdown** mode
- in the bottom-right corner.
-1. When you're done, click **Submit changes...**.
-1. Optional. Adjust the default title and description of the merge request, to submit
- with your changes. Alternatively, select a [merge request template](../../../user/project/description_templates.md#create-a-merge-request-template)
- from the dropdown menu and edit it accordingly.
-1. Select **Submit changes**.
-1. A new merge request is automatically created and you can assign a colleague for review.
-
-### Text
-
-> Support for `*.md.erb` files [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223171) in GitLab 13.2.
-
-The Static Site Editors supports Markdown files (`.md`, `.md.erb`) for editing text.
-
-### Images
-
-> - Support for adding images through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216640) in GitLab 13.1.
-> - Support for uploading images via the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218529) in GitLab 13.6.
-
-#### Upload an image
-
-You can upload image files via the WYSIWYG editor directly to the repository to default upload directory
-`source/images`. To do so:
-
-1. Select the image icon (**{doc-image}**).
-1. Select the **Upload file** tab.
-1. To select a file from your computer, select **Choose file**.
-1. Optional. Add a description to the image for SEO and accessibility ([ALT text](https://moz.com/learn/seo/alt-text)).
-1. Select **Insert image**.
-
-The selected file can be any supported image file (`.png`, `.jpg`, `.jpeg`, `.gif`). The editor renders
-thumbnail previews so you can verify the correct image is included and there aren't any references to
-missing images.
-
-#### Link to an image
-
-You can also link to an image if you'd like:
-
-1. Select the image icon (**{doc-image}**).
-1. Select the **Link to an image** tab.
-1. Add the link to the image into the **Image URL** field (use the full path; relative paths are not supported yet).
-1. Optional. Add a description to the image for SEO and accessibility ([ALT text](https://moz.com/learn/seo/alt-text)).
-1. Select **Insert image**.
-
-The link can reference images already hosted in your project, an asset hosted
-externally on a content delivery network, or any other external URL. The editor renders thumbnail previews
-so you can verify the correct image is included and there aren't any references to missing images.
-
-### Videos
-
-> - Support for embedding YouTube videos through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216642) in GitLab 13.5.
-
-You can embed YouTube videos on the WYSIWYG mode by clicking the video icon (**{live-preview}**).
-The following URL/ID formats are supported:
-
-- **YouTube watch URLs**: `https://www.youtube.com/watch?v=0t1DgySidms`
-- **YouTube embed URLs**: `https://www.youtube.com/embed/0t1DgySidms`
-- **YouTube video IDs**: `0t1DgySidms`
-
-### Front matter
-
-> - Markdown front matter hidden on the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216834) in GitLab 13.1.
-> - Ability to edit page front matter [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235921) in GitLab 13.5.
-
-Front matter is a flexible and convenient way to define page-specific variables in data files
-intended to be parsed by a static site generator. Use it to set a page's
-title, layout template, or author. You can also pass any kind of metadata to the
-generator as the page renders out to HTML. Included at the very top of each data file, the
-front matter is often formatted as YAML or JSON, and requires consistent and accurate syntax.
-
-To edit the front matter from the Static Site Editor you can use the GitLab regular file editor,
-the Web IDE, or update the data directly from the WYSIWYG editor:
-
-1. Click the **Page settings** button on the bottom-right to reveal a web form with the data you
- have on the page's front matter. The form is populated with the current data:
-
- ![Editing page front matter in the Static Site Editor](img/front_matter_ui_v13_4.png)
-
-1. Update the values as you wish and close the panel.
-1. When you're done, click **Submit changes...**.
-1. Describe your changes (add a commit message).
-1. Click **Submit changes**.
-1. Click **View merge request** to view it.
-
-Adding new attributes to the page's front matter from the form is not supported.
-To add new attributes:
-
-- Edit the file locally
-- Edit the file with the GitLab regular file editor.
-- Edit the file with the Web IDE.
-
-After adding an attribute, the form loads the new fields.
-
-## Configuration files
-
-You can customize the behavior of a project which uses the Static Site Editor with
-the following configuration files:
-
-- The [`.gitlab/static-site-editor.yml`](#static-site-editor-configuration-file), which customizes the
- behavior of the Static Site Editor.
-- [Static Site Generator configuration files](#static-site-generator-configuration),
- such as `data/config.yml`, which configures the Static Site Generator itself.
- It also controls the **Edit this page** button when the site is generated.
-
-### Static Site Editor configuration file
-
-> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4267) in GitLab 13.6.
-
-The `.gitlab/static-site-editor.yml` configuration file contains entries you can
-use to customize behavior of the Static Site Editor (SSE). If the file does not exist,
-default values which support a default Middleman project configuration are used.
-The [Static Site Editor - Middleman](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman) project template generates a file pre-populated with these defaults.
-
-To customize the behavior of the SSE, edit `.gitlab/static-site-editor.yml`'s entries,
-according to your project's needs. Make sure to respect YAML syntax.
-
-After the table, see an [example of the SSE configuration file](#gitlabstatic-site-editoryml-example).
-
-| Entry | GitLab version | Type | Default value | Description |
-|---|---|---|---|---|
-| `image_upload_path` | [13.6](https://gitlab.com/gitlab-org/gitlab/-/issues/216641) | String | `source/images` | Directory for images uploaded from the WYSIWYG editor. |
-
-#### `.gitlab/static-site-editor.yml` example
-
-```yaml
-image_upload_path: 'source/images' # Relative path to the project's root. Don't include leading or trailing slashes.
-```
-
-### Static Site Generator configuration
-
-The Static Site Editor uses Middleman's configuration file, `data/config.yml`
-to customize the behavior of the project itself. This file also controls the
-**Edit this page** button, rendered through the file
-[`layout.erb`](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/-/blob/master/source/layouts/layout.erb).
-
-To [configure the project template to your own project](#set-up-your-project),
-you must replace the `<username>` and `<project-name>` in the `data/config.yml`
-file with the proper values for your project's path.
-
-[Other Static Site Generators](#using-other-static-site-generators) used with
-the Static Site Editor may use different configuration files or approaches.
-
-## Using Other Static Site Generators
-
-Although Middleman is the only Static Site Generator officially supported
-by the Static Site Editor, you can configure your project's build and deployment
-to use a different Static Site Generator. In this case, use the Middleman layout
-as an example, and follow a similar approach to properly render an **Edit this page**
-button in your Static Site Generator's layout.
-
-## Upgrade from GitLab 12.10 to 13.0
-
-In GitLab 13.0, we [introduced breaking changes](https://gitlab.com/gitlab-org/gitlab/-/issues/213282)
-to the URL structure of the Static Site Editor. Follow the instructions in this
-[snippet](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/snippets/1976539)
-to update your project with the 13.0 changes.
-
-## Limitations
-
-- The Static Site Editor still cannot be quickly added to existing Middleman sites.
- Follow this [epic](https://gitlab.com/groups/gitlab-org/-/epics/2784) for updates.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 0cdece3990d..b1824e17c03 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -395,11 +395,6 @@ msgid_plural "%d tags per image name"
msgstr[0] ""
msgstr[1] ""
-msgid "%d token has expired"
-msgid_plural "%d tokens have expired"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "%d unassigned issue"
msgid_plural "%d unassigned issues"
msgstr[0] ""
@@ -3575,9 +3570,6 @@ msgstr ""
msgid "AlertSettings|You can map default GitLab alert fields to your payload keys in the dropdowns below."
msgstr ""
-msgid "AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated."
-msgstr ""
-
msgid "AlertSettings|{ \"events\": [{ \"application\": \"Name of application\" }] }"
msgstr ""
@@ -5140,10 +5132,10 @@ msgstr ""
msgid "At least one of group_id or project_id must be specified"
msgstr ""
-msgid "At least one of your Personal Access Tokens is expired, but expiration enforcement is disabled. %{generate_new}"
+msgid "At least one of your Personal Access Tokens is expired. %{generate_new}"
msgstr ""
-msgid "At least one of your Personal Access Tokens will expire soon, but expiration enforcement is disabled. %{generate_new}"
+msgid "At least one of your Personal Access Tokens will expire soon. %{generate_new}"
msgstr ""
msgid "At risk"
@@ -5355,9 +5347,6 @@ msgstr ""
msgid "Authorization code:"
msgstr ""
-msgid "Authorization key"
-msgstr ""
-
msgid "Authorization required"
msgstr ""
@@ -9966,6 +9955,9 @@ msgstr ""
msgid "Copy URL"
msgstr ""
+msgid "Copy audio URL"
+msgstr ""
+
msgid "Copy branch name"
msgstr ""
@@ -9999,6 +9991,9 @@ msgstr ""
msgid "Copy file path"
msgstr ""
+msgid "Copy image URL"
+msgstr ""
+
msgid "Copy issue URL to clipboard"
msgstr ""
@@ -10047,6 +10042,9 @@ msgstr ""
msgid "Copy value"
msgstr ""
+msgid "Copy video URL"
+msgstr ""
+
msgid "Corpus Management"
msgstr ""
@@ -11817,6 +11815,9 @@ msgstr ""
msgid "Delete artifacts"
msgstr ""
+msgid "Delete audio"
+msgstr ""
+
msgid "Delete badge"
msgstr ""
@@ -11838,6 +11839,9 @@ msgstr ""
msgid "Delete file"
msgstr ""
+msgid "Delete image"
+msgstr ""
+
msgid "Delete image repository"
msgstr ""
@@ -11892,6 +11896,9 @@ msgstr ""
msgid "Delete variable"
msgstr ""
+msgid "Delete video"
+msgstr ""
+
msgid "DeleteProject|Failed to remove events. Please try again or contact administrator."
msgstr ""
@@ -12533,6 +12540,9 @@ msgstr ""
msgid "Description"
msgstr ""
+msgid "Description (alt text)"
+msgstr ""
+
msgid "Description (optional)"
msgstr ""
@@ -13440,6 +13450,9 @@ msgstr ""
msgid "Edit application"
msgstr ""
+msgid "Edit audio description"
+msgstr ""
+
msgid "Edit comment"
msgstr ""
@@ -13476,6 +13489,9 @@ msgstr ""
msgid "Edit identity for %{user_name}"
msgstr ""
+msgid "Edit image description"
+msgstr ""
+
msgid "Edit in pipeline editor"
msgstr ""
@@ -13518,6 +13534,9 @@ msgstr ""
msgid "Edit user: %{user_name}"
msgstr ""
+msgid "Edit video description"
+msgstr ""
+
msgid "Edit wiki page"
msgstr ""
@@ -13935,9 +13954,6 @@ msgstr ""
msgid "Enforce SSH key expiration"
msgstr ""
-msgid "Enforce access token expiration"
-msgstr ""
-
msgid "Enforce two-factor authentication"
msgstr ""
@@ -15427,9 +15443,6 @@ msgstr ""
msgid "Failed to request attention because no user was found."
msgstr ""
-msgid "Failed to reset key. Please try again."
-msgstr ""
-
msgid "Failed to retrieve page"
msgstr ""
@@ -16212,15 +16225,9 @@ msgstr ""
msgid "Generate group access tokens scoped to this group for your applications that need access to the GitLab API."
msgstr ""
-msgid "Generate key"
-msgstr ""
-
msgid "Generate new export"
msgstr ""
-msgid "Generate new token"
-msgstr ""
-
msgid "Generate project access tokens scoped to this project for your applications that need access to the GitLab API."
msgstr ""
@@ -22734,7 +22741,7 @@ msgstr ""
msgid "List of suitable GCP locations"
msgstr ""
-msgid "List of users allowed to exceed the rate limit."
+msgid "List of users who are allowed to exceed the rate limit. Example: username1, username2"
msgstr ""
msgid "List options"
@@ -27278,9 +27285,6 @@ msgstr ""
msgid "Personal Access Token prefix"
msgstr ""
-msgid "Personal access tokens are not revoked upon expiration."
-msgstr ""
-
msgid "Personal project creation is not allowed. Please contact your administrator with questions"
msgstr ""
@@ -30866,9 +30870,6 @@ msgstr ""
msgid "Receive a %{strongOpen}$50 gift card%{strongClose} as a thank you for your time."
msgstr ""
-msgid "Receive alerts from manually configured Prometheus servers."
-msgstr ""
-
msgid "Receive any notifications from GitLab."
msgstr ""
@@ -31497,9 +31498,18 @@ msgstr ""
msgid "Replace all label(s)"
msgstr ""
+msgid "Replace audio"
+msgstr ""
+
msgid "Replace file"
msgstr ""
+msgid "Replace image"
+msgstr ""
+
+msgid "Replace video"
+msgstr ""
+
msgid "Replaced all labels with %{label_references} %{label_text}."
msgstr ""
@@ -31979,12 +31989,6 @@ msgstr ""
msgid "Reset"
msgstr ""
-msgid "Reset authorization key"
-msgstr ""
-
-msgid "Reset authorization key?"
-msgstr ""
-
msgid "Reset file"
msgstr ""
@@ -31994,9 +31998,6 @@ msgstr ""
msgid "Reset health check access token"
msgstr ""
-msgid "Reset key"
-msgstr ""
-
msgid "Reset link will be generated and sent to the user. %{break} User will be forced to set the password on first sign in."
msgstr ""
@@ -32012,9 +32013,6 @@ msgstr ""
msgid "Reset to project defaults"
msgstr ""
-msgid "Resetting the authorization key will invalidate the previous key. Existing alert configurations will need to be updated with the new key."
-msgstr ""
-
msgid "Resolve"
msgstr ""
@@ -34062,9 +34060,6 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
-msgid "See the list of available commands in Slack after setting up this service by entering"
-msgstr ""
-
msgid "See vulnerability %{vulnerability_link} for any Remediation details."
msgstr ""
@@ -35059,12 +35054,21 @@ msgstr ""
msgid "SlackIntegration|GitLab for Slack was successfully installed."
msgstr ""
+msgid "SlackIntegration|Install Slack app"
+msgstr ""
+
msgid "SlackIntegration|Project alias"
msgstr ""
+msgid "SlackIntegration|Reinstall Slack app"
+msgstr ""
+
msgid "SlackIntegration|Remove project"
msgstr ""
+msgid "SlackIntegration|See the list of available commands in Slack after setting up this integration by entering"
+msgstr ""
+
msgid "SlackIntegration|Select a GitLab project to link with your Slack workspace."
msgstr ""
@@ -35074,12 +35078,15 @@ msgstr ""
msgid "SlackIntegration|Team name"
msgstr ""
-msgid "SlackIntegration|To set up this integration press \"Add to Slack\""
+msgid "SlackIntegration|This integration allows users to perform common operations on this project by entering slash commands in Slack."
msgstr ""
msgid "SlackIntegration|You can now close this window and go to your Slack workspace."
msgstr ""
+msgid "SlackIntegration|You may need to reinstall the Slack application when we %{linkStart}make updates or change permissions%{linkEnd}."
+msgstr ""
+
msgid "SlackService|1. %{slash_command_link_start}Add a slash command%{slash_command_link_end} in your Slack team using this information:"
msgstr ""
@@ -38731,9 +38738,6 @@ msgstr ""
msgid "This runner will only run on pipelines triggered on protected branches"
msgstr ""
-msgid "This service allows users to perform common operations on this project by entering slash commands in Slack."
-msgstr ""
-
msgid "This setting can be overridden in each project."
msgstr ""
@@ -39275,9 +39279,6 @@ msgstr ""
msgid "To reactivate your account, sign in to GitLab at %{gitlab_url}."
msgstr ""
-msgid "To receive alerts from manually configured Prometheus services, add the following URL and Authorization key to your Prometheus webhook config file. Learn more about %{linkStart}configuring Prometheus%{linkEnd} to send alerts to GitLab."
-msgstr ""
-
msgid "To renew, export your license usage file and email it to %{renewal_service_email}. A new license will be emailed to the email address registered in the %{customers_dot}. You can add this license to your instance."
msgstr ""
@@ -39443,9 +39444,6 @@ msgstr ""
msgid "Token name"
msgstr ""
-msgid "Token valid until revoked"
-msgstr ""
-
msgid "Tokens|Scopes set the permission levels granted to the token."
msgstr ""
@@ -40187,9 +40185,6 @@ msgstr ""
msgid "Unsupported todo type passed. Supported todo types are: %{todo_types}"
msgstr ""
-msgid "Until revoked, expired personal access tokens pose a security risk."
-msgstr ""
-
msgid "Unused"
msgstr ""
@@ -41546,9 +41541,6 @@ msgstr ""
msgid "VisibilityLevel|Unknown"
msgstr ""
-msgid "Visit settings page"
-msgstr ""
-
msgid "Visual Studio Code (HTTPS)"
msgstr ""
diff --git a/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb
index d066953d12e..b6296b5a263 100644
--- a/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb
+++ b/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb
@@ -3,7 +3,10 @@
require 'parallel'
module QA
- RSpec.describe 'Create' do
+ RSpec.describe 'Create', quarantine: {
+ issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/361382',
+ type: :investigating
+ } do
context 'Gitaly Cluster replication queue', :orchestrated, :gitaly_cluster, :skip_live_env do
let(:praefect_manager) { Service::PraefectManager.new }
let(:project) do
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 04f73050ea5..c7ab509bf59 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -79,7 +79,7 @@ RSpec.describe 'Database schema' do
repository_languages: %w[programming_language_id],
routes: %w[source_id],
sent_notifications: %w[project_id noteable_id recipient_id commit_id in_reply_to_discussion_id],
- slack_integrations: %w[team_id user_id],
+ slack_integrations: %w[team_id user_id bot_user_id], # these are external Slack IDs
snippets: %w[author_id],
spam_logs: %w[user_id],
status_check_responses: %w[external_approval_rule_id],
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index 507d427bf0b..b87ac743d02 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -76,14 +76,12 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j
end
it 'has a link to resolve all threads by creating an issue' do
- page.within '.mr-widget-body' do
- expect(page).to have_link 'Create issue to resolve all threads', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
- end
+ expect(page).to have_link 'Create issue to resolve all threads', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
context 'creating an issue for threads' do
before do
- page.within '.mr-widget-body' do
+ page.within '.mr-state-widget' do
page.click_link 'Create issue to resolve all threads', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
wait_for_all_requests
diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb
index d4b185a82e9..a680ec78b2f 100644
--- a/spec/features/merge_request/user_accepts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
click_button('Merge')
- expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
+ expect(page).to have_content("Changes merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
end
context 'when merge method is set to fast-forward merge' do
@@ -31,7 +31,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
click_button('Merge')
- expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
+ expect(page).to have_content("Changes merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
end
it 'accepts a merge request with squash and merge' do
@@ -41,7 +41,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
click_button('Merge')
- expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
+ expect(page).to have_content("Changes merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
end
end
end
@@ -55,7 +55,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
check('Delete source branch')
click_button('Merge')
- expect(page).to have_content('The changes were merged into')
+ expect(page).to have_content('Changes merged into')
expect(page).not_to have_selector('.js-remove-branch-button')
# Wait for View Resource requests to complete so they don't blow up if they are
@@ -72,7 +72,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
it 'accepts a merge request' do
click_button('Merge')
- expect(page).to have_content('The changes were merged into')
+ expect(page).to have_content('Changes merged into')
expect(page).to have_selector('.js-remove-branch-button')
# Wait for View Resource requests to complete so they don't blow up if they are
@@ -90,7 +90,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
check('Delete source branch')
click_button('Merge')
- expect(page).to have_content('The changes were merged into')
+ expect(page).to have_content('Changes merged into')
expect(page).not_to have_selector('.js-remove-branch-button')
# Wait for View Resource requests to complete so they don't blow up if they are
@@ -107,7 +107,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
end
it 'accepts a merge request' do
- find('.js-mr-widget-commits-count').click
+ find('[data-testid="widget_edit_commit_message"]').click
fill_in('merge-message-edit', with: 'wow such merge')
click_button('Merge')
diff --git a/spec/features/merge_request/user_assigns_themselves_spec.rb b/spec/features/merge_request/user_assigns_themselves_spec.rb
index fc925781a3b..2aaddc7791b 100644
--- a/spec/features/merge_request/user_assigns_themselves_spec.rb
+++ b/spec/features/merge_request/user_assigns_themselves_spec.rb
@@ -30,12 +30,6 @@ RSpec.describe 'Merge request > User assigns themselves' do
end.to change { merge_request.reload.updated_at }
end
- it 'returns user to the merge request', :js do
- click_link 'Assign yourself to these issues'
-
- expect(page).to have_content merge_request.description
- end
-
context 'when related issues are already assigned' do
before do
[issue1, issue2].each { |issue| issue.update!(assignees: [user]) }
diff --git a/spec/features/merge_request/user_awards_emoji_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb
index 35eadb34799..81a88cad458 100644
--- a/spec/features/merge_request/user_awards_emoji_spec.rb
+++ b/spec/features/merge_request/user_awards_emoji_spec.rb
@@ -38,6 +38,10 @@ RSpec.describe 'Merge request > User awards emoji', :js do
it 'adds awards to note' do
page.within('.note-actions') do
first('.note-emoji-button').click
+
+ # make sure emoji popup is visible
+ execute_script("window.scrollBy(0, 200)")
+
find('gl-emoji[data-name="8ball"]').click
end
diff --git a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
index 67a232607cd..059e1eb89c5 100644
--- a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
+++ b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'Merge request < User customizes merge commit message', :js do
it 'has commit message without description' do
expect(page).not_to have_selector('#merge-message-edit')
- first('.js-mr-widget-commits-count').click
+ find('[data-testid="widget_edit_commit_message"]').click
expect(merge_textbox).to be_visible
expect(merge_textbox.value).to eq(default_merge_commit_message)
end
@@ -51,7 +51,7 @@ RSpec.describe 'Merge request < User customizes merge commit message', :js do
it 'uses merge commit template' do
expect(page).not_to have_selector('#merge-message-edit')
- first('.js-mr-widget-commits-count').click
+ find('[data-testid="widget_edit_commit_message"]').click
expect(merge_textbox).to be_visible
expect(merge_textbox.value).to eq(merge_request.title)
end
@@ -62,7 +62,7 @@ RSpec.describe 'Merge request < User customizes merge commit message', :js do
it 'has default message with merge request title' do
expect(page).not_to have_selector('#squash-message-edit')
- first('.js-mr-widget-commits-count').click
+ find('[data-testid="widget_edit_commit_message"]').click
expect(squash_textbox).to be_visible
expect(merge_textbox).to be_visible
expect(squash_textbox.value).to eq(merge_request.title)
@@ -74,7 +74,7 @@ RSpec.describe 'Merge request < User customizes merge commit message', :js do
it 'uses squash commit template' do
expect(page).not_to have_selector('#squash-message-edit')
- first('.js-mr-widget-commits-count').click
+ find('[data-testid="widget_edit_commit_message"]').click
expect(squash_textbox).to be_visible
expect(merge_textbox).to be_visible
expect(squash_textbox.value).to eq(merge_request.description)
diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb
index 3a05f35a671..91327059e0f 100644
--- a/spec/features/merge_request/user_merges_immediately_spec.rb
+++ b/spec/features/merge_request/user_merges_immediately_spec.rb
@@ -30,17 +30,17 @@ RSpec.describe 'Merge requests > User merges immediately', :js do
it 'enables merge immediately' do
wait_for_requests
- page.within '.mr-widget-body' do
+ page.within '[data-testid="ready_to_merge_state"]' do
find('.dropdown-toggle').click
Sidekiq::Testing.fake! do
click_button 'Merge immediately'
-
- expect(find('.media-body h4')).to have_content('Merging!')
-
- wait_for_requests
end
end
+
+ expect(find('.media-body h4')).to have_content('Merging!')
+
+ wait_for_requests
end
end
end
diff --git a/spec/features/merge_request/user_merges_merge_request_spec.rb b/spec/features/merge_request/user_merges_merge_request_spec.rb
index a861ca2eea5..12518d634ec 100644
--- a/spec/features/merge_request/user_merges_merge_request_spec.rb
+++ b/spec/features/merge_request/user_merges_merge_request_spec.rb
@@ -27,6 +27,7 @@ RSpec.describe "User merges a merge request", :js do
let(:project) { create(:project, :public, :repository, merge_requests_ff_only_enabled: true) }
before do
+ stub_feature_flags(restructured_mr_widget: false)
visit(merge_request_path(merge_request))
end
diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
index 4d7ee11e366..d6b132b18da 100644
--- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do
wait_for_requests
- expect(page).to have_css('button[disabled="disabled"]', text: 'Merge')
+ expect(page).not_to have_button('Merge')
expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or learn about other solutions.')
end
end
diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
index 9057b96bff0..21f96299958 100644
--- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
click_button "Merge when pipeline succeeds"
expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds"
- expect(page).to have_content "Does not delete the source branch"
+ expect(page).to have_content "Source branch will not be deleted"
expect(page).to have_selector ".js-cancel-auto-merge"
visit project_merge_request_path(project, merge_request) # Needed to refresh the page
expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
@@ -64,6 +64,9 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
context 'when enabled after it was previously canceled' do
before do
click_button "Merge when pipeline succeeds"
+
+ wait_for_requests
+
click_button "Cancel auto-merge"
wait_for_requests
@@ -123,12 +126,6 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
expect(page).to have_content "canceled the automatic merge"
end
- it 'allows to delete source branch' do
- click_button "Delete source branch"
-
- expect(page).to have_content "Deletes the source branch"
- end
-
context 'when pipeline succeeds' do
before do
build.success
@@ -136,7 +133,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
end
it 'merges merge request', :sidekiq_might_not_need_inline do
- expect(page).to have_content 'The changes were merged'
+ expect(page).to have_content 'Changes merged'
expect(merge_request.reload).to be_merged
end
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 150eee87cb0..2dafd66b406 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -321,8 +321,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- expect(page).to have_selector('.accept-merge-request')
- expect(find('.accept-merge-request')['disabled']).not_to be(true)
+ expect(page).not_to have_selector('.accept-merge-request')
end
end
@@ -385,9 +384,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Merge Merge blocked: fast-forward merge is not possible. To merge this request, first rebase locally.')
- end
+ expect(page).to have_content('Merge blocked: fast-forward merge is not possible. To merge this request, first rebase locally.')
end
end
@@ -445,7 +442,6 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
it 'user cannot remove source branch', :sidekiq_might_not_need_inline do
expect(page).not_to have_field('remove-source-branch-input')
- expect(page).to have_content('Deletes the source branch')
end
end
diff --git a/spec/features/merge_request/user_squashes_merge_request_spec.rb b/spec/features/merge_request/user_squashes_merge_request_spec.rb
index 2a48657ac4f..da0d4ca23d1 100644
--- a/spec/features/merge_request/user_squashes_merge_request_spec.rb
+++ b/spec/features/merge_request/user_squashes_merge_request_spec.rb
@@ -79,7 +79,7 @@ RSpec.describe 'User squashes a merge request', :js do
context 'when squash message is the same as existing commit message' do
before do
- click_button("Modify commit messages")
+ find('[data-testid="widget_edit_commit_message"]').click
fill_in('Squash commit message', with: project.commit(source_branch).safe_message)
accept_mr
end
diff --git a/spec/features/projects/integrations/prometheus_external_alerts_spec.rb b/spec/features/projects/integrations/prometheus_external_alerts_spec.rb
deleted file mode 100644
index 7e56ca13e23..00000000000
--- a/spec/features/projects/integrations/prometheus_external_alerts_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Prometheus external alerts', :js do
- include_context 'project integration activation'
-
- let(:alerts_section_selector) { '.js-prometheus-alerts' }
- let(:alerts_section) { page.find(alerts_section_selector) }
-
- context 'with manual configuration' do
- before do
- create(:prometheus_integration, project: project, api_url: 'http://prometheus.example.com', manual_configuration: '1', active: true)
- end
-
- it 'shows the Alerts section' do
- visit_project_integration('Prometheus')
-
- expect(alerts_section).to have_content('Alerts')
- expect(alerts_section).to have_content('Receive alerts from manually configured Prometheus servers.')
- expect(alerts_section).to have_content('URL')
- expect(alerts_section).to have_content('Authorization key')
- end
- end
-
- context 'with no configuration' do
- it 'does not show the Alerts section' do
- visit_project_integration('Prometheus')
- wait_for_requests
-
- expect(page).not_to have_css(alerts_section_selector)
- end
- end
-end
diff --git a/spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb b/spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb
new file mode 100644
index 00000000000..8cdfa13ba3a
--- /dev/null
+++ b/spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::ProjectsRequiringAuthorizationsRefresh::OnDirectMembershipFinder do
+ # rubocop:disable Layout/LineLength
+
+ # Group X Group A ------shared with-------------> Group B Group C
+ # | Group X_subgroup_1 | | |
+ # | | Project X_subgroup_1 ---shared with----->| Group A_subgroup_1 | Group B_subgroup_1 <--shared with--------- | Group C_subgroup_1
+ # | | | Project A_subgroup_1 | | Project B_subgroup_1 | | Project C_subgroup_1
+ # | Group A_subgroup_2 | Group B_subgroup_2 <----shared with ------- Project C
+ # | |Project A_subgroup_2 | | Project B_subgroup_2
+
+ # rubocop:enable Layout/LineLength
+
+ let_it_be(:group_x) { create(:group) }
+ let_it_be(:group_a) { create(:group) }
+ let_it_be(:group_b) { create(:group) }
+ let_it_be(:group_c) { create(:group) }
+ let_it_be(:group_x_subgroup_1) { create(:group, parent: group_x) }
+ let_it_be(:group_a_subgroup_1) { create(:group, parent: group_a) }
+ let_it_be(:group_a_subgroup_2) { create(:group, parent: group_a) }
+ let_it_be(:group_b_subgroup_1) { create(:group, parent: group_b) }
+ let_it_be(:group_b_subgroup_2) { create(:group, parent: group_b) }
+ let_it_be(:group_c_subgroup_1) { create(:group, parent: group_c) }
+ let_it_be(:project_x_subgroup_1) { create(:project, group: group_x_subgroup_1, name: 'project_x_subgroup_1') }
+ let_it_be(:project_a_subgroup_1) { create(:project, group: group_a_subgroup_1, name: 'project_a_subgroup_1') }
+ let_it_be(:project_a_subgroup_2) { create(:project, group: group_a_subgroup_2, name: 'project_a_subgroup_2') }
+ let_it_be(:project_b_subgroup_1) { create(:project, group: group_b_subgroup_1, name: 'project_b_subgroup_1') }
+ let_it_be(:project_b_subgroup_2) { create(:project, group: group_b_subgroup_2, name: 'project_b_subgroup_2') }
+ let_it_be(:project_c_subgroup_1) { create(:project, group: group_c_subgroup_1, name: 'project_c_subgroup_1') }
+ let_it_be(:project_c) { create(:project, group: group_c, name: 'project_c') }
+
+ describe '#execute' do
+ context 'projects affected when a new member is added to a specific group (here, `Group B`)' do
+ subject(:result) { described_class.new(group_b).execute }
+
+ before do
+ create(:project_group_link, project: project_x_subgroup_1, group: group_a_subgroup_1)
+ create(:project_group_link, project: project_c, group: group_b_subgroup_2)
+ create(:group_group_link, shared_group: group_a, shared_with_group: group_b)
+ create(:group_group_link, shared_group: group_c_subgroup_1, shared_with_group: group_b_subgroup_1)
+ end
+
+ it 'returns all projects IDs where authorizations need to be created for the user'\
+ 'due to their new membership being created in `Group B`' do
+ new_user = create(:user)
+ group_b.add_maintainer(new_user)
+
+ expect(result).to match_array(new_user.authorized_projects.ids)
+ end
+
+ it 'includes only the expected projects' do
+ expected_projects = Project.id_in(
+ [
+ project_b_subgroup_1, # direct member of Group B gets access to this project due to group hierarchy
+ project_b_subgroup_2, # direct member of Group B gets access to this project due to group hierarchy
+ project_c, # direct member of Group B gets access to this project via project-group share
+ project_a_subgroup_1, # direct member of Group B gets access to this project via group share
+ project_a_subgroup_2, # direct member of Group B gets access to this project via group share
+
+ # direct member of Group B gets access to any projects shared with groups within its shared groups.
+ project_x_subgroup_1
+ ]
+ )
+ # project_c_subgroup_1 is not included in the list because only 'direct' members of
+ # `group_b_subgroup_1` gets access to that project via the group-group share.
+ expect(result).to match_array(expected_projects.ids)
+ end
+ end
+ end
+end
diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb
index 7607d08dc64..f22bff62082 100644
--- a/spec/finders/personal_access_tokens_finder_spec.rb
+++ b/spec/finders/personal_access_tokens_finder_spec.rb
@@ -286,24 +286,6 @@ RSpec.describe PersonalAccessTokensFinder do
end
end
- describe 'with active or expired state' do
- before do
- params[:state] = 'active_or_expired'
- end
-
- it 'includes active tokens' do
- is_expected.to include(active_personal_access_token, active_impersonation_token)
- end
-
- it 'includes expired tokens' do
- is_expected.to include(expired_personal_access_token, expired_impersonation_token)
- end
-
- it 'does not include revoked tokens' do
- is_expected.not_to include(revoked_personal_access_token, revoked_impersonation_token)
- end
- end
-
describe 'with id' do
subject { finder(params).find_by_id(active_personal_access_token.id) }
diff --git a/spec/frontend/content_editor/components/bubble_menus/link_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_spec.js
index 4d6796a9da4..5910b9c110d 100644
--- a/spec/frontend/content_editor/components/bubble_menus/link_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/link_spec.js
@@ -1,6 +1,5 @@
-import { GlLink, GlForm, GlFormInput } from '@gitlab/ui';
+import { GlLink, GlForm } from '@gitlab/ui';
import { BubbleMenu } from '@tiptap/vue-2';
-import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
@@ -12,11 +11,13 @@ const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jes
describe('content_editor/components/bubble_menus/link', () => {
let wrapper;
let tiptapEditor;
+ let contentEditor;
let bubbleMenu;
let eventHub;
const buildEditor = () => {
tiptapEditor = createTestEditor({ extensions: [Link] });
+ contentEditor = { resolveLink: jest.fn() };
eventHub = eventHubFactory();
};
@@ -24,6 +25,7 @@ describe('content_editor/components/bubble_menus/link', () => {
wrapper = mountExtended(LinkBubbleMenu, {
provide: {
tiptapEditor,
+ contentEditor,
eventHub,
},
});
@@ -133,8 +135,8 @@ describe('content_editor/components/bubble_menus/link', () => {
beforeEach(async () => {
await wrapper.findByTestId('edit-link').vm.$emit('click');
- linkHrefInput = wrapper.findByTestId('link-href-group').findComponent(GlFormInput);
- linkTitleInput = wrapper.findByTestId('link-title-group').findComponent(GlFormInput);
+ linkHrefInput = wrapper.findByTestId('link-href');
+ linkTitleInput = wrapper.findByTestId('link-title');
});
it('hides the link and copy/edit/remove link buttons', async () => {
@@ -160,11 +162,9 @@ describe('content_editor/components/bubble_menus/link', () => {
tiptapEditor.commands.setTextSelection(3);
await emitEditorEvent({ event: 'transaction', tiptapEditor });
- await nextTick();
- // tiptapEditor.commands.setTextSelection(14);
- // await emitEditorEvent({ event: 'transaction', tiptapEditor });
- // await nextTick();
+ tiptapEditor.commands.setTextSelection(14);
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
expectLinkButtonsToExist(true);
expect(wrapper.findComponent(GlForm).exists()).toBe(false);
@@ -175,6 +175,8 @@ describe('content_editor/components/bubble_menus/link', () => {
linkHrefInput.setValue('https://google.com');
linkTitleInput.setValue('Search Google');
+ contentEditor.resolveLink.mockResolvedValue('https://google.com');
+
await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
});
@@ -214,8 +216,8 @@ describe('content_editor/components/bubble_menus/link', () => {
// click edit once again to show the form back
await wrapper.findByTestId('edit-link').vm.$emit('click');
- linkHrefInput = wrapper.findByTestId('link-href-group').findComponent(GlFormInput);
- linkTitleInput = wrapper.findByTestId('link-title-group').findComponent(GlFormInput);
+ linkHrefInput = wrapper.findByTestId('link-href');
+ linkTitleInput = wrapper.findByTestId('link-title');
expect(linkHrefInput.element.value).toBe('uploads/my_file.pdf');
expect(linkTitleInput.element.value).toBe('Click here to download');
diff --git a/spec/frontend/content_editor/components/bubble_menus/media_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_spec.js
new file mode 100644
index 00000000000..a4bcfa9c39e
--- /dev/null
+++ b/spec/frontend/content_editor/components/bubble_menus/media_spec.js
@@ -0,0 +1,234 @@
+import { GlLink, GlForm } from '@gitlab/ui';
+import { BubbleMenu } from '@tiptap/vue-2';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media.vue';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import Image from '~/content_editor/extensions/image';
+import Audio from '~/content_editor/extensions/audio';
+import Video from '~/content_editor/extensions/video';
+import { createTestEditor, emitEditorEvent, mockChainedCommands } from '../../test_utils';
+import {
+ PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
+ PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
+ PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
+} from '../../test_constants';
+
+const TIPTAP_IMAGE_HTML = `<p>
+ <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon" data-canonical-src="https://gitlab.com/favicon.png">
+</p>`;
+
+const TIPTAP_AUDIO_HTML = `<p>
+ <span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
+</p>`;
+
+const TIPTAP_VIDEO_HTML = `<p>
+ <span class="media-container video-container"><video src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></video><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
+</p>`;
+
+const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
+
+describe.each`
+ mediaType | mediaHTML | filePath | mediaOutputHTML
+ ${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML}
+ ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML}
+ ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML}
+`(
+ 'content_editor/components/bubble_menus/media ($mediaType)',
+ ({ mediaType, mediaHTML, filePath, mediaOutputHTML }) => {
+ let wrapper;
+ let tiptapEditor;
+ let contentEditor;
+ let bubbleMenu;
+ let eventHub;
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video] });
+ contentEditor = { resolveLink: jest.fn() };
+ eventHub = eventHubFactory();
+ };
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(MediaBubbleMenu, {
+ provide: {
+ tiptapEditor,
+ contentEditor,
+ eventHub,
+ },
+ });
+ };
+
+ const selectFile = async (file) => {
+ const input = wrapper.find({ ref: 'fileSelector' });
+
+ // override the property definition because `input.files` isn't directly modifyable
+ Object.defineProperty(input.element, 'files', { value: [file], writable: true });
+ await input.trigger('change');
+ };
+
+ const expectLinkButtonsToExist = (exist = true) => {
+ expect(wrapper.findComponent(GlLink).exists()).toBe(exist);
+ expect(wrapper.findByTestId('copy-media-src').exists()).toBe(exist);
+ expect(wrapper.findByTestId('edit-media').exists()).toBe(exist);
+ expect(wrapper.findByTestId('delete-media').exists()).toBe(exist);
+ };
+
+ beforeEach(async () => {
+ buildEditor();
+ buildWrapper();
+
+ tiptapEditor
+ .chain()
+ .insertContent(mediaHTML)
+ .setNodeSelection(4) // select the media
+ .run();
+
+ contentEditor.resolveLink.mockResolvedValue(`/group1/project1/-/wikis/${filePath}`);
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ bubbleMenu = wrapper.findComponent(BubbleMenu);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders bubble menu component', async () => {
+ expect(bubbleMenu.props('editor')).toBe(tiptapEditor);
+ expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
+ });
+
+ it('shows a clickable link to the image', async () => {
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes()).toEqual(
+ expect.objectContaining({
+ href: `/group1/project1/-/wikis/${filePath}`,
+ 'aria-label': filePath,
+ title: filePath,
+ target: '_blank',
+ }),
+ );
+ expect(link.text()).toBe(filePath);
+ });
+
+ describe('copy button', () => {
+ it(`copies the canonical link to the ${mediaType} to clipboard`, async () => {
+ jest.spyOn(navigator.clipboard, 'writeText');
+
+ await wrapper.findByTestId('copy-media-src').vm.$emit('click');
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(filePath);
+ });
+ });
+
+ describe(`remove ${mediaType} button`, () => {
+ it(`removes the ${mediaType}`, async () => {
+ await wrapper.findByTestId('delete-media').vm.$emit('click');
+
+ expect(tiptapEditor.getHTML()).toBe('<p>\n \n</p>');
+ });
+ });
+
+ describe(`replace ${mediaType} button`, () => {
+ it('uploads and replaces the selected image when file input changes', async () => {
+ const commands = mockChainedCommands(tiptapEditor, [
+ 'focus',
+ 'deleteSelection',
+ 'uploadAttachment',
+ 'run',
+ ]);
+ const file = new File(['foo'], 'foo.png', { type: 'image/png' });
+
+ await wrapper.findByTestId('replace-media').vm.$emit('click');
+ await selectFile(file);
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.deleteSelection).toHaveBeenCalled();
+ expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
+ expect(commands.run).toHaveBeenCalled();
+ });
+ });
+
+ describe('edit button', () => {
+ let mediaSrcInput;
+ let mediaTitleInput;
+ let mediaAltInput;
+
+ beforeEach(async () => {
+ await wrapper.findByTestId('edit-media').vm.$emit('click');
+
+ mediaSrcInput = wrapper.findByTestId('media-src');
+ mediaTitleInput = wrapper.findByTestId('media-title');
+ mediaAltInput = wrapper.findByTestId('media-alt');
+ });
+
+ it('hides the link and copy/edit/remove link buttons', async () => {
+ expectLinkButtonsToExist(false);
+ });
+
+ it(`shows a form to edit the ${mediaType} src/title/alt`, () => {
+ expect(wrapper.findComponent(GlForm).exists()).toBe(true);
+
+ expect(mediaSrcInput.element.value).toBe(filePath);
+ expect(mediaTitleInput.element.value).toBe('');
+ expect(mediaAltInput.element.value).toBe('test-file');
+ });
+
+ describe('after making changes in the form and clicking apply', () => {
+ beforeEach(async () => {
+ mediaSrcInput.setValue('https://gitlab.com/favicon.png');
+ mediaAltInput.setValue('gitlab favicon');
+ mediaTitleInput.setValue('gitlab favicon');
+
+ contentEditor.resolveLink.mockResolvedValue('https://gitlab.com/favicon.png');
+
+ await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
+ });
+
+ it(`updates prosemirror doc with new src to the ${mediaType}`, async () => {
+ expect(tiptapEditor.getHTML()).toBe(mediaOutputHTML);
+ });
+
+ it(`updates the link to the ${mediaType} in the bubble menu`, () => {
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes()).toEqual(
+ expect.objectContaining({
+ href: 'https://gitlab.com/favicon.png',
+ 'aria-label': 'https://gitlab.com/favicon.png',
+ title: 'https://gitlab.com/favicon.png',
+ target: '_blank',
+ }),
+ );
+ expect(link.text()).toBe('https://gitlab.com/favicon.png');
+ });
+ });
+
+ describe('after making changes in the form and clicking cancel', () => {
+ beforeEach(async () => {
+ mediaSrcInput.setValue('https://gitlab.com/favicon.png');
+ mediaAltInput.setValue('gitlab favicon');
+ mediaTitleInput.setValue('gitlab favicon');
+
+ await wrapper.findByTestId('cancel-editing-media').vm.$emit('click');
+ });
+
+ it('hides the form and shows the copy/edit/remove link buttons', () => {
+ expectLinkButtonsToExist();
+ });
+
+ it(`resets the form with old values of the ${mediaType} from prosemirror`, async () => {
+ // click edit once again to show the form back
+ await wrapper.findByTestId('edit-media').vm.$emit('click');
+
+ mediaSrcInput = wrapper.findByTestId('media-src');
+ mediaTitleInput = wrapper.findByTestId('media-title');
+ mediaAltInput = wrapper.findByTestId('media-alt');
+
+ expect(mediaSrcInput.element.value).toBe(filePath);
+ expect(mediaAltInput.element.value).toBe('test-file');
+ expect(mediaTitleInput.element.value).toBe('');
+ });
+ });
+ });
+ },
+);
diff --git a/spec/frontend/content_editor/components/wrappers/media_spec.js b/spec/frontend/content_editor/components/wrappers/media_spec.js
deleted file mode 100644
index 3e95e2f3914..00000000000
--- a/spec/frontend/content_editor/components/wrappers/media_spec.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { NodeViewWrapper } from '@tiptap/vue-2';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import MediaWrapper from '~/content_editor/components/wrappers/media.vue';
-
-describe('content/components/wrappers/media', () => {
- let wrapper;
-
- const createWrapper = async (nodeAttrs = {}) => {
- wrapper = shallowMountExtended(MediaWrapper, {
- propsData: {
- node: {
- attrs: nodeAttrs,
- type: {
- name: 'image',
- },
- },
- },
- });
- };
- const findMedia = () => wrapper.findByTestId('media');
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders a node-view-wrapper with display-inline-block class', () => {
- createWrapper();
-
- expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-display-inline-block');
- });
-
- it('renders an image that displays the node src', () => {
- const src = 'foobar.png';
-
- createWrapper({ src });
-
- expect(findMedia().attributes().src).toBe(src);
- });
-
- describe('when uploading', () => {
- beforeEach(() => {
- createWrapper({ uploading: true });
- });
-
- it('renders a gl-loading-icon component', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
- it('adds gl-opacity-5 class selector to the media tag', () => {
- expect(findMedia().classes()).toContain('gl-opacity-5');
- });
- });
-
- describe('when not uploading', () => {
- beforeEach(() => {
- createWrapper({ uploading: false });
- });
-
- it('does not render a gl-loading-icon component', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('does not add gl-opacity-5 class selector to the media tag', () => {
- expect(findMedia().classes()).not.toContain('gl-opacity-5');
- });
- });
-});
diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js
index d3c42104e47..d528096be34 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -11,32 +11,12 @@ import { VARIANT_DANGER } from '~/flash';
import httpStatus from '~/lib/utils/http_status';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, createDocBuilder } from '../test_utils';
-
-const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
- <a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.png">
- <img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png">
- </a>
-</p>`;
-
-const PROJECT_WIKI_ATTACHMENT_VIDEO_HTML = `<p data-sourcepos="1:1-1:132" dir="auto">
- <span class="media-container video-container">
- <video src="/group1/project1/-/wikis/test-file.mp4" controls="true" data-setup="{}" data-title="test-file" width="400" preload="metadata" data-canonical-src="test-file.mp4">
- </video>
- <a href="/himkp/test/-/wikis/test-file.mp4" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp4">test-file</a>
- </span>
-</p>`;
-
-const PROJECT_WIKI_ATTACHMENT_AUDIO_HTML = `<p data-sourcepos="3:1-3:74" dir="auto">
- <span class="media-container audio-container">
- <audio src="/himkp/test/-/wikis/test-file.mp3" controls="true" data-setup="{}" data-title="test-file" data-canonical-src="test-file.mp3">
- </audio>
- <a href="/himkp/test/-/wikis/test-file.mp3" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp3">test-file</a>
- </span>
-</p>`;
-
-const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto">
- <a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a>
-</p>`;
+import {
+ PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
+ PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
+ PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
+ PROJECT_WIKI_ATTACHMENT_LINK_HTML,
+} from '../test_constants';
describe('content_editor/extensions/attachment', () => {
let tiptapEditor;
diff --git a/spec/frontend/content_editor/services/asset_resolver_spec.js b/spec/frontend/content_editor/services/asset_resolver_spec.js
new file mode 100644
index 00000000000..f4e7d9bf881
--- /dev/null
+++ b/spec/frontend/content_editor/services/asset_resolver_spec.js
@@ -0,0 +1,23 @@
+import createAssetResolver from '~/content_editor/services/asset_resolver';
+
+describe('content_editor/services/asset_resolver', () => {
+ let renderMarkdown;
+ let assetResolver;
+
+ beforeEach(() => {
+ renderMarkdown = jest.fn();
+ assetResolver = createAssetResolver({ renderMarkdown });
+ });
+
+ describe('resolveUrl', () => {
+ it('resolves a canonical url to an absolute url', async () => {
+ renderMarkdown.mockResolvedValue(
+ '<p><a href="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png">link</a></p>',
+ );
+
+ expect(await assetResolver.resolveUrl('test-file.png')).toBe(
+ '/group1/project1/-/wikis/test-file.png',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js
index 18c5d89e391..fde4f8f6282 100644
--- a/spec/frontend/content_editor/services/content_editor_spec.js
+++ b/spec/frontend/content_editor/services/content_editor_spec.js
@@ -26,7 +26,7 @@ describe('content_editor/services/content_editor', () => {
tiptapEditor,
}));
- serializer = { deserialize: jest.fn() };
+ serializer = { serialize: jest.fn() };
deserializer = { deserialize: jest.fn() };
languageLoader = { loadLanguages: jest.fn() };
eventHub = eventHubFactory();
diff --git a/spec/frontend/content_editor/test_constants.js b/spec/frontend/content_editor/test_constants.js
new file mode 100644
index 00000000000..45a0e4a8bd1
--- /dev/null
+++ b/spec/frontend/content_editor/test_constants.js
@@ -0,0 +1,25 @@
+export const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
+ <a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.png">
+ <img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png">
+ </a>
+</p>`;
+
+export const PROJECT_WIKI_ATTACHMENT_VIDEO_HTML = `<p data-sourcepos="1:1-1:132" dir="auto">
+ <span class="media-container video-container">
+ <video src="/group1/project1/-/wikis/test-file.mp4" controls="true" data-setup="{}" data-title="test-file" width="400" preload="metadata" data-canonical-src="test-file.mp4">
+ </video>
+ <a href="/group1/project1/-/wikis/test-file.mp4" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp4">test-file</a>
+ </span>
+</p>`;
+
+export const PROJECT_WIKI_ATTACHMENT_AUDIO_HTML = `<p data-sourcepos="3:1-3:74" dir="auto">
+ <span class="media-container audio-container">
+ <audio src="/group1/project1/-/wikis/test-file.mp3" controls="true" data-setup="{}" data-title="test-file" data-canonical-src="test-file.mp3">
+ </audio>
+ <a href="/group1/project1/-/wikis/test-file.mp3" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp3">test-file</a>
+ </span>
+</p>`;
+
+export const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto">
+ <a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a>
+</p>`;
diff --git a/spec/frontend/prometheus_alerts/components/reset_key_spec.js b/spec/frontend/prometheus_alerts/components/reset_key_spec.js
deleted file mode 100644
index dc5fdb1dffc..00000000000
--- a/spec/frontend/prometheus_alerts/components/reset_key_spec.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-import ResetKey from '~/prometheus_alerts/components/reset_key.vue';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-
-describe('ResetKey', () => {
- let mock;
- let vm;
-
- const propsData = {
- initialAuthorizationKey: 'abcd1234',
- changeKeyUrl: '/updateKeyUrl',
- notifyUrl: '/root/autodevops-deploy/prometheus/alerts/notify.json',
- learnMoreUrl: '/learnMore',
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- setFixtures('<div class="flash-container"></div><div id="reset-key"></div>');
- });
-
- afterEach(() => {
- mock.restore();
- vm.destroy();
- });
-
- describe('authorization key exists', () => {
- beforeEach(() => {
- propsData.initialAuthorizationKey = 'abcd1234';
- vm = shallowMount(ResetKey, {
- propsData,
- });
- });
-
- it('shows fields and buttons', () => {
- expect(vm.find('#notify-url').attributes('value')).toEqual(propsData.notifyUrl);
- expect(vm.find('#authorization-key').attributes('value')).toEqual(
- propsData.initialAuthorizationKey,
- );
-
- expect(vm.findAll(ClipboardButton).length).toBe(2);
- expect(vm.find('.js-reset-auth-key').text()).toEqual('Reset key');
- });
-
- it('reset updates key', async () => {
- mock.onPost(propsData.changeKeyUrl).replyOnce(200, { token: 'newToken' });
-
- vm.find(GlModal).vm.$emit('ok');
-
- await nextTick();
- await waitForPromises();
- expect(vm.vm.authorizationKey).toEqual('newToken');
- expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken');
- });
-
- it('reset key failure shows error', async () => {
- mock.onPost(propsData.changeKeyUrl).replyOnce(500);
-
- vm.find(GlModal).vm.$emit('ok');
-
- await nextTick();
- await waitForPromises();
- expect(vm.find('#authorization-key').attributes('value')).toEqual(
- propsData.initialAuthorizationKey,
- );
-
- expect(document.querySelector('.flash-container').innerText.trim()).toEqual(
- 'Failed to reset key. Please try again.',
- );
- });
- });
-
- describe('authorization key has not been set', () => {
- beforeEach(() => {
- propsData.initialAuthorizationKey = '';
- vm = shallowMount(ResetKey, {
- propsData,
- });
- });
-
- it('shows Generate Key button', () => {
- expect(vm.find('.js-reset-auth-key').text()).toEqual('Generate key');
- expect(vm.find('#authorization-key').attributes('value')).toEqual('');
- });
-
- it('Generate key button triggers key change', async () => {
- mock.onPost(propsData.changeKeyUrl).replyOnce(200, { token: 'newToken' });
-
- vm.find('.js-reset-auth-key').vm.$emit('click');
-
- await waitForPromises();
- expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken');
- });
- });
-});
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index 2ef856c90ab..405813be4e3 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -35,6 +35,8 @@ import {
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_STALE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import adminRunnersQuery from '~/runner/graphql/list/admin_runners.query.graphql';
@@ -52,6 +54,7 @@ import {
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
const mockRunners = runnersData.data.runners.nodes;
+const mockRunnersCount = runnersCountData.data.runners.count;
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -124,18 +127,6 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
- it('shows total runner counts', async () => {
- createComponent({ mountFn: mountExtended });
-
- await waitForPromises();
-
- const stats = findRunnerStats().text();
-
- expect(stats).toMatch('Online runners 4');
- expect(stats).toMatch('Offline runners 4');
- expect(stats).toMatch('Stale runners 4');
- });
-
it('shows the runner tabs with a runner count for each type', async () => {
mockRunnersCountQuery.mockImplementation(({ type }) => {
let count;
@@ -197,6 +188,24 @@ describe('AdminRunnersApp', () => {
expect(findRegistrationDropdown().props('type')).toBe(INSTANCE_TYPE);
});
+ it('shows total runner counts', async () => {
+ expect(mockRunnersCountQuery).toHaveBeenCalledWith({
+ status: STATUS_ONLINE,
+ });
+ expect(mockRunnersCountQuery).toHaveBeenCalledWith({
+ status: STATUS_OFFLINE,
+ });
+ expect(mockRunnersCountQuery).toHaveBeenCalledWith({
+ status: STATUS_STALE,
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ onlineRunnersCount: mockRunnersCount,
+ offlineRunnersCount: mockRunnersCount,
+ staleRunnersCount: mockRunnersCount,
+ });
+ });
+
it('shows the runners list', () => {
expect(findRunnerList().props('runners')).toEqual(mockRunners);
});
@@ -329,13 +338,30 @@ describe('AdminRunnersApp', () => {
first: RUNNER_PAGE_SIZE,
});
});
+
+ it('fetches count results for requested status', () => {
+ expect(mockRunnersCountQuery).toHaveBeenCalledWith({
+ type: INSTANCE_TYPE,
+ status: STATUS_ONLINE,
+ tagList: ['tag1'],
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ onlineRunnersCount: mockRunnersCount,
+ });
+ });
});
describe('when a filter is selected by the user', () => {
beforeEach(() => {
+ mockRunnersCountQuery.mockClear();
+
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
- filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
+ filters: [
+ { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
+ { type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } },
+ ],
sort: CREATED_ASC,
});
});
@@ -343,17 +369,45 @@ describe('AdminRunnersApp', () => {
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
- url: 'http://test.host/admin/runners?status[]=ONLINE&sort=CREATED_ASC',
+ url: 'http://test.host/admin/runners?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC',
});
});
it('requests the runners with filters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
status: STATUS_ONLINE,
+ tagList: ['tag1'],
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
});
+
+ it('fetches count results for requested status', () => {
+ expect(mockRunnersCountQuery).toHaveBeenCalledWith({
+ tagList: ['tag1'],
+ status: STATUS_ONLINE,
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ onlineRunnersCount: mockRunnersCount,
+ });
+ });
+
+ it('skips fetching count results for status that were not in filter', () => {
+ expect(mockRunnersCountQuery).not.toHaveBeenCalledWith({
+ tagList: ['tag1'],
+ status: STATUS_OFFLINE,
+ });
+ expect(mockRunnersCountQuery).not.toHaveBeenCalledWith({
+ tagList: ['tag1'],
+ status: STATUS_STALE,
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ offlineRunnersCount: null,
+ staleRunnersCount: null,
+ });
+ });
});
it('when runners have not loaded, shows a loading state', () => {
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 02348bf737a..52bd51a974b 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -30,7 +30,10 @@ import {
PROJECT_TYPE,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
+ PARAM_KEY_TAG,
STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_STALE,
RUNNER_PAGE_SIZE,
I18N_EDIT,
} from '~/runner/constants';
@@ -53,7 +56,7 @@ Vue.use(GlToast);
const mockGroupFullPath = 'group1';
const mockRegistrationToken = 'AABBCC';
const mockGroupRunnersEdges = groupRunnersData.data.group.runners.edges;
-const mockGroupRunnersLimitedCount = mockGroupRunnersEdges.length;
+const mockGroupRunnersCount = mockGroupRunnersEdges.length;
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -94,7 +97,7 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersLimitedCount,
+ groupRunnersLimitedCount: mockGroupRunnersCount,
...props,
},
provide: {
@@ -115,15 +118,24 @@ describe('GroupRunnersApp', () => {
});
it('shows total runner counts', async () => {
- createComponent({ mountFn: mountExtended });
-
- await waitForPromises();
-
- const stats = findRunnerStats().text();
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ status: STATUS_ONLINE,
+ });
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ status: STATUS_OFFLINE,
+ });
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ status: STATUS_STALE,
+ });
- expect(stats).toMatch('Online runners 2');
- expect(stats).toMatch('Offline runners 2');
- expect(stats).toMatch('Stale runners 2');
+ expect(findRunnerStats().props()).toMatchObject({
+ onlineRunnersCount: mockGroupRunnersCount,
+ offlineRunnersCount: mockGroupRunnersCount,
+ staleRunnersCount: mockGroupRunnersCount,
+ });
});
it('shows the runner tabs with a runner count for each type', async () => {
@@ -281,13 +293,28 @@ describe('GroupRunnersApp', () => {
first: RUNNER_PAGE_SIZE,
});
});
+
+ it('fetches count results for requested status', () => {
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ type: INSTANCE_TYPE,
+ status: STATUS_ONLINE,
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ onlineRunnersCount: mockGroupRunnersCount,
+ });
+ });
});
describe('when a filter is selected by the user', () => {
beforeEach(async () => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
- filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
+ filters: [
+ { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
+ { type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } },
+ ],
sort: CREATED_ASC,
});
@@ -297,7 +324,7 @@ describe('GroupRunnersApp', () => {
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
- url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&sort=CREATED_ASC',
+ url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC',
});
});
@@ -305,10 +332,41 @@ describe('GroupRunnersApp', () => {
expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
status: STATUS_ONLINE,
+ tagList: ['tag1'],
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
});
+
+ it('fetches count results for requested status', () => {
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ tagList: ['tag1'],
+ status: STATUS_ONLINE,
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ onlineRunnersCount: mockGroupRunnersCount,
+ });
+ });
+
+ it('skips fetching count results for status that were not in filter', () => {
+ expect(mockGroupRunnersCountQuery).not.toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ tagList: ['tag1'],
+ status: STATUS_OFFLINE,
+ });
+ expect(mockGroupRunnersCountQuery).not.toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ tagList: ['tag1'],
+ status: STATUS_STALE,
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ offlineRunnersCount: null,
+ staleRunnersCount: null,
+ });
+ });
});
it('when runners have not loaded, shows a loading state', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 22c5735803b..da3a323e8ea 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -322,7 +322,6 @@ describe('ReadyToMerge', () => {
await waitForPromises();
- expect(wrapper.vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
transition: 'start-auto-merge',
@@ -349,7 +348,6 @@ describe('ReadyToMerge', () => {
await waitForPromises();
- expect(wrapper.vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
const params = wrapper.vm.service.merge.mock.calls[0][0];
@@ -372,7 +370,6 @@ describe('ReadyToMerge', () => {
await waitForPromises();
- expect(wrapper.vm.isMakingRequest).toBeTruthy();
expect(wrapper.vm.mr.transitionStateMachine).toHaveBeenCalledWith({
transition: 'start-merge',
});
diff --git a/spec/graphql/mutations/container_repositories/destroy_spec.rb b/spec/graphql/mutations/container_repositories/destroy_spec.rb
index 3903196a511..7c674dddb15 100644
--- a/spec/graphql/mutations/container_repositories/destroy_spec.rb
+++ b/spec/graphql/mutations/container_repositories/destroy_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Mutations::ContainerRepositories::Destroy do
let_it_be(:user) { create(:user) }
let(:project) { container_repository.project }
- let(:id) { container_repository.to_global_id.to_s }
+ let(:id) { container_repository.to_global_id }
specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image) }
diff --git a/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb b/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb
index f22d9ffe753..3e5f28ee244 100644
--- a/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb
+++ b/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb
@@ -3,10 +3,12 @@
require 'spec_helper'
RSpec.describe Mutations::ContainerRepositories::DestroyTags do
+ include GraphqlHelpers
+
include_context 'container repository delete tags service shared context'
using RSpec::Parameterized::TableSyntax
- let(:id) { repository.to_global_id.to_s }
+ let(:id) { repository.to_global_id }
specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image) }
@@ -67,8 +69,8 @@ RSpec.describe Mutations::ContainerRepositories::DestroyTags do
end
end
- context 'with invalid id' do
- let(:id) { 'gid://gitlab/ContainerRepository/5555' }
+ context 'with non-existing id' do
+ let(:id) { global_id_of(id: non_existing_record_id, model_name: 'ContainerRepository') }
it_behaves_like 'denying access to container respository'
end
diff --git a/spec/graphql/mutations/customer_relations/contacts/create_spec.rb b/spec/graphql/mutations/customer_relations/contacts/create_spec.rb
index d17d11305b1..dafc7b4c367 100644
--- a/spec/graphql/mutations/customer_relations/contacts/create_spec.rb
+++ b/spec/graphql/mutations/customer_relations/contacts/create_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Mutations::CustomerRelations::Contacts::Create do
+ include GraphqlHelpers
+
let_it_be(:user) { create(:user) }
let(:group) { create(:group, :crm_enabled) }
@@ -78,9 +80,9 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Create do
end
end
- context 'when organization_id is invalid' do
+ context 'when organization does not exist' do
before do
- valid_params[:organization_id] = "gid://gitlab/CustomerRelations::Organization/#{non_existing_record_id}"
+ valid_params[:organization_id] = global_id_of(model_name: 'CustomerRelations::Organization', id: non_existing_record_id)
end
it 'returns the relevant error' do
diff --git a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb
index 2041b86d6e7..e5dc6f85c2a 100644
--- a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb
+++ b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Mutations::Discussions::ToggleResolve do
+ include GraphqlHelpers
+
subject(:mutation) do
described_class.new(object: nil, context: { current_user: user }, field: nil)
end
@@ -15,7 +17,7 @@ RSpec.describe Mutations::Discussions::ToggleResolve do
mutation.resolve(id: id_arg, resolve: resolve_arg)
end
- let(:id_arg) { discussion.to_global_id.to_s }
+ let(:id_arg) { global_id_of(discussion) }
let(:resolve_arg) { true }
let(:mutated_discussion) { subject[:discussion] }
let(:errors) { subject[:errors] }
@@ -36,7 +38,7 @@ RSpec.describe Mutations::Discussions::ToggleResolve do
let_it_be(:user) { create(:user, developer_projects: [project]) }
context 'when discussion cannot be found' do
- let(:id_arg) { "#{discussion.to_global_id}foo" }
+ let(:id_arg) { global_id_of(id: non_existing_record_id, model_name: discussion.class.name) }
it 'raises an error' do
expect { subject }.to raise_error(
@@ -52,7 +54,7 @@ RSpec.describe Mutations::Discussions::ToggleResolve do
it 'raises an error' do
expect { subject }.to raise_error(
GraphQL::CoercionError,
- "\"#{discussion.to_global_id}\" does not represent an instance of Discussion"
+ "\"#{global_id_of(discussion)}\" does not represent an instance of Discussion"
)
end
end
diff --git a/spec/graphql/mutations/environments/canary_ingress/update_spec.rb b/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
index fdf9cbaf25b..e719ca050a8 100644
--- a/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
+++ b/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Mutations::Environments::CanaryIngress::Update do
describe '#resolve' do
subject { mutation.resolve(id: environment_id, weight: weight) }
- let(:environment_id) { environment.to_global_id.to_s }
+ let(:environment_id) { environment.to_global_id }
let(:weight) { 50 }
let(:update_service) { double('update_service') }
diff --git a/spec/graphql/mutations/release_asset_links/delete_spec.rb b/spec/graphql/mutations/release_asset_links/delete_spec.rb
index cda292f2ffa..67576bdda57 100644
--- a/spec/graphql/mutations/release_asset_links/delete_spec.rb
+++ b/spec/graphql/mutations/release_asset_links/delete_spec.rb
@@ -52,7 +52,9 @@ RSpec.describe Mutations::ReleaseAssetLinks::Delete do
end
context "when the link doesn't exist" do
- let(:mutation_arguments) { super().merge(id: "gid://gitlab/Releases::Link/#{non_existing_record_id}") }
+ let(:mutation_arguments) do
+ super().merge(id: global_id_of(id: non_existing_record_id, model_name: release_link.class.name))
+ end
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
diff --git a/spec/graphql/mutations/release_asset_links/update_spec.rb b/spec/graphql/mutations/release_asset_links/update_spec.rb
index 64648687336..cb7474123ad 100644
--- a/spec/graphql/mutations/release_asset_links/update_spec.rb
+++ b/spec/graphql/mutations/release_asset_links/update_spec.rb
@@ -186,7 +186,9 @@ RSpec.describe Mutations::ReleaseAssetLinks::Update do
end
context "when the link doesn't exist" do
- let(:mutation_arguments) { super().merge(id: "gid://gitlab/Releases::Link/#{non_existing_record_id}") }
+ let(:mutation_arguments) do
+ super().merge(id: global_id_of(id: non_existing_record_id, model_name: "Releases::Link"))
+ end
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
diff --git a/spec/graphql/mutations/timelogs/delete_spec.rb b/spec/graphql/mutations/timelogs/delete_spec.rb
index 5012d10f32e..f4a258e0f78 100644
--- a/spec/graphql/mutations/timelogs/delete_spec.rb
+++ b/spec/graphql/mutations/timelogs/delete_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Mutations::Timelogs::Delete do
+ include GraphqlHelpers
+
let_it_be(:author) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:administrator) { create(:user, :admin) }
@@ -11,7 +13,7 @@ RSpec.describe Mutations::Timelogs::Delete do
let_it_be_with_reload(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) }
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
- let(:timelog_id) { timelog.to_global_id.to_s }
+ let(:timelog_id) { global_id_of(timelog) }
let(:mutation_arguments) { { id: timelog_id } }
describe '#resolve' do
@@ -21,7 +23,7 @@ RSpec.describe Mutations::Timelogs::Delete do
context 'when the timelog id is not valid' do
let(:current_user) { author }
- let(:timelog_id) { 'gid://gitlab/Timelog/%d' % non_existing_record_id }
+ let(:timelog_id) { global_id_of(model_name: 'Timelog', id: non_existing_record_id) }
it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
diff --git a/spec/graphql/resolvers/concerns/resolves_ids_spec.rb b/spec/graphql/resolvers/concerns/resolves_ids_spec.rb
index 1dd27c0eff0..732b7cd2bbc 100644
--- a/spec/graphql/resolvers/concerns/resolves_ids_spec.rb
+++ b/spec/graphql/resolvers/concerns/resolves_ids_spec.rb
@@ -3,33 +3,35 @@
require 'spec_helper'
RSpec.describe ResolvesIds do
+ include GraphqlHelpers
+
# gid://gitlab/Project/6
# gid://gitlab/Issue/6
# gid://gitlab/Project/6 gid://gitlab/Issue/6
context 'with a single project' do
- let(:ids) { 'gid://gitlab/Project/6' }
+ let(:ids) { global_id_of(model_name: 'Project', id: 6) }
let(:type) { ::Types::GlobalIDType[::Project] }
it 'returns the correct array' do
- expect(resolve_ids).to match_array(['6'])
+ expect(resolve_ids).to contain_exactly('6')
end
end
context 'with a single issue' do
- let(:ids) { 'gid://gitlab/Issue/9' }
+ let(:ids) { global_id_of(model_name: 'Issue', id: 9) }
let(:type) { ::Types::GlobalIDType[::Issue] }
it 'returns the correct array' do
- expect(resolve_ids).to match_array(['9'])
+ expect(resolve_ids).to contain_exactly('9')
end
end
context 'with multiple users' do
- let(:ids) { ['gid://gitlab/User/7', 'gid://gitlab/User/13', 'gid://gitlab/User/21'] }
+ let(:ids) { [7, 13, 21].map { global_id_of(model_name: 'User', id: _1) } }
let(:type) { ::Types::GlobalIDType[::User] }
it 'returns the correct array' do
- expect(resolve_ids).to match_array(%w[7 13 21])
+ expect(resolve_ids).to eq %w[7 13 21]
end
end
diff --git a/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb b/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb
index a16e8821cb5..3fe1ec4b5a4 100644
--- a/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb
+++ b/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Resolvers::DesignManagement::DesignAtVersionResolver do
let(:current_user) { user }
let(:object) { issue.design_collection }
- let(:global_id) { GitlabSchema.id_from_object(design_at_version).to_s }
+ let(:global_id) { GitlabSchema.id_from_object(design_at_version) }
let(:design_at_version) { ::DesignManagement::DesignAtVersion.new(design: design_a, version: version_a) }
diff --git a/spec/graphql/resolvers/design_management/design_resolver_spec.rb b/spec/graphql/resolvers/design_management/design_resolver_spec.rb
index 4c8b3116875..5b530b68a5b 100644
--- a/spec/graphql/resolvers/design_management/design_resolver_spec.rb
+++ b/spec/graphql/resolvers/design_management/design_resolver_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Resolvers::DesignManagement::DesignResolver do
create(:design, issue: create(:issue, project: project), versions: [create(:design_version)])
end
- let(:args) { { id: GitlabSchema.id_from_object(first_design).to_s } }
+ let(:args) { { id: GitlabSchema.id_from_object(first_design) } }
let(:gql_context) { { current_user: current_user } }
before do
@@ -50,7 +50,7 @@ RSpec.describe Resolvers::DesignManagement::DesignResolver do
end
context 'when both arguments have been passed' do
- let(:args) { { filename: first_design.filename, id: GitlabSchema.id_from_object(first_design).to_s } }
+ let(:args) { { filename: first_design.filename, id: GitlabSchema.id_from_object(first_design) } }
it 'generates an error' do
expect_graphql_error_to_be_created(::Gitlab::Graphql::Errors::ArgumentError, /may/) do
diff --git a/spec/graphql/resolvers/design_management/designs_resolver_spec.rb b/spec/graphql/resolvers/design_management/designs_resolver_spec.rb
index b091e58b06f..64eae14d888 100644
--- a/spec/graphql/resolvers/design_management/designs_resolver_spec.rb
+++ b/spec/graphql/resolvers/design_management/designs_resolver_spec.rb
@@ -109,6 +109,8 @@ RSpec.describe Resolvers::DesignManagement::DesignsResolver do
end
def resolve_designs
- resolve(described_class, obj: issue.design_collection, args: args, ctx: gql_context)
+ Gitlab::Graphql::Lazy.force(
+ resolve(described_class, obj: issue.design_collection, args: args, ctx: gql_context)
+ )
end
end
diff --git a/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb b/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb
index 32c53ba2302..4b34a750883 100644
--- a/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb
+++ b/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb
@@ -74,6 +74,6 @@ RSpec.describe Resolvers::ErrorTracking::SentryDetailedErrorResolver do
end
def issue_global_id(issue_id)
- Gitlab::ErrorTracking::DetailedError.new(id: issue_id).to_global_id.to_s
+ Gitlab::ErrorTracking::DetailedError.new(id: issue_id).to_global_id
end
end
diff --git a/spec/graphql/resolvers/timelog_resolver_spec.rb b/spec/graphql/resolvers/timelog_resolver_spec.rb
index 84fa2932829..da2747fdf72 100644
--- a/spec/graphql/resolvers/timelog_resolver_spec.rb
+++ b/spec/graphql/resolvers/timelog_resolver_spec.rb
@@ -265,7 +265,7 @@ RSpec.describe Resolvers::TimelogResolver do
context 'when > `default_max_page_size` records' do
let(:object) { nil }
let!(:timelog_list) { create_list(:timelog, 101, issue: issue) }
- let(:args) { { project_id: "gid://gitlab/Project/#{project.id}" } }
+ let(:args) { { project_id: global_id_of(project) } }
let(:extra_args) { {} }
it 'pagination returns `default_max_page_size` and sets `has_next_page` true' do
diff --git a/spec/graphql/resolvers/work_item_resolver_spec.rb b/spec/graphql/resolvers/work_item_resolver_spec.rb
index bfa0cf1d8a2..c44ed395102 100644
--- a/spec/graphql/resolvers/work_item_resolver_spec.rb
+++ b/spec/graphql/resolvers/work_item_resolver_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Resolvers::WorkItemResolver do
let(:current_user) { developer }
- subject(:resolved_work_item) { resolve_work_item('id' => work_item.to_gid.to_s) }
+ subject(:resolved_work_item) { resolve_work_item('id' => work_item.to_gid) }
context 'when the user can read the work item' do
it { is_expected.to eq(work_item) }
diff --git a/spec/graphql/types/terraform/state_version_type_spec.rb b/spec/graphql/types/terraform/state_version_type_spec.rb
index b015a2045da..6a17d932d03 100644
--- a/spec/graphql/types/terraform/state_version_type_spec.rb
+++ b/spec/graphql/types/terraform/state_version_type_spec.rb
@@ -52,8 +52,8 @@ RSpec.describe GitlabSchema.types['TerraformStateVersion'] do
shared_examples 'returning latest version' do
it 'returns latest version of terraform state' do
- expect(execute.dig('data', 'project', 'terraformState', 'latestVersion', 'id')).to eq(
- global_id_of(terraform_state.latest_version)
+ expect(execute.dig('data', 'project', 'terraformState', 'latestVersion')).to match a_graphql_entity_for(
+ terraform_state.latest_version
)
end
end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 125ac7fb102..69866d497a1 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -94,14 +94,6 @@ RSpec.describe PersonalAccessToken do
end
end
- describe '#expired_but_not_enforced?' do
- let(:token) { build(:personal_access_token) }
-
- it 'returns false', :aggregate_failures do
- expect(token).not_to be_expired_but_not_enforced
- end
- end
-
describe 'Redis storage' do
let(:user_id) { 123 }
let(:token) { 'KS3wegQYXBLYhQsciwsj' }
diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
index e8fb9daa43b..eb206465bce 100644
--- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
@@ -69,6 +69,10 @@ RSpec.describe 'get board lists' do
let(:data_path) { [board_parent_type, :boards, :nodes, 0, :lists] }
+ def pagination_results_data(lists)
+ lists
+ end
+
def pagination_query(params)
graphql_query_for(
board_parent_type,
@@ -94,7 +98,7 @@ RSpec.describe 'get board lists' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { }
let(:first_param) { 2 }
- let(:all_records) { lists.map { |list| global_id_of(list) } }
+ let(:all_records) { lists.map { |list| a_graphql_entity_for(list) } }
end
end
end
diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb
index ddb2664d353..2fb90dcd92b 100644
--- a/spec/requests/api/graphql/ci/job_spec.rb
+++ b/spec/requests/api/graphql/ci/job_spec.rb
@@ -47,10 +47,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
)
post_graphql(query, current_user: user)
- expect(graphql_data_at(*path)).to match a_hash_including(
- 'id' => global_id_of(job_2),
- 'name' => job_2.name,
- 'allowFailure' => job_2.allow_failure,
+ expect(graphql_data_at(*path)).to match a_graphql_entity_for(
+ job_2, :name, :allow_failure,
'duration' => 25,
'kind' => 'BUILD',
'queuedDuration' => 2.0,
@@ -66,10 +64,7 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
it 'retrieves scalar fields' do
post_graphql(query, current_user: user)
- expect(graphql_data_at(*path)).to match a_hash_including(
- 'id' => global_id_of(job_2),
- 'name' => job_2.name
- )
+ expect(graphql_data_at(*path)).to match a_graphql_entity_for(job_2, :name)
end
end
end
@@ -102,8 +97,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
'name' => test_stage.name,
'jobs' => a_hash_including(
'nodes' => contain_exactly(
- a_hash_including('id' => global_id_of(job_2)),
- a_hash_including('id' => global_id_of(job_3))
+ a_graphql_entity_for(job_2),
+ a_graphql_entity_for(job_3)
)
)
)
diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
index 922a9ab277e..847fa72522e 100644
--- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
+++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
@@ -127,7 +127,7 @@ RSpec.describe 'container repository details' do
let(:query) do
<<~GQL
- query($id: ID!, $n: Int) {
+ query($id: ContainerRepositoryID!, $n: Int) {
containerRepository(id: $id) {
tags(first: $n) {
edges {
@@ -157,7 +157,7 @@ RSpec.describe 'container repository details' do
let(:query) do
<<~GQL
- query($id: ID!, $n: ContainerRepositoryTagSort) {
+ query($id: ContainerRepositoryID!, $n: ContainerRepositoryTagSort) {
containerRepository(id: $id) {
tags(sort: $n) {
edges {
@@ -194,7 +194,7 @@ RSpec.describe 'container repository details' do
let(:query) do
<<~GQL
- query($id: ID!, $n: String) {
+ query($id: ContainerRepositoryID!, $n: String) {
containerRepository(id: $id) {
tags(name: $n) {
edges {
@@ -232,7 +232,7 @@ RSpec.describe 'container repository details' do
let(:query) do
<<~GQL
- query($id: ID!) {
+ query($id: ContainerRepositoryID!) {
containerRepository(id: $id) {
size
}
diff --git a/spec/requests/api/graphql/current_user_todos_spec.rb b/spec/requests/api/graphql/current_user_todos_spec.rb
index 7f37abba74a..da1c893ec2b 100644
--- a/spec/requests/api/graphql/current_user_todos_spec.rb
+++ b/spec/requests/api/graphql/current_user_todos_spec.rb
@@ -37,8 +37,8 @@ RSpec.describe 'A Todoable that implements the CurrentUserTodos interface' do
post_graphql(query, current_user: current_user)
expect(todoable_response).to contain_exactly(
- a_hash_including('id' => global_id_of(done_todo)),
- a_hash_including('id' => global_id_of(pending_todo))
+ a_graphql_entity_for(done_todo),
+ a_graphql_entity_for(pending_todo)
)
end
@@ -63,7 +63,7 @@ RSpec.describe 'A Todoable that implements the CurrentUserTodos interface' do
post_graphql(query, current_user: current_user)
expect(todoable_response).to contain_exactly(
- a_hash_including('id' => global_id_of(pending_todo))
+ a_graphql_entity_for(pending_todo)
)
end
end
@@ -75,7 +75,7 @@ RSpec.describe 'A Todoable that implements the CurrentUserTodos interface' do
post_graphql(query, current_user: current_user)
expect(todoable_response).to contain_exactly(
- a_hash_including('id' => global_id_of(done_todo))
+ a_graphql_entity_for(done_todo)
)
end
end
diff --git a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb
index 3527c8183f6..c7149c100b2 100644
--- a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb
+++ b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb
@@ -122,12 +122,12 @@ RSpec.describe 'getting dependency proxy manifests in a group' do
let(:current_user) { owner }
context 'with default sorting' do
- let_it_be(:descending_manifests) { manifests.reverse.map { |manifest| global_id_of(manifest)} }
+ let_it_be(:descending_manifests) { manifests.reverse.map { |manifest| global_id_of(manifest) } }
it_behaves_like 'sorted paginated query' do
let(:sort_param) { '' }
let(:first_param) { 2 }
- let(:all_records) { descending_manifests }
+ let(:all_records) { descending_manifests.map(&:to_s) }
end
end
diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb
index 8830320c6f7..fec866486ae 100644
--- a/spec/requests/api/graphql/group/group_members_spec.rb
+++ b/spec/requests/api/graphql/group/group_members_spec.rb
@@ -24,8 +24,8 @@ RSpec.describe 'getting group members information' do
expect(graphql_errors).to be_nil
expect(graphql_data_at(:group, :group_members, :edges, :node)).to contain_exactly(
- { 'user' => { 'id' => global_id_of(user_1) } },
- { 'user' => { 'id' => global_id_of(user_2) } },
+ { 'user' => a_graphql_entity_for(user_1) },
+ { 'user' => a_graphql_entity_for(user_2) },
'user' => nil
)
end
@@ -224,8 +224,8 @@ RSpec.describe 'getting group members information' do
def expect_array_response(*items)
expect(response).to have_gitlab_http_status(:success)
- member_gids = graphql_data_at(:group, :group_members, :edges, :node, :user, :id)
+ members = graphql_data_at(:group, :group_members, :edges, :node, :user)
- expect(member_gids).to match_array(items.map { |u| global_id_of(u) })
+ expect(members).to match_array(items.map { |u| a_graphql_entity_for(u) })
end
end
diff --git a/spec/requests/api/graphql/group/merge_requests_spec.rb b/spec/requests/api/graphql/group/merge_requests_spec.rb
index c0faff11c8d..434b0d16569 100644
--- a/spec/requests/api/graphql/group/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/group/merge_requests_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'Query.group.mergeRequests' do
end
def expected_mrs(mrs)
- mrs.map { |mr| a_hash_including('id' => global_id_of(mr)) }
+ mrs.map { |mr| a_graphql_entity_for(mr) }
end
describe 'not passing any arguments' do
diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb
index 2b80b5239c8..7c51409f907 100644
--- a/spec/requests/api/graphql/group/milestones_spec.rb
+++ b/spec/requests/api/graphql/group/milestones_spec.rb
@@ -170,10 +170,8 @@ RSpec.describe 'Milestones through GroupQuery' do
end
it 'returns correct values for scalar fields' do
- expect(post_query).to eq({
- 'id' => global_id_of(milestone),
- 'title' => milestone.title,
- 'description' => milestone.description,
+ expect(post_query).to match a_graphql_entity_for(
+ milestone, :title, :description,
'state' => 'active',
'webPath' => milestone_path(milestone),
'dueDate' => milestone.due_date.iso8601,
@@ -183,7 +181,7 @@ RSpec.describe 'Milestones through GroupQuery' do
'projectMilestone' => false,
'groupMilestone' => true,
'subgroupMilestone' => false
- })
+ )
end
context 'milestone statistics' do
diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb
index 42ca3348384..05fd6bf3022 100644
--- a/spec/requests/api/graphql/issue/issue_spec.rb
+++ b/spec/requests/api/graphql/issue/issue_spec.rb
@@ -8,8 +8,8 @@ RSpec.describe 'Query.issue(id)' do
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:issue_params) { { 'id' => issue.to_global_id.to_s } }
+ let(:issue_params) { { 'id' => global_id_of(issue) } }
let(:issue_data) { graphql_data['issue'] }
let(:issue_fields) { all_graphql_fields_for('Issue'.classify) }
@@ -100,7 +100,8 @@ RSpec.describe 'Query.issue(id)' do
let_it_be(:issue_fields) { ['moved', 'movedTo { title }'] }
let_it_be(:new_issue) { create(:issue) }
let_it_be(:issue) { create(:issue, project: project, moved_to: new_issue) }
- let_it_be(:issue_params) { { 'id' => issue.to_global_id.to_s } }
+
+ let(:issue_params) { { 'id' => global_id_of(issue) } }
before_all do
new_issue.project.add_developer(current_user)
diff --git a/spec/requests/api/graphql/merge_request/merge_request_spec.rb b/spec/requests/api/graphql/merge_request/merge_request_spec.rb
index 75dd01a0763..d89f381753e 100644
--- a/spec/requests/api/graphql/merge_request/merge_request_spec.rb
+++ b/spec/requests/api/graphql/merge_request/merge_request_spec.rb
@@ -8,8 +8,8 @@ RSpec.describe 'Query.merge_request(id)' do
let_it_be(:project) { create(:project, :empty_repo) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:merge_request_params) { { 'id' => merge_request.to_global_id.to_s } }
+ let(:merge_request_params) { { 'id' => global_id_of(merge_request) } }
let(:merge_request_data) { graphql_data['mergeRequest'] }
let(:merge_request_fields) { all_graphql_fields_for('MergeRequest'.classify) }
diff --git a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
index 02b79dac489..715507c3cc5 100644
--- a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Setting issues crm contacts' do
let(:operation_mode) { Types::MutationOperationModeEnum.default_mode }
let(:initial_contacts) { contacts[0..1] }
let(:mutation_contacts) { contacts[1..2] }
- let(:contact_ids) { contact_global_ids(mutation_contacts) }
+ let(:contact_ids) { mutation_contacts.map { global_id_of(_1) } }
let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
let(:mutation) do
@@ -45,8 +45,8 @@ RSpec.describe 'Setting issues crm contacts' do
graphql_mutation_response(:issue_set_crm_contacts)
end
- def contact_global_ids(contacts)
- contacts.map { |contact| global_id_of(contact) }
+ def expected_contacts(contacts)
+ contacts.map { |contact| a_graphql_entity_for(contact) }
end
before do
@@ -58,8 +58,8 @@ RSpec.describe 'Setting issues crm contacts' do
it 'updates the issue with correct contacts' do
post_graphql_mutation(mutation, current_user: user)
- expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
- .to match_array(contact_global_ids(mutation_contacts))
+ expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes))
+ .to match_array(expected_contacts(mutation_contacts))
end
end
@@ -70,8 +70,8 @@ RSpec.describe 'Setting issues crm contacts' do
it 'updates the issue with correct contacts' do
post_graphql_mutation(mutation, current_user: user)
- expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
- .to match_array(contact_global_ids(initial_contacts + mutation_contacts))
+ expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes))
+ .to match_array(expected_contacts(initial_contacts + mutation_contacts))
end
end
@@ -82,8 +82,8 @@ RSpec.describe 'Setting issues crm contacts' do
it 'updates the issue with correct contacts' do
post_graphql_mutation(mutation, current_user: user)
- expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
- .to match_array(contact_global_ids(initial_contacts - mutation_contacts))
+ expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes))
+ .to match_array(expected_contacts(initial_contacts - mutation_contacts))
end
end
end
@@ -117,7 +117,7 @@ RSpec.describe 'Setting issues crm contacts' do
it_behaves_like 'successful mutation'
context 'when the contact does not exist' do
- let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] }
+ let(:contact_ids) { [global_id_of(model_name: 'CustomerRelations::Contact', id: non_existing_record_id)] }
it 'returns expected error' do
post_graphql_mutation(mutation, current_user: user)
@@ -159,7 +159,7 @@ RSpec.describe 'Setting issues crm contacts' do
context 'when trying to remove non-existent contact' do
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
- let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] }
+ let(:contact_ids) { [global_id_of(model_name: 'CustomerRelations::Contact', id: non_existing_record_id)] }
it 'raises expected error' do
post_graphql_mutation(mutation, current_user: user)
diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
index 63b94dccca0..dee8f80bc5d 100644
--- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe 'Adding a Note' do
it 'creates a Note in a discussion' do
post_graphql_mutation(mutation, current_user: current_user)
- expect(mutation_response['note']['discussion']['id']).to eq(discussion.to_global_id.to_s)
+ expect(mutation_response['note']['discussion']).to match a_graphql_entity_for(discussion)
end
context 'when the discussion_id is not for a Discussion' do
diff --git a/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb
index 89e3a71280f..0f7ccac3179 100644
--- a/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'Repositioning an ImageDiffNote' do
post_graphql_mutation(mutation, current_user: current_user)
end.to change { note.reset.position.x }.to(10)
- expect(mutation_response['note']).to eq('id' => global_id_of(note))
+ expect(mutation_response['note']).to match a_graphql_entity_for(note)
expect(mutation_response['errors']).to be_empty
end
@@ -59,7 +59,7 @@ RSpec.describe 'Repositioning an ImageDiffNote' do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { note.reset.position.x }
- expect(mutation_response['note']).to eq('id' => global_id_of(note))
+ expect(mutation_response['note']).to match a_graphql_entity_for(note)
expect(mutation_response['errors']).to be_empty
end
end
diff --git a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb
index c5c34e16717..dc20fde8e3c 100644
--- a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb
@@ -46,8 +46,8 @@ RSpec.describe 'Marking all todos done' do
expect(todo3.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
- updated_todo_ids = mutation_response['todos'].map { |todo| todo['id'] }
- expect(updated_todo_ids).to contain_exactly(global_id_of(todo1), global_id_of(todo3))
+ updated_todos = mutation_response['todos']
+ expect(updated_todos).to contain_exactly(a_graphql_entity_for(todo1), a_graphql_entity_for(todo3))
end
context 'when target_id is given', :aggregate_failures do
@@ -66,8 +66,8 @@ RSpec.describe 'Marking all todos done' do
expect(todo1.reload.state).to eq('pending')
expect(todo3.reload.state).to eq('pending')
- updated_todo_ids = mutation_response['todos'].map { |todo| todo['id'] }
- expect(updated_todo_ids).to contain_exactly(global_id_of(target_todo1), global_id_of(target_todo2))
+ updated_todos = mutation_response['todos']
+ expect(updated_todos).to contain_exactly(a_graphql_entity_for(target_todo1), a_graphql_entity_for(target_todo2))
end
context 'when target does not exist' do
diff --git a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
index 70e3cc7f5cd..4316bd060c1 100644
--- a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
@@ -11,8 +11,8 @@ RSpec.describe 'Restoring many Todos' do
let_it_be(:author) { create(:user) }
let_it_be(:other_user) { create(:user) }
- let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :done, target: issue) }
- let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :done, target: issue) }
+ let_it_be_with_reload(:todo1) { create(:todo, user: current_user, author: author, state: :done, target: issue) }
+ let_it_be_with_reload(:todo2) { create(:todo, user: current_user, author: author, state: :done, target: issue) }
let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :done) }
@@ -50,8 +50,8 @@ RSpec.describe 'Restoring many Todos' do
expect(mutation_response).to include(
'errors' => be_empty,
'todos' => contain_exactly(
- { 'id' => global_id_of(todo1), 'state' => 'pending' },
- { 'id' => global_id_of(todo2), 'state' => 'pending' }
+ a_graphql_entity_for(todo1, 'state' => 'pending'),
+ a_graphql_entity_for(todo2, 'state' => 'pending')
)
)
end
diff --git a/spec/requests/api/graphql/packages/conan_spec.rb b/spec/requests/api/graphql/packages/conan_spec.rb
index 84c5af33e5d..1f3732980d9 100644
--- a/spec/requests/api/graphql/packages/conan_spec.rb
+++ b/spec/requests/api/graphql/packages/conan_spec.rb
@@ -37,22 +37,19 @@ RSpec.describe 'conan package details' do
it_behaves_like 'a package with files'
it 'has the correct metadata' do
- expect(metadata_response).to include(
- 'id' => global_id_of(package.conan_metadatum),
- 'recipe' => package.conan_metadatum.recipe,
- 'packageChannel' => package.conan_metadatum.package_channel,
- 'packageUsername' => package.conan_metadatum.package_username,
- 'recipePath' => package.conan_metadatum.recipe_path
+ expect(metadata_response).to match(
+ a_graphql_entity_for(package.conan_metadatum,
+ :recipe, :package_channel, :package_username, :recipe_path)
)
end
it 'has the correct file metadata' do
- expect(first_file_response_metadata).to include(
- 'id' => global_id_of(first_file.conan_file_metadatum),
- 'packageRevision' => first_file.conan_file_metadatum.package_revision,
- 'conanPackageReference' => first_file.conan_file_metadatum.conan_package_reference,
- 'recipeRevision' => first_file.conan_file_metadatum.recipe_revision,
- 'conanFileType' => first_file.conan_file_metadatum.conan_file_type.upcase
+ expect(first_file_response_metadata).to match(
+ a_graphql_entity_for(
+ first_file.conan_file_metadatum,
+ :package_revision, :conan_package_reference, :recipe_revision,
+ conan_file_type: first_file.conan_file_metadatum.conan_file_type.upcase
+ )
)
end
end
diff --git a/spec/requests/api/graphql/packages/maven_spec.rb b/spec/requests/api/graphql/packages/maven_spec.rb
index d28d32b0df5..9d59a922660 100644
--- a/spec/requests/api/graphql/packages/maven_spec.rb
+++ b/spec/requests/api/graphql/packages/maven_spec.rb
@@ -11,12 +11,8 @@ RSpec.describe 'maven package details' do
shared_examples 'correct maven metadata' do
it 'has the correct metadata' do
- expect(metadata_response).to include(
- 'id' => global_id_of(package.maven_metadatum),
- 'path' => package.maven_metadatum.path,
- 'appGroup' => package.maven_metadatum.app_group,
- 'appVersion' => package.maven_metadatum.app_version,
- 'appName' => package.maven_metadatum.app_name
+ expect(metadata_response).to match a_graphql_entity_for(
+ package.maven_metadatum, :path, :app_group, :app_version, :app_name
)
end
end
diff --git a/spec/requests/api/graphql/packages/nuget_spec.rb b/spec/requests/api/graphql/packages/nuget_spec.rb
index ba8d2ca42d2..87cffc67ce5 100644
--- a/spec/requests/api/graphql/packages/nuget_spec.rb
+++ b/spec/requests/api/graphql/packages/nuget_spec.rb
@@ -22,24 +22,19 @@ RSpec.describe 'nuget package details' do
it_behaves_like 'a package with files'
it 'has the correct metadata' do
- expect(metadata_response).to include(
- 'id' => global_id_of(package.nuget_metadatum),
- 'licenseUrl' => package.nuget_metadatum.license_url,
- 'projectUrl' => package.nuget_metadatum.project_url,
- 'iconUrl' => package.nuget_metadatum.icon_url
+ expect(metadata_response).to match a_graphql_entity_for(
+ package.nuget_metadatum, :license_url, :project_url, :icon_url
)
end
it 'has dependency links' do
- expect(dependency_link_response).to include(
- 'id' => global_id_of(dependency_link),
+ expect(dependency_link_response).to match a_graphql_entity_for(
+ dependency_link,
'dependencyType' => dependency_link.dependency_type.upcase
)
- expect(dependency_response).to include(
- 'id' => global_id_of(dependency_link.dependency),
- 'name' => dependency_link.dependency.name,
- 'versionPattern' => dependency_link.dependency.version_pattern
+ expect(dependency_response).to match a_graphql_entity_for(
+ dependency_link.dependency, :name, :version_pattern
)
end
diff --git a/spec/requests/api/graphql/packages/pypi_spec.rb b/spec/requests/api/graphql/packages/pypi_spec.rb
index 64fe7d29a7a..0cc5bd2e3b2 100644
--- a/spec/requests/api/graphql/packages/pypi_spec.rb
+++ b/spec/requests/api/graphql/packages/pypi_spec.rb
@@ -19,9 +19,8 @@ RSpec.describe 'pypi package details' do
it_behaves_like 'a package with files'
it 'has the correct metadata' do
- expect(metadata_response).to include(
- 'id' => global_id_of(package.pypi_metadatum),
- 'requiredPython' => package.pypi_metadatum.required_python
+ expect(metadata_response).to match a_graphql_entity_for(
+ package.pypi_metadatum, :required_python
)
end
end
diff --git a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
index 1793d4961eb..773922c1864 100644
--- a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
@@ -53,33 +53,24 @@ RSpec.describe 'getting Alert Management Integrations' do
end
context 'when no extra params given' do
- let(:http_integration_response) { integrations.first }
- let(:prometheus_integration_response) { integrations.second }
-
it_behaves_like 'a working graphql query'
- it { expect(integrations.size).to eq(2) }
-
it 'returns the correct properties of the integrations' do
- expect(http_integration_response).to include(
- 'id' => global_id_of(active_http_integration),
- 'type' => 'HTTP',
- 'name' => active_http_integration.name,
- 'active' => active_http_integration.active,
- 'token' => active_http_integration.token,
- 'url' => active_http_integration.url,
- 'apiUrl' => nil
- )
-
- expect(prometheus_integration_response).to include(
- 'id' => global_id_of(prometheus_integration),
- 'type' => 'PROMETHEUS',
- 'name' => 'Prometheus',
- 'active' => prometheus_integration.manual_configuration?,
- 'token' => project_alerting_setting.token,
- 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json",
- 'apiUrl' => prometheus_integration.api_url
- )
+ expect(integrations).to match [
+ a_graphql_entity_for(
+ active_http_integration,
+ :name, :active, :token, :url, type: 'HTTP', api_url: nil
+ ),
+ a_graphql_entity_for(
+ prometheus_integration,
+ 'type' => 'PROMETHEUS',
+ 'name' => 'Prometheus',
+ 'active' => prometheus_integration.manual_configuration?,
+ 'token' => project_alerting_setting.token,
+ 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json",
+ 'apiUrl' => prometheus_integration.api_url
+ )
+ ]
end
end
@@ -88,17 +79,9 @@ RSpec.describe 'getting Alert Management Integrations' do
it_behaves_like 'a working graphql query'
- it { expect(integrations).to be_one }
-
it 'returns the correct properties of the HTTP integration' do
- expect(integrations.first).to include(
- 'id' => global_id_of(active_http_integration),
- 'type' => 'HTTP',
- 'name' => active_http_integration.name,
- 'active' => active_http_integration.active,
- 'token' => active_http_integration.token,
- 'url' => active_http_integration.url,
- 'apiUrl' => nil
+ expect(integrations).to contain_exactly a_graphql_entity_for(
+ active_http_integration, :name, :active, :token, :url, type: 'HTTP', api_url: nil
)
end
end
@@ -108,11 +91,9 @@ RSpec.describe 'getting Alert Management Integrations' do
it_behaves_like 'a working graphql query'
- it { expect(integrations).to be_one }
-
it 'returns the correct properties of the Prometheus Integration' do
- expect(integrations.first).to include(
- 'id' => global_id_of(prometheus_integration),
+ expect(integrations).to contain_exactly a_graphql_entity_for(
+ prometheus_integration,
'type' => 'PROMETHEUS',
'name' => 'Prometheus',
'active' => prometheus_integration.manual_configuration?,
diff --git a/spec/requests/api/graphql/project/cluster_agents_spec.rb b/spec/requests/api/graphql/project/cluster_agents_spec.rb
index c9900fea277..a34df0ee6f4 100644
--- a/spec/requests/api/graphql/project/cluster_agents_spec.rb
+++ b/spec/requests/api/graphql/project/cluster_agents_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'Project.cluster_agents' do
post_graphql(query, current_user: current_user)
expect(graphql_data_at(:project, :cluster_agents, :nodes)).to match_array(
- agents.map { |agent| a_hash_including('id' => global_id_of(agent)) }
+ agents.map { |agent| a_graphql_entity_for(agent) }
)
end
@@ -62,9 +62,9 @@ RSpec.describe 'Project.cluster_agents' do
tokens = graphql_data_at(:project, :cluster_agents, :nodes, :tokens, :nodes)
expect(tokens).to match([
- a_hash_including('id' => global_id_of(token_3)),
- a_hash_including('id' => global_id_of(token_2)),
- a_hash_including('id' => global_id_of(token_1))
+ a_graphql_entity_for(token_3),
+ a_graphql_entity_for(token_2),
+ a_graphql_entity_for(token_1)
])
end
diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
index f544d78ecbb..8cda61f0628 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
@@ -71,11 +71,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
it 'finds all the designs as of the given version' do
post_query
- expect(data).to match(
- a_hash_including(
- 'id' => global_id_of(design_at_version),
- 'filename' => design.filename
- ))
+ expect(data).to match a_graphql_entity_for(design_at_version, filename: design.filename)
end
context 'when the current_user is not authorized' do
@@ -119,7 +115,8 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
let(:results) do
issue.designs.visible_at_version(version).map do |d|
dav = build(:design_at_version, design: d, version: version)
- { 'id' => global_id_of(dav), 'filename' => d.filename }
+
+ a_graphql_entity_for(dav, filename: d.filename)
end
end
@@ -132,8 +129,8 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
describe 'filtering' do
let(:designs) { issue.designs.sample(3) }
let(:filenames) { designs.map(&:filename) }
- let(:ids) do
- designs.map { |d| global_id_of(build(:design_at_version, design: d, version: version)) }
+ let(:expected_designs) do
+ designs.map { |d| a_graphql_entity_for(build(:design_at_version, design: d, version: version)) }
end
before do
@@ -144,7 +141,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
let(:dav_params) { { filenames: filenames } }
it 'finds the designs by filename' do
- expect(data.map { |e| e.dig('node', 'id') }).to match_array(ids)
+ expect(data.map { |e| e['node'] }).to match_array expected_designs
end
end
@@ -160,9 +157,9 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
describe 'pagination' do
let(:end_cursor) { graphql_data_at(*path_prefix, :designs_at_version, :page_info, :end_cursor) }
- let(:ids) do
+ let(:entities) do
::DesignManagement::Design.visible_at_version(version).order(:id).map do |d|
- global_id_of(build(:design_at_version, design: d, version: version))
+ a_graphql_entity_for(build(:design_at_version, design: d, version: version))
end
end
@@ -178,19 +175,19 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
let(:fields) { ['pageInfo { endCursor }', 'edges { node { id } }'] }
def response_values(data = graphql_data)
- data.dig(*path).map { |e| e.dig('node', 'id') }
+ data.dig(*path).map { |e| e['node'] }
end
it 'sorts designs for reliable pagination' do
post_graphql(query, current_user: current_user)
- expect(response_values).to match_array(ids.take(2))
+ expect(response_values).to match_array(entities.take(2))
post_graphql(cursored_query, current_user: current_user)
new_data = Gitlab::Json.parse(response.body).fetch('data')
- expect(response_values(new_data)).to match_array(ids.drop(2))
+ expect(response_values(new_data)).to match_array(entities.drop(2))
end
end
end
@@ -202,9 +199,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
end
let(:results) do
- version.designs.map do |design|
- { 'id' => global_id_of(design), 'filename' => design.filename }
- end
+ version.designs.map { |design| a_graphql_entity_for(design, :filename) }
end
it 'finds all the designs as of the given version' do
diff --git a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
index 459a30508eb..02bc9457c07 100644
--- a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
+++ b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
@@ -58,8 +58,8 @@ RSpec.describe 'Getting designs related to an issue' do
post_graphql(query, current_user: current_user)
- expect(design_response).to eq(
- 'id' => design.to_global_id.to_s,
+ expect(design_response).to match a_graphql_entity_for(
+ design,
'event' => 'CREATION',
'fullPath' => design.full_path,
'filename' => design.filename,
@@ -93,7 +93,7 @@ RSpec.describe 'Getting designs related to an issue' do
let(:end_cursor) { design_collection.dig('designs', 'pageInfo', 'endCursor') }
- let(:ids) { issue.designs.order(:id).map { |d| global_id_of(d) } }
+ let(:expected_designs) { issue.designs.order(:id).map { |d| a_graphql_entity_for(d) } }
let(:query) { make_query(designs_fragment(first: 2)) }
@@ -107,19 +107,19 @@ RSpec.describe 'Getting designs related to an issue' do
query_graphql_field(:designs, params, design_query_fields)
end
- def response_ids(data = graphql_data)
+ def response_designs(data = graphql_data)
path = %w[project issue designCollection designs edges]
- data.dig(*path).map { |e| e.dig('node', 'id') }
+ data.dig(*path).map { |e| e['node'] }
end
it 'sorts designs for reliable pagination' do
- expect(response_ids).to match_array(ids.take(2))
+ expect(response_designs).to match_array(expected_designs.take(2))
post_graphql(cursored_query, current_user: current_user)
new_data = Gitlab::Json.parse(response.body).fetch('data')
- expect(response_ids(new_data)).to match_array(ids.drop(2))
+ expect(response_designs(new_data)).to match_array(expected_designs.drop(2))
end
end
diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
index de2ace95757..3b1eb0b4b02 100644
--- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
+++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe 'Getting designs related to an issue' do
design_data = designs_data['nodes'].first
note_data = design_data['notes']['nodes'].first
- expect(note_data['id']).to eq(note.to_global_id.to_s)
+ expect(note_data).to match(a_graphql_entity_for(note))
end
def query(note_fields = all_graphql_fields_for(Note, max_depth: 1))
diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb
index ddf63a8f2c9..2415e9ef60f 100644
--- a/spec/requests/api/graphql/project/issue_spec.rb
+++ b/spec/requests/api/graphql/project/issue_spec.rb
@@ -144,10 +144,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid)' do
data = graphql_data.dig(*path)
- expect(data).to match(
- a_hash_including('id' => global_id_of(version),
- 'sha' => version.sha)
- )
+ expect(data).to match a_graphql_entity_for(version, :sha)
end
end
@@ -184,6 +181,6 @@ RSpec.describe 'Query.project(fullPath).issue(iid)' do
end
def id_hash(object)
- a_hash_including('id' => global_id_of(object))
+ a_graphql_entity_for(object)
end
end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index cefe88aafc8..395b052369e 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe 'getting merge request information nested in a project' do
it 'includes reviewers' do
expected = merge_request.reviewers.map do |r|
- a_hash_including('id' => global_id_of(r), 'username' => r.username)
+ a_graphql_entity_for(r, :username)
end
post_graphql(query, current_user: current_user)
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index 303748bc70e..c7f121c48ab 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
let_it_be(:current_user) { create(:user) }
let_it_be(:label) { create(:label, project: project) }
- let_it_be(:merge_request_a) do
+ let_it_be_with_reload(:merge_request_a) do
create(:labeled_merge_request, :unique_branches, source_project: project, labels: [label])
end
@@ -412,6 +412,10 @@ RSpec.describe 'getting merge request listings nested in a project' do
describe 'sorting and pagination' do
let(:data_path) { [:project, :mergeRequests] }
+ def pagination_results_data(nodes)
+ nodes
+ end
+
def pagination_query(params)
graphql_query_for(:project, { full_path: project.full_path }, <<~QUERY)
mergeRequests(#{params}) {
@@ -429,7 +433,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
merge_request_c,
merge_request_e,
merge_request_a
- ].map { |mr| global_id_of(mr) }
+ ].map { |mr| a_graphql_entity_for(mr) }
end
before do
@@ -455,7 +459,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
query = pagination_query(params)
post_graphql(query, current_user: current_user)
- expect(results.map { |item| item["id"] }).to eq(all_records.last(2))
+ expect(results).to match(all_records.last(2))
end
end
end
@@ -469,7 +473,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
merge_request_c,
merge_request_e,
merge_request_a
- ].map { |mr| global_id_of(mr) }
+ ].map { |mr| a_graphql_entity_for(mr) }
end
before do
@@ -495,7 +499,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
query = pagination_query(params)
post_graphql(query, current_user: current_user)
- expect(results.map { |item| item["id"] }).to eq(all_records.last(2))
+ expect(results).to match(all_records.last(2))
end
end
end
diff --git a/spec/requests/api/graphql/project/milestones_spec.rb b/spec/requests/api/graphql/project/milestones_spec.rb
index 2fede4c7285..3e8948d83b1 100644
--- a/spec/requests/api/graphql/project/milestones_spec.rb
+++ b/spec/requests/api/graphql/project/milestones_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'getting milestone listings nested in a project' do
def result_list(expected)
expected.map do |milestone|
- a_hash_including('id' => global_id_of(milestone))
+ a_graphql_entity_for(milestone)
end
end
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
index 73e02e2a4b1..ccf97918021 100644
--- a/spec/requests/api/graphql/project/pipeline_spec.rb
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -89,17 +89,16 @@ RSpec.describe 'getting pipeline information nested in a project' do
post_graphql(query, current_user: current_user)
expect(graphql_data_at(*path, :jobs, :nodes)).to contain_exactly(
- a_hash_including(
- 'name' => build_job.name,
- 'status' => build_job.status.upcase,
- 'duration' => build_job.duration
+ a_graphql_entity_for(
+ build_job, :name, :duration,
+ 'status' => build_job.status.upcase
),
- a_hash_including(
- 'id' => global_id_of(failed_build),
+ a_graphql_entity_for(
+ failed_build,
'status' => failed_build.status.upcase
),
- a_hash_including(
- 'id' => global_id_of(bridge),
+ a_graphql_entity_for(
+ bridge,
'status' => bridge.status.upcase
)
)
@@ -135,7 +134,7 @@ RSpec.describe 'getting pipeline information nested in a project' do
post_graphql(query, current_user: current_user, variables: variables)
expect(graphql_data_at(*path, :jobs, :nodes))
- .to contain_exactly(a_hash_including('id' => global_id_of(failed_build)))
+ .to contain_exactly(a_graphql_entity_for(failed_build))
end
end
@@ -166,7 +165,7 @@ RSpec.describe 'getting pipeline information nested in a project' do
end
let(:the_job) do
- a_hash_including('name' => build_job.name, 'id' => global_id_of(build_job))
+ a_graphql_entity_for(build_job, :name)
end
it 'can request a build by name' do
diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb
index 315d44884ff..c3281b44954 100644
--- a/spec/requests/api/graphql/project/project_members_spec.rb
+++ b/spec/requests/api/graphql/project/project_members_spec.rb
@@ -60,7 +60,10 @@ RSpec.describe 'getting project members information' do
fetch_members(project: parent_project, args: { relations: [:DIRECT] })
expect(graphql_errors).to be_nil
- expect(graphql_data_at(:project, :project_members, :edges, :node)).to contain_exactly({ 'user' => { 'id' => global_id_of(user) } }, 'user' => nil)
+ expect(graphql_data_at(:project, :project_members, :edges, :node)).to contain_exactly(
+ a_graphql_entity_for(user: a_graphql_entity_for(user)),
+ { 'user' => nil }
+ )
end
end
@@ -238,7 +241,7 @@ RSpec.describe 'getting project members information' do
def expect_array_response(*items)
expect(response).to have_gitlab_http_status(:success)
- member_gids = graphql_data_at(:project, :project_members, :edges, :node, :user, :id)
- expect(member_gids).to match_array(items.map { |u| global_id_of(u) })
+ members = graphql_data_at(:project, :project_members, :edges, :node, :user)
+ expect(members).to match_array(items.map { |u| a_graphql_entity_for(u) })
end
end
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index 77abac4ef04..c4899dbb71e 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -77,10 +77,10 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
post_query
expected = release.milestones.order_by_dates_and_title.map do |milestone|
- { 'id' => global_id_of(milestone), 'title' => milestone.title }
+ a_graphql_entity_for(milestone, :title)
end
- expect(data).to eq(expected)
+ expect(data).to match(expected)
end
end
@@ -94,10 +94,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
it 'finds the author of the release' do
post_query
- expect(data).to eq(
- 'id' => global_id_of(release.author),
- 'username' => release.author.username
- )
+ expect(data).to match a_graphql_entity_for(release.author, :username)
end
end
@@ -142,13 +139,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
post_query
expected = release.links.map do |link|
- {
- 'id' => global_id_of(link),
- 'name' => link.name,
- 'url' => link.url,
+ a_graphql_entity_for(
+ link, :name, :url,
'external' => link.external?,
'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url
- }
+ )
end
expect(data).to match_array(expected)
@@ -218,10 +213,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
evidence = release.evidences.first.present
- expect(data["nodes"].first).to eq(
- 'id' => global_id_of(evidence),
- 'sha' => evidence.sha,
- 'filepath' => evidence.filepath,
+ expect(data["nodes"].first).to match a_graphql_entity_for(
+ evidence, :sha, :filepath,
'collectedAt' => evidence.collected_at.utc.iso8601
)
end
@@ -274,10 +267,10 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
post_query
expected = release.milestones.order_by_dates_and_title.map do |milestone|
- { 'id' => global_id_of(milestone), 'title' => milestone.title }
+ a_graphql_entity_for(milestone, :title)
end
- expect(data).to eq(expected)
+ expect(data).to match(expected)
end
end
@@ -291,10 +284,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
it 'finds the author of the release' do
post_query
- expect(data).to eq(
- 'id' => global_id_of(release.author),
- 'username' => release.author.username
- )
+ expect(data).to match a_graphql_entity_for(release.author, :username)
end
end
@@ -339,13 +329,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
post_query
expected = release.links.map do |link|
- {
- 'id' => global_id_of(link),
- 'name' => link.name,
- 'url' => link.url,
+ a_graphql_entity_for(
+ link, :name, :url,
'external' => true,
'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url
- }
+ )
end
expect(data).to match_array(expected)
diff --git a/spec/requests/api/graphql/project/terraform/state_spec.rb b/spec/requests/api/graphql/project/terraform/state_spec.rb
index 9f1d9ab204a..8f2d2cffef2 100644
--- a/spec/requests/api/graphql/project/terraform/state_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/state_spec.rb
@@ -57,22 +57,22 @@ RSpec.describe 'query a single terraform state' do
it_behaves_like 'a working graphql query'
it 'returns terraform state data' do
- expect(data).to match(a_hash_including({
- 'id' => global_id_of(terraform_state),
- 'name' => terraform_state.name,
+ expect(data).to match a_graphql_entity_for(
+ terraform_state,
+ :name,
'lockedAt' => terraform_state.locked_at.iso8601,
'createdAt' => terraform_state.created_at.iso8601,
'updatedAt' => terraform_state.updated_at.iso8601,
- 'lockedByUser' => { 'id' => global_id_of(terraform_state.locked_by_user) },
- 'latestVersion' => {
- 'id' => eq(global_id_of(latest_version)),
+ 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user),
+ 'latestVersion' => a_graphql_entity_for(
+ latest_version,
'serial' => eq(latest_version.version),
'createdAt' => eq(latest_version.created_at.iso8601),
'updatedAt' => eq(latest_version.updated_at.iso8601),
- 'createdByUser' => { 'id' => eq(global_id_of(latest_version.created_by_user)) },
+ 'createdByUser' => a_graphql_entity_for(latest_version.created_by_user),
'job' => { 'name' => eq(latest_version.build.name) }
- }
- }))
+ )
+ )
end
context 'unauthorized users' do
diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb
index 2879530acc5..a7ec6f69776 100644
--- a/spec/requests/api/graphql/project/terraform/states_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/states_spec.rb
@@ -62,23 +62,22 @@ RSpec.describe 'query terraform states' do
)
)
- expect(data['nodes']).to contain_exactly({
- 'id' => global_id_of(terraform_state),
- 'name' => terraform_state.name,
+ expect(data['nodes']).to contain_exactly a_graphql_entity_for(
+ terraform_state, :name,
'lockedAt' => terraform_state.locked_at.iso8601,
'createdAt' => terraform_state.created_at.iso8601,
'updatedAt' => terraform_state.updated_at.iso8601,
- 'lockedByUser' => { 'id' => global_id_of(terraform_state.locked_by_user) },
- 'latestVersion' => {
- 'id' => eq(global_id_of(latest_version)),
+ 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user),
+ 'latestVersion' => a_graphql_entity_for(
+ latest_version,
'serial' => eq(latest_version.version),
'downloadPath' => eq(download_path),
'createdAt' => eq(latest_version.created_at.iso8601),
'updatedAt' => eq(latest_version.updated_at.iso8601),
- 'createdByUser' => { 'id' => eq(global_id_of(latest_version.created_by_user)) },
+ 'createdByUser' => a_graphql_entity_for(latest_version.created_by_user),
'job' => { 'name' => eq(latest_version.build.name) }
- }
- })
+ )
+ )
end
it 'returns count of terraform states' do
diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb
index d650acc8354..4aa9c4b8254 100644
--- a/spec/requests/api/graphql/query_spec.rb
+++ b/spec/requests/api/graphql/query_spec.rb
@@ -76,10 +76,8 @@ RSpec.describe 'Query' do
it_behaves_like 'a working graphql query'
it_behaves_like 'a query that needs authorization'
- context 'the current user is able to read designs' do
- it 'fetches the expected data' do
- expect(query_result).to eq('id' => global_id_of(version), 'sha' => version.sha)
- end
+ it 'fetches the expected data' do
+ expect(query_result).to match a_graphql_entity_for(version, :sha)
end
end
@@ -106,13 +104,13 @@ RSpec.describe 'Query' do
context 'the current user is able to read designs' do
it 'fetches the expected data, including the correct associations' do
- expect(query_result).to eq(
- 'id' => global_id_of(design_at_version),
+ expect(query_result).to match a_graphql_entity_for(
+ design_at_version,
'filename' => design_at_version.design.filename,
- 'version' => { 'id' => global_id_of(version), 'sha' => version.sha },
- 'design' => { 'id' => global_id_of(design) },
+ 'version' => a_graphql_entity_for(version, :sha),
+ 'design' => a_graphql_entity_for(design),
'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s },
- 'project' => { 'id' => global_id_of(project), 'fullPath' => project.full_path }
+ 'project' => a_graphql_entity_for(project, :full_path)
)
end
end
diff --git a/spec/requests/api/graphql/user/starred_projects_query_spec.rb b/spec/requests/api/graphql/user/starred_projects_query_spec.rb
index a8c087d1fbf..37a85b98e5f 100644
--- a/spec/requests/api/graphql/user/starred_projects_query_spec.rb
+++ b/spec/requests/api/graphql/user/starred_projects_query_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe 'Getting starredProjects of the user' do
it 'found only public project' do
expect(starred_projects).to contain_exactly(
- a_hash_including('id' => global_id_of(project_a))
+ a_graphql_entity_for(project_a)
)
end
@@ -51,9 +51,9 @@ RSpec.describe 'Getting starredProjects of the user' do
it 'found all projects' do
expect(starred_projects).to contain_exactly(
- a_hash_including('id' => global_id_of(project_a)),
- a_hash_including('id' => global_id_of(project_b)),
- a_hash_including('id' => global_id_of(project_c))
+ a_graphql_entity_for(project_a),
+ a_graphql_entity_for(project_b),
+ a_graphql_entity_for(project_c)
)
end
end
@@ -69,8 +69,8 @@ RSpec.describe 'Getting starredProjects of the user' do
it 'finds public and member projects' do
expect(starred_projects).to contain_exactly(
- a_hash_including('id' => global_id_of(project_a)),
- a_hash_including('id' => global_id_of(project_b))
+ a_graphql_entity_for(project_a),
+ a_graphql_entity_for(project_b)
)
end
end
@@ -93,9 +93,9 @@ RSpec.describe 'Getting starredProjects of the user' do
it 'finds all projects starred by the user, which the current user has access to' do
expect(starred_projects).to contain_exactly(
- a_hash_including('id' => global_id_of(project_a)),
- a_hash_including('id' => global_id_of(project_b)),
- a_hash_including('id' => global_id_of(project_c))
+ a_graphql_entity_for(project_a),
+ a_graphql_entity_for(project_b),
+ a_graphql_entity_for(project_c)
)
end
end
diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb
index 1cba3674d25..8f286180617 100644
--- a/spec/requests/api/graphql/user_query_spec.rb
+++ b/spec/requests/api/graphql/user_query_spec.rb
@@ -91,11 +91,11 @@ RSpec.describe 'getting user information' do
presenter = UserPresenter.new(user)
expect(graphql_data['user']).to match(
- a_hash_including(
- 'id' => global_id_of(user),
+ a_graphql_entity_for(
+ user,
+ :username,
'state' => presenter.state,
'name' => presenter.name,
- 'username' => presenter.username,
'webUrl' => presenter.web_url,
'avatarUrl' => presenter.avatar_url,
'email' => presenter.public_email,
@@ -121,9 +121,9 @@ RSpec.describe 'getting user information' do
it 'can be found' do
expect(assigned_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(assigned_mr)),
- a_hash_including('id' => global_id_of(assigned_mr_b)),
- a_hash_including('id' => global_id_of(assigned_mr_c))
+ a_graphql_entity_for(assigned_mr),
+ a_graphql_entity_for(assigned_mr_b),
+ a_graphql_entity_for(assigned_mr_c)
)
end
@@ -145,7 +145,7 @@ RSpec.describe 'getting user information' do
it 'selects the correct MRs' do
expect(assigned_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(assigned_mr_b))
+ a_graphql_entity_for(assigned_mr_b)
)
end
end
@@ -157,8 +157,8 @@ RSpec.describe 'getting user information' do
it 'selects the correct MRs' do
expect(assigned_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(assigned_mr_b)),
- a_hash_including('id' => global_id_of(assigned_mr_c))
+ a_graphql_entity_for(assigned_mr_b),
+ a_graphql_entity_for(assigned_mr_c)
)
end
end
@@ -169,7 +169,7 @@ RSpec.describe 'getting user information' do
it 'finds the authored mrs' do
expect(assigned_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(assigned_mr_b))
+ a_graphql_entity_for(assigned_mr_b)
)
end
end
@@ -185,8 +185,8 @@ RSpec.describe 'getting user information' do
post_graphql(query, current_user: current_user)
expect(assigned_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(assigned_mr_b)),
- a_hash_including('id' => global_id_of(assigned_mr_c))
+ a_graphql_entity_for(assigned_mr_b),
+ a_graphql_entity_for(assigned_mr_c)
)
end
end
@@ -212,9 +212,9 @@ RSpec.describe 'getting user information' do
it 'can be found' do
expect(reviewed_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(reviewed_mr)),
- a_hash_including('id' => global_id_of(reviewed_mr_b)),
- a_hash_including('id' => global_id_of(reviewed_mr_c))
+ a_graphql_entity_for(reviewed_mr),
+ a_graphql_entity_for(reviewed_mr_b),
+ a_graphql_entity_for(reviewed_mr_c)
)
end
@@ -236,7 +236,7 @@ RSpec.describe 'getting user information' do
it 'selects the correct MRs' do
expect(reviewed_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(reviewed_mr_b))
+ a_graphql_entity_for(reviewed_mr_b)
)
end
end
@@ -248,8 +248,8 @@ RSpec.describe 'getting user information' do
it 'selects the correct MRs' do
expect(reviewed_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(reviewed_mr_b)),
- a_hash_including('id' => global_id_of(reviewed_mr_c))
+ a_graphql_entity_for(reviewed_mr_b),
+ a_graphql_entity_for(reviewed_mr_c)
)
end
end
@@ -260,7 +260,7 @@ RSpec.describe 'getting user information' do
it 'finds the authored mrs' do
expect(reviewed_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(reviewed_mr_b))
+ a_graphql_entity_for(reviewed_mr_b)
)
end
end
@@ -275,7 +275,7 @@ RSpec.describe 'getting user information' do
post_graphql(query, current_user: current_user)
expect(reviewed_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(reviewed_mr_c))
+ a_graphql_entity_for(reviewed_mr_c)
)
end
end
@@ -301,9 +301,9 @@ RSpec.describe 'getting user information' do
it 'can be found' do
expect(authored_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(authored_mr)),
- a_hash_including('id' => global_id_of(authored_mr_b)),
- a_hash_including('id' => global_id_of(authored_mr_c))
+ a_graphql_entity_for(authored_mr),
+ a_graphql_entity_for(authored_mr_b),
+ a_graphql_entity_for(authored_mr_c)
)
end
@@ -329,8 +329,8 @@ RSpec.describe 'getting user information' do
post_graphql(query, current_user: current_user)
expect(authored_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(authored_mr)),
- a_hash_including('id' => global_id_of(authored_mr_c))
+ a_graphql_entity_for(authored_mr),
+ a_graphql_entity_for(authored_mr_c)
)
end
end
@@ -346,8 +346,8 @@ RSpec.describe 'getting user information' do
post_graphql(query, current_user: current_user)
expect(authored_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(authored_mr_b)),
- a_hash_including('id' => global_id_of(authored_mr_c))
+ a_graphql_entity_for(authored_mr_b),
+ a_graphql_entity_for(authored_mr_c)
)
end
end
@@ -359,7 +359,7 @@ RSpec.describe 'getting user information' do
it 'selects the correct MRs' do
expect(authored_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(authored_mr_b))
+ a_graphql_entity_for(authored_mr_b)
)
end
end
@@ -371,8 +371,8 @@ RSpec.describe 'getting user information' do
it 'selects the correct MRs' do
expect(authored_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(authored_mr_b)),
- a_hash_including('id' => global_id_of(authored_mr_c))
+ a_graphql_entity_for(authored_mr_b),
+ a_graphql_entity_for(authored_mr_c)
)
end
end
@@ -417,7 +417,7 @@ RSpec.describe 'getting user information' do
it 'can be found' do
expect(group_memberships).to include(
- a_hash_including('id' => global_id_of(membership_a))
+ a_graphql_entity_for(membership_a)
)
end
end
@@ -440,7 +440,7 @@ RSpec.describe 'getting user information' do
it 'can be found' do
expect(project_memberships).to include(
- a_hash_including('id' => global_id_of(membership_a))
+ a_graphql_entity_for(membership_a)
)
end
end
@@ -460,7 +460,7 @@ RSpec.describe 'getting user information' do
it 'can be found' do
expect(authored_mrs).to include(
- a_hash_including('id' => global_id_of(authored_mr))
+ a_graphql_entity_for(authored_mr)
)
end
end
@@ -480,9 +480,9 @@ RSpec.describe 'getting user information' do
it 'can be found' do
expect(assigned_mrs).to contain_exactly(
- a_hash_including('id' => global_id_of(assigned_mr)),
- a_hash_including('id' => global_id_of(assigned_mr_b)),
- a_hash_including('id' => global_id_of(assigned_mr_c))
+ a_graphql_entity_for(assigned_mr),
+ a_graphql_entity_for(assigned_mr_b),
+ a_graphql_entity_for(assigned_mr_c)
)
end
end
diff --git a/spec/requests/api/graphql/users_spec.rb b/spec/requests/api/graphql/users_spec.rb
index fe824834a2c..a6bbfc75451 100644
--- a/spec/requests/api/graphql/users_spec.rb
+++ b/spec/requests/api/graphql/users_spec.rb
@@ -72,12 +72,12 @@ RSpec.describe 'Users' do
post_query
expect(graphql_data.dig('users', 'nodes')).to include(
- { "id" => user0.to_global_id.to_s },
- { "id" => user1.to_global_id.to_s },
- { "id" => user2.to_global_id.to_s },
- { "id" => user3.to_global_id.to_s },
- { "id" => admin.to_global_id.to_s },
- { "id" => another_admin.to_global_id.to_s }
+ a_graphql_entity_for(user0),
+ a_graphql_entity_for(user1),
+ a_graphql_entity_for(user2),
+ a_graphql_entity_for(user3),
+ a_graphql_entity_for(admin),
+ a_graphql_entity_for(another_admin)
)
end
end
@@ -91,15 +91,15 @@ RSpec.describe 'Users' do
post_graphql(query, current_user: current_user)
expect(graphql_data.dig('users', 'nodes')).to include(
- { "id" => another_admin.to_global_id.to_s },
- { "id" => admin.to_global_id.to_s }
+ a_graphql_entity_for(another_admin),
+ a_graphql_entity_for(admin)
)
expect(graphql_data.dig('users', 'nodes')).not_to include(
- { "id" => user0.to_global_id.to_s },
- { "id" => user1.to_global_id.to_s },
- { "id" => user2.to_global_id.to_s },
- { "id" => user3.to_global_id.to_s }
+ a_graphql_entity_for(user0),
+ a_graphql_entity_for(user1),
+ a_graphql_entity_for(user2),
+ a_graphql_entity_for(user3)
)
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 88f10cc2a01..cf2c0780298 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -292,9 +292,6 @@ RSpec.configure do |config|
# tests, until we introduce it in user settings
stub_feature_flags(forti_token_cloud: false)
- # Disable for now whilst we add more states
- stub_feature_flags(restructured_mr_widget: false)
-
# These feature flag are by default disabled and used in disaster recovery mode
stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: false)
stub_feature_flags(ci_queueing_disaster_recovery_disable_quota: false)
diff --git a/spec/support/graphql/arguments.rb b/spec/support/graphql/arguments.rb
index 20e940030f8..a5bb01c31a3 100644
--- a/spec/support/graphql/arguments.rb
+++ b/spec/support/graphql/arguments.rb
@@ -40,7 +40,7 @@ module Graphql
when Array then "[#{value.map { |v| as_graphql_literal(v) }.join(',')}]"
when Hash then "{#{new(value)}}"
when Integer, Float, Symbol then value.to_s
- when String then "\"#{value.gsub(/"/, '\\"')}\""
+ when String, GlobalID then "\"#{value.to_s.gsub(/"/, '\\"')}\""
when Time, Date then "\"#{value.iso8601}\""
when nil then 'null'
when true then 'true'
@@ -49,7 +49,7 @@ module Graphql
value.to_graphql_value
end
rescue NoMethodError
- raise ArgumentError, "Cannot represent #{value} as GraphQL literal"
+ raise ArgumentError, "Cannot represent #{value} (instance of #{value.class}) as GraphQL literal"
end
def merge(other)
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 652cc51fcc0..eb0e5a25733 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -244,6 +244,7 @@ module GraphqlHelpers
def graphql_mutation(name, input, fields = nil, &block)
raise ArgumentError, 'Please pass either `fields` parameter or a block to `#graphql_mutation`, but not both.' if fields.present? && block_given?
+ name = name.graphql_name if name.respond_to?(:graphql_name)
mutation_name = GraphqlHelpers.fieldnamerize(name)
input_variable_name = "$#{input_variable_name_for_mutation(name)}"
mutation_field = GitlabSchema.mutation.fields[mutation_name]
@@ -264,7 +265,7 @@ module GraphqlHelpers
end
def variables_for_mutation(name, input)
- graphql_input = prepare_input_for_mutation(input)
+ graphql_input = prepare_variables(input)
{ input_variable_name_for_mutation(name) => graphql_input }
end
@@ -273,18 +274,28 @@ module GraphqlHelpers
return unless variables
return variables if variables.is_a?(String)
- ::Gitlab::Utils::MergeHash.merge(Array.wrap(variables).map(&:to_h)).to_json
+ # Combine variables into a single hash.
+ hash = ::Gitlab::Utils::MergeHash.merge(Array.wrap(variables).map(&:to_h))
+
+ prepare_variables(hash).to_json
end
- # Recursively convert a Hash with Ruby-style keys to GraphQL fieldname-style keys
+ # Recursively convert any ruby object we can pass as a variable value
+ # to an object we can serialize with JSON, using fieldname-style keys
#
- # prepare_input_for_mutation({ 'my_key' => 1 })
- # => { 'myKey' => 1}
- def prepare_input_for_mutation(input)
- input.to_h do |name, value|
- value = prepare_input_for_mutation(value) if value.is_a?(Hash)
+ # prepare_variables({ 'my_key' => 1 })
+ # => { 'myKey' => 1 }
+ # prepare_variables({ enums: [:FOO, :BAR], user_id: global_id_of(user) })
+ # => { 'enums' => ['FOO', 'BAR'], 'userId' => "gid://User/123" }
+ # prepare_variables({ nested: { hash_values: { are_supported: true } } })
+ # => { 'nested' => { 'hashValues' => { 'areSupported' => true } } }
+ def prepare_variables(input)
+ return input.map { prepare_variables(_1) } if input.is_a?(Array)
+ return input.to_s if input.is_a?(GlobalID) || input.is_a?(Symbol)
+ return input unless input.is_a?(Hash)
- [GraphqlHelpers.fieldnamerize(name), value]
+ input.to_h do |name, value|
+ [GraphqlHelpers.fieldnamerize(name), prepare_variables(value)]
end
end
@@ -650,9 +661,9 @@ module GraphqlHelpers
end
end
- def global_id_of(model, id: nil, model_name: nil)
+ def global_id_of(model = nil, id: nil, model_name: nil)
if id || model_name
- ::Gitlab::GlobalId.build(model, id: id, model_name: model_name).to_s
+ ::Gitlab::GlobalId.as_global_id(id || model.id, model_name: model_name || model.class.name).to_s
else
model.to_global_id.to_s
end
@@ -714,6 +725,67 @@ module GraphqlHelpers
end
end
+ # Wrapper around a_hash_including that supports unpacking with **
+ class UnpackableMatcher < SimpleDelegator
+ include RSpec::Matchers
+
+ attr_reader :to_hash
+
+ def initialize(hash)
+ @to_hash = hash
+ super(a_hash_including(hash))
+ end
+
+ def to_json(_opts = {})
+ to_hash.to_json
+ end
+
+ def as_json(opts = {})
+ to_hash.as_json(opts)
+ end
+ end
+
+ # Construct a matcher for GraphQL entity response objects, of the form
+ # `{ "id" => "some-gid" }`.
+ #
+ # Usage:
+ #
+ # ```ruby
+ # expect(graphql_data_at(:path, :to, :entity)).to match a_graphql_entity_for(user)
+ # ```
+ #
+ # This can be called as:
+ #
+ # ```ruby
+ # a_graphql_entity_for(project, :full_path) # also checks that `entity['fullPath'] == project.full_path
+ # a_graphql_entity_for(project, full_path: 'some/path') # same as above, with explicit values
+ # a_graphql_entity_for(user, :username, foo: 'bar') # combinations of the above
+ # a_graphql_entity_for(foo: 'bar') # if properties are defined, the model is not necessary
+ # ```
+ #
+ # Note that the model instance must not be nil, unless some properties are
+ # explicitly passed in. The following are rejected with `ArgumentError`:
+ #
+ # ```
+ # a_graphql_entity_for(nil, :username)
+ # a_graphql_entity_for(:username)
+ # a_graphql_entity_for
+ # ```
+ #
+ def a_graphql_entity_for(model = nil, *fields, **attrs)
+ raise ArgumentError, 'model is nil' if model.nil? && fields.any?
+
+ attrs.transform_keys! { GraphqlHelpers.fieldnamerize(_1) }
+ attrs['id'] = global_id_of(model) if model
+ fields.each do |name|
+ attrs[GraphqlHelpers.fieldnamerize(name)] = model.public_send(name)
+ end
+
+ raise ArgumentError, 'no attributes' if attrs.empty?
+
+ UnpackableMatcher.new(attrs)
+ end
+
# A lookahead that selects everything
def positive_lookahead
double(selects?: true).tap do |selection|
diff --git a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
index 13e7ecf2669..b29a231f3a6 100644
--- a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
@@ -14,7 +14,7 @@ RSpec.shared_context 'package details setup' do
let(:user) { project.first_owner }
let(:package_details) { graphql_data_at(:package) }
let(:metadata_response) { graphql_data_at(:package, :metadata) }
- let(:first_file) { package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
+ let(:first_file) { package.package_files.find { |f| a_graphql_entity_for(f).matches?(first_file_response) } }
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)}
diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
index 37a805902a9..6d6e7b761f6 100644
--- a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
@@ -101,7 +101,7 @@ RSpec.shared_examples 'sorted paginated query' do |conditions = {}|
context 'when sorting' do
it 'sorts correctly' do
- expect(results).to eq all_records
+ expect(results).to match all_records
end
context 'when paginating' do
@@ -110,17 +110,17 @@ RSpec.shared_examples 'sorted paginated query' do |conditions = {}|
let(:rest) { all_records.drop(first_param) }
it 'paginates correctly' do
- expect(results).to eq first_page
+ expect(results).to match first_page
fwds = pagination_query(sort_argument.merge(after: end_cursor))
post_graphql(fwds, current_user: current_user)
- expect(results).to eq rest
+ expect(results).to match rest
bwds = pagination_query(sort_argument.merge(before: start_cursor))
post_graphql(bwds, current_user: current_user)
- expect(results).to eq first_page
+ expect(results).to match first_page
end
end
@@ -130,7 +130,7 @@ RSpec.shared_examples 'sorted paginated query' do |conditions = {}|
it 'fetches last elements without error' do
post_graphql(pagination_query(params), current_user: current_user)
- expect(results.first).to eq(all_records.last)
+ expect(results.first).to match all_records.last
end
end
end
diff --git a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb
index 7e1f4500779..31f2519a132 100644
--- a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb
@@ -12,8 +12,8 @@ RSpec.shared_examples 'a noteable graphql type we can query' do
def expected
noteable.discussions.map do |discussion|
- include(
- 'id' => global_id_of(discussion),
+ a_graphql_entity_for(
+ discussion,
'replyId' => global_id_of(discussion, id: discussion.reply_id),
'createdAt' => discussion.created_at.iso8601,
'notes' => include(
@@ -50,8 +50,8 @@ RSpec.shared_examples 'a noteable graphql type we can query' do
post_graphql(query(fields), current_user: current_user)
- data = graphql_data_at(*path_to_noteable, :discussions, :nodes, :noteable, :id)
- expect(data[0]).to eq(global_id_of(noteable))
+ entities = graphql_data_at(*path_to_noteable, :discussions, :nodes, :noteable)
+ expect(entities).to all(match(a_graphql_entity_for(noteable)))
end
end
@@ -62,10 +62,10 @@ RSpec.shared_examples 'a noteable graphql type we can query' do
def expected
noteable.notes.map do |note|
- include(
- 'id' => global_id_of(note),
- 'project' => include('id' => global_id_of(project)),
- 'author' => include('id' => global_id_of(note.author)),
+ a_graphql_entity_for(
+ note,
+ 'project' => a_graphql_entity_for(project),
+ 'author' => a_graphql_entity_for(note.author),
'createdAt' => note.created_at.iso8601,
'body' => eq(note.note)
)
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
index 5d6e95f2fbc..9f7ec6e90e9 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
@@ -104,7 +104,7 @@ RSpec.shared_examples 'group and project packages query' do
}
end
- let(:expected_packages) { sorted_packages.map { |package| global_id_of(package) } }
+ let(:expected_packages) { sorted_packages.map { |package| global_id_of(package).to_s } }
let(:data_path) { [resource_type, :packages] }
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
index ab93f54111b..b4019d7c232 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
@@ -28,14 +28,10 @@ RSpec.shared_examples 'a package with files' do
end
it 'has the basic package files data' do
- expect(first_file_response).to include(
- 'id' => global_id_of(first_file),
- 'fileName' => first_file.file_name,
- 'size' => first_file.size.to_s,
- 'downloadPath' => first_file.download_path,
- 'fileSha1' => first_file.file_sha1,
- 'fileMd5' => first_file.file_md5,
- 'fileSha256' => first_file.file_sha256
+ expect(first_file_response).to match a_graphql_entity_for(
+ first_file,
+ :file_name, :download_path, :file_sha1, :file_md5, :file_sha256,
+ 'size' => first_file.size.to_s
)
end
diff --git a/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb
index c134f7d1839..3c5f25baaa1 100644
--- a/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb
@@ -30,14 +30,12 @@ RSpec.shared_examples 'GraphQL query with several integrations requested' do |gr
it 'returns the correct properties of the integrations', :aggregate_failures do
post_graphql(multi_selection_query, current_user: current_user)
- expect(graphql_data.dig('project', 'ai', 'nodes')).to include(
- 'id' => global_id_of(active_http_integration),
- 'name' => active_http_integration.name
+ expect(graphql_data.dig('project', 'ai', 'nodes')).to match a_graphql_entity_for(
+ active_http_integration, :name
)
- expect(graphql_data.dig('project', 'ii', 'nodes')).to include(
- 'id' => global_id_of(inactive_http_integration),
- 'name' => inactive_http_integration.name
+ expect(graphql_data.dig('project', 'ii', 'nodes')).to match a_graphql_entity_for(
+ inactive_http_integration, :name
)
end
diff --git a/spec/support_specs/helpers/graphql_helpers_spec.rb b/spec/support_specs/helpers/graphql_helpers_spec.rb
index fae29ec32f5..0f9918351e2 100644
--- a/spec/support_specs/helpers/graphql_helpers_spec.rb
+++ b/spec/support_specs/helpers/graphql_helpers_spec.rb
@@ -10,6 +10,81 @@ RSpec.describe GraphqlHelpers do
query.tr("\n", ' ').gsub(/\s+/, ' ').strip
end
+ describe 'a_graphql_entity_for' do
+ context 'when no arguments are passed' do
+ it 'raises an error' do
+ expect { a_graphql_entity_for }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when the model is nil, with no properties' do
+ it 'raises an error' do
+ expect { a_graphql_entity_for(nil) }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when the model is nil, any fields are passed' do
+ it 'raises an error' do
+ expect { a_graphql_entity_for(nil, :username) }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'with no model' do
+ it 'behaves like hash-inclusion with camel-casing' do
+ response = { 'foo' => 1, 'bar' => 2, 'camelCased' => 3 }
+
+ expect(response).to match a_graphql_entity_for(foo: 1, camel_cased: 3)
+ expect(response).not_to match a_graphql_entity_for(missing: 5)
+ end
+ end
+
+ context 'with just a model' do
+ it 'only considers the ID' do
+ user = build_stubbed(:user)
+ response = { 'username' => 'foo', 'id' => global_id_of(user) }
+
+ expect(response).to match a_graphql_entity_for(user)
+ end
+ end
+
+ context 'with a model and some method names' do
+ it 'also considers the method names' do
+ user = build_stubbed(:user)
+ response = { 'username' => user.username, 'id' => global_id_of(user) }
+
+ expect(response).to match a_graphql_entity_for(user, :username)
+ expect(response).not_to match a_graphql_entity_for(user, :name)
+ end
+ end
+
+ context 'with a model and some other properties' do
+ it 'behaves like the superset' do
+ user = build_stubbed(:user)
+ response = { 'username' => 'foo', 'id' => global_id_of(user) }
+
+ expect(response).to match a_graphql_entity_for(user, username: 'foo')
+ expect(response).not_to match a_graphql_entity_for(user, name: 'foo')
+ end
+ end
+
+ context 'with a model, method names, and some other properties' do
+ it 'behaves like the superset' do
+ user = build_stubbed(:user)
+ response = {
+ 'username' => user.username,
+ 'name' => user.name,
+ 'foo' => 'bar',
+ 'baz' => 'fop',
+ 'id' => global_id_of(user)
+ }
+
+ expect(response).to match a_graphql_entity_for(user, :username, :name, foo: 'bar')
+ expect(response).to match a_graphql_entity_for(user, :name, foo: 'bar')
+ expect(response).not_to match a_graphql_entity_for(user, :name, bar: 'foo')
+ end
+ end
+ end
+
describe 'graphql_dig_at' do
it 'transforms symbol keys to graphql field names' do
data = { 'camelCased' => 'names' }
diff --git a/spec/views/shared/access_tokens/_table.html.haml_spec.rb b/spec/views/shared/access_tokens/_table.html.haml_spec.rb
index fca2fc3183c..74de9e12d04 100644
--- a/spec/views/shared/access_tokens/_table.html.haml_spec.rb
+++ b/spec/views/shared/access_tokens/_table.html.haml_spec.rb
@@ -6,7 +6,6 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
let(:type) { 'token' }
let(:type_plural) { 'tokens' }
let(:empty_message) { nil }
- let(:token_expiry_enforced?) { false }
let(:impersonation) { false }
let_it_be(:user) { create(:user) }
@@ -14,12 +13,6 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
let_it_be(:resource) { false }
before do
- stub_licensed_features(enforce_personal_access_token_expiration: true)
- allow(Gitlab::CurrentSettings).to receive(:enforce_pat_expiration?).and_return(false)
-
- allow(view).to receive(:personal_access_token_expiration_enforced?).and_return(token_expiry_enforced?)
- allow(view).to receive(:show_profile_token_expiry_notification?).and_return(true)
-
if resource
resource.add_maintainer(user)
end
@@ -51,22 +44,6 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
expect(rendered).not_to have_content 'To see all the user\'s personal access tokens you must impersonate them first.'
expect(rendered).not_to have_selector 'th', text: 'Role'
end
-
- context 'if token expiration is enforced' do
- let(:token_expiry_enforced?) { true }
-
- it 'does not show the subtext' do
- expect(rendered).not_to have_content 'Personal access tokens are not revoked upon expiration.'
- end
- end
-
- context 'if token expiration is not enforced' do
- let(:token_expiry_enforced?) { false }
-
- it 'does show the subtext' do
- expect(rendered).to have_content 'Personal access tokens are not revoked upon expiration.'
- end
- end
end
context 'if impersonation' do
@@ -124,16 +101,16 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
context 'with tokens' do
let_it_be(:tokens) do
[
- create(:personal_access_token, user: user, name: 'Access token', last_used_at: 1.day.ago, expires_at: nil),
- create(:personal_access_token, user: user, expires_at: 5.days.ago),
- create(:personal_access_token, user: user, expires_at: Time.now),
- create(:personal_access_token, user: user, expires_at: 5.days.from_now, scopes: [:read_api, :read_user])
+ create(:personal_access_token, user: user, name: 'Access token', last_used_at: 4.days.from_now, expires_at: nil, scopes: [:read_api, :read_user]),
+ create(:personal_access_token, user: user, expires_at: 1.day.from_now, scopes: [:read_api, :read_user])
]
end
+ let_it_be(:expired_token) { build(:personal_access_token, name: "Expired token", expires_at: 2.days.ago).tap { |t| t.save!(validate: false) } }
+
it 'has the correct content', :aggregate_failures do
# Heading content
- expect(rendered).to have_content 'Active tokens (4)'
+ expect(rendered).to have_content 'Active tokens (2)'
# Table headers
expect(rendered).to have_selector 'th', text: 'Token name'
@@ -144,17 +121,15 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
# Table contents
expect(rendered).to have_content 'Access token'
+ expect(rendered).not_to have_content 'Expired token'
expect(rendered).to have_content 'read_api, read_user'
expect(rendered).to have_content 'no scopes selected'
expect(rendered).to have_content Time.now.to_date.to_s(:medium)
- expect(rendered).to have_content l(1.day.ago, format: "%b %d, %Y")
-
- # Expiry
- expect(rendered).to have_content 'Expired', count: 2
+ expect(rendered).to have_content l(4.days.from_now, format: "%b %d, %Y")
# Revoke buttons
expect(rendered).to have_link 'Revoke', href: 'path/', class: 'btn-danger-secondary', count: 1
- expect(rendered).to have_link 'Revoke', href: 'path/', count: 4
+ expect(rendered).to have_link 'Revoke', href: 'path/', count: 2
end
context 'without the last used time' do
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 52df224e9d4..adee70fbf87 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -350,7 +350,6 @@ RSpec.describe 'Every Sidekiq worker' do
'Namespaces::RefreshRootStatisticsWorker' => 3,
'Namespaces::RootStatisticsWorker' => 3,
'Namespaces::ScheduleAggregationWorker' => 3,
- 'NetworkPolicyMetricsWorker' => 3,
'NewEpicWorker' => 3,
'NewIssueWorker' => 3,
'NewMergeRequestWorker' => 3,