diff options
Diffstat (limited to 'app')
110 files changed, 555 insertions, 919 deletions
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index 9e69c7d7164..37e348d93d3 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -1,4 +1,4 @@ -/* eslint-disable class-methods-use-this */ +/* eslint-disable class-methods-use-this, no-unused-vars */ import $ from 'jquery'; @@ -61,7 +61,7 @@ export default class TemplateSelector { return this.requestFile(item); } - requestFile() { + requestFile(item) { // This `requestFile` method is an abstract method that should // be added by all subclasses. } diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 28850710f80..d386960f3b6 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -111,15 +111,25 @@ export default class Clusters { this.initApplications(clusterType); this.initEnvironments(); - if (clusterEnvironmentsPath) { - this.fetchEnvironments(); + if (clusterEnvironmentsPath && this.environments) { + this.store.toggleFetchEnvironments(true); + + this.initPolling( + 'fetchClusterEnvironments', + data => this.handleClusterEnvironmentsSuccess(data), + () => this.handleEnvironmentsPollError(), + ); } this.updateContainer(null, this.store.state.status, this.store.state.statusReason); this.addListeners(); if (statusPath && !this.environments) { - this.initPolling(); + this.initPolling( + 'fetchClusterStatus', + data => this.handleClusterStatusSuccess(data), + () => this.handlePollError(), + ); } } @@ -179,16 +189,9 @@ export default class Clusters { }); } - fetchEnvironments() { - this.store.toggleFetchEnvironments(true); - - this.service - .fetchClusterEnvironments() - .then(data => { - this.store.toggleFetchEnvironments(false); - this.store.updateEnvironments(data.data); - }) - .catch(() => Clusters.handleError()); + handleClusterEnvironmentsSuccess(data) { + this.store.toggleFetchEnvironments(false); + this.store.updateEnvironments(data.data); } static initDismissableCallout() { @@ -224,21 +227,16 @@ export default class Clusters { eventHub.$off('uninstallApplication'); } - initPolling() { + initPolling(method, successCallback, errorCallback) { this.poll = new Poll({ resource: this.service, - method: 'fetchData', - successCallback: data => this.handleSuccess(data), - errorCallback: () => Clusters.handleError(), + method, + successCallback, + errorCallback, }); if (!Visibility.hidden()) { this.poll.makeRequest(); - } else { - this.service - .fetchData() - .then(data => this.handleSuccess(data)) - .catch(() => Clusters.handleError()); } Visibility.change(() => { @@ -250,11 +248,21 @@ export default class Clusters { }); } + handlePollError() { + this.constructor.handleError(); + } + + handleEnvironmentsPollError() { + this.store.toggleFetchEnvironments(false); + + this.handlePollError(); + } + static handleError() { Flash(s__('ClusterIntegration|Something went wrong on our end.')); } - handleSuccess(data) { + handleClusterStatusSuccess(data) { const prevStatus = this.store.state.status; const prevApplicationMap = Object.assign({}, this.store.state.applications); diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js index 9139e0beafb..fa12802b3de 100644 --- a/app/assets/javascripts/clusters/services/clusters_service.js +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -17,7 +17,7 @@ export default class ClusterService { }; } - fetchData() { + fetchClusterStatus() { return axios.get(this.options.endpoint); } diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index a032f589ee4..5cddb4cc098 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -218,6 +218,7 @@ export default class ClusterStore { environmentPath: environment.environment_path, lastDeployment: environment.last_deployment, rolloutStatus: { + status: environment.rollout_status ? environment.rollout_status.status : null, instances: environment.rollout_status ? environment.rollout_status.instances : [], }, updatedAt: environment.updated_at, diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 9454f760df8..bc666aef54b 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, one-var, no-return-assign, no-unused-expressions, no-sequences */ +/* eslint-disable func-names, no-var, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, one-var, no-unused-vars, no-return-assign, no-unused-expressions, no-sequences */ import $ from 'jquery'; @@ -12,8 +12,11 @@ export default class ImageFile { this.requestImageInfo( $('.two-up.view .frame.deleted img', this.file), (function(_this) { - return function() { - return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function() { + return function(deletedWidth, deletedHeight) { + return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function( + width, + height, + ) { _this.initViewModes(); // Load two-up view after images are loaded @@ -109,7 +112,7 @@ export default class ImageFile { maxHeight = 0; $('.frame', view) .each( - (function() { + (function(_this) { return function(index, frame) { var height, width; width = $(frame).width(); @@ -193,7 +196,13 @@ export default class ImageFile { return $('.onion-skin.view', this.file).each( (function(_this) { return function(index, view) { - var $frame, $track, $dragger, $frameAdded, framePadding, ref; + var $frame, + $track, + $dragger, + $frameAdded, + framePadding, + ref, + dragging = false; (ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref); $frame = $('.onion-skin-frame', view); $frameAdded = $('.frame.added', view); diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue deleted file mode 100644 index f9465da6fda..00000000000 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue +++ /dev/null @@ -1,182 +0,0 @@ -<script> -import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue'; -import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; -import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; - -export default { - components: { - DropdownButton, - DropdownSearchInput, - DropdownHiddenInput, - }, - props: { - fieldName: { - type: String, - required: false, - default: '', - }, - placeholder: { - type: String, - required: false, - default: '', - }, - defaultValue: { - type: String, - required: false, - default: '', - }, - value: { - type: Object, - required: false, - default: () => null, - }, - labelProperty: { - type: String, - required: false, - default: 'name', - }, - valueProperty: { - type: String, - required: false, - default: 'value', - }, - items: { - type: Array, - required: false, - default: () => [], - }, - loading: { - type: Boolean, - required: false, - default: false, - }, - disabled: { - type: Boolean, - required: false, - default: false, - }, - loadingText: { - type: String, - required: false, - default: '', - }, - disabledText: { - type: String, - required: false, - default: '', - }, - hasErrors: { - type: Boolean, - required: false, - default: false, - }, - errorMessage: { - type: String, - required: false, - default: '', - }, - searchFieldPlaceholder: { - type: String, - required: false, - default: '', - }, - emptyText: { - type: String, - required: false, - default: '', - }, - searchFn: { - type: Function, - required: false, - default: searchQuery => item => item.name.toLowerCase().indexOf(searchQuery) > -1, - }, - }, - data() { - return { - searchQuery: '', - selectedItem: null, - }; - }, - computed: { - toggleText() { - if (this.loading && this.loadingText) { - return this.loadingText; - } - - if (this.disabled && this.disabledText) { - return this.disabledText; - } - - if (!this.selectedItem) { - return this.placeholder; - } - - return this.selectedItemLabel; - }, - results() { - if (!this.items) { - return []; - } - - return this.items.filter(this.searchFn(this.searchQuery)); - }, - selectedItemLabel() { - return this.selectedItem && this.selectedItem[this.labelProperty]; - }, - selectedItemValue() { - return (this.selectedItem && this.selectedItem[this.valueProperty]) || ''; - }, - }, - methods: { - select(item) { - this.selectedItem = item; - this.$emit('input', item); - }, - }, -}; -</script> - -<template> - <div> - <div class="js-gcp-machine-type-dropdown dropdown"> - <dropdown-hidden-input :name="fieldName" :value="selectedItemValue" /> - <dropdown-button - :class="{ 'border-danger': hasErrors }" - :is-disabled="disabled" - :is-loading="loading" - :toggle-text="toggleText" - /> - <div class="dropdown-menu dropdown-select"> - <dropdown-search-input v-model="searchQuery" :placeholder-text="searchFieldPlaceholder" /> - <div class="dropdown-content"> - <ul> - <li v-if="!results.length"> - <span class="js-empty-text menu-item"> - {{ emptyText }} - </span> - </li> - <li v-for="item in results" :key="item.id"> - <button class="js-dropdown-item" type="button" @click.prevent="select(item)"> - <slot name="item" :item="item"> - {{ item.name }} - </slot> - </button> - </li> - </ul> - </div> - </div> - </div> - <span - v-if="hasErrors && errorMessage" - :class="[ - 'form-text js-eks-dropdown-error-message', - { - 'text-danger': hasErrors, - 'text-muted': !hasErrors, - }, - ]" - > - {{ errorMessage }} - </span> - </div> -</template> diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue index 6e74963dcb0..1ec45c8b651 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue @@ -14,12 +14,5 @@ export default { }; </script> <template> - <form name="eks-cluster-configuration-form"> - <div class="form-group"> - <label class="label-bold" name="role" for="eks-role"> - {{ s__('ClusterIntegration|Role name') }} - </label> - <role-name-dropdown /> - </div> - </form> + <form name="eks-cluster-configuration-form"></form> </template> diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/role_name_dropdown.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/role_name_dropdown.vue index 70230b294ac..e69de29bb2d 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/role_name_dropdown.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/role_name_dropdown.vue @@ -1,53 +0,0 @@ -<script> -import { sprintf, s__ } from '~/locale'; - -import ClusterFormDropdown from './cluster_form_dropdown.vue'; - -export default { - components: { - ClusterFormDropdown, - }, - props: { - roles: { - type: Array, - required: false, - default: () => [], - }, - loading: { - type: Boolean, - required: false, - default: false, - }, - }, - computed: { - helpText() { - return sprintf( - s__( - 'ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services%{endLink}.', - ), - { - startLink: - '<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">', - endLink: '</a>', - }, - false, - ); - }, - }, -}; -</script> -<template> - <div> - <cluster-form-dropdown - field-id="eks-role-name" - field-name="eks-role-name" - :items="roles" - :loading="loading" - :loading-text="s__('ClusterIntegration|Loading IAM Roles')" - :placeholder="s__('ClusterIntergation|Select role name')" - :search-field-placeholder="s__('ClusterIntegration|Search IAM Roles')" - :empty-text="s__('ClusterIntegration|No IAM Roles found')" - /> - <p class="form-text text-muted" v-html="helpText"></p> - </div> -</template> diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index f49246cf07b..515402fc506 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, vars-on-top, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */ +/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, vars-on-top, no-unused-vars, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */ +/* global fuzzaldrinPlus */ import $ from 'jquery'; import _ from 'underscore'; @@ -65,10 +66,12 @@ GitLabDropdownInput = (function() { })(); GitLabDropdownFilter = (function() { - var BLUR_KEYCODES, HAS_VALUE_CLASS; + var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS; BLUR_KEYCODES = [27, 40]; + ARROW_KEY_CODES = [38, 40]; + HAS_VALUE_CLASS = 'has-value'; function GitLabDropdownFilter(input, options) { @@ -874,8 +877,9 @@ GitLabDropdown = (function() { }; GitLabDropdown.prototype.addArrowKeyEvent = function() { - var ARROW_KEY_CODES, selector; + var $input, ARROW_KEY_CODES, selector; ARROW_KEY_CODES = [38, 40]; + $input = this.dropdown.find('.dropdown-input-field'); selector = SELECTABLE_CLASSES; if (this.dropdown.find('.dropdown-toggle-page').length) { selector = '.dropdown-page-one ' + selector; diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index a9e086fade8..db4607ca58d 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,9 +1,10 @@ -/* eslint-disable no-var, one-var, consistent-return */ +/* eslint-disable no-var, one-var, no-unused-vars, consistent-return */ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import { addDelimiter } from './lib/utils/text_utility'; import flash from './flash'; +import TaskList from './task_list'; import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import IssuablesHelper from './helpers/issuables_helper'; import { __ } from './locale'; diff --git a/app/assets/javascripts/jobs/components/log/duration_badge.vue b/app/assets/javascripts/jobs/components/log/duration_badge.vue deleted file mode 100644 index 83f62703d27..00000000000 --- a/app/assets/javascripts/jobs/components/log/duration_badge.vue +++ /dev/null @@ -1,13 +0,0 @@ -<script> -export default { - props: { - duration: { - type: String, - required: true, - }, - }, -}; -</script> -<template> - <div class="duration rounded align-self-start px-2 ml-2 flex-shrink-0">{{ duration }}</div> -</template> diff --git a/app/assets/javascripts/jobs/components/log/line.vue b/app/assets/javascripts/jobs/components/log/line.vue index 336ae623f0f..86d0fcc3b74 100644 --- a/app/assets/javascripts/jobs/components/log/line.vue +++ b/app/assets/javascripts/jobs/components/log/line.vue @@ -21,7 +21,7 @@ export default { <template> <div class="line"> <line-number :line-number="line.lineNumber" :path="path" /> - <span v-for="(content, i) in line.content" :key="i" :class="content.style">{{ + <span v-for="(content, i) in line.content" :key="i" class="line-text" :class="content.style">{{ content.text }}</span> </div> diff --git a/app/assets/javascripts/jobs/components/log/line_header.vue b/app/assets/javascripts/jobs/components/log/line_header.vue index af8de9ec0fa..4ec212d2333 100644 --- a/app/assets/javascripts/jobs/components/log/line_header.vue +++ b/app/assets/javascripts/jobs/components/log/line_header.vue @@ -1,13 +1,11 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; import LineNumber from './line_number.vue'; -import DurationBadge from './duration_badge.vue'; export default { components: { Icon, LineNumber, - DurationBadge, }, props: { line: { @@ -22,11 +20,6 @@ export default { type: String, required: true, }, - duration: { - type: String, - required: false, - default: '', - }, }, computed: { iconName() { @@ -42,16 +35,11 @@ export default { </script> <template> - <div - class="line collapsible-line d-flex justify-content-between" - role="button" - @click="handleOnClick" - > - <icon :name="iconName" class="arrow position-absolute" /> + <div class="line collapsible-line" role="button" @click="handleOnClick"> + <icon :name="iconName" class="arrow" /> <line-number :line-number="line.lineNumber" :path="path" /> <span v-for="(content, i) in line.content" :key="i" class="line-text" :class="content.style">{{ content.text }}</span> - <duration-badge v-if="duration" :duration="duration" /> </div> </template> diff --git a/app/assets/javascripts/jobs/components/log/line_number.vue b/app/assets/javascripts/jobs/components/log/line_number.vue index 6c76bef13d3..e06836e2e97 100644 --- a/app/assets/javascripts/jobs/components/log/line_number.vue +++ b/app/assets/javascripts/jobs/components/log/line_number.vue @@ -46,10 +46,7 @@ export default { }; </script> <template> - <gl-link - :id="lineNumberId" - class="d-inline-block text-right position-absolute line-number" - :href="buildLineNumber" - >{{ parsedLineNumber }}</gl-link - > + <gl-link :id="lineNumberId" class="line-number" :href="buildLineNumber">{{ + parsedLineNumber + }}</gl-link> </template> diff --git a/app/assets/javascripts/jobs/components/log/log.vue b/app/assets/javascripts/jobs/components/log/log.vue index 429796aeb4e..5db866afe5a 100644 --- a/app/assets/javascripts/jobs/components/log/log.vue +++ b/app/assets/javascripts/jobs/components/log/log.vue @@ -9,7 +9,7 @@ export default { LogLineHeader, }, computed: { - ...mapState(['traceEndpoint', 'trace', 'isTraceComplete']), + ...mapState(['traceEndpoint', 'trace']), }, methods: { ...mapActions(['toggleCollapsibleLine']), @@ -20,13 +20,12 @@ export default { }; </script> <template> - <code class="job-log d-block"> + <code class="job-log"> <template v-for="(section, index) in trace"> <template v-if="section.isHeader"> <log-line-header :key="`collapsible-${index}`" :line="section.line" - :duration="section.section_duration" :path="traceEndpoint" :is-closed="section.isClosed" @toggleLine="handleOnClickCollapsibleLine(section)" @@ -42,11 +41,5 @@ export default { </template> <log-line v-else :key="section.offset" :line="section" :path="traceEndpoint" /> </template> - - <div v-if="!isTraceComplete" class="js-log-animation loader-animation pt-3 pl-3"> - <div class="dot"></div> - <div class="dot"></div> - <div class="dot"></div> - </div> </code> </template> diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js index 261ec90cd12..f6a87b9a212 100644 --- a/app/assets/javascripts/jobs/store/utils.js +++ b/app/assets/javascripts/jobs/store/utils.js @@ -1,21 +1,10 @@ /** - * Adds the line number property - * @param Object line - * @param Number lineNumber - */ -export const parseLine = (line = {}, lineNumber) => ({ - ...line, - lineNumber, -}); - -/** * Parses the job log content into a structure usable by the template * * For collaspible lines (section_header = true): * - creates a new array to hold the lines that are collpasible, * - adds a isClosed property to handle toggle * - adds a isHeader property to handle template logic - * - adds the section_duration * For each line: * - adds the index as lineNumber * @@ -25,21 +14,27 @@ export const parseLine = (line = {}, lineNumber) => ({ export const logLinesParser = (lines = [], lineNumberStart) => lines.reduce((acc, line, index) => { const lineNumber = lineNumberStart ? lineNumberStart + index : index; - const last = acc[acc.length - 1]; - if (line.section_header) { acc.push({ isClosed: true, isHeader: true, - line: parseLine(line, lineNumber), + line: { + ...line, + lineNumber, + }, + lines: [], }); - } else if (acc.length && last.isHeader && !line.section_duration && line.content.length) { - last.lines.push(parseLine(line, lineNumber)); - } else if (acc.length && last.isHeader && line.section_duration) { - last.section_duration = line.section_duration; - } else if (line.content.length) { - acc.push(parseLine(line, lineNumber)); + } else if (acc.length && acc[acc.length - 1].isHeader) { + acc[acc.length - 1].lines.push({ + ...line, + lineNumber, + }); + } else { + acc.push({ + ...line, + lineNumber, + }); } return acc; diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 5dcc719f7c3..7064731a5ea 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -1,4 +1,4 @@ -/* eslint-disable class-methods-use-this, no-underscore-dangle, no-param-reassign, func-names */ +/* eslint-disable class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, func-names */ import $ from 'jquery'; import Sortable from 'sortablejs'; @@ -50,7 +50,7 @@ export default class LabelManager { $(e.currentTarget).tooltip('hide'); } - toggleEmptyState() { + toggleEmptyState($label, $btn, action) { this.emptyState.classList.toggle( 'hidden', Boolean(this.prioritizedLabels[0].querySelector(':scope > li')), @@ -61,6 +61,7 @@ export default class LabelManager { if (persistState == null) { persistState = true; } + const _this = this; const url = $label.find('.js-toggle-priority').data('url'); let $target = this.prioritizedLabels; let $from = this.otherLabels; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 8cc3bc8373f..177aa02b8e0 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,4 +1,4 @@ -/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, prefer-arrow-callback, one-var, prefer-template, no-new, consistent-return, object-shorthand, no-shadow, no-param-reassign, vars-on-top, no-lonely-if, no-else-return, dot-notation, no-empty */ +/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, prefer-arrow-callback, one-var, no-unused-vars, prefer-template, no-new, consistent-return, object-shorthand, no-shadow, no-param-reassign, vars-on-top, no-lonely-if, no-else-return, dot-notation, no-empty */ /* global Issuable */ /* global ListLabel */ @@ -26,6 +26,7 @@ export default class LabelsSelect { $els.each(function(i, dropdown) { var $block, + $colorPreview, $dropdown, $form, $loading, @@ -34,6 +35,8 @@ export default class LabelsSelect { $value, abilityName, defaultLabel, + enableLabelCreateButton, + issueURLSplit, issueUpdateURL, labelUrl, namespacePath, @@ -44,11 +47,16 @@ export default class LabelsSelect { showNo, $sidebarLabelTooltip, initialSelected, + $toggleText, fieldName, + useId, + propertyName, showMenuAbove, + $container, $dropdownContainer; $dropdown = $(dropdown); $dropdownContainer = $dropdown.closest('.labels-filter'); + $toggleText = $dropdown.find('.dropdown-toggle-text'); namespacePath = $dropdown.data('namespacePath'); projectPath = $dropdown.data('projectPath'); issueUpdateURL = $dropdown.data('issueUpdate'); @@ -69,6 +77,10 @@ export default class LabelsSelect { $value = $block.find('.value'); $loading = $block.find('.block-loading').fadeOut(); fieldName = $dropdown.data('fieldName'); + useId = $dropdown.is( + '.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown', + ); + propertyName = useId ? 'id' : 'title'; initialSelected = $selectbox .find('input[name="' + $dropdown.data('fieldName') + '"]') .map(function() { @@ -112,7 +124,7 @@ export default class LabelsSelect { axios .put(issueUpdateURL, data) .then(({ data }) => { - var labelCount, template, labelTooltipTitle, labelTitles; + var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels; $loading.fadeOut(); $dropdown.trigger('loaded.gl.dropdown'); $selectbox.hide(); @@ -234,10 +246,12 @@ export default class LabelsSelect { renderRow: function(label) { var linkEl, listItemEl, + color, colorEl, indeterminate, removesAll, selectedClass, + spacing, i, marked, dropdownValue; @@ -364,7 +378,7 @@ export default class LabelsSelect { } }, hidden: function() { - var isIssueIndex, isMRIndex, page; + var isIssueIndex, isMRIndex, page, selectedLabels; page = $('body').attr('data-page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = page === 'projects:merge_requests:index'; @@ -381,6 +395,9 @@ export default class LabelsSelect { } if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + selectedLabels = $dropdown + .closest('form') + .find("input:hidden[name='" + $dropdown.data('fieldName') + "']"); Issuable.filterResults($dropdown.closest('form')); } else if ($dropdown.hasClass('js-filter-submit')) { $dropdown.closest('form').submit(); @@ -478,7 +495,7 @@ export default class LabelsSelect { } } }, - opened: function() { + opened: function(e) { if ($dropdown.hasClass('js-issue-board-sidebar')) { const previousSelection = $dropdown.attr('data-selected'); this.selected = previousSelection ? previousSelection.split(',') : []; diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 7873eaf059f..b7922e29bb0 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, no-param-reassign, one-var, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, consistent-return */ +/* eslint-disable func-names, no-var, no-param-reassign, one-var, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, consistent-return, no-unused-vars */ import $ from 'jquery'; import { insertText } from '~/lib/utils/common_utils'; @@ -157,7 +157,7 @@ export function insertMarkdownText({ if (tag === LINK_TAG_PATTERN) { if (URL) { try { - new URL(selected); // eslint-disable-line no-new + const ignoredUrl = new URL(selected); // valid url tag = '[text]({text})'; select = 'text'; diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 9c0d55326ee..8f077685b07 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,4 +1,4 @@ -/* eslint-disable one-var, object-shorthand, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */ +/* eslint-disable one-var, no-unused-vars, object-shorthand, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */ /* global Issuable */ /* global ListMilestone */ @@ -37,6 +37,7 @@ export default class MilestoneSelect { selectedMilestone, selectedMilestoneDefault; const $dropdown = $(dropdown); + const projectId = $dropdown.data('projectId'); const milestonesUrl = $dropdown.data('milestones'); const issueUpdateURL = $dropdown.data('issueUpdate'); const showNo = $dropdown.data('showNo'); @@ -47,6 +48,7 @@ export default class MilestoneSelect { const useId = $dropdown.data('useId'); const defaultLabel = $dropdown.data('defaultLabel'); const defaultNo = $dropdown.data('defaultNo'); + const issuableId = $dropdown.data('issuableId'); const abilityName = $dropdown.data('abilityName'); const $selectBox = $dropdown.closest('.selectbox'); const $block = $selectBox.closest('.block'); @@ -119,7 +121,7 @@ export default class MilestoneSelect { fields: ['title'], }, selectable: true, - toggleLabel: (selected, el) => { + toggleLabel: (selected, el, e) => { if (selected && 'id' in selected && $(el).hasClass('is-active')) { return selected.title; } else { @@ -151,7 +153,7 @@ export default class MilestoneSelect { }, vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: clickEvent => { - const { e } = clickEvent; + const { $el, e } = clickEvent; let selected = clickEvent.selectedObj; let data, modalStoreFilter; diff --git a/app/assets/javascripts/monitoring/components/charts/column.vue b/app/assets/javascripts/monitoring/components/charts/column.vue index ee6aaeb7dde..83136d43479 100644 --- a/app/assets/javascripts/monitoring/components/charts/column.vue +++ b/app/assets/javascripts/monitoring/components/charts/column.vue @@ -100,7 +100,7 @@ export default { }; </script> <template> - <div class="prometheus-graph"> + <div class="prometheus-graph col-12 col-lg-6"> <div class="prometheus-graph-header"> <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> diff --git a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue index eedc5162e0c..73682adc4ee 100644 --- a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue +++ b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue @@ -27,7 +27,7 @@ export default { }; </script> <template> - <div class="prometheus-graph d-flex flex-column justify-content-center"> + <div class="prometheus-graph col-12 col-lg-6 d-flex flex-column justify-content-center"> <div class="prometheus-graph-header"> <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphTitle }}</h5> </div> diff --git a/app/assets/javascripts/monitoring/components/charts/single_stat.vue b/app/assets/javascripts/monitoring/components/charts/single_stat.vue index 076682820e6..7428b27a9c3 100644 --- a/app/assets/javascripts/monitoring/components/charts/single_stat.vue +++ b/app/assets/javascripts/monitoring/components/charts/single_stat.vue @@ -29,7 +29,7 @@ export default { }; </script> <template> - <div class="prometheus-graph"> + <div class="prometheus-graph col-12 col-lg-6"> <div class="prometheus-graph-header"> <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphTitle }}</h5> </div> diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue index 5f1d742d952..02e7a7ba0a6 100644 --- a/app/assets/javascripts/monitoring/components/charts/time_series.vue +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -43,6 +43,11 @@ export default { required: false, default: '', }, + showBorder: { + type: Boolean, + required: false, + default: false, + }, singleEmbed: { type: Boolean, required: false, @@ -267,66 +272,71 @@ export default { </script> <template> - <div class="prometheus-graph"> - <div class="prometheus-graph-header"> - <h5 class="prometheus-graph-title js-graph-title">{{ graphData.title }}</h5> - <gl-button - v-if="exportMetricsToCsvEnabled" - :href="downloadLink" - :title="__('Download CSV')" - :aria-label="__('Download CSV')" - style="margin-left: 200px;" - download="chart_metrics.csv" - > - {{ __('Download CSV') }} - </gl-button> - <div class="prometheus-graph-widgets js-graph-widgets"> - <slot></slot> + <div + class="prometheus-graph col-12" + :class="[showBorder ? 'p-2' : 'p-0', { 'col-lg-6': !singleEmbed }]" + > + <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> + <div class="prometheus-graph-header"> + <h5 class="prometheus-graph-title js-graph-title">{{ graphData.title }}</h5> + <gl-button + v-if="exportMetricsToCsvEnabled" + :href="downloadLink" + :title="__('Download CSV')" + :aria-label="__('Download CSV')" + style="margin-left: 200px;" + download="chart_metrics.csv" + > + {{ __('Download CSV') }} + </gl-button> + <div class="prometheus-graph-widgets js-graph-widgets"> + <slot></slot> + </div> </div> - </div> - <component - :is="glChartComponent" - ref="chart" - v-bind="$attrs" - :data="chartData" - :option="chartOptions" - :format-tooltip-text="formatTooltipText" - :thresholds="thresholds" - :width="width" - :height="height" - @updated="onChartUpdated" - > - <template v-if="tooltip.isDeployment"> - <template slot="tooltipTitle"> - {{ __('Deployed') }} - </template> - <div slot="tooltipContent" class="d-flex align-items-center"> - <icon name="commit" class="mr-2" /> - <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link> - </div> - </template> - <template v-else> - <template slot="tooltipTitle"> - <div class="text-nowrap"> - {{ tooltip.title }} + <component + :is="glChartComponent" + ref="chart" + v-bind="$attrs" + :data="chartData" + :option="chartOptions" + :format-tooltip-text="formatTooltipText" + :thresholds="thresholds" + :width="width" + :height="height" + @updated="onChartUpdated" + > + <template v-if="tooltip.isDeployment"> + <template slot="tooltipTitle"> + {{ __('Deployed') }} + </template> + <div slot="tooltipContent" class="d-flex align-items-center"> + <icon name="commit" class="mr-2" /> + <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link> </div> </template> - <template slot="tooltipContent"> - <div - v-for="(content, key) in tooltip.content" - :key="key" - class="d-flex justify-content-between" - > - <gl-chart-series-label :color="isMultiSeries ? content.color : ''"> - {{ content.name }} - </gl-chart-series-label> - <div class="prepend-left-32"> - {{ content.value }} + <template v-else> + <template slot="tooltipTitle"> + <div class="text-nowrap"> + {{ tooltip.title }} </div> - </div> + </template> + <template slot="tooltipContent"> + <div + v-for="(content, key) in tooltip.content" + :key="key" + class="d-flex justify-content-between" + > + <gl-chart-series-label :color="isMultiSeries ? content.color : ''"> + {{ content.name }} + </gl-chart-series-label> + <div class="prepend-left-32"> + {{ content.value }} + </div> + </div> + </template> </template> - </template> - </component> + </component> + </div> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 7a5a3789bd6..d330ceb836c 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -456,7 +456,6 @@ export default { <panel-type v-for="(graphData, graphIndex) in groupData.metrics" :key="`panel-type-${graphIndex}`" - class="col-12 col-lg-6 pb-3" :clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)" :graph-data="graphData" :dashboard-width="elWidth" @@ -469,7 +468,6 @@ export default { <monitor-time-series-chart v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)" :key="graphIndex" - class="col-12 col-lg-6 pb-3" :graph-data="graphData" :deployment-data="deploymentData" :thresholds="getGraphAlertValues(graphData.queries)" diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue index da1e88071ab..b516a82c170 100644 --- a/app/assets/javascripts/monitoring/components/embed.vue +++ b/app/assets/javascripts/monitoring/components/embed.vue @@ -95,7 +95,6 @@ export default { <monitor-time-series-chart v-for="graphData in charts" :key="graphData.title" - class="w-100" :graph-data="graphData" :container-width="elWidth" group-id="monitor-area-chart" diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index fcfc2570b3d..d1fa9f5e2a2 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,8 +1,9 @@ -/* eslint-disable func-names, no-var, one-var, no-loop-func, consistent-return, prefer-template, prefer-arrow-callback, camelcase */ +/* eslint-disable func-names, no-var, one-var, no-loop-func, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase */ import $ from 'jquery'; import { __ } from '../locale'; import axios from '../lib/utils/axios_utils'; +import flash from '../flash'; import Raphael from './raphael'; export default (function() { @@ -103,7 +104,7 @@ export default (function() { }; BranchGraph.prototype.buildGraph = function() { - var cuday, cumonth, day, len, mm, ref; + var cuday, cumonth, day, j, len, mm, ref; const { r } = this; cuday = 0; cumonth = ''; @@ -177,7 +178,7 @@ export default (function() { return $(element).scroll( (function(_this) { - return function() { + return function(event) { return _this.renderPartialGraph(); }; })(this), @@ -213,7 +214,7 @@ export default (function() { }; BranchGraph.prototype.appendLabel = function(x, y, commit) { - var label, rect, shortrefs, text, textbox; + var label, rect, shortrefs, text, textbox, triangle; if (!commit.refs) { return; @@ -238,8 +239,7 @@ export default (function() { 'fill-opacity': 0.5, stroke: 'none', }); - // Generate the triangle right of the tag box - r.path(['M', x - 5, y, 'L', x - 15, y - 4, 'L', x - 15, y + 4, 'Z']).attr({ + triangle = r.path(['M', x - 5, y, 'L', x - 15, y - 4, 'L', x - 15, y + 4, 'Z']).attr({ fill: '#000', 'fill-opacity': 0.5, stroke: 'none', diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 9cc56b34c75..9cc31e26648 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -2,9 +2,10 @@ no-unused-expressions, one-var, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, vars-on-top, -no-shadow, no-useless-escape, class-methods-use-this */ +no-unused-vars, no-shadow, no-useless-escape, class-methods-use-this */ /* global ResolveService */ +/* global mrRefreshWidgetUrl */ /* old_notes_spec.js is the spec for the legacy, jQuery notes application. It has nothing to do with the new, fancy Vue notes app. @@ -36,6 +37,7 @@ import { isMetaKey, isInMRPage, } from './lib/utils/common_utils'; +import imageDiffHelper from './image_diff/helpers/index'; import { localTimeAgo } from './lib/utils/datetime_utility'; import { sprintf, s__, __ } from './locale'; @@ -681,7 +683,7 @@ export default class Notes { ); } - updateNoteError() { + updateNoteError($parentTimeline) { // eslint-disable-next-line no-new new Flash( __('Your comment could not be updated! Please check your network connection and try again.'), @@ -695,6 +697,7 @@ export default class Notes { */ addDiscussionNote($form, note, isNewDiffComment) { if ($form.attr('data-resolve-all') != null) { + var projectPath = $form.data('projectPath'); var discussionId = $form.data('discussionId'); var mergeRequestId = $form.data('noteableIid'); @@ -743,6 +746,7 @@ export default class Notes { if (currentContent === initialContent) { this.removeNoteEditForm($el); } else { + var $buttons = $el.find('.note-form-actions'); var isWidgetVisible = isInViewport($el.get(0)); if (!isWidgetVisible) { @@ -762,7 +766,7 @@ export default class Notes { * Replaces the note text with the note edit form * Adds a data attribute to the form with the original content of the note for cancellations */ - showEditForm(e) { + showEditForm(e, scrollTo, myLastNote) { e.preventDefault(); var $target = $(e.target); @@ -846,11 +850,16 @@ export default class Notes { * Removes the whole discussion if the last note is being removed. */ removeNote(e) { - var noteElId, $note; + var noteElId, noteId, dataNoteId, $note, lineHolder; $note = $(e.currentTarget).closest('.note'); noteElId = $note.attr('id'); + noteId = $note.attr('data-note-id'); + lineHolder = $(e.currentTarget) + .closest('.notes[data-discussion-id]') + .closest('.notes_holder') + .prev('.line_holder'); $(`.note[id="${noteElId}"]`).each( - (function() { + (function(_this) { // A same note appears in the "Discussion" and in the "Changes" tab, we have // to remove all. Using $('.note[id='noteId']') ensure we get all the notes, // where $('#noteId') would return only one. @@ -1055,8 +1064,25 @@ export default class Notes { this.setupDiscussionNoteForm($link, newForm); } - toggleDiffNote({ target, lineType, forceShow, showReplyInput = false }) { - var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd; + toggleDiffNote({ + target, + lineType, + forceShow, + showReplyInput = false, + currentUsername, + currentUserAvatar, + currentUserFullname, + }) { + var $link, + addForm, + hasNotes, + newForm, + noteForm, + replyButton, + row, + rowCssToAdd, + targetContent, + isDiffCommentAvatar; $link = $(target); row = $link.closest('tr'); const nextRow = row.next(); @@ -1489,7 +1515,7 @@ export default class Notes { let tempFormContent; // Identify executed quick actions from `formContent` - const executedCommands = availableQuickActions.filter(command => { + const executedCommands = availableQuickActions.filter((command, index) => { const commandRegex = new RegExp(`/${command.name}`); return commandRegex.test(formContent); }); @@ -1814,6 +1840,8 @@ export default class Notes { const $noteBody = $editingNote.find('.js-task-list-container'); const $noteBodyText = $noteBody.find('.note-text'); const { formData, formContent, formAction } = this.getFormData($form); + const $diffFile = $form.closest('.diff-file'); + const $notesContainer = $form.closest('.notes'); // Cache original comment content const cachedNoteBodyText = $noteBodyText.html(); diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue index e3be91a4966..6bbf2fa6ee4 100644 --- a/app/assets/javascripts/notes/components/discussion_actions.vue +++ b/app/assets/javascripts/notes/components/discussion_actions.vue @@ -36,10 +36,11 @@ export default { }, }, computed: { + resolvableNotes() { + return this.discussion.notes.filter(x => x.resolvable); + }, userCanResolveDiscussion() { - return this.discussion.notes.every( - note => note.current_user && note.current_user.can_resolve, - ); + return this.resolvableNotes.every(note => note.current_user && note.current_user.can_resolve); }, }, }; diff --git a/app/assets/javascripts/pages/admin/application_settings/general/index.js b/app/assets/javascripts/pages/admin/application_settings/show/index.js index 5ec9688a6e4..5ec9688a6e4 100644 --- a/app/assets/javascripts/pages/admin/application_settings/general/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/show/index.js diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js index ec3919dd073..988ae164955 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, object-shorthand, no-var, one-var, camelcase, no-param-reassign, no-return-assign, prefer-arrow-callback, consistent-return, no-cond-assign, no-else-return */ +/* eslint-disable func-names, object-shorthand, no-var, one-var, camelcase, no-param-reassign, no-return-assign, prefer-arrow-callback, consistent-return, no-unused-vars, no-cond-assign, no-else-return */ import _ from 'underscore'; export default { @@ -126,7 +126,7 @@ export default { _.each( _.omit(log_entry, 'author_name', 'author_email'), (function(_this) { - return function(value) { + return function(value, key) { if (_this.in_range(value.date, date_range)) { parsed_entry.dates[value.date] = value[field]; parsed_entry.commits += value.commits; diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js index 61b35b4b8f5..d8c23c82f7f 100644 --- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -1,6 +1,10 @@ +import Vue from 'vue'; +import _ from 'underscore'; import axios from '../../lib/utils/axios_utils'; import { parseBoolean } from '~/lib/utils/common_utils'; +let vueResourceInterceptor; + export default class PerformanceBarService { static fetchRequestDetails(peekUrl, requestId) { return axios.get(peekUrl, { params: { request_id: requestId } }); @@ -20,11 +24,16 @@ export default class PerformanceBarService { return response; }; + vueResourceInterceptor = (request, next) => next(interceptor); + + Vue.http.interceptors.push(vueResourceInterceptor); + return axios.interceptors.response.use(interceptor); } static removeInterceptor(interceptor) { axios.interceptors.response.eject(interceptor); + Vue.http.interceptors = _.without(Vue.http.interceptors, vueResourceInterceptor); } static callbackParams(response, peekUrl) { diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index 6a07ccc7586..befe91c332f 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -1,10 +1,10 @@ -/* eslint-disable no-useless-escape, no-var, no-underscore-dangle, func-names, no-return-assign, object-shorthand, one-var, consistent-return, class-methods-use-this */ +/* eslint-disable no-useless-escape, no-var, no-underscore-dangle, func-names, no-unused-vars, no-return-assign, object-shorthand, one-var, consistent-return, class-methods-use-this */ import $ from 'jquery'; import 'cropper'; import _ from 'underscore'; -(() => { +(global => { // Matches everything but the file name const FILENAMEREGEX = /^.*[\\\/]/; @@ -69,7 +69,7 @@ import _ from 'underscore'; this.modalCrop.on('shown.bs.modal', this.onModalShow); this.modalCrop.on('hidden.bs.modal', this.onModalHide); this.uploadImageBtn.on('click', this.onUploadImageBtnClick); - this.cropActionsBtn.on('click', function() { + this.cropActionsBtn.on('click', function(e) { var btn; btn = this; return _this.onActionBtnClick(btn); @@ -128,10 +128,10 @@ import _ from 'underscore'; } onActionBtnClick(btn) { - var data; + var data, result; data = $(btn).data(); if (this.modalCropImg.data('cropper') && data.method) { - return this.modalCropImg.cropper(data.method, data.option); + return (result = this.modalCropImg.cropper(data.method, data.option)); } } @@ -151,11 +151,12 @@ import _ from 'underscore'; } dataURLtoBlob(dataURL) { - var array, binary, i, len; + var array, binary, i, len, v; binary = atob(dataURL.split(',')[1]); array = []; for (i = 0, len = binary.length; i < len; i += 1) { + v = binary[i]; array.push(binary.charCodeAt(i)); } return new Blob([new Uint8Array(array)], { diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index e73a828c0ae..765cb868f80 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, consistent-return, one-var, no-cond-assign, prefer-template, no-return-assign */ +/* eslint-disable func-names, no-var, consistent-return, one-var, no-cond-assign, prefer-template, no-unused-vars, no-return-assign */ import $ from 'jquery'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; @@ -8,8 +8,9 @@ import { __ } from '~/locale'; // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> ) const highlighter = function(element, text, matches) { - var j, lastIndex, len, matchIndex, matchedChars, unmatched; + var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched; lastIndex = 0; + highlightText = ''; matchedChars = []; for (j = 0, len = matches.length; j < len; j += 1) { matchIndex = matches[j]; diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 0cc7a22325b..40a2158de78 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, consistent-return, one-var, prefer-template, no-else-return, no-param-reassign */ +/* eslint-disable func-names, no-var, no-unused-vars, consistent-return, one-var, prefer-template, no-else-return, no-param-reassign */ import $ from 'jquery'; import _ from 'underscore'; @@ -7,7 +7,7 @@ import flash from './flash'; import axios from './lib/utils/axios_utils'; import { sprintf, s__, __ } from './locale'; -function Sidebar() { +function Sidebar(currentUser) { this.toggleTodo = this.toggleTodo.bind(this); this.sidebar = $('aside'); @@ -15,9 +15,9 @@ function Sidebar() { this.addEventListeners(); } -Sidebar.initialize = function() { +Sidebar.initialize = function(currentUser) { if (!this.instance) { - this.instance = new Sidebar(); + this.instance = new Sidebar(currentUser); } }; @@ -77,7 +77,7 @@ Sidebar.prototype.sidebarToggleClicked = function(e, triggered) { }; Sidebar.prototype.toggleTodo = function(e) { - var $this, ajaxType, url; + var $btnText, $this, $todoLoading, ajaxType, url; $this = $(e.currentTarget); ajaxType = $this.data('deletePath') ? 'delete' : 'post'; @@ -140,7 +140,7 @@ Sidebar.prototype.todoUpdateDone = function(data) { }); }; -Sidebar.prototype.sidebarDropdownLoading = function() { +Sidebar.prototype.sidebarDropdownLoading = function(e) { var $loading, $sidebarCollapsedIcon, i, img; $sidebarCollapsedIcon = $(this) .closest('.block') @@ -157,7 +157,7 @@ Sidebar.prototype.sidebarDropdownLoading = function() { } }; -Sidebar.prototype.sidebarDropdownLoaded = function() { +Sidebar.prototype.sidebarDropdownLoaded = function(e) { var $sidebarCollapsedIcon, i, img; $sidebarCollapsedIcon = $(this) .closest('.block') diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index f02c55c3d5b..510a2441924 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -1,10 +1,11 @@ -/* eslint-disable no-return-assign, one-var, no-var, consistent-return, object-shorthand, prefer-template, class-methods-use-this, no-lonely-if, vars-on-top */ +/* eslint-disable no-return-assign, one-var, no-var, no-unused-vars, consistent-return, object-shorthand, prefer-template, class-methods-use-this, no-lonely-if, vars-on-top */ import $ from 'jquery'; import { escape, throttle } from 'underscore'; -import { s__, __ } from '~/locale'; +import { s__, __, sprintf } from '~/locale'; import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper'; import axios from './lib/utils/axios_utils'; +import DropdownUtils from './filtered_search/dropdown_utils'; import { isInGroupsPage, isInProjectPage, @@ -141,7 +142,7 @@ export class SearchAutocomplete { }); } - getSearchText(selectedObject) { + getSearchText(selectedObject, el) { return selectedObject.id ? selectedObject.text : ''; } @@ -401,7 +402,7 @@ export class SearchAutocomplete { return this.searchInput.val('').focus(); } - onSearchInputBlur() { + onSearchInputBlur(e) { this.isFocused = false; this.wrap.removeClass('search-active'); // If input is blank then restore state diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js index f4090de3f1e..c9bf234fcce 100644 --- a/app/assets/javascripts/test_utils/simulate_drag.js +++ b/app/assets/javascripts/test_utils/simulate_drag.js @@ -2,8 +2,8 @@ function simulateEvent(el, type, options = {}) { let event; if (!el) return null; - if (/^(pointer|mouse)/.test(type)) { - event = el.ownerDocument.createEvent('MouseEvent'); + if (/^mouse/.test(type)) { + event = el.ownerDocument.createEvent('MouseEvents'); event.initMouseEvent( type, true, @@ -125,7 +125,7 @@ export default function simulateDrag(options) { const startTime = new Date().getTime(); const duration = options.duration || 1000; - simulateEvent(fromEl, 'pointerdown', { + simulateEvent(fromEl, 'mousedown', { button: 0, clientX: fromRect.cx, clientY: fromRect.cy, @@ -146,7 +146,7 @@ export default function simulateDrag(options) { const y = fromRect.cy + (toRect.cy - fromRect.cy) * progress; const overEl = fromEl.ownerDocument.elementFromPoint(x, y); - simulateEvent(overEl, 'pointermove', { + simulateEvent(overEl, 'mousemove', { clientX: x, clientY: y, }); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 4b3c42ae848..57efde7f027 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */ +/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */ /* global Issuable */ /* global emitSidebarEvent */ @@ -405,7 +405,7 @@ function UsersSelect(currentUser, els, options = {}) { } }, defaultLabel: defaultLabel, - hidden: function() { + hidden: function(e) { if ($dropdown.hasClass('js-multiselect')) { emitSidebarEvent('sidebar.saveAssignees'); } @@ -442,6 +442,7 @@ function UsersSelect(currentUser, els, options = {}) { if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') { // Unassigned selected previouslySelected.each((index, element) => { + const id = parseInt(element.value, 10); element.remove(); }); emitSidebarEvent('sidebar.removeAllAssignees'); @@ -547,7 +548,7 @@ function UsersSelect(currentUser, els, options = {}) { }, updateLabel: $dropdown.data('dropdownTitle'), renderRow: function(user) { - var avatar, img, username; + var avatar, img, listClosingTags, listWithName, listWithUserName, username; username = user.username ? '@' + user.username : ''; avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url; diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 5438572eadf..e98c4d7bf7a 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, prefer-arrow-callback, consistent-return, camelcase, class-methods-use-this */ +/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, class-methods-use-this */ // Zen Mode (full screen) textarea // @@ -62,7 +62,7 @@ export default class ZenMode { $(document).on( 'zen_mode:leave', (function(_this) { - return function() { + return function(e) { return _this.exit(); }; })(this), diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index af05d069f97..7e7b08797b2 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -131,16 +131,16 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25); } } -.with-system-header .flash-container { +.with-system-header .flash-container.sticky { top: $flash-container-top + $system-header-height; } .with-performance-bar { - .flash-container { + .flash-container.sticky { top: $flash-container-top + $performance-bar-height; } - &.with-system-header .flash-container { + &.with-system-header .flash-container.sticky { top: $flash-container-top + $performance-bar-height + $system-header-height; } } diff --git a/app/assets/stylesheets/framework/job_log.scss b/app/assets/stylesheets/framework/job_log.scss deleted file mode 100644 index 5c2491c8233..00000000000 --- a/app/assets/stylesheets/framework/job_log.scss +++ /dev/null @@ -1,49 +0,0 @@ -.job-log { - font-family: $monospace-font; - padding: $gl-padding-8 $input-horizontal-padding; - margin: 0 0 $gl-padding-8; - font-size: 13px; - word-break: break-all; - word-wrap: break-word; - color: $gl-text-color-inverted; - border-radius: $border-radius-small; - min-height: 42px; - background-color: $builds-trace-bg; -} - -.line { - padding: 1px $gl-padding 1px $job-log-line-padding; -} - -.line-number { - color: $gl-text-color-inverted; - padding: 0 $gl-padding-8; - min-width: $job-line-number-width; - margin-left: -$job-line-number-width; - padding-right: 1em; - - &:hover, - &:active, - &:visited { - text-decoration: underline; - color: $gl-text-color-inverted; - } -} - -.collapsible-line { - &:hover { - background-color: rgba($white-light, 0.2); - } - - .arrow { - margin-left: -$job-arrow-margin; - } -} - -.duration { - background: $gl-gray-400; -} - -.loader-animation { - @include build-loader-animation; -} diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index f57b1d9f351..3ab83f4c8e6 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -39,6 +39,10 @@ min-height: $header-height; } +.snippet-edited-ago { + color: $gray-darkest; +} + .snippet-actions { @include media-breakpoint-up(sm) { float: right; diff --git a/app/assets/stylesheets/framework/sortable.scss b/app/assets/stylesheets/framework/sortable.scss index 25868061d04..8c070200135 100644 --- a/app/assets/stylesheets/framework/sortable.scss +++ b/app/assets/stylesheets/framework/sortable.scss @@ -90,21 +90,3 @@ padding: 0; } } - -.is-dragging { - // Important because plugin sets inline CSS - opacity: 1 !important; - - * { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - // !important to make sure no style can override this when dragging - cursor: grabbing !important; - } - - &.no-drop * { - cursor: no-drop !important; - } -} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index e77527ac130..faa0a9909d5 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -606,9 +606,6 @@ $blame-blue: #254e77; */ $builds-trace-bg: #111; $job-log-highlight-height: 18px; -$job-log-line-padding: 62px; -$job-line-number-width: 40px; -$job-arrow-margin: 50px; /* * Commit Page diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 4bf0abccd00..e77a2d1e333 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -2,6 +2,20 @@ cursor: grab; } +.is-dragging { + // Important because plugin sets inline CSS + opacity: 1 !important; + + * { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + // !important to make sure no style can override this when dragging + cursor: grabbing !important; + } +} + .is-ghost { opacity: 0.3; pointer-events: none; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index a37cbda8558..8359a60ec9f 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -29,6 +29,10 @@ .author-link { display: inline-block; } + + .issuable-comments { + height: 18px; + } } .icon-merge-request-unmerged { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index e6feded1d4f..c8d155706a9 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -461,6 +461,10 @@ .author-link { display: inline-block; } + + .issuable-comments { + height: 18px; + } } .merge-request-title { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 801e9e7204c..c80beceae52 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -889,7 +889,11 @@ pre.light-well { @include basic-list-stats; display: flex; align-items: center; - padding: $gl-padding-12 0; + padding: $gl-padding 0; + + @include media-breakpoint-up(lg) { + padding: $gl-padding 0; + } &.no-description { @include media-breakpoint-up(sm) { @@ -905,7 +909,7 @@ pre.light-well { } h2 { - font-size: $gl-font-size; + font-size: $gl-font-size-large; font-weight: $gl-font-weight-bold; margin-bottom: 0; @@ -947,7 +951,6 @@ pre.light-well { .description { line-height: 1.5; - color: $gl-text-color-secondary; } @include media-breakpoint-down(md) { @@ -1093,6 +1096,7 @@ pre.light-well { &:not(.explore) { .forks { display: none; + } } diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 03e935fceae..dc16ad80980 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -6,16 +6,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController before_action :set_application_setting before_action :whitelist_query_limiting, only: [:usage_data] - VALID_SETTING_PANELS = %w(general integrations repository templates + VALID_SETTING_PANELS = %w(show integrations repository templates ci_cd reporting metrics_and_profiling network geo preferences).freeze - VALID_SETTING_PANELS.each do |action| - define_method(action) { perform_update if submitted? } + def show end - def show - render :general + (VALID_SETTING_PANELS - %w(show)).each do |action| + define_method(action) { perform_update if submitted? } end def update @@ -145,7 +144,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end def render_update_error - action = VALID_SETTING_PANELS.include?(action_name) ? action_name : :general + action = VALID_SETTING_PANELS.include?(action_name) ? action_name : :show render action end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index af87e04f9c8..0d992bb35f8 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -211,7 +211,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def discussions - merge_request.discussions_diffs.load_highlight + merge_request.preload_discussions_diff_highlight super end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 477ba36e9d1..28c25dbc1e6 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -8,9 +8,6 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :authorize_read_build!, only: [:index] before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] - before_action do - push_frontend_feature_flag(:hide_dismissed_vulnerabilities) - end around_action :allow_gitaly_ref_name_caching, only: [:index, :show] @@ -199,7 +196,12 @@ class Projects::PipelinesController < Projects::ApplicationController end def latest_pipeline - @project.latest_pipeline_for_ref(params['ref']) + ref = params['ref'].presence || @project.default_branch + sha = @project.commit(ref)&.sha + + @project.ci_pipelines + .newest_first(ref: ref, sha: sha) + .first &.present(current_user: current_user) end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 2364777cdc5..8ed6ff56e2b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -46,13 +46,10 @@ class IssuableFinder # This is used in unassigning users NONE = '0' - NEGATABLE_PARAMS_HELPER_KEYS = %i[include_subgroups in].freeze - attr_accessor :current_user, :params - class << self - def scalar_params - @scalar_params ||= %i[ + def self.scalar_params + @scalar_params ||= %i[ assignee_id assignee_username author_id @@ -63,30 +60,14 @@ class IssuableFinder search in ] - end - - def array_params - @array_params ||= { label_name: [], assignee_username: [] } - end - - # This should not be used in controller strong params! - def negatable_scalar_params - @negatable_scalar_params ||= scalar_params + %i[project_id group_id] - end - - # This should not be used in controller strong params! - def negatable_array_params - @negatable_array_params ||= array_params.keys.append(:iids) - end + end - # This should not be used in controller strong params! - def negatable_params - @negatable_params ||= negatable_scalar_params + negatable_array_params - end + def self.array_params + @array_params ||= { label_name: [], assignee_username: [] } + end - def valid_params - @valid_params ||= scalar_params + [array_params] + [{ not: [] }] - end + def self.valid_params + @valid_params ||= scalar_params + [array_params] end def initialize(current_user, params = {}) @@ -98,9 +79,6 @@ class IssuableFinder items = init_collection items = filter_items(items) - # Let's see if we have to negate anything - items = by_negation(items) - # This has to be last as we use a CTE as an optimization fence # for counts by passing the force_cte param and enabling the # attempt_group_search_optimizations feature flag @@ -388,33 +366,6 @@ class IssuableFinder Array(value).last.to_sym end - # Negates all params found in `negatable_params` - # rubocop: disable CodeReuse/ActiveRecord - def by_negation(items) - not_params = params[:not].dup - # API endpoints send in `nil` values so we test if there are any non-nil - return items unless not_params.present? && not_params.values.any? - - not_params.keep_if { |_k, v| v.present? }.each do |(key, value)| - # These aren't negatable params themselves, but rather help other searches, so we skip them. - # They will be added into all the NOT searches. - next if NEGATABLE_PARAMS_HELPER_KEYS.include?(key.to_sym) - next unless self.class.negatable_params.include?(key.to_sym) - - # These are "helper" params that are required inside the NOT to get the right results. They usually come in - # at the top-level params, but if they do come in inside the `:not` params, they should take precedence. - not_helpers = params.slice(*NEGATABLE_PARAMS_HELPER_KEYS).merge(params[:not].slice(*NEGATABLE_PARAMS_HELPER_KEYS)) - not_param = { key => value }.with_indifferent_access.merge(not_helpers) - - items_to_negate = self.class.new(current_user, not_param).execute - - items = items.where.not(id: items_to_negate) - end - - items - end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def by_scope(items) return items.none if current_user_related? && !current_user diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index 4e71a7a9ead..b50186c5a82 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -11,32 +11,31 @@ module Resolvers description: 'The list of IIDs of issues, e.g., [1, 2]' argument :state, Types::IssuableStateEnum, required: false, - description: 'Current state of Issue' + description: "Current state of Issue" argument :label_name, GraphQL::STRING_TYPE.to_list_type, required: false, - description: 'Labels applied to the Issue' + description: "Labels applied to the Issue" argument :created_before, Types::TimeType, required: false, - description: 'Issues created before this date' + description: "Issues created before this date" argument :created_after, Types::TimeType, required: false, - description: 'Issues created after this date' + description: "Issues created after this date" argument :updated_before, Types::TimeType, required: false, - description: 'Issues updated before this date' + description: "Issues updated before this date" argument :updated_after, Types::TimeType, required: false, - description: 'Issues updated after this date' + description: "Issues updated after this date" argument :closed_before, Types::TimeType, required: false, - description: 'Issues closed before this date' + description: "Issues closed before this date" argument :closed_after, Types::TimeType, required: false, - description: 'Issues closed after this date' + description: "Issues closed after this date" argument :search, GraphQL::STRING_TYPE, # rubocop:disable Graphql/Descriptions required: false - argument :sort, Types::SortEnum, - description: 'Sort issues by this criteria', + argument :sort, Types::Sort, # rubocop:disable Graphql/Descriptions required: false, default_value: 'created_desc' diff --git a/app/graphql/types/order.rb b/app/graphql/types/order.rb new file mode 100644 index 00000000000..c5e1cc406b4 --- /dev/null +++ b/app/graphql/types/order.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Types + class Types::Order < Types::BaseEnum + value "id", "Created at date" + value "updated_at", "Updated at date" + end +end diff --git a/app/graphql/types/sort.rb b/app/graphql/types/sort.rb new file mode 100644 index 00000000000..1f756fdab69 --- /dev/null +++ b/app/graphql/types/sort.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + class Types::Sort < Types::BaseEnum + value "updated_desc", "Updated at descending order" + value "updated_asc", "Updated at ascending order" + value "created_desc", "Created at descending order" + value "created_asc", "Created at ascending order" + end +end diff --git a/app/graphql/types/sort_enum.rb b/app/graphql/types/sort_enum.rb deleted file mode 100644 index 3245cb33e0d..00000000000 --- a/app/graphql/types/sort_enum.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module Types - class SortEnum < BaseEnum - graphql_name 'Sort' - description 'Common sort values' - - value 'updated_desc', 'Updated at descending order' - value 'updated_asc', 'Updated at ascending order' - value 'created_desc', 'Created at descending order' - value 'created_asc', 'Created at ascending order' - end -end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 46e2c9ce56e..ec1d8577f36 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # frozen_string_literal: true module PageLayoutHelper diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5ed95311767..3fb39a19cf0 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -562,7 +562,7 @@ module ProjectsHelper allowedVisibilityOptions: project_allowed_visibility_levels(project), visibilityHelpPath: help_page_path('public_access/public_access'), registryAvailable: Gitlab.config.registry.enabled, - registryHelpPath: help_page_path('user/packages/container_registry/index'), + registryHelpPath: help_page_path('user/project/container_registry'), lfsAvailable: Gitlab.config.lfs.enabled, lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs'), pagesAvailable: Gitlab.config.pages.enabled, diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb index ef00ad75683..d87d6a5cb2f 100644 --- a/app/models/ci/artifact_blob.rb +++ b/app/models/ci/artifact_blob.rb @@ -4,7 +4,7 @@ module Ci class ArtifactBlob include BlobLike - EXTENSIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json .xml .log].freeze + EXTENSIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json .log].freeze attr_reader :entry diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 3097e40dd3b..4046048be1c 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -4,12 +4,9 @@ module Ci # The purpose of this class is to store Build related data that can be disposed. # Data that should be persisted forever, should be stored with Ci::Build model. class BuildMetadata < ApplicationRecord - BuildTimeout = Struct.new(:value, :source) - extend Gitlab::Ci::Model include Presentable include ChronicDurationAttribute - include Gitlab::Utils::StrongMemoize self.table_name = 'ci_builds_metadata' @@ -31,16 +28,17 @@ module Ci enum timeout_source: { unknown_timeout_source: 1, project_timeout_source: 2, - runner_timeout_source: 3, - job_timeout_source: 4 + runner_timeout_source: 3 } def update_timeout_state - timeout = timeout_with_highest_precedence + return unless build.runner.present? - return unless timeout + project_timeout = project&.build_timeout + timeout = [project_timeout, build.runner.maximum_timeout].compact.min + timeout_source = timeout < project_timeout ? :runner_timeout_source : :project_timeout_source - update(timeout: timeout.value, timeout_source: timeout.source) + update(timeout: timeout, timeout_source: timeout_source) end private @@ -48,37 +46,5 @@ module Ci def set_build_project self.project_id ||= self.build.project_id end - - def timeout_with_highest_precedence - [(job_timeout || project_timeout), runner_timeout].compact.min_by { |timeout| timeout.value } - end - - def project_timeout - strong_memoize(:project_timeout) do - BuildTimeout.new(project&.build_timeout, :project_timeout_source) - end - end - - def job_timeout - return unless build.options - - strong_memoize(:job_timeout) do - if timeout_from_options = build.options[:job_timeout] - BuildTimeout.new(timeout_from_options, :job_timeout_source) - end - end - end - - def runner_timeout - return unless runner_timeout_set? - - strong_memoize(:runner_timeout) do - BuildTimeout.new(build.runner.maximum_timeout, :runner_timeout_source) - end - end - - def runner_timeout_set? - build.runner&.maximum_timeout.to_i > 0 - end end end diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 50def3ba38c..44c66f06059 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -35,6 +35,10 @@ module Clusters 'stable/nginx-ingress' end + def values + content_values.to_yaml + end + def allowed_to_uninstall? external_ip_or_hostname? && application_jupyter_nil_or_installable? end @@ -67,6 +71,23 @@ module Clusters private + def specification + return {} unless Feature.enabled?(:ingress_modsecurity) + + { + "controller" => { + "config" => { + "enable-modsecurity" => "true", + "enable-owasp-modsecurity-crs" => "true" + } + } + } + end + + def content_values + YAML.load_file(chart_values_file).deep_merge!(specification) + end + def application_jupyter_nil_or_installable? cluster.application_jupyter.nil? || cluster.application_jupyter&.installable? end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 7a61622b139..7855fb69bd6 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -37,18 +37,13 @@ module Clusters has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', inverse_of: :cluster, autosave: true - def self.has_one_cluster_application(name) # rubocop:disable Naming/PredicateName - application = APPLICATIONS[name.to_s] - has_one application.association_name, class_name: application.to_s # rubocop:disable Rails/ReflectionClassName - end - - has_one_cluster_application :helm - has_one_cluster_application :ingress - has_one_cluster_application :cert_manager - has_one_cluster_application :prometheus - has_one_cluster_application :runner - has_one_cluster_application :jupyter - has_one_cluster_application :knative + has_one :application_helm, class_name: 'Clusters::Applications::Helm' + has_one :application_ingress, class_name: 'Clusters::Applications::Ingress' + has_one :application_cert_manager, class_name: 'Clusters::Applications::CertManager' + has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus' + has_one :application_runner, class_name: 'Clusters::Applications::Runner' + has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter' + has_one :application_knative, class_name: 'Clusters::Applications::Knative' has_many :kubernetes_namespaces @@ -132,9 +127,15 @@ module Clusters end def applications - APPLICATIONS.values.map do |application_class| - public_send(application_class.association_name) || public_send("build_#{application_class.association_name}") # rubocop:disable GitlabSecurity/PublicSend - end + [ + application_helm || build_application_helm, + application_ingress || build_application_ingress, + application_cert_manager || build_application_cert_manager, + application_prometheus || build_application_prometheus, + application_runner || build_application_runner, + application_jupyter || build_application_jupyter, + application_knative || build_application_knative + ] end def provider diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb index d1b57a21a7d..803a65726d3 100644 --- a/app/models/clusters/concerns/application_core.rb +++ b/app/models/clusters/concerns/application_core.rb @@ -32,10 +32,6 @@ module Clusters self.to_s.demodulize.underscore end - def self.association_name - :"application_#{application_name}" - end - def name self.class.application_name end diff --git a/app/models/commit.rb b/app/models/commit.rb index a442f607fbf..1470b50f396 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # frozen_string_literal: true class Commit diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 57118bf7a6b..8b011bca72c 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -33,9 +33,16 @@ module Routable # # Returns a single object, or nil. def find_by_full_path(path, follow_redirects: false) - # Case sensitive match first (it's cheaper and the usual case) - # If we didn't have an exact match, we perform a case insensitive search - found = includes(:route).find_by(routes: { path: path }) || where_full_path_in([path]).take + routable_calls_counter.increment(method: 'find_by_full_path') + + if Feature.enabled?(:routable_two_step_lookup) + # Case sensitive match first (it's cheaper and the usual case) + # If we didn't have an exact match, we perform a case insensitive search + found = includes(:route).find_by(routes: { path: path }) || where_full_path_in([path]).take + else + order_sql = Arel.sql("(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)") + found = where_full_path_in([path]).reorder(order_sql).take + end return found if found @@ -54,12 +61,19 @@ module Routable def where_full_path_in(paths) return none if paths.empty? + routable_calls_counter.increment(method: 'where_full_path_in') + wheres = paths.map do |path| "(LOWER(routes.path) = LOWER(#{connection.quote(path)}))" end includes(:route).where(wheres.join(' OR ')).references(:routes) end + + # Temporary instrumentation of method calls + def routable_calls_counter + @routable_calls_counter ||= Gitlab::Metrics.counter(:gitlab_routable_calls_total, 'Number of calls to Routable by method') + end end def full_name diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index aa7286a9971..0b00cf10714 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -108,10 +108,13 @@ class DiffNote < Note end def fetch_diff_file - return note_diff_file.raw_diff_file if note_diff_file - file = - if created_at_diff?(noteable.diff_refs) + if note_diff_file + diff = Gitlab::Git::Diff.new(note_diff_file.to_hash) + Gitlab::Diff::File.new(diff, + repository: repository, + diff_refs: original_position.diff_refs) + elsif created_at_diff?(noteable.diff_refs) # We're able to use the already persisted diffs (Postgres) if we're # presenting a "current version" of the MR discussion diff. # So no need to make an extra Gitaly diff request for it. @@ -123,7 +126,9 @@ class DiffNote < Note original_position.diff_file(repository) end - file&.unfold_diff_lines(position) + # Since persisted diff files already have its content "unfolded" + # there's no need to make it pass through the unfolding process. + file&.unfold_diff_lines(position) unless note_diff_file file end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ac26d29ad19..90061fe181e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -454,17 +454,24 @@ class MergeRequest < ApplicationRecord true end + def preload_discussions_diff_highlight + preloadable_files = note_diff_files.for_commit_or_unresolved + + discussions_diffs.load_highlight(preloadable_files.pluck(:id)) + end + def discussions_diffs strong_memoize(:discussions_diffs) do - note_diff_files = NoteDiffFile - .joins(:diff_note) - .merge(notes.or(commit_notes)) - .includes(diff_note: :project) - Gitlab::DiscussionsDiff::FileCollection.new(note_diff_files.to_a) end end + def note_diff_files + NoteDiffFile + .where(diff_note: discussion_notes) + .includes(diff_note: :project) + end + def diff_size # Calling `merge_request_diff.diffs.real_size` will also perform # highlighting, which we don't need here. diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 800c492e8e2..4b9fee2bbdf 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -27,8 +27,11 @@ class Milestone < ApplicationRecord belongs_to :project belongs_to :group - has_many :milestone_releases - has_many :releases, through: :milestone_releases + # A one-to-one relationship is set up here as part of a MVC: https://gitlab.com/gitlab-org/gitlab-ce/issues/62402 + # However, on the long term, we will want a many-to-many relationship between Release and Milestone. + # The "has_one through" allows us today to set up this one-to-one relationship while setting up the architecture for the long-term (ie intermediate table). + has_one :milestone_release + has_one :release, through: :milestone_release has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.milestones&.maximum(:iid) } has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.milestones&.maximum(:iid) } @@ -65,7 +68,7 @@ class Milestone < ApplicationRecord validate :milestone_type_check validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? } validate :dates_within_4_digits - validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } + validates_associated :milestone_release, message: -> (_, obj) { obj[:value].errors.full_messages.join(",") } strip_attributes :title diff --git a/app/models/milestone_release.rb b/app/models/milestone_release.rb index f7127df339d..c8743a8cad8 100644 --- a/app/models/milestone_release.rb +++ b/app/models/milestone_release.rb @@ -4,11 +4,9 @@ class MilestoneRelease < ApplicationRecord belongs_to :milestone belongs_to :release + validates :milestone_id, uniqueness: { scope: [:release_id] } validate :same_project_between_milestone_and_release - # Keep until 2019-11-29 - self.ignored_columns += %i[id] - private def same_project_between_milestone_and_release diff --git a/app/models/note_diff_file.rb b/app/models/note_diff_file.rb index 67a6d5d6d6b..fcc9e2b3fd8 100644 --- a/app/models/note_diff_file.rb +++ b/app/models/note_diff_file.rb @@ -3,11 +3,15 @@ class NoteDiffFile < ApplicationRecord include DiffFile + scope :for_commit_or_unresolved, -> do + joins(:diff_note).where("resolved_at IS NULL OR noteable_type = 'Commit'") + end + scope :referencing_sha, -> (oids, project_id:) do joins(:diff_note).where(notes: { project_id: project_id, commit_id: oids }) end - delegate :original_position, :project, :resolved_at, to: :diff_note + delegate :original_position, :project, to: :diff_note belongs_to :diff_note, inverse_of: :note_diff_file diff --git a/app/models/project.rb b/app/models/project.rb index 57f1ca98ee2..96c715095f6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -58,7 +58,6 @@ class Project < ApplicationRecord ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10 SORTING_PREFERENCE_FIELD = :projects_sort - MAX_BUILD_TIMEOUT = 1.month cache_markdown_field :description, pipeline: :description @@ -431,7 +430,7 @@ class Project < ApplicationRecord validates :build_timeout, allow_nil: true, numericality: { greater_than_or_equal_to: 10.minutes, - less_than: MAX_BUILD_TIMEOUT, + less_than: 1.month, only_integer: true, message: _('needs to be between 10 minutes and 1 month') } @@ -753,15 +752,6 @@ class Project < ApplicationRecord latest_successful_build_for_ref(job_name, ref) || raise(ActiveRecord::RecordNotFound.new("Couldn't find job #{job_name}")) end - def latest_pipeline_for_ref(ref = default_branch) - ref = ref.presence || default_branch - sha = commit(ref)&.sha - - return unless sha - - ci_pipelines.newest_first(ref: ref, sha: sha).first - end - def merge_base_commit(first_commit_id, second_commit_id) sha = repository.merge_base(first_commit_id, second_commit_id) commit_by(oid: sha) if sha diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb index 0a498fde95a..8b79b5e9f0c 100644 --- a/app/models/project_services/bugzilla_service.rb +++ b/app/models/project_services/bugzilla_service.rb @@ -3,6 +3,8 @@ class BugzillaService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? + prop_accessor :project_url, :issues_url, :new_issue_url + def default_title 'Bugzilla' end diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index dbc42b1b86d..535fcf6b94e 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -3,6 +3,8 @@ class CustomIssueTrackerService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + def default_title 'Custom Issue Tracker' end diff --git a/app/models/project_services/data_fields.rb b/app/models/project_services/data_fields.rb index 0f5385f8ce2..438d85098c8 100644 --- a/app/models/project_services/data_fields.rb +++ b/app/models/project_services/data_fields.rb @@ -3,56 +3,8 @@ module DataFields extend ActiveSupport::Concern - class_methods do - # Provide convenient accessor methods for data fields. - # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084 - def data_field(*args) - args.each do |arg| - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - unless method_defined?(arg) - def #{arg} - data_fields.send('#{arg}') || (properties && properties['#{arg}']) - end - end - - def #{arg}=(value) - @old_data_fields ||= {} - @old_data_fields['#{arg}'] ||= #{arg} # set only on the first assignment, IOW we remember the original value only - data_fields.send('#{arg}=', value) - end - - def #{arg}_touched? - @old_data_fields ||= {} - @old_data_fields.has_key?('#{arg}') - end - - def #{arg}_changed? - #{arg}_touched? && @old_data_fields['#{arg}'] != #{arg} - end - - def #{arg}_was - return unless #{arg}_touched? - return if data_fields.persisted? # arg_was does not work for attr_encrypted - - legacy_properties_data['#{arg}'] - end - RUBY - end - end - end - included do - has_one :issue_tracker_data, autosave: true - has_one :jira_tracker_data, autosave: true - - def data_fields - raise NotImplementedError - end - - def data_fields_present? - data_fields.persisted? - rescue NotImplementedError - false - end + has_one :issue_tracker_data + has_one :jira_tracker_data end end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index ec28602b5e6..51032932eab 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -5,6 +5,8 @@ class GitlabIssueTrackerService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? + prop_accessor :project_url, :issues_url, :new_issue_url + default_value_for :default, true def default_title diff --git a/app/models/project_services/issue_tracker_data.rb b/app/models/project_services/issue_tracker_data.rb index b1d67657fe6..2c1d28ed421 100644 --- a/app/models/project_services/issue_tracker_data.rb +++ b/app/models/project_services/issue_tracker_data.rb @@ -6,6 +6,9 @@ class IssueTrackerData < ApplicationRecord delegate :activated?, to: :service, allow_nil: true validates :service, presence: true + validates :project_url, presence: true, public_url: { enforce_sanitization: true }, if: :activated? + validates :issues_url, presence: true, public_url: { enforce_sanitization: true }, if: :activated? + validates :new_issue_url, public_url: { enforce_sanitization: true }, if: :activated? def self.encryption_options { diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index c201bd2ea18..b6ad46513db 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -3,14 +3,9 @@ class IssueTrackerService < Service validate :one_issue_tracker, if: :activated?, on: :manual_change - # TODO: we can probably just delegate as part of - # https://gitlab.com/gitlab-org/gitlab-ce/issues/63084 - data_field :project_url, :issues_url, :new_issue_url - default_value_for :category, 'issue_tracker' - before_validation :handle_properties - before_validation :set_default_data, on: :create + before_save :handle_properties # Pattern used to extract links from comments # Override this method on services that uses different patterns @@ -48,31 +43,12 @@ class IssueTrackerService < Service end def handle_properties - # this has been moved from initialize_properties and should be improved - # as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084 - return unless properties - - @legacy_properties_data = properties.dup - data_values = properties.slice!('title', 'description') - properties.each do |key, _| + properties.slice('title', 'description').each do |key, _| current_value = self.properties.delete(key) value = attribute_changed?(key) ? attribute_change(key).last : current_value write_attribute(key, value) end - - data_values.reject! { |key| data_fields.changed.include?(key) } - data_fields.assign_attributes(data_values) if data_values.present? - - self.properties = {} - end - - def legacy_properties_data - @legacy_properties_data ||= {} - end - - def data_fields - issue_tracker_data || self.build_issue_tracker_data end def default? @@ -80,7 +56,7 @@ class IssueTrackerService < Service end def issue_url(iid) - issues_url.gsub(':id', iid.to_s) + self.issues_url.gsub(':id', iid.to_s) end def issue_tracker_path @@ -104,22 +80,25 @@ class IssueTrackerService < Service ] end - def initialize_properties - {} - end - # Initialize with default properties values - def set_default_data - return unless issues_tracker.present? - - self.title ||= issues_tracker['title'] - - # we don't want to override if we have set something - return if project_url || issues_url || new_issue_url - - data_fields.project_url = issues_tracker['project_url'] - data_fields.issues_url = issues_tracker['issues_url'] - data_fields.new_issue_url = issues_tracker['new_issue_url'] + # or receive a block with custom properties + def initialize_properties(&block) + return unless properties.nil? + + if enabled_in_gitlab_config + if block_given? + yield + else + self.properties = { + title: issues_tracker['title'], + project_url: issues_tracker['project_url'], + issues_url: issues_tracker['issues_url'], + new_issue_url: issues_tracker['new_issue_url'] + } + end + else + self.properties = {} + end end def self.supported_events diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 61ae78a0b95..0728c83005e 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -17,10 +17,7 @@ class JiraService < IssueTrackerService # Jira Cloud version is deprecating authentication via username and password. # We should use username/password for Jira Server and email/api_token for Jira Cloud, # for more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/49936. - - # TODO: we can probably just delegate as part of - # https://gitlab.com/gitlab-org/gitlab-ce/issues/63084 - data_field :username, :password, :url, :api_url, :jira_issue_transition_id + prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id before_update :reset_password @@ -38,34 +35,24 @@ class JiraService < IssueTrackerService end def initialize_properties - {} - end - - def data_fields - jira_tracker_data || self.build_jira_tracker_data + super do + self.properties = { + url: issues_tracker['url'], + api_url: issues_tracker['api_url'] + } + end end def reset_password - data_fields.password = nil if reset_password? - end - - def set_default_data - return unless issues_tracker.present? - - self.title ||= issues_tracker['title'] - - return if url - - data_fields.url ||= issues_tracker['url'] - data_fields.api_url ||= issues_tracker['api_url'] + self.password = nil if reset_password? end def options url = URI.parse(client_url) { - username: username, - password: password, + username: self.username, + password: self.password, site: URI.join(url, '/').to_s, # Intended to find the root context_path: url.path, auth_type: :basic, diff --git a/app/models/project_services/jira_tracker_data.rb b/app/models/project_services/jira_tracker_data.rb index e4e0f64150a..4f528e3d81b 100644 --- a/app/models/project_services/jira_tracker_data.rb +++ b/app/models/project_services/jira_tracker_data.rb @@ -6,6 +6,13 @@ class JiraTrackerData < ApplicationRecord delegate :activated?, to: :service, allow_nil: true validates :service, presence: true + validates :url, public_url: { enforce_sanitization: true }, presence: true, if: :activated? + validates :api_url, public_url: { enforce_sanitization: true }, allow_blank: true + validates :username, presence: true, if: :activated? + validates :password, presence: true, if: :activated? + validates :jira_issue_transition_id, + format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|transition ids can have only numbers which can be split with , or ;") }, + allow_blank: true def self.encryption_options { diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index a4ca0d20669..5ca057ca833 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -3,6 +3,8 @@ class RedmineService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? + prop_accessor :project_url, :issues_url, :new_issue_url + def default_title 'Redmine' end diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb index 0416eaa5be0..f9de1f7dc49 100644 --- a/app/models/project_services/youtrack_service.rb +++ b/app/models/project_services/youtrack_service.rb @@ -3,6 +3,8 @@ class YoutrackService < IssueTrackerService validates :project_url, :issues_url, presence: true, public_url: true, if: :activated? + prop_accessor :project_url, :issues_url + # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030 def self.reference_pattern(only_long: false) if only_long diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 1857a59e01c..8769d3eb916 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -40,11 +40,6 @@ class ProtectedBranch < ApplicationRecord def self.protected_refs(project) project.protected_branches.select(:name) end - - def self.branch_requires_code_owner_approval?(project, branch_name) - # NOOP - # - end end ProtectedBranch.prepend_if_ee('EE::ProtectedBranch') diff --git a/app/models/release.rb b/app/models/release.rb index cd63b4d5fef..b2e65974aa0 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -12,8 +12,11 @@ class Release < ApplicationRecord has_many :links, class_name: 'Releases::Link' - has_many :milestone_releases - has_many :milestones, through: :milestone_releases + # A one-to-one relationship is set up here as part of a MVC: https://gitlab.com/gitlab-org/gitlab-ce/issues/62402 + # However, on the long term, we will want a many-to-many relationship between Release and Milestone. + # The "has_one through" allows us today to set up this one-to-one relationship while setting up the architecture for the long-term (ie intermediate table). + has_one :milestone_release + has_one :milestone, through: :milestone_release default_value_for :released_at, allows_nil: false do Time.zone.now @@ -23,7 +26,7 @@ class Release < ApplicationRecord validates :description, :project, :tag, presence: true validates :name, presence: true, on: :create - validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } + validates_associated :milestone_release, message: -> (_, obj) { obj[:value].errors.full_messages.join(",") } scope :sorted, -> { order(released_at: :desc) } diff --git a/app/presenters/ci/build_metadata_presenter.rb b/app/presenters/ci/build_metadata_presenter.rb index 4871bb3a919..015b1f67db7 100644 --- a/app/presenters/ci/build_metadata_presenter.rb +++ b/app/presenters/ci/build_metadata_presenter.rb @@ -5,8 +5,7 @@ module Ci TIMEOUT_SOURCES = { unknown_timeout_source: nil, project_timeout_source: 'project', - runner_timeout_source: 'runner', - job_timeout_source: 'job' + runner_timeout_source: 'runner' }.freeze presents :metadata diff --git a/app/services/clusters/applications/base_service.rb b/app/services/clusters/applications/base_service.rb index 67fb3ac8355..a9feb60be6e 100644 --- a/app/services/clusters/applications/base_service.rb +++ b/app/services/clusters/applications/base_service.rb @@ -77,10 +77,6 @@ module Clusters params[:application] end - def application_class - Clusters::Cluster::APPLICATIONS[application_name] - end - def create_oauth_application(application, request) oauth_application_params = { name: params[:application], diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb index 2a626a402e4..f723c42c049 100644 --- a/app/services/clusters/applications/create_service.rb +++ b/app/services/clusters/applications/create_service.rb @@ -10,7 +10,7 @@ module Clusters end def builder - cluster.public_send(application_class.association_name) || # rubocop:disable GitlabSecurity/PublicSend + cluster.public_send(:"application_#{application_name}") || # rubocop:disable GitlabSecurity/PublicSend cluster.public_send(:"build_application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/services/clusters/applications/destroy_service.rb b/app/services/clusters/applications/destroy_service.rb index d666682487b..f3a4c4f754a 100644 --- a/app/services/clusters/applications/destroy_service.rb +++ b/app/services/clusters/applications/destroy_service.rb @@ -16,7 +16,7 @@ module Clusters private def builder - cluster.public_send(application_class.association_name) # rubocop:disable GitlabSecurity/PublicSend + cluster.public_send(:"application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/app/services/clusters/applications/update_service.rb b/app/services/clusters/applications/update_service.rb index 7a36401f156..0fa937da865 100644 --- a/app/services/clusters/applications/update_service.rb +++ b/app/services/clusters/applications/update_service.rb @@ -10,7 +10,7 @@ module Clusters end def builder - cluster.public_send(application_class.association_name) # rubocop:disable GitlabSecurity/PublicSend + cluster.public_send(:"application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/app/services/projects/create_from_template_service.rb b/app/services/projects/create_from_template_service.rb index 91ece024e13..31977a7c76a 100644 --- a/app/services/projects/create_from_template_service.rb +++ b/app/services/projects/create_from_template_service.rb @@ -9,9 +9,7 @@ module Projects end def execute - return project unless validate_template! - - file = built_in_template&.file + file = Gitlab::ProjectTemplate.find(template_name)&.file override_params = params.dup params[:file] = file @@ -26,25 +24,6 @@ module Projects params.delete(:template_name).presence end end - - private - - def validate_template! - return true if built_in_template - - project.errors.add(:template_name, _("'%{template_name}' is unknown or invalid" % { template_name: template_name })) - false - end - - def built_in_template - strong_memoize(:built_in_template) do - Gitlab::ProjectTemplate.find(template_name) - end - end - - def project - @project ||= ::Project.new(namespace_id: params[:namespace_id]) - end end end diff --git a/app/services/releases/concerns.rb b/app/services/releases/concerns.rb index a0ebaea77c8..b5412e97284 100644 --- a/app/services/releases/concerns.rb +++ b/app/services/releases/concerns.rb @@ -48,29 +48,25 @@ module Releases end end - def milestones - return [] unless param_for_milestone_titles_provided? + def milestone + return unless params[:milestone] - strong_memoize(:milestones) do + strong_memoize(:milestone) do MilestonesFinder.new( project: project, current_user: current_user, project_ids: Array(project.id), - state: 'all', - title: params[:milestones] - ).execute + title: params[:milestone] + ).execute.first end end - def inexistent_milestones - return [] unless param_for_milestone_titles_provided? - - existing_milestone_titles = milestones.map(&:title) - Array(params[:milestones]) - existing_milestone_titles + def inexistent_milestone? + params[:milestone] && !params[:milestone].empty? && !milestone end - def param_for_milestone_titles_provided? - params.key?(:milestones) + def param_for_milestone_title_provided? + params[:milestone].present? || params[:milestone]&.empty? end end end diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb index 9a0a876454f..c91d43084d3 100644 --- a/app/services/releases/create_service.rb +++ b/app/services/releases/create_service.rb @@ -7,7 +7,7 @@ module Releases def execute return error('Access Denied', 403) unless allowed? return error('Release already exists', 409) if release - return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any? + return error('Milestone does not exist', 400) if inexistent_milestone? tag = ensure_tag @@ -61,7 +61,7 @@ module Releases sha: tag.dereferenced_target.sha, released_at: released_at, links_attributes: params.dig(:assets, 'links') || [], - milestones: milestones + milestone: milestone ) end end diff --git a/app/services/releases/update_service.rb b/app/services/releases/update_service.rb index 7aa51c4a332..70acc68f747 100644 --- a/app/services/releases/update_service.rb +++ b/app/services/releases/update_service.rb @@ -9,9 +9,9 @@ module Releases return error('Release does not exist', 404) unless release return error('Access Denied', 403) unless allowed? return error('params is empty', 400) if empty_params? - return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any? + return error('Milestone does not exist', 400) if inexistent_milestone? - params[:milestones] = milestones if param_for_milestone_titles_provided? + params[:milestone] = milestone if param_for_milestone_title_provided? if release.update(params) success(tag: existing_tag, release: release) diff --git a/app/services/service_response.rb b/app/services/service_response.rb index 08b7e9d0831..f3437ba16de 100644 --- a/app/services/service_response.rb +++ b/app/services/service_response.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class ServiceResponse - def self.success(message: nil, payload: {}, http_status: :ok) - new(status: :success, message: message, payload: payload, http_status: http_status) + def self.success(message: nil, payload: {}) + new(status: :success, message: message, payload: payload) end def self.error(message:, payload: {}, http_status: nil) diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index 4358365504a..9ed4bc44aae 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-account-settings'), html: { class: 'fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-account-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset diff --git a/app/views/admin/application_settings/_diff_limits.html.haml b/app/views/admin/application_settings/_diff_limits.html.haml index 137b7281e0f..408e569fe07 100644 --- a/app/views/admin/application_settings/_diff_limits.html.haml +++ b/app/views/admin/application_settings/_diff_limits.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-merge-request-settings'), html: { class: 'fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-merge-request-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset diff --git a/app/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml index 73412133979..7587ecbf9d3 100644 --- a/app/views/admin/application_settings/_external_authorization_service_form.html.haml +++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml @@ -8,7 +8,7 @@ = _('External Classification Policy Authorization') .settings-content - = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form' } do |f| + = form_for @application_setting, url: admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml index 0e45301b598..5f36358f599 100644 --- a/app/views/admin/application_settings/_signin.html.haml +++ b/app/views/admin/application_settings/_signin.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signin-settings'), html: { class: 'fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-signin-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml index 7c1df78f30c..a0a58b811ee 100644 --- a/app/views/admin/application_settings/_signup.html.haml +++ b/app/views/admin/application_settings/_signup.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signup-settings'), html: { class: 'fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-signup-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml index 654aed54a15..49980e1e1a7 100644 --- a/app/views/admin/application_settings/_terminal.html.haml +++ b/app/views/admin/application_settings/_terminal.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terminal-settings'), html: { class: 'fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-terminal-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml index 19e7ab7c99a..ef58e9b1128 100644 --- a/app/views/admin/application_settings/_terms.html.haml +++ b/app/views/admin/application_settings/_terms.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terms-settings'), html: { class: 'fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-terms-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index e57ef1ea18f..c07bafbe302 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-visibility-settings'), html: { class: 'fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-visibility-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/show.html.haml index b9f49fdc9de..31f18ba0d56 100644 --- a/app/views/admin/application_settings/general.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -1,5 +1,5 @@ -- breadcrumb_title _("General") -- page_title _("General") +- breadcrumb_title _("Settings") +- page_title _("Settings") - @content_class = "limit-container-width" unless fluid_layout %section.settings.as-visibility-access.no-animate#js-visibility-settings{ class: ('expanded' if expanded_by_default?) } @@ -90,7 +90,7 @@ %p = _('Manage Web IDE features') .settings-content - = form_for @application_setting, url: general_admin_application_settings_path(anchor: "#js-web-ide-settings"), html: { class: 'fieldset-form' } do |f| + = form_for @application_setting, url: admin_application_settings_path(anchor: "#js-web-ide-settings"), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 784fe556123..f76268bc29b 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -232,7 +232,7 @@ = _('Settings') %li.divider.fly-out-top-item = nav_link(path: 'application_settings#show') do - = link_to general_admin_application_settings_path, title: _('General'), class: 'qa-admin-settings-general-item' do + = link_to admin_application_settings_path, title: _('General'), class: 'qa-admin-settings-general-item' do %span = _('General') = nav_link(path: 'application_settings#integrations') do diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index 104c68919f0..84f0900d9c1 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -49,12 +49,12 @@ = render_if_exists 'projects/mirrors/table_pull_row' - @project.remote_mirrors.each_with_index do |mirror, index| - next if mirror.new_record? - %tr.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?), data: { qa_selector: 'mirrored_repository_row' } } - %td{ data: { qa_selector: 'mirror_repository_url_cell' } }= mirror.safe_url || _('Invalid URL') + %tr.qa-mirrored-repository-row.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) } + %td.qa-mirror-repository-url= mirror.safe_url || _('Invalid URL') %td= _('Push') %td = mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never') - %td{ data: { qa_selector: 'mirror_last_update_at_cell' } }= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') + %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') %td - if mirror.disabled? = render 'projects/mirrors/disabled_mirror_badge' diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml index bba4949277d..3644a623d2c 100644 --- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml @@ -29,4 +29,4 @@ = yield :push_access_levels .card-footer - = f.submit 'Protect', class: 'btn-success btn', disabled: true, data: { qa_selector: 'protect_button' } + = f.submit 'Protect', class: 'btn-success btn', disabled: true diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index d0d06a0df7e..e34973f1f43 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -2,7 +2,7 @@ .row.registry-placeholder.prepend-bottom-10 .col-12 #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json), - "help_page_path" => help_page_path('user/packages/container_registry/index'), + "help_page_path" => help_page_path('user/project/container_registry'), "no_containers_image" => image_path('illustrations/docker-empty-state.svg'), "containers_error_image" => image_path('illustrations/docker-error-state.svg'), "repository_url" => escape_once(@project.container_registry_url), diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 959a2423e02..2f277e8147a 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -2,10 +2,8 @@ .col-lg-3 %h4.prepend-top-0 = @service.title - - [true, false].each do |value| - - hide_class = 'd-none' if @service.activated? != value - %span.js-service-active-status{ class: hide_class, data: { value: value.to_s } } - = boolean_to_icon value + = boolean_to_icon @service.activated? + %p= #{@service.description}. - if @service.respond_to?(:detailed_description) diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index bcce7cb52fb..573ed36d7f4 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -60,11 +60,6 @@ .controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0.text-secondary{ class: css_controls_class } .icon-container.d-flex.align-items-center - - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project) - - pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref) - %span.icon-wrapper.pipeline-status - = render 'ci/status/icon', status: project.commit.last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path - - if project.archived %span.d-flex.icon-wrapper.badge.badge-warning archived - if stars @@ -91,6 +86,10 @@ title: _('Issues'), data: { container: 'body', placement: 'top' } do = sprite_icon('issues', size: 14, css_class: 'append-right-4') = number_with_delimiter(project.open_issues_count) + - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project) + - pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref) + %span.icon-wrapper.pipeline-status + = render 'ci/status/icon', status: project.commit.last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path .updated-note %span = _('Updated') diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 73401029da4..2d2382e469a 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -22,7 +22,7 @@ = f.label :file_name, "File" .col-sm-10 .file-holder.snippet - .js-file-title.file-title-flex-parent + .js-file-title.file-title = f.text_field :file_name, placeholder: "Optionally name this file to add code highlighting, e.g. example.rb for Ruby.", class: 'form-control snippet-file-name qa-snippet-file-name' .file-content.code %pre#editor= @snippet.content diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 69481293f90..1a9ae68f53d 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -28,7 +28,7 @@ = @snippet.description - if @snippet.updated_at != @snippet.created_at - = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', exclude_author: true) + = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true) - if @snippet.embeddable? .embed-snippet |