+
diff --git a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
index e12815f0094..b7b154bfc23 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
+++ b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
@@ -79,6 +79,7 @@ export function mountMarkdownEditor(options = {}) {
const supportsQuickActions = parseBoolean(el.dataset.supportsQuickActions ?? true);
const enableAutocomplete = parseBoolean(el.dataset.enableAutocomplete ?? true);
const disableAttachments = parseBoolean(el.dataset.disableAttachments ?? false);
+ const autofocus = parseBoolean(el.dataset.autofocus ?? true);
const hiddenInput = el.querySelector('input[type="hidden"]');
const formFieldName = hiddenInput.getAttribute('name');
const formFieldId = hiddenInput.getAttribute('id');
@@ -128,7 +129,7 @@ export function mountMarkdownEditor(options = {}) {
autocompleteDataSources: gl.GfmAutoComplete?.dataSources,
supportsQuickActions,
disableAttachments,
- autofocus: true,
+ autofocus,
},
});
},
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 207f8ec556a..058cce9af36 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -76,6 +76,33 @@
}
}
+.settings-section {
+ @include gl-pt-6;
+
+ &::after {
+ content: '';
+ display: block;
+ @include gl-pb-7;
+ }
+}
+
+.settings-section:first-of-type,
+.settings-section-no-bottom + .settings-section {
+ @include gl-pt-0;
+}
+
+.settings-section:not(.settings-section-no-bottom) + .settings-section {
+ @include gl-border-t;
+}
+
+.settings-section-no-bottom::after {
+ @include gl-pb-0;
+
+ @include media-breakpoint-up(sm) {
+ @include gl-pb-5;
+ }
+}
+
$sticky-header-z-index: 98;
.settings-sticky-header,
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 5f6b55ea928..cbed75019f2 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -9,6 +9,10 @@ class Groups::MilestonesController < Groups::ApplicationController
feature_category :team_planning
urgency :low
+ before_action do
+ push_frontend_feature_flag(:content_editor_on_issues, group)
+ end
+
def index
respond_to do |format|
format.html do
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 35b65dbce7e..1f4e5b54500 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -24,6 +24,10 @@ class Projects::MilestonesController < Projects::ApplicationController
feature_category :team_planning
urgency :low
+ before_action do
+ push_frontend_feature_flag(:content_editor_on_issues, @project)
+ end
+
def index
@sort = params[:sort] || 'due_date_asc'
@milestones = milestones.sort_by_attribute(@sort)
diff --git a/app/controllers/projects/service_desk/custom_email_controller.rb b/app/controllers/projects/service_desk/custom_email_controller.rb
new file mode 100644
index 00000000000..fb5e87f9a97
--- /dev/null
+++ b/app/controllers/projects/service_desk/custom_email_controller.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module Projects
+ module ServiceDesk
+ class CustomEmailController < Projects::ApplicationController
+ before_action :check_feature_flag_enabled
+ before_action :authorize_admin_project!
+
+ feature_category :service_desk
+ urgency :low
+
+ def create
+ response = ::ServiceDesk::CustomEmails::CreateService.new(
+ project: project,
+ current_user: current_user,
+ params: params
+ ).execute
+
+ json_response(service_response: response)
+ end
+
+ def update
+ response = ServiceDeskSettings::UpdateService.new(project, current_user, update_setting_params).execute
+
+ if response.error?
+ json_response(
+ error_message: s_("ServiceDesk|Cannot update custom email"),
+ status: :unprocessable_entity
+ )
+ return
+ end
+
+ json_response
+ end
+
+ def destroy
+ response = ::ServiceDesk::CustomEmails::DestroyService.new(
+ project: project,
+ current_user: current_user
+ ).execute
+
+ json_response(service_response: response)
+ end
+
+ def show
+ json_response
+ end
+
+ private
+
+ def update_setting_params
+ params.permit(:custom_email_enabled)
+ end
+
+ def json_response(error_message: nil, status: :ok, service_response: nil)
+ if service_response.present?
+ status = service_response.success? ? :ok : :unprocessable_entity
+ error_message = service_response.message
+ end
+
+ respond_to do |format|
+ format.json { render json: custom_email_attributes(error_message: error_message), status: status }
+ end
+ end
+
+ def custom_email_attributes(error_message:)
+ setting = project.service_desk_setting
+
+ {
+ custom_email: setting&.custom_email,
+ custom_email_enabled: setting&.custom_email_enabled || false,
+ custom_email_verification_state: setting&.custom_email_verification&.state,
+ custom_email_verification_error: setting&.custom_email_verification&.error,
+ custom_email_smtp_address: setting&.custom_email_credential&.smtp_address,
+ error_message: error_message
+ }
+ end
+
+ def check_feature_flag_enabled
+ render_404 unless Feature.enabled?(:service_desk_custom_email, @project)
+ end
+ end
+ end
+end
diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb
index 8861f1ffe9a..31fcc77925b 100644
--- a/app/helpers/packages_helper.rb
+++ b/app/helpers/packages_helper.rb
@@ -74,6 +74,16 @@ module PackagesHelper
Ability.allowed?(current_user, :admin_group, group)
end
+ def can_delete_packages?(project)
+ Gitlab.config.packages.enabled &&
+ Ability.allowed?(current_user, :destroy_package, project)
+ end
+
+ def can_delete_group_packages?(group)
+ group.packages_feature_enabled? &&
+ Ability.allowed?(current_user, :destroy_package, group)
+ end
+
def cleanup_settings_data
{
project_id: @project.id,
diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb
index d1277efac7b..5c5f8d3b2db 100644
--- a/app/models/ml/experiment.rb
+++ b/app/models/ml/experiment.rb
@@ -11,6 +11,7 @@ module Ml
belongs_to :project
belongs_to :user
+ belongs_to :model, optional: true, inverse_of: :default_experiment
has_many :candidates, class_name: 'Ml::Candidate'
has_many :metadata, class_name: 'Ml::ExperimentMetadata'
@@ -22,10 +23,21 @@ module Ml
has_internal_id :iid, scope: :project
+ before_destroy :stop_destroy
+
def package_name
"#{PACKAGE_PREFIX}#{iid}"
end
+ def stop_destroy
+ return unless model_id
+
+ errors[:base] << "Cannot delete an experiment associated to a model"
+ # According to docs, throw is the correct way to stop on a callback
+ # https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#module-ActiveRecord::Callbacks-label-Canceling+callbacks
+ throw :abort # rubocop:disable Cop/BanCatchThrow
+ end
+
class << self
def by_project_id_and_iid(project_id, iid)
find_by(project_id: project_id, iid: iid)
diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb
new file mode 100644
index 00000000000..b23aa724fad
--- /dev/null
+++ b/app/models/ml/model.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Ml
+ class Model < ApplicationRecord
+ validates :project, :default_experiment, presence: true
+ validates :name,
+ format: Gitlab::Regex.ml_model_name_regex,
+ uniqueness: { scope: :project },
+ presence: true,
+ length: { maximum: 255 }
+
+ validate :valid_default_experiment?
+
+ has_one :default_experiment, class_name: 'Ml::Experiment'
+ belongs_to :project
+
+ def valid_default_experiment?
+ return unless default_experiment
+
+ errors.add(:default_experiment) unless default_experiment.name == name
+ errors.add(:default_experiment) unless default_experiment.project_id == project_id
+ end
+ end
+end
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index 89f460606cb..e84fd7a8692 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -10,12 +10,16 @@
= render "shared/milestones/form_dates", f: f
.form-group
= f.label :description, _("Description")
- = render layout: 'shared/md_preview', locals: { url: group_preview_markdown_path } do
- = render 'shared/zen', f: f, attr: :description,
- classes: 'note-textarea',
- qa_selector: 'milestone_description_field',
- supports_autocomplete: true,
- placeholder: _('Write milestone description...')
+ - @gfm_form = true
+ .js-markdown-editor{ data: { render_markdown_path: group_preview_markdown_path,
+ markdown_docs_path: help_page_path('user/markdown'),
+ qa_selector: 'milestone_description_field',
+ form_field_placeholder: _('Write milestone description...'),
+ supports_quick_actions: 'false',
+ enable_autocomplete: 'true',
+ autofocus: 'false',
+ form_field_classes: 'note-textarea js-gfm-input markdown-area' } }
+ = f.hidden_field :description
.clearfix
.error-alert
diff --git a/app/views/groups/packages/index.html.haml b/app/views/groups/packages/index.html.haml
index b6cf26c3677..6d0f24bf08c 100644
--- a/app/views/groups/packages/index.html.haml
+++ b/app/views/groups/packages/index.html.haml
@@ -10,4 +10,5 @@
npm_instance_url: package_registry_instance_url(:npm),
project_list_url: '',
settings_path: show_group_package_registry_settings(@group) ? group_settings_packages_and_registries_path(@group) : '',
+ can_delete_packages: can_delete_group_packages?(@group).to_s,
group_list_url: group_packages_path(@group) } }
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 6ca9e1a2ad3..ebdea5786f5 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -8,7 +8,7 @@
.js-user-profile
- else
= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
- .js-search-settings-section
+ .settings-section.js-search-settings-section
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0
@@ -44,9 +44,8 @@
button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } },
method: :delete) do
= s_("Profiles|Remove avatar")
- .gl-pb-8
- .js-search-settings-section.gl-border-t.gl-pt-6
+ .settings-section.js-search-settings-section.gl-border-t.gl-pt-6
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0= s_("Profiles|Current status")
@@ -60,18 +59,16 @@
= status_form.hidden_field :clear_status_after,
value: user_clear_status_at(@user),
data: { js_name: 'clearStatusAfter' }
- .gl-pb-7
- .user-time-preferences.js-search-settings-section.gl-border-t.gl-pt-6
+ .settings-section.user-time-preferences.js-search-settings-section.gl-border-t.gl-pt-6
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0= s_("Profiles|Time settings")
%p.gl-text-secondary= s_("Profiles|Set your local time zone.")
= f.label :user_timezone, _("Time zone")
.js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @user.timezone, name: 'user[timezone]' } }
- .gl-pb-7
- .js-search-settings-section.gl-border-t.gl-pt-6
+ .settings-section.js-search-settings-section.gl-border-t.gl-pt-6
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0
@@ -168,7 +165,6 @@
= s_("Profiles|Achievements")
= f.gitlab_ui_checkbox_component :achievements_enabled,
s_('Profiles|Display achievements on your profile')
- .gl-pb-7
.js-hide-when-nothing-matches-search.settings-sticky-footer
= f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index be6f9ac83dc..a1300ccd835 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -12,13 +12,16 @@
= render 'shared/milestones/form_dates', f: f
.form-group
= f.label :description, _('Description')
- = render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project) } do
- = render 'shared/zen', f: f, attr: :description,
- classes: 'note-textarea',
- qa_selector: 'milestone_description_field',
- supports_autocomplete: true,
- placeholder: _('Write milestone description...')
- = render 'shared/notes/hints'
+ - @gfm_form = true
+ .js-markdown-editor{ data: { render_markdown_path: preview_markdown_path(@project),
+ markdown_docs_path: help_page_path('user/markdown'),
+ qa_selector: 'milestone_description_field',
+ form_field_placeholder: _('Write milestone description...'),
+ supports_quick_actions: 'false',
+ enable_autocomplete: 'true',
+ autofocus: 'false',
+ form_field_classes: 'note-textarea js-gfm-input markdown-area' } }
+ = f.hidden_field :description
.clearfix
.error-alert
diff --git a/app/views/projects/packages/packages/index.html.haml b/app/views/projects/packages/packages/index.html.haml
index 48aaf0884c8..5397828d48e 100644
--- a/app/views/projects/packages/packages/index.html.haml
+++ b/app/views/projects/packages/packages/index.html.haml
@@ -10,4 +10,5 @@
npm_instance_url: package_registry_instance_url(:npm),
project_list_url: project_packages_path(@project),
settings_path: show_package_registry_settings(@project) ? project_settings_packages_and_registries_path(@project) : '',
+ can_delete_packages: can_delete_packages?(@project).to_s,
group_list_url: '' } }
diff --git a/config/feature_flags/development/load_merge_request_via_links.yml b/config/feature_flags/development/load_merge_request_via_links.yml
deleted file mode 100644
index a9db587a7a9..00000000000
--- a/config/feature_flags/development/load_merge_request_via_links.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: load_merge_request_via_links
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117321
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412177
-milestone: '16.1'
-type: development
-group: group::threat insights
-default_enabled: false
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 787d95669d1..4469795ee2c 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -489,6 +489,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :candidates, only: [:show, :destroy], controller: 'candidates', param: :iid
resources :models, only: [:index], controller: 'models'
end
+
+ namespace :service_desk do
+ resource :custom_email, only: [:show, :create, :update, :destroy], controller: 'custom_email'
+ end
end
# End of the /-/ scope.
diff --git a/db/docs/ml_models.yml b/db/docs/ml_models.yml
new file mode 100644
index 00000000000..112a1f9263c
--- /dev/null
+++ b/db/docs/ml_models.yml
@@ -0,0 +1,10 @@
+---
+table_name: ml_models
+classes:
+ - Ml::Model
+feature_categories:
+ - mlops
+description: A machine learning model for the model registry
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125302
+milestone: '16.2'
+gitlab_schema: gitlab_main
diff --git a/db/migrate/20230705092150_create_ml_models.rb b/db/migrate/20230705092150_create_ml_models.rb
new file mode 100644
index 00000000000..df8827a781f
--- /dev/null
+++ b/db/migrate/20230705092150_create_ml_models.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateMlModels < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def up
+ create_table :ml_models do |t|
+ t.timestamps_with_timezone null: false
+ t.references :project, foreign_key: true, index: true, on_delete: :cascade, null: false
+ t.text :name, limit: 255, null: false
+
+ t.index [:project_id, :name], unique: true
+ end
+ end
+
+ def down
+ drop_table :ml_models
+ end
+end
diff --git a/db/migrate/20230706130217_add_column_model_id_to_ml_experiments.rb b/db/migrate/20230706130217_add_column_model_id_to_ml_experiments.rb
new file mode 100644
index 00000000000..4eab027bc22
--- /dev/null
+++ b/db/migrate/20230706130217_add_column_model_id_to_ml_experiments.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AddColumnModelIdToMlExperiments < Gitlab::Database::Migration[2.1]
+ def change
+ # rubocop:disable Migration/AddReference
+ add_reference :ml_experiments,
+ :model,
+ index: true,
+ null: true,
+ unique: true,
+ foreign_key: { on_delete: :cascade, to_table: :ml_models }
+ # rubocop:enable Migration/AddReference
+ end
+end
diff --git a/db/schema_migrations/20230705092150 b/db/schema_migrations/20230705092150
new file mode 100644
index 00000000000..96e51e689e2
--- /dev/null
+++ b/db/schema_migrations/20230705092150
@@ -0,0 +1 @@
+5954829dd244b4536beeb9b92a157539feb5207bc1309e177310895f269f54d8
\ No newline at end of file
diff --git a/db/schema_migrations/20230706130217 b/db/schema_migrations/20230706130217
new file mode 100644
index 00000000000..8d2a74a3fc2
--- /dev/null
+++ b/db/schema_migrations/20230706130217
@@ -0,0 +1 @@
+3a355ebb2299786d9aa5ce9bf1f07b2bbd3b6aca719c12c78c4a945c4a96cfe3
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 5efc4c0765f..7db5bfbca1e 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -18592,6 +18592,7 @@ CREATE TABLE ml_experiments (
user_id bigint,
name text NOT NULL,
deleted_on timestamp with time zone,
+ model_id bigint,
CONSTRAINT check_ee07a0be2c CHECK ((char_length(name) <= 255))
);
@@ -18604,6 +18605,24 @@ CREATE SEQUENCE ml_experiments_id_seq
ALTER SEQUENCE ml_experiments_id_seq OWNED BY ml_experiments.id;
+CREATE TABLE ml_models (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ project_id bigint NOT NULL,
+ name text NOT NULL,
+ CONSTRAINT check_1fd2cc7d93 CHECK ((char_length(name) <= 255))
+);
+
+CREATE SEQUENCE ml_models_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE ml_models_id_seq OWNED BY ml_models.id;
+
CREATE TABLE namespace_admin_notes (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -25629,6 +25648,8 @@ ALTER TABLE ONLY ml_experiment_metadata ALTER COLUMN id SET DEFAULT nextval('ml_
ALTER TABLE ONLY ml_experiments ALTER COLUMN id SET DEFAULT nextval('ml_experiments_id_seq'::regclass);
+ALTER TABLE ONLY ml_models ALTER COLUMN id SET DEFAULT nextval('ml_models_id_seq'::regclass);
+
ALTER TABLE ONLY namespace_admin_notes ALTER COLUMN id SET DEFAULT nextval('namespace_admin_notes_id_seq'::regclass);
ALTER TABLE ONLY namespace_bans ALTER COLUMN id SET DEFAULT nextval('namespace_bans_id_seq'::regclass);
@@ -27848,6 +27869,9 @@ ALTER TABLE ONLY ml_experiment_metadata
ALTER TABLE ONLY ml_experiments
ADD CONSTRAINT ml_experiments_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY ml_models
+ ADD CONSTRAINT ml_models_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY namespace_admin_notes
ADD CONSTRAINT namespace_admin_notes_pkey PRIMARY KEY (id);
@@ -31974,12 +31998,18 @@ CREATE INDEX index_ml_candidates_on_user_id ON ml_candidates USING btree (user_i
CREATE UNIQUE INDEX index_ml_experiment_metadata_on_experiment_id_and_name ON ml_experiment_metadata USING btree (experiment_id, name);
+CREATE INDEX index_ml_experiments_on_model_id ON ml_experiments USING btree (model_id);
+
CREATE UNIQUE INDEX index_ml_experiments_on_project_id_and_iid ON ml_experiments USING btree (project_id, iid);
CREATE UNIQUE INDEX index_ml_experiments_on_project_id_and_name ON ml_experiments USING btree (project_id, name);
CREATE INDEX index_ml_experiments_on_user_id ON ml_experiments USING btree (user_id);
+CREATE INDEX index_ml_models_on_project_id ON ml_models USING btree (project_id);
+
+CREATE UNIQUE INDEX index_ml_models_on_project_id_and_name ON ml_models USING btree (project_id, name);
+
CREATE UNIQUE INDEX index_mr_blocks_on_blocking_and_blocked_mr_ids ON merge_request_blocks USING btree (blocking_merge_request_id, blocked_merge_request_id);
CREATE INDEX index_mr_cleanup_schedules_timestamps_status ON merge_request_cleanup_schedules USING btree (scheduled_at) WHERE ((completed_at IS NULL) AND (status = 0));
@@ -36969,6 +36999,9 @@ ALTER TABLE ONLY project_repository_storage_moves
ALTER TABLE ONLY ml_candidate_metadata
ADD CONSTRAINT fk_rails_5117dddf22 FOREIGN KEY (candidate_id) REFERENCES ml_candidates(id) ON DELETE CASCADE;
+ALTER TABLE ONLY ml_models
+ ADD CONSTRAINT fk_rails_51e87f7c50 FOREIGN KEY (project_id) REFERENCES projects(id);
+
ALTER TABLE ONLY elastic_group_index_statuses
ADD CONSTRAINT fk_rails_52b9969b12 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@@ -37428,6 +37461,9 @@ ALTER TABLE ONLY boards_epic_board_recent_visits
ALTER TABLE ONLY packages_dependency_links
ADD CONSTRAINT fk_rails_96ef1c00d3 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
+ALTER TABLE ONLY ml_experiments
+ ADD CONSTRAINT fk_rails_97194a054e FOREIGN KEY (model_id) REFERENCES ml_models(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY group_repository_storage_moves
ADD CONSTRAINT fk_rails_982bb5daf1 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
diff --git a/doc/architecture/blueprints/modular_monolith/references.md b/doc/architecture/blueprints/modular_monolith/references.md
index df8e021890c..2c7d3dc972d 100644
--- a/doc/architecture/blueprints/modular_monolith/references.md
+++ b/doc/architecture/blueprints/modular_monolith/references.md
@@ -60,8 +60,8 @@ concepts to look at as an example.
App Continuum:
-An illustration of how an application can evolve from a small, unstructed app, through various
-stages including a modular well-structured, monolith, all the way to a microservices architecture.
+An illustration of how an application can evolve from a small, unstructured app, through various
+stages including a modular well-structured monolith, all the way to a microservices architecture.
Includes discussion of why you might want to stop at various stages, and specifically the
challenges/concerns with making the jump to microservices, and why sticking with a
diff --git a/doc/ci/cloud_deployment/index.md b/doc/ci/cloud_deployment/index.md
index ce03e9e3916..3e77e8c6c25 100644
--- a/doc/ci/cloud_deployment/index.md
+++ b/doc/ci/cloud_deployment/index.md
@@ -13,6 +13,12 @@ to AWS. You can reference these images in your CI/CD pipeline.
If you're using GitLab.com and deploying to the [Amazon Elastic Container Service](https://aws.amazon.com/ecs/) (ECS),
read about [deploying to ECS](ecs/deploy_to_aws_ecs.md).
+NOTE:
+If you are comfortable configuring a deployment yourself and just need to retrieve
+AWS credentials, consider using [ID tokens and OpenID Connect](../cloud_services/aws/index.md).
+ID tokens are more secure than storing credentials in CI/CD variables, but do not
+work with the guidance on this page.
+
## Authenticate GitLab with AWS
To use GitLab CI/CD to connect to AWS, you must authenticate.
diff --git a/doc/development/testing_guide/end_to_end/feature_flags.md b/doc/development/testing_guide/end_to_end/feature_flags.md
index e473b158087..228f63d2354 100644
--- a/doc/development/testing_guide/end_to_end/feature_flags.md
+++ b/doc/development/testing_guide/end_to_end/feature_flags.md
@@ -55,18 +55,16 @@ RSpec.describe "with feature flag enabled", feature_flag: {
let(:project) { Resource::Project.fabricate_via_api! }
- before do
+ around do |example|
Runtime::Feature.enable(:feature_flag_name, project: project)
+ example.run
+ Runtime::Feature.disable(:feature_flag_name, project: project)
end
it "feature flag test" do
# Execute the test with the feature flag enabled.
# It will only affect the project created in this test.
end
-
- after do
- Runtime::Feature.disable(:feature_flag_name, project: project)
- end
end
```
diff --git a/doc/integration/jira/troubleshooting.md b/doc/integration/jira/troubleshooting.md
index d592455788d..49b5dfba566 100644
--- a/doc/integration/jira/troubleshooting.md
+++ b/doc/integration/jira/troubleshooting.md
@@ -108,3 +108,12 @@ Check [`production.log`](../../administration/logs/index.md#productionlog) to se
```
If that's the case, ensure the **Due date** field is visible for issues in the integrated Jira project.
+
+## `An error occurred while requesting data from Jira` when viewing the Jira issues list in GitLab
+
+You might see a `An error occurred while requesting data from Jira` message when you attempt to view the Jira issues list in GitLab.
+
+You can see this error when the authentication details in the Jira integration settings are incomplete or incorrect.
+
+To attempt to resolve this error, try [configuring the integration](configure.md#configure-the-integration) again. Verify that the
+authentication details are correct, re-enter your API token or password, and save your changes.
diff --git a/gems/config/rubocop.yml b/gems/config/rubocop.yml
index f591cde534f..3458998e114 100644
--- a/gems/config/rubocop.yml
+++ b/gems/config/rubocop.yml
@@ -15,6 +15,7 @@ inherit_mode:
AllCops:
# Target the current Ruby version. For example, "3.0" or "3.1".
TargetRubyVersion: <%= RUBY_VERSION[/^\d+\.\d+/, 0] %>
+ SuggestExtensions: false
# This cop doesn't make sense in the context of gems
CodeReuse/ActiveRecord:
@@ -59,6 +60,18 @@ Naming/FileName:
Exclude:
- spec/**/*.rb
+RSpec/ContextWording:
+ Prefixes:
+ - 'when'
+ - 'with'
+ - 'without'
+ - 'for'
+ - 'and'
+ - 'on'
+ - 'in'
+ - 'as'
+ - 'if'
+
# This cop doesn't make sense in the context of gems
RSpec/MissingFeatureCategory:
Enabled: false
diff --git a/gems/ipynbdiff/.rubocop.yml b/gems/ipynbdiff/.rubocop.yml
index f3a696778a4..e30c6c44434 100644
--- a/gems/ipynbdiff/.rubocop.yml
+++ b/gems/ipynbdiff/.rubocop.yml
@@ -4,8 +4,25 @@ inherit_from:
CodeReuse/ActiveRecord:
Enabled: false
+Gitlab/Json:
+ Enabled: false
+
+# FIXME
+Gitlab/RSpec/AvoidSetup:
+ Enabled: false
+
Naming/FileName:
Exclude:
- spec/**/*.rb
- lib/gitlab/rspec.rb
- lib/gitlab/rspec/all.rb
+
+Rails/Pluck:
+ Enabled: false
+
+RSpec/AvoidConditionalStatements:
+ Enabled: false
+
+RSpec/MultipleMemoizedHelpers:
+ Max: 6
+ AllowSubject: true
diff --git a/gems/ipynbdiff/Gemfile.lock b/gems/ipynbdiff/Gemfile.lock
index 1c52ff8c829..9a583bdf274 100644
--- a/gems/ipynbdiff/Gemfile.lock
+++ b/gems/ipynbdiff/Gemfile.lock
@@ -1,26 +1,46 @@
PATH
remote: .
specs:
- gitlab-ipynbdiff (0.4.7)
+ ipynbdiff (0.4.7)
diffy (~> 3.4)
oj (~> 3.13.16)
GEM
remote: https://rubygems.org/
specs:
+ activesupport (7.0.6)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (>= 1.6, < 2)
+ minitest (>= 5.1)
+ tzinfo (~> 2.0)
ast (2.4.2)
benchmark-memory (0.2.0)
memory_profiler (~> 1)
- binding_ninja (0.2.3)
+ binding_of_caller (1.0.0)
+ debug_inspector (>= 0.0.1)
coderay (1.1.3)
+ concurrent-ruby (1.2.2)
+ debug_inspector (1.1.0)
diff-lcs (1.5.0)
diffy (3.4.2)
docile (1.4.0)
+ gitlab-styles (10.1.0)
+ rubocop (~> 1.50.2)
+ rubocop-graphql (~> 0.18)
+ rubocop-performance (~> 1.15)
+ rubocop-rails (~> 2.17)
+ rubocop-rspec (~> 2.22)
+ i18n (1.14.1)
+ concurrent-ruby (~> 1.0)
+ json (2.6.3)
memory_profiler (1.0.0)
method_source (1.0.0)
+ minitest (5.18.1)
oj (3.13.23)
- parser (3.1.2.0)
+ parallel (1.23.0)
+ parser (3.2.2.3)
ast (~> 2.4.1)
+ racc
proc_to_ast (0.1.0)
coderay
parser
@@ -28,7 +48,12 @@ GEM
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
+ racc (1.7.1)
+ rack (3.0.8)
+ rainbow (3.1.1)
rake (13.0.6)
+ regexp_parser (2.8.1)
+ rexml (3.2.5)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
@@ -41,22 +66,60 @@ GEM
rspec-mocks (3.11.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
- rspec-parameterized (0.5.2)
- binding_ninja (>= 0.2.3)
+ rspec-parameterized (1.0.0)
+ rspec-parameterized-core (< 2)
+ rspec-parameterized-table_syntax (< 2)
+ rspec-parameterized-core (1.0.0)
parser
proc_to_ast
rspec (>= 2.13, < 4)
unparser
+ rspec-parameterized-table_syntax (1.0.0)
+ binding_of_caller
+ rspec-parameterized-core (< 2)
rspec-support (3.11.0)
+ rubocop (1.50.2)
+ json (~> 2.3)
+ parallel (~> 1.10)
+ parser (>= 3.2.0.0)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
+ rexml (>= 3.2.5, < 4.0)
+ rubocop-ast (>= 1.28.0, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 3.0)
+ rubocop-ast (1.29.0)
+ parser (>= 3.2.1.0)
+ rubocop-capybara (2.18.0)
+ rubocop (~> 1.41)
+ rubocop-factory_bot (2.23.1)
+ rubocop (~> 1.33)
+ rubocop-graphql (0.19.0)
+ rubocop (>= 0.87, < 2)
+ rubocop-performance (1.18.0)
+ rubocop (>= 1.7.0, < 2.0)
+ rubocop-ast (>= 0.4.0)
+ rubocop-rails (2.20.2)
+ activesupport (>= 4.2.0)
+ rack (>= 1.1)
+ rubocop (>= 1.33.0, < 2.0)
+ rubocop-rspec (2.22.0)
+ rubocop (~> 1.33)
+ rubocop-capybara (~> 2.17)
+ rubocop-factory_bot (~> 2.22)
+ ruby-progressbar (1.13.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
- unparser (0.6.5)
+ tzinfo (2.0.6)
+ concurrent-ruby (~> 1.0)
+ unicode-display_width (2.4.2)
+ unparser (0.6.8)
diff-lcs (~> 1.3)
- parser (>= 3.1.0)
+ parser (>= 3.2.0)
PLATFORMS
ruby
@@ -64,12 +127,13 @@ PLATFORMS
DEPENDENCIES
benchmark-memory (~> 0.2.0)
bundler (~> 2.2)
- gitlab-ipynbdiff!
+ gitlab-styles (~> 10.1.0)
+ ipynbdiff!
pry (~> 0.14)
rake (~> 13.0)
rspec (~> 3.10)
- rspec-parameterized (~> 0.5.1)
- simplecov
+ rspec-parameterized (~> 1.0)
+ simplecov (~> 0.22.0)
BUNDLED WITH
2.3.16
diff --git a/gems/ipynbdiff/ipynbdiff.gemspec b/gems/ipynbdiff/ipynbdiff.gemspec
index 1ec68557a05..8bc3e1b142d 100644
--- a/gems/ipynbdiff/ipynbdiff.gemspec
+++ b/gems/ipynbdiff/ipynbdiff.gemspec
@@ -24,9 +24,10 @@ Gem::Specification.new do |s|
s.add_development_dependency 'benchmark-memory', '~>0.2.0'
s.add_development_dependency 'bundler', '~> 2.2'
+ s.add_development_dependency 'gitlab-styles', '~> 10.1.0'
s.add_development_dependency 'pry', '~> 0.14'
s.add_development_dependency 'rake', '~> 13.0'
s.add_development_dependency 'rspec', '~> 3.10'
- s.add_development_dependency 'rspec-parameterized', '~> 0.5.1'
- s.add_development_dependency 'simplecov', '~> 0.12.0'
+ s.add_development_dependency 'rspec-parameterized', '~> 1.0'
+ s.add_development_dependency 'simplecov', '~> 0.22.0'
end
diff --git a/gems/ipynbdiff/spec/ipynb_diff/symbol_map_spec.rb b/gems/ipynbdiff/spec/ipynb_diff/symbol_map_spec.rb
index 66fb7af66af..94cb10772aa 100644
--- a/gems/ipynbdiff/spec/ipynb_diff/symbol_map_spec.rb
+++ b/gems/ipynbdiff/spec/ipynb_diff/symbol_map_spec.rb
@@ -8,7 +8,7 @@ describe IpynbDiff::SymbolMap do
end
describe '.parse' do
- subject { described_class.parse(JSON.pretty_generate(source)) }
+ subject(:symbol_map) { described_class.parse(JSON.pretty_generate(source)) }
context 'when object has blank key' do
let(:source) { { "": { "": 5 } } }
@@ -37,8 +37,8 @@ describe IpynbDiff::SymbolMap do
context 'when object has inner object and number, string and array with object' do
let(:source) { { obj1: { obj2: [123, 2, true], obj3: "hel\nlo", obj4: true, obj5: 123, obj6: 'a' } } }
- it do
- is_expected.to match_array(
+ specify do
+ expect(symbol_map).to match_array(
res(['.obj1', 2],
['.obj1.obj2', 3],
['.obj1.obj2.0', 4],
diff --git a/gems/ipynbdiff/spec/ipynb_diff_spec.rb b/gems/ipynbdiff/spec/ipynb_diff_spec.rb
index 44fcd99f131..3ca092aaf5c 100644
--- a/gems/ipynbdiff/spec/ipynb_diff_spec.rb
+++ b/gems/ipynbdiff/spec/ipynb_diff_spec.rb
@@ -4,7 +4,7 @@ require_relative 'test_helper'
describe IpynbDiff do
def diff_signs(diff)
- diff.to_s(:text).scan(/.*\n/).map { |l| l[0] }.join('') # rubocop:disable Rails/Pluck
+ diff.to_s(:text).scan(/.*\n/).map { |l| l[0] }.join('')
end
describe '.diff' do
@@ -18,9 +18,7 @@ describe IpynbDiff do
subject { described_class.diff(from, to, include_frontmatter: include_frontmatter, hide_images: hide_images) }
context 'if preprocessing is active' do
- it 'html tables are stripped' do
- is_expected.not_to include('| ')
- end
+ it { is_expected.not_to include(' | ') }
end
context 'when to is nil' do
diff --git a/lefthook.yml b/lefthook.yml
index 7a3c7e5d174..f29de0a791d 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -44,7 +44,7 @@ pre-push:
tags: backend style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: '*.{rb,rake}'
- run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --force-exclusion {files}
+ run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --config .rubocop.yml --parallel --force-exclusion {files}
sidekiq-queues:
tags: backend
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
@@ -134,7 +134,7 @@ auto-fix:
tags: backend style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: '*.{rb,rake}'
- run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --autocorrect --force-exclusion {files}
+ run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --config .rubocop.yml --parallel --autocorrect --force-exclusion {files}
gettext:
tags: backend frontend view haml
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD | while read file;do git diff --unified=1 $(git merge-base origin/master HEAD)..HEAD $file | grep -Fqe '_(' && echo $file;done; true
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d01da0c4ea9..ade7bb076ac 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -42370,6 +42370,9 @@ msgstr ""
msgid "ServiceDesk|Cannot create custom email"
msgstr ""
+msgid "ServiceDesk|Cannot update custom email"
+msgstr ""
+
msgid "ServiceDesk|Custom email address could not be verified."
msgstr ""
diff --git a/spec/factories/ml/models.rb b/spec/factories/ml/models.rb
new file mode 100644
index 00000000000..2d1b29289a5
--- /dev/null
+++ b/spec/factories/ml/models.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ml_models, class: '::Ml::Model' do
+ sequence(:name) { |n| "model#{n}" }
+
+ project
+ default_experiment { association :ml_experiments, project_id: project.id, name: name }
+ end
+end
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 0a697eaa798..bb7cc3db452 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Group milestones', feature_category: :groups_and_projects do
click_button("Preview")
- preview = find('.js-md-preview')
+ preview = find('.js-vue-md-preview')
expect(preview).to have_content('Nothing to preview.')
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index 57debf79c7b..ba4d838e44b 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -1,6 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
-import htmlNewMilestone from 'test_fixtures/milestones/new-milestone.html';
import mock from 'xhr-mock';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
@@ -9,6 +8,7 @@ import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
import dropzoneInput from '~/dropzone_input';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import htmlNewMilestone from 'test_fixtures_static/textarea.html';
const TEST_FILE = new File([], 'somefile.jpg');
TEST_FILE.upload = {};
diff --git a/spec/frontend/fixtures/milestones.rb b/spec/frontend/fixtures/milestones.rb
deleted file mode 100644
index 5e39dcf190a..00000000000
--- a/spec/frontend/fixtures/milestones.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Projects::MilestonesController, '(JavaScript fixtures)', :with_license, feature_category: :team_planning, type: :controller do
- include JavaScriptFixturesHelpers
-
- let_it_be(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
- let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
- let_it_be(:project) { create(:project_empty_repo, namespace: namespace, path: 'milestones-project') }
-
- render_views
-
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- after do
- remove_repository(project)
- end
-
- it 'milestones/new-milestone.html' do
- get :new, params: {
- namespace_id: project.namespace.to_param,
- project_id: project
- }
-
- expect(response).to be_successful
- end
-
- private
-
- def render_milestone(milestone)
- get :show, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: milestone.to_param
- }
-
- expect(response).to be_successful
- end
-end
diff --git a/spec/frontend/fixtures/static/textarea.html b/spec/frontend/fixtures/static/textarea.html
new file mode 100644
index 00000000000..68d5a0f2d4d
--- /dev/null
+++ b/spec/frontend/fixtures/static/textarea.html
@@ -0,0 +1,27 @@
+
+
+Document with Textarea
+
+
+
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index 7a96ddc7afc..523d5f855fc 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -27,11 +27,11 @@ describe('packages_list_row', () => {
const defaultProvide = {
isGroupPage: false,
+ canDeletePackages: true,
};
const packageWithoutTags = { ...packageData(), project: packageProject(), ...linksData };
const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } };
- const packageCannotDestroy = { ...packageData(), ...linksData, canDestroy: false };
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findDeleteDropdown = () => wrapper.findByTestId('action-delete');
@@ -105,7 +105,9 @@ describe('packages_list_row', () => {
describe('delete button', () => {
it('does not exist when package cannot be destroyed', () => {
- mountComponent({ packageEntity: packageCannotDestroy });
+ mountComponent({
+ packageEntity: { ...packageWithoutTags, canDestroy: false },
+ });
expect(findDeleteDropdown().exists()).toBe(false);
});
@@ -168,7 +170,10 @@ describe('packages_list_row', () => {
describe('left action template', () => {
it('does not render checkbox if not permitted', () => {
mountComponent({
- packageEntity: { ...packageWithoutTags, canDestroy: false },
+ provide: {
+ ...defaultProvide,
+ canDeletePackages: false,
+ },
});
expect(findBulkDeleteAction().exists()).toBe(false);
@@ -248,6 +253,7 @@ describe('packages_list_row', () => {
it('if the package is published through CI show the project and author name', () => {
mountComponent({
provide: {
+ ...defaultProvide,
isGroupPage: true,
},
packageEntity: { ...packageWithoutTags, pipelines: { nodes: packagePipelines() } },
@@ -261,6 +267,7 @@ describe('packages_list_row', () => {
it('if the package is published manually dont show project and the author name', () => {
mountComponent({
provide: {
+ ...defaultProvide,
isGroupPage: true,
},
packageEntity: { ...packageWithoutTags },
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
index 483b7a9383d..fad8863e3d9 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
@@ -41,6 +41,10 @@ describe('packages_list', () => {
groupSettings: defaultPackageGroupSettings,
};
+ const defaultProvide = {
+ canDeletePackages: true,
+ };
+
const EmptySlotStub = { name: 'empty-slot-stub', template: ' bar ' };
const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
@@ -52,8 +56,9 @@ describe('packages_list', () => {
const showMock = jest.fn();
- const mountComponent = (props) => {
+ const mountComponent = ({ props = {}, provide = defaultProvide } = {}) => {
wrapper = shallowMountExtended(PackagesList, {
+ provide,
propsData: {
...defaultProps,
...props,
@@ -75,7 +80,7 @@ describe('packages_list', () => {
describe('when is loading', () => {
beforeEach(() => {
- mountComponent({ isLoading: true });
+ mountComponent({ props: { isLoading: true } });
});
it('shows skeleton loader', () => {
@@ -109,6 +114,7 @@ describe('packages_list', () => {
title: '2 packages',
items: defaultProps.list,
pagination: defaultProps.pageInfo,
+ hiddenDelete: false,
isLoading: false,
});
});
@@ -137,6 +143,16 @@ describe('packages_list', () => {
});
});
+ describe('when the user does not have permission to destroy packages', () => {
+ beforeEach(() => {
+ mountComponent({ provide: { canDeletePackages: false } });
+ });
+
+ it('sets the hidden delete prop of registry list to true', () => {
+ expect(findRegistryList().props('hiddenDelete')).toBe(true);
+ });
+ });
+
describe.each`
description | finderFunction | deletePayload
${'when the user can destroy the package'} | ${findPackagesListRow} | ${firstPackage}
@@ -262,7 +278,7 @@ describe('packages_list', () => {
describe('when an error package is present', () => {
beforeEach(() => {
- mountComponent({ list: [firstPackage, errorPackage] });
+ mountComponent({ props: { list: [firstPackage, errorPackage] } });
return nextTick();
});
@@ -290,7 +306,7 @@ describe('packages_list', () => {
describe('when the list is empty', () => {
beforeEach(() => {
- mountComponent({ list: [] });
+ mountComponent({ props: { list: [] } });
});
it('show the empty slot', () => {
@@ -301,7 +317,7 @@ describe('packages_list', () => {
describe('pagination', () => {
beforeEach(() => {
- mountComponent({ pageInfo: { hasPreviousPage: true } });
+ mountComponent({ props: { pageInfo: { hasPreviousPage: true } } });
});
it('emits prev-page events when the prev event is fired', () => {
diff --git a/spec/helpers/packages_helper_spec.rb b/spec/helpers/packages_helper_spec.rb
index ae8a7f0c14c..6d6b8e4c707 100644
--- a/spec/helpers/packages_helper_spec.rb
+++ b/spec/helpers/packages_helper_spec.rb
@@ -132,7 +132,6 @@ RSpec.describe PackagesHelper, feature_category: :package_registry do
describe '#show_container_registry_settings' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
- let_it_be(:admin) { create(:admin) }
before do
allow(helper).to receive(:current_user) { user }
@@ -252,4 +251,114 @@ RSpec.describe PackagesHelper, feature_category: :package_registry do
end
end
end
+
+ describe '#can_delete_packages?' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user) { user }
+ end
+
+ subject { helper.can_delete_packages?(project) }
+
+ context 'with package registry config enabled' do
+ before do
+ stub_config(packages: { enabled: true })
+ end
+
+ context 'when user has permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(true)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when user does not have permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(false)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
+
+ context 'with package registry config disabled' do
+ before do
+ stub_config(packages: { enabled: false })
+ end
+
+ context 'when user has permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(true)
+ end
+
+ it { is_expected.to be(false) }
+ end
+
+ context 'when user does not have permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(false)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
+ end
+
+ describe '#can_delete_group_packages?' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user) { user }
+ end
+
+ subject { helper.can_delete_group_packages?(group) }
+
+ context 'with package registry config enabled' do
+ before do
+ stub_config(packages: { enabled: true })
+ end
+
+ context 'when user has permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(true)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when user does not have permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(false)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
+
+ context 'with package registry config disabled' do
+ before do
+ stub_config(packages: { enabled: false })
+ end
+
+ context 'when user has permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(true)
+ end
+
+ it { is_expected.to be(false) }
+ end
+
+ context 'when user does not have permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(false)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
+ end
end
diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb
index 9738a88b5b8..1ee35d6da03 100644
--- a/spec/models/ml/experiment_spec.rb
+++ b/spec/models/ml/experiment_spec.rb
@@ -14,6 +14,21 @@ RSpec.describe Ml::Experiment, feature_category: :mlops do
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:candidates) }
it { is_expected.to have_many(:metadata) }
+ it { is_expected.to belong_to(:model).class_name('Ml::Model') }
+ end
+
+ describe '#destroy' do
+ it 'allow experiment without model to be destroyed' do
+ experiment = create(:ml_experiments, project: exp.project)
+
+ expect { experiment.destroy! }.to change { Ml::Experiment.count }.by(-1)
+ end
+
+ it 'throws error when destroying experiment with model' do
+ experiment = create(:ml_models, project: exp.project).default_experiment
+
+ expect { experiment.destroy! }.to raise_error(ActiveRecord::ActiveRecordError)
+ end
end
describe '.package_name' do
diff --git a/spec/models/ml/model_spec.rb b/spec/models/ml/model_spec.rb
new file mode 100644
index 00000000000..61466ca3a6a
--- /dev/null
+++ b/spec/models/ml/model_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::Model, feature_category: :mlops do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_one(:default_experiment) }
+ end
+
+ describe '#valid?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:existing_model) { create(:ml_models, name: 'an_existing_model', project: project) }
+ let_it_be(:valid_name) { 'a_valid_name' }
+ let_it_be(:default_experiment) { create(:ml_experiments, name: valid_name, project: project) }
+
+ let(:name) { valid_name }
+
+ subject(:errors) do
+ m = described_class.new(name: name, project: project, default_experiment: default_experiment)
+ m.validate
+ m.errors
+ end
+
+ it 'validates a valid model version' do
+ expect(errors).to be_empty
+ end
+
+ describe 'name' do
+ where(:ctx, :name) do
+ 'name is blank' | ''
+ 'name is not valid package name' | '!!()()'
+ 'name is too large' | ('a' * 256)
+ 'name is not unique in the project' | 'an_existing_model'
+ end
+ with_them do
+ it { expect(errors).to include(:name) }
+ end
+ end
+
+ describe 'default_experiment' do
+ context 'when experiment name name is different than model name' do
+ before do
+ allow(default_experiment).to receive(:name).and_return("#{name}a")
+ end
+
+ it { expect(errors).to include(:default_experiment) }
+ end
+
+ context 'when model version project is different than model project' do
+ before do
+ allow(default_experiment).to receive(:project_id).and_return(project.id + 1)
+ end
+
+ it { expect(errors).to include(:default_experiment) }
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/service_desk/custom_email_controller_spec.rb b/spec/requests/projects/service_desk/custom_email_controller_spec.rb
new file mode 100644
index 00000000000..8ce238ab99c
--- /dev/null
+++ b/spec/requests/projects/service_desk/custom_email_controller_spec.rb
@@ -0,0 +1,380 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ServiceDesk::CustomEmailController, feature_category: :service_desk do
+ let_it_be_with_reload(:project) do
+ create(:project, :private, service_desk_enabled: true)
+ end
+
+ let_it_be(:custom_email_path) { project_service_desk_custom_email_path(project, format: :json) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:illegitimite_user) { create(:user) }
+
+ let(:message) { instance_double(Mail::Message) }
+ let(:error_cannot_create_custom_email) { s_("ServiceDesk|Cannot create custom email") }
+ let(:error_cannot_update_custom_email) { s_("ServiceDesk|Cannot update custom email") }
+ let(:error_does_not_exist) { s_('ServiceDesk|Custom email does not exist') }
+ let(:error_custom_email_exists) { s_('ServiceDesk|Custom email already exists') }
+
+ let(:custom_email_params) do
+ {
+ custom_email: 'user@example.com',
+ smtp_address: 'smtp.example.com',
+ smtp_port: '587',
+ smtp_username: 'user@example.com',
+ smtp_password: 'supersecret'
+ }
+ end
+
+ let(:empty_json_response) do
+ {
+ "custom_email" => nil,
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => nil,
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => nil,
+ "error_message" => nil
+ }
+ end
+
+ before_all do
+ project.add_developer(illegitimite_user)
+ project.add_maintainer(user)
+ end
+
+ shared_examples 'a json response with empty values' do
+ it 'returns json response with empty values' do
+ perform_request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(empty_json_response)
+ end
+ end
+
+ shared_examples 'a controller that responds with status' do |status|
+ it "responds with #{status} for GET custom email" do
+ get custom_email_path
+ expect(response).to have_gitlab_http_status(status)
+ end
+
+ it "responds with #{status} for POST custom email" do
+ post custom_email_path
+ expect(response).to have_gitlab_http_status(status)
+ end
+
+ it "responds with #{status} for PUT custom email" do
+ put custom_email_path
+ expect(response).to have_gitlab_http_status(status)
+ end
+
+ it "responds with #{status} for DELETE custom email" do
+ delete custom_email_path
+ expect(response).to have_gitlab_http_status(status)
+ end
+ end
+
+ shared_examples 'a controller with disabled feature flag with status' do |status|
+ context 'when feature flag service_desk_custom_email is disabled' do
+ before do
+ stub_feature_flags(service_desk_custom_email: false)
+ end
+
+ it_behaves_like 'a controller that responds with status', status
+ end
+ end
+
+ shared_examples 'a deletable resource' do
+ describe 'DELETE custom email' do
+ let(:perform_request) { delete custom_email_path }
+
+ it_behaves_like 'a json response with empty values'
+ end
+ end
+
+ context 'with legitimate user signed in' do
+ before do
+ sign_out(illegitimite_user)
+ sign_in(user)
+ end
+
+ # because CustomEmailController check_feature_flag_enabled responds
+ it_behaves_like 'a controller with disabled feature flag with status', :not_found
+
+ describe 'GET custom email' do
+ let(:perform_request) { get custom_email_path }
+
+ it_behaves_like 'a json response with empty values'
+ end
+
+ describe 'POST custom email' do
+ before do
+ # We send verification email directly
+ allow(message).to receive(:deliver)
+ allow(Notify).to receive(:service_desk_custom_email_verification_email).and_return(message)
+ end
+
+ it 'adds custom email and kicks of verification' do
+ post custom_email_path, params: custom_email_params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "custom_email" => custom_email_params[:custom_email],
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "started",
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => custom_email_params[:smtp_address],
+ "error_message" => nil
+ )
+ end
+
+ context 'when custom_email param is not valid' do
+ it 'does not add custom email' do
+ post custom_email_path, params: custom_email_params.merge(custom_email: 'useratexample.com')
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ empty_json_response.merge("error_message" => error_cannot_create_custom_email)
+ )
+ end
+ end
+
+ context 'when smtp_password param is not valid' do
+ it 'does not add custom email' do
+ post custom_email_path, params: custom_email_params.merge(smtp_password: '2short')
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ empty_json_response.merge("error_message" => error_cannot_create_custom_email)
+ )
+ end
+ end
+
+ context 'when the verification process fails fast' do
+ before do
+ # Could not establish connection, invalid host etc.
+ allow(message).to receive(:deliver).and_raise(SocketError)
+ end
+
+ it 'adds custom email and kicks of verification and returns verification error state' do
+ post custom_email_path, params: custom_email_params
+
+ # In terms of "custom email object creation", failing fast on the
+ # verification is a legit state that we don't treat as an error.
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "custom_email" => custom_email_params[:custom_email],
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "failed",
+ "custom_email_verification_error" => "smtp_host_issue",
+ "custom_email_smtp_address" => custom_email_params[:smtp_address],
+ "error_message" => nil
+ )
+ end
+ end
+ end
+
+ describe 'PUT custom email' do
+ let(:custom_email_params) { { custom_email_enabled: true } }
+
+ it 'does not update records' do
+ put custom_email_path, params: custom_email_params
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ empty_json_response.merge("error_message" => error_cannot_update_custom_email)
+ )
+ end
+ end
+
+ describe 'DELETE custom email' do
+ it 'does not touch any records' do
+ delete custom_email_path
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ empty_json_response.merge("error_message" => error_does_not_exist)
+ )
+ end
+ end
+
+ context 'when custom email is set up' do
+ let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') }
+ let!(:credential) { create(:service_desk_custom_email_credential, project: project) }
+
+ before do
+ project.reset
+ end
+
+ context 'and verification started' do
+ let!(:verification) do
+ create(:service_desk_custom_email_verification, project: project)
+ end
+
+ it_behaves_like 'a deletable resource'
+
+ describe 'GET custom email' do
+ it 'returns custom email in its current state' do
+ get custom_email_path
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "custom_email" => "user@example.com",
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "started",
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => "smtp.example.com",
+ "error_message" => nil
+ )
+ end
+ end
+
+ describe 'POST custom email' do
+ it 'returns custom email in its current state' do
+ post custom_email_path, params: custom_email_params
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ "custom_email" => custom_email_params[:custom_email],
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "started",
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => custom_email_params[:smtp_address],
+ "error_message" => error_custom_email_exists
+ )
+ end
+ end
+
+ describe 'PUT custom email' do
+ let(:custom_email_params) { { custom_email_enabled: true } }
+
+ it 'marks custom email as enabled' do
+ put custom_email_path, params: custom_email_params
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ "custom_email" => "user@example.com",
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "started",
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => "smtp.example.com",
+ "error_message" => error_cannot_update_custom_email
+ )
+ end
+ end
+ end
+
+ context 'and verification finished' do
+ let!(:verification) do
+ create(:service_desk_custom_email_verification, project: project, state: :finished, token: nil)
+ end
+
+ it_behaves_like 'a deletable resource'
+
+ describe 'GET custom email' do
+ it 'returns custom email in its current state' do
+ get custom_email_path
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "custom_email" => "user@example.com",
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "finished",
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => "smtp.example.com",
+ "error_message" => nil
+ )
+ end
+ end
+
+ describe 'PUT custom email' do
+ let(:custom_email_params) { { custom_email_enabled: true } }
+
+ it 'marks custom email as enabled' do
+ put custom_email_path, params: custom_email_params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "custom_email" => "user@example.com",
+ "custom_email_enabled" => true,
+ "custom_email_verification_state" => "finished",
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => "smtp.example.com",
+ "error_message" => nil
+ )
+ end
+ end
+ end
+
+ context 'and verification failed' do
+ let!(:verification) do
+ create(:service_desk_custom_email_verification,
+ project: project,
+ state: :failed,
+ token: nil,
+ error: :smtp_host_issue
+ )
+ end
+
+ it_behaves_like 'a deletable resource'
+
+ describe 'GET custom email' do
+ it 'returns custom email in its current state' do
+ get custom_email_path
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "custom_email" => "user@example.com",
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "failed",
+ "custom_email_verification_error" => "smtp_host_issue",
+ "custom_email_smtp_address" => "smtp.example.com",
+ "error_message" => nil
+ )
+ end
+ end
+
+ describe 'PUT custom email' do
+ let(:custom_email_params) { { custom_email_enabled: true } }
+
+ it 'does not mark custom email as enabled' do
+ put custom_email_path, params: custom_email_params
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ "custom_email" => "user@example.com",
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "failed",
+ "custom_email_verification_error" => "smtp_host_issue",
+ "custom_email_smtp_address" => "smtp.example.com",
+ "error_message" => error_cannot_update_custom_email
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context 'when user is anonymous' do
+ before do
+ sign_out(user)
+ sign_out(illegitimite_user)
+ end
+
+ # because Projects::ApplicationController :authenticate_user! responds
+ # with redirect to login page
+ it_behaves_like 'a controller that responds with status', :found
+ it_behaves_like 'a controller with disabled feature flag with status', :found
+ end
+
+ context 'with illegitimate user signed in' do
+ before do
+ sign_out(user)
+ sign_in(illegitimite_user)
+ end
+
+ it_behaves_like 'a controller that responds with status', :not_found
+ # because CustomEmailController check_feature_flag_enabled responds
+ it_behaves_like 'a controller with disabled feature flag with status', :not_found
+ end
+end
diff --git a/spec/support/shared_examples/features/milestone_editing_shared_examples.rb b/spec/support/shared_examples/features/milestone_editing_shared_examples.rb
index d21bf62ecfa..53498a1bb39 100644
--- a/spec/support/shared_examples/features/milestone_editing_shared_examples.rb
+++ b/spec/support/shared_examples/features/milestone_editing_shared_examples.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
RSpec.shared_examples 'milestone handling version conflicts' do
- it 'warns about version conflict when milestone has been updated in the background' do
+ it 'warns about version conflict when milestone has been updated in the background', :js do
+ wait_for_all_requests
+
# Update the milestone in the background in order to trigger a version conflict
milestone.update!(title: "New title")
diff --git a/spec/views/groups/packages/index.html.haml_spec.rb b/spec/views/groups/packages/index.html.haml_spec.rb
index 26f6268a224..3c6305d1ed9 100644
--- a/spec/views/groups/packages/index.html.haml_spec.rb
+++ b/spec/views/groups/packages/index.html.haml_spec.rb
@@ -36,4 +36,22 @@ RSpec.describe 'groups/packages/index.html.haml', feature_category: :package_reg
)
end
end
+
+ describe 'can_delete_packages' do
+ it 'without permission sets false' do
+ allow(view).to receive(:can_delete_group_packages?).and_return(false)
+
+ render
+
+ expect(rendered).to have_selector('[data-can-delete-packages="false"]')
+ end
+
+ it 'with permission sets true' do
+ allow(view).to receive(:can_delete_group_packages?).and_return(true)
+
+ render
+
+ expect(rendered).to have_selector('[data-can-delete-packages="true"]')
+ end
+ end
end
diff --git a/spec/views/projects/packages/index.html.haml_spec.rb b/spec/views/projects/packages/index.html.haml_spec.rb
index 2557ceb70b3..e59db289ad4 100644
--- a/spec/views/projects/packages/index.html.haml_spec.rb
+++ b/spec/views/projects/packages/index.html.haml_spec.rb
@@ -36,4 +36,22 @@ RSpec.describe 'projects/packages/packages/index.html.haml', feature_category: :
)
end
end
+
+ describe 'can_delete_packages' do
+ it 'without permission sets empty settings path' do
+ allow(view).to receive(:can_delete_packages?).and_return(false)
+
+ render
+
+ expect(rendered).to have_selector('[data-can-delete-packages="false"]')
+ end
+
+ it 'with permission sets project settings path' do
+ allow(view).to receive(:can_delete_packages?).and_return(true)
+
+ render
+
+ expect(rendered).to have_selector('[data-can-delete-packages="true"]')
+ end
+ end
end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index d4c41829127..a716cc4d012 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -245,6 +245,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Geo::RepositoryVerification::Primary::SingleWorker' => false,
'Geo::RepositoryVerification::Secondary::SingleWorker' => false,
'Geo::ReverificationBatchWorker' => 0,
+ 'Geo::BulkMarkPendingBatchWorker' => 0,
'Geo::Scheduler::Primary::SchedulerWorker' => false,
'Geo::Scheduler::SchedulerWorker' => false,
'Geo::Scheduler::Secondary::SchedulerWorker' => false,
|