diff options
16 files changed, 301 insertions, 1 deletions
diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue new file mode 100644 index 00000000000..7a0ee85b551 --- /dev/null +++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue @@ -0,0 +1,69 @@ +<script> +import { GlTooltipDirective } from '@gitlab/ui'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { s__, sprintf } from '~/locale'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import eventHub from '../event_hub'; + +export default { + id: 'delete-environment-modal', + name: 'DeleteEnvironmentModal', + + components: { + GlModal, + LoadingButton, + }, + + directives: { + GlTooltip: GlTooltipDirective, + }, + + props: { + environment: { + type: Object, + required: true, + }, + }, + + computed: { + confirmDeleteMessage() { + return sprintf( + s__( + `Environments|Deleting the '%{environmentName}' environment cannot be undone. Are you sure?`, + ), + { + environmentName: this.environment.name, + }, + false, + ); + }, + }, + + methods: { + onSubmit() { + eventHub.$emit('deleteEnvironment', this.environment); + }, + }, +}; +</script> + +<template> + <gl-modal + :id="$options.id" + :footer-primary-button-text="s__('Environments|Delete environment')" + footer-primary-button-variant="danger" + @submit="onSubmit" + > + <template slot="header"> + <h4 class="modal-title d-flex mw-100"> + {{ __('Delete') }} + <span v-gl-tooltip :title="environment.name" class="text-truncate ml-1 mr-1 flex-fill">{{ + environment.name + }}</span> + ? + </h4> + </template> + + <p v-html="confirmDeleteMessage"></p> + </gl-modal> +</template> diff --git a/app/assets/javascripts/environments/components/environment_delete.vue b/app/assets/javascripts/environments/components/environment_delete.vue new file mode 100644 index 00000000000..b53c5fa6583 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_delete.vue @@ -0,0 +1,70 @@ +<script> +/** + * Renders the delete button that allows deleting a stopped environment. + * Used in the environments table and the environment detail view. + */ + +import $ from 'jquery'; +import { GlTooltipDirective } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; +import { s__ } from '~/locale'; +import eventHub from '../event_hub'; +import LoadingButton from '../../vue_shared/components/loading_button.vue'; + +export default { + components: { + Icon, + LoadingButton, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + environment: { + type: Object, + required: true, + }, + }, + data() { + return { + isLoading: false, + }; + }, + computed: { + title() { + return s__('Environments|Delete environment'); + }, + }, + mounted() { + eventHub.$on('deleteEnvironment', this.onDeleteEnvironment); + }, + beforeDestroy() { + eventHub.$off('deleteEnvironment', this.onDeleteEnvironment); + }, + methods: { + onClick() { + $(this.$el).tooltip('dispose'); + eventHub.$emit('requestDeleteEnvironment', this.environment); + }, + onDeleteEnvironment(environment) { + if (this.environment.id === environment.id) { + this.isLoading = true; + } + }, + }, +}; +</script> +<template> + <loading-button + v-gl-tooltip + :loading="isLoading" + :title="title" + :aria-label="title" + container-class="btn btn-danger d-none d-sm-none d-md-block" + data-toggle="modal" + data-target="#delete-environment-modal" + @click="onClick" + > + <icon name="remove" /> + </loading-button> +</template> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 95e1e8af9b3..c15bf939cf9 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -10,6 +10,7 @@ import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_ite import ActionsComponent from './environment_actions.vue'; import ExternalUrlComponent from './environment_external_url.vue'; import StopComponent from './environment_stop.vue'; +import DeleteComponent from './environment_delete.vue'; import RollbackComponent from './environment_rollback.vue'; import TerminalButtonComponent from './environment_terminal_button.vue'; import MonitoringButtonComponent from './environment_monitoring.vue'; @@ -32,6 +33,7 @@ export default { ActionsComponent, ExternalUrlComponent, StopComponent, + DeleteComponent, RollbackComponent, TerminalButtonComponent, MonitoringButtonComponent, @@ -90,6 +92,15 @@ export default { }, /** + * Returns whether the environment can be deleted. + * + * @returns {Boolean} + */ + canDeleteEnvironment() { + return this.model && !this.model.can_stop && this.model.can_update && this.model.delete_path; + }, + + /** * Verifies if the `deployable` key is present in `last_deployment` key. * Used to verify whether we should or not render the rollback partial. * @@ -431,6 +442,7 @@ export default { this.externalURL || this.monitoringUrl || this.canStopEnvironment || + this.canDeleteEnvironment || this.canRetry ); }, @@ -582,6 +594,8 @@ export default { /> <stop-component v-if="canStopEnvironment" :environment="model" /> + + <delete-component v-if="canDeleteEnvironment" :environment="model" /> </div> </div> </div> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index 81927d18f8b..99df5687057 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -7,12 +7,14 @@ import eventHub from '../event_hub'; import environmentsMixin from '../mixins/environments_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import StopEnvironmentModal from './stop_environment_modal.vue'; +import DeleteEnvironmentModal from './delete_environment_modal.vue'; import ConfirmRollbackModal from './confirm_rollback_modal.vue'; export default { components: { emptyState, StopEnvironmentModal, + DeleteEnvironmentModal, ConfirmRollbackModal, }, @@ -95,6 +97,7 @@ export default { <template> <div :class="cssContainerClass"> <stop-environment-modal :environment="environmentInStopModal" /> + <delete-environment-modal :environment="environmentInDeleteModal" /> <confirm-rollback-modal :environment="environmentInRollbackModal" /> <div class="top-area"> diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 6fd0561f682..604abb2ecb3 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -3,10 +3,12 @@ import folderMixin from 'ee_else_ce/environments/mixins/environments_folder_view import environmentsMixin from '../mixins/environments_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import StopEnvironmentModal from '../components/stop_environment_modal.vue'; +import DeleteEnvironmentModal from '../components/delete_environment_modal.vue'; export default { components: { StopEnvironmentModal, + DeleteEnvironmentModal, }, mixins: [environmentsMixin, CIPaginationMixin, folderMixin], @@ -39,6 +41,7 @@ export default { <template> <div :class="cssContainerClass"> <stop-environment-modal :environment="environmentInStopModal" /> + <delete-environment-modal :environment="environmentInDeleteModal" /> <div v-if="!isLoading" class="top-area"> <h4 class="js-folder-name environments-folder-name"> diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js index dcdaf8731f8..b2aef256bd3 100644 --- a/app/assets/javascripts/environments/index.js +++ b/app/assets/javascripts/environments/index.js @@ -14,7 +14,8 @@ export default () => }, mixins: [canaryCalloutMixin], data() { - const environmentsData = document.querySelector(this.$options.el).dataset; + const domEl = document.querySelector(this.$options.el); + const environmentsData = domEl.dataset; return { endpoint: environmentsData.environmentsDataEndpoint, diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index 31347d95a25..d0189ef35f5 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -36,6 +36,7 @@ export default { page: getParameterByName('page') || '1', requestData: {}, environmentInStopModal: {}, + environmentInDeleteModal: {}, environmentInRollbackModal: {}, }; }, @@ -117,6 +118,10 @@ export default { this.environmentInStopModal = environment; }, + updateDeleteModal(environment) { + this.environmentInDeleteModal = environment; + }, + updateRollbackModal(environment) { this.environmentInRollbackModal = environment; }, @@ -129,6 +134,23 @@ export default { this.postAction({ endpoint, errorMessage }); }, + deleteEnvironment(environment) { + const endpoint = environment.delete_path; + const errorMessage = s__( + 'Environments|An error occurred while deleting the environment, please try again', + ); + + this.service + .deleteAction(endpoint) + .then(() => { + // Reload to as a first solution to bust the ETag cache + window.location.reload(); + }) + .catch(() => { + Flash(errorMessage || s__('Environments|An error occurred while making the request.')); + }); + }, + rollbackEnvironment(environment) { const { retryUrl, isLastDeployment } = environment; const errorMessage = isLastDeployment @@ -194,18 +216,26 @@ export default { }); eventHub.$on('postAction', this.postAction); + eventHub.$on('requestStopEnvironment', this.updateStopModal); eventHub.$on('stopEnvironment', this.stopEnvironment); + eventHub.$on('requestDeleteEnvironment', this.updateDeleteModal); + eventHub.$on('deleteEnvironment', this.deleteEnvironment); + eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal); eventHub.$on('rollbackEnvironment', this.rollbackEnvironment); }, beforeDestroy() { eventHub.$off('postAction', this.postAction); + eventHub.$off('requestStopEnvironment', this.updateStopModal); eventHub.$off('stopEnvironment', this.stopEnvironment); + eventHub.$off('requestDeleteEnvironment', this.updateDeleteModal); + eventHub.$off('deleteEnvironment', this.deleteEnvironment); + eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal); eventHub.$off('rollbackEnvironment', this.rollbackEnvironment); }, diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js index cb4ff6856db..122c8f84a2c 100644 --- a/app/assets/javascripts/environments/services/environments_service.js +++ b/app/assets/javascripts/environments/services/environments_service.js @@ -16,6 +16,11 @@ export default class EnvironmentsService { return axios.post(endpoint, {}); } + // eslint-disable-next-line class-methods-use-this + deleteAction(endpoint) { + return axios.delete(endpoint, {}); + } + getFolderContent(folderUrl) { return axios.get(`${folderUrl}.json?per_page=${this.folderResults}`); } diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 04cf43be452..510176759c6 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -29,6 +29,10 @@ module GitlabRoutingHelper metrics_project_environment_path(environment.project, environment, *args) end + def environment_delete_path(project, environment, *args) + "#{Settings.gitlab.url}/api/v4/projects/#{project.id}/environments/#{environment.id}" + end + def issue_path(entity, *args) project_issue_path(entity.project, entity, *args) end diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb index d1243491f5a..be349b81b85 100644 --- a/app/policies/environment_policy.rb +++ b/app/policies/environment_policy.rb @@ -12,5 +12,11 @@ class EnvironmentPolicy < BasePolicy !@subject.stop_action_available? && can?(:update_environment, @subject) end + condition(:update_allowed) do + can?(:update_environment, @subject) + end + rule { stop_with_deployment_allowed | stop_with_update_allowed }.enable :stop_environment + + rule { update_allowed }.enable :update_environment end diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index 8258135da4e..21f3e399c8a 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -24,6 +24,10 @@ class EnvironmentEntity < Grape::Entity stop_project_environment_path(environment.project, environment) end + expose :delete_path do |environment| + environment_delete_path(environment.project, environment) + end + expose :cluster_type, if: ->(environment, _) { cluster_platform_kubernetes? } do |environment| cluster.cluster_type end @@ -42,6 +46,10 @@ class EnvironmentEntity < Grape::Entity environment.available? && can?(current_user, :stop_environment, environment) end + expose :can_update do |environment| + !environment.available? && can?(current_user, :update_environment, environment) + end + private alias_method :environment, :object diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 6100fd3ad37..462995b77e3 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -34,6 +34,24 @@ = button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do = s_('Environments|Stop environment') +%div{ class: container_class } + - if can?(current_user, :update_environment, @environment) && @environment.stopped? + #delete-environment-modal.modal.fade{ tabindex: -1 } + .modal-dialog + .modal-content + .modal-header + %h4.modal-title.d-flex.mw-100 + = s_("Environments|Delete environment") + %span.has-tooltip.text-truncate.ml-1.mr-1.flex-fill{ title: @environment.name, data: { container: '#delete-environment-modal' } } + = @environment.name + ? + .modal-body + %p= s_("Environments|Deleting the '%{environment_name}' environment cannot be undone. Are you sure?") % { environment_name: @environment.name } + .modal-footer + = button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' } + = button_to environment_delete_path(@project, @environment), class: 'btn btn-danger has-tooltip', data: { redirect_url: project_environments_path(@project) }, method: :delete do + = s_('Environments|Delete environment') + .top-area %h3.page-title= @environment.name .nav-controls.ml-auto.my-2 @@ -47,6 +65,10 @@ target: '#stop-environment-modal' } do = sprite_icon('stop') = s_('Environments|Stop') + - if can?(current_user, :update_environment, @environment) && @environment.stopped? + = button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal', + target: '#delete-environment-modal' } do + = s_('Environments|Delete') .environments-container - if @deployments.blank? diff --git a/changelogs/unreleased/41845-delete-environment.yml b/changelogs/unreleased/41845-delete-environment.yml new file mode 100644 index 00000000000..197115ac238 --- /dev/null +++ b/changelogs/unreleased/41845-delete-environment.yml @@ -0,0 +1,5 @@ +--- +title: Adds features to delete stopped environments +merge_request: 31032 +author: +type: added diff --git a/doc/ci/environments.md b/doc/ci/environments.md index f6c47a99712..385ca37be47 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -561,6 +561,28 @@ to automatically stop. You can read more in the [`.gitlab-ci.yml` reference](yaml/README.md#environmenton_stop). +### Deleting an environment + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31032) in GitLab 12.3. + +You can delete stopped environments in one of two ways. + +The first way is to access the **Delete** button by viewing the list of +**Stopped** environments. + + 1. Navigate to **Operations > Environments**. + 1. Click the **Stopped** tab to access the list of stopped environments. + 1. Click the **Delete** button that appears next to the environment you want to delete. + 1. Finally, confirm your chosen environment in the modal that appears to delete it. + +The second way is to access the **Delete** button by viewing the details for a +stopped environment. + + 1. Navigate to **Operations > Environments**. + 1. Click on the name of an environment within the **Stopped** enfironments list. + 1. Click on the **Delete** button that appears at the top for all stopped environments. + 1. Finally, confirm your chosen environment in the modal that appears to delete it. + ### Grouping similar environments > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7015) in GitLab 8.14. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8c8574d0a48..9ddd1a4d9ed 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4405,6 +4405,9 @@ msgstr "" msgid "Environments allow you to track deployments of your application %{link_to_read_more}." msgstr "" +msgid "Environments|An error occurred while deleting the environment, please try again" +msgstr "" + msgid "Environments|An error occurred while fetching the environments." msgstr "" @@ -4426,6 +4429,18 @@ msgstr "" msgid "Environments|Commit" msgstr "" +msgid "Environments|Delete" +msgstr "" + +msgid "Environments|Delete environment" +msgstr "" + +msgid "Environments|Deleting the '%{environmentName}' environment cannot be undone. Are you sure?" +msgstr "" + +msgid "Environments|Deleting the '%{environment_name}' environment cannot be undone. Are you sure?" +msgstr "" + msgid "Environments|Deploy to..." msgstr "" diff --git a/spec/javascripts/environments/environment_delete_spec.js b/spec/javascripts/environments/environment_delete_spec.js new file mode 100644 index 00000000000..8afaa03e2fb --- /dev/null +++ b/spec/javascripts/environments/environment_delete_spec.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import deleteComp from '~/environments/components/environment_delete.vue'; + +describe('Delete Component', () => { + let DeleteComponent; + let component; + + beforeEach(() => { + DeleteComponent = Vue.extend(deleteComp); + spyOn(window, 'confirm').and.returnValue(true); + + component = new DeleteComponent({ + propsData: { + environment: {}, + }, + }).$mount(); + }); + + it('should render a button to delete the environment', () => { + expect(component.$el.tagName).toEqual('BUTTON'); + expect(component.$el.getAttribute('data-original-title')).toEqual('Delete environment'); + }); +}); |