diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-11 03:08:58 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-11 03:08:58 +0300 |
commit | 5427433c6d79f9131f4025cabb7e3208380bce9a (patch) | |
tree | ea0a22450f467f1ef1e3449255017dbe0f178882 /app | |
parent | 13bcb8221306526671a61df589f7c05505c9934c (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
54 files changed, 808 insertions, 161 deletions
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index a800cc8edc8..9d8d184a3f6 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -20,6 +20,8 @@ import HiddenFilesWarning from './hidden_files_warning.vue'; import MergeConflictWarning from './merge_conflict_warning.vue'; import CollapsedFilesWarning from './collapsed_files_warning.vue'; +import { diffsApp } from '../utils/performance'; + import { TREE_LIST_WIDTH_STORAGE_KEY, INITIAL_TREE_WIDTH, @@ -272,8 +274,12 @@ export default { ); } }, + beforeCreate() { + diffsApp.instrument(); + }, created() { this.adjustView(); + eventHub.$once('fetchDiffData', this.fetchData); eventHub.$on('refetchDiffData', this.refetchDiffData); this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES; @@ -294,6 +300,8 @@ export default { ); }, beforeDestroy() { + diffsApp.deinstrument(); + eventHub.$off('fetchDiffData', this.fetchData); eventHub.$off('refetchDiffData', this.refetchDiffData); this.removeEventListeners(); @@ -487,9 +495,11 @@ export default { <div v-if="isBatchLoading" class="loading"><gl-loading-icon size="lg" /></div> <template v-else-if="renderDiffFiles"> <diff-file - v-for="file in diffs" + v-for="(file, index) in diffs" :key="file.newPath" :file="file" + :is-first-file="index === 0" + :is-last-file="index === diffs.length - 1" :help-page-path="helpPagePath" :can-current-user-fork="canCurrentUserFork" :view-diffs-file-by-file="viewDiffsFileByFile" diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 27bda194c5c..59bd2d90158 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -15,6 +15,8 @@ import { DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE, EVT_EXPAND_ALL_FILES, + EVT_PERF_MARK_DIFF_FILES_END, + EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN, } from '../constants'; import { DIFF_FILE, GENERIC_ERROR } from '../i18n'; import eventHub from '../event_hub'; @@ -35,6 +37,16 @@ export default { type: Object, required: true, }, + isFirstFile: { + type: Boolean, + required: false, + default: false, + }, + isLastFile: { + type: Boolean, + required: false, + default: false, + }, canCurrentUserFork: { type: Boolean, required: true, @@ -160,6 +172,11 @@ export default { notesEventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.requestDiff); eventHub.$on(EVT_EXPAND_ALL_FILES, this.expandAllListener); }, + async mounted() { + if (this.hasDiff) { + await this.postRender(); + } + }, beforeDestroy() { eventHub.$off(EVT_EXPAND_ALL_FILES, this.expandAllListener); }, @@ -175,6 +192,23 @@ export default { this.handleToggle(); } }, + async postRender() { + const eventsForThisFile = []; + + if (this.isFirstFile) { + eventsForThisFile.push(EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN); + } + + if (this.isLastFile) { + eventsForThisFile.push(EVT_PERF_MARK_DIFF_FILES_END); + } + + await this.$nextTick(); + + eventsForThisFile.forEach(event => { + eventHub.$emit(event); + }); + }, handleToggle() { const currentCollapsedFlag = this.isCollapsed; @@ -197,7 +231,8 @@ export default { }) .then(() => { requestIdleCallback( - () => { + async () => { + await this.postRender(); this.assignDiscussionsToDiff(this.getDiffFileDiscussions(this.file)); }, { timeout: 1000 }, diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 709bfe693e6..79f8c08e389 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -98,3 +98,8 @@ export const RENAMED_DIFF_TRANSITIONS = { // MR Diffs known events export const EVT_EXPAND_ALL_FILES = 'mr:diffs:expandAllFiles'; +export const EVT_PERF_MARK_FILE_TREE_START = 'mr:diffs:perf:fileTreeStart'; +export const EVT_PERF_MARK_FILE_TREE_END = 'mr:diffs:perf:fileTreeEnd'; +export const EVT_PERF_MARK_DIFF_FILES_START = 'mr:diffs:perf:filesStart'; +export const EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN = 'mr:diffs:perf:firstFileShown'; +export const EVT_PERF_MARK_DIFF_FILES_END = 'mr:diffs:perf:filesEnd'; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 5a3d836a158..72b99ca8486 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -8,7 +8,8 @@ import { __, s__ } from '~/locale'; import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils'; import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility'; import TreeWorker from '../workers/tree_worker'; -import eventHub from '../../notes/event_hub'; +import notesEventHub from '../../notes/event_hub'; +import eventHub from '../event_hub'; import { getDiffPositionByLineCode, getNoteFormData, @@ -42,6 +43,9 @@ import { NO_SHOW_WHITESPACE, DIFF_FILE_MANUAL_COLLAPSE, DIFF_FILE_AUTOMATIC_COLLAPSE, + EVT_PERF_MARK_FILE_TREE_START, + EVT_PERF_MARK_FILE_TREE_END, + EVT_PERF_MARK_DIFF_FILES_START, } from '../constants'; import { diffViewerModes } from '~/ide/constants'; import { isCollapsed } from '../diff_file'; @@ -78,6 +82,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { commit(types.SET_BATCH_LOADING, true); commit(types.SET_RETRIEVING_BATCHES, true); + eventHub.$emit(EVT_PERF_MARK_DIFF_FILES_START); const getBatch = (page = 1) => axios @@ -139,9 +144,11 @@ export const fetchDiffFilesMeta = ({ commit, state }) => { }; commit(types.SET_LOADING, true); + eventHub.$emit(EVT_PERF_MARK_FILE_TREE_START); worker.addEventListener('message', ({ data }) => { commit(types.SET_TREE_DATA, data); + eventHub.$emit(EVT_PERF_MARK_FILE_TREE_END); worker.terminate(); }); @@ -215,7 +222,7 @@ export const assignDiscussionsToDiff = ( } Vue.nextTick(() => { - eventHub.$emit('scrollToDiscussion'); + notesEventHub.$emit('scrollToDiscussion'); }); }; @@ -240,7 +247,7 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi } if (file.viewer.automaticallyCollapsed) { - eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`); + notesEventHub.$emit(`loadCollapsedDiff/${file.file_hash}`); scrollToElement(document.getElementById(file.file_hash)); } else if (file.viewer.manuallyCollapsed) { commit(types.SET_FILE_COLLAPSED, { @@ -248,9 +255,9 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi collapsed: false, trigger: DIFF_FILE_AUTOMATIC_COLLAPSE, }); - eventHub.$emit('scrollToDiscussion'); + notesEventHub.$emit('scrollToDiscussion'); } else { - eventHub.$emit('scrollToDiscussion'); + notesEventHub.$emit('scrollToDiscussion'); } } } @@ -485,7 +492,7 @@ export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = fals historyPushState(mergeUrlParams({ w }, window.location.href)); } - eventHub.$emit('refetchDiffData'); + notesEventHub.$emit('refetchDiffData'); }; export const toggleFileFinder = ({ commit }, visible) => { diff --git a/app/assets/javascripts/diffs/utils/performance.js b/app/assets/javascripts/diffs/utils/performance.js new file mode 100644 index 00000000000..dcde6f4ecc4 --- /dev/null +++ b/app/assets/javascripts/diffs/utils/performance.js @@ -0,0 +1,80 @@ +import { performanceMarkAndMeasure } from '~/performance/utils'; +import { + MR_DIFFS_MARK_FILE_TREE_START, + MR_DIFFS_MARK_FILE_TREE_END, + MR_DIFFS_MARK_DIFF_FILES_START, + MR_DIFFS_MARK_FIRST_DIFF_FILE_SHOWN, + MR_DIFFS_MARK_DIFF_FILES_END, + MR_DIFFS_MEASURE_FILE_TREE_DONE, + MR_DIFFS_MEASURE_DIFF_FILES_DONE, +} from '../../performance/constants'; + +import eventHub from '../event_hub'; +import { + EVT_PERF_MARK_FILE_TREE_START, + EVT_PERF_MARK_FILE_TREE_END, + EVT_PERF_MARK_DIFF_FILES_START, + EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN, + EVT_PERF_MARK_DIFF_FILES_END, +} from '../constants'; + +function treeStart() { + performanceMarkAndMeasure({ + mark: MR_DIFFS_MARK_FILE_TREE_START, + }); +} + +function treeEnd() { + performanceMarkAndMeasure({ + mark: MR_DIFFS_MARK_FILE_TREE_END, + measures: [ + { + name: MR_DIFFS_MEASURE_FILE_TREE_DONE, + start: MR_DIFFS_MARK_FILE_TREE_START, + end: MR_DIFFS_MARK_FILE_TREE_END, + }, + ], + }); +} + +function filesStart() { + performanceMarkAndMeasure({ + mark: MR_DIFFS_MARK_DIFF_FILES_START, + }); +} + +function filesEnd() { + performanceMarkAndMeasure({ + mark: MR_DIFFS_MARK_DIFF_FILES_END, + measures: [ + { + name: MR_DIFFS_MEASURE_DIFF_FILES_DONE, + start: MR_DIFFS_MARK_DIFF_FILES_START, + end: MR_DIFFS_MARK_DIFF_FILES_END, + }, + ], + }); +} + +function firstFile() { + performanceMarkAndMeasure({ + mark: MR_DIFFS_MARK_FIRST_DIFF_FILE_SHOWN, + }); +} + +export const diffsApp = { + instrument() { + eventHub.$on(EVT_PERF_MARK_FILE_TREE_START, treeStart); + eventHub.$on(EVT_PERF_MARK_FILE_TREE_END, treeEnd); + eventHub.$on(EVT_PERF_MARK_DIFF_FILES_START, filesStart); + eventHub.$on(EVT_PERF_MARK_DIFF_FILES_END, filesEnd); + eventHub.$on(EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN, firstFile); + }, + deinstrument() { + eventHub.$off(EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN, firstFile); + eventHub.$off(EVT_PERF_MARK_DIFF_FILES_END, filesEnd); + eventHub.$off(EVT_PERF_MARK_DIFF_FILES_START, filesStart); + eventHub.$off(EVT_PERF_MARK_FILE_TREE_END, treeEnd); + eventHub.$off(EVT_PERF_MARK_FILE_TREE_START, treeStart); + }, +}; diff --git a/app/assets/javascripts/performance/constants.js b/app/assets/javascripts/performance/constants.js index 6b6b6f1da40..816eb9b3a66 100644 --- a/app/assets/javascripts/performance/constants.js +++ b/app/assets/javascripts/performance/constants.js @@ -29,3 +29,17 @@ export const WEBIDE_MARK_FILE_FINISH = 'webide-file-finished'; export const WEBIDE_MEASURE_TREE_FROM_REQUEST = 'webide-tree-loading-from-request'; export const WEBIDE_MEASURE_FILE_FROM_REQUEST = 'webide-file-loading-from-request'; export const WEBIDE_MEASURE_FILE_AFTER_INTERACTION = 'webide-file-loading-after-interaction'; + +// +// MR Diffs namespace + +// Marks +export const MR_DIFFS_MARK_FILE_TREE_START = 'mr-diffs-mark-file-tree-start'; +export const MR_DIFFS_MARK_FILE_TREE_END = 'mr-diffs-mark-file-tree-end'; +export const MR_DIFFS_MARK_DIFF_FILES_START = 'mr-diffs-mark-diff-files-start'; +export const MR_DIFFS_MARK_FIRST_DIFF_FILE_SHOWN = 'mr-diffs-mark-first-diff-file-shown'; +export const MR_DIFFS_MARK_DIFF_FILES_END = 'mr-diffs-mark-diff-files-end'; + +// Measures +export const MR_DIFFS_MEASURE_FILE_TREE_DONE = 'mr-diffs-measure-file-tree-done'; +export const MR_DIFFS_MEASURE_DIFF_FILES_DONE = 'mr-diffs-measure-diff-files-done'; diff --git a/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue b/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue new file mode 100644 index 00000000000..08619fa2066 --- /dev/null +++ b/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue @@ -0,0 +1,108 @@ +<script> +import { mapState } from 'vuex'; +import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; +import { setUrlParams, visitUrl } from '~/lib/utils/url_utility'; +import { sprintf, s__ } from '~/locale'; + +export default { + name: 'DropdownFilter', + components: { + GlDropdown, + GlDropdownItem, + GlDropdownDivider, + }, + props: { + filterData: { + type: Object, + required: true, + }, + }, + computed: { + ...mapState(['query']), + scope() { + return this.query.scope; + }, + supportedScopes() { + return Object.values(this.filterData.scopes); + }, + initialFilter() { + return this.query[this.filterData.filterParam]; + }, + filter() { + return this.initialFilter || this.filterData.filters.ANY.value; + }, + filtersArray() { + return this.filterData.filterByScope[this.scope]; + }, + selectedFilter: { + get() { + if (this.filtersArray.some(({ value }) => value === this.filter)) { + return this.filter; + } + + return this.filterData.filters.ANY.value; + }, + set(filter) { + // we need to remove the pagination cursor to ensure the + // relevancy of the new results + + visitUrl( + setUrlParams({ + page: null, + [this.filterData.filterParam]: filter, + }), + ); + }, + }, + selectedFilterText() { + const f = this.filtersArray.find(({ value }) => value === this.selectedFilter); + if (!f || f === this.filterData.filters.ANY) { + return sprintf(s__('Any %{header}'), { header: this.filterData.header }); + } + + return f.label; + }, + showDropdown() { + return this.supportedScopes.includes(this.scope); + }, + }, + methods: { + dropDownItemClass(filter) { + return { + 'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2': + filter === this.filterData.filters.ANY, + }; + }, + isFilterSelected(filter) { + return filter === this.selectedFilter; + }, + handleFilterChange(filter) { + this.selectedFilter = filter; + }, + }, +}; +</script> + +<template> + <gl-dropdown + v-if="showDropdown" + :text="selectedFilterText" + class="col-3 gl-pt-4 gl-pl-0 gl-pr-0 gl-mr-4" + menu-class="gl-w-full! gl-pl-0" + > + <header class="gl-text-center gl-font-weight-bold gl-font-lg"> + {{ filterData.header }} + </header> + <gl-dropdown-divider /> + <gl-dropdown-item + v-for="f in filtersArray" + :key="f.value" + :is-check-item="true" + :is-checked="isFilterSelected(f.value)" + :class="dropDownItemClass(f)" + @click="handleFilterChange(f.value)" + > + {{ f.label }} + </gl-dropdown-item> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js b/app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js new file mode 100644 index 00000000000..b29daca89cb --- /dev/null +++ b/app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js @@ -0,0 +1,36 @@ +import { __ } from '~/locale'; + +const header = __('Confidentiality'); + +const filters = { + ANY: { + label: __('Any'), + value: null, + }, + CONFIDENTIAL: { + label: __('Confidential'), + value: 'yes', + }, + NOT_CONFIDENTIAL: { + label: __('Not confidential'), + value: 'no', + }, +}; + +const scopes = { + ISSUES: 'issues', +}; + +const filterByScope = { + [scopes.ISSUES]: [filters.ANY, filters.CONFIDENTIAL, filters.NOT_CONFIDENTIAL], +}; + +const filterParam = 'confidential'; + +export default { + header, + filters, + scopes, + filterByScope, + filterParam, +}; diff --git a/app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js b/app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js new file mode 100644 index 00000000000..0b93aa0be29 --- /dev/null +++ b/app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js @@ -0,0 +1,42 @@ +import { __ } from '~/locale'; + +const header = __('Status'); + +const filters = { + ANY: { + label: __('Any'), + value: 'all', + }, + OPEN: { + label: __('Open'), + value: 'opened', + }, + CLOSED: { + label: __('Closed'), + value: 'closed', + }, + MERGED: { + label: __('Merged'), + value: 'merged', + }, +}; + +const scopes = { + ISSUES: 'issues', + MERGE_REQUESTS: 'merge_requests', +}; + +const filterByScope = { + [scopes.ISSUES]: [filters.ANY, filters.OPEN, filters.CLOSED], + [scopes.MERGE_REQUESTS]: [filters.ANY, filters.OPEN, filters.MERGED, filters.CLOSED], +}; + +const filterParam = 'state'; + +export default { + header, + filters, + scopes, + filterByScope, + filterParam, +}; diff --git a/app/assets/javascripts/search/dropdown_filter/index.js b/app/assets/javascripts/search/dropdown_filter/index.js new file mode 100644 index 00000000000..e5e0745d990 --- /dev/null +++ b/app/assets/javascripts/search/dropdown_filter/index.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import DropdownFilter from './components/dropdown_filter.vue'; +import stateFilterData from './constants/state_filter_data'; +import confidentialFilterData from './constants/confidential_filter_data'; + +Vue.use(Translate); + +const mountDropdownFilter = (store, { id, filterData }) => { + const el = document.getElementById(id); + + if (!el) return false; + + return new Vue({ + el, + store, + render(createElement) { + return createElement(DropdownFilter, { + props: { + filterData, + }, + }); + }, + }); +}; + +const dropdownFilters = [ + { + id: 'js-search-filter-by-state', + filterData: stateFilterData, + }, + { + id: 'js-search-filter-by-confidential', + filterData: confidentialFilterData, + }, +]; + +export default store => [...dropdownFilters].map(filter => mountDropdownFilter(store, filter)); diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js index 7508b3c9a55..275d6351adc 100644 --- a/app/assets/javascripts/search/index.js +++ b/app/assets/javascripts/search/index.js @@ -1,11 +1,17 @@ import { queryToObject } from '~/lib/utils/url_utility'; import createStore from './store'; +import initDropdownFilters from './dropdown_filter'; import { initSidebar } from './sidebar'; import initGroupFilter from './group_filter'; export default () => { const store = createStore({ query: queryToObject(window.location.search) }); - initSidebar(store); + if (gon.features.searchFacets) { + initSidebar(store); + } else { + initDropdownFilters(store); + } + initGroupFilter(store); }; diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue deleted file mode 100644 index 0c50f93d381..00000000000 --- a/app/assets/javascripts/search/sidebar/components/app.vue +++ /dev/null @@ -1,41 +0,0 @@ -<script> -import { mapActions, mapState } from 'vuex'; -import { GlButton, GlLink } from '@gitlab/ui'; -import StatusFilter from './status_filter.vue'; -import ConfidentialityFilter from './confidentiality_filter.vue'; - -export default { - name: 'GlobalSearchSidebar', - components: { - GlButton, - GlLink, - StatusFilter, - ConfidentialityFilter, - }, - computed: { - ...mapState(['query']), - showReset() { - return this.query.state || this.query.confidential; - }, - }, - methods: { - ...mapActions(['applyQuery', 'resetQuery']), - }, -}; -</script> - -<template> - <form - class="gl-display-flex gl-flex-direction-column col-md-3 gl-mr-4 gl-mb-6 gl-mb gl-mt-5" - @submit.prevent="applyQuery" - > - <status-filter /> - <confidentiality-filter /> - <div class="gl-display-flex gl-align-items-center gl-mt-3"> - <gl-button variant="success" type="submit">{{ __('Apply') }}</gl-button> - <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{ - __('Reset filters') - }}</gl-link> - </div> - </form> -</template> diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue index 38dccb9675d..f8938e799aa 100644 --- a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue +++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue @@ -21,6 +21,5 @@ export default { <template> <div v-if="showDropdown"> <radio-filter :filter-data="$options.confidentialFilterData" /> - <hr class="gl-my-5 gl-border-gray-100" /> </div> </template> diff --git a/app/assets/javascripts/search/sidebar/components/status_filter.vue b/app/assets/javascripts/search/sidebar/components/status_filter.vue index 5cec2090906..876123ccc52 100644 --- a/app/assets/javascripts/search/sidebar/components/status_filter.vue +++ b/app/assets/javascripts/search/sidebar/components/status_filter.vue @@ -21,6 +21,5 @@ export default { <template> <div v-if="showDropdown"> <radio-filter :filter-data="$options.stateFilterData" /> - <hr class="gl-my-5 gl-border-gray-100" /> </div> </template> diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js index 6419e8ac2c6..b19016edf3d 100644 --- a/app/assets/javascripts/search/sidebar/index.js +++ b/app/assets/javascripts/search/sidebar/index.js @@ -1,11 +1,12 @@ import Vue from 'vue'; import Translate from '~/vue_shared/translate'; -import GlobalSearchSidebar from './components/app.vue'; +import StatusFilter from './components/status_filter.vue'; +import ConfidentialityFilter from './components/confidentiality_filter.vue'; Vue.use(Translate); -export const initSidebar = store => { - const el = document.getElementById('js-search-sidebar'); +const mountRadioFilters = (store, { id, component }) => { + const el = document.getElementById(id); if (!el) return false; @@ -13,7 +14,21 @@ export const initSidebar = store => { el, store, render(createElement) { - return createElement(GlobalSearchSidebar); + return createElement(component); }, }); }; + +const radioFilters = [ + { + id: 'js-search-filter-by-state', + component: StatusFilter, + }, + { + id: 'js-search-filter-by-confidential', + component: ConfidentialityFilter, + }, +]; + +export const initSidebar = store => + [...radioFilters].map(filter => mountRadioFilters(store, filter)); diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js index 447278aa223..722ed2eec26 100644 --- a/app/assets/javascripts/search/store/actions.js +++ b/app/assets/javascripts/search/store/actions.js @@ -1,7 +1,6 @@ import Api from '~/api'; import createFlash from '~/flash'; import { __ } from '~/locale'; -import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; import * as types from './mutation_types'; export const fetchGroups = ({ commit }, search) => { @@ -19,11 +18,3 @@ export const fetchGroups = ({ commit }, search) => { export const setQuery = ({ commit }, { key, value }) => { commit(types.SET_QUERY, { key, value }); }; - -export const applyQuery = ({ state }) => { - visitUrl(setUrlParams({ ...state.query, page: null })); -}; - -export const resetQuery = ({ state }) => { - visitUrl(setUrlParams({ ...state.query, page: null, state: null, confidential: null })); -}; diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb index 2844acea271..bc3fd32759f 100644 --- a/app/controllers/concerns/lfs_request.rb +++ b/app/controllers/concerns/lfs_request.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true # This concern assumes: +# - a `#container` accessor # - a `#project` accessor # - a `#user` accessor # - a `#authentication_result` accessor @@ -11,6 +12,7 @@ # - a `#has_authentication_ability?(ability)` method module LfsRequest extend ActiveSupport::Concern + include Gitlab::Utils::StrongMemoize CONTENT_TYPE = 'application/vnd.git-lfs+json' @@ -29,16 +31,19 @@ module LfsRequest message: _('Git LFS is not enabled on this GitLab server, contact your admin.'), documentation_url: help_url }, + content_type: CONTENT_TYPE, status: :not_implemented ) end def lfs_check_access! - return render_lfs_not_found unless project + return render_lfs_not_found unless container&.lfs_enabled? return if download_request? && lfs_download_access? return if upload_request? && lfs_upload_access? - if project.public? || can?(user, :read_project, project) + # Only return a 403 response if the user has download access permission, + # otherwise return a 404 to avoid exposing the existence of the container. + if lfs_download_access? lfs_forbidden! else render_lfs_not_found @@ -72,9 +77,9 @@ module LfsRequest end def lfs_download_access? - return false unless project.lfs_enabled? - - ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? || deploy_token_can_download_code? + strong_memoize(:lfs_download_access) do + ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? || deploy_token_can_download_code? + end end def deploy_token_can_download_code? @@ -93,11 +98,12 @@ module LfsRequest end def lfs_upload_access? - return false unless project.lfs_enabled? - return false unless has_authentication_ability?(:push_code) - return false if limit_exceeded? + strong_memoize(:lfs_upload_access) do + next false unless has_authentication_ability?(:push_code) + next false if limit_exceeded? - lfs_deploy_token? || can?(user, :push_code, project) + lfs_deploy_token? || can?(user, :push_code, project) + end end def lfs_deploy_token? diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index c9333bab69b..c2d72610c66 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -8,7 +8,7 @@ class Groups::BoardsController < Groups::ApplicationController before_action :assign_endpoint_vars before_action do push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false) - push_frontend_feature_flag(:boards_with_swimlanes, group, default_enabled: false) + push_frontend_feature_flag(:boards_with_swimlanes, group, default_enabled: true) end feature_category :boards diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 87207e30c1f..fe4502a0e06 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -8,7 +8,7 @@ class Projects::BoardsController < Projects::ApplicationController before_action :authorize_read_board!, only: [:index, :show] before_action :assign_endpoint_vars before_action do - push_frontend_feature_flag(:boards_with_swimlanes, project, default_enabled: false) + push_frontend_feature_flag(:boards_with_swimlanes, project, default_enabled: true) end feature_category :boards diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 8ac83edef8d..f76278a12a4 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -5,6 +5,8 @@ module Projects class CiCdController < Projects::ApplicationController include RunnerSetupScripts + NUMBER_OF_RUNNERS_PER_PAGE = 20 + before_action :authorize_admin_pipeline! before_action :define_variables before_action do @@ -108,13 +110,13 @@ module Projects end def define_runners_variables - @project_runners = @project.runners.ordered + @project_runners = @project.runners.ordered.page(params[:project_page]).per(NUMBER_OF_RUNNERS_PER_PAGE).with_tags @assignable_runners = current_user .ci_owned_runners .assignable_for(project) .ordered - .page(params[:page]).per(20) + .page(params[:specific_page]).per(NUMBER_OF_RUNNERS_PER_PAGE) @shared_runners = ::Ci::Runner.instance_type.active diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb index de452aa69b7..ec854bd0dde 100644 --- a/app/controllers/repositories/git_http_client_controller.rb +++ b/app/controllers/repositories/git_http_client_controller.rb @@ -6,7 +6,7 @@ module Repositories include KerberosSpnegoHelper include Gitlab::Utils::StrongMemoize - attr_reader :authentication_result, :redirected_path, :container + attr_reader :authentication_result, :redirected_path delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result @@ -75,6 +75,12 @@ module Repositories headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? end + def container + parse_repo_path unless defined?(@container) + + @container + end + def project parse_repo_path unless defined?(@project) diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb index 35751a2578f..96185608c09 100644 --- a/app/controllers/repositories/lfs_api_controller.rb +++ b/app/controllers/repositories/lfs_api_controller.rb @@ -17,9 +17,9 @@ module Repositories end if download_request? - render json: { objects: download_objects! } + render json: { objects: download_objects! }, content_type: LfsRequest::CONTENT_TYPE elsif upload_request? - render json: { objects: upload_objects! } + render json: { objects: upload_objects! }, content_type: LfsRequest::CONTENT_TYPE else raise "Never reached" end @@ -31,6 +31,7 @@ module Repositories message: _('Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.'), documentation_url: "#{Gitlab.config.gitlab.url}/help" }, + content_type: LfsRequest::CONTENT_TYPE, status: :not_implemented ) end diff --git a/app/controllers/repositories/lfs_storage_controller.rb b/app/controllers/repositories/lfs_storage_controller.rb index 64a634b2cc2..48784842d48 100644 --- a/app/controllers/repositories/lfs_storage_controller.rb +++ b/app/controllers/repositories/lfs_storage_controller.rb @@ -29,7 +29,7 @@ module Repositories def upload_finalize if store_file!(oid, size) - head 200 + head 200, content_type: LfsRequest::CONTENT_TYPE else render plain: 'Unprocessable entity', status: :unprocessable_entity end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 4b21edc98d5..0f149c24a59 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -24,6 +24,10 @@ class SearchController < ApplicationController search_term_present && !params[:project_id].present? end + before_action do + push_frontend_feature_flag(:search_facets) + end + layout 'search' feature_category :global_search diff --git a/app/graphql/mutations/releases/base.rb b/app/graphql/mutations/releases/base.rb new file mode 100644 index 00000000000..d53cfbe6a11 --- /dev/null +++ b/app/graphql/mutations/releases/base.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Mutations + module Releases + class Base < BaseMutation + include ResolvesProject + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'Full path of the project the release is associated with' + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + end + end +end diff --git a/app/graphql/mutations/releases/create.rb b/app/graphql/mutations/releases/create.rb new file mode 100644 index 00000000000..57c1541c368 --- /dev/null +++ b/app/graphql/mutations/releases/create.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Mutations + module Releases + class Create < Base + graphql_name 'ReleaseCreate' + + field :release, + Types::ReleaseType, + null: true, + description: 'The release after mutation' + + argument :tag_name, GraphQL::STRING_TYPE, + required: true, as: :tag, + description: 'Name of the tag to associate with the release' + + argument :ref, GraphQL::STRING_TYPE, + required: false, + description: 'The commit SHA or branch name to use if creating a new tag' + + argument :name, GraphQL::STRING_TYPE, + required: false, + description: 'Name of the release' + + argument :description, GraphQL::STRING_TYPE, + required: false, + description: 'Description (also known as "release notes") of the release' + + argument :released_at, Types::TimeType, + required: false, + description: 'The date when the release will be/was ready. Defaults to the current time.' + + argument :milestones, [GraphQL::STRING_TYPE], + required: false, + description: 'The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.' + + argument :assets, Types::ReleaseAssetsInputType, + required: false, + description: 'Assets associated to the release' + + authorize :create_release + + def resolve(project_path:, milestones: nil, assets: nil, **scalars) + project = authorized_find!(full_path: project_path) + + params = { + **scalars, + milestones: milestones.presence || [], + assets: assets.to_h + }.with_indifferent_access + + result = ::Releases::CreateService.new(project, current_user, params).execute + + if result[:status] == :success + { + release: result[:release], + errors: [] + } + else + { + release: nil, + errors: [result[:message]] + } + end + end + end + end +end diff --git a/app/graphql/resolvers/metadata_resolver.rb b/app/graphql/resolvers/metadata_resolver.rb index 3a79e6434fb..26bfa81038c 100644 --- a/app/graphql/resolvers/metadata_resolver.rb +++ b/app/graphql/resolvers/metadata_resolver.rb @@ -5,7 +5,7 @@ module Resolvers type Types::MetadataType, null: false def resolve(**args) - { version: Gitlab::VERSION, revision: Gitlab.revision } + ::InstanceMetadata.new end end end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 1c5202f428c..c2262fdd0e3 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -63,6 +63,7 @@ module Types 'destroyed during the update, and no Note will be returned' mount_mutation Mutations::Notes::RepositionImageDiffNote mount_mutation Mutations::Notes::Destroy + mount_mutation Mutations::Releases::Create mount_mutation Mutations::Terraform::State::Delete mount_mutation Mutations::Terraform::State::Lock mount_mutation Mutations::Terraform::State::Unlock diff --git a/app/graphql/types/release_asset_link_input_type.rb b/app/graphql/types/release_asset_link_input_type.rb new file mode 100644 index 00000000000..d13861fad8f --- /dev/null +++ b/app/graphql/types/release_asset_link_input_type.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class ReleaseAssetLinkInputType < BaseInputObject + graphql_name 'ReleaseAssetLinkInput' + description 'Fields that are available when modifying a release asset link' + + argument :name, GraphQL::STRING_TYPE, + required: true, + description: 'Name of the asset link' + + argument :url, GraphQL::STRING_TYPE, + required: true, + description: 'URL of the asset link' + + argument :direct_asset_path, GraphQL::STRING_TYPE, + required: false, as: :filepath, + description: 'Relative path for a direct asset link' + + argument :link_type, Types::ReleaseAssetLinkTypeEnum, + required: false, default_value: 'other', + description: 'The type of the asset link' + end +end diff --git a/app/graphql/types/release_asset_link_type_enum.rb b/app/graphql/types/release_asset_link_type_enum.rb index 01862ada56d..70601b9f8da 100644 --- a/app/graphql/types/release_asset_link_type_enum.rb +++ b/app/graphql/types/release_asset_link_type_enum.rb @@ -3,7 +3,7 @@ module Types class ReleaseAssetLinkTypeEnum < BaseEnum graphql_name 'ReleaseAssetLinkType' - description 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`' + description 'Type of the link: `other`, `runbook`, `image`, `package`' ::Releases::Link.link_types.keys.each do |link_type| value link_type.upcase, value: link_type, description: "#{link_type.titleize} link type" diff --git a/app/graphql/types/release_assets_input_type.rb b/app/graphql/types/release_assets_input_type.rb new file mode 100644 index 00000000000..3fcb517e044 --- /dev/null +++ b/app/graphql/types/release_assets_input_type.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class ReleaseAssetsInputType < BaseInputObject + graphql_name 'ReleaseAssetsInput' + description 'Fields that are available when modifying release assets' + + argument :links, [Types::ReleaseAssetLinkInputType], + required: false, + description: 'A list of asset links to associate to the release' + end +end diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb index 61cc15a522e..7ce7f40b6a8 100644 --- a/app/models/alert_management/alert.rb +++ b/app/models/alert_management/alert.rb @@ -34,7 +34,7 @@ module AlertManagement has_many :ordered_notes, -> { fresh }, as: :noteable, class_name: 'Note' has_many :user_mentions, class_name: 'AlertManagement::AlertUserMention', foreign_key: :alert_management_alert_id - has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) } + has_internal_id :iid, scope: :project sha_attribute :fingerprint diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 8083d4ed48a..c516b42dbd0 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -42,9 +42,16 @@ module Ci belongs_to :external_pull_request belongs_to :ci_ref, class_name: 'Ci::Ref', foreign_key: :ci_ref_id, inverse_of: :pipelines - has_internal_id :iid, scope: :project, presence: false, track_if: -> { !importing? }, ensure_if: -> { !importing? }, init: ->(s) do - s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count - end + has_internal_id :iid, scope: :project, presence: false, + track_if: -> { !importing? }, + ensure_if: -> { !importing? }, + init: ->(pipeline, scope) do + if pipeline + pipeline.project&.all_pipelines&.maximum(:iid) || pipeline.project&.all_pipelines&.count + elsif scope + ::Ci::Pipeline.where(**scope).maximum(:iid) + end + end has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb index 4a632e8cd0c..baa99fa5a7f 100644 --- a/app/models/concerns/atomic_internal_id.rb +++ b/app/models/concerns/atomic_internal_id.rb @@ -27,16 +27,42 @@ module AtomicInternalId extend ActiveSupport::Concern class_methods do - def has_internal_id(column, scope:, init:, ensure_if: nil, track_if: nil, presence: true, backfill: false) # rubocop:disable Naming/PredicateName - # We require init here to retain the ability to recalculate in the absence of a - # InternalId record (we may delete records in `internal_ids` for example). - raise "has_internal_id requires a init block, none given." unless init + def has_internal_id( # rubocop:disable Naming/PredicateName + column, scope:, init: :not_given, ensure_if: nil, track_if: nil, + presence: true, backfill: false, hook_names: :create) + raise "has_internal_id init must not be nil if given." if init.nil? raise "has_internal_id needs to be defined on association." unless self.reflect_on_association(scope) - before_validation :"track_#{scope}_#{column}!", on: :create, if: track_if - before_validation :"ensure_#{scope}_#{column}!", on: :create, if: ensure_if + init = infer_init(scope) if init == :not_given + before_validation :"track_#{scope}_#{column}!", on: hook_names, if: track_if + before_validation :"ensure_#{scope}_#{column}!", on: hook_names, if: ensure_if validates column, presence: presence + define_singleton_internal_id_methods(scope, column, init) + define_instance_internal_id_methods(scope, column, init, backfill) + end + + private + + def infer_init(scope) + case scope + when :project + AtomicInternalId.project_init(self) + when :group + AtomicInternalId.group_init(self) + else + # We require init here to retain the ability to recalculate in the absence of a + # InternalId record (we may delete records in `internal_ids` for example). + raise "has_internal_id - cannot infer init for scope: #{scope}" + end + end + + # Defines instance methods: + # - ensure_{scope}_{column}! + # - track_{scope}_{column}! + # - reset_{scope}_{column} + # - {column}= + def define_instance_internal_id_methods(scope, column, init, backfill) define_method("ensure_#{scope}_#{column}!") do return if backfill && self.class.where(column => nil).exists? @@ -103,19 +129,95 @@ module AtomicInternalId read_attribute(column) end end + + # Defines class methods: + # + # - with_{scope}_{column}_supply + # This method can be used to allocate a block of IID values during + # bulk operations (importing/copying, etc). This can be more efficient + # than creating instances one-by-one. + # + # Pass in a block that receives a `Supply` instance. To allocate a new + # IID value, call `Supply#next_value`. + # + # Example: + # + # MyClass.with_project_iid_supply(project) do |supply| + # attributes = MyClass.where(project: project).find_each do |record| + # record.attributes.merge(iid: supply.next_value) + # end + # + # bulk_insert(attributes) + # end + def define_singleton_internal_id_methods(scope, column, init) + define_singleton_method("with_#{scope}_#{column}_supply") do |scope_value, &block| + subject = find_by(scope => scope_value) || self + scope_attrs = ::AtomicInternalId.scope_attrs(scope_value) + usage = ::AtomicInternalId.scope_usage(self) + + generator = InternalId::InternalIdGenerator.new(subject, scope_attrs, usage, init) + + generator.with_lock do + supply = Supply.new(generator.record.last_value) + block.call(supply) + ensure + generator.track_greatest(supply.current_value) if supply + end + end + end + end + + def self.scope_attrs(scope_value) + { scope_value.class.table_name.singularize.to_sym => scope_value } if scope_value end def internal_id_scope_attrs(scope) scope_value = internal_id_read_scope(scope) - { scope_value.class.table_name.singularize.to_sym => scope_value } if scope_value + ::AtomicInternalId.scope_attrs(scope_value) end def internal_id_scope_usage - self.class.table_name.to_sym + ::AtomicInternalId.scope_usage(self.class) + end + + def self.scope_usage(including_class) + including_class.table_name.to_sym + end + + def self.project_init(klass, column_name = :iid) + ->(instance, scope) do + if instance + klass.where(project_id: instance.project_id).maximum(column_name) + elsif scope.present? + klass.where(**scope).maximum(column_name) + end + end + end + + def self.group_init(klass, column_name = :iid) + ->(instance, scope) do + if instance + klass.where(group_id: instance.group_id).maximum(column_name) + elsif scope.present? + klass.where(group: scope[:namespace]).maximum(column_name) + end + end end def internal_id_read_scope(scope) association(scope).reader end + + class Supply + attr_reader :current_value + + def initialize(start_value) + @current_value = start_value + end + + def next_value + @current_value += 1 + end + end end diff --git a/app/models/concerns/enums/internal_id.rb b/app/models/concerns/enums/internal_id.rb index 2d51d232e93..f01bd60ef16 100644 --- a/app/models/concerns/enums/internal_id.rb +++ b/app/models/concerns/enums/internal_id.rb @@ -14,7 +14,8 @@ module Enums operations_feature_flags: 6, operations_user_lists: 7, alert_management_alerts: 8, - sprints: 9 # iterations + sprints: 9, # iterations + design_management_designs: 10 } end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index b58794eb4d1..36ac1bdb236 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -21,9 +21,7 @@ class Deployment < ApplicationRecord has_one :deployment_cluster - has_internal_id :iid, scope: :project, track_if: -> { !importing? }, init: ->(s) do - Deployment.where(project: s.project).maximum(:iid) if s&.project - end + has_internal_id :iid, scope: :project, track_if: -> { !importing? } validates :sha, presence: true validates :ref, presence: true diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb index 2f8232de592..9f2eada0890 100644 --- a/app/models/design_management/design.rb +++ b/app/models/design_management/design.rb @@ -2,6 +2,7 @@ module DesignManagement class Design < ApplicationRecord + include AtomicInternalId include Importable include Noteable include Gitlab::FileTypeDetection @@ -26,6 +27,10 @@ module DesignManagement has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_internal_id :iid, scope: :project, presence: true, + hook_names: %i[create update], # Deal with old records + track_if: -> { !importing? } + validates :project, :filename, presence: true validates :issue, presence: true, unless: :importing? validates :filename, uniqueness: { scope: :issue_id }, length: { maximum: 255 } diff --git a/app/models/instance_metadata.rb b/app/models/instance_metadata.rb new file mode 100644 index 00000000000..96622d0b1b3 --- /dev/null +++ b/app/models/instance_metadata.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class InstanceMetadata + attr_reader :version, :revision + + def initialize(version: Gitlab::VERSION, revision: Gitlab.revision) + @version = version + @revision = revision + end +end diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index 4c0469d849a..c735e593da7 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -61,13 +61,13 @@ class InternalId < ApplicationRecord class << self def track_greatest(subject, scope, usage, new_value, init) - InternalIdGenerator.new(subject, scope, usage) - .track_greatest(init, new_value) + InternalIdGenerator.new(subject, scope, usage, init) + .track_greatest(new_value) end def generate_next(subject, scope, usage, init) - InternalIdGenerator.new(subject, scope, usage) - .generate(init) + InternalIdGenerator.new(subject, scope, usage, init) + .generate end def reset(subject, scope, usage, value) @@ -99,15 +99,18 @@ class InternalId < ApplicationRecord # 4) In the absence of a record in the internal_ids table, one will be created # and last_value will be calculated on the fly. # - # subject: The instance we're generating an internal id for. Gets passed to init if called. + # subject: The instance or class we're generating an internal id for. # scope: Attributes that define the scope for id generation. + # Valid keys are `project/project_id` and `namespace/namespace_id`. # usage: Symbol to define the usage of the internal id, see InternalId.usages - attr_reader :subject, :scope, :scope_attrs, :usage + # init: Proc that accepts the subject and the scope and returns Integer|NilClass + attr_reader :subject, :scope, :scope_attrs, :usage, :init - def initialize(subject, scope, usage) + def initialize(subject, scope, usage, init = nil) @subject = subject @scope = scope @usage = usage + @init = init raise ArgumentError, 'Scope is not well-defined, need at least one column for scope (given: 0)' if scope.empty? @@ -119,13 +122,13 @@ class InternalId < ApplicationRecord # Generates next internal id and returns it # init: Block that gets called to initialize InternalId record if not present # Make sure to not throw exceptions in the absence of records (if this is expected). - def generate(init) + def generate subject.transaction do # Create a record in internal_ids if one does not yet exist # and increment its last value # # Note this will acquire a ROW SHARE lock on the InternalId record - (lookup || create_record(init)).increment_and_save! + record.increment_and_save! end end @@ -148,12 +151,20 @@ class InternalId < ApplicationRecord # and set its new_value if it is higher than the current last_value # # Note this will acquire a ROW SHARE lock on the InternalId record - def track_greatest(init, new_value) + def track_greatest(new_value) subject.transaction do - (lookup || create_record(init)).track_greatest_and_save!(new_value) + record.track_greatest_and_save!(new_value) end end + def record + @record ||= (lookup || create_record) + end + + def with_lock(&block) + record.with_lock(&block) + end + private # Retrieve InternalId record for (project, usage) combination, if it exists @@ -171,12 +182,16 @@ class InternalId < ApplicationRecord # was faster in doing this, we'll realize once we hit the unique key constraint # violation. We can safely roll-back the nested transaction and perform # a lookup instead to retrieve the record. - def create_record(init) + def create_record + raise ArgumentError, 'Cannot initialize without init!' unless init + + instance = subject.is_a?(::Class) ? nil : subject + subject.transaction(requires_new: true) do InternalId.create!( **scope, usage: usage_value, - last_value: init.call(subject) || 0 + last_value: init.call(instance, scope) || 0 ) end rescue ActiveRecord::RecordNotUnique diff --git a/app/models/issue.rb b/app/models/issue.rb index ffdde91b2a2..7dc18cacd7c 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -48,7 +48,7 @@ class Issue < ApplicationRecord belongs_to :moved_to, class_name: 'Issue' has_one :moved_from, class_name: 'Issue', foreign_key: :moved_to_id - has_internal_id :iid, scope: :project, track_if: -> { !importing? }, init: ->(s) { s&.project&.issues&.maximum(:iid) } + has_internal_id :iid, scope: :project, track_if: -> { !importing? } has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/models/iteration.rb b/app/models/iteration.rb index bd245de411c..ba7cd973e9d 100644 --- a/app/models/iteration.rb +++ b/app/models/iteration.rb @@ -17,8 +17,8 @@ class Iteration < ApplicationRecord belongs_to :project belongs_to :group - has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.iterations&.maximum(:iid) } - has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.iterations&.maximum(:iid) } + has_internal_id :iid, scope: :project + has_internal_id :iid, scope: :group validates :start_date, presence: true validates :due_date, presence: true diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 45eb2361cf4..e6c61c674d3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -41,7 +41,14 @@ class MergeRequest < ApplicationRecord belongs_to :merge_user, class_name: "User" belongs_to :iteration, foreign_key: 'sprint_id' - has_internal_id :iid, scope: :target_project, track_if: -> { !importing? }, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) } + has_internal_id :iid, scope: :target_project, track_if: -> { !importing? }, + init: ->(mr, scope) do + if mr + mr.target_project&.merge_requests&.maximum(:iid) + elsif scope[:project] + where(target_project: scope[:project]).maximum(:iid) + end + end has_many :merge_request_diffs has_many :merge_request_context_commits, inverse_of: :merge_request diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 0a315ba8db2..c8776be5e4a 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -12,8 +12,8 @@ class Milestone < ApplicationRecord has_many :milestone_releases has_many :releases, through: :milestone_releases - has_internal_id :iid, scope: :project, track_if: -> { !importing? }, init: ->(s) { s&.project&.milestones&.maximum(:iid) } - has_internal_id :iid, scope: :group, track_if: -> { !importing? }, init: ->(s) { s&.group&.milestones&.maximum(:iid) } + has_internal_id :iid, scope: :project, track_if: -> { !importing? } + has_internal_id :iid, scope: :group, track_if: -> { !importing? } has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb index c9e52fe51f2..442f9d36c43 100644 --- a/app/models/operations/feature_flag.rb +++ b/app/models/operations/feature_flag.rb @@ -13,7 +13,7 @@ module Operations belongs_to :project - has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.operations_feature_flags&.maximum(:iid) } + has_internal_id :iid, scope: :project default_value_for :active, true diff --git a/app/models/operations/feature_flags/user_list.rb b/app/models/operations/feature_flags/user_list.rb index 782f7a54058..3e492eaa892 100644 --- a/app/models/operations/feature_flags/user_list.rb +++ b/app/models/operations/feature_flags/user_list.rb @@ -13,7 +13,7 @@ module Operations has_many :strategy_user_lists has_many :strategies, through: :strategy_user_lists - has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.operations_feature_flags_user_lists&.maximum(:iid) }, presence: true + has_internal_id :iid, scope: :project, presence: true validates :project, presence: true validates :name, diff --git a/app/policies/instance_metadata_policy.rb b/app/policies/instance_metadata_policy.rb new file mode 100644 index 00000000000..3386217044d --- /dev/null +++ b/app/policies/instance_metadata_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class InstanceMetadataPolicy < BasePolicy + delegate { :global } +end diff --git a/app/services/design_management/copy_design_collection/copy_service.rb b/app/services/design_management/copy_design_collection/copy_service.rb index 5099c2c5704..c0b32e1e9ae 100644 --- a/app/services/design_management/copy_design_collection/copy_service.rb +++ b/app/services/design_management/copy_design_collection/copy_service.rb @@ -172,20 +172,26 @@ module DesignManagement def copy_designs! design_attributes = attributes_config[:design_attributes] - new_rows = designs.map do |design| - design.attributes.slice(*design_attributes).merge( - issue_id: target_issue.id, - project_id: target_project.id + ::DesignManagement::Design.with_project_iid_supply(target_project) do |supply| + new_rows = designs.each_with_index.map do |design, i| + design.attributes.slice(*design_attributes).merge( + issue_id: target_issue.id, + project_id: target_project.id, + iid: supply.next_value + ) + end + + # TODO Replace `Gitlab::Database.bulk_insert` with `BulkInsertSafe` + # once https://gitlab.com/gitlab-org/gitlab/-/issues/247718 is fixed. + # When this is fixed, we can remove the call to + # `with_project_iid_supply` above, since the objects will be instantiated + # and callbacks (including `ensure_project_iid!`) will fire. + ::Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert + DesignManagement::Design.table_name, + new_rows, + return_ids: true ) end - - # TODO Replace `Gitlab::Database.bulk_insert` with `BulkInsertSafe` - # once https://gitlab.com/gitlab-org/gitlab/-/issues/247718 is fixed. - ::Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert - DesignManagement::Design.table_name, - new_rows, - return_ids: true - ) end def copy_versions! diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 74b6e981c00..1a3ba690184 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -37,8 +37,8 @@ - if runner.description.present? %p.runner-description = runner.description - - if runner.tag_list.present? + - if runner.tags.present? %p - - runner.tag_list.sort.each do |tag| + - runner.tags.map(&:name).sort.each do |tag| %span.badge.badge-primary = tag diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index d7fe141e802..e02e2cc784a 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -17,9 +17,10 @@ %h4.underlined-title= _('Runners activated for this project') %ul.bordered-list.activated-specific-runners = render partial: 'projects/runners/runner', collection: @project_runners, as: :runner + = paginate @project_runners, theme: "gitlab", param_name: "project_page", params: { expand_runners: true, anchor: 'js-runners-settings' } - if @assignable_runners.any? %h4.underlined-title= _('Available specific runners') %ul.bordered-list.available-specific-runners = render partial: 'projects/runners/runner', collection: @assignable_runners, as: :runner - = paginate @assignable_runners, theme: "gitlab", :params => { :anchor => '#js-runners-settings' } + = paginate @assignable_runners, theme: "gitlab", param_name: "specific_page", :params => { :anchor => 'js-runners-settings'} diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 31669a11b8e..f6ecb923100 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -33,7 +33,7 @@ = render_if_exists 'projects/settings/ci_cd/protected_environments', expanded: expanded -%section.settings.no-animate#js-runners-settings{ class: ('expanded' if expanded), data: { qa_selector: 'runners_settings_content' } } +%section.settings.no-animate#js-runners-settings{ class: ('expanded' if expanded || params[:expand_runners]), data: { qa_selector: 'runners_settings_content' } } .settings-header %h4 = _("Runners") diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 3af4437a63a..607e759928c 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -1,10 +1,7 @@ - if @search_objects.to_a.empty? - .gl-display-md-flex - - if %w(issues merge_requests).include?(@scope) - #js-search-sidebar.gl-display-flex.gl-flex-direction-column.col-md-3.gl-mr-4{ } - .gl-w-full - = render partial: "search/results/empty" - = render_if_exists 'shared/promotions/promote_advanced_search' + = render partial: "search/results/filters" + = render partial: "search/results/empty" + = render_if_exists 'shared/promotions/promote_advanced_search' - else .search-results-status .row-content-block.gl-display-flex @@ -27,21 +24,19 @@ .gl-display-md-flex.gl-flex-direction-column = render partial: 'search/sort_dropdown' = render_if_exists 'shared/promotions/promote_advanced_search' + = render partial: "search/results/filters" - .results.gl-display-md-flex.gl-mt-3 - - if %w(issues merge_requests).include?(@scope) - #js-search-sidebar{ } - .gl-w-full - - if @scope == 'commits' - %ul.content-list.commit-list - = render partial: "search/results/commit", collection: @search_objects - - else - .search-results - - if @scope == 'projects' - .term - = render 'shared/projects/list', projects: @search_objects, pipeline_status: false - - else - = render_if_exists partial: "search/results/#{@scope.singularize}", collection: @search_objects + .results.gl-mt-3 + - if @scope == 'commits' + %ul.content-list.commit-list + = render partial: "search/results/commit", collection: @search_objects + - else + .search-results + - if @scope == 'projects' + .term + = render 'shared/projects/list', projects: @search_objects, pipeline_status: false + - else + = render_if_exists partial: "search/results/#{@scope.singularize}", collection: @search_objects - - if @scope != 'projects' - = paginate_collection(@search_objects) + - if @scope != 'projects' + = paginate_collection(@search_objects) diff --git a/app/views/search/results/_filters.html.haml b/app/views/search/results/_filters.html.haml new file mode 100644 index 00000000000..2356a6e1f2c --- /dev/null +++ b/app/views/search/results/_filters.html.haml @@ -0,0 +1,6 @@ +.d-lg-flex.align-items-end + #js-search-filter-by-state{ 'v-cloak': true } + #js-search-filter-by-confidential{ 'v-cloak': true } + + - if %w(issues merge_requests).include?(@scope) + %hr.gl-mt-4.gl-mb-4 diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml index 9993342df38..ce48691166b 100644 --- a/app/views/shared/boards/_show.html.haml +++ b/app/views/shared/boards/_show.html.haml @@ -17,7 +17,7 @@ = render 'shared/issuable/search_bar', type: :boards, board: board #board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" } - - if Feature.enabled?(:boards_with_swimlanes, current_board_parent) || Feature.enabled?(:graphql_board_lists, current_board_parent) + - if Feature.enabled?(:boards_with_swimlanes, current_board_parent, default_enabled: true) || Feature.enabled?(:graphql_board_lists, current_board_parent) %board-content{ "v-cloak" => "true", "ref" => "board_content", ":lists" => "state.lists", diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index e991b901961..00b235809ed 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -182,7 +182,7 @@ = render 'shared/issuable/board_create_list_dropdown', board: board - if @project #js-add-issues-btn.gl-ml-3{ data: { can_admin_list: can?(current_user, :admin_list, @project) } } - - if current_user && Feature.enabled?(:boards_with_swimlanes, @group) + - if current_user && Feature.enabled?(:boards_with_swimlanes, @group, default_enabled: true) #js-board-epics-swimlanes-toggle #js-toggle-focus-btn - elsif is_not_boards_modal_or_productivity_analytics && show_sorting_dropdown |