diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-29 18:10:08 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-29 18:10:08 +0300 |
commit | 20fda899a62cc27a4d40a168640e7e926c69eb62 (patch) | |
tree | 8fa2bca2431010c15b681fdec8c0cfba2ad78885 /app | |
parent | 933a571ac8c9ada219dd15079221ff3dba8043be (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
60 files changed, 498 insertions, 281 deletions
diff --git a/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue b/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue deleted file mode 100644 index 1308ca53e74..00000000000 --- a/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue +++ /dev/null @@ -1,50 +0,0 @@ -<script> -import { GlAlert, GlButton } from '@gitlab/ui'; -import axios from '~/lib/utils/axios_utils'; - -export default { - components: { - GlAlert, - GlButton, - }, - props: { - dismissEndpoint: { - type: String, - required: true, - }, - featureId: { - type: String, - required: true, - }, - editPath: { - type: String, - required: true, - }, - }, - data() { - return { - showAlert: true, - }; - }, - methods: { - dismissAlert() { - this.showAlert = false; - - return axios.post(this.dismissEndpoint, { - feature_name: this.featureId, - }); - }, - }, -}; -</script> - -<template> - <gl-alert v-if="showAlert" class="gl-mt-5" @dismiss="dismissAlert"> - {{ __('The Web IDE offers advanced syntax highlighting capabilities and more.') }} - <div class="gl-mt-5"> - <gl-button :href="editPath" category="primary" variant="info">{{ - __('Open Web IDE') - }}</gl-button> - </div> - </gl-alert> -</template> diff --git a/app/assets/javascripts/blob/suggest_web_ide_ci/index.js b/app/assets/javascripts/blob/suggest_web_ide_ci/index.js deleted file mode 100644 index eadf3cd6216..00000000000 --- a/app/assets/javascripts/blob/suggest_web_ide_ci/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import Vue from 'vue'; -import WebIdeAlert from './components/web_ide_alert.vue'; - -export default el => { - const { dismissEndpoint, featureId, editPath } = el.dataset; - - // eslint-disable-next-line no-new - new Vue({ - el, - render(createElement) { - return createElement(WebIdeAlert, { - props: { - dismissEndpoint, - featureId, - editPath, - }, - }); - }, - }); -}; diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 05ee8e49eb1..0fb803cdfec 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -179,9 +179,7 @@ export default class BlobViewer { viewer.innerHTML = data.html; viewer.setAttribute('data-loaded', 'true'); - if (window.gon?.features?.codeNavigation) { - eventHub.$emit('showBlobInteractionZones', viewer.dataset.path); - } + eventHub.$emit('showBlobInteractionZones', viewer.dataset.path); return viewer; }); diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index c9972f0b43c..d1e5dad7971 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -7,14 +7,12 @@ import BlobFileDropzone from '../blob/blob_file_dropzone'; import initPopover from '~/blob/suggest_gitlab_ci_yml'; import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils'; import Tracking from '~/tracking'; -import initWebIdeAlert from '~/blob/suggest_web_ide_ci'; export default () => { const editBlobForm = $('.js-edit-blob-form'); const uploadBlobForm = $('.js-upload-blob-form'); const deleteBlobForm = $('.js-delete-blob-form'); const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml'); - const alertEl = document.getElementById('js-suggest-web-ide-ci'); if (editBlobForm.length) { const urlRoot = editBlobForm.data('relativeUrlRoot'); @@ -85,8 +83,4 @@ export default () => { }); } } - - if (alertEl) { - initWebIdeAlert(alertEl); - } }; diff --git a/app/assets/javascripts/code_navigation/index.js b/app/assets/javascripts/code_navigation/index.js index 362c26ae065..fa5835245bc 100644 --- a/app/assets/javascripts/code_navigation/index.js +++ b/app/assets/javascripts/code_navigation/index.js @@ -1,13 +1,17 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import store from './store'; +import createStore from './store'; import App from './components/app.vue'; -Vue.use(Vuex); - export default initialData => { const el = document.getElementById('js-code-navigation'); + if (!el) return null; + + Vue.use(Vuex); + + const store = createStore(); + store.dispatch('setInitialData', initialData); return new Vue({ diff --git a/app/assets/javascripts/code_navigation/store/index.js b/app/assets/javascripts/code_navigation/store/index.js index fe48f3ac7f5..9b60fc337fe 100644 --- a/app/assets/javascripts/code_navigation/store/index.js +++ b/app/assets/javascripts/code_navigation/store/index.js @@ -3,8 +3,9 @@ import createState from './state'; import actions from './actions'; import mutations from './mutations'; -export default new Vuex.Store({ - actions, - mutations, - state: createState(), -}); +export default () => + new Vuex.Store({ + actions, + mutations, + state: createState(), + }); diff --git a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue index afca8ed2c6f..2719d701c12 100644 --- a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue +++ b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue @@ -64,9 +64,9 @@ export default { </script> <template> - <div v-if="designsCount" class="d-flex align-items-center"> + <div v-if="designsCount" class="gl-display-flex gl-align-items-center"> {{ paginationText }} - <gl-button-group class="ml-3 mr-3"> + <gl-button-group class="gl-mx-5"> <gl-button :disabled="!previousDesign" :title="s__('DesignManagement|Go to previous design')" diff --git a/app/assets/javascripts/design_management/components/toolbar/index.vue b/app/assets/javascripts/design_management/components/toolbar/index.vue index a1cb57123ab..8d25d467d59 100644 --- a/app/assets/javascripts/design_management/components/toolbar/index.vue +++ b/app/assets/javascripts/design_management/components/toolbar/index.vue @@ -106,12 +106,12 @@ export default { > <gl-icon name="close" /> </router-link> - <div class="overflow-hidden d-flex align-items-center"> - <h2 class="m-0 str-truncated-100 gl-font-base">{{ filename }}</h2> - <small v-if="updatedAt" class="text-secondary">{{ updatedText }}</small> + <div class="gl-overflow-hidden gl-display-flex gl-align-items-center"> + <h2 class="gl-m-0 str-truncated-100 gl-font-base">{{ filename }}</h2> + <small v-if="updatedAt" class="gl-text-gray-500">{{ updatedText }}</small> </div> </div> - <design-navigation :id="id" class="ml-auto flex-shrink-0" /> + <design-navigation :id="id" class="gl-ml-auto gl-flex-shrink-0" /> <gl-button :href="image" icon="download" /> <delete-button v-if="isLatestVersion && canDeleteDesign" diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index 9ecb9a44443..e68260b3e62 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -85,11 +85,9 @@ export default { }, }, updated() { - if (window.gon?.features?.codeNavigation) { - this.$nextTick(() => { - eventHub.$emit('showBlobInteractionZones', this.diffFile.new_path); - }); - } + this.$nextTick(() => { + eventHub.$emit('showBlobInteractionZones', this.diffFile.new_path); + }); }, methods: { ...mapActions('diffs', ['saveDiffDiscussion', 'closeDiffFileCommentForm']), diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 02396a4ba1b..529723a349d 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -44,7 +44,7 @@ export default { return { isLoadingCollapsedDiff: false, forkMessageVisible: false, - isCollapsed: this.file.viewer.collapsed || false, + isCollapsed: this.file.viewer.automaticallyCollapsed || false, }; }, computed: { @@ -96,16 +96,16 @@ export default { }, 'file.file_hash': { handler: function watchFileHash() { - if (this.viewDiffsFileByFile && this.file.viewer.collapsed) { + if (this.viewDiffsFileByFile && this.file.viewer.automaticallyCollapsed) { this.isCollapsed = false; this.handleLoadCollapsedDiff(); } else { - this.isCollapsed = this.file.viewer.collapsed || false; + this.isCollapsed = this.file.viewer.automaticallyCollapsed || false; } }, immediate: true, }, - 'file.viewer.collapsed': function setIsCollapsed(newVal) { + 'file.viewer.automaticallyCollapsed': function setIsCollapsed(newVal) { if (!this.viewDiffsFileByFile) { this.isCollapsed = newVal; } diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index cea98d6dcd3..6205d113e5e 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -330,7 +330,7 @@ export default { </gl-dropdown-item> </template> - <template v-if="!diffFile.viewer.collapsed"> + <template v-if="!diffFile.viewer.automaticallyCollapsed"> <gl-dropdown-divider v-if="!diffFile.is_fully_expanded || diffHasDiscussions(diffFile)" /> diff --git a/app/assets/javascripts/diffs/diff_file.js b/app/assets/javascripts/diffs/diff_file.js index 610b71235d9..933197a2c7f 100644 --- a/app/assets/javascripts/diffs/diff_file.js +++ b/app/assets/javascripts/diffs/diff_file.js @@ -18,9 +18,21 @@ function fileSymlinkInformation(file, fileList) { ); } +function collapsed(file) { + const viewer = file.viewer || {}; + + return { + automaticallyCollapsed: viewer.automaticallyCollapsed || viewer.collapsed || false, + }; +} + export function prepareRawDiffFile({ file, allFiles }) { Object.assign(file, { brokenSymlink: fileSymlinkInformation(file, allFiles), + viewer: { + ...file.viewer, + ...collapsed(file), + }, }); return file; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 0f275f1cb3e..966b706fc31 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -103,7 +103,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { commit(types.VIEW_DIFF_FILE, state.diffFiles[0].file_hash); } - if (gon.features?.codeNavigation) { + if (state.diffFiles?.length) { // eslint-disable-next-line promise/catch-or-return,promise/no-nesting import('~/code_navigation').then(m => m.default({ @@ -236,7 +236,7 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi commit(types.RENDER_FILE, file); } - if (file.viewer.collapsed) { + if (file.viewer.automaticallyCollapsed) { eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`); scrollToElement(document.getElementById(file.file_hash)); } else { @@ -252,7 +252,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => { const nextFile = state.diffFiles.find( file => !file.renderIt && - (file.viewer && (!file.viewer.collapsed || file.viewer.name !== diffViewerModes.text)), + (file.viewer && + (!file.viewer.automaticallyCollapsed || file.viewer.name !== diffViewerModes.text)), ); if (nextFile) { @@ -631,7 +632,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d filePath: diffFile.file_path, viewer: { ...diffFile.alternate_viewer, - collapsed: false, + automaticallyCollapsed: false, }, }); commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines }); diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index 0a33f978f4f..91425c7825b 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -9,7 +9,7 @@ export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE; export const hasCollapsedFile = state => - state.diffFiles.some(file => file.viewer && file.viewer.collapsed); + state.diffFiles.some(file => file.viewer && file.viewer.automaticallyCollapsed); export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null); diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 7925c620c4e..13ecf6a997d 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -172,7 +172,7 @@ export default { state.diffFiles.forEach(file => { Object.assign(file, { viewer: Object.assign(file.viewer, { - collapsed: false, + automaticallyCollapsed: false, }), }); }); @@ -355,7 +355,7 @@ export default { const file = state.diffFiles.find(f => f.file_path === filePath); if (file && file.viewer) { - file.viewer.collapsed = collapsed; + file.viewer.automaticallyCollapsed = collapsed; } }, [types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) { diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js index 80f78c154ee..1bfbab9ef96 100644 --- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js +++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js @@ -63,4 +63,47 @@ export default IssuableTokenKeys => { IssuableTokenKeys.tokenKeys.push(targetBranchToken); IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken); + + const approvedBy = { + token: { + formattedKey: __('Approved-By'), + key: 'approved-by', + type: 'array', + param: 'usernames[]', + symbol: '@', + icon: 'approval', + tag: '@approved-by', + }, + condition: [ + { + url: 'approved_by_usernames[]=None', + tokenKey: 'approved-by', + value: __('None'), + operator: '=', + }, + { + url: 'not[approved_by_usernames][]=None', + tokenKey: 'approved-by', + value: __('None'), + operator: '!=', + }, + { + url: 'approved_by_usernames[]=Any', + tokenKey: 'approved-by', + value: __('Any'), + operator: '=', + }, + { + url: 'not[approved_by_usernames][]=Any', + tokenKey: 'approved-by', + value: __('Any'), + operator: '!=', + }, + ], + }; + + const tokenPosition = 2; + IssuableTokenKeys.tokenKeys.splice(tokenPosition, 0, ...[approvedBy.token]); + IssuableTokenKeys.tokenKeysWithAlternative.splice(tokenPosition, 0, ...[approvedBy.token]); + IssuableTokenKeys.conditions.push(...approvedBy.condition); }; diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js index 49bd3cda127..5b4af96c861 100644 --- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js +++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js @@ -69,6 +69,11 @@ export default class AvailableDropdownMappings { gl: DropdownUser, element: this.container.querySelector('#js-dropdown-assignee'), }, + 'approved-by': { + reference: null, + gl: DropdownUser, + element: this.container.querySelector('#js-dropdown-approved-by'), + }, milestone: { reference: null, gl: DropdownNonUser, diff --git a/app/assets/javascripts/filtered_search/constants.js b/app/assets/javascripts/filtered_search/constants.js index 0b9fe969da1..6cd6f9c9906 100644 --- a/app/assets/javascripts/filtered_search/constants.js +++ b/app/assets/javascripts/filtered_search/constants.js @@ -1,4 +1,4 @@ -export const USER_TOKEN_TYPES = ['author', 'assignee']; +export const USER_TOKEN_TYPES = ['author', 'assignee', 'approved-by']; export const DROPDOWN_TYPE = { hint: 'hint', diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index bc3a130432a..cc338237191 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -4,6 +4,7 @@ import { escape, template } from 'lodash'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import glRegexp from './lib/utils/regexp'; import AjaxCache from './lib/utils/ajax_cache'; +import axios from '~/lib/utils/axios_utils'; import { spriteIcon } from './lib/utils/common_utils'; import * as Emoji from '~/emoji'; @@ -61,6 +62,7 @@ class GfmAutoComplete { this.dataSources = dataSources; this.cachedData = {}; this.isLoadingData = {}; + this.previousQuery = ''; } setup(input, enableMap = defaultAutocompleteConfig) { @@ -526,7 +528,7 @@ class GfmAutoComplete { } getDefaultCallbacks() { - const fetchData = this.fetchData.bind(this); + const self = this; return { sorter(query, items, searchKey) { @@ -539,7 +541,15 @@ class GfmAutoComplete { }, filter(query, data, searchKey) { if (GfmAutoComplete.isLoading(data)) { - fetchData(this.$inputor, this.at); + self.fetchData(this.$inputor, this.at); + return data; + } + if ( + GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[this.at]) && + self.previousQuery !== query + ) { + self.fetchData(this.$inputor, this.at, query); + self.previousQuery = query; return data; } return $.fn.atwho.default.callbacks.filter(query, data, searchKey); @@ -587,13 +597,22 @@ class GfmAutoComplete { }; } - fetchData($input, at) { + fetchData($input, at, search) { if (this.isLoadingData[at]) return; this.isLoadingData[at] = true; const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]]; - if (this.cachedData[at]) { + if (GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[at])) { + axios + .get(dataSource, { params: { search } }) + .then(({ data }) => { + this.loadData($input, at, data); + }) + .catch(() => { + this.isLoadingData[at] = false; + }); + } else if (this.cachedData[at]) { this.loadData($input, at, this.cachedData[at]); } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { Emoji.initEmojiMap() @@ -687,6 +706,8 @@ GfmAutoComplete.atTypeMap = { $: 'snippets', }; +GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities']; + // Emoji GfmAutoComplete.glEmojiTag = null; GfmAutoComplete.Emoji = { diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index c01cd8f8037..a4271852563 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -76,7 +76,7 @@ export default { :discussion-path="discussion.discussion_path" :diff-file="discussion.diff_file" :can-current-user-fork="false" - :expanded="!discussion.diff_file.viewer.collapsed" + :expanded="!discussion.diff_file.viewer.automaticallyCollapsed" /> <div v-if="isTextFile" class="diff-content"> <table class="code js-syntax-highlight" :class="$options.userColorSchemeClass"> diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index 988bea5343a..812e7176b4c 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -58,7 +58,7 @@ document.addEventListener('DOMContentLoaded', () => { const codeNavEl = document.getElementById('js-code-navigation'); - if (gon.features?.codeNavigation && codeNavEl) { + if (codeNavEl) { const { codeNavigationPath, blobPath, definitionPathPrefix } = codeNavEl.dataset; // eslint-disable-next-line promise/catch-or-return diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 8615bd4bb2a..63288587606 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -777,7 +777,6 @@ button.mini-pipeline-graph-dropdown-toggle { .ci-action-icon-container { position: absolute; right: 8px; - top: 8px; &.ci-action-icon-wrapper { height: $ci-action-dropdown-button-size; @@ -799,8 +798,6 @@ button.mini-pipeline-graph-dropdown-toggle { width: $ci-action-dropdown-svg-size; height: $ci-action-dropdown-svg-size; fill: $gl-text-color-secondary; - position: relative; - top: 1px; vertical-align: initial; } } diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 1568d9966dd..5df4385ecea 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -33,7 +33,6 @@ class Projects::BlobController < Projects::ApplicationController before_action :set_last_commit_sha, only: [:edit, :update] before_action only: :show do - push_frontend_feature_flag(:code_navigation, @project, default_enabled: true) push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline) push_frontend_feature_flag(:gitlab_ci_yml_preview, @project, default_enabled: false) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 92785540172..e2fa373a88c 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -29,7 +29,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action only: [:show] do push_frontend_feature_flag(:deploy_from_footer, @project, default_enabled: true) push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline) - push_frontend_feature_flag(:code_navigation, @project, default_enabled: true) push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true) push_frontend_feature_flag(:merge_ref_head_comments, @project, default_enabled: true) push_frontend_feature_flag(:mr_commit_neighbor_nav, @project, default_enabled: true) diff --git a/app/finders/merge_requests/by_approvals_finder.rb b/app/finders/merge_requests/by_approvals_finder.rb new file mode 100644 index 00000000000..e6ab1467f06 --- /dev/null +++ b/app/finders/merge_requests/by_approvals_finder.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module MergeRequests + # Used to filter MergeRequest collections by approvers + class ByApprovalsFinder + attr_reader :usernames, :ids + + # We apply a limitation to the amount of elements that can be part of the filter condition + MAX_FILTER_ELEMENTS = 5 + + # Initialize the finder + # + # @param [Array<String>] usernames + # @param [Array<Integers>] ids + def initialize(usernames, ids) + # rubocop:disable CodeReuse/ActiveRecord + @usernames = Array(usernames).map(&:to_s).uniq.take(MAX_FILTER_ELEMENTS) + @ids = Array(ids).uniq.take(MAX_FILTER_ELEMENTS) + # rubocop:enable CodeReuse/ActiveRecord + end + + # Filter MergeRequest collections by approvers + # + # @param [ActiveRecord::Relation] items the activerecord relation + def execute(items) + if by_no_approvals? + without_approvals(items) + elsif by_any_approvals? + with_any_approvals(items) + elsif ids.present? + find_approved_by_ids(items) + elsif usernames.present? + find_approved_by_names(items) + else + items + end + end + + private + + # Is param using special condition: "None" ? + # + # @return [Boolean] whether special condition "None" is being used + def by_no_approvals? + includes_special_label?(IssuableFinder::Params::FILTER_NONE) + end + + # Is param using special condition: "Any" ? + # + # @return [Boolean] whether special condition "Any" is being used + def by_any_approvals? + includes_special_label?(IssuableFinder::Params::FILTER_ANY) + end + + # Check if we have the special label in ids or usernames field + # + # @param [String] label the special label + # @return [Boolean] whether ids or usernames includes the special label + def includes_special_label?(label) + ids.first.to_s.downcase == label || usernames.map(&:downcase).include?(label) + end + + # Merge Requests without any approval + # + # @param [ActiveRecord::Relation] items + def without_approvals(items) + items.without_approvals + end + + # Merge Requests with any number of approvals + # + # @param [ActiveRecord::Relation] items the activerecord relation + def with_any_approvals(items) + items.select_from_union([ + items.with_approvals + ]) + end + + # Merge Requests approved by given usernames + # + # @param [ActiveRecord::Relation] items the activerecord relation + def find_approved_by_names(items) + items.approved_by_users_with_usernames(*usernames) + end + + # Merge Requests approved by given user IDs + # + # @param [ActiveRecord::Relation] items the activerecord relation + def find_approved_by_ids(items) + items.approved_by_users_with_ids(*ids) + end + end +end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 37da29b32ff..7bdc98d2e3d 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -33,7 +33,11 @@ class MergeRequestsFinder < IssuableFinder include MergedAtFilter def self.scalar_params - @scalar_params ||= super + [:wip, :draft, :target_branch, :merged_after, :merged_before] + @scalar_params ||= super + [:wip, :draft, :target_branch, :merged_after, :merged_before, :approved_by_ids] + end + + def self.array_params + @array_params ||= super.merge(approved_by_usernames: []) end def klass @@ -47,6 +51,7 @@ class MergeRequestsFinder < IssuableFinder items = by_draft(items) items = by_target_branch(items) items = by_merged_at(items) + items = by_approvals(items) by_source_project_id(items) end @@ -131,6 +136,15 @@ class MergeRequestsFinder < IssuableFinder def deployment_id @deployment_id ||= params[:deployment_id].presence end + + # Filter by merge requests that had been approved by specific users + # rubocop: disable CodeReuse/Finder + def by_approvals(items) + MergeRequests::ByApprovalsFinder + .new(params[:approved_by_usernames], params[:approved_by_ids]) + .execute(items) + end + # rubocop: enable CodeReuse/Finder end MergeRequestsFinder.prepend_if_ee('EE::MergeRequestsFinder') diff --git a/app/helpers/gitpod_helper.rb b/app/helpers/gitpod_helper.rb new file mode 100644 index 00000000000..7edf7dc218d --- /dev/null +++ b/app/helpers/gitpod_helper.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module GitpodHelper + def gitpod_enable_description + link_start = '<a href="https://gitpod.io/" target="_blank" rel="noopener noreferrer">'.html_safe + link_end = "#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}</a>".html_safe + + s_('Enable %{link_start}Gitpod%{link_end} integration to launch a development environment in your browser directly from GitLab.').html_safe % { link_start: link_start, link_end: link_end } + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 72cc07b13a5..48bbc979947 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -758,10 +758,6 @@ module ProjectsHelper !project.repository.gitlab_ci_yml end - def native_code_navigation_enabled?(project) - Feature.enabled?(:code_navigation, project, default_enabled: true) - end - def show_visibility_confirm_modal?(project) project.unlink_forks_upon_visibility_decrease_enabled? && project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && project.forks_count > 0 end diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index 967271a8431..b0cfda67ad4 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -9,7 +9,6 @@ module UserCalloutsHelper TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight' WEBHOOKS_MOVED = 'webhooks_moved' CUSTOMIZE_HOMEPAGE = 'customize_homepage' - WEB_IDE_ALERT_DISMISSED = 'web_ide_alert_dismissed' def show_admin_integrations_moved? !user_dismissed?(ADMIN_INTEGRATIONS_MOVED) @@ -51,10 +50,6 @@ module UserCalloutsHelper customize_homepage && !user_dismissed?(CUSTOMIZE_HOMEPAGE) end - def show_web_ide_alert? - !user_dismissed?(WEB_IDE_ALERT_DISMISSED) - end - private def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil) diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable_base.rb index d07c4ec43ac..c2d94b50f8d 100644 --- a/app/models/concerns/approvable_base.rb +++ b/app/models/concerns/approvable_base.rb @@ -2,10 +2,34 @@ module ApprovableBase extend ActiveSupport::Concern + include FromUnion included do has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :approved_by_users, through: :approvals, source: :user + + scope :without_approvals, -> { left_outer_joins(:approvals).where(approvals: { id: nil }) } + scope :with_approvals, -> { joins(:approvals) } + scope :approved_by_users_with_ids, -> (*user_ids) do + with_approvals + .merge(Approval.with_user) + .where(users: { id: user_ids }) + .group(:id) + .having("COUNT(users.id) = ?", user_ids.size) + end + scope :approved_by_users_with_usernames, -> (*usernames) do + with_approvals + .merge(Approval.with_user) + .where(users: { username: usernames }) + .group(:id) + .having("COUNT(users.id) = ?", usernames.size) + end + end + + class_methods do + def select_from_union(relations) + where(id: from_union(relations)) + end end def approved_by?(user) diff --git a/app/models/concerns/integration.rb b/app/models/concerns/integration.rb index 34ff5bb1195..9d446841a9f 100644 --- a/app/models/concerns/integration.rb +++ b/app/models/concerns/integration.rb @@ -16,7 +16,7 @@ module Integration Project.where(id: custom_integration_project_ids) end - def ids_without_integration(integration, limit) + def without_integration(integration) services = Service .select('1') .where('services.project_id = projects.id') @@ -26,8 +26,6 @@ module Integration .where('NOT EXISTS (?)', services) .where(pending_delete: false) .where(archived: false) - .limit(limit) - .pluck(:id) end end end diff --git a/app/models/data_list.rb b/app/models/data_list.rb index 2cee3447886..adad8e3013e 100644 --- a/app/models/data_list.rb +++ b/app/models/data_list.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class DataList - def initialize(batch_ids, data_fields_hash, klass) - @batch_ids = batch_ids + def initialize(batch, data_fields_hash, klass) + @batch = batch @data_fields_hash = data_fields_hash @klass = klass end @@ -13,15 +13,15 @@ class DataList private - attr_reader :batch_ids, :data_fields_hash, :klass + attr_reader :batch, :data_fields_hash, :klass def columns data_fields_hash.keys << 'service_id' end def values - batch_ids.map do |row| - data_fields_hash.values << row['id'] + batch.map do |record| + data_fields_hash.values << record['id'] end end end diff --git a/app/models/group.rb b/app/models/group.rb index c0f145997cc..abb3f6c96c4 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -15,6 +15,7 @@ class Group < Namespace include WithUploads include Gitlab::Utils::StrongMemoize include GroupAPICompatibility + include EachBatch ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10 @@ -140,6 +141,15 @@ class Group < Namespace end end + def without_integration(integration) + services = Service + .select('1') + .where('services.group_id = namespaces.id') + .where(type: integration.type) + + where('NOT EXISTS (?)', services) + end + private def public_to_user_arel(user) diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 6a6b2bb1b58..79a84231083 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -5,7 +5,7 @@ class NotificationRecipient attr_reader :user, :type, :reason - def initialize(user, type, **opts) + def initialize(user, type, opts = {}) unless NotificationSetting.levels.key?(type) || type == :subscription raise ArgumentError, "invalid type: #{type.inspect}" end diff --git a/app/models/project.rb b/app/models/project.rb index dacad903439..8934c09f7b1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -33,6 +33,7 @@ class Project < ApplicationRecord include FromUnion include IgnorableColumns include Integration + include EachBatch extend Gitlab::Cache::RequestCache extend Gitlab::ConfigHelper diff --git a/app/models/service.rb b/app/models/service.rb index 087985b192a..48fa62d3d46 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -63,6 +63,7 @@ class Service < ApplicationRecord scope :active, -> { where(active: true) } scope :by_type, -> (type) { where(type: type) } scope :by_active_flag, -> (flag) { where(active: flag) } + scope :inherit_from_id, -> (id) { where(inherit_from_id: id) } scope :for_group, -> (group) { where(group_id: group, type: available_services_types) } scope :for_template, -> { where(template: true, type: available_services_types) } scope :for_instance, -> { where(instance: true, type: available_services_types) } diff --git a/app/models/service_list.rb b/app/models/service_list.rb index 9cbc5e68059..5eca5f2bda1 100644 --- a/app/models/service_list.rb +++ b/app/models/service_list.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class ServiceList - def initialize(batch_ids, service_hash, association) - @batch_ids = batch_ids + def initialize(batch, service_hash, association) + @batch = batch @service_hash = service_hash @association = association end @@ -13,15 +13,15 @@ class ServiceList private - attr_reader :batch_ids, :service_hash, :association + attr_reader :batch, :service_hash, :association def columns - (service_hash.keys << "#{association}_id") + service_hash.keys << "#{association}_id" end def values - batch_ids.map do |id| - (service_hash.values << id) + batch.select(:id).map do |record| + service_hash.values << record.id end end end diff --git a/app/models/user.rb b/app/models/user.rb index 30e468ee5f1..9305e2518c1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -69,7 +69,7 @@ class User < ApplicationRecord LOGIN_FORBIDDEN = "Your account does not have the required permission to login. Please contact your GitLab " \ "administrator if you think this is an error." - MINIMUM_INACTIVE_DAYS = 180 + MINIMUM_INACTIVE_DAYS = 90 # Override Devise::Models::Trackable#update_tracked_fields! # to limit database writes to at most once every hour diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index 0ba319aa444..e39ff8712fc 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -19,7 +19,7 @@ class UserCallout < ApplicationRecord webhooks_moved: 13, service_templates_deprecated: 14, admin_integrations_moved: 15, - web_ide_alert_dismissed: 16, + web_ide_alert_dismissed: 16, # no longer in use active_user_count_threshold: 18, # EE-only buy_pipeline_minutes_notification_dot: 19, # EE-only personal_access_token_expiry: 21, # EE-only diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb index 1c615777018..7e7a387d3d4 100644 --- a/app/models/user_interacted_project.rb +++ b/app/models/user_interacted_project.rb @@ -21,7 +21,7 @@ class UserInteractedProject < ApplicationRecord user_id: event.author_id } - cached_exists?(attributes) do + cached_exists?(**attributes) do transaction(requires_new: true) do where(attributes).select(1).first || create!(attributes) true # not caching the whole record here for now diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index ef75c160b2d..0be8a4d5472 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -118,10 +118,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated add_special_file_path(file_name: ci_config_path_or_default) end - def add_ci_yml_ide_path - ide_edit_path(project, default_branch_or_master, ci_config_path_or_default) - end - def add_readme_path add_special_file_path(file_name: 'README.md') end @@ -330,7 +326,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated if cicd_missing? AnchorData.new(false, statistic_icon + _('Set up CI/CD'), - add_ci_yml_ide_path) + add_ci_yml_path) elsif repository.gitlab_ci_yml.present? AnchorData.new(false, statistic_icon('doc-text') + _('CI/CD configuration'), diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb index 6ef524b5bec..0b4f21c55f4 100644 --- a/app/serializers/diffs_entity.rb +++ b/app/serializers/diffs_entity.rb @@ -78,7 +78,7 @@ class DiffsEntity < Grape::Entity options[:merge_request_diffs] end - expose :definition_path_prefix, if: -> (diff_file) { Feature.enabled?(:code_navigation, merge_request.project, default_enabled: true) } do |diffs| + expose :definition_path_prefix do |diffs| project_blob_path(merge_request.project, diffs.diff_refs&.head_sha) end @@ -89,8 +89,6 @@ class DiffsEntity < Grape::Entity private def code_navigation_path(diffs) - return unless Feature.enabled?(:code_navigation, merge_request.project, default_enabled: true) - Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha) end diff --git a/app/serializers/paginated_diff_entity.rb b/app/serializers/paginated_diff_entity.rb index 37c48338e55..f24571f7d7d 100644 --- a/app/serializers/paginated_diff_entity.rb +++ b/app/serializers/paginated_diff_entity.rb @@ -37,8 +37,6 @@ class PaginatedDiffEntity < Grape::Entity private def code_navigation_path(diffs) - return unless Feature.enabled?(:code_navigation, merge_request.project, default_enabled: true) - Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha) end diff --git a/app/services/admin/propagate_integration_service.rb b/app/services/admin/propagate_integration_service.rb index 34d6008cb6a..80e27c21d5b 100644 --- a/app/services/admin/propagate_integration_service.rb +++ b/app/services/admin/propagate_integration_service.rb @@ -14,59 +14,19 @@ module Admin private # rubocop: disable Cop/InBatches - # rubocop: disable CodeReuse/ActiveRecord def update_inherited_integrations - Service.where(type: integration.type, inherit_from_id: integration.id).in_batches(of: BATCH_SIZE) do |batch| - bulk_update_from_integration(batch) + Service.by_type(integration.type).inherit_from_id(integration.id).in_batches(of: BATCH_SIZE) do |services| + min_id, max_id = services.pick("MIN(services.id), MAX(services.id)") + PropagateIntegrationInheritWorker.perform_async(integration.id, min_id, max_id) end end # rubocop: enable Cop/InBatches - # rubocop: enable CodeReuse/ActiveRecord - - # rubocop: disable CodeReuse/ActiveRecord - def bulk_update_from_integration(batch) - # Retrieving the IDs instantiates the ActiveRecord relation (batch) - # into concrete models, otherwise update_all will clear the relation. - # https://stackoverflow.com/q/34811646/462015 - batch_ids = batch.pluck(:id) - - Service.transaction do - batch.update_all(service_hash) - - if data_fields_present? - integration.data_fields.class.where(service_id: batch_ids).update_all(data_fields_hash) - end - end - end - # rubocop: enable CodeReuse/ActiveRecord def create_integration_for_groups_without_integration - loop do - batch = Group.uncached { group_ids_without_integration(integration, BATCH_SIZE) } - - bulk_create_from_integration(batch, 'group') unless batch.empty? - - break if batch.size < BATCH_SIZE + Group.without_integration(integration).each_batch(of: BATCH_SIZE) do |groups| + min_id, max_id = groups.pick("MIN(namespaces.id), MAX(namespaces.id)") + PropagateIntegrationGroupWorker.perform_async(integration.id, min_id, max_id) end end - - def service_hash - @service_hash ||= integration.to_service_hash - .tap { |json| json['inherit_from_id'] = integration.id } - end - - # rubocop:disable CodeReuse/ActiveRecord - def group_ids_without_integration(integration, limit) - services = Service - .select('1') - .where('services.group_id = namespaces.id') - .where(type: integration.type) - - Group - .where('NOT EXISTS (?)', services) - .limit(limit) - .pluck(:id) - end - # rubocop:enable CodeReuse/ActiveRecord end end diff --git a/app/services/admin/propagate_service_template.rb b/app/services/admin/propagate_service_template.rb index cd0d2d5d03f..07be3c1027d 100644 --- a/app/services/admin/propagate_service_template.rb +++ b/app/services/admin/propagate_service_template.rb @@ -9,11 +9,5 @@ module Admin create_integration_for_projects_without_integration end - - private - - def service_hash - @service_hash ||= integration.to_service_hash - end end end diff --git a/app/services/bulk_create_integration_service.rb b/app/services/bulk_create_integration_service.rb new file mode 100644 index 00000000000..23b89b0d8a9 --- /dev/null +++ b/app/services/bulk_create_integration_service.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class BulkCreateIntegrationService + def initialize(integration, batch, association) + @integration = integration + @batch = batch + @association = association + end + + def execute + service_list = ServiceList.new(batch, service_hash, association).to_array + + Service.transaction do + results = bulk_insert(*service_list) + + if integration.data_fields_present? + data_list = DataList.new(results, data_fields_hash, integration.data_fields.class).to_array + + bulk_insert(*data_list) + end + + run_callbacks(batch) if association == 'project' + end + end + + private + + attr_reader :integration, :batch, :association + + def bulk_insert(klass, columns, values_array) + items_to_insert = values_array.map { |array| Hash[columns.zip(array)] } + + klass.insert_all(items_to_insert, returning: [:id]) + end + + # rubocop: disable CodeReuse/ActiveRecord + def run_callbacks(batch) + if integration.issue_tracker? + Project.where(id: batch.select(:id)).update_all(has_external_issue_tracker: true) + end + + if integration.type == 'ExternalWikiService' + Project.where(id: batch.select(:id)).update_all(has_external_wiki: true) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def service_hash + if integration.template? + integration.to_service_hash + else + integration.to_service_hash.tap { |json| json['inherit_from_id'] = integration.id } + end + end + + def data_fields_hash + integration.to_data_fields_hash + end +end diff --git a/app/services/bulk_update_integration_service.rb b/app/services/bulk_update_integration_service.rb new file mode 100644 index 00000000000..74d77618f2c --- /dev/null +++ b/app/services/bulk_update_integration_service.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class BulkUpdateIntegrationService + def initialize(integration, batch) + @integration = integration + @batch = batch + end + + # rubocop: disable CodeReuse/ActiveRecord + def execute + Service.transaction do + batch.update_all(service_hash) + + if integration.data_fields_present? + integration.data_fields.class.where(service_id: batch.select(:id)).update_all(data_fields_hash) + end + end + end + # rubocop: enable CodeReuse/ActiveRecord + + private + + attr_reader :integration, :batch + + def service_hash + integration.to_service_hash.tap { |json| json['inherit_from_id'] = integration.id } + end + + def data_fields_hash + integration.to_data_fields_hash + end +end diff --git a/app/services/ci/create_job_artifacts_service.rb b/app/services/ci/create_job_artifacts_service.rb index 1fe65898d55..5efb3805bf7 100644 --- a/app/services/ci/create_job_artifacts_service.rb +++ b/app/services/ci/create_job_artifacts_service.rb @@ -52,24 +52,15 @@ module Ci attr_reader :job, :project def validate_requirements(artifact_type:, filesize:) - return forbidden_type_error(artifact_type) if forbidden_type?(artifact_type) return too_large_error if too_large?(artifact_type, filesize) success end - def forbidden_type?(type) - lsif?(type) && !code_navigation_enabled? - end - def too_large?(type, size) size > max_size(type) if size end - def code_navigation_enabled? - Feature.enabled?(:code_navigation, project, default_enabled: true) - end - def lsif?(type) type == LSIF_ARTIFACT_TYPE end diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 6b2e6c245f3..f397ada0696 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -58,7 +58,7 @@ module Ci build = project.builds.new(attributes) build.assign_attributes(::Gitlab::Ci::Pipeline::Seed::Build.environment_attributes_for(build)) build.retried = false - BulkInsertableAssociations.with_bulk_insert(enabled: ::Gitlab::Ci::Features.bulk_insert_on_create?(project)) do + BulkInsertableAssociations.with_bulk_insert do build.save! end build diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb index 31c7178c9e7..241eba733ea 100644 --- a/app/services/ci/update_build_queue_service.rb +++ b/app/services/ci/update_build_queue_service.rb @@ -9,9 +9,7 @@ module Ci private def tick_for(build, runners) - if Feature.enabled?(:ci_update_queues_for_online_runners, build.project, default_enabled: true) - runners = runners.with_recent_runner_queue - end + runners = runners.with_recent_runner_queue runners.each do |runner| runner.pick_build!(build) diff --git a/app/services/concerns/admin/propagate_service.rb b/app/services/concerns/admin/propagate_service.rb index 974408f678c..065ab6f7ff9 100644 --- a/app/services/concerns/admin/propagate_service.rb +++ b/app/services/concerns/admin/propagate_service.rb @@ -4,9 +4,7 @@ module Admin module PropagateService extend ActiveSupport::Concern - BATCH_SIZE = 100 - - delegate :data_fields_present?, to: :integration + BATCH_SIZE = 10_000 class_methods do def propagate(integration) @@ -23,51 +21,10 @@ module Admin attr_reader :integration def create_integration_for_projects_without_integration - loop do - batch_ids = Project.uncached { Project.ids_without_integration(integration, BATCH_SIZE) } - - bulk_create_from_integration(batch_ids, 'project') unless batch_ids.empty? - - break if batch_ids.size < BATCH_SIZE - end - end - - def bulk_create_from_integration(batch_ids, association) - service_list = ServiceList.new(batch_ids, service_hash, association).to_array - - Service.transaction do - results = bulk_insert(*service_list) - - if data_fields_present? - data_list = DataList.new(results, data_fields_hash, integration.data_fields.class).to_array - - bulk_insert(*data_list) - end - - run_callbacks(batch_ids) if association == 'project' + Project.without_integration(integration).each_batch(of: BATCH_SIZE) do |projects| + min_id, max_id = projects.pick("MIN(projects.id), MAX(projects.id)") + PropagateIntegrationProjectWorker.perform_async(integration.id, min_id, max_id) end end - - def bulk_insert(klass, columns, values_array) - items_to_insert = values_array.map { |array| Hash[columns.zip(array)] } - - klass.insert_all(items_to_insert, returning: [:id]) - end - - # rubocop: disable CodeReuse/ActiveRecord - def run_callbacks(batch_ids) - if integration.issue_tracker? - Project.where(id: batch_ids).update_all(has_external_issue_tracker: true) - end - - if integration.type == 'ExternalWikiService' - Project.where(id: batch_ids).update_all(has_external_wiki: true) - end - end - # rubocop: enable CodeReuse/ActiveRecord - - def data_fields_hash - @data_fields_hash ||= integration.to_data_fields_hash - end end end diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml index bbad5155ada..cf40eb7b108 100644 --- a/app/views/admin/application_settings/_gitpod.html.haml +++ b/app/views/admin/application_settings/_gitpod.html.haml @@ -1,6 +1,5 @@ - return unless Gitlab::Gitpod.feature_available? - expanded = integration_expanded?('gitpod_') -- gitpod_link = link_to("Gitpod#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}".html_safe, 'https://gitpod.io/', target: '_blank', rel: 'noopener noreferrer') %section.settings.no-animate#js-gitpod-settings{ class: ('expanded' if expanded) } .settings-header @@ -9,7 +8,7 @@ %button.btn.btn-default.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p - = s_('Enable %{gitpod_link} integration to launch a development environment in your browser directly from GitLab.').html_safe % { gitpod_link: gitpod_link } + = gitpod_enable_description = link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information') diff --git a/app/views/profiles/preferences/_gitpod.html.haml b/app/views/profiles/preferences/_gitpod.html.haml index 69c9443ebbb..589c3a27c18 100644 --- a/app/views/profiles/preferences/_gitpod.html.haml +++ b/app/views/profiles/preferences/_gitpod.html.haml @@ -1,5 +1,3 @@ -- gitpod_link = link_to("Gitpod#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}".html_safe, 'https://gitpod.io/', target: '_blank', rel: 'noopener noreferrer') - %label.label-bold#gitpod = s_('Gitpod') = link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information') @@ -8,4 +6,4 @@ = f.label :gitpod_enabled, class: 'form-check-label' do = s_('Gitpod|Enable Gitpod integration').html_safe .form-text.text-muted - = s_('Enable %{gitpod_link} integration to launch a development environment in your browser directly from GitLab.').html_safe % { gitpod_link: gitpod_link } + = gitpod_enable_description diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 9bb4342ffb4..54c47e7af38 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -9,9 +9,6 @@ = link_to "the file", project_blob_path(@project, tree_join(@branch_name, @file_path)), target: "_blank", rel: 'noopener noreferrer', class: 'gl-link' and make sure your changes will not unintentionally remove theirs. -- if editing_ci_config? && show_web_ide_alert? - #js-suggest-web-ide-ci{ data: { dismiss_endpoint: user_callouts_path, feature_id: UserCalloutsHelper::WEB_IDE_ALERT_DISMISSED, edit_path: ide_edit_path } } - .editor-title-row %h3.page-title.blob-edit-page-title Edit file diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 9a3078991c0..fcb79782acf 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -43,7 +43,6 @@ .tab-content#diff-notes-app #js-diff-file-finder - - if native_code_navigation_enabled?(@project) #js-code-navigation = render "projects/merge_requests/tabs/pane", id: "notes", class: "notes voting_notes" do .row diff --git a/app/views/shared/issuable/_approved_by_dropdown.html.haml b/app/views/shared/issuable/_approved_by_dropdown.html.haml new file mode 100644 index 00000000000..8014545ab85 --- /dev/null +++ b/app/views/shared/issuable/_approved_by_dropdown.html.haml @@ -0,0 +1,16 @@ +#js-dropdown-approved-by.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'None' } } + %button.btn.btn-link{ type: 'button' } + = _('None') + %li.filter-dropdown-item{ data: { value: 'Any' } } + %button.btn.btn-link{ type: 'button' } + = _('Any') + %li.divider.droplab-item-ignore + - if current_user + = render 'shared/issuable/user_dropdown_item', + user: current_user + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + = render 'shared/issuable/user_dropdown_item', + user: User.new(username: '{{username}}', name: '{{name}}'), + avatar: { lazy: true, url: '{{avatar_url}}' } diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 6a22844038e..d8cf05b9136 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1724,6 +1724,30 @@ :weight: 1 :idempotent: true :tags: [] +- :name: propagate_integration_group + :feature_category: :integrations + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] +- :name: propagate_integration_inherit + :feature_category: :integrations + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] +- :name: propagate_integration_project + :feature_category: :integrations + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: propagate_service_template :feature_category: :integrations :has_external_dependencies: diff --git a/app/workers/propagate_integration_group_worker.rb b/app/workers/propagate_integration_group_worker.rb new file mode 100644 index 00000000000..e539c6d4719 --- /dev/null +++ b/app/workers/propagate_integration_group_worker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class PropagateIntegrationGroupWorker + include ApplicationWorker + + feature_category :integrations + idempotent! + + # rubocop: disable CodeReuse/ActiveRecord + def perform(integration_id, min_id, max_id) + integration = Service.find_by_id(integration_id) + return unless integration + + batch = Group.where(id: min_id..max_id).without_integration(integration) + + BulkCreateIntegrationService.new(integration, batch, 'group').execute + end + # rubocop: enable CodeReuse/ActiveRecord +end diff --git a/app/workers/propagate_integration_inherit_worker.rb b/app/workers/propagate_integration_inherit_worker.rb new file mode 100644 index 00000000000..ef3132202f6 --- /dev/null +++ b/app/workers/propagate_integration_inherit_worker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class PropagateIntegrationInheritWorker + include ApplicationWorker + + feature_category :integrations + idempotent! + + # rubocop: disable CodeReuse/ActiveRecord + def perform(integration_id, min_id, max_id) + integration = Service.find_by_id(integration_id) + return unless integration + + services = Service.where(id: min_id..max_id).by_type(integration.type).inherit_from_id(integration.id) + + BulkUpdateIntegrationService.new(integration, services).execute + end + # rubocop: enable CodeReuse/ActiveRecord +end diff --git a/app/workers/propagate_integration_project_worker.rb b/app/workers/propagate_integration_project_worker.rb new file mode 100644 index 00000000000..c1e286b24fc --- /dev/null +++ b/app/workers/propagate_integration_project_worker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class PropagateIntegrationProjectWorker + include ApplicationWorker + + feature_category :integrations + idempotent! + + # rubocop: disable CodeReuse/ActiveRecord + def perform(integration_id, min_id, max_id) + integration = Service.find_by_id(integration_id) + return unless integration + + batch = Project.where(id: min_id..max_id).without_integration(integration) + + BulkCreateIntegrationService.new(integration, batch, 'project').execute + end + # rubocop: enable CodeReuse/ActiveRecord +end |