Skip to content

Commit a8281ac

Browse files
author
GitLab Bot
committed
Add latest changes from gitlab-org/gitlab@master
1 parent 22391da commit a8281ac

File tree

56 files changed

+642
-223
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+642
-223
lines changed

.gitlab/ci/rails.gitlab-ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ db:backup_and_restore:
463463
script:
464464
- . scripts/prepare_build.sh
465465
- bundle exec rake db:drop db:create db:structure:load db:seed_fu
466-
- mkdir -p tmp/tests/public/uploads tmp/tests/{artifacts,pages,lfs-objects,terraform_state,registry}
466+
- mkdir -p tmp/tests/public/uploads tmp/tests/{artifacts,pages,lfs-objects,terraform_state,registry,packages}
467467
- bundle exec rake gitlab:backup:create
468468
- date
469469
- bundle exec rake gitlab:backup:restore

app/assets/javascripts/behaviors/markdown/render_gfm.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import initUserPopovers from '../../user_popovers';
44
import highlightCurrentUser from './highlight_current_user';
55
import renderMath from './render_math';
66
import renderMermaid from './render_mermaid';
7+
import renderSandboxedMermaid from './render_sandboxed_mermaid';
78
import renderMetrics from './render_metrics';
89

910
// Render GitLab flavoured Markdown
@@ -13,7 +14,11 @@ import renderMetrics from './render_metrics';
1314
$.fn.renderGFM = function renderGFM() {
1415
syntaxHighlight(this.find('.js-syntax-highlight').get());
1516
renderMath(this.find('.js-render-math'));
16-
renderMermaid(this.find('.js-render-mermaid'));
17+
if (gon.features?.sandboxedMermaid) {
18+
renderSandboxedMermaid(this.find('.js-render-mermaid'));
19+
} else {
20+
renderMermaid(this.find('.js-render-mermaid'));
21+
}
1722
highlightCurrentUser(this.find('.gfm-project_member').get());
1823
initUserPopovers(this.find('.js-user-link').get());
1924

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import $ from 'jquery';
2+
import { once, countBy } from 'lodash';
3+
import { __ } from '~/locale';
4+
import {
5+
getBaseURL,
6+
relativePathToAbsolute,
7+
setUrlParams,
8+
joinPaths,
9+
} from '~/lib/utils/url_utility';
10+
import { darkModeEnabled } from '~/lib/utils/color_utils';
11+
import { setAttributes } from '~/lib/utils/dom_utils';
12+
13+
// Renders diagrams and flowcharts from text using Mermaid in any element with the
14+
// `js-render-mermaid` class.
15+
//
16+
// Example markup:
17+
//
18+
// <pre class="js-render-mermaid">
19+
// graph TD;
20+
// A-- > B;
21+
// A-- > C;
22+
// B-- > D;
23+
// C-- > D;
24+
// </pre>
25+
//
26+
27+
const SANDBOX_FRAME_PATH = '/-/sandbox/mermaid';
28+
// This is an arbitrary number; Can be iterated upon when suitable.
29+
const MAX_CHAR_LIMIT = 2000;
30+
// Max # of mermaid blocks that can be rendered in a page.
31+
const MAX_MERMAID_BLOCK_LIMIT = 50;
32+
// Max # of `&` allowed in Chaining of links syntax
33+
const MAX_CHAINING_OF_LINKS_LIMIT = 30;
34+
// Keep a map of mermaid blocks we've already rendered.
35+
const elsProcessingMap = new WeakMap();
36+
let renderedMermaidBlocks = 0;
37+
38+
// Pages without any restrictions on mermaid rendering
39+
const PAGES_WITHOUT_RESTRICTIONS = [
40+
// Group wiki
41+
'groups:wikis:show',
42+
'groups:wikis:edit',
43+
'groups:wikis:create',
44+
45+
// Project wiki
46+
'projects:wikis:show',
47+
'projects:wikis:edit',
48+
'projects:wikis:create',
49+
50+
// Project files
51+
'projects:show',
52+
'projects:blob:show',
53+
];
54+
55+
function shouldLazyLoadMermaidBlock(source) {
56+
/**
57+
* If source contains `&`, which means that it might
58+
* contain Chaining of links a new syntax in Mermaid.
59+
*/
60+
if (countBy(source)['&'] > MAX_CHAINING_OF_LINKS_LIMIT) {
61+
return true;
62+
}
63+
64+
return false;
65+
}
66+
67+
function fixElementSource(el) {
68+
// Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly.
69+
const source = el.textContent?.replace(/<br\s*\/>/g, '<br>');
70+
71+
// Remove any extra spans added by the backend syntax highlighting.
72+
Object.assign(el, { textContent: source });
73+
74+
return { source };
75+
}
76+
77+
function getSandboxFrameSrc() {
78+
const path = joinPaths(gon.relative_url_root || '', SANDBOX_FRAME_PATH);
79+
if (!darkModeEnabled()) {
80+
return path;
81+
}
82+
const absoluteUrl = relativePathToAbsolute(path, getBaseURL());
83+
return setUrlParams({ darkMode: darkModeEnabled() }, absoluteUrl);
84+
}
85+
86+
function renderMermaidEl(el, source) {
87+
const iframeEl = document.createElement('iframe');
88+
setAttributes(iframeEl, {
89+
src: getSandboxFrameSrc(),
90+
sandbox: 'allow-scripts',
91+
frameBorder: 0,
92+
scrolling: 'no',
93+
});
94+
95+
// Add the original source into the DOM
96+
// to allow Copy-as-GFM to access it.
97+
const sourceEl = document.createElement('text');
98+
sourceEl.textContent = source;
99+
sourceEl.classList.add('gl-display-none');
100+
101+
const wrapper = document.createElement('div');
102+
wrapper.appendChild(iframeEl);
103+
wrapper.appendChild(sourceEl);
104+
105+
el.closest('pre').replaceWith(wrapper);
106+
107+
// Event Listeners
108+
iframeEl.addEventListener('load', () => {
109+
// Potential risk associated with '*' discussed in below thread
110+
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74414#note_735183398
111+
iframeEl.contentWindow.postMessage(source, '*');
112+
});
113+
114+
window.addEventListener(
115+
'message',
116+
(event) => {
117+
if (event.origin !== 'null' || event.source !== iframeEl.contentWindow) {
118+
return;
119+
}
120+
const { h, w } = event.data;
121+
iframeEl.width = w;
122+
iframeEl.height = h;
123+
},
124+
false,
125+
);
126+
}
127+
128+
function renderMermaids($els) {
129+
if (!$els.length) return;
130+
131+
const pageName = document.querySelector('body').dataset.page;
132+
133+
// A diagram may have been truncated in search results which will cause errors, so abort the render.
134+
if (pageName === 'search:show') return;
135+
136+
let renderedChars = 0;
137+
138+
$els.each((i, el) => {
139+
// Skipping all the elements which we've already queued in requestIdleCallback
140+
if (elsProcessingMap.has(el)) {
141+
return;
142+
}
143+
144+
const { source } = fixElementSource(el);
145+
/**
146+
* Restrict the rendering to a certain amount of character
147+
* and mermaid blocks to prevent mermaidjs from hanging
148+
* up the entire thread and causing a DoS.
149+
*/
150+
if (
151+
!PAGES_WITHOUT_RESTRICTIONS.includes(pageName) &&
152+
((source && source.length > MAX_CHAR_LIMIT) ||
153+
renderedChars > MAX_CHAR_LIMIT ||
154+
renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||
155+
shouldLazyLoadMermaidBlock(source))
156+
) {
157+
const html = `
158+
<div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert">
159+
<div>
160+
<div>
161+
<div class="js-warning-text"></div>
162+
<div class="gl-alert-actions">
163+
<button type="button" class="js-lazy-render-mermaid btn gl-alert-action btn-warning btn-md gl-button">Display</button>
164+
</div>
165+
</div>
166+
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
167+
<span aria-hidden="true">&times;</span>
168+
</button>
169+
</div>
170+
</div>
171+
`;
172+
173+
const $parent = $(el).parent();
174+
175+
if (!$parent.hasClass('lazy-alert-shown')) {
176+
$parent.after(html);
177+
$parent
178+
.siblings()
179+
.find('.js-warning-text')
180+
.text(
181+
__('Warning: Displaying this diagram might cause performance issues on this page.'),
182+
);
183+
$parent.addClass('lazy-alert-shown');
184+
}
185+
186+
return;
187+
}
188+
189+
renderedChars += source.length;
190+
renderedMermaidBlocks += 1;
191+
192+
const requestId = window.requestIdleCallback(() => {
193+
renderMermaidEl(el, source);
194+
});
195+
196+
elsProcessingMap.set(el, requestId);
197+
});
198+
}
199+
200+
const hookLazyRenderMermaidEvent = once(() => {
201+
$(document.body).on('click', '.js-lazy-render-mermaid', function eventHandler() {
202+
const parent = $(this).closest('.js-lazy-render-mermaid-container');
203+
const pre = parent.prev();
204+
205+
const el = pre.find('.js-render-mermaid');
206+
207+
parent.remove();
208+
209+
// sandbox update
210+
const element = el.get(0);
211+
const { source } = fixElementSource(element);
212+
213+
renderMermaidEl(element, source);
214+
});
215+
});
216+
217+
export default function renderMermaid($els) {
218+
if (!$els.length) return;
219+
220+
const visibleMermaids = $els.filter(function filter() {
221+
return $(this).closest('details').length === 0 && $(this).is(':visible');
222+
});
223+
224+
renderMermaids(visibleMermaids);
225+
226+
$els.closest('details').one('toggle', function toggle() {
227+
if (this.open) {
228+
renderMermaids($(this).find('.js-render-mermaid'));
229+
}
230+
});
231+
232+
hookLazyRenderMermaidEvent();
233+
}

app/assets/javascripts/lib/mermaid.js

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import mermaid from 'mermaid';
2+
import { getParameterByName } from '~/lib/utils/url_utility';
3+
4+
const setIframeRenderedSize = (h, w) => {
5+
const { origin } = window.location;
6+
window.parent.postMessage({ h, w }, origin);
7+
};
8+
9+
const drawDiagram = (source) => {
10+
const element = document.getElementById('app');
11+
const insertSvg = (svgCode) => {
12+
element.innerHTML = svgCode;
13+
14+
const height = parseInt(element.firstElementChild.getAttribute('height'), 10);
15+
const width = parseInt(element.firstElementChild.style.maxWidth, 10);
16+
setIframeRenderedSize(height, width);
17+
};
18+
mermaid.mermaidAPI.render('mermaid', source, insertSvg);
19+
};
20+
21+
const darkModeEnabled = () => getParameterByName('darkMode') === 'true';
22+
23+
const initMermaid = () => {
24+
let theme = 'neutral';
25+
26+
if (darkModeEnabled()) {
27+
theme = 'dark';
28+
}
29+
30+
mermaid.initialize({
31+
// mermaid core options
32+
mermaid: {
33+
startOnLoad: false,
34+
},
35+
// mermaidAPI options
36+
theme,
37+
flowchart: {
38+
useMaxWidth: true,
39+
htmlLabels: true,
40+
},
41+
secure: ['secure', 'securityLevel', 'startOnLoad', 'maxTextSize', 'htmlLabels'],
42+
securityLevel: 'strict',
43+
});
44+
};
45+
46+
const addListener = () => {
47+
window.addEventListener(
48+
'message',
49+
(event) => {
50+
if (event.origin !== window.location.origin) {
51+
return;
52+
}
53+
drawDiagram(event.data);
54+
},
55+
false,
56+
);
57+
};
58+
59+
addListener();
60+
initMermaid();
61+
export default {};

app/assets/javascripts/pipelines/components/header_component.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,9 @@ export default {
212212
</script>
213213
<template>
214214
<div class="js-pipeline-header-container">
215-
<gl-alert v-if="hasError" :variant="failure.variant">{{ failure.text }}</gl-alert>
215+
<gl-alert v-if="hasError" :variant="failure.variant" :dismissible="false">{{
216+
failure.text
217+
}}</gl-alert>
216218
<ci-header
217219
v-if="shouldRenderContent"
218220
:status="pipeline.detailedStatus"

app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue

+6-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script>
2-
import { GlIcon, GlPopover, GlTooltipDirective } from '@gitlab/ui';
2+
import { GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui';
33
import { __, n__, sprintf } from '~/locale';
44
import createFlash from '~/flash';
55
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -10,6 +10,7 @@ import issueCrmContactsSubscription from './queries/issue_crm_contacts.subscript
1010
export default {
1111
components: {
1212
GlIcon,
13+
GlLink,
1314
GlPopover,
1415
},
1516
directives: {
@@ -85,9 +86,6 @@ export default {
8586
);
8687
},
8788
},
88-
i18n: {
89-
help: __('Work in progress- click here to find out more'),
90-
},
9189
};
9290
</script>
9391
@@ -97,11 +95,10 @@ export default {
9795
<gl-icon name="users" />
9896
<span> {{ contactCount }} </span>
9997
</div>
100-
<div
101-
v-gl-tooltip.left.viewport="$options.i18n.help"
102-
class="hide-collapsed help-button float-right"
103-
>
104-
<a href="https://gitlab.com/gitlab-org/gitlab/-/issues/2256"><gl-icon name="question-o" /></a>
98+
<div class="hide-collapsed help-button gl-float-right">
99+
<gl-link href="https://docs.gitlab.com/ee/user/crm/" target="_blank"
100+
><gl-icon name="question-o"
101+
/></gl-link>
105102
</div>
106103
<div class="title hide-collapsed gl-mb-2 gl-line-height-20">
107104
{{ contactsLabel }}

app/assets/stylesheets/utilities.scss

-10
Original file line numberDiff line numberDiff line change
@@ -247,16 +247,6 @@ $gl-line-height-42: px-to-rem(42px);
247247
max-width: 50%;
248248
}
249249

250-
// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465
251-
.gl-popover {
252-
.popover-header {
253-
.gl-button.close {
254-
margin-top: -$gl-spacing-scale-3;
255-
margin-right: -$gl-spacing-scale-4;
256-
}
257-
}
258-
}
259-
260250
// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1490
261251
.gl-w-grid-size-28 {
262252
width: $grid-size * 28;

0 commit comments

Comments
 (0)