Commit f9875875 authored by Phil Hughes's avatar Phil Hughes

Merge branch '60777-uninstall-button' into 'master'

Implement UI for uninstalling Cluster’s managed apps

Closes #60777

See merge request gitlab-org/gitlab-ce!27559
parents d6aa8a05 bf229a6c
...@@ -132,6 +132,7 @@ export default class Clusters { ...@@ -132,6 +132,7 @@ export default class Clusters {
eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId)); eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data)); eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data)); eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
} }
removeListeners() { removeListeners() {
...@@ -141,6 +142,7 @@ export default class Clusters { ...@@ -141,6 +142,7 @@ export default class Clusters {
eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess); eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
eventHub.$off('saveKnativeDomain'); eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname'); eventHub.$off('setKnativeHostname');
eventHub.$off('uninstallApplication');
} }
initPolling() { initPolling() {
...@@ -249,14 +251,13 @@ export default class Clusters { ...@@ -249,14 +251,13 @@ export default class Clusters {
} }
} }
installApplication(data) { installApplication({ id: appId, params }) {
const appId = data.id;
this.store.updateAppProperty(appId, 'requestReason', null); this.store.updateAppProperty(appId, 'requestReason', null);
this.store.updateAppProperty(appId, 'statusReason', null); this.store.updateAppProperty(appId, 'statusReason', null);
this.store.installApplication(appId); 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.notifyInstallFailure(appId);
this.store.updateAppProperty( this.store.updateAppProperty(
appId, appId,
...@@ -266,6 +267,22 @@ export default class Clusters { ...@@ -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) { upgradeApplication(data) {
const appId = data.id; const appId = data.id;
......
<script> <script>
/* eslint-disable vue/require-default-prop */ /* 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 TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue'; import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue';
import UninstallApplicationButton from './uninstall_application_button.vue'; import UninstallApplicationButton from './uninstall_application_button.vue';
import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
import { APPLICATION_STATUS } from '../constants'; import { APPLICATION_STATUS } from '../constants';
...@@ -17,6 +18,10 @@ export default { ...@@ -17,6 +18,10 @@ export default {
TimeagoTooltip, TimeagoTooltip,
GlLink, GlLink,
UninstallApplicationButton, UninstallApplicationButton,
UninstallApplicationConfirmationModal,
},
directives: {
GlModalDirective,
}, },
props: { props: {
id: { id: {
...@@ -94,6 +99,16 @@ export default { ...@@ -94,6 +99,16 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
uninstallFailed: {
type: Boolean,
required: false,
default: false,
},
uninstallSuccessful: {
type: Boolean,
required: false,
default: false,
},
updateAcknowledged: { updateAcknowledged: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -170,10 +185,21 @@ export default { ...@@ -170,10 +185,21 @@ export default {
manageButtonLabel() { manageButtonLabel() {
return s__('ClusterIntegration|Manage'); return s__('ClusterIntegration|Manage');
}, },
hasError() {
return this.installFailed || this.uninstallFailed;
},
generalErrorDescription() { generalErrorDescription() {
return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), { let errorDescription;
title: this.title,
}); 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() { versionLabel() {
if (this.updateFailed) { if (this.updateFailed) {
...@@ -214,13 +240,23 @@ export default { ...@@ -214,13 +240,23 @@ export default {
// AND new upgrade is unavailable AND version information is present. // AND new upgrade is unavailable AND version information is present.
return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version; return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version;
}, },
uninstallSuccessDescription() {
return sprintf(s__('ClusterIntegration|%{title} uninstalled successfully.'), {
title: this.title,
});
},
}, },
watch: { watch: {
updateSuccessful() { updateSuccessful(updateSuccessful) {
if (this.updateSuccessful) { if (updateSuccessful) {
this.$toast.show(this.upgradeSuccessDescription); this.$toast.show(this.upgradeSuccessDescription);
} }
}, },
uninstallSuccessful(uninstallSuccessful) {
if (uninstallSuccessful) {
this.$toast.show(this.uninstallSuccessDescription);
}
},
}, },
methods: { methods: {
installClicked() { installClicked() {
...@@ -235,6 +271,11 @@ export default { ...@@ -235,6 +271,11 @@ export default {
params: this.installApplicationRequestParams, params: this.installApplicationRequestParams,
}); });
}, },
uninstallConfirmed() {
eventHub.$emit('uninstallApplication', {
id: this.id,
});
},
}, },
}; };
</script> </script>
...@@ -271,10 +312,7 @@ export default { ...@@ -271,10 +312,7 @@ export default {
<span v-else class="js-cluster-application-title">{{ title }}</span> <span v-else class="js-cluster-application-title">{{ title }}</span>
</strong> </strong>
<slot name="description"></slot> <slot name="description"></slot>
<div <div v-if="hasError" class="cluster-application-error text-danger prepend-top-10">
v-if="installFailed || isUnknownStatus"
class="cluster-application-error text-danger prepend-top-10"
>
<p class="js-cluster-application-general-error-message append-bottom-0"> <p class="js-cluster-application-general-error-message append-bottom-0">
{{ generalErrorDescription }} {{ generalErrorDescription }}
</p> </p>
...@@ -325,9 +363,9 @@ export default { ...@@ -325,9 +363,9 @@ export default {
role="gridcell" role="gridcell"
> >
<div v-if="showManageButton" class="btn-group table-action-buttons"> <div v-if="showManageButton" class="btn-group table-action-buttons">
<a :href="manageLink" :class="{ disabled: disabled }" class="btn"> <a :href="manageLink" :class="{ disabled: disabled }" class="btn">{{
{{ manageButtonLabel }} manageButtonLabel
</a> }}</a>
</div> </div>
<div class="btn-group table-action-buttons"> <div class="btn-group table-action-buttons">
<loading-button <loading-button
...@@ -340,8 +378,15 @@ export default { ...@@ -340,8 +378,15 @@ export default {
/> />
<uninstall-application-button <uninstall-application-button
v-if="displayUninstallButton" v-if="displayUninstallButton"
v-gl-modal-directive="'uninstall-' + id"
:status="status"
class="js-cluster-application-uninstall-button" class="js-cluster-application-uninstall-button"
/> />
<uninstall-application-confirmation-modal
:application="id"
:application-title="title"
@confirm="uninstallConfirmed()"
/>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -240,6 +240,9 @@ export default { ...@@ -240,6 +240,9 @@ export default {
:request-reason="applications.helm.requestReason" :request-reason="applications.helm.requestReason"
:installed="applications.helm.installed" :installed="applications.helm.installed"
:install-failed="applications.helm.installFailed" :install-failed="applications.helm.installFailed"
:uninstallable="applications.helm.uninstallable"
:uninstall-successful="applications.helm.uninstallSuccessful"
:uninstall-failed="applications.helm.uninstallFailed"
class="rounded-top" class="rounded-top"
title-link="https://docs.helm.sh/" title-link="https://docs.helm.sh/"
> >
...@@ -269,6 +272,9 @@ export default { ...@@ -269,6 +272,9 @@ export default {
:request-reason="applications.ingress.requestReason" :request-reason="applications.ingress.requestReason"
:installed="applications.ingress.installed" :installed="applications.ingress.installed"
:install-failed="applications.ingress.installFailed" :install-failed="applications.ingress.installFailed"
:uninstallable="applications.ingress.uninstallable"
:uninstall-successful="applications.ingress.uninstallSuccessful"
:uninstall-failed="applications.ingress.uninstallFailed"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
> >
...@@ -312,9 +318,9 @@ export default { ...@@ -312,9 +318,9 @@ export default {
generated endpoint in order to access generated endpoint in order to access
your application after it has been deployed.`) your application after it has been deployed.`)
}} }}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
{{ __('More information') }} __('More information')
</a> }}</a>
</p> </p>
</div> </div>
...@@ -324,9 +330,9 @@ export default { ...@@ -324,9 +330,9 @@ export default {
the process of being assigned. Please check your Kubernetes the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}} }}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
{{ __('More information') }} __('More information')
</a> }}</a>
</p> </p>
</template> </template>
<template v-if="!ingressInstalled"> <template v-if="!ingressInstalled">
...@@ -345,6 +351,9 @@ export default { ...@@ -345,6 +351,9 @@ export default {
:installed="applications.cert_manager.installed" :installed="applications.cert_manager.installed"
:install-failed="applications.cert_manager.installFailed" :install-failed="applications.cert_manager.installFailed"
:install-application-request-params="{ email: applications.cert_manager.email }" :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" :disabled="!helmInstalled"
title-link="https://cert-manager.readthedocs.io/en/latest/#" title-link="https://cert-manager.readthedocs.io/en/latest/#"
> >
...@@ -352,9 +361,9 @@ export default { ...@@ -352,9 +361,9 @@ export default {
<div slot="description"> <div slot="description">
<p v-html="certManagerDescription"></p> <p v-html="certManagerDescription"></p>
<div class="form-group"> <div class="form-group">
<label for="cert-manager-issuer-email"> <label for="cert-manager-issuer-email">{{
{{ s__('ClusterIntegration|Issuer Email') }} s__('ClusterIntegration|Issuer Email')
</label> }}</label>
<div class="input-group"> <div class="input-group">
<input <input
v-model="applications.cert_manager.email" v-model="applications.cert_manager.email"
...@@ -391,6 +400,9 @@ export default { ...@@ -391,6 +400,9 @@ export default {
:request-reason="applications.prometheus.requestReason" :request-reason="applications.prometheus.requestReason"
:installed="applications.prometheus.installed" :installed="applications.prometheus.installed"
:install-failed="applications.prometheus.installFailed" :install-failed="applications.prometheus.installFailed"
:uninstallable="applications.prometheus.uninstallable"
:uninstall-successful="applications.prometheus.uninstallSuccessful"
:uninstall-failed="applications.prometheus.uninstallFailed"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://prometheus.io/docs/introduction/overview/" title-link="https://prometheus.io/docs/introduction/overview/"
> >
...@@ -411,6 +423,9 @@ export default { ...@@ -411,6 +423,9 @@ export default {
:install-failed="applications.runner.installFailed" :install-failed="applications.runner.installFailed"
:update-successful="applications.runner.updateSuccessful" :update-successful="applications.runner.updateSuccessful"
:update-failed="applications.runner.updateFailed" :update-failed="applications.runner.updateFailed"
:uninstallable="applications.runner.uninstallable"
:uninstall-successful="applications.runner.uninstallSuccessful"
:uninstall-failed="applications.runner.uninstallFailed"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/" title-link="https://docs.gitlab.com/runner/"
> >
...@@ -434,6 +449,9 @@ export default { ...@@ -434,6 +449,9 @@ export default {
:request-reason="applications.jupyter.requestReason" :request-reason="applications.jupyter.requestReason"
:installed="applications.jupyter.installed" :installed="applications.jupyter.installed"
:install-failed="applications.jupyter.installFailed" :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 }" :install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://jupyterhub.readthedocs.io/en/stable/" title-link="https://jupyterhub.readthedocs.io/en/stable/"
...@@ -474,9 +492,9 @@ export default { ...@@ -474,9 +492,9 @@ export default {
s__(`ClusterIntegration|Replace this with your own hostname if you want. s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`) If you do so, point hostname to Ingress IP Address from above.`)
}} }}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
{{ __('More information') }} __('More information')
</a> }}</a>
</p> </p>
</div> </div>
</template> </template>
...@@ -494,6 +512,9 @@ export default { ...@@ -494,6 +512,9 @@ export default {
:installed="applications.knative.installed" :installed="applications.knative.installed"
:install-failed="applications.knative.installFailed" :install-failed="applications.knative.installFailed"
:install-application-request-params="{ hostname: applications.knative.hostname }" :install-application-request-params="{ hostname: applications.knative.hostname }"
:uninstallable="applications.knative.uninstallable"
:uninstall-successful="applications.knative.uninstallSuccessful"
:uninstall-failed="applications.knative.uninstallFailed"
:disabled="!helmInstalled" :disabled="!helmInstalled"
v-bind="applications.knative" v-bind="applications.knative"
title-link="https://github.com/knative/docs" title-link="https://github.com/knative/docs"
...@@ -505,9 +526,9 @@ export default { ...@@ -505,9 +526,9 @@ export default {
s__(`ClusterIntegration|You must have an RBAC-enabled cluster s__(`ClusterIntegration|You must have an RBAC-enabled cluster
to install Knative.`) to install Knative.`)
}} }}
<a :href="helpPath" target="_blank" rel="noopener noreferrer"> <a :href="helpPath" target="_blank" rel="noopener noreferrer">{{
{{ __('More information') }} __('More information')
</a> }}</a>
</p> </p>
<br /> <br />
</span> </span>
...@@ -572,9 +593,9 @@ export default { ...@@ -572,9 +593,9 @@ export default {
`ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`, `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
) )
}} }}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
{{ __('More information') }} __('More information')
</a> }}</a>
</p> </p>
<p <p
......
<script> <script>
// TODO: Implement loading button component
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default { export default {
components: { components: {
LoadingButton, 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> </script>
<template> <template>
<loading-button @click="$emit('click')" /> <loading-button :label="label" :disabled="disabled" :loading="loading" />
</template> </template>
<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>
...@@ -28,16 +28,23 @@ export const APPLICATION_STATUS = { ...@@ -28,16 +28,23 @@ export const APPLICATION_STATUS = {
export const APPLICATION_INSTALLED_STATUSES = [ export const APPLICATION_INSTALLED_STATUSES = [
APPLICATION_STATUS.INSTALLED, APPLICATION_STATUS.INSTALLED,
APPLICATION_STATUS.UPDATING, APPLICATION_STATUS.UPDATING,
APPLICATION_STATUS.UNINSTALLING,
]; ];
// These are only used client-side // These are only used client-side
export const UPDATE_EVENT = 'update'; export const UPDATE_EVENT = 'update';
export const INSTALL_EVENT = 'install'; export const INSTALL_EVENT = 'install';
export const UNINSTALL_EVENT = 'uninstall';
export const HELM = 'helm';
export const INGRESS = 'ingress'; export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter'; export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative'; export const KNATIVE = 'knative';
export const RUNNER = 'runner'; export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager'; 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'; export const INGRESS_DOMAIN_SUFFIX = '.nip.io';
import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '../constants'; import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT, UNINSTALL_EVENT } from '../constants';
const { const {
NO_STATUS, NO_STATUS,
...@@ -11,6 +11,8 @@ const { ...@@ -11,6 +11,8 @@ const {
UPDATING, UPDATING,
UPDATED, UPDATED,
UPDATE_ERRORED, UPDATE_ERRORED,
UNINSTALLING,
UNINSTALL_ERRORED,
} = APPLICATION_STATUS; } = APPLICATION_STATUS;
const applicationStateMachine = { const applicationStateMachine = {
...@@ -52,6 +54,15 @@ const applicationStateMachine = { ...@@ -52,6 +54,15 @@ const applicationStateMachine = {
updateFailed: true, updateFailed: true,
}, },
}, },
[UNINSTALLING]: {
target: UNINSTALLING,
},
[UNINSTALL_ERRORED]: {
target: INSTALLED,
effects: {
uninstallFailed: true,
},
},
}, },
}, },
[NOT_INSTALLABLE]: { [NOT_INSTALLABLE]: {
...@@ -97,6 +108,13 @@ const applicationStateMachine = { ...@@ -97,6 +108,13 @@ const applicationStateMachine = {
updateSuccessful: false, updateSuccessful: false,
}, },
}, },
[UNINSTALL_EVENT]: {
target: UNINSTALLING,
effects: {
uninstallFailed: false,
uninstallSuccessful: false,
},
},
}, },
}, },
[UPDATING]: { [UPDATING]: {
...@@ -116,6 +134,22 @@ const applicationStateMachine = { ...@@ -116,6 +134,22 @@ const applicationStateMachine = {
}, },
}, },
}, },
[UNINSTALLING]: {
on: {
[INSTALLABLE]: {
target: INSTALLABLE,
effects: {
uninstallSuccessful: true,
},
},
[UNINSTALL_ERRORED]: {
target: INSTALLED,
effects: {
uninstallFailed: true,
},
},
},
},
}; };
/** /**
......
...@@ -29,6 +29,10 @@ export default class ClusterService { ...@@ -29,6 +29,10 @@ export default class ClusterService {
return axios.patch(this.appUpdateEndpointMap[appId], params); return axios.patch(this.appUpdateEndpointMap[appId], params);
} }
uninstallApplication(appId, params) {
return axios.delete(this.appInstallEndpointMap[appId], params);
}
static updateCluster(endpoint, data) { static updateCluster(endpoint, data) {
return axios.put(endpoint, data); return axios.put(endpoint, data);
} }
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
APPLICATION_STATUS, APPLICATION_STATUS,
INSTALL_EVENT, INSTALL_EVENT,
UPDATE_EVENT, UPDATE_EVENT,
UNINSTALL_EVENT,
} from '../constants'; } from '../constants';
import transitionApplicationState from '../services/application_state_machine'; import transitionApplicationState from '../services/application_state_machine';
...@@ -21,6 +22,9 @@ const applicationInitialState = { ...@@ -21,6 +22,9 @@ const applicationInitialState = {
requestReason: null, requestReason: null,
installed: false, installed: false,
installFailed: false, installFailed: false,
uninstallable: false,
uninstallFailed: false,
uninstallSuccessful: false,
}; };
export default class ClusterStore { export default class ClusterStore {
...@@ -116,6 +120,14 @@ export default class ClusterStore { ...@@ -116,6 +120,14 @@ export default class ClusterStore {
this.handleApplicationEvent(appId, APPLICATION_STATUS.UPDATE_ERRORED); 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) { handleApplicationEvent(appId, event) {
const currentAppState = this.state.applications[appId]; const currentAppState = this.state.applications[appId];
...@@ -141,6 +153,7 @@ export default class ClusterStore { ...@@ -141,6 +153,7 @@ export default class ClusterStore {
status_reason: statusReason, status_reason: statusReason,
version, version,
update_available: upgradeAvailable, update_available: upgradeAvailable,
can_uninstall: uninstallable,
} = serverAppEntry; } = serverAppEntry;
const currentApplicationState = this.state.applications[appId] || {}; const currentApplicationState = this.state.applications[appId] || {};
const nextApplicationState = transitionApplicationState(currentApplicationState, status); const nextApplicationState = transitionApplicationState(currentApplicationState, status);
...@@ -150,8 +163,7 @@ export default class ClusterStore { ...@@ -150,8 +163,7 @@ export default class ClusterStore {
...nextApplicationState, ...nextApplicationState,
statusReason, statusReason,
installed: isApplicationInstalled(nextApplicationState.status), installed: isApplicationInstalled(nextApplicationState.status),
// Make sure uninstallable is always false until this feature is unflagged uninstallable,
uninstallable: false,
}; };
if (appId === INGRESS) { if (appId === INGRESS) {
......
...@@ -34,10 +34,10 @@ ...@@ -34,10 +34,10 @@
.modal-body { .modal-body {
background-color: $modal-body-bg; background-color: $modal-body-bg;
line-height: $line-height-base; line-height: $line-height-base;
min-height: $modal-body-height;
position: relative; position: relative;
padding: #{3 * $grid-size} #{2 * $grid-size}; padding: #{3 * $grid-size} #{2 * $grid-size};
text-align: left; text-align: left;
white-space: normal;
.form-actions { .form-actions {
margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size}; margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size};
......
---
title: Implement UI for uninstalling Cluster’s managed apps
merge_request: 27559
author:
type: added
...@@ -1984,6 +1984,9 @@ msgstr "" ...@@ -1984,6 +1984,9 @@ msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|%{title} uninstalled successfully."
msgstr ""
msgid "ClusterIntegration|%{title} upgraded successfully." msgid "ClusterIntegration|%{title} upgraded successfully."
msgstr "" msgstr ""
...@@ -2011,6 +2014,9 @@ msgstr "" ...@@ -2011,6 +2014,9 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration" msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr "" msgstr ""
msgid "ClusterIntegration|All data will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|Alternatively" msgid "ClusterIntegration|Alternatively"
msgstr "" msgstr ""
...@@ -2026,6 +2032,9 @@ msgstr "" ...@@ -2026,6 +2032,9 @@ msgstr ""
msgid "ClusterIntegration|An error occurred while trying to fetch zone machine types: %{error}" msgid "ClusterIntegration|An error occurred while trying to fetch zone machine types: %{error}"
msgstr "" msgstr ""
msgid "ClusterIntegration|Any running pipelines will be canceled."
msgstr ""
msgid "ClusterIntegration|Applications" msgid "ClusterIntegration|Applications"
msgstr "" msgstr ""
...@@ -2317,6 +2326,9 @@ msgstr "" ...@@ -2317,6 +2326,9 @@ msgstr ""
msgid "ClusterIntegration|Request to begin installing failed" msgid "ClusterIntegration|Request to begin installing failed"
msgstr "" msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
msgid "ClusterIntegration|Retry update" msgid "ClusterIntegration|Retry update"
msgstr "" msgstr ""
...@@ -2371,6 +2383,9 @@ msgstr "" ...@@ -2371,6 +2383,9 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while installing %{title}" msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr "" 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." 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 "" msgstr ""
...@@ -2380,6 +2395,15 @@ msgstr "" ...@@ -2380,6 +2395,15 @@ msgstr ""
msgid "ClusterIntegration|The URL used to access the Kubernetes API." msgid "ClusterIntegration|The URL used to access the Kubernetes API."
msgstr "" 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." 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 "" msgstr ""
...@@ -2395,6 +2419,9 @@ msgstr "" ...@@ -2395,6 +2419,9 @@ msgstr ""
msgid "ClusterIntegration|Toggle Kubernetes cluster" msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|Uninstall %{appTitle}"
msgstr ""
msgid "ClusterIntegration|Update failed. Please check the logs and try again." msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr "" msgstr ""
...@@ -2422,6 +2449,9 @@ 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." 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 "" 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" msgid "ClusterIntegration|You must first install Helm Tiller before installing the applications below"
msgstr "" msgstr ""
......
import Clusters from '~/clusters/clusters_bundle'; 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 MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { loadHTMLFixture } from 'helpers/fixtures'; import { loadHTMLFixture } from 'helpers/fixtures';
import { setTestTimeout } from 'helpers/timeout'; import { setTestTimeout } from 'helpers/timeout';
import $ from 'jquery'; import $ from 'jquery';
const { INSTALLING, INSTALLABLE, INSTALLED } = APPLICATION_STATUS; const { INSTALLING, INSTALLABLE, INSTALLED, UNINSTALLING } = APPLICATION_STATUS;
describe('Clusters', () => { describe('Clusters', () => {
setTestTimeout(1000); setTestTimeout(1000);
...@@ -212,73 +212,61 @@ describe('Clusters', () => { ...@@ -212,73 +212,61 @@ describe('Clusters', () => {
}); });
describe('installApplication', () => { describe('installApplication', () => {
it('tries to install helm', () => { it.each(APPLICATIONS)('tries to install %s', applicationId => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); 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[applicationId].status).toEqual(INSTALLING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null); expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined); expect(cluster.service.installApplication).toHaveBeenCalledWith(applicationId, undefined);
}); });
it('tries to install ingress', () => { it('sets error request status when the request fails', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); jest
.spyOn(cluster.service, 'installApplication')
cluster.store.state.applications.ingress.status = INSTALLABLE; .mockRejectedValueOnce(new Error('STUBBED ERROR'));
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('tries to install runner', () => { cluster.store.state.applications.helm.status = INSTALLABLE;
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
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.helm.requestReason).toBeDefined();
expect(cluster.store.state.applications.runner.requestReason).toEqual(null); });
expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
}); });
});
it('tries to install jupyter', () => { describe('uninstallApplication', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); it.each(APPLICATIONS)('tries to uninstall %s', applicationId => {
jest.spyOn(cluster.service, 'uninstallApplication').mockResolvedValueOnce();
cluster.installApplication({ cluster.store.state.applications[applicationId].status = INSTALLED;
id: 'jupyter',
params: { hostname: cluster.store.state.applications.jupyter.hostname },
});
cluster.store.state.applications.jupyter.status = INSTALLABLE; cluster.uninstallApplication({ id: applicationId });
expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', { expect(cluster.store.state.applications[applicationId].status).toEqual(UNINSTALLING);
hostname: cluster.store.state.applications.jupyter.hostname, 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 jest
.spyOn(cluster.service, 'installApplication') .spyOn(cluster.service, 'uninstallApplication')
.mockRejectedValueOnce(new Error('STUBBED ERROR')); .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' }); const promise = cluster.uninstallApplication({ 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();
return promise.then(() => { return promise.then(() => {
expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE); expect(cluster.store.state.applications.helm.status).toEqual(INSTALLED);
expect(cluster.store.state.applications.helm.installFailed).toBe(true); expect(cluster.store.state.applications.helm.uninstallFailed).toBe(true);
expect(cluster.store.state.applications.helm.requestReason).toBeDefined(); expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import eventHub from '~/clusters/event_hub'; import eventHub from '~/clusters/event_hub';
import { APPLICATION_STATUS } from '~/clusters/constants'; import { APPLICATION_STATUS } from '~/clusters/constants';
import applicationRow from '~/clusters/components/application_row.vue'; 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 mountComponent from 'helpers/vue_mount_component_helper';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data'; import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
...@@ -194,11 +197,52 @@ describe('Application Row', () => { ...@@ -194,11 +197,52 @@ describe('Application Row', () => {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
installed: true, installed: true,
uninstallable: true, uninstallable: true,
status: APPLICATION_STATUS.NOT_INSTALLABLE,
}); });
const uninstallButton = vm.$el.querySelector('.js-cluster-application-uninstall-button'); const uninstallButton = vm.$el.querySelector('.js-cluster-application-uninstall-button');
expect(uninstallButton).toBeTruthy(); 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', () => { describe('Upgrade button', () => {
...@@ -304,7 +348,7 @@ describe('Application Row', () => { ...@@ -304,7 +348,7 @@ describe('Application Row', () => {
vm.$toast = { show: jest.fn() }; vm.$toast = { show: jest.fn() };
vm.updateSuccessful = true; vm.updateSuccessful = true;
vm.$nextTick(() => { return vm.$nextTick(() => {
expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner upgraded successfully.'); expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner upgraded successfully.');
}); });
}); });
...@@ -360,60 +404,88 @@ describe('Application Row', () => { ...@@ -360,60 +404,88 @@ describe('Application Row', () => {
}); });
describe('Error block', () => { describe('Error block', () => {
it('does not show error block when there is no error', () => { describe('when nothing fails', () => {
vm = mountComponent(ApplicationRow, { it('does not show error block', () => {
...DEFAULT_APPLICATION_STATE, vm = mountComponent(ApplicationRow, {
status: null, ...DEFAULT_APPLICATION_STATE,
}); });
const generalErrorMessage = vm.$el.querySelector( const generalErrorMessage = vm.$el.querySelector(
'.js-cluster-application-general-error-message', '.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'; const statusReason = 'We broke it 0.0';
vm = mountComponent(ApplicationRow, { const requestReason = 'We broke the request 0.0';
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.ERROR, beforeEach(() => {
statusReason, vm = mountComponent(ApplicationRow, {
installFailed: true, ...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( it('shows status reason if it is available', () => {
`Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`, 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', () => { describe('when uninstall fails', () => {
const requestReason = 'We broke thre request 0.0'; beforeEach(() => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE, status: APPLICATION_STATUS.ERROR,
installFailed: true, uninstallFailed: true,
requestReason, });
}); });
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( it('shows a general message indicating the uninstalling failed', () => {
`Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`, 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}`,
);
});
}); });
}); });
}); });
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 });
});
});
});
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.`,
);
});
});
import transitionApplicationState from '~/clusters/services/application_state_machine'; 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 { const {
NO_STATUS, NO_STATUS,
...@@ -12,6 +17,8 @@ const { ...@@ -12,6 +17,8 @@ const {
UPDATING, UPDATING,
UPDATED, UPDATED,
UPDATE_ERRORED, UPDATE_ERRORED,
UNINSTALLING,
UNINSTALL_ERRORED,
} = APPLICATION_STATUS; } = APPLICATION_STATUS;
const NO_EFFECTS = 'no effects'; const NO_EFFECTS = 'no effects';
...@@ -21,16 +28,18 @@ describe('applicationStateMachine', () => { ...@@ -21,16 +28,18 @@ describe('applicationStateMachine', () => {
describe(`current state is ${NO_STATUS}`, () => { describe(`current state is ${NO_STATUS}`, () => {
it.each` it.each`
expectedState | event | effects expectedState | event | effects
${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS} ${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS}
${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS} ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS} ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS} ${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS}
${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS} ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }} ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
${UPDATING} | ${UPDATING} | ${NO_EFFECTS} ${UPDATING} | ${UPDATING} | ${NO_EFFECTS}
${INSTALLED} | ${UPDATED} | ${NO_EFFECTS} ${INSTALLED} | ${UPDATED} | ${NO_EFFECTS}
${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }} ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
${UNINSTALLING} | ${UNINSTALLING} | ${NO_EFFECTS}
${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, data => { `(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data; const { expectedState, event, effects } = data;
const currentAppState = { const currentAppState = {
...@@ -99,8 +108,9 @@ describe('applicationStateMachine', () => { ...@@ -99,8 +108,9 @@ describe('applicationStateMachine', () => {
describe(`current state is ${INSTALLED}`, () => { describe(`current state is ${INSTALLED}`, () => {
it.each` it.each`
expectedState | event | effects expectedState | event | effects
${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }} ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
`(`transitions to $expectedState on $event event and applies $effects`, data => { `(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data; const { expectedState, event, effects } = data;
const currentAppState = { const currentAppState = {
...@@ -131,4 +141,22 @@ describe('applicationStateMachine', () => { ...@@ -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,
});
});
});
}); });
...@@ -11,6 +11,7 @@ const CLUSTERS_MOCK_DATA = { ...@@ -11,6 +11,7 @@ const CLUSTERS_MOCK_DATA = {
name: 'helm', name: 'helm',
status: APPLICATION_STATUS.INSTALLABLE, status: APPLICATION_STATUS.INSTALLABLE,
status_reason: null, status_reason: null,
can_uninstall: false,
}, },
{ {
name: 'ingress', name: 'ingress',
...@@ -18,32 +19,38 @@ const CLUSTERS_MOCK_DATA = { ...@@ -18,32 +19,38 @@ const CLUSTERS_MOCK_DATA = {
status_reason: 'Cannot connect', status_reason: 'Cannot connect',
external_ip: null, external_ip: null,
external_hostname: null, external_hostname: null,
can_uninstall: false,
}, },
{ {
name: 'runner', name: 'runner',
status: APPLICATION_STATUS.INSTALLING, status: APPLICATION_STATUS.INSTALLING,
status_reason: null, status_reason: null,
can_uninstall: false,
}, },
{ {
name: 'prometheus', name: 'prometheus',
status: APPLICATION_STATUS.ERROR, status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect', status_reason: 'Cannot connect',
can_uninstall: false,
}, },
{ {
name: 'jupyter', name: 'jupyter',
status: APPLICATION_STATUS.INSTALLING, status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect', status_reason: 'Cannot connect',
can_uninstall: false,
}, },
{ {
name: 'knative', name: 'knative',
status: APPLICATION_STATUS.INSTALLING, status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect', status_reason: 'Cannot connect',
can_uninstall: false,
}, },
{ {
name: 'cert_manager', name: 'cert_manager',
status: APPLICATION_STATUS.ERROR, status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect', status_reason: 'Cannot connect',
email: 'test@example.com', email: 'test@example.com',
can_uninstall: false,
}, },
], ],
}, },
......
...@@ -63,6 +63,8 @@ describe('Clusters Store', () => { ...@@ -63,6 +63,8 @@ describe('Clusters Store', () => {
installed: false, installed: false,
installFailed: false, installFailed: false,
uninstallable: false, uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
}, },
ingress: { ingress: {
title: 'Ingress', title: 'Ingress',
...@@ -74,6 +76,8 @@ describe('Clusters Store', () => { ...@@ -74,6 +76,8 @@ describe('Clusters Store', () => {
installed: false, installed: false,
installFailed: true, installFailed: true,
uninstallable: false, uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
}, },
runner: { runner: {
title: 'GitLab Runner', title: 'GitLab Runner',
...@@ -89,6 +93,8 @@ describe('Clusters Store', () => { ...@@ -89,6 +93,8 @@ describe('Clusters Store', () => {
updateFailed: false, updateFailed: false,
updateSuccessful: false, updateSuccessful: false,
uninstallable: false, uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
}, },
prometheus: { prometheus: {
title: 'Prometheus', title: 'Prometheus',
...@@ -98,6 +104,8 @@ describe('Clusters Store', () => { ...@@ -98,6 +104,8 @@ describe('Clusters Store', () => {
installed: false, installed: false,
installFailed: true, installFailed: true,
uninstallable: false, uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
}, },
jupyter: { jupyter: {
title: 'JupyterHub', title: 'JupyterHub',
...@@ -108,6 +116,8 @@ describe('Clusters Store', () => { ...@@ -108,6 +116,8 @@ describe('Clusters Store', () => {
installed: false, installed: false,
installFailed: false, installFailed: false,
uninstallable: false, uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
}, },
knative: { knative: {
title: 'Knative', title: 'Knative',
...@@ -121,6 +131,8 @@ describe('Clusters Store', () => { ...@@ -121,6 +131,8 @@ describe('Clusters Store', () => {
installed: false, installed: false,
installFailed: false, installFailed: false,
uninstallable: false, uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
}, },
cert_manager: { cert_manager: {
title: 'Cert-Manager', title: 'Cert-Manager',
...@@ -131,6 +143,8 @@ describe('Clusters Store', () => { ...@@ -131,6 +143,8 @@ describe('Clusters Store', () => {
email: mockResponseData.applications[6].email, email: mockResponseData.applications[6].email,
installed: false, installed: false,
uninstallable: false, uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
}, },
}, },
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment