diff options
98 files changed, 1206 insertions, 375 deletions
diff --git a/app/assets/javascripts/add_context_commits_modal/store/actions.js b/app/assets/javascripts/add_context_commits_modal/store/actions.js index 7b6f4c81bd2..4e5a2c7b371 100644 --- a/app/assets/javascripts/add_context_commits_modal/store/actions.js +++ b/app/assets/javascripts/add_context_commits_modal/store/actions.js @@ -1,6 +1,6 @@ import _ from 'lodash'; import Api from '~/api'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale'; import * as types from './mutation_types'; @@ -71,7 +71,9 @@ export const createContextCommits = ({ state }, { commits, forceReload = false } }) .catch(() => { if (forceReload) { - createFlash(s__('ContextCommits|Failed to create context commits. Please try again.')); + createFlash({ + message: s__('ContextCommits|Failed to create context commits. Please try again.'), + }); } return false; @@ -111,7 +113,9 @@ export const removeContextCommits = ({ state }, forceReload = false) => }) .catch(() => { if (forceReload) { - createFlash(s__('ContextCommits|Failed to delete context commits. Please try again.')); + createFlash({ + message: s__('ContextCommits|Failed to delete context commits. Please try again.'), + }); } return false; diff --git a/app/assets/javascripts/admin/statistics_panel/store/actions.js b/app/assets/javascripts/admin/statistics_panel/store/actions.js index 459f11c02f1..77782cdc187 100644 --- a/app/assets/javascripts/admin/statistics_panel/store/actions.js +++ b/app/assets/javascripts/admin/statistics_panel/store/actions.js @@ -1,5 +1,5 @@ import Api from '~/api'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; import * as types from './mutation_types'; @@ -21,5 +21,7 @@ export const receiveStatisticsSuccess = ({ commit }, statistics) => export const receiveStatisticsError = ({ commit }, error) => { commit(types.RECEIVE_STATISTICS_ERROR, error); - createFlash(s__('AdminDashboard|Error loading the statistics. Please try again')); + createFlash({ + message: s__('AdminDashboard|Error loading the statistics. Please try again'), + }); }; diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index d26af07d54f..76d9b18b777 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -3,7 +3,7 @@ import $ from 'jquery'; import initPopover from '~/blob/suggest_gitlab_ci_yml'; import initCodeQualityWalkthrough from '~/code_quality_walkthrough'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils'; import Tracking from '~/tracking'; import BlobFileDropzone from '../blob/blob_file_dropzone'; @@ -84,7 +84,11 @@ export default () => { initPopovers(); initCodeQualityWalkthroughStep(); }) - .catch((e) => createFlash(e)); + .catch((e) => + createFlash({ + message: e, + }), + ); cancelLink.on('click', () => { window.onbeforeunload = null; diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index ab2fc80e653..7c8d0d5ded0 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import EditorLite from '~/editor/editor_lite'; import { FileTemplateExtension } from '~/editor/extensions/editor_file_template_ext'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown'; import { insertFinalNewline } from '~/lib/utils/text_utility'; @@ -21,7 +21,11 @@ export default class EditBlob { this.editor.use(new MarkdownExtension()); addEditorMarkdownListeners(this.editor); }) - .catch((e) => createFlash(`${BLOB_EDITOR_ERROR}: ${e}`)); + .catch((e) => + createFlash({ + message: `${BLOB_EDITOR_ERROR}: ${e}`, + }), + ); } this.initModePanesAndLinks(); @@ -94,7 +98,11 @@ export default class EditBlob { currentPane.empty().append(data); currentPane.renderGFM(); }) - .catch(() => createFlash(BLOB_PREVIEW_ERROR)); + .catch(() => + createFlash({ + message: BLOB_PREVIEW_ERROR, + }), + ); } this.$toggleButton.show(); diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js index 66e8d982113..b88c056b00f 100644 --- a/app/assets/javascripts/branches/divergence_graph.js +++ b/app/assets/javascripts/branches/divergence_graph.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { deprecatedCreateFlash as createFlash } from '../flash'; +import createFlash from '../flash'; import axios from '../lib/utils/axios_utils'; import { __ } from '../locale'; import DivergenceGraph from './components/divergence_graph.vue'; @@ -51,6 +51,8 @@ export default (endpoint, defaultBranch) => { }); }) .catch(() => - createFlash(__('Error fetching diverging counts for branches. Please try again.')), + createFlash({ + message: __('Error fetching diverging counts for branches. Please try again.'), + }), ); }; diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js index 8569cecc6a7..8a182737e7b 100644 --- a/app/assets/javascripts/ci_variable_list/store/actions.js +++ b/app/assets/javascripts/ci_variable_list/store/actions.js @@ -1,5 +1,5 @@ import Api from '~/api'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import * as types from './mutation_types'; @@ -48,7 +48,9 @@ export const addVariable = ({ state, dispatch }) => { dispatch('fetchVariables'); }) .catch((error) => { - createFlash(error.response.data[0]); + createFlash({ + message: error.response.data[0], + }); dispatch('receiveAddVariableError', error); }); }; @@ -78,7 +80,9 @@ export const updateVariable = ({ state, dispatch }) => { dispatch('fetchVariables'); }) .catch((error) => { - createFlash(error.response.data[0]); + createFlash({ + message: error.response.data[0], + }); dispatch('receiveUpdateVariableError', error); }); }; @@ -105,7 +109,9 @@ export const fetchVariables = ({ dispatch, state }) => { dispatch('receiveVariablesSuccess', prepareDataForDisplay(data.variables)); }) .catch(() => { - createFlash(__('There was an error fetching the variables.')); + createFlash({ + message: __('There was an error fetching the variables.'), + }); }); }; @@ -133,7 +139,9 @@ export const deleteVariable = ({ dispatch, state }) => { dispatch('fetchVariables'); }) .catch((error) => { - createFlash(error.response.data[0]); + createFlash({ + message: error.response.data[0], + }); dispatch('receiveDeleteVariableError', error); }); }; @@ -154,7 +162,9 @@ export const fetchEnvironments = ({ dispatch, state }) => { dispatch('receiveEnvironmentsSuccess', prepareEnvironments(res.data)); }) .catch(() => { - createFlash(__('There was an error fetching the environments information.')); + createFlash({ + message: __('There was an error fetching the environments information.'), + }); }); }; diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js index 8b7c93ad880..cd8212a40f9 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { DEFAULT_REGION } from '../constants'; @@ -102,7 +102,9 @@ export const createClusterSuccess = (_, location) => { export const createClusterError = ({ commit }, error) => { commit(types.CREATE_CLUSTER_ERROR, error); - createFlash(getErrorMessage(error)); + createFlash({ + message: getErrorMessage(error), + }); }; export const setRegion = ({ commit }, payload) => { diff --git a/app/assets/javascripts/deploy_freeze/store/actions.js b/app/assets/javascripts/deploy_freeze/store/actions.js index 56e45595dc5..fed80b46eda 100644 --- a/app/assets/javascripts/deploy_freeze/store/actions.js +++ b/app/assets/javascripts/deploy_freeze/store/actions.js @@ -1,5 +1,5 @@ import Api from '~/api'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; import * as types from './mutation_types'; @@ -26,7 +26,9 @@ const receiveFreezePeriod = (store, request) => { dispatch('fetchFreezePeriods'); }) .catch((error) => { - createFlash(__('Error: Unable to create deploy freeze')); + createFlash({ + message: __('Error: Unable to create deploy freeze'), + }); dispatch('receiveFreezePeriodError', error); }); }; @@ -58,7 +60,9 @@ export const fetchFreezePeriods = ({ commit, state }) => { commit(types.RECEIVE_FREEZE_PERIODS_SUCCESS, data); }) .catch(() => { - createFlash(__('There was an error fetching the deploy freezes.')); + createFlash({ + message: __('There was an error fetching the deploy freezes.'), + }); }); }; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index d0730e18228..bb13ad5f426 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -1,7 +1,7 @@ import Cookies from 'js-cookie'; import Vue from 'vue'; import api from '~/api'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { diffViewerModes } from '~/ide/constants'; import axios from '~/lib/utils/axios_utils'; import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils'; @@ -240,7 +240,10 @@ export const fetchCoverageFiles = ({ commit, state }) => { coveragePoll.stop(); } }, - errorCallback: () => createFlash(__('Something went wrong on our end. Please try again!')), + errorCallback: () => + createFlash({ + message: __('Something went wrong on our end. Please try again!'), + }), }); coveragePoll.makeRequest(); @@ -504,7 +507,11 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => { .then((discussion) => dispatch('assignDiscussionsToDiff', [discussion])) .then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true })) .then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash)) - .catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); + .catch(() => + createFlash({ + message: s__('MergeRequests|Saving the comment failed'), + }), + ); }; export const toggleTreeOpen = ({ commit }, path) => { @@ -595,7 +602,9 @@ export const cacheTreeListWidth = (_, size) => { export const receiveFullDiffError = ({ commit }, filePath) => { commit(types.RECEIVE_FULL_DIFF_ERROR, filePath); - createFlash(s__('MergeRequest|Error loading full diff. Please try again.')); + createFlash({ + message: s__('MergeRequest|Error loading full diff. Please try again.'), + }); }; export const setExpandedDiffLines = ({ commit }, { file, data }) => { @@ -727,7 +736,9 @@ export const setSuggestPopoverDismissed = ({ commit, state }) => commit(types.SET_SHOW_SUGGEST_POPOVER); }) .catch(() => { - createFlash(s__('MergeRequest|Error dismissing suggestion popover. Please try again.')); + createFlash({ + message: s__('MergeRequest|Error dismissing suggestion popover. Please try again.'), + }); }); export function changeCurrentCommit({ dispatch, commit, state }, { commitId }) { diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js index a27ebd16956..fbfcd6ce2df 100644 --- a/app/assets/javascripts/error_tracking/store/actions.js +++ b/app/assets/javascripts/error_tracking/store/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { visitUrl } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import service from '../services'; @@ -17,7 +17,11 @@ export const updateStatus = ({ commit }, { endpoint, redirectUrl, status }) => return resp.data.result; }) - .catch(() => createFlash(__('Failed to update issue status'))); + .catch(() => + createFlash({ + message: __('Failed to update issue status'), + }), + ); export const updateResolveStatus = ({ commit, dispatch }, params) => { commit(types.SET_UPDATING_RESOLVE_STATUS, true); diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js index 7319d45bbd2..09fa650f64b 100644 --- a/app/assets/javascripts/error_tracking/store/details/actions.js +++ b/app/assets/javascripts/error_tracking/store/details/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import Poll from '~/lib/utils/poll'; import { __ } from '~/locale'; import service from '../../services'; @@ -26,7 +26,9 @@ export function startPollingStacktrace({ commit }, endpoint) { }, errorCallback: () => { commit(types.SET_LOADING_STACKTRACE, false); - createFlash(__('Failed to load stacktrace.')); + createFlash({ + message: __('Failed to load stacktrace.'), + }); }, }); diff --git a/app/assets/javascripts/error_tracking/store/list/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js index f07e546241a..418056314f6 100644 --- a/app/assets/javascripts/error_tracking/store/list/actions.js +++ b/app/assets/javascripts/error_tracking/store/list/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import Poll from '~/lib/utils/poll'; import { __ } from '~/locale'; import Service from '../../services'; @@ -33,7 +33,9 @@ export function startPolling({ state, commit, dispatch }) { }, errorCallback: () => { commit(types.SET_LOADING, false); - createFlash(__('Failed to load errors from Sentry.')); + createFlash({ + message: __('Failed to load errors from Sentry.'), + }); }, }); diff --git a/app/assets/javascripts/error_tracking_settings/store/actions.js b/app/assets/javascripts/error_tracking_settings/store/actions.js index 7eb684fb52c..c945a9e2316 100644 --- a/app/assets/javascripts/error_tracking_settings/store/actions.js +++ b/app/assets/javascripts/error_tracking_settings/store/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { refreshCurrentPage } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; @@ -46,7 +46,10 @@ export const requestSettings = ({ commit }) => { export const receiveSettingsError = ({ commit }, { response = {} }) => { const message = response.data && response.data.message ? response.data.message : ''; - createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert'); + createFlash({ + message: `${__('There was an error saving your changes.')} ${message}`, + type: 'alert', + }); commit(types.UPDATE_SETTINGS_LOADING, false); }; diff --git a/app/assets/javascripts/feature_flags/store/edit/actions.js b/app/assets/javascripts/feature_flags/store/edit/actions.js index 72b17333832..54c7e8c4453 100644 --- a/app/assets/javascripts/feature_flags/store/edit/actions.js +++ b/app/assets/javascripts/feature_flags/store/edit/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; @@ -55,7 +55,9 @@ export const receiveFeatureFlagSuccess = ({ commit }, response) => commit(types.RECEIVE_FEATURE_FLAG_SUCCESS, response); export const receiveFeatureFlagError = ({ commit }) => { commit(types.RECEIVE_FEATURE_FLAG_ERROR); - createFlash(__('Something went wrong on our end. Please try again!')); + createFlash({ + message: __('Something went wrong on our end. Please try again!'), + }); }; export const toggleActive = ({ commit }, active) => commit(types.TOGGLE_ACTIVE, active); diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js index e317700b09b..35c79891458 100644 --- a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js +++ b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js @@ -1,6 +1,6 @@ import { __ } from '~/locale'; import AjaxFilter from '../droplab/plugins/ajax_filter'; -import { deprecatedCreateFlash as createFlash } from '../flash'; +import createFlash from '../flash'; import DropdownUtils from './dropdown_utils'; import FilteredSearchDropdown from './filtered_search_dropdown'; import FilteredSearchTokenizer from './filtered_search_tokenizer'; @@ -27,7 +27,9 @@ export default class DropdownAjaxFilter extends FilteredSearchDropdown { searchValueFunction: this.getSearchInput.bind(this), loadingTemplate: this.loadingTemplate, onError() { - createFlash(__('An error occurred fetching the dropdown data.')); + createFlash({ + message: __('An error occurred fetching the dropdown data.'), + }); }, }; } diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js index cde2cd6d6ab..fa6f07edfcf 100644 --- a/app/assets/javascripts/gpg_badges.js +++ b/app/assets/javascripts/gpg_badges.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { parseQueryStringIntoObject } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; @@ -16,7 +16,10 @@ export default class GpgBadges { badges.html('<span class="gl-spinner gl-spinner-orange gl-spinner-sm"></span>'); badges.children().attr('aria-label', __('Loading')); - const displayError = () => createFlash(__('An error occurred while loading commit signatures')); + const displayError = () => + createFlash({ + message: __('An error occurred while loading commit signatures'), + }); const endpoint = tag.data('signaturesPath'); if (!endpoint) { diff --git a/app/assets/javascripts/grafana_integration/store/actions.js b/app/assets/javascripts/grafana_integration/store/actions.js index 7c5d4695731..77d2acd3393 100644 --- a/app/assets/javascripts/grafana_integration/store/actions.js +++ b/app/assets/javascripts/grafana_integration/store/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { refreshCurrentPage } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; @@ -38,5 +38,8 @@ export const receiveGrafanaIntegrationUpdateError = (_, error) => { const { response } = error; const message = response.data && response.data.message ? response.data.message : ''; - createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert'); + createFlash({ + message: `${__('There was an error saving your changes.')} ${message}`, + type: 'alert', + }); }; diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index cafb58b0e2c..f8dc10420d0 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -1,7 +1,7 @@ <script> import { GlModal, GlButton } from '@gitlab/ui'; import { mapActions, mapState, mapGetters } from 'vuex'; -import { deprecatedCreateFlash as flash } from '~/flash'; +import createFlash from '~/flash'; import { __, sprintf, s__ } from '~/locale'; import { modalTypes } from '../../constants'; import { trimPathComponents, getPathParent } from '../../utils'; @@ -57,16 +57,16 @@ export default { if (this.modalType === modalTypes.rename) { if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) { - flash( - sprintf(s__('The name "%{name}" is already taken in this directory.'), { + createFlash({ + message: sprintf(s__('The name "%{name}" is already taken in this directory.'), { name: this.entryName, }), - 'alert', - document, - null, - false, - true, - ); + type: 'alert', + parent: document, + actionConfig: null, + fadeTransition: false, + addBodyClass: true, + }); } else { let parentPath = this.entryName.split('/'); const name = parentPath.pop(); diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 74423cd7376..5e020f16104 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as flash } from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '../../constants'; import service from '../../services'; @@ -34,14 +34,14 @@ export const getMergeRequestsForBranch = ( } }) .catch((e) => { - flash( - __(`Error fetching merge requests for ${branchId}`), - 'alert', - document, - null, - false, - true, - ); + createFlash({ + message: __(`Error fetching merge requests for ${branchId}`), + type: 'alert', + parent: document, + actionConfig: null, + fadeTransition: false, + addBodyClass: true, + }); throw e; }); }; @@ -236,7 +236,7 @@ export const openMergeRequest = async ( await dispatch('openMergeRequestChanges', changes); } catch (e) { - flash(__('Error while loading the merge request. Please try again.')); + createFlash({ message: __('Error while loading the merge request. Please try again.') }); throw e; } }; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js index 6c9be6d10c9..82d9300d30b 100644 --- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js +++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as flash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; import * as terminalService from '../../../../services/terminals'; @@ -26,7 +26,7 @@ export const receiveStartSessionSuccess = ({ commit, dispatch }, data) => { }; export const receiveStartSessionError = ({ dispatch }) => { - flash(messages.UNEXPECTED_ERROR_STARTING); + createFlash({ message: messages.UNEXPECTED_ERROR_STARTING }); dispatch('killSession'); }; @@ -59,7 +59,7 @@ export const receiveStopSessionSuccess = ({ dispatch }) => { }; export const receiveStopSessionError = ({ dispatch }) => { - flash(messages.UNEXPECTED_ERROR_STOPPING); + createFlash({ message: messages.UNEXPECTED_ERROR_STOPPING }); dispatch('killSession'); }; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js index da10894c2c6..7fe1a8cc2df 100644 --- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js +++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as flash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import * as messages from '../messages'; import * as types from '../mutation_types'; @@ -42,7 +42,7 @@ export const receiveSessionStatusSuccess = ({ commit, dispatch }, data) => { }; export const receiveSessionStatusError = ({ dispatch }) => { - flash(messages.UNEXPECTED_ERROR_STATUS); + createFlash({ message: messages.UNEXPECTED_ERROR_STATUS }); dispatch('killSession'); }; diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue index 60cd5bb0a96..e86c418f216 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue @@ -15,7 +15,7 @@ import ImportStatus from '../../components/import_status.vue'; import { STATUSES } from '../../constants'; import addValidationErrorMutation from '../graphql/mutations/add_validation_error.mutation.graphql'; import removeValidationErrorMutation from '../graphql/mutations/remove_validation_error.mutation.graphql'; -import groupQuery from '../graphql/queries/group.query.graphql'; +import groupAndProjectQuery from '../graphql/queries/groupAndProject.query.graphql'; const DEBOUNCE_INTERVAL = 300; @@ -47,21 +47,21 @@ export default { }, apollo: { - existingGroup: { - query: groupQuery, + existingGroupAndProject: { + query: groupAndProjectQuery, debounce: DEBOUNCE_INTERVAL, variables() { return { fullPath: this.fullPath, }; }, - update({ existingGroup }) { + update({ existingGroup, existingProject }) { const variables = { field: 'new_name', sourceGroupId: this.group.id, }; - if (!existingGroup) { + if (!existingGroup && !existingProject) { this.$apollo.mutate({ mutation: removeValidationErrorMutation, variables, @@ -71,7 +71,7 @@ export default { mutation: addValidationErrorMutation, variables: { ...variables, - message: s__('BulkImport|Name already exists.'), + message: this.$options.i18n.NAME_ALREADY_EXISTS, }, }); } @@ -115,6 +115,10 @@ export default { return joinPaths(gon.relative_url_root || '/', this.fullPath); }, }, + + i18n: { + NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'), + }, }; </script> diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/group.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/group.query.graphql deleted file mode 100644 index 52df3581ac4..00000000000 --- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/group.query.graphql +++ /dev/null @@ -1,5 +0,0 @@ -query group($fullPath: ID!) { - existingGroup: group(fullPath: $fullPath) { - id - } -} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql new file mode 100644 index 00000000000..d6124f84025 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql @@ -0,0 +1,9 @@ +query groupAndProject($fullPath: ID!) { + existingGroup: group(fullPath: $fullPath) { + id + } + + existingProject: project(fullPath: $fullPath) { + id + } +} diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js index 33f8dbb8737..5cbc6e85bf3 100644 --- a/app/assets/javascripts/import_entities/import_projects/store/actions.js +++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js @@ -1,5 +1,5 @@ import Visibility from 'visibilityjs'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import httpStatusCodes from '~/lib/utils/http_status'; @@ -75,19 +75,19 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit }) if (hasRedirectInError(e)) { redirectToUrlInError(e); } else if (tooManyRequests(e)) { - createFlash( - sprintf(s__('ImportProjects|%{provider} rate limit exceeded. Try again later'), { + createFlash({ + message: sprintf(s__('ImportProjects|%{provider} rate limit exceeded. Try again later'), { provider: capitalizeFirstCharacter(provider), }), - ); + }); commit(types.RECEIVE_REPOS_ERROR); } else { - createFlash( - sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), { + createFlash({ + message: sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), { provider, }), - ); + }); commit(types.RECEIVE_REPOS_ERROR); } @@ -126,7 +126,9 @@ const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, gett ) : s__('ImportProjects|Importing the project failed'); - createFlash(flashMessage); + createFlash({ + message: flashMessage, + }); commit(types.RECEIVE_IMPORT_ERROR, repoId); }); @@ -149,7 +151,9 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d if (hasRedirectInError(e)) { redirectToUrlInError(e); } else { - createFlash(s__('ImportProjects|Update of imported projects with realtime changes failed')); + createFlash({ + message: s__('ImportProjects|Update of imported projects with realtime changes failed'), + }); } }, }); @@ -175,7 +179,9 @@ const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) = commit(types.RECEIVE_NAMESPACES_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })), ) .catch(() => { - createFlash(s__('ImportProjects|Requesting namespaces failed')); + createFlash({ + message: s__('ImportProjects|Requesting namespaces failed'), + }); commit(types.RECEIVE_NAMESPACES_ERROR); }); diff --git a/app/assets/javascripts/incidents_settings/incidents_settings_service.js b/app/assets/javascripts/incidents_settings/incidents_settings_service.js index 82b94c08381..83fd29a058e 100644 --- a/app/assets/javascripts/incidents_settings/incidents_settings_service.js +++ b/app/assets/javascripts/incidents_settings/incidents_settings_service.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { refreshCurrentPage } from '~/lib/utils/url_utility'; import { ERROR_MSG } from './constants'; @@ -22,7 +22,10 @@ export default class IncidentsSettingsService { .catch(({ response }) => { const message = response?.data?.message || ''; - createFlash(`${ERROR_MSG} ${message}`, 'alert'); + createFlash({ + message: `${ERROR_MSG} ${message}`, + type: 'alert', + }); }); } diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/manual_ordering.js index 540314f8f9b..9613246d6a6 100644 --- a/app/assets/javascripts/manual_ordering.js +++ b/app/assets/javascripts/manual_ordering.js @@ -3,7 +3,7 @@ import { getBoardSortableDefaultOptions, sortableStart, } from '~/boards/mixins/sortable_default_options'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale'; @@ -15,7 +15,9 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) => group_full_path: issueList.dataset.groupFullPath, }) .catch(() => { - createFlash(s__("ManualOrdering|Couldn't save the order of the issues")); + createFlash({ + message: s__("ManualOrdering|Couldn't save the order of the issues"), + }); }); const initManualOrdering = (draggableSelector = 'li.issue') => { diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 1a0156f8c0e..feaf8b0d996 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, no-underscore-dangle, consistent-return */ import $ from 'jquery'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; import eventHub from '~/vue_merge_request_widget/event_hub'; import axios from './lib/utils/axios_utils'; @@ -36,11 +36,11 @@ function MergeRequest(opts) { document.querySelector('#task_status_short').innerText = result.task_status_short; }, onError: () => { - createFlash( - __( + createFlash({ + message: __( 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.', ), - ); + }); }, }); } @@ -93,7 +93,9 @@ MergeRequest.prototype.initMRBtnListeners = function () { }) .catch(() => { draftToggle.removeAttribute('disabled'); - createFlash(__('Something went wrong. Please try again.')); + createFlash({ + message: __('Something went wrong. Please try again.'), + }); }); }); }); @@ -169,7 +171,10 @@ MergeRequest.hideCloseButton = function () { MergeRequest.toggleDraftStatus = function (title, isReady) { if (isReady) { - createFlash(__('The merge request can now be merged.'), 'notice'); + createFlash({ + message: __('The merge request can now be merged.'), + type: 'notice', + }); } const titleEl = document.querySelector('.merge-request .detail-page-description .title'); diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index a0b4fd0b608..215b4b7b2d7 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/browser'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { convertToFixedRange } from '~/lib/utils/datetime_range'; import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils'; @@ -134,15 +134,17 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => { if (state.showErrorBanner) { if (error.response.data && error.response.data.message) { const { message } = error.response.data; - createFlash( - sprintf( + createFlash({ + message: sprintf( s__('Metrics|There was an error while retrieving metrics. %{message}'), { message }, false, ), - ); + }); } else { - createFlash(s__('Metrics|There was an error while retrieving metrics')); + createFlash({ + message: s__('Metrics|There was an error while retrieving metrics'), + }); } } }); @@ -174,7 +176,10 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => { dispatch('fetchDeploymentsData'); if (!state.timeRange) { - createFlash(s__(`Metrics|Invalid time range, please verify.`), 'warning'); + createFlash({ + message: s__(`Metrics|Invalid time range, please verify.`), + type: 'warning', + }); return Promise.reject(); } @@ -202,7 +207,10 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => { }); }) .catch(() => { - createFlash(s__(`Metrics|There was an error while retrieving metrics`), 'warning'); + createFlash({ + message: s__(`Metrics|There was an error while retrieving metrics`), + type: 'warning', + }); }); }; @@ -254,7 +262,9 @@ export const fetchDeploymentsData = ({ state, dispatch }) => { .then((resp) => resp.data) .then((response) => { if (!response || !response.deployments) { - createFlash(s__('Metrics|Unexpected deployment data response from prometheus endpoint')); + createFlash({ + message: s__('Metrics|Unexpected deployment data response from prometheus endpoint'), + }); } dispatch('receiveDeploymentsDataSuccess', response.deployments); @@ -262,7 +272,9 @@ export const fetchDeploymentsData = ({ state, dispatch }) => { .catch((error) => { Sentry.captureException(error); dispatch('receiveDeploymentsDataFailure'); - createFlash(s__('Metrics|There was an error getting deployment information.')); + createFlash({ + message: s__('Metrics|There was an error getting deployment information.'), + }); }); }; export const receiveDeploymentsDataSuccess = ({ commit }, data) => { @@ -290,9 +302,11 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => { ) .then((environments) => { if (!environments) { - createFlash( - s__('Metrics|There was an error fetching the environments data, please try again'), - ); + createFlash({ + message: s__( + 'Metrics|There was an error fetching the environments data, please try again', + ), + }); } dispatch('receiveEnvironmentsDataSuccess', environments); @@ -300,7 +314,9 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => { .catch((err) => { Sentry.captureException(err); dispatch('receiveEnvironmentsDataFailure'); - createFlash(s__('Metrics|There was an error getting environments information.')); + createFlash({ + message: s__('Metrics|There was an error getting environments information.'), + }); }); }; export const requestEnvironmentsData = ({ commit }) => { @@ -332,7 +348,9 @@ export const fetchAnnotations = ({ state, dispatch, getters }) => { .then(parseAnnotationsResponse) .then((annotations) => { if (!annotations) { - createFlash(s__('Metrics|There was an error fetching annotations. Please try again.')); + createFlash({ + message: s__('Metrics|There was an error fetching annotations. Please try again.'), + }); } dispatch('receiveAnnotationsSuccess', annotations); @@ -340,7 +358,9 @@ export const fetchAnnotations = ({ state, dispatch, getters }) => { .catch((err) => { Sentry.captureException(err); dispatch('receiveAnnotationsFailure'); - createFlash(s__('Metrics|There was an error getting annotations information.')); + createFlash({ + message: s__('Metrics|There was an error getting annotations information.'), + }); }); }; @@ -377,9 +397,11 @@ export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) = .catch((err) => { Sentry.captureException(err); dispatch('receiveDashboardValidationWarningsFailure'); - createFlash( - s__('Metrics|There was an error getting dashboard validation warnings information.'), - ); + createFlash({ + message: s__( + 'Metrics|There was an error getting dashboard validation warnings information.', + ), + }); }); }; @@ -480,11 +502,14 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery commit(types.UPDATE_VARIABLE_METRIC_LABEL_VALUES, { variable, label, data }); }) .catch(() => { - createFlash( - sprintf(s__('Metrics|There was an error getting options for variable "%{name}".'), { - name: variable.name, - }), - ); + createFlash({ + message: sprintf( + s__('Metrics|There was an error getting options for variable "%{name}".'), + { + name: variable.name, + }, + ), + }); }); optionsRequests.push(optionsRequest); } diff --git a/app/assets/javascripts/notes/mixins/diff_line_note_form.js b/app/assets/javascripts/notes/mixins/diff_line_note_form.js index 76342e07c04..7b9c0959464 100644 --- a/app/assets/javascripts/notes/mixins/diff_line_note_form.js +++ b/app/assets/javascripts/notes/mixins/diff_line_note_form.js @@ -1,7 +1,7 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import { getDraftReplyFormData, getDraftFormData } from '~/batch_comments/utils'; import { TEXT_DIFF_POSITION_TYPE, IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { clearDraft } from '~/lib/utils/autosave'; import { s__ } from '~/locale'; import { formatLineRange } from '~/notes/components/multiline_comment_utils'; @@ -42,7 +42,9 @@ export default { this.handleClearForm(this.discussion.line_code); }) .catch(() => { - createFlash(s__('MergeRequests|An error occurred while saving the draft comment.')); + createFlash({ + message: s__('MergeRequests|An error occurred while saving the draft comment.'), + }); }); }, addToReview(note) { @@ -80,7 +82,9 @@ export default { } }) .catch(() => { - createFlash(s__('MergeRequests|An error occurred while saving the draft comment.')); + createFlash({ + message: s__('MergeRequests|An error occurred while saving the draft comment.'), + }); }); }, handleClearForm(lineCode) { diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js index af66e344b35..969904bc6d0 100644 --- a/app/assets/javascripts/operation_settings/store/actions.js +++ b/app/assets/javascripts/operation_settings/store/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { refreshCurrentPage } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; @@ -35,5 +35,8 @@ export const receiveSaveChangesError = (_, error) => { const { response = {} } = error; const message = response.data && response.data.message ? response.data.message : ''; - createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert'); + createFlash({ + message: `${__('There was an error saving your changes.')} ${message}`, + type: 'alert', + }); }; diff --git a/app/assets/javascripts/packages/list/stores/actions.js b/app/assets/javascripts/packages/list/stores/actions.js index 8dfe3c82ab3..81f587971c2 100644 --- a/app/assets/javascripts/packages/list/stores/actions.js +++ b/app/assets/javascripts/packages/list/stores/actions.js @@ -1,5 +1,5 @@ import Api from '~/api'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants'; import { @@ -43,7 +43,9 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => { dispatch('receivePackagesListSuccess', { data, headers }); }) .catch(() => { - createFlash(FETCH_PACKAGES_LIST_ERROR_MESSAGE); + createFlash({ + message: FETCH_PACKAGES_LIST_ERROR_MESSAGE, + }); }) .finally(() => { dispatch('setLoading', false); @@ -52,7 +54,9 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => { export const requestDeletePackage = ({ dispatch, state }, { _links }) => { if (!_links || !_links.delete_api_path) { - createFlash(DELETE_PACKAGE_ERROR_MESSAGE); + createFlash({ + message: DELETE_PACKAGE_ERROR_MESSAGE, + }); const error = new Error(MISSING_DELETE_PATH_ERROR); return Promise.reject(error); } @@ -65,10 +69,15 @@ export const requestDeletePackage = ({ dispatch, state }, { _links }) => { const page = getNewPaginationPage(currentPage, perPage, total - 1); dispatch('requestPackagesList', { page }); - createFlash(DELETE_PACKAGE_SUCCESS_MESSAGE, 'success'); + createFlash({ + message: DELETE_PACKAGE_SUCCESS_MESSAGE, + type: 'success', + }); }) .catch(() => { dispatch('setLoading', false); - createFlash(DELETE_PACKAGE_ERROR_MESSAGE); + createFlash({ + message: DELETE_PACKAGE_ERROR_MESSAGE, + }); }); }; diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js index b5441127797..226ef4c4e23 100644 --- a/app/assets/javascripts/pages/profiles/show/index.js +++ b/app/assets/javascripts/pages/profiles/show/index.js @@ -2,7 +2,7 @@ import emojiRegex from 'emoji-regex'; import $ from 'jquery'; import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; import * as Emoji from '~/emoji'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; import EmojiMenu from './emoji_menu'; @@ -81,4 +81,8 @@ Emoji.initEmojiMap() } }); }) - .catch(() => createFlash(__('Failed to load emoji list.'))); + .catch(() => + createFlash({ + message: __('Failed to load emoji list.'), + }), + ); diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js index d9c9289f66e..082d67c938c 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js @@ -1,5 +1,5 @@ import Visibility from 'visibilityjs'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; import Poll from '~/lib/utils/poll'; import { __ } from '~/locale'; @@ -169,7 +169,11 @@ export default { this.service .postAction(endpoint) .then(() => this.updateTable()) - .catch(() => createFlash(__('An error occurred while making the request.'))); + .catch(() => + createFlash({ + message: __('An error occurred while making the request.'), + }), + ); }, /** @@ -189,9 +193,11 @@ export default { .runMRPipeline(options) .then(() => this.updateTable()) .catch(() => { - createFlash( - __('An error occurred while trying to run a new pipeline for this merge request.'), - ); + createFlash({ + message: __( + 'An error occurred while trying to run a new pipeline for this merge request.', + ), + }); }) .finally(() => this.store.toggleIsRunningPipeline(false)); }, diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js index 6de345233ae..7b28d48b5b6 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale'; import * as types from './mutation_types'; @@ -12,7 +12,9 @@ export const fetchSummary = ({ state, commit, dispatch }) => { commit(types.SET_SUMMARY, data); }) .catch(() => { - createFlash(s__('TestReports|There was an error fetching the summary.')); + createFlash({ + message: s__('TestReports|There was an error fetching the summary.'), + }); }) .finally(() => { dispatch('toggleLoading'); @@ -36,7 +38,9 @@ export const fetchTestSuite = ({ state, commit, dispatch }, index) => { .get(state.suiteEndpoint, { params: { build_ids } }) .then(({ data }) => commit(types.SET_SUITE, { suite: data, index })) .catch(() => { - createFlash(s__('TestReports|There was an error fetching the test suite.')); + createFlash({ + message: s__('TestReports|There was an error fetching the test suite.'), + }); }) .finally(() => { dispatch('toggleLoading'); diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue index f18c4d8f03e..7917a9a75e0 100644 --- a/app/assets/javascripts/profile/account/components/update_username.vue +++ b/app/assets/javascripts/profile/account/components/update_username.vue @@ -1,7 +1,7 @@ <script> import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; import { escape } from 'lodash'; -import { deprecatedCreateFlash as Flash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { s__, sprintf } from '~/locale'; @@ -85,15 +85,16 @@ Please update your Git repository remotes as soon as possible.`), return axios .put(this.actionUrl, putData) .then((result) => { - Flash(result.data.message, 'notice'); + createFlash({ message: result.data.message, type: 'notice' }); this.username = username; this.isRequestPending = false; }) .catch((error) => { - Flash( - error?.response?.data?.message || + createFlash({ + message: + error?.response?.data?.message || s__('Profiles|An error occurred while updating your username, please try again.'), - ); + }); this.isRequestPending = false; throw error; }); diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js index 741dc20b1f1..795c293d14b 100644 --- a/app/assets/javascripts/projects/commits/store/actions.js +++ b/app/assets/javascripts/projects/commits/store/actions.js @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/browser'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { joinPaths } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; @@ -13,7 +13,9 @@ export default { commit(types.COMMITS_AUTHORS, authors); }, receiveAuthorsError() { - createFlash(__('An error occurred fetching the project authors.')); + createFlash({ + message: __('An error occurred fetching the project authors.'), + }); }, fetchAuthors({ dispatch, state }, author = null) { const { projectId } = state; diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue index c35a1ff0b63..7e2fda8495c 100644 --- a/app/assets/javascripts/related_issues/components/related_issues_root.vue +++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue @@ -23,7 +23,7 @@ Your caret can stop touching a `rawReference` can happen in a variety of ways: and hide the `AddIssuableForm` area. */ -import { deprecatedCreateFlash as Flash } from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; import { relatedIssuesRemoveErrorMap, @@ -122,11 +122,11 @@ export default { }) .catch((res) => { if (res && res.status !== 404) { - Flash(relatedIssuesRemoveErrorMap[this.issuableType]); + createFlash({ message: relatedIssuesRemoveErrorMap[this.issuableType] }); } }); } else { - Flash(pathIndeterminateErrorMap[this.issuableType]); + createFlash({ message: pathIndeterminateErrorMap[this.issuableType] }); } }, onToggleAddRelatedIssuesForm() { @@ -155,7 +155,7 @@ export default { if (response && response.data && response.data.message) { errorMessage = response.data.message; } - Flash(errorMessage); + createFlash({ message: errorMessage }); }) .finally(() => { this.isSubmitting = false; @@ -176,7 +176,7 @@ export default { }) .catch(() => { this.store.setRelatedIssues([]); - Flash(__('An error occurred while fetching issues.')); + createFlash({ message: __('An error occurred while fetching issues.') }); }) .finally(() => { this.isFetching = false; @@ -197,7 +197,7 @@ export default { } }) .catch(() => { - Flash(__('An error occurred while reordering issues.')); + createFlash({ message: __('An error occurred while reordering issues.') }); }); } }, diff --git a/app/assets/javascripts/related_merge_requests/store/actions.js b/app/assets/javascripts/related_merge_requests/store/actions.js index e9f0793a350..652d03a0fd0 100644 --- a/app/assets/javascripts/related_merge_requests/store/actions.js +++ b/app/assets/javascripts/related_merge_requests/store/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { normalizeHeaders } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; @@ -29,6 +29,8 @@ export const fetchMergeRequests = ({ state, dispatch }) => { }) .catch(() => { dispatch('receiveDataError'); - createFlash(s__('Something went wrong while fetching related merge requests.')); + createFlash({ + message: s__('Something went wrong while fetching related merge requests.'), + }); }); }; diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js index b312c2a7506..5955ec3352e 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { redirectTo } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql'; @@ -39,7 +39,9 @@ export const fetchRelease = async ({ commit, state }) => { commit(types.RECEIVE_RELEASE_SUCCESS, release); } catch (error) { commit(types.RECEIVE_RELEASE_ERROR, error); - createFlash(s__('Release|Something went wrong while getting the release details.')); + createFlash({ + message: s__('Release|Something went wrong while getting the release details.'), + }); } }; @@ -124,7 +126,9 @@ export const createRelease = async ({ commit, dispatch, state, getters }) => { dispatch('receiveSaveReleaseSuccess', response.data.releaseCreate.release.links.selfUrl); } catch (error) { commit(types.RECEIVE_SAVE_RELEASE_ERROR, error); - createFlash(s__('Release|Something went wrong while creating a new release.')); + createFlash({ + message: s__('Release|Something went wrong while creating a new release.'), + }); } }; @@ -214,6 +218,8 @@ export const updateRelease = async ({ commit, dispatch, state, getters }) => { dispatch('receiveSaveReleaseSuccess', state.release._links.self); } catch (error) { commit(types.RECEIVE_SAVE_RELEASE_ERROR, error); - createFlash(s__('Release|Something went wrong while saving the release details.')); + createFlash({ + message: s__('Release|Something went wrong while saving the release details.'), + }); } }; diff --git a/app/assets/javascripts/releases/stores/modules/index/actions.js b/app/assets/javascripts/releases/stores/modules/index/actions.js index 00be25f089b..d3bb11cab30 100644 --- a/app/assets/javascripts/releases/stores/modules/index/actions.js +++ b/app/assets/javascripts/releases/stores/modules/index/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; import { PAGE_SIZE } from '~/releases/constants'; import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql'; @@ -57,7 +57,9 @@ export const fetchReleases = ({ dispatch, commit, state }, { before, after }) => export const receiveReleasesError = ({ commit }) => { commit(types.RECEIVE_RELEASES_ERROR); - createFlash(__('An error occurred while fetching the releases. Please try again.')); + createFlash({ + message: __('An error occurred while fetching the releases. Please try again.'), + }); }; export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data); diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js index a6c0380a789..166cd796680 100644 --- a/app/assets/javascripts/serverless/store/actions.js +++ b/app/assets/javascripts/serverless/store/actions.js @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { backOff } from '~/lib/utils/common_utils'; import statusCodes from '~/lib/utils/http_status'; @@ -59,7 +59,9 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => { .then((data) => { if (data === TIMEOUT) { dispatch('receiveFunctionsTimeout'); - createFlash(__('Loading functions timed out. Please reload the page to try again.')); + createFlash({ + message: __('Loading functions timed out. Please reload the page to try again.'), + }); } else if (data.functions !== null && data.functions.length) { dispatch('receiveFunctionsSuccess', data); } else { @@ -68,7 +70,9 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => { }) .catch((error) => { dispatch('receiveFunctionsError', error); - createFlash(error); + createFlash({ + message: error, + }); }); }; @@ -120,6 +124,8 @@ export const fetchMetrics = ({ dispatch }, { metricsPath, hasPrometheus }) => { }) .catch((error) => { dispatch('receiveMetricsError', error); - createFlash(error); + createFlash({ + message: error, + }); }); }; diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 2c4928fc338..d2841156e55 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -3,7 +3,7 @@ import $ from 'jquery'; import { spriteIcon } from '~/lib/utils/common_utils'; import FilesCommentButton from './files_comment_button'; -import { deprecatedCreateFlash as createFlash } from './flash'; +import createFlash from './flash'; import initImageDiffHelper from './image_diff/helpers/init_image_diff'; import axios from './lib/utils/axios_utils'; import { __ } from './locale'; @@ -95,7 +95,9 @@ export default class SingleFileDiff { if (cb) cb(); }) .catch(() => { - createFlash(__('An error occurred while retrieving diff')); + createFlash({ + message: __('An error occurred while retrieving diff'), + }); }); } } diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue index 4fb27397039..612b4c7d2e3 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue @@ -1,7 +1,7 @@ <script> import { GlLoadingIcon } from '@gitlab/ui'; import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue'; -import { deprecatedCreateFlash as Flash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { getBaseURL, joinPaths } from '~/lib/utils/url_utility'; import { sprintf } from '~/locale'; @@ -63,7 +63,7 @@ export default { .catch((e) => this.flashAPIFailure(e)); }, flashAPIFailure(err) { - Flash(sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err })); + createFlash({ message: sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err }) }); }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js index 4dfc61f1fff..f4317ba90a2 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js @@ -1,5 +1,5 @@ import Api from '~/api'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import * as types from './mutation_types'; @@ -24,7 +24,9 @@ export function fetchBranches({ commit, state }, search = '') { .catch(({ response }) => { const { status } = response; commit(types.RECEIVE_BRANCHES_ERROR, status); - createFlash(__('Failed to load branches. Please try again.')); + createFlash({ + message: __('Failed to load branches. Please try again.'), + }); }); } @@ -41,7 +43,9 @@ export const fetchMilestones = ({ commit, state }, search_title = '') => { .catch(({ response }) => { const { status } = response; commit(types.RECEIVE_MILESTONES_ERROR, status); - createFlash(__('Failed to load milestones. Please try again.')); + createFlash({ + message: __('Failed to load milestones. Please try again.'), + }); }); }; @@ -57,7 +61,9 @@ export const fetchLabels = ({ commit, state }, search = '') => { .catch(({ response }) => { const { status } = response; commit(types.RECEIVE_LABELS_ERROR, status); - createFlash(__('Failed to load labels. Please try again.')); + createFlash({ + message: __('Failed to load labels. Please try again.'), + }); }); }; @@ -80,7 +86,9 @@ function fetchUser(options = {}) { .catch(({ response }) => { const { status } = response; commit(`RECEIVE_${action}_ERROR`, status); - createFlash(errorMessage); + createFlash({ + message: errorMessage, + }); }); } diff --git a/config/feature_flags/development/prevent_retry_of_retried_jobs.yml b/config/feature_flags/development/prevent_retry_of_retried_jobs.yml index 0179f84f92a..f5b113eb8c7 100644 --- a/config/feature_flags/development/prevent_retry_of_retried_jobs.yml +++ b/config/feature_flags/development/prevent_retry_of_retried_jobs.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331695 milestone: '14.0' type: development group: group::continuous integration -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/redirect_to_latest_template_jobs_browser_performance_testing.yml b/config/feature_flags/development/redirect_to_latest_template_jobs_browser_performance_testing.yml new file mode 100644 index 00000000000..37c475067a3 --- /dev/null +++ b/config/feature_flags/development/redirect_to_latest_template_jobs_browser_performance_testing.yml @@ -0,0 +1,8 @@ +--- +name: redirect_to_latest_template_jobs_browser_performance_testing +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144 +rollout_issue_url: +milestone: '14.0' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/feature_flags/development/redirect_to_latest_template_jobs_deploy.yml b/config/feature_flags/development/redirect_to_latest_template_jobs_deploy.yml new file mode 100644 index 00000000000..d113531c30c --- /dev/null +++ b/config/feature_flags/development/redirect_to_latest_template_jobs_deploy.yml @@ -0,0 +1,8 @@ +--- +name: redirect_to_latest_template_jobs_deploy +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332660 +milestone: '14.0' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/feature_flags/development/redirect_to_latest_template_security_api_fuzzing.yml b/config/feature_flags/development/redirect_to_latest_template_security_api_fuzzing.yml new file mode 100644 index 00000000000..96606515bda --- /dev/null +++ b/config/feature_flags/development/redirect_to_latest_template_security_api_fuzzing.yml @@ -0,0 +1,8 @@ +--- +name: redirect_to_latest_template_security_api_fuzzing +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144 +rollout_issue_url: +milestone: '14.0' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/feature_flags/development/redirect_to_latest_template_security_dast.yml b/config/feature_flags/development/redirect_to_latest_template_security_dast.yml new file mode 100644 index 00000000000..a95c1e1a045 --- /dev/null +++ b/config/feature_flags/development/redirect_to_latest_template_security_dast.yml @@ -0,0 +1,8 @@ +--- +name: redirect_to_latest_template_security_dast +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144 +rollout_issue_url: +milestone: '14.0' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/feature_flags/development/redirect_to_latest_template_terraform.yml b/config/feature_flags/development/redirect_to_latest_template_terraform.yml new file mode 100644 index 00000000000..cb5d833fa2d --- /dev/null +++ b/config/feature_flags/development/redirect_to_latest_template_terraform.yml @@ -0,0 +1,8 @@ +--- +name: redirect_to_latest_template_terraform +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144 +rollout_issue_url: +milestone: '14.0' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/feature_flags/development/redirect_to_latest_template_verify_browser_performance.yml b/config/feature_flags/development/redirect_to_latest_template_verify_browser_performance.yml new file mode 100644 index 00000000000..4df74a5b07a --- /dev/null +++ b/config/feature_flags/development/redirect_to_latest_template_verify_browser_performance.yml @@ -0,0 +1,8 @@ +--- +name: redirect_to_latest_template_verify_browser_performance +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144 +rollout_issue_url: +milestone: '14.0' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/doc/administration/redis/replication_and_failover.md b/doc/administration/redis/replication_and_failover.md index 20a9fbd7d68..9fde91903e8 100644 --- a/doc/administration/redis/replication_and_failover.md +++ b/doc/administration/redis/replication_and_failover.md @@ -42,7 +42,7 @@ There should be no more than one Sentinel on the same machine though. You also need to take into consideration the underlying network topology, making sure you have redundant connectivity between Redis / Sentinel and -GitLab instances, otherwise the networks will become a single point of +GitLab instances, otherwise the networks become a single point of failure. Running Redis in a scaled environment requires a few things: @@ -73,7 +73,7 @@ whole cluster down, invalidating the failover effort. ## Recommended setup -For a minimal setup, you will install the Omnibus GitLab package in `3` +For a minimal setup, you need to install the Omnibus GitLab package in `3` **independent** machines, both with **Redis** and **Sentinel**: - Redis Primary + Sentinel @@ -84,7 +84,7 @@ If you are not sure or don't understand why and where the amount of nodes come from, read [Redis setup overview](#redis-setup-overview) and [Sentinel setup overview](#sentinel-setup-overview). -For a recommended setup that can resist more failures, you will install +For a recommended setup that can resist more failures, you need to install the Omnibus GitLab package in `5` **independent** machines, both with **Redis** and **Sentinel**: @@ -99,9 +99,9 @@ the Omnibus GitLab package in `5` **independent** machines, both with You must have at least `3` Redis servers: `1` primary, `2` Replicas, and they need to each be on independent machines (see explanation above). -You can have additional Redis nodes, that will help survive a situation +You can have additional Redis nodes, that helps to survive a situation where more nodes goes down. Whenever there is only `2` nodes online, a failover -will not be initiated. +is not initiated. As an example, if you have `6` Redis nodes, a maximum of `3` can be simultaneously down. @@ -117,7 +117,7 @@ in a failover situation, any **Replica** can be promoted as the new **Primary** the Sentinel servers. The replication requires authentication, so you need to define a password to -protect all Redis nodes and the Sentinels. They will all share the same +protect all Redis nodes and the Sentinels. All of them share the same password, and all instances must be able to talk to each other over the network. @@ -130,7 +130,7 @@ of Sentinels agreeing a node is down) to be able to start a failover. Whenever the **quorum** is met, the **majority** of all known Sentinel nodes need to be available and reachable, so that they can elect the Sentinel **leader** -who will take all the decisions to restore the service availability by: +who takes all the decisions to restore the service availability by: - Promoting a new **Primary** - Reconfiguring the other **Replicas** and make them point to the new **Primary** @@ -150,7 +150,7 @@ consensus algorithm to be effective in the case of a failure. In a `3` nodes topology, you can only afford `1` Sentinel node going down. Whenever the **majority** of the Sentinels goes down, the network partition -protection prevents destructive actions and a failover **will not be started**. +protection prevents destructive actions and a failover **is not started**. Here are some examples: @@ -159,11 +159,11 @@ Here are some examples: The **Leader** election can sometimes fail the voting round when **consensus** is not achieved (see the odd number of nodes requirement above). In that case, -a new attempt will be made after the amount of time defined in +a new attempt is made after the amount of time defined in `sentinel['failover_timeout']` (in milliseconds). NOTE: -We will see where `sentinel['failover_timeout']` is defined later. +We can see where `sentinel['failover_timeout']` is defined later. The `failover_timeout` variable has a lot of different use cases. According to the official documentation: @@ -183,7 +183,7 @@ the official documentation: - The maximum time a failover in progress waits for all the replicas to be reconfigured as replicas of the new primary. However even after this time - the replicas will be reconfigured by the Sentinels anyway, but not with + the replicas are reconfigured by the Sentinels anyway, but not with the exact parallel-syncs progression as specified. ## Configuring Redis @@ -195,7 +195,7 @@ If you already have Redis installed and running, read how to [switch from a single-machine installation](#switching-from-an-existing-single-machine-installation). NOTE: -Redis nodes (both primary and replica) will need the same password defined in +Redis nodes (both primary and replica) need the same password defined in `redis['password']`. At any time during a failover the Sentinels can reconfigure a node and change its status from primary to replica and vice versa. @@ -218,14 +218,14 @@ The requirements for a Redis setup are the following: ### Switching from an existing single-machine installation -If you already have a single-machine GitLab install running, you will need to +If you already have a single-machine GitLab install running, you need to replicate from this machine first, before de-activating the Redis instance inside it. -Your single-machine install will be the initial **Primary**, and the `3` others +Your single-machine install is the initial **Primary**, and the `3` others should be configured as **Replica** pointing to this machine. -After replication catches up, you will need to stop services in the +After replication catches up, you need to stop services in the single-machine install, to rotate the **Primary** to one of the new nodes. Make the required changes in configuration and restart the new nodes again. @@ -259,7 +259,7 @@ If you fail to replicate first, you may loose data (unprocessed background jobs) # sure you add extra firewall rules to prevent unauthorized access. redis['bind'] = '10.0.0.1' - # Define a port so Redis can listen for TCP requests which will allow other + # Define a port so Redis can listen for TCP requests which allows other # machines to connect to it. redis['port'] = 6379 @@ -303,7 +303,7 @@ Read more about [roles](https://docs.gitlab.com/omnibus/roles/). # sure you add extra firewall rules to prevent unauthorized access. redis['bind'] = '10.0.0.2' - # Define a port so Redis can listen for TCP requests which will allow other + # Define a port so Redis can listen for TCP requests which allows other # machines to connect to it. redis['port'] = 6379 @@ -333,8 +333,8 @@ You can specify multiple roles like sentinel and Redis as: Read more about [roles](https://docs.gitlab.com/omnibus/roles/). These values don't have to be changed again in `/etc/gitlab/gitlab.rb` after -a failover, as the nodes will be managed by the Sentinels, and even after a -`gitlab-ctl reconfigure`, they will get their configuration restored by +a failover, as the nodes are managed by the Sentinels, and even after a +`gitlab-ctl reconfigure`, they get their configuration restored by the same Sentinels. ### Step 3. Configuring the Redis Sentinel instances @@ -342,7 +342,7 @@ the same Sentinels. NOTE: If you are using an external Redis Sentinel instance, be sure to exclude the `requirepass` parameter from the Sentinel -configuration. This parameter will cause clients to report `NOAUTH +configuration. This parameter causes clients to report `NOAUTH Authentication required.`. [Redis Sentinel 3.2.x does not support password authentication](https://github.com/antirez/redis/issues/3279). @@ -362,8 +362,8 @@ multiple machines with the Sentinel daemon. --- -1. SSH into the server that will host Redis Sentinel. -1. **You can omit this step if the Sentinels will be hosted in the same node as +1. SSH into the server that hosts Redis Sentinel. +1. **You can omit this step if the Sentinels is hosted in the same node as the other Redis instances.** [Download/install](https://about.gitlab.com/install/) the @@ -389,7 +389,7 @@ multiple machines with the Sentinel daemon. # The IP of the primary Redis node. redis['master_ip'] = '10.0.0.1' - # Define a port so Redis can listen for TCP requests which will allow other + # Define a port so Redis can listen for TCP requests which allows other # machines to connect to it. redis['port'] = 6379 @@ -437,7 +437,7 @@ multiple machines with the Sentinel daemon. ## ## - The maximum time a failover in progress waits for all the replica to be ## reconfigured as replicas of the new primary. However even after this time - ## the replicas will be reconfigured by the Sentinels anyway, but not with + ## the replicas are reconfigured by the Sentinels anyway, but not with ## the exact parallel-syncs progression as specified. # sentinel['failover_timeout'] = 60000 ``` @@ -511,7 +511,7 @@ If you enable Monitoring, it must be enabled on **all** Redis servers. retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z), } - # Set the network addresses that the exporters will listen on + # Set the network addresses that the exporters listen on node_exporter['listen_address'] = '0.0.0.0:9100' redis_exporter['listen_address'] = '0.0.0.0:9121' ``` @@ -528,7 +528,7 @@ In a real world usage, you would also set up firewall rules to prevent unauthorized access from other machines and block traffic from the outside (Internet). -We will use the same `3` nodes with **Redis** + **Sentinel** topology +We use the same `3` nodes with **Redis** + **Sentinel** topology discussed in [Redis setup overview](#redis-setup-overview) and [Sentinel setup overview](#sentinel-setup-overview) documentation. @@ -540,11 +540,11 @@ Here is a list and description of each **machine** and the assigned **IP**: - `10.0.0.4`: GitLab application Please note that after the initial configuration, if a failover is initiated -by the Sentinel nodes, the Redis nodes will be reconfigured and the **Primary** -will change permanently (including in `redis.conf`) from one node to the other, +by the Sentinel nodes, the Redis nodes are reconfigured and the **Primary** +changes permanently (including in `redis.conf`) from one node to the other, until a new failover is initiated again. -The same thing will happen with `sentinel.conf` that will be overridden after the +The same thing happens with `sentinel.conf` that is overridden after the initial execution, after any new sentinel node starts watching the **Primary**, or a failover promotes a different **Primary** node. @@ -691,7 +691,7 @@ To make this work with Sentinel: ``` NOTE: -For each persistence class, GitLab will default to using the +For each persistence class, GitLab defaults to using the configuration specified in `gitlab_rails['redis_sentinels']` unless overridden by the previously described settings. @@ -726,7 +726,7 @@ redis_replica_role['enable'] = true # enable only one of them # When Redis primary or Replica role are enabled, the following services are # enabled/disabled. Note that if Redis and Sentinel roles are combined, both -# services will be enabled. +# services are enabled. # The following services are disabled sentinel['enable'] = false diff --git a/doc/api/group_protected_environments.md b/doc/api/group_protected_environments.md new file mode 100644 index 00000000000..d4e27a7200a --- /dev/null +++ b/doc/api/group_protected_environments.md @@ -0,0 +1,154 @@ +--- +stage: Release +group: Release +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +type: concepts, howto +--- + +# Group-level protected environments API **(PREMIUM)** + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215888) in [GitLab Premium](https://about.gitlab.com/pricing/) 14.0. +> - [Deployed behind a feature flag](../user/feature_flags.md), disabled by default. +> - Disabled on GitLab.com. +> - Not recommended for production use. +> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](../ci/environments/protected_environments.md#enable-or-disable-group-level-protected-environments). **(FREE SELF)** + +This in-development feature might not be available for your use. There can be +[risks when enabling features still in development](../user/feature_flags.md#risks-when-enabling-features-still-in-development). +Refer to this feature's version history for more details. + +Read more about [group-level protected environments](../ci/environments/protected_environments.md#group-level-protected-environments), + +## Valid access levels + +The access levels are defined in the `ProtectedEnvironment::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method. +Currently, these levels are recognized: + +```plaintext +30 => Developer access +40 => Maintainer access +60 => Admin access +``` + +## List group-level protected environments + +Gets a list of protected environments from a group. + +```shell +GET /groups/:id/protected_environments +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) maintained by the authenticated user. | + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/protected_environments/" +``` + +Example response: + +```json +[ + { + "name":"production", + "deploy_access_levels":[ + { + "access_level":40, + "access_level_description":"Maintainers", + "user_id":null, + "group_id":null + } + ] + } +] +``` + +## Get a single protected environment + +Gets a single protected environment. + +```shell +GET /groups/:id/protected_environments/:name +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) maintained by the authenticated user. | +| `name` | string | yes | The deployment tier of the protected environment. One of `production`, `staging`, `testing`, `development`, or `other`. Read more about [deployment tiers](../ci/environments/index.md#deployment-tier-of-environments).| + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/protected_environments/production" +``` + +Example response: + +```json +{ + "name":"production", + "deploy_access_levels":[ + { + "access_level":40, + "access_level_description":"Maintainers", + "user_id":null, + "group_id":null + } + ] +} +``` + +## Protect an environment + +Protects a single environment. + +```shell +POST /groups/:id/protected_environments +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) maintained by the authenticated user. | +| `name` | string | yes | The deployment tier of the protected environment. One of `production`, `staging`, `testing`, `development`, or `other`. Read more about [deployment tiers](../ci/environments/index.md#deployment-tier-of-environments).| +| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. | + +The assignable `user_id` are the users who belong to the given group with the Maintainer role (or above). +The assignable `group_id` are the sub-groups under the given group. + +```shell +curl --header 'Content-Type: application/json' --request POST --data '{"name": "production", "deploy_access_levels": [{"group_id": 9899826}]}' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22034114/protected_environments" +``` + +Example response: + +```json +{ + "name":"production", + "deploy_access_levels":[ + { + "access_level":40, + "access_level_description":"protected-access-group", + "user_id":null, + "group_id":9899826 + } + ] +} +``` + +## Unprotect environment + +Unprotects the given protected environment. + +```shell +DELETE /groups/:id/protected_environments/:name +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) maintained by the authenticated user. | +| `name` | string | yes | The deployment tier of the protected environment. One of `production`, `staging`, `testing`, `development`, or `other`. Read more about [deployment tiers](../ci/environments/index.md#deployment-tier-of-environments).| + +```shell +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/protected_environments/staging" +``` + +The response should return a 200 code. diff --git a/doc/ci/environments/protected_environments.md b/doc/ci/environments/protected_environments.md index b39c9fa2196..a6c9e4bd38e 100644 --- a/doc/ci/environments/protected_environments.md +++ b/doc/ci/environments/protected_environments.md @@ -154,6 +154,129 @@ be re-entered if the environment is re-protected. For more information, see [Deployment safety](deployment_safety.md). +## Group-level protected environments + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215888) in [GitLab Premium](https://about.gitlab.com/pricing/) 14.0. +> - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default. +> - Disabled on GitLab.com. +> - Not recommended for production use. +> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-group-level-protected-environments). **(FREE SELF)** + +This in-development feature might not be available for your use. There can be +[risks when enabling features still in development](../../user/feature_flags.md#risks-when-enabling-features-still-in-development). +Refer to this feature's version history for more details. + +Typically, large enterprise organizations have an explicit permission boundary +between [developers and operators](https://about.gitlab.com/topics/devops/). +Developers build and test their code, and operators deploy and monitor the +application. With group-level protected environments, the permission of each +group is carefully configured in order to prevent unauthorized access and +maintain proper separation of duty. Group-level protected environments +extend the [project-level protected environments](#protecting-environments) +to the group-level. + +The permissions of deployments can be illustrated in the following table: + +| Environment | Developer | Operator | Category | +|-------------|------------|----------|----------| +| Development | Allowed | Allowed | Lower environment | +| Testing | Allowed | Allowed | Lower environment | +| Staging | Disallowed | Allowed | Higher environment | +| Production | Disallowed | Allowed | Higher environment | + +_(Reference: [Deployment environments on Wikipedia](https://en.wikipedia.org/wiki/Deployment_environment))_ + +### Group-level protected environments names + +Contrary to project-level protected environments, group-level protected +environments use the [deployment tier](index.md#deployment-tier-of-environments) +as their name. + +A group may consist of many project environments that have unique names. +For example, Project-A has a `gprd` environment and Project-B has a `Production` +environment, so protecting a specific environment name doesn't scale well. +By using deployment tiers, both are recognized as `production` deployment tier +and are protected at the same time. + +### Configure group-level memberships + +In an enterprise organization, with thousands of projects under a single group, +ensuring that all of the [project-level protected environments](#protecting-environments) +are properly configured is not a scalable solution. For example, a developer +might gain privileged access to a higher environment when they are added as a +maintainer to a new project. Group-level protected environments can be a solution +in this situation. + +To maximize the effectiveness of group-level protected environments, +[group-level memberships](../../user/group/index.md) must be correctly +configured: + +- Operators should be assigned the [maintainer role](../../user/permissions.md) + (or above) to the top-level group. They can maintain CI/CD configurations for + the higher environments (such as production) in the group-level settings page, + wnich includes group-level protected environments, + [group-level runners](../runners/README.md#group-runners), + [group-level clusters](../../user/group/clusters/index.md), etc. Those + configurations are inherited to the child projects as read-only entries. + This ensures that only operators can configure the organization-wide + deployment ruleset. +- Developers should be assigned the [developer role](../../user/permissions.md) + (or below) at the top-level group, or explicitly assigned to a child project + as maintainers. They do *NOT* have access to the CI/CD configurations in the + top-level group, so operators can ensure that the critical configuration won't + be accidentally changed by the developers. +- For sub-groups and child projects: + - Regarding [sub-groups](../../user/group/subgroups/index.md), if a higher + group has configured the group-level protected environment, the lower groups + cannot override it. + - [Project-level protected environments](#protecting-environments) can be + combined with the group-level setting. If both group-level and project-level + environment configurations exist, the user must be allowed in **both** + rulesets in order to run a deployment job. + - Within a project or a sub-group of the top-level group, developers can be + safely assigned the Maintainer role to tune their lower environments (such + as `testing`). + +Having this configuration in place: + +- If a user is about to run a deployment job in a project and allowed to deploy + to the environment, the deployment job proceeds. +- If a user is about to run a deployment job in a project but disallowed to + deploy to the environment, the deployment job fails with an error message. + +### Protect a group-level environment + +To protect a group-level environment: + +1. Make sure your environments have the correct + [`deployment_tier`](index.md#deployment-tier-of-environments) defined in + `gitlab-ci.yml`. +1. Configure the group-level protected environments via the + [REST API](../../api/group_protected_environments.md). + +NOTE: +Configuration [via the UI](https://gitlab.com/gitlab-org/gitlab/-/issues/325249) +is scheduled for a later release. + +### Enable or disable Group-level protected environments **(FREE SELF)** + +Group-level protected environments is under development and not ready for production use. It is +deployed behind a feature flag that is **disabled by default**. +[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) +can enable it. + +To enable it: + +```ruby +Feature.enable(:group_level_protected_environments) +``` + +To disable it: + +```ruby +Feature.disable(:group_level_protected_environments) +``` + <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues diff --git a/doc/development/cicd/templates.md b/doc/development/cicd/templates.md index fc342794919..8331985697e 100644 --- a/doc/development/cicd/templates.md +++ b/doc/development/cicd/templates.md @@ -307,6 +307,26 @@ include: - remote: https://gitlab.com/gitlab-org/gitlab/-/raw/v13.0.1-ee/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml ``` +### Use a feature flag to roll out a `latest` template + +With a major version release like 13.0 or 14.0, [stable templates](#stable-version) must be +updated with their corresponding [latest template versions](#latest-version). +It may be hard to gauge the impact of this change, so use the `redirect_to_latest_template_<name>` +feature flag to test the impact on a subset of users. Using a feature flag can help +reduce the risk of reverts or rollbacks on production. + +For example, to redirect the stable `Jobs/Deploy` template to its latest template in 25% of +projects on `gitlab.com`: + +```shell +/chatops run feature set redirect_to_latest_template_jobs_deploy 25 --actors +``` + +After you're confident the latest template can be moved to stable: + +1. Update the stable template with the content of the latest version. +1. Remove the corresponding feature flag. + ### Further reading There is an [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/17716) about diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md index 8bc5a035e2a..dce2bc990d8 100644 --- a/doc/user/admin_area/settings/account_and_limit_settings.md +++ b/doc/user/admin_area/settings/account_and_limit_settings.md @@ -33,6 +33,13 @@ You can change the maximum push size for your repository. Navigate to **Admin Area > Settings > General**, then expand **Account and Limit**. From here, you can increase or decrease by changing the value in `Maximum push size (MB)`. +NOTE: +When you [add files to a repository](../../project/repository/web_editor.md#create-a-file) +through the web UI, the maximum **attachment** size is the limiting factor, +because the [web server](../../../development/architecture.md#components) +must receive the file before GitLab can generate the commit. +Use [Git LFS](../../../topics/git/lfs/index.md) to add large files to a repository. + ## Max import size You can change the maximum file size for imports in GitLab. diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index cafa1e43e76..fc54e5caea6 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -89,8 +89,9 @@ artifacts, as described in the [troubleshooting documentation](../../../administ > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50889) in GitLab Core 13.9. -When enabled (default), the artifacts for the most recent pipeline for a ref are -locked against deletion and kept regardless of the expiry time. +When enabled (default), the artifacts of the most recent pipeline for each Git ref +([branches and tags](https://git-scm.com/book/en/v2/Git-Internals-Git-References)) +are locked against deletion and kept regardless of the expiry time. When disabled, the latest artifacts for any **new** successful or fixed pipelines are allowed to expire. diff --git a/doc/user/project/merge_requests/squash_and_merge.md b/doc/user/project/merge_requests/squash_and_merge.md index 4315e5a0305..47c7e208f2d 100644 --- a/doc/user/project/merge_requests/squash_and_merge.md +++ b/doc/user/project/merge_requests/squash_and_merge.md @@ -7,7 +7,7 @@ type: reference, concepts # Squash and merge **(FREE)** -> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18956) to GitLab Free in 11.0. +> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18956) from GitLab Premium to GitLab Free in 11.0. With squash and merge you can combine all your merge request's commits into one and retain a clean history. diff --git a/lib/gitlab/ci/config/external/file/template.rb b/lib/gitlab/ci/config/external/file/template.rb index c4b4a7a0a73..47441fa3818 100644 --- a/lib/gitlab/ci/config/external/file/template.rb +++ b/lib/gitlab/ci/config/external/file/template.rb @@ -6,7 +6,7 @@ module Gitlab module External module File class Template < Base - attr_reader :location, :project + attr_reader :location SUFFIX = '.gitlab-ci.yml' @@ -41,7 +41,7 @@ module Gitlab end def fetch_template_content - Gitlab::Template::GitlabCiYmlTemplate.find(template_name, project)&.content + Gitlab::Template::GitlabCiYmlTemplate.find(template_name, context.project)&.content end end end diff --git a/lib/gitlab/ci/variables/collection.rb b/lib/gitlab/ci/variables/collection.rb index c7829b12557..ef9ba1b73c7 100644 --- a/lib/gitlab/ci/variables/collection.rb +++ b/lib/gitlab/ci/variables/collection.rb @@ -68,11 +68,19 @@ module Gitlab end def expand_value(value, keep_undefined: false) - value.gsub(ExpandVariables::VARIABLES_REGEXP) do + value.gsub(Item::VARIABLES_REGEXP) do match = Regexp.last_match - result = @variables_by_key[match[1] || match[2]]&.value - result ||= match[0] if keep_undefined - result + if match[:key] + # we matched variable + if variable = @variables_by_key[match[:key]] + variable.value + elsif keep_undefined + match[0] + end + else + # we escape sequence + match[0] + end end end diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb index 77da2c4cb91..0217e6129ca 100644 --- a/lib/gitlab/ci/variables/collection/item.rb +++ b/lib/gitlab/ci/variables/collection/item.rb @@ -7,6 +7,9 @@ module Gitlab class Item include Gitlab::Utils::StrongMemoize + VARIABLES_REGEXP = /\$\$|%%|\$(?<key>[a-zA-Z_][a-zA-Z0-9_]*)|\${\g<key>?}|%\g<key>%/.freeze.freeze + VARIABLE_REF_CHARS = %w[$ %].freeze + def initialize(key:, value:, public: true, file: false, masked: false, raw: false) raise ArgumentError, "`#{key}` must be of type String or nil value, while it was: #{value.class}" unless value.is_a?(String) || value.nil? @@ -34,9 +37,9 @@ module Gitlab strong_memoize(:depends_on) do next if raw - next unless ExpandVariables.possible_var_reference?(value) + next unless self.class.possible_var_reference?(value) - value.scan(ExpandVariables::VARIABLES_REGEXP).map(&:first) + value.scan(VARIABLES_REGEXP).filter_map(&:last) end end @@ -64,6 +67,12 @@ module Gitlab end end + def self.possible_var_reference?(value) + return unless value + + VARIABLE_REF_CHARS.any? { |symbol| value.include?(symbol) } + end + def to_s return to_runner_variable.to_s unless depends_on diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb index e1ca4b5ff6a..bf5c66ab054 100644 --- a/lib/gitlab/template/gitlab_ci_yml_template.rb +++ b/lib/gitlab/template/gitlab_ci_yml_template.rb @@ -5,11 +5,20 @@ module Gitlab class GitlabCiYmlTemplate < BaseTemplate BASE_EXCLUDED_PATTERNS = [%r{\.latest\.}].freeze + TEMPLATES_WITH_LATEST_VERSION = { + 'Jobs/Deploy' => true, + 'Jobs/Browser-Performance-Testing' => true, + 'Security/API-Fuzzing' => true, + 'Security/DAST' => true, + 'Terraform' => true + }.freeze + def description "# This file is a template, and might need editing before it works on your project." end class << self + extend ::Gitlab::Utils::Override include Gitlab::Utils::StrongMemoize def extension @@ -54,6 +63,31 @@ module Gitlab excluded_patterns: self.excluded_patterns ) end + + override :find + def find(key, project = nil) + if try_redirect_to_latest?(key, project) + key += '.latest' + end + + super(key, project) + end + + private + + # To gauge the impact of the latest template, + # you can redirect the stable template to the latest template by enabling the feature flag. + # See https://docs.gitlab.com/ee/development/cicd/templates.html#versioning for more information. + def try_redirect_to_latest?(key, project) + return false unless templates_with_latest_version[key] + + flag_name = "redirect_to_latest_template_#{key.underscore.tr('/', '_')}" + ::Feature.enabled?(flag_name, project, default_enabled: :yaml) + end + + def templates_with_latest_version + TEMPLATES_WITH_LATEST_VERSION + end end end end diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js index be3640936dc..426e6cae8fb 100644 --- a/spec/frontend/ci_variable_list/store/actions_spec.js +++ b/spec/frontend/ci_variable_list/store/actions_spec.js @@ -5,7 +5,7 @@ import * as actions from '~/ci_variable_list/store/actions'; import * as types from '~/ci_variable_list/store/mutation_types'; import getInitialState from '~/ci_variable_list/store/state'; import { prepareDataForDisplay, prepareEnvironments } from '~/ci_variable_list/store/utils'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import mockData from '../services/mock_data'; @@ -240,7 +240,9 @@ describe('CI variable list store actions', () => { mock.onGet(state.endpoint).reply(500); testAction(actions.fetchVariables, {}, state, [], [{ type: 'requestVariables' }], () => { - expect(createFlash).toHaveBeenCalledWith('There was an error fetching the variables.'); + expect(createFlash).toHaveBeenCalledWith({ + message: 'There was an error fetching the variables.', + }); done(); }); }); @@ -278,9 +280,9 @@ describe('CI variable list store actions', () => { [], [{ type: 'requestEnvironments' }], () => { - expect(createFlash).toHaveBeenCalledWith( - 'There was an error fetching the environments information.', - ); + expect(createFlash).toHaveBeenCalledWith({ + message: 'There was an error fetching the environments information.', + }); done(); }, ); diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js index f10cf4b4140..8d7b22fe4ff 100644 --- a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js +++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js @@ -24,7 +24,7 @@ import { CREATE_CLUSTER_ERROR, } from '~/create_cluster/eks_cluster/store/mutation_types'; import createState from '~/create_cluster/eks_cluster/store/state'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; jest.mock('~/flash'); @@ -358,7 +358,9 @@ describe('EKS Cluster Store Actions', () => { testAction(actions.createClusterError, payload, state, [ { type: CREATE_CLUSTER_ERROR, payload }, ]).then(() => { - expect(createFlash).toHaveBeenCalledWith(payload.name[0]); + expect(createFlash).toHaveBeenCalledWith({ + message: payload.name[0], + }); })); }); }); diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js index 9c784f3c5a2..6bc9c4d374c 100644 --- a/spec/frontend/deploy_freeze/store/actions_spec.js +++ b/spec/frontend/deploy_freeze/store/actions_spec.js @@ -4,7 +4,7 @@ import Api from '~/api'; import * as actions from '~/deploy_freeze/store/actions'; import * as types from '~/deploy_freeze/store/mutation_types'; import getInitialState from '~/deploy_freeze/store/state'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { freezePeriodsFixture, timezoneDataFixture } from '../helpers'; @@ -189,9 +189,9 @@ describe('deploy freeze store actions', () => { [{ type: types.REQUEST_FREEZE_PERIODS }], [], () => - expect(createFlash).toHaveBeenCalledWith( - 'There was an error fetching the deploy freezes.', - ), + expect(createFlash).toHaveBeenCalledWith({ + message: 'There was an error fetching the deploy freezes.', + }), ); }); }); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index f46a42fae7a..0f0f7342ff4 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -54,7 +54,7 @@ import { } from '~/diffs/store/actions'; import * as types from '~/diffs/store/mutation_types'; import * as utils from '~/diffs/store/utils'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import * as commonUtils from '~/lib/utils/common_utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; @@ -293,7 +293,9 @@ describe('DiffsStoreActions', () => { testAction(fetchCoverageFiles, {}, { endpointCoverage }, [], [], () => { expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith(expect.stringMatching('Something went wrong')); + expect(createFlash).toHaveBeenCalledWith({ + message: expect.stringMatching('Something went wrong'), + }); done(); }); }); diff --git a/spec/frontend/error_tracking/store/actions_spec.js b/spec/frontend/error_tracking/store/actions_spec.js index 9d598344acd..aaaa1194a29 100644 --- a/spec/frontend/error_tracking/store/actions_spec.js +++ b/spec/frontend/error_tracking/store/actions_spec.js @@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import * as actions from '~/error_tracking/store/actions'; import * as types from '~/error_tracking/store/mutation_types'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; diff --git a/spec/frontend/error_tracking/store/details/actions_spec.js b/spec/frontend/error_tracking/store/details/actions_spec.js index 0c19dce7bad..623cb82851d 100644 --- a/spec/frontend/error_tracking/store/details/actions_spec.js +++ b/spec/frontend/error_tracking/store/details/actions_spec.js @@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import * as actions from '~/error_tracking/store/details/actions'; import * as types from '~/error_tracking/store/details/mutation_types'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import Poll from '~/lib/utils/poll'; diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js index 39481a8576f..5465bde397c 100644 --- a/spec/frontend/error_tracking/store/list/actions_spec.js +++ b/spec/frontend/error_tracking/store/list/actions_spec.js @@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import * as actions from '~/error_tracking/store/list/actions'; import * as types from '~/error_tracking/store/list/mutation_types'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import httpStatusCodes from '~/lib/utils/http_status'; diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js index d894cc7cc75..3cb4dd41574 100644 --- a/spec/frontend/grafana_integration/components/grafana_integration_spec.js +++ b/spec/frontend/grafana_integration/components/grafana_integration_spec.js @@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { TEST_HOST } from 'helpers/test_constants'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue'; import { createStore } from '~/grafana_integration/store'; import axios from '~/lib/utils/axios_utils'; @@ -112,10 +112,10 @@ describe('grafana integration component', () => { .$nextTick() .then(jest.runAllTicks) .then(() => - expect(createFlash).toHaveBeenCalledWith( - `There was an error saving your changes. ${message}`, - 'alert', - ), + expect(createFlash).toHaveBeenCalledWith({ + message: `There was an error saving your changes. ${message}`, + type: 'alert', + }), ); }); }); diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js index 0600fcea917..fce6ccf4b58 100644 --- a/spec/frontend/ide/components/new_dropdown/modal_spec.js +++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import modal from '~/ide/components/new_dropdown/modal.vue'; import { createStore } from '~/ide/stores'; @@ -182,14 +182,14 @@ describe('new file modal component', () => { vm.submitForm(); - expect(createFlash).toHaveBeenCalledWith( - 'The name "test-path/test" is already taken in this directory.', - 'alert', - expect.anything(), - null, - false, - true, - ); + expect(createFlash).toHaveBeenCalledWith({ + message: 'The name "test-path/test" is already taken in this directory.', + type: 'alert', + parent: expect.anything(), + actionConfig: null, + fadeTransition: false, + addBodyClass: true, + }); }); it('does not throw error when target entry does not exist', () => { diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js index 600bd5fe9e1..a923d0df99f 100644 --- a/spec/frontend/ide/stores/actions/merge_request_spec.js +++ b/spec/frontend/ide/stores/actions/merge_request_spec.js @@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import { range } from 'lodash'; import { TEST_HOST } from 'helpers/test_constants'; import testAction from 'helpers/vuex_action_helper'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '~/ide/constants'; import service from '~/ide/services'; import { createStore } from '~/ide/stores'; @@ -145,7 +145,9 @@ describe('IDE store merge request actions', () => { .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' }) .catch(() => { expect(createFlash).toHaveBeenCalled(); - expect(createFlash.mock.calls[0][0]).toBe('Error fetching merge requests for bar'); + expect(createFlash.mock.calls[0][0].message).toBe( + 'Error fetching merge requests for bar', + ); }) .then(done) .catch(done.fail); @@ -562,7 +564,9 @@ describe('IDE store merge request actions', () => { openMergeRequest(store, mr) .catch(() => { - expect(createFlash).toHaveBeenCalledWith(expect.any(String)); + expect(createFlash).toHaveBeenCalledWith({ + message: expect.any(String), + }); }) .then(done) .catch(done.fail); diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js index e42e760b841..3bb67cb3341 100644 --- a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js +++ b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import * as actions from '~/ide/stores/modules/terminal/actions/session_controls'; import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants'; import * as messages from '~/ide/stores/modules/terminal/messages'; @@ -89,7 +89,9 @@ describe('IDE store terminal session controls actions', () => { it('flashes message', () => { actions.receiveStartSessionError({ dispatch }); - expect(createFlash).toHaveBeenCalledWith(messages.UNEXPECTED_ERROR_STARTING); + expect(createFlash).toHaveBeenCalledWith({ + message: messages.UNEXPECTED_ERROR_STARTING, + }); }); it('sets session status', () => { @@ -161,7 +163,9 @@ describe('IDE store terminal session controls actions', () => { it('flashes message', () => { actions.receiveStopSessionError({ dispatch }); - expect(createFlash).toHaveBeenCalledWith(messages.UNEXPECTED_ERROR_STOPPING); + expect(createFlash).toHaveBeenCalledWith({ + message: messages.UNEXPECTED_ERROR_STOPPING, + }); }); it('kills the session', () => { diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js index 0227955754c..eabc69b23aa 100644 --- a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js +++ b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import * as actions from '~/ide/stores/modules/terminal/actions/session_status'; import { PENDING, RUNNING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants'; import * as messages from '~/ide/stores/modules/terminal/messages'; @@ -115,7 +115,9 @@ describe('IDE store terminal session controls actions', () => { it('flashes message', () => { actions.receiveSessionStatusError({ dispatch }); - expect(createFlash).toHaveBeenCalledWith(messages.UNEXPECTED_ERROR_STATUS); + expect(createFlash).toHaveBeenCalledWith({ + message: messages.UNEXPECTED_ERROR_STATUS, + }); }); it('kills the session', () => { diff --git a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js index 0c69cfb3bc5..aa6a40cad18 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js @@ -5,11 +5,15 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { STATUSES } from '~/import_entities/constants'; import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue'; -import groupQuery from '~/import_entities/import_groups/graphql/queries/group.query.graphql'; +import addValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql'; +import removeValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql'; +import groupAndProjectQuery from '~/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql'; import { availableNamespacesFixture } from '../graphql/fixtures'; Vue.use(VueApollo); +const { i18n: I18N } = ImportTableRow; + const getFakeGroup = (status) => ({ web_url: 'https://fake.host/', full_path: 'fake_group_1', @@ -25,6 +29,7 @@ const getFakeGroup = (status) => ({ const EXISTING_GROUP_TARGET_NAMESPACE = 'existing-group'; const EXISTING_GROUP_PATH = 'existing-path'; +const EXISTING_PROJECT_PATH = 'existing-project-path'; describe('import table row', () => { let wrapper; @@ -41,13 +46,19 @@ describe('import table row', () => { const createComponent = (props) => { apolloProvider = createMockApollo([ [ - groupQuery, + groupAndProjectQuery, ({ fullPath }) => { const existingGroup = fullPath === `${EXISTING_GROUP_TARGET_NAMESPACE}/${EXISTING_GROUP_PATH}` ? { id: 1 } : null; - return Promise.resolve({ data: { existingGroup } }); + + const existingProject = + fullPath === `${EXISTING_GROUP_TARGET_NAMESPACE}/${EXISTING_PROJECT_PATH}` + ? { id: 1 } + : null; + + return Promise.resolve({ data: { existingGroup, existingProject } }); }, ], ]); @@ -173,7 +184,7 @@ describe('import table row', () => { }); describe('validations', () => { - it('Reports invalid group name when name is not matching regex', () => { + it('reports invalid group name when name is not matching regex', () => { createComponent({ group: { ...getFakeGroup(STATUSES.NONE), @@ -188,7 +199,7 @@ describe('import table row', () => { expect(wrapper.text()).toContain('Please choose a group URL with no special characters.'); }); - it('Reports invalid group name if relevant validation error exists', async () => { + it('reports invalid group name if relevant validation error exists', async () => { const FAKE_ERROR_MESSAGE = 'fake error'; createComponent({ @@ -208,5 +219,101 @@ describe('import table row', () => { expect(wrapper.text()).toContain(FAKE_ERROR_MESSAGE); }); + + it('sets validation error when targetting existing group', async () => { + const testGroup = getFakeGroup(STATUSES.NONE); + + createComponent({ + group: { + ...testGroup, + import_target: { + target_namespace: EXISTING_GROUP_TARGET_NAMESPACE, + new_name: EXISTING_GROUP_PATH, + }, + }, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate'); + + jest.runOnlyPendingTimers(); + await nextTick(); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: addValidationErrorMutation, + variables: { + field: 'new_name', + message: I18N.NAME_ALREADY_EXISTS, + sourceGroupId: testGroup.id, + }, + }); + }); + + it('sets validation error when targetting existing project', async () => { + const testGroup = getFakeGroup(STATUSES.NONE); + + createComponent({ + group: { + ...testGroup, + import_target: { + target_namespace: EXISTING_GROUP_TARGET_NAMESPACE, + new_name: EXISTING_PROJECT_PATH, + }, + }, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate'); + + jest.runOnlyPendingTimers(); + await nextTick(); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: addValidationErrorMutation, + variables: { + field: 'new_name', + message: I18N.NAME_ALREADY_EXISTS, + sourceGroupId: testGroup.id, + }, + }); + }); + + it('clears validation error when target is updated', async () => { + const testGroup = getFakeGroup(STATUSES.NONE); + + createComponent({ + group: { + ...testGroup, + import_target: { + target_namespace: EXISTING_GROUP_TARGET_NAMESPACE, + new_name: EXISTING_PROJECT_PATH, + }, + }, + }); + + jest.runOnlyPendingTimers(); + await nextTick(); + + jest.spyOn(wrapper.vm.$apollo, 'mutate'); + + await wrapper.setProps({ + group: { + ...testGroup, + import_target: { + target_namespace: 'valid_namespace', + new_name: 'valid_path', + }, + }, + }); + + jest.runOnlyPendingTimers(); + await nextTick(); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: removeValidationErrorMutation, + variables: { + field: 'new_name', + sourceGroupId: testGroup.id, + }, + }); + }); }); }); diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js index 9bff77cd34a..f2bfc61381c 100644 --- a/spec/frontend/import_entities/import_projects/store/actions_spec.js +++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js @@ -1,7 +1,7 @@ import MockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'helpers/test_constants'; import testAction from 'helpers/vuex_action_helper'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { STATUSES } from '~/import_entities/constants'; import actionsFactory from '~/import_entities/import_projects/store/actions'; import { getImportTarget } from '~/import_entities/import_projects/store/getters'; @@ -168,7 +168,9 @@ describe('import_projects store actions', () => { [], ); - expect(createFlash).toHaveBeenCalledWith('Provider rate limit exceeded. Try again later'); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Provider rate limit exceeded. Try again later', + }); }); }); @@ -245,7 +247,9 @@ describe('import_projects store actions', () => { [], ); - expect(createFlash).toHaveBeenCalledWith('Importing the project failed'); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Importing the project failed', + }); }); it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows detailed error message on an unsuccessful request with errors fields in response', async () => { @@ -266,7 +270,9 @@ describe('import_projects store actions', () => { [], ); - expect(createFlash).toHaveBeenCalledWith(`Importing the project failed: ${ERROR_MESSAGE}`); + expect(createFlash).toHaveBeenCalledWith({ + message: `Importing the project failed: ${ERROR_MESSAGE}`, + }); }); }); @@ -365,7 +371,9 @@ describe('import_projects store actions', () => { [], ); - expect(createFlash).toHaveBeenCalledWith('Requesting namespaces failed'); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Requesting namespaces failed', + }); }); }); diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js index 5476e895c68..f4342c56f98 100644 --- a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js +++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js @@ -1,5 +1,5 @@ import AxiosMockAdapter from 'axios-mock-adapter'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { ERROR_MSG } from '~/incidents_settings/constants'; import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service'; import axios from '~/lib/utils/axios_utils'; @@ -37,7 +37,10 @@ describe('IncidentsSettingsService', () => { mock.onPatch().reply(httpStatusCodes.BAD_REQUEST); return service.updateSettings({}).then(() => { - expect(createFlash).toHaveBeenCalledWith(expect.stringContaining(ERROR_MSG), 'alert'); + expect(createFlash).toHaveBeenCalledWith({ + message: expect.stringContaining(ERROR_MSG), + type: 'alert', + }); }); }); }); diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js index e5e3478dc59..3099e0b639b 100644 --- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js +++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js @@ -6,7 +6,7 @@ import { issuable1, issuable2, } from 'jest/vue_shared/components/issue/related_issuable_mock_data'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue'; import { linkedIssueTypesMap } from '~/related_issues/constants'; @@ -195,7 +195,9 @@ describe('RelatedIssuesRoot', () => { wrapper.vm.onPendingFormSubmit(input); return waitForPromises().then(() => { - expect(createFlash).toHaveBeenCalledWith(message); + expect(createFlash).toHaveBeenCalledWith({ + message, + }); }); }); }); diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index b7f741c449f..f60c531e3f6 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -1,7 +1,7 @@ import MockAdapter from 'axios-mock-adapter'; import { backoffMockImplementation } from 'helpers/backoff_helper'; import testAction from 'helpers/vuex_action_helper'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import * as commonUtils from '~/lib/utils/common_utils'; import statusCodes from '~/lib/utils/http_status'; @@ -257,9 +257,9 @@ describe('Monitoring store actions', () => { 'receiveMetricsDashboardFailure', new Error('Request failed with status code 500'), ); - expect(createFlash).toHaveBeenCalledWith( - expect.stringContaining(mockDashboardsErrorResponse.message), - ); + expect(createFlash).toHaveBeenCalledWith({ + message: expect.stringContaining(mockDashboardsErrorResponse.message), + }); done(); }) .catch(done.fail); @@ -1148,9 +1148,9 @@ describe('Monitoring store actions', () => { return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then( () => { expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith( - expect.stringContaining('error getting options for variable "label1"'), - ); + expect(createFlash).toHaveBeenCalledWith({ + message: expect.stringContaining('error getting options for variable "label1"'), + }); }, ); }); diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js index 9d2530ded1e..5eecfd395e2 100644 --- a/spec/frontend/operation_settings/components/metrics_settings_spec.js +++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js @@ -1,7 +1,7 @@ import { GlButton, GlLink, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import { TEST_HOST } from 'helpers/test_constants'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { refreshCurrentPage } from '~/lib/utils/url_utility'; import { timezones } from '~/monitoring/format_date'; @@ -203,10 +203,10 @@ describe('operation settings external dashboard component', () => { .$nextTick() .then(jest.runAllTicks) .then(() => - expect(createFlash).toHaveBeenCalledWith( - `There was an error saving your changes. ${message}`, - 'alert', - ), + expect(createFlash).toHaveBeenCalledWith({ + message: `There was an error saving your changes. ${message}`, + type: 'alert', + }), ); }); }); diff --git a/spec/frontend/packages/list/stores/actions_spec.js b/spec/frontend/packages/list/stores/actions_spec.js index 52966c1be5e..adccb7436e1 100644 --- a/spec/frontend/packages/list/stores/actions_spec.js +++ b/spec/frontend/packages/list/stores/actions_spec.js @@ -2,7 +2,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import Api from '~/api'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { MISSING_DELETE_PATH_ERROR } from '~/packages/list/constants'; import * as actions from '~/packages/list/stores/actions'; import * as types from '~/packages/list/stores/mutation_types'; @@ -241,7 +241,9 @@ describe('Actions Package list store', () => { `('should reject and createFlash when $property is missing', ({ actionPayload }, done) => { testAction(actions.requestDeletePackage, actionPayload, null, [], []).catch((e) => { expect(e).toEqual(new Error(MISSING_DELETE_PATH_ERROR)); - expect(createFlash).toHaveBeenCalledWith(DELETE_PACKAGE_ERROR_MESSAGE); + expect(createFlash).toHaveBeenCalledWith({ + message: DELETE_PACKAGE_ERROR_MESSAGE, + }); done(); }); }); diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js index 6258b08dfbb..e931ddb8496 100644 --- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js @@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import { getJSONFixture } from 'helpers/fixtures'; import { TEST_HOST } from 'helpers/test_constants'; import testAction from 'helpers/vuex_action_helper'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import * as actions from '~/pipelines/stores/test_reports/actions'; import * as types from '~/pipelines/stores/test_reports/mutation_types'; diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js index a3d7b63373c..42adefcd0bb 100644 --- a/spec/frontend/profile/account/components/update_username_spec.js +++ b/spec/frontend/profile/account/components/update_username_spec.js @@ -2,7 +2,7 @@ import { GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'helpers/test_constants'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import UpdateUsername from '~/profile/account/components/update_username.vue'; @@ -146,7 +146,9 @@ describe('UpdateUsername component', () => { await expect(wrapper.vm.onConfirm()).rejects.toThrow(); - expect(createFlash).toBeCalledWith('Invalid username'); + expect(createFlash).toBeCalledWith({ + message: 'Invalid username', + }); }); it("shows a fallback error message if the error response doesn't have a `message` property", async () => { @@ -156,9 +158,9 @@ describe('UpdateUsername component', () => { await expect(wrapper.vm.onConfirm()).rejects.toThrow(); - expect(createFlash).toBeCalledWith( - 'An error occurred while updating your username, please try again.', - ); + expect(createFlash).toBeCalledWith({ + message: 'An error occurred while updating your username, please try again.', + }); }); }); }); diff --git a/spec/frontend/projects/commits/store/actions_spec.js b/spec/frontend/projects/commits/store/actions_spec.js index e2c993b8395..fdb12640b26 100644 --- a/spec/frontend/projects/commits/store/actions_spec.js +++ b/spec/frontend/projects/commits/store/actions_spec.js @@ -1,7 +1,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import actions from '~/projects/commits/store/actions'; import * as types from '~/projects/commits/store/mutation_types'; import createState from '~/projects/commits/store/state'; @@ -39,7 +39,9 @@ describe('Project commits actions', () => { actions.receiveAuthorsError(mockDispatchContext); expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith('An error occurred fetching the project authors.'); + expect(createFlash).toHaveBeenCalledWith({ + message: 'An error occurred fetching the project authors.', + }); }); }); diff --git a/spec/frontend/related_merge_requests/store/actions_spec.js b/spec/frontend/related_merge_requests/store/actions_spec.js index a14096388e6..3bd07c34b6f 100644 --- a/spec/frontend/related_merge_requests/store/actions_spec.js +++ b/spec/frontend/related_merge_requests/store/actions_spec.js @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import * as actions from '~/related_merge_requests/store/actions'; import * as types from '~/related_merge_requests/store/mutation_types'; @@ -100,7 +100,9 @@ describe('RelatedMergeRequest store actions', () => { [{ type: 'requestData' }, { type: 'receiveDataError' }], () => { expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith(expect.stringMatching('Something went wrong')); + expect(createFlash).toHaveBeenCalledWith({ + message: expect.stringMatching('Something went wrong'), + }); done(); }, diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js index 688ec4c0a50..6504a09df2f 100644 --- a/spec/frontend/releases/stores/modules/detail/actions_spec.js +++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js @@ -1,7 +1,7 @@ import { cloneDeep } from 'lodash'; import { getJSONFixture } from 'helpers/fixtures'; import testAction from 'helpers/vuex_action_helper'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { redirectTo } from '~/lib/utils/url_utility'; import { ASSET_LINK_TYPE } from '~/releases/constants'; import createReleaseAssetLinkMutation from '~/releases/graphql/mutations/create_release_link.mutation.graphql'; @@ -151,9 +151,9 @@ describe('Release edit/new actions', () => { it(`shows a flash message`, () => { return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => { expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith( - 'Something went wrong while getting the release details.', - ); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Something went wrong while getting the release details.', + }); }); }); }); @@ -352,9 +352,9 @@ describe('Release edit/new actions', () => { .createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} }) .then(() => { expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith( - 'Something went wrong while creating a new release.', - ); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Something went wrong while creating a new release.', + }); }); }); }); @@ -483,9 +483,9 @@ describe('Release edit/new actions', () => { await actions.updateRelease({ commit, dispatch, state, getters }); expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith( - 'Something went wrong while saving the release details.', - ); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Something went wrong while saving the release details.', + }); }); }); @@ -503,9 +503,9 @@ describe('Release edit/new actions', () => { await actions.updateRelease({ commit, dispatch, state, getters }); expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith( - 'Something went wrong while saving the release details.', - ); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Something went wrong while saving the release details.', + }); }); }; diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js index a7ab205ca7b..4b3b21c5507 100644 --- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js @@ -4,7 +4,7 @@ import AxiosMockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { joinPaths } from '~/lib/utils/url_utility'; import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue'; @@ -125,9 +125,9 @@ describe('Snippet Blob Edit component', () => { it('should call flash', async () => { await waitForPromises(); - expect(createFlash).toHaveBeenCalledWith( - "Can't fetch content for the blob: Error: Request failed with status code 500", - ); + expect(createFlash).toHaveBeenCalledWith({ + message: "Can't fetch content for the blob: Error: Request failed with status code 500", + }); }); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js index e0077a008a2..0609086997b 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; @@ -63,10 +63,10 @@ describe('Wip', () => { setImmediate(() => { expect(vm.isMakingRequest).toBeTruthy(); expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); - expect(createFlash).toHaveBeenCalledWith( - 'The merge request can now be merged.', - 'notice', - ); + expect(createFlash).toHaveBeenCalledWith({ + message: 'The merge request can now be merged.', + type: 'notice', + }); done(); }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js index 05bad572472..4140ec09b4e 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js @@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data'; import Api from '~/api'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import httpStatusCodes from '~/lib/utils/http_status'; import * as actions from '~/vue_shared/components/filtered_search_bar/store/modules/filters/actions'; import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types'; diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb index 407187ea05f..1108d26b2a9 100644 --- a/spec/lib/expand_variables_spec.rb +++ b/spec/lib/expand_variables_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'rspec-parameterized' RSpec.describe ExpandVariables do shared_examples 'common variable expansion' do |expander| @@ -231,41 +232,4 @@ RSpec.describe ExpandVariables do end end end - - describe '#possible_var_reference?' do - context 'table tests' do - using RSpec::Parameterized::TableSyntax - - where do - { - "empty value": { - value: '', - result: false - }, - "normal value": { - value: 'some value', - result: false - }, - "simple expansions": { - value: 'key$variable', - result: true - }, - "complex expansions": { - value: 'key${variable}${variable2}', - result: true - }, - "complex expansions for Windows": { - value: 'key%variable%%variable2%', - result: true - } - } - end - - with_them do - subject { ExpandVariables.possible_var_reference?(value) } - - it { is_expected.to eq(result) } - end - end - end end diff --git a/spec/lib/gitlab/ci/templates/templates_spec.rb b/spec/lib/gitlab/ci/templates/templates_spec.rb index 56443e611e8..5eb85601982 100644 --- a/spec/lib/gitlab/ci/templates/templates_spec.rb +++ b/spec/lib/gitlab/ci/templates/templates_spec.rb @@ -20,6 +20,13 @@ RSpec.describe 'CI YML Templates' do all_templates - excluded_templates end + before do + stub_feature_flags( + redirect_to_latest_template_terraform: false, + redirect_to_latest_template_security_api_fuzzing: false, + redirect_to_latest_template_security_dast: false) + end + with_them do let(:content) do if template_name == 'Security/DAST-API.gitlab-ci.yml' diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb index ca9dc95711d..9443bf6d6d5 100644 --- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb @@ -70,6 +70,43 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do end end + describe '.possible_var_reference?' do + context 'table tests' do + using RSpec::Parameterized::TableSyntax + + where do + { + "empty value": { + value: '', + result: false + }, + "normal value": { + value: 'some value', + result: false + }, + "simple expansions": { + value: 'key$variable', + result: true + }, + "complex expansions": { + value: 'key${variable}${variable2}', + result: true + }, + "complex expansions for Windows": { + value: 'key%variable%%variable2%', + result: true + } + } + end + + with_them do + subject { Gitlab::Ci::Variables::Collection::Item.possible_var_reference?(value) } + + it { is_expected.to eq(result) } + end + end + end + describe '#depends_on' do let(:item) { Gitlab::Ci::Variables::Collection::Item.new(**variable) } @@ -128,7 +165,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do end it 'supports using an active record resource' do - variable = create(:ci_variable, key: 'CI_VAR', value: '123') + variable = build(:ci_variable, key: 'CI_VAR', value: '123') resource = described_class.fabricate(variable) expect(resource).to be_a(described_class) diff --git a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb index 73cf0e19d00..01eef673c35 100644 --- a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'rspec-parameterized' RSpec.describe Gitlab::Ci::Variables::Collection::Sort do describe '#initialize with non-Collection value' do @@ -57,9 +58,9 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sort do }, "variable containing escaped variable reference": { variables: [ - { key: 'variable_a', value: 'value' }, { key: 'variable_b', value: '$$variable_a' }, - { key: 'variable_c', value: '$variable_b' } + { key: 'variable_c', value: '$variable_a' }, + { key: 'variable_a', value: 'value' } ], expected_errors: nil } @@ -144,11 +145,11 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sort do }, "variable containing escaped variable reference": { variables: [ - { key: 'variable_c', value: '$variable_b' }, { key: 'variable_b', value: '$$variable_a' }, + { key: 'variable_c', value: '$variable_a' }, { key: 'variable_a', value: 'value' } ], - result: %w[variable_a variable_b variable_c] + result: %w[variable_b variable_a variable_c] } } end diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb index acf5744f566..abda27f0d6e 100644 --- a/spec/lib/gitlab/ci/variables/collection_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection_spec.rb @@ -253,6 +253,11 @@ RSpec.describe Gitlab::Ci::Variables::Collection do value: 'key${MISSING_VAR}-${CI_JOB_NAME}', result: 'key${MISSING_VAR}-test-1', keep_undefined: true + }, + "escaped characters are kept intact": { + value: 'key-$TEST1-%%HOME%%-$${HOME}', + result: 'key-test-3-%%HOME%%-$${HOME}', + keep_undefined: false } } end @@ -315,6 +320,14 @@ RSpec.describe Gitlab::Ci::Variables::Collection do ], keep_undefined: false }, + "escaped characters in complex expansions are kept intact": { + variables: [ + { key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' }, + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: 'value2' } + ], + keep_undefined: false + }, "array with cyclic dependency": { variables: [ { key: 'variable', value: '$variable2' }, @@ -415,6 +428,30 @@ RSpec.describe Gitlab::Ci::Variables::Collection do { key: 'variable3', value: 'keyvalueresult' } ] }, + "escaped characters in complex expansions keeping undefined are kept intact": { + variables: [ + { key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' }, + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: 'value' } + ], + keep_undefined: true, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'value' }, + { key: 'variable3', value: 'key_value_$${HOME}_%%HOME%%' } + ] + }, + "escaped characters in complex expansions discarding undefined are kept intact": { + variables: [ + { key: 'variable2', value: 'key_${variable4}_$${HOME}_%%HOME%%' }, + { key: 'variable', value: 'value_$${HOME}_%%HOME%%' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: 'value_$${HOME}_%%HOME%%' }, + { key: 'variable2', value: 'key__$${HOME}_%%HOME%%' } + ] + }, "out-of-order expansion": { variables: [ { key: 'variable3', value: 'key$variable2$variable' }, @@ -441,7 +478,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection do { key: 'variable3', value: 'keyresultvalue' } ] }, - "missing variable": { + "missing variable discarding original": { variables: [ { key: 'variable2', value: 'key$variable' } ], @@ -485,6 +522,19 @@ RSpec.describe Gitlab::Ci::Variables::Collection do { key: 'variable3', value: 'key_$variable2_value2' } ] }, + "variable value referencing password with special characters": { + variables: [ + { key: 'VAR', value: '$PASSWORD' }, + { key: 'PASSWORD', value: 'my_password$$_%%_$A' }, + { key: 'A', value: 'value' } + ], + keep_undefined: false, + result: [ + { key: 'VAR', value: 'my_password$$_%%_value' }, + { key: 'PASSWORD', value: 'my_password$$_%%_value' }, + { key: 'A', value: 'value' } + ] + }, "cyclic dependency causes original array to be returned": { variables: [ { key: 'variable', value: '$variable2' }, diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb index 26c83ed6793..226fdb9c948 100644 --- a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb +++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb @@ -21,6 +21,55 @@ RSpec.describe Gitlab::Template::GitlabCiYmlTemplate do end end + describe '.find' do + let_it_be(:project) { create(:project) } + let_it_be(:other_project) { create(:project) } + + described_class::TEMPLATES_WITH_LATEST_VERSION.keys.each do |key| + it "finds the latest template for #{key}" do + result = described_class.find(key, project) + expect(result.full_name).to eq("#{key}.latest.gitlab-ci.yml") + expect(result.content).to be_present + end + + context 'when `redirect_to_latest_template` feature flag is disabled' do + before do + stub_feature_flags("redirect_to_latest_template_#{key.underscore.tr('/', '_')}".to_sym => false) + end + + it "finds the stable template for #{key}" do + result = described_class.find(key, project) + expect(result.full_name).to eq("#{key}.gitlab-ci.yml") + expect(result.content).to be_present + end + end + + context 'when `redirect_to_latest_template` feature flag is enabled on the project' do + before do + stub_feature_flags("redirect_to_latest_template_#{key.underscore.tr('/', '_')}".to_sym => project) + end + + it "finds the latest template for #{key}" do + result = described_class.find(key, project) + expect(result.full_name).to eq("#{key}.latest.gitlab-ci.yml") + expect(result.content).to be_present + end + end + + context 'when `redirect_to_latest_template` feature flag is enabled on the other project' do + before do + stub_feature_flags("redirect_to_latest_template_#{key.underscore.tr('/', '_')}".to_sym => other_project) + end + + it "finds the stable template for #{key}" do + result = described_class.find(key, project) + expect(result.full_name).to eq("#{key}.gitlab-ci.yml") + expect(result.content).to be_present + end + end + end + end + describe '#content' do it 'loads the full file' do gitignore = subject.new(Rails.root.join('lib/gitlab/ci/templates/Ruby.gitlab-ci.yml')) diff --git a/workhorse/internal/artifacts/entry.go b/workhorse/internal/artifacts/entry.go index 0c697d40020..e727e82ec5f 100644 --- a/workhorse/internal/artifacts/entry.go +++ b/workhorse/internal/artifacts/entry.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "io" - "mime" "net/http" "os" "os/exec" @@ -53,14 +52,6 @@ func (e *entry) Inject(w http.ResponseWriter, r *http.Request, sendData string) } } -func detectFileContentType(fileName string) string { - contentType := mime.TypeByExtension(filepath.Ext(fileName)) - if contentType == "" { - contentType = "application/octet-stream" - } - return contentType -} - func unpackFileFromZip(ctx context.Context, archivePath, encodedFilename string, headers http.Header, output io.Writer) error { fileName, err := zipartifacts.DecodeFileEntry(encodedFilename) if err != nil { @@ -97,7 +88,15 @@ func unpackFileFromZip(ctx context.Context, archivePath, encodedFilename string, // Write http headers about the file headers.Set("Content-Length", contentLength) - headers.Set("Content-Type", detectFileContentType(fileName)) + + // Using application/octet-stream tells the client that we don't + // really know what Content-Type is. Since this file is being sent + // as attachment, browsers don't need to know to save the + // file. Chrome doesn't appear to pay attention to Content-Type when + // Content-Disposition is an attachment, and Firefox only uses it if there + // is no extension in the filename. Thus, there's no need for + // Workhorse to guess Content-Type based on the filename. + headers.Set("Content-Type", "application/octet-stream") headers.Set("Content-Disposition", "attachment; filename=\""+escapeQuotes(basename)+"\"") // Copy file body to client if _, err := io.Copy(output, reader); err != nil { diff --git a/workhorse/internal/artifacts/entry_test.go b/workhorse/internal/artifacts/entry_test.go index 6f1e9d360aa..35e2bf06a0c 100644 --- a/workhorse/internal/artifacts/entry_test.go +++ b/workhorse/internal/artifacts/entry_test.go @@ -54,7 +54,7 @@ func TestDownloadingFromValidArchive(t *testing.T) { testhelper.RequireResponseHeader(t, response, "Content-Type", - "text/plain; charset=utf-8") + "application/octet-stream") testhelper.RequireResponseHeader(t, response, "Content-Disposition", "attachment; filename=\"test.txt\"") @@ -88,7 +88,7 @@ func TestDownloadingFromValidHTTPArchive(t *testing.T) { testhelper.RequireResponseHeader(t, response, "Content-Type", - "text/plain; charset=utf-8") + "application/octet-stream") testhelper.RequireResponseHeader(t, response, "Content-Disposition", "attachment; filename=\"test.txt\"") |