diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index f4a7e64ceee8cbb92ec209e74506fd8389a6d2cc..d990d2677a8166356731b2f359075e44056425e4 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -12,6 +12,7 @@ import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX, CROSSPLANE } from ' import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; import Applications from './components/applications.vue'; +import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue'; import setupToggleButtons from '../toggle_buttons'; import initProjectSelectDropdown from '~/project_select'; @@ -144,6 +145,8 @@ export default class Clusters { () => this.handlePollError(), ); } + + this.initRemoveClusterActions(); } initApplications(type) { @@ -205,6 +208,25 @@ export default class Clusters { }); } + initRemoveClusterActions() { + const el = document.querySelector('#js-cluster-remove-actions'); + if (el && el.dataset) { + const { clusterName, clusterPath } = el.dataset; + + this.removeClusterAction = new Vue({ + el, + render(createElement) { + return createElement(RemoveClusterConfirmation, { + props: { + clusterName, + clusterPath, + }, + }); + }, + }); + } + } + handleClusterEnvironmentsSuccess(data) { this.store.toggleFetchEnvironments(false); this.store.updateEnvironments(data.data); diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue new file mode 100644 index 0000000000000000000000000000000000000000..c31ba7ef14adcb4236d58a3f54fcdf1008c72dae --- /dev/null +++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue @@ -0,0 +1,168 @@ +<script> +import _ from 'underscore'; +import SplitButton from '~/vue_shared/components/split_button.vue'; +import { GlModal, GlButton, GlFormInput } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; +import csrf from '~/lib/utils/csrf'; + +const splitButtonActionItems = [ + { + title: s__('ClusterIntegration|Remove integration and resources'), + description: s__( + 'ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal', + ), + eventName: 'remove-cluster-and-cleanup', + }, + { + title: s__('ClusterIntegration|Remove integration'), + description: s__( + 'ClusterIntegration|Removes cluster from project but keeps associated resources', + ), + eventName: 'remove-cluster', + }, +]; + +export default { + splitButtonActionItems, + components: { + SplitButton, + GlModal, + GlButton, + GlFormInput, + }, + props: { + clusterPath: { + type: String, + required: true, + }, + clusterName: { + type: String, + required: true, + }, + }, + data() { + return { + enteredClusterName: '', + confirmCleanup: false, + }; + }, + computed: { + csrfToken() { + return csrf.token; + }, + modalTitle() { + return this.confirmCleanup + ? s__('ClusterIntegration|Remove integration and resources?') + : s__('ClusterIntegration|Remove integration?'); + }, + warningMessage() { + return this.confirmCleanup + ? s__( + 'ClusterIntegration|You are about to remove your cluster integration and all GitLab-created resources associated with this cluster.', + ) + : s__('ClusterIntegration|You are about to remove your cluster integration.'); + }, + warningToBeRemoved() { + return s__(`ClusterIntegration| + This will permanently delete the following resources: + <ul> + <li>All installed applications and related resources</li> + <li>The <code>gitlab-managed-apps</code> namespace</li> + <li>Any project namespaces</li> + <li><code>clusterroles</code></li> + <li><code>clusterrolebindings</code></li> + </ul> + `); + }, + confirmationTextLabel() { + return sprintf( + this.confirmCleanup + ? s__( + 'ClusterIntegration|To remove your integration and resources, type %{clusterName} to confirm:', + ) + : s__('ClusterIntegration|To remove your integration, type %{clusterName} to confirm:'), + { + clusterName: `<code>${_.escape(this.clusterName)}</code>`, + }, + false, + ); + }, + canSubmit() { + return this.enteredClusterName === this.clusterName; + }, + }, + methods: { + handleClickRemoveCluster(cleanup = false) { + this.confirmCleanup = cleanup; + this.$refs.modal.show(); + }, + handleCancel() { + this.$refs.modal.hide(); + this.enteredClusterName = ''; + }, + handleSubmit(cleanup = false) { + this.$refs.cleanup.name = cleanup === true ? 'cleanup' : 'no_cleanup'; + this.$refs.form.submit(); + this.enteredClusterName = ''; + }, + }, +}; +</script> + +<template> + <div> + <split-button + :action-items="$options.splitButtonActionItems" + menu-class="dropdown-menu-large" + variant="danger" + @remove-cluster="handleClickRemoveCluster(false)" + @remove-cluster-and-cleanup="handleClickRemoveCluster(true)" + /> + <gl-modal + ref="modal" + size="lg" + modal-id="delete-cluster-modal" + :title="modalTitle" + kind="danger" + > + <template> + <p>{{ warningMessage }}</p> + <div v-if="confirmCleanup" v-html="warningToBeRemoved"></div> + <strong v-html="confirmationTextLabel"></strong> + <form ref="form" :action="clusterPath" method="post" class="append-bottom-20"> + <input ref="method" type="hidden" name="_method" value="delete" /> + <input :value="csrfToken" type="hidden" name="authenticity_token" /> + <input ref="cleanup" type="hidden" name="cleanup" value="true" /> + <gl-form-input + v-model="enteredClusterName" + autofocus + type="text" + name="confirm_cluster_name_input" + autocomplete="off" + /> + </form> + <span v-if="confirmCleanup">{{ + s__( + 'ClusterIntegration|If you do not wish to delete all associated GitLab resources, you can simply remove the integration.', + ) + }}</span> + </template> + <template slot="modal-footer"> + <gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button> + <template v-if="confirmCleanup"> + <gl-button :disabled="!canSubmit" variant="warning" @click="handleSubmit">{{ + s__('ClusterIntegration|Remove integration') + }}</gl-button> + <gl-button :disabled="!canSubmit" variant="danger" @click="handleSubmit(true)">{{ + s__('ClusterIntegration|Remove integration and resources') + }}</gl-button> + </template> + <template v-else> + <gl-button :disabled="!canSubmit" variant="danger" @click="handleSubmit">{{ + s__('ClusterIntegration|Remove integration') + }}</gl-button> + </template> + </template> + </gl-modal> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue index f7dc00a345c13b685be51386470568aa5c6a195e..9aacde492641b4160bded23b917c29a5c8df078a 100644 --- a/app/assets/javascripts/vue_shared/components/split_button.vue +++ b/app/assets/javascripts/vue_shared/components/split_button.vue @@ -26,6 +26,11 @@ export default { required: false, default: '', }, + variant: { + type: String, + required: false, + default: 'secondary', + }, }, data() { @@ -53,6 +58,7 @@ export default { :menu-class="`dropdown-menu-selectable ${menuClass}`" split :text="dropdownToggleText" + :variant="variant" v-bind="$attrs" @click="triggerEvent" > diff --git a/app/views/clusters/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml index 5e34b4572311cd7d7f08a064fa5974e74bba2783..77f7c478ffadf9136aab8c0197e9b8b7d127203b 100644 --- a/app/views/clusters/clusters/_advanced_settings.html.haml +++ b/app/views/clusters/clusters/_advanced_settings.html.haml @@ -41,4 +41,5 @@ = s_('ClusterIntegration|Remove Kubernetes cluster integration') %p = s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.") - = link_to(s_('ClusterIntegration|Remove integration'), clusterable.cluster_path(@cluster), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")}) + + #js-cluster-remove-actions{ data: { cluster_path: clusterable.cluster_path(@cluster), cluster_name: @cluster.name } } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 006e5df4d7e70f18546caa48eca28d8ea700a060..d0cb07b3859e323468ffcc6778ab326a19b2ac31 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3580,6 +3580,9 @@ msgstr "" msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}." msgstr "" +msgid "ClusterIntegration| This will permanently delete the following resources: <ul> <li>All installed applications and related resources</li> <li>The <code>gitlab-managed-apps</code> namespace</li> <li>Any project namespaces</li> <li><code>clusterroles</code></li> <li><code>clusterrolebindings</code></li> </ul>" +msgstr "" + msgid "ClusterIntegration| can be used instead of a custom domain." msgstr "" @@ -3667,9 +3670,6 @@ msgstr "" msgid "ClusterIntegration|Apply for credit" msgstr "" -msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster." -msgstr "" - msgid "ClusterIntegration|Authenticate with AWS" msgstr "" @@ -3799,6 +3799,9 @@ msgstr "" msgid "ClusterIntegration|Crossplane enables declarative provisioning of managed services from your cloud of choice using %{kubectl} or %{gitlabIntegrationLink}. Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on." msgstr "" +msgid "ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal" +msgstr "" + msgid "ClusterIntegration|Did you know?" msgstr "" @@ -3898,6 +3901,9 @@ msgstr "" msgid "ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{help_link_start}read this first%{help_link_end}." msgstr "" +msgid "ClusterIntegration|If you do not wish to delete all associated GitLab resources, you can simply remove the integration." +msgstr "" + msgid "ClusterIntegration|In order to view the health of your cluster, you must first install Prometheus below." msgstr "" @@ -4120,9 +4126,21 @@ msgstr "" msgid "ClusterIntegration|Remove integration" msgstr "" +msgid "ClusterIntegration|Remove integration and resources" +msgstr "" + +msgid "ClusterIntegration|Remove integration and resources?" +msgstr "" + +msgid "ClusterIntegration|Remove integration?" +msgstr "" + msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster." msgstr "" +msgid "ClusterIntegration|Removes cluster from project but keeps associated resources" +msgstr "" + msgid "ClusterIntegration|Replace this with your own hostname if you want. If you do so, point hostname to Ingress IP Address from above." msgstr "" @@ -4291,6 +4309,12 @@ msgstr "" msgid "ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint." msgstr "" +msgid "ClusterIntegration|To remove your integration and resources, type %{clusterName} to confirm:" +msgstr "" + +msgid "ClusterIntegration|To remove your integration, type %{clusterName} to confirm:" +msgstr "" + msgid "ClusterIntegration|Toggle Kubernetes cluster" msgstr "" @@ -4315,6 +4339,12 @@ msgstr "" msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" +msgid "ClusterIntegration|You are about to remove your cluster integration and all GitLab-created resources associated with this cluster." +msgstr "" + +msgid "ClusterIntegration|You are about to remove your cluster integration." +msgstr "" + msgid "ClusterIntegration|You are about to uninstall %{appTitle} from your cluster." msgstr "" diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb index e06f2efe1837442e53c73c373442c367699b62ee..ceec50e4f58f12821203daaa6dc91d0c1dbb6752 100644 --- a/spec/features/groups/clusters/user_spec.rb +++ b/spec/features/groups/clusters/user_spec.rb @@ -115,11 +115,11 @@ end end - context 'when user destroy the cluster' do + context 'when user destroys the cluster' do before do - page.accept_confirm do - click_link 'Remove integration' - end + click_button 'Remove integration and resources' + fill_in 'confirm_cluster_name_input', with: cluster.name + click_button 'Remove integration' end it 'user sees creation form with the successful message' do diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 741f46cef45f214c23e69135eef5bbab2a14b2d6..7c8b2640e89549b73bc7b5d6f4493d567b6c9379 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -131,11 +131,11 @@ end end - context 'when user destroy the cluster' do + context 'when user destroys the cluster' do before do - page.accept_confirm do - click_link 'Remove integration' - end + click_button 'Remove integration and resources' + fill_in 'confirm_cluster_name_input', with: cluster.name + click_button 'Remove integration' end it 'user sees creation form with the successful message' do diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index bdaeda839260f58a91bff24536feb793340bf2c5..38efcf758e1e0b0225aabc22f0961d2e79e5bae0 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -101,11 +101,11 @@ end end - context 'when user destroy the cluster' do + context 'when user destroys the cluster' do before do - page.accept_confirm do - click_link 'Remove integration' - end + click_button 'Remove integration and resources' + fill_in 'confirm_cluster_name_input', with: cluster.name + click_button 'Remove integration' end it 'user sees creation form with the successful message' do diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..8f406c6282472b1906aff4f8538104f6fd8601d2 --- /dev/null +++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Remove cluster confirmation modal renders splitbutton with modal included 1`] = ` +<div> + <div + class="dropdown btn-group b-dropdown gl-dropdown" + > + <button + class="btn btn-danger" + type="button" + > + + Remove integration and resources + + <!----> + </button> + <button + aria-expanded="false" + aria-haspopup="true" + class="btn dropdown-toggle btn-danger dropdown-toggle-split" + type="button" + > + <span + class="sr-only" + > + Toggle Dropdown + </span> + </button> + <ul + class="dropdown-menu dropdown-menu-selectable dropdown-menu-large" + role="menu" + tabindex="-1" + > + <li> + <button + class="dropdown-item is-active" + role="menuitem" + type="button" + > + <strong> + Remove integration and resources + </strong> + + <div> + Deletes all GitLab resources attached to this cluster during removal + </div> + </button> + </li> + + <li> + <hr + aria-orientation="horizontal" + class="dropdown-divider" + role="separator" + /> + </li> + <li> + <button + class="dropdown-item" + role="menuitem" + type="button" + > + <strong> + Remove integration + </strong> + + <div> + Removes cluster from project but keeps associated resources + </div> + </button> + </li> + + <!----> + + </ul> + </div> + + <!----> +</div> +`; diff --git a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..b5aead238adca469b3d3cf01ba93c718503b963a --- /dev/null +++ b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js @@ -0,0 +1,57 @@ +import { mount } from '@vue/test-utils'; +import { GlModal } from '@gitlab/ui'; +import SplitButton from '~/vue_shared/components/split_button.vue'; +import RemoveClusterConfirmation from '~/clusters/components/remove_cluster_confirmation.vue'; + +describe('Remove cluster confirmation modal', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = mount(RemoveClusterConfirmation, { + propsData: { + clusterPath: 'clusterPath', + clusterName: 'clusterName', + ...props, + }, + sync: false, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders splitbutton with modal included', () => { + createComponent(); + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('split button dropdown', () => { + const findModal = () => wrapper.find(GlModal).vm; + const findSplitButton = () => wrapper.find(SplitButton).vm; + + beforeEach(() => { + createComponent({ clusterName: 'my-test-cluster' }); + jest.spyOn(findModal(), 'show').mockReturnValue(); + }); + + it('opens modal with "cleanup" option', () => { + findSplitButton().$emit('remove-cluster-and-cleanup'); + + return wrapper.vm.$nextTick().then(() => { + expect(findModal().show).toHaveBeenCalled(); + expect(wrapper.vm.confirmCleanup).toEqual(true); + }); + }); + + it('opens modal without "cleanup" option', () => { + findSplitButton().$emit('remove-cluster'); + + return wrapper.vm.$nextTick().then(() => { + expect(findModal().show).toHaveBeenCalled(); + expect(wrapper.vm.confirmCleanup).toEqual(false); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap index 95296de5a5dafa67294be1d5b7a0eced7ccb9c9f..530428ef27cde3df0df086ab3a3c6b61de0e40a2 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap @@ -5,6 +5,7 @@ exports[`SplitButton renders actionItems 1`] = ` menu-class="dropdown-menu-selectable " split="true" text="professor" + variant="secondary" > <gldropdownitem-stub active="true"