summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDietrich Stein <dstein@gitlab.com>2019-07-22 15:08:11 -0400
committerDietrich Stein <dstein@gitlab.com>2019-08-26 11:46:05 -0400
commit3525898be83a72a09e680b95b7d4652b8e271b28 (patch)
tree2cd710669adc2d4fd3e0650b20f7e269059f5bdb
parent336ef2a98cf74b0a49474816031657efbe14f3b2 (diff)
downloadgitlab-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
-rw-r--r--app/assets/javascripts/environments/components/delete_environment_modal.vue69
-rw-r--r--app/assets/javascripts/environments/components/environment_delete.vue70
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue14
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue3
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue3
-rw-r--r--app/assets/javascripts/environments/index.js3
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js30
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js5
-rw-r--r--app/helpers/gitlab_routing_helper.rb4
-rw-r--r--app/policies/environment_policy.rb6
-rw-r--r--app/serializers/environment_entity.rb8
-rw-r--r--app/views/projects/environments/show.html.haml22
-rw-r--r--changelogs/unreleased/41845-delete-environment.yml5
-rw-r--r--doc/ci/environments.md22
-rw-r--r--locale/gitlab.pot15
-rw-r--r--spec/javascripts/environments/environment_delete_spec.js23
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');
+ });
+});