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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-19 21:09:09 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-19 21:09:09 +0300
commit6463521e08b00e62d3c877aefd8517f5387d54ab (patch)
tree1e9c49e6a7cd0e926d32f81c92604cd03ee57fac
parent6a3c4476fa8f1c686eadbed05262bce95504ffa7 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/changelog_config.yml38
-rw-r--r--.gitlab/issue_templates/Geo: Replicate a new Git repository type.md756
-rw-r--r--.gitlab/issue_templates/Geo: Replicate a new blob type.md722
-rw-r--r--.haml-lint.yml1
-rw-r--r--.rubocop_manual_todo.yml16
-rw-r--r--app/assets/javascripts/group.js6
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue5
-rw-r--r--app/assets/javascripts/jira_connect/components/app.vue2
-rw-r--r--app/assets/javascripts/jira_connect/components/group_item_name.vue7
-rw-r--r--app/assets/javascripts/jira_connect/components/subscriptions_list.vue13
-rw-r--r--app/assets/javascripts/jira_connect/index.js46
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql52
-rw-r--r--app/assets/javascripts/jobs/components/table/index.js33
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table.vue67
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_app.vue85
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue66
-rw-r--r--app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js11
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js9
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/jobs/index/index.js34
-rw-r--r--app/assets/javascripts/tooltips/index.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue8
-rw-r--r--app/assets/stylesheets/page_bundles/jira_connect.scss53
-rw-r--r--app/controllers/projects/jobs_controller.rb5
-rw-r--r--app/helpers/ci/jobs_helper.rb15
-rw-r--r--app/models/namespace.rb2
-rw-r--r--app/views/groups/_activities.html.haml2
-rw-r--r--app/views/groups/activity.html.haml2
-rw-r--r--app/views/groups/milestones/_form.html.haml14
-rw-r--r--app/views/groups/milestones/edit.html.haml2
-rw-r--r--app/views/help/instance_configuration/_gitlab_ci.html.haml14
-rw-r--r--app/views/profiles/gpg_keys/_form.html.haml2
-rw-r--r--app/views/projects/jobs/index.html.haml13
-rw-r--r--app/views/projects/merge_requests/show.html.haml8
-rw-r--r--app/views/shared/issuable/_nav.html.haml2
-rw-r--r--changelogs/unreleased/215845-uncheck-delete-source-branch-does-nothing-2.yml5
-rw-r--r--changelogs/unreleased/328050-honor-haml-tooltips-delay.yml5
-rw-r--r--changelogs/unreleased/Externalize-strings-in-groups-_activities-html-haml.yml5
-rw-r--r--changelogs/unreleased/Externalize-strings-in-groups-activity-html-haml.yml5
-rw-r--r--changelogs/unreleased/Externalize-strings-in-instance_configuration-_gitlab_ci-html-haml.yml5
-rw-r--r--changelogs/unreleased/Externalize-strings-in-milestones-_form-html-haml.yml5
-rw-r--r--changelogs/unreleased/Externalize-strings-in-milestones-edit-html-haml.yml5
-rw-r--r--changelogs/unreleased/align-center-subproject.yml5
-rw-r--r--changelogs/unreleased/fix-namespace-existence-check.yml5
-rw-r--r--changelogs/unreleased/gl-badge-mr-nav.yml5
-rw-r--r--changelogs/unreleased/gl-form-gpg.yml5
-rw-r--r--changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-helpers.yml5
-rw-r--r--changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-boards.yml5
-rw-r--r--config/feature_flags/development/jobs_table_vue.yml8
-rw-r--r--danger/changelog/Dangerfile23
-rw-r--r--doc/administration/restart_gitlab.md9
-rw-r--r--doc/api/api_resources.md14
-rw-r--r--doc/api/graphql/reference/index.md8
-rw-r--r--doc/api/namespaces.md30
-rw-r--r--doc/api/users.md2
-rw-r--r--doc/development/distributed_tracing.md2
-rw-r--r--doc/development/geo/framework.md864
-rw-r--r--doc/development/testing_guide/best_practices.md65
-rw-r--r--doc/development/testing_guide/flaky_tests.md2
-rw-r--r--doc/install/relative_url.md4
-rw-r--r--doc/update/index.md2
-rw-r--r--doc/update/patch_versions.md2
-rw-r--r--doc/update/restore_after_failure.md2
-rw-r--r--doc/update/upgrading_from_ce_to_ee.md2
-rw-r--r--doc/update/upgrading_from_source.md2
-rw-r--r--doc/user/admin_area/settings/rate_limit_on_issues_creation.md2
-rw-r--r--doc/user/admin_area/settings/rate_limit_on_notes_creation.md2
-rw-r--r--doc/user/clusters/agent/repository.md4
-rw-r--r--doc/user/group/devops_adoption/index.md2
-rw-r--r--lib/api/entities/namespace_existence.rb9
-rw-r--r--lib/api/namespaces.rb17
-rw-r--r--locale/gitlab.pot27
-rw-r--r--package.json4
-rw-r--r--spec/features/groups_spec.rb23
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb2
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb1
-rw-r--r--spec/features/projects/jobs_spec.rb1
-rw-r--r--spec/frontend/jira_connect/components/__snapshots__/group_item_name_spec.js.snap5
-rw-r--r--spec/frontend/jira_connect/components/subscriptions_list_spec.js122
-rw-r--r--spec/frontend/jira_connect/index_spec.js29
-rw-r--r--spec/frontend/jira_connect/mock_data.js6
-rw-r--r--spec/frontend/jobs/components/table/jobs_table_spec.js31
-rw-r--r--spec/frontend/jobs/components/table/jobs_table_tabs_spec.js42
-rw-r--r--spec/frontend/jobs/mock_data.js128
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js29
-rw-r--r--spec/helpers/blob_helper_spec.rb1
-rw-r--r--spec/helpers/broadcast_messages_helper_spec.rb1
-rw-r--r--spec/helpers/ci/runners_helper_spec.rb1
-rw-r--r--spec/helpers/invite_members_helper_spec.rb3
-rw-r--r--spec/helpers/jira_connect_helper_spec.rb1
-rw-r--r--spec/helpers/labels_helper_spec.rb1
-rw-r--r--spec/helpers/markup_helper_spec.rb2
-rw-r--r--spec/helpers/notes_helper_spec.rb2
-rw-r--r--spec/helpers/projects/alert_management_helper_spec.rb1
-rw-r--r--spec/helpers/projects/issues/service_desk_helper_spec.rb1
-rw-r--r--spec/helpers/projects/project_members_helper_spec.rb2
-rw-r--r--spec/helpers/projects/terraform_helper_spec.rb1
-rw-r--r--spec/helpers/projects_helper_spec.rb1
-rw-r--r--spec/helpers/search_helper_spec.rb1
-rw-r--r--spec/models/namespace_spec.rb27
-rw-r--r--spec/requests/api/namespaces_spec.rb73
-rw-r--r--spec/services/boards/destroy_service_spec.rb2
-rw-r--r--spec/services/boards/issues/move_service_spec.rb1
-rw-r--r--yarn.lock18
104 files changed, 2717 insertions, 1137 deletions
diff --git a/.gitlab/changelog_config.yml b/.gitlab/changelog_config.yml
new file mode 100644
index 00000000000..7aa18cc8f36
--- /dev/null
+++ b/.gitlab/changelog_config.yml
@@ -0,0 +1,38 @@
+---
+# Settings for generating changelogs using the GitLab API. See
+# https://docs.gitlab.com/ee/api/repositories.html#generate-changelog-data for
+# more information.
+categories:
+ added: Added
+ fixed: Fixed
+ changed: Changed
+ deprecated: Deprecated
+ removed: Removed
+ security: Security
+ performance: Performance
+ other: Other
+template: |
+ {% if categories %}
+ {% each categories %}
+ ### {{ title }} ({% if single_change %}1 change{% else %}{{ count }} changes{% end %})
+
+ {% each entries %}
+ - [{{ title }}]({{ commit.reference }})\
+ {% if author.contributor %} by {{ author.reference }}{% end %}\
+ {% if commit.trailers.MR %}\
+ ([merge request]({{ commit.trailers.MR }}))\
+ {% else %}\
+ {% if merge_request %}\
+ ([merge request]({{ merge_request.reference }}))\
+ {% end %}\
+ {% end %}\
+ {% if commit.trailers.EE %}\
+ **GitLab Enterprise Edition**\
+ {% end %}
+
+ {% end %}
+
+ {% end %}
+ {% else %}
+ No changes.
+ {% end %}
diff --git a/.gitlab/issue_templates/Geo: Replicate a new Git repository type.md b/.gitlab/issue_templates/Geo: Replicate a new Git repository type.md
new file mode 100644
index 00000000000..6b2d732f246
--- /dev/null
+++ b/.gitlab/issue_templates/Geo: Replicate a new Git repository type.md
@@ -0,0 +1,756 @@
+<!--
+
+This template is based on a model named `CoolWidget`.
+
+To adapt this template, find and replace the following tokens:
+
+- `CoolWidget`
+- `Cool Widget`
+- `cool_widget`
+- `coolWidget`
+
+If your Model's pluralized form is non-standard, i.e. it doesn't just end in `s`, then find and replace the following tokens *first*:
+
+- `CoolWidgets`
+- `Cool Widgets`
+- `cool_widgets`
+- `coolWidgets`
+
+-->
+
+## Replicate Cool Widgets
+
+This issue is for implementing Geo replication and verification of Cool Widgets.
+
+For more background, see [Geo self-service framework](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/geo/framework.md).
+
+In order to implement and test this feature, you need to first [set up Geo locally](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/geo.md).
+
+There are three main sections below. It is a good idea to structure your merge requests this way as well:
+
+1. Modify database schemas to prepare to add Geo support for Cool Widgets
+1. Implement Geo support of Cool Widgets behind a feature flag
+1. Release Geo support of Cool Widgets
+
+It is also a good idea to first open a proof-of-concept merge request. It can be helpful for working out kinks and getting initial support and feedback from the Geo team. As an example, see the [Proof of Concept to replicate Pipeline Artifacts](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56423).
+
+### Modify database schemas to prepare to add Geo support for Cool Widgets
+
+You might do this section in its own merge request, but it is not required.
+
+#### Add the registry table to track replication and verification state
+
+Geo secondary sites have a [Geo tracking database](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/geo.md#tracking-database) independent of the main database. It is used to track the replication and verification state of all replicables. Every Model has a corresponding "registry" table in the Geo tracking database.
+
+- [ ] Create the migration file in `ee/db/geo/migrate`:
+
+ ```shell
+ bin/rails generate geo_migration CreateCoolWidgetRegistry
+ ```
+
+- [ ] Replace the contents of the migration file with the following. Note that we cannot add a foreign key constraint on `cool_widget_id` because the `cool_widgets` table is in a different database. The application code must handle logic such as propagating deletions.
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class CreateCoolWidgetRegistry < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:cool_widget_registry)
+ ActiveRecord::Base.transaction do
+ create_table :cool_widget_registry, id: :bigserial, force: :cascade do |t|
+ t.bigint :cool_widget_id, null: false
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :last_synced_at
+ t.datetime_with_timezone :retry_at
+ t.datetime_with_timezone :verified_at
+ t.datetime_with_timezone :verification_started_at
+ t.datetime_with_timezone :verification_retry_at
+ t.integer :state, default: 0, null: false, limit: 2
+ t.integer :verification_state, default: 0, null: false, limit: 2
+ t.integer :retry_count, default: 0, limit: 2, null: false
+ t.integer :verification_retry_count, default: 0, limit: 2, null: false
+ t.boolean :checksum_mismatch, default: false, null: false
+ t.boolean :force_to_redownload, default: false, null: false
+ t.boolean :missing_on_primary, default: false, null: false
+ t.binary :verification_checksum
+ t.binary :verification_checksum_mismatched
+ t.string :verification_failure, limit: 255 # rubocop:disable Migration/PreventStrings see https://gitlab.com/gitlab-org/gitlab/-/issues/323806
+ t.string :last_sync_failure, limit: 255 # rubocop:disable Migration/PreventStrings see https://gitlab.com/gitlab-org/gitlab/-/issues/323806
+
+ t.index :cool_widget_id, name: :index_cool_widget_registry_on_cool_widget_id, unique: true
+ t.index :retry_at
+ t.index :state
+ # To optimize performance of CoolWidgetRegistry.verification_failed_batch
+ t.index :verification_retry_at, name: :cool_widget_registry_failed_verification, order: "NULLS FIRST", where: "((state = 2) AND (verification_state = 3))"
+ # To optimize performance of CoolWidgetRegistry.needs_verification_count
+ t.index :verification_state, name: :cool_widget_registry_needs_verification, where: "((state = 2) AND (verification_state = ANY (ARRAY[0, 3])))"
+ # To optimize performance of CoolWidgetRegistry.verification_pending_batch
+ t.index :verified_at, name: :cool_widget_registry_pending_verification, order: "NULLS FIRST", where: "((state = 2) AND (verification_state = 0))"
+ end
+ end
+ end
+ end
+
+ def down
+ drop_table :cool_widget_registry
+ end
+ end
+ ```
+
+- [ ] If deviating from the above example, then be sure to order columns according to [our guidelines](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/ordering_table_columns.md).
+- [ ] Run Geo tracking database migrations:
+
+ ```shell
+ bin/rake geo:db:migrate
+ ```
+
+- [ ] Be sure to commit the relevant changes in `ee/db/geo/schema.rb`
+
+### Add verification state fields on the Geo primary site
+
+The Geo primary site needs to checksum every replicable in order for secondaries to verify their own checksums. To do this, Geo requires fields on the Model. There are two ways to add the necessary verification state fields. If the table is large and wide, then it may be a good idea to add verification state fields to a separate table (Option 2). Consult a database expert if needed.
+
+#### Add verification state fields to the model table (Option 1)
+
+- [ ] Create the migration file in `db/migrate`:
+
+ ```shell
+ bin/rails generate migration AddVerificationStateToCoolWidgets
+ ```
+
+- [ ] Replace the contents of the migration file with:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class AddVerificationStateToCoolWidgets < ActiveRecord::Migration[6.0]
+ def change
+ change_table(:cool_widgets) do |t|
+ t.integer :verification_state, default: 0, limit: 2, null: false
+ t.column :verification_started_at, :datetime_with_timezone
+ t.integer :verification_retry_count, limit: 2, null: false
+ t.column :verification_retry_at, :datetime_with_timezone
+ t.column :verified_at, :datetime_with_timezone
+ t.binary :verification_checksum, using: 'verification_checksum::bytea'
+
+ t.text :verification_failure # rubocop:disable Migration/AddLimitToTextColumns
+ end
+ end
+ end
+ ```
+
+- [ ] If deviating from the above example, then be sure to order columns according to [our guidelines](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/ordering_table_columns.md).
+- [ ] If `cool_widgets` is a high-traffic table, follow [the database documentation to use `with_lock_retries`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/migration_style_guide.md#when-to-use-the-helper-method)
+- [ ] Adding a `text` column also [requires](../database/strings_and_the_text_data_type.md#add-a-text-column-to-an-existing-table) setting a limit. Create the migration file in `db/migrate`:
+
+ ```shell
+ bin/rails generate migration AddVerificationFailureLimitToCoolWidgets
+ ```
+
+- [ ] Replace the contents of the migration file with:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class AddVerificationFailureLimitToCoolWidgets < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ CONSTRAINT_NAME = 'cool_widget_verification_failure_text_limit'
+
+ def up
+ add_text_limit :cool_widget, :verification_failure, 255, constraint_name: CONSTRAINT_NAME
+ end
+
+ def down
+ remove_check_constraint(:cool_widget, CONSTRAINT_NAME)
+ end
+ end
+ ```
+
+- [ ] Add indexes on verification fields to ensure verification can be performed efficiently. Some or all of these indexes can be omitted if the table is guaranteed to be small. Ask a database expert if you are considering omitting indexes. Create the migration file in `db/migrate`:
+
+ ```shell
+ bin/rails generate migration AddVerificationIndexesToCoolWidgets
+ ```
+
+- [ ] Replace the contents of the migration file with:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class AddVerificationIndexesToCoolWidgets < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ VERIFICATION_STATE_INDEX_NAME = "index_cool_widgets_on_verification_state"
+ PENDING_VERIFICATION_INDEX_NAME = "index_cool_widgets_pending_verification"
+ FAILED_VERIFICATION_INDEX_NAME = "index_cool_widgets_failed_verification"
+ NEEDS_VERIFICATION_INDEX_NAME = "index_cool_widgets_needs_verification"
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :cool_widgets, :verification_state, name: VERIFICATION_STATE_INDEX_NAME
+ add_concurrent_index :cool_widgets, :verified_at, where: "(verification_state = 0)", order: { verified_at: 'ASC NULLS FIRST' }, name: PENDING_VERIFICATION_INDEX_NAME
+ add_concurrent_index :cool_widgets, :verification_retry_at, where: "(verification_state = 3)", order: { verification_retry_at: 'ASC NULLS FIRST' }, name: FAILED_VERIFICATION_INDEX_NAME
+ add_concurrent_index :cool_widgets, :verification_state, where: "(verification_state = 0 OR verification_state = 3)", name: NEEDS_VERIFICATION_INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :cool_widgets, VERIFICATION_STATE_INDEX_NAME
+ remove_concurrent_index_by_name :cool_widgets, PENDING_VERIFICATION_INDEX_NAME
+ remove_concurrent_index_by_name :cool_widgets, FAILED_VERIFICATION_INDEX_NAME
+ remove_concurrent_index_by_name :cool_widgets, NEEDS_VERIFICATION_INDEX_NAME
+ end
+ end
+ ```
+
+- [ ] Run database migrations:
+
+ ```shell
+ bin/rake db:migrate
+ ```
+
+- [ ] Be sure to commit the relevant changes in `db/structure.sql`
+
+#### Add verification state fields to a separate table (Option 2)
+
+- [ ] Create the migration file in `db/migrate`:
+
+ ```shell
+ bin/rails generate migration CreateCoolWidgetStates
+ ```
+
+- [ ] Replace the contents of the migration file with:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class CreateCoolWidgetStates < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ VERIFICATION_STATE_INDEX_NAME = "index_cool_widget_states_on_verification_state"
+ PENDING_VERIFICATION_INDEX_NAME = "index_cool_widget_states_pending_verification"
+ FAILED_VERIFICATION_INDEX_NAME = "index_cool_widget_states_failed_verification"
+ NEEDS_VERIFICATION_INDEX_NAME = "index_cool_widget_states_needs_verification"
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:cool_widget_states)
+ with_lock_retries do
+ create_table :cool_widget_states, id: false do |t|
+ t.references :cool_widget, primary_key: true, null: false, foreign_key: { on_delete: :cascade }
+ t.integer :verification_state, default: 0, limit: 2, null: false
+ t.column :verification_started_at, :datetime_with_timezone
+ t.datetime_with_timezone :verification_retry_at
+ t.datetime_with_timezone :verified_at
+ t.integer :verification_retry_count, limit: 2
+ t.binary :verification_checksum, using: 'verification_checksum::bytea'
+ t.text :verification_failure
+
+ t.index :verification_state, name: VERIFICATION_STATE_INDEX_NAME
+ t.index :verified_at, where: "(verification_state = 0)", order: { verified_at: 'ASC NULLS FIRST' }, name: PENDING_VERIFICATION_INDEX_NAME
+ t.index :verification_retry_at, where: "(verification_state = 3)", order: { verification_retry_at: 'ASC NULLS FIRST' }, name: FAILED_VERIFICATION_INDEX_NAME
+ t.index :verification_state, where: "(verification_state = 0 OR verification_state = 3)", name: NEEDS_VERIFICATION_INDEX_NAME
+ end
+ end
+ end
+
+ add_text_limit :cool_widget_states, :verification_failure, 255
+ end
+
+ def down
+ drop_table :cool_widget_states
+ end
+ end
+ ```
+
+- [ ] If deviating from the above example, then be sure to order columns according to [our guidelines](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/ordering_table_columns.md).
+- [ ] Run database migrations:
+
+ ```shell
+ bin/rake db:migrate
+ ```
+
+- [ ] Be sure to commit the relevant changes in `db/structure.sql`
+
+That's all of the required database changes.
+
+### Implement Geo support of Cool Widgets behind a feature flag
+
+#### Step 1. Implement replication and verification
+
+- [ ] Include `Gitlab::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
+
+ Pay some attention to method `pool_repository`. Not every repository type uses repository pooling. As Geo prefers to use repository snapshotting, it can lead to data loss. Make sure to overwrite `pool_repository` so it returns nil for repositories that do not have pools.
+
+ At this point the `CoolWidget` class should look like this:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class CoolWidget < ApplicationRecord
+ include ::Gitlab::Geo::ReplicableModel
+ include ::Gitlab::Geo::VerificationState
+
+ with_replicator Geo::CoolWidgetReplicator
+
+ mount_uploader :file, CoolWidgetUploader
+
+ # Override the `all` default if not all records can be replicated. For an
+ # example of an existing Model that needs to do this, see
+ # `EE::MergeRequestDiff`.
+ # scope :available_replicables, -> { all }
+
+ # @param primary_key_in [Range, CoolWidget] arg to pass to primary_key_in scope
+ # @return [ActiveRecord::Relation<CoolWidget>] everything that should be synced to this node, restricted by primary key
+ def self.replicables_for_current_secondary(primary_key_in)
+ # This issue template does not help you write this method.
+ #
+ # This method is called only on Geo secondary sites. It is called when
+ # we want to know which records to replicate. This is not easy to automate
+ # because for example:
+ #
+ # * The "selective sync" feature allows admins to choose which namespaces # to replicate, per secondary site. Most Models are scoped to a
+ # namespace, but the nature of the relationship to a namespace varies
+ # between Models.
+ # * The "selective sync" feature allows admins to choose which shards to
+ # replicate, per secondary site. Repositories are associated with
+ # shards. Most blob types are not, but Project Uploads are.
+ # * Remote stored replicables are not replicated, by default. But the
+ # setting `sync_object_storage` enables replication of remote stored
+ # replicables.
+ #
+ # Search the codebase for examples, and consult a Geo expert if needed.
+ end
+
+ # Geo checks this method in FrameworkRepositorySyncService to avoid
+ # snapshotting repositories using object pools
+ def pool_repository
+ nil
+ end
+
+ ...
+ end
+ ```
+
+- [ ] Implement `CoolWidget.replicables_for_current_secondary` above.
+- [ ] Ensure `CoolWidget.replicables_for_current_secondary` is well-tested. Search the codebase for `replicables_for_current_secondary` to find examples of parameterized table specs. You may need to add more `FactoryBot` traits.
+- [ ] If you are using a separate table `cool_widget_states` to track verification state on the Geo primary site, then:
+ - [ ] Do not include `::Gitlab::Geo::VerificationState` on the `CoolWidget` class.
+ - [ ] Add the following lines to the `cool_widget_state.rb` model:
+
+ ```ruby
+ class CoolWidgetState < ApplicationRecord
+ ...
+ self.primary_key = :cool_widget_id
+
+ include ::Gitlab::Geo::VerificationState
+
+ belongs_to :cool_widget, inverse_of: :cool_widget_state
+ ...
+ end
+ ```
+
+ - [ ] Add the following lines to the `cool_widget` model:
+
+ ```ruby
+ class CoolWidget < ApplicationRecord
+ ...
+ has_one :cool_widget_state, inverse_of: :cool_widget
+
+ delegate :verification_retry_at, :verification_retry_at=,
+ :verified_at, :verified_at=,
+ :verification_checksum, :verification_checksum=,
+ :verification_failure, :verification_failure=,
+ :verification_retry_count, :verification_retry_count=,
+ to: :cool_widget_state
+ ...
+ end
+ ```
+
+- [ ] Create `ee/app/replicators/geo/cool_widget_replicator.rb`. Implement the `#repository` method which should return a `<Repository>` instance, and implement the class method `.model` to return the `CoolWidget` class:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ module Geo
+ class CoolWidgetReplicator < Gitlab::Geo::Replicator
+ include ::Geo::RepositoryReplicatorStrategy
+
+ def self.model
+ ::CoolWidget
+ end
+
+ def repository
+ model_record.repository
+ end
+
+ def self.git_access_class
+ ::Gitlab::GitAccessCoolWidget
+ end
+
+ # The feature flag follows the format `geo_#{replicable_name}_replication`,
+ # so here it would be `geo_cool_widget_replication`
+ def self.replication_enabled_by_default?
+ false
+ end
+
+ override :verification_feature_flag_enabled?
+ def self.verification_feature_flag_enabled?
+ # We are adding verification at the same time as replication, so we
+ # don't need to toggle verification separately from replication. When
+ # the replication feature flag is off, then verification is also off
+ # (see `VerifiableReplicator.verification_enabled?`)
+ true
+ end
+
+ end
+ end
+ ```
+
+- [ ] Make sure Geo push events are created. Usually it needs some change in the `app/workers/post_receive.rb` file. Example:
+
+ ```ruby
+ def replicate_cool_widget_changes(cool_widget)
+ if ::Gitlab::Geo.primary?
+ cool_widget.replicator.handle_after_update if cool_widget
+ end
+ end
+ ```
+
+ See `app/workers/post_receive.rb` for more examples.
+
+- [ ] Make sure the repository removal is also handled. You may need to add something like the following in the destroy service of the repository:
+
+ ```ruby
+ cool_widget.replicator.handle_after_destroy if cool_widget.repository
+ ```
+
+- [ ] Make sure a Geo secondary site can request and download Cool Widgets on the Geo primary site. You may need to make some changes to `Gitlab::GitAccessCoolWidget`. For example, see [this change for Group-level Wikis](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54914/diffs?commit_id=0f2b36f66697b4addbc69bd377ee2818f648dd33).
+- [ ] Generate the feature flag definition file by running the feature flag command and following the command prompts:
+
+ ```shell
+ bin/feature-flag --ee geo_cool_widget_replication --type development --group 'group::geo'
+ ```
+
+- [ ] Add this replicator class to the method `replicator_classes` in
+ `ee/lib/gitlab/geo.rb`:
+
+ ```ruby
+ REPLICATOR_CLASSES = [
+ ::Geo::PackageFileReplicator,
+ ::Geo::CoolWidgetReplicator
+ ]
+ end
+ ```
+
+- [ ] Create `ee/spec/replicators/geo/cool_widget_replicator_spec.rb` and perform the necessary setup to define the `model_record` variable for the shared examples:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ RSpec.describe Geo::CoolWidgetReplicator do
+ let(:model_record) { build(:cool_widget) }
+
+ include_examples 'a repository replicator'
+ include_examples 'a verifiable replicator'
+ end
+ ```
+
+- [ ] Create `ee/app/models/geo/cool_widget_registry.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class Geo::CoolWidgetRegistry < Geo::BaseRegistry
+ include ::Geo::ReplicableRegistry
+ include ::Geo::VerifiableRegistry
+
+ MODEL_CLASS = ::CoolWidget
+ MODEL_FOREIGN_KEY = :cool_widget_id
+
+ belongs_to :cool_widget, class_name: 'CoolWidget'
+ end
+ ```
+
+- [ ] Update `REGISTRY_CLASSES` in `ee/app/workers/geo/secondary/registry_consistency_worker.rb`.
+- [ ] Update `def model_class_factory_name` in `ee/spec/services/geo/registry_consistency_service_spec.rb`.
+- [ ] Update `it 'creates missing registries for each registry class'` in `ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb`.
+- [ ] Add `cool_widget_registry` to `ActiveSupport::Inflector.inflections` in `config/initializers_before_autoloader/000_inflections.rb`.
+- [ ] Create `ee/spec/factories/geo/cool_widget_registry.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ FactoryBot.define do
+ factory :geo_cool_widget_registry, class: 'Geo::CoolWidgetRegistry' do
+ cool_widget
+ state { Geo::CoolWidgetRegistry.state_value(:pending) }
+
+ trait :synced do
+ state { Geo::CoolWidgetRegistry.state_value(:synced) }
+ last_synced_at { 5.days.ago }
+ end
+
+ trait :failed do
+ state { Geo::CoolWidgetRegistry.state_value(:failed) }
+ last_synced_at { 1.day.ago }
+ retry_count { 2 }
+ last_sync_failure { 'Random error' }
+ end
+
+ trait :started do
+ state { Geo::CoolWidgetRegistry.state_value(:started) }
+ last_synced_at { 1.day.ago }
+ retry_count { 0 }
+ end
+ end
+ end
+ ```
+
+- [ ] Create `ee/spec/models/geo/cool_widget_registry_spec.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ RSpec.describe Geo::CoolWidgetRegistry, :geo, type: :model do
+ let_it_be(:registry) { create(:geo_cool_widget_registry) }
+
+ specify 'factory is valid' do
+ expect(registry).to be_valid
+ end
+
+ include_examples 'a Geo framework registry'
+ include_examples 'a Geo verifiable registry'
+ end
+ ```
+
+#### Step 2. Implement metrics gathering
+
+Metrics are gathered by `Geo::MetricsUpdateWorker`, persisted in `GeoNodeStatus` for display in the UI, and sent to Prometheus:
+
+- [ ] Add the following fields to Geo Node Status example responses in `doc/api/geo_nodes.md`:
+ - `cool_widgets_count`
+ - `cool_widgets_checksum_total_count`
+ - `cool_widgets_checksummed_count`
+ - `cool_widgets_checksum_failed_count`
+ - `cool_widgets_synced_count`
+ - `cool_widgets_failed_count`
+ - `cool_widgets_registry_count`
+ - `cool_widgets_verification_total_count`
+ - `cool_widgets_verified_count`
+ - `cool_widgets_verification_failed_count`
+ - `cool_widgets_synced_in_percentage`
+ - `cool_widgets_verified_in_percentage`
+- [ ] Add the same fields to `GET /geo_nodes/status` example response in
+ `ee/spec/fixtures/api/schemas/public_api/v4/geo_node_status.json`.
+- [ ] Add the following fields to the `Sidekiq metrics` table in `doc/administration/monitoring/prometheus/gitlab_metrics.md`:
+ - `geo_cool_widgets`
+ - `geo_cool_widgets_checksum_total`
+ - `geo_cool_widgets_checksummed`
+ - `geo_cool_widgets_checksum_failed`
+ - `geo_cool_widgets_synced`
+ - `geo_cool_widgets_failed`
+ - `geo_cool_widgets_registry`
+ - `geo_cool_widgets_verification_total`
+ - `geo_cool_widgets_verified`
+ - `geo_cool_widgets_verification_failed`
+- [ ] Add the following to the parameterized table in the `context 'Replicator stats' do` block in `ee/spec/models/geo_node_status_spec.rb`:
+
+ ```ruby
+ Geo::CoolWidgetReplicator | :cool_widget | :geo_cool_widget_registry
+ ```
+
+- [ ] Add the following to `spec/factories/cool_widgets.rb`:
+
+ ```ruby
+ trait(:verification_succeeded) do
+ with_file
+ verification_checksum { 'abc' }
+ verification_state { CoolWidget.verification_state_value(:verification_succeeded) }
+ end
+
+ trait(:verification_failed) do
+ with_file
+ verification_failure { 'Could not calculate the checksum' }
+ verification_state { CoolWidget.verification_state_value(:verification_failed) }
+ end
+ ```
+
+- [ ] Make sure the factory also allows setting a `project` attribute. If the model does not have a direct relation to a project, you can use a `transient` attribute. Check out `spec/factories/merge_request_diffs.rb` for an example.
+
+Cool Widget replication and verification metrics should now be available in the API, the `Admin > Geo > Nodes` view, and Prometheus.
+
+#### Step 3. Implement the GraphQL API
+
+The GraphQL API is used by `Admin > Geo > Replication Details` views, and is directly queryable by administrators.
+
+- [ ] Add a new field to `GeoNodeType` in `ee/app/graphql/types/geo/geo_node_type.rb`:
+
+ ```ruby
+ field :cool_widget_registries, ::Types::Geo::CoolWidgetRegistryType.connection_type,
+ null: true,
+ resolver: ::Resolvers::Geo::CoolWidgetRegistriesResolver,
+ description: 'Find Cool Widget registries on this Geo node',
+ feature_flag: :geo_cool_widget_replication
+ ```
+
+- [ ] Add the new `cool_widget_registries` field name to the `expected_fields` array in `ee/spec/graphql/types/geo/geo_node_type_spec.rb`.
+- [ ] Create `ee/app/graphql/resolvers/geo/cool_widget_registries_resolver.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ module Resolvers
+ module Geo
+ class CoolWidgetRegistriesResolver < BaseResolver
+ type ::Types::Geo::GeoNodeType.connection_type, null: true
+
+ include RegistriesResolver
+ end
+ end
+ end
+ ```
+
+- [ ] Create `ee/spec/graphql/resolvers/geo/cool_widget_registries_resolver_spec.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ RSpec.describe Resolvers::Geo::CoolWidgetRegistriesResolver do
+ it_behaves_like 'a Geo registries resolver', :geo_cool_widget_registry
+ end
+ ```
+
+- [ ] Create `ee/app/finders/geo/cool_widget_registry_finder.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ module Geo
+ class CoolWidgetRegistryFinder
+ include FrameworkRegistryFinder
+ end
+ end
+ ```
+
+- [ ] Create `ee/spec/finders/geo/cool_widget_registry_finder_spec.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ RSpec.describe Geo::CoolWidgetRegistryFinder do
+ it_behaves_like 'a framework registry finder', :geo_cool_widget_registry
+ end
+ ```
+
+- [ ] Create `ee/app/graphql/types/geo/cool_widget_registry_type.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ module Types
+ module Geo
+ # rubocop:disable Graphql/AuthorizeTypes because it is included
+ class CoolWidgetRegistryType < BaseObject
+ include ::Types::Geo::RegistryType
+
+ graphql_name 'CoolWidgetRegistry'
+ description 'Represents the Geo replication and verification state of a cool_widget'
+
+ field :cool_widget_id, GraphQL::ID_TYPE, null: false, description: 'ID of the Cool Widget'
+ end
+ end
+ end
+ ```
+
+- [ ] Create `ee/spec/graphql/types/geo/cool_widget_registry_type_spec.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ RSpec.describe GitlabSchema.types['CoolWidgetRegistry'] do
+ it_behaves_like 'a Geo registry type'
+
+ it 'has the expected fields (other than those included in RegistryType)' do
+ expected_fields = %i[cool_widget_id]
+
+ expect(described_class).to have_graphql_fields(*expected_fields).at_least
+ end
+ end
+ ```
+
+- [ ] Add integration tests for providing CoolWidget registry data to the frontend via the GraphQL API, by duplicating and modifying the following shared examples in `ee/spec/requests/api/graphql/geo/registries_spec.rb`:
+
+ ```ruby
+ it_behaves_like 'gets registries for', {
+ field_name: 'coolWidgetRegistries',
+ registry_class_name: 'CoolWidgetRegistry',
+ registry_factory: :geo_cool_widget_registry,
+ registry_foreign_key_field_name: 'coolWidgetId'
+ }
+ ```
+
+- [ ] Update the GraphQL reference documentation:
+
+ ```shell
+ bundle exec rake gitlab:graphql:compile_docs
+ ```
+
+Individual Cool Widget replication and verification data should now be available via the GraphQL API.
+
+### Release Geo support of Cool Widgets
+
+- [ ] In the rollout issue you created when creating the feature flag, modify the Roll Out Steps:
+ - [ ] Cross out any steps related to testing on production GitLab.com, because Geo is not running on production GitLab.com at the moment.
+ - [ ] Add a step to `Test replication and verification of Cool Widgets on a non-GDK-deployment. For example, using GitLab Environment Toolkit`.
+ - [ ] Add a step to `Ping the Geo PM and EM to coordinate testing`. For example, you might add steps to generate Cool Widgets, and then a Geo engineer may take it from there.
+- [ ] In `ee/config/feature_flags/development/geo_cool_widget_replication.yml`, set `default_enabled: true`
+
+- [ ] In `ee/app/replicators/geo/cool_widget_replicator.rb`, delete the `self.replication_enabled_by_default?` method:
+
+ ```ruby
+ module Geo
+ class CoolWidgetReplicator < Gitlab::Geo::Replicator
+ ...
+
+ # REMOVE THIS METHOD
+ def self.replication_enabled_by_default?
+ false
+ end
+ # REMOVE THIS METHOD
+
+ ...
+ end
+ end
+ ```
+
+- [ ] In `ee/app/graphql/types/geo/geo_node_type.rb`, remove the `feature_flag` option for the released type:
+
+ ```ruby
+ field :cool_widget_registries, ::Types::Geo::CoolWidgetRegistryType.connection_type,
+ null: true,
+ resolver: ::Resolvers::Geo::CoolWidgetRegistriesResolver,
+ description: 'Find Cool Widget registries on this Geo node',
+ feature_flag: :geo_cool_widget_replication # REMOVE THIS LINE
+ ```
+
+- [ ] Add a row for Cool Widgets to the `Data types` table in [Geo data types support](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/administration/geo/replication/datatypes.md#data-types)
+- [ ] Add a row for Cool Widgets to the `Limitations on replication/verification` table in [Geo data types support](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/administration/geo/replication/datatypes.md#limitations-on-replicationverification). If the row already exists, then update it to show that Replication and Verification is released in the current version.
diff --git a/.gitlab/issue_templates/Geo: Replicate a new blob type.md b/.gitlab/issue_templates/Geo: Replicate a new blob type.md
new file mode 100644
index 00000000000..12fe6a6f5bb
--- /dev/null
+++ b/.gitlab/issue_templates/Geo: Replicate a new blob type.md
@@ -0,0 +1,722 @@
+<!--
+
+This template is based on a model named `CoolWidget`.
+
+To adapt this template, find and replace the following tokens:
+
+- `CoolWidget`
+- `Cool Widget`
+- `cool_widget`
+- `coolWidget`
+
+If your Model's pluralized form is non-standard, i.e. it doesn't just end in `s`, find and replace the following tokens *first*:
+
+- `CoolWidgets`
+- `Cool Widgets`
+- `cool_widgets`
+- `coolWidgets`
+
+-->
+
+## Replicate Cool Widgets
+
+This issue is for implementing Geo replication and verification of Cool Widgets.
+
+For more background, see [Geo self-service framework](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/geo/framework.md).
+
+In order to implement and test this feature, you need to first [set up Geo locally](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/geo.md).
+
+There are three main sections below. It is a good idea to structure your merge requests this way as well:
+
+1. Modify database schemas to prepare to add Geo support for Cool Widgets
+1. Implement Geo support of Cool Widgets behind a feature flag
+1. Release Geo support of Cool Widgets
+
+It is also a good idea to first open a proof-of-concept merge request. It can be helpful for working out kinks and getting initial support and feedback from the Geo team. As an example, see the [Proof of Concept to replicate Pipeline Artifacts](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56423).
+
+### Modify database schemas to prepare to add Geo support for Cool Widgets
+
+You might do this section in its own merge request, but it is not required.
+
+#### Add the registry table to track replication and verification state
+
+Geo secondary sites have a [Geo tracking database](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/geo.md#tracking-database) independent of the main database. It is used to track the replication and verification state of all replicables. Every Model has a corresponding "registry" table in the Geo tracking database.
+
+- [ ] Create the migration file in `ee/db/geo/migrate`:
+
+ ```shell
+ bin/rails generate geo_migration CreateCoolWidgetRegistry
+ ```
+
+- [ ] Replace the contents of the migration file with the following. Note that we cannot add a foreign key constraint on `cool_widget_id` because the `cool_widgets` table is in a different database. The application code must handle logic such as propagating deletions.
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class CreateCoolWidgetRegistry < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:cool_widget_registry)
+ ActiveRecord::Base.transaction do
+ create_table :cool_widget_registry, id: :bigserial, force: :cascade do |t|
+ t.bigint :cool_widget_id, null: false
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :last_synced_at
+ t.datetime_with_timezone :retry_at
+ t.datetime_with_timezone :verified_at
+ t.datetime_with_timezone :verification_started_at
+ t.datetime_with_timezone :verification_retry_at
+ t.integer :state, default: 0, null: false, limit: 2
+ t.integer :verification_state, default: 0, null: false, limit: 2
+ t.integer :retry_count, default: 0, limit: 2, null: false
+ t.integer :verification_retry_count, default: 0, limit: 2, null: false
+ t.boolean :checksum_mismatch, default: false, null: false
+ t.binary :verification_checksum
+ t.binary :verification_checksum_mismatched
+ t.string :verification_failure, limit: 255 # rubocop:disable Migration/PreventStrings see https://gitlab.com/gitlab-org/gitlab/-/issues/323806
+ t.string :last_sync_failure, limit: 255 # rubocop:disable Migration/PreventStrings see https://gitlab.com/gitlab-org/gitlab/-/issues/323806
+
+ t.index :cool_widget_id, name: :index_cool_widget_registry_on_cool_widget_id, unique: true
+ t.index :retry_at
+ t.index :state
+ # To optimize performance of CoolWidgetRegistry.verification_failed_batch
+ t.index :verification_retry_at, name: :cool_widget_registry_failed_verification, order: "NULLS FIRST", where: "((state = 2) AND (verification_state = 3))"
+ # To optimize performance of CoolWidgetRegistry.needs_verification_count
+ t.index :verification_state, name: :cool_widget_registry_needs_verification, where: "((state = 2) AND (verification_state = ANY (ARRAY[0, 3])))"
+ # To optimize performance of CoolWidgetRegistry.verification_pending_batch
+ t.index :verified_at, name: :cool_widget_registry_pending_verification, order: "NULLS FIRST", where: "((state = 2) AND (verification_state = 0))"
+ end
+ end
+ end
+ end
+
+ def down
+ drop_table :cool_widget_registry
+ end
+ end
+ ```
+
+- [ ] If deviating from the above example, then be sure to order columns according to [our guidelines](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/ordering_table_columns.md).
+- [ ] Run Geo tracking database migrations:
+
+ ```shell
+ bin/rake geo:db:migrate
+ ```
+
+- [ ] Be sure to commit the relevant changes in `ee/db/geo/schema.rb`
+
+### Add verification state fields on the Geo primary site
+
+The Geo primary site needs to checksum every replicable in order for secondaries to verify their own checksums. To do this, Geo requires fields on the Model. There are two ways to add the necessary verification state fields. If the table is large and wide, then it may be a good idea to add verification state fields to a separate table (Option 2). Consult a database expert if needed.
+
+#### Add verification state fields to the model table (Option 1)
+
+- [ ] Create the migration file in `db/migrate`:
+
+ ```shell
+ bin/rails generate migration AddVerificationStateToCoolWidgets
+ ```
+
+- [ ] Replace the contents of the migration file with:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class AddVerificationStateToCoolWidgets < ActiveRecord::Migration[6.0]
+ def change
+ change_table(:cool_widgets) do |t|
+ t.integer :verification_state, default: 0, limit: 2, null: false
+ t.column :verification_started_at, :datetime_with_timezone
+ t.integer :verification_retry_count, limit: 2, null: false
+ t.column :verification_retry_at, :datetime_with_timezone
+ t.column :verified_at, :datetime_with_timezone
+ t.binary :verification_checksum, using: 'verification_checksum::bytea'
+
+ t.text :verification_failure # rubocop:disable Migration/AddLimitToTextColumns
+ end
+ end
+ end
+ ```
+
+- [ ] If deviating from the above example, then be sure to order columns according to [our guidelines](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/ordering_table_columns.md).
+- [ ] If `cool_widgets` is a high-traffic table, follow [the database documentation to use `with_lock_retries`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/migration_style_guide.md#when-to-use-the-helper-method)
+- [ ] Adding a `text` column also [requires](../database/strings_and_the_text_data_type.md#add-a-text-column-to-an-existing-table) setting a limit. Create the migration file in `db/migrate`:
+
+ ```shell
+ bin/rails generate migration AddVerificationFailureLimitToCoolWidgets
+ ```
+
+- [ ] Replace the contents of the migration file with:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class AddVerificationFailureLimitToCoolWidgets < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ CONSTRAINT_NAME = 'cool_widget_verification_failure_text_limit'
+
+ def up
+ add_text_limit :cool_widget, :verification_failure, 255, constraint_name: CONSTRAINT_NAME
+ end
+
+ def down
+ remove_check_constraint(:cool_widget, CONSTRAINT_NAME)
+ end
+ end
+ ```
+
+- [ ] Add indexes on verification fields to ensure verification can be performed efficiently. Some or all of these indexes can be omitted if the table is guaranteed to be small. Ask a database expert if you are considering omitting indexes. Create the migration file in `db/migrate`:
+
+ ```shell
+ bin/rails generate migration AddVerificationIndexesToCoolWidgets
+ ```
+
+- [ ] Replace the contents of the migration file with:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class AddVerificationIndexesToCoolWidgets < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ VERIFICATION_STATE_INDEX_NAME = "index_cool_widgets_on_verification_state"
+ PENDING_VERIFICATION_INDEX_NAME = "index_cool_widgets_pending_verification"
+ FAILED_VERIFICATION_INDEX_NAME = "index_cool_widgets_failed_verification"
+ NEEDS_VERIFICATION_INDEX_NAME = "index_cool_widgets_needs_verification"
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :cool_widgets, :verification_state, name: VERIFICATION_STATE_INDEX_NAME
+ add_concurrent_index :cool_widgets, :verified_at, where: "(verification_state = 0)", order: { verified_at: 'ASC NULLS FIRST' }, name: PENDING_VERIFICATION_INDEX_NAME
+ add_concurrent_index :cool_widgets, :verification_retry_at, where: "(verification_state = 3)", order: { verification_retry_at: 'ASC NULLS FIRST' }, name: FAILED_VERIFICATION_INDEX_NAME
+ add_concurrent_index :cool_widgets, :verification_state, where: "(verification_state = 0 OR verification_state = 3)", name: NEEDS_VERIFICATION_INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :cool_widgets, VERIFICATION_STATE_INDEX_NAME
+ remove_concurrent_index_by_name :cool_widgets, PENDING_VERIFICATION_INDEX_NAME
+ remove_concurrent_index_by_name :cool_widgets, FAILED_VERIFICATION_INDEX_NAME
+ remove_concurrent_index_by_name :cool_widgets, NEEDS_VERIFICATION_INDEX_NAME
+ end
+ end
+ ```
+
+- [ ] Run database migrations:
+
+ ```shell
+ bin/rake db:migrate
+ ```
+
+- [ ] Be sure to commit the relevant changes in `db/structure.sql`
+
+#### Add verification state fields to a separate table (Option 2)
+
+- [ ] Create the migration file in `db/migrate`:
+
+ ```shell
+ bin/rails generate migration CreateCoolWidgetStates
+ ```
+
+- [ ] Replace the contents of the migration file with:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class CreateCoolWidgetStates < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ VERIFICATION_STATE_INDEX_NAME = "index_cool_widget_states_on_verification_state"
+ PENDING_VERIFICATION_INDEX_NAME = "index_cool_widget_states_pending_verification"
+ FAILED_VERIFICATION_INDEX_NAME = "index_cool_widget_states_failed_verification"
+ NEEDS_VERIFICATION_INDEX_NAME = "index_cool_widget_states_needs_verification"
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:cool_widget_states)
+ with_lock_retries do
+ create_table :cool_widget_states, id: false do |t|
+ t.references :cool_widget, primary_key: true, null: false, foreign_key: { on_delete: :cascade }
+ t.integer :verification_state, default: 0, limit: 2, null: false
+ t.column :verification_started_at, :datetime_with_timezone
+ t.datetime_with_timezone :verification_retry_at
+ t.datetime_with_timezone :verified_at
+ t.integer :verification_retry_count, limit: 2
+ t.binary :verification_checksum, using: 'verification_checksum::bytea'
+ t.text :verification_failure
+
+ t.index :verification_state, name: VERIFICATION_STATE_INDEX_NAME
+ t.index :verified_at, where: "(verification_state = 0)", order: { verified_at: 'ASC NULLS FIRST' }, name: PENDING_VERIFICATION_INDEX_NAME
+ t.index :verification_retry_at, where: "(verification_state = 3)", order: { verification_retry_at: 'ASC NULLS FIRST' }, name: FAILED_VERIFICATION_INDEX_NAME
+ t.index :verification_state, where: "(verification_state = 0 OR verification_state = 3)", name: NEEDS_VERIFICATION_INDEX_NAME
+ end
+ end
+ end
+
+ add_text_limit :cool_widget_states, :verification_failure, 255
+ end
+
+ def down
+ drop_table :cool_widget_states
+ end
+ end
+ ```
+
+- [ ] If deviating from the above example, then be sure to order columns according to [our guidelines](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/ordering_table_columns.md).
+- [ ] Run database migrations:
+
+ ```shell
+ bin/rake db:migrate
+ ```
+
+- [ ] Be sure to commit the relevant changes in `db/structure.sql`
+
+That's all of the required database changes.
+
+### Implement Geo support of Cool Widgets behind a feature flag
+
+#### Step 1. Implement replication and verification
+
+- [ ] Include `Gitlab::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
+
+ At this point the `CoolWidget` class should look like this:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class CoolWidget < ApplicationRecord
+ include ::Gitlab::Geo::ReplicableModel
+ include ::Gitlab::Geo::VerificationState
+
+ with_replicator Geo::CoolWidgetReplicator
+
+ mount_uploader :file, CoolWidgetUploader
+
+ # Override the `all` default if not all records can be replicated. For an
+ # example of an existing Model that needs to do this, see
+ # `EE::MergeRequestDiff`.
+ # scope :available_replicables, -> { all }
+
+ # @param primary_key_in [Range, CoolWidget] arg to pass to primary_key_in scope
+ # @return [ActiveRecord::Relation<CoolWidget>] everything that should be synced to this node, restricted by primary key
+ def self.replicables_for_current_secondary(primary_key_in)
+ # This issue template does not help you write this method.
+ #
+ # This method is called only on Geo secondary sites. It is called when
+ # we want to know which records to replicate. This is not easy to automate
+ # because for example:
+ #
+ # * The "selective sync" feature allows admins to choose which namespaces # to replicate, per secondary site. Most Models are scoped to a
+ # namespace, but the nature of the relationship to a namespace varies
+ # between Models.
+ # * The "selective sync" feature allows admins to choose which shards to
+ # replicate, per secondary site. Repositories are associated with
+ # shards. Most blob types are not, but Project Uploads are.
+ # * Remote stored replicables are not replicated, by default. But the
+ # setting `sync_object_storage` enables replication of remote stored
+ # replicables.
+ #
+ # Search the codebase for examples, and consult a Geo expert if needed.
+ end
+ ...
+ end
+ ```
+
+- [ ] Implement `CoolWidget.replicables_for_current_secondary` above.
+- [ ] Ensure `CoolWidget.replicables_for_current_secondary` is well-tested. Search the codebase for `replicables_for_current_secondary` to find examples of parameterized table specs. You may need to add more `FactoryBot` traits.
+- [ ] If you are using a separate table `cool_widget_states` to track verification state on the Geo primary site, then:
+ - [ ] Do not include `::Gitlab::Geo::VerificationState` on the `CoolWidget` class.
+ - [ ] Add the following lines to the `cool_widget_state.rb` model:
+
+ ```ruby
+ class CoolWidgetState < ApplicationRecord
+ ...
+ self.primary_key = :cool_widget_id
+
+ include ::Gitlab::Geo::VerificationState
+
+ belongs_to :cool_widget, inverse_of: :cool_widget_state
+ ...
+ end
+ ```
+
+ - [ ] Add the following lines to the `cool_widget` model:
+
+ ```ruby
+ class CoolWidget < ApplicationRecord
+ ...
+ has_one :cool_widget_state, inverse_of: :cool_widget
+
+ delegate :verification_retry_at, :verification_retry_at=,
+ :verified_at, :verified_at=,
+ :verification_checksum, :verification_checksum=,
+ :verification_failure, :verification_failure=,
+ :verification_retry_count, :verification_retry_count=,
+ to: :cool_widget_state
+ ...
+ end
+ ```
+
+- [ ] Create `ee/app/replicators/geo/cool_widget_replicator.rb`. Implement the `#carrierwave_uploader` method which should return a `CarrierWave::Uploader`, and implement the class method `.model` to return the `CoolWidget` class:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ module Geo
+ class CoolWidgetReplicator < Gitlab::Geo::Replicator
+ include ::Geo::BlobReplicatorStrategy
+
+ def self.model
+ ::CoolWidget
+ end
+
+ def carrierwave_uploader
+ model_record.file
+ end
+
+ # The feature flag follows the format `geo_#{replicable_name}_replication`,
+ # so here it would be `geo_cool_widget_replication`
+ def self.replication_enabled_by_default?
+ false
+ end
+
+ override :verification_feature_flag_enabled?
+ def self.verification_feature_flag_enabled?
+ # We are adding verification at the same time as replication, so we
+ # don't need to toggle verification separately from replication. When
+ # the replication feature flag is off, then verification is also off
+ # (see `VerifiableReplicator.verification_enabled?`)
+ true
+ end
+
+ end
+ end
+ ```
+
+- [ ] Generate the feature flag definition file by running the feature flag command and following the command prompts:
+
+ ```shell
+ bin/feature-flag --ee geo_cool_widget_replication --type development --group 'group::geo'
+ ```
+
+- [ ] Add this replicator class to the method `replicator_classes` in
+ `ee/lib/gitlab/geo.rb`:
+
+ ```ruby
+ REPLICATOR_CLASSES = [
+ ::Geo::PackageFileReplicator,
+ ::Geo::CoolWidgetReplicator
+ ]
+ end
+ ```
+
+- [ ] Create `ee/spec/replicators/geo/cool_widget_replicator_spec.rb` and perform the necessary setup to define the `model_record` variable for the shared examples:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ RSpec.describe Geo::CoolWidgetReplicator do
+ let(:model_record) { build(:cool_widget) }
+
+ include_examples 'a blob replicator'
+ include_examples 'a verifiable replicator'
+ end
+ ```
+
+- [ ] Create `ee/app/models/geo/cool_widget_registry.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class Geo::CoolWidgetRegistry < Geo::BaseRegistry
+ include ::Geo::ReplicableRegistry
+ include ::Geo::VerifiableRegistry
+
+ MODEL_CLASS = ::CoolWidget
+ MODEL_FOREIGN_KEY = :cool_widget_id
+
+ belongs_to :cool_widget, class_name: 'CoolWidget'
+ end
+ ```
+
+- [ ] Update `REGISTRY_CLASSES` in `ee/app/workers/geo/secondary/registry_consistency_worker.rb`.
+- [ ] Update `def model_class_factory_name` in `ee/spec/services/geo/registry_consistency_service_spec.rb`.
+- [ ] Update `it 'creates missing registries for each registry class'` in `ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb`.
+- [ ] Add `cool_widget_registry` to `ActiveSupport::Inflector.inflections` in `config/initializers_before_autoloader/000_inflections.rb`.
+- [ ] Create `ee/spec/factories/geo/cool_widget_registry.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ FactoryBot.define do
+ factory :geo_cool_widget_registry, class: 'Geo::CoolWidgetRegistry' do
+ cool_widget
+ state { Geo::CoolWidgetRegistry.state_value(:pending) }
+
+ trait :synced do
+ state { Geo::CoolWidgetRegistry.state_value(:synced) }
+ last_synced_at { 5.days.ago }
+ end
+
+ trait :failed do
+ state { Geo::CoolWidgetRegistry.state_value(:failed) }
+ last_synced_at { 1.day.ago }
+ retry_count { 2 }
+ last_sync_failure { 'Random error' }
+ end
+
+ trait :started do
+ state { Geo::CoolWidgetRegistry.state_value(:started) }
+ last_synced_at { 1.day.ago }
+ retry_count { 0 }
+ end
+ end
+ end
+ ```
+
+- [ ] Create `ee/spec/models/geo/cool_widget_registry_spec.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ RSpec.describe Geo::CoolWidgetRegistry, :geo, type: :model do
+ let_it_be(:registry) { create(:geo_cool_widget_registry) }
+
+ specify 'factory is valid' do
+ expect(registry).to be_valid
+ end
+
+ include_examples 'a Geo framework registry'
+ include_examples 'a Geo verifiable registry'
+ end
+ ```
+
+#### Step 2. Implement metrics gathering
+
+Metrics are gathered by `Geo::MetricsUpdateWorker`, persisted in `GeoNodeStatus` for display in the UI, and sent to Prometheus:
+
+- [ ] Add the following fields to Geo Node Status example responses in `doc/api/geo_nodes.md`:
+ - `cool_widgets_count`
+ - `cool_widgets_checksum_total_count`
+ - `cool_widgets_checksummed_count`
+ - `cool_widgets_checksum_failed_count`
+ - `cool_widgets_synced_count`
+ - `cool_widgets_failed_count`
+ - `cool_widgets_registry_count`
+ - `cool_widgets_verification_total_count`
+ - `cool_widgets_verified_count`
+ - `cool_widgets_verification_failed_count`
+ - `cool_widgets_synced_in_percentage`
+ - `cool_widgets_verified_in_percentage`
+- [ ] Add the same fields to `GET /geo_nodes/status` example response in
+ `ee/spec/fixtures/api/schemas/public_api/v4/geo_node_status.json`.
+- [ ] Add the following fields to the `Sidekiq metrics` table in `doc/administration/monitoring/prometheus/gitlab_metrics.md`:
+ - `geo_cool_widgets`
+ - `geo_cool_widgets_checksum_total`
+ - `geo_cool_widgets_checksummed`
+ - `geo_cool_widgets_checksum_failed`
+ - `geo_cool_widgets_synced`
+ - `geo_cool_widgets_failed`
+ - `geo_cool_widgets_registry`
+ - `geo_cool_widgets_verification_total`
+ - `geo_cool_widgets_verified`
+ - `geo_cool_widgets_verification_failed`
+- [ ] Add the following to the parameterized table in the `context 'Replicator stats' do` block in `ee/spec/models/geo_node_status_spec.rb`:
+
+ ```ruby
+ Geo::CoolWidgetReplicator | :cool_widget | :geo_cool_widget_registry
+ ```
+
+- [ ] Add the following to `spec/factories/cool_widgets.rb`:
+
+ ```ruby
+ trait(:verification_succeeded) do
+ with_file
+ verification_checksum { 'abc' }
+ verification_state { CoolWidget.verification_state_value(:verification_succeeded) }
+ end
+
+ trait(:verification_failed) do
+ with_file
+ verification_failure { 'Could not calculate the checksum' }
+ verification_state { CoolWidget.verification_state_value(:verification_failed) }
+ end
+ ```
+
+- [ ] Make sure the factory also allows setting a `project` attribute. If the model does not have a direct relation to a project, you can use a `transient` attribute. Check out `spec/factories/merge_request_diffs.rb` for an example.
+
+Cool Widget replication and verification metrics should now be available in the API, the `Admin > Geo > Nodes` view, and Prometheus.
+
+#### Step 3. Implement the GraphQL API
+
+The GraphQL API is used by `Admin > Geo > Replication Details` views, and is directly queryable by administrators.
+
+- [ ] Add a new field to `GeoNodeType` in `ee/app/graphql/types/geo/geo_node_type.rb`:
+
+ ```ruby
+ field :cool_widget_registries, ::Types::Geo::CoolWidgetRegistryType.connection_type,
+ null: true,
+ resolver: ::Resolvers::Geo::CoolWidgetRegistriesResolver,
+ description: 'Find Cool Widget registries on this Geo node',
+ feature_flag: :geo_cool_widget_replication
+ ```
+
+- [ ] Add the new `cool_widget_registries` field name to the `expected_fields` array in `ee/spec/graphql/types/geo/geo_node_type_spec.rb`.
+- [ ] Create `ee/app/graphql/resolvers/geo/cool_widget_registries_resolver.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ module Resolvers
+ module Geo
+ class CoolWidgetRegistriesResolver < BaseResolver
+ type ::Types::Geo::GeoNodeType.connection_type, null: true
+
+ include RegistriesResolver
+ end
+ end
+ end
+ ```
+
+- [ ] Create `ee/spec/graphql/resolvers/geo/cool_widget_registries_resolver_spec.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ RSpec.describe Resolvers::Geo::CoolWidgetRegistriesResolver do
+ it_behaves_like 'a Geo registries resolver', :geo_cool_widget_registry
+ end
+ ```
+
+- [ ] Create `ee/app/finders/geo/cool_widget_registry_finder.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ module Geo
+ class CoolWidgetRegistryFinder
+ include FrameworkRegistryFinder
+ end
+ end
+ ```
+
+- [ ] Create `ee/spec/finders/geo/cool_widget_registry_finder_spec.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ RSpec.describe Geo::CoolWidgetRegistryFinder do
+ it_behaves_like 'a framework registry finder', :geo_cool_widget_registry
+ end
+ ```
+
+- [ ] Create `ee/app/graphql/types/geo/cool_widget_registry_type.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ module Types
+ module Geo
+ # rubocop:disable Graphql/AuthorizeTypes because it is included
+ class CoolWidgetRegistryType < BaseObject
+ include ::Types::Geo::RegistryType
+
+ graphql_name 'CoolWidgetRegistry'
+ description 'Represents the Geo replication and verification state of a cool_widget'
+
+ field :cool_widget_id, GraphQL::ID_TYPE, null: false, description: 'ID of the Cool Widget'
+ end
+ end
+ end
+ ```
+
+- [ ] Create `ee/spec/graphql/types/geo/cool_widget_registry_type_spec.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ RSpec.describe GitlabSchema.types['CoolWidgetRegistry'] do
+ it_behaves_like 'a Geo registry type'
+
+ it 'has the expected fields (other than those included in RegistryType)' do
+ expected_fields = %i[cool_widget_id]
+
+ expect(described_class).to have_graphql_fields(*expected_fields).at_least
+ end
+ end
+ ```
+
+- [ ] Add integration tests for providing CoolWidget registry data to the frontend via the GraphQL API, by duplicating and modifying the following shared examples in `ee/spec/requests/api/graphql/geo/registries_spec.rb`:
+
+ ```ruby
+ it_behaves_like 'gets registries for', {
+ field_name: 'coolWidgetRegistries',
+ registry_class_name: 'CoolWidgetRegistry',
+ registry_factory: :geo_cool_widget_registry,
+ registry_foreign_key_field_name: 'coolWidgetId'
+ }
+ ```
+
+- [ ] Update the GraphQL reference documentation:
+
+ ```shell
+ bundle exec rake gitlab:graphql:compile_docs
+ ```
+
+Individual Cool Widget replication and verification data should now be available via the GraphQL API.
+
+### Release Geo support of Cool Widgets
+
+- [ ] In the rollout issue you created when creating the feature flag, modify the Roll Out Steps:
+ - [ ] Cross out any steps related to testing on production GitLab.com, because Geo is not running on production GitLab.com at the moment.
+ - [ ] Add a step to `Test replication and verification of Cool Widgets on a non-GDK-deployment. For example, using GitLab Environment Toolkit`.
+ - [ ] Add a step to `Ping the Geo PM and EM to coordinate testing`. For example, you might add steps to generate Cool Widgets, and then a Geo engineer may take it from there.
+- [ ] In `ee/config/feature_flags/development/geo_cool_widget_replication.yml`, set `default_enabled: true`
+
+- [ ] In `ee/app/replicators/geo/cool_widget_replicator.rb`, delete the `self.replication_enabled_by_default?` method:
+
+ ```ruby
+ module Geo
+ class CoolWidgetReplicator < Gitlab::Geo::Replicator
+ ...
+
+ # REMOVE THIS METHOD
+ def self.replication_enabled_by_default?
+ false
+ end
+ # REMOVE THIS METHOD
+
+ ...
+ end
+ end
+ ```
+
+- [ ] In `ee/app/graphql/types/geo/geo_node_type.rb`, remove the `feature_flag` option for the released type:
+
+ ```ruby
+ field :cool_widget_registries, ::Types::Geo::CoolWidgetRegistryType.connection_type,
+ null: true,
+ resolver: ::Resolvers::Geo::CoolWidgetRegistriesResolver,
+ description: 'Find Cool Widget registries on this Geo node',
+ feature_flag: :geo_cool_widget_replication # REMOVE THIS LINE
+ ```
+
+- [ ] Add a row for Cool Widgets to the `Data types` table in [Geo data types support](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/administration/geo/replication/datatypes.md#data-types)
+- [ ] Add a row for Cool Widgets to the `Limitations on replication/verification` table in [Geo data types support](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/administration/geo/replication/datatypes.md#limitations-on-replicationverification). If the row already exists, then update it to show that Replication and Verification is released in the current version.
diff --git a/.haml-lint.yml b/.haml-lint.yml
index db3dc8a45cd..0ec1af6a6c1 100644
--- a/.haml-lint.yml
+++ b/.haml-lint.yml
@@ -116,7 +116,6 @@ linters:
- Layout/SpaceInsideHashLiteralBraces
- Layout/SpaceInsideStringInterpolation
- Layout/TrailingEmptyLines
- - Lint/BooleanSymbol
- Lint/LiteralInInterpolation
- Lint/ParenthesesAsGroupedExpression
- Lint/RedundantWithIndex
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index c4e37e6c42f..d258efd2322 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -680,20 +680,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
- spec/graphql/resolvers/project_pipeline_resolver_spec.rb
- spec/graphql/resolvers/project_pipelines_resolver_spec.rb
- spec/graphql/resolvers/timelog_resolver_spec.rb
- - spec/helpers/blob_helper_spec.rb
- - spec/helpers/broadcast_messages_helper_spec.rb
- - spec/helpers/ci/runners_helper_spec.rb
- - spec/helpers/invite_members_helper_spec.rb
- - spec/helpers/jira_connect_helper_spec.rb
- - spec/helpers/labels_helper_spec.rb
- - spec/helpers/markup_helper_spec.rb
- - spec/helpers/notes_helper_spec.rb
- - spec/helpers/projects/alert_management_helper_spec.rb
- - spec/helpers/projects/issues/service_desk_helper_spec.rb
- - spec/helpers/projects/project_members_helper_spec.rb
- - spec/helpers/projects/terraform_helper_spec.rb
- - spec/helpers/projects_helper_spec.rb
- - spec/helpers/search_helper_spec.rb
- spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb
- spec/lib/extracts_path_spec.rb
- spec/lib/extracts_ref_spec.rb
@@ -894,8 +880,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
- spec/services/audit_event_service_spec.rb
- spec/services/auth/dependency_proxy_authentication_service_spec.rb
- spec/services/auto_merge_service_spec.rb
- - spec/services/boards/destroy_service_spec.rb
- - spec/services/boards/issues/move_service_spec.rb
- spec/services/bulk_create_integration_service_spec.rb
- spec/services/ci/change_variable_service_spec.rb
- spec/services/ci/change_variables_service_spec.rb
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index 39c8a88d485..c1fc75fbea6 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -16,9 +16,7 @@ export default class Group {
if (groupName.value === '') {
groupName.addEventListener('keyup', this.updateHandler);
- if (!this.parentId.value) {
- groupName.addEventListener('blur', this.updateGroupPathSlugHandler);
- }
+ groupName.addEventListener('blur', this.updateGroupPathSlugHandler);
}
});
@@ -53,7 +51,7 @@ export default class Group {
const slug = this.groupPaths[0]?.value || slugify(value);
if (!slug) return;
- fetchGroupPathAvailability(slug)
+ fetchGroupPathAvailability(slug, this.parentId?.value)
.then(({ data }) => data)
.then(({ exists, suggests }) => {
if (exists && suggests.length) {
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 22d401c451d..f2c608a8912 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -182,7 +182,10 @@ export default {
</div>
<div class="metadata d-flex flex-grow-1 flex-shrink-0 flex-wrap justify-content-md-between">
<item-actions v-if="isGroup" :group="group" :parent-group="parentGroup" />
- <item-stats :item="group" class="group-stats gl-mt-2 d-none d-md-flex" />
+ <item-stats
+ :item="group"
+ class="group-stats gl-mt-2 d-none d-md-flex gl-align-items-center"
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue
index ffc95130783..ff4dfb23687 100644
--- a/app/assets/javascripts/jira_connect/components/app.vue
+++ b/app/assets/javascripts/jira_connect/components/app.vue
@@ -3,7 +3,6 @@ import { GlAlert, GlButton, GlLink, GlModal, GlModalDirective, GlSprintf } from
import { mapState, mapMutations } from 'vuex';
import { retrieveAlert, getLocation } from '~/jira_connect/utils';
import { __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { SET_ALERT } from '../store/mutation_types';
import GroupsList from './groups_list.vue';
import SubscriptionsList from './subscriptions_list.vue';
@@ -22,7 +21,6 @@ export default {
directives: {
GlModalDirective,
},
- mixins: [glFeatureFlagsMixin()],
inject: {
usersPath: {
default: '',
diff --git a/app/assets/javascripts/jira_connect/components/group_item_name.vue b/app/assets/javascripts/jira_connect/components/group_item_name.vue
index c5ce24b3de3..e6c172dae9e 100644
--- a/app/assets/javascripts/jira_connect/components/group_item_name.vue
+++ b/app/assets/javascripts/jira_connect/components/group_item_name.vue
@@ -23,13 +23,10 @@ export default {
</div>
<div>
- <span
- class="gl-mr-3 gl-text-gray-900! gl-font-weight-bold"
- data-testid="group-list-item-name"
- >
+ <span class="gl-mr-3 gl-text-gray-900! gl-font-weight-bold">
{{ group.full_name }}
</span>
- <div v-if="group.description" data-testid="group-list-item-description">
+ <div v-if="group.description">
<p class="gl-mt-2! gl-mb-0 gl-text-gray-600" v-text="group.description"></p>
</div>
</div>
diff --git a/app/assets/javascripts/jira_connect/components/subscriptions_list.vue b/app/assets/javascripts/jira_connect/components/subscriptions_list.vue
index 4b0f9acd6ca..a606e2edbbb 100644
--- a/app/assets/javascripts/jira_connect/components/subscriptions_list.vue
+++ b/app/assets/javascripts/jira_connect/components/subscriptions_list.vue
@@ -1,10 +1,12 @@
<script>
import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
import { isEmpty } from 'lodash';
+import { mapMutations } from 'vuex';
import { removeSubscription } from '~/jira_connect/api';
import { reloadPage } from '~/jira_connect/utils';
import { __, s__ } from '~/locale';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { SET_ALERT } from '../store/mutation_types';
import GroupItemName from './group_item_name.vue';
export default {
@@ -46,8 +48,12 @@ export default {
emptyDescription: s__(
'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
),
+ unlinkError: s__('Integrations|Failed to unlink namespace. Please try again.'),
},
methods: {
+ ...mapMutations({
+ setAlert: SET_ALERT,
+ }),
isEmpty,
isLoadingItem(item) {
return this.loadingItem === item;
@@ -62,7 +68,11 @@ export default {
.then(() => {
reloadPage();
})
- .catch(() => {
+ .catch((error) => {
+ this.setAlert({
+ message: error?.response?.data?.error || this.$options.i18n.unlinkError,
+ variant: 'danger',
+ });
this.loadingItem = null;
});
},
@@ -89,6 +99,7 @@ export default {
:class="unlinkBtnClass(item)"
category="secondary"
:loading="isLoadingItem(item)"
+ :disabled="!isEmpty(loadingItem)"
@click.prevent="onClick(item)"
>{{ __('Unlink') }}</gl-button
>
diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/index.js
index 24828e2f1d1..dc8bb3b0c77 100644
--- a/app/assets/javascripts/jira_connect/index.js
+++ b/app/assets/javascripts/jira_connect/index.js
@@ -1,22 +1,14 @@
import setConfigs from '@gitlab/ui/dist/config';
import Vue from 'vue';
-import { addSubscription, removeSubscription } from '~/jira_connect/api';
-import { getLocation, reloadPage, sizeToParent } from '~/jira_connect/utils';
+import { getLocation, sizeToParent } from '~/jira_connect/utils';
import GlFeatureFlagsPlugin from '~/vue_shared/gl_feature_flags_plugin';
import Translate from '~/vue_shared/translate';
import JiraConnectApp from './components/app.vue';
import createStore from './store';
-import { SET_ALERT } from './store/mutation_types';
const store = createStore();
-const reqFailed = (res, fallbackErrorMessage) => {
- const { error = fallbackErrorMessage } = res || {};
-
- store.commit(SET_ALERT, { message: error, variant: 'danger' });
-};
-
const updateSignInLinks = async () => {
const location = await getLocation();
Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => {
@@ -25,43 +17,7 @@ const updateSignInLinks = async () => {
});
};
-const initRemoveSubscriptionButtonHandlers = () => {
- Array.from(document.querySelectorAll('.js-jira-connect-remove-subscription')).forEach((el) => {
- el.addEventListener('click', function onRemoveSubscriptionClick(e) {
- e.preventDefault();
-
- const removePath = e.target.getAttribute('href');
- removeSubscription(removePath)
- .then(reloadPage)
- .catch((err) =>
- reqFailed(err.response.data, 'Failed to remove namespace. Please try again.'),
- );
- });
- });
-};
-
-const initAddSubscriptionFormHandler = () => {
- const formEl = document.querySelector('#add-subscription-form');
- if (!formEl) {
- return;
- }
-
- formEl.addEventListener('submit', function onAddSubscriptionForm(e) {
- e.preventDefault();
-
- const addPath = e.target.getAttribute('action');
- const namespace = (e.target.querySelector('#namespace-input') || {}).value;
-
- addSubscription(addPath, namespace)
- .then(reloadPage)
- .catch((err) => reqFailed(err.response.data, 'Failed to add namespace. Please try again.'));
- });
-};
-
export async function initJiraConnect() {
- initAddSubscriptionFormHandler();
- initRemoveSubscriptionButtonHandlers();
-
await updateSignInLinks();
const el = document.querySelector('.js-jira-connect-app');
diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
new file mode 100644
index 00000000000..d9e51b0345a
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
@@ -0,0 +1,52 @@
+query getJobs($fullPath: ID!, $statuses: [CiJobStatus!]) {
+ project(fullPath: $fullPath) {
+ jobs(first: 20, statuses: $statuses) {
+ pageInfo {
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ }
+ nodes {
+ detailedStatus {
+ icon
+ label
+ text
+ tooltip
+ action {
+ buttonTitle
+ icon
+ method
+ path
+ title
+ }
+ }
+ id
+ refName
+ refPath
+ tags
+ shortSha
+ commitPath
+ pipeline {
+ id
+ path
+ user {
+ webPath
+ avatarUrl
+ }
+ }
+ stage {
+ name
+ }
+ name
+ duration
+ finishedAt
+ coverage
+ retryable
+ playable
+ cancelable
+ active
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/table/index.js b/app/assets/javascripts/jobs/components/table/index.js
new file mode 100644
index 00000000000..b6b3bb6d379
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/index.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default (containerId = 'js-jobs-table') => {
+ const containerEl = document.getElementById(containerId);
+
+ if (!containerEl) {
+ return false;
+ }
+
+ const { fullPath, jobCounts, jobStatuses } = containerEl.dataset;
+
+ return new Vue({
+ el: containerEl,
+ apolloProvider,
+ provide: {
+ fullPath,
+ jobStatuses: JSON.parse(jobStatuses),
+ jobCounts: JSON.parse(jobCounts),
+ },
+ render(createElement) {
+ return createElement(JobsTableApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue
new file mode 100644
index 00000000000..32b26d45dfe
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlTable } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+const defaultTableClasses = {
+ tdClass: 'gl-p-5!',
+ thClass: 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!',
+};
+
+export default {
+ fields: [
+ {
+ key: 'status',
+ label: __('Status'),
+ ...defaultTableClasses,
+ },
+ {
+ key: 'job',
+ label: __('Job'),
+ ...defaultTableClasses,
+ },
+ {
+ key: 'pipeline',
+ label: __('Pipeline'),
+ ...defaultTableClasses,
+ },
+ {
+ key: 'stage',
+ label: __('Stage'),
+ ...defaultTableClasses,
+ },
+ {
+ key: 'name',
+ label: __('Name'),
+ ...defaultTableClasses,
+ },
+ {
+ key: 'duration',
+ label: __('Duration'),
+ ...defaultTableClasses,
+ },
+ {
+ key: 'coverage',
+ label: __('Coverage'),
+ ...defaultTableClasses,
+ },
+ {
+ key: 'actions',
+ label: '',
+ ...defaultTableClasses,
+ },
+ ],
+ components: {
+ GlTable,
+ },
+ props: {
+ jobs: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-table :items="jobs" :fields="$options.fields" />
+</template>
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
new file mode 100644
index 00000000000..55954e31654
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
@@ -0,0 +1,85 @@
+<script>
+import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
+import { __ } from '~/locale';
+import GetJobs from './graphql/queries/get_jobs.query.graphql';
+import JobsTable from './jobs_table.vue';
+import JobsTableTabs from './jobs_table_tabs.vue';
+
+export default {
+ i18n: {
+ errorMsg: __('There was an error fetching the jobs for your project.'),
+ },
+ components: {
+ GlAlert,
+ GlSkeletonLoader,
+ JobsTable,
+ JobsTableTabs,
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ },
+ apollo: {
+ jobs: {
+ query: GetJobs,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ };
+ },
+ update({ project }) {
+ return project?.jobs;
+ },
+ error() {
+ this.hasError = true;
+ },
+ },
+ },
+ data() {
+ return {
+ jobs: null,
+ hasError: false,
+ isAlertDismissed: false,
+ };
+ },
+ computed: {
+ shouldShowAlert() {
+ return this.hasError && !this.isAlertDismissed;
+ },
+ },
+ methods: {
+ fetchJobsByStatus(scope) {
+ this.$apollo.queries.jobs.refetch({ statuses: scope });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-alert
+ v-if="shouldShowAlert"
+ class="gl-mt-2"
+ variant="danger"
+ dismissible
+ @dismiss="isAlertDismissed = true"
+ >
+ {{ $options.i18n.errorMsg }}
+ </gl-alert>
+
+ <jobs-table-tabs @fetchJobsByStatus="fetchJobsByStatus" />
+
+ <div v-if="$apollo.loading" class="gl-mt-5">
+ <gl-skeleton-loader
+ preserve-aspect-ratio="none"
+ equal-width-lines
+ :lines="5"
+ :width="600"
+ :height="66"
+ />
+ </div>
+
+ <jobs-table v-else :jobs="jobs.nodes" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
new file mode 100644
index 00000000000..95d265fce60
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlBadge, GlTab, GlTabs } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlBadge,
+ GlTab,
+ GlTabs,
+ },
+ inject: {
+ jobCounts: {
+ default: {},
+ },
+ jobStatuses: {
+ default: {},
+ },
+ },
+ computed: {
+ tabs() {
+ return [
+ {
+ text: __('All'),
+ count: this.jobCounts.all,
+ scope: null,
+ testId: 'jobs-all-tab',
+ },
+ {
+ text: __('Pending'),
+ count: this.jobCounts.pending,
+ scope: this.jobStatuses.pending,
+ testId: 'jobs-pending-tab',
+ },
+ {
+ text: __('Running'),
+ count: this.jobCounts.running,
+ scope: this.jobStatuses.running,
+ testId: 'jobs-running-tab',
+ },
+ {
+ text: __('Finished'),
+ count: this.jobCounts.finished,
+ scope: [this.jobStatuses.success, this.jobStatuses.failed, this.jobStatuses.canceled],
+ testId: 'jobs-finished-tab',
+ },
+ ];
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-tabs>
+ <gl-tab
+ v-for="tab in tabs"
+ :key="tab.text"
+ :title-link-attributes="{ 'data-testid': tab.testId }"
+ @click="$emit('fetchJobsByStatus', tab.scope)"
+ >
+ <template #title>
+ <span>{{ tab.text }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge">{{ tab.count }}</gl-badge>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+</template>
diff --git a/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js b/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js
index 1d68ccd724d..301e0b4f7a2 100644
--- a/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js
+++ b/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js
@@ -1,7 +1,12 @@
+import { buildApiUrl } from '~/api/api_utils';
import axios from '~/lib/utils/axios_utils';
-const rootUrl = gon.relative_url_root;
+const NAMESPACE_EXISTS_PATH = '/api/:version/namespaces/:id/exists';
-export default function fetchGroupPathAvailability(groupPath) {
- return axios.get(`${rootUrl}/users/${groupPath}/suggests`);
+export default function fetchGroupPathAvailability(groupPath, parentId) {
+ const url = buildApiUrl(NAMESPACE_EXISTS_PATH).replace(':id', encodeURIComponent(groupPath));
+
+ return axios.get(url, {
+ params: { parent_id: parentId },
+ });
}
diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js
index 89dccea2812..a0ff98645fb 100644
--- a/app/assets/javascripts/pages/groups/new/group_path_validator.js
+++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js
@@ -8,6 +8,7 @@ import fetchGroupPathAvailability from './fetch_group_path_availability';
const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline';
const successInputClass = 'gl-field-success-outline';
+const parentIdSelector = 'group_parent_id';
const successMessageSelector = '.validation-success';
const pendingMessageSelector = '.validation-pending';
const unavailableMessageSelector = '.validation-error';
@@ -20,9 +21,10 @@ export default class GroupPathValidator extends InputValidator {
const container = opts.container || '';
const validateElements = document.querySelectorAll(`${container} .js-validate-group-path`);
+ const parentIdElement = document.getElementById(parentIdSelector);
this.debounceValidateInput = debounce((inputDomElement) => {
- GroupPathValidator.validateGroupPathInput(inputDomElement);
+ GroupPathValidator.validateGroupPathInput(inputDomElement, parentIdElement);
}, debounceTimeoutDuration);
validateElements.forEach((element) =>
@@ -37,13 +39,14 @@ export default class GroupPathValidator extends InputValidator {
this.debounceValidateInput(inputDomElement);
}
- static validateGroupPathInput(inputDomElement) {
+ static validateGroupPathInput(inputDomElement, parentIdElement) {
const groupPath = inputDomElement.value;
+ const parentId = parentIdElement.value;
if (inputDomElement.checkValidity() && groupPath.length > 1) {
GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector);
- fetchGroupPathAvailability(groupPath)
+ fetchGroupPathAvailability(groupPath, parentId)
.then(({ data }) => data)
.then((data) => {
GroupPathValidator.setInputState(inputDomElement, !data.exists);
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 322ad2c79e7..569b5afd676 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -5,10 +5,8 @@ import Group from '~/group';
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
import GroupPathValidator from './group_path_validator';
-const parentId = $('#group_parent_id');
-if (!parentId.val()) {
- new GroupPathValidator(); // eslint-disable-line no-new
-}
+new GroupPathValidator(); // eslint-disable-line no-new
+
BindInOut.initAll();
initFilePickers();
diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js
index 681d151b77f..75194499a7f 100644
--- a/app/assets/javascripts/pages/projects/jobs/index/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/index/index.js
@@ -1,17 +1,23 @@
import Vue from 'vue';
+import initJobsTable from '~/jobs/components/table';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-const remainingTimeElements = document.querySelectorAll('.js-remaining-time');
-remainingTimeElements.forEach(
- (el) =>
- new Vue({
- el,
- render(h) {
- return h(GlCountdown, {
- props: {
- endDateString: el.dateTime,
- },
- });
- },
- }),
-);
+if (gon.features?.jobsTableVue) {
+ initJobsTable();
+} else {
+ const remainingTimeElements = document.querySelectorAll('.js-remaining-time');
+
+ remainingTimeElements.forEach(
+ (el) =>
+ new Vue({
+ el,
+ render(h) {
+ return h(GlCountdown, {
+ props: {
+ endDateString: el.dateTime,
+ },
+ });
+ },
+ }),
+ );
+}
diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js
index f60c0759c72..49a43b120e0 100644
--- a/app/assets/javascripts/tooltips/index.js
+++ b/app/assets/javascripts/tooltips/index.js
@@ -44,10 +44,7 @@ const addTooltips = (elements, config) => {
const handleTooltipEvent = (rootTarget, e, selector, config = {}) => {
for (let { target } = e; target && target !== rootTarget; target = target.parentNode) {
if (isTooltip(target, selector)) {
- addTooltips([target], {
- show: true,
- ...config,
- });
+ addTooltips([target], config);
break;
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 84a21a25552..6d68c15cf2d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -71,11 +71,11 @@ export default {
return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).targetBranch;
},
shouldRemoveSourceBranch() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.state.shouldRemoveSourceBranch || this.state.forceRemoveSourceBranch;
- }
+ if (!this.glFeatures.mergeRequestWidgetGraphql) return this.mr.shouldRemoveSourceBranch;
+
+ if (!this.state.shouldRemoveSourceBranch) return false;
- return this.mr.shouldRemoveSourceBranch;
+ return this.state.shouldRemoveSourceBranch || this.state.forceRemoveSourceBranch;
},
autoMergeStrategy() {
return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).autoMergeStrategy;
diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss
index 33a7e8c8dda..db4be3f18e8 100644
--- a/app/assets/stylesheets/page_bundles/jira_connect.scss
+++ b/app/assets/stylesheets/page_bundles/jira_connect.scss
@@ -8,7 +8,6 @@
// We should only import styles that we actually use.
@import '@gitlab/ui/src/components/base/alert/alert';
@import '@gitlab/ui/src/components/base/avatar/avatar';
-@import '@gitlab/ui/src/components/base/badge/badge';
@import '@gitlab/ui/src/components/base/button/button';
@import '@gitlab/ui/src/components/base/icon/icon';
@import '@gitlab/ui/src/components/base/link/link';
@@ -19,22 +18,8 @@
@import '@gitlab/ui/src/components/base/tooltip/tooltip';
@import '@gitlab/ui/src/components/base/search_box_by_type/search_box_by_type';
-$atlaskit-border-color: #dfe1e6;
$header-height: 40px;
-.subscription-form {
- .field-group-input {
- display: flex;
- padding-top: $gl-padding-4;
-
- .ak-button {
- align-items: center;
- height: auto;
- margin-left: $btn-margin-5;
- }
- }
-}
-
.jira-connect-header {
min-height: $header-height;
position: fixed;
@@ -60,41 +45,3 @@ $header-height: 40px;
margin-left: auto;
margin-right: auto;
}
-
-// for external_link buttons
-svg {
- fill: currentColor;
-
- &.s16 {
- height: 16px;
- width: 16px;
- }
-}
-
-.ak-field-group label {
- text-align: left;
-}
-
-.ak-button__appearance-primary {
- &:hover {
- color: $white;
- text-decoration: none;
- }
-
- svg {
- align-self: center;
- margin-left: 4px;
- }
-}
-
-.subscriptions {
- tbody {
- tr {
- border-bottom: 1px solid $atlaskit-border-color;
- }
-
- td {
- padding: $gl-padding-8;
- }
- }
-}
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index f19a86209fc..92442fd4e28 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -15,6 +15,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :authorize_create_proxy_build!, only: :proxy_websocket_authorize
before_action :verify_proxy_request!, only: :proxy_websocket_authorize
+ before_action :push_jobs_table_vue, only: [:index]
layout 'project'
@@ -256,4 +257,8 @@ class Projects::JobsController < Projects::ApplicationController
::Gitlab::Workhorse.channel_websocket(service)
end
+
+ def push_jobs_table_vue
+ push_frontend_feature_flag(:jobs_table_vue, @project, default_enabled: :yaml)
+ end
end
diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb
index ec17eccf693..a0d169c1358 100644
--- a/app/helpers/ci/jobs_helper.rb
+++ b/app/helpers/ci/jobs_helper.rb
@@ -18,6 +18,21 @@ module Ci
"retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings', anchor: 'retry-outdated-jobs')
}
end
+
+ def job_counts
+ {
+ "all" => limited_counter_with_delimiter(@all_builds),
+ "pending" => limited_counter_with_delimiter(@all_builds.pending),
+ "running" => limited_counter_with_delimiter(@all_builds.running),
+ "finished" => limited_counter_with_delimiter(@all_builds.finished)
+ }
+ end
+
+ def job_statuses
+ statuses = Ci::HasStatus::AVAILABLE_STATUSES
+
+ statuses.to_h { |status| [status, status.upcase] }
+ end
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 393580e0554..455429608b4 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -92,6 +92,8 @@ class Namespace < ApplicationRecord
scope :for_user, -> { where('type IS NULL') }
scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
scope :include_route, -> { includes(:route) }
+ scope :by_parent, -> (parent) { where(parent_id: parent) }
+ scope :filter_by_path, -> (query) { where('lower(path) = :query', query: query.downcase) }
scope :with_statistics, -> do
joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index 769455dc951..b1a40bfc96b 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -1,7 +1,7 @@
.nav-block.activities
= render 'shared/event_filter', show_group_events: @group.supports_events?
.controls
- = link_to group_path(@group, rss_url_options), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip' , title: 'Subscribe' do
+ = link_to group_path(@group, rss_url_options), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip' , title: _('Subscribe') do
= sprite_icon('rss', css_class: 'qa-rss-icon gl-icon')
.content_list
diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml
index bc75fada937..6ba6dab96ae 100644
--- a/app/views/groups/activity.html.haml
+++ b/app/views/groups/activity.html.haml
@@ -1,5 +1,5 @@
= content_for :meta_tags do
- = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
+ = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: _("%{group_name} activity") % { group_name: @group.name })
- page_title _("Activity")
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index 52060e2be16..d4d8a7a57ef 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -4,23 +4,23 @@
.col-md-6
.form-group.row
.col-form-label.col-sm-2
- = f.label :title, "Title"
+ = f.label :title, _("Title")
.col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control", data: { qa_selector: "milestone_title_field" }, required: true, autofocus: true
.form-group.row.milestone-description
.col-form-label.col-sm-2
- = f.label :description, "Description"
+ = f.label :description, _("Description")
.col-sm-10
= render layout: 'shared/md_preview', locals: { url: group_preview_markdown_path } do
- = render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', qa_selector: 'milestone_description_field', placeholder: 'Write milestone description...', supports_autocomplete: false
+ = render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', qa_selector: 'milestone_description_field', placeholder: _('Write milestone description...'), supports_autocomplete: false
.clearfix
.error-alert
= render "shared/milestones/form_dates", f: f
.form-actions
- if @milestone.new_record?
- = f.submit 'Create milestone', class: "btn-confirm gl-button btn", data: { qa_selector: "create_milestone_button" }
- = link_to "Cancel", group_milestones_path(@group), class: "btn gl-button btn-cancel"
+ = f.submit _('Create milestone'), class: "btn-confirm gl-button btn", data: { qa_selector: "create_milestone_button" }
+ = link_to _("Cancel"), group_milestones_path(@group), class: "btn gl-button btn-cancel"
- else
- = f.submit 'Update milestone', class: "btn-confirm gl-button btn"
- = link_to "Cancel", group_milestone_path(@group, @milestone), class: "btn gl-button btn-cancel"
+ = f.submit _('Update milestone'), class: "btn-confirm gl-button btn"
+ = link_to _("Cancel"), group_milestone_path(@group, @milestone), class: "btn gl-button btn-cancel"
diff --git a/app/views/groups/milestones/edit.html.haml b/app/views/groups/milestones/edit.html.haml
index c703d5f7f93..187c2d24b56 100644
--- a/app/views/groups/milestones/edit.html.haml
+++ b/app/views/groups/milestones/edit.html.haml
@@ -4,7 +4,7 @@
- render "header_title"
%h3.page-title
- Edit Milestone
+ = _('Edit Milestone')
%hr
= render "form"
diff --git a/app/views/help/instance_configuration/_gitlab_ci.html.haml b/app/views/help/instance_configuration/_gitlab_ci.html.haml
index 7fa8bd086d4..53fa3f89873 100644
--- a/app/views/help/instance_configuration/_gitlab_ci.html.haml
+++ b/app/views/help/instance_configuration/_gitlab_ci.html.haml
@@ -1,24 +1,24 @@
- content_for :table_content do
- %li= link_to 'GitLab CI', '#gitlab-ci'
+ %li= link_to _('GitLab CI'), '#gitlab-ci'
- content_for :settings_content do
%h2#gitlab-ci
- GitLab CI
+ = _('GitLab CI')
%p
- Below are the current settings regarding
- = succeed('.') { link_to('GitLab CI', 'https://about.gitlab.com/gitlab-ci', target: '_blank') }
+ = _('Below are the current settings regarding')
+ = succeed('.') { link_to(_('GitLab CI'), 'https://about.gitlab.com/gitlab-ci', target: '_blank') }
.table-responsive
%table
%thead
%tr
- %th Setting
+ %th= _('Setting')
%th= instance_configuration_host(@instance_configuration.settings[:host])
- %th Default
+ %th= _('Default')
%tbody
%tr
- artifacts_size = @instance_configuration.settings[:gitlab_ci][:artifacts_max_size]
- %td Artifacts maximum size
+ %td= _('Artifacts maximum size')
%td= instance_configuration_human_size_cell(artifacts_size[:value])
%td= instance_configuration_human_size_cell(artifacts_size[:default])
diff --git a/app/views/profiles/gpg_keys/_form.html.haml b/app/views/profiles/gpg_keys/_form.html.haml
index a86de0681f7..9804a3b7735 100644
--- a/app/views/profiles/gpg_keys/_form.html.haml
+++ b/app/views/profiles/gpg_keys/_form.html.haml
@@ -4,7 +4,7 @@
.form-group
= f.label :key, s_('Profiles|Key'), class: 'label-bold'
- = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: _("Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'.")
+ = f.text_area :key, class: "form-control gl-form-input", rows: 8, required: true, placeholder: _("Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'.")
.gl-mt-3
= f.submit s_('Profiles|Add key'), class: "gl-button btn btn-confirm"
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index 0f9cf1c511e..f2aab3d9394 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -1,9 +1,12 @@
- page_title _("Jobs")
- add_page_specific_style 'page_bundles/ci_status'
-.top-area
- - build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) }
- = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
+- if Feature.enabled?(:jobs_table_vue, @project, default_enabled: :yaml)
+ #js-jobs-table{ data: { full_path: @project.full_path, job_counts: job_counts.to_json, job_statuses: job_statuses.to_json } }
+- else
+ .top-area
+ - build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) }
+ = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
-.content-list.builds-content-list
- = render "table", builds: @builds, project: @project
+ .content-list.builds-content-list
+ = render "table", builds: @builds, project: @project
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 92bc1874c97..416cb932ec9 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -26,21 +26,21 @@
= render "projects/merge_requests/tabs/tab", class: "notes-tab", qa_selector: "notes_tab" do
= tab_link_for @merge_request, :show, force_link: @commit.present? do
= _("Overview")
- %span.badge.badge-pill= @merge_request.related_notes.user.count
+ %span.badge.badge-pill.gl-badge.badge-muted.sm= @merge_request.related_notes.user.count
- if @merge_request.source_project
= render "projects/merge_requests/tabs/tab", name: "commits", class: "commits-tab", qa_selector: "commits_tab" do
= tab_link_for @merge_request, :commits do
= _("Commits")
- %span.badge.badge-pill= @commits_count
+ %span.badge.badge-pill.gl-badge.badge-muted.sm= @commits_count
- if number_of_pipelines.nonzero?
= render "projects/merge_requests/tabs/tab", name: "pipelines", class: "pipelines-tab" do
= tab_link_for @merge_request, :pipelines do
= _("Pipelines")
- %span.badge.badge-pill.js-pipelines-mr-count= number_of_pipelines
+ %span.badge.badge-pill.gl-badge.badge-muted.sm.js-pipelines-mr-count= number_of_pipelines
= render "projects/merge_requests/tabs/tab", name: "diffs", class: "diffs-tab", id: "diffs-tab", qa_selector: "diffs_tab" do
= tab_link_for @merge_request, :diffs do
= _("Changes")
- %span.badge.badge-pill= @merge_request.diff_size
+ %span.badge.badge-pill.gl-badge.badge-muted.sm= @merge_request.diff_size
.d-flex.flex-wrap.align-items-center.justify-content-lg-end
#js-vue-discussion-counter
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index a3d6a2c8e04..cff50eef88b 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -1,6 +1,6 @@
- type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false)
-- display_count = local_assigns.fetch(:display_count, :true)
+- display_count = local_assigns.fetch(:display_count, true)
%ul.nav-links.issues-state-filters.mobile-separator.nav.nav-tabs
%li{ class: active_when(params[:state] == 'opened') }>
diff --git a/changelogs/unreleased/215845-uncheck-delete-source-branch-does-nothing-2.yml b/changelogs/unreleased/215845-uncheck-delete-source-branch-does-nothing-2.yml
new file mode 100644
index 00000000000..a43643a9d9e
--- /dev/null
+++ b/changelogs/unreleased/215845-uncheck-delete-source-branch-does-nothing-2.yml
@@ -0,0 +1,5 @@
+---
+title: Fix delete source branch status message
+merge_request: 58605
+author:
+type: fixed
diff --git a/changelogs/unreleased/328050-honor-haml-tooltips-delay.yml b/changelogs/unreleased/328050-honor-haml-tooltips-delay.yml
new file mode 100644
index 00000000000..3da1529c1d7
--- /dev/null
+++ b/changelogs/unreleased/328050-honor-haml-tooltips-delay.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure all tooltips appear with a 500ms delay
+merge_request: 59561
+author:
+type: fixed
diff --git a/changelogs/unreleased/Externalize-strings-in-groups-_activities-html-haml.yml b/changelogs/unreleased/Externalize-strings-in-groups-_activities-html-haml.yml
new file mode 100644
index 00000000000..53604db6738
--- /dev/null
+++ b/changelogs/unreleased/Externalize-strings-in-groups-_activities-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalise strings in groups/_activities.html.haml
+merge_request: 58324
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Externalize-strings-in-groups-activity-html-haml.yml b/changelogs/unreleased/Externalize-strings-in-groups-activity-html-haml.yml
new file mode 100644
index 00000000000..f3ea68dea19
--- /dev/null
+++ b/changelogs/unreleased/Externalize-strings-in-groups-activity-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalises strings in groups/activity.html.haml
+merge_request: 58332
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Externalize-strings-in-instance_configuration-_gitlab_ci-html-haml.yml b/changelogs/unreleased/Externalize-strings-in-instance_configuration-_gitlab_ci-html-haml.yml
new file mode 100644
index 00000000000..f11db4e4f95
--- /dev/null
+++ b/changelogs/unreleased/Externalize-strings-in-instance_configuration-_gitlab_ci-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings in instance_configuration/_gitlab_ci.html.haml
+merge_request: 58435
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Externalize-strings-in-milestones-_form-html-haml.yml b/changelogs/unreleased/Externalize-strings-in-milestones-_form-html-haml.yml
new file mode 100644
index 00000000000..b856877de5a
--- /dev/null
+++ b/changelogs/unreleased/Externalize-strings-in-milestones-_form-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings in milestones/_form.html.haml
+merge_request: 58298
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Externalize-strings-in-milestones-edit-html-haml.yml b/changelogs/unreleased/Externalize-strings-in-milestones-edit-html-haml.yml
new file mode 100644
index 00000000000..27e935115af
--- /dev/null
+++ b/changelogs/unreleased/Externalize-strings-in-milestones-edit-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings in milestones/edit.html.haml
+merge_request: 58306
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/align-center-subproject.yml b/changelogs/unreleased/align-center-subproject.yml
new file mode 100644
index 00000000000..96854a752a2
--- /dev/null
+++ b/changelogs/unreleased/align-center-subproject.yml
@@ -0,0 +1,5 @@
+---
+title: Align project stars and date to center of project in groups page
+merge_request: 57972
+author: Yogi (@yo)
+type: changed
diff --git a/changelogs/unreleased/fix-namespace-existence-check.yml b/changelogs/unreleased/fix-namespace-existence-check.yml
new file mode 100644
index 00000000000..6331b3c3f63
--- /dev/null
+++ b/changelogs/unreleased/fix-namespace-existence-check.yml
@@ -0,0 +1,5 @@
+---
+title: Fix namespace validation (unique path) on group creation
+merge_request: 57563
+author: Jonas Wälter @wwwjon
+type: fixed
diff --git a/changelogs/unreleased/gl-badge-mr-nav.yml b/changelogs/unreleased/gl-badge-mr-nav.yml
new file mode 100644
index 00000000000..f8b8f5e537d
--- /dev/null
+++ b/changelogs/unreleased/gl-badge-mr-nav.yml
@@ -0,0 +1,5 @@
+---
+title: Add gl-badge for badges in MR page nav
+merge_request: 57969
+author: Yogi (@yo)
+type: changed
diff --git a/changelogs/unreleased/gl-form-gpg.yml b/changelogs/unreleased/gl-form-gpg.yml
new file mode 100644
index 00000000000..c0ac79e168e
--- /dev/null
+++ b/changelogs/unreleased/gl-form-gpg.yml
@@ -0,0 +1,5 @@
+---
+title: Apply gl-form-input for fields in GPG keys page
+merge_request: 58002
+author: Yogi (@yo)
+type: changed
diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-helpers.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-helpers.yml
new file mode 100644
index 00000000000..2b9b15425c1
--- /dev/null
+++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-helpers.yml
@@ -0,0 +1,5 @@
+---
+title: Fix EmptyLineAfterFinalLetItBe Rubocop offenses for helpers
+merge_request: 58192
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-boards.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-boards.yml
new file mode 100644
index 00000000000..5d273baeffc
--- /dev/null
+++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-boards.yml
@@ -0,0 +1,5 @@
+---
+title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/boards
+merge_request: 58413
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/config/feature_flags/development/jobs_table_vue.yml b/config/feature_flags/development/jobs_table_vue.yml
new file mode 100644
index 00000000000..ef6fda61736
--- /dev/null
+++ b/config/feature_flags/development/jobs_table_vue.yml
@@ -0,0 +1,8 @@
+---
+name: jobs_table_vue
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57155
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327500
+milestone: '13.11'
+type: development
+group: group::continuous integration
+default_enabled: false
diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile
index 4507f41b798..6f045d7b5ab 100644
--- a/danger/changelog/Dangerfile
+++ b/danger/changelog/Dangerfile
@@ -74,3 +74,26 @@ elsif changelog.required?
elsif changelog.optional?
message changelog.optional_text
end
+
+message <<~MSG
+ We are in the process of rolling out a new workflow for adding changelog entries. This new workflow uses Git commit subjects and Git trailers to generate changelogs. This new approach will soon replace the current YAML based approach.
+
+ To ease the transition process, we recommend you start using both the old and new approach in parallel. This is not required at this time, but will make it easier to transition to the new approach in the future. To do so, pick the commit that should go in the changelog and add a `Changelog` trailer to it. For example:
+
+ ```
+ This is my commit's subject line
+
+ This is the optional commit body.
+
+ Changelog: added
+ ```
+
+ The value of the `Changelog` trailer should be one of the following: added, fixed, changed, deprecated, removed, security, performance, other.
+
+ For more information, take a look at the following resources:
+
+ - https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/1564
+ - https://docs.gitlab.com/ee/api/repositories.html#generate-changelog-data
+
+ If you'd like to see the new approach in action, take a look at the commits in [the Omnibus repository](https://gitlab.com/gitlab-org/omnibus-gitlab/-/commits/master).
+MSG
diff --git a/doc/administration/restart_gitlab.md b/doc/administration/restart_gitlab.md
index 69b3ae5282f..f4cc98ca145 100644
--- a/doc/administration/restart_gitlab.md
+++ b/doc/administration/restart_gitlab.md
@@ -4,18 +4,11 @@ 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
---
-# How to restart GitLab
+# How to restart GitLab **(FREE SELF)**
Depending on how you installed GitLab, there are different methods to restart
its service(s).
-If you want the TL;DR versions, jump to:
-
-- [Omnibus GitLab restart](#omnibus-gitlab-restart)
-- [Omnibus GitLab reconfigure](#omnibus-gitlab-reconfigure)
-- [Source installation restart](#installations-from-source)
-- [Helm chart installation restart](#helm-chart-installations)
-
## Omnibus installations
If you have used the [Omnibus packages](https://about.gitlab.com/install/) to install GitLab, then
diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index ce07bdd2966..b0d03ebad74 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -4,7 +4,7 @@ group: unassigned
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
---
-# API resources
+# API resources **(FREE)**
Available resources for the [GitLab API](README.md) can be grouped in the following contexts:
@@ -45,13 +45,13 @@ The following API resources are available in the project context:
| [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) |
| [Issues Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) |
| [Issue boards](boards.md) | `/projects/:id/boards` |
-| [Issue links](issue_links.md) **(STARTER)** | `/projects/:id/issues/.../links` |
-| [Iterations](iterations.md) **(STARTER)** | `/projects/:id/iterations` (also available for groups) |
+| [Issue links](issue_links.md). | `/projects/:id/issues/.../links` |
+| [Iterations](iterations.md) **(PREMIUM)** | `/projects/:id/iterations` (also available for groups) |
| [Jobs](jobs.md) | `/projects/:id/jobs`, `/projects/:id/pipelines/.../jobs` |
| [Labels](labels.md) | `/projects/:id/labels` |
| [Managed licenses](managed_licenses.md) **(ULTIMATE)** | `/projects/:id/managed_licenses` |
| [Members](members.md) | `/projects/:id/members` (also available for groups) |
-| [Merge request approvals](merge_request_approvals.md) **(STARTER)** | `/projects/:id/approvals`, `/projects/:id/merge_requests/.../approvals` |
+| [Merge request approvals](merge_request_approvals.md) **(PREMIUM)** | `/projects/:id/approvals`, `/projects/:id/merge_requests/.../approvals` |
| [Merge requests](merge_requests.md) | `/projects/:id/merge_requests` (also available for groups and standalone) |
| [Merge trains](merge_trains.md) | `/projects/:id/merge_trains` |
| [Notes](notes.md) (comments) | `/projects/:id/issues/.../notes`, `/projects/:id/snippets/.../notes`, `/projects/:id/merge_requests/.../notes` (also available for groups) |
@@ -84,7 +84,7 @@ The following API resources are available in the project context:
| [Services](services.md) | `/projects/:id/services` |
| [Tags](tags.md) | `/projects/:id/repository/tags` |
| [User-starred metrics dashboards](metrics_user_starred_dashboards.md ) | `/projects/:id/metrics/user_starred_dashboards` |
-| [Visual Review discussions](visual_review_discussions.md) **(STARTER)** | `/projects/:id/merge_requests/:merge_request_id/visual_review_discussions` |
+| [Visual Review discussions](visual_review_discussions.md) **(PREMIUM)** | `/projects/:id/merge_requests/:merge_request_id/visual_review_discussions` |
| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/vulnerabilities/:id` |
| [Vulnerability exports](vulnerability_exports.md) **(ULTIMATE)** | `/projects/:id/vulnerability_exports` |
| [Project vulnerabilities](project_vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities` |
@@ -106,7 +106,7 @@ The following API resources are available in the group context:
| [Groups](groups.md) | `/groups`, `/groups/.../subgroups` |
| [Group badges](group_badges.md) | `/groups/:id/badges` |
| [Group issue boards](group_boards.md) | `/groups/:id/boards` |
-| [Group iterations](group_iterations.md) **(STARTER)** | `/groups/:id/iterations` (also available for projects) |
+| [Group iterations](group_iterations.md) **(PREMIUM)** | `/groups/:id/iterations` (also available for projects) |
| [Group labels](group_labels.md) | `/groups/:id/labels` |
| [Group-level variables](group_level_variables.md) | `/groups/:id/variables` |
| [Group milestones](group_milestones.md) | `/groups/:id/milestones` |
@@ -140,7 +140,7 @@ The following API resources are available outside of project and group contexts
| [Events](events.md) | `/events`, `/users/:id/events` (also available for projects) |
| [Feature flags](features.md) | `/features` |
| [Geo Nodes](geo_nodes.md) **(PREMIUM SELF)** | `/geo_nodes` |
-| [Group Activity Analytics](group_activity_analytics.md) **(STARTER)** | `/analytics/group_activity/{issues_count | merge_requests_count | new_members_count }` |
+| [Group Activity Analytics](group_activity_analytics.md) | `/analytics/group_activity/{issues_count | merge_requests_count | new_members_count }` |
| [Group repository storage moves](group_repository_storage_moves.md) **(PREMIUM SELF)** | `/group_repository_storage_moves` |
| [Import repository from GitHub](import.md) | `/import/github` |
| [Instance clusters](instance_clusters.md) | `/admin/clusters` |
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 08f47e7fceb..a2547b9a691 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -397,7 +397,7 @@ Returns [`VulnerabilityConnection`](#vulnerabilityconnection).
| `projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
| `reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. |
| `scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. |
-| `scannerId` | [`[Int!]`](#int) | Filter vulnerabilities by scanner ID. |
+| `scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. |
| `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. |
| `sort` | [`VulnerabilitySort`](#vulnerabilitysort) | List vulnerabilities by sort order. |
| `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. |
@@ -9156,6 +9156,12 @@ A `VulnerabilitiesExternalIssueLinkID` is a global ID. It is encoded as a string
An example `VulnerabilitiesExternalIssueLinkID` is: `"gid://gitlab/Vulnerabilities::ExternalIssueLink/1"`.
+### `VulnerabilitiesScannerID`
+
+A `VulnerabilitiesScannerID` is a global ID. It is encoded as a string.
+
+An example `VulnerabilitiesScannerID` is: `"gid://gitlab/Vulnerabilities::Scanner/1"`.
+
### `VulnerabilityID`
A `VulnerabilityID` is a global ID. It is encoded as a string.
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index a89e91f82e5..c3e88532430 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -225,3 +225,33 @@ Example response:
"trial": false
}
```
+
+## Get existence of a namespace
+
+Get existence of a namespace by path. Suggests a new namespace path that does not already exist.
+
+```plaintext
+GET /namespaces/:namespace/exists
+```
+
+| Attribute | Type | Required | Description |
+| ----------- | ------- | -------- | ----------- |
+| `namespace` | string | yes | Namespace's path. |
+| `parent_id` | integer | no | The ID of the parent namespace. If no ID is specified, only top-level namespaces are considered. |
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/namespaces/my-group/exists?parent_id=1"
+```
+
+Example response:
+
+```json
+{
+ "exists": true,
+ "suggests": [
+ "my-group1"
+ ]
+}
+```
diff --git a/doc/api/users.md b/doc/api/users.md
index 86f1548dac4..0e4012935f9 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -645,6 +645,7 @@ Example response:
```json
{
"emoji":"coffee",
+ "availability":"busy",
"message":"I crave coffee :coffee:",
"message_html": "I crave coffee <gl-emoji title=\"hot beverage\" data-name=\"coffee\" data-unicode-version=\"4.0\">☕</gl-emoji>",
"clear_status_at": null
@@ -672,6 +673,7 @@ Example response:
```json
{
"emoji":"coffee",
+ "availability":"busy",
"message":"I crave coffee :coffee:",
"message_html": "I crave coffee <gl-emoji title=\"hot beverage\" data-name=\"coffee\" data-unicode-version=\"4.0\">☕</gl-emoji>",
"clear_status_at": null
diff --git a/doc/development/distributed_tracing.md b/doc/development/distributed_tracing.md
index 4f53d6b7385..e5293c0804c 100644
--- a/doc/development/distributed_tracing.md
+++ b/doc/development/distributed_tracing.md
@@ -157,7 +157,7 @@ This should start the process with the default listening ports.
Once you have Jaeger running, configure the `GITLAB_TRACING` variable with the
appropriate configuration string.
-**TL;DR:** If you are running everything on the same host, use the following value:
+If you're running everything on the same host, use the following value:
```shell
export GITLAB_TRACING="opentracing://jaeger?http_endpoint=http%3A%2F%2Flocalhost%3A14268%2Fapi%2Ftraces&sampler=const&sampler_param=1"
diff --git a/doc/development/geo/framework.md b/doc/development/geo/framework.md
index 071d6aa0456..2b3e4826de5 100644
--- a/doc/development/geo/framework.md
+++ b/doc/development/geo/framework.md
@@ -160,868 +160,16 @@ the Geo team if you are unsure.
### Blob Replicator Strategy
-Models that use
-[CarrierWave's](https://github.com/carrierwaveuploader/carrierwave) `Uploader::Base`
-can be easily supported by Geo with the `Geo::BlobReplicatorStrategy` module.
+Models that use [CarrierWave's](https://github.com/carrierwaveuploader/carrierwave) `Uploader::Base` are supported by Geo with the `Geo::BlobReplicatorStrategy` module. For example, see how [Geo replication was implemented for Pipeline Artifacts](https://gitlab.com/gitlab-org/gitlab/-/issues/238464).
-First, each file should have its own primary ID and model. Geo strongly
-recommends treating *every single file* as a first-class citizen, because in
-our experience this greatly simplifies tracking replication and verification
-state.
+Each file is expected to have its own primary ID and model. Geo strongly recommends treating *every single file* as a first-class citizen, because in our experience this greatly simplifies tracking replication and verification state.
-For example, to add support for files referenced by a `Widget` model with a
-`widgets` table, you would perform the following steps:
-
-#### Replication
-
-1. Include `Gitlab::Geo::ReplicableModel` in the `Widget` class, and specify
- the Replicator class `with_replicator Geo::WidgetReplicator`.
-
- At this point the `Widget` class should look like this:
-
- ```ruby
- # frozen_string_literal: true
-
- class Widget < ApplicationRecord
- include ::Gitlab::Geo::ReplicableModel
-
- with_replicator Geo::WidgetReplicator
-
- mount_uploader :file, WidgetUploader
-
- # @param primary_key_in [Range, Widget] arg to pass to primary_key_in scope
- # @return [ActiveRecord::Relation<Widget>] everything that should be synced to this node, restricted by primary key
- def self.replicables_for_current_secondary(primary_key_in)
- # Should be implemented. The idea of the method is to restrict
- # the set of synced items depending on synchronization settings
- end
- ...
- end
- ```
-
- If there is a common constraint for records to be available for replication,
- make sure to also overwrite the `available_replicables` scope.
-
-1. Create `ee/app/replicators/geo/widget_replicator.rb`. Implement the
- `#carrierwave_uploader` method which should return a `CarrierWave::Uploader`,
- and implement the class method `.model` to return the `Widget` class:
-
- ```ruby
- # frozen_string_literal: true
-
- module Geo
- class WidgetReplicator < Gitlab::Geo::Replicator
- include ::Geo::BlobReplicatorStrategy
-
- def self.model
- ::Widget
- end
-
- def carrierwave_uploader
- model_record.file
- end
-
- # The feature flag follows the format `geo_#{replicable_name}_replication`,
- # so here it would be `geo_widget_replication`
- def self.replication_enabled_by_default?
- false
- end
- end
- end
- ```
-
-1. Add this replicator class to the method `replicator_classes` in
- `ee/lib/gitlab/geo.rb`:
-
- ```ruby
- REPLICATOR_CLASSES = [
- ::Geo::PackageFileReplicator,
- ::Geo::WidgetReplicator
- ]
- end
- ```
-
-1. Create `ee/spec/replicators/geo/widget_replicator_spec.rb` and perform
- the necessary setup to define the `model_record` variable for the shared
- examples:
-
- ```ruby
- # frozen_string_literal: true
-
- require 'spec_helper'
-
- RSpec.describe Geo::WidgetReplicator do
- let(:model_record) { build(:widget) }
-
- it_behaves_like 'a blob replicator'
- end
- ```
-
-1. Create the `widget_registry` table, with columns ordered according to [our guidelines](../ordering_table_columns.md) so Geo secondaries can track the sync and
- verification state of each Widget's file. This migration belongs in `ee/db/geo/migrate`:
-
- ```ruby
- # frozen_string_literal: true
-
- class CreateWidgetRegistry < ActiveRecord::Migration[6.0]
- include Gitlab::Database::MigrationHelpers
-
- disable_ddl_transaction!
-
- def up
- unless table_exists?(:widget_registry)
- ActiveRecord::Base.transaction do
- create_table :widget_registry, id: :bigserial, force: :cascade do |t|
- t.bigint :widget_id, null: false
- t.datetime_with_timezone :created_at, null: false
- t.datetime_with_timezone :last_synced_at
- t.datetime_with_timezone :retry_at
- t.datetime_with_timezone :verified_at
- t.datetime_with_timezone :verification_started_at
- t.datetime_with_timezone :verification_retry_at
- t.integer :state, default: 0, null: false, limit: 2
- t.integer :verification_state, default: 0, null: false, limit: 2
- t.integer :retry_count, default: 0, limit: 2
- t.integer :verification_retry_count, default: 0, limit: 2
- t.boolean :checksum_mismatch, default: false, null: false
- t.binary :verification_checksum
- t.binary :verification_checksum_mismatched
- t.string :verification_failure, limit: 255
- t.string :last_sync_failure, limit: 255
-
- t.index :widget_id, name: :index_widget_registry_on_widget_id, unique: true
- t.index :retry_at
- t.index :state
- # To optimize performance of WidgetRegistry.verification_failed_batch
- t.index :verification_retry_at, name: :widget_registry_failed_verification, order: "NULLS FIRST", where: "((state = 2) AND (verification_state = 3))"
- # To optimize performance of WidgetRegistry.needs_verification_count
- t.index :verification_state, name: :widget_registry_needs_verification, where: "((state = 2) AND (verification_state = ANY (ARRAY[0, 3])))"
- # To optimize performance of WidgetRegistry.verification_pending_batch
- t.index :verified_at, name: :widget_registry_pending_verification, order: "NULLS FIRST", where: "((state = 2) AND (verification_state = 0))"
- end
- end
- end
- end
-
- def down
- drop_table :widget_registry
- end
- end
- ```
-
-1. Create `ee/app/models/geo/widget_registry.rb`:
-
- ```ruby
- # frozen_string_literal: true
-
- class Geo::WidgetRegistry < Geo::BaseRegistry
- include ::Geo::ReplicableRegistry
- include ::Geo::VerifiableRegistry
-
- MODEL_CLASS = ::Widget
- MODEL_FOREIGN_KEY = :widget_id
-
- belongs_to :widget, class_name: 'Widget'
- end
- ```
-
-1. Update `REGISTRY_CLASSES` in `ee/app/workers/geo/secondary/registry_consistency_worker.rb`.
-1. Add `widget_registry` to `ActiveSupport::Inflector.inflections` in `config/initializers_before_autoloader/000_inflections.rb`.
-1. Create `ee/spec/factories/geo/widget_registry.rb`:
-
- ```ruby
- # frozen_string_literal: true
-
- FactoryBot.define do
- factory :geo_widget_registry, class: 'Geo::WidgetRegistry' do
- widget
- state { Geo::WidgetRegistry.state_value(:pending) }
-
- trait :synced do
- state { Geo::WidgetRegistry.state_value(:synced) }
- last_synced_at { 5.days.ago }
- end
-
- trait :failed do
- state { Geo::WidgetRegistry.state_value(:failed) }
- last_synced_at { 1.day.ago }
- retry_count { 2 }
- last_sync_failure { 'Random error' }
- end
-
- trait :started do
- state { Geo::WidgetRegistry.state_value(:started) }
- last_synced_at { 1.day.ago }
- retry_count { 0 }
- end
- end
- end
- ```
-
-1. Create `ee/spec/models/geo/widget_registry_spec.rb`:
-
- ```ruby
- # frozen_string_literal: true
-
- require 'spec_helper'
-
- RSpec.describe Geo::WidgetRegistry, :geo, type: :model do
- let_it_be(:registry) { create(:geo_widget_registry) }
-
- specify 'factory is valid' do
- expect(registry).to be_valid
- end
-
- include_examples 'a Geo framework registry'
- include_examples 'a Geo verifiable registry'
-
- describe '.find_registry_differences' do
- ... # To be implemented
- end
- end
- ```
-
-Widgets should now be replicated by Geo.
-
-#### Verification
-
-There are two ways to add verification related fields so that the Geo primary
-can track verification state.
-
-##### Option 1: Add verification state fields to the existing `widgets` table itself
-
-1. Add a migration to add columns ordered according to [our guidelines](../ordering_table_columns.md)
- for verification state to the widgets table:
-
- ```ruby
- # frozen_string_literal: true
-
- class AddVerificationStateToWidgets < ActiveRecord::Migration[6.0]
- def change
- change_table(:widgets) do |t|
- t.integer :verification_state, default: 0, limit: 2, null: false
- t.column :verification_started_at, :datetime_with_timezone
- t.integer :verification_retry_count, limit: 2
- t.column :verification_retry_at, :datetime_with_timezone
- t.column :verified_at, :datetime_with_timezone
- t.binary :verification_checksum, using: 'verification_checksum::bytea'
-
- # rubocop:disable Migration/AddLimitToTextColumns
- t.text :verification_failure
- # rubocop:enable Migration/AddLimitToTextColumns
- end
- end
- end
- ```
-
-1. Adding a `text` column also [requires](../database/strings_and_the_text_data_type.md#add-a-text-column-to-an-existing-table)
- setting a limit:
-
- ```ruby
- # frozen_string_literal: true
-
- class AddVerificationFailureLimitToWidgets < ActiveRecord::Migration[6.0]
- include Gitlab::Database::MigrationHelpers
-
- disable_ddl_transaction!
-
- CONSTRAINT_NAME = 'widget_verification_failure_text_limit'
-
- def up
- add_text_limit :widget, :verification_failure, 255, constraint_name: CONSTRAINT_NAME
- end
-
- def down
- remove_check_constraint(:widget, CONSTRAINT_NAME)
- end
- end
- ```
-
-1. Add indexes on verification fields to ensure verification can be performed efficiently:
-
- Some or all of these indexes can be omitted if the table is guaranteed to be small. Ask a database expert if you are unsure.
-
- ```ruby
- # frozen_string_literal: true
-
- class AddVerificationIndexesToWidgets < ActiveRecord::Migration[6.0]
- include Gitlab::Database::MigrationHelpers
-
- PENDING_VERIFICATION_INDEX_NAME = "index_widgets_pending_verification"
- FAILED_VERIFICATION_INDEX_NAME = "index_widgets_failed_verification"
- NEEDS_VERIFICATION_INDEX_NAME = "index_widgets_needs_verification"
-
- disable_ddl_transaction!
-
- def up
- add_concurrent_index :widgets, :verification_state, name: VERIFICATION_STATE_INDEX_NAME
- add_concurrent_index :widgets, :verified_at, where: "(verification_state = 0)", order: { verified_at: 'ASC NULLS FIRST' }, name: PENDING_VERIFICATION_INDEX_NAME
- add_concurrent_index :widgets, :verification_retry_at, where: "(verification_state = 3)", order: { verification_retry_at: 'ASC NULLS FIRST' }, name: FAILED_VERIFICATION_INDEX_NAME
- add_concurrent_index :widgets, :verification_state, where: "(verification_state = 0 OR verification_state = 3)", name: NEEDS_VERIFICATION_INDEX_NAME
- end
-
- def down
- remove_concurrent_index_by_name :widgets, VERIFICATION_STATE_INDEX_NAME
- remove_concurrent_index_by_name :widgets, PENDING_VERIFICATION_INDEX_NAME
- remove_concurrent_index_by_name :widgets, FAILED_VERIFICATION_INDEX_NAME
- remove_concurrent_index_by_name :widgets, NEEDS_VERIFICATION_INDEX_NAME
- end
- end
- ```
-
-1. Add the `Gitlab::Geo::VerificationState` concern to the `widget` model if it is not already included in `Gitlab::Geo::ReplicableModel`:
-
- ```ruby
- class Widget < ApplicationRecord
- ...
- include ::Gitlab::Geo::VerificationState
- ...
- end
- ```
-
-##### Option 2: Create a separate `widget_states` table with verification state fields
-
-1. Create a `widget_states` table and add an index on `verification_state` to ensure verification can be performed efficiently. Order the columns according to [the guidelines](../ordering_table_columns.md):
-
- ```ruby
- # frozen_string_literal: true
-
- class CreateWidgetStates < ActiveRecord::Migration[6.0]
- include Gitlab::Database::MigrationHelpers
-
- disable_ddl_transaction!
-
- def up
- unless table_exists?(:widget_states)
- with_lock_retries do
- create_table :widget_states, id: false do |t|
- t.references :widget, primary_key: true, null: false, foreign_key: { on_delete: :cascade }
- t.integer :verification_state, default: 0, limit: 2, null: false
- t.column :verification_started_at, :datetime_with_timezone
- t.datetime_with_timezone :verification_retry_at
- t.datetime_with_timezone :verified_at
- t.integer :verification_retry_count, limit: 2
- t.binary :verification_checksum, using: 'verification_checksum::bytea'
- t.text :verification_failure
-
- t.index :verification_state, name: "index_widget_states_on_verification_state"
- end
- end
- end
-
- add_text_limit :widget_states, :verification_failure, 255
- end
-
- def down
- drop_table :widget_states
- end
- end
- ```
-
-1. Add the following lines to the `widget_state.rb` model:
-
- ```ruby
- class WidgetState < ApplicationRecord
- ...
- self.primary_key = :widget_id
-
- include ::Gitlab::Geo::VerificationState
-
- belongs_to :widget, inverse_of: :widget_state
- ...
- end
- ```
-
-1. Add the following lines to the `widget` model:
-
- ```ruby
- class Widget < ApplicationRecord
- ...
- has_one :widget_state, inverse_of: :widget
-
- delegate :verification_retry_at, :verification_retry_at=,
- :verified_at, :verified_at=,
- :verification_checksum, :verification_checksum=,
- :verification_failure, :verification_failure=,
- :verification_retry_count, :verification_retry_count=,
- to: :widget_state
- ...
- end
- ```
-
-To do: Add verification on secondaries. This should be done as part of
-[Geo: Self Service Framework - First Implementation for Package File verification](https://gitlab.com/groups/gitlab-org/-/epics/1817)
-
-Widgets should now be verified by Geo.
-
-#### Metrics
-
-Metrics are gathered by `Geo::MetricsUpdateWorker`, persisted in
-`GeoNodeStatus` for display in the UI, and sent to Prometheus:
-
-1. Add fields `widgets_count`, `widgets_checksummed_count`,
- `widgets_checksum_failed_count`, `widgets_synced_count`,
- `widgets_failed_count`, and `widgets_registry_count` to
- `GET /geo_nodes/status` example response in
- `doc/api/geo_nodes.md`.
-1. Add the same fields to `GET /geo_nodes/status` example response in
- `ee/spec/fixtures/api/schemas/public_api/v4/geo_node_status.json`.
-1. Add fields `geo_widgets`, `geo_widgets_checksummed`,
- `geo_widgets_checksum_failed`, `geo_widgets_synced`,
- `geo_widgets_failed`, and `geo_widgets_registry` to
- `Sidekiq metrics` table in
- `doc/administration/monitoring/prometheus/gitlab_metrics.md`.
-1. Add the following to the parameterized table in
- `ee/spec/models/geo_node_status_spec.rb`:
-
- ```ruby
- Geo::WidgetReplicator | :widget | :geo_widget_registry
- ```
-
-1. Add the following to `spec/factories/widgets.rb`:
-
- ```ruby
- trait(:verification_succeeded) do
- with_file
- verification_checksum { 'abc' }
- verification_state { Widget.verification_state_value(:verification_succeeded) }
- end
-
- trait(:verification_failed) do
- with_file
- verification_failure { 'Could not calculate the checksum' }
- verification_state { Widget.verification_state_value(:verification_failed) }
- end
- ```
-
-1. Make sure the factory also allows setting a `project` attribute. If the model
- does not have a direct relation to a project, you can use a `transient`
- attribute. Check out `spec/factories/merge_request_diffs.rb` for an example.
-
-Widget replication and verification metrics should now be available in the API,
-the Admin Area UI, and Prometheus.
-
-#### GraphQL API
-
-1. Add a new field to `GeoNodeType` in
- `ee/app/graphql/types/geo/geo_node_type.rb`:
-
- ```ruby
- field :widget_registries, ::Types::Geo::WidgetRegistryType.connection_type,
- null: true,
- resolver: ::Resolvers::Geo::WidgetRegistriesResolver,
- description: 'Find widget registries on this Geo node',
- feature_flag: :geo_widget_replication
- ```
-
-1. Add the new `widget_registries` field name to the `expected_fields` array in
- `ee/spec/graphql/types/geo/geo_node_type_spec.rb`.
-1. Create `ee/app/graphql/resolvers/geo/widget_registries_resolver.rb`:
-
- ```ruby
- # frozen_string_literal: true
-
- module Resolvers
- module Geo
- class WidgetRegistriesResolver < BaseResolver
- include RegistriesResolver
- end
- end
- end
- ```
-
-1. Create `ee/spec/graphql/resolvers/geo/widget_registries_resolver_spec.rb`:
-
- ```ruby
- # frozen_string_literal: true
-
- require 'spec_helper'
-
- RSpec.describe Resolvers::Geo::WidgetRegistriesResolver do
- it_behaves_like 'a Geo registries resolver', :geo_widget_registry
- end
- ```
-
-1. Create `ee/app/finders/geo/widget_registry_finder.rb`:
-
- ```ruby
- # frozen_string_literal: true
-
- module Geo
- class WidgetRegistryFinder
- include FrameworkRegistryFinder
- end
- end
- ```
-
-1. Create `ee/spec/finders/geo/widget_registry_finder_spec.rb`:
-
- ```ruby
- # frozen_string_literal: true
-
- require 'spec_helper'
-
- RSpec.describe Geo::WidgetRegistryFinder do
- it_behaves_like 'a framework registry finder', :geo_widget_registry
- end
- ```
-
-1. Create `ee/app/graphql/types/geo/widget_registry_type.rb`:
-
- ```ruby
- # frozen_string_literal: true
-
- module Types
- module Geo
- # rubocop:disable Graphql/AuthorizeTypes because it is included
- class WidgetRegistryType < BaseObject
- include ::Types::Geo::RegistryType
-
- graphql_name 'WidgetRegistry'
- description 'Represents the Geo sync and verification state of a widget'
-
- field :widget_id, GraphQL::ID_TYPE, null: false, description: 'ID of the Widget'
- end
- end
- end
- ```
-
-1. Create `ee/spec/graphql/types/geo/widget_registry_type_spec.rb`:
-
- ```ruby
- # frozen_string_literal: true
-
- require 'spec_helper'
-
- RSpec.describe GitlabSchema.types['WidgetRegistry'] do
- it_behaves_like 'a Geo registry type'
-
- it 'has the expected fields (other than those included in RegistryType)' do
- expected_fields = %i[widget_id]
-
- expect(described_class).to have_graphql_fields(*expected_fields).at_least
- end
- end
- ```
-
-1. Add integration tests for providing Widget registry data to the frontend via
- the GraphQL API, by duplicating and modifying the following shared examples
- in `ee/spec/requests/api/graphql/geo/registries_spec.rb`:
-
- ```ruby
- it_behaves_like 'gets registries for', {
- field_name: 'widgetRegistries',
- registry_class_name: 'WidgetRegistry',
- registry_factory: :geo_widget_registry,
- registry_foreign_key_field_name: 'widgetId'
- }
- ```
-
-1. Update the GraphQL reference documentation:
-
- ```shell
- bundle exec rake gitlab:graphql:compile_docs
- ```
-
-Individual widget synchronization and verification data should now be available
-via the GraphQL API.
-
-Make sure to replicate the "update" events. Geo Framework does not currently support
-replicating "update" events because all entities added to the framework, by this time,
-are immutable. If this is the case
-for the entity you're going to add, follow <https://gitlab.com/gitlab-org/gitlab/-/issues/118743>
-and <https://gitlab.com/gitlab-org/gitlab/-/issues/118745> as examples to add the new event type.
-Also, remove this notice when you've added it.
-
-#### Admin UI
-
-To do: This should be done as part of
-[Geo: Implement frontend for Self-Service Framework replicables](https://gitlab.com/groups/gitlab-org/-/epics/2525)
-
-Widget sync and verification data (aggregate and individual) should now be
-available in the Admin UI.
-
-#### Releasing the feature
-
-1. In `ee/config/feature_flags/development/geo_widget_replication.yml`, set `default_enabled: true`
-
-1. In `ee/app/replicators/geo/widget_replicator.rb`, delete the `self.replication_enabled_by_default?` method:
-
- ```ruby
- module Geo
- class WidgetReplicator < Gitlab::Geo::Replicator
- ...
-
- # REMOVE THIS METHOD
- def self.replication_enabled_by_default?
- false
- end
- # REMOVE THIS METHOD
-
- ...
- end
- end
- ```
-
-1. In `ee/app/graphql/types/geo/geo_node_type.rb`, remove the `feature_flag` option for the released type:
-
- ```ruby
- field :widget_registries, ::Types::Geo::WidgetRegistryType.connection_type,
- null: true,
- resolver: ::Resolvers::Geo::WidgetRegistriesResolver,
- description: 'Find widget registries on this Geo node',
- feature_flag: :geo_widget_replication # REMOVE THIS LINE
- ```
+To implement Geo replication of a new blob-type Model, [open an issue with the provided issue template](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Geo%3A%20Replicate%20a%20new%20blob%20type).
### Repository Replicator Strategy
-Models that refer to any repository on the disk
-can be easily supported by Geo with the `Geo::RepositoryReplicatorStrategy` module.
-
-For example, to add support for files referenced by a `Gizmos` model with a
-`gizmos` table, you would perform the following steps.
-
-#### Replication
-
-1. Include `Gitlab::Geo::ReplicableModel` in the `Gizmo` class, and specify
- the Replicator class `with_replicator Geo::GizmoReplicator`.
-
- At this point the `Gizmo` class should look like this:
-
- ```ruby
- # frozen_string_literal: true
-
- class Gizmo < ApplicationRecord
- include ::Gitlab::Geo::ReplicableModel
-
- with_replicator Geo::GizmoReplicator
-
- # @param primary_key_in [Range, Gizmo] arg to pass to primary_key_in scope
- # @return [ActiveRecord::Relation<Gizmo>] everything that should be synced to this node, restricted by primary key
- def self.replicables_for_current_secondary(primary_key_in)
- # Should be implemented. The idea of the method is to restrict
- # the set of synced items depending on synchronization settings
- end
-
- # Geo checks this method in FrameworkRepositorySyncService to avoid
- # snapshotting repositories using object pools
- def pool_repository
- nil
- end
- ...
- end
- ```
-
- Pay some attention to method `pool_repository`. Not every repository type uses
- repository pooling. As Geo prefers to use repository snapshotting, it can lead to data loss.
- Make sure to overwrite `pool_repository` so it returns nil for repositories that do not
- have pools.
-
- If there is a common constraint for records to be available for replication,
- make sure to also overwrite the `available_replicables` scope.
-
-1. Create `ee/app/replicators/geo/gizmo_replicator.rb`. Implement the
- `#repository` method which should return a `<Repository>` instance,
- and implement the class method `.model` to return the `Gizmo` class:
-
- ```ruby
- # frozen_string_literal: true
-
- module Geo
- class GizmoReplicator < Gitlab::Geo::Replicator
- include ::Geo::RepositoryReplicatorStrategy
-
- def self.model
- ::Gizmo
- end
-
- def repository
- model_record.repository
- end
-
- def self.git_access_class
- ::Gitlab::GitAccessGizmo
- end
-
- # The feature flag follows the format `geo_#{replicable_name}_replication`,
- # so here it would be `geo_gizmo_replication`
- def self.replication_enabled_by_default?
- false
- end
- end
- end
- ```
-
-1. Generate the feature flag definition file by running the feature flag command
- and running through the steps:
-
- ```shell
- bin/feature-flag --ee geo_gizmo_replication --type development --group 'group::geo'
- ```
-
-1. Make sure Geo push events are created. Usually it needs some
- change in the `app/workers/post_receive.rb` file. Example:
-
- ```ruby
- def replicate_gizmo_changes(gizmo)
- if ::Gitlab::Geo.primary?
- gizmo.replicator.handle_after_update if gizmo
- end
- end
- ```
-
- See `app/workers/post_receive.rb` for more examples.
-
-1. Make sure the repository removal is also handled. You may need to add something
- like the following in the destroy service of the repository:
-
- ```ruby
- gizmo.replicator.handle_after_destroy if gizmo.repository
- ```
-
-1. Add this replicator class to the method `replicator_classes` in
- `ee/lib/gitlab/geo.rb`:
-
- ```ruby
- REPLICATOR_CLASSES = [
- ...
- ::Geo::PackageFileReplicator,
- ::Geo::GizmoReplicator
- ]
- end
- ```
-
-1. Create `ee/spec/replicators/geo/gizmo_replicator_spec.rb` and perform
- the necessary setup to define the `model_record` variable for the shared
- examples:
-
- ```ruby
- # frozen_string_literal: true
-
- require 'spec_helper'
-
- RSpec.describe Geo::GizmoReplicator do
- let(:model_record) { build(:gizmo) }
-
- include_examples 'a repository replicator'
- end
- ```
-
-1. Create the `gizmo_registry` table, with columns ordered according to [our guidelines](../ordering_table_columns.md) so Geo secondaries can track the sync and
- verification state of each Gizmo. This migration belongs in `ee/db/geo/migrate`:
-
- ```ruby
- # frozen_string_literal: true
-
- class CreateGizmoRegistry < ActiveRecord::Migration[6.0]
- include Gitlab::Database::MigrationHelpers
-
- disable_ddl_transaction!
-
- def up
- create_table :gizmo_registry, id: :bigserial, force: :cascade do |t|
- t.datetime_with_timezone :retry_at
- t.datetime_with_timezone :last_synced_at
- t.datetime_with_timezone :created_at, null: false
- t.bigint :gizmo_id, null: false
- t.integer :state, default: 0, null: false, limit: 2
- t.integer :retry_count, default: 0, limit: 2
- t.string :last_sync_failure, limit: 255
- t.boolean :force_to_redownload
- t.boolean :missing_on_primary
-
- t.index :gizmo_id, name: :index_gizmo_registry_on_gizmo_id, unique: true
- t.index :retry_at
- t.index :state
- end
- end
-
- def down
- drop_table :gizmo_registry
- end
- end
- ```
-
-1. Create `ee/app/models/geo/gizmo_registry.rb`:
-
- ```ruby
- # frozen_string_literal: true
-
- class Geo::GizmoRegistry < Geo::BaseRegistry
- include Geo::ReplicableRegistry
-
- MODEL_CLASS = ::Gizmo
- MODEL_FOREIGN_KEY = :gizmo_id
-
- belongs_to :gizmo, class_name: 'Gizmo'
- end
- ```
-
-1. Update `REGISTRY_CLASSES` in `ee/app/workers/geo/secondary/registry_consistency_worker.rb`.
-1. Add `gizmo_registry` to `ActiveSupport::Inflector.inflections` in `config/initializers_before_autoloader/000_inflections.rb`.
-1. Create `ee/spec/factories/geo/gizmo_registry.rb`:
-
- ```ruby
- # frozen_string_literal: true
-
- FactoryBot.define do
- factory :geo_gizmo_registry, class: 'Geo::GizmoRegistry' do
- gizmo
- state { Geo::GizmoRegistry.state_value(:pending) }
-
- trait :synced do
- state { Geo::GizmoRegistry.state_value(:synced) }
- last_synced_at { 5.days.ago }
- end
-
- trait :failed do
- state { Geo::GizmoRegistry.state_value(:failed) }
- last_synced_at { 1.day.ago }
- retry_count { 2 }
- last_sync_failure { 'Random error' }
- end
-
- trait :started do
- state { Geo::GizmoRegistry.state_value(:started) }
- last_synced_at { 1.day.ago }
- retry_count { 0 }
- end
- end
- end
- ```
-
-1. Create `ee/spec/models/geo/gizmo_registry_spec.rb`:
-
- ```ruby
- # frozen_string_literal: true
-
- require 'spec_helper'
-
- RSpec.describe Geo::GizmoRegistry, :geo, type: :model do
- let_it_be(:registry) { create(:geo_gizmo_registry) }
-
- specify 'factory is valid' do
- expect(registry).to be_valid
- end
-
- include_examples 'a Geo framework registry'
- end
- ```
-
-1. Make sure the newly added repository type can be accessed by a secondary.
- You may need to make some changes to one of the Git access classes.
-
- Gizmos should now be replicated by Geo.
-
-#### Metrics
-
-You need to make the same changes as for Blob Replicator Strategy.
-You need to make the same changes for the [metrics as in the Blob Replicator Strategy](#metrics).
-
-#### GraphQL API
-
-You need to make the same changes for the GraphQL API [as in the Blob Replicator Strategy](#graphql-api).
+Models that refer to any Git repository on disk are supported by Geo with the `Geo::RepositoryReplicatorStrategy` module. For example, see how [Geo replication was implemented for Group-level Wikis](https://gitlab.com/gitlab-org/gitlab/-/issues/208147). Note that this issue does not implement verification, since verification of Git repositories was not yet added to the Geo self-service framework. An example implementing verification can be found in the merge request to [Add Snippet repository verification](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56596).
-#### Releasing the feature
+Each Git repository is expected to have its own primary ID and model.
-You need to make the same changes for [releasing the feature as in the Blob Replicator Strategy](#releasing-the-feature).
+To implement Geo replication of a new Git repository-type Model, [open an issue with the provided issue template](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Geo%3A%20Replicate%20a%20new%20Git%20repository%20type).
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index e2b66e6019b..828e9925d46 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -362,20 +362,16 @@ The benefits of testing in this way are that:
- It is less brittle, as it avoids querying by IDs, classes, and attributes, which are not visible to the user.
We strongly recommend that you query by the element's text label instead of by ID, class name, or `data-testid`.
+
If needed, you can scope interactions within a specific area of the page by using `within`.
+As you will likely be scoping to an element such as a `div`, which typically does not have a label,
+you may use a `data-testid` selector in this case.
##### Actions
Where possible, use more specific [actions](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Actions), such as the ones below.
```ruby
-# bad
-find(".group-name", text: group.name).click
-find('.js-show-diff-settings').click
-find('[data-testid="submit-review"]').click
-find('input[type="checkbox"]').click
-find('.search').native.send_keys('gitlab')
-
# good
click_button 'Submit review'
@@ -391,6 +387,14 @@ uncheck 'Checkbox label'
choose 'Radio input label'
attach_file('Attach a file', '/path/to/file.png')
+
+# bad - interactive elements must have accessible names, so
+# we should be able to use one of the specific actions above
+find('.group-name', text: group.name).click
+find('.js-show-diff-settings').click
+find('[data-testid="submit-review"]').click
+find('input[type="checkbox"]').click
+find('.search').native.send_keys('gitlab')
```
##### Finders
@@ -398,9 +402,6 @@ attach_file('Attach a file', '/path/to/file.png')
Where possible, use more specific [finders](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Finders), such as the ones below.
```ruby
-# bad
-find('[data-testid="submit-review"]')
-
# good
find_button 'Submit review'
find_button 'Submit review', disabled: true
@@ -413,24 +414,26 @@ find_field 'Search projects', with: 'gitlab' # find the input field with text
find_field 'Search projects', disabled: true
find_field 'Checkbox label', checked: true
find_field 'Checkbox label', unchecked: true
+
+# acceptable when finding a element that is not a button, link, or field
+find('[data-testid="element"]')
```
##### Matchers
-Where possible, use more specific [matchers](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Matchers), such as the ones below.
+Where possible, use more specific [matchers](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/RSpecMatchers), such as the ones below.
```ruby
-# bad
-expect(find('[data-testid="submit-review"]')).to have_content(content)
-expect(page).to have_selector('[data-testid="submit-review"]')
-
# good
expect(page).to have_button 'Submit review'
+expect(page).to have_button 'Submit review', disabled: true
+expect(page).to have_button 'Notifications', class: 'is-checked' # assert the "Notifications" GlToggle is checked
expect(page).to have_link 'UI testing docs'
expect(page).to have_link 'UI testing docs', href: docs_url # assert the link has an href
expect(page).to have_field 'Search projects'
+expect(page).to have_field 'Search projects', disabled: true
expect(page).to have_field 'Search projects', with: 'gitlab' # assert the input field has text
expect(page).to have_checked_field 'Checkbox label'
@@ -443,8 +446,40 @@ expect(page).to have_select 'Sort by', with_options: ['Created date', 'Due date'
expect(page).to have_text 'Some paragraph text.'
expect(page).to have_text 'Some paragraph text.', exact: true # assert exact match
+
+expect(page).to have_current_path 'gitlab/gitlab-test/-/issues'
+
+expect(page).to have_title 'Not Found'
+
+# acceptable when a more specific matcher above is not possible
+expect(page).to have_css 'h2', text: 'Issue title'
+expect(page).to have_css 'p', text: 'Issue description', exact: true
+expect(page).to have_css '[data-testid="weight"]', text: 2
+expect(page).to have_css '.atwho-view ul', visible: true
+```
+
+##### Other useful methods
+
+After you retrieve an element using a [finder method](#finders), you can invoke a number of
+[element methods](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Element)
+on it, such as `hover`.
+
+Capybara tests also have a number of [session methods](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Session) available, such as `accept_confirm`.
+
+Some other useful methods are shown below:
+
+```ruby
+refresh # refresh the page
+
+send_keys([:shift, 'i']) # press Shift+I keys to go to the Issues dashboard page
+
+current_window.resize_to(1000, 1000) # resize the window
+
+scroll_to(find_field('Comment')) # scroll to an element
```
+You can also find a number of GitLab custom helpers in the `spec/support/helpers/` directory.
+
#### Live debug
Sometimes you may need to debug Capybara tests by observing browser behavior.
diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md
index 126d8725c21..a9af8f03d63 100644
--- a/doc/development/testing_guide/flaky_tests.md
+++ b/doc/development/testing_guide/flaky_tests.md
@@ -108,7 +108,7 @@ For instance `RETRIES=1 bin/rspec ...` would retry the failing examples once.
#### PhantomJS / WebKit related issues
-- Memory is through the roof! (TL;DR: Load images but block images requests!): <https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12003>
+- Memory is through the roof! (Load images but block images requests!): <https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12003>
#### Capybara expectation times out
diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md
index e0971d7f354..d04f55c43a3 100644
--- a/doc/install/relative_url.md
+++ b/doc/install/relative_url.md
@@ -24,8 +24,8 @@ Note that by changing the URL on an existing GitLab installation, all remote
URLs will change, so you'll have to manually edit them in any local repository
that points to your GitLab instance.
-The TL;DR list of configuration files that you need to change in order to
-serve GitLab under a relative URL is:
+The list of configuration files you must change to serve GitLab from a
+relative URL is:
- `/home/git/gitlab/config/initializers/relative_url.rb`
- `/home/git/gitlab/config/gitlab.yml`
diff --git a/doc/update/index.md b/doc/update/index.md
index 75bc1ef62f6..71d1bd06ff0 100644
--- a/doc/update/index.md
+++ b/doc/update/index.md
@@ -4,7 +4,7 @@ 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
---
-# Upgrading GitLab
+# Upgrading GitLab **(FREE SELF)**
Upgrading GitLab is a relatively straightforward process, but the complexity
can increase based on the installation method you have used, how old your
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index e27adc80fa7..ce0ba46b518 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
comments: false
---
-# Universal update guide for patch versions of source installations
+# Universal update guide for patch versions of source installations **(FREE SELF)**
## Select Version to Install
diff --git a/doc/update/restore_after_failure.md b/doc/update/restore_after_failure.md
index 6da9f11509a..0847fc82f19 100644
--- a/doc/update/restore_after_failure.md
+++ b/doc/update/restore_after_failure.md
@@ -4,7 +4,7 @@ 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
---
-# Restoring from backup after a failed upgrade
+# Restoring from backup after a failed upgrade **(FREE SELF)**
Upgrades are usually smooth and restoring from backup is a rare occurrence.
However, it's important to know how to recover when problems do arise.
diff --git a/doc/update/upgrading_from_ce_to_ee.md b/doc/update/upgrading_from_ce_to_ee.md
index a8a3d2e4fa4..50d169917ba 100644
--- a/doc/update/upgrading_from_ce_to_ee.md
+++ b/doc/update/upgrading_from_ce_to_ee.md
@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
comments: false
---
-# Upgrading from Community Edition to Enterprise Edition from source
+# Upgrading from Community Edition to Enterprise Edition from source **(FREE SELF)**
NOTE:
In the past we used separate documents for upgrading from
diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md
index 145999132d7..33ae9befd16 100644
--- a/doc/update/upgrading_from_source.md
+++ b/doc/update/upgrading_from_source.md
@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
comments: false
---
-# Upgrading Community Edition and Enterprise Edition from source
+# Upgrading Community Edition and Enterprise Edition from source **(FREE SELF)**
NOTE:
Users wishing to upgrade to 12.0.0 must take some extra steps. See the
diff --git a/doc/user/admin_area/settings/rate_limit_on_issues_creation.md b/doc/user/admin_area/settings/rate_limit_on_issues_creation.md
index 30cc64ccaa0..3acfb636a13 100644
--- a/doc/user/admin_area/settings/rate_limit_on_issues_creation.md
+++ b/doc/user/admin_area/settings/rate_limit_on_issues_creation.md
@@ -5,7 +5,7 @@ group: Project Management
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
---
-# Rate limits on issue creation
+# Rate limits on issue creation **(FREE SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28129) in GitLab 12.10.
diff --git a/doc/user/admin_area/settings/rate_limit_on_notes_creation.md b/doc/user/admin_area/settings/rate_limit_on_notes_creation.md
index 54b5da35dac..1997e6b5149 100644
--- a/doc/user/admin_area/settings/rate_limit_on_notes_creation.md
+++ b/doc/user/admin_area/settings/rate_limit_on_notes_creation.md
@@ -5,7 +5,7 @@ group: Project Management
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
---
-# Rate limits on note creation
+# Rate limits on note creation **(FREE SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53637) in GitLab 13.9.
diff --git a/doc/user/clusters/agent/repository.md b/doc/user/clusters/agent/repository.md
index 60d8cd352fc..9caa4a89daf 100644
--- a/doc/user/clusters/agent/repository.md
+++ b/doc/user/clusters/agent/repository.md
@@ -4,10 +4,10 @@ group: Configure
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/#designated-technical-writers
---
-# Kubernetes Agent configuration repository **(PREMIUM SELF)**
+# Kubernetes Agent configuration repository **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.7.
-> - It's disabled on GitLab.com. Rolling this feature out to GitLab.com is [planned](https://gitlab.com/groups/gitlab-org/-/epics/3834).
+> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3834) in GitLab 13.11, the Kubernetes Agent became available on GitLab.com.
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
diff --git a/doc/user/group/devops_adoption/index.md b/doc/user/group/devops_adoption/index.md
index 3d7e679d071..920421ef9bb 100644
--- a/doc/user/group/devops_adoption/index.md
+++ b/doc/user/group/devops_adoption/index.md
@@ -17,6 +17,8 @@ This feature might not be available to you. Check the **version history** note a
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321083) in GitLab 13.11 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta).
+To access Group DevOps Adoption, navigate to your group sidebar and select **Analytics > DevOps Adoption**
+
Group DevOps Adoption shows you how individual groups and sub-groups within your organization use the following features:
- Issues
diff --git a/lib/api/entities/namespace_existence.rb b/lib/api/entities/namespace_existence.rb
new file mode 100644
index 00000000000..d93078ecdac
--- /dev/null
+++ b/lib/api/entities/namespace_existence.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class NamespaceExistence < Grape::Entity
+ expose :exists, :suggests
+ end
+ end
+end
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index 25a901c18b6..465d2f23e9d 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -56,6 +56,23 @@ module API
present user_namespace, with: Entities::Namespace, current_user: current_user
end
+
+ desc 'Get existence of a namespace including alternative suggestions' do
+ success Entities::NamespaceExistence
+ end
+ params do
+ requires :namespace, type: String, desc: "Namespace's path"
+ optional :parent_id, type: Integer, desc: "The ID of the parent namespace. If no ID is specified, only top-level namespaces are considered."
+ end
+ get ':namespace/exists', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ namespace_path = params[:namespace]
+
+ exists = Namespace.by_parent(params[:parent_id]).filter_by_path(namespace_path).exists?
+ suggestions = exists ? [Namespace.clean_path(namespace_path)] : []
+
+ present :exists, exists
+ present :suggests, suggestions
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4ca3bc505db..da415e61dab 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -559,6 +559,9 @@ msgstr ""
msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
msgstr ""
+msgid "%{group_name} activity"
+msgstr ""
+
msgid "%{group_name} group members"
msgstr ""
@@ -4328,6 +4331,9 @@ msgstr ""
msgid "Artifacts"
msgstr ""
+msgid "Artifacts maximum size"
+msgstr ""
+
msgid "As we continue to build more features for SAST, we'd love your feedback on the SAST configuration feature in %{linkStart}this issue%{linkEnd}."
msgstr ""
@@ -4926,6 +4932,9 @@ msgstr ""
msgid "Begin with the selected commit"
msgstr ""
+msgid "Below are the current settings regarding"
+msgstr ""
+
msgid "Below are the fingerprints for the current instance SSH host keys."
msgstr ""
@@ -10321,6 +10330,9 @@ msgstr ""
msgid "Decrease"
msgstr ""
+msgid "Default"
+msgstr ""
+
msgid "Default CI configuration path"
msgstr ""
@@ -14650,6 +14662,9 @@ msgstr ""
msgid "GitLab Billing Team."
msgstr ""
+msgid "GitLab CI"
+msgstr ""
+
msgid "GitLab Import"
msgstr ""
@@ -17158,6 +17173,9 @@ msgstr ""
msgid "Integrations|Failed to load namespaces. Please try again."
msgstr ""
+msgid "Integrations|Failed to unlink namespace. Please try again."
+msgstr ""
+
msgid "Integrations|Includes Standard plus entire commit message, commit hash, and issue IDs"
msgstr ""
@@ -28841,6 +28859,9 @@ msgstr ""
msgid "Sets weight to %{weight}."
msgstr ""
+msgid "Setting"
+msgstr ""
+
msgid "Setting this to 0 means using the system default timeout value."
msgstr ""
@@ -31704,6 +31725,9 @@ msgstr ""
msgid "There was an error fetching the environments information."
msgstr ""
+msgid "There was an error fetching the jobs for your project."
+msgstr ""
+
msgid "There was an error fetching the top labels for the selected group"
msgstr ""
@@ -33675,6 +33699,9 @@ msgstr ""
msgid "Update iteration"
msgstr ""
+msgid "Update milestone"
+msgstr ""
+
msgid "Update now"
msgstr ""
diff --git a/package.json b/package.json
index e6f52b60c36..9ee955bc9e2 100644
--- a/package.json
+++ b/package.json
@@ -57,8 +57,8 @@
"@rails/ujs": "^6.0.3-4",
"@sentry/browser": "^5.22.3",
"@sourcegraph/code-host-integration": "0.0.52",
- "@toast-ui/editor": "^2.5.1",
- "@toast-ui/vue-editor": "^2.5.1",
+ "@toast-ui/editor": "^2.5.2",
+ "@toast-ui/vue-editor": "^2.5.2",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
"apollo-link": "^1.2.14",
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 22785adae4a..33d2ac50628 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -230,6 +230,29 @@ RSpec.describe 'Group' do
end
end
end
+
+ describe 'real-time group url validation', :js do
+ let_it_be(:subgroup) { create(:group, path: 'sub', parent: group) }
+
+ before do
+ group.add_owner(user)
+ visit new_group_path(parent_id: group.id)
+ end
+
+ it 'shows a message if group url is available' do
+ fill_in 'Group URL', with: group.path
+ wait_for_requests
+
+ expect(page).to have_content('Group path is available')
+ end
+
+ it 'shows an error if group url is taken' do
+ fill_in 'Group URL', with: subgroup.path
+ wait_for_requests
+
+ expect(page).to have_content('Group path is already taken')
+ end
+ end
end
it 'checks permissions to avoid exposing groups by parent_id' do
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index e87880d74b1..140d5dee270 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -12,6 +12,8 @@ RSpec.describe 'Project Jobs Permissions' do
let_it_be(:job) { create(:ci_build, :running, :coverage, :trace_artifact, pipeline: pipeline) }
before do
+ stub_feature_flags(jobs_table_vue: false)
+
sign_in(user)
project.enable_ci
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index 345bceeed31..dbcd7b5caf5 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe 'User browses jobs' do
let(:user) { create(:user) }
before do
+ stub_feature_flags(jobs_table_vue: false)
project.add_maintainer(user)
project.enable_ci
project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/)
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 7242e44bd64..18a6ad12240 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
end
before do
+ stub_feature_flags(jobs_table_vue: false)
project.add_role(user, user_access_level)
sign_in(user)
end
diff --git a/spec/frontend/jira_connect/components/__snapshots__/group_item_name_spec.js.snap b/spec/frontend/jira_connect/components/__snapshots__/group_item_name_spec.js.snap
index df524c6a19a..21c903f064d 100644
--- a/spec/frontend/jira_connect/components/__snapshots__/group_item_name_spec.js.snap
+++ b/spec/frontend/jira_connect/components/__snapshots__/group_item_name_spec.js.snap
@@ -26,16 +26,13 @@ exports[`GroupItemName template matches the snapshot 1`] = `
<div>
<span
class="gl-mr-3 gl-text-gray-900! gl-font-weight-bold"
- data-testid="group-list-item-name"
>
Gitlab Org
</span>
- <div
- data-testid="group-list-item-description"
- >
+ <div>
<p
class="gl-mt-2! gl-mb-0 gl-text-gray-600"
>
diff --git a/spec/frontend/jira_connect/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/components/subscriptions_list_spec.js
new file mode 100644
index 00000000000..ff86969367d
--- /dev/null
+++ b/spec/frontend/jira_connect/components/subscriptions_list_spec.js
@@ -0,0 +1,122 @@
+import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import * as JiraConnectApi from '~/jira_connect/api';
+import SubscriptionsList from '~/jira_connect/components/subscriptions_list.vue';
+import createStore from '~/jira_connect/store';
+import { SET_ALERT } from '~/jira_connect/store/mutation_types';
+import { reloadPage } from '~/jira_connect/utils';
+import { mockSubscription } from '../mock_data';
+
+jest.mock('~/jira_connect/utils');
+
+describe('SubscriptionsList', () => {
+ let wrapper;
+ let store;
+
+ const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => {
+ store = createStore();
+
+ wrapper = mountFn(SubscriptionsList, {
+ provide,
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findGlTable = () => wrapper.findComponent(GlTable);
+ const findUnlinkButton = () => findGlTable().findComponent(GlButton);
+ const clickUnlinkButton = () => findUnlinkButton().trigger('click');
+
+ describe('template', () => {
+ it('renders GlEmptyState when subscriptions is empty', () => {
+ createComponent();
+
+ expect(findGlEmptyState().exists()).toBe(true);
+ expect(findGlTable().exists()).toBe(false);
+ });
+
+ it('renders GlTable when subscriptions are present', () => {
+ createComponent({
+ provide: {
+ subscriptions: [mockSubscription],
+ },
+ });
+
+ expect(findGlEmptyState().exists()).toBe(false);
+ expect(findGlTable().exists()).toBe(true);
+ });
+ });
+
+ describe('on "Unlink" button click', () => {
+ let removeSubscriptionSpy;
+
+ beforeEach(() => {
+ createComponent({
+ mountFn: mount,
+ provide: {
+ subscriptions: [mockSubscription],
+ },
+ });
+ removeSubscriptionSpy = jest.spyOn(JiraConnectApi, 'removeSubscription').mockResolvedValue();
+ });
+
+ it('sets button to loading and sends request', async () => {
+ expect(findUnlinkButton().props('loading')).toBe(false);
+
+ clickUnlinkButton();
+
+ await wrapper.vm.$nextTick();
+
+ expect(findUnlinkButton().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(removeSubscriptionSpy).toHaveBeenCalledWith(mockSubscription.unlink_path);
+ });
+
+ describe('when request is successful', () => {
+ it('reloads the page', async () => {
+ clickUnlinkButton();
+
+ await waitForPromises();
+
+ expect(reloadPage).toHaveBeenCalled();
+ });
+ });
+
+ describe('when request has errors', () => {
+ const mockErrorMessage = 'error message';
+ const mockError = { response: { data: { error: mockErrorMessage } } };
+
+ beforeEach(() => {
+ jest.spyOn(JiraConnectApi, 'removeSubscription').mockRejectedValue(mockError);
+ jest.spyOn(store, 'commit');
+ });
+
+ it('sets alert', async () => {
+ clickUnlinkButton();
+
+ await waitForPromises();
+
+ expect(reloadPage).not.toHaveBeenCalled();
+ expect(store.commit.mock.calls).toEqual(
+ expect.arrayContaining([
+ [
+ SET_ALERT,
+ {
+ message: mockErrorMessage,
+ variant: 'danger',
+ },
+ ],
+ ]),
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/index_spec.js b/spec/frontend/jira_connect/index_spec.js
index b050515c118..0161cfa0273 100644
--- a/spec/frontend/jira_connect/index_spec.js
+++ b/spec/frontend/jira_connect/index_spec.js
@@ -1,10 +1,4 @@
-import waitForPromises from 'helpers/wait_for_promises';
import { initJiraConnect } from '~/jira_connect';
-import { removeSubscription } from '~/jira_connect/api';
-
-jest.mock('~/jira_connect/api', () => ({
- removeSubscription: jest.fn().mockResolvedValue(),
-}));
jest.mock('~/jira_connect/utils', () => ({
getLocation: jest.fn().mockResolvedValue('test/location'),
@@ -15,10 +9,6 @@ describe('initJiraConnect', () => {
setFixtures(`
<a class="js-jira-connect-sign-in" href="https://gitlab.com">Sign In</a>
<a class="js-jira-connect-sign-in" href="https://gitlab.com">Another Sign In</a>
-
- <a href="https://gitlab.com/sub1" class="js-jira-connect-remove-subscription">Remove</a>
- <a href="https://gitlab.com/sub2" class="js-jira-connect-remove-subscription">Remove</a>
- <a href="https://gitlab.com/sub3" class="js-jira-connect-remove-subscription">Remove</a>
`);
await initJiraConnect();
@@ -31,23 +21,4 @@ describe('initJiraConnect', () => {
});
});
});
-
- describe('`remove subscription` buttons', () => {
- describe('on click', () => {
- it('calls `removeSubscription`', () => {
- Array.from(document.querySelectorAll('.js-jira-connect-remove-subscription')).forEach(
- (removeSubscriptionButton) => {
- removeSubscriptionButton.dispatchEvent(new Event('click'));
-
- waitForPromises();
-
- expect(removeSubscription).toHaveBeenCalledWith(removeSubscriptionButton.href);
- expect(removeSubscription).toHaveBeenCalledTimes(1);
-
- removeSubscription.mockClear();
- },
- );
- });
- });
- });
});
diff --git a/spec/frontend/jira_connect/mock_data.js b/spec/frontend/jira_connect/mock_data.js
index 22255fabc3d..5247a3dc522 100644
--- a/spec/frontend/jira_connect/mock_data.js
+++ b/spec/frontend/jira_connect/mock_data.js
@@ -15,3 +15,9 @@ export const mockGroup2 = {
full_path: 'gitlab-com',
description: 'For GitLab company related projects',
};
+
+export const mockSubscription = {
+ group: mockGroup1,
+ created_at: '2021-04-14T08:52:23.115Z',
+ unlink_path: '/-/jira_connect/subscriptions/1',
+};
diff --git a/spec/frontend/jobs/components/table/jobs_table_spec.js b/spec/frontend/jobs/components/table/jobs_table_spec.js
new file mode 100644
index 00000000000..db057efbfb4
--- /dev/null
+++ b/spec/frontend/jobs/components/table/jobs_table_spec.js
@@ -0,0 +1,31 @@
+import { GlTable } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import JobsTable from '~/jobs/components/table/jobs_table.vue';
+import { mockJobsInTable } from '../../mock_data';
+
+describe('Jobs Table', () => {
+ let wrapper;
+
+ const findTable = () => wrapper.findComponent(GlTable);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(JobsTable, {
+ propsData: {
+ jobs: mockJobsInTable,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays a table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js b/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js
new file mode 100644
index 00000000000..ac9b45be932
--- /dev/null
+++ b/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js
@@ -0,0 +1,42 @@
+import { mount } from '@vue/test-utils';
+import { trimText } from 'helpers/text_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
+
+describe('Jobs Table Tabs', () => {
+ let wrapper;
+
+ const defaultProps = {
+ jobCounts: { all: 848, pending: 0, running: 0, finished: 704 },
+ };
+
+ const findTab = (testId) => wrapper.findByTestId(testId);
+
+ const createComponent = () => {
+ wrapper = extendedWrapper(
+ mount(JobsTableTabs, {
+ provide: {
+ ...defaultProps,
+ },
+ }),
+ );
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ tabId | text | count
+ ${'jobs-all-tab'} | ${'All'} | ${defaultProps.jobCounts.all}
+ ${'jobs-pending-tab'} | ${'Pending'} | ${defaultProps.jobCounts.pending}
+ ${'jobs-running-tab'} | ${'Running'} | ${defaultProps.jobCounts.running}
+ ${'jobs-finished-tab'} | ${'Finished'} | ${defaultProps.jobCounts.finished}
+ `('displays the right tab text and badge count', ({ tabId, text, count }) => {
+ expect(trimText(findTab(tabId).text())).toBe(`${text} ${count}`);
+ });
+});
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index c3115dd78e2..1432c6d7e9b 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1276,3 +1276,131 @@ export const mockPipelineDetached = {
name: 'test-branch',
},
};
+
+export const mockJobsInTable = [
+ {
+ detailedStatus: {
+ icon: 'status_manual',
+ label: 'manual play action',
+ text: 'manual',
+ tooltip: 'manual action',
+ action: {
+ buttonTitle: 'Trigger this manual action',
+ icon: 'play',
+ method: 'post',
+ path: '/root/ci-project/-/jobs/2004/play',
+ title: 'Play',
+ __typename: 'StatusAction',
+ },
+ __typename: 'DetailedStatus',
+ },
+ id: 'gid://gitlab/Ci::Build/2004',
+ refName: 'master',
+ refPath: '/root/ci-project/-/commits/master',
+ tags: [],
+ shortSha: '2d5d8323',
+ commitPath: '/root/ci-project/-/commit/2d5d83230bdea0e003d83ef4c16d2bf9a8808ebe',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/423',
+ path: '/root/ci-project/-/pipelines/423',
+ user: {
+ webPath: '/root',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ __typename: 'User',
+ },
+ __typename: 'Pipeline',
+ },
+ stage: { name: 'test', __typename: 'CiStage' },
+ name: 'test_manual_job',
+ duration: null,
+ finishedAt: null,
+ coverage: null,
+ retryable: false,
+ playable: true,
+ cancelable: false,
+ active: false,
+ __typename: 'CiJob',
+ },
+ {
+ detailedStatus: {
+ icon: 'status_skipped',
+ label: 'skipped',
+ text: 'skipped',
+ tooltip: 'skipped',
+ action: null,
+ __typename: 'DetailedStatus',
+ },
+ id: 'gid://gitlab/Ci::Build/2021',
+ refName: 'master',
+ refPath: '/root/ci-project/-/commits/master',
+ tags: [],
+ shortSha: '2d5d8323',
+ commitPath: '/root/ci-project/-/commit/2d5d83230bdea0e003d83ef4c16d2bf9a8808ebe',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/425',
+ path: '/root/ci-project/-/pipelines/425',
+ user: {
+ webPath: '/root',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ __typename: 'User',
+ },
+ __typename: 'Pipeline',
+ },
+ stage: { name: 'test', __typename: 'CiStage' },
+ name: 'coverage_job',
+ duration: null,
+ finishedAt: null,
+ coverage: null,
+ retryable: false,
+ playable: false,
+ cancelable: false,
+ active: false,
+ __typename: 'CiJob',
+ },
+ {
+ detailedStatus: {
+ icon: 'status_success',
+ label: 'passed',
+ text: 'passed',
+ tooltip: 'passed',
+ action: {
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ method: 'post',
+ path: '/root/ci-project/-/jobs/2015/retry',
+ title: 'Retry',
+ __typename: 'StatusAction',
+ },
+ __typename: 'DetailedStatus',
+ },
+ id: 'gid://gitlab/Ci::Build/2015',
+ refName: 'master',
+ refPath: '/root/ci-project/-/commits/master',
+ tags: [],
+ shortSha: '2d5d8323',
+ commitPath: '/root/ci-project/-/commit/2d5d83230bdea0e003d83ef4c16d2bf9a8808ebe',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/424',
+ path: '/root/ci-project/-/pipelines/424',
+ user: {
+ webPath: '/root',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ __typename: 'User',
+ },
+ __typename: 'Pipeline',
+ },
+ stage: { name: 'deploy', __typename: 'CiStage' },
+ name: 'artifact_job',
+ duration: 2,
+ finishedAt: '2021-04-01T17:36:18Z',
+ coverage: null,
+ retryable: true,
+ playable: false,
+ cancelable: false,
+ active: false,
+ __typename: 'CiJob',
+ },
+];
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index 4dd1bd2aa9c..1af96717b56 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -28,11 +28,11 @@ function convertPropsToGraphqlState(props) {
};
}
-function factory(propsData) {
+function factory(propsData, stateOverride = {}) {
let state = {};
if (mergeRequestWidgetGraphqlEnabled) {
- state = convertPropsToGraphqlState(propsData);
+ state = { ...convertPropsToGraphqlState(propsData), ...stateOverride };
}
wrapper = extendedWrapper(
@@ -125,7 +125,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
},
);
- it('should return false when shouldRemoveSourceBranch set to false', () => {
+ it('should not find "Delete" button when shouldRemoveSourceBranch set to true', () => {
factory({
...defaultMrProps(),
shouldRemoveSourceBranch: true,
@@ -134,6 +134,29 @@ describe('MRWidgetAutoMergeEnabled', () => {
expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false);
});
+ it('should find "Delete" button when shouldRemoveSourceBranch overrides state.forceRemoveSourceBranch', () => {
+ factory(
+ {
+ ...defaultMrProps(),
+ shouldRemoveSourceBranch: false,
+ },
+ {
+ forceRemoveSourceBranch: true,
+ },
+ );
+
+ expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(true);
+ });
+
+ it('should find "Delete" button when shouldRemoveSourceBranch set to false', () => {
+ factory({
+ ...defaultMrProps(),
+ shouldRemoveSourceBranch: false,
+ });
+
+ expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(true);
+ });
+
it('should return false if user is not able to remove the source branch', () => {
factory({
...defaultMrProps(),
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 8f3ad8b71ed..885569574a4 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -304,6 +304,7 @@ RSpec.describe BlobHelper do
let_it_be(:namespace) { create(:namespace, name: 'gitlab') }
let_it_be(:project) { create(:project, :repository, namespace: namespace) }
let_it_be(:current_user) { create(:user) }
+
let(:can_push_code) { true }
let(:blob) { project.repository.blob_at('refs/heads/master', 'README.md') }
diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb
index 21fde35954e..3e8cbdf89a0 100644
--- a/spec/helpers/broadcast_messages_helper_spec.rb
+++ b/spec/helpers/broadcast_messages_helper_spec.rb
@@ -28,6 +28,7 @@ RSpec.describe BroadcastMessagesHelper do
describe 'broadcast_message' do
let_it_be(:user) { create(:user) }
+
let(:current_broadcast_message) { BroadcastMessage.new(message: 'Current Message') }
before do
diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb
index 4041d66d5c4..94d4d620de9 100644
--- a/spec/helpers/ci/runners_helper_spec.rb
+++ b/spec/helpers/ci/runners_helper_spec.rb
@@ -84,6 +84,7 @@ RSpec.describe Ci::RunnersHelper do
describe '#toggle_shared_runners_settings_data' do
let_it_be(:group) { create(:group) }
+
let(:project_with_runners) { create(:project, namespace: group, shared_runners_enabled: true) }
let(:project_without_runners) { create(:project, namespace: group, shared_runners_enabled: false) }
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index 62bd953cce8..109b1fc4441 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -5,6 +5,7 @@ require "spec_helper"
RSpec.describe InviteMembersHelper do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user, developer_projects: [project]) }
+
let(:owner) { project.owner }
before do
@@ -253,6 +254,7 @@ RSpec.describe InviteMembersHelper do
context 'with a project' do
let_it_be(:form_model) { project }
+
let(:link_href) { "href=\"#{project_project_members_path(form_model)}\"" }
it_behaves_like 'dropdown invite members link'
@@ -260,6 +262,7 @@ RSpec.describe InviteMembersHelper do
context 'with a group' do
let_it_be(:form_model) { create(:group) }
+
let(:link_href) { "href=\"#{group_group_members_path(form_model)}\"" }
it_behaves_like 'dropdown invite members link'
diff --git a/spec/helpers/jira_connect_helper_spec.rb b/spec/helpers/jira_connect_helper_spec.rb
index 9695bed948b..55a5c724665 100644
--- a/spec/helpers/jira_connect_helper_spec.rb
+++ b/spec/helpers/jira_connect_helper_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe JiraConnectHelper do
describe '#jira_connect_app_data' do
let_it_be(:subscription) { create(:jira_connect_subscription) }
+
let(:user) { create(:user) }
subject { helper.jira_connect_app_data([subscription]) }
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index b93dc03e434..526983a0d5f 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -36,6 +36,7 @@ RSpec.describe LabelsHelper do
context 'with a group label' do
let_it_be(:group) { create(:group) }
+
let(:label) { create(:group_label, group: group, title: 'bug') }
context 'when asking for an issue link' do
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 08a20e87f4b..00a59f037e0 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe MarkupHelper do
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let_it_be(:snippet) { create(:project_snippet, project: project) }
+
let(:commit) { project.commit }
before do
@@ -453,6 +454,7 @@ FooBar
let_it_be(:project_base) { create(:project, :repository) }
let_it_be(:context) { { project: project_base } }
+
let(:file_name) { 'foo.bar' }
let(:text) { 'Noël' }
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index b8502cdf25e..fc62bbf8bf8 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe NotesHelper do
let_it_be(:owner_note) { create(:note, author: owner, project: project) }
let_it_be(:maintainer_note) { create(:note, author: maintainer, project: project) }
let_it_be(:reporter_note) { create(:note, author: reporter, project: project) }
+
let!(:notes) { [owner_note, maintainer_note, reporter_note] }
before_all do
@@ -73,6 +74,7 @@ RSpec.describe NotesHelper do
describe '#discussion_path' do
let_it_be(:project) { create(:project, :repository) }
+
let(:anchor) { discussion.line_code }
context 'for a merge request discusion' do
diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb
index 0df194e460a..e836461b099 100644
--- a/spec/helpers/projects/alert_management_helper_spec.rb
+++ b/spec/helpers/projects/alert_management_helper_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Projects::AlertManagementHelper do
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:current_user) { create(:user) }
+
let(:project_path) { project.full_path }
let(:project_id) { project.id }
diff --git a/spec/helpers/projects/issues/service_desk_helper_spec.rb b/spec/helpers/projects/issues/service_desk_helper_spec.rb
index 3f488fe692d..05766ee13c6 100644
--- a/spec/helpers/projects/issues/service_desk_helper_spec.rb
+++ b/spec/helpers/projects/issues/service_desk_helper_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Projects::Issues::ServiceDeskHelper do
let_it_be(:project) { create(:project, :public, service_desk_enabled: true) }
+
let(:user) { build_stubbed(:user) }
let(:current_user) { user }
diff --git a/spec/helpers/projects/project_members_helper_spec.rb b/spec/helpers/projects/project_members_helper_spec.rb
index 1a55840a58a..0e08a18f912 100644
--- a/spec/helpers/projects/project_members_helper_spec.rb
+++ b/spec/helpers/projects/project_members_helper_spec.rb
@@ -128,6 +128,7 @@ RSpec.describe Projects::ProjectMembersHelper do
describe "when current user is not the owner of the project's parent group" do
let_it_be(:user) { create(:user) }
+
let(:project2) { create(:project, namespace: group) }
before do
@@ -174,6 +175,7 @@ RSpec.describe Projects::ProjectMembersHelper do
describe 'project group links' do
let_it_be(:project_group_links) { create_list(:project_group_link, 1, project: project) }
+
let(:allow_admin_project) { true }
describe '#project_group_links_data_json' do
diff --git a/spec/helpers/projects/terraform_helper_spec.rb b/spec/helpers/projects/terraform_helper_spec.rb
index 70b08f4139b..8833e23c47d 100644
--- a/spec/helpers/projects/terraform_helper_spec.rb
+++ b/spec/helpers/projects/terraform_helper_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::TerraformHelper do
describe '#js_terraform_list_data' do
let_it_be(:project) { create(:project) }
+
let(:current_user) { project.creator }
subject { helper.js_terraform_list_data(current_user, project) }
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index e6cd11a4d70..124cdcec05d 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -85,6 +85,7 @@ RSpec.describe ProjectsHelper do
describe "can_change_visibility_level?" do
let_it_be(:user) { create(:project_member, :reporter, user: create(:user), project: project).user }
+
let(:forked_project) { fork_project(project, user) }
it "returns false if there are no appropriate permissions" do
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 13d3a80bd13..7b2334ab79e 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -489,6 +489,7 @@ RSpec.describe SearchHelper do
describe '#repository_ref' do
let_it_be(:project) { create(:project, :repository) }
+
let(:params) { { repository_ref: 'the-repository-ref-param' } }
subject { repository_ref(project) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index ce29181dab8..96ecc9836d4 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -155,6 +155,33 @@ RSpec.describe Namespace do
end
end
+ describe 'scopes' do
+ let_it_be(:namespace1) { create(:group, name: 'Namespace 1', path: 'namespace-1') }
+ let_it_be(:namespace2) { create(:group, name: 'Namespace 2', path: 'namespace-2') }
+ let_it_be(:namespace1sub) { create(:group, name: 'Sub Namespace', path: 'sub-namespace', parent: namespace1) }
+ let_it_be(:namespace2sub) { create(:group, name: 'Sub Namespace', path: 'sub-namespace', parent: namespace2) }
+
+ describe '.by_parent' do
+ it 'includes correct namespaces' do
+ expect(described_class.by_parent(namespace1.id)).to eq([namespace1sub])
+ expect(described_class.by_parent(namespace2.id)).to eq([namespace2sub])
+ expect(described_class.by_parent(nil)).to match_array([namespace, namespace1, namespace2])
+ end
+ end
+
+ describe '.filter_by_path' do
+ it 'includes correct namespaces' do
+ expect(described_class.filter_by_path(namespace1.path)).to eq([namespace1])
+ expect(described_class.filter_by_path(namespace2.path)).to eq([namespace2])
+ expect(described_class.filter_by_path('sub-namespace')).to match_array([namespace1sub, namespace2sub])
+ end
+
+ it 'filters case-insensitive' do
+ expect(described_class.filter_by_path(namespace1.path.upcase)).to eq([namespace1])
+ end
+ end
+ end
+
describe 'delegate' do
it { is_expected.to delegate_method(:name).to(:owner).with_prefix.with_arguments(allow_nil: true) }
it { is_expected.to delegate_method(:avatar_url).to(:owner).with_arguments(allow_nil: true) }
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 2ac76d469d5..1ed06a40f16 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -216,4 +216,77 @@ RSpec.describe API::Namespaces do
end
end
end
+
+ describe 'GET /namespaces/:namespace/exists' do
+ let!(:namespace1) { create(:group, name: 'Namespace 1', path: 'namespace-1') }
+ let!(:namespace2) { create(:group, name: 'Namespace 2', path: 'namespace-2') }
+ let!(:namespace1sub) { create(:group, name: 'Sub Namespace 1', path: 'sub-namespace-1', parent: namespace1) }
+ let!(:namespace2sub) { create(:group, name: 'Sub Namespace 2', path: 'sub-namespace-2', parent: namespace2) }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api("/namespaces/#{namespace1.path}/exists")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns JSON indicating the namespace exists and a suggestion' do
+ get api("/namespaces/#{namespace1.path}/exists", user)
+
+ expected_json = { exists: true, suggests: ["#{namespace1.path}1"] }.to_json
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(expected_json)
+ end
+
+ it 'returns JSON indicating the namespace does not exist without a suggestion' do
+ get api("/namespaces/non-existing-namespace/exists", user)
+
+ expected_json = { exists: false, suggests: [] }.to_json
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(expected_json)
+ end
+
+ it 'checks the existence of a namespace in case-insensitive manner' do
+ get api("/namespaces/#{namespace1.path.upcase}/exists", user)
+
+ expected_json = { exists: true, suggests: ["#{namespace1.path.upcase}1"] }.to_json
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(expected_json)
+ end
+
+ it 'checks the existence within the parent namespace only' do
+ get api("/namespaces/#{namespace1sub.path}/exists", user), params: { parent_id: namespace1.id }
+
+ expected_json = { exists: true, suggests: ["#{namespace1sub.path}1"] }.to_json
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(expected_json)
+ end
+
+ it 'ignores nested namespaces when checking for top-level namespace' do
+ get api("/namespaces/#{namespace1sub.path}/exists", user)
+
+ expected_json = { exists: false, suggests: [] }.to_json
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(expected_json)
+ end
+
+ it 'ignores top-level namespaces when checking with parent_id' do
+ get api("/namespaces/#{namespace1.path}/exists", user), params: { parent_id: namespace1.id }
+
+ expected_json = { exists: false, suggests: [] }.to_json
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(expected_json)
+ end
+
+ it 'ignores namespaces of other parent namespaces when checking with parent_id' do
+ get api("/namespaces/#{namespace2sub.path}/exists", user), params: { parent_id: namespace1.id }
+
+ expected_json = { exists: false, suggests: [] }.to_json
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(expected_json)
+ end
+ end
+ end
end
diff --git a/spec/services/boards/destroy_service_spec.rb b/spec/services/boards/destroy_service_spec.rb
index ec0d264d726..cd6df832547 100644
--- a/spec/services/boards/destroy_service_spec.rb
+++ b/spec/services/boards/destroy_service_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Boards::DestroyService do
context 'with project board' do
let_it_be(:parent) { create(:project) }
+
let(:boards) { parent.boards }
let(:board_factory) { :board }
@@ -13,6 +14,7 @@ RSpec.describe Boards::DestroyService do
context 'with group board' do
let_it_be(:parent) { create(:group) }
+
let(:boards) { parent.boards }
let(:board_factory) { :board }
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 01a3ec72987..3a25f13762c 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -62,6 +62,7 @@ RSpec.describe Boards::Issues::MoveService do
let_it_be(:testing) { create(:group_label, group: group, name: 'Testing') }
let_it_be(:list1) { create(:list, board: board1, label: development, position: 0) }
let_it_be(:list2) { create(:list, board: board1, label: testing, position: 1) }
+
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } }
before do
diff --git a/yarn.lock b/yarn.lock
index ba652b69651..de7810e1d45 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1291,20 +1291,20 @@
dom-accessibility-api "^0.5.1"
pretty-format "^26.4.2"
-"@toast-ui/editor@^2.5.1":
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/@toast-ui/editor/-/editor-2.5.1.tgz#42671c52ca4b97c84f684d09c2966711b36f41a7"
- integrity sha512-LVNo/YaNItUemEaRFvFAVn7w/0U7yxEheMdn6GEGxqo727rRZD1MH7OTDVq6NeQ+P93VwFpa0i9GGRBhNNEbPQ==
+"@toast-ui/editor@^2.5.2":
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/@toast-ui/editor/-/editor-2.5.2.tgz#0637e1bbdb205c1ab53b6d3722ced26399b2f0ca"
+ integrity sha512-ldTOMCVKpSIummf9X3JqPuOUMjlAxyFePniHChmCMst9j+ZbHxdalOhEUDV/ASrq4jkjgRm4m0nCCUvjdljZSw==
dependencies:
"@types/codemirror" "0.0.71"
codemirror "^5.48.4"
-"@toast-ui/vue-editor@^2.5.1":
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/@toast-ui/vue-editor/-/vue-editor-2.5.1.tgz#0a221d74d5305c8ca20cb11d9eb8ff9206455cfc"
- integrity sha512-vD0FowDrlMPfR4m1Sd91YthkMLul4lTdiwl1QcDYX+JhIzxXMuQhFABezny/TvKJLxkCkHGpt7XsTjXvMUa04w==
+"@toast-ui/vue-editor@^2.5.2":
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/@toast-ui/vue-editor/-/vue-editor-2.5.2.tgz#0b54107a196471eacb18aabb7100101606917b27"
+ integrity sha512-zF33zKsG4wusL/OgwIUHWWDAXHuNx94tL1Jzgww9SbJ/mF23WRbvbOT4tvhpr6KZhbIbUUwzxwhPQabYSLjNRw==
dependencies:
- "@toast-ui/editor" "^2.5.1"
+ "@toast-ui/editor" "^2.5.2"
"@types/aria-query@^4.2.0":
version "4.2.0"