Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ad05e1db03
commit
f96f2720d1
|
@ -66,6 +66,13 @@ docs-lint links:
|
|||
- bundle exec nanoc
|
||||
# Check the internal links
|
||||
- bundle exec nanoc check internal_links
|
||||
# Delete the redirect files, rebuild, and check internal links again, to see if we are linking to redirects.
|
||||
# Don't delete the documentation/index.md, which is a false positive for the simple grep.
|
||||
- grep -rl "redirect_to:" /tmp/gitlab-docs/content/ee/ | grep -v "development/documentation/index.md" | xargs rm -f
|
||||
- bundle exec nanoc
|
||||
- echo -e "\e[96mThe following test fails when a doc links to a redirect file."
|
||||
- echo -e "\e[96mMake sure all links point to the correct page."
|
||||
- bundle exec nanoc check internal_links
|
||||
# Check the internal anchor links
|
||||
- bundle exec nanoc check internal_anchors
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
3cbd24e3e2fd09eb526d04f8a419f6d103c440dc
|
||||
32bbe0bf214e052e107021742e801cffb09b8ca5
|
||||
|
|
|
@ -10,7 +10,10 @@ import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
|||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { isSingleViewStyle } from '~/helpers/diffs_helper';
|
||||
import { updateHistory } from '~/lib/utils/url_utility';
|
||||
import eventHub from '../../notes/event_hub';
|
||||
|
||||
import notesEventHub from '../../notes/event_hub';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
import CompareVersions from './compare_versions.vue';
|
||||
import DiffFile from './diff_file.vue';
|
||||
import NoChanges from './no_changes.vue';
|
||||
|
@ -22,6 +25,7 @@ import MergeConflictWarning from './merge_conflict_warning.vue';
|
|||
import CollapsedFilesWarning from './collapsed_files_warning.vue';
|
||||
|
||||
import { diffsApp } from '../utils/performance';
|
||||
import { fileByFile } from '../utils/preferences';
|
||||
|
||||
import {
|
||||
TREE_LIST_WIDTH_STORAGE_KEY,
|
||||
|
@ -34,6 +38,7 @@ import {
|
|||
ALERT_OVERFLOW_HIDDEN,
|
||||
ALERT_MERGE_CONFLICT,
|
||||
ALERT_COLLAPSED_FILES,
|
||||
EVT_VIEW_FILE_BY_FILE,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
|
@ -114,7 +119,7 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
viewDiffsFileByFile: {
|
||||
fileByFileUserPreference: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
|
@ -154,6 +159,7 @@ export default {
|
|||
'conflictResolutionPath',
|
||||
'canMerge',
|
||||
'hasConflicts',
|
||||
'viewDiffsFileByFile',
|
||||
]),
|
||||
...mapGetters('diffs', ['whichCollapsedTypes', 'isParallelView', 'currentDiffIndex']),
|
||||
...mapGetters(['isNotesFetched', 'getNoteableData']),
|
||||
|
@ -254,7 +260,7 @@ export default {
|
|||
projectPath: this.projectPath,
|
||||
dismissEndpoint: this.dismissEndpoint,
|
||||
showSuggestPopover: this.showSuggestPopover,
|
||||
viewDiffsFileByFile: this.viewDiffsFileByFile,
|
||||
viewDiffsFileByFile: fileByFile(this.fileByFileUserPreference),
|
||||
});
|
||||
|
||||
if (this.shouldShow) {
|
||||
|
@ -278,8 +284,10 @@ export default {
|
|||
created() {
|
||||
this.adjustView();
|
||||
|
||||
eventHub.$once('fetchDiffData', this.fetchData);
|
||||
eventHub.$on('refetchDiffData', this.refetchDiffData);
|
||||
notesEventHub.$once('fetchDiffData', this.fetchData);
|
||||
notesEventHub.$on('refetchDiffData', this.refetchDiffData);
|
||||
eventHub.$on(EVT_VIEW_FILE_BY_FILE, this.fileByFileListener);
|
||||
|
||||
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
|
||||
|
||||
this.unwatchDiscussions = this.$watch(
|
||||
|
@ -300,8 +308,10 @@ export default {
|
|||
beforeDestroy() {
|
||||
diffsApp.deinstrument();
|
||||
|
||||
eventHub.$off('fetchDiffData', this.fetchData);
|
||||
eventHub.$off('refetchDiffData', this.refetchDiffData);
|
||||
eventHub.$off(EVT_VIEW_FILE_BY_FILE, this.fileByFileListener);
|
||||
notesEventHub.$off('refetchDiffData', this.refetchDiffData);
|
||||
notesEventHub.$off('fetchDiffData', this.fetchData);
|
||||
|
||||
this.removeEventListeners();
|
||||
},
|
||||
methods: {
|
||||
|
@ -319,7 +329,11 @@ export default {
|
|||
'scrollToFile',
|
||||
'setShowTreeList',
|
||||
'navigateToDiffFileIndex',
|
||||
'setFileByFile',
|
||||
]),
|
||||
fileByFileListener({ setting } = {}) {
|
||||
this.setFileByFile({ fileByFile: setting });
|
||||
},
|
||||
navigateToDiffFileNumber(number) {
|
||||
this.navigateToDiffFileIndex(number - 1);
|
||||
},
|
||||
|
@ -371,7 +385,7 @@ export default {
|
|||
}
|
||||
|
||||
if (!this.isNotesFetched) {
|
||||
eventHub.$emit('fetchNotesData');
|
||||
notesEventHub.$emit('fetchNotesData');
|
||||
}
|
||||
},
|
||||
setDiscussions() {
|
||||
|
|
|
@ -1,16 +1,38 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { GlButtonGroup, GlButton, GlDropdown } from '@gitlab/ui';
|
||||
import { GlButtonGroup, GlButton, GlDropdown, GlFormCheckbox } from '@gitlab/ui';
|
||||
|
||||
import eventHub from '../event_hub';
|
||||
import { EVT_VIEW_FILE_BY_FILE } from '../constants';
|
||||
import { SETTINGS_DROPDOWN } from '../i18n';
|
||||
|
||||
export default {
|
||||
i18n: SETTINGS_DROPDOWN,
|
||||
components: {
|
||||
GlButtonGroup,
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
GlFormCheckbox,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
checked: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
|
||||
...mapState('diffs', ['renderTreeList', 'showWhitespace']),
|
||||
...mapState('diffs', ['renderTreeList', 'showWhitespace', 'viewDiffsFileByFile']),
|
||||
},
|
||||
watch: {
|
||||
viewDiffsFileByFile() {
|
||||
this.checked = this.viewDiffsFileByFile;
|
||||
},
|
||||
checked() {
|
||||
eventHub.$emit(EVT_VIEW_FILE_BY_FILE, { setting: this.checked });
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.checked = this.viewDiffsFileByFile;
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', [
|
||||
|
@ -19,6 +41,9 @@ export default {
|
|||
'setRenderTreeList',
|
||||
'setShowWhitespace',
|
||||
]),
|
||||
toggleFileByFile() {
|
||||
eventHub.$emit(EVT_VIEW_FILE_BY_FILE, { setting: !this.viewDiffsFileByFile });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -84,5 +109,10 @@ export default {
|
|||
{{ __('Show whitespace changes') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="gl-mt-3 gl-px-3">
|
||||
<gl-form-checkbox v-model="checked" data-testid="file-by-file" class="gl-mb-0">
|
||||
{{ $options.i18n.fileByFile }}
|
||||
</gl-form-checkbox>
|
||||
</div>
|
||||
</gl-dropdown>
|
||||
</template>
|
||||
|
|
|
@ -77,6 +77,11 @@ export const ALERT_COLLAPSED_FILES = 'collapsed';
|
|||
export const DIFF_FILE_AUTOMATIC_COLLAPSE = 'automatic';
|
||||
export const DIFF_FILE_MANUAL_COLLAPSE = 'manual';
|
||||
|
||||
// Diff view single file mode
|
||||
export const DIFF_FILE_BY_FILE_COOKIE_NAME = 'fileViewMode';
|
||||
export const DIFF_VIEW_FILE_BY_FILE = 'single';
|
||||
export const DIFF_VIEW_ALL_FILES = 'all';
|
||||
|
||||
// State machine states
|
||||
export const STATE_IDLING = 'idle';
|
||||
export const STATE_LOADING = 'loading';
|
||||
|
@ -98,6 +103,7 @@ export const RENAMED_DIFF_TRANSITIONS = {
|
|||
|
||||
// MR Diffs known events
|
||||
export const EVT_EXPAND_ALL_FILES = 'mr:diffs:expandAllFiles';
|
||||
export const EVT_VIEW_FILE_BY_FILE = 'mr:diffs:preference:fileByFile';
|
||||
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';
|
||||
|
|
|
@ -16,3 +16,7 @@ export const DIFF_FILE = {
|
|||
autoCollapsed: __('Files with large changes are collapsed by default.'),
|
||||
expand: __('Expand file'),
|
||||
};
|
||||
|
||||
export const SETTINGS_DROPDOWN = {
|
||||
fileByFile: __('Show one file at a time'),
|
||||
};
|
||||
|
|
|
@ -116,7 +116,7 @@ export default function initDiffsApp(store) {
|
|||
isFluidLayout: this.isFluidLayout,
|
||||
dismissEndpoint: this.dismissEndpoint,
|
||||
showSuggestPopover: this.showSuggestPopover,
|
||||
viewDiffsFileByFile: this.viewDiffsFileByFile,
|
||||
fileByFileUserPreference: this.viewDiffsFileByFile,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -44,6 +44,9 @@ import {
|
|||
EVT_PERF_MARK_FILE_TREE_START,
|
||||
EVT_PERF_MARK_FILE_TREE_END,
|
||||
EVT_PERF_MARK_DIFF_FILES_START,
|
||||
DIFF_VIEW_FILE_BY_FILE,
|
||||
DIFF_VIEW_ALL_FILES,
|
||||
DIFF_FILE_BY_FILE_COOKIE_NAME,
|
||||
} from '../constants';
|
||||
import { diffViewerModes } from '~/ide/constants';
|
||||
import { isCollapsed } from '../diff_file';
|
||||
|
@ -57,6 +60,7 @@ export const setBaseConfig = ({ commit }, options) => {
|
|||
projectPath,
|
||||
dismissEndpoint,
|
||||
showSuggestPopover,
|
||||
viewDiffsFileByFile,
|
||||
} = options;
|
||||
commit(types.SET_BASE_CONFIG, {
|
||||
endpoint,
|
||||
|
@ -66,6 +70,7 @@ export const setBaseConfig = ({ commit }, options) => {
|
|||
projectPath,
|
||||
dismissEndpoint,
|
||||
showSuggestPopover,
|
||||
viewDiffsFileByFile,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -694,3 +699,14 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => {
|
|||
|
||||
commit(types.VIEW_DIFF_FILE, fileHash);
|
||||
};
|
||||
|
||||
export const setFileByFile = ({ commit }, { fileByFile }) => {
|
||||
const fileViewMode = fileByFile ? DIFF_VIEW_FILE_BY_FILE : DIFF_VIEW_ALL_FILES;
|
||||
commit(types.SET_FILE_BY_FILE, fileByFile);
|
||||
|
||||
Cookies.set(DIFF_FILE_BY_FILE_COOKIE_NAME, fileViewMode);
|
||||
|
||||
historyPushState(
|
||||
mergeUrlParams({ [DIFF_FILE_BY_FILE_COOKIE_NAME]: fileViewMode }, window.location.href),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,6 +5,8 @@ import {
|
|||
DIFF_VIEW_COOKIE_NAME,
|
||||
DIFF_WHITESPACE_COOKIE_NAME,
|
||||
} from '../../constants';
|
||||
|
||||
import { fileByFile } from '../../utils/preferences';
|
||||
import { getDefaultWhitespace } from '../utils';
|
||||
|
||||
const viewTypeFromQueryString = getParameterValues('view')[0];
|
||||
|
@ -39,6 +41,7 @@ export default () => ({
|
|||
highlightedRow: null,
|
||||
renderTreeList: true,
|
||||
showWhitespace: getDefaultWhitespace(whiteSpaceFromQueryString, whiteSpaceFromCookie),
|
||||
viewDiffsFileByFile: fileByFile(),
|
||||
fileFinderVisible: false,
|
||||
dismissEndpoint: '',
|
||||
showSuggestPopover: true,
|
||||
|
|
|
@ -28,6 +28,7 @@ export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW';
|
|||
export const SET_TREE_DATA = 'SET_TREE_DATA';
|
||||
export const SET_RENDER_TREE_LIST = 'SET_RENDER_TREE_LIST';
|
||||
export const SET_SHOW_WHITESPACE = 'SET_SHOW_WHITESPACE';
|
||||
export const SET_FILE_BY_FILE = 'SET_FILE_BY_FILE';
|
||||
export const TOGGLE_FILE_FINDER_VISIBLE = 'TOGGLE_FILE_FINDER_VISIBLE';
|
||||
|
||||
export const REQUEST_FULL_DIFF = 'REQUEST_FULL_DIFF';
|
||||
|
|
|
@ -36,6 +36,7 @@ export default {
|
|||
projectPath,
|
||||
dismissEndpoint,
|
||||
showSuggestPopover,
|
||||
viewDiffsFileByFile,
|
||||
} = options;
|
||||
Object.assign(state, {
|
||||
endpoint,
|
||||
|
@ -45,6 +46,7 @@ export default {
|
|||
projectPath,
|
||||
dismissEndpoint,
|
||||
showSuggestPopover,
|
||||
viewDiffsFileByFile,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -352,4 +354,7 @@ export default {
|
|||
[types.SET_SHOW_SUGGEST_POPOVER](state) {
|
||||
state.showSuggestPopover = false;
|
||||
},
|
||||
[types.SET_FILE_BY_FILE](state, fileByFile) {
|
||||
state.viewDiffsFileByFile = fileByFile;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import Cookies from 'js-cookie';
|
||||
import { getParameterValues } from '~/lib/utils/url_utility';
|
||||
|
||||
import { DIFF_FILE_BY_FILE_COOKIE_NAME, DIFF_VIEW_FILE_BY_FILE } from '../constants';
|
||||
|
||||
export function fileByFile(pref = false) {
|
||||
const search = getParameterValues(DIFF_FILE_BY_FILE_COOKIE_NAME)?.[0];
|
||||
const cookie = Cookies.get(DIFF_FILE_BY_FILE_COOKIE_NAME);
|
||||
let viewFileByFile = pref;
|
||||
|
||||
// use the cookie first, if it exists
|
||||
if (cookie) {
|
||||
viewFileByFile = cookie === DIFF_VIEW_FILE_BY_FILE;
|
||||
}
|
||||
|
||||
// the search parameter of the URL should override, if it exists
|
||||
if (search) {
|
||||
viewFileByFile = search === DIFF_VIEW_FILE_BY_FILE;
|
||||
}
|
||||
|
||||
return viewFileByFile;
|
||||
}
|
|
@ -12,10 +12,12 @@ import {
|
|||
GlLink,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlSearchBoxByType,
|
||||
GlSprintf,
|
||||
GlLoadingIcon,
|
||||
} from '@gitlab/ui';
|
||||
import * as Sentry from '~/sentry/wrapper';
|
||||
import { s__, __, n__ } from '~/locale';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
|
@ -46,6 +48,7 @@ export default {
|
|||
GlLink,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlSearchBoxByType,
|
||||
GlSprintf,
|
||||
GlLoadingIcon,
|
||||
|
@ -59,11 +62,19 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
defaultBranch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
refs: {
|
||||
branches: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
@ -94,7 +105,9 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
refValue: this.refParam,
|
||||
refValue: {
|
||||
shortName: this.refParam,
|
||||
},
|
||||
form: {},
|
||||
error: null,
|
||||
warnings: [],
|
||||
|
@ -104,9 +117,21 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
filteredRefs() {
|
||||
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
|
||||
return this.refs.filter(ref => ref.toLowerCase().includes(lowerCasedSearchTerm));
|
||||
lowerCasedSearchTerm() {
|
||||
return this.searchTerm.toLowerCase();
|
||||
},
|
||||
filteredBranches() {
|
||||
return this.branches.filter(branch =>
|
||||
branch.shortName.toLowerCase().includes(this.lowerCasedSearchTerm),
|
||||
);
|
||||
},
|
||||
filteredTags() {
|
||||
return this.tags.filter(tag =>
|
||||
tag.shortName.toLowerCase().includes(this.lowerCasedSearchTerm),
|
||||
);
|
||||
},
|
||||
hasTags() {
|
||||
return this.tags.length > 0;
|
||||
},
|
||||
overMaxWarningsLimit() {
|
||||
return this.totalWarnings > this.maxWarnings;
|
||||
|
@ -120,14 +145,27 @@ export default {
|
|||
shouldShowWarning() {
|
||||
return this.warnings.length > 0 && !this.isWarningDismissed;
|
||||
},
|
||||
refShortName() {
|
||||
return this.refValue.shortName;
|
||||
},
|
||||
refFullName() {
|
||||
return this.refValue.fullName;
|
||||
},
|
||||
variables() {
|
||||
return this.form[this.refValue]?.variables ?? [];
|
||||
return this.form[this.refFullName]?.variables ?? [];
|
||||
},
|
||||
descriptions() {
|
||||
return this.form[this.refValue]?.descriptions ?? {};
|
||||
return this.form[this.refFullName]?.descriptions ?? {};
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// this is needed until we add support for ref type in url query strings
|
||||
// ensure default branch is called with full ref on load
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/287815
|
||||
if (this.refValue.shortName === this.defaultBranch) {
|
||||
this.refValue.fullName = `refs/heads/${this.refValue.shortName}`;
|
||||
}
|
||||
|
||||
this.setRefSelected(this.refValue);
|
||||
},
|
||||
methods: {
|
||||
|
@ -170,19 +208,19 @@ export default {
|
|||
setRefSelected(refValue) {
|
||||
this.refValue = refValue;
|
||||
|
||||
if (!this.form[refValue]) {
|
||||
this.fetchConfigVariables(refValue)
|
||||
if (!this.form[this.refFullName]) {
|
||||
this.fetchConfigVariables(this.refFullName || this.refShortName)
|
||||
.then(({ descriptions, params }) => {
|
||||
Vue.set(this.form, refValue, {
|
||||
Vue.set(this.form, this.refFullName, {
|
||||
variables: [],
|
||||
descriptions,
|
||||
});
|
||||
|
||||
// Add default variables from yml
|
||||
this.setVariableParams(refValue, VARIABLE_TYPE, params);
|
||||
this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
|
||||
})
|
||||
.catch(() => {
|
||||
Vue.set(this.form, refValue, {
|
||||
Vue.set(this.form, this.refFullName, {
|
||||
variables: [],
|
||||
descriptions: {},
|
||||
});
|
||||
|
@ -190,20 +228,19 @@ export default {
|
|||
.finally(() => {
|
||||
// Add/update variables, e.g. from query string
|
||||
if (this.variableParams) {
|
||||
this.setVariableParams(refValue, VARIABLE_TYPE, this.variableParams);
|
||||
this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
|
||||
}
|
||||
if (this.fileParams) {
|
||||
this.setVariableParams(refValue, FILE_TYPE, this.fileParams);
|
||||
this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
|
||||
}
|
||||
|
||||
// Adds empty var at the end of the form
|
||||
this.addEmptyVariable(refValue);
|
||||
this.addEmptyVariable(this.refFullName);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
isSelected(ref) {
|
||||
return ref === this.refValue;
|
||||
return ref.fullName === this.refValue.fullName;
|
||||
},
|
||||
removeVariable(index) {
|
||||
this.variables.splice(index, 1);
|
||||
|
@ -211,7 +248,6 @@ export default {
|
|||
canRemove(index) {
|
||||
return index < this.variables.length - 1;
|
||||
},
|
||||
|
||||
fetchConfigVariables(refValue) {
|
||||
if (!gon?.features?.newPipelineFormPrefilledVars) {
|
||||
return Promise.resolve({ params: {}, descriptions: {} });
|
||||
|
@ -251,9 +287,11 @@ export default {
|
|||
|
||||
return { params, descriptions };
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(error => {
|
||||
this.isLoading = false;
|
||||
|
||||
Sentry.captureException(error);
|
||||
|
||||
return { params: {}, descriptions: {} };
|
||||
});
|
||||
},
|
||||
|
@ -268,7 +306,9 @@ export default {
|
|||
|
||||
return axios
|
||||
.post(this.pipelinesPath, {
|
||||
ref: this.refValue,
|
||||
// send shortName as fall back for query params
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/287815
|
||||
ref: this.refValue.fullName || this.refShortName,
|
||||
variables_attributes: filteredVariables,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
|
@ -326,20 +366,29 @@ export default {
|
|||
</details>
|
||||
</gl-alert>
|
||||
<gl-form-group :label="s__('Pipeline|Run for')">
|
||||
<gl-dropdown :text="refValue" block>
|
||||
<gl-search-box-by-type
|
||||
v-model.trim="searchTerm"
|
||||
:placeholder="__('Search branches and tags')"
|
||||
/>
|
||||
<gl-dropdown :text="refShortName" block>
|
||||
<gl-search-box-by-type v-model.trim="searchTerm" :placeholder="__('Search refs')" />
|
||||
<gl-dropdown-section-header>{{ __('Branches') }}</gl-dropdown-section-header>
|
||||
<gl-dropdown-item
|
||||
v-for="(ref, index) in filteredRefs"
|
||||
:key="index"
|
||||
v-for="branch in filteredBranches"
|
||||
:key="branch.fullName"
|
||||
class="gl-font-monospace"
|
||||
is-check-item
|
||||
:is-checked="isSelected(ref)"
|
||||
@click="setRefSelected(ref)"
|
||||
:is-checked="isSelected(branch)"
|
||||
@click="setRefSelected(branch)"
|
||||
>
|
||||
{{ ref }}
|
||||
{{ branch.shortName }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-section-header v-if="hasTags">{{ __('Tags') }}</gl-dropdown-section-header>
|
||||
<gl-dropdown-item
|
||||
v-for="tag in filteredTags"
|
||||
:key="tag.fullName"
|
||||
class="gl-font-monospace"
|
||||
is-check-item
|
||||
:is-checked="isSelected(tag)"
|
||||
@click="setRefSelected(tag)"
|
||||
>
|
||||
{{ tag.shortName }}
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
|
||||
|
@ -372,7 +421,7 @@ export default {
|
|||
:placeholder="s__('CiVariables|Input variable key')"
|
||||
:class="$options.formElementClasses"
|
||||
data-testid="pipeline-form-ci-variable-key"
|
||||
@change="addEmptyVariable(refValue)"
|
||||
@change="addEmptyVariable(refFullName)"
|
||||
/>
|
||||
<gl-form-input
|
||||
v-model="variable.value"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export const VARIABLE_TYPE = 'env_var';
|
||||
export const FILE_TYPE = 'file';
|
||||
export const CONFIG_VARIABLES_TIMEOUT = 5000;
|
||||
export const BRANCH_REF_TYPE = 'branch';
|
||||
export const TAG_REF_TYPE = 'tag';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import PipelineNewForm from './components/pipeline_new_form.vue';
|
||||
import formatRefs from './utils/format_refs';
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-new-pipeline');
|
||||
|
@ -7,17 +8,20 @@ export default () => {
|
|||
projectId,
|
||||
pipelinesPath,
|
||||
configVariablesPath,
|
||||
defaultBranch,
|
||||
refParam,
|
||||
varParam,
|
||||
fileParam,
|
||||
refNames,
|
||||
branchRefs,
|
||||
tagRefs,
|
||||
settingsLink,
|
||||
maxWarnings,
|
||||
} = el?.dataset;
|
||||
|
||||
const variableParams = JSON.parse(varParam);
|
||||
const fileParams = JSON.parse(fileParam);
|
||||
const refs = JSON.parse(refNames);
|
||||
const branches = formatRefs(JSON.parse(branchRefs), 'branch');
|
||||
const tags = formatRefs(JSON.parse(tagRefs), 'tag');
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
|
@ -27,10 +31,12 @@ export default () => {
|
|||
projectId,
|
||||
pipelinesPath,
|
||||
configVariablesPath,
|
||||
defaultBranch,
|
||||
refParam,
|
||||
variableParams,
|
||||
fileParams,
|
||||
refs,
|
||||
branches,
|
||||
tags,
|
||||
settingsLink,
|
||||
maxWarnings: Number(maxWarnings),
|
||||
},
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { BRANCH_REF_TYPE, TAG_REF_TYPE } from '../constants';
|
||||
|
||||
export default (refs, type) => {
|
||||
let fullName;
|
||||
|
||||
return refs.map(ref => {
|
||||
if (type === BRANCH_REF_TYPE) {
|
||||
fullName = `refs/heads/${ref}`;
|
||||
} else if (type === TAG_REF_TYPE) {
|
||||
fullName = `refs/tags/${ref}`;
|
||||
}
|
||||
|
||||
return {
|
||||
shortName: ref,
|
||||
fullName,
|
||||
};
|
||||
});
|
||||
};
|
|
@ -36,7 +36,8 @@ export default {
|
|||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { isEqual, get } from 'lodash';
|
||||
import { isEqual, get, isEmpty } from 'lodash';
|
||||
import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.graphql';
|
||||
import { FETCH_SETTINGS_ERROR_MESSAGE } from '../../shared/constants';
|
||||
|
||||
|
@ -60,6 +60,9 @@ export default {
|
|||
return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT;
|
||||
},
|
||||
isEdited() {
|
||||
if (isEmpty(this.containerExpirationPolicy) && isEmpty(this.workingCopy)) {
|
||||
return false;
|
||||
}
|
||||
return !isEqual(this.containerExpirationPolicy, this.workingCopy);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,21 +1,43 @@
|
|||
<script>
|
||||
import { GlCard, GlButton } from '@gitlab/ui';
|
||||
import { GlCard, GlButton, GlSprintf } from '@gitlab/ui';
|
||||
import Tracking from '~/tracking';
|
||||
import {
|
||||
UPDATE_SETTINGS_ERROR_MESSAGE,
|
||||
UPDATE_SETTINGS_SUCCESS_MESSAGE,
|
||||
} from '../../shared/constants';
|
||||
import ExpirationPolicyFields from '../../shared/components/expiration_policy_fields.vue';
|
||||
import { SET_CLEANUP_POLICY_BUTTON, CLEANUP_POLICY_CARD_HEADER } from '../constants';
|
||||
} from '~/registry/shared/constants';
|
||||
import {
|
||||
SET_CLEANUP_POLICY_BUTTON,
|
||||
KEEP_HEADER_TEXT,
|
||||
KEEP_INFO_TEXT,
|
||||
KEEP_N_LABEL,
|
||||
NAME_REGEX_KEEP_LABEL,
|
||||
NAME_REGEX_KEEP_DESCRIPTION,
|
||||
REMOVE_HEADER_TEXT,
|
||||
REMOVE_INFO_TEXT,
|
||||
EXPIRATION_SCHEDULE_LABEL,
|
||||
NAME_REGEX_LABEL,
|
||||
NAME_REGEX_PLACEHOLDER,
|
||||
NAME_REGEX_DESCRIPTION,
|
||||
CADENCE_LABEL,
|
||||
EXPIRATION_POLICY_FOOTER_NOTE,
|
||||
} from '~/registry/settings/constants';
|
||||
import { formOptionsGenerator } from '~/registry/shared/utils';
|
||||
import updateContainerExpirationPolicyMutation from '../graphql/mutations/update_container_expiration_policy.graphql';
|
||||
import { updateContainerExpirationPolicy } from '../graphql/utils/cache_update';
|
||||
import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.graphql';
|
||||
import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update';
|
||||
import ExpirationDropdown from './expiration_dropdown.vue';
|
||||
import ExpirationTextarea from './expiration_textarea.vue';
|
||||
import ExpirationToggle from './expiration_toggle.vue';
|
||||
import ExpirationRunText from './expiration_run_text.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlCard,
|
||||
GlButton,
|
||||
ExpirationPolicyFields,
|
||||
GlSprintf,
|
||||
ExpirationDropdown,
|
||||
ExpirationTextarea,
|
||||
ExpirationToggle,
|
||||
ExpirationRunText,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
inject: ['projectPath'],
|
||||
|
@ -35,22 +57,31 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
labelsConfig: {
|
||||
cols: 3,
|
||||
align: 'right',
|
||||
},
|
||||
|
||||
formOptions: formOptionsGenerator(),
|
||||
i18n: {
|
||||
CLEANUP_POLICY_CARD_HEADER,
|
||||
KEEP_HEADER_TEXT,
|
||||
KEEP_INFO_TEXT,
|
||||
KEEP_N_LABEL,
|
||||
NAME_REGEX_KEEP_LABEL,
|
||||
SET_CLEANUP_POLICY_BUTTON,
|
||||
NAME_REGEX_KEEP_DESCRIPTION,
|
||||
REMOVE_HEADER_TEXT,
|
||||
REMOVE_INFO_TEXT,
|
||||
EXPIRATION_SCHEDULE_LABEL,
|
||||
NAME_REGEX_LABEL,
|
||||
NAME_REGEX_PLACEHOLDER,
|
||||
NAME_REGEX_DESCRIPTION,
|
||||
CADENCE_LABEL,
|
||||
EXPIRATION_POLICY_FOOTER_NOTE,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tracking: {
|
||||
label: 'docker_container_retention_and_expiration_policies',
|
||||
},
|
||||
fieldsAreValid: true,
|
||||
apiErrors: null,
|
||||
apiErrors: {},
|
||||
localErrors: {},
|
||||
mutationLoading: false,
|
||||
};
|
||||
},
|
||||
|
@ -66,12 +97,18 @@ export default {
|
|||
showLoadingIcon() {
|
||||
return this.isLoading || this.mutationLoading;
|
||||
},
|
||||
fieldsAreValid() {
|
||||
return Object.values(this.localErrors).every(error => error);
|
||||
},
|
||||
isSubmitButtonDisabled() {
|
||||
return !this.fieldsAreValid || this.showLoadingIcon;
|
||||
},
|
||||
isCancelButtonDisabled() {
|
||||
return !this.isEdited || this.isLoading || this.mutationLoading;
|
||||
},
|
||||
isFieldDisabled() {
|
||||
return this.showLoadingIcon || !this.value.enabled;
|
||||
},
|
||||
mutationVariables() {
|
||||
return {
|
||||
projectPath: this.projectPath,
|
||||
|
@ -90,7 +127,8 @@ export default {
|
|||
},
|
||||
reset() {
|
||||
this.track('reset_form');
|
||||
this.apiErrors = null;
|
||||
this.apiErrors = {};
|
||||
this.localErrors = {};
|
||||
this.$emit('reset');
|
||||
},
|
||||
setApiErrors(response) {
|
||||
|
@ -101,9 +139,15 @@ export default {
|
|||
return acc;
|
||||
}, {});
|
||||
},
|
||||
setLocalErrors(state, model) {
|
||||
this.localErrors = {
|
||||
...this.localErrors,
|
||||
[model]: state,
|
||||
};
|
||||
},
|
||||
submit() {
|
||||
this.track('submit_form');
|
||||
this.apiErrors = null;
|
||||
this.apiErrors = {};
|
||||
this.mutationLoading = true;
|
||||
return this.$apollo
|
||||
.mutate({
|
||||
|
@ -129,11 +173,9 @@ export default {
|
|||
this.mutationLoading = false;
|
||||
});
|
||||
},
|
||||
onModelChange(changePayload) {
|
||||
this.$emit('input', changePayload.newValue);
|
||||
if (this.apiErrors) {
|
||||
this.apiErrors[changePayload.modified] = undefined;
|
||||
}
|
||||
onModelChange(newValue, model) {
|
||||
this.$emit('input', { ...this.value, [model]: newValue });
|
||||
this.apiErrors[model] = undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -141,42 +183,129 @@ export default {
|
|||
|
||||
<template>
|
||||
<form ref="form-element" @submit.prevent="submit" @reset.prevent="reset">
|
||||
<gl-card>
|
||||
<expiration-toggle
|
||||
:value="prefilledForm.enabled"
|
||||
:disabled="showLoadingIcon"
|
||||
class="gl-mb-0!"
|
||||
data-testid="enable-toggle"
|
||||
@input="onModelChange($event, 'enabled')"
|
||||
/>
|
||||
|
||||
<div class="gl-display-flex gl-mt-7">
|
||||
<expiration-dropdown
|
||||
v-model="prefilledForm.cadence"
|
||||
:disabled="isFieldDisabled"
|
||||
:form-options="$options.formOptions.cadence"
|
||||
:label="$options.i18n.CADENCE_LABEL"
|
||||
name="cadence"
|
||||
class="gl-mr-7 gl-mb-0!"
|
||||
data-testid="cadence-dropdown"
|
||||
@input="onModelChange($event, 'cadence')"
|
||||
/>
|
||||
<expiration-run-text :value="prefilledForm.nextRunAt" class="gl-mb-0!" />
|
||||
</div>
|
||||
<gl-card class="gl-mt-7">
|
||||
<template #header>
|
||||
{{ $options.i18n.CLEANUP_POLICY_CARD_HEADER }}
|
||||
{{ $options.i18n.KEEP_HEADER_TEXT }}
|
||||
</template>
|
||||
<template #default>
|
||||
<expiration-policy-fields
|
||||
:value="prefilledForm"
|
||||
:form-options="$options.formOptions"
|
||||
:is-loading="isLoading"
|
||||
:api-errors="apiErrors"
|
||||
@validated="fieldsAreValid = true"
|
||||
@invalidated="fieldsAreValid = false"
|
||||
@input="onModelChange"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<gl-button
|
||||
ref="cancel-button"
|
||||
type="reset"
|
||||
class="gl-mr-3 gl-display-block float-right"
|
||||
:disabled="isCancelButtonDisabled"
|
||||
>
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
ref="save-button"
|
||||
type="submit"
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
:loading="showLoadingIcon"
|
||||
variant="success"
|
||||
category="primary"
|
||||
class="js-no-auto-disable"
|
||||
>
|
||||
{{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }}
|
||||
</gl-button>
|
||||
<div>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.KEEP_INFO_TEXT">
|
||||
<template #strong="{content}">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
<template #secondStrong="{content}">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<expiration-dropdown
|
||||
v-model="prefilledForm.keepN"
|
||||
:disabled="isFieldDisabled"
|
||||
:form-options="$options.formOptions.keepN"
|
||||
:label="$options.i18n.KEEP_N_LABEL"
|
||||
name="keep-n"
|
||||
data-testid="keep-n-dropdown"
|
||||
@input="onModelChange($event, 'keepN')"
|
||||
/>
|
||||
<expiration-textarea
|
||||
v-model="prefilledForm.nameRegexKeep"
|
||||
:error="apiErrors.nameRegexKeep"
|
||||
:disabled="isFieldDisabled"
|
||||
:label="$options.i18n.NAME_REGEX_KEEP_LABEL"
|
||||
:description="$options.i18n.NAME_REGEX_KEEP_DESCRIPTION"
|
||||
name="keep-regex"
|
||||
data-testid="keep-regex-textarea"
|
||||
@input="onModelChange($event, 'nameRegexKeep')"
|
||||
@validation="setLocalErrors($event, 'nameRegexKeep')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</gl-card>
|
||||
<gl-card class="gl-mt-7">
|
||||
<template #header>
|
||||
{{ $options.i18n.REMOVE_HEADER_TEXT }}
|
||||
</template>
|
||||
<template #default>
|
||||
<div>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.REMOVE_INFO_TEXT">
|
||||
<template #strong="{content}">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
<template #secondStrong="{content}">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<expiration-dropdown
|
||||
v-model="prefilledForm.olderThan"
|
||||
:disabled="isFieldDisabled"
|
||||
:form-options="$options.formOptions.olderThan"
|
||||
:label="$options.i18n.EXPIRATION_SCHEDULE_LABEL"
|
||||
name="older-than"
|
||||
data-testid="older-than-dropdown"
|
||||
@input="onModelChange($event, 'olderThan')"
|
||||
/>
|
||||
<expiration-textarea
|
||||
v-model="prefilledForm.nameRegex"
|
||||
:error="apiErrors.nameRegex"
|
||||
:disabled="isFieldDisabled"
|
||||
:label="$options.i18n.NAME_REGEX_LABEL"
|
||||
:placeholder="$options.i18n.NAME_REGEX_PLACEHOLDER"
|
||||
:description="$options.i18n.NAME_REGEX_DESCRIPTION"
|
||||
name="remove-regex"
|
||||
data-testid="remove-regex-textarea"
|
||||
@input="onModelChange($event, 'nameRegex')"
|
||||
@validation="setLocalErrors($event, 'nameRegex')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</gl-card>
|
||||
<div class="gl-mt-7 gl-display-flex gl-align-items-center">
|
||||
<gl-button
|
||||
data-testid="save-button"
|
||||
type="submit"
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
:loading="showLoadingIcon"
|
||||
variant="success"
|
||||
category="primary"
|
||||
class="js-no-auto-disable gl-mr-4"
|
||||
>
|
||||
{{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
data-testid="cancel-button"
|
||||
type="reset"
|
||||
:disabled="isCancelButtonDisabled"
|
||||
class="gl-mr-4"
|
||||
>
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
<span class="gl-font-style-italic gl-text-gray-400">{{
|
||||
$options.i18n.EXPIRATION_POLICY_FOOTER_NOTE
|
||||
}}</span>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { s__, __ } from '~/locale';
|
||||
|
||||
export const SET_CLEANUP_POLICY_BUTTON = __('Save');
|
||||
export const CLEANUP_POLICY_CARD_HEADER = s__('ContainerRegistry|Tag expiration policy');
|
||||
export const UNAVAILABLE_FEATURE_TITLE = s__(
|
||||
`ContainerRegistry|Cleanup policy for tags is disabled`,
|
||||
);
|
||||
|
@ -19,34 +18,33 @@ export const TEXT_AREA_INVALID_FEEDBACK = s__(
|
|||
|
||||
export const KEEP_HEADER_TEXT = s__('ContainerRegistry|Keep these tags');
|
||||
export const KEEP_INFO_TEXT = s__(
|
||||
'ContainerRegistry|Tags that match these rules will always be %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag will always be kept.',
|
||||
'ContainerRegistry|Tags that match these rules are %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag is always kept.',
|
||||
);
|
||||
export const KEEP_N_LABEL = s__('ContainerRegistry|Keep the most recent:');
|
||||
export const NAME_REGEX_KEEP_LABEL = s__('ContainerRegistry|Keep tags matching:');
|
||||
export const NAME_REGEX_KEEP_PLACEHOLDER = 'production-v.*';
|
||||
export const NAME_REGEX_KEEP_DESCRIPTION = s__(
|
||||
'ContainerRegistry|Tags with names matching this regex pattern will be kept. %{linkStart}More information%{linkEnd}',
|
||||
'ContainerRegistry|Tags with names that match this regex pattern are kept. %{linkStart}More information%{linkEnd}',
|
||||
);
|
||||
|
||||
export const REMOVE_HEADER_TEXT = s__('ContainerRegistry|Remove these tags');
|
||||
export const REMOVE_INFO_TEXT = s__(
|
||||
'ContainerRegistry|Tags that match these rules will be %{strongStart}removed%{strongEnd}, unless kept by a rule above.',
|
||||
'ContainerRegistry|Tags that match these rules are %{strongStart}removed%{strongEnd}, unless a rule above says to keep them.',
|
||||
);
|
||||
export const EXPIRATION_SCHEDULE_LABEL = s__('ContainerRegistry|Remove tags older than:');
|
||||
export const NAME_REGEX_LABEL = s__('ContainerRegistry|Remove tags matching:');
|
||||
export const NAME_REGEX_PLACEHOLDER = '.*';
|
||||
export const NAME_REGEX_DESCRIPTION = s__(
|
||||
'ContainerRegistry|Tags with names matching this regex pattern will be removed. %{linkStart}More information%{linkEnd}',
|
||||
'ContainerRegistry|Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}',
|
||||
);
|
||||
|
||||
export const ENABLED_TEXT = __('Enabled');
|
||||
export const DISABLED_TEXT = __('Disabled');
|
||||
|
||||
export const ENABLE_TOGGLE_DESCRIPTION = s__(
|
||||
'ContainerRegistry|%{toggleStatus} - Tags matching the rules defined below will be automatically scheduled for deletion.',
|
||||
'ContainerRegistry|%{toggleStatus} - Tags that match the rules on this page are automatically scheduled for deletion.',
|
||||
);
|
||||
|
||||
export const CADENCE_LABEL = s__('ContainerRegistry|Run cleanup every:');
|
||||
export const CADENCE_LABEL = s__('ContainerRegistry|Run cleanup:');
|
||||
|
||||
export const NEXT_CLEANUP_LABEL = s__('ContainerRegistry|Next cleanup scheduled to run on:');
|
||||
export const NOT_SCHEDULED_POLICY_TEXT = s__('ContainerRegistry|Not yet scheduled');
|
||||
|
|
|
@ -21,12 +21,7 @@ export const mapComputedToEvent = (list, root) => {
|
|||
return result;
|
||||
};
|
||||
|
||||
export const olderThanTranslationGenerator = variable =>
|
||||
n__(
|
||||
'%d day until tags are automatically removed',
|
||||
'%d days until tags are automatically removed',
|
||||
variable,
|
||||
);
|
||||
export const olderThanTranslationGenerator = variable => n__('%d day', '%d days', variable);
|
||||
|
||||
export const keepNTranslationGenerator = variable =>
|
||||
n__('%d tag per image name', '%d tags per image name', variable);
|
||||
|
|
|
@ -15,7 +15,7 @@ module SnippetsActions
|
|||
skip_before_action :verify_authenticity_token,
|
||||
if: -> { action_name == 'show' && js_request? }
|
||||
|
||||
track_redis_hll_event :show, name: 'i_snippets_show', feature: :usage_data_i_snippets_show, feature_default_enabled: false
|
||||
track_redis_hll_event :show, name: 'i_snippets_show', feature: :usage_data_i_snippets_show, feature_default_enabled: true
|
||||
|
||||
respond_to :html
|
||||
end
|
||||
|
|
|
@ -88,7 +88,7 @@ module HasRepository
|
|||
|
||||
group_branch_default_name = group&.default_branch_name if respond_to?(:group)
|
||||
|
||||
group_branch_default_name || Gitlab::CurrentSettings.default_branch_name
|
||||
(group_branch_default_name || Gitlab::CurrentSettings.default_branch_name).presence
|
||||
end
|
||||
|
||||
def reload_default_branch
|
||||
|
|
|
@ -17,7 +17,7 @@ class CustomEmoji < ApplicationRecord
|
|||
uniqueness: { scope: [:namespace_id, :name] },
|
||||
presence: true,
|
||||
length: { maximum: 36 },
|
||||
format: { with: /\A([a-z0-9]+[-_]?)+[a-z0-9]+\z/ }
|
||||
format: { with: /\A[a-z0-9][a-z0-9\-_]*[a-z0-9]\z/ }
|
||||
|
||||
private
|
||||
|
||||
|
|
|
@ -3,12 +3,6 @@
|
|||
module Terraform
|
||||
class State < ApplicationRecord
|
||||
include UsageStatistics
|
||||
include IgnorableColumns
|
||||
# These columns are being removed since geo replication falls to the versioned state
|
||||
# Tracking in https://gitlab.com/gitlab-org/gitlab/-/issues/258262
|
||||
ignore_columns %i[verification_failure verification_retry_at verified_at verification_retry_count verification_checksum],
|
||||
remove_with: '13.7',
|
||||
remove_after: '2020-12-22'
|
||||
|
||||
HEX_REGEXP = %r{\A\h+\z}.freeze
|
||||
UUID_LENGTH = 32
|
||||
|
|
|
@ -10,10 +10,12 @@
|
|||
#js-new-pipeline{ data: { project_id: @project.id,
|
||||
pipelines_path: project_pipelines_path(@project),
|
||||
config_variables_path: config_variables_namespace_project_pipelines_path(@project.namespace, @project),
|
||||
default_branch: @project.default_branch,
|
||||
ref_param: params[:ref] || @project.default_branch,
|
||||
var_param: params[:var].to_json,
|
||||
file_param: params[:file_var].to_json,
|
||||
ref_names: @project.repository.ref_names.to_json.html_safe,
|
||||
branch_refs: @project.repository.branch_names.to_json.html_safe,
|
||||
tag_refs: @project.repository.tag_names.to_json.html_safe,
|
||||
settings_link: project_settings_ci_cd_path(@project),
|
||||
max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } }
|
||||
|
||||
|
|
|
@ -66,11 +66,11 @@
|
|||
%section.settings.no-animate#js-registry-policies{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
= _("Cleanup policy for tags")
|
||||
= _("Clean up image tags")
|
||||
%button.btn.js-settings-toggle{ type: 'button' }
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _("Save space and find tags in the Container Registry more easily. Enable the cleanup policy to remove stale tags and keep only the ones you need.")
|
||||
= _("Save space and find images in the Container Registry. Remove unneeded tags and keep only the ones you want.")
|
||||
= link_to _('More information'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy', target: '_blank', rel: 'noopener noreferrer')
|
||||
.settings-content
|
||||
= render 'projects/registry/settings/index'
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update Design of the Container Registry Cleanup Policy for tags
|
||||
merge_request: 48243
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add MAU counter for snippet show action
|
||||
merge_request: 48477
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix regular expression backtracking issue in custom emoji name validation
|
||||
merge_request:
|
||||
author:
|
||||
type: security
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added epic boards and epic board labels tables
|
||||
merge_request: 48658
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Toggle File-By-File setting from the MR settings dropdown
|
||||
merge_request: 47726
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Ensure default_branch from settings is not blank
|
||||
merge_request: 49018
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add uuid column into security_findings table
|
||||
merge_request: 48968
|
||||
author: Harrison Brock @harrisonbrock
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Finish removing unused replication columns from terraform state
|
||||
merge_request: 48839
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Manually trigger pipelines correctly when branches and tags have the same name. Separate tags and branches in trigger pipeline form.
|
||||
merge_request: 48142
|
||||
author:
|
||||
type: fixed
|
|
@ -5,4 +5,4 @@ rollout_issue_url:
|
|||
milestone: '13.7'
|
||||
type: development
|
||||
group: group::editor
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddEpicBoards < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
create_table :boards_epic_boards do |t|
|
||||
t.boolean :hide_backlog_list, default: false, null: false
|
||||
t.boolean :hide_closed_list, default: false, null: false
|
||||
t.references :group, index: true, foreign_key: { to_table: :namespaces, on_delete: :cascade }, null: false
|
||||
t.timestamps_with_timezone
|
||||
t.text :name, default: 'Development', null: false
|
||||
end
|
||||
end
|
||||
|
||||
add_text_limit :boards_epic_boards, :name, 255
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
drop_table :boards_epic_boards
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddEpicBoardLabels < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
create_table :boards_epic_board_labels do |t|
|
||||
t.references :epic_board, index: true, foreign_key: { to_table: :boards_epic_boards, on_delete: :cascade }, null: false
|
||||
t.references :label, index: true, foreign_key: { on_delete: :cascade }, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
drop_table :boards_epic_board_labels
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
#
|
||||
class AddColumnToSecurityFindings < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :security_findings, :uuid, :uuid
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToSecurityFindingsUuid < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
INDEX_NAME = 'index_security_findings_on_uuid'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :security_findings, :uuid, name: INDEX_NAME, unique: true
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :security_findings, INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
a68c609800f5bdb0a77e39f706b410477493e7b7db3af11e4b2a67534df31079
|
|
@ -0,0 +1 @@
|
|||
65935afe9b4ad195aaf31cddb915dcd62b23674e278e93ce7ff9b4ae98e32331
|
|
@ -0,0 +1 @@
|
|||
3b0e685327e2199e0a6721e00d1fa3c9fee3a173ce1cf5ddd99df3349a28fea9
|
|
@ -0,0 +1 @@
|
|||
d0706f4a60ae6f26be206aee80fdeb4a7e5c4c0b99e518140ae3cb8c47ed7a82
|
|
@ -9849,6 +9849,41 @@ CREATE TABLE boards (
|
|||
hide_closed_list boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE boards_epic_board_labels (
|
||||
id bigint NOT NULL,
|
||||
epic_board_id bigint NOT NULL,
|
||||
label_id bigint NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE boards_epic_board_labels_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE boards_epic_board_labels_id_seq OWNED BY boards_epic_board_labels.id;
|
||||
|
||||
CREATE TABLE boards_epic_boards (
|
||||
id bigint NOT NULL,
|
||||
hide_backlog_list boolean DEFAULT false NOT NULL,
|
||||
hide_closed_list boolean DEFAULT false NOT NULL,
|
||||
group_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
name text DEFAULT 'Development'::text NOT NULL,
|
||||
CONSTRAINT check_bcbbffe601 CHECK ((char_length(name) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE boards_epic_boards_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE boards_epic_boards_id_seq OWNED BY boards_epic_boards.id;
|
||||
|
||||
CREATE TABLE boards_epic_user_preferences (
|
||||
id bigint NOT NULL,
|
||||
board_id bigint NOT NULL,
|
||||
|
@ -16202,6 +16237,7 @@ CREATE TABLE security_findings (
|
|||
project_fingerprint text NOT NULL,
|
||||
deduplicated boolean DEFAULT false NOT NULL,
|
||||
"position" integer,
|
||||
uuid uuid,
|
||||
CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40))
|
||||
);
|
||||
|
||||
|
@ -17878,6 +17914,10 @@ ALTER TABLE ONLY board_user_preferences ALTER COLUMN id SET DEFAULT nextval('boa
|
|||
|
||||
ALTER TABLE ONLY boards ALTER COLUMN id SET DEFAULT nextval('boards_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY boards_epic_board_labels ALTER COLUMN id SET DEFAULT nextval('boards_epic_board_labels_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY boards_epic_boards ALTER COLUMN id SET DEFAULT nextval('boards_epic_boards_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY boards_epic_user_preferences ALTER COLUMN id SET DEFAULT nextval('boards_epic_user_preferences_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY broadcast_messages ALTER COLUMN id SET DEFAULT nextval('broadcast_messages_id_seq'::regclass);
|
||||
|
@ -18906,6 +18946,12 @@ ALTER TABLE ONLY board_project_recent_visits
|
|||
ALTER TABLE ONLY board_user_preferences
|
||||
ADD CONSTRAINT board_user_preferences_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY boards_epic_board_labels
|
||||
ADD CONSTRAINT boards_epic_board_labels_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY boards_epic_boards
|
||||
ADD CONSTRAINT boards_epic_boards_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY boards_epic_user_preferences
|
||||
ADD CONSTRAINT boards_epic_user_preferences_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -20525,6 +20571,12 @@ CREATE INDEX index_board_user_preferences_on_user_id ON board_user_preferences U
|
|||
|
||||
CREATE UNIQUE INDEX index_board_user_preferences_on_user_id_and_board_id ON board_user_preferences USING btree (user_id, board_id);
|
||||
|
||||
CREATE INDEX index_boards_epic_board_labels_on_epic_board_id ON boards_epic_board_labels USING btree (epic_board_id);
|
||||
|
||||
CREATE INDEX index_boards_epic_board_labels_on_label_id ON boards_epic_board_labels USING btree (label_id);
|
||||
|
||||
CREATE INDEX index_boards_epic_boards_on_group_id ON boards_epic_boards USING btree (group_id);
|
||||
|
||||
CREATE INDEX index_boards_epic_user_preferences_on_board_id ON boards_epic_user_preferences USING btree (board_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_boards_epic_user_preferences_on_board_user_epic_unique ON boards_epic_user_preferences USING btree (board_id, user_id, epic_id);
|
||||
|
@ -22213,6 +22265,8 @@ CREATE INDEX index_security_findings_on_scanner_id ON security_findings USING bt
|
|||
|
||||
CREATE INDEX index_security_findings_on_severity ON security_findings USING btree (severity);
|
||||
|
||||
CREATE UNIQUE INDEX index_security_findings_on_uuid ON security_findings USING btree (uuid);
|
||||
|
||||
CREATE INDEX index_self_managed_prometheus_alert_events_on_environment_id ON self_managed_prometheus_alert_events USING btree (environment_id);
|
||||
|
||||
CREATE INDEX index_sent_notifications_on_noteable_type_noteable_id ON sent_notifications USING btree (noteable_id) WHERE ((noteable_type)::text = 'Issue'::text);
|
||||
|
@ -23844,6 +23898,9 @@ ALTER TABLE ONLY group_group_links
|
|||
ALTER TABLE ONLY geo_repository_updated_events
|
||||
ADD CONSTRAINT fk_rails_2b70854c08 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY boards_epic_board_labels
|
||||
ADD CONSTRAINT fk_rails_2bedeb8799 FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY protected_branch_unprotect_access_levels
|
||||
ADD CONSTRAINT fk_rails_2d2aba21ef FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -24249,6 +24306,9 @@ ALTER TABLE ONLY slack_integrations
|
|||
ALTER TABLE ONLY custom_emoji
|
||||
ADD CONSTRAINT fk_rails_745925b412 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY boards_epic_board_labels
|
||||
ADD CONSTRAINT fk_rails_7471128a8e FOREIGN KEY (epic_board_id) REFERENCES boards_epic_boards(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY dast_site_profiles
|
||||
ADD CONSTRAINT fk_rails_747dc64abc FOREIGN KEY (dast_site_id) REFERENCES dast_sites(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -24333,6 +24393,9 @@ ALTER TABLE ONLY clusters_applications_crossplane
|
|||
ALTER TABLE ONLY packages_package_file_build_infos
|
||||
ADD CONSTRAINT fk_rails_871ca3ae21 FOREIGN KEY (package_file_id) REFERENCES packages_package_files(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY boards_epic_boards
|
||||
ADD CONSTRAINT fk_rails_874c573878 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ci_runner_namespaces
|
||||
ADD CONSTRAINT fk_rails_8767676b7a FOREIGN KEY (runner_id) REFERENCES ci_runners(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ The following documentation relates to the DevOps **Plan** stage:
|
|||
|
||||
| Plan topics | Description |
|
||||
|:-----------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [Burndown charts](user/project/milestones/burndown_charts.md) **(STARTER)** | Watch your project's progress throughout a specific milestone. |
|
||||
| [Burndown charts](user/project/milestones/burndown_and_burnup_charts.md) **(STARTER)** | Watch your project's progress throughout a specific milestone. |
|
||||
| [Discussions](user/discussions/index.md) | Threads, comments, and resolvable threads in issues, commits, and merge requests. |
|
||||
| [Due dates](user/project/issues/due_dates.md) | Keep track of issue deadlines. |
|
||||
| [Epics](user/group/epics/index.md) **(ULTIMATE)** | Tracking groups of issues that share a theme. |
|
||||
|
|
|
@ -65,7 +65,7 @@ GET /users?active=true
|
|||
GET /users?blocked=true
|
||||
```
|
||||
|
||||
GitLab supports bot users such as the [alert bot](../operations/incident_management/generic_alerts.md)
|
||||
GitLab supports bot users such as the [alert bot](../operations/incident_management/alert_integrations.md)
|
||||
or the [support bot](../user/project/service_desk.md#support-bot-user).
|
||||
To exclude these users from the users' list, you can use the parameter `exclude_internal=true`
|
||||
([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241144) in GitLab 13.4).
|
||||
|
|
|
@ -259,7 +259,7 @@ To access environment variables, use the syntax for your runner's [shell](https:
|
|||
|----------------------|------------------------------------------|
|
||||
| bash/sh | `$variable` |
|
||||
| PowerShell | `$env:variable` (primary) or `$variable` |
|
||||
| Windows Batch | `%variable%` |
|
||||
| Windows Batch | `%variable%`, or `!variable!` for [delayed expansion](https://ss64.com/nt/delayedexpansion.html), which can be used for variables that contain white spaces or newlines. |
|
||||
|
||||
### Bash
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ from:
|
|||
- [Shell commands](shell_commands.md) in the GitLab codebase
|
||||
- [`Gemfile` guidelines](gemfile.md)
|
||||
- [Pry debugging](pry_debugging.md)
|
||||
- [Sidekiq debugging](sidekiq_debugging.md)
|
||||
- [Sidekiq debugging](../administration/troubleshooting/sidekiq.md)
|
||||
- [Accessing session data](session.md)
|
||||
- [Gotchas](gotchas.md) to avoid
|
||||
- [Avoid modules with instance variables](module_with_instance_variables.md) if possible
|
||||
|
|
|
@ -39,6 +39,9 @@ go get github.com/Arkweid/lefthook
|
|||
## Or with Rubygems
|
||||
gem install lefthook
|
||||
|
||||
### You may need to run the following if you're using rbenv
|
||||
rbenv rehash
|
||||
|
||||
# 3. Install the Git hooks
|
||||
lefthook install -f
|
||||
```
|
||||
|
|
|
@ -11,7 +11,7 @@ blocks of Ruby code. Method instrumentation is the primary form of
|
|||
instrumentation with block-based instrumentation only being used when we want to
|
||||
drill down to specific regions of code within a method.
|
||||
|
||||
Please refer to [Product Analytics](product_analytics/index.md) if you are tracking product usage patterns.
|
||||
Please refer to [Product Analytics](https://about.gitlab.com/handbook/product/product-analytics-guide/) if you are tracking product usage patterns.
|
||||
|
||||
## Instrumenting Methods
|
||||
|
||||
|
|
|
@ -50,6 +50,22 @@ bundle exec guard
|
|||
|
||||
When using spring and guard together, use `SPRING=1 bundle exec guard` instead to make use of spring.
|
||||
|
||||
### Ruby warnings
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47767) in GitLab 13.7.
|
||||
|
||||
We've enabled [deprecation warnings](https://ruby-doc.org/core-2.7.2/Warning.html)
|
||||
by default when running specs. Making these warnings more visible to developers
|
||||
helps upgrading to newer Ruby versions.
|
||||
|
||||
You can silence deprecation warnings by setting the environment variable
|
||||
`SILENCE_DEPRECATIONS`, for example:
|
||||
|
||||
```shell
|
||||
# silence all deprecation warnings
|
||||
SILENCE_DEPRECATIONS=1 bin/rspec spec/models/project_spec.rb
|
||||
```
|
||||
|
||||
### Test speed
|
||||
|
||||
GitLab has a massive test suite that, without [parallelization](ci.md#test-suite-parallelization-on-the-ci), can take hours
|
||||
|
|
|
@ -181,7 +181,7 @@ the GitLab alerts list is scheduled for deprecation following the 13.7 release o
|
|||
You can monitor alerts using a GitLab integration with [Opsgenie](https://www.atlassian.com/software/opsgenie).
|
||||
|
||||
If you enable the Opsgenie integration, you can't have other GitLab alert
|
||||
services, such as [Generic Alerts](generic_alerts.md) or Prometheus alerts,
|
||||
services, such as [Generic Alerts](alert_integrations.md) or Prometheus alerts,
|
||||
active at the same time.
|
||||
|
||||
To enable Opsgenie integration:
|
||||
|
|
|
@ -80,7 +80,7 @@ Prometheus. The value of this should match the name of your environment in GitLa
|
|||
|
||||
In GitLab versions 13.1 and greater, you can configure your manually configured
|
||||
Prometheus server to use the
|
||||
[Generic alerts integration](../incident_management/generic_alerts.md).
|
||||
[Generic alerts integration](../incident_management/alert_integrations.md).
|
||||
|
||||
## Trigger actions from alerts **(ULTIMATE)**
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ With GitLab Enterprise Edition, you can also:
|
|||
- [Multiple Assignees for Issues](project/issues/multiple_assignees_for_issues.md). **(STARTER)**
|
||||
- [Multiple Issue Boards](project/issue_board.md#multiple-issue-boards).
|
||||
- Create formal relationships between issues with [Related Issues](project/issues/related_issues.md).
|
||||
- Use [Burndown Charts](project/milestones/burndown_charts.md) to track progress during a sprint or while working on a new version of their software.
|
||||
- Use [Burndown Charts](project/milestones/burndown_and_burnup_charts.md) to track progress during a sprint or while working on a new version of their software.
|
||||
- Leverage [Elasticsearch](../integration/elasticsearch.md) with [Advanced Search](search/advanced_global_search.md) and [Advanced Search Syntax](search/advanced_search_syntax.md) for faster, more advanced code search across your entire GitLab instance.
|
||||
- [Authenticate users with Kerberos](../integration/kerberos.md).
|
||||
- [Mirror a repository](project/repository/repository_mirroring.md) from elsewhere on your local server.
|
||||
|
|
|
@ -513,24 +513,24 @@ You can create a cleanup policy in [the API](#use-the-cleanup-policy-api) or the
|
|||
To create a cleanup policy in the UI:
|
||||
|
||||
1. For your project, go to **Settings > CI/CD**.
|
||||
1. Expand the **Cleanup policy for tags** section.
|
||||
1. Expand the **Clean up image tags** section.
|
||||
1. Complete the fields.
|
||||
|
||||
| Field | Description |
|
||||
|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
|
||||
| **Cleanup policy** | Turn the policy on or off. |
|
||||
| **Expiration interval** | How long tags are exempt from being deleted. |
|
||||
| **Expiration schedule** | How often the policy should run. |
|
||||
| **Number of tags to retain** | How many tags to _always_ keep for each image. |
|
||||
| **Tags with names matching this regex pattern expire:** | The regex pattern that determines which tags to remove. This value cannot be blank. For all tags, use `.*`. See other [regex pattern examples](#regex-pattern-examples). |
|
||||
| **Tags with names matching this regex pattern are preserved:** | The regex pattern that determines which tags to preserve. The `latest` tag is always preserved. For all tags, use `.*`. See other [regex pattern examples](#regex-pattern-examples). |
|
||||
| **Toggle** | Turn the policy on or off. |
|
||||
| **Run cleanup** | How often the policy should run. |
|
||||
| **Keep the most recent** | How many tags to _always_ keep for each image. |
|
||||
| **Keep tags matching** | The regex pattern that determines which tags to preserve. The `latest` tag is always preserved. For all tags, use `.*`. See other [regex pattern examples](#regex-pattern-examples). |
|
||||
| **Remove tags older than** | Remove only tags older than X days. |
|
||||
| **Remove tags matching** | The regex pattern that determines which tags to remove. This value cannot be blank. For all tags, use `.*`. See other [regex pattern examples](#regex-pattern-examples). |
|
||||
|
||||
1. Click **Set cleanup policy**.
|
||||
1. Click **Save**.
|
||||
|
||||
Depending on the interval you chose, the policy is scheduled to run.
|
||||
|
||||
NOTE: **Note:**
|
||||
If you edit the policy and click **Set cleanup policy** again, the interval is reset.
|
||||
If you edit the policy and click **Save** again, the interval is reset.
|
||||
|
||||
### Regex pattern examples
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ Click on the service links to see further configuration instructions and details
|
|||
| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients | No |
|
||||
| External Wiki | Replaces the link to the internal wiki with a link to an external wiki | No |
|
||||
| Flowdock | Flowdock is a collaboration web app for technical teams | No |
|
||||
| [Generic alerts](../../../operations/incident_management/generic_alerts.md) **(ULTIMATE)** | Receive alerts on GitLab from any source | No |
|
||||
| [Generic alerts](../../../operations/incident_management/alert_integrations.md) **(ULTIMATE)** | Receive alerts on GitLab from any source | No |
|
||||
| [GitHub](github.md) **(PREMIUM)** | Sends pipeline notifications to GitHub | No |
|
||||
| [Hangouts Chat](hangouts_chat.md) | Receive events notifications in Google Hangouts Chat | No |
|
||||
| [HipChat](hipchat.md) | Private group chat and IM | No |
|
||||
|
|
|
@ -92,6 +92,17 @@ From there, when reviewing merge requests' **Changes** tab, you will see only on
|
|||
|
||||
![File-by-file diff navigation](img/file_by_file_v13_2.png)
|
||||
|
||||
From [GitLab 13.7](https://gitlab.com/gitlab-org/gitlab/-/issues/233898) onwards, if you want to change
|
||||
this behavior, you can do so from your **User preferences** (as explained above) or directly in a
|
||||
merge request:
|
||||
|
||||
1. Go to the merge request's **Changes** tab.
|
||||
1. Click the cog icon (**{settings}**) to reveal the merge request's settings dropdown.
|
||||
1. Select or unselect the checkbox **Show one file at a time** to change the setting accordingly.
|
||||
|
||||
This change overrides the choice you made in your user preferences and persists until you clear your
|
||||
browser's cookies or change this behavior again.
|
||||
|
||||
#### Enable or disable file-by-file diff navigation **(CORE ONLY)**
|
||||
|
||||
File-by-file diff navigation is under development but ready for production use. It is
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
redirect_to: './burndown_and_burnup_charts.md'
|
||||
redirect_to: 'burndown_and_burnup_charts.md'
|
||||
---
|
||||
|
||||
This document was moved to [another location](burndown_and_burnup_charts.md).
|
||||
|
|
|
@ -148,13 +148,13 @@ There are also tabs below these that show the following:
|
|||
|
||||
### Project Burndown Charts **(STARTER)**
|
||||
|
||||
For project milestones in [GitLab Starter](https://about.gitlab.com/pricing/), a [burndown chart](burndown_charts.md) is in the milestone view, showing the progress of completing a milestone.
|
||||
For project milestones in [GitLab Starter](https://about.gitlab.com/pricing/), a [burndown chart](burndown_and_burnup_charts.md) is in the milestone view, showing the progress of completing a milestone.
|
||||
|
||||
![burndown chart](img/burndown_chart_v13_6.png)
|
||||
|
||||
### Group Burndown Charts **(STARTER)**
|
||||
|
||||
For group milestones in [GitLab Starter](https://about.gitlab.com/pricing/), a [burndown chart](burndown_charts.md) is in the milestone view, showing the progress of completing a milestone.
|
||||
For group milestones in [GitLab Starter](https://about.gitlab.com/pricing/), a [burndown chart](burndown_and_burnup_charts.md) is in the milestone view, showing the progress of completing a milestone.
|
||||
|
||||
### Milestone sidebar
|
||||
|
||||
|
|
|
@ -80,6 +80,9 @@ module Gitlab
|
|||
},
|
||||
jobs_empty_state: {
|
||||
tracking_category: 'Growth::Activation::Experiment::JobsEmptyState'
|
||||
},
|
||||
remove_known_trial_form_fields: {
|
||||
tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFields'
|
||||
}
|
||||
}.freeze
|
||||
|
||||
|
|
|
@ -170,11 +170,6 @@ msgid_plural "%d days"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d day until tags are automatically removed"
|
||||
msgid_plural "%d days until tags are automatically removed"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d error"
|
||||
msgid_plural "%d errors"
|
||||
msgstr[0] ""
|
||||
|
@ -5571,7 +5566,7 @@ msgstr ""
|
|||
msgid "ClassificationLabelUnavailable|is unavailable: %{reason}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cleanup policy for tags"
|
||||
msgid "Clean up image tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cleanup policy maximum processing time (seconds)"
|
||||
|
@ -7266,7 +7261,7 @@ msgstr ""
|
|||
msgid "ContainerRegistry|%{toggleStatus} - Tags matching the patterns defined below will be scheduled for deletion"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|%{toggleStatus} - Tags matching the rules defined below will be automatically scheduled for deletion."
|
||||
msgid "ContainerRegistry|%{toggleStatus} - Tags that match the rules on this page are automatically scheduled for deletion."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Build an image"
|
||||
|
@ -7403,7 +7398,7 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Remove these tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Run cleanup every:"
|
||||
msgid "ContainerRegistry|Run cleanup:"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Some tags were not deleted"
|
||||
|
@ -7436,19 +7431,16 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Sorry, your filter produced no results."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Tag expiration policy"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Tag successfully marked for deletion."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Tags successfully marked for deletion."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Tags that match these rules will always be %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag will always be kept."
|
||||
msgid "ContainerRegistry|Tags that match these rules are %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag is always kept."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Tags that match these rules will be %{strongStart}removed%{strongEnd}, unless kept by a rule above."
|
||||
msgid "ContainerRegistry|Tags that match these rules are %{strongStart}removed%{strongEnd}, unless a rule above says to keep them."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
|
||||
|
@ -7457,10 +7449,10 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Tags with names matching this regex pattern will be kept. %{linkStart}More information%{linkEnd}"
|
||||
msgid "ContainerRegistry|Tags with names that match this regex pattern are kept. %{linkStart}More information%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Tags with names matching this regex pattern will be removed. %{linkStart}More information%{linkEnd}"
|
||||
msgid "ContainerRegistry|Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}"
|
||||
|
@ -23757,7 +23749,7 @@ msgstr ""
|
|||
msgid "Save pipeline schedule"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save space and find tags in the Container Registry more easily. Enable the cleanup policy to remove stale tags and keep only the ones you need."
|
||||
msgid "Save space and find images in the Container Registry. Remove unneeded tags and keep only the ones you want."
|
||||
msgstr ""
|
||||
|
||||
msgid "Saved scan settings and target site settings which are reusable."
|
||||
|
@ -23919,6 +23911,9 @@ msgstr ""
|
|||
msgid "Search projects..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Search refs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search requirements"
|
||||
msgstr ""
|
||||
|
||||
|
@ -25104,6 +25099,9 @@ msgstr ""
|
|||
msgid "Show me the basics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show one file at a time"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show only direct members"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -26,20 +26,20 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
|||
subject
|
||||
|
||||
settings_block = find('#js-registry-policies')
|
||||
expect(settings_block).to have_text 'Cleanup policy for tags'
|
||||
expect(settings_block).to have_text 'Clean up image tags'
|
||||
end
|
||||
|
||||
it 'saves cleanup policy submit the form' do
|
||||
subject
|
||||
|
||||
within '#js-registry-policies' do
|
||||
within '.gl-card-body' do
|
||||
select('7 days until tags are automatically removed', from: 'Expiration interval:')
|
||||
select('Every day', from: 'Expiration schedule:')
|
||||
select('50 tags per image name', from: 'Number of tags to retain:')
|
||||
fill_in('Tags with names matching this regex pattern will expire:', with: '.*-production')
|
||||
end
|
||||
submit_button = find('.gl-card-footer .btn.btn-success')
|
||||
select('Every day', from: 'Run cleanup')
|
||||
select('50 tags per image name', from: 'Keep the most recent:')
|
||||
fill_in('Keep tags matching:', with: 'stable')
|
||||
select('7 days', from: 'Remove tags older than:')
|
||||
fill_in('Remove tags matching:', with: '.*-production')
|
||||
|
||||
submit_button = find('.btn.btn-success')
|
||||
expect(submit_button).not_to be_disabled
|
||||
submit_button.click
|
||||
end
|
||||
|
@ -51,10 +51,9 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
|||
subject
|
||||
|
||||
within '#js-registry-policies' do
|
||||
within '.gl-card-body' do
|
||||
fill_in('Tags with names matching this regex pattern will expire:', with: '*-production')
|
||||
end
|
||||
submit_button = find('.gl-card-footer .btn.btn-success')
|
||||
fill_in('Remove tags matching:', with: '*-production')
|
||||
|
||||
submit_button = find('.btn.btn-success')
|
||||
expect(submit_button).not_to be_disabled
|
||||
submit_button.click
|
||||
end
|
||||
|
@ -85,7 +84,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
|||
within '#js-registry-policies' do
|
||||
case result
|
||||
when :available_section
|
||||
expect(find('.gl-card-header')).to have_content('Tag expiration policy')
|
||||
expect(find('[data-testid="enable-toggle"]')).to have_content('Tags that match the rules on this page are automatically scheduled for deletion.')
|
||||
when :disabled_message
|
||||
expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled')
|
||||
end
|
||||
|
|
|
@ -17,10 +17,14 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import * as urlUtils from '~/lib/utils/url_utility';
|
||||
import diffsMockData from '../mock_data/merge_request_diffs';
|
||||
|
||||
import { EVT_VIEW_FILE_BY_FILE } from '~/diffs/constants';
|
||||
|
||||
import eventHub from '~/diffs/event_hub';
|
||||
|
||||
const mergeRequestDiff = { version_index: 1 };
|
||||
const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`;
|
||||
const COMMIT_URL = '[BASE URL]/OLD';
|
||||
const UPDATED_COMMIT_URL = '[BASE URL]/NEW';
|
||||
const COMMIT_URL = `${TEST_HOST}/COMMIT/OLD`;
|
||||
const UPDATED_COMMIT_URL = `${TEST_HOST}/COMMIT/NEW`;
|
||||
|
||||
function getCollapsedFilesWarning(wrapper) {
|
||||
return wrapper.find(CollapsedFilesWarning);
|
||||
|
@ -61,7 +65,7 @@ describe('diffs/components/app', () => {
|
|||
changesEmptyStateIllustration: '',
|
||||
dismissEndpoint: '',
|
||||
showSuggestPopover: true,
|
||||
viewDiffsFileByFile: false,
|
||||
fileByFileUserPreference: false,
|
||||
...props,
|
||||
},
|
||||
provide,
|
||||
|
@ -700,12 +704,14 @@ describe('diffs/components/app', () => {
|
|||
});
|
||||
|
||||
describe('file-by-file', () => {
|
||||
it('renders a single diff', () => {
|
||||
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
|
||||
it('renders a single diff', async () => {
|
||||
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
|
||||
state.diffs.diffFiles.push({ file_hash: '123' });
|
||||
state.diffs.diffFiles.push({ file_hash: '312' });
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findAll(DiffFile).length).toBe(1);
|
||||
});
|
||||
|
||||
|
@ -713,31 +719,37 @@ describe('diffs/components/app', () => {
|
|||
const fileByFileNav = () => wrapper.find('[data-testid="file-by-file-navigation"]');
|
||||
const paginator = () => fileByFileNav().find(GlPagination);
|
||||
|
||||
it('sets previous button as disabled', () => {
|
||||
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
|
||||
it('sets previous button as disabled', async () => {
|
||||
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
|
||||
state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(paginator().attributes('prevpage')).toBe(undefined);
|
||||
expect(paginator().attributes('nextpage')).toBe('2');
|
||||
});
|
||||
|
||||
it('sets next button as disabled', () => {
|
||||
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
|
||||
it('sets next button as disabled', async () => {
|
||||
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
|
||||
state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
|
||||
state.diffs.currentDiffFileId = '312';
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(paginator().attributes('prevpage')).toBe('1');
|
||||
expect(paginator().attributes('nextpage')).toBe(undefined);
|
||||
});
|
||||
|
||||
it("doesn't display when there's fewer than 2 files", () => {
|
||||
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
|
||||
it("doesn't display when there's fewer than 2 files", async () => {
|
||||
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
|
||||
state.diffs.diffFiles.push({ file_hash: '123' });
|
||||
state.diffs.currentDiffFileId = '123';
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(fileByFileNav().exists()).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -748,11 +760,13 @@ describe('diffs/components/app', () => {
|
|||
`(
|
||||
'it calls navigateToDiffFileIndex with $index when $link is clicked',
|
||||
async ({ currentDiffFileId, targetFile }) => {
|
||||
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
|
||||
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
|
||||
state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
|
||||
state.diffs.currentDiffFileId = currentDiffFileId;
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
jest.spyOn(wrapper.vm, 'navigateToDiffFileIndex');
|
||||
|
||||
paginator().vm.$emit('input', targetFile);
|
||||
|
@ -763,5 +777,24 @@ describe('diffs/components/app', () => {
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('control via event stream', () => {
|
||||
it.each`
|
||||
setting
|
||||
${true}
|
||||
${false}
|
||||
`(
|
||||
'triggers the action with the new fileByFile setting - $setting - when the event with that setting is received',
|
||||
async ({ setting }) => {
|
||||
createComponent();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
eventHub.$emit(EVT_VIEW_FILE_BY_FILE, { setting });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(store.state.diffs.viewDiffsFileByFile).toBe(setting);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,12 +2,18 @@ import { mount, createLocalVue } from '@vue/test-utils';
|
|||
import Vuex from 'vuex';
|
||||
import diffModule from '~/diffs/store/modules';
|
||||
import SettingsDropdown from '~/diffs/components/settings_dropdown.vue';
|
||||
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
|
||||
import {
|
||||
EVT_VIEW_FILE_BY_FILE,
|
||||
PARALLEL_DIFF_VIEW_TYPE,
|
||||
INLINE_DIFF_VIEW_TYPE,
|
||||
} from '~/diffs/constants';
|
||||
import eventHub from '~/diffs/event_hub';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('Diff settings dropdown component', () => {
|
||||
let wrapper;
|
||||
let vm;
|
||||
let actions;
|
||||
|
||||
|
@ -25,10 +31,15 @@ describe('Diff settings dropdown component', () => {
|
|||
|
||||
extendStore(store);
|
||||
|
||||
vm = mount(SettingsDropdown, {
|
||||
wrapper = mount(SettingsDropdown, {
|
||||
localVue,
|
||||
store,
|
||||
});
|
||||
vm = wrapper.vm;
|
||||
}
|
||||
|
||||
function getFileByFileCheckbox(vueWrapper) {
|
||||
return vueWrapper.find('[data-testid="file-by-file"]');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -41,14 +52,14 @@ describe('Diff settings dropdown component', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.destroy();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('tree view buttons', () => {
|
||||
it('list view button dispatches setRenderTreeList with false', () => {
|
||||
createComponent();
|
||||
|
||||
vm.find('.js-list-view').trigger('click');
|
||||
wrapper.find('.js-list-view').trigger('click');
|
||||
|
||||
expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), false);
|
||||
});
|
||||
|
@ -56,7 +67,7 @@ describe('Diff settings dropdown component', () => {
|
|||
it('tree view button dispatches setRenderTreeList with true', () => {
|
||||
createComponent();
|
||||
|
||||
vm.find('.js-tree-view').trigger('click');
|
||||
wrapper.find('.js-tree-view').trigger('click');
|
||||
|
||||
expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), true);
|
||||
});
|
||||
|
@ -68,8 +79,8 @@ describe('Diff settings dropdown component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
expect(vm.find('.js-list-view').classes('selected')).toBe(true);
|
||||
expect(vm.find('.js-tree-view').classes('selected')).toBe(false);
|
||||
expect(wrapper.find('.js-list-view').classes('selected')).toBe(true);
|
||||
expect(wrapper.find('.js-tree-view').classes('selected')).toBe(false);
|
||||
});
|
||||
|
||||
it('sets tree button as selected when renderTreeList is true', () => {
|
||||
|
@ -79,8 +90,8 @@ describe('Diff settings dropdown component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
expect(vm.find('.js-list-view').classes('selected')).toBe(false);
|
||||
expect(vm.find('.js-tree-view').classes('selected')).toBe(true);
|
||||
expect(wrapper.find('.js-list-view').classes('selected')).toBe(false);
|
||||
expect(wrapper.find('.js-tree-view').classes('selected')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -92,8 +103,8 @@ describe('Diff settings dropdown component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
expect(vm.find('.js-inline-diff-button').classes('selected')).toBe(true);
|
||||
expect(vm.find('.js-parallel-diff-button').classes('selected')).toBe(false);
|
||||
expect(wrapper.find('.js-inline-diff-button').classes('selected')).toBe(true);
|
||||
expect(wrapper.find('.js-parallel-diff-button').classes('selected')).toBe(false);
|
||||
});
|
||||
|
||||
it('sets parallel button as selected', () => {
|
||||
|
@ -103,14 +114,14 @@ describe('Diff settings dropdown component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
expect(vm.find('.js-inline-diff-button').classes('selected')).toBe(false);
|
||||
expect(vm.find('.js-parallel-diff-button').classes('selected')).toBe(true);
|
||||
expect(wrapper.find('.js-inline-diff-button').classes('selected')).toBe(false);
|
||||
expect(wrapper.find('.js-parallel-diff-button').classes('selected')).toBe(true);
|
||||
});
|
||||
|
||||
it('calls setInlineDiffViewType when clicking inline button', () => {
|
||||
createComponent();
|
||||
|
||||
vm.find('.js-inline-diff-button').trigger('click');
|
||||
wrapper.find('.js-inline-diff-button').trigger('click');
|
||||
|
||||
expect(actions.setInlineDiffViewType).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -118,7 +129,7 @@ describe('Diff settings dropdown component', () => {
|
|||
it('calls setParallelDiffViewType when clicking parallel button', () => {
|
||||
createComponent();
|
||||
|
||||
vm.find('.js-parallel-diff-button').trigger('click');
|
||||
wrapper.find('.js-parallel-diff-button').trigger('click');
|
||||
|
||||
expect(actions.setParallelDiffViewType).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -132,7 +143,7 @@ describe('Diff settings dropdown component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
expect(vm.find('#show-whitespace').element.checked).toBe(false);
|
||||
expect(wrapper.find('#show-whitespace').element.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('sets as checked when showWhitespace is true', () => {
|
||||
|
@ -142,13 +153,13 @@ describe('Diff settings dropdown component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
expect(vm.find('#show-whitespace').element.checked).toBe(true);
|
||||
expect(wrapper.find('#show-whitespace').element.checked).toBe(true);
|
||||
});
|
||||
|
||||
it('calls setShowWhitespace on change', () => {
|
||||
createComponent();
|
||||
|
||||
const checkbox = vm.find('#show-whitespace');
|
||||
const checkbox = wrapper.find('#show-whitespace');
|
||||
|
||||
checkbox.element.checked = true;
|
||||
checkbox.trigger('change');
|
||||
|
@ -159,4 +170,52 @@ describe('Diff settings dropdown component', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('file-by-file toggle', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
});
|
||||
|
||||
it.each`
|
||||
fileByFile | checked
|
||||
${true} | ${true}
|
||||
${false} | ${false}
|
||||
`(
|
||||
'sets { checked: $checked } if the fileByFile setting is $fileByFile',
|
||||
async ({ fileByFile, checked }) => {
|
||||
createComponent(store => {
|
||||
Object.assign(store.state.diffs, {
|
||||
viewDiffsFileByFile: fileByFile,
|
||||
});
|
||||
});
|
||||
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(vm.checked).toBe(checked);
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
start | emit
|
||||
${true} | ${false}
|
||||
${false} | ${true}
|
||||
`(
|
||||
'when the file by file setting starts as $start, toggling the checkbox should emit an event set to $emit',
|
||||
async ({ start, emit }) => {
|
||||
createComponent(store => {
|
||||
Object.assign(store.state.diffs, {
|
||||
viewDiffsFileByFile: start,
|
||||
});
|
||||
});
|
||||
|
||||
await vm.$nextTick();
|
||||
|
||||
getFileByFileCheckbox(wrapper).trigger('click');
|
||||
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith(EVT_VIEW_FILE_BY_FILE, { setting: emit });
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -48,6 +48,7 @@ import {
|
|||
moveToNeighboringCommit,
|
||||
setCurrentDiffFileIdFromNote,
|
||||
navigateToDiffFileIndex,
|
||||
setFileByFile,
|
||||
} from '~/diffs/store/actions';
|
||||
import eventHub from '~/notes/event_hub';
|
||||
import * as types from '~/diffs/store/mutation_types';
|
||||
|
@ -1455,4 +1456,20 @@ describe('DiffsStoreActions', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setFileByFile', () => {
|
||||
it.each`
|
||||
value
|
||||
${true}
|
||||
${false}
|
||||
`('commits SET_FILE_BY_FILE with the new value $value', ({ value }) => {
|
||||
return testAction(
|
||||
setFileByFile,
|
||||
{ fileByFile: value },
|
||||
{ viewDiffsFileByFile: null },
|
||||
[{ type: types.SET_FILE_BY_FILE, payload: value }],
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -892,4 +892,18 @@ describe('DiffsStoreMutations', () => {
|
|||
expect(state.showSuggestPopover).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_FILE_BY_FILE', () => {
|
||||
it.each`
|
||||
value | opposite
|
||||
${true} | ${false}
|
||||
${false} | ${true}
|
||||
`('sets viewDiffsFileByFile to $value', ({ value, opposite }) => {
|
||||
const state = { viewDiffsFileByFile: opposite };
|
||||
|
||||
mutations[types.SET_FILE_BY_FILE](state, value);
|
||||
|
||||
expect(state.viewDiffsFileByFile).toBe(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import Cookies from 'js-cookie';
|
||||
import { getParameterValues } from '~/lib/utils/url_utility';
|
||||
|
||||
import { fileByFile } from '~/diffs/utils/preferences';
|
||||
import {
|
||||
DIFF_FILE_BY_FILE_COOKIE_NAME,
|
||||
DIFF_VIEW_FILE_BY_FILE,
|
||||
DIFF_VIEW_ALL_FILES,
|
||||
} from '~/diffs/constants';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility');
|
||||
|
||||
describe('diffs preferences', () => {
|
||||
describe('fileByFile', () => {
|
||||
it.each`
|
||||
result | preference | cookie | searchParam
|
||||
${false} | ${false} | ${undefined} | ${undefined}
|
||||
${true} | ${true} | ${undefined} | ${undefined}
|
||||
${true} | ${false} | ${DIFF_VIEW_FILE_BY_FILE} | ${undefined}
|
||||
${false} | ${true} | ${DIFF_VIEW_ALL_FILES} | ${undefined}
|
||||
${true} | ${false} | ${undefined} | ${[DIFF_VIEW_FILE_BY_FILE]}
|
||||
${false} | ${true} | ${undefined} | ${[DIFF_VIEW_ALL_FILES]}
|
||||
${true} | ${false} | ${DIFF_VIEW_FILE_BY_FILE} | ${[DIFF_VIEW_FILE_BY_FILE]}
|
||||
${true} | ${true} | ${DIFF_VIEW_ALL_FILES} | ${[DIFF_VIEW_FILE_BY_FILE]}
|
||||
${false} | ${false} | ${DIFF_VIEW_ALL_FILES} | ${[DIFF_VIEW_ALL_FILES]}
|
||||
${false} | ${true} | ${DIFF_VIEW_FILE_BY_FILE} | ${[DIFF_VIEW_ALL_FILES]}
|
||||
`(
|
||||
'should return $result when { preference: $preference, cookie: $cookie, search: $searchParam }',
|
||||
({ result, preference, cookie, searchParam }) => {
|
||||
if (cookie) {
|
||||
Cookies.set(DIFF_FILE_BY_FILE_COOKIE_NAME, cookie);
|
||||
}
|
||||
|
||||
getParameterValues.mockReturnValue(searchParam);
|
||||
|
||||
expect(fileByFile(preference)).toBe(result);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -5,7 +5,14 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
|
||||
import { mockRefs, mockParams, mockPostParams, mockProjectId, mockError } from '../mock_data';
|
||||
import {
|
||||
mockBranches,
|
||||
mockTags,
|
||||
mockParams,
|
||||
mockPostParams,
|
||||
mockProjectId,
|
||||
mockError,
|
||||
} from '../mock_data';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
|
@ -37,6 +44,10 @@ describe('Pipeline New Form', () => {
|
|||
const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
|
||||
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
const getExpectedPostParams = () => JSON.parse(mock.history.post[0].data);
|
||||
const changeRef = i =>
|
||||
findDropdownItems()
|
||||
.at(i)
|
||||
.vm.$emit('click');
|
||||
|
||||
const createComponent = (term = '', props = {}, method = shallowMount) => {
|
||||
wrapper = method(PipelineNewForm, {
|
||||
|
@ -44,7 +55,8 @@ describe('Pipeline New Form', () => {
|
|||
projectId: mockProjectId,
|
||||
pipelinesPath,
|
||||
configVariablesPath,
|
||||
refs: mockRefs,
|
||||
branches: mockBranches,
|
||||
tags: mockTags,
|
||||
defaultBranch: 'master',
|
||||
settingsLink: '',
|
||||
maxWarnings: 25,
|
||||
|
@ -76,8 +88,11 @@ describe('Pipeline New Form', () => {
|
|||
});
|
||||
|
||||
it('displays dropdown with all branches and tags', () => {
|
||||
const refLength = mockBranches.length + mockTags.length;
|
||||
|
||||
createComponent();
|
||||
expect(findDropdownItems()).toHaveLength(mockRefs.length);
|
||||
|
||||
expect(findDropdownItems()).toHaveLength(refLength);
|
||||
});
|
||||
|
||||
it('when user enters search term the list is filtered', () => {
|
||||
|
@ -130,15 +145,6 @@ describe('Pipeline New Form', () => {
|
|||
expect(findVariableRows()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('creates a pipeline on submit', async () => {
|
||||
findForm().vm.$emit('submit', dummySubmitEvent);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(getExpectedPostParams()).toEqual(mockPostParams);
|
||||
expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`);
|
||||
});
|
||||
|
||||
it('creates blank variable on input change event', async () => {
|
||||
const input = findKeyInputs().at(2);
|
||||
input.element.value = 'test_var_2';
|
||||
|
@ -150,45 +156,81 @@ describe('Pipeline New Form', () => {
|
|||
expect(findKeyInputs().at(3).element.value).toBe('');
|
||||
expect(findValueInputs().at(3).element.value).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the form has been modified', () => {
|
||||
const selectRef = i =>
|
||||
findDropdownItems()
|
||||
.at(i)
|
||||
.vm.$emit('click');
|
||||
describe('Pipeline creation', () => {
|
||||
beforeEach(async () => {
|
||||
mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse);
|
||||
|
||||
beforeEach(async () => {
|
||||
const input = findKeyInputs().at(0);
|
||||
input.element.value = 'test_var_2';
|
||||
input.trigger('change');
|
||||
await waitForPromises();
|
||||
});
|
||||
it('creates pipeline with full ref and variables', async () => {
|
||||
createComponent();
|
||||
|
||||
findRemoveIcons()
|
||||
.at(1)
|
||||
.trigger('click');
|
||||
changeRef(0);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
findForm().vm.$emit('submit', dummySubmitEvent);
|
||||
|
||||
it('form values are restored when the ref changes', async () => {
|
||||
expect(findVariableRows()).toHaveLength(2);
|
||||
await waitForPromises();
|
||||
|
||||
selectRef(1);
|
||||
await waitForPromises();
|
||||
expect(getExpectedPostParams().ref).toEqual(wrapper.vm.$data.refValue.fullName);
|
||||
expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`);
|
||||
});
|
||||
it('creates a pipeline with short ref and variables', async () => {
|
||||
// query params are used
|
||||
createComponent('', mockParams);
|
||||
|
||||
expect(findVariableRows()).toHaveLength(3);
|
||||
expect(findKeyInputs().at(0).element.value).toBe('test_var');
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
it('form values are restored again when the ref is reverted', async () => {
|
||||
selectRef(1);
|
||||
await waitForPromises();
|
||||
findForm().vm.$emit('submit', dummySubmitEvent);
|
||||
|
||||
selectRef(2);
|
||||
await waitForPromises();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findVariableRows()).toHaveLength(2);
|
||||
expect(findKeyInputs().at(0).element.value).toBe('test_var_2');
|
||||
});
|
||||
expect(getExpectedPostParams()).toEqual(mockPostParams);
|
||||
expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the ref has been changed', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent('', {}, mount);
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
it('variables persist between ref changes', async () => {
|
||||
changeRef(0); // change to master
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
const masterInput = findKeyInputs().at(0);
|
||||
masterInput.element.value = 'build_var';
|
||||
masterInput.trigger('change');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
changeRef(1); // change to branch-1
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
const branchOneInput = findKeyInputs().at(0);
|
||||
branchOneInput.element.value = 'deploy_var';
|
||||
branchOneInput.trigger('change');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
changeRef(0); // change back to master
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findKeyInputs().at(0).element.value).toBe('build_var');
|
||||
expect(findVariableRows().length).toBe(2);
|
||||
|
||||
changeRef(1); // change back to branch-1
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
|
||||
expect(findVariableRows().length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -321,6 +363,7 @@ describe('Pipeline New Form', () => {
|
|||
|
||||
it('shows the correct warning title', () => {
|
||||
const { length } = mockError.warnings;
|
||||
|
||||
expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
export const mockRefs = ['master', 'branch-1', 'tag-1'];
|
||||
export const mockBranches = [
|
||||
{ shortName: 'master', fullName: 'refs/heads/master' },
|
||||
{ shortName: 'branch-1', fullName: 'refs/heads/branch-1' },
|
||||
{ shortName: 'branch-2', fullName: 'refs/heads/branch-2' },
|
||||
];
|
||||
|
||||
export const mockTags = [
|
||||
{ shortName: '1.0.0', fullName: 'refs/tags/1.0.0' },
|
||||
{ shortName: '1.1.0', fullName: 'refs/tags/1.1.0' },
|
||||
{ shortName: '1.2.0', fullName: 'refs/tags/1.2.0' },
|
||||
];
|
||||
|
||||
export const mockParams = {
|
||||
refParam: 'tag-1',
|
||||
|
@ -31,3 +41,7 @@ export const mockError = {
|
|||
],
|
||||
total_warnings: 7,
|
||||
};
|
||||
|
||||
export const mockBranchRefs = ['master', 'dev', 'release'];
|
||||
|
||||
export const mockTagRefs = ['1.0.0', '1.1.0', '1.2.0'];
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import formatRefs from '~/pipeline_new/utils/format_refs';
|
||||
import { BRANCH_REF_TYPE, TAG_REF_TYPE } from '~/pipeline_new/constants';
|
||||
import { mockBranchRefs, mockTagRefs } from '../mock_data';
|
||||
|
||||
describe('Format refs util', () => {
|
||||
it('formats branch ref correctly', () => {
|
||||
expect(formatRefs(mockBranchRefs, BRANCH_REF_TYPE)).toEqual([
|
||||
{ fullName: 'refs/heads/master', shortName: 'master' },
|
||||
{ fullName: 'refs/heads/dev', shortName: 'dev' },
|
||||
{ fullName: 'refs/heads/release', shortName: 'release' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('formats tag ref correctly', () => {
|
||||
expect(formatRefs(mockTagRefs, TAG_REF_TYPE)).toEqual([
|
||||
{ fullName: 'refs/tags/1.0.0', shortName: '1.0.0' },
|
||||
{ fullName: 'refs/tags/1.1.0', shortName: '1.1.0' },
|
||||
{ fullName: 'refs/tags/1.2.0', shortName: '1.2.0' },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Settings Form Cadence matches snapshot 1`] = `
|
||||
<expiration-dropdown-stub
|
||||
class="gl-mr-7 gl-mb-0!"
|
||||
data-testid="cadence-dropdown"
|
||||
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]"
|
||||
label="Run cleanup:"
|
||||
name="cadence"
|
||||
value="EVERY_DAY"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Settings Form Enable matches snapshot 1`] = `
|
||||
<expiration-toggle-stub
|
||||
class="gl-mb-0!"
|
||||
data-testid="enable-toggle"
|
||||
value="true"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Settings Form Keep N matches snapshot 1`] = `
|
||||
<expiration-dropdown-stub
|
||||
data-testid="keep-n-dropdown"
|
||||
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
|
||||
label="Keep the most recent:"
|
||||
name="keep-n"
|
||||
value="TEN_TAGS"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Settings Form Keep Regex matches snapshot 1`] = `
|
||||
<expiration-textarea-stub
|
||||
data-testid="keep-regex-textarea"
|
||||
description="Tags with names that match this regex pattern are kept. %{linkStart}More information%{linkEnd}"
|
||||
error=""
|
||||
label="Keep tags matching:"
|
||||
name="keep-regex"
|
||||
placeholder=""
|
||||
value="sss"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Settings Form OlderThan matches snapshot 1`] = `
|
||||
<expiration-dropdown-stub
|
||||
data-testid="older-than-dropdown"
|
||||
formoptions="[object Object],[object Object],[object Object],[object Object]"
|
||||
label="Remove tags older than:"
|
||||
name="older-than"
|
||||
value="FOURTEEN_DAYS"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Settings Form Remove regex matches snapshot 1`] = `
|
||||
<expiration-textarea-stub
|
||||
data-testid="remove-regex-textarea"
|
||||
description="Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}"
|
||||
error=""
|
||||
label="Remove tags matching:"
|
||||
name="remove-regex"
|
||||
placeholder=".*"
|
||||
value="asdasdssssdfdf"
|
||||
/>
|
||||
`;
|
|
@ -11,7 +11,11 @@ import {
|
|||
UNAVAILABLE_USER_FEATURE_TEXT,
|
||||
} from '~/registry/settings/constants';
|
||||
|
||||
import { expirationPolicyPayload, emptyExpirationPolicyPayload } from '../mock_data';
|
||||
import {
|
||||
expirationPolicyPayload,
|
||||
emptyExpirationPolicyPayload,
|
||||
containerExpirationPolicyData,
|
||||
} from '../mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
||||
|
@ -62,6 +66,29 @@ describe('Registry Settings App', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('isEdited status', () => {
|
||||
it.each`
|
||||
description | apiResponse | workingCopy | result
|
||||
${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false}
|
||||
${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true}
|
||||
${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false}
|
||||
${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true}
|
||||
${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true}
|
||||
`('$description', async ({ apiResponse, workingCopy, result }) => {
|
||||
const requests = mountComponentWithApollo({
|
||||
provide: { ...defaultProvidedValues, enableHistoricEntries: true },
|
||||
resolver: jest.fn().mockResolvedValue(apiResponse),
|
||||
});
|
||||
await Promise.all(requests);
|
||||
|
||||
findSettingsComponent().vm.$emit('input', workingCopy);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findSettingsComponent().props('isEdited')).toBe(result);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the setting form', async () => {
|
||||
const requests = mountComponentWithApollo({
|
||||
resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
|
||||
|
|
|
@ -4,7 +4,6 @@ import createMockApollo from 'jest/helpers/mock_apollo_helper';
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import Tracking from '~/tracking';
|
||||
import component from '~/registry/settings/components/settings_form.vue';
|
||||
import expirationPolicyFields from '~/registry/shared/components/expiration_policy_fields.vue';
|
||||
import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.graphql';
|
||||
import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.graphql';
|
||||
import {
|
||||
|
@ -39,9 +38,15 @@ describe('Settings Form', () => {
|
|||
};
|
||||
|
||||
const findForm = () => wrapper.find({ ref: 'form-element' });
|
||||
const findFields = () => wrapper.find(expirationPolicyFields);
|
||||
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
|
||||
const findSaveButton = () => wrapper.find({ ref: 'save-button' });
|
||||
|
||||
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"');
|
||||
const findSaveButton = () => wrapper.find('[data-testid="save-button"');
|
||||
const findEnableToggle = () => wrapper.find('[data-testid="enable-toggle"]');
|
||||
const findCadenceDropdown = () => wrapper.find('[data-testid="cadence-dropdown"]');
|
||||
const findKeepNDropdown = () => wrapper.find('[data-testid="keep-n-dropdown"]');
|
||||
const findKeepRegexTextarea = () => wrapper.find('[data-testid="keep-regex-textarea"]');
|
||||
const findOlderThanDropdown = () => wrapper.find('[data-testid="older-than-dropdown"]');
|
||||
const findRemoveRegexTextarea = () => wrapper.find('[data-testid="remove-regex-textarea"]');
|
||||
|
||||
const mountComponent = ({
|
||||
props = defaultProps,
|
||||
|
@ -109,45 +114,136 @@ describe('Settings Form', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('data binding', () => {
|
||||
it('v-model change update the settings property', () => {
|
||||
describe.each`
|
||||
model | finder | fieldName | type | defaultValue
|
||||
${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false}
|
||||
${'cadence'} | ${findCadenceDropdown} | ${'Cadence'} | ${'dropdown'} | ${'EVERY_DAY'}
|
||||
${'keepN'} | ${findKeepNDropdown} | ${'Keep N'} | ${'dropdown'} | ${'TEN_TAGS'}
|
||||
${'nameRegexKeep'} | ${findKeepRegexTextarea} | ${'Keep Regex'} | ${'textarea'} | ${''}
|
||||
${'olderThan'} | ${findOlderThanDropdown} | ${'OlderThan'} | ${'dropdown'} | ${'NINETY_DAYS'}
|
||||
${'nameRegex'} | ${findRemoveRegexTextarea} | ${'Remove regex'} | ${'textarea'} | ${''}
|
||||
`('$fieldName', ({ model, finder, type, defaultValue }) => {
|
||||
it('matches snapshot', () => {
|
||||
mountComponent();
|
||||
findFields().vm.$emit('input', { newValue: 'foo' });
|
||||
expect(wrapper.emitted('input')).toEqual([['foo']]);
|
||||
|
||||
expect(finder().element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('v-model change update the api error property', () => {
|
||||
const apiErrors = { baz: 'bar' };
|
||||
mountComponent({ data: { apiErrors } });
|
||||
expect(findFields().props('apiErrors')).toEqual(apiErrors);
|
||||
findFields().vm.$emit('input', { newValue: 'foo', modified: 'baz' });
|
||||
expect(findFields().props('apiErrors')).toEqual({});
|
||||
it('input event triggers a model update', () => {
|
||||
mountComponent();
|
||||
|
||||
finder().vm.$emit('input', 'foo');
|
||||
expect(wrapper.emitted('input')[0][0]).toMatchObject({
|
||||
[model]: 'foo',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the default option when none are selected', () => {
|
||||
mountComponent({ props: { value: {} } });
|
||||
expect(findFields().props('value')).toEqual({
|
||||
cadence: 'EVERY_DAY',
|
||||
keepN: 'TEN_TAGS',
|
||||
olderThan: 'NINETY_DAYS',
|
||||
});
|
||||
expect(finder().props('value')).toEqual(defaultValue);
|
||||
});
|
||||
|
||||
if (type !== 'toggle') {
|
||||
it.each`
|
||||
isLoading | mutationLoading | enabledValue
|
||||
${false} | ${false} | ${false}
|
||||
${true} | ${false} | ${false}
|
||||
${true} | ${true} | ${true}
|
||||
${false} | ${true} | ${true}
|
||||
${false} | ${false} | ${false}
|
||||
`(
|
||||
'is disabled when is loading is $isLoading, mutationLoading is $mutationLoading and enabled is $enabledValue',
|
||||
({ isLoading, mutationLoading, enabledValue }) => {
|
||||
mountComponent({
|
||||
props: { isLoading, value: { enabled: enabledValue } },
|
||||
data: { mutationLoading },
|
||||
});
|
||||
expect(finder().props('disabled')).toEqual(true);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
it.each`
|
||||
isLoading | mutationLoading
|
||||
${true} | ${false}
|
||||
${true} | ${true}
|
||||
${false} | ${true}
|
||||
`(
|
||||
'is disabled when is loading is $isLoading and mutationLoading is $mutationLoading',
|
||||
({ isLoading, mutationLoading }) => {
|
||||
mountComponent({
|
||||
props: { isLoading, value: {} },
|
||||
data: { mutationLoading },
|
||||
});
|
||||
expect(finder().props('disabled')).toEqual(true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'textarea') {
|
||||
it('input event updates the api error property', async () => {
|
||||
const apiErrors = { [model]: 'bar' };
|
||||
mountComponent({ data: { apiErrors } });
|
||||
|
||||
finder().vm.$emit('input', 'foo');
|
||||
expect(finder().props('error')).toEqual('bar');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(finder().props('error')).toEqual('');
|
||||
});
|
||||
|
||||
it('validation event updates buttons disabled state', async () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findSaveButton().props('disabled')).toBe(false);
|
||||
|
||||
finder().vm.$emit('validation', false);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findSaveButton().props('disabled')).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'dropdown') {
|
||||
it('has the correct formOptions', () => {
|
||||
mountComponent();
|
||||
expect(finder().props('formOptions')).toEqual(wrapper.vm.$options.formOptions[model]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('form', () => {
|
||||
describe('form reset event', () => {
|
||||
beforeEach(() => {
|
||||
it('calls the appropriate function', () => {
|
||||
mountComponent();
|
||||
|
||||
findForm().trigger('reset');
|
||||
});
|
||||
it('calls the appropriate function', () => {
|
||||
|
||||
expect(wrapper.emitted('reset')).toEqual([[]]);
|
||||
});
|
||||
|
||||
it('tracks the reset event', () => {
|
||||
mountComponent();
|
||||
|
||||
findForm().trigger('reset');
|
||||
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload);
|
||||
});
|
||||
|
||||
it('resets the errors objects', async () => {
|
||||
mountComponent({
|
||||
data: { apiErrors: { nameRegex: 'bar' }, localErrors: { nameRegexKeep: false } },
|
||||
});
|
||||
|
||||
findForm().trigger('reset');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findKeepRegexTextarea().props('error')).toBe('');
|
||||
expect(findRemoveRegexTextarea().props('error')).toBe('');
|
||||
expect(findSaveButton().props('disabled')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('form submit event ', () => {
|
||||
|
@ -209,6 +305,7 @@ describe('Settings Form', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('global errors', () => {
|
||||
it('shows an error', async () => {
|
||||
const handlers = mountComponentWithApollo({
|
||||
|
@ -230,7 +327,7 @@ describe('Settings Form', () => {
|
|||
graphQLErrors: [
|
||||
{
|
||||
extensions: {
|
||||
problems: [{ path: ['name'], message: 'baz' }],
|
||||
problems: [{ path: ['nameRegexKeep'], message: 'baz' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -241,7 +338,7 @@ describe('Settings Form', () => {
|
|||
await waitForPromises();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findFields().props('apiErrors')).toEqual({ name: 'baz' });
|
||||
expect(findKeepRegexTextarea().props('error')).toEqual('baz');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -257,23 +354,21 @@ describe('Settings Form', () => {
|
|||
});
|
||||
|
||||
it.each`
|
||||
isLoading | isEdited | mutationLoading | isDisabled
|
||||
${true} | ${true} | ${true} | ${true}
|
||||
${false} | ${true} | ${true} | ${true}
|
||||
${false} | ${false} | ${true} | ${true}
|
||||
${true} | ${false} | ${false} | ${true}
|
||||
${false} | ${false} | ${false} | ${true}
|
||||
${false} | ${true} | ${false} | ${false}
|
||||
isLoading | isEdited | mutationLoading
|
||||
${true} | ${true} | ${true}
|
||||
${false} | ${true} | ${true}
|
||||
${false} | ${false} | ${true}
|
||||
${true} | ${false} | ${false}
|
||||
${false} | ${false} | ${false}
|
||||
`(
|
||||
'when isLoading is $isLoading and isEdited is $isEdited and mutationLoading is $mutationLoading is $isDisabled that the is disabled',
|
||||
({ isEdited, isLoading, mutationLoading, isDisabled }) => {
|
||||
'when isLoading is $isLoading, isEdited is $isEdited and mutationLoading is $mutationLoading is disabled',
|
||||
({ isEdited, isLoading, mutationLoading }) => {
|
||||
mountComponent({
|
||||
props: { ...defaultProps, isEdited, isLoading },
|
||||
data: { mutationLoading },
|
||||
});
|
||||
|
||||
const expectation = isDisabled ? 'true' : undefined;
|
||||
expect(findCancelButton().attributes('disabled')).toBe(expectation);
|
||||
expect(findCancelButton().props('disabled')).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -284,24 +379,24 @@ describe('Settings Form', () => {
|
|||
|
||||
expect(findSaveButton().attributes('type')).toBe('submit');
|
||||
});
|
||||
|
||||
it.each`
|
||||
isLoading | fieldsAreValid | mutationLoading | isDisabled
|
||||
${true} | ${true} | ${true} | ${true}
|
||||
${false} | ${true} | ${true} | ${true}
|
||||
${false} | ${false} | ${true} | ${true}
|
||||
${true} | ${false} | ${false} | ${true}
|
||||
${false} | ${false} | ${false} | ${true}
|
||||
${false} | ${true} | ${false} | ${false}
|
||||
isLoading | localErrors | mutationLoading
|
||||
${true} | ${{}} | ${true}
|
||||
${true} | ${{}} | ${false}
|
||||
${false} | ${{}} | ${true}
|
||||
${false} | ${{ foo: false }} | ${true}
|
||||
${true} | ${{ foo: false }} | ${false}
|
||||
${false} | ${{ foo: false }} | ${false}
|
||||
`(
|
||||
'when isLoading is $isLoading and fieldsAreValid is $fieldsAreValid and mutationLoading is $mutationLoading is $isDisabled that the is disabled',
|
||||
({ fieldsAreValid, isLoading, mutationLoading, isDisabled }) => {
|
||||
'when isLoading is $isLoading, localErrors is $localErrors and mutationLoading is $mutationLoading is disabled',
|
||||
({ localErrors, isLoading, mutationLoading }) => {
|
||||
mountComponent({
|
||||
props: { ...defaultProps, isLoading },
|
||||
data: { mutationLoading, fieldsAreValid },
|
||||
data: { mutationLoading, localErrors },
|
||||
});
|
||||
|
||||
const expectation = isDisabled ? 'true' : undefined;
|
||||
expect(findSaveButton().attributes('disabled')).toBe(expectation);
|
||||
expect(findSaveButton().props('disabled')).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -76,25 +76,25 @@ Array [
|
|||
Object {
|
||||
"default": false,
|
||||
"key": "SEVEN_DAYS",
|
||||
"label": "7 days until tags are automatically removed",
|
||||
"label": "7 days",
|
||||
"variable": 7,
|
||||
},
|
||||
Object {
|
||||
"default": false,
|
||||
"key": "FOURTEEN_DAYS",
|
||||
"label": "14 days until tags are automatically removed",
|
||||
"label": "14 days",
|
||||
"variable": 14,
|
||||
},
|
||||
Object {
|
||||
"default": false,
|
||||
"key": "THIRTY_DAYS",
|
||||
"label": "30 days until tags are automatically removed",
|
||||
"label": "30 days",
|
||||
"variable": 30,
|
||||
},
|
||||
Object {
|
||||
"default": true,
|
||||
"key": "NINETY_DAYS",
|
||||
"label": "90 days until tags are automatically removed",
|
||||
"label": "90 days",
|
||||
"variable": 90,
|
||||
},
|
||||
]
|
||||
|
|
|
@ -11,10 +11,7 @@ describe('Utils', () => {
|
|||
[{ variable: 1 }, { variable: 2 }],
|
||||
olderThanTranslationGenerator,
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{ variable: 1, label: '1 day until tags are automatically removed' },
|
||||
{ variable: 2, label: '2 days until tags are automatically removed' },
|
||||
]);
|
||||
expect(result).toEqual([{ variable: 1, label: '1 day' }, { variable: 2, label: '2 days' }]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -86,6 +86,7 @@ label:
|
|||
- issues
|
||||
- merge_requests
|
||||
- priorities
|
||||
- epic_board_labels
|
||||
milestone:
|
||||
- group
|
||||
- project
|
||||
|
|
|
@ -22,6 +22,15 @@ RSpec.describe CustomEmoji do
|
|||
expect(new_emoji.errors.messages).to eq(name: ["#{emoji_name} is already being used for another emoji"])
|
||||
end
|
||||
|
||||
it 'disallows very long invalid emoji name without regular expression backtracking issues' do
|
||||
new_emoji = build(:custom_emoji, name: 'a' * 10000 + '!', group: group)
|
||||
|
||||
Timeout.timeout(1) do
|
||||
expect(new_emoji).not_to be_valid
|
||||
expect(new_emoji.errors.messages).to eq(name: ["is too long (maximum is 36 characters)", "is invalid"])
|
||||
end
|
||||
end
|
||||
|
||||
it 'disallows duplicate custom emoji names within namespace' do
|
||||
old_emoji = create(:custom_emoji, group: group)
|
||||
new_emoji = build(:custom_emoji, name: old_emoji.name, namespace: old_emoji.namespace, group: group)
|
||||
|
|
|
@ -5071,11 +5071,11 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#default_branch" do
|
||||
context "with an empty repository" do
|
||||
describe '#default_branch' do
|
||||
context 'with an empty repository' do
|
||||
let_it_be(:project) { create(:project_empty_repo) }
|
||||
|
||||
context "group.default_branch_name is available" do
|
||||
context 'group.default_branch_name is available' do
|
||||
let(:project_group) { create(:group) }
|
||||
let(:project) { create(:project, path: 'avatar', namespace: project_group) }
|
||||
|
||||
|
@ -5088,19 +5088,19 @@ RSpec.describe Project, factory_default: :keep do
|
|||
.and_return('example_branch')
|
||||
end
|
||||
|
||||
it "returns the group default value" do
|
||||
expect(project.default_branch).to eq("example_branch")
|
||||
it 'returns the group default value' do
|
||||
expect(project.default_branch).to eq('example_branch')
|
||||
end
|
||||
end
|
||||
|
||||
context "Gitlab::CurrentSettings.default_branch_name is available" do
|
||||
context 'Gitlab::CurrentSettings.default_branch_name is available' do
|
||||
before do
|
||||
expect(Gitlab::CurrentSettings)
|
||||
.to receive(:default_branch_name)
|
||||
.and_return(example_branch_name)
|
||||
end
|
||||
|
||||
context "is missing or nil" do
|
||||
context 'is missing or nil' do
|
||||
let(:example_branch_name) { nil }
|
||||
|
||||
it "returns nil" do
|
||||
|
@ -5108,10 +5108,18 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
context "is present" do
|
||||
let(:example_branch_name) { "example_branch_name" }
|
||||
context 'is blank' do
|
||||
let(:example_branch_name) { '' }
|
||||
|
||||
it "returns the expected branch name" do
|
||||
it 'returns nil' do
|
||||
expect(project.default_branch).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'is present' do
|
||||
let(:example_branch_name) { 'example_branch_name' }
|
||||
|
||||
it 'returns the expected branch name' do
|
||||
expect(project.default_branch).to eq(example_branch_name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,10 @@ if $".include?(File.expand_path('fast_spec_helper.rb', __dir__))
|
|||
abort 'Aborting...'
|
||||
end
|
||||
|
||||
# Enable deprecation warnings by default and make them more visible
|
||||
# to developers to ease upgrading to newer Ruby versions.
|
||||
Warning[:deprecated] = true unless ENV.key?('SILENCE_DEPRECATIONS')
|
||||
|
||||
require './spec/deprecation_toolkit_env'
|
||||
|
||||
require './spec/simplecov_env'
|
||||
|
|
Loading…
Reference in New Issue