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-11-24 00:10:02 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-24 00:10:02 +0300
commitabd24a801e8dbe0942558dd2b2cad90e7e01938e (patch)
tree27b00caff863be07505fa60e63ece1d55f10e2b9
parentfee19ef336bc64155e0d9e8697834ff529bb6d93 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/editor/source_editor.js48
-rw-r--r--app/assets/javascripts/editor/source_editor_instance.js4
-rw-r--r--app/experiments/application_experiment.rb4
-rw-r--r--app/models/clusters/agent.rb2
-rw-r--r--app/models/clusters/agents/activity_event.rb34
-rw-r--r--app/services/clusters/agent_tokens/create_service.rb12
-rw-r--r--app/views/users/show.html.haml3
-rw-r--r--config/feature_flags/development/ci_require_credit_card_on_free_plan.yml4
-rw-r--r--config/feature_flags/development/ci_require_credit_card_on_trial_plan.yml4
-rw-r--r--db/migrate/20211110014701_create_agent_activity_events.rb22
-rw-r--r--db/migrate/20211110015252_add_agent_activity_events_foreign_keys.rb35
-rw-r--r--db/schema_migrations/202111100147011
-rw-r--r--db/schema_migrations/202111100152521
-rw-r--r--db/structure.sql54
-rw-r--r--doc/api/members.md21
-rw-r--r--doc/user/project/merge_requests/squash_and_merge.md3
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml1
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/experiments/application_experiment_spec.rb124
-rw-r--r--spec/factories/clusters/agents/activity_events.rb13
-rw-r--r--spec/features/users/show_spec.rb24
-rw-r--r--spec/frontend/editor/helpers.js33
-rw-r--r--spec/frontend/editor/source_editor_extension_spec.js24
-rw-r--r--spec/frontend/editor/source_editor_instance_spec.js158
-rw-r--r--spec/frontend/editor/source_editor_markdown_ext_spec.js2
-rw-r--r--spec/frontend/editor/source_editor_spec.js107
-rw-r--r--spec/models/clusters/agents/activity_event_spec.rb32
-rw-r--r--spec/services/clusters/agent_tokens/create_service_spec.rb19
-rw-r--r--spec/support/shared_examples/features/page_description_shared_examples.rb10
30 files changed, 506 insertions, 301 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 52fd1c1ceef..dc5302cb2fd 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-9e5735cc1b202ce5e5657ad83eeeb7b037141e09
+4e18794f846ad0d27bea3443caa2b51cd9afd722
diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js
index 81ddf8d77fa..26fbd1f4d8a 100644
--- a/app/assets/javascripts/editor/source_editor.js
+++ b/app/assets/javascripts/editor/source_editor.js
@@ -26,26 +26,6 @@ export default class SourceEditor {
registerLanguages(...languages);
}
- static pushToImportsArray(arr, toImport) {
- arr.push(import(toImport));
- }
-
- static loadExtensions(extensions) {
- if (!extensions) {
- return Promise.resolve();
- }
- const promises = [];
- const extensionsArray = typeof extensions === 'string' ? extensions.split(',') : extensions;
-
- extensionsArray.forEach((ext) => {
- const prefix = ext.includes('/') ? '' : 'editor/';
- const trimmedExt = ext.replace(/^\//, '').trim();
- SourceEditor.pushToImportsArray(promises, `~/${prefix}${trimmedExt}`);
- });
-
- return Promise.all(promises);
- }
-
static mixIntoInstance(source, inst) {
if (!inst) {
return;
@@ -71,23 +51,6 @@ export default class SourceEditor {
});
}
- static manageDefaultExtensions(instance, el, extensions) {
- SourceEditor.loadExtensions(extensions, instance)
- .then((modules) => {
- if (modules) {
- modules.forEach((module) => {
- instance.use(module.default);
- });
- }
- })
- .then(() => {
- el.dispatchEvent(new Event(EDITOR_READY_EVENT));
- })
- .catch((e) => {
- throw e;
- });
- }
-
static createEditorModel({
blobPath,
blobContent,
@@ -187,7 +150,6 @@ export default class SourceEditor {
blobContent = '',
blobOriginalContent = '',
blobGlobalId = uuids()[0],
- extensions = [],
isDiff = false,
...instanceOptions
} = {}) {
@@ -218,9 +180,8 @@ export default class SourceEditor {
SourceEditor.instanceDisposeModels(this, instance, model);
});
- SourceEditor.manageDefaultExtensions(instance, el, extensions);
-
this.instances.push(instance);
+ el.dispatchEvent(new CustomEvent(EDITOR_READY_EVENT, { instance }));
return instance;
}
@@ -234,11 +195,4 @@ export default class SourceEditor {
dispose() {
this.instances.forEach((instance) => instance.dispose());
}
-
- use(exts) {
- this.instances.forEach((inst) => {
- inst.use(exts);
- });
- return this;
- }
}
diff --git a/app/assets/javascripts/editor/source_editor_instance.js b/app/assets/javascripts/editor/source_editor_instance.js
index e0ca4ea518b..052a73d7091 100644
--- a/app/assets/javascripts/editor/source_editor_instance.js
+++ b/app/assets/javascripts/editor/source_editor_instance.js
@@ -73,9 +73,7 @@ export default class EditorInstance {
if (methodExtension) {
const extension = extensionsStore.get(methodExtension);
- return (...args) => {
- return extension.api[prop].call(seInstance, ...args, receiver);
- };
+ return (...args) => extension.api[prop].call(seInstance, receiver, ...args);
}
return Reflect.get(seInstance[prop] ? seInstance : target, prop, receiver);
},
diff --git a/app/experiments/application_experiment.rb b/app/experiments/application_experiment.rb
index 37d87baf30b..22c632a08aa 100644
--- a/app/experiments/application_experiment.rb
+++ b/app/experiments/application_experiment.rb
@@ -32,8 +32,8 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
subject = value[:namespace] || value[:group] || value[:project] || value[:user] || value[:actor]
return unless ExperimentSubject.valid_subject?(subject)
- variant = :experimental if @variant_name != :control
- Experiment.add_subject(name, variant: variant || :control, subject: subject)
+ variant_name = :experimental if variant&.name != 'control'
+ Experiment.add_subject(name, variant: variant_name || :control, subject: subject)
end
def record!
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index cf6d95fc6df..f04ac6f1722 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -16,6 +16,8 @@ module Clusters
has_many :project_authorizations, class_name: 'Clusters::Agents::ProjectAuthorization'
has_many :authorized_projects, class_name: '::Project', through: :project_authorizations, source: :project
+ has_many :activity_events, -> { in_timeline_order }, class_name: 'Clusters::Agents::ActivityEvent', inverse_of: :agent
+
scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }
diff --git a/app/models/clusters/agents/activity_event.rb b/app/models/clusters/agents/activity_event.rb
new file mode 100644
index 00000000000..668aba74821
--- /dev/null
+++ b/app/models/clusters/agents/activity_event.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Agents
+ class ActivityEvent < ApplicationRecord
+ include NullifyIfBlank
+
+ self.table_name = 'agent_activity_events'
+
+ belongs_to :agent, class_name: 'Clusters::Agent', optional: false
+ belongs_to :user
+ belongs_to :agent_token, class_name: 'Clusters::AgentToken'
+
+ scope :in_timeline_order, -> { order(recorded_at: :desc, id: :desc) }
+
+ validates :recorded_at, :kind, :level, presence: true
+
+ nullify_if_blank :detail
+
+ enum kind: {
+ token_created: 0
+ }, _prefix: true
+
+ enum level: {
+ debug: 0,
+ info: 1,
+ warn: 2,
+ error: 3,
+ fatal: 4,
+ unknown: 5
+ }, _prefix: true
+ end
+ end
+end
diff --git a/app/services/clusters/agent_tokens/create_service.rb b/app/services/clusters/agent_tokens/create_service.rb
index ae2617f510b..5b8a0e46a6c 100644
--- a/app/services/clusters/agent_tokens/create_service.rb
+++ b/app/services/clusters/agent_tokens/create_service.rb
@@ -11,6 +11,8 @@ module Clusters
token = ::Clusters::AgentToken.new(filtered_params.merge(created_by_user: current_user))
if token.save
+ log_activity_event!(token)
+
ServiceResponse.success(payload: { secret: token.token, token: token })
else
ServiceResponse.error(message: token.errors.full_messages)
@@ -26,6 +28,16 @@ module Clusters
def filtered_params
params.slice(*ALLOWED_PARAMS)
end
+
+ def log_activity_event!(token)
+ token.agent.activity_events.create!(
+ kind: :token_created,
+ level: :info,
+ recorded_at: token.created_at,
+ user: current_user,
+ agent_token: token
+ )
+ end
end
end
end
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 522f0f771cd..ca276519758 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -2,8 +2,7 @@
- @hide_breadcrumbs = true
- @no_container = true
- page_title user_display_name(@user)
-- page_description @user.bio
-- header_title @user.name, user_path(@user)
+- page_description @user.bio unless @user.blocked? || !@user.confirmed?
- page_itemtype 'http://schema.org/Person'
- link_classes = "flex-grow-1 mx-1 "
diff --git a/config/feature_flags/development/ci_require_credit_card_on_free_plan.yml b/config/feature_flags/development/ci_require_credit_card_on_free_plan.yml
index 868c3c84649..7e5795de6a0 100644
--- a/config/feature_flags/development/ci_require_credit_card_on_free_plan.yml
+++ b/config/feature_flags/development/ci_require_credit_card_on_free_plan.yml
@@ -1,8 +1,8 @@
---
name: ci_require_credit_card_on_free_plan
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61152
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61152
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330104
milestone: '13.12'
type: development
-group: group::pipeline execution
+group: group::fulfillment
default_enabled: false
diff --git a/config/feature_flags/development/ci_require_credit_card_on_trial_plan.yml b/config/feature_flags/development/ci_require_credit_card_on_trial_plan.yml
index 6a946f0959c..578101a1ba4 100644
--- a/config/feature_flags/development/ci_require_credit_card_on_trial_plan.yml
+++ b/config/feature_flags/development/ci_require_credit_card_on_trial_plan.yml
@@ -1,8 +1,8 @@
---
name: ci_require_credit_card_on_trial_plan
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61152
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61152
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330105
milestone: '13.12'
type: development
-group: group::pipeline execution
+group: group::fulfillment
default_enabled: false
diff --git a/db/migrate/20211110014701_create_agent_activity_events.rb b/db/migrate/20211110014701_create_agent_activity_events.rb
new file mode 100644
index 00000000000..11b9c6d03b3
--- /dev/null
+++ b/db/migrate/20211110014701_create_agent_activity_events.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class CreateAgentActivityEvents < Gitlab::Database::Migration[1.0]
+ def change
+ create_table :agent_activity_events do |t|
+ t.bigint :agent_id, null: false
+ t.bigint :user_id, index: { where: 'user_id IS NOT NULL' }
+ t.bigint :project_id, index: { where: 'project_id IS NOT NULL' }
+ t.bigint :merge_request_id, index: { where: 'merge_request_id IS NOT NULL' }
+ t.bigint :agent_token_id, index: { where: 'agent_token_id IS NOT NULL' }
+
+ t.datetime_with_timezone :recorded_at, null: false
+ t.integer :kind, limit: 2, null: false
+ t.integer :level, limit: 2, null: false
+
+ t.binary :sha
+ t.text :detail, limit: 255
+
+ t.index [:agent_id, :recorded_at, :id]
+ end
+ end
+end
diff --git a/db/migrate/20211110015252_add_agent_activity_events_foreign_keys.rb b/db/migrate/20211110015252_add_agent_activity_events_foreign_keys.rb
new file mode 100644
index 00000000000..fcbafcccb06
--- /dev/null
+++ b/db/migrate/20211110015252_add_agent_activity_events_foreign_keys.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class AddAgentActivityEventsForeignKeys < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :agent_activity_events, :cluster_agents, column: :agent_id, on_delete: :cascade
+ add_concurrent_foreign_key :agent_activity_events, :users, column: :user_id, on_delete: :nullify
+ add_concurrent_foreign_key :agent_activity_events, :projects, column: :project_id, on_delete: :nullify
+ add_concurrent_foreign_key :agent_activity_events, :merge_requests, column: :merge_request_id, on_delete: :nullify
+ add_concurrent_foreign_key :agent_activity_events, :cluster_agent_tokens, column: :agent_token_id, on_delete: :nullify
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists :agent_activity_events, column: :agent_id
+ end
+
+ with_lock_retries do
+ remove_foreign_key_if_exists :agent_activity_events, column: :user_id
+ end
+
+ with_lock_retries do
+ remove_foreign_key_if_exists :agent_activity_events, column: :project_id
+ end
+
+ with_lock_retries do
+ remove_foreign_key_if_exists :agent_activity_events, column: :merge_request_id
+ end
+
+ with_lock_retries do
+ remove_foreign_key_if_exists :agent_activity_events, column: :agent_token_id
+ end
+ end
+end
diff --git a/db/schema_migrations/20211110014701 b/db/schema_migrations/20211110014701
new file mode 100644
index 00000000000..fe3721eb055
--- /dev/null
+++ b/db/schema_migrations/20211110014701
@@ -0,0 +1 @@
+1c5f65a25c9cf81a50bd9ffa2e74e2621cff04e58a2f90b19c66741ebb459d3e \ No newline at end of file
diff --git a/db/schema_migrations/20211110015252 b/db/schema_migrations/20211110015252
new file mode 100644
index 00000000000..06a6a5b0ad7
--- /dev/null
+++ b/db/schema_migrations/20211110015252
@@ -0,0 +1 @@
+4038c269ce9c47ca9327fb1b81bb588e9065f0821f291d17c7965d7f8fe1f275 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index caa1f603df3..c9b3243f025 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9721,6 +9721,30 @@ CREATE SEQUENCE abuse_reports_id_seq
ALTER SEQUENCE abuse_reports_id_seq OWNED BY abuse_reports.id;
+CREATE TABLE agent_activity_events (
+ id bigint NOT NULL,
+ agent_id bigint NOT NULL,
+ user_id bigint,
+ project_id bigint,
+ merge_request_id bigint,
+ agent_token_id bigint,
+ recorded_at timestamp with time zone NOT NULL,
+ kind smallint NOT NULL,
+ level smallint NOT NULL,
+ sha bytea,
+ detail text,
+ CONSTRAINT check_068205e735 CHECK ((char_length(detail) <= 255))
+);
+
+CREATE SEQUENCE agent_activity_events_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE agent_activity_events_id_seq OWNED BY agent_activity_events.id;
+
CREATE TABLE agent_group_authorizations (
id bigint NOT NULL,
group_id bigint NOT NULL,
@@ -21064,6 +21088,8 @@ ALTER SEQUENCE zoom_meetings_id_seq OWNED BY zoom_meetings.id;
ALTER TABLE ONLY abuse_reports ALTER COLUMN id SET DEFAULT nextval('abuse_reports_id_seq'::regclass);
+ALTER TABLE ONLY agent_activity_events ALTER COLUMN id SET DEFAULT nextval('agent_activity_events_id_seq'::regclass);
+
ALTER TABLE ONLY agent_group_authorizations ALTER COLUMN id SET DEFAULT nextval('agent_group_authorizations_id_seq'::regclass);
ALTER TABLE ONLY agent_project_authorizations ALTER COLUMN id SET DEFAULT nextval('agent_project_authorizations_id_seq'::regclass);
@@ -22412,6 +22438,9 @@ ALTER TABLE ONLY gitlab_partitions_static.product_analytics_events_experimental_
ALTER TABLE ONLY abuse_reports
ADD CONSTRAINT abuse_reports_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY agent_activity_events
+ ADD CONSTRAINT agent_activity_events_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY agent_group_authorizations
ADD CONSTRAINT agent_group_authorizations_pkey PRIMARY KEY (id);
@@ -24907,6 +24936,16 @@ CREATE UNIQUE INDEX idx_vulnerability_issue_links_on_vulnerability_id_and_link_t
CREATE INDEX index_abuse_reports_on_user_id ON abuse_reports USING btree (user_id);
+CREATE INDEX index_agent_activity_events_on_agent_id_and_recorded_at_and_id ON agent_activity_events USING btree (agent_id, recorded_at, id);
+
+CREATE INDEX index_agent_activity_events_on_agent_token_id ON agent_activity_events USING btree (agent_token_id) WHERE (agent_token_id IS NOT NULL);
+
+CREATE INDEX index_agent_activity_events_on_merge_request_id ON agent_activity_events USING btree (merge_request_id) WHERE (merge_request_id IS NOT NULL);
+
+CREATE INDEX index_agent_activity_events_on_project_id ON agent_activity_events USING btree (project_id) WHERE (project_id IS NOT NULL);
+
+CREATE INDEX index_agent_activity_events_on_user_id ON agent_activity_events USING btree (user_id) WHERE (user_id IS NOT NULL);
+
CREATE UNIQUE INDEX index_agent_group_authorizations_on_agent_id_and_group_id ON agent_group_authorizations USING btree (agent_id, group_id);
CREATE INDEX index_agent_group_authorizations_on_group_id ON agent_group_authorizations USING btree (group_id);
@@ -28803,6 +28842,9 @@ ALTER TABLE ONLY import_failures
ALTER TABLE ONLY project_ci_cd_settings
ADD CONSTRAINT fk_24c15d2f2e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY agent_activity_events
+ ADD CONSTRAINT fk_256c631779 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY epics
ADD CONSTRAINT fk_25b99c1be3 FOREIGN KEY (parent_id) REFERENCES epics(id) ON DELETE CASCADE;
@@ -28875,6 +28917,9 @@ ALTER TABLE ONLY bulk_import_exports
ALTER TABLE ONLY ci_builds
ADD CONSTRAINT fk_3a9eaa254d FOREIGN KEY (stage_id) REFERENCES ci_stages(id) ON DELETE CASCADE;
+ALTER TABLE ONLY agent_activity_events
+ ADD CONSTRAINT fk_3af186389b FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY issues
ADD CONSTRAINT fk_3b8c72ea56 FOREIGN KEY (sprint_id) REFERENCES sprints(id) ON DELETE SET NULL;
@@ -29319,6 +29364,12 @@ ALTER TABLE ONLY geo_event_log
ALTER TABLE ONLY issues
ADD CONSTRAINT fk_c63cbf6c25 FOREIGN KEY (closed_by_id) REFERENCES users(id) ON DELETE SET NULL;
+ALTER TABLE ONLY agent_activity_events
+ ADD CONSTRAINT fk_c815368376 FOREIGN KEY (agent_id) REFERENCES cluster_agents(id) ON DELETE CASCADE;
+
+ALTER TABLE ONLY agent_activity_events
+ ADD CONSTRAINT fk_c8b006d40f FOREIGN KEY (agent_token_id) REFERENCES cluster_agent_tokens(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY issue_links
ADD CONSTRAINT fk_c900194ff2 FOREIGN KEY (source_id) REFERENCES issues(id) ON DELETE CASCADE;
@@ -29373,6 +29424,9 @@ ALTER TABLE ONLY geo_event_log
ALTER TABLE ONLY lists
ADD CONSTRAINT fk_d6cf4279f7 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+ALTER TABLE ONLY agent_activity_events
+ ADD CONSTRAINT fk_d6f785c9fc FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY metrics_users_starred_dashboards
ADD CONSTRAINT fk_d76a2b9a8c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/doc/api/members.md b/doc/api/members.md
index ce276487f21..497c9a00dfd 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -65,7 +65,8 @@ Example response:
"web_url": "http://192.168.1.8:3000/root",
"expires_at": "2012-10-22T14:13:35Z",
"access_level": 30,
- "group_saml_identity": null
+ "group_saml_identity": null,
+ "membership_state": "active"
},
{
"id": 2,
@@ -81,7 +82,8 @@ Example response:
"extern_uid":"ABC-1234567890",
"provider": "group_saml",
"saml_provider_id": 10
- }
+ },
+ "membership_state": "active"
}
]
```
@@ -126,7 +128,8 @@ Example response:
"web_url": "http://192.168.1.8:3000/root",
"expires_at": "2012-10-22T14:13:35Z",
"access_level": 30,
- "group_saml_identity": null
+ "group_saml_identity": null,
+ "membership_state": "active"
},
{
"id": 2,
@@ -142,7 +145,8 @@ Example response:
"extern_uid":"ABC-1234567890",
"provider": "group_saml",
"saml_provider_id": 10
- }
+ },
+ "membership_state": "active"
},
{
"id": 3,
@@ -153,7 +157,8 @@ Example response:
"web_url": "http://192.168.1.8:3000/root",
"expires_at": "2012-11-22T14:13:35Z",
"access_level": 30,
- "group_saml_identity": null
+ "group_saml_identity": null,
+ "membership_state": "active"
}
]
```
@@ -191,7 +196,8 @@ Example response:
"email": "john@example.com",
"created_at": "2012-10-22T14:13:35Z",
"expires_at": null,
- "group_saml_identity": null
+ "group_saml_identity": null,
+ "membership_state": "active"
}
```
@@ -229,7 +235,8 @@ Example response:
"access_level": 30,
"email": "john@example.com",
"expires_at": null,
- "group_saml_identity": null
+ "group_saml_identity": null,
+ "membership_state": "active"
}
```
diff --git a/doc/user/project/merge_requests/squash_and_merge.md b/doc/user/project/merge_requests/squash_and_merge.md
index c3fc2fa871f..e551f65b75d 100644
--- a/doc/user/project/merge_requests/squash_and_merge.md
+++ b/doc/user/project/merge_requests/squash_and_merge.md
@@ -30,9 +30,6 @@ The squashed commit in this example is followed by a merge commit, because the m
The squashed commit's default commit message is taken from the merge request title.
-NOTE:
-This only takes effect if there are at least 2 commits. As there is nothing to squash, the commit message does not change if there is only 1 commit.
-
It can be customized before merging a merge request.
![A squash commit message editor](img/squash_mr_message.png)
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index 66157e998a0..5a553a6ef56 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -1,4 +1,5 @@
abuse_reports: :gitlab_main
+agent_activity_events: :gitlab_main
agent_group_authorizations: :gitlab_main
agent_project_authorizations: :gitlab_main
alert_management_alert_assignees: :gitlab_main
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b9f393c43ea..adc07db990c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -38540,9 +38540,15 @@ msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state."
msgstr ""
+msgid "VulnerabilityManagement|Summary, detailed description, steps to reproduce, etc."
+msgstr ""
+
msgid "VulnerabilityManagement|Verified as fixed or mitigated"
msgstr ""
+msgid "VulnerabilityManagement|Vulnerability name or type. Ex: Cross-site scripting"
+msgstr ""
+
msgid "VulnerabilityManagement|Will not fix or a false-positive"
msgstr ""
diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb
index b0788eec808..dda8ca66eee 100644
--- a/spec/experiments/application_experiment_spec.rb
+++ b/spec/experiments/application_experiment_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe ApplicationExperiment, :experiment do
- subject { described_class.new('namespaced/stub', **context) }
+ subject(:application_experiment) { described_class.new('namespaced/stub', **context) }
let(:context) { {} }
let(:feature_definition) { { name: 'namespaced_stub', type: 'experiment', default_enabled: false } }
@@ -15,7 +15,7 @@ RSpec.describe ApplicationExperiment, :experiment do
end
before do
- allow(subject).to receive(:enabled?).and_return(true)
+ allow(application_experiment).to receive(:enabled?).and_return(true)
end
it "doesn't raise an exception without a defined control" do
@@ -26,7 +26,7 @@ RSpec.describe ApplicationExperiment, :experiment do
describe "#enabled?" do
before do
- allow(subject).to receive(:enabled?).and_call_original
+ allow(application_experiment).to receive(:enabled?).and_call_original
allow(Feature::Definition).to receive(:get).and_return('_instance_')
allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
@@ -34,25 +34,25 @@ RSpec.describe ApplicationExperiment, :experiment do
end
it "is enabled when all criteria are met" do
- expect(subject).to be_enabled
+ expect(application_experiment).to be_enabled
end
it "isn't enabled if the feature definition doesn't exist" do
expect(Feature::Definition).to receive(:get).with('namespaced_stub').and_return(nil)
- expect(subject).not_to be_enabled
+ expect(application_experiment).not_to be_enabled
end
it "isn't enabled if we're not in dev or dotcom environments" do
expect(Gitlab).to receive(:dev_env_or_com?).and_return(false)
- expect(subject).not_to be_enabled
+ expect(application_experiment).not_to be_enabled
end
it "isn't enabled if the feature flag state is :off" do
expect(Feature).to receive(:get).with('namespaced_stub').and_return(double(state: :off))
- expect(subject).not_to be_enabled
+ expect(application_experiment).not_to be_enabled
end
end
@@ -60,11 +60,11 @@ RSpec.describe ApplicationExperiment, :experiment do
let(:should_track) { true }
before do
- allow(subject).to receive(:should_track?).and_return(should_track)
+ allow(application_experiment).to receive(:should_track?).and_return(should_track)
end
it "tracks the assignment", :snowplow do
- subject.publish
+ application_experiment.publish
expect_snowplow_event(
category: 'namespaced/stub',
@@ -74,24 +74,24 @@ RSpec.describe ApplicationExperiment, :experiment do
end
it "publishes to the client" do
- expect(subject).to receive(:publish_to_client)
+ expect(application_experiment).to receive(:publish_to_client)
- subject.publish
+ application_experiment.publish
end
it "publishes to the database if we've opted for that" do
- subject.record!
+ application_experiment.record!
- expect(subject).to receive(:publish_to_database)
+ expect(application_experiment).to receive(:publish_to_database)
- subject.publish
+ application_experiment.publish
end
context 'when we should not track' do
let(:should_track) { false }
it 'does not track an event to Snowplow', :snowplow do
- subject.publish
+ application_experiment.publish
expect_no_snowplow_event
end
@@ -102,13 +102,13 @@ RSpec.describe ApplicationExperiment, :experiment do
signature = { key: '86208ac54ca798e11f127e8b23ec396a', variant: 'control' }
expect(Gon).to receive(:push).with({ experiment: { 'namespaced/stub' => hash_including(signature) } }, true)
- subject.publish_to_client
+ application_experiment.publish_to_client
end
it "handles when Gon raises exceptions (like when it can't be pushed into)" do
expect(Gon).to receive(:push).and_raise(NoMethodError)
- expect { subject.publish_to_client }.not_to raise_error
+ expect { application_experiment.publish_to_client }.not_to raise_error
end
context 'when we should not track' do
@@ -117,7 +117,7 @@ RSpec.describe ApplicationExperiment, :experiment do
it 'returns early' do
expect(Gon).not_to receive(:push)
- subject.publish_to_client
+ application_experiment.publish_to_client
end
end
end
@@ -125,13 +125,15 @@ RSpec.describe ApplicationExperiment, :experiment do
describe '#publish_to_database' do
using RSpec::Parameterized::TableSyntax
+ let(:publish_to_database) { application_experiment.publish_to_database }
+
shared_examples 'does not record to the database' do
it 'does not create an experiment record' do
- expect { subject.publish_to_database }.not_to change(Experiment, :count)
+ expect { publish_to_database }.not_to change(Experiment, :count)
end
it 'does not create an experiment subject record' do
- expect { subject.publish_to_database }.not_to change(ExperimentSubject, :count)
+ expect { publish_to_database }.not_to change(ExperimentSubject, :count)
end
end
@@ -139,16 +141,16 @@ RSpec.describe ApplicationExperiment, :experiment do
let(:context) { { context_key => context_value } }
where(:context_key, :context_value, :object_type) do
- :namespace | build(:namespace) | :namespace
- :group | build(:namespace) | :namespace
- :project | build(:project) | :project
- :user | build(:user) | :user
- :actor | build(:user) | :user
+ :namespace | build(:namespace, id: non_existing_record_id) | :namespace
+ :group | build(:namespace, id: non_existing_record_id) | :namespace
+ :project | build(:project, id: non_existing_record_id) | :project
+ :user | build(:user, id: non_existing_record_id) | :user
+ :actor | build(:user, id: non_existing_record_id) | :user
end
with_them do
it 'creates an experiment and experiment subject record' do
- expect { subject.publish_to_database }.to change(Experiment, :count).by(1)
+ expect { publish_to_database }.to change(Experiment, :count).by(1)
expect(Experiment.last.name).to eq('namespaced/stub')
expect(ExperimentSubject.last.send(object_type)).to eq(context[context_key])
@@ -156,6 +158,16 @@ RSpec.describe ApplicationExperiment, :experiment do
end
end
+ context "when experiment hasn't ran" do
+ let(:context) { { user: create(:user) } }
+
+ it 'sets a variant on the experiment subject' do
+ publish_to_database
+
+ expect(ExperimentSubject.last.variant).to eq('control')
+ end
+ end
+
context 'when there is not a usable subject' do
let(:context) { { context_key => context_value } }
@@ -183,15 +195,15 @@ RSpec.describe ApplicationExperiment, :experiment do
end
it "doesn't track if we shouldn't track" do
- allow(subject).to receive(:should_track?).and_return(false)
+ allow(application_experiment).to receive(:should_track?).and_return(false)
- subject.track(:action)
+ application_experiment.track(:action)
expect_no_snowplow_event
end
it "tracks the event with the expected arguments and merged contexts" do
- subject.track(:action, property: '_property_', context: [fake_context])
+ application_experiment.track(:action, property: '_property_', context: [fake_context])
expect_snowplow_event(
category: 'namespaced/stub',
@@ -233,7 +245,7 @@ RSpec.describe ApplicationExperiment, :experiment do
describe "#key_for" do
it "generates MD5 hashes" do
- expect(subject.key_for(foo: :bar)).to eq('6f9ac12afdb9b58c2f19a136d09f9153')
+ expect(application_experiment.key_for(foo: :bar)).to eq('6f9ac12afdb9b58c2f19a136d09f9153')
end
end
@@ -256,26 +268,26 @@ RSpec.describe ApplicationExperiment, :experiment do
with_them do
it "returns the url or nil if invalid" do
allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
- expect(subject.process_redirect_url(url)).to eq(processed_url)
+ expect(application_experiment.process_redirect_url(url)).to eq(processed_url)
end
it "considers all urls invalid when not on dev or com" do
allow(Gitlab).to receive(:dev_env_or_com?).and_return(false)
- expect(subject.process_redirect_url(url)).to be_nil
+ expect(application_experiment.process_redirect_url(url)).to be_nil
end
end
it "generates the correct urls based on where the engine was mounted" do
- url = Rails.application.routes.url_helpers.experiment_redirect_url(subject, url: 'https://docs.gitlab.com')
- expect(url).to include("/-/experiment/namespaced%2Fstub:#{subject.context.key}?https://docs.gitlab.com")
+ url = Rails.application.routes.url_helpers.experiment_redirect_url(application_experiment, url: 'https://docs.gitlab.com')
+ expect(url).to include("/-/experiment/namespaced%2Fstub:#{application_experiment.context.key}?https://docs.gitlab.com")
end
end
context "when resolving variants" do
it "uses the default value as specified in the yaml" do
- expect(Feature).to receive(:enabled?).with('namespaced_stub', subject, type: :experiment, default_enabled: :yaml)
+ expect(Feature).to receive(:enabled?).with('namespaced_stub', application_experiment, type: :experiment, default_enabled: :yaml)
- expect(subject.variant.name).to eq('control')
+ expect(application_experiment.variant.name).to eq('control')
end
context "when rolled out to 100%" do
@@ -284,10 +296,10 @@ RSpec.describe ApplicationExperiment, :experiment do
end
it "returns the first variant name" do
- subject.try(:variant1) {}
- subject.try(:variant2) {}
+ application_experiment.try(:variant1) {}
+ application_experiment.try(:variant2) {}
- expect(subject.variant.name).to eq('variant1')
+ expect(application_experiment.variant.name).to eq('variant1')
end
end
end
@@ -298,18 +310,18 @@ RSpec.describe ApplicationExperiment, :experiment do
before do
allow(Gitlab::Experiment::Configuration).to receive(:cache).and_call_original
- cache.clear(key: subject.name)
+ cache.clear(key: application_experiment.name)
- subject.use { } # setup the control
- subject.try { } # setup the candidate
+ application_experiment.use { } # setup the control
+ application_experiment.try { } # setup the candidate
end
it "caches the variant determined by the variant resolver" do
- expect(subject.variant.name).to eq('candidate') # we should be in the experiment
+ expect(application_experiment.variant.name).to eq('candidate') # we should be in the experiment
- subject.run
+ application_experiment.run
- expect(subject.cache.read).to eq('candidate')
+ expect(application_experiment.cache.read).to eq('candidate')
end
it "doesn't cache a variant if we don't explicitly provide one" do
@@ -320,11 +332,11 @@ RSpec.describe ApplicationExperiment, :experiment do
# the control.
stub_feature_flags(namespaced_stub: false) # simulate being not rolled out
- expect(subject.variant.name).to eq('control') # if we ask, it should be control
+ expect(application_experiment.variant.name).to eq('control') # if we ask, it should be control
- subject.run
+ application_experiment.run
- expect(subject.cache.read).to be_nil
+ expect(application_experiment.cache.read).to be_nil
end
it "caches a control variant if we assign it specifically" do
@@ -332,27 +344,27 @@ RSpec.describe ApplicationExperiment, :experiment do
# that this context will always get the control variant unless we delete
# the field from the cache (or clear the entire experiment cache) -- or
# write code that would specify a different variant.
- subject.run(:control)
+ application_experiment.run(:control)
- expect(subject.cache.read).to eq('control')
+ expect(application_experiment.cache.read).to eq('control')
end
context "arbitrary attributes" do
before do
- subject.cache.store.clear(key: subject.name + '_attrs')
+ application_experiment.cache.store.clear(key: application_experiment.name + '_attrs')
end
it "sets and gets attributes about an experiment" do
- subject.cache.attr_set(:foo, :bar)
+ application_experiment.cache.attr_set(:foo, :bar)
- expect(subject.cache.attr_get(:foo)).to eq('bar')
+ expect(application_experiment.cache.attr_get(:foo)).to eq('bar')
end
it "increments a value for an experiment" do
- expect(subject.cache.attr_get(:foo)).to be_nil
+ expect(application_experiment.cache.attr_get(:foo)).to be_nil
- expect(subject.cache.attr_inc(:foo)).to eq(1)
- expect(subject.cache.attr_inc(:foo)).to eq(2)
+ expect(application_experiment.cache.attr_inc(:foo)).to eq(1)
+ expect(application_experiment.cache.attr_inc(:foo)).to eq(2)
end
end
end
diff --git a/spec/factories/clusters/agents/activity_events.rb b/spec/factories/clusters/agents/activity_events.rb
new file mode 100644
index 00000000000..ff73f617964
--- /dev/null
+++ b/spec/factories/clusters/agents/activity_events.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :agent_activity_event, class: 'Clusters::Agents::ActivityEvent' do
+ association :agent, factory: :cluster_agent
+ association :agent_token, factory: :cluster_agent_token
+ user
+
+ kind { :token_created }
+ level { :info }
+ recorded_at { Time.current }
+ end
+end
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index 61672662fbe..8edbf639c81 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -207,34 +207,31 @@ RSpec.describe 'User page' do
state: :blocked,
organization: 'GitLab - work info test',
job_title: 'Frontend Engineer',
- pronunciation: 'pruh-nuhn-see-ay-shn'
+ pronunciation: 'pruh-nuhn-see-ay-shn',
+ bio: 'My personal bio'
)
end
let_it_be(:status) { create(:user_status, user: user, message: "Working hard!") }
- it 'shows no tab' do
- subject
+ before do
+ visit_profile
+ end
+ it 'shows no tab' do
expect(page).to have_css("div.profile-header")
expect(page).not_to have_css("ul.nav-links")
end
it 'shows blocked message' do
- subject
-
expect(page).to have_content("This user is blocked")
end
it 'shows user name as blocked' do
- subject
-
expect(page).to have_css(".cover-title", text: 'Blocked user')
end
it 'shows no additional fields' do
- subject
-
expect(page).not_to have_css(".profile-user-bio")
expect(page).not_to have_content('GitLab - work info test')
expect(page).not_to have_content('Frontend Engineer')
@@ -243,10 +240,10 @@ RSpec.describe 'User page' do
end
it 'shows username' do
- subject
-
expect(page).to have_content("@#{user.username}")
end
+
+ it_behaves_like 'default brand title page meta description'
end
context 'with unconfirmed user' do
@@ -256,7 +253,8 @@ RSpec.describe 'User page' do
:unconfirmed,
organization: 'GitLab - work info test',
job_title: 'Frontend Engineer',
- pronunciation: 'pruh-nuhn-see-ay-shn'
+ pronunciation: 'pruh-nuhn-see-ay-shn',
+ bio: 'My personal bio'
)
end
@@ -287,6 +285,8 @@ RSpec.describe 'User page' do
it 'shows private profile message' do
expect(page).to have_content("This user has a private profile")
end
+
+ it_behaves_like 'default brand title page meta description'
end
context 'when visited by an authenticated user' do
diff --git a/spec/frontend/editor/helpers.js b/spec/frontend/editor/helpers.js
index 6f7cdf6efb3..c77be4f8c58 100644
--- a/spec/frontend/editor/helpers.js
+++ b/spec/frontend/editor/helpers.js
@@ -1,4 +1,4 @@
-export class MyClassExtension {
+export class SEClassExtension {
// eslint-disable-next-line class-methods-use-this
provides() {
return {
@@ -8,7 +8,7 @@ export class MyClassExtension {
}
}
-export function MyFnExtension() {
+export function SEFnExtension() {
return {
fnExtMethod: () => 'fn own method',
provides: () => {
@@ -19,7 +19,7 @@ export function MyFnExtension() {
};
}
-export const MyConstExt = () => {
+export const SEConstExt = () => {
return {
provides: () => {
return {
@@ -29,6 +29,33 @@ export const MyConstExt = () => {
};
};
+export function SEWithSetupExt() {
+ return {
+ onSetup: (setupOptions = {}, instance) => {
+ if (setupOptions && !Array.isArray(setupOptions)) {
+ Object.entries(setupOptions).forEach(([key, value]) => {
+ Object.assign(instance, {
+ [key]: value,
+ });
+ });
+ }
+ },
+ provides: () => {
+ return {
+ returnInstanceAndProps: (instance, stringProp, objProp = {}) => {
+ return [stringProp, objProp, instance];
+ },
+ returnInstance: (instance) => {
+ return instance;
+ },
+ giveMeContext: () => {
+ return this;
+ },
+ };
+ },
+ };
+}
+
export const conflictingExtensions = {
WithInstanceExt: () => {
return {
diff --git a/spec/frontend/editor/source_editor_extension_spec.js b/spec/frontend/editor/source_editor_extension_spec.js
index 6f2eb07a043..de3f9da0aed 100644
--- a/spec/frontend/editor/source_editor_extension_spec.js
+++ b/spec/frontend/editor/source_editor_extension_spec.js
@@ -22,15 +22,15 @@ describe('Editor Extension', () => {
it.each`
definition | setupOptions | expectedName
- ${helpers.MyClassExtension} | ${undefined} | ${'MyClassExtension'}
- ${helpers.MyClassExtension} | ${{}} | ${'MyClassExtension'}
- ${helpers.MyClassExtension} | ${dummyObj} | ${'MyClassExtension'}
- ${helpers.MyFnExtension} | ${undefined} | ${'MyFnExtension'}
- ${helpers.MyFnExtension} | ${{}} | ${'MyFnExtension'}
- ${helpers.MyFnExtension} | ${dummyObj} | ${'MyFnExtension'}
- ${helpers.MyConstExt} | ${undefined} | ${'MyConstExt'}
- ${helpers.MyConstExt} | ${{}} | ${'MyConstExt'}
- ${helpers.MyConstExt} | ${dummyObj} | ${'MyConstExt'}
+ ${helpers.SEClassExtension} | ${undefined} | ${'SEClassExtension'}
+ ${helpers.SEClassExtension} | ${{}} | ${'SEClassExtension'}
+ ${helpers.SEClassExtension} | ${dummyObj} | ${'SEClassExtension'}
+ ${helpers.SEFnExtension} | ${undefined} | ${'SEFnExtension'}
+ ${helpers.SEFnExtension} | ${{}} | ${'SEFnExtension'}
+ ${helpers.SEFnExtension} | ${dummyObj} | ${'SEFnExtension'}
+ ${helpers.SEConstExt} | ${undefined} | ${'SEConstExt'}
+ ${helpers.SEConstExt} | ${{}} | ${'SEConstExt'}
+ ${helpers.SEConstExt} | ${dummyObj} | ${'SEConstExt'}
`(
'correctly creates extension for definition = $definition and setupOptions = $setupOptions',
({ definition, setupOptions, expectedName }) => {
@@ -51,9 +51,9 @@ describe('Editor Extension', () => {
describe('api', () => {
it.each`
definition | expectedKeys
- ${helpers.MyClassExtension} | ${['shared', 'classExtMethod']}
- ${helpers.MyFnExtension} | ${['fnExtMethod']}
- ${helpers.MyConstExt} | ${['constExtMethod']}
+ ${helpers.SEClassExtension} | ${['shared', 'classExtMethod']}
+ ${helpers.SEFnExtension} | ${['fnExtMethod']}
+ ${helpers.SEConstExt} | ${['constExtMethod']}
`('correctly returns API for $definition', ({ definition, expectedKeys }) => {
const extension = new EditorExtension({ definition });
const expectedApi = Object.fromEntries(
diff --git a/spec/frontend/editor/source_editor_instance_spec.js b/spec/frontend/editor/source_editor_instance_spec.js
index 87b20a4ba73..a46eea4c4cd 100644
--- a/spec/frontend/editor/source_editor_instance_spec.js
+++ b/spec/frontend/editor/source_editor_instance_spec.js
@@ -6,23 +6,29 @@ import {
EDITOR_EXTENSION_NOT_REGISTERED_ERROR,
EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR,
} from '~/editor/constants';
-import Instance from '~/editor/source_editor_instance';
+import SourceEditorInstance from '~/editor/source_editor_instance';
import { sprintf } from '~/locale';
-import { MyClassExtension, conflictingExtensions, MyFnExtension, MyConstExt } from './helpers';
+import {
+ SEClassExtension,
+ conflictingExtensions,
+ SEFnExtension,
+ SEConstExt,
+ SEWithSetupExt,
+} from './helpers';
describe('Source Editor Instance', () => {
let seInstance;
const defSetupOptions = { foo: 'bar' };
const fullExtensionsArray = [
- { definition: MyClassExtension },
- { definition: MyFnExtension },
- { definition: MyConstExt },
+ { definition: SEClassExtension },
+ { definition: SEFnExtension },
+ { definition: SEConstExt },
];
const fullExtensionsArrayWithOptions = [
- { definition: MyClassExtension, setupOptions: defSetupOptions },
- { definition: MyFnExtension, setupOptions: defSetupOptions },
- { definition: MyConstExt, setupOptions: defSetupOptions },
+ { definition: SEClassExtension, setupOptions: defSetupOptions },
+ { definition: SEFnExtension, setupOptions: defSetupOptions },
+ { definition: SEConstExt, setupOptions: defSetupOptions },
];
const fooFn = jest.fn();
@@ -40,26 +46,26 @@ describe('Source Editor Instance', () => {
});
it('sets up the registry for the methods coming from extensions', () => {
- seInstance = new Instance();
+ seInstance = new SourceEditorInstance();
expect(seInstance.methods).toBeDefined();
- seInstance.use({ definition: MyClassExtension });
+ seInstance.use({ definition: SEClassExtension });
expect(seInstance.methods).toEqual({
- shared: 'MyClassExtension',
- classExtMethod: 'MyClassExtension',
+ shared: 'SEClassExtension',
+ classExtMethod: 'SEClassExtension',
});
- seInstance.use({ definition: MyFnExtension });
+ seInstance.use({ definition: SEFnExtension });
expect(seInstance.methods).toEqual({
- shared: 'MyClassExtension',
- classExtMethod: 'MyClassExtension',
- fnExtMethod: 'MyFnExtension',
+ shared: 'SEClassExtension',
+ classExtMethod: 'SEClassExtension',
+ fnExtMethod: 'SEFnExtension',
});
});
describe('proxy', () => {
it('returns prop from an extension if extension provides it', () => {
- seInstance = new Instance();
+ seInstance = new SourceEditorInstance();
seInstance.use({ definition: DummyExt });
expect(fooFn).not.toHaveBeenCalled();
@@ -67,8 +73,58 @@ describe('Source Editor Instance', () => {
expect(fooFn).toHaveBeenCalled();
});
+ it.each`
+ stringPropToPass | objPropToPass | setupOptions
+ ${undefined} | ${undefined} | ${undefined}
+ ${'prop'} | ${undefined} | ${undefined}
+ ${'prop'} | ${[]} | ${undefined}
+ ${'prop'} | ${{}} | ${undefined}
+ ${'prop'} | ${{ alpha: 'beta' }} | ${undefined}
+ ${'prop'} | ${{ alpha: 'beta' }} | ${defSetupOptions}
+ ${'prop'} | ${undefined} | ${defSetupOptions}
+ ${undefined} | ${undefined} | ${defSetupOptions}
+ ${''} | ${{}} | ${defSetupOptions}
+ `(
+ 'correctly passes arguments ("$stringPropToPass", "$objPropToPass") and instance (with "$setupOptions" setupOptions) to extension methods',
+ ({ stringPropToPass, objPropToPass, setupOptions }) => {
+ seInstance = new SourceEditorInstance();
+ seInstance.use({ definition: SEWithSetupExt, setupOptions });
+
+ const [stringProp, objProp, instance] = seInstance.returnInstanceAndProps(
+ stringPropToPass,
+ objPropToPass,
+ );
+ const expectedObjProps = objPropToPass || {};
+
+ expect(instance).toBe(seInstance);
+ expect(stringProp).toBe(stringPropToPass);
+ expect(objProp).toEqual(expectedObjProps);
+ if (setupOptions) {
+ Object.keys(setupOptions).forEach((key) => {
+ expect(instance[key]).toBe(setupOptions[key]);
+ });
+ }
+ },
+ );
+
+ it('correctly passes instance to the methods even if no additional props have been passed', () => {
+ seInstance = new SourceEditorInstance();
+ seInstance.use({ definition: SEWithSetupExt });
+
+ const instance = seInstance.returnInstance();
+
+ expect(instance).toBe(seInstance);
+ });
+
+ it("correctly sets the context of the 'this' keyword for the extension's methods", () => {
+ seInstance = new SourceEditorInstance();
+ seInstance.use({ definition: SEWithSetupExt });
+
+ expect(seInstance.giveMeContext().constructor).toEqual(SEWithSetupExt);
+ });
+
it('returns props from SE instance itself if no extension provides the prop', () => {
- seInstance = new Instance({
+ seInstance = new SourceEditorInstance({
use: fooFn,
});
jest.spyOn(seInstance, 'use').mockImplementation(() => {});
@@ -80,7 +136,7 @@ describe('Source Editor Instance', () => {
});
it('returns props from Monaco instance when the prop does not exist on the SE instance', () => {
- seInstance = new Instance({
+ seInstance = new SourceEditorInstance({
fooFn,
});
@@ -92,13 +148,13 @@ describe('Source Editor Instance', () => {
describe('public API', () => {
it.each(['use', 'unuse'], 'provides "%s" as public method by default', (method) => {
- seInstance = new Instance();
+ seInstance = new SourceEditorInstance();
expect(seInstance[method]).toBeDefined();
});
describe('use', () => {
it('extends the SE instance with methods provided by an extension', () => {
- seInstance = new Instance();
+ seInstance = new SourceEditorInstance();
seInstance.use({ definition: DummyExt });
expect(fooFn).not.toHaveBeenCalled();
@@ -108,15 +164,15 @@ describe('Source Editor Instance', () => {
it.each`
extensions | expectedProps
- ${{ definition: MyClassExtension }} | ${['shared', 'classExtMethod']}
- ${{ definition: MyFnExtension }} | ${['fnExtMethod']}
- ${{ definition: MyConstExt }} | ${['constExtMethod']}
+ ${{ definition: SEClassExtension }} | ${['shared', 'classExtMethod']}
+ ${{ definition: SEFnExtension }} | ${['fnExtMethod']}
+ ${{ definition: SEConstExt }} | ${['constExtMethod']}
${fullExtensionsArray} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
${fullExtensionsArrayWithOptions} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
`(
'Should register $expectedProps when extension is "$extensions"',
({ extensions, expectedProps }) => {
- seInstance = new Instance();
+ seInstance = new SourceEditorInstance();
expect(seInstance.extensionsAPI).toHaveLength(0);
seInstance.use(extensions);
@@ -127,15 +183,15 @@ describe('Source Editor Instance', () => {
it.each`
definition | preInstalledExtDefinition | expectedErrorProp
- ${conflictingExtensions.WithInstanceExt} | ${MyClassExtension} | ${'use'}
+ ${conflictingExtensions.WithInstanceExt} | ${SEClassExtension} | ${'use'}
${conflictingExtensions.WithInstanceExt} | ${null} | ${'use'}
${conflictingExtensions.WithAnotherExt} | ${null} | ${undefined}
- ${conflictingExtensions.WithAnotherExt} | ${MyClassExtension} | ${'shared'}
- ${MyClassExtension} | ${conflictingExtensions.WithAnotherExt} | ${'shared'}
+ ${conflictingExtensions.WithAnotherExt} | ${SEClassExtension} | ${'shared'}
+ ${SEClassExtension} | ${conflictingExtensions.WithAnotherExt} | ${'shared'}
`(
'logs the naming conflict error when registering $definition',
({ definition, preInstalledExtDefinition, expectedErrorProp }) => {
- seInstance = new Instance();
+ seInstance = new SourceEditorInstance();
jest.spyOn(console, 'error').mockImplementation(() => {});
if (preInstalledExtDefinition) {
@@ -175,7 +231,7 @@ describe('Source Editor Instance', () => {
`(
'Should throw $thrownError when extension is "$extensions"',
({ extensions, thrownError }) => {
- seInstance = new Instance();
+ seInstance = new SourceEditorInstance();
const useExtension = () => {
seInstance.use(extensions);
};
@@ -188,24 +244,24 @@ describe('Source Editor Instance', () => {
beforeEach(() => {
extensionStore = new Map();
- seInstance = new Instance({}, extensionStore);
+ seInstance = new SourceEditorInstance({}, extensionStore);
});
it('stores _instances_ of the used extensions in a global registry', () => {
- const extension = seInstance.use({ definition: MyClassExtension });
+ const extension = seInstance.use({ definition: SEClassExtension });
expect(extensionStore.size).toBe(1);
- expect(extensionStore.entries().next().value).toEqual(['MyClassExtension', extension]);
+ expect(extensionStore.entries().next().value).toEqual(['SEClassExtension', extension]);
});
it('does not duplicate entries in the registry', () => {
jest.spyOn(extensionStore, 'set');
- const extension1 = seInstance.use({ definition: MyClassExtension });
- seInstance.use({ definition: MyClassExtension });
+ const extension1 = seInstance.use({ definition: SEClassExtension });
+ seInstance.use({ definition: SEClassExtension });
expect(extensionStore.set).toHaveBeenCalledTimes(1);
- expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension1);
+ expect(extensionStore.set).toHaveBeenCalledWith('SEClassExtension', extension1);
});
it.each`
@@ -222,20 +278,20 @@ describe('Source Editor Instance', () => {
jest.spyOn(extensionStore, 'set');
const extension1 = seInstance.use({
- definition: MyClassExtension,
+ definition: SEClassExtension,
setupOptions: currentSetupOptions,
});
const extension2 = seInstance.use({
- definition: MyClassExtension,
+ definition: SEClassExtension,
setupOptions: newSetupOptions,
});
expect(extensionStore.size).toBe(1);
expect(extensionStore.set).toHaveBeenCalledTimes(expectedCallTimes);
if (expectedCallTimes > 1) {
- expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension2);
+ expect(extensionStore.set).toHaveBeenCalledWith('SEClassExtension', extension2);
} else {
- expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension1);
+ expect(extensionStore.set).toHaveBeenCalledWith('SEClassExtension', extension1);
}
},
);
@@ -252,7 +308,7 @@ describe('Source Editor Instance', () => {
`(
`Should throw "${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}" when extension is "$unuseExtension"`,
({ unuseExtension, thrownError }) => {
- seInstance = new Instance();
+ seInstance = new SourceEditorInstance();
const unuse = () => {
seInstance.unuse(unuseExtension);
};
@@ -262,16 +318,16 @@ describe('Source Editor Instance', () => {
it.each`
initExtensions | unuseExtensionIndex | remainingAPI
- ${{ definition: MyClassExtension }} | ${0} | ${[]}
- ${{ definition: MyFnExtension }} | ${0} | ${[]}
- ${{ definition: MyConstExt }} | ${0} | ${[]}
+ ${{ definition: SEClassExtension }} | ${0} | ${[]}
+ ${{ definition: SEFnExtension }} | ${0} | ${[]}
+ ${{ definition: SEConstExt }} | ${0} | ${[]}
${fullExtensionsArray} | ${0} | ${['fnExtMethod', 'constExtMethod']}
${fullExtensionsArray} | ${1} | ${['shared', 'classExtMethod', 'constExtMethod']}
${fullExtensionsArray} | ${2} | ${['shared', 'classExtMethod', 'fnExtMethod']}
`(
'un-registers properties introduced by single extension $unuseExtension',
({ initExtensions, unuseExtensionIndex, remainingAPI }) => {
- seInstance = new Instance();
+ seInstance = new SourceEditorInstance();
const extensions = seInstance.use(initExtensions);
if (Array.isArray(initExtensions)) {
@@ -291,7 +347,7 @@ describe('Source Editor Instance', () => {
`(
'un-registers properties introduced by multiple extensions $unuseExtension',
({ unuseExtensionIndex, remainingAPI }) => {
- seInstance = new Instance();
+ seInstance = new SourceEditorInstance();
const extensions = seInstance.use(fullExtensionsArray);
const extensionsToUnuse = extensions.filter((ext, index) =>
unuseExtensionIndex.includes(index),
@@ -304,11 +360,11 @@ describe('Source Editor Instance', () => {
it('it does not remove entry from the global registry to keep for potential future re-use', () => {
const extensionStore = new Map();
- seInstance = new Instance({}, extensionStore);
+ seInstance = new SourceEditorInstance({}, extensionStore);
const extensions = seInstance.use(fullExtensionsArray);
const verifyExpectations = () => {
const entries = extensionStore.entries();
- const mockExtensions = ['MyClassExtension', 'MyFnExtension', 'MyConstExt'];
+ const mockExtensions = ['SEClassExtension', 'SEFnExtension', 'SEConstExt'];
expect(extensionStore.size).toBe(mockExtensions.length);
mockExtensions.forEach((ext, index) => {
expect(entries.next().value).toEqual([ext, extensions[index]]);
@@ -326,7 +382,7 @@ describe('Source Editor Instance', () => {
beforeEach(() => {
instanceModel = monacoEditor.createModel('');
- seInstance = new Instance({
+ seInstance = new SourceEditorInstance({
getModel: () => instanceModel,
});
});
@@ -363,7 +419,7 @@ describe('Source Editor Instance', () => {
};
it('passes correct arguments to callback fns when using an extension', () => {
- seInstance = new Instance();
+ seInstance = new SourceEditorInstance();
seInstance.use({
definition: MyFullExtWithCallbacks,
setupOptions: defSetupOptions,
@@ -373,7 +429,7 @@ describe('Source Editor Instance', () => {
});
it('passes correct arguments to callback fns when un-using an extension', () => {
- seInstance = new Instance();
+ seInstance = new SourceEditorInstance();
const extension = seInstance.use({
definition: MyFullExtWithCallbacks,
setupOptions: defSetupOptions,
diff --git a/spec/frontend/editor/source_editor_markdown_ext_spec.js b/spec/frontend/editor/source_editor_markdown_ext_spec.js
index 245c6c28d31..4a53f870f6d 100644
--- a/spec/frontend/editor/source_editor_markdown_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_ext_spec.js
@@ -57,7 +57,7 @@ describe('Markdown Extension for Source Editor', () => {
blobPath: markdownPath,
blobContent: text,
});
- editor.use(new EditorMarkdownExtension({ instance, previewMarkdownPath }));
+ instance.use(new EditorMarkdownExtension({ instance, previewMarkdownPath }));
panelSpy = jest.spyOn(EditorMarkdownExtension, 'togglePreviewPanel');
});
diff --git a/spec/frontend/editor/source_editor_spec.js b/spec/frontend/editor/source_editor_spec.js
index d87d373c952..f1b887b2dc1 100644
--- a/spec/frontend/editor/source_editor_spec.js
+++ b/spec/frontend/editor/source_editor_spec.js
@@ -1,6 +1,5 @@
/* eslint-disable max-classes-per-file */
import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor';
-import waitForPromises from 'helpers/wait_for_promises';
import {
SOURCE_EDITOR_INSTANCE_ERROR_NO_EL,
URI_PREFIX,
@@ -531,105 +530,19 @@ describe('Base editor', () => {
instance.use(FunctionExt);
expect(instance.inst()).toEqual(editor.instances[0]);
});
- });
-
- describe('extensions as an instance parameter', () => {
- let editorExtensionSpy;
- const instanceConstructor = (extensions = []) => {
- return editor.createInstance({
- el: editorEl,
- blobPath,
- blobContent,
- extensions,
- });
- };
-
- beforeEach(() => {
- editorExtensionSpy = jest
- .spyOn(SourceEditor, 'pushToImportsArray')
- .mockImplementation((arr) => {
- arr.push(
- Promise.resolve({
- default: {},
- }),
- );
- });
- });
-
- it.each([undefined, [], [''], ''])(
- 'does not fail and makes no fetch if extensions is %s',
- () => {
- instance = instanceConstructor(null);
- expect(editorExtensionSpy).not.toHaveBeenCalled();
- },
- );
-
- it.each`
- type | value | callsCount
- ${'simple string'} | ${'foo'} | ${1}
- ${'combined string'} | ${'foo, bar'} | ${2}
- ${'array of strings'} | ${['foo', 'bar']} | ${2}
- `('accepts $type as an extension parameter', ({ value, callsCount }) => {
- instance = instanceConstructor(value);
- expect(editorExtensionSpy).toHaveBeenCalled();
- expect(editorExtensionSpy.mock.calls).toHaveLength(callsCount);
- });
- it.each`
- desc | path | expectation
- ${'~/editor'} | ${'foo'} | ${'~/editor/foo'}
- ${'~/CUSTOM_PATH with leading slash'} | ${'/my_custom_path/bar'} | ${'~/my_custom_path/bar'}
- ${'~/CUSTOM_PATH without leading slash'} | ${'my_custom_path/delta'} | ${'~/my_custom_path/delta'}
- `('fetches extensions from $desc path', ({ path, expectation }) => {
- instance = instanceConstructor(path);
- expect(editorExtensionSpy).toHaveBeenCalledWith(expect.any(Array), expectation);
- });
-
- it('emits EDITOR_READY_EVENT event after all extensions were applied', async () => {
- const calls = [];
- const eventSpy = jest.fn().mockImplementation(() => {
- calls.push('event');
- });
- const useSpy = jest.fn().mockImplementation(() => {
- calls.push('use');
- });
- jest.spyOn(SourceEditor, 'convertMonacoToELInstance').mockImplementation((inst) => {
- const decoratedInstance = inst;
- decoratedInstance.use = useSpy;
- return decoratedInstance;
+ it('emits the EDITOR_READY_EVENT event after setting up the instance', () => {
+ jest.spyOn(monacoEditor, 'create').mockImplementation(() => {
+ return {
+ setModel: jest.fn(),
+ onDidDispose: jest.fn(),
+ };
});
+ const eventSpy = jest.fn();
editorEl.addEventListener(EDITOR_READY_EVENT, eventSpy);
- instance = instanceConstructor('foo, bar');
- await waitForPromises();
- expect(useSpy.mock.calls).toHaveLength(2);
- expect(calls).toEqual(['use', 'use', 'event']);
- });
- });
-
- describe('multiple instances', () => {
- let inst1;
- let inst2;
- let editorEl1;
- let editorEl2;
-
- beforeEach(() => {
- setFixtures('<div id="editor1"></div><div id="editor2"></div>');
- editorEl1 = document.getElementById('editor1');
- editorEl2 = document.getElementById('editor2');
- inst1 = editor.createInstance({ el: editorEl1, blobPath: `foo-${blobPath}` });
- inst2 = editor.createInstance({ el: editorEl2, blobPath: `bar-${blobPath}` });
- });
-
- afterEach(() => {
- editor.dispose();
- editorEl1.remove();
- editorEl2.remove();
- });
-
- it('extends all instances if no specific instance is passed', () => {
- editor.use(AlphaExt);
- expect(inst1.alpha()).toEqual(alphaRes);
- expect(inst2.alpha()).toEqual(alphaRes);
+ expect(eventSpy).not.toHaveBeenCalled();
+ instance = editor.createInstance({ el: editorEl });
+ expect(eventSpy).toHaveBeenCalled();
});
});
});
diff --git a/spec/models/clusters/agents/activity_event_spec.rb b/spec/models/clusters/agents/activity_event_spec.rb
new file mode 100644
index 00000000000..18b9c82fa6a
--- /dev/null
+++ b/spec/models/clusters/agents/activity_event_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::ActivityEvent do
+ it { is_expected.to belong_to(:agent).class_name('Clusters::Agent').required }
+ it { is_expected.to belong_to(:user).optional }
+ it { is_expected.to belong_to(:agent_token).class_name('Clusters::AgentToken').optional }
+
+ it { is_expected.to validate_presence_of(:kind) }
+ it { is_expected.to validate_presence_of(:level) }
+ it { is_expected.to validate_presence_of(:recorded_at) }
+ it { is_expected.to nullify_if_blank(:detail) }
+
+ describe 'scopes' do
+ let_it_be(:agent) { create(:cluster_agent) }
+
+ describe '.in_timeline_order' do
+ let(:recorded_at) { 1.hour.ago }
+
+ let!(:event1) { create(:agent_activity_event, agent: agent, recorded_at: recorded_at) }
+ let!(:event2) { create(:agent_activity_event, agent: agent, recorded_at: Time.current) }
+ let!(:event3) { create(:agent_activity_event, agent: agent, recorded_at: recorded_at) }
+
+ subject { described_class.in_timeline_order }
+
+ it 'sorts by recorded_at: :desc, id: :desc' do
+ is_expected.to eq([event2, event3, event1])
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/agent_tokens/create_service_spec.rb b/spec/services/clusters/agent_tokens/create_service_spec.rb
index 92629af06c8..dc7abd1504b 100644
--- a/spec/services/clusters/agent_tokens/create_service_spec.rb
+++ b/spec/services/clusters/agent_tokens/create_service_spec.rb
@@ -47,6 +47,21 @@ RSpec.describe Clusters::AgentTokens::CreateService do
expect(token.name).to eq(params[:name])
end
+ it 'creates an activity event' do
+ expect { subject }.to change { ::Clusters::Agents::ActivityEvent.count }.by(1)
+
+ token = subject.payload[:token].reload
+ event = cluster_agent.activity_events.last
+
+ expect(event).to have_attributes(
+ kind: 'token_created',
+ level: 'info',
+ recorded_at: token.created_at,
+ user: token.created_by_user,
+ agent_token: token
+ )
+ end
+
context 'when params are invalid' do
let(:params) { { agent_id: 'bad_id' } }
@@ -54,6 +69,10 @@ RSpec.describe Clusters::AgentTokens::CreateService do
expect { subject }.not_to change(::Clusters::AgentToken, :count)
end
+ it 'does not create an activity event' do
+ expect { subject }.not_to change { ::Clusters::Agents::ActivityEvent.count }
+ end
+
it 'returns validation errors', :aggregate_failures do
expect(subject.status).to eq(:error)
expect(subject.message).to eq(["Agent must exist", "Name can't be blank"])
diff --git a/spec/support/shared_examples/features/page_description_shared_examples.rb b/spec/support/shared_examples/features/page_description_shared_examples.rb
index 81653220b4c..e3ea36633d1 100644
--- a/spec/support/shared_examples/features/page_description_shared_examples.rb
+++ b/spec/support/shared_examples/features/page_description_shared_examples.rb
@@ -7,3 +7,13 @@ RSpec.shared_examples 'page meta description' do |expected_description|
end
end
end
+
+RSpec.shared_examples 'default brand title page meta description' do
+ include AppearancesHelper
+
+ it 'renders the page with description, og:description, and twitter:description meta tags with the default brand title', :aggregate_failures do
+ %w(name='description' property='og:description' property='twitter:description').each do |selector|
+ expect(page).to have_selector("meta[#{selector}][content='#{default_brand_title}']", visible: false)
+ end
+ end
+end