diff options
author | Enrique Alcántara <ealcantara@gitlab.com> | 2019-05-02 14:17:03 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2019-05-02 14:17:03 +0000 |
commit | bf229a6c632709ef7c69a7033eadfd74f5ddcc97 (patch) | |
tree | 876b70f0ddb0e111199a685dd53fb71adeba5552 | |
parent | d6aa8a0553e30ef2d0d303fc79515cd5cf3ba1b9 (diff) | |
download | gitlab-ce-bf229a6c632709ef7c69a7033eadfd74f5ddcc97.tar.gz |
Uninstall application confirm modal component
- Vue confirmation modal implementation
- CSS tweaks for modal default height
19 files changed, 591 insertions, 143 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..287bdbcf873 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" @@ -391,6 +400,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 +423,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 +449,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 +492,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 +512,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 +526,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 +593,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..80ba2f22198 --- /dev/null +++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue @@ -0,0 +1,66 @@ +<script> +import { GlModal } from '@gitlab/ui'; +import { sprintf, s__ } from '~/locale'; +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, + }, + 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}`; + }, + }, +}; +</script> +<template> + <gl-modal + ok-variant="danger" + cancel-variant="light" + :ok-title="title" + :modal-id="modalId" + :title="title" + @ok="$emit('confirm')" + >{{ 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/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) { diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 53222a2bd4d..323a3dbecd5 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -34,10 +34,10 @@ .modal-body { background-color: $modal-body-bg; line-height: $line-height-base; - min-height: $modal-body-height; position: relative; padding: #{3 * $grid-size} #{2 * $grid-size}; text-align: left; + white-space: normal; .form-actions { margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size}; diff --git a/changelogs/unreleased/60777-uninstall-button.yml b/changelogs/unreleased/60777-uninstall-button.yml new file mode 100644 index 00000000000..a2727b16ef1 --- /dev/null +++ b/changelogs/unreleased/60777-uninstall-button.yml @@ -0,0 +1,5 @@ +--- +title: Implement UI for uninstalling Cluster’s managed apps +merge_request: 27559 +author: +type: added diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e6dda6a35c2..cae94fb2af4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1984,6 +1984,9 @@ msgstr "" msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" msgstr "" +msgid "ClusterIntegration|%{title} uninstalled successfully." +msgstr "" + msgid "ClusterIntegration|%{title} upgraded successfully." msgstr "" @@ -2011,6 +2014,9 @@ msgstr "" msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration" msgstr "" +msgid "ClusterIntegration|All data will be deleted and cannot be restored." +msgstr "" + msgid "ClusterIntegration|Alternatively" msgstr "" @@ -2026,6 +2032,9 @@ msgstr "" msgid "ClusterIntegration|An error occurred while trying to fetch zone machine types: %{error}" msgstr "" +msgid "ClusterIntegration|Any running pipelines will be canceled." +msgstr "" + msgid "ClusterIntegration|Applications" msgstr "" @@ -2317,6 +2326,9 @@ msgstr "" msgid "ClusterIntegration|Request to begin installing failed" msgstr "" +msgid "ClusterIntegration|Request to begin uninstalling failed" +msgstr "" + msgid "ClusterIntegration|Retry update" msgstr "" @@ -2371,6 +2383,9 @@ msgstr "" msgid "ClusterIntegration|Something went wrong while installing %{title}" msgstr "" +msgid "ClusterIntegration|Something went wrong while uninstalling %{title}" +msgstr "" + msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain." msgstr "" @@ -2380,6 +2395,15 @@ msgstr "" msgid "ClusterIntegration|The URL used to access the Kubernetes API." msgstr "" +msgid "ClusterIntegration|The associated IP will be deleted and cannot be restored." +msgstr "" + +msgid "ClusterIntegration|The associated certifcate will be deleted and cannot be restored." +msgstr "" + +msgid "ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored." +msgstr "" + msgid "ClusterIntegration|The endpoint is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time." msgstr "" @@ -2395,6 +2419,9 @@ msgstr "" msgid "ClusterIntegration|Toggle Kubernetes cluster" msgstr "" +msgid "ClusterIntegration|Uninstall %{appTitle}" +msgstr "" + msgid "ClusterIntegration|Update failed. Please check the logs and try again." msgstr "" @@ -2422,6 +2449,9 @@ 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 uninstall %{appTitle} from your cluster." +msgstr "" + msgid "ClusterIntegration|You must first install Helm Tiller before installing the applications below" msgstr "" diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js index a61103397eb..73897107f67 100644 --- a/spec/frontend/clusters/clusters_bundle_spec.js +++ b/spec/frontend/clusters/clusters_bundle_spec.js @@ -1,12 +1,12 @@ import Clusters from '~/clusters/clusters_bundle'; -import { APPLICATION_STATUS, INGRESS_DOMAIN_SUFFIX } from '~/clusters/constants'; +import { APPLICATION_STATUS, INGRESS_DOMAIN_SUFFIX, APPLICATIONS } from '~/clusters/constants'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import { loadHTMLFixture } from 'helpers/fixtures'; import { setTestTimeout } from 'helpers/timeout'; import $ from 'jquery'; -const { INSTALLING, INSTALLABLE, INSTALLED } = APPLICATION_STATUS; +const { INSTALLING, INSTALLABLE, INSTALLED, UNINSTALLING } = APPLICATION_STATUS; describe('Clusters', () => { setTestTimeout(1000); @@ -212,73 +212,61 @@ describe('Clusters', () => { }); describe('installApplication', () => { - it('tries to install helm', () => { + it.each(APPLICATIONS)('tries to install %s', applicationId => { jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); - cluster.store.state.applications.helm.status = INSTALLABLE; + cluster.store.state.applications[applicationId].status = INSTALLABLE; - cluster.installApplication({ id: 'helm' }); + cluster.installApplication({ id: applicationId }); - expect(cluster.store.state.applications.helm.status).toEqual(INSTALLING); - expect(cluster.store.state.applications.helm.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined); + expect(cluster.store.state.applications[applicationId].status).toEqual(INSTALLING); + expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null); + expect(cluster.service.installApplication).toHaveBeenCalledWith(applicationId, undefined); }); - it('tries to install ingress', () => { - jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); - - cluster.store.state.applications.ingress.status = INSTALLABLE; - - cluster.installApplication({ id: 'ingress' }); - - expect(cluster.store.state.applications.ingress.status).toEqual(INSTALLING); - expect(cluster.store.state.applications.ingress.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined); - }); + it('sets error request status when the request fails', () => { + jest + .spyOn(cluster.service, 'installApplication') + .mockRejectedValueOnce(new Error('STUBBED ERROR')); - it('tries to install runner', () => { - jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); + cluster.store.state.applications.helm.status = INSTALLABLE; - cluster.store.state.applications.runner.status = INSTALLABLE; + const promise = cluster.installApplication({ id: 'helm' }); - cluster.installApplication({ id: 'runner' }); + return promise.then(() => { + expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE); + expect(cluster.store.state.applications.helm.installFailed).toBe(true); - expect(cluster.store.state.applications.runner.status).toEqual(INSTALLING); - expect(cluster.store.state.applications.runner.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined); + expect(cluster.store.state.applications.helm.requestReason).toBeDefined(); + }); }); + }); - it('tries to install jupyter', () => { - jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); + describe('uninstallApplication', () => { + it.each(APPLICATIONS)('tries to uninstall %s', applicationId => { + jest.spyOn(cluster.service, 'uninstallApplication').mockResolvedValueOnce(); - cluster.installApplication({ - id: 'jupyter', - params: { hostname: cluster.store.state.applications.jupyter.hostname }, - }); + cluster.store.state.applications[applicationId].status = INSTALLED; - cluster.store.state.applications.jupyter.status = INSTALLABLE; - expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', { - hostname: cluster.store.state.applications.jupyter.hostname, - }); + cluster.uninstallApplication({ id: applicationId }); + + expect(cluster.store.state.applications[applicationId].status).toEqual(UNINSTALLING); + expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null); + expect(cluster.service.uninstallApplication).toHaveBeenCalledWith(applicationId); }); - it('sets error request status when the request fails', () => { + it('sets error request status when the uninstall request fails', () => { jest - .spyOn(cluster.service, 'installApplication') + .spyOn(cluster.service, 'uninstallApplication') .mockRejectedValueOnce(new Error('STUBBED ERROR')); - cluster.store.state.applications.helm.status = INSTALLABLE; + cluster.store.state.applications.helm.status = INSTALLED; - const promise = cluster.installApplication({ id: 'helm' }); - - expect(cluster.store.state.applications.helm.status).toEqual(INSTALLING); - expect(cluster.store.state.applications.helm.requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalled(); + const promise = cluster.uninstallApplication({ id: 'helm' }); return promise.then(() => { - expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE); - expect(cluster.store.state.applications.helm.installFailed).toBe(true); + expect(cluster.store.state.applications.helm.status).toEqual(INSTALLED); + expect(cluster.store.state.applications.helm.uninstallFailed).toBe(true); expect(cluster.store.state.applications.helm.requestReason).toBeDefined(); }); diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js index 17273b7d5b1..7c781b72355 100644 --- a/spec/frontend/clusters/components/application_row_spec.js +++ b/spec/frontend/clusters/components/application_row_spec.js @@ -1,7 +1,10 @@ import Vue from 'vue'; +import { shallowMount } from '@vue/test-utils'; import eventHub from '~/clusters/event_hub'; import { APPLICATION_STATUS } from '~/clusters/constants'; import applicationRow from '~/clusters/components/application_row.vue'; +import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue'; + import mountComponent from 'helpers/vue_mount_component_helper'; import { DEFAULT_APPLICATION_STATE } from '../services/mock_data'; @@ -194,11 +197,52 @@ describe('Application Row', () => { ...DEFAULT_APPLICATION_STATE, installed: true, uninstallable: true, + status: APPLICATION_STATUS.NOT_INSTALLABLE, }); const uninstallButton = vm.$el.querySelector('.js-cluster-application-uninstall-button'); expect(uninstallButton).toBeTruthy(); }); + + it('displays a success toast message if application uninstall was successful', () => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + title: 'GitLab Runner', + uninstallSuccessful: false, + }); + + vm.$toast = { show: jest.fn() }; + vm.uninstallSuccessful = true; + + return vm.$nextTick(() => { + expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner uninstalled successfully.'); + }); + }); + }); + + describe('when confirmation modal triggers confirm event', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(ApplicationRow, { + propsData: { + ...DEFAULT_APPLICATION_STATE, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('triggers uninstallApplication event', () => { + jest.spyOn(eventHub, '$emit'); + wrapper.find(UninstallApplicationConfirmationModal).vm.$emit('confirm'); + + expect(eventHub.$emit).toHaveBeenCalledWith('uninstallApplication', { + id: DEFAULT_APPLICATION_STATE.id, + }); + }); }); describe('Upgrade button', () => { @@ -304,7 +348,7 @@ describe('Application Row', () => { vm.$toast = { show: jest.fn() }; vm.updateSuccessful = true; - vm.$nextTick(() => { + return vm.$nextTick(() => { expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner upgraded successfully.'); }); }); @@ -360,60 +404,88 @@ describe('Application Row', () => { }); describe('Error block', () => { - it('does not show error block when there is no error', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: null, - }); - const generalErrorMessage = vm.$el.querySelector( - '.js-cluster-application-general-error-message', - ); + describe('when nothing fails', () => { + it('does not show error block', () => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + }); + const generalErrorMessage = vm.$el.querySelector( + '.js-cluster-application-general-error-message', + ); - expect(generalErrorMessage).toBeNull(); + expect(generalErrorMessage).toBeNull(); + }); }); - it('shows status reason when install fails', () => { + describe('when install or uninstall fails', () => { const statusReason = 'We broke it 0.0'; - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.ERROR, - statusReason, - installFailed: true, + const requestReason = 'We broke the request 0.0'; + + beforeEach(() => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_STATUS.ERROR, + statusReason, + requestReason, + installFailed: true, + }); }); - const generalErrorMessage = vm.$el.querySelector( - '.js-cluster-application-general-error-message', - ); - const statusErrorMessage = vm.$el.querySelector( - '.js-cluster-application-status-error-message', - ); - expect(generalErrorMessage.textContent.trim()).toEqual( - `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`, - ); + it('shows status reason if it is available', () => { + const statusErrorMessage = vm.$el.querySelector( + '.js-cluster-application-status-error-message', + ); + + expect(statusErrorMessage.textContent.trim()).toEqual(statusReason); + }); + + it('shows request reason if it is available', () => { + const requestErrorMessage = vm.$el.querySelector( + '.js-cluster-application-request-error-message', + ); + + expect(requestErrorMessage.textContent.trim()).toEqual(requestReason); + }); + }); + + describe('when install fails', () => { + beforeEach(() => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_STATUS.ERROR, + installFailed: true, + }); + }); - expect(statusErrorMessage.textContent.trim()).toEqual(statusReason); + it('shows a general message indicating the installation failed', () => { + const generalErrorMessage = vm.$el.querySelector( + '.js-cluster-application-general-error-message', + ); + + expect(generalErrorMessage.textContent.trim()).toEqual( + `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`, + ); + }); }); - it('shows request reason when REQUEST_FAILURE', () => { - const requestReason = 'We broke thre request 0.0'; - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.INSTALLABLE, - installFailed: true, - requestReason, + describe('when uninstall fails', () => { + beforeEach(() => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_STATUS.ERROR, + uninstallFailed: true, + }); }); - const generalErrorMessage = vm.$el.querySelector( - '.js-cluster-application-general-error-message', - ); - const requestErrorMessage = vm.$el.querySelector( - '.js-cluster-application-request-error-message', - ); - expect(generalErrorMessage.textContent.trim()).toEqual( - `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`, - ); + it('shows a general message indicating the uninstalling failed', () => { + const generalErrorMessage = vm.$el.querySelector( + '.js-cluster-application-general-error-message', + ); - expect(requestErrorMessage.textContent.trim()).toEqual(requestReason); + expect(generalErrorMessage.textContent.trim()).toEqual( + `Something went wrong while uninstalling ${DEFAULT_APPLICATION_STATE.title}`, + ); + }); }); }); }); diff --git a/spec/frontend/clusters/components/uninstall_application_button_spec.js b/spec/frontend/clusters/components/uninstall_application_button_spec.js new file mode 100644 index 00000000000..9f9397d4d41 --- /dev/null +++ b/spec/frontend/clusters/components/uninstall_application_button_spec.js @@ -0,0 +1,32 @@ +import { shallowMount } from '@vue/test-utils'; +import UninstallApplicationButton from '~/clusters/components/uninstall_application_button.vue'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import { APPLICATION_STATUS } from '~/clusters/constants'; + +const { INSTALLED, UPDATING, UNINSTALLING } = APPLICATION_STATUS; + +describe('UninstallApplicationButton', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(UninstallApplicationButton, { + propsData: { ...props }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe.each` + status | loading | disabled | label + ${INSTALLED} | ${false} | ${false} | ${'Uninstall'} + ${UPDATING} | ${false} | ${true} | ${'Uninstall'} + ${UNINSTALLING} | ${true} | ${true} | ${'Uninstalling'} + `('when app status is $status', ({ loading, disabled, status, label }) => { + it(`renders a loading=${loading}, disabled=${disabled} button with label="${label}"`, () => { + createComponent({ status }); + expect(wrapper.find(LoadingButton).props()).toMatchObject({ loading, disabled, label }); + }); + }); +}); diff --git a/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js b/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js new file mode 100644 index 00000000000..6a7126b45cd --- /dev/null +++ b/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js @@ -0,0 +1,47 @@ +import { shallowMount } from '@vue/test-utils'; +import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue'; +import { GlModal } from '@gitlab/ui'; +import { INGRESS } from '~/clusters/constants'; + +describe('UninstallApplicationConfirmationModal', () => { + let wrapper; + const appTitle = 'Ingress'; + + const createComponent = (props = {}) => { + wrapper = shallowMount(UninstallApplicationConfirmationModal, { + propsData: { ...props }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(() => { + createComponent({ application: INGRESS, applicationTitle: appTitle }); + }); + + it(`renders a modal with a title "Uninstall ${appTitle}"`, () => { + expect(wrapper.find(GlModal).attributes('title')).toEqual(`Uninstall ${appTitle}`); + }); + + it(`renders a modal with an ok button labeled "Uninstall ${appTitle}"`, () => { + expect(wrapper.find(GlModal).attributes('ok-title')).toEqual(`Uninstall ${appTitle}`); + }); + + it('triggers confirm event when ok button is clicked', () => { + wrapper.find(GlModal).vm.$emit('ok'); + + expect(wrapper.emitted('confirm')).toBeTruthy(); + }); + + it('displays a warning text indicating the app will be uninstalled', () => { + expect(wrapper.text()).toContain(`You are about to uninstall ${appTitle} from your cluster.`); + }); + + it('displays a custom warning text depending on the application', () => { + expect(wrapper.text()).toContain( + `The associated load balancer and IP will be deleted and cannot be restored.`, + ); + }); +}); diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js index e74b7910572..e057e2ac955 100644 --- a/spec/frontend/clusters/services/application_state_machine_spec.js +++ b/spec/frontend/clusters/services/application_state_machine_spec.js @@ -1,5 +1,10 @@ import transitionApplicationState from '~/clusters/services/application_state_machine'; -import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '~/clusters/constants'; +import { + APPLICATION_STATUS, + UNINSTALL_EVENT, + UPDATE_EVENT, + INSTALL_EVENT, +} from '~/clusters/constants'; const { NO_STATUS, @@ -12,6 +17,8 @@ const { UPDATING, UPDATED, UPDATE_ERRORED, + UNINSTALLING, + UNINSTALL_ERRORED, } = APPLICATION_STATUS; const NO_EFFECTS = 'no effects'; @@ -21,16 +28,18 @@ describe('applicationStateMachine', () => { describe(`current state is ${NO_STATUS}`, () => { it.each` - expectedState | event | effects - ${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS} - ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS} - ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS} - ${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS} - ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS} - ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }} - ${UPDATING} | ${UPDATING} | ${NO_EFFECTS} - ${INSTALLED} | ${UPDATED} | ${NO_EFFECTS} - ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }} + expectedState | event | effects + ${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS} + ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS} + ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS} + ${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS} + ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS} + ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }} + ${UPDATING} | ${UPDATING} | ${NO_EFFECTS} + ${INSTALLED} | ${UPDATED} | ${NO_EFFECTS} + ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }} + ${UNINSTALLING} | ${UNINSTALLING} | ${NO_EFFECTS} + ${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }} `(`transitions to $expectedState on $event event and applies $effects`, data => { const { expectedState, event, effects } = data; const currentAppState = { @@ -99,8 +108,9 @@ describe('applicationStateMachine', () => { describe(`current state is ${INSTALLED}`, () => { it.each` - expectedState | event | effects - ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }} + expectedState | event | effects + ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }} + ${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }} `(`transitions to $expectedState on $event event and applies $effects`, data => { const { expectedState, event, effects } = data; const currentAppState = { @@ -131,4 +141,22 @@ describe('applicationStateMachine', () => { }); }); }); + + describe(`current state is ${UNINSTALLING}`, () => { + it.each` + expectedState | event | effects + ${INSTALLABLE} | ${INSTALLABLE} | ${{ uninstallSuccessful: true }} + ${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }} + `(`transitions to $expectedState on $event event and applies $effects`, data => { + const { expectedState, event, effects } = data; + const currentAppState = { + status: UNINSTALLING, + }; + + expect(transitionApplicationState(currentAppState, event)).toEqual({ + status: expectedState, + ...effects, + }); + }); + }); }); diff --git a/spec/frontend/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js index 1e896af1c7d..41ad398e924 100644 --- a/spec/frontend/clusters/services/mock_data.js +++ b/spec/frontend/clusters/services/mock_data.js @@ -11,6 +11,7 @@ const CLUSTERS_MOCK_DATA = { name: 'helm', status: APPLICATION_STATUS.INSTALLABLE, status_reason: null, + can_uninstall: false, }, { name: 'ingress', @@ -18,32 +19,38 @@ const CLUSTERS_MOCK_DATA = { status_reason: 'Cannot connect', external_ip: null, external_hostname: null, + can_uninstall: false, }, { name: 'runner', status: APPLICATION_STATUS.INSTALLING, status_reason: null, + can_uninstall: false, }, { name: 'prometheus', status: APPLICATION_STATUS.ERROR, status_reason: 'Cannot connect', + can_uninstall: false, }, { name: 'jupyter', status: APPLICATION_STATUS.INSTALLING, status_reason: 'Cannot connect', + can_uninstall: false, }, { name: 'knative', status: APPLICATION_STATUS.INSTALLING, status_reason: 'Cannot connect', + can_uninstall: false, }, { name: 'cert_manager', status: APPLICATION_STATUS.ERROR, status_reason: 'Cannot connect', email: 'test@example.com', + can_uninstall: false, }, ], }, diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js index a20e0439555..aa926bb36d7 100644 --- a/spec/frontend/clusters/stores/clusters_store_spec.js +++ b/spec/frontend/clusters/stores/clusters_store_spec.js @@ -63,6 +63,8 @@ describe('Clusters Store', () => { installed: false, installFailed: false, uninstallable: false, + uninstallSuccessful: false, + uninstallFailed: false, }, ingress: { title: 'Ingress', @@ -74,6 +76,8 @@ describe('Clusters Store', () => { installed: false, installFailed: true, uninstallable: false, + uninstallSuccessful: false, + uninstallFailed: false, }, runner: { title: 'GitLab Runner', @@ -89,6 +93,8 @@ describe('Clusters Store', () => { updateFailed: false, updateSuccessful: false, uninstallable: false, + uninstallSuccessful: false, + uninstallFailed: false, }, prometheus: { title: 'Prometheus', @@ -98,6 +104,8 @@ describe('Clusters Store', () => { installed: false, installFailed: true, uninstallable: false, + uninstallSuccessful: false, + uninstallFailed: false, }, jupyter: { title: 'JupyterHub', @@ -108,6 +116,8 @@ describe('Clusters Store', () => { installed: false, installFailed: false, uninstallable: false, + uninstallSuccessful: false, + uninstallFailed: false, }, knative: { title: 'Knative', @@ -121,6 +131,8 @@ describe('Clusters Store', () => { installed: false, installFailed: false, uninstallable: false, + uninstallSuccessful: false, + uninstallFailed: false, }, cert_manager: { title: 'Cert-Manager', @@ -131,6 +143,8 @@ describe('Clusters Store', () => { email: mockResponseData.applications[6].email, installed: false, uninstallable: false, + uninstallSuccessful: false, + uninstallFailed: false, }, }, }); |