Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-11 03:08:58 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-11 03:08:58 +0300
commit5427433c6d79f9131f4025cabb7e3208380bce9a (patch)
treeea0a22450f467f1ef1e3449255017dbe0f178882 /app
parent13bcb8221306526671a61df589f7c05505c9934c (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/diffs/components/app.vue12
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue37
-rw-r--r--app/assets/javascripts/diffs/constants.js5
-rw-r--r--app/assets/javascripts/diffs/store/actions.js19
-rw-r--r--app/assets/javascripts/diffs/utils/performance.js80
-rw-r--r--app/assets/javascripts/performance/constants.js14
-rw-r--r--app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue108
-rw-r--r--app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js36
-rw-r--r--app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js42
-rw-r--r--app/assets/javascripts/search/dropdown_filter/index.js38
-rw-r--r--app/assets/javascripts/search/index.js8
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue41
-rw-r--r--app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue1
-rw-r--r--app/assets/javascripts/search/sidebar/components/status_filter.vue1
-rw-r--r--app/assets/javascripts/search/sidebar/index.js23
-rw-r--r--app/assets/javascripts/search/store/actions.js9
-rw-r--r--app/controllers/concerns/lfs_request.rb24
-rw-r--r--app/controllers/groups/boards_controller.rb2
-rw-r--r--app/controllers/projects/boards_controller.rb2
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb6
-rw-r--r--app/controllers/repositories/git_http_client_controller.rb8
-rw-r--r--app/controllers/repositories/lfs_api_controller.rb5
-rw-r--r--app/controllers/repositories/lfs_storage_controller.rb2
-rw-r--r--app/controllers/search_controller.rb4
-rw-r--r--app/graphql/mutations/releases/base.rb19
-rw-r--r--app/graphql/mutations/releases/create.rb68
-rw-r--r--app/graphql/resolvers/metadata_resolver.rb2
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/graphql/types/release_asset_link_input_type.rb25
-rw-r--r--app/graphql/types/release_asset_link_type_enum.rb2
-rw-r--r--app/graphql/types/release_assets_input_type.rb13
-rw-r--r--app/models/alert_management/alert.rb2
-rw-r--r--app/models/ci/pipeline.rb13
-rw-r--r--app/models/concerns/atomic_internal_id.rb118
-rw-r--r--app/models/concerns/enums/internal_id.rb3
-rw-r--r--app/models/deployment.rb4
-rw-r--r--app/models/design_management/design.rb5
-rw-r--r--app/models/instance_metadata.rb10
-rw-r--r--app/models/internal_id.rb41
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/iteration.rb4
-rw-r--r--app/models/merge_request.rb9
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/operations/feature_flag.rb2
-rw-r--r--app/models/operations/feature_flags/user_list.rb2
-rw-r--r--app/policies/instance_metadata_policy.rb5
-rw-r--r--app/services/design_management/copy_design_collection/copy_service.rb30
-rw-r--r--app/views/projects/runners/_runner.html.haml4
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml3
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/search/_results.html.haml39
-rw-r--r--app/views/search/results/_filters.html.haml6
-rw-r--r--app/views/shared/boards/_show.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
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