diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-10 00:10:34 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-10 00:10:34 +0300 |
commit | f820d18e56f2bd63dd0f91b076ace59345a036a1 (patch) | |
tree | 93faf6cea37b703dc3cd01ef81b4b8d63f855582 | |
parent | d40d684afaec767bb05efdbeaa4ce3620cd337bb (diff) |
Add latest changes from gitlab-org/gitlab@master
19 files changed, 447 insertions, 139 deletions
diff --git a/app/assets/javascripts/branches/components/delete_branch_modal.vue b/app/assets/javascripts/branches/components/delete_branch_modal.vue index 040346e56e3..14c2badeb3f 100644 --- a/app/assets/javascripts/branches/components/delete_branch_modal.vue +++ b/app/assets/javascripts/branches/components/delete_branch_modal.vue @@ -1,6 +1,5 @@ <script> import { GlButton, GlFormInput, GlModal, GlSprintf, GlAlert } from '@gitlab/ui'; -import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; import csrf from '~/lib/utils/csrf'; import { sprintf, s__ } from '~/locale'; import eventHub from '../event_hub'; @@ -16,7 +15,6 @@ export default { }, data() { return { - visible: false, isProtectedBranch: false, branchName: '', defaultBranchName: '', @@ -69,9 +67,10 @@ export default { }, }, mounted() { - eventHub.$on('openModal', (options) => { - this.openModal(options); - }); + eventHub.$on('openModal', this.openModal); + }, + destroyed() { + eventHub.$off('openModal', this.openModal); }, methods: { openModal({ isProtectedBranch, branchName, defaultBranchName, deletePath, merged }) { @@ -81,13 +80,13 @@ export default { this.deletePath = deletePath; this.merged = merged; - this.$root.$emit(BV_SHOW_MODAL, this.modalId); + this.$refs.modal.show(); }, submitForm() { this.$refs.form.submit(); }, closeModal() { - this.$root.$emit(BV_HIDE_MODAL, this.modalId); + this.$refs.modal.hide(); }, }, i18n: { @@ -117,7 +116,7 @@ export default { </script> <template> - <gl-modal :visible="visible" size="sm" :modal-id="modalId" :title="title"> + <gl-modal ref="modal" size="sm" :modal-id="modalId" :title="title"> <gl-alert class="gl-mb-5" variant="danger" :dismissible="false"> <div data-testid="modal-message"> <gl-sprintf :message="message"> @@ -175,7 +174,7 @@ export default { <template #modal-footer> <div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0"> - <gl-button @click="closeModal"> + <gl-button data-testid="delete-branch-cancel-button" @click="closeModal"> {{ $options.i18n.cancelButtonText }} </gl-button> <div class="gl-mr-3"></div> @@ -184,7 +183,7 @@ export default { :disabled="deleteButtonDisabled" variant="danger" data-qa-selector="delete_branch_confirmation_button" - data-testid="delete_branch_confirmation_button" + data-testid="delete-branch-confirmation-button" @click="submitForm" >{{ buttonText }}</gl-button > diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index 3f746731e34..b3c5af5418f 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -58,7 +58,7 @@ export default { }, computed: { tooltipText() { - return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} + return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} - ${this.sourceJobInfo}`; }, buttonId() { @@ -71,7 +71,7 @@ export default { return this.pipeline.project.name; }, downstreamTitle() { - return this.childPipeline ? __('child-pipeline') : this.pipeline.project.name; + return this.childPipeline ? this.sourceJobName : this.pipeline.project.name; }, parentPipeline() { return this.isUpstream && this.isSameProject; @@ -163,7 +163,7 @@ export default { /> <div v-else class="gl-pr-2"><gl-loading-icon inline /></div> <div class="gl-display-flex gl-flex-direction-column gl-w-13"> - <span class="gl-text-truncate"> + <span class="gl-text-truncate" data-testid="downstream-title"> {{ downstreamTitle }} </span> <div class="gl-text-truncate"> diff --git a/app/assets/javascripts/security_configuration/components/redesigned_app.vue b/app/assets/javascripts/security_configuration/components/redesigned_app.vue index 1d51fe14901..c2d57e8f0c8 100644 --- a/app/assets/javascripts/security_configuration/components/redesigned_app.vue +++ b/app/assets/javascripts/security_configuration/components/redesigned_app.vue @@ -35,13 +35,27 @@ export default { type: Array, required: true, }, - + gitlabCiPresent: { + type: Boolean, + required: false, + default: false, + }, + gitlabCiHistoryPath: { + type: String, + required: false, + default: '', + }, latestPipelinePath: { type: String, required: false, default: '', }, }, + computed: { + canViewCiHistory() { + return Boolean(this.gitlabCiPresent && this.gitlabCiHistoryPath); + }, + }, }; </script> @@ -66,6 +80,11 @@ export default { </template> </gl-sprintf> </p> + <p v-if="canViewCiHistory"> + <gl-link data-testid="security-view-history-link" :href="gitlabCiHistoryPath">{{ + $options.i18n.configurationHistory + }}</gl-link> + </p> </template> <template #features> @@ -92,6 +111,11 @@ export default { </template> </gl-sprintf> </p> + <p v-if="canViewCiHistory"> + <gl-link data-testid="compliance-view-history-link" :href="gitlabCiHistoryPath">{{ + $options.i18n.configurationHistory + }}</gl-link> + </p> </template> <template #features> <feature-card diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js index 6c4a01ea76b..e1dc6f24737 100644 --- a/app/assets/javascripts/security_configuration/index.js +++ b/app/assets/javascripts/security_configuration/index.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; +import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils'; import SecurityConfigurationApp from './components/app.vue'; import { securityFeatures, complianceFeatures } from './components/constants'; import RedesignedSecurityConfigurationApp from './components/redesigned_app.vue'; @@ -17,7 +18,13 @@ export const initStaticSecurityConfiguration = (el) => { defaultClient: createDefaultClient(), }); - const { projectPath, upgradePath, features, latestPipelinePath } = el.dataset; + const { + projectPath, + upgradePath, + features, + latestPipelinePath, + gitlabCiHistoryPath, + } = el.dataset; if (gon.features.securityConfigurationRedesign) { const { augmentedSecurityFeatures, augmentedComplianceFeatures } = augmentFeatures( @@ -39,6 +46,8 @@ export const initStaticSecurityConfiguration = (el) => { augmentedComplianceFeatures, augmentedSecurityFeatures, latestPipelinePath, + gitlabCiHistoryPath, + ...parseBooleanDataAttributes(el, ['gitlabCiPresent']), }, }); }, diff --git a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue new file mode 100644 index 00000000000..8fdc5ca78db --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue @@ -0,0 +1,82 @@ +<script> +import { reportTypeToSecurityReportTypeEnum } from 'ee_else_ce/vue_shared/security_reports/constants'; +import createFlash from '~/flash'; +import { s__ } from '~/locale'; +import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; +import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql'; +import { extractSecurityReportArtifactsFromMergeRequest } from '~/vue_shared/security_reports/utils'; + +export default { + components: { + SecurityReportDownloadDropdown, + }, + props: { + reportTypes: { + type: Array, + required: true, + validator: (reportType) => { + return reportType.every((report) => reportTypeToSecurityReportTypeEnum[report]); + }, + }, + targetProjectFullPath: { + type: String, + required: true, + }, + mrIid: { + type: Number, + required: true, + }, + }, + data() { + return { + reportArtifacts: [], + }; + }, + apollo: { + reportArtifacts: { + query: securityReportMergeRequestDownloadPathsQuery, + variables() { + return { + projectPath: this.targetProjectFullPath, + iid: String(this.mrIid), + reportTypes: this.reportTypes.map( + (reportType) => reportTypeToSecurityReportTypeEnum[reportType], + ), + }; + }, + update(data) { + return extractSecurityReportArtifactsFromMergeRequest(this.reportTypes, data); + }, + error(error) { + this.showError(error); + }, + }, + }, + computed: { + isLoadingReportArtifacts() { + return this.$apollo.queries.reportArtifacts.loading; + }, + }, + methods: { + showError(error) { + createFlash({ + message: this.$options.i18n.apiError, + captureError: true, + error, + }); + }, + }, + i18n: { + apiError: s__( + 'SecurityReports|Failed to get security report information. Please reload the page or try again later.', + ), + }, +}; +</script> + +<template> + <security-report-download-dropdown + :artifacts="reportArtifacts" + :loading="isLoadingReportArtifacts" + /> +</template> diff --git a/app/models/environment.rb b/app/models/environment.rb index 53ee90a1b67..558963c98c4 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -223,6 +223,7 @@ class Environment < ApplicationRecord Gitlab::Ci::Variables::Collection.new .append(key: 'CI_ENVIRONMENT_NAME', value: name) .append(key: 'CI_ENVIRONMENT_SLUG', value: slug) + .append(key: 'CI_ENVIRONMENT_TIER', value: tier) end def recently_updated_on_branch?(ref) diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md index 595f843907c..fe168461a68 100644 --- a/doc/ci/variables/predefined_variables.md +++ b/doc/ci/variables/predefined_variables.md @@ -53,6 +53,7 @@ There are also [Kubernetes-specific deployment variables](../../user/project/clu | `CI_ENVIRONMENT_SLUG` | 8.15 | all | The simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, and so on. Available if [`environment:name`](../yaml/README.md#environmentname) is set. The slug is [truncated to 24 characters](https://gitlab.com/gitlab-org/gitlab/-/issues/20941). | | `CI_ENVIRONMENT_URL` | 9.3 | all | The URL of the environment for this job. Available if [`environment:url`](../yaml/README.md#environmenturl) is set. | | `CI_ENVIRONMENT_ACTION` | 13.11 | all | The action annotation specified for this job's environment. Available if [`environment:action`](../yaml/README.md#environmentaction) is set. Can be `start`, `prepare`, or `stop`. | +| `CI_ENVIRONMENT_TIER` | 14.0 | all | The [deployment tier of the environment](../environments/index.md#deployment-tier-of-environments) for this job. | | `CI_HAS_OPEN_REQUIREMENTS` | 13.1 | all | Only available if the pipeline's project has an open [requirement](../../user/project/requirements/index.md). `true` when available. | | `CI_JOB_ID` | 9.0 | all | The internal ID of the job, unique across all jobs in the GitLab instance. | | `CI_JOB_IMAGE` | 12.9 | 12.9 | The name of the Docker image running the job. | diff --git a/doc/user/application_security/dependency_scanning/analyzers.md b/doc/user/application_security/dependency_scanning/analyzers.md index 0faa33e0123..fae0f457a20 100644 --- a/doc/user/application_security/dependency_scanning/analyzers.md +++ b/doc/user/application_security/dependency_scanning/analyzers.md @@ -80,7 +80,7 @@ include: template: Dependency-Scanning.gitlab-ci.yml variables: - DS_EXCLUDED_ANALYZERS: "gemnasium, gemansium-maven, gemnasium-python, bundler-audit, retire.js" + DS_EXCLUDED_ANALYZERS: "gemnasium, gemnasium-maven, gemnasium-python, bundler-audit, retire.js" ``` This is used when one totally relies on [custom analyzers](#custom-analyzers). diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 813c3d88538..b5c51c42ae9 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -76,6 +76,35 @@ can assign, categorize, and track progress on a merge request: - [**Notifications**](../../profile/notifications.md): A toggle to select whether or not to receive notifications for updates to a merge request. +## Close a merge request + +If you decide to permanently stop work on a merge request, +GitLab recommends you close the merge request rather than +[delete it](#delete-a-merge-request). Users with +Developer, Maintainer, or Owner [roles](../../permissions.md) in a project +can close merge requests in the project: + +1. Go to the merge request you want to close. +1. Scroll to the comment box at the bottom of the page. +1. Following the comment box, select **Close merge request**. + +GitLab closes the merge request, but preserves records of the merge request, +its comments, and any associated pipelines. + +### Delete a merge request + +GitLab recommends you close, rather than delete, merge requests. + +WARNING: +You cannot undo the deletion of a merge request. + +To delete a merge request: + +1. Sign in to GitLab as a user with the project Owner [role](../../permissions.md). + Only users with this role can delete merge requests in a project. +1. Go to the merge request you want to delete, and select **Edit**. +1. Scroll to the bottom of the page, and select **Delete merge request**. + ## Merge request workflows For a software developer working in a team: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c3f328bde31..78a85532525 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -38033,9 +38033,6 @@ msgstr "" msgid "cannot merge" msgstr "" -msgid "child-pipeline" -msgstr "" - msgid "ciReport|%{degradedNum} degraded" msgstr "" diff --git a/package.json b/package.json index 15a29b7bd36..d5de6a60924 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@gitlab/favicon-overlay": "2.0.0", "@gitlab/svgs": "1.199.0", "@gitlab/tributejs": "1.0.0", - "@gitlab/ui": "29.33.0", + "@gitlab/ui": "29.34.0", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "6.1.3-2", "@rails/ujs": "6.1.3-2", diff --git a/spec/frontend/branches/components/__snapshots__/delete_branch_modal_spec.js.snap b/spec/frontend/branches/components/__snapshots__/delete_branch_modal_spec.js.snap deleted file mode 100644 index 187ba4ba7f9..00000000000 --- a/spec/frontend/branches/components/__snapshots__/delete_branch_modal_spec.js.snap +++ /dev/null @@ -1,67 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Delete branch modal Deleting a protected branch (for owner or maintainer) renders the modal correctly 1`] = ` -"<div visible=\\"visible\\"> - <gl-alert-stub title=\\"\\" dismisslabel=\\"Dismiss\\" variant=\\"danger\\" primarybuttonlink=\\"\\" primarybuttontext=\\"\\" secondarybuttonlink=\\"\\" secondarybuttontext=\\"\\" class=\\"gl-mb-5\\"> - <div data-testid=\\"modal-message\\"> - <gl-sprintf-stub message=\\"You're about to permanently delete the protected branch %{strongStart}test_modal.%{strongEnd}\\"></gl-sprintf-stub> - <p class=\\"gl-mb-0 gl-mt-4\\"> - This branch hasn’t been merged into default. To avoid data loss, consider merging this branch before deleting it. - </p> - </div> - </gl-alert-stub> - <form action=\\"/path/to/branch\\" method=\\"post\\"> - <div class=\\"gl-mt-4\\"> - <p> - <gl-sprintf-stub message=\\"Once you confirm and press %{strongStart}Yes, delete protected branch,%{strongEnd} it cannot be undone or recovered.\\"></gl-sprintf-stub> - </p> - <p> - <gl-sprintf-stub message=\\"Please type the following to confirm:\\"></gl-sprintf-stub> <code class=\\"gl-white-space-pre-wrap\\"> test_modal </code> - <b-form-input-stub name=\\"delete_branch_input\\" value=\\"\\" autocomplete=\\"off\\" debounce=\\"0\\" type=\\"text\\" aria-labelledby=\\"input-label\\" class=\\"gl-form-input gl-mt-4\\"></b-form-input-stub> - </p> - </div> <input type=\\"hidden\\" name=\\"_method\\" value=\\"delete\\"> <input type=\\"hidden\\" name=\\"authenticity_token\\"> - </form> - <div class=\\"gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0\\"> - <b-button-stub size=\\"md\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" class=\\"gl-button\\"> - <!----> - <!----> <span class=\\"gl-button-text\\"> - Cancel, keep branch - </span></b-button-stub> - <div class=\\"gl-mr-3\\"></div> - <b-button-stub disabled=\\"true\\" size=\\"md\\" variant=\\"danger\\" type=\\"button\\" tag=\\"button\\" data-qa-selector=\\"delete_branch_confirmation_button\\" data-testid=\\"delete_branch_confirmation_button\\" class=\\"gl-button\\"> - <!----> - <!----> <span class=\\"gl-button-text\\">Yes, delete protected branch</span></b-button-stub> - </div> -</div>" -`; - -exports[`Delete branch modal Deleting a regular branch renders the modal correctly 1`] = ` -"<div visible=\\"visible\\"> - <gl-alert-stub title=\\"\\" dismisslabel=\\"Dismiss\\" variant=\\"danger\\" primarybuttonlink=\\"\\" primarybuttontext=\\"\\" secondarybuttonlink=\\"\\" secondarybuttontext=\\"\\" class=\\"gl-mb-5\\"> - <div data-testid=\\"modal-message\\"> - <gl-sprintf-stub message=\\"You're about to permanently delete the branch %{strongStart}test_modal.%{strongEnd}\\"></gl-sprintf-stub> - <p class=\\"gl-mb-0 gl-mt-4\\"> - This branch hasn’t been merged into default. To avoid data loss, consider merging this branch before deleting it. - </p> - </div> - </gl-alert-stub> - <form action=\\"/path/to/branch\\" method=\\"post\\"> - <div> - <p class=\\"gl-mt-4\\"> - <gl-sprintf-stub message=\\"Deleting the %{strongStart}test_modal%{strongEnd} branch cannot be undone. Are you sure?\\"></gl-sprintf-stub> - </p> - </div> <input type=\\"hidden\\" name=\\"_method\\" value=\\"delete\\"> <input type=\\"hidden\\" name=\\"authenticity_token\\"> - </form> - <div class=\\"gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0\\"> - <b-button-stub size=\\"md\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" class=\\"gl-button\\"> - <!----> - <!----> <span class=\\"gl-button-text\\"> - Cancel, keep branch - </span></b-button-stub> - <div class=\\"gl-mr-3\\"></div> - <b-button-stub size=\\"md\\" variant=\\"danger\\" type=\\"button\\" tag=\\"button\\" data-qa-selector=\\"delete_branch_confirmation_button\\" data-testid=\\"delete_branch_confirmation_button\\" class=\\"gl-button\\"> - <!----> - <!----> <span class=\\"gl-button-text\\">Yes, delete branch</span></b-button-stub> - </div> -</div>" -`; diff --git a/spec/frontend/branches/components/delete_branch_button_spec.js b/spec/frontend/branches/components/delete_branch_button_spec.js index 515c8f0ec6f..acbc83a9bdc 100644 --- a/spec/frontend/branches/components/delete_branch_button_spec.js +++ b/spec/frontend/branches/components/delete_branch_button_spec.js @@ -29,7 +29,7 @@ describe('Delete branch button', () => { wrapper.destroy(); }); - it('renders the button with correct tooltip, style, and icon', () => { + it('renders the button with default tooltip, style, and icon', () => { createComponent(); expect(findDeleteButton().attributes()).toMatchObject({ @@ -42,7 +42,20 @@ describe('Delete branch button', () => { it('renders a different tooltip for a protected branch', () => { createComponent({ isProtectedBranch: true }); - expect(findDeleteButton().attributes('title')).toBe('Delete protected branch'); + expect(findDeleteButton().attributes()).toMatchObject({ + title: 'Delete protected branch', + variant: 'danger', + icon: 'remove', + }); + }); + + it('renders a different protected tooltip when it is both protected and disabled', () => { + createComponent({ isProtectedBranch: true, disabled: true }); + + expect(findDeleteButton().attributes()).toMatchObject({ + title: 'Only a project maintainer or owner can delete a protected branch', + variant: 'default', + }); }); it('emits the data to eventHub when button is clicked', () => { @@ -63,14 +76,21 @@ describe('Delete branch button', () => { it('does not disable the button by default when mounted', () => { createComponent(); - expect(findDeleteButton().attributes('disabled')).not.toBe('true'); + expect(findDeleteButton().attributes()).toMatchObject({ + title: 'Delete branch', + variant: 'danger', + }); }); // Used for unallowed users and for the default branch. it('disables the button when mounted for a disabled modal', () => { - createComponent({ disabled: true }); + createComponent({ disabled: true, tooltip: 'The default branch cannot be deleted' }); - expect(findDeleteButton().attributes('disabled')).toBe('true'); + expect(findDeleteButton().attributes()).toMatchObject({ + title: 'The default branch cannot be deleted', + disabled: 'true', + variant: 'default', + }); }); }); }); diff --git a/spec/frontend/branches/components/delete_branch_modal_spec.js b/spec/frontend/branches/components/delete_branch_modal_spec.js index 62110af7e84..0c6111bda9e 100644 --- a/spec/frontend/branches/components/delete_branch_modal_spec.js +++ b/spec/frontend/branches/components/delete_branch_modal_spec.js @@ -1,86 +1,157 @@ -import { GlButton, GlModal, GlFormInput } from '@gitlab/ui'; +import { GlButton, GlModal, GlFormInput, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { stubComponent } from 'helpers/stub_component'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import DeleteBranchModal from '~/branches/components/delete_branch_modal.vue'; +import eventHub from '~/branches/event_hub'; let wrapper; const branchName = 'test_modal'; +const defaultBranchName = 'default'; +const deletePath = '/path/to/branch'; +const merged = false; +const isProtectedBranch = false; const createComponent = (data = {}) => { - wrapper = shallowMount(DeleteBranchModal, { - data() { - return { - branchName, - deletePath: '/path/to/branch', - defaultBranchName: 'default', - ...data, - }; - }, - attrs: { - visible: true, - }, - stubs: { - GlModal: stubComponent(GlModal, { - template: - '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>', - }), - GlButton, - GlFormInput, - }, - }); + wrapper = extendedWrapper( + shallowMount(DeleteBranchModal, { + data() { + return { + branchName, + deletePath, + defaultBranchName, + merged, + isProtectedBranch, + ...data, + }; + }, + stubs: { + GlModal: stubComponent(GlModal, { + template: + '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>', + }), + GlButton, + GlFormInput, + GlSprintf, + }, + }), + ); }; -const findDeleteButton = () => wrapper.find('[data-testid="delete_branch_confirmation_button"]'); +const findModal = () => wrapper.findComponent(GlModal); +const findModalMessage = () => wrapper.findByTestId('modal-message'); +const findDeleteButton = () => wrapper.findByTestId('delete-branch-confirmation-button'); +const findCancelButton = () => wrapper.findByTestId('delete-branch-cancel-button'); const findFormInput = () => wrapper.findComponent(GlFormInput); +const findForm = () => wrapper.find('form'); describe('Delete branch modal', () => { + const expectedUnmergedWarning = + 'This branch hasn’t been merged into default. To avoid data loss, consider merging this branch before deleting it.'; + afterEach(() => { wrapper.destroy(); }); describe('Deleting a regular branch', () => { + const expectedTitle = 'Delete branch. Are you ABSOLUTELY SURE?'; + const expectedWarning = "You're about to permanently delete the branch test_modal."; + const expectedMessage = `${expectedWarning} ${expectedUnmergedWarning}`; + beforeEach(() => { createComponent(); }); it('renders the modal correctly', () => { - expect(wrapper.html()).toMatchSnapshot(); + expect(findModal().props('title')).toBe(expectedTitle); + expect(findModalMessage().text()).toMatchInterpolatedText(expectedMessage); + expect(findCancelButton().text()).toBe('Cancel, keep branch'); + expect(findDeleteButton().text()).toBe('Yes, delete branch'); + expect(findForm().attributes('action')).toBe(deletePath); }); - it('submits the form when clicked', () => { + it('submits the form when the delete button is clicked', () => { const submitFormSpy = jest.spyOn(wrapper.vm.$refs.form, 'submit'); - return wrapper.vm.$nextTick().then(() => { - findDeleteButton().trigger('click'); + findDeleteButton().trigger('click'); + + expect(findForm().attributes('action')).toBe(deletePath); + expect(submitFormSpy).toHaveBeenCalled(); + }); + + it('calls show on the modal when a `openModal` event is received through the event hub', async () => { + const showSpy = jest.spyOn(wrapper.vm.$refs.modal, 'show'); - expect(submitFormSpy).toHaveBeenCalled(); + eventHub.$emit('openModal', { + isProtectedBranch, + branchName, + defaultBranchName, + deletePath, + merged, }); + + expect(showSpy).toHaveBeenCalled(); + }); + + it('calls hide on the modal when cancel button is clicked', () => { + const closeModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide'); + + findCancelButton().trigger('click'); + + expect(closeModalSpy).toHaveBeenCalled(); }); }); describe('Deleting a protected branch (for owner or maintainer)', () => { + const expectedTitleProtected = 'Delete protected branch. Are you ABSOLUTELY SURE?'; + const expectedWarningProtected = + "You're about to permanently delete the protected branch test_modal."; + const expectedMessageProtected = `${expectedWarningProtected} ${expectedUnmergedWarning}`; + const expectedConfirmationText = + 'Once you confirm and press Yes, delete protected branch, it cannot be undone or recovered. Please type the following to confirm: test_modal'; + beforeEach(() => { createComponent({ isProtectedBranch: true }); }); - it('renders the modal correctly', () => { - expect(wrapper.html()).toMatchSnapshot(); + describe('rendering the modal correctly for a protected branch', () => { + it('sets the modal title for a protected branch', () => { + expect(findModal().props('title')).toBe(expectedTitleProtected); + }); + + it('renders the correct text in the modal message', () => { + expect(findModalMessage().text()).toMatchInterpolatedText(expectedMessageProtected); + }); + + it('renders the protected branch name confirmation form with expected text and action', () => { + expect(findForm().text()).toMatchInterpolatedText(expectedConfirmationText); + expect(findForm().attributes('action')).toBe(deletePath); + }); + + it('renders the buttons with the correct button text', () => { + expect(findCancelButton().text()).toBe('Cancel, keep branch'); + expect(findDeleteButton().text()).toBe('Yes, delete protected branch'); + }); }); - it('disables the delete button when branch name input is unconfirmed', () => { - expect(findDeleteButton().attributes('disabled')).toBe('true'); + it('opens with the delete button disabled and enables it when branch name is confirmed', async () => { + expect(findDeleteButton().props('disabled')).toBe(true); + + findFormInput().vm.$emit('input', branchName); + + await waitForPromises(); + + expect(findDeleteButton().props('disabled')).not.toBe(true); }); + }); + + describe('Deleting a merged branch', () => { + it('does not include the unmerged branch warning when merged is true', () => { + createComponent({ merged: true }); - it('enables the delete button when branch name input is confirmed', () => { - return wrapper.vm - .$nextTick() - .then(() => { - findFormInput().vm.$emit('input', branchName); - }) - .then(() => { - expect(findDeleteButton()).not.toBeDisabled(); - }); + expect(findModalMessage().html()).not.toContain(expectedUnmergedWarning); }); }); }); diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js index 96f2cd1e371..c7d95526a0c 100644 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js @@ -14,6 +14,7 @@ describe('Linked pipeline', () => { let wrapper; const findButton = () => wrapper.find(GlButton); + const findDownstreamPipelineTitle = () => wrapper.find('[data-testid="downstream-title"]'); const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]'); const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' }); const findLoadingIcon = () => wrapper.find(GlLoadingIcon); @@ -119,6 +120,11 @@ describe('Linked pipeline', () => { expect(findPipelineLabel().exists()).toBe(true); }); + it('should have the name of the trigger job on the card when it is a child pipeline', () => { + createWrapper(downstreamProps); + expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.source_job.name); + }); + it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => { createWrapper(upstreamProps); expect(findPipelineLabel().exists()).toBe(true); diff --git a/spec/frontend/security_configuration/components/redesigned_app_spec.js b/spec/frontend/security_configuration/components/redesigned_app_spec.js index a1da7e8584c..d55d8d183ce 100644 --- a/spec/frontend/security_configuration/components/redesigned_app_spec.js +++ b/spec/frontend/security_configuration/components/redesigned_app_spec.js @@ -36,6 +36,8 @@ describe('redesigned App component', () => { const findTabs = () => wrapper.findAllComponents(GlTab); const findByTestId = (id) => wrapper.findByTestId(id); const findFeatureCards = () => wrapper.findAllComponents(FeatureCard); + const findComplianceViewHistoryLink = () => findByTestId('compliance-view-history-link'); + const findSecurityViewHistoryLink = () => findByTestId('security-view-history-link'); const securityFeaturesMock = [ { @@ -103,6 +105,11 @@ describe('redesigned App component', () => { it('should not show latest pipeline link when latestPipelinePath is not defined', () => { expect(findByTestId('latest-pipeline-info').exists()).toBe(false); }); + + it('should not show configuration History Link when gitlabCiPresent & gitlabCiHistoryPath are not defined', () => { + expect(findComplianceViewHistoryLink().exists()).toBe(false); + expect(findSecurityViewHistoryLink().exists()).toBe(false); + }); }); describe('when given latestPipelinePath props', () => { @@ -134,4 +141,23 @@ describe('redesigned App component', () => { expect(latestPipelineInfoCompliance.find('a').attributes('href')).toBe('test/path'); }); }); + + describe('given gitlabCiPresent & gitlabCiHistoryPath props', () => { + beforeEach(() => { + createComponent({ + augmentedSecurityFeatures: securityFeaturesMock, + augmentedComplianceFeatures: complianceFeaturesMock, + gitlabCiPresent: true, + gitlabCiHistoryPath: 'test/historyPath', + }); + }); + + it('should show configuration History Link', () => { + expect(findComplianceViewHistoryLink().exists()).toBe(true); + expect(findSecurityViewHistoryLink().exists()).toBe(true); + + expect(findComplianceViewHistoryLink().attributes('href')).toBe('test/historyPath'); + expect(findSecurityViewHistoryLink().attributes('href')).toBe('test/historyPath'); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js new file mode 100644 index 00000000000..d58c87d66cb --- /dev/null +++ b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js @@ -0,0 +1,108 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { + expectedDownloadDropdownProps, + securityReportMergeRequestDownloadPathsQueryResponse, +} from 'jest/vue_shared/security_reports/mock_data'; +import createFlash from '~/flash'; +import Component from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue'; +import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; +import { + REPORT_TYPE_SAST, + REPORT_TYPE_SECRET_DETECTION, +} from '~/vue_shared/security_reports/constants'; +import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql'; + +jest.mock('~/flash'); + +describe('Merge request artifact Download', () => { + let wrapper; + + const defaultProps = { + reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION], + targetProjectFullPath: '/path', + mrIid: 123, + }; + + const createWrapper = ({ propsData, options }) => { + wrapper = shallowMount(Component, { + stubs: { + SecurityReportDownloadDropdown, + }, + propsData: { + ...defaultProps, + ...propsData, + }, + ...options, + }); + }; + + const pendingHandler = () => new Promise(() => {}); + const successHandler = () => + Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse }); + const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] }); + const createMockApolloProvider = (handler) => { + Vue.use(VueApollo); + const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]]; + + return createMockApollo(requestHandlers); + }; + + const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('given the query is loading', () => { + beforeEach(() => { + createWrapper({ + options: { + apolloProvider: createMockApolloProvider(pendingHandler), + }, + }); + }); + + it('loading is true', () => { + expect(findDownloadDropdown().props('loading')).toBe(true); + }); + }); + + describe('given the query loads successfully', () => { + beforeEach(() => { + createWrapper({ + options: { + apolloProvider: createMockApolloProvider(successHandler), + }, + }); + }); + + it('renders the download dropdown', () => { + expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps); + }); + }); + + describe('given the query fails', () => { + beforeEach(() => { + createWrapper({ + options: { + apolloProvider: createMockApolloProvider(failureHandler), + }, + }); + }); + + it('calls createFlash correctly', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: Component.i18n.apiError, + captureError: true, + error: expect.any(Error), + }); + }); + + it('renders nothing', () => { + expect(findDownloadDropdown().props('artifacts')).toEqual([]); + }); + }); +}); diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 352badf4133..ed8487cac88 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2720,7 +2720,7 @@ RSpec.describe Ci::Build do let(:expected_variables) do predefined_variables.map { |variable| variable.fetch(:key) } + %w[YAML_VARIABLE CI_ENVIRONMENT_NAME CI_ENVIRONMENT_SLUG - CI_ENVIRONMENT_ACTION CI_ENVIRONMENT_URL] + CI_ENVIRONMENT_TIER CI_ENVIRONMENT_ACTION CI_ENVIRONMENT_URL] end before do @@ -2820,7 +2820,8 @@ RSpec.describe Ci::Build do let(:environment_variables) do [ { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true, masked: false }, - { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true, masked: false } + { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true, masked: false }, + { key: 'CI_ENVIRONMENT_TIER', value: 'production', public: true, masked: false } ] end @@ -2829,6 +2830,7 @@ RSpec.describe Ci::Build do project: build.project, name: 'production', slug: 'prod-slug', + tier: 'production', external_url: '') end diff --git a/yarn.lock b/yarn.lock index 2a1e03422c2..2100dad5b9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -908,10 +908,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8" integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw== -"@gitlab/ui@29.33.0": - version "29.33.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.33.0.tgz#2fb06dfe95f86dff6840bcb097ab1a2fd640a0ce" - integrity sha512-WhmJu3vaacBzIPOtKS0uD8htp8L6gjKXfqgWedu/Ukncs02OvlhAGy9CC4SHSTMhjYSMkQIGHrFvBPIW2DPEOg== +"@gitlab/ui@29.34.0": + version "29.34.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.34.0.tgz#c8e9d7411f98537d3153d99b6c614583d4d1285d" + integrity sha512-ukEHnvd+4f9M+K4b5EArVygUDbS+kUcsP94f6I7Rvd95TR/LlJXwf6vtFdgdBNbk8W98AzW9GwdlT4hmgWfRdw== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" |