diff options
Diffstat (limited to 'app/assets')
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; |