diff options
author | Dietrich Stein <dstein@gitlab.com> | 2019-07-22 15:08:11 -0400 |
---|---|---|
committer | Dietrich Stein <dstein@gitlab.com> | 2019-08-26 11:46:05 -0400 |
commit | 3525898be83a72a09e680b95b7d4652b8e271b28 (patch) | |
tree | 2cd710669adc2d4fd3e0650b20f7e269059f5bdb | |
parent | 336ef2a98cf74b0a49474816031657efbe14f3b2 (diff) | |
download | gitlab-ce-41845-delete-environment.tar.gz |
Adds environments delete button and modal41845-delete-environment
Adds delete functionality
Adds a property the 'api' library to enable access for buildUrl
Passes the project ID as a prop to the environments component
Imports the Delete component for use within the environment items
Adds canDeleteEnvironment as a computed method for environment items
Defines project ID as a required prop for the environments component
Imports the delete modal for the environments folder view
Sets up the delete modal within the environments modal
Creates a deleteAction helper method within the environments mixin
Creates an update method for the delete modal
Adds a method to facilitate API access to delete an environment
Adds events for reactivity with the delete environment feature
Adds an axios helper for DELETE calls in the environments service
Exposes a data attribute with the project ID on the environments list
Adds a test for the delete environment component
Adds a project ID to the mock data for the environments app
Adds translations and formatting
Fixes commas, semicolons, and whitespace using prettier
Adds generated environments-related translations
Implements delete button for detail view
Removes front-end buildable delete endpoint
Updates deletion method to ensure endpoint access
Removes previously-added project id property
Accesses exposed deletion endpoint
Adds helper function for model-based access to API endpoint
Exposes update rule for front-end access
Exposes helper-based delete endpoint
Removes previously added project id property
Adds modal and button for detail view
Removes project ID from mock data
Using const instead of let
Removes now unused import
Adds entry to changelog
Adds reload on delete modal success
Adds reload on delete for etag expiration
Modifies method name
Modifies delete path helper on entity
Modifies delete action helper
Adds request_url param
Adding docs for delete feature
Adds translations and lint fixes
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'); + }); +}); |