Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-14 15:08:43 +00:00
parent 7a124e225e
commit 9b8269e570
104 changed files with 759 additions and 458 deletions

View File

@ -9,4 +9,3 @@
/sitespeed-result/ /sitespeed-result/
/fixtures/**/*.graphql /fixtures/**/*.graphql
spec/fixtures/**/*.graphql spec/fixtures/**/*.graphql
**/contracts/consumer/

View File

@ -18,6 +18,20 @@ export default {
required: true, required: true,
}, },
}, },
modal: {
actionPrimary: {
text: __('Discard changes'),
attributes: {
variant: 'danger',
},
},
actionCancel: {
text: __('Cancel'),
attributes: {
variant: 'default',
},
},
},
computed: { computed: {
discardModalId() { discardModalId() {
return `discard-file-${this.activeFile.path}`; return `discard-file-${this.activeFile.path}`;
@ -66,12 +80,11 @@ export default {
</div> </div>
<gl-modal <gl-modal
ref="discardModal" ref="discardModal"
ok-variant="danger"
cancel-variant="light"
:ok-title="__('Discard changes')"
:modal-id="discardModalId" :modal-id="discardModalId"
:title="discardModalTitle" :title="discardModalTitle"
@ok="discardChanges(activeFile.path)" :action-primary="$options.modal.actionPrimary"
:action-cancel="$options.modal.actionCancel"
@primary="discardChanges(activeFile.path)"
> >
{{ __("You will lose all changes you've made to this file. This action cannot be undone.") }} {{ __("You will lose all changes you've made to this file. This action cannot be undone.") }}
</gl-modal> </gl-modal>

View File

@ -38,6 +38,20 @@ export default {
default: __('No changes'), default: __('No changes'),
}, },
}, },
modal: {
actionPrimary: {
text: __('Discard all changes'),
attributes: {
variant: 'danger',
},
},
actionCancel: {
text: __('Cancel'),
attributes: {
variant: 'default',
},
},
},
computed: { computed: {
titleText() { titleText() {
if (!this.title) return __('Changes'); if (!this.title) return __('Changes');
@ -106,11 +120,11 @@ export default {
<gl-modal <gl-modal
v-if="!stagedList" v-if="!stagedList"
ref="discardAllModal" ref="discardAllModal"
ok-variant="danger"
modal-id="discard-all-changes" modal-id="discard-all-changes"
:ok-title="__('Discard all changes')"
:title="__('Discard all changes?')" :title="__('Discard all changes?')"
@ok="unstageAndDiscardAllChanges" :action-primary="$options.modal.actionPrimary"
:action-cancel="$options.modal.actionCancel"
@primary="unstageAndDiscardAllChanges"
> >
{{ $options.discardModalText }} {{ $options.discardModalText }}
</gl-modal> </gl-modal>

View File

@ -174,9 +174,7 @@ export default {
this.removeAllPointerEventListeners(); this.removeAllPointerEventListeners();
if (this.issuableType === IssuableType.Issue) { this.renderSortableLists();
this.renderSortableLists();
}
if (this.workItemsEnabled) { if (this.workItemsEnabled) {
this.renderTaskActions(); this.renderTaskActions();
@ -184,7 +182,10 @@ export default {
} }
}, },
renderSortableLists() { renderSortableLists() {
const lists = document.querySelectorAll('.description ul, .description ol'); // We exclude GLFM table of contents which have a `section-nav` class on the root `ul`.
const lists = document.querySelectorAll(
'.description .md > ul:not(.section-nav), .description .md > ul:not(.section-nav) ul, .description ol',
);
lists.forEach((list) => { lists.forEach((list) => {
if (list.children.length <= 1) { if (list.children.length <= 1) {
return; return;

View File

@ -31,6 +31,7 @@ export default class Todos {
$('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper); $('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper);
$('.js-todos-mark-all', '.js-todos-undo-all').off('click', this.updateallStateClickedWrapper); $('.js-todos-mark-all', '.js-todos-undo-all').off('click', this.updateallStateClickedWrapper);
$('.todo').off('click', this.goToTodoUrl); $('.todo').off('click', this.goToTodoUrl);
$('.todo').off('auxclick', this.goToTodoUrl);
} }
bindEvents() { bindEvents() {
@ -40,6 +41,7 @@ export default class Todos {
$('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper); $('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper);
$('.js-todos-mark-all, .js-todos-undo-all').on('click', this.updateAllStateClickedWrapper); $('.js-todos-mark-all, .js-todos-undo-all').on('click', this.updateAllStateClickedWrapper);
$('.todo').on('click', this.goToTodoUrl); $('.todo').on('click', this.goToTodoUrl);
$('.todo').on('auxclick', this.goToTodoUrl);
} }
initFilters() { initFilters() {
@ -198,11 +200,13 @@ export default class Todos {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
const isPrimaryClick = e.button === 0;
if (isMetaClick(e)) { if (isMetaClick(e)) {
const windowTarget = '_blank'; const windowTarget = '_blank';
window.open(todoLink, windowTarget); window.open(todoLink, windowTarget);
} else { } else if (isPrimaryClick) {
visitUrl(todoLink); visitUrl(todoLink);
} }
} }

View File

@ -4,10 +4,9 @@ import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue'; import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
import currentLicenseQuery from '~/security_configuration/graphql/current_license.query.graphql';
import AutoDevOpsAlert from './auto_dev_ops_alert.vue'; import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue'; import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY, LICENSE_ULTIMATE } from './constants'; import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
import FeatureCard from './feature_card.vue'; import FeatureCard from './feature_card.vue';
import TrainingProviderList from './training_provider_list.vue'; import TrainingProviderList from './training_provider_list.vue';
import UpgradeBanner from './upgrade_banner.vue'; import UpgradeBanner from './upgrade_banner.vue';
@ -51,17 +50,6 @@ export default {
TrainingProviderList, TrainingProviderList,
}, },
inject: ['projectFullPath', 'vulnerabilityTrainingDocsPath'], inject: ['projectFullPath', 'vulnerabilityTrainingDocsPath'],
apollo: {
currentLicensePlan: {
query: currentLicenseQuery,
update({ currentLicense }) {
return currentLicense?.plan;
},
error() {
this.hasCurrentLicenseFetchError = true;
},
},
},
props: { props: {
augmentedSecurityFeatures: { augmentedSecurityFeatures: {
type: Array, type: Array,
@ -96,13 +84,15 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
securityTrainingEnabled: {
type: Boolean,
required: true,
},
}, },
data() { data() {
return { return {
autoDevopsEnabledAlertDismissedProjects: [], autoDevopsEnabledAlertDismissedProjects: [],
errorMessage: '', errorMessage: '',
currentLicensePlan: '',
hasCurrentLicenseFetchError: false,
}; };
}, },
computed: { computed: {
@ -123,12 +113,6 @@ export default {
!this.autoDevopsEnabledAlertDismissedProjects.includes(this.projectFullPath) !this.autoDevopsEnabledAlertDismissedProjects.includes(this.projectFullPath)
); );
}, },
shouldShowVulnerabilityManagementTab() {
// if the query fails (if the plan is `null` also means an error has occurred) we still want to show the feature
const hasQueryError = this.hasCurrentLicenseFetchError || this.currentLicensePlan === null;
return hasQueryError || this.currentLicensePlan === LICENSE_ULTIMATE;
},
}, },
methods: { methods: {
dismissAutoDevopsEnabledAlert() { dismissAutoDevopsEnabledAlert() {
@ -270,7 +254,7 @@ export default {
</section-layout> </section-layout>
</gl-tab> </gl-tab>
<gl-tab <gl-tab
v-if="shouldShowVulnerabilityManagementTab" v-if="securityTrainingEnabled"
data-testid="vulnerability-management-tab" data-testid="vulnerability-management-tab"
:title="$options.i18n.vulnerabilityManagement" :title="$options.i18n.vulnerabilityManagement"
query-param-value="vulnerability-management" query-param-value="vulnerability-management"

View File

@ -310,7 +310,3 @@ export const TEMP_PROVIDER_URLS = {
Kontra: 'https://application.security/', Kontra: 'https://application.security/',
[__('Secure Code Warrior')]: 'https://www.securecodewarrior.com/', [__('Secure Code Warrior')]: 'https://www.securecodewarrior.com/',
}; };
export const LICENSE_ULTIMATE = 'ultimate';
export const LICENSE_FREE = 'free';
export const LICENSE_PREMIUM = 'premium';

View File

@ -1,6 +0,0 @@
query getCurrentLicensePlan {
currentLicense {
id
plan
}
}

View File

@ -56,6 +56,7 @@ export const initSecurityConfiguration = (el) => {
'gitlabCiPresent', 'gitlabCiPresent',
'autoDevopsEnabled', 'autoDevopsEnabled',
'canEnableAutoDevops', 'canEnableAutoDevops',
'securityTrainingEnabled',
]), ]),
}, },
}); });

View File

@ -49,14 +49,14 @@ export default {
}, },
request() { request() {
const state = { const state = {
variant: 'default', selected: false,
icon: 'attention', icon: 'attention',
direction: 'add', direction: 'add',
}; };
if (this.user.attention_requested) { if (this.user.attention_requested) {
Object.assign(state, { Object.assign(state, {
variant: 'warning', selected: true,
icon: 'attention-solid', icon: 'attention-solid',
direction: 'remove', direction: 'remove',
}); });
@ -92,7 +92,7 @@ export default {
> >
<gl-button <gl-button
:loading="loading" :loading="loading"
:variant="request.variant" :selected="request.selected"
:icon="request.icon" :icon="request.icon"
:aria-label="tooltipTitle" :aria-label="tooltipTitle"
:class="{ 'gl-pointer-events-none': !user.can_update_merge_request }" :class="{ 'gl-pointer-events-none': !user.can_update_merge_request }"

View File

@ -32,7 +32,7 @@ export default {
computed: { computed: {
arrowIconName() { arrowIconName() {
return this.isCollapsed ? 'angle-right' : 'angle-down'; return this.isCollapsed ? 'chevron-lg-right' : 'chevron-lg-down';
}, },
ariaLabel() { ariaLabel() {
return this.isCollapsed ? __('Expand') : __('Collapse'); return this.isCollapsed ? __('Expand') : __('Collapse');

View File

@ -270,6 +270,10 @@
.reviewer-grid { .reviewer-grid {
[data-css-area='attention'] { [data-css-area='attention'] {
grid-area: attention; grid-area: attention;
button.selected svg {
fill: $orange-500;
}
} }
[data-css-area='user'] { [data-css-area='user'] {

View File

@ -1,6 +1,6 @@
= render Pajamas::AlertComponent.new(title: _('Too many changes to show.'), = render Pajamas::AlertComponent.new(title: _('Too many changes to show.'),
variant: :warning, variant: :warning,
alert_class: 'gl-mb-5') do |c| alert_options: { class: 'gl-mb-5' }) do |c|
= c.body do = c.body do
= message = message

View File

@ -1,11 +1,10 @@
.gl-alert{ @alert_options, role: 'alert', class: [base_class, @alert_class], data: @alert_data } .gl-alert{ @alert_options, role: 'alert', class: base_class }
- if @show_icon - if @show_icon
= sprite_icon(icon, css_class: icon_classes) = sprite_icon(icon, css_class: icon_classes)
- if @dismissible - if @dismissible
%button.btn.gl-dismiss-btn.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon.js-close{ type: 'button', %button.btn.gl-dismiss-btn.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon.js-close{ @close_button_options,
aria: { label: _('Dismiss') }, type: 'button',
class: @close_button_class, aria: { label: _('Dismiss') } }
data: @close_button_data }
= sprite_icon('close') = sprite_icon('close')
.gl-alert-content{ role: 'alert' } .gl-alert-content{ role: 'alert' }
- if @title - if @title

View File

@ -7,22 +7,17 @@ module Pajamas
# @param [Symbol] variant # @param [Symbol] variant
# @param [Boolean] dismissible # @param [Boolean] dismissible
# @param [Boolean] show_icon # @param [Boolean] show_icon
# @param [String] alert_class # @param [Hash] alert_options
# @param [Hash] alert_data # @param [Hash] close_button_options
# @param [String] close_button_class
# @param [Hash] close_button_data
def initialize( def initialize(
title: nil, variant: :info, dismissible: true, show_icon: true, title: nil, variant: :info, dismissible: true, show_icon: true,
alert_class: nil, alert_data: {}, alert_options: {}, close_button_class: nil, close_button_data: {}) alert_options: {}, close_button_options: {})
@title = title @title = title
@variant = variant @variant = variant
@dismissible = dismissible @dismissible = dismissible
@show_icon = show_icon @show_icon = show_icon
@alert_class = alert_class
@alert_data = alert_data
@alert_options = alert_options @alert_options = alert_options
@close_button_class = close_button_class @close_button_options = close_button_options
@close_button_data = close_button_data
end end
def base_class def base_class

View File

@ -142,16 +142,22 @@ module Types
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def batched_owners(runner_assoc_type, assoc_type, key, column_name) def batched_owners(runner_assoc_type, assoc_type, key, column_name)
BatchLoader::GraphQL.for(runner.id).batch(key: key) do |runner_ids, loader, args| BatchLoader::GraphQL.for(runner.id).batch(key: key) do |runner_ids, loader|
runner_and_owner_ids = runner_assoc_type.where(runner_id: runner_ids).pluck(:runner_id, column_name) plucked_runner_and_owner_ids = runner_assoc_type
.select(:runner_id, column_name)
owner_ids_by_runner_id = runner_and_owner_ids.group_by(&:first).transform_values { |v| v.pluck(1) } .where(runner_id: runner_ids)
owner_ids = runner_and_owner_ids.pluck(1).uniq .pluck(:runner_id, column_name)
# In plucked_runner_and_owner_ids, first() represents the runner ID, and second() the owner ID,
# so let's group the owner IDs by runner ID
runner_owner_ids_by_runner_id = plucked_runner_and_owner_ids
.group_by(&:first)
.transform_values { |runner_and_owner_id| runner_and_owner_id.map(&:second) }
owner_ids = runner_owner_ids_by_runner_id.values.flatten.uniq
owners = assoc_type.where(id: owner_ids).index_by(&:id) owners = assoc_type.where(id: owner_ids).index_by(&:id)
runner_ids.each do |runner_id| runner_ids.each do |runner_id|
loader.call(runner_id, owner_ids_by_runner_id[runner_id]&.map { |owner_id| owners[owner_id] } || []) loader.call(runner_id, runner_owner_ids_by_runner_id[runner_id]&.map { |owner_id| owners[owner_id] } || [])
end end
end end
end end

View File

@ -30,8 +30,7 @@ module FormHelper
variant: :danger, variant: :danger,
title: headline, title: headline,
dismissible: false, dismissible: false,
alert_class: 'gl-mb-5', alert_options: { id: 'error_explanation', class: 'gl-mb-5' }
alert_options: { id: 'error_explanation' }
) do |c| ) do |c|
c.body do c.body do
tag.ul(class: 'gl-pl-5 gl-mb-0') do tag.ul(class: 'gl-pl-5 gl-mb-0') do

View File

@ -2,11 +2,11 @@
= render Pajamas::AlertComponent.new(variant: :tip, = render Pajamas::AlertComponent.new(variant: :tip,
title: s_('AdminArea|Get security updates from GitLab and stay up to date'), title: s_('AdminArea|Get security updates from GitLab and stay up to date'),
alert_class: 'js-security-newsletter-callout', alert_options: { class: 'js-security-newsletter-callout',
alert_data: { feature_id: Users::CalloutsHelper::SECURITY_NEWSLETTER_CALLOUT, data: { feature_id: Users::CalloutsHelper::SECURITY_NEWSLETTER_CALLOUT,
dismiss_endpoint: callouts_path, dismiss_endpoint: callouts_path,
defer_links: 'true' }, defer_links: 'true' }},
close_button_data: { testid: 'close-security-newsletter-callout' }) do |c| close_button_options: { data: { testid: 'close-security-newsletter-callout' }}) do |c|
= c.body do = c.body do
= s_('AdminArea|Sign up for the GitLab Security Newsletter to get notified for security updates.') = s_('AdminArea|Sign up for the GitLab Security Newsletter to get notified for security updates.')
= c.actions do = c.actions do

View File

@ -15,8 +15,8 @@
.row .row
.col-md-12 .col-md-12
= render Pajamas::AlertComponent.new(variant: :danger, = render Pajamas::AlertComponent.new(variant: :danger,
alert_class: 'gl-mb-5', alert_options: { class: 'gl-mb-5',
alert_data: { testid: 'last-repository-check-failed-alert' }) do |c| data: { testid: 'last-repository-check-failed-alert' }}) do |c|
= c.body do = c.body do
- last_check_message = _("Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages.") - last_check_message = _("Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages.")
- last_check_message = last_check_message % { last_check_timestamp: time_ago_with_tooltip(@project.last_repository_check_at) } - last_check_message = last_check_message % { last_check_timestamp: time_ago_with_tooltip(@project.last_repository_check_at) }

View File

@ -1,6 +1,6 @@
- if registration_features_can_be_prompted? - if registration_features_can_be_prompted?
= render Pajamas::AlertComponent.new(variant: :tip, = render Pajamas::AlertComponent.new(variant: :tip,
alert_class: 'gl-my-5', alert_options: { class: 'gl-my-5' },
dismissible: false) do |c| dismissible: false) do |c|
= c.body do = c.body do
= render 'shared/registration_features_discovery_message', feature_title: s_('RegistrationFeatures|send emails to users') = render 'shared/registration_features_discovery_message', feature_title: s_('RegistrationFeatures|send emails to users')

View File

@ -7,12 +7,12 @@
%span.gl-ml-2= s_('ClusterIntegration|Kubernetes cluster is being created...') %span.gl-ml-2= s_('ClusterIntegration|Kubernetes cluster is being created...')
= render Pajamas::AlertComponent.new(variant: :warning, = render Pajamas::AlertComponent.new(variant: :warning,
alert_class: 'hidden js-cluster-api-unreachable') do |c| alert_options: { class: 'hidden js-cluster-api-unreachable' }) do |c|
= c.body do = c.body do
= s_('ClusterIntegration|Your cluster API is unreachable. Please ensure your API URL is correct.') = s_('ClusterIntegration|Your cluster API is unreachable. Please ensure your API URL is correct.')
= render Pajamas::AlertComponent.new(variant: :warning, = render Pajamas::AlertComponent.new(variant: :warning,
alert_class: 'hidden js-cluster-authentication-failure js-cluster-api-unreachable') do |c| alert_options: { class: 'hidden js-cluster-authentication-failure js-cluster-api-unreachable' }) do |c|
= c.body do = c.body do
= s_('ClusterIntegration|There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid.') = s_('ClusterIntegration|There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid.')

View File

@ -1,4 +1,4 @@
= render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_class: 'gl-mt-6 gl-mb-3') do |c| = render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_options: { class: 'gl-mt-6 gl-mb-3' }) do |c|
= c.body do = c.body do
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- issue_link_start = link_start % { url: 'https://gitlab.com/gitlab-org/configure/general/-/issues/199' } - issue_link_start = link_start % { url: 'https://gitlab.com/gitlab-org/configure/general/-/issues/199' }

View File

@ -1,8 +1,8 @@
- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer') - link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
= render Pajamas::AlertComponent.new(title: s_('ClusterIntegration|Did you know?'), = render Pajamas::AlertComponent.new(title: s_('ClusterIntegration|Did you know?'),
alert_class: 'gcp-signup-offer', alert_options: { class: 'gcp-signup-offer',
alert_data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path }) do |c| data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path }}) do |c|
= c.body do = c.body do
= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link } = s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
= c.actions do = c.actions do

View File

@ -3,7 +3,7 @@
.sub-section .sub-section
%h4= s_('GroupSettings|Export group') %h4= s_('GroupSettings|Export group')
%p= _('Export this group with all related data.') %p= _('Export this group with all related data.')
= render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_class: 'gl-mb-4') do |c| = render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_options: { class: 'gl-mb-4' }) do |c|
= c.body do = c.body do
- docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') } - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') }
- docs_link_end = '</a>'.html_safe - docs_link_end = '</a>'.html_safe
@ -12,7 +12,7 @@
- export_information = _('After the export is complete, download the data file from a notification email or from this page. You can then import the data file from the %{strong_text_start}Create new group%{strong_text_end} page of another GitLab instance.') % { strong_text_start: '<strong>'.html_safe, strong_text_end: '</strong>'.html_safe} - export_information = _('After the export is complete, download the data file from a notification email or from this page. You can then import the data file from the %{strong_text_start}Create new group%{strong_text_end} page of another GitLab instance.') % { strong_text_start: '<strong>'.html_safe, strong_text_end: '</strong>'.html_safe}
= export_information.html_safe = export_information.html_safe
= link_to _('Learn more.'), help_page_path('user/group/settings/import_export.md'), target: '_blank', rel: 'noopener noreferrer' = link_to _('Learn more.'), help_page_path('user/group/settings/import_export.md'), target: '_blank', rel: 'noopener noreferrer'
= render Pajamas::AlertComponent.new(dismissible: false, alert_class: 'gl-mb-5') do |c| = render Pajamas::AlertComponent.new(dismissible: false, alert_options: { class: 'gl-mb-5' }) do |c|
= c.body do = c.body do
%p.gl-mb-0 %p.gl-mb-0
%p= _('The following items will be exported:') %p= _('The following items will be exported:')

View File

@ -1,7 +1,7 @@
- remove_form_id = local_assigns.fetch(:remove_form_id, nil) - remove_form_id = local_assigns.fetch(:remove_form_id, nil)
- if group.paid? - if group.paid?
= render Pajamas::AlertComponent.new(dismissible: false, alert_class: 'gl-mb-5', alert_data: { testid: 'group-has-linked-subscription-alert' }) do |c| = render Pajamas::AlertComponent.new(dismissible: false, alert_options: { class: 'gl-mb-5', data: { testid: 'group-has-linked-subscription-alert' }}) do |c|
= c.body do = c.body do
= html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe } = html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }

View File

@ -13,7 +13,7 @@
%li= s_('GroupSettings|You will need to update your local repositories to point to the new location.') %li= s_('GroupSettings|You will need to update your local repositories to point to the new location.')
%li= s_("GroupSettings|If the parent group's visibility is lower than the group's current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.") %li= s_("GroupSettings|If the parent group's visibility is lower than the group's current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.")
- if group.paid? - if group.paid?
= render Pajamas::AlertComponent.new(dismissible: false, alert_class: 'gl-mb-5') do |c| = render Pajamas::AlertComponent.new(dismissible: false, alert_options: { class: 'gl-mb-5' }) do |c|
= c.body do = c.body do
= html_escape(_("This group can't be transferred because it is linked to a subscription. To transfer this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe } = html_escape(_("This group can't be transferred because it is linked to a subscription. To transfer this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
.js-transfer-group-form{ data: initial_data } .js-transfer-group-form{ data: initial_data }

View File

@ -1,7 +1,7 @@
- if @errors.present? - if @errors.present?
= render Pajamas::AlertComponent.new(variant: :danger, = render Pajamas::AlertComponent.new(variant: :danger,
dismissible: false, dismissible: false,
alert_class: 'gl-mb-5') do |c| alert_options: { class: 'gl-mb-5' }) do |c|
= c.body do = c.body do
- @errors.each do |error| - @errors.each do |error|
= error = error

View File

@ -2,10 +2,10 @@
= render Pajamas::AlertComponent.new(title: _('Anyone can register for an account.'), = render Pajamas::AlertComponent.new(title: _('Anyone can register for an account.'),
variant: :warning, variant: :warning,
alert_class: 'js-registration-enabled-callout', alert_options: { class: 'js-registration-enabled-callout',
alert_data: { feature_id: Users::CalloutsHelper::REGISTRATION_ENABLED_CALLOUT, data: { feature_id: Users::CalloutsHelper::REGISTRATION_ENABLED_CALLOUT,
dismiss_endpoint: callouts_path }, dismiss_endpoint: callouts_path }},
close_button_data: { testid: 'close-registration-enabled-callout' }) do |c| close_button_options: { data: { testid: 'close-registration-enabled-callout' }}) do |c|
= c.body do = c.body do
= _('Only allow anyone to register for accounts on GitLab instances that you intend to be used by anyone. Allowing anyone to register makes GitLab instances more vulnerable.') = _('Only allow anyone to register for accounts on GitLab instances that you intend to be used by anyone. Allowing anyone to register makes GitLab instances more vulnerable.')
= c.actions do = c.actions do

View File

@ -4,11 +4,11 @@
- return unless banner_info.present? - return unless banner_info.present?
= render Pajamas::AlertComponent.new(variant: :warning, = render Pajamas::AlertComponent.new(variant: :warning,
alert_class: 'js-storage-enforcement-banner', alert_options: { class: 'js-storage-enforcement-banner',
alert_data: { feature_id: banner_info[:callouts_feature_name], data: { feature_id: banner_info[:callouts_feature_name],
dismiss_endpoint: banner_info[:callouts_path], dismiss_endpoint: banner_info[:callouts_path],
group_id: namespace.id, group_id: namespace.id,
defer_links: "true" }) do |c| defer_links: "true" }}) do |c|
= c.body do = c.body do
= banner_info[:text] = banner_info[:text]
= banner_info[:learn_more_link] = banner_info[:learn_more_link]

View File

@ -2,15 +2,15 @@
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- if current_user.ldap_user? - if current_user.ldap_user?
= render Pajamas::AlertComponent.new(alert_class: 'gl-my-5', = render Pajamas::AlertComponent.new(alert_options: { class: 'gl-my-5' },
dismissible: false) do |c| dismissible: false) do |c|
= c.body do = c.body do
= s_('Profiles|Some options are unavailable for LDAP accounts') = s_('Profiles|Some options are unavailable for LDAP accounts')
- if params[:two_factor_auth_enabled_successfully] - if params[:two_factor_auth_enabled_successfully]
= render Pajamas::AlertComponent.new(variant: :success, = render Pajamas::AlertComponent.new(variant: :success,
alert_class: 'gl-my-5', alert_options: { class: 'gl-my-5' },
close_button_class: 'js-close-2fa-enabled-success-alert') do |c| close_button_options: { class: 'js-close-2fa-enabled-success-alert' }) do |c|
= c.body do = c.body do
= html_escape(_('You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can %{anchorOpen}use that key to generate additional recovery codes%{anchorClose}.')) % { anchorOpen: '<a href="%{href}">'.html_safe % { href: help_page_path('user/profile/account/two_factor_authentication', anchor: 'generate-new-recovery-codes-using-ssh') }, anchorClose: '</a>'.html_safe } = html_escape(_('You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can %{anchorOpen}use that key to generate additional recovery codes%{anchorClose}.')) % { anchorOpen: '<a href="%{href}">'.html_safe % { href: help_page_path('user/profile/account/two_factor_authentication', anchor: 'generate-new-recovery-codes-using-ssh') }, anchorClose: '</a>'.html_safe }

View File

@ -3,7 +3,7 @@
= render Pajamas::AlertComponent.new(variant: :warning, = render Pajamas::AlertComponent.new(variant: :warning,
dismissible: false, dismissible: false,
alert_class: 'project-deletion-failed-message') do |c| alert_options: { class: 'project-deletion-failed-message' }) do |c|
= c.body do = c.body do
This project was scheduled for deletion, but failed with the following message: This project was scheduled for deletion, but failed with the following message:
= project.delete_error = project.delete_error

View File

@ -1,8 +1,8 @@
- event = last_push_event - event = last_push_event
- if event && show_last_push_widget?(event) - if event && show_last_push_widget?(event)
= render Pajamas::AlertComponent.new(variant: :success, = render Pajamas::AlertComponent.new(variant: :success,
alert_class: 'gl-mt-3', alert_options: { class: 'gl-mt-3' },
close_button_class: 'js-close-banner') do |c| close_button_options: { class: 'js-close-banner' }) do |c|
= c.body do = c.body do
%span= s_("LastPushEvent|You pushed to") %span= s_("LastPushEvent|You pushed to")
%strong.gl-display-inline-flex.gl-max-w-50p{ data: { toggle: 'tooltip' }, title: event.ref_name } %strong.gl-display-inline-flex.gl-max-w-50p{ data: { toggle: 'tooltip' }, title: event.ref_name }

View File

@ -37,7 +37,7 @@
- link_start_group_path = '<a href="%{path}">' % { path: new_group_path } - link_start_group_path = '<a href="%{path}">' % { path: new_group_path }
- project_tip = s_('ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}') % { link_start: link_start_group_path, link_end: '</a>' } - project_tip = s_('ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}') % { link_start: link_start_group_path, link_end: '</a>' }
= project_tip.html_safe = project_tip.html_safe
= render Pajamas::AlertComponent.new(alert_class: "gl-mb-4 gl-display-none js-user-readme-repo", = render Pajamas::AlertComponent.new(alert_options: { class: "gl-mb-4 gl-display-none js-user-readme-repo" },
dismissible: false, dismissible: false,
variant: :success) do |c| variant: :success) do |c|
= c.body do = c.body do

View File

@ -16,7 +16,7 @@
%br %br
= render Pajamas::AlertComponent.new(variant: :danger, = render Pajamas::AlertComponent.new(variant: :danger,
alert_class: 'dropzone-alerts gl-alert gl-alert-danger gl-mb-5 data gl-display-none', alert_options: { class: 'dropzone-alerts gl-alert gl-alert-danger gl-mb-5 data gl-display-none' },
dismissible: false) dismissible: false)
= render 'shared/new_commit_form', placeholder: placeholder, ref: local_assigns[:ref] = render 'shared/new_commit_form', placeholder: placeholder, ref: local_assigns[:ref]

View File

@ -4,7 +4,7 @@
- webpack_preload_asset_tag('monaco') - webpack_preload_asset_tag('monaco')
- if @conflict - if @conflict
= render Pajamas::AlertComponent.new(alert_class: 'gl-mb-5 gl-mt-5', = render Pajamas::AlertComponent.new(alert_options: { class: 'gl-mb-5 gl-mt-5' },
variant: :danger, variant: :danger,
dismissible: false) do |c| dismissible: false) do |c|
- blob_url = project_blob_path(@project, @id) - blob_url = project_blob_path(@project, @id)

View File

@ -2,7 +2,7 @@
- if @forked_project && !@forked_project.saved? - if @forked_project && !@forked_project.saved?
= render Pajamas::AlertComponent.new(title: _('Fork Error!'), = render Pajamas::AlertComponent.new(title: _('Fork Error!'),
variant: :danger, variant: :danger,
alert_class: 'gl-mt-5', alert_options: { class: 'gl-mt-5' },
dismissible: false) do |c| dismissible: false) do |c|
= c.body do = c.body do
%p %p

View File

@ -3,6 +3,6 @@
- service_desk_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: service_desk_link_url } - service_desk_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: service_desk_link_url }
= render Pajamas::AlertComponent.new(variant: :warning, = render Pajamas::AlertComponent.new(variant: :warning,
alert_class: 'hide js-alert-moved-from-service-desk-warning gl-mt-5') do |c| alert_options: { class: 'hide js-alert-moved-from-service-desk-warning gl-mt-5' }) do |c|
= c.body do = c.body do
= s_('This project does not have %{service_desk_link_start}Service Desk%{service_desk_link_end} enabled, so the user who created the issue will no longer receive email notifications about new activity.').html_safe % { service_desk_link_start: service_desk_link_start, service_desk_link_end: '</a>'.html_safe } = s_('This project does not have %{service_desk_link_start}Service Desk%{service_desk_link_end} enabled, so the user who created the issue will no longer receive email notifications about new activity.').html_safe % { service_desk_link_start: service_desk_link_start, service_desk_link_end: '</a>'.html_safe }

View File

@ -7,7 +7,7 @@
= cache(cache_key, expires_in: 1.day) do = cache(cache_key, expires_in: 1.day) do
- if @merge_request.closed_or_merged_without_fork? - if @merge_request.closed_or_merged_without_fork?
= render Pajamas::AlertComponent.new(alert_class: 'gl-mb-5', = render Pajamas::AlertComponent.new(alert_options: { class: 'gl-mb-5' },
variant: :danger, variant: :danger,
dismissible: false) do |c| dismissible: false) do |c|
= c.body do = c.body do

View File

@ -13,8 +13,8 @@
- if can?(current_user, :read_issue, @project) && @milestone.total_issues_count == 0 - if can?(current_user, :read_issue, @project) && @milestone.total_issues_count == 0
= render Pajamas::AlertComponent.new(dismissible: false, = render Pajamas::AlertComponent.new(dismissible: false,
alert_data: { testid: 'no-issues-alert' }, alert_options: { class: 'gl-mt-3 gl-mb-5',
alert_class: 'gl-mt-3 gl-mb-5') do |c| data: { testid: 'no-issues-alert' }}) do |c|
= c.body do = c.body do
= _('Assign some issues to this milestone.') = _('Assign some issues to this milestone.')
- else - else

View File

@ -2,7 +2,7 @@
- default_ref = params[:ref] || @project.default_branch - default_ref = params[:ref] || @project.default_branch
- if @error - if @error
= render Pajamas::AlertComponent.new(variant: :danger, dismissible: true, close_button_class: 'gl-alert-dismiss') do |c| = render Pajamas::AlertComponent.new(variant: :danger, dismissible: true, close_button_options: { class: 'gl-alert-dismiss' }) do |c|
= c.body do = c.body do
= @error = @error

View File

@ -2,7 +2,7 @@
= render Pajamas::AlertComponent.new(title: _('Repository usage recalculation started'), = render Pajamas::AlertComponent.new(title: _('Repository usage recalculation started'),
variant: :info, variant: :info,
alert_class: 'js-recalculation-started-alert gl-mt-4 gl-mb-5 gl-display-none') do |c| alert_options: { class: 'js-recalculation-started-alert gl-mt-4 gl-mb-5 gl-display-none' }) do |c|
= c.body do = c.body do
= _('To view usage, refresh this page in a few minutes.') = _('To view usage, refresh this page in a few minutes.')

View File

@ -1,7 +1,7 @@
- if show_auto_devops_implicitly_enabled_banner?(project, current_user) - if show_auto_devops_implicitly_enabled_banner?(project, current_user)
= render Pajamas::AlertComponent.new(alert_class: 'qa-auto-devops-banner auto-devops-implicitly-enabled-banner', = render Pajamas::AlertComponent.new(alert_options: { class: 'qa-auto-devops-banner auto-devops-implicitly-enabled-banner' },
close_button_class: 'hide-auto-devops-implicitly-enabled-banner', close_button_options: { class: 'hide-auto-devops-implicitly-enabled-banner',
close_button_data: { project_id: project.id }) do |c| data: { project_id: project.id }}) do |c|
= c.body do = c.body do
= s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found.") = s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found.")
- unless Gitlab.config.registry.enabled - unless Gitlab.config.registry.enabled

View File

@ -22,9 +22,9 @@
= f.text_field :import_url, value: import_url.sanitized_url, = f.text_field :import_url, value: import_url.sanitized_url,
autocomplete: 'off', class: 'form-control gl-form-input', placeholder: 'https://gitlab.company.com/group/project.git', required: true autocomplete: 'off', class: 'form-control gl-form-input', placeholder: 'https://gitlab.company.com/group/project.git', required: true
= render Pajamas::AlertComponent.new(variant: :danger, = render Pajamas::AlertComponent.new(variant: :danger,
alert_class: 'gl-mt-3 js-import-url-error hide', alert_options: { class: 'gl-mt-3 js-import-url-error hide' },
dismissible: false, dismissible: false,
close_button_class: 'js-close-2fa-enabled-success-alert') do |c| close_button_options: { class: 'js-close-2fa-enabled-success-alert' }) do |c|
= c.body do = c.body do
= s_('Import|There is not a valid Git repository at this URL. If your HTTP repository is not publicly accessible, verify your credentials.') = s_('Import|There is not a valid Git repository at this URL. If your HTTP repository is not publicly accessible, verify your credentials.')
= render_if_exists 'shared/ee/import_form', f: f, ci_cd_only: ci_cd_only = render_if_exists 'shared/ee/import_form', f: f, ci_cd_only: ci_cd_only

View File

@ -1,7 +1,7 @@
- if show_no_password_message? - if show_no_password_message?
= render Pajamas::AlertComponent.new(variant: :warning, = render Pajamas::AlertComponent.new(variant: :warning,
alert_class: 'js-no-password-message', alert_options: { class: 'js-no-password-message' },
close_button_class: 'js-hide-no-password-message') do |c| close_button_options: { class: 'js-hide-no-password-message' }) do |c|
= c.body do = c.body do
= no_password_message = no_password_message
= c.actions do = c.actions do

View File

@ -1,7 +1,7 @@
- if show_no_ssh_key_message? - if show_no_ssh_key_message?
= render Pajamas::AlertComponent.new(variant: :warning, = render Pajamas::AlertComponent.new(variant: :warning,
alert_class: 'js-no-ssh-message', alert_options: { class: 'js-no-ssh-message' },
close_button_class: 'js-hide-no-ssh-message') do |c| close_button_options: { class: 'js-hide-no-ssh-message'}) do |c|
= c.body do = c.body do
= s_("MissingSSHKeyWarningLink|You can't push or pull repositories using SSH until you add an SSH key to your profile.") = s_("MissingSSHKeyWarningLink|You can't push or pull repositories using SSH until you add an SSH key to your profile.")
= c.actions do = c.actions do

View File

@ -1,7 +1,7 @@
- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? && current_user.projects_limit > 0 - if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? && current_user.projects_limit > 0
= render Pajamas::AlertComponent.new(variant: :warning, = render Pajamas::AlertComponent.new(variant: :warning,
dismissible: false, dismissible: false,
alert_class: 'project-limit-message') do |c| alert_options: { class: 'project-limit-message' }) do |c|
= c.body do = c.body do
= _("You won't be able to create new projects because you have reached your project limit.") = _("You won't be able to create new projects because you have reached your project limit.")
= c.actions do = c.actions do

View File

@ -1,5 +1,5 @@
- if session[:ask_for_usage_stats_consent] - if session[:ask_for_usage_stats_consent]
= render Pajamas::AlertComponent.new(alert_class: 'service-ping-consent-message') do |c| = render Pajamas::AlertComponent.new(alert_options: { class: 'service-ping-consent-message' }) do |c|
= c.body do = c.body do
- docs_link = link_to _('collect usage information'), help_page_path('user/admin_area/settings/usage_statistics.md'), class: 'gl-link' - docs_link = link_to _('collect usage information'), help_page_path('user/admin_area/settings/usage_statistics.md'), class: 'gl-link'
- settings_link = link_to _('your settings'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'gl-link' - settings_link = link_to _('your settings'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'gl-link'

View File

@ -1,9 +1,9 @@
= render Pajamas::AlertComponent.new(variant: :warning, = render Pajamas::AlertComponent.new(variant: :warning,
alert_class: 'js-recovery-settings-callout gl-mt-5', alert_options: { class: 'js-recovery-settings-callout gl-mt-5',
alert_data: { feature_id: Users::CalloutsHelper::TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK, data: { feature_id: Users::CalloutsHelper::TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK,
dismiss_endpoint: callouts_path, dismiss_endpoint: callouts_path,
defer_links: 'true' }, defer_links: 'true' }},
close_button_data: { testid: 'close-account-recovery-regular-check-callout' }) do |c| close_button_options: { data: { testid: 'close-account-recovery-regular-check-callout' }}) do |c|
= c.body do = c.body do
= s_('Profiles|Ensure you have two-factor authentication recovery codes stored in a safe place.') = s_('Profiles|Ensure you have two-factor authentication recovery codes stored in a safe place.')
= link_to _('Learn more.'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'recovery-codes'), target: '_blank', rel: 'noopener noreferrer' = link_to _('Learn more.'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'recovery-codes'), target: '_blank', rel: 'noopener noreferrer'

View File

@ -1,4 +1,4 @@
= render Pajamas::AlertComponent.new(alert_class: 'gl-my-5', = render Pajamas::AlertComponent.new(alert_options: { class: 'gl-my-5' },
variant: :danger, variant: :danger,
dismissible: false, dismissible: false,
title: reason) do |c| title: reason) do |c|

View File

@ -8,7 +8,7 @@
- if @conflict - if @conflict
= render Pajamas::AlertComponent.new(variant: :danger, = render Pajamas::AlertComponent.new(variant: :danger,
dismissible: false, dismissible: false,
alert_class: 'gl-mb-5') do |c| alert_options: { class: 'gl-mb-5' }) do |c|
= c.body do = c.body do
Someone edited the #{issuable.class.model_name.human.downcase} the same time you did. Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
Please check out Please check out

View File

@ -6,7 +6,7 @@
.dropdown-page-two.dropdown-new-label .dropdown-page-two.dropdown-new-label
= dropdown_title(create_label_title(subject), options: { back: true, close: show_close }) = dropdown_title(create_label_title(subject), options: { back: true, close: show_close })
= dropdown_content do = dropdown_content do
= render Pajamas::AlertComponent.new(variant: :danger, alert_class: 'js-label-error gl-mb-3', dismissible: false) = render Pajamas::AlertComponent.new(variant: :danger, alert_options: { class: 'js-label-error gl-mb-3' }, dismissible: false)
%input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') } %input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') }
.suggest-colors.suggest-colors-dropdown .suggest-colors.suggest-colors-dropdown
= render_suggested_colors = render_suggested_colors

View File

@ -37,7 +37,7 @@
data: { placeholder: _('Select branch'), endpoint: refs_project_path(@project, sort: 'updated_desc', find: 'branches') }}) data: { placeholder: _('Select branch'), endpoint: refs_project_path(@project, sort: 'updated_desc', find: 'branches') }})
- if source_level < target_level - if source_level < target_level
= render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_class: 'gl-mb-4') do |c| = render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_options: { class: 'gl-mb-4' }) do |c|
= c.body do = c.body do
= visibilityMismatchString = visibilityMismatchString
%br %br

View File

@ -2,7 +2,7 @@
- if milestone.complete? && milestone.active? - if milestone.complete? && milestone.active?
= render Pajamas::AlertComponent.new(variant: :success, = render Pajamas::AlertComponent.new(variant: :success,
alert_data: { testid: 'all-issues-closed-alert' }, alert_options: { data: { testid: 'all-issues-closed-alert' }},
dismissible: false) do |c| dismissible: false) do |c|
= c.body do = c.body do
= yield = yield

View File

@ -4,4 +4,4 @@
- deletion_date = inactive_project_deletion_date(@project) - deletion_date = inactive_project_deletion_date(@project)
- title = _('Due to inactivity, this project is scheduled to be deleted on %{deletion_date}. %{link_start}Why is this scheduled?%{link_end}').html_safe % { deletion_date: deletion_date, link_start: link_start, link_end: link_end } - title = _('Due to inactivity, this project is scheduled to be deleted on %{deletion_date}. %{link_start}Why is this scheduled?%{link_end}').html_safe % { deletion_date: deletion_date, link_start: link_start, link_end: link_end }
= render Pajamas::AlertComponent.new(title: title, variant: :warning, alert_class: 'gl-pb-3', dismissible: false) = render Pajamas::AlertComponent.new(title: title, variant: :warning, alert_options: { class: 'gl-pb-3' }, dismissible: false)

View File

@ -1,14 +1,14 @@
- alert_class = 'gl-mb-5' - alert_options = { class: 'gl-mb-5' }
- if runner.group_type? - if runner.group_type?
= render Pajamas::AlertComponent.new(alert_class: alert_class, = render Pajamas::AlertComponent.new(alert_options: alert_options,
title: s_('Runners|This runner is available to all projects and subgroups in a group.'), title: s_('Runners|This runner is available to all projects and subgroups in a group.'),
dismissible: false) do |c| dismissible: false) do |c|
= c.body do = c.body do
= s_('Runners|Use Group runners when you want all projects in a group to have access to a set of runners.') = s_('Runners|Use Group runners when you want all projects in a group to have access to a set of runners.')
= link_to _('Learn more.'), help_page_path('ci/runners/runners_scope', anchor: 'group-runners'), target: '_blank', rel: 'noopener noreferrer' = link_to _('Learn more.'), help_page_path('ci/runners/runners_scope', anchor: 'group-runners'), target: '_blank', rel: 'noopener noreferrer'
- else - else
= render Pajamas::AlertComponent.new(alert_class: alert_class, = render Pajamas::AlertComponent.new(alert_options: alert_options,
title: s_('Runners|This runner is associated with specific projects.'), title: s_('Runners|This runner is associated with specific projects.'),
dismissible: false) do |c| dismissible: false) do |c|
= c.body do = c.body do

View File

@ -14,6 +14,7 @@ first: '\b([A-Z]{3,5})\b'
second: '(?:\b[A-Z][a-z]+ )+\(([A-Z]{3,5})\)' second: '(?:\b[A-Z][a-z]+ )+\(([A-Z]{3,5})\)'
# ... with the exception of these: # ... with the exception of these:
exceptions: exceptions:
- ACL
- AJAX - AJAX
- ANSI - ANSI
- APAC - APAC

View File

@ -18,8 +18,8 @@ If you have any doubts about the consistency of the data on this site, we recomm
## Configure the former **primary** site to be a **secondary** site ## Configure the former **primary** site to be a **secondary** site
Since the former **primary** site will be out of sync with the current **primary** site, the first step is to bring the former **primary** site up to date. Note, deletion of data stored on disk like Since the former **primary** site is out of sync with the current **primary** site, the first step is to bring the former **primary** site up to date. Note, deletion of data stored on disk like
repositories and uploads will not be replayed when bringing the former **primary** site back repositories and uploads is not replayed when bringing the former **primary** site back
into sync, which may result in increased disk usage. into sync, which may result in increased disk usage.
Alternatively, you can [set up a new **secondary** GitLab instance](../setup/index.md) to avoid this. Alternatively, you can [set up a new **secondary** GitLab instance](../setup/index.md) to avoid this.

View File

@ -143,7 +143,7 @@ If the **primary** site uses custom or self-signed TLS certificates to secure in
### Ensure Geo replication is up-to-date ### Ensure Geo replication is up-to-date
The maintenance window won't end until Geo replication and verification is The maintenance window does not end until Geo replication and verification is
completely finished. To keep the window as short as possible, you should completely finished. To keep the window as short as possible, you should
ensure these processes are close to 100% as possible during active use. ensure these processes are close to 100% as possible during active use.
@ -201,7 +201,7 @@ be disabled on the **primary** site:
## Finish replicating and verifying all data ## Finish replicating and verifying all data
NOTE: NOTE:
GitLab 13.9 through GitLab 14.3 are affected by a bug in which the Geo secondary site statuses will appear to stop updating and become unhealthy. For more information, see [Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode](../replication/troubleshooting.md#geo-admin-area-shows-unhealthy-after-enabling-maintenance-mode). GitLab 13.9 through GitLab 14.3 are affected by a bug in which the Geo secondary site statuses appears to stop updating and become unhealthy. For more information, see [Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode](../replication/troubleshooting.md#geo-admin-area-shows-unhealthy-after-enabling-maintenance-mode).
1. If you are manually replicating any data not managed by Geo, trigger the 1. If you are manually replicating any data not managed by Geo, trigger the
final replication process now. final replication process now.

View File

@ -32,10 +32,10 @@ To ensure that problems with pipelines (for example, syncs failing too many time
the number of concurrent syncs falls below `repos_max_capacity` and there are no new projects waiting to be synced. the number of concurrent syncs falls below `repos_max_capacity` and there are no new projects waiting to be synced.
Geo also has a checksum feature which runs a SHA256 sum across all the Git references to the SHA values. Geo also has a checksum feature which runs a SHA256 sum across all the Git references to the SHA values.
If the refs don't match between the **primary** site and the **secondary** site, then the **secondary** site will mark that project as dirty and try to resync it. If the refs don't match between the **primary** site and the **secondary** site, then the **secondary** site marks that project as dirty and try to resync it.
So even if we have an outdated tracking database, the validation should activate and find discrepancies in the repository state and resync. So even if we have an outdated tracking database, the validation should activate and find discrepancies in the repository state and resync.
## Can I use Geo in a disaster recovery situation? ## Can you use Geo in a disaster recovery situation?
Yes, but there are limitations to what we replicate (see Yes, but there are limitations to what we replicate (see
[What data is replicated to a **secondary** site?](#what-data-is-replicated-to-a-secondary-site)). [What data is replicated to a **secondary** site?](#what-data-is-replicated-to-a-secondary-site)).
@ -46,7 +46,7 @@ Read the documentation for [Disaster Recovery](../disaster_recovery/index.md).
We currently replicate project repositories, LFS objects, generated We currently replicate project repositories, LFS objects, generated
attachments and avatars, and the whole database. This means user accounts, attachments and avatars, and the whole database. This means user accounts,
issues, merge requests, groups, project data, and so on, will be available for issues, merge requests, groups, project data, and so on, are available for
query. query.
For more details, see the [supported Geo data types](datatypes.md). For more details, see the [supported Geo data types](datatypes.md).
@ -69,6 +69,6 @@ That's totally fine. We use HTTP(s) to fetch repository changes from the **prima
Yes. See [Docker Registry for a **secondary** site](docker_registry.md). Yes. See [Docker Registry for a **secondary** site](docker_registry.md).
## Can I login to a secondary site? ## Can you login to a secondary site?
Yes, but secondary sites receive all authentication data (like user accounts and logins) from the primary instance. This means you are re-directed to the primary for authentication and then routed back. Yes, but secondary sites receive all authentication data (like user accounts and logins) from the primary instance. This means you are re-directed to the primary for authentication and then routed back.

View File

@ -44,7 +44,7 @@ Once GitLab has been uninstalled from each node on the **secondary** site, the r
``` ```
NOTE: NOTE:
Using `gitlab-rails dbconsole` will not work, because managing replication slots requires superuser permissions. Using `gitlab-rails dbconsole` does not work, because managing replication slots requires superuser permissions.
1. Find the name of the relevant replication slot. This is the slot that is specified with `--slot-name` when running the replicate command: `gitlab-ctl replicate-geo-database`. 1. Find the name of the relevant replication slot. This is the slot that is specified with `--slot-name` when running the replicate command: `gitlab-ctl replicate-geo-database`.

View File

@ -398,7 +398,7 @@ where some queries never complete due to being canceled on every replication.
These long-running queries are These long-running queries are
[planned to be removed in the future](https://gitlab.com/gitlab-org/gitlab/-/issues/34269), [planned to be removed in the future](https://gitlab.com/gitlab-org/gitlab/-/issues/34269),
but as a workaround, we recommend enabling but as a workaround, we recommend enabling
[hot_standby_feedback](https://www.postgresql.org/docs/10/hot-standby.html#HOT-STANDBY-CONFLICT). [`hot_standby_feedback`](https://www.postgresql.org/docs/10/hot-standby.html#HOT-STANDBY-CONFLICT).
This increases the likelihood of bloat on the **primary** node as it prevents This increases the likelihood of bloat on the **primary** node as it prevents
`VACUUM` from removing recently-dead rows. However, it has been used `VACUUM` from removing recently-dead rows. However, it has been used
successfully in production on GitLab.com. successfully in production on GitLab.com.
@ -767,7 +767,7 @@ The appropriate action sometimes depends on the cause. For example, you can remo
In some cases, a file may be determined to be of low value, and so it may be worth deleting the record. In some cases, a file may be determined to be of low value, and so it may be worth deleting the record.
Geo itself is an excellent mitigation for files missing on the primary. If a file disappears on the primary but it was already synced to the secondary, you can grab the secondary's file. In cases like this, the `File is not checksummable` error message will not occur on Geo secondary sites, and only the primary will log this error message. Geo itself is an excellent mitigation for files missing on the primary. If a file disappears on the primary but it was already synced to the secondary, you can grab the secondary's file. In cases like this, the `File is not checksummable` error message does not occur on Geo secondary sites, and only the primary logs this error message.
This problem is more likely to show up in Geo secondary sites which were set up long after the original GitLab site. In this case, Geo is only surfacing an existing problem. This problem is more likely to show up in Geo secondary sites which were set up long after the original GitLab site. In this case, Geo is only surfacing an existing problem.
@ -1104,9 +1104,9 @@ If using a load balancer, ensure that the load balancer's URL is set as the `ext
### Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode ### Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode
In GitLab 13.9 through GitLab 14.3, when [GitLab Maintenance Mode](../../maintenance_mode/index.md) is enabled, the status of Geo secondary sites will stop getting updated. After 10 minutes, the status changes to `Unhealthy`. In GitLab 13.9 through GitLab 14.3, when [GitLab Maintenance Mode](../../maintenance_mode/index.md) is enabled, the status of Geo secondary sites stops getting updated. After 10 minutes, the status changes to `Unhealthy`.
Geo secondary sites will continue to replicate and verify data, and the secondary sites should still be usable. You can use the [Sync status Rake task](#sync-status-rake-task) to determine the actual status of a secondary site during Maintenance Mode. Geo secondary sites continue to replicate and verify data, and the secondary sites should still be usable. You can use the [Sync status Rake task](#sync-status-rake-task) to determine the actual status of a secondary site during Maintenance Mode.
This bug was [fixed in GitLab 14.4](https://gitlab.com/gitlab-org/gitlab/-/issues/292983). This bug was [fixed in GitLab 14.4](https://gitlab.com/gitlab-org/gitlab/-/issues/292983).

View File

@ -25,7 +25,7 @@ On the **primary** site:
- Container repositories synchronization concurrency limit - Container repositories synchronization concurrency limit
- Verification concurrency limit - Verification concurrency limit
Increasing the concurrency values will increase the number of jobs that are scheduled. Increasing the concurrency values increases the number of jobs that are scheduled.
However, this may not lead to more downloads in parallel unless the number of However, this may not lead to more downloads in parallel unless the number of
available Sidekiq threads is also increased. For example, if repository synchronization available Sidekiq threads is also increased. For example, if repository synchronization
concurrency is increased from 25 to 50, you may also want to increase the number concurrency is increased from 25 to 50, you may also want to increase the number

View File

@ -22,7 +22,7 @@ Upgrading Geo sites involves performing:
NOTE: NOTE:
These general upgrade steps are not intended for multi-site deployments, These general upgrade steps are not intended for multi-site deployments,
and will cause downtime. If you want to avoid downtime, consider using and cause downtime. If you want to avoid downtime, consider using
[zero downtime upgrades](../../../update/zero_downtime.md#multi-node--ha-deployment-with-geo). [zero downtime upgrades](../../../update/zero_downtime.md#multi-node--ha-deployment-with-geo).
To upgrade the Geo sites when a new GitLab version is released, upgrade **primary** To upgrade the Geo sites when a new GitLab version is released, upgrade **primary**

View File

@ -11,9 +11,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
After you set up the [database replication and configure the Geo nodes](../index.md#setup-instructions), use your closest GitLab site as you would do with the primary one. After you set up the [database replication and configure the Geo nodes](../index.md#setup-instructions), use your closest GitLab site as you would do with the primary one.
You can push directly to a **secondary** site (for both HTTP, SSH including You can push directly to a **secondary** site (for both HTTP, SSH including
Git LFS), and the request will be proxied to the primary site instead. Git LFS), and the request is proxied to the primary site instead.
Example of the output you will see when pushing to a **secondary** site: Example of the output you see when pushing to a **secondary** site:
```shell ```shell
$ git push $ git push
@ -31,7 +31,7 @@ If you're using HTTPS instead of [SSH](../../../user/ssh.md) to push to the seco
you can't store credentials in the URL like `user:password@URL`. Instead, you can use a you can't store credentials in the URL like `user:password@URL`. Instead, you can use a
[`.netrc` file](https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) [`.netrc` file](https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html)
for Unix-like operating systems or `_netrc` for Windows. In that case, the credentials for Unix-like operating systems or `_netrc` for Windows. In that case, the credentials
will be stored as a plain text. If you're looking for a more secure way to store credentials, are stored as a plain text. If you're looking for a more secure way to store credentials,
you can use [Git Credential Storage](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). you can use [Git Credential Storage](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
## Fetch Go modules from Geo secondary sites ## Fetch Go modules from Geo secondary sites

View File

@ -51,7 +51,7 @@ developed and tested. We aim to be compatible with most external
gitlab-ctl set-geo-primary-node gitlab-ctl set-geo-primary-node
``` ```
This command will use your defined `external_url` in `/etc/gitlab/gitlab.rb`. This command uses your defined `external_url` in `/etc/gitlab/gitlab.rb`.
### Configure the external database to be replicated ### Configure the external database to be replicated
@ -64,7 +64,7 @@ To set up an external database, you can either:
Given you have a primary node set up on AWS EC2 that uses RDS. Given you have a primary node set up on AWS EC2 that uses RDS.
You can now just create a read-only replica in a different region and the You can now just create a read-only replica in a different region and the
replication process will be managed by AWS. Make sure you've set Network ACL, Subnet, and replication process is managed by AWS. Make sure you've set Network ACL (Access Control List), Subnet, and
Security Group according to your needs, so the secondary application node can access the database. Security Group according to your needs, so the secondary application node can access the database.
The following instructions detail how to create a read-only replica for common The following instructions detail how to create a read-only replica for common

View File

@ -19,7 +19,7 @@ The steps below should be followed in the order they appear. **Make sure the Git
If you installed GitLab using the Omnibus packages (highly recommended): If you installed GitLab using the Omnibus packages (highly recommended):
1. [Install GitLab Enterprise Edition](https://about.gitlab.com/install/) on the nodes that will serve as the **secondary** site. Do not create an account or log in to the new **secondary** site. The **GitLab version must match** across primary and secondary sites. 1. [Install GitLab Enterprise Edition](https://about.gitlab.com/install/) on the nodes that serve as the **secondary** site. Do not create an account or log in to the new **secondary** site. The **GitLab version must match** across primary and secondary sites.
1. [Add the GitLab License](../../../user/admin_area/license.md) on the **primary** site to unlock Geo. The license must be for [GitLab Premium](https://about.gitlab.com/pricing/) or higher. 1. [Add the GitLab License](../../../user/admin_area/license.md) on the **primary** site to unlock Geo. The license must be for [GitLab Premium](https://about.gitlab.com/pricing/) or higher.
1. [Set up the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology). 1. [Set up the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology).
1. [Configure fast lookup of authorized SSH keys in the database](../../operations/fast_ssh_key_lookup.md). This step is required and needs to be done on **both** the **primary** and **secondary** sites. 1. [Configure fast lookup of authorized SSH keys in the database](../../operations/fast_ssh_key_lookup.md). This step is required and needs to be done on **both** the **primary** and **secondary** sites.

View File

@ -33,7 +33,7 @@ sudo -u git -H bundle exec rake geo:git:housekeeping:incremental_repack RAILS_EN
### Full Repack ### Full Repack
This is equivalent of running `git repack -d -A --pack-kept-objects` on a This is equivalent of running `git repack -d -A --pack-kept-objects` on a
_bare_ repository which will optionally, write a reachability bitmap index _bare_ repository which optionally, writes a reachability bitmap index
when this is enabled in GitLab. when this is enabled in GitLab.
**Omnibus Installation** **Omnibus Installation**

View File

@ -6,21 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Release links API **(FREE)** # Release links API **(FREE)**
> Support for [GitLab CI/CD job token](../../ci/jobs/ci_job_token.md) authentication [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250819) in GitLab 15.1.
Use this API to manipulate GitLab [Release](../../user/project/releases/index.md) Use this API to manipulate GitLab [Release](../../user/project/releases/index.md)
links. For manipulating other Release assets, see [Release API](index.md). links. For manipulating other Release assets, see [Release API](index.md).
GitLab supports links to `http`, `https`, and `ftp` assets. GitLab supports links to `http`, `https`, and `ftp` assets.
## Authentication
For authentication, the Release Links API accepts:
- A [Personal Access Token](../../user/profile/personal_access_tokens.md) using the
`PRIVATE-TOKEN` header.
- A [Project Access Token](../../user/project/settings/project_access_tokens.md) using the `PRIVATE-TOKEN` header.
The [GitLab CI/CD job token](../../ci/jobs/ci_job_token.md) `$CI_JOB_TOKEN` is not supported. See [GitLab issue #50819](https://gitlab.com/gitlab-org/gitlab/-/issues/250819) for more details.
## Get links ## Get links
Get assets as links from a Release. Get assets as links from a Release.

View File

@ -1,7 +1,8 @@
--- ---
stage: Data Stores
group: Database
comments: false comments: false
description: 'Database Scalability / Limit table sizes' description: 'Database Scalability / Limit table sizes'
group: database
--- ---
# Database Scalability: Limit on-disk table size to < 100 GB for GitLab.com # Database Scalability: Limit on-disk table size to < 100 GB for GitLab.com

View File

@ -20,7 +20,7 @@ You can use a GitLab CI/CD job token to authenticate with specific API endpoints
- [Get job artifacts](../../api/job_artifacts.md#get-job-artifacts). - [Get job artifacts](../../api/job_artifacts.md#get-job-artifacts).
- [Get job token's job](../../api/jobs.md#get-job-tokens-job). - [Get job token's job](../../api/jobs.md#get-job-tokens-job).
- [Pipeline triggers](../../api/pipeline_triggers.md), using the `token=` parameter. - [Pipeline triggers](../../api/pipeline_triggers.md), using the `token=` parameter.
- [Releases](../../api/releases/index.md). - [Releases](../../api/releases/index.md) and [Release links](../../api/releases/links.md).
- [Terraform plan](../../user/infrastructure/index.md). - [Terraform plan](../../user/infrastructure/index.md).
NOTE: NOTE:

View File

@ -33,15 +33,15 @@ for new events and creates background jobs for each specific event type.
For example when a repository is updated, the Geo **primary** site creates For example when a repository is updated, the Geo **primary** site creates
a Geo event with an associated repository updated event. The Geo Log Cursor daemon a Geo event with an associated repository updated event. The Geo Log Cursor daemon
picks the event up and schedules a `Geo::ProjectSyncWorker` job which will picks the event up and schedules a `Geo::ProjectSyncWorker` job which
use the `Geo::RepositorySyncService` and `Geo::WikiSyncService` classes uses the `Geo::RepositorySyncService` and `Geo::WikiSyncService` classes
to update the repository and the wiki respectively. to update the repository and the wiki respectively.
The Geo Log Cursor daemon can operate in High Availability mode automatically. The Geo Log Cursor daemon can operate in High Availability mode automatically.
The daemon will try to acquire a lock from time to time and once acquired, it The daemon tries to acquire a lock from time to time and once acquired, it
will behave as the *active* daemon. behaves as the *active* daemon.
Any additional running daemons on the same site, will be in standby Any additional running daemons on the same site, is in standby
mode, ready to resume work if the *active* daemon releases its lock. mode, ready to resume work if the *active* daemon releases its lock.
We use the [`ExclusiveLease`](https://www.rubydoc.info/github/gitlabhq/gitlabhq/Gitlab/ExclusiveLease) lock type with a small TTL, that is renewed at every We use the [`ExclusiveLease`](https://www.rubydoc.info/github/gitlabhq/gitlabhq/Gitlab/ExclusiveLease) lock type with a small TTL, that is renewed at every

View File

@ -59,7 +59,7 @@ naming conventions:
consume) events. It takes care of the communication between the consume) events. It takes care of the communication between the
primary site (where events are produced) and the secondary site primary site (where events are produced) and the secondary site
(where events are consumed). The engineer who wants to incorporate (where events are consumed). The engineer who wants to incorporate
Geo in their feature will use the API of replicators to make this Geo in their feature uses the API of replicators to make this
happen. happen.
- **Geo Domain-Specific Language**: - **Geo Domain-Specific Language**:
@ -99,7 +99,7 @@ end
The class name should be unique. It also is tightly coupled to the The class name should be unique. It also is tightly coupled to the
table name for the registry, so for this example the registry table table name for the registry, so for this example the registry table
will be `package_file_registry`. is `package_file_registry`.
For the different data types Geo supports there are different For the different data types Geo supports there are different
strategies to include. Pick one that fits your needs. strategies to include. Pick one that fits your needs.

View File

@ -1,7 +1,7 @@
--- ---
type: reference, dev type: reference, dev
stage: create stage: Create
group: code_review group: Code Review
info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines" info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines"
--- ---

View File

@ -1015,9 +1015,9 @@ more of the following options:
- `BACKUP=timestamp_of_backup`: Required if more than one backup exists. - `BACKUP=timestamp_of_backup`: Required if more than one backup exists.
Read what the [backup timestamp is about](#backup-timestamp). Read what the [backup timestamp is about](#backup-timestamp).
- `force=yes`: Doesn't ask if the authorized_keys file should get regenerated, - `force=yes`: Doesn't ask if the `authorized_keys` file should get regenerated,
and assumes 'yes' for warning about database tables being removed, and assumes 'yes' for warning about database tables being removed,
enabling the "Write to authorized_keys file" setting, and updating LDAP enabling the `Write to authorized_keys file` setting, and updating LDAP
providers. providers.
If you're restoring into directories that are mount points, you must ensure these directories are If you're restoring into directories that are mount points, you must ensure these directories are
@ -1407,7 +1407,7 @@ There is an **experimental** script that attempts to automate this process in
## Back up and restore for installations using PgBouncer ## Back up and restore for installations using PgBouncer
Do NOT back up or restore GitLab through a PgBouncer connection. These Do not back up or restore GitLab through a PgBouncer connection. These
tasks must [bypass PgBouncer and connect directly to the PostgreSQL primary database node](#bypassing-pgbouncer), tasks must [bypass PgBouncer and connect directly to the PostgreSQL primary database node](#bypassing-pgbouncer),
or they cause a GitLab outage. or they cause a GitLab outage.
@ -1418,7 +1418,7 @@ following error message is shown:
ActiveRecord::StatementInvalid: PG::UndefinedTable ActiveRecord::StatementInvalid: PG::UndefinedTable
``` ```
Each time the GitLab backup runs, GitLab will start generating 500 errors and errors about missing Each time the GitLab backup runs, GitLab starts generating 500 errors and errors about missing
tables will [be logged by PostgreSQL](../administration/logs.md#postgresql-logs): tables will [be logged by PostgreSQL](../administration/logs.md#postgresql-logs):
```plaintext ```plaintext
@ -1480,7 +1480,7 @@ WARNING:
Avoid uncoordinated data processing by both the new and old servers, where multiple Avoid uncoordinated data processing by both the new and old servers, where multiple
servers could connect concurrently and process the same data. For example, when using servers could connect concurrently and process the same data. For example, when using
[incoming email](../administration/incoming_email.md), if both GitLab instances are [incoming email](../administration/incoming_email.md), if both GitLab instances are
processing email at the same time, then both instances will end up missing some data. processing email at the same time, then both instances miss some data.
This type of problem can occur with other services as well, such as a This type of problem can occur with other services as well, such as a
[non-packaged database](https://docs.gitlab.com/omnibus/settings/database.html#using-a-non-packaged-postgresql-database-management-server), [non-packaged database](https://docs.gitlab.com/omnibus/settings/database.html#using-a-non-packaged-postgresql-database-management-server),
a non-packaged Redis instance, or non-packaged Sidekiq. a non-packaged Redis instance, or non-packaged Sidekiq.

View File

@ -1,7 +1,7 @@
--- ---
type: reference type: reference
stage: Manage stage: Manage
group: Authentication & Authorization group: Authentication and Authorization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
--- ---

View File

@ -87,6 +87,26 @@ To edit an epic's start date, due date, or labels:
1. Next to each section in the right sidebar, select **Edit**. 1. Next to each section in the right sidebar, select **Edit**.
1. Select the dates or labels for your epic. 1. Select the dates or labels for your epic.
### Reorder list items in the epic description
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15260) in GitLab 15.1.
When you view an epic that has a list in the description, you can also reorder the list items.
Prerequisites:
- You must have at least the Reporter role for the project, be the author of the epic, or be
assigned to the epic.
- The epic's description must have an [ordered, unordered](../../markdown.md#lists), or
[task](../../markdown.md#task-lists) list.
To reorder list items, when viewing an epic:
1. Hover over the list item row to make the drag icon (**{drag-vertical}**) visible.
1. Select and hold the drag icon.
1. Drag the row to the new position in the list.
1. Release the drag icon.
## Bulk edit epics ## Bulk edit epics
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7250) in GitLab 12.2. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7250) in GitLab 12.2.

View File

@ -29,6 +29,7 @@ module API
params do params do
use :pagination use :pagination
end end
route_setting :authentication, job_token_allowed: true
get 'links' do get 'links' do
authorize! :read_release, release authorize! :read_release, release
@ -45,6 +46,7 @@ module API
optional :filepath, type: String, desc: 'The filepath of the link' optional :filepath, type: String, desc: 'The filepath of the link'
optional :link_type, type: String, desc: 'The link type, one of: "runbook", "image", "package" or "other"' optional :link_type, type: String, desc: 'The link type, one of: "runbook", "image", "package" or "other"'
end end
route_setting :authentication, job_token_allowed: true
post 'links' do post 'links' do
authorize! :create_release, release authorize! :create_release, release
@ -65,6 +67,7 @@ module API
detail 'This feature was introduced in GitLab 11.7.' detail 'This feature was introduced in GitLab 11.7.'
success Entities::Releases::Link success Entities::Releases::Link
end end
route_setting :authentication, job_token_allowed: true
get do get do
authorize! :read_release, release authorize! :read_release, release
@ -82,6 +85,7 @@ module API
optional :link_type, type: String, desc: 'The link type' optional :link_type, type: String, desc: 'The link type'
at_least_one_of :name, :url at_least_one_of :name, :url
end end
route_setting :authentication, job_token_allowed: true
put do put do
authorize! :update_release, release authorize! :update_release, release
@ -96,6 +100,7 @@ module API
detail 'This feature was introduced in GitLab 11.7.' detail 'This feature was introduced in GitLab 11.7.'
success Entities::Releases::Link success Entities::Releases::Link
end end
route_setting :authentication, job_token_allowed: true
delete do delete do
authorize! :destroy_release, release authorize! :destroy_release, release

View File

@ -12,6 +12,10 @@ module Gitlab
def name def name
@name || _('An unauthenticated user') @name || _('An unauthenticated user')
end end
def impersonated?
false
end
end end
end end
end end

View File

@ -3,9 +3,10 @@ module Gitlab
module Diff module Diff
module Rendered module Rendered
module Notebook module Notebook
include Gitlab::Utils::StrongMemoize
class DiffFile < Gitlab::Diff::File class DiffFile < Gitlab::Diff::File
include Gitlab::Diff::Rendered::Notebook::DiffFileHelper
include Gitlab::Utils::StrongMemoize
RENDERED_TIMEOUT_BACKGROUND = 10.seconds RENDERED_TIMEOUT_BACKGROUND = 10.seconds
RENDERED_TIMEOUT_FOREGROUND = 1.5.seconds RENDERED_TIMEOUT_FOREGROUND = 1.5.seconds
BACKGROUND_EXECUTION = 'background' BACKGROUND_EXECUTION = 'background'
@ -14,7 +15,6 @@ module Gitlab
LOG_IPYNBDIFF_TIMEOUT = 'IPYNB_DIFF_TIMEOUT' LOG_IPYNBDIFF_TIMEOUT = 'IPYNB_DIFF_TIMEOUT'
LOG_IPYNBDIFF_INVALID = 'IPYNB_DIFF_INVALID' LOG_IPYNBDIFF_INVALID = 'IPYNB_DIFF_INVALID'
LOG_IPYNBDIFF_TRUNCATED = 'IPYNB_DIFF_TRUNCATED' LOG_IPYNBDIFF_TRUNCATED = 'IPYNB_DIFF_TRUNCATED'
EMBEDDED_IMAGE_PATTERN = ' ![](data:image'
attr_reader :source_diff attr_reader :source_diff
@ -51,7 +51,8 @@ module Gitlab
def highlighted_diff_lines def highlighted_diff_lines
@highlighted_diff_lines ||= begin @highlighted_diff_lines ||= begin
removal_line_maps, addition_line_maps = compute_end_start_map removal_line_maps, addition_line_maps = map_diff_block_to_source_line(
source_diff.highlighted_diff_lines, source_diff.new_file?, source_diff.deleted_file?)
Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight.map do |line| Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight.map do |line|
mutate_line(line, addition_line_maps, removal_line_maps) mutate_line(line, addition_line_maps, removal_line_maps)
end end
@ -90,55 +91,6 @@ module Gitlab
diff diff
end end
def strip_diff_frontmatter(diff_content)
diff_content.scan(/.*\n/)[2..]&.join('') if diff_content.present?
end
def transformed_line_to_source(transformed_line, transformed_blocks)
transformed_blocks.empty? ? 0 : ( transformed_blocks[transformed_line - 1][:source_line] || -1 ) + 1
end
def mutate_line(line, addition_line_maps, removal_line_maps)
line.new_pos = transformed_line_to_source(line.new_pos, notebook_diff.to.blocks)
line.old_pos = transformed_line_to_source(line.old_pos, notebook_diff.from.blocks)
line.old_pos = addition_line_maps[line.new_pos] if line.old_pos == 0 && line.new_pos != 0
line.new_pos = removal_line_maps[line.old_pos] if line.new_pos == 0 && line.old_pos != 0
# Lines that do not appear on the original diff should not be commentable
line.type = "#{line.type || 'unchanged'}-nomappinginraw" unless addition_line_maps[line.new_pos] || removal_line_maps[line.old_pos]
line.line_code = line_code(line)
line.rich_text = image_as_rich_text(line)
line
end
def compute_end_start_map
# line_codes are used for assigning notes to diffs, and these depend on the line on the new version and the
# line that would have been that one in the previous version. However, since we do a transformation on the
# file, that map gets lost. To overcome this, we look at the original source lines and build two maps:
# - For additions, we look at the latest line change for that line and pick the old line for that id
# - For removals, we look at the first line in the old version, and pick the first line on the new version
#
#
# The caveat here is that we can't have notes on lines that are not a translation of a line in the source
# diff
#
# (gitlab/diff/file.rb:75)
removals = {}
additions = {}
source_diff.highlighted_diff_lines.each do |line|
removals[line.old_pos] = line.new_pos unless source_diff.new_file?
additions[line.new_pos] = line.old_pos unless source_diff.deleted_file?
end
[removals, additions]
end
def rendered_timeout def rendered_timeout
@rendered_timeout ||= Gitlab::Metrics.counter( @rendered_timeout ||= Gitlab::Metrics.counter(
:ipynb_semantic_diff_timeouts_total, :ipynb_semantic_diff_timeouts_total,
@ -156,16 +108,27 @@ module Gitlab
nil nil
end end
def image_as_rich_text(line) def compute_line_numbers(transformed_old_pos, transformed_new_pos, addition_line_maps, removal_line_maps)
# Strip the initial +, -, or space for the diff context new_pos = map_transformed_line_to_source(transformed_new_pos, notebook_diff.to.blocks)
line_text = line.text[1..] old_pos = map_transformed_line_to_source(transformed_old_pos, notebook_diff.from.blocks)
if line_text.starts_with?(EMBEDDED_IMAGE_PATTERN) old_pos = addition_line_maps[new_pos] if old_pos == 0 && new_pos != 0
image_body = line_text.delete_prefix(EMBEDDED_IMAGE_PATTERN).delete_suffix(')') new_pos = removal_line_maps[old_pos] if new_pos == 0 && old_pos != 0
"<img src=\"data:image#{CGI.escapeHTML(image_body)}\">".html_safe
else [old_pos, new_pos]
line.rich_text end
end
def mutate_line(line, addition_line_maps, removal_line_maps)
line.old_pos, line.new_pos = compute_line_numbers(line.old_pos, line.new_pos, addition_line_maps, removal_line_maps)
# Lines that do not appear on the original diff should not be commentable
line.type = "#{line.type || 'unchanged'}-nomappinginraw" unless addition_line_maps[line.new_pos] || removal_line_maps[line.old_pos]
line.line_code = line_code(line)
line.rich_text = image_as_rich_text(line.text) || line.rich_text
line
end end
end end
end end

View File

@ -0,0 +1,89 @@
# frozen_string_literal: true
module Gitlab
module Diff
module Rendered
module Notebook
module DiffFileHelper
EMBEDDED_IMAGE_PATTERN = ' ![](data:image'
def strip_diff_frontmatter(diff_content)
diff_content.scan(/.*\n/)[2..]&.join('') if diff_content.present?
end
def map_transformed_line_to_source(transformed_line, transformed_blocks)
transformed_blocks.empty? ? 0 : ( transformed_blocks[transformed_line - 1][:source_line] || -1 ) + 1
end
# line_codes are used for assigning notes to diffs, and these depend on the line on the new version and the
# line that would have been that one in the previous version. However, since we do a transformation on the
# file, that mapping gets lost. To overcome this, we look at the original source lines and build two maps:
# - For additions, we look at the latest line change for that line and pick the old line for that id
# - For removals, we look at the first line in the old version, and pick the first line on the new version
#
# Note: ipynb files never change the first or last line (open and closure of the
# json object), unless the file is removed or deleted
#
# Example: Additions and removals
# Old: New:
# A A
# B D
# C E
# F F
#
# Diff:
# 1 A A 1 | line code: 1_1
# 2 -B | line code: 2_2 -> new line is what it is after been without the removal, 2
# 3 -C | line code: 3_2
# + D 2 | line code: 4_2 -> old line is what would have been before the addition, 4
# + E 3 | line code: 4_3
# 4 F F 4 | line code: 4_4
#
# Example: only additions
# Old: New:
# A A
# F B
# C
# F
#
# Diff:
# A A | line code: 1_1
# + B | line code: 2_2 -> old line is the next after the additions, 2
# + C | line code: 2_3
# F F | line code: 2_4
#
# Example: only removals
# Old: New:
# A A
# B F
# C
# F
#
# Diff:
# A A | line code: 1_1
# -B | line code: 2_2 -> new line is what it is after been without the removal, 2
# -C | line code: 3_2
# F F | line code: 4_2
def map_diff_block_to_source_line(lines, file_added, file_deleted)
removals = {}
additions = {}
lines.each do |line|
removals[line.old_pos] = line.new_pos unless file_added
additions[line.new_pos] = line.old_pos unless file_deleted
end
[removals, additions]
end
def image_as_rich_text(line_text)
return unless line_text[1..].starts_with?(EMBEDDED_IMAGE_PATTERN)
image_body = line_text[1..].delete_prefix(EMBEDDED_IMAGE_PATTERN).delete_suffix(')')
"<img src=\"data:image#{CGI.escapeHTML(image_body)}\">".html_safe
end
end
end
end
end
end

View File

@ -34527,9 +34527,6 @@ msgstr ""
msgid "SecurityReports|Unable to add %{invalidProjectsMessage}: %{errorMessage}" msgid "SecurityReports|Unable to add %{invalidProjectsMessage}: %{errorMessage}"
msgstr "" msgstr ""
msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
msgid "SecurityReports|Undo dismiss" msgid "SecurityReports|Undo dismiss"
msgstr "" msgstr ""
@ -35923,9 +35920,6 @@ msgstr ""
msgid "Something went wrong, unable to add %{project} to dashboard" msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr "" msgstr ""
msgid "Something went wrong, unable to add projects to dashboard"
msgstr ""
msgid "Something went wrong, unable to delete project" msgid "Something went wrong, unable to delete project"
msgstr "" msgstr ""

View File

@ -50,10 +50,12 @@ RSpec.describe Pajamas::AlertComponent, :aggregate_failures, type: :component do
before do before do
render_inline described_class.new( render_inline described_class.new(
title: '_title_', title: '_title_',
alert_class: '_alert_class_', alert_options: {
alert_data: { class: '_alert_class_',
feature_id: '_feature_id_', data: {
dismiss_endpoint: '_dismiss_endpoint_' feature_id: '_feature_id_',
dismiss_endpoint: '_dismiss_endpoint_'
}
} }
) )
end end
@ -106,9 +108,11 @@ RSpec.describe Pajamas::AlertComponent, :aggregate_failures, type: :component do
context 'with dismissible content' do context 'with dismissible content' do
before do before do
render_inline described_class.new( render_inline described_class.new(
close_button_class: '_close_button_class_', close_button_options: {
close_button_data: { class: '_close_button_class_',
testid: '_close_button_testid_' data: {
testid: '_close_button_testid_'
}
} }
) )
end end
@ -138,35 +142,5 @@ RSpec.describe Pajamas::AlertComponent, :aggregate_failures, type: :component do
end end
end end
end end
context 'with alert_options' do
let(:options) { { alert_options: { id: 'test_id', class: 'baz', data: { foo: 'bar' } } } }
before do
render_inline described_class.new(**options)
end
it 'renders the extra options' do
expect(rendered_component).to have_css "#test_id.gl-alert.baz[data-foo='bar']"
end
context 'with custom classes or data' do
let(:options) do
{
variant: :danger,
alert_class: 'custom',
alert_data: { foo: 'bar' },
alert_options: {
class: 'extra special',
data: { foo: 'conflict' }
}
}
end
it 'doesn\'t conflict with internal alert_class or alert_data' do
expect(rendered_component).to have_css ".extra.special.custom.gl-alert.gl-alert-danger[data-foo='bar']"
end
end
end
end end
end end

View File

@ -0,0 +1,7 @@
---
extends:
- 'plugin:@gitlab/jest'
settings:
import/core-modules:
- '@pact-foundation/pact'
- jest-pact

View File

@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}

View File

@ -1,42 +1,34 @@
'use strict'; import { request } from 'axios';
const axios = require('axios'); export function getMetadata(endpoint) {
const { url } = endpoint;
exports.getMetadata = (endpoint) => { return request({
const url = endpoint.url; method: 'GET',
baseURL: url,
url: '/gitlab-org/gitlab-qa/-/merge_requests/1/diffs_metadata.json',
headers: { Accept: '*/*' },
}).then((response) => response.data);
}
return axios export function getDiscussions(endpoint) {
.request({ const { url } = endpoint;
method: 'GET',
baseURL: url,
url: '/gitlab-org/gitlab-qa/-/merge_requests/1/diffs_metadata.json',
headers: { Accept: '*/*' },
})
.then((response) => response.data);
};
exports.getDiscussions = (endpoint) => { return request({
const url = endpoint.url; method: 'GET',
baseURL: url,
url: '/gitlab-org/gitlab-qa/-/merge_requests/1/discussions.json',
headers: { Accept: '*/*' },
}).then((response) => response.data);
}
return axios export function getDiffs(endpoint) {
.request({ const { url } = endpoint;
method: 'GET',
baseURL: url,
url: '/gitlab-org/gitlab-qa/-/merge_requests/1/discussions.json',
headers: { Accept: '*/*' },
})
.then((response) => response.data);
};
exports.getDiffs = (endpoint) => { return request({
const url = endpoint.url; method: 'GET',
baseURL: url,
return axios url: '/gitlab-org/gitlab-qa/-/merge_requests/1/diffs_batch.json?page=0',
.request({ headers: { Accept: '*/*' },
method: 'GET', }).then((response) => response.data);
baseURL: url, }
url: '/gitlab-org/gitlab-qa/-/merge_requests/1/diffs_batch.json?page=0',
headers: { Accept: '*/*' },
})
.then((response) => response.data);
};

View File

@ -1,6 +1,6 @@
'use strict'; /* eslint-disable @gitlab/require-i18n-strings */
const { Matchers } = require('@pact-foundation/pact'); import { Matchers } from '@pact-foundation/pact';
const body = { const body = {
diff_files: Matchers.eachLike({ diff_files: Matchers.eachLike({
@ -48,9 +48,9 @@ const body = {
context_lines_path: Matchers.string('/gitlab-qa-bot/...'), context_lines_path: Matchers.string('/gitlab-qa-bot/...'),
highlighted_diff_lines: Matchers.eachLike({ highlighted_diff_lines: Matchers.eachLike({
// The following values can also be null which is not supported // The following values can also be null which is not supported
//line_code: Matchers.string('de3150c01c3a946a6168173c4116741379fe3579_1_1'), // line_code: Matchers.string('de3150c01c3a946a6168173c4116741379fe3579_1_1'),
//old_line: Matchers.integer(1), // old_line: Matchers.integer(1),
//new_line: Matchers.integer(1), // new_line: Matchers.integer(1),
text: Matchers.string('source'), text: Matchers.string('source'),
rich_text: Matchers.string('<span></span>'), rich_text: Matchers.string('<span></span>'),
can_receive_suggestion: Matchers.boolean(true), can_receive_suggestion: Matchers.boolean(true),
@ -70,7 +70,7 @@ const Diffs = {
headers: { headers: {
'Content-Type': 'application/json; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8',
}, },
body: body, body,
}, },
request: { request: {
@ -86,4 +86,5 @@ const Diffs = {
}, },
}; };
exports.Diffs = Diffs; export { Diffs };
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -1,6 +1,6 @@
'use strict'; /* eslint-disable @gitlab/require-i18n-strings */
const { Matchers } = require('@pact-foundation/pact'); import { Matchers } from '@pact-foundation/pact';
const body = Matchers.eachLike({ const body = Matchers.eachLike({
id: Matchers.string('fd73763cbcbf7b29eb8765d969a38f7d735e222a'), id: Matchers.string('fd73763cbcbf7b29eb8765d969a38f7d735e222a'),
@ -67,7 +67,7 @@ const Discussions = {
headers: { headers: {
'Content-Type': 'application/json; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8',
}, },
body: body, body,
}, },
request: { request: {
@ -82,4 +82,5 @@ const Discussions = {
}, },
}; };
exports.Discussions = Discussions; export { Discussions };
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -1,6 +1,6 @@
'use strict'; /* eslint-disable @gitlab/require-i18n-strings */
const { Matchers } = require('@pact-foundation/pact'); import { Matchers } from '@pact-foundation/pact';
const body = { const body = {
real_size: Matchers.string('1'), real_size: Matchers.string('1'),
@ -78,7 +78,7 @@ const Metadata = {
headers: { headers: {
'Content-Type': 'application/json; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8',
}, },
body: body, body,
}, },
request: { request: {
@ -93,4 +93,5 @@ const Metadata = {
}, },
}; };
exports.Metadata = Metadata; export { Metadata };
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -13,5 +13,14 @@
}, },
"scripts": { "scripts": {
"test": "jest --runInBand" "test": "jest --runInBand"
},
"jest": {
"transform": {
"^.+\\.[t|j]sx?$": "babel-jest"
}
},
"devDependencies": {
"@babel/preset-env": "^7.18.2",
"babel-jest": "^28.1.1"
} }
} }

View File

@ -1,9 +1,9 @@
'use strict'; /* eslint-disable @gitlab/require-i18n-strings */
const { pactWith } = require('jest-pact'); import { pactWith } from 'jest-pact';
const { Diffs } = require('../fixtures/diffs.fixture'); import { Diffs } from '../fixtures/diffs.fixture';
const { getDiffs } = require('../endpoints/merge_requests'); import { getDiffs } from '../endpoints/merge_requests';
pactWith( pactWith(
{ {
@ -34,3 +34,4 @@ pactWith(
}); });
}, },
); );
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -1,9 +1,9 @@
'use strict'; /* eslint-disable @gitlab/require-i18n-strings */
const { pactWith } = require('jest-pact'); import { pactWith } from 'jest-pact';
const { Discussions } = require('../fixtures/discussions.fixture'); import { Discussions } from '../fixtures/discussions.fixture';
const { getDiscussions } = require('../endpoints/merge_requests'); import { getDiscussions } from '../endpoints/merge_requests';
pactWith( pactWith(
{ {
@ -34,3 +34,4 @@ pactWith(
}); });
}, },
); );
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -1,9 +1,9 @@
'use strict'; /* eslint-disable @gitlab/require-i18n-strings */
const { pactWith } = require('jest-pact'); import { pactWith } from 'jest-pact';
const { Metadata } = require('../fixtures/metadata.fixture'); import { Metadata } from '../fixtures/metadata.fixture';
const { getMetadata } = require('../endpoints/merge_requests'); import { getMetadata } from '../endpoints/merge_requests';
pactWith( pactWith(
{ {
@ -34,3 +34,4 @@ pactWith(
}); });
}, },
); );
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -68,7 +68,7 @@ describe('IDE commit editor header', () => {
it('calls discardFileChanges if dialog result is confirmed', () => { it('calls discardFileChanges if dialog result is confirmed', () => {
expect(store.dispatch).not.toHaveBeenCalled(); expect(store.dispatch).not.toHaveBeenCalled();
findDiscardModal().vm.$emit('ok'); findDiscardModal().vm.$emit('primary');
expect(store.dispatch).toHaveBeenCalledWith('discardFileChanges', TEST_FILE_PATH); expect(store.dispatch).toHaveBeenCalledWith('discardFileChanges', TEST_FILE_PATH);
}); });

View File

@ -2,7 +2,6 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { GlTab, GlTabs, GlLink } from '@gitlab/ui'; import { GlTab, GlTabs, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser'; import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import stubChildren from 'helpers/stub_children'; import stubChildren from 'helpers/stub_children';
@ -20,22 +19,14 @@ import {
LICENSE_COMPLIANCE_DESCRIPTION, LICENSE_COMPLIANCE_DESCRIPTION,
LICENSE_COMPLIANCE_HELP_PATH, LICENSE_COMPLIANCE_HELP_PATH,
AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY, AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
LICENSE_ULTIMATE,
LICENSE_PREMIUM,
LICENSE_FREE,
} from '~/security_configuration/components/constants'; } from '~/security_configuration/components/constants';
import FeatureCard from '~/security_configuration/components/feature_card.vue'; import FeatureCard from '~/security_configuration/components/feature_card.vue';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import currentLicenseQuery from '~/security_configuration/graphql/current_license.query.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue'; import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
import { import {
REPORT_TYPE_LICENSE_COMPLIANCE, REPORT_TYPE_LICENSE_COMPLIANCE,
REPORT_TYPE_SAST, REPORT_TYPE_SAST,
} from '~/vue_shared/security_reports/constants'; } from '~/vue_shared/security_reports/constants';
import { getCurrentLicensePlanResponse } from '../mock_data';
const upgradePath = '/upgrade'; const upgradePath = '/upgrade';
const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath'; const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
@ -50,31 +41,16 @@ Vue.use(VueApollo);
describe('App component', () => { describe('App component', () => {
let wrapper; let wrapper;
let userCalloutDismissSpy; let userCalloutDismissSpy;
let mockApollo;
const createComponent = ({ const createComponent = ({ shouldShowCallout = true, ...propsData }) => {
shouldShowCallout = true,
licenseQueryResponse = LICENSE_ULTIMATE,
...propsData
}) => {
userCalloutDismissSpy = jest.fn(); userCalloutDismissSpy = jest.fn();
mockApollo = createMockApollo([
[
currentLicenseQuery,
jest
.fn()
.mockResolvedValue(
licenseQueryResponse instanceof Error
? licenseQueryResponse
: getCurrentLicensePlanResponse(licenseQueryResponse),
),
],
]);
wrapper = extendedWrapper( wrapper = extendedWrapper(
mount(SecurityConfigurationApp, { mount(SecurityConfigurationApp, {
propsData, propsData: {
securityTrainingEnabled: true,
...propsData,
},
provide: { provide: {
upgradePath, upgradePath,
autoDevopsHelpPagePath, autoDevopsHelpPagePath,
@ -82,7 +58,6 @@ describe('App component', () => {
projectFullPath, projectFullPath,
vulnerabilityTrainingDocsPath, vulnerabilityTrainingDocsPath,
}, },
apolloProvider: mockApollo,
stubs: { stubs: {
...stubChildren(SecurityConfigurationApp), ...stubChildren(SecurityConfigurationApp),
GlLink: false, GlLink: false,
@ -157,7 +132,6 @@ describe('App component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
mockApollo = null;
}); });
describe('basic structure', () => { describe('basic structure', () => {
@ -166,7 +140,6 @@ describe('App component', () => {
augmentedSecurityFeatures: securityFeaturesMock, augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock, augmentedComplianceFeatures: complianceFeaturesMock,
}); });
await waitForPromises();
}); });
it('renders main-heading with correct text', () => { it('renders main-heading with correct text', () => {
@ -469,47 +442,42 @@ describe('App component', () => {
}); });
describe('Vulnerability management', () => { describe('Vulnerability management', () => {
beforeEach(async () => { it('does not show tab if security training is disabled', () => {
createComponent({ createComponent({
augmentedSecurityFeatures: securityFeaturesMock, augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock, augmentedComplianceFeatures: complianceFeaturesMock,
securityTrainingEnabled: false,
}); });
await waitForPromises();
expect(findVulnerabilityManagementTab().exists()).toBe(false);
}); });
it('renders TrainingProviderList component', () => { describe('security training enabled', () => {
expect(findTrainingProviderList().exists()).toBe(true); beforeEach(async () => {
});
it('renders security training description', () => {
expect(findVulnerabilityManagementTab().text()).toContain(i18n.securityTrainingDescription);
});
it('renders link to help docs', () => {
const trainingLink = findVulnerabilityManagementTab().findComponent(GlLink);
expect(trainingLink.text()).toBe('Learn more about vulnerability training');
expect(trainingLink.attributes('href')).toBe(vulnerabilityTrainingDocsPath);
});
it.each`
licenseQueryResponse | display
${LICENSE_ULTIMATE} | ${true}
${LICENSE_PREMIUM} | ${false}
${LICENSE_FREE} | ${false}
${null} | ${true}
${new Error()} | ${true}
`(
'displays $display for license $licenseQueryResponse',
async ({ licenseQueryResponse, display }) => {
createComponent({ createComponent({
licenseQueryResponse,
augmentedSecurityFeatures: securityFeaturesMock, augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock, augmentedComplianceFeatures: complianceFeaturesMock,
}); });
await waitForPromises(); });
expect(findVulnerabilityManagementTab().exists()).toBe(display);
}, it('shows the tab if security training is enabled', () => {
); expect(findVulnerabilityManagementTab().exists()).toBe(true);
});
it('renders TrainingProviderList component', () => {
expect(findTrainingProviderList().exists()).toBe(true);
});
it('renders security training description', () => {
expect(findVulnerabilityManagementTab().text()).toContain(i18n.securityTrainingDescription);
});
it('renders link to help docs', () => {
const trainingLink = findVulnerabilityManagementTab().findComponent(GlLink);
expect(trainingLink.text()).toBe('Learn more about vulnerability training');
expect(trainingLink.attributes('href')).toBe(vulnerabilityTrainingDocsPath);
});
});
}); });
}); });

View File

@ -111,12 +111,3 @@ export const tempProviderLogos = {
svg: `<svg>${[testProviderName[1]]}</svg>`, svg: `<svg>${[testProviderName[1]]}</svg>`,
}, },
}; };
export const getCurrentLicensePlanResponse = (plan) => ({
data: {
currentLicense: {
id: 'gid://gitlab/License/1',
plan,
},
},
});

View File

@ -41,18 +41,18 @@ describe('Attention require toggle', () => {
); );
it.each` it.each`
attentionRequested | variant attentionRequested | selected
${true} | ${'warning'} ${true} | ${true}
${false} | ${'default'} ${false} | ${false}
`( `(
'renders button with variant $variant when attention_requested is $attentionRequested', 'renders button with as selected when $selected when attention_requested is $attentionRequested',
({ attentionRequested, variant }) => { ({ attentionRequested, selected }) => {
factory({ factory({
type: 'reviewer', type: 'reviewer',
user: { attention_requested: attentionRequested, can_update_merge_request: true }, user: { attention_requested: attentionRequested, can_update_merge_request: true },
}); });
expect(findToggle().props('variant')).toBe(variant); expect(findToggle().props('selected')).toBe(selected);
}, },
); );

View File

@ -42,8 +42,8 @@ describe('Merge Request Collapsible Extension', () => {
expect(wrapper.find('[data-testid="collapsed-header"]').text()).toBe('hello there'); expect(wrapper.find('[data-testid="collapsed-header"]').text()).toBe('hello there');
}); });
it('renders angle-right icon', () => { it('renders chevron-lg-right icon', () => {
expect(findIcon().props('name')).toBe('angle-right'); expect(findIcon().props('name')).toBe('chevron-lg-right');
}); });
describe('onClick', () => { describe('onClick', () => {
@ -60,8 +60,8 @@ describe('Merge Request Collapsible Extension', () => {
expect(findTitle().text()).toBe('Collapse'); expect(findTitle().text()).toBe('Collapse');
}); });
it('renders angle-down icon', () => { it('renders chevron-lg-down icon', () => {
expect(findIcon().props('name')).toBe('angle-down'); expect(findIcon().props('name')).toBe('chevron-lg-down');
}); });
}); });
}); });

View File

@ -18,7 +18,7 @@ RSpec.describe FormHelper do
expect(helper.form_errors(model, pajamas_alert: true)) expect(helper.form_errors(model, pajamas_alert: true))
.to include( .to include(
'<div class="gl-alert gl-alert-danger gl-alert-not-dismissible gl-mb-5" id="error_explanation" role="alert">' '<div class="gl-alert gl-mb-5 gl-alert-danger gl-alert-not-dismissible" id="error_explanation" role="alert">'
) )
end end

View File

@ -13,5 +13,11 @@ RSpec.describe Gitlab::Audit::UnauthenticatedAuthor do
expect(described_class.new) expect(described_class.new)
.to have_attributes(id: -1, name: 'An unauthenticated user') .to have_attributes(id: -1, name: 'An unauthenticated user')
end end
describe '#impersonated?' do
it 'returns false' do
expect(described_class.new.impersonated?).to be(false)
end
end
end end
end end

View File

@ -0,0 +1,134 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rspec-parameterized'
RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFileHelper do
let(:dummy) { Class.new { include Gitlab::Diff::Rendered::Notebook::DiffFileHelper }.new }
describe '#strip_diff_frontmatter' do
using RSpec::Parameterized::TableSyntax
subject { dummy.strip_diff_frontmatter(diff) }
where(:diff, :result) do
"FileLine1\nFileLine2\n@@ -1,76 +1,74 @@\nhello\n" | "@@ -1,76 +1,74 @@\nhello\n"
"" | nil
nil | nil
end
with_them do
it { is_expected.to eq(result) }
end
end
describe '#map_transformed_line_to_source' do
using RSpec::Parameterized::TableSyntax
subject { dummy.map_transformed_line_to_source(1, transformed_blocks) }
where(:case, :transformed_blocks, :result) do
'if transformed diff is empty' | [] | 0
'if the transformed line does not map to any in the original file' | [{ source_line: nil }] | 0
'if the transformed line maps to a line in the source file' | [{ source_line: 2 }] | 3
end
with_them do
it { is_expected.to eq(result) }
end
end
describe '#map_diff_block_to_source_line' do
let(:file_added) { false }
let(:file_deleted) { false }
let(:old_positions) { [1] }
let(:new_positions) { [1] }
let(:lines) { old_positions.zip(new_positions).map { |old, new| Gitlab::Diff::Line.new("", "", 0, old, new) } }
subject { dummy.map_diff_block_to_source_line(lines, file_added, file_deleted)}
context 'only additions' do
let(:old_positions) { [1, 2, 2, 2] }
let(:new_positions) { [1, 2, 3, 4] }
it 'computes the removals correctly' do
expect(subject[0]).to eq({ 1 => 1, 2 => 4 })
end
it 'computes the additions correctly' do
expect(subject[1]).to eq({ 1 => 1, 2 => 2, 3 => 2, 4 => 2 })
end
end
context 'only additions' do
let(:old_positions) { [1, 2, 3, 4] }
let(:new_positions) { [1, 2, 2, 2] }
it 'computes the removals correctly' do
expect(subject[0]).to eq({ 1 => 1, 2 => 2, 3 => 2, 4 => 2 })
end
it 'computes the additions correctly' do
expect(subject[1]).to eq({ 1 => 1, 2 => 4 })
end
end
context 'with additions and removals' do
let(:old_positions) { [1, 2, 3, 4, 4, 4] }
let(:new_positions) { [1, 2, 2, 2, 3, 4] }
it 'computes the removals correctly' do
expect(subject[0]).to eq({ 1 => 1, 2 => 2, 3 => 2, 4 => 4 })
end
it 'computes the additions correctly' do
expect(subject[1]).to eq({ 1 => 1, 2 => 4, 3 => 4, 4 => 4 })
end
end
context 'is new file' do
let(:file_added) { true }
it 'removals is empty' do
expect(subject[0]).to be_empty
end
end
context 'is deleted file' do
let(:file_deleted) { true }
it 'additions is empty' do
expect(subject[1]).to be_empty
end
end
end
describe '#image_as_rich_text' do
let(:img) { 'data:image/png;base64,some_image_here' }
let(:line_text) { " ![](#{img})"}
subject { dummy.image_as_rich_text(line_text) }
context 'text does not contain image' do
let(:img) { "not an image" }
it { is_expected.to be_nil }
end
context 'text contains image' do
it { is_expected.to eq("<img src=\"#{img}\">") }
end
context 'text contains image that has malicious html' do
let(:img) { 'data:image/png;base64,some_image_here"<div>Hello</div>' }
it 'sanitizes the html' do
expect(subject).not_to include('<div>Hello')
end
it 'adds image to src' do
expect(subject).to end_with('/div&gt;">')
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More