Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-15 18:09:09 +00:00
parent e69e3f1eb6
commit da1962d9ac
73 changed files with 1477 additions and 421 deletions

View file

@ -8,6 +8,7 @@ exclude:
- 'spec/**/*' - 'spec/**/*'
require: require:
- './haml_lint/linter/no_plain_nodes.rb' - './haml_lint/linter/no_plain_nodes.rb'
- './haml_lint/linter/documentation_links.rb'
linters: linters:
AltText: AltText:
@ -26,6 +27,12 @@ linters:
enabled: false enabled: false
max_consecutive: 2 max_consecutive: 2
DocumentationLinks:
enabled: false
include:
- 'app/views/**/*.haml'
- 'ee/app/views/**/*.haml'
EmptyObjectReference: EmptyObjectReference:
enabled: true enabled: true

View file

@ -1,10 +1,14 @@
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlButton, GlTabs, GlTab, GlLink, GlBadge } from '@gitlab/ui';
import DocLine from './doc_line.vue'; import DocLine from './doc_line.vue';
export default { export default {
components: { components: {
GlButton, GlButton,
GlTabs,
GlTab,
GlLink,
GlBadge,
DocLine, DocLine,
}, },
props: { props: {
@ -54,6 +58,9 @@ export default {
isDefinitionCurrentBlob() { isDefinitionCurrentBlob() {
return this.data.definition_path.indexOf(this.blobPath) === 0; return this.data.definition_path.indexOf(this.blobPath) === 0;
}, },
references() {
return this.data.references || [];
},
}, },
watch: { watch: {
position: { position: {
@ -82,37 +89,61 @@ export default {
class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show" class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show"
> >
<div :style="{ left: `${offsetLeft}px` }" class="arrow"></div> <div :style="{ left: `${offsetLeft}px` }" class="arrow"></div>
<div class="overflow-auto code-navigation-popover-container"> <gl-tabs nav-class="gl-hidden" content-class="gl-py-0">
<div <gl-tab :title="__('Definition')">
v-for="(hover, index) in data.hover" <div class="overflow-auto code-navigation-popover-container">
:key="index" <div
:class="{ 'border-bottom': index !== data.hover.length - 1 }" v-for="(hover, index) in data.hover"
> :key="index"
<pre :class="{ 'border-bottom': index !== data.hover.length - 1 }"
v-if="hover.language" >
ref="code-output" <pre
:class="$options.colorScheme" v-if="hover.language"
class="border-0 bg-transparent m-0 code highlight text-wrap" ref="code-output"
><doc-line v-for="(tokens, tokenIndex) in hover.tokens" :key="tokenIndex" :language="hover.language" :tokens="tokens"/></pre> :class="$options.colorScheme"
<p v-else ref="doc-output" class="p-3 m-0 gl-font-base"> class="border-0 bg-transparent m-0 code highlight text-wrap"
{{ hover.value }} ><doc-line v-for="(tokens, tokenIndex) in hover.tokens" :key="tokenIndex" :language="hover.language" :tokens="tokens"/></pre>
<p v-else ref="doc-output" class="p-3 m-0">
{{ hover.value }}
</p>
</div>
</div>
<div v-if="definitionPath || isCurrentDefinition" class="popover-body border-top">
<span v-if="isCurrentDefinition" class="gl-font-weight-bold gl-font-base">
{{ s__('CodeIntelligence|This is the definition') }}
</span>
<gl-button
v-else
:href="definitionPath"
:target="isDefinitionCurrentBlob ? null : '_blank'"
class="w-100"
variant="default"
data-testid="go-to-definition-btn"
>
{{ __('Go to definition') }}
</gl-button>
</div>
</gl-tab>
<gl-tab data-testid="references-tab" class="py-2">
<template #title>
{{ __('References') }}
<gl-badge size="sm" class="gl-tab-counter-badge">{{ references.length }}</gl-badge>
</template>
<template v-if="references.length">
<div v-for="(reference, index) in references" :key="index" class="gl-dropdown-item">
<gl-link
:href="`${definitionPathPrefix}/${reference.path}`"
class="dropdown-item"
data-testid="reference-link"
>
{{ reference.path }}
</gl-link>
</div>
</template>
<p v-else class="gl-my-4 gl-px-4">
{{ s__('CodeNavigation|No references found') }}
</p> </p>
</div> </gl-tab>
</div> </gl-tabs>
<div v-if="definitionPath || isCurrentDefinition" class="popover-body border-top">
<span v-if="isCurrentDefinition" class="gl-font-weight-bold gl-font-base">
{{ s__('CodeIntelligence|This is the definition') }}
</span>
<gl-button
v-else
:href="definitionPath"
:target="isDefinitionCurrentBlob ? null : '_blank'"
class="w-100"
variant="default"
data-testid="go-to-definition-btn"
>
{{ __('Go to definition') }}
</gl-button>
</div>
</div> </div>
</template> </template>

View file

@ -53,7 +53,7 @@ export default {
return this.type === 'jira'; return this.type === 'jira';
}, },
showJiraIssuesFields() { showJiraIssuesFields() {
return this.isJira && this.glFeatures.jiraIntegration; return this.isJira && this.glFeatures.jiraIssuesIntegration;
}, },
}, },
}; };

View file

@ -293,10 +293,10 @@ export default {
this.filters = filters; this.filters = filters;
}, },
refetchIssuables() { refetchIssuables() {
const ignored = ['utf8', 'state']; const ignored = ['utf8'];
const params = omit(this.filters, ignored); const params = omit(this.filters, ignored);
historyPushState(setUrlParams(params, window.location.href, true)); historyPushState(setUrlParams(params, window.location.href, true, true));
this.fetchIssuables(); this.fetchIssuables();
}, },
handleFilter(filters) { handleFilter(filters) {

View file

@ -36,7 +36,7 @@ function mountIssuableListRootApp() {
} }
function mountIssuablesListApp() { function mountIssuablesListApp() {
if (!gon.features?.vueIssuablesList && !gon.features?.jiraIntegration) { if (!gon.features?.vueIssuablesList && !gon.features?.jiraIssuesIntegration) {
return; return;
} }

View file

@ -344,9 +344,15 @@ export function objectToQuery(obj) {
* @param {Object} params The query params to be set/updated * @param {Object} params The query params to be set/updated
* @param {String} url The url to be operated on * @param {String} url The url to be operated on
* @param {Boolean} clearParams Indicates whether existing query params should be removed or not * @param {Boolean} clearParams Indicates whether existing query params should be removed or not
* @param {Boolean} railsArraySyntax When enabled, changes the array syntax from `keys=` to `keys[]=` according to Rails conventions
* @returns {String} A copy of the original with the updated query params * @returns {String} A copy of the original with the updated query params
*/ */
export const setUrlParams = (params, url = window.location.href, clearParams = false) => { export const setUrlParams = (
params,
url = window.location.href,
clearParams = false,
railsArraySyntax = false,
) => {
const urlObj = new URL(url); const urlObj = new URL(url);
const queryString = urlObj.search; const queryString = urlObj.search;
const searchParams = clearParams ? new URLSearchParams('') : new URLSearchParams(queryString); const searchParams = clearParams ? new URLSearchParams('') : new URLSearchParams(queryString);
@ -355,11 +361,12 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f
if (params[key] === null || params[key] === undefined) { if (params[key] === null || params[key] === undefined) {
searchParams.delete(key); searchParams.delete(key);
} else if (Array.isArray(params[key])) { } else if (Array.isArray(params[key])) {
const keyName = railsArraySyntax ? `${key}[]` : key;
params[key].forEach((val, idx) => { params[key].forEach((val, idx) => {
if (idx === 0) { if (idx === 0) {
searchParams.set(key, val); searchParams.set(keyName, val);
} else { } else {
searchParams.append(key, val); searchParams.append(keyName, val);
} }
}); });
} else { } else {

View file

@ -7,11 +7,13 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers'; import initFilePickers from '~/file_pickers';
import initProjectLoadingSpinner from '../shared/save_project_loader'; import initProjectLoadingSpinner from '../shared/save_project_loader';
import initProjectPermissionsSettings from '../shared/permissions'; import initProjectPermissionsSettings from '../shared/permissions';
import initProjectRemoveModal from '~/projects/project_remove_modal';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initFilePickers(); initFilePickers();
initConfirmDangerModal(); initConfirmDangerModal();
initSettingsPanels(); initSettingsPanels();
initProjectRemoveModal();
mountBadgeSettings(PROJECT_BADGE); mountBadgeSettings(PROJECT_BADGE);
initProjectLoadingSpinner(); initProjectLoadingSpinner();

View file

@ -0,0 +1,108 @@
<script>
import { GlModal, GlModalDirective, GlSprintf, GlFormInput, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import { rstrip } from '~/lib/utils/common_utils';
import csrf from '~/lib/utils/csrf';
export default {
components: {
GlModal,
GlSprintf,
GlFormInput,
GlButton,
},
directives: {
GlModal: GlModalDirective,
},
props: {
confirmPhrase: {
type: String,
required: true,
},
warningMessage: {
type: String,
required: true,
},
formPath: {
type: String,
required: true,
},
},
data() {
return {
userInput: null,
};
},
computed: {
buttonDisabled() {
return rstrip(this.userInput) !== this.confirmPhrase;
},
csrfToken() {
return csrf.token;
},
},
methods: {
submitForm() {
this.$refs.form.submit();
},
},
strings: {
removeProject: __('Remove project'),
title: __('Confirmation required'),
confirm: __('Confirm'),
dataLoss: __(
'This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.',
),
confirmText: __('Please type %{phrase_code} to proceed or close this modal to cancel.'),
},
modalId: 'remove-project-modal',
};
</script>
<template>
<form ref="form" :action="formPath" method="post">
<input type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<gl-button v-gl-modal="$options.modalId" category="primary" variant="danger">{{
$options.strings.removeProject
}}</gl-button>
<gl-modal
ref="removeModal"
:modal-id="$options.modalId"
size="sm"
ok-variant="danger"
footer-class="bg-gray-light gl-p-5"
>
<template #modal-title>{{ $options.strings.title }}</template>
<template #modal-footer>
<div class="gl-w-full gl-display-flex gl-just-content-start gl-m-0">
<gl-button
:disabled="buttonDisabled"
category="primary"
variant="danger"
@click="submitForm"
>
{{ $options.strings.confirm }}
</gl-button>
</div>
</template>
<div>
<p class="gl-text-red-500 gl-font-weight-bold">{{ warningMessage }}</p>
<p class="gl-mb-0">{{ $options.strings.dataLoss }}</p>
<p>
<gl-sprintf :message="$options.strings.confirmText">
<template #phrase_code>
<code>{{ confirmPhrase }}</code>
</template>
</gl-sprintf>
</p>
<gl-form-input
id="confirm_name_input"
v-model="userInput"
name="confirm_name_input"
type="text"
/>
</div>
</gl-modal>
</form>
</template>

View file

@ -0,0 +1,24 @@
import Vue from 'vue';
import RemoveProjectModal from './components/remove_modal.vue';
export default (selector = '#js-confirm-project-remove') => {
const el = document.querySelector(selector);
if (!el) return;
const { formPath, confirmPhrase, warningMessage } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
render(createElement) {
return createElement(RemoveProjectModal, {
props: {
confirmPhrase,
warningMessage,
formPath,
},
});
},
});
};

View file

@ -1,4 +1,4 @@
import renderHtml from './renderers/render_html'; import renderBlockHtml from './renderers/render_html_block';
import renderKramdownList from './renderers/render_kramdown_list'; import renderKramdownList from './renderers/render_kramdown_list';
import renderKramdownText from './renderers/render_kramdown_text'; import renderKramdownText from './renderers/render_kramdown_text';
import renderIdentifierParagraph from './renderers/render_identifier_paragraph'; import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
@ -6,7 +6,7 @@ import renderEmbeddedRubyText from './renderers/render_embedded_ruby_text';
import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline'; import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
const htmlInlineRenderers = [renderFontAwesomeHtmlInline]; const htmlInlineRenderers = [renderFontAwesomeHtmlInline];
const htmlRenderers = [renderHtml]; const htmlBlockRenderers = [renderBlockHtml];
const listRenderers = [renderKramdownList]; const listRenderers = [renderKramdownList];
const paragraphRenderers = [renderIdentifierParagraph]; const paragraphRenderers = [renderIdentifierParagraph];
const textRenderers = [renderKramdownText, renderEmbeddedRubyText]; const textRenderers = [renderKramdownText, renderEmbeddedRubyText];
@ -32,9 +32,9 @@ const buildCustomHTMLRenderer = (
) => { ) => {
const defaults = { const defaults = {
htmlBlock(node, context) { htmlBlock(node, context) {
const allHtmlRenderers = [...customRenderers.list, ...htmlRenderers]; const allHtmlBlockRenderers = [...customRenderers.htmlBlock, ...htmlBlockRenderers];
return executeRenderer(allHtmlRenderers, node, context); return executeRenderer(allHtmlBlockRenderers, node, context);
}, },
htmlInline(node, context) { htmlInline(node, context) {
const allHtmlInlineRenderers = [...customRenderers.htmlInline, ...htmlInlineRenderers]; const allHtmlInlineRenderers = [...customRenderers.htmlInline, ...htmlInlineRenderers];
@ -47,7 +47,7 @@ const buildCustomHTMLRenderer = (
return executeRenderer(allListRenderers, node, context); return executeRenderer(allListRenderers, node, context);
}, },
paragraph(node, context) { paragraph(node, context) {
const allParagraphRenderers = [...customRenderers.list, ...paragraphRenderers]; const allParagraphRenderers = [...customRenderers.paragraph, ...paragraphRenderers];
return executeRenderer(allParagraphRenderers, node, context); return executeRenderer(allParagraphRenderers, node, context);
}, },

View file

@ -4,25 +4,36 @@ const buildToken = (type, tagName, props) => {
const TAG_TYPES = { const TAG_TYPES = {
block: 'div', block: 'div',
inline: 'span', inline: 'a',
}; };
export const buildUneditableOpenTokens = (token, type = TAG_TYPES.block) => { // Open helpers (singular and multiple)
return [
buildToken('openTag', type, { const buildUneditableOpenToken = (tagType = TAG_TYPES.block) =>
attributes: { contenteditable: false }, buildToken('openTag', tagType, {
classNames: [ attributes: { contenteditable: false },
'gl-px-4 gl-py-2 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed', classNames: [
], 'gl-px-4 gl-py-2 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed',
}), ],
token, });
];
export const buildUneditableOpenTokens = (token, tagType = TAG_TYPES.block) => {
return [buildUneditableOpenToken(tagType), token];
}; };
export const buildUneditableCloseToken = (type = TAG_TYPES.block) => buildToken('closeTag', type); // Close helpers (singular and multiple)
export const buildUneditableCloseTokens = (token, type = TAG_TYPES.block) => { export const buildUneditableCloseToken = (tagType = TAG_TYPES.block) =>
return [token, buildUneditableCloseToken(type)]; buildToken('closeTag', tagType);
export const buildUneditableCloseTokens = (token, tagType = TAG_TYPES.block) => {
return [token, buildUneditableCloseToken(tagType)];
};
// Complete helpers (open plus close)
export const buildUneditableTokens = token => {
return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()];
}; };
export const buildUneditableInlineTokens = token => { export const buildUneditableInlineTokens = token => {
@ -32,6 +43,19 @@ export const buildUneditableInlineTokens = token => {
]; ];
}; };
export const buildUneditableTokens = token => { export const buildUneditableHtmlAsTextTokens = node => {
return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()]; /*
Toast UI internally appends ' data-tomark-pass ' attribute flags so it can target certain
nested nodes for internal use during Markdown <=> WYSIWYG conversions. In our case, we want
to prevent HTML being rendered completely in WYSIWYG mode and thus we use a `text` vs. `html`
type when building the token. However, in doing so, we need to strip out the ` data-tomark-pass `
to prevent their persistence within the `text` content as the user did not intend these as edits.
https://github.com/nhn/tui.editor/blob/cc54ec224fc3a4b6e5a2b19a71650959f41adc0e/apps/editor/src/js/convertor.js#L72
*/
const regex = / data-tomark-pass /gm;
const content = node.literal.replace(regex, '');
const htmlAsTextToken = buildToken('text', null, { content });
return [buildUneditableOpenToken(), htmlAsTextToken, buildUneditableCloseToken()];
}; };

View file

@ -1,9 +0,0 @@
import { buildUneditableTokens } from './build_uneditable_token';
const canRender = ({ type }) => {
return type === 'htmlBlock';
};
const render = (_, { origin }) => buildUneditableTokens(origin());
export default { canRender, render };

View file

@ -0,0 +1,9 @@
import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
const canRender = ({ type }) => {
return type === 'htmlBlock';
};
const render = node => buildUneditableHtmlAsTextTokens(node);
export default { canRender, render };

View file

@ -15,7 +15,7 @@ class Projects::PipelinesController < Projects::ApplicationController
push_frontend_feature_flag(:junit_pipeline_view, project) push_frontend_feature_flag(:junit_pipeline_view, project)
push_frontend_feature_flag(:build_report_summary, project) push_frontend_feature_flag(:build_report_summary, project)
push_frontend_feature_flag(:filter_pipelines_search, project, default_enabled: true) push_frontend_feature_flag(:filter_pipelines_search, project, default_enabled: true)
push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: false) push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true)
push_frontend_feature_flag(:pipelines_security_report_summary, project) push_frontend_feature_flag(:pipelines_security_report_summary, project)
end end
before_action :ensure_pipeline, only: [:show] before_action :ensure_pipeline, only: [:show]

View file

@ -13,7 +13,7 @@ class Projects::ServicesController < Projects::ApplicationController
before_action :redirect_deprecated_prometheus_service, only: [:update] before_action :redirect_deprecated_prometheus_service, only: [:update]
before_action only: :edit do before_action only: :edit do
push_frontend_feature_flag(:integration_form_refactor, default_enabled: true) push_frontend_feature_flag(:integration_form_refactor, default_enabled: true)
push_frontend_feature_flag(:jira_integration, @project) push_frontend_feature_flag(:jira_issues_integration, @project, { default_enabled: true })
end end
respond_to :html respond_to :html

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
module ApprovableBase
extend ActiveSupport::Concern
included do
has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approved_by_users, through: :approvals, source: :user
end
def has_approved?(user)
return false unless user
approved_by_users.include?(user)
end
end

View file

@ -20,6 +20,7 @@ class MergeRequest < ApplicationRecord
include IgnorableColumns include IgnorableColumns
include MilestoneEventable include MilestoneEventable
include StateEventable include StateEventable
include ApprovableBase
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
@ -92,9 +93,6 @@ class MergeRequest < ApplicationRecord
has_many :draft_notes has_many :draft_notes
has_many :reviews, inverse_of: :merge_request has_many :reviews, inverse_of: :merge_request
has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approved_by_users, through: :approvals, source: :user
KNOWN_MERGE_PARAMS = [ KNOWN_MERGE_PARAMS = [
:auto_merge_strategy, :auto_merge_strategy,
:should_remove_source_branch, :should_remove_source_branch,

View file

@ -8,6 +8,8 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
include ChecksCollaboration include ChecksCollaboration
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
APPROVALS_WIDGET_BASE_TYPE = 'base'
presents :merge_request presents :merge_request
def ci_status def ci_status
@ -224,6 +226,22 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end end
end end
def api_approvals_path
expose_path(api_v4_projects_merge_requests_approvals_path(id: project.id, merge_request_iid: merge_request.iid))
end
def api_approve_path
expose_path(api_v4_projects_merge_requests_approve_path(id: project.id, merge_request_iid: merge_request.iid))
end
def api_unapprove_path
expose_path(api_v4_projects_merge_requests_unapprove_path(id: project.id, merge_request_iid: merge_request.iid))
end
def approvals_widget_type
APPROVALS_WIDGET_BASE_TYPE
end
private private
def cached_can_be_reverted? def cached_can_be_reverted?

View file

@ -86,6 +86,18 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
presenter(merge_request).squash_on_merge? presenter(merge_request).squash_on_merge?
end end
expose :api_approvals_path do |merge_request|
presenter(merge_request).api_approvals_path
end
expose :api_approve_path do |merge_request|
presenter(merge_request).api_approve_path
end
expose :api_unapprove_path do |merge_request|
presenter(merge_request).api_unapprove_path
end
private private
delegate :current_user, to: :request delegate :current_user, to: :request

View file

@ -157,6 +157,10 @@ class MergeRequestPollWidgetEntity < Grape::Entity
presenter(merge_request).squash_on_merge? presenter(merge_request).squash_on_merge?
end end
expose :approvals_widget_type do |merge_request|
presenter(merge_request).approvals_widget_type
end
private private
delegate :current_user, to: :request delegate :current_user, to: :request

View file

@ -22,7 +22,11 @@ module Ci
return result unless result[:status] == :success return result unless result[:status] == :success
headers = JobArtifactUploader.workhorse_authorize(has_length: false, maximum_size: max_size(artifact_type)) headers = JobArtifactUploader.workhorse_authorize(has_length: false, maximum_size: max_size(artifact_type))
headers[:ProcessLsif] = true if lsif?(artifact_type)
if lsif?(artifact_type)
headers[:ProcessLsif] = true
headers[:ProcessLsifReferences] = Feature.enabled?(:code_navigation_references, project, default_enabled: false)
end
success(headers: headers) success(headers: headers)
end end

View file

@ -4,7 +4,7 @@ module MergeRequests
class RemoveApprovalService < MergeRequests::BaseService class RemoveApprovalService < MergeRequests::BaseService
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def execute(merge_request) def execute(merge_request)
return unless approved_by_user?(merge_request) return unless merge_request.has_approved?(current_user)
# paranoid protection against running wrong deletes # paranoid protection against running wrong deletes
return unless merge_request.id && current_user.id return unless merge_request.id && current_user.id
@ -24,10 +24,6 @@ module MergeRequests
private private
def approved_by_user?(merge_request)
merge_request.approved_by_users.include?(current_user)
end
def reset_approvals_cache(merge_request) def reset_approvals_cache(merge_request)
merge_request.approvals.reset merge_request.approvals.reset
end end

View file

@ -10,7 +10,7 @@
= f.check_box :container_expiration_policies_enable_historic_entries, class: 'form-check-input' = f.check_box :container_expiration_policies_enable_historic_entries, class: 'form-check-input'
= f.label :container_expiration_policies_enable_historic_entries, class: 'form-check-label' do = f.label :container_expiration_policies_enable_historic_entries, class: 'form-check-label' do
= _("Enable container expiration and retention policies for projects created earlier than GitLab 12.7.") = _("Enable container expiration and retention policies for projects created earlier than GitLab 12.7.")
= link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy') = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy')
.form-text.text-muted .form-text.text-muted
= _("Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project.") = _("Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project.")
= link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'use-with-external-container-registries') = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'use-with-external-container-registries')

View file

@ -4,7 +4,6 @@
%h4.danger-title= _('Remove project') %h4.danger-title= _('Remove project')
%p %p
%strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.') %strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
= form_tag(project_path(project), method: :delete) do %p
%p %strong= _('Removed projects cannot be restored!')
%strong= _('Removed projects cannot be restored!') #js-confirm-project-remove{ data: { form_path: project_path(project), confirm_phrase: project.path, warning_message: remove_project_message(project) } }
= button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(project) }

View file

@ -8,4 +8,4 @@
- viewer.errors.messages.each do |error| - viewer.errors.messages.each do |error|
%li= error.join(': ') %li= error.join(': ')
= link_to _('Learn more'), help_page_path('user/project/integrations/prometheus.md', anchor: 'defining-custom-dashboards-per-project') = link_to _('Learn more'), help_page_path('operations/metrics/dashboards/index.md', anchor: 'defining-custom-dashboards-per-project')

View file

@ -3,7 +3,7 @@
.col-lg-3 .col-lg-3
%p %p
= s_('PrometheusService|Custom metrics require Prometheus installed on a cluster with environment scope "*" OR a manually configured Prometheus to be available.') = s_('PrometheusService|Custom metrics require Prometheus installed on a cluster with environment scope "*" OR a manually configured Prometheus to be available.')
= link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'adding-custom-metrics'), target: '_blank', rel: "noopener noreferrer" = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/index.md', anchor: 'adding-custom-metrics'), target: '_blank', rel: "noopener noreferrer"
.col-lg-9 .col-lg-9
.card.custom-monitored-metrics.js-panel-custom-monitored-metrics{ data: { qa_selector: 'custom_metrics_container', active_custom_metrics: project_prometheus_metrics_path(project), environments_data: environments_list_data, service_active: "#{@service.active}" } } .card.custom-monitored-metrics.js-panel-custom-monitored-metrics{ data: { qa_selector: 'custom_metrics_container', active_custom_metrics: project_prometheus_metrics_path(project), environments_data: environments_list_data, service_active: "#{@service.active}" } }

View file

@ -3,6 +3,6 @@
- notify_url = notify_project_prometheus_alerts_url(@project, format: :json) - notify_url = notify_project_prometheus_alerts_url(@project, format: :json)
- authorization_key = @project.alerting_setting.try(:token) - authorization_key = @project.alerting_setting.try(:token)
- learn_more_url = help_page_path('user/project/integrations/prometheus', anchor: 'external-prometheus-instances') - learn_more_url = help_page_path('operations/metrics/index.md', anchor: 'external-prometheus-instances')
#js-settings-prometheus-alerts{ data: { notify_url: notify_url, authorization_key: authorization_key, change_key_url: reset_alerting_token_project_settings_operations_path(@project), learn_more_url: learn_more_url, disabled: true } } #js-settings-prometheus-alerts{ data: { notify_url: notify_url, authorization_key: authorization_key, change_key_url: reset_alerting_token_project_settings_operations_path(@project), learn_more_url: learn_more_url, disabled: true } }

View file

@ -33,5 +33,5 @@
.flash-notice .flash-notice
.flash-text .flash-text
= s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe = s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe
= link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'query-variables') = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/dashboards/variables.md', anchor: 'query-variables')
%ul.list-unstyled.metrics-list.js-missing-var-metrics-list %ul.list-unstyled.metrics-list.js-missing-var-metrics-list

View file

@ -71,6 +71,6 @@
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p %p
= _("Save space and find tags in the Container Registry more easily. Enable the cleanup policy to remove stale tags and keep only the ones you need.") = _("Save space and find tags in the Container Registry more easily. Enable the cleanup policy to remove stale tags and keep only the ones you need.")
= link_to _('More information'), help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy', target: '_blank', rel: 'noopener noreferrer') = link_to _('More information'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy', target: '_blank', rel: 'noopener noreferrer')
.settings-content .settings-content
= render 'projects/registry/settings/index' = render 'projects/registry/settings/index'

View file

@ -0,0 +1,5 @@
---
title: Add a custom HTML renderer to the Static Site Editor for HTML block syntax
merge_request: 36330
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Expose approvals fields for FOSS FE
merge_request: 36564
author:
type: changed

View file

@ -4,7 +4,6 @@ THROUGHPUT_LABELS = [
'Community contribution', 'Community contribution',
'security', 'security',
'bug', 'bug',
'backstage', # To be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/222360.
'feature', 'feature',
'feature::addition', 'feature::addition',
'feature::maintenance', 'feature::maintenance',

View file

@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
NO_SPECS_LABELS = [ NO_SPECS_LABELS = [
'backstage', # To be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/222360.
'tooling', 'tooling',
'tooling::pipelines', 'tooling::pipelines',
'tooling::workflow', 'tooling::workflow',

View file

@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: howto type: howto
--- ---
# Geo Troubleshooting **(PREMIUM ONLY)** # Troubleshooting Geo **(PREMIUM ONLY)**
Setting up Geo requires careful attention to details and sometimes it's easy to Setting up Geo requires careful attention to details and sometimes it's easy to
miss a step. miss a step.

View file

@ -1831,12 +1831,19 @@ Example response:
## Remove project ## Remove project
This endpoint either: This endpoint:
- Removes a project including all associated resources (issues, merge requests etc). - Removes a project including all associated resources (issues, merge requests etc).
- From [GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, marks a project for deletion. Actual - From [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers,
deletion happens after number of days specified in group admins can [configure](../user/group/index.md#enabling-delayed-project-removal-premium) projects within a group
[instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). to be deleted after a delayed period.
When enabled, actual deletion happens after the number of days
specified in the [default deletion period](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
CAUTION: **Warning:**
The default behavior of [Delayed Project deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6
was changed to [Immediate deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/220382)
in GitLab 13.2, as discussed in [Enabling delayed project removal](../user/group/index.md#enabling-delayed-project-removal-premium).
```plaintext ```plaintext
DELETE /projects/:id DELETE /projects/:id

View file

@ -82,10 +82,10 @@ are certain use cases that you may need to work around. For more information:
## DAG Visualization ## DAG Visualization
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215517) in GitLab 13.1 as a [Beta feature](https://about.gitlab.com/handbook/product/#beta). > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215517) in GitLab 13.1 as a [Beta feature](https://about.gitlab.com/handbook/product/#beta).
> - It's deployed behind a feature flag, disabled by default. > - It was deployed behind a feature flag, disabled by default.
> - It became [enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36802) in 13.2.
> - It's enabled on GitLab.com. > - It's enabled on GitLab.com.
> - It's not recommended for production use. > - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-dag-visualization-core-only).
> - For GitLab self-managed instances, GitLab administrators can opt to [enable it](#enable-or-disable-dag-visualization-core-only)
The DAG visualization makes it easier to visualize the relationships between dependent jobs in a DAG. This graph will display all the jobs in a pipeline that need or are needed by other jobs. Jobs with no relationships are not displayed in this view. The DAG visualization makes it easier to visualize the relationships between dependent jobs in a DAG. This graph will display all the jobs in a pipeline that need or are needed by other jobs. Jobs with no relationships are not displayed in this view.
@ -97,15 +97,15 @@ Clicking a node will highlight all the job paths it depends on.
### Enable or disable DAG Visualization **(CORE ONLY)** ### Enable or disable DAG Visualization **(CORE ONLY)**
DAG Visualization is under development and requires more testing, but is being made available as a beta features so users can check its limitations and uses. DAG Visualization is under development, but is being made available as a beta feature so users can check its limitations and uses.
It is deployed behind a feature flag that is **disabled by default**. It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) [GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can opt to enable it for your instance: can opt to disable it for your instance:
```ruby ```ruby
# Instance-wide # Instance-wide
Feature.enable(:dag_pipeline_tab) Feature.disable(:dag_pipeline_tab)
# or by project # or by project
Feature.enable(:dag_pipeline_tab, Project.find(<project id>)) Feature.disable(:dag_pipeline_tab, Project.find(<project id>))
``` ```

View file

@ -69,7 +69,7 @@ Panels in a panel group are laid out in rows consisting of two panels per row. A
| Property | Type | Required | Description | | Property | Type | Required | Description |
| ----------- | ------ | ----------------------------- | -------------------------------------------------------------------- | | ----------- | ------ | ----------------------------- | -------------------------------------------------------------------- |
| `name` | string | no, but highly encouraged | Y-Axis label for the panel. Replaces `y_label` if set. | | `name` | string | no, but highly encouraged | Y-Axis label for the panel. Replaces `y_label` if set. |
| `format` | string | no, defaults to `engineering` | Unit format used. See the [full list of units](../../../user/project/integrations/prometheus_units.md). | | `format` | string | no, defaults to `engineering` | Unit format used. See the [full list of units](yaml_number_format.md). |
| `precision` | number | no, defaults to `2` | Number of decimal places to display in the number. | | | `precision` | number | no, defaults to `2` | Number of decimal places to display in the number. | |
## **Metrics (`metrics`) properties** ## **Metrics (`metrics`) properties**

View file

@ -0,0 +1,177 @@
---
stage: Monitor
group: APM
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/#designated-technical-writers
---
# Unit formats reference
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/201999) in GitLab 12.9.
Format the data in your dashboard panels.
You can select units to format your charts by adding `format` to your
[axis configuration](yaml.md).
## Internationalization and localization
Currently, your [internationalization and localization options](https://en.wikipedia.org/wiki/Internationalization_and_localization) for number formatting are dependent on the system you are using i.e. your OS or browser.
## Engineering Notation
For generic or default data, numbers are formatted according to the current locale in [engineering notation](https://en.wikipedia.org/wiki/Engineering_notation).
While an [engineering notation exists for the web](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat), GitLab uses a version based off the [scientific notation](https://en.wikipedia.org/wiki/Scientific_notation). GitLab formatting acts in accordance with SI prefixes. For example, using GitLab notation, `1500.00` becomes `1.5k` instead of `1.5E3`. Keep this distinction in mind when using the engineering notation for your metrics.
Formats: `engineering`
SI prefixes:
| Name | Symbol | Value |
| ---------- | ------- | -------------------------- |
| `yotta` | Y | 1000000000000000000000000 |
| `zetta` | Z | 1000000000000000000000 |
| `exa` | E | 1000000000000000000 |
| `peta` | P | 1000000000000000 |
| `tera` | T | 1000000000000 |
| `giga` | G | 1000000000 |
| `mega` | M | 1000000 |
| `kilo` | k | 1000 |
| `milli` | m | 0.001 |
| `micro` | μ | 0.000001 |
| `nano` | n | 0.000000001 |
| `pico` | p | 0.000000000001 |
| `femto` | f | 0.000000000000001 |
| `atto` | a | 0.000000000000000001 |
| `zepto` | z | 0.000000000000000000001 |
| `yocto` | y | 0.000000000000000000000001 |
**Examples:**
| Data | Displayed |
| --------------------------------- | --------- |
| `0.000000000000000000000008` | 8y |
| `0.000000000000000000008` | 8z |
| `0.000000000000000008` | 8a |
| `0.000000000000008` | 8f |
| `0.000000000008` | 8p |
| `0.000000008` | 8n |
| `0.000008` | 8μ |
| `0.008` | 8m |
| `10` | 10 |
| `1080` | 1.08k |
| `18000` | 18k |
| `18888` | 18.9k |
| `188888` | 189k |
| `18888888` | 18.9M |
| `1888888888` | 1.89G |
| `1888888888888` | 1.89T |
| `1888888888888888` | 1.89P |
| `1888888888888888888` | 1.89E |
| `1888888888888888888888` | 1.89Z |
| `1888888888888888888888888` | 1.89Y |
| `1888888888888888888888888888` | 1.89e+27 |
## Numbers
For number data, numbers are formatted according to the current locale.
Formats: `number`
**Examples:**
| Data | Displayed |
| ---------- | --------- |
| `10` | 1 |
| `1000` | 1,000 |
| `1000000` | 1,000,000 |
## Percentage
For percentage data, format numbers in the chart with a `%` symbol.
Formats supported: `percent`, `percentHundred`
**Examples:**
| Format | Data | Displayed |
| ---------------- | ----- | --------- |
| `percent` | `0.5` | 50% |
| `percent` | `1` | 100% |
| `percent` | `2` | 200% |
| `percentHundred` | `50` | 50% |
| `percentHundred` | `100` | 100% |
| `percentHundred` | `200` | 200% |
## Duration
For time durations, format numbers in the chart with a time unit symbol.
Formats supported: `milliseconds`, `seconds`
**Examples:**
| Format | Data | Displayed |
| -------------- | ------ | --------- |
| `milliseconds` | `10` | 10ms |
| `milliseconds` | `500` | 100ms |
| `milliseconds` | `1000` | 1000ms |
| `seconds` | `10` | 10s |
| `seconds` | `500` | 500s |
| `seconds` | `1000` | 1000s |
## Digital (Metric)
Converts a number of bytes using metric prefixes. It scales to
use the unit that's the best fit.
Formats supported:
- `decimalBytes`
- `kilobytes`
- `megabytes`
- `gigabytes`
- `terabytes`
- `petabytes`
**Examples:**
| Format | Data | Displayed |
| -------------- | --------- | --------- |
| `decimalBytes` | `1` | 1B |
| `decimalBytes` | `1000` | 1kB |
| `decimalBytes` | `1000000` | 1MB |
| `kilobytes` | `1` | 1kB |
| `kilobytes` | `1000` | 1MB |
| `kilobytes` | `1000000` | 1GB |
| `megabytes` | `1` | 1MB |
| `megabytes` | `1000` | 1GB |
| `megabytes` | `1000000` | 1TB |
## Digital (IEC)
Converts a number of bytes using binary prefixes. It scales to
use the unit that's the best fit.
Formats supported:
- `bytes`
- `kibibytes`
- `mebibytes`
- `gibibytes`
- `tebibytes`
- `pebibytes`
**Examples:**
| Format | Data | Displayed |
| ----------- | ------------- | --------- |
| `bytes` | `1` | 1B |
| `bytes` | `1024` | 1KiB |
| `bytes` | `1024 * 1024` | 1MiB |
| `kibibytes` | `1` | 1KiB |
| `kibibytes` | `1024` | 1MiB |
| `kibibytes` | `1024 * 1024` | 1GiB |
| `mebibytes` | `1` | 1MiB |
| `mebibytes` | `1024` | 1GiB |
| `mebibytes` | `1024 * 1024` | 1TiB |

View file

@ -132,7 +132,7 @@ If the metric exceeds the threshold of the alert for over 5 minutes, an email wi
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202146) in GitLab 13.2. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202146) in GitLab 13.2.
You can use keyboard shortcuts to interact more quickly with your currently-focused chartpanel. To activate keyboard shortcuts, use keyboard tabs to highlight the**{ellipsis_v}** **More actions** dropdown menu, or hover over the dropdown menuwith your mouse, then press the key corresponding to your desired action: You can use keyboard shortcuts to interact more quickly with your currently-focused chartpanel. To activate keyboard shortcuts, use keyboard tabs to highlight the**{ellipsis_v}** **More actions** dropdown menu, or hover over the dropdown menu with your mouse, then press the key corresponding to your desired action:
- **Expand panel** - <kbd>e</kbd> - **Expand panel** - <kbd>e</kbd>
- **View logs** - <kbd>l</kbd> (lowercase 'L') - **View logs** - <kbd>l</kbd> (lowercase 'L')

View file

@ -68,8 +68,16 @@ To ensure only admin users can delete projects:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6.
By default, a project or group marked for removal will be permanently removed after 7 days. By default, a project marked for deletion will be permanently removed with immediate effect.
This period may be changed, and setting this period to 0 will enable immediate removal By default, a group marked for deletion will be permanently removed after 7 days.
CAUTION: **Warning:**
The default behavior of [Delayed Project deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6 was changed to
[Immediate deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) in GitLab 13.2.
Projects within a group can be deleted after a delayed period, by [configuring in Group Settings](../../group/index.md#enabling-delayed-project-removal-premium).
The default period is 7 days, and can be changed. Setting this period to 0 will enable immediate removal
of projects or groups. of projects or groups.
To change this period: To change this period:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -650,6 +650,23 @@ To enable this feature:
1. Expand the **Permissions, LFS, 2FA** section, and select **Disable group mentions**. 1. Expand the **Permissions, LFS, 2FA** section, and select **Disable group mentions**.
1. Click **Save changes**. 1. Click **Save changes**.
#### Enabling delayed Project removal **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) in GitLab 13.2.
By default, projects within a group are deleted immediately.
Optionally, on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers,
you can configure the projects within a group to be deleted after a delayed interval.
During this interval period, the projects will be in a read-only state and can be restored, if required.
The interval period defaults to 7 days, and can be modified by an admin in the [instance settings](../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
To enable delayed deletion of projects:
1. Navigate to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section, and check **Enable delayed project removal**.
1. Click **Save changes**.
### Advanced settings ### Advanced settings
- **Projects**: View all projects within that group, add members to each project, - **Projects**: View all projects within that group, add members to each project,

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -99,14 +99,24 @@ Installing and configuring Prometheus to monitor applications is fairly straight
The actual configuration of Prometheus integration within GitLab is very simple. The actual configuration of Prometheus integration within GitLab is very simple.
All you will need is the domain name or IP address of the Prometheus server you'd like All you will need is the domain name or IP address of the Prometheus server you'd like
to integrate with. to integrate with. If the Prometheus resource is secured with Google's Identity-Aware Proxy (IAP),
additional information like Client ID and Service Account credentials can be passed which
GitLab can use to access the resource. More information about authentication from a
service account can be found at Google's documentation for
[Authenticating from a service account](https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_service_account).
1. Navigate to the [Integrations page](overview.md#accessing-integrations). 1. Navigate to the [Integrations page](overview.md#accessing-integrations) at
**{settings}** **Settings > Integrations**.
1. Click the **Prometheus** service. 1. Click the **Prometheus** service.
1. Provide the domain name or IP address of your server, for example `http://prometheus.example.com/` or `http://192.0.2.1/`. 1. For **API URL**, provide the domain name or IP address of your server, such as
`http://prometheus.example.com/` or `http://192.0.2.1/`.
1. (Optional) In **Google IAP Audience Client ID**, provide the Client ID of the
Prometheus OAuth Client secured with Google IAP.
1. (Optional) In **Google IAP Service Account JSON**, provide the contents of the
Service Account credentials file that is authorized to access the Prometheus resource.
1. Click **Save changes**. 1. Click **Save changes**.
![Configure Prometheus Service](img/prometheus_service_configuration.png) ![Configure Prometheus Service](img/prometheus_manual_configuration_v13_2.png)
#### Thanos configuration in GitLab #### Thanos configuration in GitLab

View file

@ -1,175 +1,5 @@
--- ---
stage: Monitor redirect_to: '../../../operations/metrics/dashboards/yaml_number_format.md'
group: APM
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/#designated-technical-writers
--- ---
# Unit formats reference This document was moved to [another location](../../../operations/metrics/dashboards/yaml_number_format.md).
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/201999) in GitLab 12.9.
You can select units to format your charts by adding `format` to your
[axis configuration](../../../operations/metrics/dashboards/yaml.md).
## Internationalization and localization
Currently, your [internationalization and localization options](https://en.wikipedia.org/wiki/Internationalization_and_localization) for number formatting are dependent on the system you are using i.e. your OS or browser.
## Engineering Notation
For generic or default data, numbers are formatted according to the current locale in [engineering notation](https://en.wikipedia.org/wiki/Engineering_notation).
While an [engineering notation exists for the web](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat), GitLab uses a version based off the [scientific notation](https://en.wikipedia.org/wiki/Scientific_notation). GitLab formatting acts in accordance with SI prefixes. For example, using GitLab notation, `1500.00` becomes `1.5k` instead of `1.5E3`. Keep this distinction in mind when using the engineering notation for your metrics.
Formats: `engineering`
SI prefixes:
| Name | Symbol | Value |
| ---------- | ------- | -------------------------- |
| `yotta` | Y | 1000000000000000000000000 |
| `zetta` | Z | 1000000000000000000000 |
| `exa` | E | 1000000000000000000 |
| `peta` | P | 1000000000000000 |
| `tera` | T | 1000000000000 |
| `giga` | G | 1000000000 |
| `mega` | M | 1000000 |
| `kilo` | k | 1000 |
| `milli` | m | 0.001 |
| `micro` | μ | 0.000001 |
| `nano` | n | 0.000000001 |
| `pico` | p | 0.000000000001 |
| `femto` | f | 0.000000000000001 |
| `atto` | a | 0.000000000000000001 |
| `zepto` | z | 0.000000000000000000001 |
| `yocto` | y | 0.000000000000000000000001 |
**Examples:**
| Data | Displayed |
| --------------------------------- | --------- |
| `0.000000000000000000000008` | 8y |
| `0.000000000000000000008` | 8z |
| `0.000000000000000008` | 8a |
| `0.000000000000008` | 8f |
| `0.000000000008` | 8p |
| `0.000000008` | 8n |
| `0.000008` | 8μ |
| `0.008` | 8m |
| `10` | 10 |
| `1080` | 1.08k |
| `18000` | 18k |
| `18888` | 18.9k |
| `188888` | 189k |
| `18888888` | 18.9M |
| `1888888888` | 1.89G |
| `1888888888888` | 1.89T |
| `1888888888888888` | 1.89P |
| `1888888888888888888` | 1.89E |
| `1888888888888888888888` | 1.89Z |
| `1888888888888888888888888` | 1.89Y |
| `1888888888888888888888888888` | 1.89e+27 |
## Numbers
For number data, numbers are formatted according to the current locale.
Formats: `number`
**Examples:**
| Data | Displayed |
| ---------- | --------- |
| `10` | 1 |
| `1000` | 1,000 |
| `1000000` | 1,000,000 |
## Percentage
For percentage data, format numbers in the chart with a `%` symbol.
Formats supported: `percent`, `percentHundred`
**Examples:**
| Format | Data | Displayed |
| ---------------- | ----- | --------- |
| `percent` | `0.5` | 50% |
| `percent` | `1` | 100% |
| `percent` | `2` | 200% |
| `percentHundred` | `50` | 50% |
| `percentHundred` | `100` | 100% |
| `percentHundred` | `200` | 200% |
## Duration
For time durations, format numbers in the chart with a time unit symbol.
Formats supported: `milliseconds`, `seconds`
**Examples:**
| Format | Data | Displayed |
| -------------- | ------ | --------- |
| `milliseconds` | `10` | 10ms |
| `milliseconds` | `500` | 100ms |
| `milliseconds` | `1000` | 1000ms |
| `seconds` | `10` | 10s |
| `seconds` | `500` | 500s |
| `seconds` | `1000` | 1000s |
## Digital (Metric)
Converts a number of bytes using metric prefixes. It scales to
use the unit that's the best fit.
Formats supported:
- `decimalBytes`
- `kilobytes`
- `megabytes`
- `gigabytes`
- `terabytes`
- `petabytes`
**Examples:**
| Format | Data | Displayed |
| -------------- | --------- | --------- |
| `decimalBytes` | `1` | 1B |
| `decimalBytes` | `1000` | 1kB |
| `decimalBytes` | `1000000` | 1MB |
| `kilobytes` | `1` | 1kB |
| `kilobytes` | `1000` | 1MB |
| `kilobytes` | `1000000` | 1GB |
| `megabytes` | `1` | 1MB |
| `megabytes` | `1000` | 1GB |
| `megabytes` | `1000000` | 1TB |
## Digital (IEC)
Converts a number of bytes using binary prefixes. It scales to
use the unit that's the best fit.
Formats supported:
- `bytes`
- `kibibytes`
- `mebibytes`
- `gibibytes`
- `tebibytes`
- `pebibytes`
**Examples:**
| Format | Data | Displayed |
| ----------- | ------------- | --------- |
| `bytes` | `1` | 1B |
| `bytes` | `1024` | 1KiB |
| `bytes` | `1024 * 1024` | 1MiB |
| `kibibytes` | `1` | 1KiB |
| `kibibytes` | `1024` | 1MiB |
| `kibibytes` | `1024 * 1024` | 1GiB |
| `mebibytes` | `1` | 1MiB |
| `mebibytes` | `1024` | 1GiB |
| `mebibytes` | `1024 * 1024` | 1TiB |

View file

@ -22,6 +22,30 @@ Enabling any of these methods will allow the Alerts list to display. After confi
alerts, visit **{cloud-gear}** **Operations > Alerts** in your project's sidebar alerts, visit **{cloud-gear}** **Operations > Alerts** in your project's sidebar
to [view the list](#alert-management-list) of alerts. to [view the list](#alert-management-list) of alerts.
### Opsgenie integration **(PREMIUM)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3066) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
A new way of monitoring Alerts via a GitLab integration is with
[Opsgenie](https://www.atlassian.com/software/opsgenie).
NOTE: **Note:**
If you enable the Opsgenie integration, you cannot have other GitLab alert services,
such as [Generic Alerts](../integrations/generic_alerts.md) or
Prometheus alerts, active at the same time.
To enable Opsgenie integration:
1. Sign in as a user with Maintainer or Owner [permissions](../../permissions.md).
1. Navigate to **{cloud-gear}** **Operations > Alerts**.
1. In the **Integrations** select box, select Opsgenie.
1. Click the **Active** toggle.
1. In the **API URL**, enter the base URL for your Opsgenie integration, such
as `https://app.opsgenie.com/alert/list`.
1. Click **Save changes**.
After enabling the integration, navigate to the Alerts list page at **{cloud-gear}** **Operations > Alerts**, and click **View alerts in Opsgenie**.
### Enable a Generic Alerts endpoint ### Enable a Generic Alerts endpoint
GitLab provides the Generic Alerts endpoint so you can accept alerts from a third-party GitLab provides the Generic Alerts endpoint so you can accept alerts from a third-party

View file

@ -223,13 +223,18 @@ To remove a project:
1. In the Remove project section, click the **Remove project** button. 1. In the Remove project section, click the **Remove project** button.
1. Confirm the action when asked to. 1. Confirm the action when asked to.
This action either: This action:
- Removes a project including all associated resources (issues, merge requests etc). - Removes a project including all associated resources (issues, merge requests etc).
- Since [GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/-/issues/32935), on - From [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers,
[GitLab Premium or GitLab.com Silver](https://about.gitlab.com/pricing/) or higher tiers, marks a project for group admins can [configure](../../group/index.md#enabling-delayed-project-removal-premium) projects within a group
deletion. The deletion will happen 7 days later by default, but this can be changed in the to be deleted after a delayed period.
[instance settings](../../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). When enabled, actual deletion happens after number of days
specified in [instance settings](../../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
CAUTION: **Warning:**
The default behavior of [Delayed Project deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6 was changed to
[Immediate deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) in GitLab 13.2.
#### Restore a project **(PREMIUM)** #### Restore a project **(PREMIUM)**

View file

@ -0,0 +1,100 @@
# frozen_string_literal: true
require_relative '../../lib/gitlab/utils/markdown'
module HamlLint
class Linter
# This class is responsible for detection of help_page_path helpers
# with incorrect links or anchors
class DocumentationLinks < Linter
include ::HamlLint::LinterRegistry
include ::Gitlab::Utils::Markdown
DOCS_DIRECTORY = File.join(File.expand_path('../..', __dir__), 'doc')
HELP_PATH_LINK_PATTERN = <<~PATTERN
`(send nil? :help_page_path $...)
PATTERN
MARKDOWN_HEADER = %r{\A\#{1,6}\s+(?<header>.+)\Z}.freeze
def visit_script(node)
check(node)
end
def visit_silent_script(node)
check(node)
end
def visit_tag(node)
check(node)
end
private
def check(node)
match = extract_link_and_anchor(node)
return if match.empty?
path_to_file = detect_path_to_file(match[:link])
unless File.file?(path_to_file)
record_lint(node, "help_page_path points to the unknown location: #{path_to_file}")
return
end
unless correct_anchor?(path_to_file, match[:anchor])
record_lint(node, "anchor (#{match[:anchor]}) is missing in: #{path_to_file}")
end
end
def extract_link_and_anchor(node)
ast_tree = fetch_ast_tree(node)
return {} unless ast_tree
link_match, attributes_match = ::RuboCop::NodePattern.new(HELP_PATH_LINK_PATTERN).match(ast_tree)
{ link: fetch_link(link_match), anchor: fetch_anchor(attributes_match) }.compact
end
def fetch_ast_tree(node)
# Sometimes links are provided via data attributes in html tag
return node.parsed_attributes.syntax_tree if node.type == :tag
node.parsed_script.syntax_tree
end
def detect_path_to_file(link)
path = File.join(DOCS_DIRECTORY, link)
path += '.md' unless path.end_with?('.md')
path
end
def fetch_link(link_match)
return unless link_match && link_match.str_type?
link_match.value
end
def fetch_anchor(attributes_match)
return unless attributes_match
attributes_match.each_pair do |pkey, pvalue|
break pvalue.value if pkey.value == :anchor
end
end
def correct_anchor?(path_to_file, anchor)
return true unless anchor
File.open(path_to_file).any? do |line|
result = line.match(MARKDOWN_HEADER)
string_to_anchor(result[:header]) == anchor if result
end
end
end
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module API
module Entities
class Approvals < Grape::Entity
expose :user, using: ::API::Entities::UserBasic
end
end
end

View file

@ -3,6 +3,22 @@
module API module API
module Entities module Entities
class MergeRequestApprovals < Grape::Entity class MergeRequestApprovals < Grape::Entity
expose :user_has_approved do |merge_request, options|
merge_request.has_approved?(options[:current_user])
end
expose :user_can_approve do |merge_request, options|
!merge_request.has_approved?(options[:current_user]) &&
options[:current_user].can?(:approve_merge_request, merge_request)
end
expose :approved do |merge_request|
merge_request.approvals.present?
end
expose :approved_by, using: ::API::Entities::Approvals do |merge_request|
merge_request.approvals
end
end end
end end
end end

View file

@ -17,7 +17,7 @@ module Banzai
# :toc - String containing Table of Contents data as a `ul` element with # :toc - String containing Table of Contents data as a `ul` element with
# `li` child elements. # `li` child elements.
class TableOfContentsFilter < HTML::Pipeline::Filter class TableOfContentsFilter < HTML::Pipeline::Filter
PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze include Gitlab::Utils::Markdown
def call def call
return doc if context[:no_header_anchors] return doc if context[:no_header_anchors]
@ -29,14 +29,7 @@ module Banzai
doc.css('h1, h2, h3, h4, h5, h6').each do |node| doc.css('h1, h2, h3, h4, h5, h6').each do |node|
if header_content = node.children.first if header_content = node.children.first
id = node id = string_to_anchor(node.text)
.text
.strip
.downcase
.gsub(PUNCTUATION_REGEXP, '') # remove punctuation
.tr(' ', '-') # replace spaces with dash
.squeeze('-') # replace multiple dashes with one
.gsub(/\A(\d+)\z/, 'anchor-\1') # digits-only hrefs conflict with issue refs
uniq = headers[id] > 0 ? "-#{headers[id]}" : '' uniq = headers[id] > 0 ? "-#{headers[id]}" : ''
headers[id] += 1 headers[id] += 1

View file

@ -4,7 +4,6 @@ module Gitlab
module Danger module Danger
module Changelog module Changelog
NO_CHANGELOG_LABELS = [ NO_CHANGELOG_LABELS = [
'backstage', # To be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/222360.
'tooling', 'tooling',
'tooling::pipelines', 'tooling::pipelines',
'tooling::workflow', 'tooling::workflow',
@ -14,7 +13,7 @@ module Gitlab
NO_CHANGELOG_CATEGORIES = %i[docs none].freeze NO_CHANGELOG_CATEGORIES = %i[docs none].freeze
def needed? def needed?
categories_need_changelog? && (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty? categories_need_changelog? && without_no_changelog_label?
end end
def found def found
@ -34,6 +33,10 @@ module Gitlab
def categories_need_changelog? def categories_need_changelog?
(helper.changes_by_category.keys - NO_CHANGELOG_CATEGORIES).any? (helper.changes_by_category.keys - NO_CHANGELOG_CATEGORIES).any?
end end
def without_no_changelog_label?
(gitlab.mr_labels & NO_CHANGELOG_LABELS).empty?
end
end end
end end
end end

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Gitlab
module Utils
module Markdown
PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze
def string_to_anchor(string)
string
.strip
.downcase
.gsub(PUNCTUATION_REGEXP, '') # remove punctuation
.tr(' ', '-') # replace spaces with dash
.squeeze('-') # replace multiple dashes with one
.gsub(/\A(\d+)\z/, 'anchor-\1') # digits-only hrefs conflict with issue refs
end
end
end
end

View file

@ -5879,6 +5879,9 @@ msgstr ""
msgid "CodeIntelligence|This is the definition" msgid "CodeIntelligence|This is the definition"
msgstr "" msgstr ""
msgid "CodeNavigation|No references found"
msgstr ""
msgid "CodeOwner|Pattern" msgid "CodeOwner|Pattern"
msgstr "" msgstr ""
@ -7380,13 +7383,28 @@ msgstr ""
msgid "Dashboard|Unable to add %{invalidProjects}. This dashboard is available for public projects, and private projects in groups with a Silver plan." msgid "Dashboard|Unable to add %{invalidProjects}. This dashboard is available for public projects, and private projects in groups with a Silver plan."
msgstr "" msgstr ""
msgid "DastProfiles|Could not create the site profile. Please try again."
msgstr ""
msgid "DastProfiles|Do you want to discard this site profile?"
msgstr ""
msgid "DastProfiles|Manage profiles" msgid "DastProfiles|Manage profiles"
msgstr "" msgstr ""
msgid "DastProfiles|New Site Profile" msgid "DastProfiles|New site profile"
msgstr "" msgstr ""
msgid "DastProfiles|New site profile" msgid "DastProfiles|Please enter a valid URL format, ex: http://www.example.com/home"
msgstr ""
msgid "DastProfiles|Profile name"
msgstr ""
msgid "DastProfiles|Save profile"
msgstr ""
msgid "DastProfiles|Target URL"
msgstr "" msgstr ""
msgid "Data is still calculating..." msgid "Data is still calculating..."
@ -7509,6 +7527,9 @@ msgstr ""
msgid "Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here." msgid "Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here."
msgstr "" msgstr ""
msgid "Definition"
msgstr ""
msgid "Delayed Project Deletion (%{adjourned_deletion})" msgid "Delayed Project Deletion (%{adjourned_deletion})"
msgstr "" msgstr ""
@ -8164,6 +8185,9 @@ msgstr ""
msgid "Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them." msgid "Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them."
msgstr "" msgstr ""
msgid "Discard"
msgstr ""
msgid "Discard all changes" msgid "Discard all changes"
msgstr "" msgstr ""
@ -19179,6 +19203,9 @@ msgstr ""
msgid "Reference:" msgid "Reference:"
msgstr "" msgstr ""
msgid "References"
msgstr ""
msgid "Refresh" msgid "Refresh"
msgstr "" msgstr ""

View file

@ -291,6 +291,17 @@ function base_config_changed() {
curl "${CI_API_V4_URL}/projects/${CI_MERGE_REQUEST_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/changes" | jq '.changes | any(.old_path == "scripts/review_apps/base-config.yaml")' curl "${CI_API_V4_URL}/projects/${CI_MERGE_REQUEST_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/changes" | jq '.changes | any(.old_path == "scripts/review_apps/base-config.yaml")'
} }
function parse_gitaly_image_tag() {
local gitaly_version="${GITALY_VERSION}"
# returns sha if gitaly_version uses a sha
if [[ ${#gitaly_version} -eq 40 ]]; then
echo "${gitaly_version}"
else
echo "v${gitaly_version}"
fi
}
function deploy() { function deploy() {
local namespace="${KUBE_NAMESPACE}" local namespace="${KUBE_NAMESPACE}"
local release="${CI_ENVIRONMENT_SLUG}" local release="${CI_ENVIRONMENT_SLUG}"
@ -306,6 +317,7 @@ function deploy() {
gitlab_webservice_image_repository="${IMAGE_REPOSITORY}/gitlab-webservice-ee" gitlab_webservice_image_repository="${IMAGE_REPOSITORY}/gitlab-webservice-ee"
gitlab_task_runner_image_repository="${IMAGE_REPOSITORY}/gitlab-task-runner-ee" gitlab_task_runner_image_repository="${IMAGE_REPOSITORY}/gitlab-task-runner-ee"
gitlab_gitaly_image_repository="${IMAGE_REPOSITORY}/gitaly" gitlab_gitaly_image_repository="${IMAGE_REPOSITORY}/gitaly"
gitaly_image_tag=$(parse_gitaly_image_tag)
gitlab_shell_image_repository="${IMAGE_REPOSITORY}/gitlab-shell" gitlab_shell_image_repository="${IMAGE_REPOSITORY}/gitlab-shell"
gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-ee" gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-ee"
@ -327,7 +339,7 @@ HELM_CMD=$(cat << EOF
--set gitlab.migrations.image.repository="${gitlab_migrations_image_repository}" \ --set gitlab.migrations.image.repository="${gitlab_migrations_image_repository}" \
--set gitlab.migrations.image.tag="${CI_COMMIT_REF_SLUG}" \ --set gitlab.migrations.image.tag="${CI_COMMIT_REF_SLUG}" \
--set gitlab.gitaly.image.repository="${gitlab_gitaly_image_repository}" \ --set gitlab.gitaly.image.repository="${gitlab_gitaly_image_repository}" \
--set gitlab.gitaly.image.tag="v${GITALY_VERSION}" \ --set gitlab.gitaly.image.tag="${gitaly_image_tag}" \
--set gitlab.gitlab-shell.image.repository="${gitlab_shell_image_repository}" \ --set gitlab.gitlab-shell.image.repository="${gitlab_shell_image_repository}" \
--set gitlab.gitlab-shell.image.tag="v${GITLAB_SHELL_VERSION}" \ --set gitlab.gitlab-shell.image.tag="v${GITLAB_SHELL_VERSION}" \
--set gitlab.sidekiq.annotations.commit="${CI_COMMIT_SHORT_SHA}" \ --set gitlab.sidekiq.annotations.commit="${CI_COMMIT_SHORT_SHA}" \

View file

@ -10,57 +10,81 @@ exports[`Code navigation popover component renders popover 1`] = `
style="left: 0px;" style="left: 0px;"
/> />
<div <gl-tabs-stub
class="overflow-auto code-navigation-popover-container" contentclass="gl-py-0"
nav-class="gl-hidden"
theme="indigo"
> >
<div <gl-tab-stub
class="" title="Definition"
> >
<pre <div
class="border-0 bg-transparent m-0 code highlight text-wrap" class="overflow-auto code-navigation-popover-container"
> >
<span <div
class="line" class=""
lang="javascript"
> >
<span <pre
class="k" class="border-0 bg-transparent m-0 code highlight text-wrap"
> >
function <span
</span> class="line"
<span> lang="javascript"
main() { >
</span> <span
</span> class="k"
<span >
class="line" function
lang="javascript" </span>
<span>
main() {
</span>
</span>
<span
class="line"
lang="javascript"
>
<span>
}
</span>
</span>
</pre>
</div>
</div>
<div
class="popover-body border-top"
>
<gl-button-stub
category="tertiary"
class="w-100"
data-testid="go-to-definition-btn"
href="http://gitlab.com/test.js"
icon=""
size="medium"
target="_blank"
variant="default"
> >
<span>
} Go to definition
</span>
</span> </gl-button-stub>
</pre> </div>
</div> </gl-tab-stub>
</div>
<gl-tab-stub
<div class="py-2"
class="popover-body border-top" data-testid="references-tab"
>
<gl-button-stub
category="tertiary"
class="w-100"
data-testid="go-to-definition-btn"
href="http://gitlab.com/test.js"
icon=""
size="medium"
target="_blank"
variant="default"
> >
<p
class="gl-my-4 gl-px-4"
>
No references found
Go to definition </p>
</gl-tab-stub>
</gl-button-stub> </gl-tabs-stub>
</div>
</div> </div>
`; `;

View file

@ -40,6 +40,17 @@ const MOCK_DOCS_DATA = Object.freeze({
definition_path: 'test.js#L20', definition_path: 'test.js#L20',
}); });
const MOCK_DATA_WITH_REFERENCES = Object.freeze({
hover: [
{
language: null,
value: 'console.log',
},
],
references: [{ path: 'index.js' }, { path: 'app.js' }],
definition_path: 'test.js#L20',
});
let wrapper; let wrapper;
function factory({ position, data, definitionPathPrefix, blobPath = 'index.js' }) { function factory({ position, data, definitionPathPrefix, blobPath = 'index.js' }) {
@ -64,6 +75,16 @@ describe('Code navigation popover component', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
it('srender references tab with empty text when no references exist', () => {
factory({
position: { x: 0, y: 0, height: 0 },
data: MOCK_CODE_DATA,
definitionPathPrefix: DEFINITION_PATH_PREFIX,
});
expect(wrapper.find('[data-testid="references-tab"]').text()).toContain('No references found');
});
it('renders link with hash to current file', () => { it('renders link with hash to current file', () => {
factory({ factory({
position: { x: 0, y: 0, height: 0 }, position: { x: 0, y: 0, height: 0 },
@ -75,6 +96,17 @@ describe('Code navigation popover component', () => {
expect(wrapper.find('[data-testid="go-to-definition-btn"]').attributes('href')).toBe('#L20'); expect(wrapper.find('[data-testid="go-to-definition-btn"]').attributes('href')).toBe('#L20');
}); });
it('renders list of references', () => {
factory({
position: { x: 0, y: 0, height: 0 },
data: MOCK_DATA_WITH_REFERENCES,
definitionPathPrefix: DEFINITION_PATH_PREFIX,
});
expect(wrapper.find('[data-testid="references-tab"]').exists()).toBe(true);
expect(wrapper.findAll('[data-testid="reference-link"]').length).toBe(2);
});
describe('code output', () => { describe('code output', () => {
it('renders code output', () => { it('renders code output', () => {
factory({ factory({

View file

@ -88,17 +88,17 @@ describe('IntegrationForm', () => {
expect(findJiraTriggerFields().exists()).toBe(true); expect(findJiraTriggerFields().exists()).toBe(true);
}); });
describe('featureFlag jiraIntegration is false', () => { describe('featureFlag jiraIssuesIntegration is false', () => {
it('does not render JiraIssuesFields', () => { it('does not render JiraIssuesFields', () => {
createComponent({ type: 'jira' }, { jiraIntegration: false }); createComponent({ type: 'jira' }, { jiraIssuesIntegration: false });
expect(findJiraIssuesFields().exists()).toBe(false); expect(findJiraIssuesFields().exists()).toBe(false);
}); });
}); });
describe('featureFlag jiraIntegration is true', () => { describe('featureFlag jiraIssuesIntegration is true', () => {
it('renders JiraIssuesFields', () => { it('renders JiraIssuesFields', () => {
createComponent({ type: 'jira' }, { jiraIntegration: true }); createComponent({ type: 'jira' }, { jiraIssuesIntegration: true });
expect(findJiraIssuesFields().exists()).toBe(true); expect(findJiraIssuesFields().exists()).toBe(true);
}); });

View file

@ -595,6 +595,14 @@ describe('URL utility', () => {
); );
}); });
it('handles arrays properly when railsArraySyntax=true', () => {
const url = 'https://gitlab.com/test';
expect(urlUtils.setUrlParams({ labels: ['foo', 'bar'] }, url, false, true)).toEqual(
'https://gitlab.com/test?labels%5B%5D=foo&labels%5B%5D=bar',
);
});
it('removes all existing URL params and sets a new param when cleanParams=true', () => { it('removes all existing URL params and sets a new param when cleanParams=true', () => {
const url = 'https://gitlab.com/test?group_id=gitlab-org&project_id=my-project'; const url = 'https://gitlab.com/test?group_id=gitlab-org&project_id=my-project';

View file

@ -0,0 +1,126 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Project remove modal initialized matches the snapshot 1`] = `
<form
action="some/path"
method="post"
>
<input
name="_method"
type="hidden"
value="delete"
/>
<input
name="authenticity_token"
type="hidden"
/>
<b-button-stub
class="[object Object]"
event="click"
role="button"
routertag="a"
size="md"
tabindex="0"
tag="button"
type="button"
variant="danger"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Remove project
</span>
</b-button-stub>
<b-modal-stub
canceltitle="Cancel"
cancelvariant="secondary"
footerclass="bg-gray-light gl-p-5"
headerclosecontent="&times;"
headercloselabel="Close"
id="remove-project-modal"
ignoreenforcefocusselector=""
lazy="true"
modalclass="gl-modal,"
oktitle="OK"
okvariant="danger"
size="sm"
title=""
titletag="h4"
>
<div>
<p
class="gl-text-red-500 gl-font-weight-bold"
>
This can lead to data loss.
</p>
<p
class="gl-mb-0"
>
This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.
</p>
<p>
<gl-sprintf-stub
message="Please type %{phrase_code} to proceed or close this modal to cancel."
/>
</p>
<gl-form-input-stub
id="confirm_name_input"
name="confirm_name_input"
type="text"
/>
</div>
<template />
<template>
Confirmation required
</template>
<template />
<template />
<template />
<template>
<div
class="gl-w-full gl-display-flex gl-just-content-start gl-m-0"
>
<b-button-stub
class="[object Object]"
disabled="true"
event="click"
routertag="a"
size="md"
tag="button"
type="button"
variant="danger"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Confirm
</span>
</b-button-stub>
</div>
</template>
</b-modal-stub>
</form>
`;

View file

@ -0,0 +1,62 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlModal } from '@gitlab/ui';
import ProjectRemoveModal from '~/projects/components/remove_modal.vue';
describe('Project remove modal', () => {
let wrapper;
const findFormElement = () => wrapper.find('form').element;
const findConfirmButton = () => wrapper.find(GlModal).find(GlButton);
const defaultProps = {
formPath: 'some/path',
confirmPhrase: 'foo',
warningMessage: 'This can lead to data loss.',
};
const createComponent = (data = {}) => {
wrapper = shallowMount(ProjectRemoveModal, {
propsData: defaultProps,
data: () => data,
stubs: {
GlButton,
GlModal,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('initialized', () => {
beforeEach(() => {
createComponent();
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
describe('user input matches the confirmPhrase', () => {
beforeEach(() => {
createComponent({ userInput: defaultProps.confirmPhrase });
});
it('the confirm button is not dislabled', () => {
expect(findConfirmButton().attributes('disabled')).toBe(undefined);
});
describe('and when the confirmation button is clicked', () => {
beforeEach(() => {
findConfirmButton().vm.$emit('click');
});
it('submits the form element', () => {
expect(findFormElement().submit).toHaveBeenCalled();
});
});
});
});

View file

@ -2,8 +2,9 @@ import {
buildUneditableOpenTokens, buildUneditableOpenTokens,
buildUneditableCloseToken, buildUneditableCloseToken,
buildUneditableCloseTokens, buildUneditableCloseTokens,
buildUneditableInlineTokens,
buildUneditableTokens, buildUneditableTokens,
buildUneditableInlineTokens,
buildUneditableHtmlAsTextTokens,
} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
import { import {
@ -12,6 +13,7 @@ import {
uneditableOpenTokens, uneditableOpenTokens,
uneditableCloseToken, uneditableCloseToken,
uneditableCloseTokens, uneditableCloseTokens,
uneditableBlockTokens,
uneditableInlineTokens, uneditableInlineTokens,
uneditableTokens, uneditableTokens,
} from './mock_data'; } from './mock_data';
@ -41,6 +43,15 @@ describe('Build Uneditable Token renderer helper', () => {
}); });
}); });
describe('buildUneditableTokens', () => {
it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => {
const result = buildUneditableTokens(originToken);
expect(result).toHaveLength(3);
expect(result).toStrictEqual(uneditableTokens);
});
});
describe('buildUneditableInlineTokens', () => { describe('buildUneditableInlineTokens', () => {
it('returns a 3-item array of tokens with the originInlineToken wrapped in the middle of inline tokens', () => { it('returns a 3-item array of tokens with the originInlineToken wrapped in the middle of inline tokens', () => {
const result = buildUneditableInlineTokens(originInlineToken); const result = buildUneditableInlineTokens(originInlineToken);
@ -50,12 +61,20 @@ describe('Build Uneditable Token renderer helper', () => {
}); });
}); });
describe('buildUneditableTokens', () => { describe('buildUneditableHtmlAsTextTokens', () => {
it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => { it('returns a 3-item array of tokens with the htmlBlockNode wrapped as a text token in the middle of block tokens', () => {
const result = buildUneditableTokens(originToken); const htmlBlockNode = {
type: 'htmlBlock',
literal: '<div data-tomark-pass ><h1>Some header</h1><p>Some paragraph</p></div>',
};
const result = buildUneditableHtmlAsTextTokens(htmlBlockNode);
const { type, content } = result[1];
expect(type).toBe('text');
expect(content).not.toMatch(/ data-tomark-pass /);
expect(result).toHaveLength(3); expect(result).toHaveLength(3);
expect(result).toStrictEqual(uneditableTokens); expect(result).toStrictEqual(uneditableBlockTokens);
}); });
}); });
}); });

View file

@ -12,7 +12,7 @@ export const normalTextNode = buildMockTextNode('This is just normal text.');
// Token spec helpers // Token spec helpers
const buildUneditableOpenToken = type => { const buildMockUneditableOpenToken = type => {
return { return {
type: 'openTag', type: 'openTag',
tagName: type, tagName: type,
@ -23,7 +23,7 @@ const buildUneditableOpenToken = type => {
}; };
}; };
const buildUneditableCloseToken = type => { const buildMockUneditableCloseToken = type => {
return { type: 'closeTag', tagName: type }; return { type: 'closeTag', tagName: type };
}; };
@ -31,8 +31,8 @@ export const originToken = {
type: 'text', type: 'text',
content: '{:.no_toc .hidden-md .hidden-lg}', content: '{:.no_toc .hidden-md .hidden-lg}',
}; };
export const uneditableCloseToken = buildUneditableCloseToken('div'); export const uneditableCloseToken = buildMockUneditableCloseToken('div');
export const uneditableOpenTokens = [buildUneditableOpenToken('div'), originToken]; export const uneditableOpenTokens = [buildMockUneditableOpenToken('div'), originToken];
export const uneditableCloseTokens = [originToken, uneditableCloseToken]; export const uneditableCloseTokens = [originToken, uneditableCloseToken];
export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken]; export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken];
@ -41,7 +41,17 @@ export const originInlineToken = {
content: '<i>Inline</i> content', content: '<i>Inline</i> content',
}; };
export const uneditableInlineTokens = [ export const uneditableInlineTokens = [
buildUneditableOpenToken('span'), buildMockUneditableOpenToken('a'),
originInlineToken, originInlineToken,
buildUneditableCloseToken('span'), buildMockUneditableCloseToken('a'),
];
export const uneditableBlockTokens = [
buildMockUneditableOpenToken('div'),
{
type: 'text',
tagName: null,
content: '<div><h1>Some header</h1><p>Some paragraph</p></div>',
},
buildMockUneditableCloseToken('div'),
]; ];

View file

@ -0,0 +1,38 @@
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html_block';
import { buildUneditableHtmlAsTextTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
import { normalTextNode } from './mock_data';
const htmlBlockNode = {
firstChild: null,
literal: '<div><h1>Heading</h1><p>Paragraph.</p></div>',
type: 'htmlBlock',
};
describe('Render HTML renderer', () => {
describe('canRender', () => {
it('should return true when the argument is an html block', () => {
expect(renderer.canRender(htmlBlockNode)).toBe(true);
});
it('should return false when the argument is not an html block', () => {
expect(renderer.canRender(normalTextNode)).toBe(false);
});
});
describe('render', () => {
const htmlBlockNodeToMark = {
firstChild: null,
literal: '<div data-to-mark ></div>',
type: 'htmlBlock',
};
it.each`
node
${htmlBlockNode}
${htmlBlockNodeToMark}
`('should return uneditable tokens wrapping the $node as a token', ({ node }) => {
expect(renderer.render(node)).toStrictEqual(buildUneditableHtmlAsTextTokens(node));
});
});
});

View file

@ -1,34 +0,0 @@
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html';
import { buildUneditableTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
import { normalTextNode } from './mock_data';
const htmlLiteral = '<div><h1>Heading</h1><p>Paragraph.</p></div>';
const htmlBlockNode = {
firstChild: null,
literal: htmlLiteral,
type: 'htmlBlock',
};
describe('Render HTML renderer', () => {
describe('canRender', () => {
it('should return true when the argument is an html block', () => {
expect(renderer.canRender(htmlBlockNode)).toBe(true);
});
it('should return false when the argument is not an html block', () => {
expect(renderer.canRender(normalTextNode)).toBe(false);
});
});
describe('render', () => {
it('should return uneditable tokens wrapping the origin token', () => {
const origin = jest.fn();
const context = { origin };
expect(renderer.render(htmlBlockNode, context)).toStrictEqual(
buildUneditableTokens(origin()),
);
});
});
});

View file

@ -0,0 +1,82 @@
# frozen_string_literal: true
require 'spec_helper'
require 'haml_lint'
require 'haml_lint/spec'
require Rails.root.join('haml_lint/linter/documentation_links')
RSpec.describe HamlLint::Linter::DocumentationLinks do
include_context 'linter'
context 'when link_to points to the existing file path' do
let(:haml) { "= link_to 'Description', help_page_path('README.md')" }
it { is_expected.not_to report_lint }
end
context 'when link_to points to the existing file with valid anchor' do
let(:haml) { "= link_to 'Description', help_page_path('README.md', anchor: 'overview'), target: '_blank'" }
it { is_expected.not_to report_lint }
end
context 'when link_to points to the existing file path without .md extension' do
let(:haml) { "= link_to 'Description', help_page_path('README')" }
it { is_expected.not_to report_lint }
end
context 'when anchor is not correct' do
let(:haml) { "= link_to 'Description', help_page_path('README.md', anchor: 'wrong')" }
it { is_expected.to report_lint }
context 'when help_page_path has multiple options' do
let(:haml) { "= link_to 'Description', help_page_path('README.md', key: :value, anchor: 'wrong')" }
it { is_expected.to report_lint }
end
end
context 'when file path is wrong' do
let(:haml) { "= link_to 'Description', help_page_path('wrong.md'), target: '_blank'" }
it { is_expected.to report_lint }
end
context 'when link with wrong file path is assigned to a variable' do
let(:haml) { "- my_link = link_to 'Description', help_page_path('wrong.md')" }
it { is_expected.to report_lint }
end
context 'when it is a broken code' do
let(:haml) { "= I am broken! ]]]]" }
it { is_expected.not_to report_lint }
end
context 'when anchor belongs to a different element' do
let(:haml) { "= link_to 'Description', help_page_path('README.md'), target: (anchor: 'blank')" }
it { is_expected.not_to report_lint }
end
context 'when a simple help_page_path' do
let(:haml) { "- url = help_page_path('wrong.md')" }
it { is_expected.to report_lint }
end
context 'when link is not a string' do
let(:haml) { "- url = help_page_path(help_url)" }
it { is_expected.not_to report_lint }
end
context 'when link is a part of the tag' do
let(:haml) { ".data-form{ data: { url: help_page_path('wrong.md') } }" }
it { is_expected.to report_lint }
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::MergeRequestApprovals do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
subject { described_class.new(merge_request, current_user: user).as_json }
before do
merge_request.project.add_developer(user)
end
it 'serializes an approved merge request' do
create(:approval, merge_request: merge_request, user: user)
is_expected.to eq({
user_has_approved: true,
user_can_approve: false,
approved: true,
approved_by: [{
user: API::Entities::UserBasic.new(user).as_json
}]
})
end
it 'serializes a merge request that is not approved' do
is_expected.to eq({
user_has_approved: false,
user_can_approve: true,
approved: false,
approved_by: []
})
end
end

View file

@ -1,13 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'fast_spec_helper' require 'fast_spec_helper'
require 'rspec-parameterized'
require_relative 'danger_spec_helper' require_relative 'danger_spec_helper'
require 'gitlab/danger/changelog' require 'gitlab/danger/changelog'
RSpec.describe Gitlab::Danger::Changelog do RSpec.describe Gitlab::Danger::Changelog do
using RSpec::Parameterized::TableSyntax
include DangerSpecHelper include DangerSpecHelper
let(:added_files) { nil } let(:added_files) { nil }
@ -26,34 +24,36 @@ RSpec.describe Gitlab::Danger::Changelog do
subject(:changelog) { fake_danger.new(git: fake_git, gitlab: fake_gitlab, helper: fake_helper) } subject(:changelog) { fake_danger.new(git: fake_git, gitlab: fake_gitlab, helper: fake_helper) }
describe '#needed?' do describe '#needed?' do
let(:category_with_changelog) { :backend }
let(:label_with_changelog) { 'frontend' }
let(:category_without_changelog) { Gitlab::Danger::Changelog::NO_CHANGELOG_CATEGORIES.first }
let(:label_without_changelog) { Gitlab::Danger::Changelog::NO_CHANGELOG_LABELS.first }
subject { changelog.needed? } subject { changelog.needed? }
where(:categories, :labels) do context 'when MR contains only categories requiring no changelog' do
{ backend: nil } | %w[backend backstage] let(:changes_by_category) { { category_without_changelog => nil } }
{ frontend: nil, docs: nil } | ['ci-build'] let(:mr_labels) { [] }
{ engineering_productivity: nil, none: nil } | ['meta']
end
with_them do it 'is falsey' do
let(:changes_by_category) { categories }
let(:mr_labels) { labels }
it "is falsy when categories and labels require no changelog" do
is_expected.to be_falsy is_expected.to be_falsy
end end
end end
where(:categories, :labels) do context 'when MR contains a label that require no changelog' do
{ frontend: nil, docs: nil } | ['database::review pending', 'feature'] let(:changes_by_category) { { category_with_changelog => nil } }
{ backend: nil } | ['backend', 'technical debt'] let(:mr_labels) { [label_with_changelog, label_without_changelog] }
{ engineering_productivity: nil, none: nil } | ['frontend']
it 'is falsey' do
is_expected.to be_falsy
end
end end
with_them do context 'when MR contains a category that require changelog and a category that require no changelog' do
let(:changes_by_category) { categories } let(:changes_by_category) { { category_with_changelog => nil, category_without_changelog => nil } }
let(:mr_labels) { labels } let(:mr_labels) { [] }
it "is truthy when categories and labels require a changelog" do it 'is truthy' do
is_expected.to be_truthy is_expected.to be_truthy
end end
end end

View file

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Utils::Markdown do
let(:klass) do
Class.new do
include Gitlab::Utils::Markdown
end
end
subject(:object) { klass.new }
describe '#string_to_anchor' do
subject { object.string_to_anchor(string) }
let(:string) { 'My Header' }
it 'converts string to anchor' do
is_expected.to eq 'my-header'
end
context 'when string has punctuation' do
let(:string) { 'My, Header!' }
it 'removes punctuation' do
is_expected.to eq 'my-header'
end
end
context 'when string starts and ends with spaces' do
let(:string) { ' My Header ' }
it 'removes extra spaces' do
is_expected.to eq 'my-header'
end
end
context 'when string has multiple spaces and dashes in the middle' do
let(:string) { 'My - - - Header' }
it 'removes consecutive dashes' do
is_expected.to eq 'my-header'
end
end
context 'when string contains only digits' do
let(:string) { '123' }
it 'adds anchor prefix' do
is_expected.to eq 'anchor-123'
end
end
context 'when string is empty' do
let(:string) { '' }
it 'returns an empty string' do
is_expected.to eq ''
end
end
end
end

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ApprovableBase do
describe '#has_approved?' do
let(:merge_request) { create(:merge_request) }
let(:user) { create(:user) }
subject { merge_request.has_approved?(user) }
context 'when a user has not approved' do
it 'returns false' do
is_expected.to be_falsy
end
end
context 'when a user has approved' do
let!(:approval) { create(:approval, merge_request: merge_request, user: user) }
it 'returns false' do
is_expected.to be_truthy
end
end
context 'when a user is nil' do
let(:user) { nil }
it 'returns false' do
is_expected.to be_falsy
end
end
end
end

View file

@ -613,4 +613,22 @@ RSpec.describe MergeRequestPresenter do
end end
end end
end end
describe '#api_approvals_path' do
subject { described_class.new(resource, current_user: user).api_approvals_path }
it { is_expected.to eq(expose_path("/api/v4/projects/#{project.id}/merge_requests/#{resource.iid}/approvals")) }
end
describe '#api_approve_path' do
subject { described_class.new(resource, current_user: user).api_approve_path }
it { is_expected.to eq(expose_path("/api/v4/projects/#{project.id}/merge_requests/#{resource.iid}/approve")) }
end
describe '#api_unapprove_path' do
subject { described_class.new(resource, current_user: user).api_unapprove_path }
it { is_expected.to eq(expose_path("/api/v4/projects/#{project.id}/merge_requests/#{resource.iid}/unapprove")) }
end
end end

View file

@ -1823,13 +1823,36 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['ProcessLsif']).to be_truthy expect(json_response['ProcessLsif']).to be_truthy
end end
context 'code_navigation feature flag is disabled' do it 'adds ProcessLsifReferences header' do
it 'does not add ProcessLsif header' do authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
stub_feature_flags(code_navigation: false)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['ProcessLsifReferences']).to be_truthy
end
context 'code_navigation feature flag is disabled' do
it 'responds with a forbidden error' do
stub_feature_flags(code_navigation: false)
authorize_artifacts_with_token_in_headers(artifact_type: :lsif) authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
expect(response).to have_gitlab_http_status(:forbidden) aggregate_failures do
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['ProcessLsif']).to be_falsy
expect(json_response['ProcessLsifReferences']).to be_falsy
end
end
end
context 'code_navigation_references feature flag is disabled' do
it 'sets ProcessLsifReferences header to false' do
stub_feature_flags(code_navigation_references: false)
authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['ProcessLsif']).to be_truthy
expect(json_response['ProcessLsifReferences']).to be_falsy
end
end end
end end
end end