Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e69e3f1eb6
commit
da1962d9ac
73 changed files with 1477 additions and 421 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
108
app/assets/javascripts/projects/components/remove_modal.vue
Normal file
108
app/assets/javascripts/projects/components/remove_modal.vue
Normal 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>
|
24
app/assets/javascripts/projects/project_remove_modal.js
Normal file
24
app/assets/javascripts/projects/project_remove_modal.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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()];
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 };
|
|
|
@ -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 };
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
16
app/models/concerns/approvable_base.rb
Normal file
16
app/models/concerns/approvable_base.rb
Normal 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
|
|
@ -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,
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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) }
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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}" } }
|
||||||
|
|
|
@ -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 } }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
5
changelogs/unreleased/36330-v2-custom-renderer-html.yml
Normal file
5
changelogs/unreleased/36330-v2-custom-renderer-html.yml
Normal 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
|
5
changelogs/unreleased/id-expose-approvals-endpoints.yml
Normal file
5
changelogs/unreleased/id-expose-approvals-endpoints.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Expose approvals fields for FOSS FE
|
||||||
|
merge_request: 36564
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>))
|
||||||
```
|
```
|
||||||
|
|
|
@ -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**
|
||||||
|
|
177
doc/operations/metrics/dashboards/yaml_number_format.md
Normal file
177
doc/operations/metrics/dashboards/yaml_number_format.md
Normal 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 |
|
|
@ -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')
|
||||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 |
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)**
|
||||||
|
|
||||||
|
|
100
haml_lint/linter/documentation_links.rb
Normal file
100
haml_lint/linter/documentation_links.rb
Normal 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
|
9
lib/api/entities/approvals.rb
Normal file
9
lib/api/entities/approvals.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
19
lib/gitlab/utils/markdown.rb
Normal file
19
lib/gitlab/utils/markdown.rb
Normal 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
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
@ -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}" \
|
||||||
|
|
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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="×"
|
||||||
|
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>
|
||||||
|
`;
|
62
spec/frontend/projects/components/remove_modal_spec.js
Normal file
62
spec/frontend/projects/components/remove_modal_spec.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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'),
|
||||||
];
|
];
|
||||||
|
|
|
@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
82
spec/haml_lint/linter/documentation_links_spec.rb
Normal file
82
spec/haml_lint/linter/documentation_links_spec.rb
Normal 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
|
36
spec/lib/api/entities/merge_request_approvals_spec.rb
Normal file
36
spec/lib/api/entities/merge_request_approvals_spec.rb
Normal 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
|
|
@ -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
|
||||||
|
|
63
spec/lib/gitlab/utils/markdown_spec.rb
Normal file
63
spec/lib/gitlab/utils/markdown_spec.rb
Normal 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
|
34
spec/models/concerns/approvable_base_spec.rb
Normal file
34
spec/models/concerns/approvable_base_spec.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue