Skip to content
Snippets Groups Projects
Commit 6a093dcf authored by Jake Burden's avatar Jake Burden Committed by Shinya Maeda
Browse files

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
No related branches found
No related tags found
No related merge requests found
Showing
with 396 additions and 94 deletions
<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>
Loading
Loading
@@ -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';
 
/**
Loading
Loading
@@ -33,6 +34,7 @@ export default {
Icon,
MonitoringButtonComponent,
PinComponent,
DeleteComponent,
RollbackComponent,
StopComponent,
TerminalButtonComponent,
Loading
Loading
@@ -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.
Loading
Loading
@@ -485,6 +496,7 @@ export default {
this.externalURL ||
this.monitoringUrl ||
this.canStopEnvironment ||
this.canDeleteEnvironment ||
this.canRetry
);
},
Loading
Loading
@@ -680,6 +692,8 @@ export default {
/>
 
<stop-component v-if="canStopEnvironment" :environment="model" />
<delete-component v-if="canDeleteEnvironment" :environment="model" />
</div>
</div>
</div>
Loading
Loading
Loading
Loading
@@ -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 {
Loading
Loading
@@ -18,6 +19,7 @@ export default {
EnableReviewAppButton,
GlButton,
StopEnvironmentModal,
DeleteEnvironmentModal,
},
 
mixins: [CIPaginationMixin, environmentsMixin, envrionmentsAppMixin],
Loading
Loading
@@ -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">
Loading
Loading
Loading
Loading
@@ -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>
 
Loading
Loading
Loading
Loading
@@ -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],
Loading
Loading
@@ -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') }} /
Loading
Loading
Loading
Loading
@@ -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,
Loading
Loading
@@ -36,7 +40,9 @@ export default {
page: getParameterByName('page') || '1',
requestData: {},
environmentInStopModal: {},
environmentInDeleteModal: {},
environmentInRollbackModal: {},
isDetailView,
};
},
 
Loading
Loading
@@ -121,6 +127,10 @@ export default {
this.environmentInStopModal = environment;
},
 
updateDeleteModal(environment) {
this.environmentInDeleteModal = environment;
},
updateRollbackModal(environment) {
this.environmentInRollbackModal = environment;
},
Loading
Loading
@@ -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
Loading
Loading
@@ -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);
 
Loading
Loading
@@ -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);
 
Loading
Loading
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,
},
});
},
});
};
Loading
Loading
@@ -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}`);
}
Loading
Loading
import initShowEnvironment from '~/environments/mount_show';
document.addEventListener('DOMContentLoaded', () => initShowEnvironment());
Loading
Loading
@@ -50,4 +50,8 @@ def environment_logs_data(project, environment)
"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
Loading
Loading
@@ -4,6 +4,7 @@
module GitlabRoutingHelper
extend ActiveSupport::Concern
 
include API::Helpers::RelatedResourcesHelpers
included do
Gitlab::Routing.includes_helpers(self)
end
Loading
Loading
@@ -29,6 +30,10 @@ def environment_metrics_path(environment, *args)
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
Loading
Loading
Loading
Loading
@@ -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')
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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.
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
Loading
Loading
@@ -12,6 +12,7 @@ module EnvironmentPolicy
prevent :create_deployment
prevent :update_deployment
prevent :update_environment
prevent :destroy_environment
end
 
private
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment