diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-06 21:09:07 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-06 21:09:07 +0300 |
commit | f3db01da507f86cfed412c7d337e3747744cc914 (patch) | |
tree | 3862e3ca223038c1390e2d19708ebeeecb040e00 | |
parent | a268b09416c8dc3da3af38933028fa26375b88e0 (diff) |
Add latest changes from gitlab-org/gitlab@master
62 files changed, 736 insertions, 468 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue index 5ecb2dd3e58..f24c52f61da 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue @@ -4,12 +4,15 @@ import { GlButton, GlIcon, GlLoadingIcon, + GlModal, + GlModalDirective, GlTable, GlTooltipDirective, + GlSprintf, } from '@gitlab/ui'; import { s__, __ } from '~/locale'; import Tracking from '~/tracking'; -import { trackAlertIntegrationsViewsOptions } from '../constants'; +import { trackAlertIntegrationsViewsOptions, integrationToDeleteDefault } from '../constants'; export const i18n = { title: s__('AlertsIntegrations|Current integrations'), @@ -36,10 +39,13 @@ export default { GlButton, GlIcon, GlLoadingIcon, + GlModal, GlTable, + GlSprintf, }, directives: { GlTooltip: GlTooltipDirective, + GlModal: GlModalDirective, }, props: { integrations: { @@ -71,6 +77,11 @@ export default { label: __('Actions'), }, ], + data() { + return { + integrationToDelete: integrationToDeleteDefault, + }; + }, computed: { tbodyTrClass() { return { @@ -86,6 +97,14 @@ export default { const { category, action } = trackAlertIntegrationsViewsOptions; Tracking.event(category, action); }, + intergrationToDelete({ name, id }) { + this.integrationToDelete.id = id; + this.integrationToDelete.name = name; + }, + deleteIntergration() { + this.$emit('delete-integration', { id: this.integrationToDelete.id }); + this.integrationToDelete = { ...integrationToDeleteDefault }; + }, }, }; </script> @@ -127,7 +146,11 @@ export default { <template #cell(actions)="{ item }"> <gl-button-group> <gl-button icon="pencil" @click="$emit('edit-integration', { id: item.id })" /> - <gl-button icon="remove" @click="$emit('delete-integration', { id: item.id })" /> + <gl-button + v-gl-modal.deleteIntegration + icon="remove" + @click="intergrationToDelete(item)" + /> </gl-button-group> </template> @@ -143,5 +166,22 @@ export default { </div> </template> </gl-table> + <gl-modal + modal-id="deleteIntegration" + :title="__('Are you sure?')" + :ok-title="s__('AlertSettings|Delete integration')" + ok-variant="danger" + @ok="deleteIntergration" + > + <gl-sprintf + :message=" + s__( + 'AlertsIntegrations|You have opted to delete the %{integrationName} integration. Do you want to proceed? It means you will no longer receive alerts from this endpoint in your alert list, and this action cannot be undone.', + ) + " + > + <template #integrationName>{{ integrationToDelete.name }}</template> + </gl-sprintf> + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue index 059623ba11c..946da8ef34c 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue @@ -22,14 +22,12 @@ import { JSON_VALIDATE_DELAY, targetPrometheusUrlPlaceholder, typeSet, - defaultFormState, } from '../constants'; export default { targetPrometheusUrlPlaceholder, JSON_VALIDATE_DELAY, typeSet, - defaultFormState, i18n: { integrationFormSteps: { step1: { @@ -113,14 +111,18 @@ export default { data() { return { selectedIntegration: integrationTypesNew[0].value, - active: false, options: integrationTypesNew, + active: false, formVisible: false, + integrationTestPayload: { + json: null, + error: null, + }, }; }, computed: { jsonIsValid() { - return this.integrationForm.integrationTestPayload.error === null; + return this.integrationTestPayload.error === null; }, selectedIntegrationType() { switch (this.selectedIntegration) { @@ -129,43 +131,42 @@ export default { case this.$options.typeSet.prometheus: return this.prometheus; default: - return this.defaultFormState; + return {}; } }, integrationForm() { return { name: this.currentIntegration?.name || '', - integrationTestPayload: { - json: null, - error: null, - }, active: this.currentIntegration?.active || false, - token: this.currentIntegration?.token || '', - url: this.currentIntegration?.url || '', + token: this.currentIntegration?.token || this.selectedIntegrationType.token, + url: this.currentIntegration?.url || this.selectedIntegrationType.url, apiUrl: this.currentIntegration?.apiUrl || '', }; }, }, watch: { currentIntegration(val) { + if (val === null) { + return this.reset(); + } this.selectedIntegration = val.type; this.active = val.active; - this.onIntegrationTypeSelect(); + return this.integrationTypeSelect(); }, }, methods: { - onIntegrationTypeSelect() { + integrationTypeSelect() { if (this.selectedIntegration === integrationTypesNew[0].value) { this.formVisible = false; } else { this.formVisible = true; } }, - onSubmitWithTestPayload() { + submitWithTestPayload() { // TODO: Test payload before saving via GraphQL - this.onSubmit(); + this.submit(); }, - onSubmit() { + submit() { const { name, apiUrl } = this.integrationForm; const variables = this.selectedIntegration === this.$options.typeSet.http @@ -179,27 +180,45 @@ export default { return this.$emit('create-new-integration', integrationPayload); }, - onReset() { - this.integrationForm = this.defaultFormState; + reset() { this.selectedIntegration = integrationTypesNew[0].value; - this.onIntegrationTypeSelect(); + this.integrationTypeSelect(); + + if (this.currentIntegration) { + return this.$emit('clear-current-integration'); + } + + return this.resetFormValues(); + }, + resetFormValues() { + this.integrationForm.name = ''; + this.integrationForm.apiUrl = ''; + this.integrationTestPayload = { + json: null, + error: null, + }; + this.active = false; }, - onResetAuthKey() { + resetAuthKey() { + if (!this.currentIntegration) { + return; + } + this.$emit('reset-token', { type: this.selectedIntegration, variables: { id: this.currentIntegration.id }, }); }, validateJson() { - this.integrationForm.integrationTestPayload.error = null; - if (this.integrationForm.integrationTestPayload.json === '') { + this.integrationTestPayload.error = null; + if (this.integrationTestPayload.json === '') { return; } try { - JSON.parse(this.integrationForm.integrationTestPayload.json); + JSON.parse(this.integrationTestPayload.json); } catch (e) { - this.integrationForm.integrationTestPayload.error = JSON.stringify(e.message); + this.integrationTestPayload.error = JSON.stringify(e.message); } }, }, @@ -207,7 +226,7 @@ export default { </script> <template> - <gl-form class="gl-mt-6" @submit.prevent="onSubmit" @reset.prevent="onReset"> + <gl-form class="gl-mt-6" @submit.prevent="submit" @reset.prevent="reset"> <h5 class="gl-font-lg gl-my-5">{{ s__('AlertSettings|Add new integrations') }}</h5> <gl-form-group @@ -217,8 +236,9 @@ export default { > <gl-form-select v-model="selectedIntegration" + :disabled="currentIntegration !== null" :options="options" - @change="onIntegrationTypeSelect" + @change="integrationTypeSelect" /> <alert-settings-form-help-block @@ -279,7 +299,11 @@ export default { <gl-form-input-group id="url" readonly :value="integrationForm.url"> <template #append> - <clipboard-button :text="integrationForm.url" :title="__('Copy')" class="gl-m-0!" /> + <clipboard-button + :text="integrationForm.url || ''" + :title="__('Copy')" + class="gl-m-0!" + /> </template> </gl-form-input-group> </div> @@ -296,7 +320,11 @@ export default { :value="integrationForm.token" > <template #append> - <clipboard-button :text="integrationForm.token" :title="__('Copy')" class="gl-m-0!" /> + <clipboard-button + :text="integrationForm.token || ''" + :title="__('Copy')" + class="gl-m-0!" + /> </template> </gl-form-input-group> @@ -308,7 +336,7 @@ export default { :title="$options.i18n.integrationFormSteps.step3.reset" :ok-title="$options.i18n.integrationFormSteps.step3.reset" ok-variant="danger" - @ok="onResetAuthKey" + @ok="resetAuthKey" > {{ $options.i18n.integrationFormSteps.restKeyInfo.label }} </gl-modal> @@ -318,7 +346,7 @@ export default { id="test-integration" :label="$options.i18n.integrationFormSteps.step4.label" label-for="test-integration" - :invalid-feedback="integrationForm.integrationTestPayload.error" + :invalid-feedback="integrationTestPayload.error" > <alert-settings-form-help-block :message="$options.i18n.integrationFormSteps.step4.help" @@ -327,8 +355,8 @@ export default { <gl-form-textarea id="test-integration" - v-model.trim="integrationForm.integrationTestPayload.json" - :disabled="!integrationForm.active" + v-model.trim="integrationTestPayload.json" + :disabled="!active" :state="jsonIsValid" :placeholder="$options.i18n.integrationFormSteps.step4.placeholder" class="gl-my-4" @@ -354,7 +382,7 @@ export default { category="secondary" variant="success" class="gl-mr-1 js-no-auto-disable" - @click="onSubmitWithTestPayload" + @click="submitWithTestPayload" >{{ s__('AlertSettings|Save and test payload') }}</gl-button > <gl-button diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue index 0a59a5981ef..e9e7b1407bc 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue @@ -1,5 +1,4 @@ <script> -import produce from 'immer'; import { s__ } from '~/locale'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { fetchPolicies } from '~/lib/graphql'; @@ -9,12 +8,17 @@ import createHttpIntegrationMutation from '../graphql/mutations/create_http_inte import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql'; import updateHttpIntegrationMutation from '../graphql/mutations/update_http_integration.mutation.graphql'; import updatePrometheusIntegrationMutation from '../graphql/mutations/update_prometheus_integration.mutation.graphql'; +import destroyHttpIntegrationMutation from '../graphql/mutations/destroy_http_integration.mutation.graphql'; import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutation.graphql'; import resetPrometheusTokenMutation from '../graphql/mutations/reset_prometheus_token.mutation.graphql'; import IntegrationsList from './alerts_integrations_list.vue'; import SettingsFormOld from './alerts_settings_form_old.vue'; import SettingsFormNew from './alerts_settings_form_new.vue'; import { typeSet } from '../constants'; +import { + updateStoreAfterIntegrationDelete, + updateStoreAfterIntegrationAdd, +} from '../utils/cache_updates'; export default { typeSet, @@ -22,6 +26,7 @@ export default { changesSaved: s__( 'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.', ), + integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'), }, components: { IntegrationsList, @@ -89,6 +94,8 @@ export default { }, methods: { createNewIntegration({ type, variables }) { + const { projectPath } = this; + this.isUpdating = true; this.$apollo .mutate({ @@ -98,9 +105,11 @@ export default { : createPrometheusIntegrationMutation, variables: { ...variables, - projectPath: this.projectPath, + projectPath, + }, + update(store, { data }) { + updateStoreAfterIntegrationAdd(store, getIntegrationsQuery, data, { projectPath }); }, - update: this.updateIntegrations, }) .then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => { const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0]; @@ -119,41 +128,6 @@ export default { this.isUpdating = false; }); }, - updateIntegrations( - store, - { - data: { httpIntegrationCreate, prometheusIntegrationCreate }, - }, - ) { - const integration = - httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration; - if (!integration) { - return; - } - - const sourceData = store.readQuery({ - query: getIntegrationsQuery, - variables: { - projectPath: this.projectPath, - }, - }); - - const data = produce(sourceData, draftData => { - // eslint-disable-next-line no-param-reassign - draftData.project.alertManagementIntegrations.nodes = [ - integration, - ...draftData.project.alertManagementIntegrations.nodes, - ]; - }); - - store.writeQuery({ - query: getIntegrationsQuery, - variables: { - projectPath: this.projectPath, - }, - data, - }); - }, updateIntegration({ type, variables }) { this.isUpdating = true; this.$apollo @@ -201,6 +175,12 @@ export default { if (error) { return createFlash({ message: error }); } + + const integration = + httpIntegrationResetToken?.integration || + prometheusIntegrationResetToken?.integration; + this.currentIntegration = integration; + return createFlash({ message: this.$options.i18n.changesSaved, type: FLASH_TYPES.SUCCESS, @@ -217,8 +197,41 @@ export default { editIntegration({ id }) { this.currentIntegration = this.integrations.list.find(integration => integration.id === id); }, - deleteIntegration() { - // TODO, handle delete via GraphQL + deleteIntegration({ id }) { + const { projectPath } = this; + + this.isUpdating = true; + this.$apollo + .mutate({ + mutation: destroyHttpIntegrationMutation, + variables: { + id, + }, + update(store, { data }) { + updateStoreAfterIntegrationDelete(store, getIntegrationsQuery, data, { projectPath }); + }, + }) + .then(({ data: { httpIntegrationDestroy } = {} } = {}) => { + const error = httpIntegrationDestroy?.errors[0]; + if (error) { + return createFlash({ message: error }); + } + this.currentIntegration = null; + return createFlash({ + message: this.$options.i18n.integrationRemoved, + type: FLASH_TYPES.SUCCESS, + }); + }) + .catch(err => { + this.errored = true; + createFlash({ message: err }); + }) + .finally(() => { + this.isUpdating = false; + }); + }, + clearCurrentIntegration() { + this.currentIntegration = null; }, }, }; @@ -239,6 +252,7 @@ export default { @create-new-integration="createNewIntegration" @update-integration="updateIntegration" @reset-token="resetToken" + @clear-current-integration="clearCurrentIntegration" /> <settings-form-old v-else /> </div> diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js index 4ab8d215572..9cf2f356e0a 100644 --- a/app/assets/javascripts/alerts_settings/constants.js +++ b/app/assets/javascripts/alerts_settings/constants.js @@ -66,6 +66,8 @@ export const defaultFormState = { integrationTestPayload: { json: null, error: null }, }; +export const integrationToDeleteDefault = { id: null, name: '' }; + export const JSON_VALIDATE_DELAY = 250; export const targetPrometheusUrlPlaceholder = 'http://prometheus.example.com/'; diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql new file mode 100644 index 00000000000..0a49c140e6a --- /dev/null +++ b/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql @@ -0,0 +1,10 @@ +#import "../fragments/integration_item.fragment.graphql" + +mutation destroyHttpIntegration($id: ID!) { + httpIntegrationDestroy(input: { id: $id }) { + errors + integration { + ...IntegrationItem + } + } +} diff --git a/app/assets/javascripts/alerts_settings/utils/cache_updates.js b/app/assets/javascripts/alerts_settings/utils/cache_updates.js new file mode 100644 index 00000000000..18054b29fe9 --- /dev/null +++ b/app/assets/javascripts/alerts_settings/utils/cache_updates.js @@ -0,0 +1,84 @@ +import produce from 'immer'; +import createFlash from '~/flash'; + +import { DELETE_INTEGRATION_ERROR, ADD_INTEGRATION_ERROR } from './error_messages'; + +const deleteIntegrationFromStore = (store, query, { httpIntegrationDestroy }, variables) => { + const integration = httpIntegrationDestroy?.integration; + if (!integration) { + return; + } + + const sourceData = store.readQuery({ + query, + variables, + }); + + const data = produce(sourceData, draftData => { + // eslint-disable-next-line no-param-reassign + draftData.project.alertManagementIntegrations.nodes = draftData.project.alertManagementIntegrations.nodes.filter( + ({ id }) => id !== integration.id, + ); + }); + + store.writeQuery({ + query, + variables, + data, + }); +}; + +const addIntegrationToStore = ( + store, + query, + { httpIntegrationCreate, prometheusIntegrationCreate }, + variables, +) => { + const integration = + httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration; + if (!integration) { + return; + } + + const sourceData = store.readQuery({ + query, + variables, + }); + + const data = produce(sourceData, draftData => { + // eslint-disable-next-line no-param-reassign + draftData.project.alertManagementIntegrations.nodes = [ + integration, + ...draftData.project.alertManagementIntegrations.nodes, + ]; + }); + + store.writeQuery({ + query, + variables, + data, + }); +}; + +const onError = (data, message) => { + createFlash({ message }); + throw new Error(data.errors); +}; + +export const hasErrors = ({ errors = [] }) => errors?.length; + +export const updateStoreAfterIntegrationDelete = (store, query, data, variables) => { + if (hasErrors(data)) { + onError(data, DELETE_INTEGRATION_ERROR); + } else { + deleteIntegrationFromStore(store, query, data, variables); + } +}; + +export const updateStoreAfterIntegrationAdd = (store, query, data, variables) => { + if (hasErrors(data)) { + onError(data, ADD_INTEGRATION_ERROR); + } else { + addIntegrationToStore(store, query, data, variables); + } +}; diff --git a/app/assets/javascripts/alerts_settings/utils/error_messages.js b/app/assets/javascripts/alerts_settings/utils/error_messages.js new file mode 100644 index 00000000000..2e6058fc81a --- /dev/null +++ b/app/assets/javascripts/alerts_settings/utils/error_messages.js @@ -0,0 +1,9 @@ +import { s__ } from '~/locale'; + +export const DELETE_INTEGRATION_ERROR = s__( + 'AlertsIntegrations|The integration could not be deleted. Please try again.', +); + +export const ADD_INTEGRATION_ERROR = s__( + 'AlertsIntegrations|The integration could not be added. Please try again.', +); diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue index 328026d0953..2844b4ffde3 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue @@ -14,9 +14,9 @@ export default { required: false, default: () => [], }, - isDesktop: { + isMobile: { type: Boolean, - default: false, + default: true, required: false, }, }, @@ -34,7 +34,7 @@ export default { return this.tags.some(tag => this.selectedItems[tag.name]); }, showMultiDeleteButton() { - return this.tags.some(tag => tag.destroy_path) && this.isDesktop; + return this.tags.some(tag => tag.destroy_path) && !this.isMobile; }, }, methods: { @@ -68,7 +68,7 @@ export default { :tag="tag" :first="index === 0" :selected="selectedItems[tag.name]" - :is-desktop="isDesktop" + :is-mobile="isMobile" @select="updateSelectedItems(tag.name)" @delete="$emit('delete', { [tag.name]: true })" /> diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue index 0f6297ca406..2edeac1144f 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue @@ -40,9 +40,9 @@ export default { type: Object, required: true, }, - isDesktop: { + isMobile: { type: Boolean, - default: false, + default: true, required: false, }, selected: { @@ -69,7 +69,7 @@ export default { return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : ''; }, mobileClasses() { - return this.isDesktop ? '' : 'mw-s'; + return this.isMobile ? 'mw-s' : ''; }, shortDigest() { // remove sha256: from the string, and show only the first 7 char diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue index d2fb695dbfa..e110c9c9c70 100644 --- a/app/assets/javascripts/registry/explorer/pages/details.vue +++ b/app/assets/javascripts/registry/explorer/pages/details.vue @@ -37,7 +37,7 @@ export default { data() { return { itemsToBeDeleted: [], - isDesktop: true, + isMobile: false, deleteAlertType: null, dismissPartialCleanupWarning: false, }; @@ -110,7 +110,7 @@ export default { } }, handleResize() { - this.isDesktop = GlBreakpointInstance.isDesktop(); + this.isMobile = GlBreakpointInstance.getBreakpointSize() === 'xs'; }, }, }; @@ -137,7 +137,7 @@ export default { <tags-loader v-if="isLoading" /> <template v-else> <empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" /> - <tags-list v-else :tags="tags" :is-desktop="isDesktop" @delete="deleteTags" /> + <tags-list v-else :tags="tags" :is-mobile="isMobile" @delete="deleteTags" /> </template> <gl-pagination diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue index f17e409d996..b6722de5277 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue @@ -1,10 +1,10 @@ <script> -import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui'; +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; export default { components: { - GlDeprecatedDropdown, - GlDeprecatedDropdownItem, + GlDropdown, + GlDropdownItem, }, props: { commits: { @@ -18,20 +18,20 @@ export default { <template> <div> - <gl-deprecated-dropdown + <gl-dropdown right text="Use an existing commit message" variant="link" class="mr-commit-dropdown" > - <gl-deprecated-dropdown-item + <gl-dropdown-item v-for="commit in commits" :key="commit.short_id" class="text-nowrap text-truncate" @click="$emit('input', commit.message)" > <span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }} - </gl-deprecated-dropdown-item> - </gl-deprecated-dropdown> + </gl-dropdown-item> + </gl-dropdown> </div> </template> diff --git a/app/assets/stylesheets/framework/broadcast_messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss index c1647c16c77..b8934d2797a 100644 --- a/app/assets/stylesheets/framework/broadcast_messages.scss +++ b/app/assets/stylesheets/framework/broadcast_messages.scss @@ -15,10 +15,6 @@ .broadcast-banner-message { text-align: center; - - .broadcast-message-dismiss { - color: inherit; - } } .broadcast-notification-message { @@ -36,10 +32,6 @@ &.preview { position: static; } - - .broadcast-message-dismiss { - color: $gray-700; - } } .toggle-colors { diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 3f5f3b6e9df..0d7af57328a 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -150,7 +150,7 @@ module IssuableCollections common_attributes + [:project, project: :namespace] when 'MergeRequest' common_attributes + [ - :target_project, :latest_merge_request_diff, :approvals, :approved_by_users, :reviewers, + :target_project, :latest_merge_request_diff, :approvals, :approved_by_users, source_project: :route, head_pipeline: :project, target_project: :namespace ] end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d42e97fb2d8..1d28c828464 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -318,8 +318,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def export_csv - return render_404 unless Feature.enabled?(:export_merge_requests_as_csv, project, default_enabled: true) - IssuableExportCsvWorker.perform_async(:merge_request, current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker index_path = project_merge_requests_path(project) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index ebbd5d739e6..d431c3e3699 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -406,7 +406,7 @@ class IssuableFinder elsif params.filter_by_any_assignee? items.assigned elsif params.assignee - items_assigned_to(items, params.assignee) + items.assigned_to(params.assignee) elsif params.assignee_id? || params.assignee_username? # assignee not found items.none else @@ -414,10 +414,6 @@ class IssuableFinder end end - def items_assigned_to(items, user) - items.assigned_to(user) - end - def by_negated_assignee(items) # We want CE users to be able to say "Issues not assigned to either PersonA nor PersonB" if not_params.assignees.present? diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 74cc90797b1..1f847b09752 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -164,13 +164,6 @@ class MergeRequestsFinder < IssuableFinder end # rubocop: enable CodeReuse/Finder - # rubocop: disable CodeReuse/ActiveRecord - def items_assigned_to(items, user) - assignee_or_reviewer = MergeRequest.from_union([super, items.reviewer_assigned_to(user)]) - items.where(id: assignee_or_reviewer) - end - # rubocop: enable CodeReuse/ActiveRecord - def by_deployments(items) env = params[:environment] before = params[:deployed_before] diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 9d1f685960f..a3919008109 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -302,7 +302,7 @@ module ProjectsHelper end def settings_operations_available? - can?(current_user, :read_environment, @project) + !@project.archived? && can?(current_user, :admin_operations, @project) end def error_tracking_setting_project_json diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a0e0375fbe9..bf7a73f24e4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -303,19 +303,6 @@ class MergeRequest < ApplicationRecord includes(:metrics) end - scope :reviewer_assigned_to, ->(user) do - mr_reviewers_table = MergeRequestReviewer.arel_table - - inner_sql = mr_reviewers_table - .project(Arel::Nodes::True.new) - .where( - mr_reviewers_table[:merge_request_id].eq(MergeRequest.arel_table[:id]) - .and(mr_reviewers_table[:user_id].eq(user.id)) - ).exists - - where(inner_sql) - end - after_save :keep_around_commit, unless: :importing? alias_attribute :project, :target_project diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb index dbb2b428c7b..ac164783945 100644 --- a/app/models/resource_timebox_event.rb +++ b/app/models/resource_timebox_event.rb @@ -29,10 +29,10 @@ class ResourceTimeboxEvent < ResourceEvent case self when ResourceMilestoneEvent Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_milestone_changed_action(author: user) - when ResourceIterationEvent - Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_iteration_changed_action(author: user) else # no-op end end end + +ResourceTimeboxEvent.prepend_if_ee('EE::ResourceTimeboxEvent') diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index d1168437c5d..bff8c861f1f 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -423,7 +423,7 @@ = link_to project_settings_ci_cd_path(@project), title: _('CI / CD') do %span = _('CI / CD') - - if !@project.archived? && settings_operations_available? + - if settings_operations_available? = nav_link(controller: [:operations]) do = link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do = _('Operations') diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml index 9d367caa390..473490c6c35 100644 --- a/app/views/projects/merge_requests/_nav_btns.html.haml +++ b/app/views/projects/merge_requests/_nav_btns.html.haml @@ -1,6 +1,5 @@ -- if Feature.enabled?(:export_merge_requests_as_csv, @project, default_enabled: true) - .btn-group - = render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests' +.btn-group + = render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests' - if @can_bulk_update = button_tag "Edit merge requests", class: "gl-button btn gl-mr-3 js-bulk-update-toggle" @@ -8,5 +7,4 @@ = link_to new_merge_request_path, class: "gl-button btn btn-success", title: "New merge request" do New merge request - - if Feature.enabled?(:export_merge_requests_as_csv, @project, default_enabled: true) - = render 'shared/issuable/csv_export/modal', issuable_type: 'merge_requests' + = render 'shared/issuable/csv_export/modal', issuable_type: 'merge_requests' diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml index 62b344b38f1..6ab8beff99f 100644 --- a/app/views/projects/settings/operations/_error_tracking.html.haml +++ b/app/views/projects/settings/operations/_error_tracking.html.haml @@ -1,4 +1,4 @@ -- return unless can?(current_user, :read_environment, @project) +- return unless can?(current_user, :admin_operations, @project) - setting = error_tracking_setting diff --git a/app/views/shared/_broadcast_message.html.haml b/app/views/shared/_broadcast_message.html.haml index 7be11b0fb81..7aaae3a88f3 100644 --- a/app/views/shared/_broadcast_message.html.haml +++ b/app/views/shared/_broadcast_message.html.haml @@ -8,5 +8,5 @@ = render_broadcast_message(message) .gl-flex-grow-1.gl-flex-basis-0.gl-text-right - if (message.notification? || message.dismissable?) && opts[:preview].blank? - %button.broadcast-message-dismiss.js-dismiss-current-broadcast-notification.btn.btn-link.gl-button{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } } - = sprite_icon('close', size: 16, css_class: 'gl-icon gl-text-white gl-mx-3!') + %button.js-dismiss-current-broadcast-notification.btn.btn-link.gl-button{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } } + = sprite_icon('close', size: 16, css_class: "gl-icon gl-mx-3! #{is_banner ? 'gl-text-white' : 'gl-text-gray-700'}") diff --git a/changelogs/unreleased/251136-delete-selected-button-in-container-registry-is-not-visible-on-nar.yml b/changelogs/unreleased/251136-delete-selected-button-in-container-registry-is-not-visible-on-nar.yml new file mode 100644 index 00000000000..ccf44a02ca9 --- /dev/null +++ b/changelogs/unreleased/251136-delete-selected-button-in-container-registry-is-not-visible-on-nar.yml @@ -0,0 +1,5 @@ +--- +title: 'container registry: show delete selected button on medium viewports' +merge_request: 46699 +author: +type: fixed diff --git a/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-vu.yml b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-vu.yml new file mode 100644 index 00000000000..db712295b7b --- /dev/null +++ b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-vu.yml @@ -0,0 +1,5 @@ +--- +title: Replace-GlDeprecatedDropdown-with-GlDropdown-in-app/assets/javascripts/vue_merge_request_widget +merge_request: 41429 +author: nuwe1 +type: other diff --git a/changelogs/unreleased/bvl-handle-invalid-headers.yml b/changelogs/unreleased/bvl-handle-invalid-headers.yml new file mode 100644 index 00000000000..74f6b5700f1 --- /dev/null +++ b/changelogs/unreleased/bvl-handle-invalid-headers.yml @@ -0,0 +1,5 @@ +--- +title: Handle nullbytes in auth headers +merge_request: 46985 +author: +type: fixed diff --git a/changelogs/unreleased/nicolasdular-fix-bm-close-icon.yml b/changelogs/unreleased/nicolasdular-fix-bm-close-icon.yml new file mode 100644 index 00000000000..e36c32fbeaf --- /dev/null +++ b/changelogs/unreleased/nicolasdular-fix-bm-close-icon.yml @@ -0,0 +1,5 @@ +--- +title: Fix broadcast notification close icon appearance +merge_request: 46804 +author: +type: fixed diff --git a/changelogs/unreleased/pl-fix-operations-settings-wo-pipelines.yml b/changelogs/unreleased/pl-fix-operations-settings-wo-pipelines.yml new file mode 100644 index 00000000000..06af146b1a0 --- /dev/null +++ b/changelogs/unreleased/pl-fix-operations-settings-wo-pipelines.yml @@ -0,0 +1,5 @@ +--- +title: Fix operations settings when Pipelines are disabled +merge_request: 47062 +author: +type: fixed diff --git a/config/feature_flags/development/export_merge_requests_as_csv.yml b/config/feature_flags/development/export_merge_requests_as_csv.yml deleted file mode 100644 index 73ed9137ffa..00000000000 --- a/config/feature_flags/development/export_merge_requests_as_csv.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: export_merge_requests_as_csv -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45130 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267129 -type: development -group: group::compliance -default_enabled: true diff --git a/doc/administration/audit_reports.md b/doc/administration/audit_reports.md index 83fbeda26aa..9c772302375 100644 --- a/doc/administration/audit_reports.md +++ b/doc/administration/audit_reports.md @@ -18,12 +18,12 @@ needs. ## APIs -- `https://docs.gitlab.com/ee/api/audit_events.html` -- `https://docs.gitlab.com/ee/api/graphql/reference/#user` -- `https://docs.gitlab.com/ee/api/graphql/reference/#groupmember` -- `https://docs.gitlab.com/ee/api/graphql/reference/#projectmember` +- [Audit events](../api/audit_events.md) +- [GraphQL - User](../api/graphql/reference/index.md#user) +- [GraphQL - GroupMember](../api/graphql/reference/index.md#groupmember) +- [GraphQL - ProjectMember](../api/graphql/reference/index.md#projectmember) ## Features -- `https://docs.gitlab.com/ee/administration/audit_events.html` -- `https://docs.gitlab.com/ee/administration/logs.html` +- [Audit events](audit_events.md) +- [Log system](logs.md) diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md index 5bdabe547c4..4ebc541b756 100644 --- a/doc/administration/geo/disaster_recovery/index.md +++ b/doc/administration/geo/disaster_recovery/index.md @@ -133,6 +133,9 @@ Note the following when promoting a secondary: ``` 1. Promote the **secondary** node to the **primary** node. + DANGER: **Warning:** + In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later. + CAUTION: **Caution:** If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs a point-in-time recovery to the last known state. @@ -167,6 +170,9 @@ conjunction with multiple servers, as it can only perform changes on a **secondary** with only a single machine. Instead, you must do this manually. +DANGER: **Warning:** +In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later. + CAUTION: **Caution:** If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs a point-in-time recovery to the last known state. diff --git a/doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md b/doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md index c89b7929b13..daa13e52c0b 100644 --- a/doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md +++ b/doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md @@ -227,6 +227,9 @@ conjunction with multiple servers, as it can only perform changes on a **secondary** with only a single machine. Instead, you must do this manually. +DANGER: **Warning:** +In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later. + CAUTION: **Caution:** If the secondary node [has been paused](../../../geo/index.md#pausing-and-resuming-replication), this performs a point-in-time recovery to the last known state. diff --git a/doc/administration/geo/index.md b/doc/administration/geo/index.md index 085a2b52674..b9fffc9d4c2 100644 --- a/doc/administration/geo/index.md +++ b/doc/administration/geo/index.md @@ -196,6 +196,9 @@ For information on how to update your Geo nodes to the latest GitLab version, se > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35913) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2. +DANGER: **Warning:** +In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later. + CAUTION: **Caution:** Pausing and resuming of replication is currently only supported for Geo installations using an Omnibus GitLab-managed database. External databases are currently not supported. diff --git a/doc/administration/geo/replication/version_specific_updates.md b/doc/administration/geo/replication/version_specific_updates.md index f207b5901e9..e01c60c0925 100644 --- a/doc/administration/geo/replication/version_specific_updates.md +++ b/doc/administration/geo/replication/version_specific_updates.md @@ -24,6 +24,13 @@ DROP SERVER gitlab_secondary CASCADE; DROP EXTENSION IF EXISTS postgres_fdw; ``` +DANGER: **Warning:** +In GitLab 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. To avoid this issue, upgrade to GitLab 13.4 or later. + +## Updating to GitLab 13.2 + +In GitLab 13.2, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. To avoid this issue, upgrade to GitLab 13.4 or later. + ## Updating to GitLab 13.0 Upgrading to GitLab 13.0 requires GitLab 12.10 to already be using PostgreSQL diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md index 4dac9f61469..554ee3b1028 100644 --- a/doc/api/releases/index.md +++ b/doc/api/releases/index.md @@ -367,7 +367,7 @@ POST /projects/:id/releases | `assets:links` | array of hash | no | An array of assets links. | | `assets:links:name`| string | required by: `assets:links` | The name of the link. | | `assets:links:url` | string | required by: `assets:links` | The URL of the link. | -| `assets:links:filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md). +| `assets:links:filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md#permanent-links-to-release-assets). | `assets:links:link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`. | `released_at` | datetime | no | The date when the release will be/was ready. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | diff --git a/doc/api/releases/links.md b/doc/api/releases/links.md index 242b5eb41f5..2b33f6a4dc7 100644 --- a/doc/api/releases/links.md +++ b/doc/api/releases/links.md @@ -97,26 +97,29 @@ POST /projects/:id/releases/:tag_name/assets/links | `tag_name` | string | yes | The tag associated with the Release. | | `name` | string | yes | The name of the link. | | `url` | string | yes | The URL of the link. | +| `filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md#permanent-links-to-release-assets). | `link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`. | Example request: ```shell curl --request POST \ - --header "PRIVATE-TOKEN: n671WNGecHugsdEDPsyo" \ - --data name="awesome-v0.2.dmg" \ - --data url="http://192.168.10.15:3000" \ - "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links" + --header "PRIVATE-TOKEN: tkhfG7HgG-LiZd3zfdDC" \ + --data name="hellodarwin-amd64" \ + --data url="https://gitlab.example.com/mynamespace/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64" \ + --data filepath="/bin/hellodarwin-amd64" \ + "https://gitlab.example.com/api/v4/projects/20/releases/v1.7.0/assets/links" ``` Example response: ```json { - "id":1, - "name":"awesome-v0.2.dmg", - "url":"http://192.168.10.15:3000", - "external":true, + "id":2, + "name":"hellodarwin-amd64", + "url":"https://gitlab.example.com/mynamespace/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64", + "direct_asset_url":"https://gitlab.example.com/mynamespace/hello/-/releases/v1.7.0/downloads/bin/hellodarwin-amd64", + "external":false, "link_type":"other" } ``` @@ -136,6 +139,7 @@ PUT /projects/:id/releases/:tag_name/assets/links/:link_id | `link_id` | integer | yes | The ID of the link. | | `name` | string | no | The name of the link. | | `url` | string | no | The URL of the link. | +| `filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md#permanent-links-to-release-assets). | `link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`. | NOTE: **Note:** diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index fbc8557fe39..10b7cfc06e1 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -320,7 +320,7 @@ services: command: ["--registry-mirror", "https://registry-mirror.example.com"] # Specify the registry mirror to use. ``` -#### DinD service defined inside of GitLab Runner configuration +##### DinD service defined inside of GitLab Runner configuration > [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27173) in GitLab Runner 13.6. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 736a0414a3c..59f9dae660a 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -234,23 +234,23 @@ There are also two edge cases worth mentioning: > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29654) in GitLab 12.5 -The top-level `workflow:` key applies to the entirety of a pipeline, and -determines whether or not a pipeline is created. It accepts a single -`rules:` key that operates similarly to [`rules:` defined within jobs](#rules), -enabling dynamic configuration of the pipeline. +The top-level `workflow:` keyword determines whether or not a pipeline is created. +It accepts a single `rules:` keyword that is similar to [`rules:` defined within jobs](#rules). +Use it to define what can trigger a new pipeline. -If you are new to GitLab CI/CD and `workflow: rules`, you may find the [`workflow:rules` templates](#workflowrules-templates) useful. +You can use the [`workflow:rules` templates](#workflowrules-templates) to import +a preconfigured `workflow: rules` entry. -To define your own `workflow: rules`, the available configuration options are: +`workflow: rules` accepts these keywords: -- [`if`](#rulesif): Define a rule. -- [`when`](#when): May be set to `always` or `never` only. If not provided, the default value is `always`​. +- [`if`](#rulesif): Check this rule to determine when to run a pipeline. +- [`when`](#when): Specify what to do when the `if` rule evaluates to true. + - To run a pipeline, set to `always`. + - To prevent pipelines from running, set to `never`. -If a pipeline attempts to run but matches no rule, it's dropped and doesn't run. +When no rules evaluate to true, the pipeline does not run. -Use the example rules below exactly as written to allow pipelines that match the rule -to run. Add `when: never` to prevent pipelines that match the rule from running. See -the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more examples. +Some example `if` clauses for `workflow: rules`: | Example rules | Details | |------------------------------------------------------|-----------------------------------------------------------| @@ -259,9 +259,12 @@ the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more exa | `if: $CI_COMMIT_TAG` | Control when tag pipelines run. | | `if: $CI_COMMIT_BRANCH` | Control when branch pipelines run. | +See the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more examples. + For example, in the following configuration, pipelines run for all `push` events (changes to -branches and new tags). Only push events with `-wip` in the commit message are excluded. Scheduled -pipelines and merge request pipelines don't run, as there's no rule allowing them. +branches and new tags). Pipelines for push events with `-wip` in the commit message +don't run, because they are set to `when: never`. Pipelines for schedules or merge requests +don't run either, because no rules evaluate to true for them: ```yaml workflow: @@ -271,11 +274,11 @@ workflow: - if: '$CI_PIPELINE_SOURCE == "push"' ``` -This example has strict rules, and no other pipelines can run. +This example has strict rules, and pipelines do **not** run in any other case. -Alternatively, you can have loose rules by using only `when: never` rules, followed -by a final `when: always` rule. This allows all types of pipelines, except for any -that match the `when: never` rules: +Alternatively, all of the rules can be `when: never`, with a final +`when: always` rule. Pipelines that match the `when: never` rules do not run. +All other pipeline types run: ```yaml workflow: @@ -287,12 +290,13 @@ workflow: - when: always ``` -This example never allows pipelines for schedules or `push` (branches and tags) pipelines, -but does allow pipelines in **all** other cases, *including* merge request pipelines. +This example prevents pipelines for schedules or `push` (branches and tags) pipelines. +The final `when: always` rule lets all other pipeline types run, **including** merge +request pipelines. -Be careful not to use a configuration that might run -merge request pipelines and branch pipelines at the same time. As with `rules` defined in jobs, -it can cause [duplicate pipelines](#prevent-duplicate-pipelines). +Be careful not to have rules that match both branch pipelines +and merge request pipelines. Similar to `rules` defined in jobs, this can cause +[duplicate pipelines](#prevent-duplicate-pipelines). #### `workflow:rules` templates diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md index 8c0c189176c..1933b432138 100644 --- a/doc/gitlab-basics/command-line-commands.md +++ b/doc/gitlab-basics/command-line-commands.md @@ -7,14 +7,14 @@ type: howto, reference # Edit files through the command line -When [working with Git from the command line](start-using-git.md), you will need to +When [working with Git from the command line](start-using-git.md), you need to use more than just the Git commands. There are several basic commands that you should learn, in order to make full use of the command line. ## Start working on your project To work on a Git project locally (from your own computer), with the command line, -first you will need to [clone (copy) it](start-using-git.md#clone-a-repository) to +first you need to [clone (copy) it](start-using-git.md#clone-a-repository) to your computer. ## Working with files on the command line @@ -57,7 +57,7 @@ nano README.md ### Remove a file or directory -It is easy to delete (remove) a file or directory, but be careful: +It's easy to delete (remove) a file or directory, but be careful: DANGER: **Warning:** This will **permanently** delete a file. @@ -96,7 +96,7 @@ for example) . Execute the same full command with: Not all commands can be executed from a basic user account on a computer, you may need administrator's rights to execute commands that affect the system, or try to access protected data, for example. You can use `sudo` to execute these commands, but you -will likely be asked for an administrator password. +might be asked for an administrator password. ```shell sudo RESTRICTED-COMMAND @@ -108,8 +108,8 @@ damage to your data or system. ## Sample Git taskflow -If you are completely new to Git, looking through some [sample taskflows](https://rogerdudler.github.io/git-guide/) -will help you understand the best practices for using these commands as you work. +If you're completely new to Git, looking through some [sample taskflows](https://rogerdudler.github.io/git-guide/) +may help you understand the best practices for using these commands as you work. <!-- ## Troubleshooting diff --git a/doc/user/project/merge_requests/csv_export.md b/doc/user/project/merge_requests/csv_export.md index d41a2567674..52c6f8a8d41 100644 --- a/doc/user/project/merge_requests/csv_export.md +++ b/doc/user/project/merge_requests/csv_export.md @@ -6,23 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Export Merge Requests to CSV **(CORE)** -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3619) in GitLab 13.6. -> - It was [deployed behind a feature flag](../../../administration/feature_flags.md), disabled by default. -> - Became enabled by default in GitLab 13.6. -> - It's enabled on GitLab.com. -> - It's recommended for production use. -> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-export-merge-requests-to-csv). **(CORE ONLY)** -> - It can be enabled or disabled for a single project. - -CAUTION: **Warning:** -This feature might not be available to you. Check the **version history** note above for details. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3619) in GitLab 13.6. Exporting Merge Requests CSV enables you and your team to export all the data collected from merge requests into a comma-separated values (CSV) file, which stores tabular data in plain text. To export Merge Requests to CSV, navigate to your **Merge Requests** from the sidebar of a project and click **Export to CSV**. -Exported files are generated asynchronously and delivered as an email attachment upon generation. - ## CSV Output The following table shows what attributes will be present in the CSV. @@ -54,28 +43,3 @@ The following table shows what attributes will be present in the CSV. - Export merge requests to CSV is not available at the Group’s merge request list. - As the merge request CSV file is sent as an email attachment, the size is limited to 15MB to ensure successful delivery across a range of email providers. If you need to minimize the size of the file, you can narrow the search before export. For example, you can set up exports of open and closed merge requests in separate files. - -### Enable or disable Export Merge Requests to CSV **(CORE ONLY)** - -Export merge requests to CSV is under development but ready for production use. -It is deployed behind a feature flag that is **enabled by default**. -[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) -can opt to disable it. - -To enable it: - -```ruby -# For the instance -Feature.enable(:export_merge_requests_as_csv) -# For a single project -Feature.enable(:export_merge_requests_as_csv, Project.find(<project id>)) -``` - -To disable it: - -```ruby -# For the instance -Feature.disable(:export_merge_requests_as_csv) -# For a single project -Feature.disable(:export_merge_requests_as_csv, Project.find(<project id>)) -``` diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 64879b37dae..3e493f02392 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -32,6 +32,9 @@ To set up a project import/export: Note the following: +- Before you can import a project, you need to export the data first. + See [Exporting a project and its data](#exporting-a-project-and-its-data) + for how you can export a project through the UI. - Imports from a newer version of GitLab are not supported. The Importing GitLab version must be greater than or equal to the Exporting GitLab version. - Imports will fail unless the import and export GitLab instances are @@ -129,6 +132,11 @@ For more details on the specific data persisted in a project export, see the ## Exporting a project and its data +Full project export functionality is limited to project maintainers and owners. +You can configure such functionality through [project settings](index.md): + +To export a project and its data, follow these steps: + 1. Go to your project's homepage. 1. Click **Settings** in the sidebar. diff --git a/lib/gitlab/graphql/loaders/batch_model_loader.rb b/lib/gitlab/graphql/loaders/batch_model_loader.rb index 164fe74148c..9b85ba164d4 100644 --- a/lib/gitlab/graphql/loaders/batch_model_loader.rb +++ b/lib/gitlab/graphql/loaders/batch_model_loader.rb @@ -12,14 +12,11 @@ module Gitlab # rubocop: disable CodeReuse/ActiveRecord def find - BatchLoader::GraphQL.for({ model: model_class, id: model_id.to_i }).batch do |loader_info, loader| - per_model = loader_info.group_by { |info| info[:model] } - per_model.each do |model, info| - ids = info.map { |i| i[:id] } - results = model.where(id: ids) + BatchLoader::GraphQL.for(model_id.to_i).batch(key: model_class) do |ids, loader, args| + model = args[:key] + results = model.where(id: ids) - results.each { |record| loader.call({ model: model, id: record.id }, record) } - end + results.each { |record| loader.call(record.id, record) } end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/gitlab/middleware/handle_malformed_strings.rb b/lib/gitlab/middleware/handle_malformed_strings.rb index bb2a8ead525..9baa639caea 100644 --- a/lib/gitlab/middleware/handle_malformed_strings.rb +++ b/lib/gitlab/middleware/handle_malformed_strings.rb @@ -5,6 +5,8 @@ module Gitlab # There is no valid reason for a request to contain a malformed string # so just return HTTP 400 (Bad Request) if we receive one class HandleMalformedStrings + include ActionController::HttpAuthentication::Basic + NULL_BYTE_REGEX = Regexp.new(Regexp.escape("\u0000")).freeze attr_reader :app @@ -21,16 +23,26 @@ module Gitlab private - def request_contains_malformed_string?(request) + def request_contains_malformed_string?(env) return false if ENV['DISABLE_REQUEST_VALIDATION'] == '1' - request = Rack::Request.new(request) + # Duplicate the env, so it is not modified when accessing the parameters + # https://github.com/rails/rails/blob/34991a6ae2fc68347c01ea7382fa89004159e019/actionpack/lib/action_dispatch/http/parameters.rb#L59 + # The modification causes problems with our multipart middleware + request = ActionDispatch::Request.new(env.dup) return true if malformed_path?(request.path) + return true if credentials_malformed?(request) request.params.values.any? do |value| param_has_null_byte?(value) end + rescue ActionController::BadRequest + # If we can't build an ActionDispatch::Request something's wrong + # This would also happen if `#params` contains invalid UTF-8 + # in this case we'll return a 400 + # + true end def malformed_path?(path) @@ -40,6 +52,13 @@ module Gitlab true end + def credentials_malformed?(request) + credentials = decode_credentials(request).presence + return false unless credentials + + string_malformed?(credentials) + end + def param_has_null_byte?(value, depth = 0) # Guard against possible attack sending large amounts of nested params # Should be safe as deeply nested params are highly uncommon. diff --git a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb index 7d019dc5130..da013a06777 100644 --- a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb +++ b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb @@ -9,14 +9,12 @@ module Gitlab ISSUE_CREATED = 'g_project_management_issue_created' ISSUE_CLOSED = 'g_project_management_issue_closed' ISSUE_DESCRIPTION_CHANGED = 'g_project_management_issue_description_changed' - ISSUE_ITERATION_CHANGED = 'g_project_management_issue_iteration_changed' ISSUE_LABEL_CHANGED = 'g_project_management_issue_label_changed' ISSUE_MADE_CONFIDENTIAL = 'g_project_management_issue_made_confidential' ISSUE_MADE_VISIBLE = 'g_project_management_issue_made_visible' ISSUE_MILESTONE_CHANGED = 'g_project_management_issue_milestone_changed' ISSUE_REOPENED = 'g_project_management_issue_reopened' ISSUE_TITLE_CHANGED = 'g_project_management_issue_title_changed' - ISSUE_WEIGHT_CHANGED = 'g_project_management_issue_weight_changed' ISSUE_CROSS_REFERENCED = 'g_project_management_issue_cross_referenced' ISSUE_MOVED = 'g_project_management_issue_moved' ISSUE_RELATED = 'g_project_management_issue_related' @@ -24,9 +22,6 @@ module Gitlab ISSUE_MARKED_AS_DUPLICATE = 'g_project_management_issue_marked_as_duplicate' ISSUE_LOCKED = 'g_project_management_issue_locked' ISSUE_UNLOCKED = 'g_project_management_issue_unlocked' - ISSUE_ADDED_TO_EPIC = 'g_project_management_issue_added_to_epic' - ISSUE_REMOVED_FROM_EPIC = 'g_project_management_issue_removed_from_epic' - ISSUE_CHANGED_EPIC = 'g_project_management_issue_changed_epic' ISSUE_DESIGNS_ADDED = 'g_project_management_issue_designs_added' ISSUE_DESIGNS_MODIFIED = 'g_project_management_issue_designs_modified' ISSUE_DESIGNS_REMOVED = 'g_project_management_issue_designs_removed' @@ -78,14 +73,6 @@ module Gitlab track_unique_action(ISSUE_MILESTONE_CHANGED, author, time) end - def track_issue_iteration_changed_action(author:, time: Time.zone.now) - track_unique_action(ISSUE_ITERATION_CHANGED, author, time) - end - - def track_issue_weight_changed_action(author:, time: Time.zone.now) - track_unique_action(ISSUE_WEIGHT_CHANGED, author, time) - end - def track_issue_cross_referenced_action(author:, time: Time.zone.now) track_unique_action(ISSUE_CROSS_REFERENCED, author, time) end @@ -114,18 +101,6 @@ module Gitlab track_unique_action(ISSUE_UNLOCKED, author, time) end - def track_issue_added_to_epic_action(author:, time: Time.zone.now) - track_unique_action(ISSUE_ADDED_TO_EPIC, author, time) - end - - def track_issue_removed_from_epic_action(author:, time: Time.zone.now) - track_unique_action(ISSUE_REMOVED_FROM_EPIC, author, time) - end - - def track_issue_changed_epic_action(author:, time: Time.zone.now) - track_unique_action(ISSUE_CHANGED_EPIC, author, time) - end - def track_issue_designs_added_action(author:, time: Time.zone.now) track_unique_action(ISSUE_DESIGNS_ADDED, author, time) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b4898eff180..af33e6cb876 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2551,6 +2551,9 @@ msgstr "" msgid "AlertSettings|Copy" msgstr "" +msgid "AlertSettings|Delete integration" +msgstr "" + msgid "AlertSettings|Enter integration name" msgstr "" @@ -2668,9 +2671,21 @@ msgstr "" msgid "AlertsIntegrations|Prometheus" msgstr "" +msgid "AlertsIntegrations|The integration could not be added. Please try again." +msgstr "" + +msgid "AlertsIntegrations|The integration could not be deleted. Please try again." +msgstr "" + +msgid "AlertsIntegrations|The integration has been successfully removed." +msgstr "" + msgid "AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list." msgstr "" +msgid "AlertsIntegrations|You have opted to delete the %{integrationName} integration. Do you want to proceed? It means you will no longer receive alerts from this endpoint in your alert list, and this action cannot be undone." +msgstr "" + msgid "Algorithm" msgstr "" diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 31ce37a0607..d7d1278159b 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1998,10 +1998,6 @@ RSpec.describe Projects::MergeRequestsController do describe 'POST export_csv' do subject { post :export_csv, params: { namespace_id: project.namespace, project_id: project } } - before do - stub_feature_flags(export_merge_requests_as_csv: project) - end - it 'redirects to the merge request index' do subject @@ -2014,17 +2010,5 @@ RSpec.describe Projects::MergeRequestsController do subject end - - context 'feature is disabled' do - before do - stub_feature_flags(export_merge_requests_as_csv: false) - end - - it 'expects a 404 response' do - subject - - expect(response).to have_gitlab_http_status(:not_found) - end - end end end diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index a47f2285e37..952a78ec79a 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -52,29 +52,20 @@ RSpec.describe 'Dashboard Merge Requests' do end context 'merge requests exist' do - let_it_be(:author_user) { create(:user) } let(:label) { create(:label) } let!(:assigned_merge_request) do create(:merge_request, assignees: [current_user], source_project: project, - author: author_user) - end - - let!(:review_requested_merge_request) do - create(:merge_request, - reviewers: [current_user], - source_branch: 'review', - source_project: project, - author: author_user) + author: create(:user)) end let!(:assigned_merge_request_from_fork) do create(:merge_request, source_branch: 'markdown', assignees: [current_user], target_project: public_project, source_project: forked_project, - author: author_user) + author: create(:user)) end let!(:authored_merge_request) do @@ -103,7 +94,7 @@ RSpec.describe 'Dashboard Merge Requests' do create(:merge_request, source_branch: 'fix', source_project: project, - author: author_user) + author: create(:user)) end before do @@ -120,10 +111,6 @@ RSpec.describe 'Dashboard Merge Requests' do expect(page).not_to have_content(labeled_merge_request.title) end - it 'shows review requested merge requests' do - expect(page).to have_content(review_requested_merge_request.title) - end - it 'shows authored merge requests', :js do reset_filters input_filtered_search("author:=#{current_user.to_reference}") diff --git a/spec/features/file_uploads/multipart_invalid_uploads_spec.rb b/spec/features/file_uploads/multipart_invalid_uploads_spec.rb index e9e24c12af1..b3ace2e30ff 100644 --- a/spec/features/file_uploads/multipart_invalid_uploads_spec.rb +++ b/spec/features/file_uploads/multipart_invalid_uploads_spec.rb @@ -22,13 +22,13 @@ RSpec.describe 'Invalid uploads that must be rejected', :api, :js do ) end - RSpec.shared_examples 'rejecting invalid keys' do |key_name:, message: nil| + RSpec.shared_examples 'rejecting invalid keys' do |key_name:, message: nil, status: 500| context "with invalid key #{key_name}" do let(:body) { { key_name => file, 'package[test][name]' => 'test' } } it { expect { subject }.not_to change { Packages::Package.nuget.count } } - it { expect(subject.code).to eq(500) } + it { expect(subject.code).to eq(status) } it { expect(subject.body).to include(message.presence || "invalid field: \"#{key_name}\"") } end @@ -45,7 +45,7 @@ RSpec.describe 'Invalid uploads that must be rejected', :api, :js do # These keys are rejected directly by rack itself. # The request will not be received by multipart.rb (can't use the 'handling file uploads' shared example) it_behaves_like 'rejecting invalid keys', key_name: 'x' * 11000, message: 'Puma caught this error: exceeded available parameter key space (RangeError)' - it_behaves_like 'rejecting invalid keys', key_name: 'package[]test', message: 'Puma caught this error: expected Hash (got Array)' + it_behaves_like 'rejecting invalid keys', key_name: 'package[]test', status: 400, message: 'Bad Request' it_behaves_like 'handling file uploads', 'by rejecting uploads with an invalid key' end diff --git a/spec/features/merge_requests/user_exports_as_csv_spec.rb b/spec/features/merge_requests/user_exports_as_csv_spec.rb index 63ed1cb5231..a86ff9d7335 100644 --- a/spec/features/merge_requests/user_exports_as_csv_spec.rb +++ b/spec/features/merge_requests/user_exports_as_csv_spec.rb @@ -9,38 +9,23 @@ RSpec.describe 'Merge Requests > Exports as CSV', :js do before do sign_in(user) + visit(project_merge_requests_path(project)) end subject { page.find('.nav-controls') } - context 'feature is not enabled' do - before do - stub_feature_flags(export_merge_requests_as_csv: false) - visit(project_merge_requests_path(project)) - end - - it { is_expected.not_to have_button('Export as CSV') } - end + it { is_expected.to have_button('Export as CSV') } - context 'feature is enabled for a project' do + context 'button is clicked' do before do - stub_feature_flags(export_merge_requests_as_csv: project) - visit(project_merge_requests_path(project)) + click_button('Export as CSV') end - it { is_expected.to have_button('Export as CSV') } - - context 'button is clicked' do - before do - click_button('Export as CSV') - end - - it 'shows a success message' do - click_link('Export merge requests') + it 'shows a success message' do + click_link('Export merge requests') - expect(page).to have_content 'Your CSV export has started.' - expect(page).to have_content "It will be emailed to #{user.email} when complete" - end + expect(page).to have_content 'Your CSV export has started.' + expect(page).to have_content "It will be emailed to #{user.email} when complete" end end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 3ab1b26d71d..68958e37001 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -333,8 +333,6 @@ RSpec.describe MergeRequestsFinder do end context 'assignee filtering' do - let_it_be(:user3) { create(:user) } - let(:issuables) { described_class.new(user, params).execute } it_behaves_like 'assignee ID filter' do @@ -353,6 +351,7 @@ RSpec.describe MergeRequestsFinder do merge_request3.assignees = [user2, user3] end + let_it_be(:user3) { create(:user) } let(:params) { { assignee_username: [user2.username, user3.username] } } let(:expected_issuables) { [merge_request3] } end @@ -367,6 +366,7 @@ RSpec.describe MergeRequestsFinder do end it_behaves_like 'no assignee filter' do + let_it_be(:user3) { create(:user) } let(:expected_issuables) { [merge_request4, merge_request5] } end @@ -374,54 +374,30 @@ RSpec.describe MergeRequestsFinder do let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] } end - context 'with just reviewers' do - it_behaves_like 'assignee username filter' do - before do - merge_request4.reviewers = [user3] - merge_request4.assignees = [] - end + context 'filtering by group milestone' do + let(:group_milestone) { create(:milestone, group: group) } - let(:params) { { assignee_username: [user3.username] } } - let(:expected_issuables) { [merge_request4] } + before do + merge_request1.update!(milestone: group_milestone) + merge_request2.update!(milestone: group_milestone) end - end - context 'with an additional reviewer' do - it_behaves_like 'assignee username filter' do - before do - merge_request3.assignees = [user3] - merge_request4.reviewers = [user3] - end + it 'returns merge requests assigned to that group milestone' do + params = { milestone_title: group_milestone.title } - let(:params) { { assignee_username: [user3.username] } } - let(:expected_issuables) { [merge_request3, merge_request4] } - end - end - end - - context 'filtering by group milestone' do - let(:group_milestone) { create(:milestone, group: group) } - - before do - merge_request1.update!(milestone: group_milestone) - merge_request2.update!(milestone: group_milestone) - end - - it 'returns merge requests assigned to that group milestone' do - params = { milestone_title: group_milestone.title } - - merge_requests = described_class.new(user, params).execute + merge_requests = described_class.new(user, params).execute - expect(merge_requests).to contain_exactly(merge_request1, merge_request2) - end + expect(merge_requests).to contain_exactly(merge_request1, merge_request2) + end - context 'using NOT' do - let(:params) { { not: { milestone_title: group_milestone.title } } } + context 'using NOT' do + let(:params) { { not: { milestone_title: group_milestone.title } } } - it 'returns MRs not assigned to that group milestone' do - merge_requests = described_class.new(user, params).execute + it 'returns MRs not assigned to that group milestone' do + merge_requests = described_class.new(user, params).execute - expect(merge_requests).to contain_exactly(merge_request3, merge_request4, merge_request5) + expect(merge_requests).to contain_exactly(merge_request3, merge_request4, merge_request5) + end end end end @@ -587,27 +563,6 @@ RSpec.describe MergeRequestsFinder do expect(mrs).to eq([mr2]) end end - - it 'does not raise any exception with complex filters' do - # available filters from MergeRequest dashboard UI - params = { - project_id: project1.id, - scope: 'authored', - state: 'opened', - author_username: user.username, - assignee_username: user.username, - approver_usernames: [user.username], - approved_by_usernames: [user.username], - milestone_title: 'none', - release_tag: 'none', - label_names: 'none', - my_reaction_emoji: 'none', - draft: 'no' - } - - merge_requests = described_class.new(user, params).execute - expect { merge_requests.load }.not_to raise_error - end end describe '#row_count', :request_store do diff --git a/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js b/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js index 8d14babf4af..59238e40686 100644 --- a/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js +++ b/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js @@ -128,18 +128,18 @@ describe('AlertsSettingsFormNew', () => { it('allows for update-integration with the correct form values for HTTP', async () => { createComponent({ + data: { + selectedIntegration: typeSet.http, + }, props: { - currentIntegration: { id: '1' }, + currentIntegration: { id: '1', name: 'Test integration pre' }, loading: false, }, }); - const options = findSelect().findAll('option'); - await options.at(1).setSelected(); - await findFormFields() .at(0) - .setValue('Test integration'); + .setValue('Test integration post'); await findFormToggle().trigger('click'); await wrapper.vm.$nextTick(); @@ -153,27 +153,27 @@ describe('AlertsSettingsFormNew', () => { expect(wrapper.emitted('update-integration')).toBeTruthy(); expect(wrapper.emitted('update-integration')[0]).toEqual([ - { type: typeSet.http, variables: { name: 'Test integration', active: true } }, + { type: typeSet.http, variables: { name: 'Test integration post', active: true } }, ]); }); it('allows for update-integration with the correct form values for PROMETHEUS', async () => { createComponent({ + data: { + selectedIntegration: typeSet.prometheus, + }, props: { - currentIntegration: { id: '1' }, + currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' }, loading: false, }, }); - const options = findSelect().findAll('option'); - await options.at(2).setSelected(); - await findFormFields() .at(0) .setValue('Test integration'); await findFormFields() .at(1) - .setValue('https://test.com'); + .setValue('https://test-post.com'); await findFormToggle().trigger('click'); await wrapper.vm.$nextTick(); @@ -187,7 +187,7 @@ describe('AlertsSettingsFormNew', () => { expect(wrapper.emitted('update-integration')).toBeTruthy(); expect(wrapper.emitted('update-integration')[0]).toEqual([ - { type: typeSet.prometheus, variables: { apiUrl: 'https://test.com', active: true } }, + { type: typeSet.prometheus, variables: { apiUrl: 'https://test-post.com', active: true } }, ]); }); }); diff --git a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js index 5fa21d01340..e2e7034940d 100644 --- a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js +++ b/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js @@ -1,13 +1,17 @@ -import { mount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { mount, createLocalVue } from '@vue/test-utils'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; import { GlLoadingIcon } from '@gitlab/ui'; import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue'; import AlertsSettingsFormOld from '~/alerts_settings/components/alerts_settings_form_old.vue'; import AlertsSettingsFormNew from '~/alerts_settings/components/alerts_settings_form_new.vue'; import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue'; +import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql'; import createHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql'; import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql'; import updateHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql'; import updatePrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql'; +import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql'; import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql'; import resetPrometheusTokenMutation from '~/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql'; import { typeSet } from '~/alerts_settings/constants'; @@ -20,16 +24,34 @@ import { createPrometheusVariables, updatePrometheusVariables, ID, + errorMsg, + getIntegrationsQueryResponse, + destroyIntegrationResponse, + integrationToDestroy, + destroyIntegrationResponseWithErrors, } from './mocks/apollo_mock'; jest.mock('~/flash'); +const localVue = createLocalVue(); + describe('AlertsSettingsWrapper', () => { let wrapper; + let fakeApollo; + let destroyIntegrationHandler; const findLoader = () => wrapper.find(IntegrationsList).find(GlLoadingIcon); const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr'); + async function destroyHttpIntegration(localWrapper) { + await jest.runOnlyPendingTimers(); + await localWrapper.vm.$nextTick(); + + localWrapper + .find(IntegrationsList) + .vm.$emit('delete-integration', { id: integrationToDestroy.id }); + } + const createComponent = ({ data = {}, provide = {}, loading = false } = {}) => { wrapper = mount(AlertsSettingsWrapper, { data() { @@ -54,6 +76,29 @@ describe('AlertsSettingsWrapper', () => { }); }; + function createComponentWithApollo({ + destroyHandler = jest.fn().mockResolvedValue(destroyIntegrationResponse), + } = {}) { + localVue.use(VueApollo); + destroyIntegrationHandler = destroyHandler; + + const requestHandlers = [ + [getIntegrationsQuery, jest.fn().mockResolvedValue(getIntegrationsQueryResponse)], + [destroyHttpIntegrationMutation, destroyIntegrationHandler], + ]; + + fakeApollo = createMockApollo(requestHandlers); + + wrapper = mount(AlertsSettingsWrapper, { + localVue, + apolloProvider: fakeApollo, + provide: { + ...defaultAlertSettingsConfig, + glFeatures: { httpIntegrationsList: true }, + }, + }); + } + afterEach(() => { if (wrapper) { wrapper.destroy(); @@ -243,7 +288,6 @@ describe('AlertsSettingsWrapper', () => { }); it('shows error alert when integration creation fails ', async () => { - const errorMsg = 'Something went wrong'; createComponent({ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, provide: { glFeatures: { httpIntegrationsList: true } }, @@ -259,7 +303,6 @@ describe('AlertsSettingsWrapper', () => { }); it('shows error alert when integration token reset fails ', () => { - const errorMsg = 'Something went wrong'; createComponent({ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, provide: { glFeatures: { httpIntegrationsList: true } }, @@ -276,7 +319,6 @@ describe('AlertsSettingsWrapper', () => { }); it('shows error alert when integration update fails ', () => { - const errorMsg = 'Something went wrong'; createComponent({ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, provide: { glFeatures: { httpIntegrationsList: true } }, @@ -292,4 +334,41 @@ describe('AlertsSettingsWrapper', () => { }); }); }); + + describe('with mocked Apollo client', () => { + it('has a selection of integrations loaded via the getIntegrationsQuery', async () => { + createComponentWithApollo(); + + await jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); + + expect(findIntegrations()).toHaveLength(4); + }); + + it('calls a mutation with correct parameters and destroys a integration', async () => { + createComponentWithApollo(); + + await destroyHttpIntegration(wrapper); + + expect(destroyIntegrationHandler).toHaveBeenCalled(); + + await wrapper.vm.$nextTick(); + + expect(findIntegrations()).toHaveLength(3); + }); + + it('displays flash if mutation had a recoverable error', async () => { + createComponentWithApollo({ + destroyHandler: jest.fn().mockResolvedValue(destroyIntegrationResponseWithErrors), + }); + + await destroyHttpIntegration(wrapper); + + await wrapper.vm.$nextTick(); // kick off the DOM update + await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises) + await wrapper.vm.$nextTick(); // kick off the DOM update for flash + + expect(createFlash).toHaveBeenCalledWith({ message: 'Houston, we have a problem' }); + }); + }); }); diff --git a/spec/frontend/alerts_settings/mocks/apollo_mock.js b/spec/frontend/alerts_settings/mocks/apollo_mock.js index d4cf0627c66..e0eba1e8421 100644 --- a/spec/frontend/alerts_settings/mocks/apollo_mock.js +++ b/spec/frontend/alerts_settings/mocks/apollo_mock.js @@ -1,5 +1,6 @@ const projectPath = ''; export const ID = 'gid://gitlab/AlertManagement::HttpIntegration/7'; +export const errorMsg = 'Something went wrong'; export const createHttpVariables = { name: 'Test Pre', @@ -24,3 +25,99 @@ export const updatePrometheusVariables = { active: true, id: ID, }; + +export const getIntegrationsQueryResponse = { + data: { + project: { + alertManagementIntegrations: { + nodes: [ + { + id: '37', + type: 'HTTP', + active: true, + name: 'Test 5', + url: + 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json', + token: '89eb01df471d990ff5162a1c640408cf', + apiUrl: null, + }, + { + id: '41', + type: 'HTTP', + active: true, + name: 'Test 9999', + url: + 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-9999/b78a566e1776cfc2.json', + token: 'f7579aa03844e07af3b1f0fca3f79f81', + apiUrl: null, + }, + { + id: '40', + type: 'HTTP', + active: true, + name: 'Test 6', + url: + 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-6/3e828ae28a240222.json', + token: '6536102a607a5dd74fcdde921f2349ee', + apiUrl: null, + }, + { + id: '12', + type: 'PROMETHEUS', + active: false, + name: 'Prometheus', + url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/prometheus/alerts/notify.json', + token: '256f687c6225aa5d6ee50c3d68120c4c', + apiUrl: 'https://localhost.ieeeesassadasasa', + }, + ], + }, + }, + }, +}; + +export const integrationToDestroy = { + id: '37', + type: 'HTTP', + active: true, + name: 'Test 5', + url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json', + token: '89eb01df471d990ff5162a1c640408cf', + apiUrl: null, +}; + +export const destroyIntegrationResponse = { + data: { + httpIntegrationDestroy: { + errors: [], + integration: { + id: '37', + type: 'HTTP', + active: true, + name: 'Test 5', + url: + 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json', + token: '89eb01df471d990ff5162a1c640408cf', + apiUrl: null, + }, + }, + }, +}; + +export const destroyIntegrationResponseWithErrors = { + data: { + httpIntegrationDestroy: { + errors: ['Houston, we have a problem'], + integration: { + id: '37', + type: 'HTTP', + active: true, + name: 'Test 5', + url: + 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json', + token: '89eb01df471d990ff5162a1c640408cf', + apiUrl: null, + }, + }, + }, +}; diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js index ef22979ca7d..3276ef911e3 100644 --- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js @@ -22,7 +22,7 @@ describe('tags list row', () => { let wrapper; const [tag] = [...tagsListResponse.data]; - const defaultProps = { tag, isDesktop: true, index: 0 }; + const defaultProps = { tag, isMobile: false, index: 0 }; const findCheckbox = () => wrapper.find(GlFormCheckbox); const findName = () => wrapper.find('[data-testid="name"]'); @@ -114,7 +114,7 @@ describe('tags list row', () => { }); it('on mobile has mw-s class', () => { - mountComponent({ ...defaultProps, isDesktop: false }); + mountComponent({ ...defaultProps, isMobile: true }); expect(findName().classes('mw-s')).toBe(true); }); diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js index 401202026bb..ebeaa8ff870 100644 --- a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js @@ -14,7 +14,7 @@ describe('Tags List', () => { const findDeleteButton = () => wrapper.find(GlButton); const findListTitle = () => wrapper.find('[data-testid="list-title"]'); - const mountComponent = (propsData = { tags, isDesktop: true }) => { + const mountComponent = (propsData = { tags, isMobile: false }) => { wrapper = shallowMount(component, { propsData, }); @@ -41,15 +41,15 @@ describe('Tags List', () => { describe('delete button', () => { it.each` - inputTags | isDesktop | isVisible - ${tags} | ${true} | ${true} - ${tags} | ${false} | ${false} - ${readOnlyTags} | ${true} | ${false} - ${readOnlyTags} | ${false} | ${false} + inputTags | isMobile | isVisible + ${tags} | ${false} | ${true} + ${tags} | ${true} | ${false} + ${readOnlyTags} | ${false} | ${false} + ${readOnlyTags} | ${true} | ${false} `( - 'is $isVisible that delete button exists when tags is $inputTags and isDesktop is $isDesktop', - ({ inputTags, isDesktop, isVisible }) => { - mountComponent({ tags: inputTags, isDesktop }); + 'is $isVisible that delete button exists when tags is $inputTags and isMobile is $isMobile', + ({ inputTags, isMobile, isVisible }) => { + mountComponent({ tags: inputTags, isMobile }); expect(findDeleteButton().exists()).toBe(isVisible); }, @@ -110,12 +110,6 @@ describe('Tags List', () => { expect(rows.at(0).attributes()).toMatchObject({ first: 'true', - isdesktop: 'true', - }); - - // The list has only two tags and for some reasons .at(-1) does not work - expect(rows.at(1).attributes()).toMatchObject({ - isdesktop: 'true', }); }); diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js index 86b52c4f06a..372875914a5 100644 --- a/spec/frontend/registry/explorer/pages/details_spec.js +++ b/spec/frontend/registry/explorer/pages/details_spec.js @@ -124,7 +124,7 @@ describe('Details Page', () => { it('has the correct props bound', () => { expect(findTagsList().props()).toMatchObject({ - isDesktop: true, + isMobile: false, tags: store.state.tags, }); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js index 5c7e6a87c16..56832f82b05 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlDeprecatedDropdownItem } from '@gitlab/ui'; +import { GlDropdownItem } from '@gitlab/ui'; import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue'; const commits = [ @@ -39,7 +39,7 @@ describe('Commits message dropdown component', () => { wrapper.destroy(); }); - const findDropdownElements = () => wrapper.findAll(GlDeprecatedDropdownItem); + const findDropdownElements = () => wrapper.findAll(GlDropdownItem); const findFirstDropdownElement = () => findDropdownElements().at(0); it('should have 3 elements in dropdown list', () => { diff --git a/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb index cf1f00bc176..7ae33346388 100644 --- a/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb +++ b/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb @@ -4,8 +4,9 @@ require 'spec_helper' RSpec.describe Gitlab::Graphql::Loaders::BatchModelLoader do describe '#find' do - let(:issue) { create(:issue) } - let(:user) { create(:user) } + let_it_be(:issue) { create(:issue) } + let_it_be(:other_user) { create(:user) } + let_it_be(:user) { create(:user) } it 'finds a model by id' do issue_result = described_class.new(Issue, issue.id).find @@ -16,15 +17,25 @@ RSpec.describe Gitlab::Graphql::Loaders::BatchModelLoader do end it 'only queries once per model' do - other_user = create(:user) - user - issue - expect do [described_class.new(User, other_user.id).find, described_class.new(User, user.id).find, described_class.new(Issue, issue.id).find].map(&:sync) end.not_to exceed_query_limit(2) end + + it 'does not force values unnecessarily' do + expect do + a = described_class.new(User, user.id).find + b = described_class.new(Issue, issue.id).find + + b.sync + + c = described_class.new(User, other_user.id).find + + a.sync + c.sync + end.not_to exceed_query_limit(2) + end end end diff --git a/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb b/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb index 5ed1580fa8d..fec273ecafd 100644 --- a/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb +++ b/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb @@ -4,6 +4,8 @@ require 'spec_helper' require "rack/test" RSpec.describe Gitlab::Middleware::HandleMalformedStrings do + include GitHttpHelpers + let(:null_byte) { "\u0000" } let(:escaped_null_byte) { "%00" } let(:invalid_string) { "mal\xC0formed" } @@ -57,6 +59,22 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do end end + context 'in authorization headers' do + let(:problematic_input) { null_byte } + + it 'rejects problematic input in the password' do + env = env_for.merge(auth_env("username", "password#{problematic_input}encoded", nil)) + + expect(subject.call(env)).to eq error_400 + end + + it 'rejects problematic input in the password' do + env = env_for.merge(auth_env("username#{problematic_input}", "password#{problematic_input}encoded", nil)) + + expect(subject.call(env)).to eq error_400 + end + end + context 'in params' do shared_examples_for 'checks params' do it 'rejects bad params in a top level param' do @@ -86,24 +104,24 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do expect(subject.call(env)).to eq error_400 end + end + + context 'with null byte' do + let(:problematic_input) { null_byte } + + it_behaves_like 'checks params' it "gives up and does not reject too deeply nested params" do env = env_for(name: [ - { - inner_key: { deeper_key: [{ hash_inside_array_key: "I am #{problematic_input} bad" }] } - } - ]) + { + inner_key: { deeper_key: [{ hash_inside_array_key: "I am #{problematic_input} bad" }] } + } + ]) expect(subject.call(env)).not_to eq error_400 end end - context 'with null byte' do - it_behaves_like 'checks params' do - let(:problematic_input) { null_byte } - end - end - context 'with malformed strings' do it_behaves_like 'checks params' do let(:problematic_input) { invalid_string } @@ -124,4 +142,10 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do expect(subject.call(env)).not_to eq error_400 end end + + it 'does not modify the env' do + env = env_for + + expect { subject.call(env) }.not_to change { env } + end end diff --git a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb index c86b3a01d1e..803eff05efe 100644 --- a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb @@ -168,36 +168,6 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end end - context 'for Issue added to epic actions' do - it_behaves_like 'a tracked issue edit event' do - let(:action) { described_class::ISSUE_ADDED_TO_EPIC} - - def track_action(params) - described_class.track_issue_added_to_epic_action(**params) - end - end - end - - context 'for Issue removed from epic actions' do - it_behaves_like 'a tracked issue edit event' do - let(:action) { described_class::ISSUE_REMOVED_FROM_EPIC} - - def track_action(params) - described_class.track_issue_removed_from_epic_action(**params) - end - end - end - - context 'for Issue changed epic actions' do - it_behaves_like 'a tracked issue edit event' do - let(:action) { described_class::ISSUE_CHANGED_EPIC} - - def track_action(params) - described_class.track_issue_changed_epic_action(**params) - end - end - end - context 'for Issue designs added actions' do it_behaves_like 'a tracked issue edit event' do let(:action) { described_class::ISSUE_DESIGNS_ADDED } diff --git a/spec/requests/user_sends_malformed_strings_spec.rb b/spec/requests/user_sends_malformed_strings_spec.rb index b6eda9159bc..da533606be5 100644 --- a/spec/requests/user_sends_malformed_strings_spec.rb +++ b/spec/requests/user_sends_malformed_strings_spec.rb @@ -2,7 +2,9 @@ require 'spec_helper' -RSpec.describe 'User sends malformed strings as params' do +RSpec.describe 'User sends malformed strings' do + include GitHttpHelpers + let(:null_byte) { "\u0000" } let(:invalid_string) { "mal\xC0formed" } @@ -17,4 +19,10 @@ RSpec.describe 'User sends malformed strings as params' do expect(response).to have_gitlab_http_status(:bad_request) end + + it 'raises a 400 error with null bytes in the auth headers' do + clone_get("project/path", user: "hello#{null_byte}", password: "nothing to see") + + expect(response).to have_gitlab_http_status(:bad_request) + end end diff --git a/spec/views/projects/settings/operations/show.html.haml_spec.rb b/spec/views/projects/settings/operations/show.html.haml_spec.rb index 24ab64b20f5..facb4e2016d 100644 --- a/spec/views/projects/settings/operations/show.html.haml_spec.rb +++ b/spec/views/projects/settings/operations/show.html.haml_spec.rb @@ -25,7 +25,7 @@ RSpec.describe 'projects/settings/operations/show' do end before_all do - project.add_reporter(user) + project.add_maintainer(user) end before do |