Commit 2d8f0b30 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 2dcdcf69 664f7196
2b34fc78dfb8e7f55f7f2fc30602381b43c54fc3
e342c59d0c6575245a335bbe9dfe95d9a06b3a2f
......@@ -22,10 +22,10 @@ export default {
strings: {
alertTitle: __('You are about to permanently delete this project'),
alertBody: __(
'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.',
'Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.',
),
modalBody: __(
"This action cannot be undone. You will lose this project's repository and all content: issues, merge requests, etc.",
"This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc.",
),
},
};
......
......@@ -26,20 +26,31 @@
module AtomicInternalId
extend ActiveSupport::Concern
MissingValueError = Class.new(StandardError)
class_methods do
def has_internal_id( # rubocop:disable Naming/PredicateName
column, scope:, init: :not_given, ensure_if: nil, track_if: nil,
presence: true, backfill: false, hook_names: :create)
column, scope:, init: :not_given, ensure_if: nil, track_if: nil, presence: true, hook_names: :create)
raise "has_internal_id init must not be nil if given." if init.nil?
raise "has_internal_id needs to be defined on association." unless self.reflect_on_association(scope)
init = infer_init(scope) if init == :not_given
before_validation :"track_#{scope}_#{column}!", on: hook_names, if: track_if
before_validation :"ensure_#{scope}_#{column}!", on: hook_names, if: ensure_if
validates column, presence: presence
callback_names = Array.wrap(hook_names).map { |hook_name| :"before_#{hook_name}" }
callback_names.each do |callback_name|
# rubocop:disable GitlabSecurity/PublicSend
public_send(callback_name, :"track_#{scope}_#{column}!", if: track_if)
public_send(callback_name, :"ensure_#{scope}_#{column}!", if: ensure_if)
# rubocop:enable GitlabSecurity/PublicSend
end
after_rollback :"clear_#{scope}_#{column}!", on: hook_names, if: ensure_if
if presence
before_create :"validate_#{column}_exists!"
before_update :"validate_#{column}_exists!"
end
define_singleton_internal_id_methods(scope, column, init)
define_instance_internal_id_methods(scope, column, init, backfill)
define_instance_internal_id_methods(scope, column, init)
end
private
......@@ -62,10 +73,8 @@ module AtomicInternalId
# - track_{scope}_{column}!
# - reset_{scope}_{column}
# - {column}=
def define_instance_internal_id_methods(scope, column, init, backfill)
def define_instance_internal_id_methods(scope, column, init)
define_method("ensure_#{scope}_#{column}!") do
return if backfill && self.class.where(column => nil).exists?
scope_value = internal_id_read_scope(scope)
value = read_attribute(column)
return value unless scope_value
......@@ -79,6 +88,8 @@ module AtomicInternalId
internal_id_scope_usage,
init)
write_attribute(column, value)
@internal_id_set_manually = false
end
value
......@@ -110,6 +121,7 @@ module AtomicInternalId
super(value).tap do |v|
# Indicate the iid was set from externally
@internal_id_needs_tracking = true
@internal_id_set_manually = true
end
end
......@@ -128,6 +140,20 @@ module AtomicInternalId
read_attribute(column)
end
define_method("clear_#{scope}_#{column}!") do
return if @internal_id_set_manually
return unless public_send(:"#{column}_previously_changed?") # rubocop:disable GitlabSecurity/PublicSend
write_attribute(column, nil)
end
define_method("validate_#{column}_exists!") do
value = read_attribute(column)
raise MissingValueError, "#{column} was unexpectedly blank!" if value.blank?
end
end
# Defines class methods:
......
......@@ -4,22 +4,20 @@
.sub-section{ data: { qa_selector: 'export_project_content' } }
%h4= _('Export project')
%p= _('Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.')
.bs-callout.bs-callout-info
%p.gl-mb-0
%p= _('The following items will be exported:')
%ul
- project_export_descriptions.each do |desc|
%li= desc
%p= _('The following items will NOT be exported:')
%ul
%li= _('Job logs and artifacts')
%li= _('Container registry images')
%li= _('CI variables')
%li= _('Webhooks')
%li= _('Any encrypted tokens')
%p= _('Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.')
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/import_export') }
%p= _('Export this project with all its related data in order to move it to a new GitLab instance. When the exported file is ready, you can download it from this page or from the download link in the email notification you will receive. You can then import it when creating a new project. %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
%p.gl-mb-0
%p= _('The following items will be exported:')
%ul
- project_export_descriptions.each do |desc|
%li= desc
%p= _('The following items will NOT be exported:')
%ul
%li= _('Job logs and artifacts')
%li= _('Container registry images')
%li= _('CI variables')
%li= _('Webhooks')
%li= _('Any encrypted tokens')
- if project.export_status == :finished
= link_to _('Download export'), download_export_project_path(project),
rel: 'nofollow', download: '', method: :get, class: "btn btn-default", data: { qa_selector: 'download_export_link' }
......
......@@ -3,7 +3,8 @@
.sub-section
%h4.danger-title= _('Delete project')
%p
%strong= _('Deleting the project will delete its repository and all related resources including issues, merge requests etc.')
%strong= _('Deleting the project will delete its repository and all related resources including issues, merge requests, etc.')
= link_to _('Learn more.'), help_page_path('user/project/settings/index', anchor: 'removing-a-fork-relationship'), target: '_blank', rel: 'noopener noreferrer'
%p
%strong= _('Deleted projects cannot be restored!')
#js-project-delete-button{ data: { form_path: project_path(project), confirm_phrase: project.path } }
......@@ -7,4 +7,5 @@
= form_for @project, url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' } do |f|
%p
%strong= _('Once removed, the fork relationship cannot be restored. This project will no longer be able to receive or send merge requests to the source project or other forks.')
= link_to _('Learn more.'), help_page_path('user/project/settings/index', anchor: 'removing-a-fork-relationship'), target: '_blank', rel: 'noopener noreferrer'
= button_to _('Remove fork relationship'), '#', class: "gl-button btn btn-danger js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) }
......@@ -4,13 +4,14 @@
%h4.danger-title= _('Transfer project')
= form_for @project, url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } do |f|
.form-group
= label_tag :new_namespace_id, nil, class: 'label-bold' do
%span= _('Select a new namespace')
.form-group
= select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2'
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'transferring-an-existing-project-into-another-namespace') }
%p= _("Transfer your project into another namespace. %{link_start}Learn more.%{link_end}").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
%ul
%li= _("Be careful. Changing the project's namespace can have unintended side effects.")
%li= _('You can only transfer the project to namespaces you manage.')
%li= _('You will need to update your local repositories to point to the new location.')
%li= _('Project visibility level will be changed to match namespace rules when transferring to a group.')
= label_tag :new_namespace_id, _('Select a new namespace'), class: 'gl-font-weight-bold'
.form-group
= select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2'
= f.submit 'Transfer project', class: "gl-button btn btn-danger js-confirm-danger qa-transfer-button", data: { "confirm-danger-message" => transfer_project_message(@project) }
......@@ -68,7 +68,9 @@
.settings-content
.sub-section
%h4= _('Housekeeping')
%p= _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.')
%p
= _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.')
= link_to _('Learn more.'), help_page_path('administration/housekeeping'), target: '_blank', rel: 'noopener noreferrer'
= link_to _('Run housekeeping'), housekeeping_project_path(@project),
method: :post, class: "gl-button btn btn-default"
......@@ -80,6 +82,13 @@
= render 'projects/errors'
= form_for @project do |f|
.form-group
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'renaming-a-repository') }
%p= _("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
%ul
%li= _("Be careful. Renaming a project's repository can have unintended side effects.")
%li= _('You will need to update your local repositories to point to the new location.')
- if @project.deployment_platform.present?
%li= _('Your deployment services will be broken, you will need to manually fix the services after renaming.')
= f.label :path, _('Path'), class: 'label-bold'
.form-group
.input-group
......@@ -87,11 +96,6 @@
.input-group-text
#{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/
= f.text_field :path, class: 'form-control qa-project-path-field h-auto'
%ul
%li= _("Be careful. Renaming a project's repository can have unintended side effects.")
%li= _('You will need to update your local repositories to point to the new location.')
- if @project.deployment_platform.present?
%li= _('Your deployment services will be broken, you will need to manually fix the services after renaming.')
= f.submit _('Change path'), class: "gl-button btn btn-warning qa-change-path-button"
= render 'transfer', project: @project
......
......@@ -39,5 +39,5 @@
= f.check_box :active, required: false, value: @schedule.active?
= f.label :active, _('Active'), class: 'gl-font-weight-normal'
.footer-block.row-content-block
= f.submit _('Save pipeline schedule'), class: 'btn btn-success'
= link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn btn-cancel'
= f.submit _('Save pipeline schedule'), class: 'btn gl-button btn-success'
= link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn gl-button btn-default btn-cancel'
......@@ -27,14 +27,14 @@
%td
.float-right.btn-group
- if can?(current_user, :play_pipeline_schedule, pipeline_schedule)
= link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn btn-svg gl-display-flex gl-align-items-center gl-justify-content-center' do
= link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn gl-button btn-default btn-svg' do
= sprite_icon('play')
- if can?(current_user, :take_ownership_pipeline_schedule, pipeline_schedule)
= link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do
= link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn gl-button btn-default' do
= s_('PipelineSchedules|Take ownership')
- if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
= link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn gl-display-flex' do
= link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn gl-button btn-default' do
= sprite_icon('pencil')
- if can?(current_user, :admin_pipeline_schedule, pipeline_schedule)
= link_to pipeline_schedule_path(pipeline_schedule), title: _('Delete'), method: :delete, class: 'gl-button btn btn-danger', data: { confirm: _("Are you sure you want to delete this pipeline schedule?") } do
= link_to pipeline_schedule_path(pipeline_schedule), title: _('Delete'), method: :delete, class: 'btn gl-button btn-danger', data: { confirm: _("Are you sure you want to delete this pipeline schedule?") } do
= sprite_icon('remove')
......@@ -9,7 +9,7 @@
- if can?(current_user, :create_pipeline_schedule, @project)
.nav-controls
= link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-success' do
= link_to new_project_pipeline_schedule_path(@project), class: 'btn gl-button btn-success' do
%span= _('New schedule')
- if @schedules.present?
......
......@@ -7,12 +7,14 @@
- else
= _('Archive project')
- if @project.archived?
%p= _("Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'unarchiving-a-project') }
%p= _("Unarchiving the project will restore its members' ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
= link_to _('Unarchive project'), unarchive_project_path(@project),
data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' },
method: :post, class: "gl-button btn btn-success"
- else
%p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'archiving-a-project') }
%p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
= link_to _('Archive project'), archive_project_path(@project),
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
method: :post, class: "gl-button btn btn-warning"
---
title: Apply new GitLab UI for buttons in pipeline schedules
merge_request:
author:
type: other
......@@ -226,6 +226,22 @@ you should fully roll out the feature by enabling the flag **globally** by runni
This changes the feature flag state to be **enabled** always, which overrides the
existing gates (e.g. `--group=gitlab-org`) in the above processes.
##### Disabling feature flags
To disable a feature flag that has been globally enabled you can run:
```shell
/chatops run feature set some_feature false
```
To disable a feature flag that has been enabled for a specific project you can run:
```shell
/chatops run feature set --group=gitlab-org some_feature false
```
You cannot selectively disable feature flags for a specific project/group/user without applying a [specific method of implementing](development.md#selectively-disable-by-actor) the feature flags.
### Feature flag change logging
#### Chatops level
......
......@@ -115,7 +115,7 @@ are resolved](#only-allow-merge-requests-to-be-merged-if-all-threads-are-resolve
there will be an **open an issue to resolve them later** link in the merge
request widget.
![Link in merge request widget](img/resolve_thread_open_issue.png)
![Link in merge request widget](img/resolve_thread_open_issue_v13_9.png)
This will prepare an issue with its content referring to the merge request and
the unresolved threads.
......@@ -161,7 +161,7 @@ box and hit **Save** for the changes to take effect.
From now on, you will not be able to merge from the UI until all threads
are resolved.
![Only allow merge if all the threads are resolved message](img/resolve_thread_open_issue.png)
![Only allow merge if all the threads are resolved message](img/resolve_thread_open_issue_v13_9.png)
### Automatically resolve merge request diff threads when they become outdated
......
......@@ -106,6 +106,9 @@ export default {
dastSiteValidationDocsPath: {
default: '',
},
profilesLibraryPath: {
default: '',
},
},
props: {
helpPagePath: {
......@@ -165,6 +168,11 @@ export default {
? s__('OnDemandScans|Edit on-demand DAST scan')
: s__('OnDemandScans|New on-demand DAST scan');
},
manageProfilesLabel() {
return this.glFeatures.dastSavedScans
? s__('OnDemandScans|Manage DAST scans')
: s__('OnDemandScans|Manage profiles');
},
selectedScannerProfile() {
return this.selectedScannerProfileId
? this.scannerProfiles.find(({ id }) => id === this.selectedScannerProfileId)
......@@ -304,7 +312,12 @@ export default {
<template>
<gl-form novalidate @submit.prevent="onSubmit()">
<header class="gl-mb-6">
<h2>{{ title }}</h2>
<div class="gl-mt-6 gl-display-flex">
<h2 class="gl-flex-grow-1 gl-my-0">{{ title }}</h2>
<gl-button :href="profilesLibraryPath" data-testid="manage-profiles-link">
{{ manageProfilesLabel }}
</gl-button>
</div>
<p>
<gl-sprintf
:message="
......
<script>
import { GlButton, GlCard, GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import {
GlButton,
GlCard,
GlFormGroup,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlTooltipDirective,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
export default {
name: 'OnDemandScansProfileSelector',
......@@ -9,6 +18,10 @@ export default {
GlFormGroup,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
libraryPath: {
......@@ -30,10 +43,24 @@ export default {
default: null,
},
},
data() {
return { searchTerm: '' };
},
computed: {
selectedProfile() {
return this.value ? this.profiles.find(({ id }) => this.value === id) : null;
},
filteredProfiles() {
if (this.searchTerm) {
return fuzzaldrinPlus.filter(this.profiles, this.searchTerm, {
key: ['profileName'],
});
}
return this.profiles;
},
filteredProfilesEmpty() {
return this.filteredProfiles.length === 0;
},
},
};
</script>
......@@ -47,24 +74,13 @@ export default {
<slot name="title"></slot>
</h3>
</div>
<div class="col-5 gl-text-right">
<gl-button
:href="profiles.length ? libraryPath : null"
:disabled="!profiles.length"
variant="success"
category="secondary"
size="small"
data-testid="manage-profiles-link"
>
{{ s__('OnDemandScans|Manage profiles') }}
</gl-button>
</div>
</div>
</template>
<gl-form-group v-if="profiles.length">
<template #label>
<slot name="label"></slot>
</template>
<gl-dropdown
:text="
selectedProfile
......@@ -74,8 +90,11 @@ export default {
class="mw-460"
data-testid="profiles-dropdown"
>
<template #header>
<gl-search-box-by-type v-model.trim="searchTerm" />
</template>
<gl-dropdown-item
v-for="profile in profiles"
v-for="profile in filteredProfiles"
:key="profile.id"
:is-checked="value === profile.id"
is-check-item
......@@ -83,12 +102,33 @@ export default {
>
{{ profile.profileName }}
</gl-dropdown-item>
<div v-show="filteredProfilesEmpty" class="gl-p-3 gl-text-center">
{{ __('No matching results...') }}
</div>
<template #footer>
<gl-dropdown-item :href="newProfilePath" data-testid="create-profile-option">
<slot name="new-profile"></slot>
</gl-dropdown-item>
<gl-dropdown-item :href="libraryPath" data-testid="manage-profiles-option">
<slot name="manage-profile"></slot>
</gl-dropdown-item>
</template>
</gl-dropdown>
<div
v-if="value && $scopedSlots.summary"
data-testid="selected-profile-summary"
class="gl-mt-6 gl-pt-6 gl-border-t-solid gl-border-gray-100 gl-border-t-1"
>
<gl-button
v-if="selectedProfile"
v-gl-tooltip
category="primary"
icon="pencil"
:title="s__('DastProfiles|Edit profile')"
:href="selectedProfile.editPath"
class="gl-absolute gl-right-7"
/>
<slot name="summary"></slot>
</div>
</gl-form-group>
......
......@@ -56,7 +56,8 @@ export default {
'OnDemandScans|No profile yet. In order to create a new scan, you need to have at least one completed scanner profile.',
)
}}</template>
<template #new-profile>{{ s__('OnDemandScans|Create a new scanner profile') }}</template>
<template #new-profile>{{ s__('OnDemandScans|Create new scanner profile') }}</template>
<template #manage-profile>{{ s__('OnDemandScans|Manage scanner profiles') }}</template>
<template #summary>
<slot name="summary"></slot>
</template>
......
......@@ -59,7 +59,8 @@ export default {
'OnDemandScans|No profile yet. In order to create a new scan, you need to have at least one completed site profile.',
)
}}</template>
<template #new-profile>{{ s__('OnDemandScans|Create a new site profile') }}</template>
<template #new-profile>{{ s__('OnDemandScans|Create new site profile') }}</template>
<template #manage-profile>{{ s__('OnDemandScans|Manage site profiles') }}</template>
<template #summary>
<slot name="summary"></slot>
</template>
......
......@@ -13,6 +13,7 @@ export default () => {
dastSiteValidationDocsPath,
projectPath,
defaultBranch,
profilesLibraryPath,
scannerProfilesLibraryPath,
siteProfilesLibraryPath,
newSiteProfilePath,
......@@ -25,6 +26,7 @@ export default () => {
el,
apolloProvider,
provide: {
profilesLibraryPath,
scannerProfilesLibraryPath,
siteProfilesLibraryPath,
newScannerProfilePath,
......
......@@ -30,7 +30,7 @@ export default {
},
strings: {
modalBody: __(
"Once a project is permanently deleted it cannot be recovered. You will lose this project's repository and all content: issues, merge requests etc.",
"Once a project is permanently deleted, it cannot be recovered. You will lose this project's repository and all related resources, including issues, merge requests etc.",
),
helpLabel: __('Recovering projects'),
recoveryMessage: __('You can recover this project until %{date}'),
......
......@@ -117,13 +117,13 @@ module EE
end
def permanent_delete_message(project)
message = _('This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all content: issues, merge requests, etc.')
message = _('This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all related resources, including issues, merge requests, etc.')
html_escape(message) % remove_message_data(project)
end
def marked_for_removal_message(project)
date = permanent_deletion_date(Time.now.utc)
message = _('This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}on %{date}%{strongClose}, including its repositories and all content: issues, merge requests, etc.')
message = _('This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}on %{date}%{strongClose}, including its repositories and all related resources, including issues, merge requests, etc.')
html_escape(message) % remove_message_data(project).merge(date: date)
end
......
......@@ -8,6 +8,7 @@ module Projects::OnDemandScansHelper
'empty-state-svg-path' => image_path('illustrations/empty-state/ondemand-scan-empty.svg'),
'default-branch' => project.default_branch,
'project-path' => project.path_with_namespace,
'profiles-library-path' => project_security_configuration_dast_profiles_path(project),
'scanner-profiles-library-path' => project_security_configuration_dast_profiles_path(project, anchor: 'scanner-profiles'),
'site-profiles-library-path' => project_security_configuration_dast_profiles_path(project, anchor: 'site-profiles'),
'new-scanner-profile-path' => new_project_security_configuration_dast_profiles_dast_scanner_profile_path(project),
......
---
title: Update project's advanced settings UI text
merge_request: 51442
author:
type: other
---
title: Improve UX for DAST Profiles selector
merge_request: 51957
author:
type: changed
......@@ -21,28 +21,6 @@ exports[`OnDemandScansScannerProfileSelector renders properly with profiles 1`]
Scanner profile
</h3>
</div>
<div
class="col-5 gl-text-right"
>
<a
class="btn btn-success btn-sm gl-button btn-success-secondary"
data-testid="manage-profiles-link"
href="/test/scanner/profiles/library/path"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Manage profiles
</span>
</a>
</div>
</div>
</div>
......@@ -104,11 +82,45 @@ exports[`OnDemandScansScannerProfileSelector renders properly with profiles 1`]
<div
class="gl-new-dropdown-inner"
>
<!---->
<div
class="gl-new-dropdown-header gl-border-b-0!"
>
<!---->
<div
class="gl-search-box-by-type"
>
<svg
aria-hidden="true"
class="gl-search-box-by-type-search-icon gl-icon s16"
data-testid="search-icon"
>
<use
href="#search"
/>
</svg>
<input
aria-label="Search"
class="gl-form-input gl-search-box-by-type-input form-control"
placeholder="Search"
type="text"
/>
<div
class="gl-search-box-by-type-right-icons"
>
<!---->
<!---->
</div>
</div>
</div>
<div
class="gl-new-dropdown-contents"
>
<li
class="gl-new-dropdown-item"
role="presentation"
......@@ -189,9 +201,86 @@ exports[`OnDemandScansScannerProfileSelector renders properly with profiles 1`]
<!---->
</button>
</li>
<div
class="gl-p-3 gl-text-center"
style="display: none;"
>
No matching results...
</div>
</div>
<!---->
<div
class="gl-new-dropdown-footer"
>
<li
class="gl-new-dropdown-item"
role="presentation"
>
<a
class="dropdown-item"
data-testid="create-profile-option"
href="/test/new/scanner/profile/path"
role="menuitem"
target="_self"
>
<!---->
<!---->
<!---->
<div
class="gl-new-dropdown-item-text-wrapper"
>
<p
class="gl-new-dropdown-item-text-primary"
>
Create new scanner profile
</p>
<!---->
</div>
<!---->
</a>
</li>
<li
class="gl-new-dropdown-item"
role="presentation"
>
<a
class="dropdown-item"
data-testid="manage-profiles-option"
href="/test/scanner/profiles/library/path"
role="menuitem"
target="_self"
>
<!---->
<!---->
<!---->
<div
class="gl-new-dropdown-item-text-wrapper"
>
<p
class="gl-new-dropdown-item-text-primary"
>
Manage scanner profiles
</p>
<!---->
</div>
<!---->
</a>
</li>
</div>
</div>
</ul>
......@@ -201,6 +290,26 @@ exports[`OnDemandScansScannerProfileSelector renders properly with profiles 1`]
class="gl-mt-6 gl-pt-6 gl-border-t-solid gl-border-gray-100 gl-border-t-1"
data-testid="selected-profile-summary"
>
<a
class="btn gl-absolute gl-right-7 btn-default btn-md gl-button btn-icon"
href="/scanner_profile/edit/1"
title="Edit profile"
>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="pencil-icon"
>
<use
href="#pencil"
/>
</svg>
<!---->
</a>
<div>
Scanner profile #1's summary
</div>
......@@ -236,29 +345,6 @@ exports[`OnDemandScansScannerProfileSelector renders properly without profiles 1
Scanner profile
</h3>
</div>
<div
class="col-5 gl-text-right"
>
<button
class="btn btn-success btn-sm disabled gl-button btn-success-secondary"
data-testid="manage-profiles-link"
disabled="disabled"
type="button"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Manage profiles
</span>
</button>
</div>
</div>
</div>
......@@ -284,7 +370,7 @@ exports[`OnDemandScansScannerProfileSelector renders properly without profiles 1
<span
class="gl-button-text"
>
Create a new scanner profile
Create new scanner profile
</span>
</a>
</div>
......
......@@ -21,28 +21,6 @@ exports[`OnDemandScansSiteProfileSelector renders properly with profiles 1`] = `
Site profile
</h3>
</div>
<div
class="col-5 gl-text-right"
>
<a
class="btn btn-success btn-sm gl-button btn-success-secondary"
data-testid="manage-profiles-link"
href="/test/site/profiles/library/path"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Manage profiles
</span>
</a>
</div>
</div>
</div>
......@@ -104,11 +82,45 @@ exports[`OnDemandScansSiteProfileSelector renders properly with profiles 1`] = `
<div
class="gl-new-dropdown-inner"
>
<!---->
<div
class="gl-new-dropdown-header gl-border-b-0!"
>
<!---->
<div
class="gl-search-box-by-type"
>
<svg
aria-hidden="true"
class="gl-search-box-by-type-search-icon gl-icon s16"
data-testid="search-icon"
>
<use
href="#search"
/>
</svg>
<input
aria-label="Search"
class="gl-form-input gl-search-box-by-type-input form-control"
placeholder="Search"
type="text"
/>
<div
class="gl-search-box-by-type-right-icons"
>
<!---->
<!---->
</div>
</div>
</div>
<div
class="gl-new-dropdown-contents"
>
<li
class="gl-new-dropdown-item"
role="presentation"
......@@ -189,9 +201,86 @@ exports[`OnDemandScansSiteProfileSelector renders properly with profiles 1`] = `
<!---->
</button>
</li>
<div
class="gl-p-3 gl-text-center"
style="display: none;"
>
No matching results...
</div>
</div>
<!---->
<div
class="gl-new-dropdown-footer"
>
<li
class="gl-new-dropdown-item"
role="presentation"
>
<a
class="dropdown-item"
data-testid="create-profile-option"
href="/test/new/site/profile/path"
role="menuitem"
target="_self"
>
<!---->
<!---->
<!---->
<div
class="gl-new-dropdown-item-text-wrapper"
>
<p
class="gl-new-dropdown-item-text-primary"
>
Create new site profile
</p>
<!---->
</div>
<!---->
</a>
</li>
<li
class="gl-new-dropdown-item"
role="presentation"
>
<a
class="dropdown-item"
data-testid="manage-profiles-option"
href="/test/site/profiles/library/path"
role="menuitem"
target="_self"
>
<!---->
<!---->
<!---->
<div
class="gl-new-dropdown-item-text-wrapper"
>
<p
class="gl-new-dropdown-item-text-primary"
>
Manage site profiles
</p>
<!---->
</div>
<!---->
</a>
</li>
</div>
</div>
</ul>
......@@ -201,6 +290,26 @@ exports[`OnDemandScansSiteProfileSelector renders properly with profiles 1`] = `
class="gl-mt-6 gl-pt-6 gl-border-t-solid gl-border-gray-100 gl-border-t-1"
data-testid="selected-profile-summary"
>
<a
class="btn gl-absolute gl-right-7 btn-default btn-md gl-button btn-icon"
href="/site_profiles/edit/1"
title="Edit profile"
>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="pencil-icon"
>
<use
href="#pencil"
/>
</svg>
<!---->
</a>
<div>
Site profile #1's summary
</div>
......@@ -236,29 +345,6 @@ exports[`OnDemandScansSiteProfileSelector renders properly without profiles 1`]
Site profile
</h3>
</div>
<div
class="col-5 gl-text-right"
>
<button
class="btn btn-success btn-sm disabled gl-button btn-success-secondary"
data-testid="manage-profiles-link"
disabled="disabled"
type="button"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Manage profiles
</span>
</button>
</div>
</div>
</div>
......@@ -284,7 +370,7 @@ exports[`OnDemandScansSiteProfileSelector renders properly without profiles 1`]
<span
class="gl-button-text"
>
Create a new site profile
Create new site profile
</span>
</a>
</div>
......
......@@ -13,8 +13,20 @@ describe('OnDemandScansProfileSelector', () => {
profiles: [],
};
const defaultDropdownItems = [
{
text: 'Create new profile',
isChecked: false,
},
{
text: 'Manage scanner profiles',
isChecked: false,
},
];
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findProfilesLibraryPathLink = () => findByTestId('manage-profiles-link');
const findCreateProfileOption = () => findByTestId('create-profile-option');
const findManageProfilesOption = () => findByTestId('manage-profiles-option');
const findProfilesDropdown = () => findByTestId('profiles-dropdown');
const findCreateNewProfileLink = () => findByTestId('create-profile-link');
const findSelectedProfileSummary = () => findByTestId('selected-profile-summary');
......@@ -25,7 +37,6 @@ describe('OnDemandScansProfileSelector', () => {
text: x.text(),
isChecked: x.props('isChecked'),
}));
const selectFirstProfile = () => {
return findProfilesDropdown().find(GlDropdownItem).vm.$emit('click');
};
......@@ -41,7 +52,8 @@ describe('OnDemandScansProfileSelector', () => {
label: 'Use existing scanner profile',
summary: `<div>Profile's summary</div>`,
'no-profiles': 'No profile yet',
'new-profile': 'Create a new profile',
'new-profile': 'Create new profile',
'manage-profile': 'Manage scanner profiles',
},
},
options,
......@@ -64,8 +76,8 @@ describe('OnDemandScansProfileSelector', () => {
createFullComponent();
});
it('disables the link to profiles library', () => {
expect(findProfilesLibraryPathLink().props('disabled')).toBe(true);
it('do not show profile selector', () => {
expect(findProfilesDropdown().exists()).toBe(false);
});
it('shows a help text and a link to create a new profile', () => {
......@@ -74,7 +86,7 @@ describe('OnDemandScansProfileSelector', () => {
expect(wrapper.text()).toContain('No profile yet');
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe('/path/to/new/profile/form');
expect(link.text()).toBe('Create a new profile');
expect(link.text()).toBe('Create new profile');
});
});
......@@ -85,11 +97,6 @@ describe('OnDemandScansProfileSelector', () => {
});
});
it('enables link to profiles management', () => {
expect(findProfilesLibraryPathLink().props('disabled')).toBe(false);
expect(findProfilesLibraryPathLink().attributes('href')).toBe('/path/to/profiles/library');
});
it('shows a dropdown containing the profiles', () => {
const dropdown = findProfilesDropdown();
......@@ -105,12 +112,21 @@ describe('OnDemandScansProfileSelector', () => {
});
it('shows dropdown items for each profile', () => {
expect(parseDropdownItems()).toEqual(
scannerProfiles.map((x) => ({
expect(parseDropdownItems()).toEqual([
...scannerProfiles.map((x) => ({
text: x.profileName,
isChecked: false,
})),
);
...defaultDropdownItems,
]);
});
it('show options for profiles management', () => {
expect(findCreateProfileOption().exists()).toBe(true);
expect(findCreateProfileOption().attributes('href')).toBe('/path/to/new/profile/form');
expect(findManageProfilesOption().exists()).toBe(true);
expect(findManageProfilesOption().attributes('href')).toBe('/path/to/profiles/library');
});
it('does not show summary', () => {
......@@ -139,12 +155,13 @@ describe('OnDemandScansProfileSelector', () => {
});
it('displays item as checked', () => {
expect(parseDropdownItems()).toEqual(
scannerProfiles.map((x, i) => ({
expect(parseDropdownItems()).toEqual([
...scannerProfiles.map((x, i) => ({
text: x.profileName,
isChecked: i === 0,
})),
);
...defaultDropdownItems,
]);
});
});
});
......@@ -43,7 +43,7 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
<div>
<p>
Once a project is permanently deleted it cannot be recovered. You will lose this project's repository and all content: issues, merge requests etc.
Once a project is permanently deleted, it cannot be recovered. You will lose this project's repository and all related resources, including issues, merge requests etc.
</p>
<p
......
......@@ -13,6 +13,7 @@ RSpec.describe Projects::OnDemandScansHelper do
'empty-state-svg-path' => match_asset_path('/assets/illustrations/empty-state/ondemand-scan-empty.svg'),
'default-branch' => project.default_branch,
'project-path' => project.path_with_namespace,
'profiles-library-path' => project_security_configuration_dast_profiles_path(project),
'scanner-profiles-library-path' => project_security_configuration_dast_profiles_path(project, anchor: 'scanner-profiles'),
'site-profiles-library-path' => project_security_configuration_dast_profiles_path(project, anchor: 'site-profiles'),
'new-scanner-profile-path' => new_project_security_configuration_dast_profiles_dast_scanner_profile_path(project),
......
......@@ -11,13 +11,17 @@ RSpec.describe EE::Issuable do
allow(InternalId).to receive(:generate_next).and_return(nil)
end
it { is_expected.to validate_presence_of(:iid) }
it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_length_of(:title).is_at_most(::Issuable::TITLE_LENGTH_MAX) }
it { is_expected.to validate_length_of(:description).is_at_most(::Issuable::DESCRIPTION_LENGTH_MAX).on(:create) }
it_behaves_like 'validates description length with custom validation'
it_behaves_like 'validates description length with custom validation' do
before do
allow(InternalId).to receive(:generate_next).and_call_original
end
end
it_behaves_like 'truncates the description to its allowed maximum length on import'
end
end
......
......@@ -1331,6 +1331,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
msgid "A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "A ready-to-go template for use with Android apps"
msgstr ""
......@@ -3722,7 +3725,7 @@ msgstr ""
msgid "Archived projects"
msgstr ""
msgid "Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}"
msgid "Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "Are you ABSOLUTELY SURE you wish to delete this project?"
......@@ -9403,7 +9406,7 @@ msgstr ""
msgid "Deleting a project places it into a read-only state until %{date}, at which point the project will be permanently deleted. Are you ABSOLUTELY sure?"
msgstr ""
msgid "Deleting the project will delete its repository and all related resources including issues, merge requests etc."
msgid "Deleting the project will delete its repository and all related resources including issues, merge requests, etc."
msgstr ""
msgid "Deletion pending. This project will be removed on %{date}. Repository and other project resources are read-only."
......@@ -11880,7 +11883,7 @@ msgstr ""
msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
msgstr ""
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgid "Export this project with all its related data in order to move it to a new GitLab instance. When the exported file is ready, you can download it from this page or from the download link in the email notification you will receive. You can then import it when creating a new project. %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "Export variable to pipelines running on protected branches and tags only."
......@@ -19444,6 +19447,9 @@ msgstr ""
msgid "No matching results for \"%{query}\""
msgstr ""
msgid "No matching results..."
msgstr ""
msgid "No members found"
msgstr ""
......@@ -19993,10 +19999,10 @@ msgstr ""
msgid "OnDemandScans|Could not run the scan. Please try again."
msgstr ""
msgid "OnDemandScans|Create a new scanner profile"
msgid "OnDemandScans|Create new scanner profile"
msgstr ""
msgid "OnDemandScans|Create a new site profile"
msgid "OnDemandScans|Create new site profile"
msgstr ""
msgid "OnDemandScans|Description (optional)"
......@@ -20008,9 +20014,18 @@ msgstr ""
msgid "OnDemandScans|For example: Tests the login page for SQL injections"
msgstr ""
msgid "OnDemandScans|Manage DAST scans"
msgstr ""
msgid "OnDemandScans|Manage profiles"
msgstr ""
msgid "OnDemandScans|Manage scanner profiles"
msgstr ""
msgid "OnDemandScans|Manage site profiles"
msgstr ""
msgid "OnDemandScans|My daily scan"
msgstr ""
......@@ -20062,10 +20077,10 @@ msgstr ""
msgid "OnDemandScans|You cannot run an active scan against an unvalidated site."
msgstr ""
msgid "Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc."
msgid "Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc."
msgstr ""
msgid "Once a project is permanently deleted it cannot be recovered. You will lose this project's repository and all content: issues, merge requests etc."
msgid "Once a project is permanently deleted, it cannot be recovered. You will lose this project's repository and all related resources, including issues, merge requests etc."
msgstr ""
msgid "Once imported, repositories can be mirrored over SSH. Read more %{link_start}here%{link_end}."
......@@ -28978,16 +28993,16 @@ msgstr ""
msgid "This action cannot be undone, and will permanently delete the %{key} SSH key"
msgstr ""
msgid "This action cannot be undone. You will lose this project's repository and all content: issues, merge requests, etc."
msgid "This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc."
msgstr ""
msgid "This action has been performed too many times. Try again later."
msgstr ""
msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all content: issues, merge requests, etc."
msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all related resources, including issues, merge requests, etc."
msgstr ""
msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}on %{date}%{strongClose}, including its repositories and all content: issues, merge requests, etc."
msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}on %{date}%{strongClose}, including its repositories and all related resources, including issues, merge requests, etc."
msgstr ""
msgid "This also resolves all related threads"
......@@ -30122,6 +30137,9 @@ msgstr ""
msgid "Transfer project"
msgstr ""
msgid "Transfer your project into another namespace. %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "TransferGroup|Cannot transfer group to one of its subgroup."
msgstr ""
......@@ -30499,7 +30517,7 @@ msgstr ""
msgid "Unarchive project"
msgstr ""
msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}"
msgid "Unarchiving the project will restore its members' ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "Unassign from commenting user"
......
......@@ -53,12 +53,12 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
variant="danger"
>
<gl-sprintf-stub
message="Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc."
message="Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc."
/>
</gl-alert-stub>
<p>
This action cannot be undone. You will lose this project's repository and all content: issues, merge requests, etc.
This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc.
</p>
<p
......
......@@ -1874,7 +1874,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
has_internal_id :iid,
scope: :project,
init: ->(s, _scope) { s&.project&.issues&.maximum(:iid) },
backfill: true,
presence: false
end
end
......@@ -1928,258 +1927,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(issue_b.iid).to eq(3)
end
context 'when the new code creates a row post deploy but before the migration runs' do
it 'does not change the row iid' do
project = setup
issue = Issue.create!(project_id: project.id)
model.backfill_iids('issues')
expect(issue.reload.iid).to eq(1)
end
it 'backfills iids for rows already in the database' do
project = setup
issue_a = issues.create!(project_id: project.id)
issue_b = issues.create!(project_id: project.id)
issue_c = Issue.create!(project_id: project.id)
model.backfill_iids('issues')
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(2)
expect(issue_c.reload.iid).to eq(3)
end
it 'backfills iids across multiple projects' do
project_a = setup
project_b = setup
issue_a = issues.create!(project_id: project_a.id)
issue_b = issues.create!(project_id: project_b.id)
issue_c = Issue.create!(project_id: project_a.id)
issue_d = Issue.create!(project_id: project_b.id)
model.backfill_iids('issues')
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(1)
expect(issue_c.reload.iid).to eq(2)
expect(issue_d.reload.iid).to eq(2)
end
it 'generates iids properly for models created after the migration' do
project = setup
issue_a = issues.create!(project_id: project.id)
issue_b = issues.create!(project_id: project.id)
issue_c = Issue.create!(project_id: project.id)
model.backfill_iids('issues')
issue_d = Issue.create!(project_id: project.id)
issue_e = Issue.create!(project_id: project.id)
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(2)
expect(issue_c.reload.iid).to eq(3)
expect(issue_d.iid).to eq(4)
expect(issue_e.iid).to eq(5)
end
it 'backfills iids and properly generates iids for new models across multiple projects' do
project_a = setup
project_b = setup
issue_a = issues.create!(project_id: project_a.id)
issue_b = issues.create!(project_id: project_b.id)
issue_c = Issue.create!(project_id: project_a.id)
issue_d = Issue.create!(project_id: project_b.id)
model.backfill_iids('issues')
issue_e = Issue.create!(project_id: project_a.id)
issue_f = Issue.create!(project_id: project_b.id)
issue_g = Issue.create!(project_id: project_a.id)
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(1)
expect(issue_c.reload.iid).to eq(2)
expect(issue_d.reload.iid).to eq(2)
expect(issue_e.iid).to eq(3)
expect(issue_f.iid).to eq(3)
expect(issue_g.iid).to eq(4)
end
end
context 'when the new code creates a model and then old code creates a model post deploy but before the migration runs' do
it 'backfills iids' do
project = setup
issue_a = issues.create!(project_id: project.id)
issue_b = Issue.create!(project_id: project.id)
issue_c = issues.create!(project_id: project.id)
model.backfill_iids('issues')
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(2)
expect(issue_c.reload.iid).to eq(3)
end
it 'generates an iid for a new model after the migration' do
project = setup
issue_a = issues.create!(project_id: project.id)
issue_b = issues.create!(project_id: project.id)
issue_c = Issue.create!(project_id: project.id)
issue_d = issues.create!(project_id: project.id)
model.backfill_iids('issues')
issue_e = Issue.create!(project_id: project.id)
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(2)
expect(issue_c.reload.iid).to eq(3)
expect(issue_d.reload.iid).to eq(4)
expect(issue_e.iid).to eq(5)
end
end
context 'when the new code and old code alternate creating models post deploy but before the migration runs' do
it 'backfills iids' do
project = setup
issue_a = issues.create!(project_id: project.id)
issue_b = Issue.create!(project_id: project.id)
issue_c = issues.create!(project_id: project.id)
issue_d = Issue.create!(project_id: project.id)
model.backfill_iids('issues')
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(2)
expect(issue_c.reload.iid).to eq(3)
expect(issue_d.reload.iid).to eq(4)
end
it 'generates an iid for a new model after the migration' do
project = setup
issue_a = issues.create!(project_id: project.id)
issue_b = issues.create!(project_id: project.id)
issue_c = Issue.create!(project_id: project.id)
issue_d = issues.create!(project_id: project.id)
issue_e = Issue.create!(project_id: project.id)
model.backfill_iids('issues')
issue_f = Issue.create!(project_id: project.id)
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(2)
expect(issue_c.reload.iid).to eq(3)
expect(issue_d.reload.iid).to eq(4)
expect(issue_e.reload.iid).to eq(5)
expect(issue_f.iid).to eq(6)
end
end
context 'when the new code creates and deletes a model post deploy but before the migration runs' do
it 'backfills iids for rows already in the database' do
project = setup
issue_a = issues.create!(project_id: project.id)
issue_b = issues.create!(project_id: project.id)
issue_c = Issue.create!(project_id: project.id)
issue_c.delete
model.backfill_iids('issues')
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(2)
end
it 'successfully creates a new model after the migration' do
project = setup
issue_a = issues.create!(project_id: project.id)
issue_b = issues.create!(project_id: project.id)
issue_c = Issue.create!(project_id: project.id)
issue_c.delete
model.backfill_iids('issues')
issue_d = Issue.create!(project_id: project.id)
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(2)
expect(issue_d.iid).to eq(3)
end
end
context 'when the new code creates and deletes a model and old code creates a model post deploy but before the migration runs' do
it 'backfills iids' do
project = setup
issue_a = issues.create!(project_id: project.id)
issue_b = issues.create!(project_id: project.id)
issue_c = Issue.create!(project_id: project.id)
issue_c.delete
issue_d = issues.create!(project_id: project.id)
model.backfill_iids('issues')
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(2)
expect(issue_d.reload.iid).to eq(3)
end
it 'successfully creates a new model after the migration' do
project = setup
issue_a = issues.create!(project_id: project.id)
issue_b = issues.create!(project_id: project.id)
issue_c = Issue.create!(project_id: project.id)
issue_c.delete
issue_d = issues.create!(project_id: project.id)
model.backfill_iids('issues')
issue_e = Issue.create!(project_id: project.id)
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(2)
expect(issue_d.reload.iid).to eq(3)
expect(issue_e.iid).to eq(4)
end
end
context 'when the new code creates and deletes a model and then creates another model post deploy but before the migration runs' do
it 'successfully generates an iid for a new model after the migration' do
project = setup
issue_a = issues.create!(project_id: project.id)
issue_b = issues.create!(project_id: project.id)
issue_c = Issue.create!(project_id: project.id)
issue_c.delete
issue_d = Issue.create!(project_id: project.id)
model.backfill_iids('issues')
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(2)
expect(issue_d.reload.iid).to eq(3)
end
it 'successfully generates an iid for a new model after the migration' do
project = setup
issue_a = issues.create!(project_id: project.id)
issue_b = issues.create!(project_id: project.id)
issue_c = Issue.create!(project_id: project.id)
issue_c.delete
issue_d = Issue.create!(project_id: project.id)
model.backfill_iids('issues')
issue_e = Issue.create!(project_id: project.id)
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(2)
expect(issue_d.reload.iid).to eq(3)
expect(issue_e.iid).to eq(4)
end
end
context 'when the first model is created for a project after the migration' do
it 'generates an iid' do
project_a = setup
......
......@@ -54,6 +54,10 @@ RSpec.describe Gitlab::Database::WithLockRetries do
lock_fiber.resume # start the transaction and lock the table
end
after do
lock_fiber.resume if lock_fiber.alive?
end
context 'lock_fiber' do
it 'acquires lock successfully' do
check_exclusive_lock_query = """
......
......@@ -87,6 +87,158 @@ RSpec.describe AtomicInternalId do
end
end
describe '#clear_scope_iid!' do
context 'when no ensure_if condition is given' do
it 'clears automatically set IIDs' do
expect(milestone).to receive(:clear_project_iid!).and_call_original
expect_iid_to_be_set_and_rollback(milestone)
expect(milestone.iid).to be_nil
end
it 'does not clear manually set IIDS' do
milestone.iid = external_iid
expect(milestone).to receive(:clear_project_iid!).and_call_original
expect_iid_to_be_set_and_rollback(milestone)
expect(milestone.iid).to eq(external_iid)
end
end
context 'when an ensure_if condition is given' do
let(:test_class) do
Class.new(ApplicationRecord) do
include AtomicInternalId
include Importable
self.table_name = :milestones
belongs_to :project
has_internal_id :iid, scope: :project, track_if: -> { !importing }, ensure_if: -> { !importing }
def self.name
'TestClass'
end
end
end
let(:instance) { test_class.new(milestone.attributes) }
context 'when the ensure_if condition evaluates to true' do
it 'clears automatically set IIDs' do
expect(instance).to receive(:clear_project_iid!).and_call_original
expect_iid_to_be_set_and_rollback(instance)
expect(instance.iid).to be_nil
end
it 'does not clear manually set IIDs' do
instance.iid = external_iid
expect(instance).to receive(:clear_project_iid!).and_call_original
expect_iid_to_be_set_and_rollback(instance)
expect(instance.iid).to eq(external_iid)
end
end
context 'when the ensure_if condition evaluates to false' do
before do
instance.importing = true
end
it 'does not clear IIDs' do
instance.iid = external_iid
expect(instance).not_to receive(:clear_project_iid!)
expect_iid_to_be_set_and_rollback(instance)
expect(instance.iid).to eq(external_iid)
end
end
end
def expect_iid_to_be_set_and_rollback(instance)
ActiveRecord::Base.transaction(requires_new: true) do
instance.save!
expect(instance.iid).not_to be_nil
raise ActiveRecord::Rollback
end
end
end
describe '#validate_scope_iid_exists!' do
let(:test_class) do
Class.new(ApplicationRecord) do
include AtomicInternalId
include Importable
self.table_name = :milestones
belongs_to :project
def self.name
'TestClass'
end
end
end
let(:instance) { test_class.new(milestone.attributes) }
before do
test_class.has_internal_id :iid, scope: :project, presence: presence, ensure_if: -> { !importing }
instance.importing = true
end
context 'when the presence flag is set' do
let(:presence) { true }
it 'raises an error for blank iids on create' do
expect do
instance.save!
end.to raise_error(described_class::MissingValueError, 'iid was unexpectedly blank!')
end
it 'raises an error for blank iids on update' do
instance.iid = 100
instance.save!
instance.iid = nil
expect do
instance.save!
end.to raise_error(described_class::MissingValueError, 'iid was unexpectedly blank!')
end
end
context 'when the presence flag is not set' do
let(:presence) { false }
it 'does not raise an error for blank iids on create' do
expect { instance.save! }.not_to raise_error
end
it 'does not raise an error for blank iids on update' do
instance.iid = 100
instance.save!
instance.iid = nil
expect { instance.save! }.not_to raise_error
end
end
end
describe '.with_project_iid_supply' do
let(:iid) { 100 }
......
......@@ -45,13 +45,17 @@ RSpec.describe Issuable do
end
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:iid) }
it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_length_of(:title).is_at_most(described_class::TITLE_LENGTH_MAX) }
it { is_expected.to validate_length_of(:description).is_at_most(described_class::DESCRIPTION_LENGTH_MAX).on(:create) }
it_behaves_like 'validates description length with custom validation'
it_behaves_like 'validates description length with custom validation' do
before do
allow(InternalId).to receive(:generate_next).and_call_original
end
end
it_behaves_like 'truncates the description to its allowed maximum length on import'
end
end
......
......@@ -9,19 +9,30 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
end
describe 'Validation' do
before do
allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
instance.valid?
end
context 'when presence validation is required' do
before do
skip unless validate_presence
end
it 'validates presence' do
expect(instance.errors[internal_id_attribute]).to include("can't be blank")
context 'when creating an object' do
before do
allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
end
it 'raises an error if the internal id is blank' do
expect { instance.save! }.to raise_error(AtomicInternalId::MissingValueError)
end
end
context 'when updating an object' do
it 'raises an error if the internal id is blank' do
instance.save!
write_internal_id(nil)
allow(instance).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
expect { instance.save! }.to raise_error(AtomicInternalId::MissingValueError)
end
end
end
......@@ -30,8 +41,27 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
skip if validate_presence
end
it 'does not validate presence' do
expect(instance.errors[internal_id_attribute]).to be_empty
context 'when creating an object' do
before do
allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
end
it 'does not raise an error if the internal id is blank' do
expect(read_internal_id).to be_nil
expect { instance.save! }.not_to raise_error
end
end
context 'when updating an object' do
it 'does not raise an error if the internal id is blank' do
instance.save!
write_internal_id(nil)
allow(instance).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
expect { instance.save! }.not_to raise_error
end
end
end
end
......@@ -76,6 +106,51 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
end
end
describe 'unsetting the instance internal id on rollback' do
context 'when the internal id has been changed' do
context 'when the internal id is automatically set' do
it 'clears it on the instance' do
expect_iid_to_be_set_and_rollback
expect(read_internal_id).to be_nil
end
end
context 'when the internal id is manually set' do
it 'does not clear it on the instance' do
write_internal_id(100)
expect_iid_to_be_set_and_rollback
expect(read_internal_id).not_to be_nil
end
end
end
context 'when the internal id has not been changed' do
it 'preserves the value on the instance' do
instance.save!
original_id = read_internal_id
expect(original_id).not_to be_nil
expect_iid_to_be_set_and_rollback
expect(read_internal_id).to eq(original_id)
end
end
def expect_iid_to_be_set_and_rollback
ActiveRecord::Base.transaction(requires_new: true) do
instance.save!
expect(read_internal_id).not_to be_nil
raise ActiveRecord::Rollback
end
end
end
describe 'supply of internal ids' do
let(:scope_value) { scope_attrs.each_value.first }
let(:method_name) { :"with_#{scope}_#{internal_id_attribute}_supply" }
......
......@@ -2,7 +2,7 @@
require 'rake_helper'
RSpec.describe 'gitlab:pages:migrate_legacy_storagerake task', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/300123' do
RSpec.describe 'gitlab:pages:migrate_legacy_storagerake task' do
before(:context) do
Rake.application.rake_require 'tasks/gitlab/pages'
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment