Commit 6a093dcf authored by Jake Burden's avatar Jake Burden Committed by Shinya Maeda

Adds environments delete button and modal

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
parent da969f7d
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
id: 'delete-environment-modal',
name: 'DeleteEnvironmentModal',
components: {
GlModal,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
environment: {
type: Object,
required: true,
},
},
computed: {
confirmDeleteMessage() {
return sprintf(
s__(
`Environments|Deleting the '%{environmentName}' environment cannot be undone. Do you want to delete it anyway?`,
),
{
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 mx-1 flex-fill">
{{ environment.name }}?
</span>
</h4>
</template>
<p>{{ confirmDeleteMessage }}</p>
</gl-modal>
</template>
<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>
......@@ -15,8 +15,9 @@ import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import PinComponent from './environment_pin.vue';
import RollbackComponent from './environment_rollback.vue';
import DeleteComponent from './environment_delete.vue';
import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
/**
......@@ -33,6 +34,7 @@ export default {
Icon,
MonitoringButtonComponent,
PinComponent,
DeleteComponent,
RollbackComponent,
StopComponent,
TerminalButtonComponent,
......@@ -112,6 +114,15 @@ export default {
return this.model && this.model.can_stop;
},
/**
* Returns whether the environment can be deleted.
*
* @returns {Boolean}
*/
canDeleteEnvironment() {
return Boolean(this.model && this.model.can_delete && 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.
......@@ -485,6 +496,7 @@ export default {
this.externalURL ||
this.monitoringUrl ||
this.canStopEnvironment ||
this.canDeleteEnvironment ||
this.canRetry
);
},
......@@ -680,6 +692,8 @@ export default {
/>
<stop-component v-if="canStopEnvironment" :environment="model" />
<delete-component v-if="canDeleteEnvironment" :environment="model" />
</div>
</div>
</div>
......
......@@ -9,6 +9,7 @@ import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import EnableReviewAppButton from './enable_review_app_button.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
import DeleteEnvironmentModal from './delete_environment_modal.vue';
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
export default {
......@@ -18,6 +19,7 @@ export default {
EnableReviewAppButton,
GlButton,
StopEnvironmentModal,
DeleteEnvironmentModal,
},
mixins: [CIPaginationMixin, environmentsMixin, envrionmentsAppMixin],
......@@ -95,6 +97,7 @@ export default {
<template>
<div>
<stop-environment-modal :environment="environmentInStopModal" />
<delete-environment-modal :environment="environmentInDeleteModal" />
<confirm-rollback-modal :environment="environmentInRollbackModal" />
<div class="top-area">
......
......@@ -63,10 +63,9 @@ export default {
<template slot="header">
<h4 class="modal-title d-flex mw-100">
Stopping
<span v-gl-tooltip :title="environment.name" class="text-truncate ml-1 mr-1 flex-fill">{{
environment.name
}}</span>
?
<span v-gl-tooltip :title="environment.name" class="text-truncate ml-1 mr-1 flex-fill">
{{ environment.name }}?
</span>
</h4>
</template>
......
......@@ -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" />
<h4 class="js-folder-name environments-folder-name">
{{ s__('Environments|Environments') }} /
......
......@@ -27,6 +27,10 @@ export default {
data() {
const store = new EnvironmentsStore();
const isDetailView = document.body.contains(
document.getElementById('environments-detail-view'),
);
return {
store,
state: store.state,
......@@ -36,7 +40,9 @@ export default {
page: getParameterByName('page') || '1',
requestData: {},
environmentInStopModal: {},
environmentInDeleteModal: {},
environmentInRollbackModal: {},
isDetailView,
};
},
......@@ -121,6 +127,10 @@ export default {
this.environmentInStopModal = environment;
},
updateDeleteModal(environment) {
this.environmentInDeleteModal = environment;
},
updateRollbackModal(environment) {
this.environmentInRollbackModal = environment;
},
......@@ -133,6 +143,30 @@ export default {
this.postAction({ endpoint, errorMessage });
},
deleteEnvironment(environment) {
const endpoint = environment.delete_path;
const mountedToShow = environment.mounted_to_show;
const errorMessage = s__(
'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
);
this.service
.deleteAction(endpoint)
.then(() => {
if (!mountedToShow) {
// Reload as a first solution to bust the ETag cache
window.location.reload();
return;
}
const url = window.location.href.split('/');
url.pop();
window.location.href = url.join('/');
})
.catch(() => {
Flash(errorMessage);
});
},
rollbackEnvironment(environment) {
const { retryUrl, isLastDeployment } = environment;
const errorMessage = isLastDeployment
......@@ -178,36 +212,42 @@ export default {
this.service = new EnvironmentsService(this.endpoint);
this.requestData = { page: this.page, scope: this.scope, nested: true };
this.poll = new Poll({
resource: this.service,
method: 'fetchEnvironments',
data: this.requestData,
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: isMakingRequest => {
this.isMakingRequest = isMakingRequest;
},
});
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
} else {
this.fetchEnvironments();
}
if (!this.isDetailView) {
this.poll = new Poll({
resource: this.service,
method: 'fetchEnvironments',
data: this.requestData,
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: isMakingRequest => {
this.isMakingRequest = isMakingRequest;
},
});
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
this.isLoading = true;
this.poll.makeRequest();
} else {
this.poll.stop();
this.fetchEnvironments();
}
});
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}
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);
......@@ -216,9 +256,13 @@ export default {
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);
......
import Vue from 'vue';
import DeleteEnvironmentModal from './components/delete_environment_modal.vue';
import environmentsMixin from './mixins/environments_mixin';
export default () => {
const el = document.getElementById('delete-environment-modal');
const container = document.getElementById('environments-detail-view');
return new Vue({
el,
components: {
DeleteEnvironmentModal,
},
mixins: [environmentsMixin],
data() {
const environment = JSON.parse(JSON.stringify(container.dataset));
environment.delete_path = environment.deletePath;
environment.mounted_to_show = true;
return {
environment,
};
},
render(createElement) {
return createElement('delete-environment-modal', {
props: {
environment: this.environment,
},
});
},
});
};
......@@ -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}`);
}
......
import initShowEnvironment from '~/environments/mount_show';
document.addEventListener('DOMContentLoaded', () => initShowEnvironment());
......@@ -50,4 +50,8 @@ module EnvironmentsHelper
"cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack')
}
end
def can_destroy_environment?(environment)
can?(current_user, :destroy_environment, environment)
end
end
......@@ -4,6 +4,7 @@
module GitlabRoutingHelper
extend ActiveSupport::Concern
include API::Helpers::RelatedResourcesHelpers
included do
Gitlab::Routing.includes_helpers(self)
end
......@@ -29,6 +30,10 @@ module GitlabRoutingHelper
metrics_project_environment_path(environment.project, environment, *args)
end
def environment_delete_path(environment, *args)
expose_path(api_v4_projects_environments_path(id: environment.project.id, environment_id: environment.id))
end
def issue_path(entity, *args)
project_issue_path(entity.project, entity, *args)
end
......
......@@ -12,7 +12,13 @@ class EnvironmentPolicy < BasePolicy
!@subject.stop_action_available? && can?(:update_environment, @subject)
end
condition(:stopped) do
@subject.stopped?
end
rule { stop_with_deployment_allowed | stop_with_update_allowed }.enable :stop_environment
rule { ~stopped }.prevent(:destroy_environment)
end
EnvironmentPolicy.prepend_if_ee('EE::EnvironmentPolicy')
......@@ -271,6 +271,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_container_image
enable :create_environment
enable :update_environment
enable :destroy_environment
enable :create_deployment
enable :update_deployment
enable :create_release
......
......@@ -28,6 +28,10 @@ class EnvironmentEntity < Grape::Entity
cancel_auto_stop_project_environment_path(environment.project, environment)
end
expose :delete_path do |environment|
environment_delete_path(environment)
end
expose :cluster_type, if: ->(environment, _) { cluster_platform_kubernetes? } do |environment|
cluster.cluster_type
end
......@@ -63,6 +67,10 @@ class EnvironmentEntity < Grape::Entity
environment.elastic_stack_available?
end
expose :can_delete do |environment|
can?(current_user, :destroy_environment, environment)
end
private
alias_method :environment, :object
......
......@@ -5,74 +5,81 @@
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/xterm'
- if @environment.available? && can?(current_user, :stop_environment, @environment)
#stop-environment-modal.modal.fade{ tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
%h4.modal-title.d-flex.mw-100
= s_("Environments|Stopping")
%span.has-tooltip.text-truncate.ml-1.mr-1.flex-fill{ title: @environment.name, data: { container: '#stop-environment-modal' } }
= @environment.name
?
.modal-body
%p= s_('Environments|Are you sure you want to stop this environment?')
- unless @environment.stop_action_available?
.warning_message
%p= s_('Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file.').html_safe % { emphasis_start: '<strong>'.html_safe,
emphasis_end: '</strong>'.html_safe,
ci_config_link_start: '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">'.html_safe,
ci_config_link_end: '</a>'.html_safe }
%a{ href: 'https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment',
target: '_blank',
rel: 'noopener noreferrer' }
= s_('Environments|Learn more about stopping environments')
.modal-footer
= button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' }
= button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
= s_('Environments|Stop environment')
#environments-detail-view{ data: { name: @environment.name, id: @environment.id, delete_path: environment_delete_path(@environment)} }
- if @environment.available? && can?(current_user, :stop_environment, @environment)
#stop-environment-modal.modal.fade{ tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
%h4.modal-title.d-flex.mw-100
= s_("Environments|Stopping")
%span.has-tooltip.text-truncate.ml-1.mr-1.flex-fill{ title: @environment.name, data: { container: '#stop-environment-modal' } }
#{@environment.name}?
.modal-body
%p= s_('Environments|Are you sure you want to stop this environment?')
- unless @environment.stop_action_available?
.warning_message
%p= s_('Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file.').html_safe % { emphasis_start: '<strong>'.html_safe,
emphasis_end: '</strong>'.html_safe,
ci_config_link_start: '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">'.html_safe,
ci_config_link_end: '</a>'.html_safe }
%a{ href: 'https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment',
target: '_blank',
rel: 'noopener noreferrer' }
= s_('Environments|Learn more about stopping environments')
.modal-footer
= button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' }
= button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
= s_('Environments|Stop environment')
.top-area.justify-content-between
.d-flex
%h3.page-title= @environment.name
- if @environment.auto_stop_at?
%p.align-self-end.prepend-left-8
= s_('Environments|Auto stops %{auto_stop_time}').html_safe % {auto_stop_time: time_ago_with_tooltip(@environment.auto_stop_at)}
.nav-controls.my-2
= render 'projects/environments/pin_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
= render 'projects/environments/metrics_button', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to _('Edit'), edit_project_environment_path(@project, @environment), class: 'btn'
- if @environment.available? && can?(current_user, :stop_environment, @environment)
= button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
target: '#stop-environment-modal' } do
= sprite_icon('stop')
= s_('Environments|Stop')
- if can_destroy_environment?(@environment)
#delete-environment-modal
.environments-container
- if @deployments.blank?
.empty-state
.text-content
%h4.state-title
= _("You don't have any deployments right now.")
%p.blank-state-text
= _("Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here.").html_safe
.text-center
= link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
.ci-table.environments{ role: 'grid' }
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-15{ role: 'columnheader' }= _('Status')
.table-section.section-10{ role: 'columnheader' }= _('ID')
.table-section.section-10{ role: 'columnheader' }= _('Triggerer')
.table-section.section-25{ role: 'columnheader' }= _('Commit')
.table-section.section-10{ role: 'columnheader' }= _('Job')
.table-section.section-10{ role: 'columnheader' }= _('Created')
.table-section.section-10{ role: 'columnheader' }= _('Deployed')
.top-area.justify-content-between
.d-flex
%h3.page-title= @environment.name
- if @environment.auto_stop_at?
%p.align-self-end.prepend-left-8
= s_('Environments|Auto stops %{auto_stop_time}').html_safe % {auto_stop_time: time_ago_with_tooltip(@environment.auto_stop_at)}
.nav-controls.my-2
= render 'projects/environments/pin_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
= render 'projects/environments/metrics_button', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to _('Edit'), edit_project_environment_path(@project, @environment), class: 'btn'
- if @environment.available? && can?(current_user, :stop_environment, @environment)
= button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
target: '#stop-environment-modal' } do
= sprite_icon('stop')
= s_('Environments|Stop')
- if can_destroy_environment?(@environment)
= button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
target: '#delete-environment-modal' } do
= s_('Environments|Delete')
= render @deployments
.environments-container
- if @deployments.blank?
.empty-state
.text-content
%h4.state-title
= _("You don't have any deployments right now.")
%p.blank-state-text
= _("Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here.").html_safe
.text-center
= link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
.ci-table.environments{ role: 'grid' }
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-15{ role: 'columnheader' }= _('Status')
.table-section.section-10{ role: 'columnheader' }= _('ID')
.table-section.section-10{ role: 'columnheader' }= _('Triggerer')
.table-section.section-25{ role: 'columnheader' }= _('Commit')
.table-section.section-10{ role: 'columnheader' }= _('Job')
.table-section.section-10{ role: 'columnheader' }= _('Created')
.table-section.section-10{ role: 'columnheader' }= _('Deployed')
= paginate @deployments, theme: 'gitlab'
= render @deployments
= paginate @deployments, theme: 'gitlab'
---
title: Adds features to delete stopped environments
merge_request: 22629
author:
type: added
......@@ -761,6 +761,33 @@ runs once every hour. This means environments will not be stopped at the exact
timestamp as the specified period, but will be stopped when the hourly cron worker
detects expired environments.
#### Delete a stopped environment
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22629) in GitLab 12.9.
You can delete [stopped environments](#stopping-an-environment) in one of two
ways: through the GitLab UI or through the API.
##### Delete environments through the UI
To view the list of **Stopped** environments, navigate to **Operations > Environments**
and click the **Stopped** tab.
From there, you can click the **Delete** button directly, or you can click the
environment name to see its details and **Delete** it from there.
You can also delete environments by viewing the details for a
stopped environment:
1. Navigate to **Operations > Environments**.
1. Click on the name of an environment within the **Stopped** environments 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.
##### Delete environments through the API
Environments can also be deleted by using the [Environments API](../api/environments.md#delete-an-environment).
### Grouping similar environments
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7015) in GitLab 8.14.
......
......@@ -4,7 +4,6 @@ module EE
module GitlabRoutingHelper
include ::ProjectsHelper
include ::ApplicationSettingsHelper
include ::API::Helpers::RelatedResourcesHelpers
def geo_primary_web_url(project_or_wiki)
File.join(::Gitlab::Geo.primary_node.url, project_or_wiki.full_path)
......
......@@ -12,6 +12,7 @@ module EE
prevent :create_deployment
prevent :update_deployment
prevent :update_environment
prevent :destroy_environment
end
private
......
......@@ -76,6 +76,12 @@
"type": "boolean"
},
"cancel_auto_stop_path": { "type": "string" },
"auto_stop_at": { "type": "string", "format": "date-time" }
"auto_stop_at": { "type": "string", "format": "date-time" },
"can_delete": {
"type": "boolean"
},
"delete_path": {
"type": "string"
}
}
}
......@@ -18,6 +18,16 @@ describe EnvironmentPolicy do
it_behaves_like 'protected environments access'
end
describe '#destroy_environment' do
subject { user.can?(:destroy_environment, environment) }
before do
environment.stop!
end
it_behaves_like 'protected environments access'
end
describe '#create_environment_terminal' do
subject { user.can?(:create_environment_terminal, environment) }
......
......@@ -82,9 +82,10 @@ module API
requires :environment_id, type: Integer, desc: 'The environment ID'
end
delete ':id/environments/:environment_id' do
authorize! :update_environment, user_project
authorize! :read_environment, user_project
environment = user_project.environments.find(params[:environment_id])
authorize! :destroy_environment, environment
destroy_conditionally!(environment)
end
......
......@@ -7672,6 +7672,9 @@ msgstr ""
msgid "Environments|An error occurred while canceling the auto stop, please try again"
msgstr ""
msgid "Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again."
msgstr ""
msgid "Environments|An error occurred while fetching the environments."
msgstr ""
......@@ -7705,6 +7708,15 @@ msgstr ""
msgid "Environments|Currently showing all results."
msgstr ""
msgid "Environments|Delete"
msgstr ""
msgid "Environments|Delete environment"
msgstr ""
msgid "Environments|Deleting the '%{environmentName}' environment cannot be undone. Do you want to delete it anyway?"
msgstr ""
msgid "Environments|Deploy to..."
msgstr ""
......
......@@ -44,7 +44,10 @@
"build_path": { "type": "string" }
}
]
}
},
"can_delete": { "type": "boolean" }
,
"delete_path": { "type": "string" }
},
"additionalProperties": false
}
import $ from 'jquery';
import { shallowMount } from '@vue/test-utils';
import DeleteComponent from '~/environments/components/environment_delete.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '~/environments/event_hub';
$.fn.tooltip = () => {};
describe('External URL Component', () => {
let wrapper;
const createWrapper = () => {
wrapper = shallowMount(DeleteComponent, {
propsData: {
environment: {},
},
});
};
const findButton = () => wrapper.find(LoadingButton);
beforeEach(() => {
jest.spyOn(window, 'confirm');
createWrapper();
});
it('should render a button to delete the environment', () => {
expect(findButton().exists()).toBe(true);
expect(wrapper.attributes('title')).toEqual('Delete environment');
});
it('emits requestDeleteEnvironment in the event hub when button is clicked', () => {
jest.spyOn(eventHub, '$emit');
findButton().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('requestDeleteEnvironment', wrapper.vm.environment);
});
});
......@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import { format } from 'timeago.js';
import EnvironmentItem from '~/environments/components/environment_item.vue';
import PinComponent from '~/environments/components/environment_pin.vue';
import DeleteComponent from '~/environments/components/environment_delete.vue';
import { environment, folder, tableData } from './mock_data';
......@@ -54,6 +55,10 @@ describe('Environment item', () => {
expect(wrapper.find('.environment-created-date-timeago').text()).toContain(formattedDate);
});
it('should not render the delete button', () => {
expect(wrapper.find(DeleteComponent).exists()).toBe(false);
});
describe('With user information', () => {
it('should render user avatar with link to profile', () => {
expect(wrapper.find('.js-deploy-user-container').attributes('href')).toEqual(
......@@ -98,7 +103,7 @@ describe('Environment item', () => {
expect(findAutoStop().exists()).toBe(false);
});
it('should not render the suto-stop button', () => {
it('should not render the auto-stop button', () => {
expect(wrapper.find(PinComponent).exists()).toBe(false);
});
});
......@@ -205,4 +210,22 @@ describe('Environment item', () => {
expect(wrapper.find('.folder-name .badge').text()).toContain(folder.size);
});
});
describe('When environment can be deleted', () => {
beforeEach(() => {
factory({
propsData: {
model: {
can_delete: true,
delete_path: 'http://0.0.0.0:3000/api/v4/projects/8/environments/45',
},
tableData,
},
});
});
it('should render the delete button', () => {
expect(wrapper.find(DeleteComponent).exists()).toBe(true);
});
});
});
......@@ -86,6 +86,50 @@ describe EnvironmentPolicy do
it { expect(policy).to be_allowed :stop_environment }
end
end
describe '#destroy_environment' do
let(:environment) do
create(:environment, project: project)
end
where(:access_level, :allowed?) do
nil | false
:guest | false
:reporter | false
:developer | true
:maintainer | true
end
with_them do
before do
project.add_user(user, access_level) unless access_level.nil?
end
it { expect(policy).to be_disallowed :destroy_environment }
context 'when environment is stopped' do
before do
environment.stop!
end
it { expect(policy.allowed?(:destroy_environment)).to be allowed? }
end
end
context 'when an admin user' do
let(:user) { create(:user, :admin) }
it { expect(policy).to be_disallowed :destroy_environment }
context 'when environment is stopped' do
before do
environment.stop!
end
it { expect(policy).to be_allowed :destroy_environment }
end
end
end
end
context 'when project is public' do
......
......@@ -171,7 +171,15 @@ describe API::Environments do
describe 'DELETE /projects/:id/environments/:environment_id' do
context 'as a maintainer' do
it 'returns a 200 for an existing environment' do
it "rejects the requests in environment isn't stopped" do
delete api("/projects/#{project.id}/environments/#{environment.id}", user)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'returns a 200 for stopped environment' do
environment.stop
delete api("/projects/#{project.id}/environments/#{environment.id}", user)
expect(response).to have_gitlab_http_status(:no_content)
......@@ -185,6 +193,10 @@ describe API::Environments do
end
it_behaves_like '412 response' do
before do
environment.stop
end
let(:request) { api("/projects/#{project.id}/environments/#{environment.id}", user) }
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment