diff options
Diffstat (limited to 'app/assets/javascripts/clusters')
10 files changed, 277 insertions, 40 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 8461e01de7b..561b6bdd9f1 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -132,6 +132,7 @@ export default class Clusters { eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId)); eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data)); eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data)); + eventHub.$on('uninstallApplication', data => this.uninstallApplication(data)); } removeListeners() { @@ -141,6 +142,7 @@ export default class Clusters { eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess); eventHub.$off('saveKnativeDomain'); eventHub.$off('setKnativeHostname'); + eventHub.$off('uninstallApplication'); } initPolling() { @@ -249,14 +251,13 @@ export default class Clusters { } } - installApplication(data) { - const appId = data.id; + installApplication({ id: appId, params }) { this.store.updateAppProperty(appId, 'requestReason', null); this.store.updateAppProperty(appId, 'statusReason', null); this.store.installApplication(appId); - return this.service.installApplication(appId, data.params).catch(() => { + return this.service.installApplication(appId, params).catch(() => { this.store.notifyInstallFailure(appId); this.store.updateAppProperty( appId, @@ -266,6 +267,22 @@ export default class Clusters { }); } + uninstallApplication({ id: appId }) { + this.store.updateAppProperty(appId, 'requestReason', null); + this.store.updateAppProperty(appId, 'statusReason', null); + + this.store.uninstallApplication(appId); + + return this.service.uninstallApplication(appId).catch(() => { + this.store.notifyUninstallFailure(appId); + this.store.updateAppProperty( + appId, + 'requestReason', + s__('ClusterIntegration|Request to begin uninstalling failed'), + ); + }); + } + upgradeApplication(data) { const appId = data.id; diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index a351916942e..5f7675bb432 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -1,12 +1,13 @@ <script> /* eslint-disable vue/require-default-prop */ -import { GlLink } from '@gitlab/ui'; +import { GlLink, GlModalDirective } from '@gitlab/ui'; import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import { s__, sprintf } from '../../locale'; import eventHub from '../event_hub'; import identicon from '../../vue_shared/components/identicon.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue'; import UninstallApplicationButton from './uninstall_application_button.vue'; +import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue'; import { APPLICATION_STATUS } from '../constants'; @@ -17,6 +18,10 @@ export default { TimeagoTooltip, GlLink, UninstallApplicationButton, + UninstallApplicationConfirmationModal, + }, + directives: { + GlModalDirective, }, props: { id: { @@ -94,6 +99,16 @@ export default { required: false, default: false, }, + uninstallFailed: { + type: Boolean, + required: false, + default: false, + }, + uninstallSuccessful: { + type: Boolean, + required: false, + default: false, + }, updateAcknowledged: { type: Boolean, required: false, @@ -170,10 +185,21 @@ export default { manageButtonLabel() { return s__('ClusterIntegration|Manage'); }, + hasError() { + return this.installFailed || this.uninstallFailed; + }, generalErrorDescription() { - return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), { - title: this.title, - }); + let errorDescription; + + if (this.installFailed) { + errorDescription = s__('ClusterIntegration|Something went wrong while installing %{title}'); + } else if (this.uninstallFailed) { + errorDescription = s__( + 'ClusterIntegration|Something went wrong while uninstalling %{title}', + ); + } + + return sprintf(errorDescription, { title: this.title }); }, versionLabel() { if (this.updateFailed) { @@ -214,13 +240,23 @@ export default { // AND new upgrade is unavailable AND version information is present. return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version; }, + uninstallSuccessDescription() { + return sprintf(s__('ClusterIntegration|%{title} uninstalled successfully.'), { + title: this.title, + }); + }, }, watch: { - updateSuccessful() { - if (this.updateSuccessful) { + updateSuccessful(updateSuccessful) { + if (updateSuccessful) { this.$toast.show(this.upgradeSuccessDescription); } }, + uninstallSuccessful(uninstallSuccessful) { + if (uninstallSuccessful) { + this.$toast.show(this.uninstallSuccessDescription); + } + }, }, methods: { installClicked() { @@ -235,6 +271,11 @@ export default { params: this.installApplicationRequestParams, }); }, + uninstallConfirmed() { + eventHub.$emit('uninstallApplication', { + id: this.id, + }); + }, }, }; </script> @@ -271,10 +312,7 @@ export default { <span v-else class="js-cluster-application-title">{{ title }}</span> </strong> <slot name="description"></slot> - <div - v-if="installFailed || isUnknownStatus" - class="cluster-application-error text-danger prepend-top-10" - > + <div v-if="hasError" class="cluster-application-error text-danger prepend-top-10"> <p class="js-cluster-application-general-error-message append-bottom-0"> {{ generalErrorDescription }} </p> @@ -325,9 +363,9 @@ export default { role="gridcell" > <div v-if="showManageButton" class="btn-group table-action-buttons"> - <a :href="manageLink" :class="{ disabled: disabled }" class="btn"> - {{ manageButtonLabel }} - </a> + <a :href="manageLink" :class="{ disabled: disabled }" class="btn">{{ + manageButtonLabel + }}</a> </div> <div class="btn-group table-action-buttons"> <loading-button @@ -340,8 +378,15 @@ export default { /> <uninstall-application-button v-if="displayUninstallButton" + v-gl-modal-directive="'uninstall-' + id" + :status="status" class="js-cluster-application-uninstall-button" /> + <uninstall-application-confirmation-modal + :application="id" + :application-title="title" + @confirm="uninstallConfirmed()" + /> </div> </div> </div> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index dfc2069f131..73760da9b98 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -240,6 +240,9 @@ export default { :request-reason="applications.helm.requestReason" :installed="applications.helm.installed" :install-failed="applications.helm.installFailed" + :uninstallable="applications.helm.uninstallable" + :uninstall-successful="applications.helm.uninstallSuccessful" + :uninstall-failed="applications.helm.uninstallFailed" class="rounded-top" title-link="https://docs.helm.sh/" > @@ -269,6 +272,9 @@ export default { :request-reason="applications.ingress.requestReason" :installed="applications.ingress.installed" :install-failed="applications.ingress.installFailed" + :uninstallable="applications.ingress.uninstallable" + :uninstall-successful="applications.ingress.uninstallSuccessful" + :uninstall-failed="applications.ingress.uninstallFailed" :disabled="!helmInstalled" title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" > @@ -312,9 +318,9 @@ export default { generated endpoint in order to access your application after it has been deployed.`) }} - <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> - {{ __('More information') }} - </a> + <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{ + __('More information') + }}</a> </p> </div> @@ -324,9 +330,9 @@ export default { the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }} - <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> - {{ __('More information') }} - </a> + <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{ + __('More information') + }}</a> </p> </template> <template v-if="!ingressInstalled"> @@ -345,6 +351,9 @@ export default { :installed="applications.cert_manager.installed" :install-failed="applications.cert_manager.installFailed" :install-application-request-params="{ email: applications.cert_manager.email }" + :uninstallable="applications.cert_manager.uninstallable" + :uninstall-successful="applications.cert_manager.uninstallSuccessful" + :uninstall-failed="applications.cert_manager.uninstallFailed" :disabled="!helmInstalled" title-link="https://cert-manager.readthedocs.io/en/latest/#" > @@ -352,9 +361,9 @@ export default { <div slot="description"> <p v-html="certManagerDescription"></p> <div class="form-group"> - <label for="cert-manager-issuer-email"> - {{ s__('ClusterIntegration|Issuer Email') }} - </label> + <label for="cert-manager-issuer-email">{{ + s__('ClusterIntegration|Issuer Email') + }}</label> <div class="input-group"> <input v-model="applications.cert_manager.email" @@ -380,7 +389,6 @@ export default { </template> </application-row> <application-row - v-if="isProjectCluster" id="prometheus" :logo-url="prometheusLogo" :title="applications.prometheus.title" @@ -391,6 +399,9 @@ export default { :request-reason="applications.prometheus.requestReason" :installed="applications.prometheus.installed" :install-failed="applications.prometheus.installFailed" + :uninstallable="applications.prometheus.uninstallable" + :uninstall-successful="applications.prometheus.uninstallSuccessful" + :uninstall-failed="applications.prometheus.uninstallFailed" :disabled="!helmInstalled" title-link="https://prometheus.io/docs/introduction/overview/" > @@ -411,6 +422,9 @@ export default { :install-failed="applications.runner.installFailed" :update-successful="applications.runner.updateSuccessful" :update-failed="applications.runner.updateFailed" + :uninstallable="applications.runner.uninstallable" + :uninstall-successful="applications.runner.uninstallSuccessful" + :uninstall-failed="applications.runner.uninstallFailed" :disabled="!helmInstalled" title-link="https://docs.gitlab.com/runner/" > @@ -434,6 +448,9 @@ export default { :request-reason="applications.jupyter.requestReason" :installed="applications.jupyter.installed" :install-failed="applications.jupyter.installFailed" + :uninstallable="applications.jupyter.uninstallable" + :uninstall-successful="applications.jupyter.uninstallSuccessful" + :uninstall-failed="applications.jupyter.uninstallFailed" :install-application-request-params="{ hostname: applications.jupyter.hostname }" :disabled="!helmInstalled" title-link="https://jupyterhub.readthedocs.io/en/stable/" @@ -474,9 +491,9 @@ export default { s__(`ClusterIntegration|Replace this with your own hostname if you want. If you do so, point hostname to Ingress IP Address from above.`) }} - <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> - {{ __('More information') }} - </a> + <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{ + __('More information') + }}</a> </p> </div> </template> @@ -494,6 +511,9 @@ export default { :installed="applications.knative.installed" :install-failed="applications.knative.installFailed" :install-application-request-params="{ hostname: applications.knative.hostname }" + :uninstallable="applications.knative.uninstallable" + :uninstall-successful="applications.knative.uninstallSuccessful" + :uninstall-failed="applications.knative.uninstallFailed" :disabled="!helmInstalled" v-bind="applications.knative" title-link="https://github.com/knative/docs" @@ -505,9 +525,9 @@ export default { s__(`ClusterIntegration|You must have an RBAC-enabled cluster to install Knative.`) }} - <a :href="helpPath" target="_blank" rel="noopener noreferrer"> - {{ __('More information') }} - </a> + <a :href="helpPath" target="_blank" rel="noopener noreferrer">{{ + __('More information') + }}</a> </p> <br /> </span> @@ -572,9 +592,9 @@ export default { `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`, ) }} - <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> - {{ __('More information') }} - </a> + <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{ + __('More information') + }}</a> </p> <p diff --git a/app/assets/javascripts/clusters/components/uninstall_application_button.vue b/app/assets/javascripts/clusters/components/uninstall_application_button.vue index 30918d1d115..ef4bcbe14dd 100644 --- a/app/assets/javascripts/clusters/components/uninstall_application_button.vue +++ b/app/assets/javascripts/clusters/components/uninstall_application_button.vue @@ -1,14 +1,33 @@ <script> -// TODO: Implement loading button component import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import { APPLICATION_STATUS } from '~/clusters/constants'; + +const { UPDATING, UNINSTALLING } = APPLICATION_STATUS; export default { components: { LoadingButton, }, + props: { + status: { + type: String, + required: true, + }, + }, + computed: { + disabled() { + return [UNINSTALLING, UPDATING].includes(this.status); + }, + loading() { + return this.status === UNINSTALLING; + }, + label() { + return this.loading ? this.__('Uninstalling') : this.__('Uninstall'); + }, + }, }; </script> <template> - <loading-button @click="$emit('click')" /> + <loading-button :label="label" :disabled="disabled" :loading="loading" /> </template> diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue new file mode 100644 index 00000000000..65827f1cb6a --- /dev/null +++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue @@ -0,0 +1,74 @@ +<script> +import { GlModal } from '@gitlab/ui'; +import { sprintf, s__ } from '~/locale'; +import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click'; +import { INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants'; + +const CUSTOM_APP_WARNING_TEXT = { + [INGRESS]: s__( + 'ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored.', + ), + [CERT_MANAGER]: s__( + 'ClusterIntegration|The associated certifcate will be deleted and cannot be restored.', + ), + [PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'), + [RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'), + [KNATIVE]: s__('ClusterIntegration|The associated IP will be deleted and cannot be restored.'), + [JUPYTER]: '', +}; + +export default { + components: { + GlModal, + }, + mixins: [trackUninstallButtonClickMixin], + props: { + application: { + type: String, + required: true, + }, + applicationTitle: { + type: String, + required: true, + }, + }, + computed: { + title() { + return sprintf(s__('ClusterIntegration|Uninstall %{appTitle}'), { + appTitle: this.applicationTitle, + }); + }, + warningText() { + return sprintf( + s__('ClusterIntegration|You are about to uninstall %{appTitle} from your cluster.'), + { + appTitle: this.applicationTitle, + }, + ); + }, + customAppWarningText() { + return CUSTOM_APP_WARNING_TEXT[this.application]; + }, + modalId() { + return `uninstall-${this.application}`; + }, + }, + methods: { + confirmUninstall() { + this.trackUninstallButtonClick(this.application); + this.$emit('confirm'); + }, + }, +}; +</script> +<template> + <gl-modal + ok-variant="danger" + cancel-variant="light" + :ok-title="title" + :modal-id="modalId" + :title="title" + @ok="confirmUninstall()" + >{{ warningText }} {{ customAppWarningText }}</gl-modal + > +</template> diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index 48dbce9676e..8fd752092c9 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -28,16 +28,23 @@ export const APPLICATION_STATUS = { export const APPLICATION_INSTALLED_STATUSES = [ APPLICATION_STATUS.INSTALLED, APPLICATION_STATUS.UPDATING, + APPLICATION_STATUS.UNINSTALLING, ]; // These are only used client-side export const UPDATE_EVENT = 'update'; export const INSTALL_EVENT = 'install'; +export const UNINSTALL_EVENT = 'uninstall'; +export const HELM = 'helm'; export const INGRESS = 'ingress'; export const JUPYTER = 'jupyter'; export const KNATIVE = 'knative'; export const RUNNER = 'runner'; export const CERT_MANAGER = 'cert_manager'; +export const PROMETHEUS = 'prometheus'; + +export const APPLICATIONS = [HELM, INGRESS, JUPYTER, KNATIVE, RUNNER, CERT_MANAGER, PROMETHEUS]; + export const INGRESS_DOMAIN_SUFFIX = '.nip.io'; diff --git a/app/assets/javascripts/clusters/mixins/track_uninstall_button_click.js b/app/assets/javascripts/clusters/mixins/track_uninstall_button_click.js new file mode 100644 index 00000000000..18f65b234d3 --- /dev/null +++ b/app/assets/javascripts/clusters/mixins/track_uninstall_button_click.js @@ -0,0 +1,5 @@ +export default { + methods: { + trackUninstallButtonClick: () => {}, + }, +}; diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js index aafb2350ae4..14b80a116a7 100644 --- a/app/assets/javascripts/clusters/services/application_state_machine.js +++ b/app/assets/javascripts/clusters/services/application_state_machine.js @@ -1,4 +1,4 @@ -import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '../constants'; +import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT, UNINSTALL_EVENT } from '../constants'; const { NO_STATUS, @@ -11,6 +11,8 @@ const { UPDATING, UPDATED, UPDATE_ERRORED, + UNINSTALLING, + UNINSTALL_ERRORED, } = APPLICATION_STATUS; const applicationStateMachine = { @@ -52,6 +54,15 @@ const applicationStateMachine = { updateFailed: true, }, }, + [UNINSTALLING]: { + target: UNINSTALLING, + }, + [UNINSTALL_ERRORED]: { + target: INSTALLED, + effects: { + uninstallFailed: true, + }, + }, }, }, [NOT_INSTALLABLE]: { @@ -97,6 +108,13 @@ const applicationStateMachine = { updateSuccessful: false, }, }, + [UNINSTALL_EVENT]: { + target: UNINSTALLING, + effects: { + uninstallFailed: false, + uninstallSuccessful: false, + }, + }, }, }, [UPDATING]: { @@ -116,6 +134,22 @@ const applicationStateMachine = { }, }, }, + [UNINSTALLING]: { + on: { + [INSTALLABLE]: { + target: INSTALLABLE, + effects: { + uninstallSuccessful: true, + }, + }, + [UNINSTALL_ERRORED]: { + target: INSTALLED, + effects: { + uninstallFailed: true, + }, + }, + }, + }, }; /** diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js index dea33ac44c5..01f3732de7e 100644 --- a/app/assets/javascripts/clusters/services/clusters_service.js +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -29,6 +29,10 @@ export default class ClusterService { return axios.patch(this.appUpdateEndpointMap[appId], params); } + uninstallApplication(appId, params) { + return axios.delete(this.appInstallEndpointMap[appId], params); + } + static updateCluster(endpoint, data) { return axios.put(endpoint, data); } diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index c2e30960659..1b4d7e8372c 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -10,6 +10,7 @@ import { APPLICATION_STATUS, INSTALL_EVENT, UPDATE_EVENT, + UNINSTALL_EVENT, } from '../constants'; import transitionApplicationState from '../services/application_state_machine'; @@ -21,6 +22,9 @@ const applicationInitialState = { requestReason: null, installed: false, installFailed: false, + uninstallable: false, + uninstallFailed: false, + uninstallSuccessful: false, }; export default class ClusterStore { @@ -116,6 +120,14 @@ export default class ClusterStore { this.handleApplicationEvent(appId, APPLICATION_STATUS.UPDATE_ERRORED); } + uninstallApplication(appId) { + this.handleApplicationEvent(appId, UNINSTALL_EVENT); + } + + notifyUninstallFailure(appId) { + this.handleApplicationEvent(appId, APPLICATION_STATUS.UNINSTALL_ERRORED); + } + handleApplicationEvent(appId, event) { const currentAppState = this.state.applications[appId]; @@ -141,6 +153,7 @@ export default class ClusterStore { status_reason: statusReason, version, update_available: upgradeAvailable, + can_uninstall: uninstallable, } = serverAppEntry; const currentApplicationState = this.state.applications[appId] || {}; const nextApplicationState = transitionApplicationState(currentApplicationState, status); @@ -150,8 +163,7 @@ export default class ClusterStore { ...nextApplicationState, statusReason, installed: isApplicationInstalled(nextApplicationState.status), - // Make sure uninstallable is always false until this feature is unflagged - uninstallable: false, + uninstallable, }; if (appId === INGRESS) { |