diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-03 00:09:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-03 00:09:44 +0300 |
commit | f96f2720d1b21b76eadedc54fdea67cb70e98d94 (patch) | |
tree | 527d27d5ceb816969e315b6223b3ddb2ca128dae /app | |
parent | ad05e1db038a2e983d25555144fa29063e060c50 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
25 files changed, 423 insertions, 125 deletions
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 9218cc00303..53eb8cd8eb8 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -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() { diff --git a/app/assets/javascripts/diffs/components/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue index 590b2127e6b..b8904de8049 100644 --- a/app/assets/javascripts/diffs/components/settings_dropdown.vue +++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue @@ -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> diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 79f8c08e389..07e27bd8e47 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -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'; diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js index 4ec24d452bf..c4ac99ead91 100644 --- a/app/assets/javascripts/diffs/i18n.js +++ b/app/assets/javascripts/diffs/i18n.js @@ -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'), +}; diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index 06a138b1e13..587220488be 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -116,7 +116,7 @@ export default function initDiffsApp(store) { isFluidLayout: this.isFluidLayout, dismissEndpoint: this.dismissEndpoint, showSuggestPopover: this.showSuggestPopover, - viewDiffsFileByFile: this.viewDiffsFileByFile, + fileByFileUserPreference: this.viewDiffsFileByFile, }, }); }, diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 1a0e65bbb3e..51d9fe12102 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -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), + ); +}; diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 001d9d9f594..c331e52c887 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -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, diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index ed694419ab1..25184028799 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -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'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 7b08c5e75e1..69ae3f705e3 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -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; + }, }; diff --git a/app/assets/javascripts/diffs/utils/preferences.js b/app/assets/javascripts/diffs/utils/preferences.js new file mode 100644 index 00000000000..e440de3350a --- /dev/null +++ b/app/assets/javascripts/diffs/utils/preferences.js @@ -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; +} diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue index 790460c79f1..f2d68054e80 100644 --- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue +++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue @@ -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="branch in filteredBranches" + :key="branch.fullName" + class="gl-font-monospace" + is-check-item + :is-checked="isSelected(branch)" + @click="setRefSelected(branch)" + > + {{ branch.shortName }} + </gl-dropdown-item> + <gl-dropdown-section-header v-if="hasTags">{{ __('Tags') }}</gl-dropdown-section-header> <gl-dropdown-item - v-for="(ref, index) in filteredRefs" - :key="index" + v-for="tag in filteredTags" + :key="tag.fullName" class="gl-font-monospace" is-check-item - :is-checked="isSelected(ref)" - @click="setRefSelected(ref)" + :is-checked="isSelected(tag)" + @click="setRefSelected(tag)" > - {{ ref }} + {{ 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" diff --git a/app/assets/javascripts/pipeline_new/constants.js b/app/assets/javascripts/pipeline_new/constants.js index 3d4a0ee328d..004bbe7daf4 100644 --- a/app/assets/javascripts/pipeline_new/constants.js +++ b/app/assets/javascripts/pipeline_new/constants.js @@ -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'; diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js index ff4f677654e..0b85184ec90 100644 --- a/app/assets/javascripts/pipeline_new/index.js +++ b/app/assets/javascripts/pipeline_new/index.js @@ -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), }, diff --git a/app/assets/javascripts/pipeline_new/utils/format_refs.js b/app/assets/javascripts/pipeline_new/utils/format_refs.js new file mode 100644 index 00000000000..e217cd25413 --- /dev/null +++ b/app/assets/javascripts/pipeline_new/utils/format_refs.js @@ -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, + }; + }); +}; diff --git a/app/assets/javascripts/registry/settings/components/expiration_textarea.vue b/app/assets/javascripts/registry/settings/components/expiration_textarea.vue index 1e1194ebb5c..3f817e37dca 100644 --- a/app/assets/javascripts/registry/settings/components/expiration_textarea.vue +++ b/app/assets/javascripts/registry/settings/components/expiration_textarea.vue @@ -36,7 +36,8 @@ export default { }, placeholder: { type: String, - required: true, + required: false, + default: '', }, description: { type: String, diff --git a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue index 264d39a406a..e236834d8e1 100644 --- a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue +++ b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue @@ -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); }, }, diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue index fe4aee6806e..d0a8081e455 100644 --- a/app/assets/javascripts/registry/settings/components/settings_form.vue +++ b/app/assets/javascripts/registry/settings/components/settings_form.vue @@ -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" - /> + <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 #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> + <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> diff --git a/app/assets/javascripts/registry/settings/constants.js b/app/assets/javascripts/registry/settings/constants.js index bc3ec3104ad..ba5820196ff 100644 --- a/app/assets/javascripts/registry/settings/constants.js +++ b/app/assets/javascripts/registry/settings/constants.js @@ -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'); diff --git a/app/assets/javascripts/registry/shared/utils.js b/app/assets/javascripts/registry/shared/utils.js index bdf1ab9507d..5c8c505f835 100644 --- a/app/assets/javascripts/registry/shared/utils.js +++ b/app/assets/javascripts/registry/shared/utils.js @@ -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); diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb index df7a574848f..c93e75b438b 100644 --- a/app/controllers/concerns/snippets_actions.rb +++ b/app/controllers/concerns/snippets_actions.rb @@ -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 diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb index 3dea4a9f5fb..9692941d8b2 100644 --- a/app/models/concerns/has_repository.rb +++ b/app/models/concerns/has_repository.rb @@ -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 diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index ed22d4ba231..4f8f86965d7 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -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 diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb index 19700587f09..1b99f310e1a 100644 --- a/app/models/terraform/state.rb +++ b/app/models/terraform/state.rb @@ -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 diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index bc8e6a6d9cc..7d5cef2015d 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -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 } } diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index f6ecb923100..0bef82ee325 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -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' |