summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js146
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue8
-rw-r--r--app/assets/javascripts/boards/models/issue.js4
-rw-r--r--app/assets/javascripts/branches/divergence_graph.js5
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js2
-rw-r--r--app/assets/javascripts/droplab/drop_lab.js4
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue20
-rw-r--r--app/assets/javascripts/flash.js21
-rw-r--r--app/assets/javascripts/groups/components/app.vue7
-rw-r--r--app/assets/javascripts/groups/service/groups_service.js21
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue6
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue2
-rw-r--r--app/assets/javascripts/ide/components/file_row_extra.vue2
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue3
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue4
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue2
-rw-r--r--app/assets/javascripts/issue_show/services/index.js4
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue6
-rw-r--r--app/assets/javascripts/jobs/components/job_log_json.vue10
-rw-r--r--app/assets/javascripts/jobs/store/utils.js40
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue304
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue7
-rw-r--r--app/assets/javascripts/project_find_file.js2
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue4
-rw-r--r--app/assets/stylesheets/framework/flash.scss47
-rw-r--r--app/assets/stylesheets/framework/forms.scss24
-rw-r--r--app/assets/stylesheets/framework/layout.scss9
-rw-r--r--app/assets/stylesheets/framework/typography.scss12
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/search.scss3
34 files changed, 320 insertions, 429 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index a68936d79e2..b5dbdbb7e86 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -1,6 +1,5 @@
-import $ from 'jquery';
-import { __ } from '~/locale';
import flash from '~/flash';
+import { s__, sprintf } from '~/locale';
// Renders math using KaTeX in any element with the
// `js-render-math` class
@@ -10,21 +9,131 @@ import flash from '~/flash';
// <code class="js-render-math"></div>
//
-// Loop over all math elements and render math
-function renderWithKaTeX(elements, katex) {
- elements.each(function katexElementsLoop() {
- const mathNode = $('<span></span>');
- const $this = $(this);
-
- const display = $this.attr('data-math-style') === 'display';
- try {
- katex.render($this.text(), mathNode.get(0), { displayMode: display, throwOnError: false });
- mathNode.insertAfter($this);
- $this.remove();
- } catch (err) {
- throw err;
+const MAX_MATH_CHARS = 1000;
+const MAX_RENDER_TIME_MS = 2000;
+
+// These messages might be used with inline errors in the future. Keep them around. For now, we will
+// display a single error message using flash().
+
+// const CHAR_LIMIT_EXCEEDED_MSG = sprintf(
+// s__(
+// 'math|The following math is too long. For performance reasons, math blocks are limited to %{maxChars} characters. Try splitting up this block, or include an image instead.',
+// ),
+// { maxChars: MAX_MATH_CHARS },
+// );
+// const RENDER_TIME_EXCEEDED_MSG = s__(
+// "math|The math in this entry is taking too long to render. Any math below this point won't be shown. Consider splitting it among multiple entries.",
+// );
+
+const RENDER_FLASH_MSG = sprintf(
+ s__(
+ 'math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead.',
+ ),
+ { maxChars: MAX_MATH_CHARS },
+);
+
+// Wait for the browser to reflow the layout. Reflowing SVG takes time.
+// This has to wrap the inner function, otherwise IE/Edge throw "invalid calling object".
+const waitForReflow = fn => {
+ window.requestAnimationFrame(fn);
+};
+
+/**
+ * Renders math blocks sequentially while protecting against DoS attacks. Math blocks have a maximum character limit of MAX_MATH_CHARS. If rendering math takes longer than MAX_RENDER_TIME_MS, all subsequent math blocks are skipped and an error message is shown.
+ */
+class SafeMathRenderer {
+ /*
+ How this works:
+
+ The performance bottleneck in rendering math is in the browser trying to reflow the generated SVG.
+ During this time, the JS is blocked and the page becomes unresponsive.
+ We want to render math blocks one by one until a certain time is exceeded, after which we stop
+ rendering subsequent math blocks, to protect against DoS. However, browsers do reflowing in an
+ asynchronous task, so we can't time it synchronously.
+
+ SafeMathRenderer essentially does the following:
+ 1. Replaces all math blocks with placeholders so that they're not mistakenly rendered twice.
+ 2. Places each placeholder element in a queue.
+ 3. Renders the element at the head of the queue and waits for reflow.
+ 4. After reflow, gets the elapsed time since step 3 and repeats step 3 until the queue is empty.
+ */
+ queue = [];
+ totalMS = 0;
+
+ constructor(elements, katex) {
+ this.elements = elements;
+ this.katex = katex;
+
+ this.renderElement = this.renderElement.bind(this);
+ this.render = this.render.bind(this);
+ }
+
+ renderElement() {
+ if (!this.queue.length) {
+ return;
}
- });
+
+ const el = this.queue.shift();
+ const text = el.textContent;
+
+ el.removeAttribute('style');
+
+ if (this.totalMS >= MAX_RENDER_TIME_MS || text.length > MAX_MATH_CHARS) {
+ if (!this.flashShown) {
+ flash(RENDER_FLASH_MSG);
+ this.flashShown = true;
+ }
+
+ // Show unrendered math code
+ const codeElement = document.createElement('pre');
+ codeElement.className = 'code';
+ codeElement.textContent = el.textContent;
+ el.parentNode.replaceChild(codeElement, el);
+
+ // Render the next math
+ this.renderElement();
+ } else {
+ this.startTime = Date.now();
+
+ try {
+ el.innerHTML = this.katex.renderToString(text, {
+ displayMode: el.getAttribute('data-math-style') === 'display',
+ throwOnError: true,
+ maxSize: 20,
+ maxExpand: 20,
+ });
+ } catch (e) {
+ // Don't show a flash for now because it would override an existing flash message
+ el.textContent = s__('math|There was an error rendering this math block');
+ // el.style.color = '#d00';
+ el.className = 'katex-error';
+ }
+
+ // Give the browser time to reflow the svg
+ waitForReflow(() => {
+ const deltaTime = Date.now() - this.startTime;
+ this.totalMS += deltaTime;
+
+ this.renderElement();
+ });
+ }
+ }
+
+ render() {
+ // Replace math blocks with a placeholder so they aren't rendered twice
+ this.elements.forEach(el => {
+ const placeholder = document.createElement('span');
+ placeholder.style.display = 'none';
+ placeholder.setAttribute('data-math-style', el.getAttribute('data-math-style'));
+ placeholder.textContent = el.textContent;
+ el.parentNode.replaceChild(placeholder, el);
+ this.queue.push(placeholder);
+ });
+
+ // If we wait for the browser thread to settle down a bit, math rendering becomes 5-10x faster
+ // and less prone to timeouts.
+ setTimeout(this.renderElement, 400);
+ }
}
export default function renderMath($els) {
@@ -34,7 +143,8 @@ export default function renderMath($els) {
import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
])
.then(([katex]) => {
- renderWithKaTeX($els, katex);
+ const renderer = new SafeMathRenderer($els.get(), katex);
+ renderer.render();
})
- .catch(() => flash(__('An error occurred while rendering KaTeX')));
+ .catch(() => {});
}
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
index b84722244d1..71e5d8058da 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
@@ -1,10 +1,10 @@
<script>
-import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
import Flash from '../../../flash';
import { __ } from '../../../locale';
import boardsStore from '../../stores/boards_store';
-export default Vue.extend({
+export default {
props: {
issue: {
type: Object,
@@ -35,7 +35,7 @@ export default Vue.extend({
}
// Post the remove data
- Vue.http.patch(this.updateUrl, data).catch(() => {
+ axios.patch(this.updateUrl, data).catch(() => {
Flash(__('Failed to remove issue from board, please try again.'));
lists.forEach(list => {
@@ -71,7 +71,7 @@ export default Vue.extend({
return req;
},
},
-});
+};
</script>
<template>
<div class="block list">
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 9069b35db9a..086340105b7 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -3,7 +3,7 @@
/* global ListMilestone */
/* global ListAssignee */
-import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
import './label';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import IssueProject from './project';
@@ -133,7 +133,7 @@ class ListIssue {
}
const projectPath = this.project ? this.project.path : '';
- return Vue.http.patch(`${this.path}.json`, data).then(({ body = {} } = {}) => {
+ return axios.patch(`${this.path}.json`, data).then(({ data: body = {} } = {}) => {
/**
* Since post implementation of Scoped labels, server can reject
* same key-ed labels. To keep the UI and server Model consistent,
diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js
index 7dbaf984acf..303735a1807 100644
--- a/app/assets/javascripts/branches/divergence_graph.js
+++ b/app/assets/javascripts/branches/divergence_graph.js
@@ -25,6 +25,11 @@ export default endpoint => {
const names = [...document.querySelectorAll('.js-branch-item')].map(
({ dataset }) => dataset.name,
);
+
+ if (names.length === 0) {
+ return true;
+ }
+
return axios
.get(endpoint, {
params: { names },
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index ada5a49e246..772f16cab4e 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -55,7 +55,7 @@ export default class ClusterStore {
...applicationInitialState,
title: s__('ClusterIntegration|GitLab Runner'),
version: null,
- chartRepo: 'https://gitlab.com/charts/gitlab-runner',
+ chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner',
updateAvailable: null,
updateSuccessful: false,
updateFailed: false,
diff --git a/app/assets/javascripts/droplab/drop_lab.js b/app/assets/javascripts/droplab/drop_lab.js
index 1339e28d8b8..33c05404493 100644
--- a/app/assets/javascripts/droplab/drop_lab.js
+++ b/app/assets/javascripts/droplab/drop_lab.js
@@ -60,7 +60,7 @@ class DropLab {
addEvents() {
this.eventWrapper.documentClicked = this.documentClicked.bind(this);
- document.addEventListener('click', this.eventWrapper.documentClicked);
+ document.addEventListener('mousedown', this.eventWrapper.documentClicked);
}
documentClicked(e) {
@@ -74,7 +74,7 @@ class DropLab {
}
removeEvents() {
- document.removeEventListener('click', this.eventWrapper.documentClicked);
+ document.removeEventListener('mousedown', this.eventWrapper.documentClicked);
}
changeHookList(trigger, list, plugins, config) {
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 95e1e8af9b3..1d4a6e64f9d 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -111,12 +111,7 @@ export default {
* @returns {Boolean|Undefined}
*/
canShowDate() {
- return (
- this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable &&
- this.model.last_deployment.deployable !== undefined
- );
+ return this.model && this.model.last_deployment && this.model.last_deployment.deployed_at;
},
/**
@@ -124,14 +119,9 @@ export default {
*
* @returns {String}
*/
- createdDate() {
- if (
- this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable &&
- this.model.last_deployment.deployable.created_at
- ) {
- return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
+ deployedDate() {
+ if (this.canShowDate) {
+ return timeagoInstance.format(this.model.last_deployment.deployed_at);
}
return '';
},
@@ -547,7 +537,7 @@ export default {
<div v-if="!model.isFolder" class="table-section section-10" role="gridcell">
<div role="rowheader" class="table-mobile-header">{{ s__('Environments|Updated') }}</div>
<span v-if="canShowDate" class="environment-created-date-timeago table-mobile-content">
- {{ createdDate }}
+ {{ deployedDate }}
</span>
</div>
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index c2397842125..660f0f0ba3e 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,4 +1,5 @@
import _ from 'underscore';
+import { spriteIcon } from './lib/utils/common_utils';
const hideFlash = (flashEl, fadeTransition = true) => {
if (fadeTransition) {
@@ -35,16 +36,11 @@ const createAction = config => `
</a>
`;
-const createFlashEl = (message, type, isFixedLayout = false) => `
- <div
- class="flash-${type}"
- >
- <div
- class="flash-text ${
- isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''
- }"
- >
+const createFlashEl = (message, type) => `
+ <div class="flash-content flash-${type} rounded">
+ <div class="flash-text">
${_.escape(message)}
+ ${spriteIcon('close', 'close-icon')}
</div>
</div>
`;
@@ -76,15 +72,10 @@ const createFlash = function createFlash(
addBodyClass = false,
) {
const flashContainer = parent.querySelector('.flash-container');
- const navigation = parent.querySelector('.content');
if (!flashContainer) return null;
- const isFixedLayout = navigation
- ? navigation.parentNode.classList.contains('container-limited')
- : true;
-
- flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout);
+ flashContainer.innerHTML = createFlashEl(message, type);
const flashEl = flashContainer.querySelector(`.flash-${type}`);
removeFlashClickListener(flashEl, fadeTransition);
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index aa50fd8ff62..8d2dac47ff2 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -95,10 +95,8 @@ export default {
if (updatePagination) {
this.updatePagination(res.headers);
}
-
- return res;
+ return res.data;
})
- .then(res => res.json())
.catch(() => {
this.isLoading = false;
$.scrollTo(0);
@@ -190,11 +188,10 @@ export default {
this.targetGroup.isBeingRemoved = true;
this.service
.leaveGroup(this.targetGroup.leavePath)
- .then(res => res.json())
.then(res => {
$.scrollTo(0);
this.store.removeGroup(this.targetGroup, this.targetParentGroup);
- Flash(res.notice, 'notice');
+ Flash(res.data.notice, 'notice');
})
.catch(err => {
let message = COMMON_STR.FAILURE;
diff --git a/app/assets/javascripts/groups/service/groups_service.js b/app/assets/javascripts/groups/service/groups_service.js
index b79ba291463..790b581a7c0 100644
--- a/app/assets/javascripts/groups/service/groups_service.js
+++ b/app/assets/javascripts/groups/service/groups_service.js
@@ -1,40 +1,39 @@
-import Vue from 'vue';
-import '../../vue_shared/vue_resource_interceptor';
+import axios from '~/lib/utils/axios_utils';
export default class GroupsService {
constructor(endpoint) {
- this.groups = Vue.resource(endpoint);
+ this.endpoint = endpoint;
}
getGroups(parentId, page, filterGroups, sort, archived) {
- const data = {};
+ const params = {};
if (parentId) {
- data.parent_id = parentId;
+ params.parent_id = parentId;
} else {
// Do not send the following param for sub groups
if (page) {
- data.page = page;
+ params.page = page;
}
if (filterGroups) {
- data.filter = filterGroups;
+ params.filter = filterGroups;
}
if (sort) {
- data.sort = sort;
+ params.sort = sort;
}
if (archived) {
- data.archived = archived;
+ params.archived = archived;
}
}
- return this.groups.get(data);
+ return axios.get(this.endpoint, { params });
}
// eslint-disable-next-line class-methods-use-this
leaveGroup(endpoint) {
- return Vue.http.delete(endpoint);
+ return axios.delete(endpoint);
}
}
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 8b356ee6e97..549324831e9 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -69,7 +69,11 @@ export default {
:disabled="currentBranch && !currentBranch.can_push"
:title="$options.currentBranchPermissionsTooltip"
>
- <span class="ide-radio-label" v-html="commitToCurrentBranchText"> </span>
+ <span
+ class="ide-radio-label"
+ data-qa-selector="commit_to_current_branch_radio"
+ v-html="commitToCurrentBranchText"
+ ></span>
</radio-group>
<radio-group
:value="$options.commitToNewBranch"
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index 22113692968..500f6737839 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -44,7 +44,7 @@ export default {
<template>
<div class="flash-container flash-container-page" @click="clickFlash">
- <div class="flash-alert">
+ <div class="flash-alert" data-qa-selector="flash_alert">
<span v-html="message.text"> </span>
<button
v-if="message.action"
diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue
index 7254c50a568..48be97c8952 100644
--- a/app/assets/javascripts/ide/components/file_row_extra.vue
+++ b/app/assets/javascripts/ide/components/file_row_extra.vue
@@ -86,7 +86,7 @@ export default {
v-else-if="showChangedFileIcon"
:file="file"
:show-tooltip="true"
- :show-staged-icon="true"
+ :show-staged-icon="false"
/>
<new-dropdown
:type="file.type"
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index 2e6bd85feec..200391282e7 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -89,7 +89,7 @@ export default {
</script>
<template>
- <div class="multi-file-commit-panel ide-right-sidebar">
+ <div class="multi-file-commit-panel ide-right-sidebar" data-qa-selector="ide_right_sidebar">
<resizable-panel
v-show="isOpen"
:collapsible="false"
@@ -120,6 +120,7 @@ export default {
}"
data-container="body"
data-placement="left"
+ :data-qa-selector="`${tab.title.toLowerCase()}_tab_button`"
class="ide-sidebar-link is-right"
type="button"
@click="clickTab($event, tab)"
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index 9ca38d6bbfa..88975c2cc73 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -300,9 +300,9 @@ export default {
this.closeRecaptcha();
},
- deleteIssuable() {
+ deleteIssuable(payload) {
this.service
- .deleteIssuable()
+ .deleteIssuable(payload)
.then(res => res.data)
.then(data => {
// Stop the poll so we don't get 404's with the issuable not existing
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index eb51a074f84..ce867f16acf 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -55,7 +55,7 @@ export default {
if (window.confirm(confirmMessage)) {
this.deleteLoading = true;
- eventHub.$emit('delete.issuable');
+ eventHub.$emit('delete.issuable', { destroy_confirm: true });
}
},
},
diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js
index 9546eb22c27..3c8334bee50 100644
--- a/app/assets/javascripts/issue_show/services/index.js
+++ b/app/assets/javascripts/issue_show/services/index.js
@@ -10,8 +10,8 @@ export default class Service {
return axios.get(this.realtimeEndpoint);
}
- deleteIssuable() {
- return axios.delete(this.endpoint);
+ deleteIssuable(payload) {
+ return axios.delete(this.endpoint, { params: payload });
}
updateIssuable(data) {
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index 8da87f424c4..ad1072366f3 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -12,7 +12,6 @@ import createStore from '../store';
import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue';
-import Log from './job_log.vue';
import LogTopBar from './job_log_controllers.vue';
import StuckBlock from './stuck_block.vue';
import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue';
@@ -30,7 +29,10 @@ export default {
EnvironmentsBlock,
ErasedBlock,
Icon,
- Log,
+ Log: () =>
+ gon && gon.features && gon.features.jobLogJson
+ ? import('./job_log_json.vue')
+ : import('./job_log.vue'),
LogTopBar,
StuckBlock,
UnmetPrerequisitesBlock,
diff --git a/app/assets/javascripts/jobs/components/job_log_json.vue b/app/assets/javascripts/jobs/components/job_log_json.vue
new file mode 100644
index 00000000000..2198b20eb8f
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job_log_json.vue
@@ -0,0 +1,10 @@
+<script>
+export default {
+ name: 'JobLogJSON',
+};
+</script>
+<template>
+ <pre>
+ {{ __('This feature is in development. Please disable the `job_log_json` feature flag') }}
+ </pre>
+</template>
diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js
new file mode 100644
index 00000000000..de7de92ed2e
--- /dev/null
+++ b/app/assets/javascripts/jobs/store/utils.js
@@ -0,0 +1,40 @@
+/**
+ * Parses the job log content into a structure usable by the template
+ *
+ * For collaspible lines (section_header = true):
+ * - creates a new array to hold the lines that are collpasible,
+ * - adds a isClosed property to handle toggle
+ * - adds a isHeader property to handle template logic
+ * For each line:
+ * - adds the index as lineNumber
+ *
+ * @param {Array} lines
+ * @returns {Array}
+ */
+export default (lines = []) =>
+ lines.reduce((acc, line, index) => {
+ if (line.section_header) {
+ acc.push({
+ isClosed: true,
+ isHeader: true,
+ line: {
+ ...line,
+ lineNumber: index,
+ },
+
+ lines: [],
+ });
+ } else if (acc.length && acc[acc.length - 1].isHeader) {
+ acc[acc.length - 1].lines.push({
+ ...line,
+ lineNumber: index,
+ });
+ } else {
+ acc.push({
+ ...line,
+ lineNumber: index,
+ });
+ }
+
+ return acc;
+ }, []);
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
deleted file mode 100644
index cac10474d06..00000000000
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ /dev/null
@@ -1,304 +0,0 @@
-<script>
-import { __ } from '~/locale';
-import { GlLink } from '@gitlab/ui';
-import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
-import dateFormat from 'dateformat';
-import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
-import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
-import Icon from '~/vue_shared/components/icon.vue';
-import { chartHeight, graphTypes, lineTypes } from '../../constants';
-import { makeDataSeries } from '~/helpers/monitor_helper';
-import { graphDataValidatorForValues } from '../../utils';
-
-let debouncedResize;
-
-// TODO: Remove this component in favor of the more general time_series.vue
-// Please port all changes here to time_series.vue as well.
-
-export default {
- components: {
- GlAreaChart,
- GlChartSeriesLabel,
- GlLink,
- Icon,
- },
- inheritAttrs: false,
- props: {
- graphData: {
- type: Object,
- required: true,
- validator: graphDataValidatorForValues.bind(null, false),
- },
- containerWidth: {
- type: Number,
- required: true,
- },
- deploymentData: {
- type: Array,
- required: false,
- default: () => [],
- },
- projectPath: {
- type: String,
- required: false,
- default: () => '',
- },
- showBorder: {
- type: Boolean,
- required: false,
- default: () => false,
- },
- singleEmbed: {
- type: Boolean,
- required: false,
- default: false,
- },
- thresholds: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- data() {
- return {
- tooltip: {
- title: '',
- content: [],
- commitUrl: '',
- isDeployment: false,
- sha: '',
- },
- width: 0,
- height: chartHeight,
- svgs: {},
- primaryColor: null,
- };
- },
- computed: {
- chartData() {
- // Transforms & supplements query data to render appropriate labels & styles
- // Input: [{ queryAttributes1 }, { queryAttributes2 }]
- // Output: [{ seriesAttributes1 }, { seriesAttributes2 }]
- return this.graphData.queries.reduce((acc, query) => {
- const { appearance } = query;
- const lineType =
- appearance && appearance.line && appearance.line.type
- ? appearance.line.type
- : lineTypes.default;
- const lineWidth =
- appearance && appearance.line && appearance.line.width
- ? appearance.line.width
- : undefined;
-
- const series = makeDataSeries(query.result, {
- name: this.formatLegendLabel(query),
- lineStyle: {
- type: lineType,
- width: lineWidth,
- },
- areaStyle: {
- opacity:
- appearance && appearance.area && typeof appearance.area.opacity === 'number'
- ? appearance.area.opacity
- : undefined,
- },
- });
-
- return acc.concat(series);
- }, []);
- },
- chartOptions() {
- return {
- xAxis: {
- name: __('Time'),
- type: 'time',
- axisLabel: {
- formatter: date => dateFormat(date, 'h:MM TT'),
- },
- axisPointer: {
- snap: true,
- },
- },
- yAxis: {
- name: this.yAxisLabel,
- axisLabel: {
- formatter: num => roundOffFloat(num, 3).toString(),
- },
- },
- series: this.scatterSeries,
- dataZoom: [this.dataZoomConfig],
- };
- },
- dataZoomConfig() {
- const handleIcon = this.svgs['scroll-handle'];
-
- return handleIcon ? { handleIcon } : {};
- },
- earliestDatapoint() {
- return this.chartData.reduce((acc, series) => {
- const { data } = series;
- const { length } = data;
- if (!length) {
- return acc;
- }
-
- const [first] = data[0];
- const [last] = data[length - 1];
- const seriesEarliest = first < last ? first : last;
-
- return seriesEarliest < acc || acc === null ? seriesEarliest : acc;
- }, null);
- },
- isMultiSeries() {
- return this.tooltip.content.length > 1;
- },
- recentDeployments() {
- return this.deploymentData.reduce((acc, deployment) => {
- if (deployment.created_at >= this.earliestDatapoint) {
- acc.push({
- id: deployment.id,
- createdAt: deployment.created_at,
- sha: deployment.sha,
- commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
- tag: deployment.tag,
- tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null,
- ref: deployment.ref.name,
- showDeploymentFlag: false,
- });
- }
-
- return acc;
- }, []);
- },
- scatterSeries() {
- return {
- type: graphTypes.deploymentData,
- data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]),
- symbol: this.svgs.rocket,
- symbolSize: 14,
- itemStyle: {
- color: this.primaryColor,
- },
- };
- },
- yAxisLabel() {
- return `${this.graphData.y_label}`;
- },
- },
- watch: {
- containerWidth: 'onResize',
- },
- beforeDestroy() {
- window.removeEventListener('resize', debouncedResize);
- },
- created() {
- debouncedResize = debounceByAnimationFrame(this.onResize);
- window.addEventListener('resize', debouncedResize);
- this.setSvg('rocket');
- this.setSvg('scroll-handle');
- },
- methods: {
- formatLegendLabel(query) {
- return `${query.label}`;
- },
- formatTooltipText(params) {
- this.tooltip.title = dateFormat(params.value, 'dd mmm yyyy, h:MMTT');
- this.tooltip.content = [];
- params.seriesData.forEach(seriesData => {
- this.tooltip.isDeployment = seriesData.componentSubType === graphTypes.deploymentData;
- if (this.tooltip.isDeployment) {
- const [deploy] = this.recentDeployments.filter(
- deployment => deployment.createdAt === seriesData.value[0],
- );
- this.tooltip.sha = deploy.sha.substring(0, 8);
- this.tooltip.commitUrl = deploy.commitUrl;
- } else {
- const { seriesName, color } = seriesData;
- // seriesData.value contains the chart's [x, y] value pair
- // seriesData.value[1] is threfore the chart y value
- const value = seriesData.value[1].toFixed(3);
-
- this.tooltip.content.push({
- name: seriesName,
- value,
- color,
- });
- }
- });
- },
- setSvg(name) {
- getSvgIconPathContent(name)
- .then(path => {
- if (path) {
- this.$set(this.svgs, name, `path://${path}`);
- }
- })
- .catch(() => {});
- },
- onChartUpdated(chart) {
- [this.primaryColor] = chart.getOption().color;
- },
- onResize() {
- if (!this.$refs.areaChart) return;
- const { width } = this.$refs.areaChart.$el.getBoundingClientRect();
- this.width = width;
- },
- },
-};
-</script>
-
-<template>
- <div
- class="prometheus-graph col-12"
- :class="[showBorder ? 'p-2' : 'p-0', { 'col-lg-6': !singleEmbed }]"
- >
- <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
- <div class="prometheus-graph-header">
- <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
- <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
- </div>
- <gl-area-chart
- ref="areaChart"
- v-bind="$attrs"
- :data="chartData"
- :option="chartOptions"
- :format-tooltip-text="formatTooltipText"
- :thresholds="thresholds"
- :width="width"
- :height="height"
- @updated="onChartUpdated"
- >
- <template v-if="tooltip.isDeployment">
- <template slot="tooltipTitle">
- {{ __('Deployed') }}
- </template>
- <div slot="tooltipContent" class="d-flex align-items-center">
- <icon name="commit" class="mr-2" />
- <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
- </div>
- </template>
- <template v-else>
- <template slot="tooltipTitle">
- <div class="text-nowrap">
- {{ tooltip.title }}
- </div>
- </template>
- <template slot="tooltipContent">
- <div
- v-for="(content, key) in tooltip.content"
- :key="key"
- class="d-flex justify-content-between"
- >
- <gl-chart-series-label :color="isMultiSeries ? content.color : ''">
- {{ content.name }}
- </gl-chart-series-label>
- <div class="prepend-left-32">
- {{ content.value }}
- </div>
- </div>
- </template>
- </template>
- </gl-area-chart>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
index 820f0f7f12d..0d377eb9c68 100644
--- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -5,9 +5,10 @@ import { parseBoolean } from '~/lib/utils/common_utils';
document.addEventListener('DOMContentLoaded', () => {
const twoFactorNode = document.querySelector('.js-two-factor-auth');
const skippable = parseBoolean(twoFactorNode.dataset.twoFactorSkippable);
+
if (skippable) {
const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`;
- const flashAlert = document.querySelector('.flash-alert .container-fluid');
+ const flashAlert = document.querySelector('.flash-alert');
if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button);
}
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index a223a8f5b08..ea867d30ce8 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -144,6 +144,10 @@ export default {
visibilityLevelDescription() {
return visibilityLevelDescriptions[this.visibilityLevel];
},
+
+ showContainerRegistryPublicNote() {
+ return this.visibilityLevel === visibilityOptions.PUBLIC;
+ },
},
watch: {
@@ -286,6 +290,9 @@ export default {
label="Container registry"
help-text="Every project can have its own space to store its Docker images"
>
+ <div v-if="showContainerRegistryPublicNote" class="text-muted">
+ {{ __('Note: the container registry is always visible when a project is public') }}
+ </div>
<project-feature-toggle
v-model="containerRegistryEnabled"
:disabled-input="!repositoryEnabled"
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 60d3d83a4b2..765cb868f80 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -113,7 +113,7 @@ export default class ProjectFindFile {
if (searchText) {
matches = fuzzaldrinPlus.match(filePath, searchText);
}
- blobItemUrl = this.options.blobUrlTemplate + '/' + filePath;
+ blobItemUrl = this.options.blobUrlTemplate + '/' + encodeURIComponent(filePath);
html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
results.push(this.element.find('.tree-table > tbody').append(html));
}
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index 7580c2d0ad0..88b6b4732b1 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -53,7 +53,7 @@ export default {
};
</script>
<template>
- <div class="card">
+ <div :id="release.tag_name" class="card">
<div class="card-body">
<h2 class="card-title mt-0">
{{ release.name }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index 3e65bdf0cb0..6f6d145815e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -5,11 +5,7 @@ export const WARNING_MESSAGE_CLASS = 'warning_message';
export const DANGER_MESSAGE_CLASS = 'danger_message';
export const MWPS_MERGE_STRATEGY = 'merge_when_pipeline_succeeds';
-export const ATMTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds';
+export const MTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds';
export const MT_MERGE_STRATEGY = 'merge_train';
-export const AUTO_MERGE_STRATEGIES = [
- MWPS_MERGE_STRATEGY,
- ATMTWPS_MERGE_STRATEGY,
- MT_MERGE_STRATEGY,
-];
+export const AUTO_MERGE_STRATEGIES = [MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY];
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 7843409f4a7..699d41494bf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -3,7 +3,7 @@ import _ from 'underscore';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
-import { ATMTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
+import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
export default class MergeRequestStore {
constructor(data) {
@@ -217,8 +217,8 @@ export default class MergeRequestStore {
}
static getPreferredAutoMergeStrategy(availableAutoMergeStrategies) {
- if (_.includes(availableAutoMergeStrategies, ATMTWPS_MERGE_STRATEGY)) {
- return ATMTWPS_MERGE_STRATEGY;
+ if (_.includes(availableAutoMergeStrategies, MTWPS_MERGE_STRATEGY)) {
+ return MTWPS_MERGE_STRATEGY;
} else if (_.includes(availableAutoMergeStrategies, MT_MERGE_STRATEGY)) {
return MT_MERGE_STRATEGY;
} else if (_.includes(availableAutoMergeStrategies, MWPS_MERGE_STRATEGY)) {
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index beb2ac09992..a97538d813a 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -24,7 +24,7 @@ export default {
showStagedIcon: {
type: Boolean,
required: false,
- default: false,
+ default: true,
},
size: {
type: Number,
@@ -41,7 +41,7 @@ export default {
changedIcon() {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
- const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : '';
+ const suffix = !this.file.changed && this.file.staged && this.showStagedIcon ? '-solid' : '';
return `${getCommitIconMap(this.file).icon}${suffix}`;
},
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 96f6d02a68f..af05d069f97 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -1,3 +1,5 @@
+$notification-box-shadow-color: rgba(0, 0, 0, 0.25);
+
.flash-container {
cursor: pointer;
margin: 0;
@@ -6,12 +8,32 @@
position: relative;
z-index: 1;
+ &.sticky {
+ position: sticky;
+ position: -webkit-sticky;
+ top: $flash-container-top;
+ z-index: 200;
+
+ .flash-content {
+ box-shadow: 0 2px 4px 0 $notification-box-shadow-color;
+ }
+ }
+
+ .close-icon {
+ width: 16px;
+ height: 16px;
+ position: absolute;
+ right: $gl-padding;
+ top: $gl-padding;
+ }
+
.flash-notice,
.flash-alert,
.flash-success,
.flash-warning {
border-radius: $border-radius-default;
color: $white-light;
+ padding-right: $gl-padding * 2;
.container-fluid,
.container-fluid.container-limited {
@@ -97,3 +119,28 @@
}
}
}
+
+.gl-browser-ie .flash-container {
+ position: fixed;
+ max-width: $limited-layout-width;
+ left: 50%;
+
+ .flash-alert {
+ position: relative;
+ left: -50%;
+ }
+}
+
+.with-system-header .flash-container {
+ top: $flash-container-top + $system-header-height;
+}
+
+.with-performance-bar {
+ .flash-container {
+ top: $flash-container-top + $performance-bar-height;
+ }
+
+ &.with-system-header .flash-container {
+ top: $flash-container-top + $performance-bar-height + $system-header-height;
+ }
+}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 821e6691fe4..69ef116043a 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -245,27 +245,3 @@ label {
.input-group-text {
max-height: $input-height;
}
-
-.gl-form-checkbox {
- align-items: baseline;
- margin-right: 1rem;
- margin-bottom: 0.25rem;
-
- .form-check-input {
- margin-right: 0;
- }
-
- .form-check-label {
- padding-left: $gl-padding-8;
- }
-
- &.form-check-inline .form-check-input {
- align-self: flex-start;
- height: 1.5 * $gl-font-size;
- }
-
- .form-check-input:disabled,
- .form-check-input:disabled ~ .form-check-label {
- cursor: not-allowed;
- }
-}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 97cb9d90ff0..7205324e86f 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -29,6 +29,15 @@ body {
}
}
+.container-fluid {
+ &.limit-container-width {
+ .flash-container.sticky {
+ max-width: $limited-layout-width;
+ margin: 0 auto;
+ }
+ }
+}
+
.content-wrapper {
margin-top: $header-height;
padding-bottom: 100px;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index afcc7f8a1db..33caac4d725 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -384,8 +384,18 @@
@extend .fa-exclamation-circle;
}
}
-}
+ .prometheus-graph-embed {
+ h3.popover-header {
+ /** Override <h3> .popover-header
+ * as embed metrics do not follow the same
+ * style as default md <h3> (which are deeply nested)
+ */
+ margin: 0;
+ font-size: $gl-font-size-small;
+ }
+ }
+}
/**
* Headers
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 7a3fd2adfbb..15a779dde1d 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -377,6 +377,7 @@ $performance-bar-height: 35px;
$system-header-height: 16px;
$system-footer-height: $system-header-height;
$flash-height: 52px;
+$flash-container-top: 48px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
$home-panel-title-row-height: 64px;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 74380ec995a..2d2f0c531c7 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -45,8 +45,7 @@ input[type='checkbox']:hover {
border: 0;
border-radius: $border-radius-default;
transition: border-color ease-in-out $default-transition-duration,
- background-color ease-in-out $default-transition-duration,
- width ease-in-out $default-transition-duration;
+ background-color ease-in-out $default-transition-duration;
@include media-breakpoint-up(xl) {
width: $search-input-xl-width;