diff options
Diffstat (limited to 'app')
98 files changed, 854 insertions, 783 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index aacfa0d87e6..5f5c8044b49 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -48,6 +48,9 @@ export default class Clusters { } = document.querySelector('.js-edit-cluster-form').dataset; this.clusterId = clusterId; + this.clusterNewlyCreatedKey = `cluster_${this.clusterId}_newly_created`; + this.clusterBannerDismissedKey = `cluster_${this.clusterId}_banner_dismissed`; + this.store = new ClustersStore(); this.store.setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath); this.store.setManagePrometheusPath(managePrometheusPath); @@ -81,18 +84,19 @@ export default class Clusters { this.showTokenButton = document.querySelector('.js-show-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token'); this.ingressDomainHelpText = document.querySelector('.js-ingress-domain-help-text'); - this.ingressDomainSnippet = this.ingressDomainHelpText.querySelector( - '.js-ingress-domain-snippet', - ); + this.ingressDomainSnippet = + this.ingressDomainHelpText && + this.ingressDomainHelpText.querySelector('.js-ingress-domain-snippet'); Clusters.initDismissableCallout(); initSettingsPanels(); - setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); + const toggleButtonsContainer = document.querySelector('.js-cluster-enable-toggle-area'); + if (toggleButtonsContainer) { + setupToggleButtons(toggleButtonsContainer); + } this.initApplications(clusterType); - if (this.store.state.status !== 'created') { - this.updateContainer(null, this.store.state.status, this.store.state.statusReason); - } + this.updateContainer(null, this.store.state.status, this.store.state.statusReason); this.addListeners(); if (statusPath) { @@ -247,35 +251,56 @@ export default class Clusters { setBannerDismissedState(status, isDismissed) { if (AccessorUtilities.isLocalStorageAccessSafe()) { - window.localStorage.setItem( - `cluster_${this.clusterId}_banner_dismissed`, - `${status}_${isDismissed}`, - ); + window.localStorage.setItem(this.clusterBannerDismissedKey, `${status}_${isDismissed}`); } } isBannerDismissed(status) { let bannerState; if (AccessorUtilities.isLocalStorageAccessSafe()) { - bannerState = window.localStorage.getItem(`cluster_${this.clusterId}_banner_dismissed`); + bannerState = window.localStorage.getItem(this.clusterBannerDismissedKey); } return bannerState === `${status}_true`; } - updateContainer(prevStatus, status, error) { - this.hideAll(); + setClusterNewlyCreated(state) { + if (AccessorUtilities.isLocalStorageAccessSafe()) { + window.localStorage.setItem(this.clusterNewlyCreatedKey, Boolean(state)); + } + } + + isClusterNewlyCreated() { + // once this is true, it will always be true for a given page load + if (!this.isNewlyCreated) { + let newlyCreated; + if (AccessorUtilities.isLocalStorageAccessSafe()) { + newlyCreated = window.localStorage.getItem(this.clusterNewlyCreatedKey); + } + + this.isNewlyCreated = newlyCreated === 'true'; + } + return this.isNewlyCreated; + } - if (this.isBannerDismissed(status)) { + updateContainer(prevStatus, status, error) { + if (status !== 'created' && this.isBannerDismissed(status)) { return; } this.setBannerDismissedState(status, false); - // We poll all the time but only want the `created` banner to show when newly created - if (this.store.state.status !== 'created' || prevStatus !== this.store.state.status) { + if (prevStatus !== status) { + this.hideAll(); + switch (status) { case 'created': - this.successContainer.classList.remove('hidden'); + if (this.isClusterNewlyCreated()) { + this.setClusterNewlyCreated(false); + this.successContainer.classList.remove('hidden'); + } else if (prevStatus) { + this.setClusterNewlyCreated(true); + window.location.reload(); + } break; case 'errored': this.errorContainer.classList.remove('hidden'); @@ -292,7 +317,6 @@ export default class Clusters { this.creatingContainer.classList.remove('hidden'); break; default: - this.hideAll(); } } } diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue index 2ff6d5e32e2..4f60e543666 100644 --- a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue +++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue @@ -2,14 +2,17 @@ import { GlModal } from '@gitlab/ui'; import { sprintf, s__ } from '~/locale'; import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click'; -import { INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants'; +import { HELM, INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants'; const CUSTOM_APP_WARNING_TEXT = { + [HELM]: s__( + 'ClusterIntegration|The associated Tiller pod will be deleted and cannot be restored.', + ), [INGRESS]: s__( 'ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored.', ), [CERT_MANAGER]: s__( - 'ClusterIntegration|The associated certifcate will be deleted and cannot be restored.', + 'ClusterIntegration|The associated private key will be deleted and cannot be restored.', ), [PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'), [RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'), diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index f64f0ca616f..ada5a49e246 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -171,6 +171,7 @@ export default class ClusterStore { this.state.applications.cert_manager.email || serverAppEntry.email; } else if (appId === JUPYTER) { this.state.applications.jupyter.hostname = + this.state.applications.jupyter.hostname || serverAppEntry.hostname || (this.state.applications.ingress.externalIp ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io` diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index daa941a63cd..7a6ad3dc771 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -12,7 +12,6 @@ import 'core-js/es/promise/finally'; import 'core-js/es/string/code-point-at'; import 'core-js/es/string/from-code-point'; import 'core-js/es/string/includes'; -import 'core-js/es/string/repeat'; import 'core-js/es/string/starts-with'; import 'core-js/es/string/ends-with'; import 'core-js/es/symbol'; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index b98fe9f6ce2..a66555838ba 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -3,16 +3,9 @@ import autosize from 'autosize'; import GfmAutoComplete, { defaultAutocompleteConfig } from 'ee_else_ce/gfm_auto_complete'; import dropzoneInput from './dropzone_input'; import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown'; -import IndentHelper from './helpers/indent_helper'; -import { keystroke } from './lib/utils/common_utils'; -import * as keys from './lib/utils/keycodes'; -import UndoStack from './lib/utils/undo_stack'; export default class GLForm { constructor(form, enableGFM = {}) { - this.handleKeyShortcuts = this.handleKeyShortcuts.bind(this); - this.setState = this.setState.bind(this); - this.form = form; this.textarea = this.form.find('textarea.js-gfm-input'); this.enableGFM = Object.assign({}, defaultAutocompleteConfig, enableGFM); @@ -23,10 +16,6 @@ export default class GLForm { this.enableGFM[item] = Boolean(dataSources[item]); } }); - - this.undoStack = new UndoStack(); - this.indentHelper = new IndentHelper(this.textarea[0]); - // Before we start, we should clean up any previous data for this form this.destroy(); // Set up the form @@ -96,84 +85,9 @@ export default class GLForm { clearEventListeners() { this.textarea.off('focus'); this.textarea.off('blur'); - this.textarea.off('keydown'); removeMarkdownListeners(this.form); } - setState(state) { - const selection = [this.textarea[0].selectionStart, this.textarea[0].selectionEnd]; - this.textarea.val(state); - this.textarea[0].setSelectionRange(selection[0], selection[1]); - } - - /* - Handle keypresses for a custom undo/redo stack. - We need this because the toolbar buttons and indentation helpers mess with the browser's - native undo/redo capability. - */ - handleUndo(event) { - const content = this.textarea.val(); - const { selectionStart, selectionEnd } = this.textarea[0]; - const stack = this.undoStack; - - if (stack.isEmpty()) { - // ==== Save initial state in undo history ==== - stack.save(content); - } - - if (keystroke(event, keys.Z_KEY_CODE, 'l')) { - // ==== Undo ==== - event.preventDefault(); - stack.save(content); - if (stack.canUndo()) { - this.setState(stack.undo()); - } - } else if (keystroke(event, keys.Z_KEY_CODE, 'ls') || keystroke(event, keys.Y_KEY_CODE, 'l')) { - // ==== Redo ==== - event.preventDefault(); - if (stack.canRedo()) { - this.setState(stack.redo()); - } - } else if ( - keystroke(event, keys.SPACE_KEY_CODE) || - keystroke(event, keys.ENTER_KEY_CODE) || - selectionStart !== selectionEnd - ) { - // ==== Save after finishing a word or before deleting a large selection ==== - stack.save(content); - } else if (content === '') { - // ==== Save after deleting everything ==== - stack.save(''); - } else { - // ==== Save after 1 second of inactivity ==== - stack.scheduleSave(content); - } - } - - handleIndent(event) { - if (keystroke(event, keys.LEFT_BRACKET_KEY_CODE, 'l')) { - // ==== Unindent selected lines ==== - event.preventDefault(); - this.indentHelper.unindent(); - } else if (keystroke(event, keys.RIGHT_BRACKET_KEY_CODE, 'l')) { - // ==== Indent selected lines ==== - event.preventDefault(); - this.indentHelper.indent(); - } else if (keystroke(event, keys.ENTER_KEY_CODE)) { - // ==== Auto-indent new lines ==== - event.preventDefault(); - this.indentHelper.newline(); - } else if (keystroke(event, keys.BACKSPACE_KEY_CODE)) { - // ==== Auto-delete indents at the beginning of the line ==== - this.indentHelper.backspace(event); - } - } - - handleKeyShortcuts(event) { - this.handleIndent(event); - this.handleUndo(event); - } - addEventListeners() { this.textarea.on('focus', function focusTextArea() { $(this) @@ -185,6 +99,5 @@ export default class GLForm { .closest('.md-area') .removeClass('is-focused'); }); - this.textarea.on('keydown', e => this.handleKeyShortcuts(e.originalEvent)); } } diff --git a/app/assets/javascripts/helpers/indent_helper.js b/app/assets/javascripts/helpers/indent_helper.js deleted file mode 100644 index a8815fac04e..00000000000 --- a/app/assets/javascripts/helpers/indent_helper.js +++ /dev/null @@ -1,182 +0,0 @@ -const INDENT_SEQUENCE = ' '; - -function countLeftSpaces(text) { - const i = text.split('').findIndex(c => c !== ' '); - return i === -1 ? text.length : i; -} - -/** - * IndentHelper provides methods that allow manual and smart indentation in - * textareas. It supports line indent/unindent, selection indent/unindent, - * auto indentation on newlines, and smart deletion of indents with backspace. - */ -export default class IndentHelper { - /** - * Creates a new IndentHelper and binds it to the given `textarea`. You can provide a custom indent sequence in the second parameter, but the `newline` and `backspace` operations may work funny if the indent sequence isn't spaces only. - */ - constructor(textarea, indentSequence = INDENT_SEQUENCE) { - this.element = textarea; - this.seq = indentSequence; - } - - getSelection() { - return { start: this.element.selectionStart, end: this.element.selectionEnd }; - } - - isRangeSelection() { - return this.element.selectionStart !== this.element.selectionEnd; - } - - /** - * Re-implementation of textarea's setRangeText method, because IE/Edge don't support it. - * - * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea%2Finput-setrangetext - */ - setRangeText(replacement, start, end, selectMode) { - // Disable eslint to remain as faithful as possible to the above linked spec - /* eslint-disable no-param-reassign, no-case-declarations */ - const text = this.element.value; - - if (start > end) { - throw new RangeError('setRangeText: start index must be less than or equal to end index'); - } - - // Clamp to [0, len] - start = Math.max(0, Math.min(start, text.length)); - end = Math.max(0, Math.min(end, text.length)); - - let selection = { start: this.element.selectionStart, end: this.element.selectionEnd }; - - this.element.value = text.slice(0, start) + replacement + text.slice(end); - - const newLength = replacement.length; - const newEnd = start + newLength; - - switch (selectMode) { - case 'select': - selection = { start, newEnd }; - break; - case 'start': - selection = { start, end: start }; - break; - case 'end': - selection = { start: newEnd, end: newEnd }; - break; - case 'preserve': - default: - const oldLength = end - start; - const delta = newLength - oldLength; - if (selection.start > end) { - selection.start += delta; - } else if (selection.start > start) { - selection.start = start; - } - if (selection.end > end) { - selection.end += delta; - } else if (selection.end > start) { - selection.end = newEnd; - } - } - - this.element.setSelectionRange(selection.start, selection.end); - - /* eslint-enable no-param-reassign, no-case-declarations */ - } - - /** - * Returns an array of lines in the textarea, with information about their - * start/end offsets and whether they are included in the current selection. - */ - splitLines() { - const { start, end } = this.getSelection(); - - const lines = this.element.value.split('\n'); - let textStart = 0; - const lineObjects = []; - lines.forEach(line => { - const lineObj = { - text: line, - start: textStart, - end: textStart + line.length, - }; - lineObj.inSelection = lineObj.start <= end && lineObj.end >= start; - lineObjects.push(lineObj); - textStart += line.length + 1; - }); - return lineObjects; - } - - /** - * Indents selected lines by one level. - */ - indent() { - const { start } = this.getSelection(); - - const selectedLines = this.splitLines().filter(line => line.inSelection); - if (!this.isRangeSelection() && start === selectedLines[0].start) { - // Special case: if cursor is at the beginning of the line, move it one - // indent right. - const line = selectedLines[0]; - this.setRangeText(this.seq, line.start, line.start, 'end'); - } else { - selectedLines.reverse(); - selectedLines.forEach(line => { - this.setRangeText(INDENT_SEQUENCE, line.start, line.start, 'preserve'); - }); - } - } - - /** - * Unindents selected lines by one level. - */ - unindent() { - const lines = this.splitLines().filter(line => line.inSelection); - lines.reverse(); - lines - .filter(line => line.text.startsWith(this.seq)) - .forEach(line => { - this.setRangeText('', line.start, line.start + this.seq.length, 'preserve'); - }); - } - - /** - * Emulates a newline keypress, automatically indenting the new line. - */ - newline() { - const { start, end } = this.getSelection(); - - if (this.isRangeSelection()) { - // Manually kill the selection before calculating the indent - this.setRangeText('', start, end, 'start'); - } - - // Auto-indent the next line - const currentLine = this.splitLines().find(line => line.end >= start); - const spaces = countLeftSpaces(currentLine.text); - this.setRangeText(`\n${' '.repeat(spaces)}`, start, start, 'end'); - } - - /** - * If the cursor is positioned at the end of a line's leading indents, - * emulates a backspace keypress by deleting a single level of indents. - * @param event The DOM KeyboardEvent that triggers this action, or null. - */ - backspace(event) { - const { start } = this.getSelection(); - - // If the cursor is at the end of leading indents, delete an indent. - if (!this.isRangeSelection()) { - const currentLine = this.splitLines().find(line => line.end >= start); - const cursorPosition = start - currentLine.start; - if (countLeftSpaces(currentLine.text) === cursorPosition && cursorPosition > 0) { - if (event) event.preventDefault(); - - let spacesToDelete = cursorPosition % this.seq.length; - if (spacesToDelete === 0) { - spacesToDelete = this.seq.length; - } - this.setRangeText('', start - spacesToDelete, start, 'start'); - } - } - } -} diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index ef9fb6d08d1..8da87f424c4 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -73,6 +73,10 @@ export default { type: String, required: true, }, + projectPath: { + type: String, + required: true, + }, logState: { type: String, required: true, @@ -258,6 +262,7 @@ export default { :quota-used="job.runners.quota.used" :quota-limit="job.runners.quota.limit" :runners-path="runnerHelpUrl" + :project-path="projectPath" /> <environments-block diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index e9704584c9f..06477477aad 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -73,15 +73,14 @@ export default { }, renderBlock() { return ( - this.job.merge_request || this.job.duration || - this.job.finished_data || + this.job.finished_at || this.job.erased_at || this.job.queued || + this.hasTimeout || this.job.runner || this.job.coverage || - this.job.tags.length || - this.job.cancel_path + this.job.tags.length ); }, hasArtifact() { @@ -160,7 +159,7 @@ export default { </gl-link> </div> - <div :class="{ block: renderBlock }"> + <div v-if="renderBlock" class="block"> <detail-row v-if="job.duration" :value="duration" diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index 06514fcce1d..add7f9b710a 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -10,16 +10,29 @@ export default () => { JobApp, }, render(createElement) { + const { + deploymentHelpUrl, + runnerHelpUrl, + runnerSettingsUrl, + variablesSettingsUrl, + endpoint, + pagePath, + logState, + buildStatus, + projectPath, + } = element.dataset; + return createElement('job-app', { props: { - deploymentHelpUrl: element.dataset.deploymentHelpUrl, - runnerHelpUrl: element.dataset.runnerHelpUrl, - runnerSettingsUrl: element.dataset.runnerSettingsUrl, - variablesSettingsUrl: element.dataset.variablesSettingsUrl, - endpoint: element.dataset.endpoint, - pagePath: element.dataset.buildOptionsPagePath, - logState: element.dataset.buildOptionsLogState, - buildStatus: element.dataset.buildOptionsBuildStatus, + deploymentHelpUrl, + runnerHelpUrl, + runnerSettingsUrl, + variablesSettingsUrl, + endpoint, + pagePath, + logState, + buildStatus, + projectPath, }, }); }, diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 1a94aee2398..5e90893b684 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -203,71 +203,6 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; // 3) Middle-click or Mouse Wheel Click (e.which is 2) export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2; -export const getPlatformLeaderKey = () => { - // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings - if (navigator && navigator.platform && navigator.platform.startsWith('Mac')) { - return 'meta'; - } - return 'ctrl'; -}; - -export const getPlatformLeaderKeyHTML = () => { - if (getPlatformLeaderKey() === 'meta') { - return '⌘'; - } - // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings - return 'Ctrl'; -}; - -export const isPlatformLeaderKey = e => { - if (getPlatformLeaderKey() === 'meta') { - return Boolean(e.metaKey); - } - return Boolean(e.ctrlKey); -}; - -/** - * Tests if a KeyboardEvent corresponds exactly to a keystroke. - * - * This function avoids hacking around an old version of Mousetrap, which we ship at the moment. It should be removed after we upgrade to the newest Mousetrap. See: - * - https://gitlab.com/gitlab-org/gitlab-ce/issues/63182 - * - https://gitlab.com/gitlab-org/gitlab-ce/issues/64246 - * - * @example - * // Matches the enter key with exactly zero modifiers - * keystroke(event, 13) - * - * @example - * // Matches Control-Shift-Z - * keystroke(event, 90, 'cs') - * - * @param e The KeyboardEvent to test. - * @param keyCode The key code of the key to test. Why keycodes? IE/Edge don't support the more convenient `key` and `code` properties. - * @param modifiers A string of modifiers keys. Each modifier key is represented by one character. The set of pressed modifier keys must match the given string exactly. Available options are 'a' for Alt/Option, 'c' for Control, 'm' for Meta/Command, 's' for Shift, and 'l' for the leader key (Meta on MacOS and Control otherwise). - * @returns {boolean} True if the KeyboardEvent corresponds to the given keystroke. - */ -export const keystroke = (e, keyCode, modifiers = '') => { - if (!e || !keyCode) { - return false; - } - - const leader = getPlatformLeaderKey(); - const mods = modifiers.toLowerCase().replace('l', leader.charAt(0)); - - // Match depressed modifier keys - if ( - e.altKey !== mods.includes('a') || - e.ctrlKey !== mods.includes('c') || - e.metaKey !== mods.includes('m') || - e.shiftKey !== mods.includes('s') - ) { - return false; - } - - // Match the depressed key - return keyCode === (e.keyCode || e.which); -}; - export const contentTop = () => { const perfBar = $('#js-peek').outerHeight() || 0; const mrTabsHeight = $('.merge-request-tabs').outerHeight() || 0; diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js index e24fcf47d71..5e0f9b612a2 100644 --- a/app/assets/javascripts/lib/utils/keycodes.js +++ b/app/assets/javascripts/lib/utils/keycodes.js @@ -1,10 +1,4 @@ -export const BACKSPACE_KEY_CODE = 8; -export const ENTER_KEY_CODE = 13; -export const ESC_KEY_CODE = 27; -export const SPACE_KEY_CODE = 32; export const UP_KEY_CODE = 38; export const DOWN_KEY_CODE = 40; -export const Y_KEY_CODE = 89; -export const Z_KEY_CODE = 90; -export const LEFT_BRACKET_KEY_CODE = 219; -export const RIGHT_BRACKET_KEY_CODE = 221; +export const ENTER_KEY_CODE = 13; +export const ESC_KEY_CODE = 27; diff --git a/app/assets/javascripts/lib/utils/undo_stack.js b/app/assets/javascripts/lib/utils/undo_stack.js deleted file mode 100644 index 6cfdc2a0a0f..00000000000 --- a/app/assets/javascripts/lib/utils/undo_stack.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * UndoStack provides a custom implementation of an undo/redo engine. It was originally written for GitLab's Markdown editor (`gl_form.js`), whose rich text editing capabilities broke native browser undo/redo behaviour. - * - * UndoStack supports predictable undos/redos, debounced saves, maximum history length, and duplicate detection. - * - * Usage: - * - `stack = new UndoStack();` - * - Saves a state to the stack with `stack.save(state)`. - * - Get the current state with `stack.current()`. - * - Revert to the previous state with `stack.undo()`. - * - Redo a previous undo with `stack.redo()`; - * - Queue a future save with `stack.scheduleSave(state, delay)`. Useful for text editors. - * - See the full undo history in `stack.history`. - */ -export default class UndoStack { - constructor(maxLength = 1000) { - this.clear(); - this.maxLength = maxLength; - - // If you're storing reference-types in the undo stack, you might want to - // reassign this property to some deep-equals function. - this.comparator = (a, b) => a === b; - } - - current() { - if (this.cursor === -1) { - return undefined; - } - return this.history[this.cursor]; - } - - isEmpty() { - return this.history.length === 0; - } - - clear() { - this.clearPending(); - this.history = []; - this.cursor = -1; - } - - save(state) { - this.clearPending(); - if (this.comparator(state, this.current())) { - // Don't save state if it's the same as the current state - return; - } - - this.history.length = this.cursor + 1; - this.history.push(state); - this.cursor += 1; - - if (this.history.length > this.maxLength) { - this.history.shift(); - this.cursor -= 1; - } - } - - scheduleSave(state, delay = 1000) { - this.clearPending(); - this.pendingState = state; - this.timeout = setTimeout(this.saveNow.bind(this), delay); - } - - saveNow() { - // Persists scheduled saves immediately - this.save(this.pendingState); - this.clearPending(); - } - - clearPending() { - // Cancels any scheduled saves - if (this.timeout) { - clearTimeout(this.timeout); - delete this.timeout; - delete this.pendingState; - } - } - - canUndo() { - return this.cursor > 0; - } - - undo() { - this.clearPending(); - if (!this.canUndo()) { - return undefined; - } - this.cursor -= 1; - return this.history[this.cursor]; - } - - canRedo() { - return this.cursor >= 0 && this.cursor < this.history.length - 1; - } - - redo() { - this.clearPending(); - if (!this.canRedo()) { - return undefined; - } - this.cursor += 1; - return this.history[this.cursor]; - } -} diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/manual_ordering.js index 012d1e70410..29a0e5a904a 100644 --- a/app/assets/javascripts/manual_ordering.js +++ b/app/assets/javascripts/manual_ordering.js @@ -21,7 +21,7 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) => const initManualOrdering = () => { const issueList = document.querySelector('.manual-ordering'); - if (!issueList || !(gon.features && gon.features.manualSorting) || !(gon.current_user_id > 0)) { + if (!issueList || !(gon.current_user_id > 0)) { return; } diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index ed4cef4a917..97dcd54fe88 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -21,7 +21,7 @@ export const getQuickActionText = note => { text = __('Applying multiple commands'); } else { const commandDescription = executedCommands[0].description.toLowerCase(); - text = sprintf(__('Applying command to %{commandDescription}', { commandDescription })); + text = sprintf(__('Applying command to %{commandDescription}'), { commandDescription }); } } diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue index ed518611d0b..3c5de189d51 100644 --- a/app/assets/javascripts/operation_settings/components/external_dashboard.vue +++ b/app/assets/javascripts/operation_settings/components/external_dashboard.vue @@ -50,9 +50,11 @@ export default { <form> <gl-form-group :label="s__('ExternalMetrics|Full dashboard URL')" + label-for="full-dashboard-url" :description="s__('ExternalMetrics|Enter the URL of the dashboard you want to link to')" > <gl-form-input + id="full-dashboard-url" v-model="userDashboardUrl" placeholder="https://my-org.gitlab.io/my-dashboards" @keydown.enter.native.prevent="updateExternalDashboardUrl" diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue index 5bc1d5e0533..73524827c5d 100644 --- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue +++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue @@ -44,7 +44,7 @@ export default { </script> <template> <div - v-if="currentRequest.details" + v-if="currentRequest.details && metricDetails" :id="`peek-view-${metric}`" class="view qa-performance-bar-detailed-metric" > diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js index 4a08e158f6b..8d6a3781048 100644 --- a/app/assets/javascripts/persistent_user_callout.js +++ b/app/assets/javascripts/persistent_user_callout.js @@ -1,13 +1,17 @@ +import { parseBoolean } from './lib/utils/common_utils'; import axios from './lib/utils/axios_utils'; import { __ } from './locale'; import Flash from './flash'; +const DEFERRED_LINK_CLASS = 'deferred-link'; + export default class PersistentUserCallout { constructor(container) { - const { dismissEndpoint, featureId } = container.dataset; + const { dismissEndpoint, featureId, deferLinks } = container.dataset; this.container = container; this.dismissEndpoint = dismissEndpoint; this.featureId = featureId; + this.deferLinks = parseBoolean(deferLinks); this.init(); } @@ -15,9 +19,21 @@ export default class PersistentUserCallout { init() { const closeButton = this.container.querySelector('.js-close'); closeButton.addEventListener('click', event => this.dismiss(event)); + + if (this.deferLinks) { + this.container.addEventListener('click', event => { + const isDeferredLink = event.target.classList.contains(DEFERRED_LINK_CLASS); + + if (isDeferredLink) { + const { href, target } = event.target; + + this.dismiss(event, { href, target }); + } + }); + } } - dismiss(event) { + dismiss(event, deferredLinkOptions = null) { event.preventDefault(); axios @@ -26,6 +42,11 @@ export default class PersistentUserCallout { }) .then(() => { this.container.remove(); + + if (deferredLinkOptions) { + const { href, target } = deferredLinkOptions; + window.open(href, target); + } }) .catch(() => { Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.')); diff --git a/app/assets/javascripts/privacy_policy_update_callout.js b/app/assets/javascripts/privacy_policy_update_callout.js new file mode 100644 index 00000000000..126b1ee1132 --- /dev/null +++ b/app/assets/javascripts/privacy_policy_update_callout.js @@ -0,0 +1,8 @@ +import PersistentUserCallout from '~/persistent_user_callout'; + +function initPrivacyPolicyUpdateCallout() { + const callout = document.querySelector('.privacy-policy-update-64341'); + PersistentUserCallout.factory(callout); +} + +export default initPrivacyPolicyUpdateCallout; diff --git a/app/assets/javascripts/projects/gke_cluster_namespace/index.js b/app/assets/javascripts/projects/gke_cluster_namespace/index.js index 288740203ad..0ec4d8807b0 100644 --- a/app/assets/javascripts/projects/gke_cluster_namespace/index.js +++ b/app/assets/javascripts/projects/gke_cluster_namespace/index.js @@ -28,8 +28,10 @@ const setState = glManagedCheckbox => { const initGkeNamespace = () => { const glManagedCheckbox = document.querySelector('.js-gl-managed'); - setState(glManagedCheckbox); // this is needed in order to set the initial state - glManagedCheckbox.addEventListener('change', () => setState(glManagedCheckbox)); + if (glManagedCheckbox) { + setState(glManagedCheckbox); // this is needed in order to set the initial state + glManagedCheckbox.addEventListener('change', () => setState(glManagedCheckbox)); + } }; export default initGkeNamespace; diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 930c0d5e958..40a2158de78 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -101,10 +101,12 @@ Sidebar.prototype.toggleTodo = function(e) { this.todoUpdateDone(data); }) .catch(() => - flash(sprintf(__('There was an error %{message} todo.')), { - message: - ajaxType === 'post' ? s__('RightSidebar|adding a') : s__('RightSidebar|deleting the'), - }), + flash( + sprintf(__('There was an error %{message} todo.'), { + message: + ajaxType === 'post' ? s__('RightSidebar|adding a') : s__('RightSidebar|deleting the'), + }), + ), ); }; diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js new file mode 100644 index 00000000000..2d0b099cf0b --- /dev/null +++ b/app/assets/javascripts/tracking.js @@ -0,0 +1,67 @@ +import $ from 'jquery'; + +const extractData = (el, opts = {}) => { + const { trackEvent, trackLabel = '', trackProperty = '' } = el.dataset; + let trackValue = el.dataset.trackValue || el.value || ''; + if (el.type === 'checkbox' && !el.checked) trackValue = false; + return [ + trackEvent + (opts.suffix || ''), + { + label: trackLabel, + property: trackProperty, + value: trackValue, + }, + ]; +}; + +export default class Tracking { + static enabled() { + return typeof window.snowplow === 'function'; + } + + static event(category = document.body.dataset.page, event = 'generic', data = {}) { + if (!this.enabled()) return false; + // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings + if (!category) throw new Error('Tracking: no category provided for tracking.'); + + return window.snowplow( + 'trackStructEvent', + category, + event, + Object.assign({}, { label: '', property: '', value: '' }, data), + ); + } + + constructor(category = document.body.dataset.page) { + this.category = category; + } + + bind(container = document) { + if (!this.constructor.enabled()) return; + container.querySelectorAll(`[data-track-event]`).forEach(el => { + if (this.customHandlingFor(el)) return; + // jquery is required for select2, so we use it always + // see: https://github.com/select2/select2/issues/4686 + $(el).on('click', this.eventHandler(this.category)); + }); + } + + customHandlingFor(el) { + const classes = el.classList; + + // bootstrap dropdowns + if (classes.contains('dropdown')) { + $(el).on('show.bs.dropdown', this.eventHandler(this.category, { suffix: '_show' })); + $(el).on('hide.bs.dropdown', this.eventHandler(this.category, { suffix: '_hide' })); + return true; + } + + return false; + } + + eventHandler(category = null, opts = {}) { + return e => { + this.constructor.event(category || this.category, ...extractData(e.currentTarget, opts)); + }; + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index 17ac8ada32d..76b96c8c1c0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -60,7 +60,7 @@ export default { return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline; }, showVisualReviewAppLink() { - return Boolean(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable); + return this.mr.visualReviewAppAvailable; }, showMergeTrainInfo() { return _.isNumber(this.mr.mergeTrainIndex); diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index 21c44b59520..8ce5b615795 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -1,6 +1,5 @@ <script> import { GlLink } from '@gitlab/ui'; -import { s__, sprintf } from '~/locale'; export default { components: { @@ -23,28 +22,8 @@ export default { }, }, computed: { - toolbarHelpHtml() { - const mdLinkStart = `<a href="${this.markdownDocsPath}" target="_blank" rel="noopener noreferrer" tabindex="-1">`; - const actionsLinkStart = `<a href="${this.quickActionsDocsPath}" target="_blank" rel="noopener noreferrer" tabindex="-1">`; - const linkEnd = '</a>'; - - if (this.markdownDocsPath && !this.quickActionsDocsPath) { - return sprintf( - s__('Editor|%{mdLinkStart}Markdown is supported%{mdLinkEnd}'), - { mdLinkStart, mdLinkEnd: linkEnd }, - false, - ); - } else if (this.markdownDocsPath && this.quickActionsDocsPath) { - return sprintf( - s__( - 'Editor|%{mdLinkStart}Markdown%{mdLinkEnd} and %{actionsLinkStart}quick actions%{actionsLinkEnd} are supported', - ), - { mdLinkStart, mdLinkEnd: linkEnd, actionsLinkStart, actionsLinkEnd: linkEnd }, - false, - ); - } - - return null; + hasQuickActionsDocsPath() { + return this.quickActionsDocsPath !== ''; }, }, }; @@ -53,7 +32,21 @@ export default { <template> <div class="comment-toolbar clearfix"> <div class="toolbar-text"> - <span v-html="toolbarHelpHtml"></span> + <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> + <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">{{ + __('Markdown is supported') + }}</gl-link> + </template> + <template v-if="hasQuickActionsDocsPath && markdownDocsPath"> + <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">{{ + __('Markdown') + }}</gl-link> + and + <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1">{{ + __('quick actions') + }}</gl-link> + are supported + </template> </div> <span v-if="canAttachFile" class="uploading-container"> <span class="uploading-progress-container hide"> diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index fbf16aa324a..e98030f1511 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -12,7 +12,6 @@ // If you need to add unique style that should affect only one page - use pages/ // directory. @import "at.js/dist/css/jquery.atwho"; -@import "pikaday/scss/pikaday"; @import "dropzone/dist/basic"; @import "select2/select2"; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 6b44834cc52..f384a49e0ae 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -435,6 +435,7 @@ img.emoji { /** COMMON SIZING CLASSES **/ .w-0 { width: 0; } .w-8em { width: 8em; } +.w-3rem { width: 3rem; } .h-12em { height: 12em; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 6e98908eeed..262c0bf5ed2 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -127,6 +127,7 @@ .section-header ~ .section.line { margin-left: $gl-padding; + display: block; } } diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 091327931c2..f111c7ca8cc 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -16,7 +16,7 @@ class AutocompleteController < ApplicationController .new(params: params, current_user: current_user, project: project, group: group) .execute - render json: UserSerializer.new.represent(users) + render json: UserSerializer.new(params).represent(users, project: project) end def user diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 90528f75ffd..1d1a72d21f1 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -26,7 +26,7 @@ module Boards list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params) issues = list_service.execute issues = issues.page(params[:page]).per(params[:per] || 20).without_count - Issue.move_to_end(issues) if Gitlab::Database.read_write? + Issue.move_nulls_to_end(issues) if Gitlab::Database.read_write? issues = issues.preload(:milestone, :assignees, project: [ diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 6fa2f75be33..398cb728e05 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -98,13 +98,12 @@ module IssuableActions render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" } end - # rubocop: disable CodeReuse/ActiveRecord + # rubocop:disable CodeReuse/ActiveRecord def discussions - notes = issuable.discussion_notes - .inc_relations_for_view - .with_notes_filter(notes_filter) - .includes(:noteable) - .fresh + notes = NotesFinder.new(current_user, finder_params_for_issuable).execute + .inc_relations_for_view + .includes(:noteable) + .fresh if notes_filter != UserPreference::NOTES_FILTERS[:only_comments] notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes) @@ -117,7 +116,7 @@ module IssuableActions render json: discussion_serializer.represent(discussions, context: self) end - # rubocop: enable CodeReuse/ActiveRecord + # rubocop:enable CodeReuse/ActiveRecord private @@ -222,4 +221,13 @@ module IssuableActions def parent @project || @group # rubocop:disable Gitlab/ModuleWithInstanceVariables end + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def finder_params_for_issuable + { + target: @issuable, + notes_filter: notes_filter + }.tap { |new_params| new_params[:project] = project if respond_to?(:project, true) } + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables end diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 0098c4cdf4c..d2a961efff7 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -243,7 +243,7 @@ module NotesActions end def notes_finder - @notes_finder ||= NotesFinder.new(project, current_user, finder_params) + @notes_finder ||= NotesFinder.new(current_user, finder_params) end def note_serializer diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index dda321bac79..5472ef05d7c 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -7,10 +7,6 @@ class GroupsController < Groups::ApplicationController include PreviewMarkdown include RecordUserLastActivity - before_action do - push_frontend_feature_flag(:manual_sorting, default_enabled: true) - end - respond_to :html prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) } diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index db7ca7ef0d7..bc9166b9df3 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -10,10 +10,6 @@ class Projects::IssuesController < Projects::ApplicationController include SpammableActions include RecordUserLastActivity - before_action do - push_frontend_feature_flag(:manual_sorting, default_enabled: true) - end - def issue_except_actions %i[index calendar new create bulk_update import_csv] end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 3152a38fd8e..65d9b074eee 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -68,7 +68,7 @@ class Projects::NotesController < Projects::ApplicationController alias_method :awardable, :note def finder_params - params.merge(last_fetched_at: last_fetched_at, notes_filter: notes_filter) + params.merge(project: project, last_fetched_at: last_fetched_at, notes_filter: notes_filter) end def authorize_admin_note! diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb index 612897f27e6..551b37cb3d3 100644 --- a/app/controllers/snippets/notes_controller.rb +++ b/app/controllers/snippets/notes_controller.rb @@ -27,7 +27,9 @@ class Snippets::NotesController < ApplicationController alias_method :noteable, :snippet def finder_params - params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet') + params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet').tap do |merged_params| + merged_params[:project] = project if respond_to?(:project) + end end def authorize_read_snippet! diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb index b462c8053fa..291a24c1405 100644 --- a/app/finders/branches_finder.rb +++ b/app/finders/branches_finder.rb @@ -69,7 +69,7 @@ class BranchesFinder return branches unless names branch_names = names.to_set - branches.filter do |branch| + branches.select do |branch| branch_names.include?(branch.name) end end diff --git a/app/finders/container_repositories_finder.rb b/app/finders/container_repositories_finder.rb new file mode 100644 index 00000000000..eb91d7f825b --- /dev/null +++ b/app/finders/container_repositories_finder.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class ContainerRepositoriesFinder + # id: group or project id + # container_type: :group or :project + def initialize(id:, container_type:) + @id = id + @type = container_type.to_sym + end + + def execute + if project_type? + project.container_repositories + else + group.container_repositories + end + end + + private + + attr_reader :id, :type + + def project_type? + type == :project + end + + def project + Project.find(id) + end + + def group + Group.find(id) + end +end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 8f610d7dddb..f7d9100bb78 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -3,6 +3,8 @@ class NotesFinder FETCH_OVERLAP = 5.seconds + attr_reader :target_type + # Used to filter Notes # When used with target_type and target_id this returns notes specifically for the controller # @@ -10,15 +12,17 @@ class NotesFinder # current_user - which user check authorizations with # project - which project to look for notes on # params: + # target: noteable # target_type: string # target_id: integer # last_fetched_at: time # search: string # - def initialize(project, current_user, params = {}) - @project = project + def initialize(current_user, params = {}) + @project = params[:project] @current_user = current_user - @params = params + @params = params.dup + @target_type = @params[:target_type] end def execute @@ -32,7 +36,27 @@ class NotesFinder def target return @target if defined?(@target) - target_type = @params[:target_type] + if target_given? + use_explicit_target + else + find_target_by_type_and_ids + end + end + + private + + def target_given? + @params.key?(:target) + end + + def use_explicit_target + @target = @params[:target] + @target_type = @target.class.name.underscore + + @target + end + + def find_target_by_type_and_ids target_id = @params[:target_id] target_iid = @params[:target_iid] @@ -45,13 +69,11 @@ class NotesFinder @project.commit(target_id) end else - noteables_for_type_by_id(target_type, target_id, target_iid) + noteable_for_type_by_id(target_type, target_id, target_iid) end end - private - - def noteables_for_type_by_id(type, id, iid) + def noteable_for_type_by_id(type, id, iid) query = if id { id: id } else @@ -77,10 +99,6 @@ class NotesFinder search(notes) end - def target_type - @params[:target_type] - end - # rubocop: disable CodeReuse/ActiveRecord def notes_of_any_type types = %w(commit issue merge_request snippet) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 3847a35fbab..acbcf0ded17 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -160,6 +160,8 @@ module ApplicationSettingsHelper :akismet_api_key, :akismet_enabled, :allow_local_requests_from_hooks_and_services, + :allow_local_requests_from_web_hooks_and_services, + :allow_local_requests_from_system_hooks, :dns_rebinding_protection_enabled, :archive_builds_in_human_readable, :authorized_keys_enabled, diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 6efd07a6008..b7a4d7aa803 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -21,7 +21,8 @@ module ApplicationSettingImplementation { after_sign_up_text: nil, akismet_enabled: false, - allow_local_requests_from_hooks_and_services: false, + allow_local_requests_from_web_hooks_and_services: false, + allow_local_requests_from_system_hooks: true, dns_rebinding_protection_enabled: true, authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand container_registry_token_expire_delay: 5, @@ -158,9 +159,20 @@ module ApplicationSettingImplementation end def outbound_local_requests_whitelist_raw=(values) + clear_memoization(:outbound_local_requests_whitelist_arrays) + self.outbound_local_requests_whitelist = domain_strings_to_array(values) end + def add_to_outbound_local_requests_whitelist(values_array) + clear_memoization(:outbound_local_requests_whitelist_arrays) + + self.outbound_local_requests_whitelist ||= [] + self.outbound_local_requests_whitelist += values_array + + self.outbound_local_requests_whitelist.uniq! + end + def outbound_local_requests_whitelist_arrays strong_memoize(:outbound_local_requests_whitelist_arrays) do next [[], []] unless self.outbound_local_requests_whitelist diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 07813e03f3a..ac88d9714ac 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -38,6 +38,7 @@ module Ci has_one :deployment, as: :deployable, class_name: 'Deployment' has_many :trace_sections, class_name: 'Ci::BuildTraceSection' has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id + has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id @@ -50,6 +51,7 @@ module Ci accepts_nested_attributes_for :runner_session accepts_nested_attributes_for :job_variables + accepts_nested_attributes_for :needs delegate :url, to: :runner_session, prefix: true, allow_nil: true delegate :terminal_specification, to: :runner_session, allow_nil: true @@ -713,11 +715,17 @@ module Ci depended_jobs = depends_on_builds - return depended_jobs unless options[:dependencies].present? + # find all jobs that are needed + if Feature.enabled?(:ci_dag_support, project) && needs.exists? + depended_jobs = depended_jobs.where(name: needs.select(:name)) + end - depended_jobs.select do |job| - options[:dependencies].include?(job.name) + # find all jobs that are dependent on + if options[:dependencies].present? + depended_jobs = depended_jobs.where(name: options[:dependencies]) end + + depended_jobs end def empty_dependencies? diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb new file mode 100644 index 00000000000..6531dfd332f --- /dev/null +++ b/app/models/ci/build_need.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Ci + class BuildNeed < ApplicationRecord + extend Gitlab::Ci::Model + + belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id, inverse_of: :needs + + validates :build, presence: true + validates :name, presence: true, length: { maximum: 128 } + + scope :scoped_build, -> { where('ci_builds.id=ci_build_needs.build_id') } + end +end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 1c76f401690..3b28eb246db 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -504,8 +504,9 @@ module Ci return [] unless config_processor strong_memoize(:stage_seeds) do - seeds = config_processor.stages_attributes.map do |attributes| - Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes) + seeds = config_processor.stages_attributes.inject([]) do |previous_stages, attributes| + seed = Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes, previous_stages) + previous_stages + [seed] end seeds.select(&:included?) @@ -611,8 +612,8 @@ module Ci end # rubocop: disable CodeReuse/ServiceClass - def process! - Ci::ProcessPipelineService.new(project, user).execute(self) + def process!(trigger_build_ids = nil) + Ci::ProcessPipelineService.new(project, user).execute(self, trigger_build_ids) end # rubocop: enable CodeReuse/ServiceClass diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index 7d5a6dec519..2fc1b67dfd2 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -24,12 +24,6 @@ module Clusters 'stable/cert-manager' end - # We will implement this in future MRs. - # Need to reverse postinstall step - def allowed_to_uninstall? - false - end - def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: 'certmanager', @@ -41,12 +35,42 @@ module Clusters ) end + def uninstall_command + Gitlab::Kubernetes::Helm::DeleteCommand.new( + name: 'certmanager', + rbac: cluster.platform_kubernetes_rbac?, + files: files, + postdelete: post_delete_script + ) + end + private def post_install_script ["kubectl create -f /data/helm/certmanager/config/cluster_issuer.yaml"] end + def post_delete_script + [ + delete_private_key, + delete_crd('certificates.certmanager.k8s.io'), + delete_crd('clusterissuers.certmanager.k8s.io'), + delete_crd('issuers.certmanager.k8s.io') + ].compact + end + + def private_key_name + @private_key_name ||= cluster_issuer_content.dig('spec', 'acme', 'privateKeySecretRef', 'name') + end + + def delete_private_key + "kubectl delete secret -n #{Gitlab::Kubernetes::Helm::NAMESPACE} #{private_key_name} --ignore-not-found" if private_key_name.present? + end + + def delete_crd(definition) + "kubectl delete crd #{definition} --ignore-not-found" + end + def cluster_issuer_file { 'cluster_issuer.yaml': cluster_issuer_yaml_content diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index a83d06c4b00..3a175fec148 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -14,6 +14,7 @@ module Clusters include ::Clusters::Concerns::ApplicationCore include ::Clusters::Concerns::ApplicationStatus + include ::Gitlab::Utils::StrongMemoize default_value_for :version, Gitlab::Kubernetes::Helm::HELM_VERSION @@ -29,11 +30,22 @@ module Clusters self.status = 'installable' if cluster&.platform_kubernetes_active? end - # We will implement this in future MRs. - # Basically we need to check all other applications are not installed - # first. + # It can only be uninstalled if there are no other applications installed + # or with intermitent installation statuses in the database. def allowed_to_uninstall? - false + strong_memoize(:allowed_to_uninstall) do + applications = nil + + Clusters::Cluster::APPLICATIONS.each do |application_name, klass| + next if application_name == 'helm' + + extra_apps = Clusters::Applications::Helm.where('EXISTS (?)', klass.select(1).where(cluster_id: cluster_id)) + + applications = applications.present? ? applications.or(extra_apps) : extra_apps + end + + !applications.exists? + end end def install_command @@ -44,6 +56,14 @@ module Clusters ) end + def uninstall_command + Gitlab::Kubernetes::Helm::ResetCommand.new( + name: name, + files: files, + rbac: cluster.platform_kubernetes_rbac? + ) + end + def has_ssl? ca_key.present? && ca_cert.present? end diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 96f526e8a36..5eae23659ae 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -84,7 +84,7 @@ module Clusters private def delete_knative_services_and_metrics - delete_knative_services + delete_knative_istio_metrics.to_a + delete_knative_services + delete_knative_istio_metrics end def delete_knative_services @@ -117,11 +117,15 @@ module Clusters end def install_knative_metrics - ["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available? + return [] unless cluster.application_prometheus_available? + + ["kubectl apply -f #{METRICS_CONFIG}"] end def delete_knative_istio_metrics - ["kubectl delete --ignore-not-found -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available? + return [] unless cluster.application_prometheus_available? + + ["kubectl delete --ignore-not-found -f #{METRICS_CONFIG}"] end def verify_cluster? diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index f5375d29f3a..5eb535cab58 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -64,7 +64,7 @@ module Clusters name: name, rbac: cluster.platform_kubernetes_rbac?, files: files, - predelete: delete_knative_istio_metrics.to_a + predelete: delete_knative_istio_metrics ) end @@ -104,11 +104,15 @@ module Clusters end def install_knative_metrics - ["kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] if cluster.application_knative_available? + return [] unless cluster.application_knative_available? + + ["kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] end def delete_knative_istio_metrics - ["kubectl delete -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] if cluster.application_knative_available? + return [] unless cluster.application_knative_available? + + ["kubectl delete -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index be6f3e9c5b0..a88cac6b8e6 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -43,6 +43,16 @@ class CommitStatus < ApplicationRecord scope :after_stage, -> (index) { where('stage_idx > ?', index) } scope :processables, -> { where(type: %w[Ci::Build Ci::Bridge]) } + scope :with_needs, -> (names = nil) do + needs = Ci::BuildNeed.scoped_build.select(1) + needs = needs.where(name: names) if names + where('EXISTS (?)', needs).preload(:needs) + end + + scope :without_needs, -> do + where('NOT EXISTS (?)', Ci::BuildNeed.scoped_build.select(1)) + end + # We use `CommitStatusEnums.failure_reasons` here so that EE can more easily # extend this `Hash` with new values. enum_with_nil failure_reason: ::CommitStatusEnums.failure_reasons @@ -116,7 +126,7 @@ class CommitStatus < ApplicationRecord commit_status.run_after_commit do if pipeline_id if complete? || manual? - PipelineProcessWorker.perform_async(pipeline_id) + PipelineProcessWorker.perform_async(pipeline_id, [id]) else PipelineUpdateWorker.perform_async(pipeline_id) end diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb index 9eed9492b37..304cc71e9dc 100644 --- a/app/models/concerns/ci/metadatable.rb +++ b/app/models/concerns/ci/metadatable.rb @@ -29,6 +29,7 @@ module Ci def degenerate! self.class.transaction do self.update!(options: nil, yaml_variables: nil) + self.needs.all.delete_all self.metadata&.destroy end end diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb index 2d09eff0111..195d9e107c5 100644 --- a/app/models/concerns/diff_positionable_note.rb +++ b/app/models/concerns/diff_positionable_note.rb @@ -10,6 +10,8 @@ module DiffPositionableNote serialize :original_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize serialize :change_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize + + validate :diff_refs_match_commit, if: :for_commit? end %i(original_position position change_position).each do |meth| @@ -71,4 +73,10 @@ module DiffPositionableNote self.position = result[:position] end end + + def diff_refs_match_commit + return if self.original_position.diff_refs == commit&.diff_refs + + errors.add(:commit_id, 'does not match the diff refs') + end end diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb index cfffd845e43..ed14b73ac1b 100644 --- a/app/models/concerns/group_descendant.rb +++ b/app/models/concerns/group_descendant.rb @@ -42,7 +42,7 @@ module GroupDescendant parent = child.parent exception = ArgumentError.new <<~MSG - parent: [GroupDescendant: #{parent.inspect}] was not preloaded for [#{child.inspect}]") + Parent was not preloaded for child when rendering group hierarchy. This error is not user facing, but causes a +1 query. MSG extras = { @@ -50,7 +50,7 @@ module GroupDescendant child: child.inspect, preloaded: preloaded.map(&:full_path) } - issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/40785' + issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/49404' Gitlab::Sentry.track_exception(exception, issue_url: issue_url, extra: extras) end diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index 9cd7b8d6258..6d3c7a7ed68 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -29,12 +29,8 @@ module RelativePositioning MAX_POSITION = Gitlab::Database::MAX_INT_VALUE IDEAL_DISTANCE = 500 - included do - after_save :save_positionable_neighbours - end - class_methods do - def move_to_end(objects) + def move_nulls_to_end(objects) objects = objects.reject(&:relative_position) return if objects.empty? @@ -43,7 +39,7 @@ module RelativePositioning self.transaction do objects.each do |object| - relative_position = position_between(max_relative_position, MAX_POSITION) + relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION) object.relative_position = relative_position max_relative_position = relative_position object.save(touch: false) @@ -114,11 +110,12 @@ module RelativePositioning return move_after(before) unless after return move_before(after) unless before - # If there is no place to insert an item we need to create one by moving the before item closer - # to its predecessor. This process will recursively move all the predecessors until we have a place + # If there is no place to insert an item we need to create one by moving the item + # before this and all preceding items until there is a gap + before, after = after, before if after.relative_position < before.relative_position if (after.relative_position - before.relative_position) < 2 - before.move_before - @positionable_neighbours = [before] # rubocop:disable Gitlab/ModuleWithInstanceVariables + after.move_sequence_before + before.reset end self.relative_position = self.class.position_between(before.relative_position, after.relative_position) @@ -128,12 +125,8 @@ module RelativePositioning pos_before = before.relative_position pos_after = before.next_relative_position - if before.shift_after? - item_to_move = self.class.relative_positioning_query_base(self).find_by!(relative_position: pos_after) - item_to_move.move_after - @positionable_neighbours = [item_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables - - pos_after = item_to_move.relative_position + if pos_after && (pos_after - pos_before) < 2 + before.move_sequence_after end self.relative_position = self.class.position_between(pos_before, pos_after) @@ -143,12 +136,8 @@ module RelativePositioning pos_after = after.relative_position pos_before = after.prev_relative_position - if after.shift_before? - item_to_move = self.class.relative_positioning_query_base(self).find_by!(relative_position: pos_before) - item_to_move.move_before - @positionable_neighbours = [item_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables - - pos_before = item_to_move.relative_position + if pos_before && (pos_after - pos_before) < 2 + after.move_sequence_before end self.relative_position = self.class.position_between(pos_before, pos_after) @@ -162,36 +151,82 @@ module RelativePositioning self.relative_position = self.class.position_between(min_relative_position || START_POSITION, MIN_POSITION) end - # Indicates if there is an item that should be shifted to free the place - def shift_after? - next_pos = next_relative_position - next_pos && (next_pos - relative_position) == 1 + # Moves the sequence before the current item to the middle of the next gap + # For example, we have 5 11 12 13 14 15 and the current item is 15 + # This moves the sequence 11 12 13 14 to 8 9 10 11 + def move_sequence_before + next_gap = find_next_gap_before + delta = optimum_delta_for_gap(next_gap) + + move_sequence(next_gap[:start], relative_position, -delta) end - # Indicates if there is an item that should be shifted to free the place - def shift_before? - prev_pos = prev_relative_position - prev_pos && (relative_position - prev_pos) == 1 + # Moves the sequence after the current item to the middle of the next gap + # For example, we have 11 12 13 14 15 21 and the current item is 11 + # This moves the sequence 12 13 14 15 to 15 16 17 18 + def move_sequence_after + next_gap = find_next_gap_after + delta = optimum_delta_for_gap(next_gap) + + move_sequence(relative_position, next_gap[:start], delta) end private - # rubocop:disable Gitlab/ModuleWithInstanceVariables - def save_positionable_neighbours - return unless @positionable_neighbours + # Supposing that we have a sequence of items: 1 5 11 12 13 and the current item is 13 + # This would return: `{ start: 11, end: 5 }` + def find_next_gap_before + items_with_next_pos = scoped_items + .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos') + .where('relative_position <= ?', relative_position) + .order(relative_position: :desc) + + find_next_gap(items_with_next_pos).tap do |gap| + gap[:end] ||= MIN_POSITION + end + end + + # Supposing that we have a sequence of items: 13 14 15 20 24 and the current item is 13 + # This would return: `{ start: 15, end: 20 }` + def find_next_gap_after + items_with_next_pos = scoped_items + .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos') + .where('relative_position >= ?', relative_position) + .order(:relative_position) - status = @positionable_neighbours.all? { |item| item.save(touch: false) } - @positionable_neighbours = nil + find_next_gap(items_with_next_pos).tap do |gap| + gap[:end] ||= MAX_POSITION + end + end + + def find_next_gap(items_with_next_pos) + gap = self.class.from(items_with_next_pos, :items_with_next_pos) + .where('ABS(pos - next_pos) > 1 OR next_pos IS NULL') + .limit(1) + .pluck(:pos, :next_pos) + .first + + { start: gap[0], end: gap[1] } + end - status + def optimum_delta_for_gap(gap) + delta = ((gap[:start] - gap[:end]) / 2.0).abs.ceil + + [delta, IDEAL_DISTANCE].min + end + + def move_sequence(start_pos, end_pos, delta) + scoped_items + .where.not(id: self.id) + .where('relative_position BETWEEN ? AND ?', start_pos, end_pos) + .update_all("relative_position = relative_position + #{delta}") end - # rubocop:enable Gitlab/ModuleWithInstanceVariables def calculate_relative_position(calculation) # When calculating across projects, this is much more efficient than # MAX(relative_position) without the GROUP BY, due to index usage: # https://gitlab.com/gitlab-org/gitlab-ce/issues/54276#note_119340977 - relation = self.class.relative_positioning_query_base(self) + relation = scoped_items .order(Gitlab::Database.nulls_last_order('position', 'DESC')) .group(self.class.relative_positioning_parent_column) .limit(1) @@ -203,4 +238,8 @@ module RelativePositioning .first&. last end + + def scoped_items + self.class.relative_positioning_query_base(self) + end end diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb index 570a735973f..869b3490f3f 100644 --- a/app/models/concerns/update_project_statistics.rb +++ b/app/models/concerns/update_project_statistics.rb @@ -73,15 +73,10 @@ module UpdateProjectStatistics def schedule_namespace_aggregation_worker run_after_commit do - next unless schedule_aggregation_worker? + next if project.nil? Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id) end end - - def schedule_aggregation_worker? - !project.nil? && - Feature.enabled?(:update_statistics_namespace, project.root_ancestor) - end end end diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index facd81dde80..2a5ae7930e6 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -70,10 +70,14 @@ class ContainerRepository < ApplicationRecord digests = tags.map { |tag| tag.digest }.to_set digests.all? do |digest| - client.delete_repository_tag(self.path, digest) + delete_tag_by_digest(digest) end end + def delete_tag_by_digest(digest) + client.delete_repository_tag(self.path, digest) + end + def self.build_from_path(path) self.new(project: path.repository_project, name: path.repository_name) diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index f75c32633b1..861185dc222 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -20,7 +20,6 @@ class DiffNote < Note validates :noteable_type, inclusion: { in: -> (_note) { noteable_types } } validate :positions_complete validate :verify_supported - validate :diff_refs_match_commit, if: :for_commit? before_validation :set_line_code, if: :on_text? after_save :keep_around_commits @@ -154,12 +153,6 @@ class DiffNote < Note errors.add(:position, "is invalid") end - def diff_refs_match_commit - return if self.original_position.diff_refs == self.commit.diff_refs - - errors.add(:commit_id, 'does not match the diff refs') - end - def keep_around_commits shas = [ self.original_position.base_sha, diff --git a/app/models/environment.rb b/app/models/environment.rb index 392481ea0cc..513427ac2c5 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -204,7 +204,7 @@ class Environment < ApplicationRecord public_path = project.public_path_for_source_path(path, commit_sha) return unless public_path - [external_url, public_path].join('/') + [external_url.delete_suffix('/'), public_path.delete_prefix('/')].join('/') end def expire_etag_cache diff --git a/app/models/group.rb b/app/models/group.rb index 74eb556b1b5..6c868b1d1f0 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -44,6 +44,8 @@ class Group < Namespace has_many :cluster_groups, class_name: 'Clusters::Group' has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster' + has_many :container_repositories, through: :projects + has_many :todos accepts_nested_attributes_for :variables, allow_destroy: true diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index 90b4588a325..3d54d17e787 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -14,8 +14,10 @@ class SystemHook < WebHook default_value_for :repository_update_events, true default_value_for :merge_requests_events, false + validates :url, system_hook_url: true + # Allow urls pointing localhost and the local network def allow_local_requests? - true + Gitlab::CurrentSettings.allow_local_requests_from_system_hooks? end end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index daf7ff4b771..16fc7fdbd48 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -15,8 +15,8 @@ class WebHook < ApplicationRecord has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - validates :url, presence: true, public_url: { allow_localhost: lambda(&:allow_local_requests?), - allow_local_network: lambda(&:allow_local_requests?) } + validates :url, presence: true + validates :url, public_url: true, unless: ->(hook) { hook.is_a?(SystemHook) } validates :token, format: { without: /\n/ } validates :push_events_branch_filter, branch_filter: true @@ -35,6 +35,6 @@ class WebHook < ApplicationRecord # Allow urls pointing localhost and the local network def allow_local_requests? - false + Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services? end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 8ade91933a4..5e8a6a7d5e5 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -752,7 +752,7 @@ class MergeRequest < ApplicationRecord end def check_mergeability - MergeRequests::MergeabilityCheckService.new(self).execute + MergeRequests::MergeabilityCheckService.new(self).execute(retry_lease: false) end # rubocop: enable CodeReuse/ServiceClass diff --git a/app/models/namespace/aggregation_schedule.rb b/app/models/namespace/aggregation_schedule.rb index 0bef352cf24..61a7eb4b576 100644 --- a/app/models/namespace/aggregation_schedule.rb +++ b/app/models/namespace/aggregation_schedule.rb @@ -6,21 +6,13 @@ class Namespace::AggregationSchedule < ApplicationRecord self.primary_key = :namespace_id - DEFAULT_LEASE_TIMEOUT = 3.hours + DEFAULT_LEASE_TIMEOUT = 1.5.hours.to_i REDIS_SHARED_KEY = 'gitlab:update_namespace_statistics_delay'.freeze belongs_to :namespace after_create :schedule_root_storage_statistics - def self.delay_timeout - redis_timeout = Gitlab::Redis::SharedState.with do |redis| - redis.get(REDIS_SHARED_KEY) - end - - redis_timeout.nil? ? DEFAULT_LEASE_TIMEOUT : redis_timeout.to_i - end - def schedule_root_storage_statistics run_after_commit_or_now do try_obtain_lease do @@ -28,7 +20,7 @@ class Namespace::AggregationSchedule < ApplicationRecord .perform_async(namespace_id) Namespaces::RootStatisticsWorker - .perform_in(self.class.delay_timeout, namespace_id) + .perform_in(DEFAULT_LEASE_TIMEOUT, namespace_id) end end end @@ -37,7 +29,7 @@ class Namespace::AggregationSchedule < ApplicationRecord # Used by ExclusiveLeaseGuard def lease_timeout - self.class.delay_timeout + DEFAULT_LEASE_TIMEOUT end # Used by ExclusiveLeaseGuard diff --git a/app/models/note.rb b/app/models/note.rb index 3f182c1f099..a12d1eb7243 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -27,6 +27,10 @@ class Note < ApplicationRecord def values constants.map {|const| self.const_get(const)} end + + def value?(val) + values.include?(val) + end end end diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 3802d258664..47999a3694e 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -93,13 +93,7 @@ class ProjectStatistics < ApplicationRecord def schedule_namespace_aggregation_worker run_after_commit do - next unless schedule_aggregation_worker? - Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id) end end - - def schedule_aggregation_worker? - Feature.enabled?(:update_statistics_namespace, project&.root_ancestor) - end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 84b1873c05d..52c944491bf 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -68,6 +68,7 @@ class GroupPolicy < BasePolicy rule { developer }.enable :admin_milestone rule { reporter }.policy do + enable :read_container_image enable :admin_label enable :admin_list enable :admin_issue diff --git a/app/serializers/analytics_issue_entity.rb b/app/serializers/analytics_issue_entity.rb index 29d4a6ae1d0..307ce14a921 100644 --- a/app/serializers/analytics_issue_entity.rb +++ b/app/serializers/analytics_issue_entity.rb @@ -26,6 +26,6 @@ class AnalyticsIssueEntity < Grape::Entity private def url_to(route, object) - public_send("#{route}_url", object[:path], object[:name], object[:iid].to_s) # rubocop:disable GitlabSecurity/PublicSend + public_send("#{route}_url", object[:namespace_path], object[:project_path], object[:iid].to_s) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 2111e1b5667..d988caea92d 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -2,4 +2,21 @@ class UserSerializer < BaseSerializer entity UserEntity + + def represent(resource, opts = {}, entity = nil) + if params[:merge_request_iid] + merge_request = opts[:project].merge_requests.find_by_iid!(params[:merge_request_iid]) + preload_max_member_access(merge_request.project, Array(resource)) + + super(resource, opts.merge(merge_request: merge_request), MergeRequestAssigneeEntity) + else + super + end + end + + private + + def preload_max_member_access(project, users) + project.team.max_member_access_for_user_ids(users.map(&:id)) + end end diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb index 7eeaf8aade1..471df6e2d0c 100644 --- a/app/services/application_settings/update_service.rb +++ b/app/services/application_settings/update_service.rb @@ -15,6 +15,8 @@ module ApplicationSettings update_terms(@params.delete(:terms)) + add_to_outbound_local_requests_whitelist(@params.delete(:add_to_outbound_local_requests_whitelist)) + if params.key?(:performance_bar_allowed_group_path) params[:performance_bar_allowed_group_id] = performance_bar_allowed_group_id end @@ -32,6 +34,13 @@ module ApplicationSettings params.key?(:usage_ping_enabled) || params.key?(:version_check_enabled) end + def add_to_outbound_local_requests_whitelist(values) + values_array = Array(values).reject(&:empty?) + return if values_array.empty? + + @application_setting.add_to_outbound_local_requests_whitelist(values_array) + end + def update_terms(terms) return unless terms.present? diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 707caee482c..0a069320936 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -17,6 +17,14 @@ module Auth end def self.full_access_token(*names) + access_token(%w(*), names) + end + + def self.pull_access_token(*names) + access_token(['pull'], names) + end + + def self.access_token(actions, names) names = names.flatten registry = Gitlab.config.registry token = JSONWebToken::RSAToken.new(registry.key) @@ -25,7 +33,7 @@ module Auth token.expire_time = token_expire_at token[:access] = names.map do |name| - { type: 'repository', name: name, actions: %w(*) } + { type: 'repository', name: name, actions: actions } end token.encoded diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 207cc5017d0..99d4ff9ecd1 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -4,35 +4,69 @@ module Ci class ProcessPipelineService < BaseService attr_reader :pipeline - def execute(pipeline) + def execute(pipeline, trigger_build_ids = nil) @pipeline = pipeline update_retried - new_builds = - stage_indexes_of_created_processables.flat_map do |index| - process_stage(index) - end + success = process_stages_without_needs + + # we evaluate dependent needs, + # only when the another job has finished + success = process_builds_with_needs(trigger_build_ids) || success @pipeline.update_status - new_builds.any? + success end private - def process_stage(index) + def process_stages_without_needs + stage_indexes_of_created_processables_without_needs.flat_map do |index| + process_stage_without_needs(index) + end.any? + end + + def process_stage_without_needs(index) current_status = status_for_prior_stages(index) - return if HasStatus::BLOCKED_STATUS.include?(current_status) + return unless HasStatus::COMPLETED_STATUSES.include?(current_status) - if HasStatus::COMPLETED_STATUSES.include?(current_status) - created_processables_in_stage(index).select do |build| - Gitlab::OptimisticLocking.retry_lock(build) do |subject| - Ci::ProcessBuildService.new(project, @user) - .execute(build, current_status) - end - end + created_processables_in_stage_without_needs(index).select do |build| + process_build(build, current_status) + end + end + + def process_builds_with_needs(trigger_build_ids) + return false unless trigger_build_ids.present? + return false unless Feature.enabled?(:ci_dag_support, project) + + # rubocop: disable CodeReuse/ActiveRecord + trigger_build_names = pipeline.statuses + .where(id: trigger_build_ids) + .select(:name) + # rubocop: enable CodeReuse/ActiveRecord + + created_processables + .with_needs(trigger_build_names) + .find_each + .map(&method(:process_build_with_needs)) + .any? + end + + def process_build_with_needs(build) + current_status = status_for_build_needs(build.needs.map(&:name)) + + return unless HasStatus::COMPLETED_STATUSES.include?(current_status) + + process_build(build, current_status) + end + + def process_build(build, current_status) + Gitlab::OptimisticLocking.retry_lock(build) do |subject| + Ci::ProcessBuildService.new(project, @user) + .execute(subject, current_status) end end @@ -43,17 +77,33 @@ module Ci # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord - def stage_indexes_of_created_processables - created_processables.order(:stage_idx).pluck(Arel.sql('DISTINCT stage_idx')) + def status_for_build_needs(needs) + pipeline.builds.where(name: needs).latest.status || 'success' end # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord - def created_processables_in_stage(index) - created_processables.where(stage_idx: index) + def stage_indexes_of_created_processables_without_needs + created_processables_without_needs.order(:stage_idx) + .pluck(Arel.sql('DISTINCT stage_idx')) end # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord + def created_processables_in_stage_without_needs(index) + created_processables_without_needs + .where(stage_idx: index) + end + # rubocop: enable CodeReuse/ActiveRecord + + def created_processables_without_needs + if Feature.enabled?(:ci_dag_support, project) + pipeline.processables.created.without_needs + else + pipeline.processables.created + end + end + def created_processables pipeline.processables.created end diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index fab8a179843..338495ba030 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -5,7 +5,7 @@ module Ci CLONE_ACCESSORS = %i[pipeline project ref tag options name allow_failure stage stage_id stage_idx trigger_request yaml_variables when environment coverage_regex - description tag_list protected].freeze + description tag_list protected needs].freeze def execute(build) reprocess!(build).tap do |new_build| diff --git a/app/services/merge_requests/mergeability_check_service.rb b/app/services/merge_requests/mergeability_check_service.rb index 9fa50c9448f..962e2327b3e 100644 --- a/app/services/merge_requests/mergeability_check_service.rb +++ b/app/services/merge_requests/mergeability_check_service.rb @@ -3,6 +3,7 @@ module MergeRequests class MergeabilityCheckService < ::BaseService include Gitlab::Utils::StrongMemoize + include Gitlab::ExclusiveLeaseHelpers delegate :project, to: :@merge_request delegate :repository, to: :project @@ -21,13 +22,35 @@ module MergeRequests # where we need the current state of the merge ref in repository, the `recheck` # argument is required. # + # retry_lease - Concurrent calls wait for at least 10 seconds until the + # lease is granted (other process finishes running). Returns an error + # ServiceResponse if the lease is not granted during this time. + # # Returns a ServiceResponse indicating merge_status is/became can_be_merged # and the merge-ref is synced. Success in case of being/becoming mergeable, # error otherwise. - def execute(recheck: false) + def execute(recheck: false, retry_lease: true) return ServiceResponse.error(message: 'Invalid argument') unless merge_request return ServiceResponse.error(message: 'Unsupported operation') if Gitlab::Database.read_only? + return check_mergeability(recheck) unless merge_ref_auto_sync_lock_enabled? + + in_write_lock(retry_lease: retry_lease) do |retried| + # When multiple calls are waiting for the same lock (retry_lease), + # it's possible that when granted, the MR status was already updated for + # that object, therefore we reset if there was a lease retry. + merge_request.reset if retried + + check_mergeability(recheck) + end + rescue FailedToObtainLockError => error + ServiceResponse.error(message: error.message) + end + + private + + attr_reader :merge_request + def check_mergeability(recheck) recheck! if recheck update_merge_status @@ -46,9 +69,21 @@ module MergeRequests ServiceResponse.success(payload: payload) end - private + # It's possible for this service to send concurrent requests to Gitaly in order + # to "git update-ref" the same ref. Therefore we handle a light exclusive + # lease here. + # + def in_write_lock(retry_lease:, &block) + lease_key = "mergeability_check:#{merge_request.id}" - attr_reader :merge_request + lease_opts = { + ttl: 1.minute, + retries: retry_lease ? 10 : 0, + sleep_sec: retry_lease ? 1.second : 0 + } + + in_lock(lease_key, lease_opts, &block) + end def payload strong_memoize(:payload) do @@ -116,5 +151,9 @@ module MergeRequests def merge_ref_auto_sync_enabled? Feature.enabled?(:merge_ref_auto_sync, project, default_enabled: true) end + + def merge_ref_auto_sync_lock_enabled? + Feature.enabled?(:merge_ref_auto_sync_lock, project, default_enabled: true) + end end end diff --git a/app/services/self_monitoring/project/create_service.rb b/app/services/self_monitoring/project/create_service.rb index e5ef8c15456..8ffd22de127 100644 --- a/app/services/self_monitoring/project/create_service.rb +++ b/app/services/self_monitoring/project/create_service.rb @@ -14,6 +14,7 @@ module SelfMonitoring steps :validate_admins, :create_project, :add_project_members, + :add_to_whitelist, :add_prometheus_manual_configuration def initialize @@ -59,15 +60,29 @@ module SelfMonitoring end end - def add_prometheus_manual_configuration + def add_to_whitelist return success unless prometheus_enabled? return success unless prometheus_listen_address.present? - # TODO: Currently, adding the internal prometheus server as a manual configuration - # is only possible if the setting to allow webhooks and services to connect - # to local network is on. - # https://gitlab.com/gitlab-org/gitlab-ce/issues/44496 will add - # a whitelist that will allow connections to certain ips on the local network. + uri = parse_url(internal_prometheus_listen_address_uri) + return error(_('Prometheus listen_address is not a valid URI')) unless uri + + result = ApplicationSettings::UpdateService.new( + Gitlab::CurrentSettings.current_application_settings, + project_owner, + outbound_local_requests_whitelist: [uri.normalized_host] + ).execute + + if result + success + else + error(_('Could not add prometheus URL to whitelist')) + end + end + + def add_prometheus_manual_configuration + return success unless prometheus_enabled? + return success unless prometheus_listen_address.present? service = project.find_or_initialize_service('prometheus') @@ -79,6 +94,11 @@ module SelfMonitoring success end + def parse_url(uri_string) + Addressable::URI.parse(uri_string) + rescue Addressable::URI::InvalidURIError, TypeError + end + def prometheus_enabled? Gitlab.config.prometheus.enable rescue Settingslogic::MissingSetting diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index 6d675c026bb..8c294218708 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -17,8 +17,10 @@ class WebHookService @hook = hook @data = data @hook_name = hook_name.to_s - @request_options = { timeout: Gitlab.config.gitlab.webhook_timeout } - @request_options.merge!(allow_local_requests: true) if @hook.is_a?(SystemHook) + @request_options = { + timeout: Gitlab.config.gitlab.webhook_timeout, + allow_local_requests: hook.allow_local_requests? + } end def execute diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb index 273e15ef925..bb445499cee 100644 --- a/app/validators/addressable_url_validator.rb +++ b/app/validators/addressable_url_validator.rb @@ -107,6 +107,6 @@ class AddressableUrlValidator < ActiveModel::EachValidator # calls this validator. # # See https://gitlab.com/gitlab-org/gitlab-ee/issues/9833 - ApplicationSetting.current&.allow_local_requests_from_hooks_and_services? + ApplicationSetting.current&.allow_local_requests_from_web_hooks_and_services? end end diff --git a/app/validators/system_hook_url_validator.rb b/app/validators/system_hook_url_validator.rb new file mode 100644 index 00000000000..f4253006dad --- /dev/null +++ b/app/validators/system_hook_url_validator.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# SystemHookUrlValidator +# +# Custom validator specific to SystemHook URLs. This validator works like AddressableUrlValidator but +# it blocks urls pointing to localhost or the local network depending on +# ApplicationSetting.allow_local_requests_from_system_hooks +# +# Example: +# class SystemHook < WebHook +# validates :url, system_hook_url: true +# end +# +class SystemHookUrlValidator < PublicUrlValidator + def self.allow_setting_local_requests? + ApplicationSetting.current&.allow_local_requests_from_system_hooks? + end +end diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml index 4fecdb59e1d..ad26f52aea7 100644 --- a/app/views/admin/application_settings/_outbound.html.haml +++ b/app/views/admin/application_settings/_outbound.html.haml @@ -4,9 +4,13 @@ %fieldset .form-group .form-check - = f.check_box :allow_local_requests_from_hooks_and_services, class: 'form-check-input' - = f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do - Allow requests to the local network from hooks and services + = f.check_box :allow_local_requests_from_web_hooks_and_services, class: 'form-check-input' + = f.label :allow_local_requests_from_web_hooks_and_services, class: 'form-check-label' do + = _('Allow requests to the local network from web hooks and services') + .form-check + = f.check_box :allow_local_requests_from_system_hooks, class: 'form-check-input' + = f.label :allow_local_requests_from_system_hooks, class: 'form-check-label' do + = _('Allow requests to the local network from system hooks') .form-group = f.label :outbound_local_requests_whitelist_raw, class: 'label-bold' do diff --git a/app/views/clusters/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml index a5de67be96b..4b4278075a6 100644 --- a/app/views/clusters/clusters/_banner.html.haml +++ b/app/views/clusters/clusters/_banner.html.haml @@ -3,7 +3,8 @@ %p.js-error-reason .hidden.js-cluster-creating.bs-callout.bs-callout-info{ role: 'alert' } - = s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...') + %span.spinner.spinner-dark.spinner-sm{ 'aria-label': 'Loading' } + %span.prepend-left-4= s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...') .hidden.row.js-cluster-api-unreachable.bs-callout.bs-callout-warning{ role: 'alert' } .col-11 @@ -18,4 +19,4 @@ %button.js-close-banner.close.cluster-application-banner-close.h-100.m-0= "×" .hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' } - = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details") + = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine.") diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index 4dfbb310142..913d4caa0bc 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -33,26 +33,29 @@ %section#cluster-integration %h4= @cluster.name = render 'banner' - = render 'form' - - = render_if_exists 'projects/clusters/prometheus_graphs' - - .cluster-applications-table#js-cluster-applications - - %section.settings#js-cluster-details{ class: ('expanded' if expanded) } - .settings-header - %h4= s_('ClusterIntegration|Kubernetes cluster details') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster') - .settings-content - = render 'clusters/platforms/kubernetes/form', cluster: @cluster, platform: @cluster.platform_kubernetes, update_cluster_url_path: clusterable.cluster_path(@cluster) - - %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) } - .settings-header - %h4= _('Advanced settings') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration") - .settings-content#advanced-settings-section - = render 'advanced_settings' + + - unless @cluster.status_name.in? %i/scheduled creating/ + = render 'form' + + - unless @cluster.status_name.in? %i/scheduled creating/ + = render_if_exists 'projects/clusters/prometheus_graphs' + + .cluster-applications-table#js-cluster-applications + + %section.settings#js-cluster-details{ class: ('expanded' if expanded) } + .settings-header + %h4= s_('ClusterIntegration|Kubernetes cluster details') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster') + .settings-content + = render 'clusters/platforms/kubernetes/form', cluster: @cluster, platform: @cluster.platform_kubernetes, update_cluster_url_path: clusterable.cluster_path(@cluster) + + %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) } + .settings-header + %h4= _('Advanced settings') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration") + .settings-content#advanced-settings-section + = render 'advanced_settings' diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml index 67f2f897137..35b32662b8a 100644 --- a/app/views/explore/projects/_projects.html.haml +++ b/app/views/explore/projects/_projects.html.haml @@ -1 +1,2 @@ -= render 'shared/projects/list', projects: projects, user: current_user +- is_explore_page = defined?(explore_page) && explore_page += render 'shared/projects/list', projects: projects, user: current_user, explore_page: is_explore_page diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index ed508fa2506..153c90e534e 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -10,4 +10,4 @@ = render 'explore/head' = render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user -= render 'projects', projects: @projects += render 'projects', projects: @projects, explore_page: true diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index efb3815b257..46d7c367aa7 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -24,11 +24,11 @@ %td.shortcut %kbd f %td Focus Filter - - if performance_bar_enabled? - %tr - %td.shortcut - %kbd p b - %td Show/hide the Performance Bar + %tr + %td.shortcut + %kbd p + %kbd b + %td Toggle the Performance Bar %tr %td.shortcut %kbd ? diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 20b844f9fd8..ac774803f95 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -78,4 +78,3 @@ = render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id') = render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id') = render_if_exists 'layouts/snowplow' - = render_if_exists 'layouts/pendo' if Feature.enabled?(:pendo_tracking) && !Rails.env.test? diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml index d95045c9cce..f9222387e97 100644 --- a/app/views/projects/_flash_messages.html.haml +++ b/app/views/projects/_flash_messages.html.haml @@ -5,7 +5,7 @@ - if current_user && can?(current_user, :download_code, project) = render 'shared/no_ssh' = render 'shared/no_password' - = render_if_exists 'shared/shared_runners_minutes_limit', project: project - unless project.empty_repo? = render 'shared/auto_devops_implicitly_enabled_banner', project: project = render_if_exists 'projects/above_size_limit_warning', project: project + = render_if_exists 'shared/shared_runners_minutes_limit', project: project, classes: [container_class, ("limit-container-width" unless fluid_layout)] diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index c7fab87a593..a3688c17041 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -7,9 +7,10 @@ = stylesheet_link_tag 'page_bundles/xterm' %div{ class: container_class } - #js-job-vue-app{ data: { endpoint: project_job_path(@project, @build, format: :json), + #js-job-vue-app{ data: { endpoint: project_job_path(@project, @build, format: :json), project_path: @project.full_path, deployment_help_url: help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting-failed-deployment-jobs'), runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'), runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings'), variables_settings_url: project_variables_path(@build.project, anchor: 'js-cicd-variables-settings'), + page_path: project_job_path(@project, @build), build_status: @build.status, build_stage: @build.stage, log_state: '', build_options: javascript_build_options } } diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml index 33e5a6e67c3..1a163cc4a54 100644 --- a/app/views/projects/mirrors/_instructions.html.haml +++ b/app/views/projects/mirrors/_instructions.html.haml @@ -2,7 +2,7 @@ %ul %li = _('The repository must be accessible over <code>http://</code>, - <code>https://</code>, <code>ssh://</code> and <code>git://</code>.').html_safe + <code>https://</code>, <code>ssh://</code> or <code>git://</code>.').html_safe %li= _('Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.').html_safe %li - minutes = Gitlab.config.gitlab_shell.git_timeout / 60 diff --git a/app/views/projects/services/prometheus/_metrics.html.haml b/app/views/projects/services/prometheus/_metrics.html.haml index a1d74b91002..3aefb3fdbb9 100644 --- a/app/views/projects/services/prometheus/_metrics.html.haml +++ b/app/views/projects/services/prometheus/_metrics.html.haml @@ -1,28 +1,34 @@ - project = local_assigns.fetch(:project) -.card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/index') } } - .card-header - = s_('PrometheusService|Common metrics') - %span.badge.badge-pill.js-monitored-count 0 - .card-body - .loading-metrics.js-loading-metrics - %p.prepend-top-10.prepend-left-10 - = icon('spinner spin', class: 'metrics-load-spinner') - = s_('PrometheusService|Finding and configuring metrics...') - .empty-metrics.hidden.js-empty-metrics - %p.text-tertiary.prepend-top-10.prepend-left-10 - = s_('PrometheusService|Waiting for your first deployment to an environment to find common metrics') - %ul.list-unstyled.metrics-list.hidden.js-metrics-list +.col-lg-3 + %p + = s_('PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters.') + = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus_library/index'), target: '_blank', rel: "noopener noreferrer" -.card.hidden.js-panel-missing-env-vars - .card-header - = icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel') - = s_('PrometheusService|Missing environment variable') - %span.badge.badge-pill.js-env-var-count 0 - .card-body.hidden - .flash-container - .flash-notice - .flash-text - = s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe - = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels') - %ul.list-unstyled.metrics-list.js-missing-var-metrics-list +.col-lg-9 + .card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/index') } } + .card-header + = s_('PrometheusService|Common metrics') + %span.badge.badge-pill.js-monitored-count 0 + .card-body + .loading-metrics.js-loading-metrics + %p.prepend-top-10.prepend-left-10 + = icon('spinner spin', class: 'metrics-load-spinner') + = s_('PrometheusService|Finding and configuring metrics...') + .empty-metrics.hidden.js-empty-metrics + %p.text-tertiary.prepend-top-10.prepend-left-10 + = s_('PrometheusService|Waiting for your first deployment to an environment to find common metrics') + %ul.list-unstyled.metrics-list.hidden.js-metrics-list + + .card.hidden.js-panel-missing-env-vars + .card-header + = icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel') + = s_('PrometheusService|Missing environment variable') + %span.badge.badge-pill.js-env-var-count 0 + .card-body.hidden + .flash-container + .flash-notice + .flash-text + = s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe + = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels') + %ul.list-unstyled.metrics-list.js-missing-var-metrics-list diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml index 6aafa85e99a..c719661d8e8 100644 --- a/app/views/projects/services/prometheus/_show.html.haml +++ b/app/views/projects/services/prometheus/_show.html.haml @@ -1,12 +1,9 @@ -.row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring +.row .col-lg-3 %h4.prepend-top-0 = s_('PrometheusService|Metrics') - %p - = s_('PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters.') - = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus_library/index'), target: '_blank', rel: "noopener noreferrer" - .col-lg-9 - = render 'projects/services/prometheus/metrics', project: @project +.row.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring + = render 'projects/services/prometheus/metrics', project: @project = render_if_exists 'projects/services/prometheus/external_alerts', project: @project diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 2d108a1cba5..498a9744783 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -99,7 +99,7 @@ %code \(\d+.\d+\%\) covered %li pytest-cov (Python) - - %code ^TOTAL\s+\d+\s+\d+\s+(\d+\%)$ + %code ^TOTAL.+?(\d+\%)$ %li phpunit --coverage-text --colors=never (PHP) - %code ^\s*Lines:\s*\d+.\d+\% diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index e253413929a..c9458475aa5 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -1,6 +1,7 @@ - type = local_assigns.fetch(:type) - board = local_assigns.fetch(:board, nil) -- block_css_class = type != :boards_modal ? 'row-content-block second-block' : '' +- is_not_boards_modal_or_productivity_analytics = type != :boards_modal && type != :productivity_analytics +- block_css_class = is_not_boards_modal_or_productivity_analytics ? 'row-content-block second-block' : '' - user_can_admin_list = board && can?(current_user, :admin_list, board.parent) .issues-filters{ class: ("w-100" if type == :boards_modal) } @@ -155,5 +156,5 @@ - if @project #js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } } #js-toggle-focus-btn - - elsif type != :boards_modal + - elsif is_not_boards_modal_or_productivity_analytics = render 'shared/issuable/sort_dropdown' diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml index df0523595f5..8260915c2ab 100644 --- a/app/views/shared/issuable/_sort_dropdown.html.haml +++ b/app/views/shared/issuable/_sort_dropdown.html.haml @@ -1,7 +1,7 @@ - sort_value = @sort - sort_title = issuable_sort_option_title(sort_value) - viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues' -- manual_sorting = viewing_issues && controller.controller_name != 'dashboard' && Feature.enabled?(:manual_sorting, default_enabled: true) +- manual_sorting = viewing_issues && controller.controller_name != 'dashboard' .dropdown.inline.prepend-left-10.issue-sort-dropdown .btn-group{ role: 'group' } diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index 43503e1d08a..fd3317341f6 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -53,7 +53,7 @@ - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.' %span All issues for this milestone are closed. #{close_msg} -= render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project += render_if_exists 'shared/milestones/burndown', milestone: milestone, project: @project - if is_dynamic_milestone .table-holder diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml index 72ede50dd8c..fae7d6526e8 100644 --- a/app/views/shared/notes/_hints.html.haml +++ b/app/views/shared/notes/_hints.html.haml @@ -1,13 +1,14 @@ - supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) .comment-toolbar.clearfix .toolbar-text - - md_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" tabindex="-1">'.html_safe % { url: help_page_path('user/markdown') } - - actions_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" tabindex="-1">'.html_safe % { url: help_page_path('user/project/quick_actions') } - - link_end = '</a>'.html_safe + = link_to _('Markdown'), help_page_path('user/markdown'), target: '_blank', tabindex: -1 - if supports_quick_actions - = s_('Editor|%{mdLinkStart}Markdown%{mdLinkEnd} and %{actionsLinkStart}quick actions%{actionsLinkEnd} are supported').html_safe % { mdLinkStart: md_link_start, mdLinkEnd: link_end, actionsLinkStart: actions_link_start, actionsLinkEnd: link_end } + and + = link_to _('quick actions'), help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1 + are - else - = s_('Editor|%{mdLinkStart}Markdown is supported%{mdLinkEnd}').html_safe % { mdLinkStart: md_link_start, mdLinkEnd: link_end } + is + supported %span.uploading-container %span.uploading-progress-container.hide diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 576ec3e1782..67cb1aa549c 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -21,6 +21,7 @@ - own_projects_current_user_empty_message_header = s_('UserProfile|You haven\'t created any personal projects.') - own_projects_current_user_empty_message_description = s_('UserProfile|Your projects can be available publicly, internally, or privately, at your choice.') - own_projects_visitor_empty_message = s_('UserProfile|This user doesn\'t have any personal projects') +- explore_page_empty_message = s_('UserProfile|Explore public groups to find projects to contribute to.') - primary_button_label = _('New project') - primary_button_link = new_project_path - secondary_button_label = _('Explore groups') @@ -58,4 +59,4 @@ current_user_empty_message_description: own_projects_current_user_empty_message_description, primary_button_label: primary_button_label, primary_button_link: primary_button_link, - visitor_empty_message: own_projects_visitor_empty_message } + visitor_empty_message: defined?(explore_page) && explore_page ? explore_page_empty_message : own_projects_visitor_empty_message } diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml index f6724f72307..ef3835332a7 100644 --- a/app/views/u2f/_register.html.haml +++ b/app/views/u2f/_register.html.haml @@ -16,7 +16,7 @@ .col-md-4 %button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true }= _("Set up new U2F device") .col-md-8 - %p.text-warning= _("You need to register a two-factor authentication app before you can set up a U2F device.") + %p= _("You need to register a two-factor authentication app before you can set up a U2F device.") %script#js-register-u2f-in-progress{ type: "text/template" } %p= _("Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.") diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 991a177018e..400becdd023 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -88,6 +88,7 @@ - pipeline_processing:ci_build_prepare - pipeline_processing:build_queue - pipeline_processing:build_success +- pipeline_processing:build_process - pipeline_processing:pipeline_process - pipeline_processing:pipeline_success - pipeline_processing:pipeline_update diff --git a/app/workers/build_process_worker.rb b/app/workers/build_process_worker.rb new file mode 100644 index 00000000000..9cd9519df1f --- /dev/null +++ b/app/workers/build_process_worker.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class BuildProcessWorker + include ApplicationWorker + include PipelineQueue + + queue_namespace :pipeline_processing + + # rubocop: disable CodeReuse/ActiveRecord + def perform(build_id) + CommitStatus.find_by(id: build_id).try do |build| + build.pipeline.process!([build_id]) + end + end + # rubocop: enable CodeReuse/ActiveRecord +end diff --git a/app/workers/namespaces/root_statistics_worker.rb b/app/workers/namespaces/root_statistics_worker.rb index 48876825564..0c1ca5eb975 100644 --- a/app/workers/namespaces/root_statistics_worker.rb +++ b/app/workers/namespaces/root_statistics_worker.rb @@ -9,7 +9,7 @@ module Namespaces def perform(namespace_id) namespace = Namespace.find(namespace_id) - return unless update_statistics_enabled_for?(namespace) && namespace.aggregation_scheduled? + return unless namespace.aggregation_scheduled? Namespaces::StatisticsRefresherService.new.execute(namespace) @@ -23,9 +23,5 @@ module Namespaces def log_error(namespace_path, error_message) Gitlab::SidekiqLogger.error("Namespace statistics can't be updated for #{namespace_path}: #{error_message}") end - - def update_statistics_enabled_for?(namespace) - Feature.enabled?(:update_statistics_namespace, namespace) - end end end diff --git a/app/workers/namespaces/schedule_aggregation_worker.rb b/app/workers/namespaces/schedule_aggregation_worker.rb index a4594b84b13..983ce4bef4a 100644 --- a/app/workers/namespaces/schedule_aggregation_worker.rb +++ b/app/workers/namespaces/schedule_aggregation_worker.rb @@ -12,7 +12,7 @@ module Namespaces namespace = Namespace.find(namespace_id) root_ancestor = namespace.root_ancestor - return unless update_statistics_enabled_for?(root_ancestor) && !root_ancestor.aggregation_scheduled? + return if root_ancestor.aggregation_scheduled? Namespace::AggregationSchedule.safe_find_or_create_by!(namespace_id: root_ancestor.id) rescue ActiveRecord::RecordNotFound @@ -37,9 +37,5 @@ module Namespaces def log_error(root_ancestor_id) Gitlab::SidekiqLogger.error("Namespace can't be scheduled for aggregation: #{root_ancestor_id} does not exist") end - - def update_statistics_enabled_for?(root_ancestor) - Feature.enabled?(:update_statistics_namespace, root_ancestor) - end end end diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb index f2aa17acb51..96524d93f8d 100644 --- a/app/workers/pipeline_process_worker.rb +++ b/app/workers/pipeline_process_worker.rb @@ -7,9 +7,10 @@ class PipelineProcessWorker queue_namespace :pipeline_processing # rubocop: disable CodeReuse/ActiveRecord - def perform(pipeline_id) - Ci::Pipeline.find_by(id: pipeline_id) - .try(:process!) + def perform(pipeline_id, build_ids = nil) + Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| + pipeline.process!(build_ids) + end end # rubocop: enable CodeReuse/ActiveRecord end |