Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
13bcb82213
commit
5427433c6d
|
|
@ -1 +1 @@
|
|||
0.0.8
|
||||
13.6.1
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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));
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 }));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class InstanceMetadataPolicy < BasePolicy
|
||||
delegate { :global }
|
||||
end
|
||||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Block LFS requests on snippets
|
||||
merge_request: 45874
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Global Search - Left Sidebar
|
||||
merge_request: 46595
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Expand postgres_indexes view
|
||||
merge_request: 47304
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add iid column to design_management_designs
|
||||
merge_request: 46596
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add performance marks and measures to the MR Diffs app at critical moments
|
||||
merge_request: 46434
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Paginate project_runners in ci_cd settings
|
||||
merge_request: 45830
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add releaseCreate mutation to GraphQL endpoint
|
||||
merge_request: 46263
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: boards_with_swimlanes
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
milestone:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/218040
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/238222
|
||||
milestone: 13.6
|
||||
group: group::product planning
|
||||
type: development
|
||||
group: group::project management
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: search_facets
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46809
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46595
|
||||
group: group::global search
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIidToDesignManagementDesign < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :design_management_designs, :iid, :integer
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnDesignManagementDesignsIidProjectId < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_design_management_designs_on_iid_and_project_id'
|
||||
|
||||
def up
|
||||
add_concurrent_index :design_management_designs, [:project_id, :iid],
|
||||
name: INDEX_NAME,
|
||||
unique: true
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :design_management_designs, INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ExtendPostgresIndexesView < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
execute(<<~SQL)
|
||||
DROP VIEW postgres_indexes;
|
||||
|
||||
CREATE VIEW postgres_indexes AS
|
||||
SELECT (pg_namespace.nspname::text || '.'::text) || pg_class.relname::text AS identifier,
|
||||
pg_index.indexrelid,
|
||||
pg_namespace.nspname AS schema,
|
||||
pg_class.relname AS name,
|
||||
pg_index.indisunique AS "unique",
|
||||
pg_index.indisvalid AS valid_index,
|
||||
pg_class.relispartition AS partitioned,
|
||||
pg_index.indisexclusion AS exclusion,
|
||||
pg_index.indexprs IS NOT NULL as expression,
|
||||
pg_index.indpred IS NOT NULL as partial,
|
||||
pg_indexes.indexdef AS definition,
|
||||
pg_relation_size(pg_class.oid::regclass) AS ondisk_size_bytes
|
||||
FROM pg_index
|
||||
JOIN pg_class ON pg_class.oid = pg_index.indexrelid
|
||||
JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
|
||||
JOIN pg_indexes ON pg_class.relname = pg_indexes.indexname
|
||||
WHERE pg_namespace.nspname <> 'pg_catalog'::name
|
||||
AND (pg_namespace.nspname = ANY (ARRAY["current_schema"(), 'gitlab_partitions_dynamic'::name, 'gitlab_partitions_static'::name]));
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
execute(<<~SQL)
|
||||
DROP VIEW postgres_indexes;
|
||||
|
||||
CREATE VIEW postgres_indexes AS
|
||||
SELECT (pg_namespace.nspname::text || '.'::text) || pg_class.relname::text AS identifier,
|
||||
pg_index.indexrelid,
|
||||
pg_namespace.nspname AS schema,
|
||||
pg_class.relname AS name,
|
||||
pg_index.indisunique AS "unique",
|
||||
pg_index.indisvalid AS valid_index,
|
||||
pg_class.relispartition AS partitioned,
|
||||
pg_index.indisexclusion AS exclusion,
|
||||
pg_indexes.indexdef AS definition,
|
||||
pg_relation_size(pg_class.oid::regclass) AS ondisk_size_bytes
|
||||
FROM pg_index
|
||||
JOIN pg_class ON pg_class.oid = pg_index.indexrelid
|
||||
JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
|
||||
JOIN pg_indexes ON pg_class.relname = pg_indexes.indexname
|
||||
WHERE pg_namespace.nspname <> 'pg_catalog'::name
|
||||
AND (pg_namespace.nspname = ANY (ARRAY["current_schema"(), 'gitlab_partitions_dynamic'::name, 'gitlab_partitions_static'::name]));
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BackfillDesignIids < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
class Designs < ActiveRecord::Base
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'design_management_designs'
|
||||
end
|
||||
|
||||
def up
|
||||
backfill = ::Gitlab::BackgroundMigration::BackfillDesignInternalIds.new(Designs)
|
||||
|
||||
Designs.select(:project_id).distinct.each_batch(of: 100, column: :project_id) do |relation|
|
||||
backfill.perform(relation)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# NOOP
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddNotNullCheckOnIidOnDesignManangementDesigns < ActiveRecord::Migration[6.0]
|
||||
include ::Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_not_null_constraint(:design_management_designs, :iid)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_not_null_constraint(:design_management_designs, :iid)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
bef50f2417b9676c89aea838f7b9c85fb88af9f52c197d8eb4613a9c91bc7741
|
||||
|
|
@ -0,0 +1 @@
|
|||
2f6c7efc1716d02dd40adb08bd09b9f1e63e4248619678c0562f4b8d581e6065
|
||||
|
|
@ -0,0 +1 @@
|
|||
3937235469c8fb1f2b0af9cdf38933db5ae61552d1a9050755cec5f7c16ebb66
|
||||
|
|
@ -0,0 +1 @@
|
|||
7ec73c06ccc4c9f618e0455d0a7aae3b591bf52b5ddb1b3f1678d2fd50b9fd5e
|
||||
|
|
@ -0,0 +1 @@
|
|||
f008d77d2a0aef463a924923d5a338030758d6b9c194756a0490b51a95681127
|
||||
|
|
@ -11650,7 +11650,9 @@ CREATE TABLE design_management_designs (
|
|||
issue_id integer,
|
||||
filename character varying NOT NULL,
|
||||
relative_position integer,
|
||||
CONSTRAINT check_07155e2715 CHECK ((char_length((filename)::text) <= 255))
|
||||
iid integer,
|
||||
CONSTRAINT check_07155e2715 CHECK ((char_length((filename)::text) <= 255)),
|
||||
CONSTRAINT check_cfb92df01a CHECK ((iid IS NOT NULL))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE design_management_designs_id_seq
|
||||
|
|
@ -14722,6 +14724,8 @@ CREATE VIEW postgres_indexes AS
|
|||
pg_index.indisvalid AS valid_index,
|
||||
pg_class.relispartition AS partitioned,
|
||||
pg_index.indisexclusion AS exclusion,
|
||||
(pg_index.indexprs IS NOT NULL) AS expression,
|
||||
(pg_index.indpred IS NOT NULL) AS partial,
|
||||
pg_indexes.indexdef AS definition,
|
||||
pg_relation_size((pg_class.oid)::regclass) AS ondisk_size_bytes
|
||||
FROM (((pg_index
|
||||
|
|
@ -20592,6 +20596,8 @@ CREATE INDEX index_description_versions_on_merge_request_id ON description_versi
|
|||
|
||||
CREATE INDEX index_design_management_designs_issue_id_relative_position_id ON design_management_designs USING btree (issue_id, relative_position, id);
|
||||
|
||||
CREATE UNIQUE INDEX index_design_management_designs_on_iid_and_project_id ON design_management_designs USING btree (project_id, iid);
|
||||
|
||||
CREATE UNIQUE INDEX index_design_management_designs_on_issue_id_and_filename ON design_management_designs USING btree (issue_id, filename);
|
||||
|
||||
CREATE INDEX index_design_management_designs_on_project_id ON design_management_designs USING btree (project_id);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
Deletes the cached blobs for a group. This endpoint requires group admin access.
|
||||
|
||||
CAUTION: **Warning:**
|
||||
[A bug exists](https://gitlab.com/gitlab-org/gitlab/-/issues/277161) for this API.
|
||||
|
||||
```plaintext
|
||||
DELETE /groups/:id/dependency_proxy/cache
|
||||
```
|
||||
|
|
|
|||
|
|
@ -13476,6 +13476,7 @@ type Mutation {
|
|||
prometheusIntegrationResetToken(input: PrometheusIntegrationResetTokenInput!): PrometheusIntegrationResetTokenPayload
|
||||
prometheusIntegrationUpdate(input: PrometheusIntegrationUpdateInput!): PrometheusIntegrationUpdatePayload
|
||||
promoteToEpic(input: PromoteToEpicInput!): PromoteToEpicPayload
|
||||
releaseCreate(input: ReleaseCreateInput!): ReleaseCreatePayload
|
||||
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload @deprecated(reason: "Use awardEmojiRemove. Deprecated in 13.2")
|
||||
removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload
|
||||
|
||||
|
|
@ -17734,7 +17735,32 @@ type ReleaseAssetLinkEdge {
|
|||
}
|
||||
|
||||
"""
|
||||
Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`
|
||||
Fields that are available when modifying a release asset link
|
||||
"""
|
||||
input ReleaseAssetLinkInput {
|
||||
"""
|
||||
Relative path for a direct asset link
|
||||
"""
|
||||
directAssetPath: String
|
||||
|
||||
"""
|
||||
The type of the asset link
|
||||
"""
|
||||
linkType: ReleaseAssetLinkType = OTHER
|
||||
|
||||
"""
|
||||
Name of the asset link
|
||||
"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
URL of the asset link
|
||||
"""
|
||||
url: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Type of the link: `other`, `runbook`, `image`, `package`
|
||||
"""
|
||||
enum ReleaseAssetLinkType {
|
||||
"""
|
||||
|
|
@ -17818,6 +17844,16 @@ type ReleaseAssets {
|
|||
): ReleaseSourceConnection
|
||||
}
|
||||
|
||||
"""
|
||||
Fields that are available when modifying release assets
|
||||
"""
|
||||
input ReleaseAssetsInput {
|
||||
"""
|
||||
A list of asset links to associate to the release
|
||||
"""
|
||||
links: [ReleaseAssetLinkInput!]
|
||||
}
|
||||
|
||||
"""
|
||||
The connection type for Release.
|
||||
"""
|
||||
|
|
@ -17843,6 +17879,76 @@ type ReleaseConnection {
|
|||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated input type of ReleaseCreate
|
||||
"""
|
||||
input ReleaseCreateInput {
|
||||
"""
|
||||
Assets associated to the release
|
||||
"""
|
||||
assets: ReleaseAssetsInput
|
||||
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
Description (also known as "release notes") of the release
|
||||
"""
|
||||
description: String
|
||||
|
||||
"""
|
||||
The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.
|
||||
"""
|
||||
milestones: [String!]
|
||||
|
||||
"""
|
||||
Name of the release
|
||||
"""
|
||||
name: String
|
||||
|
||||
"""
|
||||
Full path of the project the release is associated with
|
||||
"""
|
||||
projectPath: ID!
|
||||
|
||||
"""
|
||||
The commit SHA or branch name to use if creating a new tag
|
||||
"""
|
||||
ref: String
|
||||
|
||||
"""
|
||||
The date when the release will be/was ready. Defaults to the current time.
|
||||
"""
|
||||
releasedAt: Time
|
||||
|
||||
"""
|
||||
Name of the tag to associate with the release
|
||||
"""
|
||||
tagName: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated return type of ReleaseCreate
|
||||
"""
|
||||
type ReleaseCreatePayload {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
Errors encountered during execution of the mutation.
|
||||
"""
|
||||
errors: [String!]!
|
||||
|
||||
"""
|
||||
The release after mutation
|
||||
"""
|
||||
release: Release
|
||||
}
|
||||
|
||||
"""
|
||||
An edge in a connection.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -39200,6 +39200,33 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "releaseCreate",
|
||||
"description": null,
|
||||
"args": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "ReleaseCreateInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "ReleaseCreatePayload",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "removeAwardEmoji",
|
||||
"description": null,
|
||||
|
|
@ -51265,10 +51292,69 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "ReleaseAssetLinkInput",
|
||||
"description": "Fields that are available when modifying a release asset link",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Name of the asset link",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"description": "URL of the asset link",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "directAssetPath",
|
||||
"description": "Relative path for a direct asset link",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "linkType",
|
||||
"description": "The type of the asset link",
|
||||
"type": {
|
||||
"kind": "ENUM",
|
||||
"name": "ReleaseAssetLinkType",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": "OTHER"
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "ReleaseAssetLinkType",
|
||||
"description": "Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`",
|
||||
"description": "Type of the link: `other`, `runbook`, `image`, `package`",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
|
|
@ -51433,6 +51519,35 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "ReleaseAssetsInput",
|
||||
"description": "Fields that are available when modifying release assets",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "links",
|
||||
"description": "A list of asset links to associate to the release",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "ReleaseAssetLinkInput",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "ReleaseConnection",
|
||||
|
|
@ -51518,6 +51633,190 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "ReleaseCreateInput",
|
||||
"description": "Autogenerated input type of ReleaseCreate",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "projectPath",
|
||||
"description": "Full path of the project the release is associated with",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "tagName",
|
||||
"description": "Name of the tag to associate with the release",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "ref",
|
||||
"description": "The commit SHA or branch name to use if creating a new tag",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Name of the release",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"description": "Description (also known as \"release notes\") of the release",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "releasedAt",
|
||||
"description": "The date when the release will be/was ready. Defaults to the current time.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Time",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "milestones",
|
||||
"description": "The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assets",
|
||||
"description": "Assets associated to the release",
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "ReleaseAssetsInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": "A unique identifier for the client performing the mutation.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "ReleaseCreatePayload",
|
||||
"description": "Autogenerated return type of ReleaseCreate",
|
||||
"fields": [
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": "A unique identifier for the client performing the mutation.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "errors",
|
||||
"description": "Errors encountered during execution of the mutation.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "release",
|
||||
"description": "The release after mutation",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Release",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "ReleaseEdge",
|
||||
|
|
|
|||
|
|
@ -2496,6 +2496,16 @@ A container for all assets associated with a release.
|
|||
| `links` | ReleaseAssetLinkConnection | Asset links of the release |
|
||||
| `sources` | ReleaseSourceConnection | Sources of the release |
|
||||
|
||||
### ReleaseCreatePayload
|
||||
|
||||
Autogenerated return type of ReleaseCreate.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
| `release` | Release | The release after mutation |
|
||||
|
||||
### ReleaseEvidence
|
||||
|
||||
Evidence for a release.
|
||||
|
|
@ -4097,7 +4107,7 @@ State of a Geo registry.
|
|||
|
||||
### ReleaseAssetLinkType
|
||||
|
||||
Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`.
|
||||
Type of the link: `other`, `runbook`, `image`, `package`.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB |
|
|
@ -5,50 +5,29 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
type: reference, concepts
|
||||
---
|
||||
|
||||
# Instance-level merge request approval rules **(PREMIUM ONLY)**
|
||||
# Merge request approval rules **(PREMIUM ONLY)**
|
||||
|
||||
> Introduced in [GitLab Premium](https://gitlab.com/gitlab-org/gitlab/-/issues/39060) 12.8.
|
||||
|
||||
Merge request approvals rules prevent users overriding certain settings on a project
|
||||
level. When configured, only administrators can change these settings on a project level
|
||||
if they are enabled at an instance level.
|
||||
Merge request approval rules prevent users from overriding certain settings on the project
|
||||
level. When enabled at the instance level, these settings are no longer editable on the
|
||||
project level.
|
||||
|
||||
To enable merge request approval rules for an instance:
|
||||
|
||||
1. Navigate to **Admin Area >** **{push-rules}** **Push Rules** and expand **Merge
|
||||
requests approvals**.
|
||||
requests approvals**.
|
||||
1. Set the required rule.
|
||||
1. Click **Save changes**.
|
||||
|
||||
GitLab administrators can later override these settings in a project’s settings.
|
||||
|
||||
## Available rules
|
||||
|
||||
Merge request approval rules that can be set at an instance level are:
|
||||
|
||||
- **Prevent approval of merge requests by merge request author**. Prevents project
|
||||
maintainers from allowing request authors to merge their own merge requests.
|
||||
maintainers from allowing request authors to merge their own merge requests.
|
||||
- **Prevent approval of merge requests by merge request committers**. Prevents project
|
||||
maintainers from allowing users to approve merge requests if they have submitted
|
||||
any commits to the source branch.
|
||||
- **Can override approvers and approvals required per merge request**. Allows project
|
||||
maintainers to modify the approvers list in individual merge requests.
|
||||
|
||||
## Scope rules to compliance-labeled projects
|
||||
|
||||
> Introduced in [GitLab Premium](https://gitlab.com/groups/gitlab-org/-/epics/3432) 13.2.
|
||||
|
||||
Merge request approval rules can be further scoped to specific compliance frameworks.
|
||||
|
||||
When the compliance framework label is selected and the project is assigned the compliance
|
||||
label, the instance-level MR approval settings will take effect and the
|
||||
[project-level settings](../project/merge_requests/merge_request_approvals.md#adding--editing-a-default-approval-rule)
|
||||
is locked for modification.
|
||||
|
||||
When the compliance framework label is not selected or the project is not assigned the
|
||||
compliance label, the project-level MR approval settings will take effect and the users with
|
||||
Maintainer role and above can modify these.
|
||||
|
||||
| Instance-level | Project-level |
|
||||
| -------------- | ------------- |
|
||||
|  |  |
|
||||
maintainers from allowing users to approve merge requests if they have submitted
|
||||
any commits to the source branch.
|
||||
- **Prevent users from modifying merge request approvers list**. Prevents users from
|
||||
modifying the approvers list in project settings or in individual merge requests.
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
Each security vulnerability in a project's [Security Dashboard](../security_dashboard/index.md#project-security-dashboard) has an individual page which includes:
|
||||
|
||||
- Details of the vulnerability.
|
||||
- Details for the vulnerability.
|
||||
- The status of the vulnerability within the project.
|
||||
- Available actions for the vulnerability.
|
||||
- Issues related to the vulnerability.
|
||||
- Any issues related to the vulnerability.
|
||||
|
||||
On the vulnerability page, you can interact with the vulnerability in
|
||||
several different ways:
|
||||
|
|
@ -26,21 +26,21 @@ several different ways:
|
|||
By default, such issues are [confidential](../../project/issues/confidential_issues.md).
|
||||
- [Link issues](#link-issues-to-the-vulnerability) - Link existing issues to vulnerability.
|
||||
- [Automatic remediation](#automatic-remediation-for-vulnerabilities) - For some vulnerabilities,
|
||||
a solution is provided for how to fix the vulnerability.
|
||||
a solution is provided for how to fix the vulnerability automatically.
|
||||
|
||||
## Changing vulnerability status
|
||||
|
||||
You can switch the status of a vulnerability using the **Status** dropdown to one of
|
||||
the following values:
|
||||
|
||||
| Status | Description |
|
||||
|-----------|-------------------------------------------------------------------|
|
||||
| Detected | The default state for a newly discovered vulnerability |
|
||||
| Confirmed | A user has seen this vulnerability and confirmed it to be real |
|
||||
| Dismissed | A user has seen this vulnerability and dismissed it |
|
||||
| Resolved | The vulnerability has been fixed and is no longer in the codebase |
|
||||
| Status | Description |
|
||||
|-----------|------------------------------------------------------------------------------------------------------------------|
|
||||
| Detected | The default state for a newly discovered vulnerability |
|
||||
| Confirmed | A user has seen this vulnerability and confirmed it to be accurate |
|
||||
| Dismissed | A user has seen this vulnerability and dismissed it because it is not accurate or otherwise will not be resolved |
|
||||
| Resolved | The vulnerability has been fixed and is no longer valid |
|
||||
|
||||
A timeline shows you when the vulnerability status has changed,
|
||||
A timeline shows you when the vulnerability status has changed
|
||||
and allows you to comment on a change.
|
||||
|
||||
## Creating an issue for a vulnerability
|
||||
|
|
@ -48,7 +48,7 @@ and allows you to comment on a change.
|
|||
You can create an issue for a vulnerability by selecting the **Create issue** button.
|
||||
|
||||
This creates a [confidential issue](../../project/issues/confidential_issues.md) in the
|
||||
project the vulnerability came from, and pre-populates it with useful information from
|
||||
project the vulnerability came from and pre-populates it with useful information from
|
||||
the vulnerability report. After the issue is created, GitLab redirects you to the
|
||||
issue page so you can edit, assign, or comment on the issue.
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ To create an iteration:
|
|||
|
||||
## Edit an iteration
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218277) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.2.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218277) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.2.
|
||||
|
||||
NOTE: **Note:**
|
||||
You need Developer [permissions](../../permissions.md) or higher to edit an iteration.
|
||||
|
|
@ -73,7 +73,7 @@ An iteration report displays a list of all the issues assigned to an iteration a
|
|||
|
||||
To view an iteration report, go to the iterations list page and click an iteration's title.
|
||||
|
||||
## Disable Iterations **(CORE ONLY)**
|
||||
## Disable Iterations **(STARTER ONLY)**
|
||||
|
||||
GitLab Iterations feature is deployed with a feature flag that is **enabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ When [configuring your identify provider](#configuring-your-identity-provider),
|
|||
### Azure setup notes
|
||||
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
For a demo of the Azure SAML setup including SCIM, see [SCIM Provisioning on Azure Using SAML SSO for Groups Demo](https://youtu.be/24-ZxmTeEBU).
|
||||
For a demo of the Azure SAML setup including SCIM, see [SCIM Provisioning on Azure Using SAML SSO for Groups Demo](https://youtu.be/24-ZxmTeEBU). Please note that the video is outdated in regards to objectID mapping and the [SCIM documentation should be followed](scim_setup.md#azure-configuration-steps).
|
||||
|
||||
| GitLab Setting | Azure Field |
|
||||
|--------------|----------------|
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# Backfill design.iid for a range of projects
|
||||
class BackfillDesignInternalIds
|
||||
# See app/models/internal_id
|
||||
# This is a direct copy of the application code with the following changes:
|
||||
# - usage enum is hard-coded to the value for design_management_designs
|
||||
# - init is not passed around, but ignored
|
||||
class InternalId < ActiveRecord::Base
|
||||
def self.track_greatest(subject, scope, new_value)
|
||||
InternalIdGenerator.new(subject, scope).track_greatest(new_value)
|
||||
end
|
||||
|
||||
# Increments #last_value with new_value if it is greater than the current,
|
||||
# and saves the record
|
||||
#
|
||||
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
|
||||
# As such, the increment is atomic and safe to be called concurrently.
|
||||
def track_greatest_and_save!(new_value)
|
||||
update_and_save { self.last_value = [last_value || 0, new_value].max }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_and_save(&block)
|
||||
lock!
|
||||
yield
|
||||
# update_and_save_counter.increment(usage: usage, changed: last_value_changed?)
|
||||
save!
|
||||
last_value
|
||||
end
|
||||
end
|
||||
|
||||
# See app/models/internal_id
|
||||
class InternalIdGenerator
|
||||
attr_reader :subject, :scope, :scope_attrs
|
||||
|
||||
def initialize(subject, scope)
|
||||
@subject = subject
|
||||
@scope = scope
|
||||
|
||||
raise ArgumentError, 'Scope is not well-defined, need at least one column for scope (given: 0)' if scope.empty?
|
||||
end
|
||||
|
||||
# Create a record in internal_ids if one does not yet exist
|
||||
# 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(new_value)
|
||||
subject.transaction do
|
||||
record.track_greatest_and_save!(new_value)
|
||||
end
|
||||
end
|
||||
|
||||
def record
|
||||
@record ||= (lookup || create_record)
|
||||
end
|
||||
|
||||
def lookup
|
||||
InternalId.find_by(**scope, usage: usage_value)
|
||||
end
|
||||
|
||||
def usage_value
|
||||
10 # see Enums::InternalId - this is the value for design_management_designs
|
||||
end
|
||||
|
||||
# Create InternalId record for (scope, usage) combination, if it doesn't exist
|
||||
#
|
||||
# We blindly insert without synchronization. If another process
|
||||
# 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
|
||||
subject.transaction(requires_new: true) do
|
||||
InternalId.create!(
|
||||
**scope,
|
||||
usage: usage_value,
|
||||
last_value: 0
|
||||
)
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
lookup
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :design_class
|
||||
|
||||
def initialize(design_class)
|
||||
@design_class = design_class
|
||||
end
|
||||
|
||||
def perform(relation)
|
||||
start_id, end_id = relation.pluck("min(project_id), max(project_id)").flatten
|
||||
table = 'design_management_designs'
|
||||
|
||||
ActiveRecord::Base.connection.execute <<~SQL
|
||||
WITH
|
||||
starting_iids(project_id, iid) as (
|
||||
SELECT project_id, MAX(COALESCE(iid, 0))
|
||||
FROM #{table}
|
||||
WHERE project_id BETWEEN #{start_id} AND #{end_id}
|
||||
GROUP BY project_id
|
||||
),
|
||||
with_calculated_iid(id, iid) as (
|
||||
SELECT design.id,
|
||||
init.iid + ROW_NUMBER() OVER (PARTITION BY design.project_id ORDER BY design.id ASC)
|
||||
FROM #{table} as design, starting_iids as init
|
||||
WHERE design.project_id BETWEEN #{start_id} AND #{end_id}
|
||||
AND design.iid IS NULL
|
||||
AND init.project_id = design.project_id
|
||||
)
|
||||
|
||||
UPDATE #{table}
|
||||
SET iid = with_calculated_iid.iid
|
||||
FROM with_calculated_iid
|
||||
WHERE #{table}.id = with_calculated_iid.id
|
||||
SQL
|
||||
|
||||
# track the new greatest IID value
|
||||
relation.each do |design|
|
||||
current_max = design_class.where(project_id: design.project_id).maximum(:iid)
|
||||
scope = { project_id: design.project_id }
|
||||
InternalId.track_greatest(design, scope, current_max)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -10,6 +10,7 @@ module Gitlab
|
|||
def self.candidate_indexes
|
||||
Gitlab::Database::PostgresIndex
|
||||
.regular
|
||||
.where('NOT expression')
|
||||
.not_match("^#{ConcurrentReindex::TEMPORARY_INDEX_PREFIX}")
|
||||
.not_match("^#{ConcurrentReindex::REPLACED_INDEX_PREFIX}")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ ignore_design_attributes:
|
|||
- id
|
||||
- issue_id
|
||||
- project_id
|
||||
- iid
|
||||
|
||||
ignore_version_attributes:
|
||||
- id
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'zlib'
|
||||
|
||||
# == Experimentation
|
||||
#
|
||||
# Utility module for A/B testing experimental features. Define your experiments in the `EXPERIMENTS` constant.
|
||||
|
|
@ -87,126 +85,6 @@ module Gitlab
|
|||
}
|
||||
}.freeze
|
||||
|
||||
# Controller concern that checks if an `experimentation_subject_id cookie` is present and sets it if absent.
|
||||
# Used for A/B testing of experimental features. Exposes the `experiment_enabled?(experiment_name)` method
|
||||
# to controllers and views. It returns true when the experiment is enabled and the user is selected as part
|
||||
# of the experimental group.
|
||||
#
|
||||
module ControllerConcern
|
||||
include ::Gitlab::Experimentation::GroupTypes
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_experimentation_subject_id_cookie, unless: :dnt_enabled?
|
||||
helper_method :experiment_enabled?, :experiment_tracking_category_and_group
|
||||
end
|
||||
|
||||
def set_experimentation_subject_id_cookie
|
||||
return if cookies[:experimentation_subject_id].present?
|
||||
|
||||
cookies.permanent.signed[:experimentation_subject_id] = {
|
||||
value: SecureRandom.uuid,
|
||||
secure: ::Gitlab.config.gitlab.https,
|
||||
httponly: true
|
||||
}
|
||||
end
|
||||
|
||||
def push_frontend_experiment(experiment_key)
|
||||
var_name = experiment_key.to_s.camelize(:lower)
|
||||
enabled = experiment_enabled?(experiment_key)
|
||||
|
||||
gon.push({ experiments: { var_name => enabled } }, true)
|
||||
end
|
||||
|
||||
def experiment_enabled?(experiment_key)
|
||||
return false if dnt_enabled?
|
||||
|
||||
return true if Experimentation.enabled_for_value?(experiment_key, experimentation_subject_index(experiment_key))
|
||||
return true if forced_enabled?(experiment_key)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def track_experiment_event(experiment_key, action, value = nil)
|
||||
return if dnt_enabled?
|
||||
|
||||
track_experiment_event_for(experiment_key, action, value) do |tracking_data|
|
||||
::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), **tracking_data)
|
||||
end
|
||||
end
|
||||
|
||||
def frontend_experimentation_tracking_data(experiment_key, action, value = nil)
|
||||
return if dnt_enabled?
|
||||
|
||||
track_experiment_event_for(experiment_key, action, value) do |tracking_data|
|
||||
gon.push(tracking_data: tracking_data)
|
||||
end
|
||||
end
|
||||
|
||||
def record_experiment_user(experiment_key)
|
||||
return if dnt_enabled?
|
||||
return unless Experimentation.enabled?(experiment_key) && current_user
|
||||
|
||||
::Experiment.add_user(experiment_key, tracking_group(experiment_key), current_user)
|
||||
end
|
||||
|
||||
def experiment_tracking_category_and_group(experiment_key)
|
||||
"#{tracking_category(experiment_key)}:#{tracking_group(experiment_key, '_group')}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dnt_enabled?
|
||||
Gitlab::Utils.to_boolean(request.headers['DNT'])
|
||||
end
|
||||
|
||||
def experimentation_subject_id
|
||||
cookies.signed[:experimentation_subject_id]
|
||||
end
|
||||
|
||||
def experimentation_subject_index(experiment_key)
|
||||
return if experimentation_subject_id.blank?
|
||||
|
||||
if Experimentation.experiment(experiment_key).use_backwards_compatible_subject_index
|
||||
experimentation_subject_id.delete('-').hex % 100
|
||||
else
|
||||
Zlib.crc32("#{experiment_key}#{experimentation_subject_id}") % 100
|
||||
end
|
||||
end
|
||||
|
||||
def track_experiment_event_for(experiment_key, action, value)
|
||||
return unless Experimentation.enabled?(experiment_key)
|
||||
|
||||
yield experimentation_tracking_data(experiment_key, action, value)
|
||||
end
|
||||
|
||||
def experimentation_tracking_data(experiment_key, action, value)
|
||||
{
|
||||
category: tracking_category(experiment_key),
|
||||
action: action,
|
||||
property: tracking_group(experiment_key, "_group"),
|
||||
label: experimentation_subject_id,
|
||||
value: value
|
||||
}.compact
|
||||
end
|
||||
|
||||
def tracking_category(experiment_key)
|
||||
Experimentation.experiment(experiment_key).tracking_category
|
||||
end
|
||||
|
||||
def tracking_group(experiment_key, suffix = nil)
|
||||
return unless Experimentation.enabled?(experiment_key)
|
||||
|
||||
group = experiment_enabled?(experiment_key) ? GROUP_EXPERIMENTAL : GROUP_CONTROL
|
||||
|
||||
suffix ? "#{group}#{suffix}" : group
|
||||
end
|
||||
|
||||
def forced_enabled?(experiment_key)
|
||||
params.has_key?(:force_experiment) && params[:force_experiment] == experiment_key.to_s
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def experiment(key)
|
||||
Experiment.new(EXPERIMENTS[key].merge(key: key))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'zlib'
|
||||
|
||||
# Controller concern that checks if an `experimentation_subject_id cookie` is present and sets it if absent.
|
||||
# Used for A/B testing of experimental features. Exposes the `experiment_enabled?(experiment_name)` method
|
||||
# to controllers and views. It returns true when the experiment is enabled and the user is selected as part
|
||||
# of the experimental group.
|
||||
#
|
||||
module Gitlab
|
||||
module Experimentation
|
||||
module ControllerConcern
|
||||
include ::Gitlab::Experimentation::GroupTypes
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_experimentation_subject_id_cookie, unless: :dnt_enabled?
|
||||
helper_method :experiment_enabled?, :experiment_tracking_category_and_group
|
||||
end
|
||||
|
||||
def set_experimentation_subject_id_cookie
|
||||
return if cookies[:experimentation_subject_id].present?
|
||||
|
||||
cookies.permanent.signed[:experimentation_subject_id] = {
|
||||
value: SecureRandom.uuid,
|
||||
secure: ::Gitlab.config.gitlab.https,
|
||||
httponly: true
|
||||
}
|
||||
end
|
||||
|
||||
def push_frontend_experiment(experiment_key)
|
||||
var_name = experiment_key.to_s.camelize(:lower)
|
||||
enabled = experiment_enabled?(experiment_key)
|
||||
|
||||
gon.push({ experiments: { var_name => enabled } }, true)
|
||||
end
|
||||
|
||||
def experiment_enabled?(experiment_key)
|
||||
return false if dnt_enabled?
|
||||
|
||||
return true if Experimentation.enabled_for_value?(experiment_key, experimentation_subject_index(experiment_key))
|
||||
return true if forced_enabled?(experiment_key)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def track_experiment_event(experiment_key, action, value = nil)
|
||||
return if dnt_enabled?
|
||||
|
||||
track_experiment_event_for(experiment_key, action, value) do |tracking_data|
|
||||
::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), **tracking_data)
|
||||
end
|
||||
end
|
||||
|
||||
def frontend_experimentation_tracking_data(experiment_key, action, value = nil)
|
||||
return if dnt_enabled?
|
||||
|
||||
track_experiment_event_for(experiment_key, action, value) do |tracking_data|
|
||||
gon.push(tracking_data: tracking_data)
|
||||
end
|
||||
end
|
||||
|
||||
def record_experiment_user(experiment_key)
|
||||
return if dnt_enabled?
|
||||
return unless Experimentation.enabled?(experiment_key) && current_user
|
||||
|
||||
::Experiment.add_user(experiment_key, tracking_group(experiment_key), current_user)
|
||||
end
|
||||
|
||||
def experiment_tracking_category_and_group(experiment_key)
|
||||
"#{tracking_category(experiment_key)}:#{tracking_group(experiment_key, '_group')}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dnt_enabled?
|
||||
Gitlab::Utils.to_boolean(request.headers['DNT'])
|
||||
end
|
||||
|
||||
def experimentation_subject_id
|
||||
cookies.signed[:experimentation_subject_id]
|
||||
end
|
||||
|
||||
def experimentation_subject_index(experiment_key)
|
||||
return if experimentation_subject_id.blank?
|
||||
|
||||
if Experimentation.experiment(experiment_key).use_backwards_compatible_subject_index
|
||||
experimentation_subject_id.delete('-').hex % 100
|
||||
else
|
||||
Zlib.crc32("#{experiment_key}#{experimentation_subject_id}") % 100
|
||||
end
|
||||
end
|
||||
|
||||
def track_experiment_event_for(experiment_key, action, value)
|
||||
return unless Experimentation.enabled?(experiment_key)
|
||||
|
||||
yield experimentation_tracking_data(experiment_key, action, value)
|
||||
end
|
||||
|
||||
def experimentation_tracking_data(experiment_key, action, value)
|
||||
{
|
||||
category: tracking_category(experiment_key),
|
||||
action: action,
|
||||
property: tracking_group(experiment_key, "_group"),
|
||||
label: experimentation_subject_id,
|
||||
value: value
|
||||
}.compact
|
||||
end
|
||||
|
||||
def tracking_category(experiment_key)
|
||||
Experimentation.experiment(experiment_key).tracking_category
|
||||
end
|
||||
|
||||
def tracking_group(experiment_key, suffix = nil)
|
||||
return unless Experimentation.enabled?(experiment_key)
|
||||
|
||||
group = experiment_enabled?(experiment_key) ? GROUP_EXPERIMENTAL : GROUP_CONTROL
|
||||
|
||||
suffix ? "#{group}#{suffix}" : group
|
||||
end
|
||||
|
||||
def forced_enabled?(experiment_key)
|
||||
params.has_key?(:force_experiment) && params[:force_experiment] == experiment_key.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6958,9 +6958,6 @@ msgstr ""
|
|||
msgid "Compliance framework (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Compliance frameworks"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceDashboard|created by:"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -19103,6 +19100,9 @@ msgstr ""
|
|||
msgid "Outdent"
|
||||
msgstr ""
|
||||
|
||||
msgid "Overall Activity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Overridden"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -20339,6 +20339,9 @@ msgstr ""
|
|||
msgid "Prevent users from changing their profile name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prevent users from modifying merge request approvers list"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -22252,9 +22255,6 @@ msgstr ""
|
|||
msgid "Registry setup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Regulate approvals by authors/committers, based on compliance frameworks. Can be changed only at the instance level."
|
||||
msgstr ""
|
||||
|
||||
msgid "Reindexing status"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -22751,6 +22751,9 @@ msgstr ""
|
|||
msgid "Repositories Analytics"
|
||||
msgstr ""
|
||||
|
||||
msgid "RepositoriesAnalytics|Average Coverage by Job"
|
||||
msgstr ""
|
||||
|
||||
msgid "RepositoriesAnalytics|Coverage"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -22781,12 +22784,18 @@ msgstr ""
|
|||
msgid "RepositoriesAnalytics|Please select projects to display."
|
||||
msgstr ""
|
||||
|
||||
msgid "RepositoriesAnalytics|Projects with Tests"
|
||||
msgstr ""
|
||||
|
||||
msgid "RepositoriesAnalytics|Test Code Coverage"
|
||||
msgstr ""
|
||||
|
||||
msgid "RepositoriesAnalytics|There was an error fetching the projects."
|
||||
msgstr ""
|
||||
|
||||
msgid "RepositoriesAnalytics|Total Number of Coverages"
|
||||
msgstr ""
|
||||
|
||||
msgid "Repository"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -22974,9 +22983,6 @@ msgstr ""
|
|||
msgid "Reset authorization key?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reset filters"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reset health check access token"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26653,9 +26659,6 @@ msgstr ""
|
|||
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
|
||||
msgstr ""
|
||||
|
||||
msgid "The above settings apply to all projects with the selected compliance framework(s)."
|
||||
msgstr ""
|
||||
|
||||
msgid "The application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe LfsRequest do
|
||||
include ProjectForksHelper
|
||||
|
||||
controller(Repositories::GitHttpClientController) do
|
||||
# `described_class` is not available in this context
|
||||
include LfsRequest
|
||||
|
||||
def show
|
||||
head :ok
|
||||
end
|
||||
|
||||
def project
|
||||
@project ||= Project.find_by(id: params[:id])
|
||||
end
|
||||
|
||||
def download_request?
|
||||
true
|
||||
end
|
||||
|
||||
def upload_request?
|
||||
false
|
||||
end
|
||||
|
||||
def ci?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
let(:project) { create(:project, :public) }
|
||||
|
||||
before do
|
||||
stub_lfs_setting(enabled: true)
|
||||
end
|
||||
|
||||
context 'user is authenticated without access to lfs' do
|
||||
before do
|
||||
allow(controller).to receive(:authenticate_user)
|
||||
allow(controller).to receive(:authentication_result) do
|
||||
Gitlab::Auth::Result.new
|
||||
end
|
||||
end
|
||||
|
||||
context 'with access to the project' do
|
||||
it 'returns 403' do
|
||||
get :show, params: { id: project.id }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without access to the project' do
|
||||
context 'project does not exist' do
|
||||
it 'returns 404' do
|
||||
get :show, params: { id: 'does not exist' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'project is private' do
|
||||
let(:project) { create(:project, :private) }
|
||||
|
||||
it 'returns 404' do
|
||||
get :show, params: { id: project.id }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :design, class: 'DesignManagement::Design' do
|
||||
factory :design, traits: [:has_internal_id], class: 'DesignManagement::Design' do
|
||||
issue { association(:issue) }
|
||||
project { issue&.project || association(:project) }
|
||||
sequence(:filename) { |n| "homescreen-#{n}.jpg" }
|
||||
|
|
|
|||
|
|
@ -17,5 +17,9 @@ FactoryBot.define do
|
|||
|
||||
container { project }
|
||||
end
|
||||
|
||||
trait :empty_repo do
|
||||
after(:create, &:create_wiki_repository)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -122,6 +122,19 @@ RSpec.describe 'Runners' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when multiple runners are configured' do
|
||||
let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
|
||||
let!(:specific_runner_2) { create(:ci_runner, :project, projects: [project]) }
|
||||
|
||||
it 'adds pagination to the runner list' do
|
||||
stub_const('Projects::Settings::CiCdController::NUMBER_OF_RUNNERS_PER_PAGE', 1)
|
||||
|
||||
visit project_runners_path(project)
|
||||
|
||||
expect(find('.pagination')).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a specific runner exists in another project' do
|
||||
let(:another_project) { create(:project) }
|
||||
let!(:specific_runner) { create(:ci_runner, :project, projects: [another_project]) }
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@
|
|||
"designs":[
|
||||
{
|
||||
"id":38,
|
||||
"iid": 1,
|
||||
"project_id":30,
|
||||
"issue_id":469,
|
||||
"filename":"chirrido3.jpg",
|
||||
|
|
@ -107,6 +108,7 @@
|
|||
},
|
||||
{
|
||||
"id":39,
|
||||
"iid": 2,
|
||||
"project_id":30,
|
||||
"issue_id":469,
|
||||
"filename":"jonathan_richman.jpg",
|
||||
|
|
@ -116,6 +118,7 @@
|
|||
},
|
||||
{
|
||||
"id":40,
|
||||
"iid": 3,
|
||||
"project_id":30,
|
||||
"issue_id":469,
|
||||
"filename":"mariavontrap.jpeg",
|
||||
|
|
@ -137,6 +140,7 @@
|
|||
"event":0,
|
||||
"design":{
|
||||
"id":38,
|
||||
"iid": 1,
|
||||
"project_id":30,
|
||||
"issue_id":469,
|
||||
"filename":"chirrido3.jpg"
|
||||
|
|
@ -156,6 +160,7 @@
|
|||
"event":1,
|
||||
"design":{
|
||||
"id":38,
|
||||
"iid": 1,
|
||||
"project_id":30,
|
||||
"issue_id":469,
|
||||
"filename":"chirrido3.jpg"
|
||||
|
|
@ -167,6 +172,7 @@
|
|||
"event":0,
|
||||
"design":{
|
||||
"id":39,
|
||||
"iid": 2,
|
||||
"project_id":30,
|
||||
"issue_id":469,
|
||||
"filename":"jonathan_richman.jpg"
|
||||
|
|
@ -186,6 +192,7 @@
|
|||
"event":1,
|
||||
"design":{
|
||||
"id":38,
|
||||
"iid": 1,
|
||||
"project_id":30,
|
||||
"issue_id":469,
|
||||
"filename":"chirrido3.jpg"
|
||||
|
|
@ -197,6 +204,7 @@
|
|||
"event":2,
|
||||
"design":{
|
||||
"id":39,
|
||||
"iid": 2,
|
||||
"project_id":30,
|
||||
"issue_id":469,
|
||||
"filename":"jonathan_richman.jpg"
|
||||
|
|
@ -208,6 +216,7 @@
|
|||
"event":0,
|
||||
"design":{
|
||||
"id":40,
|
||||
"iid": 3,
|
||||
"project_id":30,
|
||||
"issue_id":469,
|
||||
"filename":"mariavontrap.jpeg"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Vuex from 'vuex';
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import createDiffsStore from '~/diffs/store/modules';
|
||||
import createNotesStore from '~/notes/stores/modules';
|
||||
import diffFileMockDataReadable from '../mock_data/diff_file';
|
||||
import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable';
|
||||
|
||||
|
|
@ -10,9 +11,13 @@ import DiffFileHeaderComponent from '~/diffs/components/diff_file_header.vue';
|
|||
import DiffContentComponent from '~/diffs/components/diff_content.vue';
|
||||
|
||||
import eventHub from '~/diffs/event_hub';
|
||||
import {
|
||||
EVT_EXPAND_ALL_FILES,
|
||||
EVT_PERF_MARK_DIFF_FILES_END,
|
||||
EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN,
|
||||
} from '~/diffs/constants';
|
||||
|
||||
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
|
||||
import { EVT_EXPAND_ALL_FILES } from '~/diffs/constants';
|
||||
|
||||
function changeViewer(store, index, { automaticallyCollapsed, manuallyCollapsed, name }) {
|
||||
const file = store.state.diffs.diffFiles[index];
|
||||
|
|
@ -58,12 +63,13 @@ function markFileToBeRendered(store, index = 0) {
|
|||
});
|
||||
}
|
||||
|
||||
function createComponent({ file }) {
|
||||
function createComponent({ file, first = false, last = false }) {
|
||||
const localVue = createLocalVue();
|
||||
|
||||
localVue.use(Vuex);
|
||||
|
||||
const store = new Vuex.Store({
|
||||
...createNotesStore(),
|
||||
modules: {
|
||||
diffs: createDiffsStore(),
|
||||
},
|
||||
|
|
@ -78,6 +84,8 @@ function createComponent({ file }) {
|
|||
file,
|
||||
canCurrentUserFork: false,
|
||||
viewDiffsFileByFile: false,
|
||||
isFirstFile: first,
|
||||
isLastFile: last,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -117,6 +125,72 @@ describe('DiffFile', () => {
|
|||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('bus events', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
describe('during mount', () => {
|
||||
it.each`
|
||||
first | last | events | file
|
||||
${false} | ${false} | ${[]} | ${{ inlineLines: [], parallelLines: [], readableText: true }}
|
||||
${true} | ${true} | ${[]} | ${{ inlineLines: [], parallelLines: [], readableText: true }}
|
||||
${true} | ${false} | ${[EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN]} | ${false}
|
||||
${false} | ${true} | ${[EVT_PERF_MARK_DIFF_FILES_END]} | ${false}
|
||||
${true} | ${true} | ${[EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN, EVT_PERF_MARK_DIFF_FILES_END]} | ${false}
|
||||
`(
|
||||
'emits the events $events based on the file and its position ({ first: $first, last: $last }) among all files',
|
||||
async ({ file, first, last, events }) => {
|
||||
if (file) {
|
||||
forceHasDiff({ store, ...file });
|
||||
}
|
||||
|
||||
({ wrapper, store } = createComponent({
|
||||
file: store.state.diffs.diffFiles[0],
|
||||
first,
|
||||
last,
|
||||
}));
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledTimes(events.length);
|
||||
events.forEach(event => {
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith(event);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('after loading the diff', () => {
|
||||
it('indicates that it loaded the file', async () => {
|
||||
forceHasDiff({ store, inlineLines: [], parallelLines: [], readableText: true });
|
||||
({ wrapper, store } = createComponent({
|
||||
file: store.state.diffs.diffFiles[0],
|
||||
first: true,
|
||||
last: true,
|
||||
}));
|
||||
|
||||
jest.spyOn(wrapper.vm, 'loadCollapsedDiff').mockResolvedValue(getReadableFile());
|
||||
jest.spyOn(window, 'requestIdleCallback').mockImplementation(fn => fn());
|
||||
|
||||
makeFileAutomaticallyCollapsed(store);
|
||||
|
||||
await wrapper.vm.$nextTick(); // Wait for store updates to flow into the component
|
||||
|
||||
toggleFile(wrapper);
|
||||
|
||||
await wrapper.vm.$nextTick(); // Wait for the load to resolve
|
||||
await wrapper.vm.$nextTick(); // Wait for the idleCallback
|
||||
await wrapper.vm.$nextTick(); // Wait for nextTick inside postRender
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledTimes(2);
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN);
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_DIFF_FILES_END);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
import Vuex from 'vuex';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { MOCK_QUERY } from 'jest/search/mock_data';
|
||||
import * as urlUtils from '~/lib/utils/url_utility';
|
||||
import initStore from '~/search/store';
|
||||
import DropdownFilter from '~/search/dropdown_filter/components/dropdown_filter.vue';
|
||||
import stateFilterData from '~/search/dropdown_filter/constants/state_filter_data';
|
||||
import confidentialFilterData from '~/search/dropdown_filter/constants/confidential_filter_data';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
visitUrl: jest.fn(),
|
||||
setUrlParams: jest.fn(),
|
||||
}));
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('DropdownFilter', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
const createStore = options => {
|
||||
store = initStore({ query: MOCK_QUERY, ...options });
|
||||
};
|
||||
|
||||
const createComponent = (props = { filterData: stateFilterData }) => {
|
||||
wrapper = shallowMount(DropdownFilter, {
|
||||
localVue,
|
||||
store,
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
store = null;
|
||||
});
|
||||
|
||||
const findGlDropdown = () => wrapper.find(GlDropdown);
|
||||
const findGlDropdownItems = () => findGlDropdown().findAll(GlDropdownItem);
|
||||
const findDropdownItemsText = () => findGlDropdownItems().wrappers.map(w => w.text());
|
||||
const firstDropDownItem = () => findGlDropdownItems().at(0);
|
||||
|
||||
describe('StatusFilter', () => {
|
||||
describe('template', () => {
|
||||
describe.each`
|
||||
scope | showDropdown
|
||||
${'issues'} | ${true}
|
||||
${'merge_requests'} | ${true}
|
||||
${'projects'} | ${false}
|
||||
${'milestones'} | ${false}
|
||||
${'users'} | ${false}
|
||||
${'notes'} | ${false}
|
||||
${'wiki_blobs'} | ${false}
|
||||
${'blobs'} | ${false}
|
||||
`(`dropdown`, ({ scope, showDropdown }) => {
|
||||
beforeEach(() => {
|
||||
createStore({ query: { ...MOCK_QUERY, scope } });
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it(`does${showDropdown ? '' : ' not'} render when scope is ${scope}`, () => {
|
||||
expect(findGlDropdown().exists()).toBe(showDropdown);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
initialFilter | label
|
||||
${stateFilterData.filters.ANY.value} | ${`Any ${stateFilterData.header}`}
|
||||
${stateFilterData.filters.OPEN.value} | ${stateFilterData.filters.OPEN.label}
|
||||
${stateFilterData.filters.CLOSED.value} | ${stateFilterData.filters.CLOSED.label}
|
||||
`(`filter text`, ({ initialFilter, label }) => {
|
||||
describe(`when initialFilter is ${initialFilter}`, () => {
|
||||
beforeEach(() => {
|
||||
createStore({ query: { ...MOCK_QUERY, [stateFilterData.filterParam]: initialFilter } });
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it(`sets dropdown label to ${label}`, () => {
|
||||
expect(findGlDropdown().attributes('text')).toBe(label);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filter options', () => {
|
||||
beforeEach(() => {
|
||||
createStore();
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders a dropdown item for each filterOption', () => {
|
||||
expect(findDropdownItemsText()).toStrictEqual(
|
||||
stateFilterData.filterByScope[stateFilterData.scopes.ISSUES].map(v => {
|
||||
return v.label;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('clicking a dropdown item calls setUrlParams', () => {
|
||||
const filter = stateFilterData.filters[Object.keys(stateFilterData.filters)[0]].value;
|
||||
firstDropDownItem().vm.$emit('click');
|
||||
|
||||
expect(urlUtils.setUrlParams).toHaveBeenCalledWith({
|
||||
page: null,
|
||||
[stateFilterData.filterParam]: filter,
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking a dropdown item calls visitUrl', () => {
|
||||
firstDropDownItem().vm.$emit('click');
|
||||
|
||||
expect(urlUtils.visitUrl).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfidentialFilter', () => {
|
||||
describe('template', () => {
|
||||
describe.each`
|
||||
scope | showDropdown
|
||||
${'issues'} | ${true}
|
||||
${'merge_requests'} | ${false}
|
||||
${'projects'} | ${false}
|
||||
${'milestones'} | ${false}
|
||||
${'users'} | ${false}
|
||||
${'notes'} | ${false}
|
||||
${'wiki_blobs'} | ${false}
|
||||
${'blobs'} | ${false}
|
||||
`(`dropdown`, ({ scope, showDropdown }) => {
|
||||
beforeEach(() => {
|
||||
createStore({ query: { ...MOCK_QUERY, scope } });
|
||||
createComponent({ filterData: confidentialFilterData });
|
||||
});
|
||||
|
||||
it(`does${showDropdown ? '' : ' not'} render when scope is ${scope}`, () => {
|
||||
expect(findGlDropdown().exists()).toBe(showDropdown);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
initialFilter | label
|
||||
${confidentialFilterData.filters.ANY.value} | ${`Any ${confidentialFilterData.header}`}
|
||||
${confidentialFilterData.filters.CONFIDENTIAL.value} | ${confidentialFilterData.filters.CONFIDENTIAL.label}
|
||||
${confidentialFilterData.filters.NOT_CONFIDENTIAL.value} | ${confidentialFilterData.filters.NOT_CONFIDENTIAL.label}
|
||||
`(`filter text`, ({ initialFilter, label }) => {
|
||||
describe(`when initialFilter is ${initialFilter}`, () => {
|
||||
beforeEach(() => {
|
||||
createStore({
|
||||
query: { ...MOCK_QUERY, [confidentialFilterData.filterParam]: initialFilter },
|
||||
});
|
||||
createComponent({ filterData: confidentialFilterData });
|
||||
});
|
||||
|
||||
it(`sets dropdown label to ${label}`, () => {
|
||||
expect(findGlDropdown().attributes('text')).toBe(label);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filter options', () => {
|
||||
beforeEach(() => {
|
||||
createStore();
|
||||
createComponent({ filterData: confidentialFilterData });
|
||||
});
|
||||
|
||||
it('renders a dropdown item for each filterOption', () => {
|
||||
expect(findDropdownItemsText()).toStrictEqual(
|
||||
confidentialFilterData.filterByScope[confidentialFilterData.scopes.ISSUES].map(v => {
|
||||
return v.label;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('clicking a dropdown item calls setUrlParams', () => {
|
||||
const filter =
|
||||
confidentialFilterData.filters[Object.keys(confidentialFilterData.filters)[0]].value;
|
||||
firstDropDownItem().vm.$emit('click');
|
||||
|
||||
expect(urlUtils.setUrlParams).toHaveBeenCalledWith({
|
||||
page: null,
|
||||
[confidentialFilterData.filterParam]: filter,
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking a dropdown item calls visitUrl', () => {
|
||||
firstDropDownItem().vm.$emit('click');
|
||||
|
||||
expect(urlUtils.visitUrl).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
import Vuex from 'vuex';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import { GlButton, GlLink } from '@gitlab/ui';
|
||||
import { MOCK_QUERY } from 'jest/search/mock_data';
|
||||
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
|
||||
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
|
||||
import StatusFilter from '~/search/sidebar/components/status_filter.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('GlobalSearchSidebar', () => {
|
||||
let wrapper;
|
||||
|
||||
const actionSpies = {
|
||||
applyQuery: jest.fn(),
|
||||
resetQuery: jest.fn(),
|
||||
};
|
||||
|
||||
const createComponent = initialState => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
query: MOCK_QUERY,
|
||||
...initialState,
|
||||
},
|
||||
actions: actionSpies,
|
||||
});
|
||||
|
||||
wrapper = shallowMount(GlobalSearchSidebar, {
|
||||
localVue,
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
const findSidebarForm = () => wrapper.find('form');
|
||||
const findStatusFilter = () => wrapper.find(StatusFilter);
|
||||
const findConfidentialityFilter = () => wrapper.find(ConfidentialityFilter);
|
||||
const findApplyButton = () => wrapper.find(GlButton);
|
||||
const findResetLinkButton = () => wrapper.find(GlLink);
|
||||
|
||||
describe('template', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders StatusFilter always', () => {
|
||||
expect(findStatusFilter().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders ConfidentialityFilter always', () => {
|
||||
expect(findConfidentialityFilter().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders ApplyButton always', () => {
|
||||
expect(findApplyButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('ResetLinkButton', () => {
|
||||
describe('with no filter selected', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ query: {} });
|
||||
});
|
||||
|
||||
it('does not render', () => {
|
||||
expect(findResetLinkButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with filter selected', () => {
|
||||
it('does render when a filter selected', () => {
|
||||
expect(findResetLinkButton().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('clicking ApplyButton calls applyQuery', () => {
|
||||
findSidebarForm().trigger('submit');
|
||||
|
||||
expect(actionSpies.applyQuery).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clicking ResetLinkButton calls resetQuery', () => {
|
||||
findResetLinkButton().vm.$emit('click');
|
||||
|
||||
expect(actionSpies.resetQuery).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -2,7 +2,6 @@ import MockAdapter from 'axios-mock-adapter';
|
|||
import testAction from 'helpers/vuex_action_helper';
|
||||
import * as actions from '~/search/store/actions';
|
||||
import * as types from '~/search/store/mutation_types';
|
||||
import * as urlUtils from '~/lib/utils/url_utility';
|
||||
import state from '~/search/store/state';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import createFlash from '~/flash';
|
||||
|
|
@ -43,47 +42,6 @@ describe('Global Search Store Actions', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setQuery', () => {
|
||||
const payload = { key: 'key1', value: 'value1' };
|
||||
|
||||
it('calls the SET_QUERY mutation', done => {
|
||||
testAction(actions.setQuery, payload, state, [{ type: types.SET_QUERY, payload }], [], done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyQuery', () => {
|
||||
beforeEach(() => {
|
||||
urlUtils.setUrlParams = jest.fn();
|
||||
urlUtils.visitUrl = jest.fn();
|
||||
});
|
||||
|
||||
it('calls visitUrl and setParams with the state.query', () => {
|
||||
testAction(actions.applyQuery, null, state, [], [], () => {
|
||||
expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ ...state.query, page: null });
|
||||
expect(urlUtils.visitUrl).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetQuery', () => {
|
||||
beforeEach(() => {
|
||||
urlUtils.setUrlParams = jest.fn();
|
||||
urlUtils.visitUrl = jest.fn();
|
||||
});
|
||||
|
||||
it('calls visitUrl and setParams with empty values', () => {
|
||||
testAction(actions.resetQuery, null, state, [], [], () => {
|
||||
expect(urlUtils.setUrlParams).toHaveBeenCalledWith({
|
||||
...state.query,
|
||||
page: null,
|
||||
state: null,
|
||||
confidential: null,
|
||||
});
|
||||
expect(urlUtils.visitUrl).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setQuery', () => {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue