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>2022-08-11 06:09:21 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-11 06:09:21 +0300
commit8754d20bbb9e573d48e80d7f6aed1ded40a40263 (patch)
tree217d3e06689c37e8626da9a2c79e40ab997d8e2b
parent4f1e40017d9eadb0abeeb89d9690a8e5f0694fd9 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/runner/components/runner_bulk_delete.vue4
-rw-r--r--app/controllers/users/project_callouts_controller.rb17
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/user.rb12
-rw-r--r--app/models/users/project_callout.rb21
-rw-r--r--app/services/users/dismiss_project_callout_service.rb11
-rw-r--r--config/routes/user.rb1
-rw-r--r--db/docs/user_project_callouts.yml9
-rw-r--r--db/migrate/20220803145637_create_user_project_callout.rb19
-rw-r--r--db/migrate/20220803154543_add_project_id_fkey_for_user_project_callout.rb15
-rw-r--r--db/migrate/20220803154758_add_user_id_fkey_for_user_project_callout.rb15
-rw-r--r--db/migrate/20220808133824_add_timestamps_to_project_statistics.rb7
-rw-r--r--db/post_migrate/20220614185644_update_index_vulnerabilities_project_id_id.rb22
-rw-r--r--db/post_migrate/20220808131659_remove_ci_namespace_monthly_usages_additional_amount_available_column.rb11
-rw-r--r--db/schema_migrations/202206141856441
-rw-r--r--db/schema_migrations/202208031456371
-rw-r--r--db/schema_migrations/202208031545431
-rw-r--r--db/schema_migrations/202208031547581
-rw-r--r--db/schema_migrations/202208081316591
-rw-r--r--db/schema_migrations/202208081338241
-rw-r--r--db/structure.sql41
-rw-r--r--doc/administration/repository_checks.md60
-rw-r--r--doc/user/application_security/dependency_scanning/index.md57
-rw-r--r--doc/user/group/saml_sso/example_saml_config.md16
-rw-r--r--doc/user/group/saml_sso/img/Okta-GroupAttribute.pngbin0 -> 7744 bytes
-rw-r--r--doc/user/group/saml_sso/img/Okta-GroupSAML.pngbin0 -> 23681 bytes
-rw-r--r--doc/user/group/saml_sso/img/Okta-SAMLsetup.pngbin18920 -> 0 bytes
-rw-r--r--doc/user/group/saml_sso/img/Okta-SM.pngbin0 -> 23386 bytes
-rw-r--r--doc/user/group/saml_sso/img/Okta-advancedsettings.pngbin15614 -> 18912 bytes
-rw-r--r--doc/user/group/saml_sso/img/Okta-attributes.pngbin5540 -> 13687 bytes
-rw-r--r--doc/user/group/saml_sso/img/Okta-linkscert.pngbin58832 -> 55321 bytes
-rw-r--r--doc/user/group/saml_sso/img/okta_admin_panel_v13_9.pngbin49319 -> 0 bytes
-rw-r--r--doc/user/group/saml_sso/img/okta_saml_settings.pngbin25470 -> 21280 bytes
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml1
-rw-r--r--spec/factories/users/project_user_callouts.rb10
-rw-r--r--spec/frontend/__helpers__/vue_test_utils_helper.js19
-rw-r--r--spec/frontend/__helpers__/vue_test_utils_helper_spec.js57
-rw-r--r--spec/frontend/runner/components/runner_bulk_delete_spec.js5
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/models/user_spec.rb79
-rw-r--r--spec/models/users/project_callout_spec.rb23
-rw-r--r--spec/requests/users/project_callouts_spec.rb58
-rw-r--r--spec/scripts/lib/glfm/update_example_snapshots_spec.rb12
-rw-r--r--spec/services/users/dismiss_project_callout_service_spec.rb25
45 files changed, 553 insertions, 84 deletions
diff --git a/app/assets/javascripts/runner/components/runner_bulk_delete.vue b/app/assets/javascripts/runner/components/runner_bulk_delete.vue
index 50791de0bda..6f065c38b25 100644
--- a/app/assets/javascripts/runner/components/runner_bulk_delete.vue
+++ b/app/assets/javascripts/runner/components/runner_bulk_delete.vue
@@ -99,10 +99,10 @@ export default {
</gl-sprintf>
</div>
<div class="gl-ml-auto">
- <gl-button data-testid="clear-btn" variant="default" @click="onClearChecked">{{
+ <gl-button variant="default" @click="onClearChecked">{{
s__('Runners|Clear selection')
}}</gl-button>
- <gl-button data-testid="delete-btn" variant="danger" @click="onClickDelete">{{
+ <gl-button variant="danger" @click="onClickDelete">{{
s__('Runners|Delete selected')
}}</gl-button>
</div>
diff --git a/app/controllers/users/project_callouts_controller.rb b/app/controllers/users/project_callouts_controller.rb
new file mode 100644
index 00000000000..64d89630021
--- /dev/null
+++ b/app/controllers/users/project_callouts_controller.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Users
+ class ProjectCalloutsController < Users::CalloutsController
+ private
+
+ def callout
+ Users::DismissProjectCalloutService.new(
+ container: nil, current_user: current_user, params: callout_params
+ ).execute
+ end
+
+ def callout_params
+ params.permit(:project_id).merge(feature_name: feature_name)
+ end
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index b0eb273e526..28dd51fc67d 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -291,6 +291,8 @@ class Project < ApplicationRecord
has_many :project_members, -> { where(requested_at: nil) },
as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+ has_many :project_callouts, class_name: 'Users::ProjectCallout', foreign_key: :project_id
+
alias_method :members, :project_members
has_many :users, through: :project_members
diff --git a/app/models/user.rb b/app/models/user.rb
index 6f6dde236ba..5503f28a4fe 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -222,6 +222,7 @@ class User < ApplicationRecord
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'Users::Callout'
has_many :group_callouts, class_name: 'Users::GroupCallout'
+ has_many :project_callouts, class_name: 'Users::ProjectCallout'
has_many :namespace_callouts, class_name: 'Users::NamespaceCallout'
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
@@ -2087,6 +2088,12 @@ class User < ApplicationRecord
callout_dismissed?(callout, ignore_dismissal_earlier_than)
end
+ def dismissed_callout_for_project?(feature_name:, project:, ignore_dismissal_earlier_than: nil)
+ callout = project_callouts.find_by(feature_name: feature_name, project: project)
+
+ callout_dismissed?(callout, ignore_dismissal_earlier_than)
+ end
+
# Load the current highest access by looking directly at the user's memberships
def current_highest_access_level
members.non_request.maximum(:access_level)
@@ -2118,6 +2125,11 @@ class User < ApplicationRecord
.find_or_initialize_by(feature_name: ::Users::NamespaceCallout.feature_names[feature_name], namespace_id: namespace_id)
end
+ def find_or_initialize_project_callout(feature_name, project_id)
+ project_callouts
+ .find_or_initialize_by(feature_name: ::Users::ProjectCallout.feature_names[feature_name], project_id: project_id)
+ end
+
def can_trigger_notifications?
confirmed? && !blocked? && !ghost?
end
diff --git a/app/models/users/project_callout.rb b/app/models/users/project_callout.rb
new file mode 100644
index 00000000000..ddc5f8fb4de
--- /dev/null
+++ b/app/models/users/project_callout.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Users
+ class ProjectCallout < ApplicationRecord
+ include Users::Calloutable
+
+ self.table_name = 'user_project_callouts'
+
+ belongs_to :project
+
+ enum feature_name: {
+ awaiting_members_banner: 1 # EE-only
+ }
+
+ validates :project, presence: true
+ validates :feature_name,
+ presence: true,
+ uniqueness: { scope: [:user_id, :project_id] },
+ inclusion: { in: ProjectCallout.feature_names.keys }
+ end
+end
diff --git a/app/services/users/dismiss_project_callout_service.rb b/app/services/users/dismiss_project_callout_service.rb
new file mode 100644
index 00000000000..23549b3b194
--- /dev/null
+++ b/app/services/users/dismiss_project_callout_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Users
+ class DismissProjectCalloutService < DismissCalloutService
+ private
+
+ def callout
+ current_user.find_or_initialize_project_callout(params[:feature_name], params[:project_id])
+ end
+ end
+end
diff --git a/config/routes/user.rb b/config/routes/user.rb
index 96e8c850da4..0c1bc1956a9 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -65,6 +65,7 @@ scope '-/users', module: :users do
resources :callouts, only: [:create]
resources :group_callouts, only: [:create]
+ resources :project_callouts, only: [:create]
end
scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) do
diff --git a/db/docs/user_project_callouts.yml b/db/docs/user_project_callouts.yml
new file mode 100644
index 00000000000..308c3048aa7
--- /dev/null
+++ b/db/docs/user_project_callouts.yml
@@ -0,0 +1,9 @@
+---
+table_name: user_project_callouts
+classes:
+- Users::ProjectCallout
+feature_categories:
+- navigation
+description: Adds the ability to track a user callout being dismissed by project
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94144
+milestone: '15.3'
diff --git a/db/migrate/20220803145637_create_user_project_callout.rb b/db/migrate/20220803145637_create_user_project_callout.rb
new file mode 100644
index 00000000000..1d0baf741a9
--- /dev/null
+++ b/db/migrate/20220803145637_create_user_project_callout.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateUserProjectCallout < Gitlab::Database::Migration[2.0]
+ def up
+ create_table :user_project_callouts do |t|
+ t.bigint :user_id, null: false
+ t.bigint :project_id, null: false
+ t.integer :feature_name, limit: 2, null: false
+ t.datetime_with_timezone :dismissed_at
+
+ t.index :project_id
+ t.index [:user_id, :feature_name, :project_id], unique: true, name: 'index_project_user_callouts_feature'
+ end
+ end
+
+ def down
+ drop_table :user_project_callouts
+ end
+end
diff --git a/db/migrate/20220803154543_add_project_id_fkey_for_user_project_callout.rb b/db/migrate/20220803154543_add_project_id_fkey_for_user_project_callout.rb
new file mode 100644
index 00000000000..3cea53dc127
--- /dev/null
+++ b/db/migrate/20220803154543_add_project_id_fkey_for_user_project_callout.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddProjectIdFkeyForUserProjectCallout < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :user_project_callouts, :projects, column: :project_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :user_project_callouts, column: :project_id
+ end
+ end
+end
diff --git a/db/migrate/20220803154758_add_user_id_fkey_for_user_project_callout.rb b/db/migrate/20220803154758_add_user_id_fkey_for_user_project_callout.rb
new file mode 100644
index 00000000000..01a05f816da
--- /dev/null
+++ b/db/migrate/20220803154758_add_user_id_fkey_for_user_project_callout.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddUserIdFkeyForUserProjectCallout < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :user_project_callouts, :users, column: :user_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :user_project_callouts, column: :user_id
+ end
+ end
+end
diff --git a/db/migrate/20220808133824_add_timestamps_to_project_statistics.rb b/db/migrate/20220808133824_add_timestamps_to_project_statistics.rb
new file mode 100644
index 00000000000..c5125ebc8ff
--- /dev/null
+++ b/db/migrate/20220808133824_add_timestamps_to_project_statistics.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddTimestampsToProjectStatistics < Gitlab::Database::Migration[2.0]
+ def change
+ add_timestamps_with_timezone(:project_statistics, null: false, default: -> { 'NOW()' })
+ end
+end
diff --git a/db/post_migrate/20220614185644_update_index_vulnerabilities_project_id_id.rb b/db/post_migrate/20220614185644_update_index_vulnerabilities_project_id_id.rb
new file mode 100644
index 00000000000..142cac09a01
--- /dev/null
+++ b/db/post_migrate/20220614185644_update_index_vulnerabilities_project_id_id.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class UpdateIndexVulnerabilitiesProjectIdId < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ NEW_INDEX_NAME = 'index_vulnerabilities_project_id_and_id_on_default_branch'
+ OLD_INDEX_NAME = 'index_vulnerabilities_on_project_id_and_id'
+
+ def up
+ add_concurrent_index :vulnerabilities, [:project_id, :id],
+ where: 'present_on_default_branch IS TRUE',
+ name: NEW_INDEX_NAME
+
+ remove_concurrent_index_by_name(:vulnerabilities, OLD_INDEX_NAME)
+ end
+
+ def down
+ add_concurrent_index :vulnerabilities, [:project_id, :id], name: OLD_INDEX_NAME
+
+ remove_concurrent_index_by_name(:vulnerabilities, NEW_INDEX_NAME)
+ end
+end
diff --git a/db/post_migrate/20220808131659_remove_ci_namespace_monthly_usages_additional_amount_available_column.rb b/db/post_migrate/20220808131659_remove_ci_namespace_monthly_usages_additional_amount_available_column.rb
new file mode 100644
index 00000000000..70a1e425e0d
--- /dev/null
+++ b/db/post_migrate/20220808131659_remove_ci_namespace_monthly_usages_additional_amount_available_column.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class RemoveCiNamespaceMonthlyUsagesAdditionalAmountAvailableColumn < Gitlab::Database::Migration[2.0]
+ def up
+ remove_column :ci_namespace_monthly_usages, :additional_amount_available
+ end
+
+ def down
+ add_column :ci_namespace_monthly_usages, :additional_amount_available, :integer, default: 0, null: false
+ end
+end
diff --git a/db/schema_migrations/20220614185644 b/db/schema_migrations/20220614185644
new file mode 100644
index 00000000000..dcc2d926276
--- /dev/null
+++ b/db/schema_migrations/20220614185644
@@ -0,0 +1 @@
+f1d4faf4d32a3271a97b389d53c9d3accbfa3fa2bd47d63257fe589efa4bb665 \ No newline at end of file
diff --git a/db/schema_migrations/20220803145637 b/db/schema_migrations/20220803145637
new file mode 100644
index 00000000000..36688f97184
--- /dev/null
+++ b/db/schema_migrations/20220803145637
@@ -0,0 +1 @@
+bf12037cb99a399302610f948dad48589eca4e631d82d9f26b04bae882b10020 \ No newline at end of file
diff --git a/db/schema_migrations/20220803154543 b/db/schema_migrations/20220803154543
new file mode 100644
index 00000000000..f5cfb3d91ba
--- /dev/null
+++ b/db/schema_migrations/20220803154543
@@ -0,0 +1 @@
+047147acc972ab8681f097d5060998a47e44612fde7f2137714683bd61350c2d \ No newline at end of file
diff --git a/db/schema_migrations/20220803154758 b/db/schema_migrations/20220803154758
new file mode 100644
index 00000000000..71ac8b4f301
--- /dev/null
+++ b/db/schema_migrations/20220803154758
@@ -0,0 +1 @@
+2cdf4c4fe218a5fb7061bf65643868c7b592cd3ef0d7611949e8fd86bc635c24 \ No newline at end of file
diff --git a/db/schema_migrations/20220808131659 b/db/schema_migrations/20220808131659
new file mode 100644
index 00000000000..65a08ad1b7a
--- /dev/null
+++ b/db/schema_migrations/20220808131659
@@ -0,0 +1 @@
+07488e8c6ea0f3dc92e1370efb0190facf520b850e170fcd8f3ce0e2a15c096a \ No newline at end of file
diff --git a/db/schema_migrations/20220808133824 b/db/schema_migrations/20220808133824
new file mode 100644
index 00000000000..bf7755d3163
--- /dev/null
+++ b/db/schema_migrations/20220808133824
@@ -0,0 +1 @@
+bab4f4d3aaedd698400fcbd5991797530450fe845a8034b03b1bf525a61e628a \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 3636c609091..73b83a51bb4 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12856,7 +12856,6 @@ CREATE TABLE ci_namespace_monthly_usages (
id bigint NOT NULL,
namespace_id bigint NOT NULL,
date date NOT NULL,
- additional_amount_available integer DEFAULT 0 NOT NULL,
amount_used numeric(18,2) DEFAULT 0.0 NOT NULL,
notification_level smallint DEFAULT 100 NOT NULL,
shared_runners_duration integer DEFAULT 0 NOT NULL,
@@ -19839,7 +19838,9 @@ CREATE TABLE project_statistics (
snippets_size bigint,
pipeline_artifacts_size bigint DEFAULT 0 NOT NULL,
uploads_size bigint DEFAULT 0 NOT NULL,
- container_registry_size bigint DEFAULT 0 NOT NULL
+ container_registry_size bigint DEFAULT 0 NOT NULL,
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
+ updated_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE SEQUENCE project_statistics_id_seq
@@ -21906,6 +21907,23 @@ CREATE SEQUENCE user_preferences_id_seq
ALTER SEQUENCE user_preferences_id_seq OWNED BY user_preferences.id;
+CREATE TABLE user_project_callouts (
+ id bigint NOT NULL,
+ user_id bigint NOT NULL,
+ project_id bigint NOT NULL,
+ feature_name smallint NOT NULL,
+ dismissed_at timestamp with time zone
+);
+
+CREATE SEQUENCE user_project_callouts_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE user_project_callouts_id_seq OWNED BY user_project_callouts.id;
+
CREATE TABLE user_statuses (
user_id integer NOT NULL,
cached_markdown_version integer,
@@ -23788,6 +23806,8 @@ ALTER TABLE ONLY user_permission_export_uploads ALTER COLUMN id SET DEFAULT next
ALTER TABLE ONLY user_preferences ALTER COLUMN id SET DEFAULT nextval('user_preferences_id_seq'::regclass);
+ALTER TABLE ONLY user_project_callouts ALTER COLUMN id SET DEFAULT nextval('user_project_callouts_id_seq'::regclass);
+
ALTER TABLE ONLY user_statuses ALTER COLUMN user_id SET DEFAULT nextval('user_statuses_user_id_seq'::regclass);
ALTER TABLE ONLY user_synced_attributes_metadata ALTER COLUMN id SET DEFAULT nextval('user_synced_attributes_metadata_id_seq'::regclass);
@@ -26076,6 +26096,9 @@ ALTER TABLE ONLY user_permission_export_uploads
ALTER TABLE ONLY user_preferences
ADD CONSTRAINT user_preferences_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY user_project_callouts
+ ADD CONSTRAINT user_project_callouts_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY user_statuses
ADD CONSTRAINT user_statuses_pkey PRIMARY KEY (user_id);
@@ -29484,6 +29507,8 @@ CREATE UNIQUE INDEX index_project_topics_on_project_id_and_topic_id ON project_t
CREATE INDEX index_project_topics_on_topic_id ON project_topics USING btree (topic_id);
+CREATE UNIQUE INDEX index_project_user_callouts_feature ON user_project_callouts USING btree (user_id, feature_name, project_id);
+
CREATE INDEX index_projects_aimed_for_deletion ON projects USING btree (marked_for_deletion_at) WHERE ((marked_for_deletion_at IS NOT NULL) AND (pending_delete = false));
CREATE INDEX index_projects_api_created_at_id_desc ON projects USING btree (created_at, id DESC);
@@ -30122,6 +30147,8 @@ CREATE INDEX index_user_preferences_on_gitpod_enabled ON user_preferences USING
CREATE UNIQUE INDEX index_user_preferences_on_user_id ON user_preferences USING btree (user_id);
+CREATE INDEX index_user_project_callouts_on_project_id ON user_project_callouts USING btree (project_id);
+
CREATE INDEX index_user_statuses_on_clear_status_at_not_null ON user_statuses USING btree (clear_status_at) WHERE (clear_status_at IS NOT NULL);
CREATE INDEX index_user_statuses_on_user_id ON user_statuses USING btree (user_id);
@@ -30218,8 +30245,6 @@ CREATE INDEX index_vulnerabilities_on_last_edited_by_id ON vulnerabilities USING
CREATE INDEX index_vulnerabilities_on_milestone_id ON vulnerabilities USING btree (milestone_id);
-CREATE INDEX index_vulnerabilities_on_project_id_and_id ON vulnerabilities USING btree (project_id, id);
-
CREATE INDEX index_vulnerabilities_on_project_id_and_id_active_cis ON vulnerabilities USING btree (project_id, id) WHERE ((report_type = 7) AND (state = ANY (ARRAY[1, 4])));
CREATE INDEX index_vulnerabilities_on_project_id_and_state_and_severity ON vulnerabilities USING btree (project_id, state, severity);
@@ -30234,6 +30259,8 @@ CREATE INDEX index_vulnerabilities_on_state_case_id_desc ON vulnerabilities USIN
CREATE INDEX index_vulnerabilities_on_updated_by_id ON vulnerabilities USING btree (updated_by_id);
+CREATE INDEX index_vulnerabilities_project_id_and_id_on_default_branch ON vulnerabilities USING btree (project_id, id) WHERE (present_on_default_branch IS TRUE);
+
CREATE INDEX index_vulnerabilities_project_id_state_severity_default_branch ON vulnerabilities USING btree (project_id, state, severity, present_on_default_branch);
CREATE INDEX index_vulnerability_exports_on_author_id ON vulnerability_exports USING btree (author_id);
@@ -32043,6 +32070,9 @@ ALTER TABLE ONLY namespaces
ALTER TABLE ONLY issue_tracker_data
ADD CONSTRAINT fk_33921c0ee1 FOREIGN KEY (integration_id) REFERENCES integrations(id) ON DELETE CASCADE;
+ALTER TABLE ONLY user_project_callouts
+ ADD CONSTRAINT fk_33b4814f6b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY namespaces
ADD CONSTRAINT fk_3448c97865 FOREIGN KEY (push_rule_id) REFERENCES push_rules(id) ON DELETE SET NULL;
@@ -32739,6 +32769,9 @@ ALTER TABLE ONLY analytics_devops_adoption_segments
ALTER TABLE ONLY boards_epic_list_user_preferences
ADD CONSTRAINT fk_f5f2fe5c1f FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+ALTER TABLE ONLY user_project_callouts
+ ADD CONSTRAINT fk_f62dd11a33 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY cluster_agents
ADD CONSTRAINT fk_f7d43dee13 FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md
index 7c9dc9f2c03..a97c8611239 100644
--- a/doc/administration/repository_checks.md
+++ b/doc/administration/repository_checks.md
@@ -7,8 +7,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Repository checks **(FREE SELF)**
You can use [`git fsck`](https://git-scm.com/docs/git-fsck) to verify the integrity of all data
-committed to a repository. GitLab administrators can trigger this check for a project using the
-GitLab UI:
+committed to a repository. GitLab administrators can:
+
+- Manually trigger this check for a project, using the GitLab UI.
+- Schedule this check to run automatically for all projects.
+- Run this check from the command line.
+- Run a [Rake task](raketasks/check.md#repository-integrity) for checking Git repositories, which can be used to run
+ `git fsck` against all repositories and generate repository checksums, as a way to compare repositories on different
+ servers.
+
+## Check a project's repository using GitLab UI
+
+To check a project's repository using GitLab UI:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Overview > Projects**.
@@ -18,9 +28,7 @@ GitLab UI:
The checks run asynchronously so it may take a few minutes before the check result is visible on the
project page in the Admin Area. If the checks fail, see [what to do](#what-to-do-if-a-check-failed).
-This setting is off by default, because it can cause many false alarms.
-
-## Enable periodic checks
+## Enable repository checks for all projects
Instead of checking repositories manually, GitLab can be configured to run the checks periodically:
@@ -45,10 +53,27 @@ the start of Sunday.
Repositories with known check failures can be found at
`/admin/projects?last_repository_check_failed=1`.
+## Run a check using the command line
+
+You can run [`git fsck`](https://git-scm.com/docs/git-fsck) using the command line on repositories on
+[Gitaly servers](gitaly/index.md). To locate the repositories:
+
+1. Go to the storage location for repositories:
+ - For Omnibus GitLab installations, repositories are stored in the `/var/opt/gitlab/git-data/repositories` directory
+ by default.
+ - For GitLab Helm chart installations, repositories are stored in the `/home/git/repositories` directory inside the
+ Gitaly pod by default.
+1. [Identify the subdirectory that contains the repository](repository_storage_types.md#from-project-name-to-hashed-path)
+ that you need to check.
+1. Run the check. For example:
+
+ ```shell
+ sudo /opt/gitlab/embedded/bin/git -C /var/opt/gitlab/git-data/repositories/@hashed/0b/91/0b91...f9.git fsck
+ ```
+
## What to do if a check failed
-If a repository check fails, locate the error in the [`repocheck.log` file](logs/index.md#repochecklog) on
-disk at:
+If a repository check fails, locate the error in the [`repocheck.log` file](logs/index.md#repochecklog) on disk at:
- `/var/log/gitlab/gitlab-rails` for Omnibus GitLab installations.
- `/home/git/gitlab/log` for installations from source.
@@ -60,24 +85,3 @@ If periodic repository checks cause false alarms, you can clear all repository c
1. On the left sidebar, select **Settings > Repository** (`/admin/application_settings/repository`).
1. Expand the **Repository maintenance** section.
1. Select **Clear all repository checks**.
-
-## Run a check using the command line
-
-You can run [`git fsck`](https://git-scm.com/docs/git-fsck) using the command line on repositories
-on [Gitaly servers](gitaly/index.md). To locate the repositories:
-
-1. Go to the storage location for repositories:
- - For Omnibus GitLab installations, repositories are stored in the `/var/opt/gitlab/git-data/repositories` directory by default.
- - For GitLab Helm chart installations, repositories are stored in the `/home/git/repositories` directory inside the Gitaly pod by default.
-1. [Identify the subdirectory that contains the repository](repository_storage_types.md#from-project-name-to-hashed-path)
- that you need to check.
-
-To run a check (for example):
-
-```shell
-sudo /opt/gitlab/embedded/bin/git -C /var/opt/gitlab/git-data/repositories/@hashed/0b/91/0b91...f9.git fsck
-```
-
-You can also run [Rake tasks](raketasks/check.md#repository-integrity) for checking Git
-repositories, which can be used to run `git fsck` against all repositories and generate repository
-checksums, as a way to compare repositories on different servers.
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index c4672e61c99..521bb6adbf0 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -109,6 +109,8 @@ maximum of two directory levels from the repository's root. For example, the
`gemnasium-dependency_scanning` job is enabled if a repository contains either `Gemfile`,
`api/Gemfile`, or `api/client/Gemfile`, but not if the only supported dependency file is `api/v1/client/Gemfile`.
+For Java and Python, when a supported depedency file is detected, Dependency Scanning attempts to build the project and execute some Java or Python commands to get the list of dependencies. For all other projects, the lock file is parsed to obtain the list of dependencies without needing to build the project first.
+
When a supported dependency file is detected, all dependencies, including transitive dependencies are analyzed. There is no limit to the depth of nested or transitive dependencies that are analyzed.
The following languages and dependency managers are supported:
@@ -148,14 +150,13 @@ table.supported-languages ul {
<th>Language Versions</th>
<th>Package Manager</th>
<th>Supported files</th>
- <th>Analyzer</th>
<th><a href="#how-multiple-files-are-processed">Processes multiple files?</a></th>
</tr>
</thead>
<tbody>
<tr>
<td>Ruby</td>
- <td>Not applicable</td>
+ <td>All versions</td>
<td><a href="https://bundler.io/">Bundler</a></td>
<td>
<ul>
@@ -163,23 +164,20 @@ table.supported-languages ul {
<li><code>gems.locked</code></li>
</ul>
</td>
- <td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>Y</td>
</tr>
<tr>
<td>PHP</td>
- <td>Not applicable</td>
+ <td>All versions</td>
<td><a href="https://getcomposer.org/">Composer</a></td>
<td><code>composer.lock</code></td>
- <td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>Y</td>
</tr>
<tr>
<td>C</td>
- <td rowspan="2">Not applicable</td>
+ <td rowspan="2">All versions</td>
<td rowspan="2"><a href="https://conan.io/">Conan</a></td>
<td rowspan="2"><a href="https://docs.conan.io/en/latest/versioning/lockfiles.html"><code>conan.lock</code></a></td>
- <td rowspan="2"><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td rowspan="2">Y</td>
</tr>
<tr>
@@ -187,10 +185,9 @@ table.supported-languages ul {
</tr>
<tr>
<td>Go</td>
- <td>Not applicable</td>
+ <td>All versions</td>
<td><a href="https://go.dev/">Go</a></td>
<td><code>go.sum</code></td>
- <td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>Y</td>
</tr>
<tr>
@@ -211,41 +208,36 @@ table.supported-languages ul {
<li><code>build.gradle.kts</code></li>
</ul>
</td>
- <td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>N</td>
</tr>
<tr>
<td><a href="https://maven.apache.org/">Maven</a></td>
<td><code>pom.xml</code></td>
- <td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>N</td>
</tr>
<tr>
<td rowspan="2">JavaScript</td>
- <td>Not applicable</td>
+ <td>All versions</td>
<td><a href="https://www.npmjs.com/">npm</a></td>
<td>
<ul>
- <li><code>package-lock.json</code></li>
+ <li><code>package-lock.json</code><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-3">3</a></b></sup></li>
<li><code>npm-shrinkwrap.json</code></li>
</ul>
</td>
- <td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>Y</td>
</tr>
<tr>
- <td>Not applicable</td>
+ <td>All versions</td>
<td><a href="https://classic.yarnpkg.com/en/">yarn</a></td>
<td><code>yarn.lock</code></td>
- <td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>Y</td>
</tr>
<tr>
<td>.NET</td>
- <td rowspan="2">Not applicable</td>
+ <td rowspan="2">All versions</td>
<td rowspan="2"><a href="https://www.nuget.org/">NuGet</a></td>
<td rowspan="2"><a href="https://docs.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#enabling-lock-file"><code>packages.lock.json</code></a></td>
- <td rowspan="2"><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td rowspan="2">Y</td>
</tr>
<tr>
@@ -256,7 +248,6 @@ table.supported-languages ul {
<td rowspan="4">3.9</td>
<td><a href="https://setuptools.readthedocs.io/en/latest/">setuptools</a></td>
<td><code>setup.py</code></td>
- <td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>N</td>
</tr>
<tr>
@@ -268,7 +259,6 @@ table.supported-languages ul {
<li><code>requires.txt</code></li>
</ul>
</td>
- <td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>N</td>
</tr>
<tr>
@@ -276,24 +266,21 @@ table.supported-languages ul {
<td>
<ul>
<li><a href="https://pipenv.pypa.io/en/latest/basics/#example-pipfile-pipfile-lock"><code>Pipfile</code></a></li>
- <li><a href="https://pipenv.pypa.io/en/latest/basics/#example-pipfile-pipfile-lock"><code>Pipfile.lock</code></a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-3">3</a></b></sup></li>
+ <li><a href="https://pipenv.pypa.io/en/latest/basics/#example-pipfile-pipfile-lock"><code>Pipfile.lock</code></a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-4">4</a></b></sup></li>
</ul>
</td>
- <td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>N</td>
</tr>
<tr>
<td><a href="https://python-poetry.org/">Poetry</a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-5">5</a></b></sup></td>
<td><code>poetry.lock</code></td>
- <td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>N</td>
</tr>
<tr>
<td>Scala</td>
- <td>Not applicable</td>
- <td><a href="https://www.scala-sbt.org/">sbt</a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-4">4</a></b></sup></td>
+ <td>All versions</td>
+ <td><a href="https://www.scala-sbt.org/">sbt</a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-6">6</a></b></sup></td>
<td><code>build.sbt</code></td>
- <td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>N</td>
</tr>
</tbody>
@@ -317,6 +304,12 @@ table.supported-languages ul {
<li>
<a id="notes-regarding-supported-languages-and-package-managers-3"></a>
<p>
+ npm is only supported when `lockfileVersion = 1` or `lockfileVersion = 2`. Work to add support for `lockfileVersion = 3` is being tracked in issue <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/365176">GitLab#365176</a>.
+ </p>
+ </li>
+ <li>
+ <a id="notes-regarding-supported-languages-and-package-managers-4"></a>
+ <p>
The presence of a <code>Pipfile.lock</code> file alone will <i>not</i> trigger the analyzer; the presence of a <code>Pipfile</code> is
still required in order for the analyzer to be executed. However, if a <code>Pipfile.lock</code> file is found, it will be used by
<code>Gemnasium</code> to scan the exact package versions listed in this file.
@@ -328,12 +321,6 @@ table.supported-languages ul {
</p>
</li>
<li>
- <a id="notes-regarding-supported-languages-and-package-managers-4"></a>
- <p>
- Support for <a href="https://www.scala-sbt.org/">sbt</a> 1.3 and above was added in GitLab 13.9.
- </p>
- </li>
- <li>
<a id="notes-regarding-supported-languages-and-package-managers-5"></a>
<p>
Support for <a href="https://python-poetry.org/">Poetry</a> projects with a <code>poetry.lock</code> file was <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/7006">added in GitLab 15.0</a>.
@@ -341,6 +328,12 @@ table.supported-languages ul {
<a href="https://gitlab.com/gitlab-org/gitlab/-/issues/32774">Poetry's pyproject.toml support for dependency scanning.</a>
</p>
</li>
+ <li>
+ <a id="notes-regarding-supported-languages-and-package-managers-6"></a>
+ <p>
+ Support for <a href="https://www.scala-sbt.org/">sbt</a> 1.3 and above was added in GitLab 13.9.
+ </p>
+ </li>
</ol>
<!-- markdownlint-enable MD044 -->
diff --git a/doc/user/group/saml_sso/example_saml_config.md b/doc/user/group/saml_sso/example_saml_config.md
index fc3d582a291..97e8f9c54a3 100644
--- a/doc/user/group/saml_sso/example_saml_config.md
+++ b/doc/user/group/saml_sso/example_saml_config.md
@@ -68,14 +68,22 @@ fingerprint.
## Okta
-Basic SAML app configuration:
+Basic SAML app configuration for GitLab.com groups:
+
+![Okta basic SAML](img/Okta-GroupSAML.png)
-![Okta basic SAML](img/Okta-SAMLsetup.png)
+Basic SAML app configuration for GitLab self-managed:
+
+![Okta admin panel view](img/Okta-SM.png)
User claims and attributes:
![Okta Attributes](img/Okta-attributes.png)
+Groups attribute:
+
+![Okta Group attribute](img/Okta-GroupAttribute.png)
+
Advanced SAML app settings (defaults):
![Okta Advanced Settings](img/Okta-advancedsettings.png)
@@ -88,10 +96,6 @@ Sign on settings:
![Okta SAML settings](img/okta_saml_settings.png)
-Self-managed instance example:
-
-![Okta admin panel view](img/okta_admin_panel_v13_9.png)
-
Setting the username for the newly provisioned users when assigning them the SCIM app:
![Assigning SCIM app to users on Okta](img/okta_setting_username.png)
diff --git a/doc/user/group/saml_sso/img/Okta-GroupAttribute.png b/doc/user/group/saml_sso/img/Okta-GroupAttribute.png
new file mode 100644
index 00000000000..54c69053754
--- /dev/null
+++ b/doc/user/group/saml_sso/img/Okta-GroupAttribute.png
Binary files differ
diff --git a/doc/user/group/saml_sso/img/Okta-GroupSAML.png b/doc/user/group/saml_sso/img/Okta-GroupSAML.png
new file mode 100644
index 00000000000..7871e4ff82b
--- /dev/null
+++ b/doc/user/group/saml_sso/img/Okta-GroupSAML.png
Binary files differ
diff --git a/doc/user/group/saml_sso/img/Okta-SAMLsetup.png b/doc/user/group/saml_sso/img/Okta-SAMLsetup.png
deleted file mode 100644
index 1bd9bf4d7e9..00000000000
--- a/doc/user/group/saml_sso/img/Okta-SAMLsetup.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/saml_sso/img/Okta-SM.png b/doc/user/group/saml_sso/img/Okta-SM.png
new file mode 100644
index 00000000000..b335009fed9
--- /dev/null
+++ b/doc/user/group/saml_sso/img/Okta-SM.png
Binary files differ
diff --git a/doc/user/group/saml_sso/img/Okta-advancedsettings.png b/doc/user/group/saml_sso/img/Okta-advancedsettings.png
index 45e378d1d12..1478dc58ccd 100644
--- a/doc/user/group/saml_sso/img/Okta-advancedsettings.png
+++ b/doc/user/group/saml_sso/img/Okta-advancedsettings.png
Binary files differ
diff --git a/doc/user/group/saml_sso/img/Okta-attributes.png b/doc/user/group/saml_sso/img/Okta-attributes.png
index a3405e4de9b..38af72474d8 100644
--- a/doc/user/group/saml_sso/img/Okta-attributes.png
+++ b/doc/user/group/saml_sso/img/Okta-attributes.png
Binary files differ
diff --git a/doc/user/group/saml_sso/img/Okta-linkscert.png b/doc/user/group/saml_sso/img/Okta-linkscert.png
index 38cae415f7e..62db5d2b7e3 100644
--- a/doc/user/group/saml_sso/img/Okta-linkscert.png
+++ b/doc/user/group/saml_sso/img/Okta-linkscert.png
Binary files differ
diff --git a/doc/user/group/saml_sso/img/okta_admin_panel_v13_9.png b/doc/user/group/saml_sso/img/okta_admin_panel_v13_9.png
deleted file mode 100644
index 2ebb1f0112c..00000000000
--- a/doc/user/group/saml_sso/img/okta_admin_panel_v13_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/saml_sso/img/okta_saml_settings.png b/doc/user/group/saml_sso/img/okta_saml_settings.png
index ee275ece369..9c774b72d66 100644
--- a/doc/user/group/saml_sso/img/okta_saml_settings.png
+++ b/doc/user/group/saml_sso/img/okta_saml_settings.png
Binary files differ
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index a99f6585d9c..06287126978 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -531,6 +531,7 @@ user_custom_attributes: :gitlab_main
user_details: :gitlab_main
user_follow_users: :gitlab_main
user_group_callouts: :gitlab_main
+user_project_callouts: :gitlab_main
user_highest_roles: :gitlab_main
user_interacted_projects: :gitlab_main
user_permission_export_uploads: :gitlab_main
diff --git a/spec/factories/users/project_user_callouts.rb b/spec/factories/users/project_user_callouts.rb
new file mode 100644
index 00000000000..50e85315bb9
--- /dev/null
+++ b/spec/factories/users/project_user_callouts.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_callout, class: 'Users::ProjectCallout' do
+ feature_name { :awaiting_members_banner }
+
+ user
+ project
+ end
+end
diff --git a/spec/frontend/__helpers__/vue_test_utils_helper.js b/spec/frontend/__helpers__/vue_test_utils_helper.js
index 2aae91f8a39..75bd5df8cbf 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper.js
@@ -7,6 +7,20 @@ const vNodeContainsText = (vnode, text) =>
(vnode.children && vnode.children.filter((child) => vNodeContainsText(child, text)).length);
/**
+ * Create a VTU wrapper from an element.
+ *
+ * If a Vue instance manages the element, the wrapper is created
+ * with that Vue instance.
+ *
+ * @param {HTMLElement} element
+ * @param {Object} options
+ * @returns VTU wrapper
+ */
+const createWrapperFromElement = (element, options) =>
+ // eslint-disable-next-line no-underscore-dangle
+ createWrapper(element.__vue__ || element, options || {});
+
+/**
* Determines whether a `shallowMount` Wrapper contains text
* within one of it's slots. This will also work on Wrappers
* acquired with `find()`, but only if it's parent Wrapper
@@ -85,8 +99,7 @@ export const extendedWrapper = (wrapper) => {
if (!elements.length) {
return new ErrorWrapper(query);
}
-
- return createWrapper(elements[0], this.options || {});
+ return createWrapperFromElement(elements[0], this.options);
},
},
};
@@ -104,7 +117,7 @@ export const extendedWrapper = (wrapper) => {
);
const wrappers = elements.map((element) => {
- const elementWrapper = createWrapper(element, this.options || {});
+ const elementWrapper = createWrapperFromElement(element, this.options);
elementWrapper.selector = text;
return elementWrapper;
diff --git a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
index 3bb228f94b8..ae180c3b49d 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
@@ -6,6 +6,7 @@ import {
WrapperArray as VTUWrapperArray,
ErrorWrapper as VTUErrorWrapper,
} from '@vue/test-utils';
+import Vue from 'vue';
import {
extendedWrapper,
shallowMountExtended,
@@ -139,9 +140,12 @@ describe('Vue test utils helpers', () => {
const text = 'foo bar';
const options = { selector: 'div' };
const mockDiv = document.createElement('div');
+ const mockVm = new Vue({ render: (h) => h('div') }).$mount();
let wrapper;
beforeEach(() => {
+ jest.spyOn(vtu, 'createWrapper');
+
wrapper = extendedWrapper(
shallowMount({
template: `<div>foo bar</div>`,
@@ -164,7 +168,6 @@ describe('Vue test utils helpers', () => {
describe('when element is found', () => {
beforeEach(() => {
jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv]);
- jest.spyOn(vtu, 'createWrapper');
});
it('returns a VTU wrapper', () => {
@@ -172,14 +175,27 @@ describe('Vue test utils helpers', () => {
expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options);
expect(result).toBeInstanceOf(VTUWrapper);
+ expect(result.vm).toBeUndefined();
});
});
+ describe('when a Vue instance element is found', () => {
+ beforeEach(() => {
+ jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockVm.$el]);
+ });
+
+ it('returns a VTU wrapper', () => {
+ const result = wrapper[findMethod](text, options);
+
+ expect(vtu.createWrapper).toHaveBeenCalledWith(mockVm, wrapper.options);
+ expect(result).toBeInstanceOf(VTUWrapper);
+ expect(result.vm).toBeInstanceOf(Vue);
+ });
+ });
describe('when multiple elements are found', () => {
beforeEach(() => {
const mockSpan = document.createElement('span');
jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv, mockSpan]);
- jest.spyOn(vtu, 'createWrapper');
});
it('returns the first element as a VTU wrapper', () => {
@@ -187,6 +203,24 @@ describe('Vue test utils helpers', () => {
expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options);
expect(result).toBeInstanceOf(VTUWrapper);
+ expect(result.vm).toBeUndefined();
+ });
+ });
+
+ describe('when multiple Vue instances are found', () => {
+ beforeEach(() => {
+ const mockVm2 = new Vue({ render: (h) => h('span') }).$mount();
+ jest
+ .spyOn(testingLibrary, expectedQuery)
+ .mockImplementation(() => [mockVm.$el, mockVm2.$el]);
+ });
+
+ it('returns the first element as a VTU wrapper', () => {
+ const result = wrapper[findMethod](text, options);
+
+ expect(vtu.createWrapper).toHaveBeenCalledWith(mockVm, wrapper.options);
+ expect(result).toBeInstanceOf(VTUWrapper);
+ expect(result.vm).toBeInstanceOf(Vue);
});
});
@@ -211,12 +245,17 @@ describe('Vue test utils helpers', () => {
${'findAllByAltText'} | ${'queryAllByAltText'}
`('$findMethod', ({ findMethod, expectedQuery }) => {
const text = 'foo bar';
- const options = { selector: 'div' };
+ const options = { selector: 'li' };
const mockElements = [
document.createElement('li'),
document.createElement('li'),
document.createElement('li'),
];
+ const mockVms = [
+ new Vue({ render: (h) => h('li') }).$mount(),
+ new Vue({ render: (h) => h('li') }).$mount(),
+ new Vue({ render: (h) => h('li') }).$mount(),
+ ];
let wrapper;
beforeEach(() => {
@@ -245,9 +284,13 @@ describe('Vue test utils helpers', () => {
);
});
- describe('when elements are found', () => {
+ describe.each`
+ case | mockResult | isVueInstance
+ ${'HTMLElements'} | ${mockElements} | ${false}
+ ${'Vue instance elements'} | ${mockVms} | ${true}
+ `('when $case are found', ({ mockResult, isVueInstance }) => {
beforeEach(() => {
- jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockElements);
+ jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockResult);
});
it('returns a VTU wrapper array', () => {
@@ -257,7 +300,9 @@ describe('Vue test utils helpers', () => {
expect(
result.wrappers.every(
(resultWrapper) =>
- resultWrapper instanceof VTUWrapper && resultWrapper.options === wrapper.options,
+ resultWrapper instanceof VTUWrapper &&
+ resultWrapper.vm instanceof Vue === isVueInstance &&
+ resultWrapper.options === wrapper.options,
),
).toBe(true);
expect(result.length).toBe(3);
diff --git a/spec/frontend/runner/components/runner_bulk_delete_spec.js b/spec/frontend/runner/components/runner_bulk_delete_spec.js
index f5b56396cf1..cc679a52b34 100644
--- a/spec/frontend/runner/components/runner_bulk_delete_spec.js
+++ b/spec/frontend/runner/components/runner_bulk_delete_spec.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import { GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { s__ } from '~/locale';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -17,8 +18,8 @@ describe('RunnerBulkDelete', () => {
let mockState;
let mockCheckedRunnerIds;
- const findClearBtn = () => wrapper.findByTestId('clear-btn');
- const findDeleteBtn = () => wrapper.findByTestId('delete-btn');
+ const findClearBtn = () => wrapper.findByText(s__('Runners|Clear selection'));
+ const findDeleteBtn = () => wrapper.findByText(s__('Runners|Delete selected'));
const createComponent = () => {
const { cacheConfig, localMutations } = mockState;
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index a324fd70424..4ffde842819 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -627,6 +627,7 @@ project:
- security_trainings
- vulnerability_reads
- build_artifacts_size_refresh
+- project_callouts
award_emoji:
- awardable
- user
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index afb321c0777..2920278679f 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -147,6 +147,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_many(:build_trace_chunks).through(:builds).dependent(:restrict_with_error) }
it { is_expected.to have_many(:secure_files).class_name('Ci::SecureFile').dependent(:restrict_with_error) }
it { is_expected.to have_one(:build_artifacts_size_refresh).class_name('Projects::BuildArtifactsSizeRefresh') }
+ it { is_expected.to have_many(:project_callouts).class_name('Users::ProjectCallout').with_foreign_key(:project_id) }
# GitLab Pages
it { is_expected.to have_many(:pages_domains) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 12029b4151d..01b6b36db77 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -137,6 +137,7 @@ RSpec.describe User do
it { is_expected.to have_many(:callouts).class_name('Users::Callout') }
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout') }
it { is_expected.to have_many(:namespace_callouts).class_name('Users::NamespaceCallout') }
+ it { is_expected.to have_many(:project_callouts).class_name('Users::ProjectCallout') }
describe '#user_detail' do
it 'does not persist `user_detail` by default' do
@@ -6671,6 +6672,40 @@ RSpec.describe User do
end
end
+ describe '#dismissed_callout_for_project?' do
+ let_it_be(:user, refind: true) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:feature_name) { Users::ProjectCallout.feature_names.each_key.first }
+
+ context 'when no callout dismissal record exists' do
+ it 'returns false when no ignore_dismissal_earlier_than provided' do
+ expect(user.dismissed_callout_for_project?(feature_name: feature_name, project: project)).to eq false
+ end
+ end
+
+ context 'when dismissed callout exists' do
+ before_all do
+ create(:project_callout,
+ user: user,
+ project_id: project.id,
+ feature_name: feature_name,
+ dismissed_at: 4.months.ago)
+ end
+
+ it 'returns true when no ignore_dismissal_earlier_than provided' do
+ expect(user.dismissed_callout_for_project?(feature_name: feature_name, project: project)).to eq true
+ end
+
+ it 'returns true when ignore_dismissal_earlier_than is earlier than dismissed_at' do
+ expect(user.dismissed_callout_for_project?(feature_name: feature_name, project: project, ignore_dismissal_earlier_than: 6.months.ago)).to eq true
+ end
+
+ it 'returns false when ignore_dismissal_earlier_than is later than dismissed_at' do
+ expect(user.dismissed_callout_for_project?(feature_name: feature_name, project: project, ignore_dismissal_earlier_than: 3.months.ago)).to eq false
+ end
+ end
+ end
+
describe '#find_or_initialize_group_callout' do
let_it_be(:user, refind: true) { create(:user) }
let_it_be(:group) { create(:group) }
@@ -6715,6 +6750,50 @@ RSpec.describe User do
end
end
+ describe '#find_or_initialize_project_callout' do
+ let_it_be(:user, refind: true) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:feature_name) { Users::ProjectCallout.feature_names.each_key.first }
+
+ subject(:callout_with_source) do
+ user.find_or_initialize_project_callout(feature_name, project.id)
+ end
+
+ context 'when callout exists' do
+ let!(:callout) do
+ create(:project_callout, user: user, feature_name: feature_name, project_id: project.id)
+ end
+
+ it 'returns existing callout' do
+ expect(callout_with_source).to eq(callout)
+ end
+ end
+
+ context 'when callout does not exist' do
+ context 'when feature name is valid' do
+ it 'initializes a new callout' do
+ expect(callout_with_source).to be_a_new(Users::ProjectCallout)
+ end
+
+ it 'is valid' do
+ expect(callout_with_source).to be_valid
+ end
+ end
+
+ context 'when feature name is not valid' do
+ let(:feature_name) { 'notvalid' }
+
+ it 'initializes a new callout' do
+ expect(callout_with_source).to be_a_new(Users::ProjectCallout)
+ end
+
+ it 'is not valid' do
+ expect(callout_with_source).not_to be_valid
+ end
+ end
+ end
+ end
+
describe '#hook_attrs' do
let(:user) { create(:user) }
let(:user_attributes) do
diff --git a/spec/models/users/project_callout_spec.rb b/spec/models/users/project_callout_spec.rb
new file mode 100644
index 00000000000..214568b4de3
--- /dev/null
+++ b/spec/models/users/project_callout_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::ProjectCallout do
+ let_it_be(:user) { create_default(:user) }
+ let_it_be(:project) { create_default(:project) }
+ let_it_be(:callout) { create(:project_callout) }
+
+ it_behaves_like 'having unique enum values'
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:feature_name) }
+ it {
+ is_expected.to validate_uniqueness_of(:feature_name).scoped_to(:user_id, :project_id).ignoring_case_sensitivity
+ }
+ end
+end
diff --git a/spec/requests/users/project_callouts_spec.rb b/spec/requests/users/project_callouts_spec.rb
new file mode 100644
index 00000000000..98c00fef052
--- /dev/null
+++ b/spec/requests/users/project_callouts_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project callouts' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'POST /-/users/project_callouts' do
+ let(:params) { { feature_name: feature_name, project_id: project.id } }
+
+ subject { post project_callouts_path, params: params, headers: { 'ACCEPT' => 'application/json' } }
+
+ context 'with valid feature name and project' do
+ let(:feature_name) { Users::ProjectCallout.feature_names.each_key.first }
+
+ context 'when callout entry does not exist' do
+ it 'creates a callout entry with dismissed state' do
+ expect { subject }.to change { Users::ProjectCallout.count }.by(1)
+ end
+
+ it 'returns success' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when callout entry already exists' do
+ let!(:callout) do
+ create(:project_callout,
+ feature_name: Users::ProjectCallout.feature_names.each_key.first,
+ user: user,
+ project: project)
+ end
+
+ it 'returns success', :aggregate_failures do
+ expect { subject }.not_to change { Users::ProjectCallout.count }
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ context 'with invalid feature name' do
+ let(:feature_name) { 'bogus_feature_name' }
+
+ it 'returns bad request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+end
diff --git a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
index 149a384d31e..fe815aa6f1e 100644
--- a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
+++ b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
@@ -65,13 +65,19 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
## Strong
+ This example doesn't have an extension after the `example` keyword, so its
+ `source_specification` will be `commonmark`.
+
```````````````````````````````` example
__bold__
.
<p><strong>bold</strong></p>
````````````````````````````````
- ```````````````````````````````` example strong
+ This example has an extension after the `example` keyword, so its
+ `source_specification` will be `github`.
+
+ ```````````````````````````````` example some_extension_name
__bold with more text__
.
<p><strong>bold with more text</strong></p>
@@ -132,6 +138,10 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
## Strong but with HTML
+ This example has the `gitlab` keyword after the `example` keyword, so its
+ `source_specification` will be `gitlab`.
+
+
```````````````````````````````` example gitlab strong
<strong>
bold
diff --git a/spec/services/users/dismiss_project_callout_service_spec.rb b/spec/services/users/dismiss_project_callout_service_spec.rb
new file mode 100644
index 00000000000..73e50a4c37d
--- /dev/null
+++ b/spec/services/users/dismiss_project_callout_service_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::DismissProjectCalloutService do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ let(:params) { { feature_name: feature_name, project_id: project.id } }
+ let(:feature_name) { Users::ProjectCallout.feature_names.each_key.first }
+
+ subject(:execute) do
+ described_class.new(
+ container: nil, current_user: user, params: params
+ ).execute
+ end
+
+ it_behaves_like 'dismissing user callout', Users::ProjectCallout
+
+ it 'sets the project_id' do
+ expect(execute.project_id).to eq(project.id)
+ end
+ end
+end