Commit 690382dd authored by Enrique Alcántara's avatar Enrique Alcántara Committed by Phil Hughes

Use a FSM to determine application next state

- Separate cluster application UI state from server-side app status
- Use a state machine to determine cluster application next state
- Instead of using two variables to keep track of when an app
is installing or updating, just use the app status property and control
server-side and user events using the FSM service.
parent 336a0a87
...@@ -7,15 +7,7 @@ import Flash from '../flash'; ...@@ -7,15 +7,7 @@ import Flash from '../flash';
import Poll from '../lib/utils/poll'; import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels'; import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub'; import eventHub from './event_hub';
import { import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX } from './constants';
APPLICATION_STATUS,
REQUEST_SUBMITTED,
REQUEST_FAILURE,
UPGRADE_REQUESTED,
UPGRADE_REQUEST_FAILURE,
INGRESS,
INGRESS_DOMAIN_SUFFIX,
} from './constants';
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store'; import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue'; import Applications from './components/applications.vue';
...@@ -137,7 +129,7 @@ export default class Clusters { ...@@ -137,7 +129,7 @@ export default class Clusters {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication); eventHub.$on('installApplication', this.installApplication);
eventHub.$on('upgradeApplication', data => this.upgradeApplication(data)); eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
eventHub.$on('upgradeFailed', appId => this.upgradeFailed(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));
} }
...@@ -146,7 +138,7 @@ export default class Clusters { ...@@ -146,7 +138,7 @@ export default class Clusters {
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication); eventHub.$off('installApplication', this.installApplication);
eventHub.$off('upgradeApplication', this.upgradeApplication); eventHub.$off('upgradeApplication', this.upgradeApplication);
eventHub.$off('upgradeFailed', this.upgradeFailed); eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
eventHub.$off('saveKnativeDomain'); eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname'); eventHub.$off('setKnativeHostname');
} }
...@@ -259,12 +251,13 @@ export default class Clusters { ...@@ -259,12 +251,13 @@ export default class Clusters {
installApplication(data) { installApplication(data) {
const appId = data.id; const appId = data.id;
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUBMITTED);
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);
return this.service.installApplication(appId, data.params).catch(() => { return this.service.installApplication(appId, data.params).catch(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE); this.store.notifyInstallFailure(appId);
this.store.updateAppProperty( this.store.updateAppProperty(
appId, appId,
'requestReason', 'requestReason',
...@@ -275,13 +268,15 @@ export default class Clusters { ...@@ -275,13 +268,15 @@ export default class Clusters {
upgradeApplication(data) { upgradeApplication(data) {
const appId = data.id; const appId = data.id;
this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUESTED);
this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING); this.store.updateApplication(appId);
this.service.installApplication(appId, data.params).catch(() => this.upgradeFailed(appId)); this.service.installApplication(appId, data.params).catch(() => {
this.store.notifyUpdateFailure(appId);
});
} }
upgradeFailed(appId) { dismissUpgradeSuccess(appId) {
this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUEST_FAILURE); this.store.acknowledgeSuccessfulUpdate(appId);
} }
toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) { toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) {
......
...@@ -8,12 +8,7 @@ import identicon from '../../vue_shared/components/identicon.vue'; ...@@ -8,12 +8,7 @@ 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 { import { APPLICATION_STATUS } from '../constants';
APPLICATION_STATUS,
REQUEST_SUBMITTED,
REQUEST_FAILURE,
UPGRADE_REQUESTED,
} from '../constants';
export default { export default {
components: { components: {
...@@ -63,10 +58,6 @@ export default { ...@@ -63,10 +58,6 @@ export default {
type: String, type: String,
required: false, required: false,
}, },
requestStatus: {
type: String,
required: false,
},
requestReason: { requestReason: {
type: String, type: String,
required: false, required: false,
...@@ -76,6 +67,11 @@ export default { ...@@ -76,6 +67,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
installFailed: {
type: Boolean,
required: false,
default: false,
},
version: { version: {
type: String, type: String,
required: false, required: false,
...@@ -88,6 +84,21 @@ export default { ...@@ -88,6 +84,21 @@ export default {
type: Boolean, type: Boolean,
required: false, required: false,
}, },
updateSuccessful: {
type: Boolean,
required: false,
default: false,
},
updateFailed: {
type: Boolean,
required: false,
default: false,
},
updateAcknowledged: {
type: Boolean,
required: false,
default: true,
},
installApplicationRequestParams: { installApplicationRequestParams: {
type: Object, type: Object,
required: false, required: false,
...@@ -102,21 +113,12 @@ export default { ...@@ -102,21 +113,12 @@ export default {
return Object.values(APPLICATION_STATUS).includes(this.status); return Object.values(APPLICATION_STATUS).includes(this.status);
}, },
isInstalling() { isInstalling() {
return ( return this.status === APPLICATION_STATUS.INSTALLING;
this.status === APPLICATION_STATUS.SCHEDULED ||
this.status === APPLICATION_STATUS.INSTALLING ||
(this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.installed)
);
}, },
canInstall() { canInstall() {
if (this.isInstalling) {
return false;
}
return ( return (
this.status === APPLICATION_STATUS.NOT_INSTALLABLE || this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.status === APPLICATION_STATUS.INSTALLABLE || this.status === APPLICATION_STATUS.INSTALLABLE ||
this.status === APPLICATION_STATUS.ERROR ||
this.isUnknownStatus this.isUnknownStatus
); );
}, },
...@@ -137,7 +139,7 @@ export default { ...@@ -137,7 +139,7 @@ export default {
return !this.installed || !this.uninstallable; return !this.installed || !this.uninstallable;
}, },
installButtonLoading() { installButtonLoading() {
return !this.status || this.status === APPLICATION_STATUS.SCHEDULED || this.isInstalling; return !this.status || this.isInstalling;
}, },
installButtonDisabled() { installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but // Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
...@@ -168,19 +170,13 @@ export default { ...@@ -168,19 +170,13 @@ export default {
manageButtonLabel() { manageButtonLabel() {
return s__('ClusterIntegration|Manage'); return s__('ClusterIntegration|Manage');
}, },
hasError() {
return (
!this.isInstalling &&
(this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE)
);
},
generalErrorDescription() { generalErrorDescription() {
return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), { return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
title: this.title, title: this.title,
}); });
}, },
versionLabel() { versionLabel() {
if (this.upgradeFailed) { if (this.updateFailed) {
return s__('ClusterIntegration|Upgrade failed'); return s__('ClusterIntegration|Upgrade failed');
} else if (this.isUpgrading) { } else if (this.isUpgrading) {
return s__('ClusterIntegration|Upgrading'); return s__('ClusterIntegration|Upgrading');
...@@ -188,19 +184,6 @@ export default { ...@@ -188,19 +184,6 @@ export default {
return s__('ClusterIntegration|Upgraded'); return s__('ClusterIntegration|Upgraded');
}, },
upgradeRequested() {
return this.requestStatus === UPGRADE_REQUESTED;
},
upgradeSuccessful() {
return this.status === APPLICATION_STATUS.UPDATED;
},
upgradeFailed() {
if (this.isUpgrading) {
return false;
}
return this.status === APPLICATION_STATUS.UPDATE_ERRORED;
},
upgradeFailureDescription() { upgradeFailureDescription() {
return s__('ClusterIntegration|Update failed. Please check the logs and try again.'); return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
}, },
...@@ -211,11 +194,11 @@ export default { ...@@ -211,11 +194,11 @@ export default {
}, },
upgradeButtonLabel() { upgradeButtonLabel() {
let label; let label;
if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) { if (this.upgradeAvailable && !this.updateFailed && !this.isUpgrading) {
label = s__('ClusterIntegration|Upgrade'); label = s__('ClusterIntegration|Upgrade');
} else if (this.isUpgrading) { } else if (this.isUpgrading) {
label = s__('ClusterIntegration|Updating'); label = s__('ClusterIntegration|Updating');
} else if (this.upgradeFailed) { } else if (this.updateFailed) {
label = s__('ClusterIntegration|Retry update'); label = s__('ClusterIntegration|Retry update');
} }
...@@ -223,25 +206,18 @@ export default { ...@@ -223,25 +206,18 @@ export default {
}, },
isUpgrading() { isUpgrading() {
// Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend // Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
return ( return this.status === APPLICATION_STATUS.UPDATING;
this.status === APPLICATION_STATUS.UPDATING ||
(this.upgradeRequested && !this.upgradeSuccessful)
);
}, },
shouldShowUpgradeDetails() { shouldShowUpgradeDetails() {
// This method only returns true when; // This method only returns true when;
// Upgrade was successful OR Upgrade failed // Upgrade was successful OR Upgrade failed
// AND new upgrade is unavailable AND version information is present. // AND new upgrade is unavailable AND version information is present.
return ( return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version;
(this.upgradeSuccessful || this.upgradeFailed) && !this.upgradeAvailable && this.version
);
}, },
}, },
watch: { watch: {
status() { updateSuccessful() {
if (this.status === APPLICATION_STATUS.UPDATE_ERRORED) { if (this.updateSuccessful) {
eventHub.$emit('upgradeFailed', this.id);
} else if (this.upgradeRequested && this.upgradeSuccessful) {
this.$toast.show(this.upgradeSuccessDescription); this.$toast.show(this.upgradeSuccessDescription);
} }
}, },
...@@ -296,7 +272,7 @@ export default { ...@@ -296,7 +272,7 @@ export default {
</strong> </strong>
<slot name="description"></slot> <slot name="description"></slot>
<div <div
v-if="hasError || isUnknownStatus" v-if="installFailed || isUnknownStatus"
class="cluster-application-error text-danger prepend-top-10" 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">
...@@ -317,10 +293,10 @@ export default { ...@@ -317,10 +293,10 @@ export default {
class="form-text text-muted label p-0 js-cluster-application-upgrade-details" class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
> >
{{ versionLabel }} {{ versionLabel }}
<span v-if="upgradeSuccessful">to</span> <span v-if="updateSuccessful">to</span>
<gl-link <gl-link
v-if="upgradeSuccessful" v-if="updateSuccessful"
:href="chartRepo" :href="chartRepo"
target="_blank" target="_blank"
class="js-cluster-application-upgrade-version" class="js-cluster-application-upgrade-version"
...@@ -329,13 +305,13 @@ export default { ...@@ -329,13 +305,13 @@ export default {
</div> </div>
<div <div
v-if="upgradeFailed && !isUpgrading" v-if="updateFailed && !isUpgrading"
class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message" class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
> >
{{ upgradeFailureDescription }} {{ upgradeFailureDescription }}
</div> </div>
<loading-button <loading-button
v-if="upgradeAvailable || upgradeFailed || isUpgrading" v-if="upgradeAvailable || updateFailed || isUpgrading"
class="btn btn-primary js-cluster-application-upgrade-button mt-2" class="btn btn-primary js-cluster-application-upgrade-button mt-2"
:loading="isUpgrading" :loading="isUpgrading"
:disabled="isUpgrading" :disabled="isUpgrading"
...@@ -349,9 +325,9 @@ export default { ...@@ -349,9 +325,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
......
...@@ -224,9 +224,9 @@ export default { ...@@ -224,9 +224,9 @@ export default {
<p class="append-bottom-0"> <p class="append-bottom-0">
{{ {{
s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster. s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.
Helm Tiller is required to install any of the following applications.`) Helm Tiller is required to install any of the following applications.`)
}} }}
<a :href="helpPath"> {{ __('More information') }} </a> <a :href="helpPath">{{ __('More information') }}</a>
</p> </p>
<div class="cluster-application-list prepend-top-10"> <div class="cluster-application-list prepend-top-10">
...@@ -239,15 +239,16 @@ export default { ...@@ -239,15 +239,16 @@ export default {
:request-status="applications.helm.requestStatus" :request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason" :request-reason="applications.helm.requestReason"
:installed="applications.helm.installed" :installed="applications.helm.installed"
:install-failed="applications.helm.installFailed"
class="rounded-top" class="rounded-top"
title-link="https://docs.helm.sh/" title-link="https://docs.helm.sh/"
> >
<div slot="description"> <div slot="description">
{{ {{
s__(`ClusterIntegration|Helm streamlines installing s__(`ClusterIntegration|Helm streamlines installing
and managing Kubernetes applications. and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster, Tiller runs inside of your Kubernetes Cluster,
and manages releases of your charts.`) and manages releases of your charts.`)
}} }}
</div> </div>
</application-row> </application-row>
...@@ -255,7 +256,7 @@ export default { ...@@ -255,7 +256,7 @@ export default {
<div class="svg-container" v-html="helmInstallIllustration"></div> <div class="svg-container" v-html="helmInstallIllustration"></div>
{{ {{
s__(`ClusterIntegration|You must first install Helm Tiller before s__(`ClusterIntegration|You must first install Helm Tiller before
installing the applications below`) installing the applications below`)
}} }}
</div> </div>
<application-row <application-row
...@@ -267,6 +268,7 @@ export default { ...@@ -267,6 +268,7 @@ export default {
:request-status="applications.ingress.requestStatus" :request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason" :request-reason="applications.ingress.requestReason"
:installed="applications.ingress.installed" :installed="applications.ingress.installed"
:install-failed="applications.ingress.installFailed"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
> >
...@@ -274,16 +276,14 @@ export default { ...@@ -274,16 +276,14 @@ export default {
<p> <p>
{{ {{
s__(`ClusterIntegration|Ingress gives you a way to route s__(`ClusterIntegration|Ingress gives you a way to route
requests to services based on the request host or path, requests to services based on the request host or path,
centralizing a number of services into a single entrypoint.`) centralizing a number of services into a single entrypoint.`)
}} }}
</p> </p>
<template v-if="ingressInstalled"> <template v-if="ingressInstalled">
<div class="form-group"> <div class="form-group">
<label for="ingress-endpoint"> <label for="ingress-endpoint">{{ s__('ClusterIntegration|Ingress Endpoint') }}</label>
{{ s__('ClusterIntegration|Ingress Endpoint') }}
</label>
<div v-if="ingressExternalEndpoint" class="input-group"> <div v-if="ingressExternalEndpoint" class="input-group">
<input <input
id="ingress-endpoint" id="ingress-endpoint"
...@@ -309,8 +309,8 @@ export default { ...@@ -309,8 +309,8 @@ export default {
<p class="form-text text-muted"> <p class="form-text text-muted">
{{ {{
s__(`ClusterIntegration|Point a wildcard DNS to this s__(`ClusterIntegration|Point a wildcard DNS to this
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') }}
...@@ -321,10 +321,9 @@ export default { ...@@ -321,10 +321,9 @@ export default {
<p v-if="!ingressExternalEndpoint" class="settings-message js-no-endpoint-message"> <p v-if="!ingressExternalEndpoint" class="settings-message js-no-endpoint-message">
{{ {{
s__(`ClusterIntegration|The endpoint is in s__(`ClusterIntegration|The endpoint is in
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>
...@@ -344,6 +343,7 @@ export default { ...@@ -344,6 +343,7 @@ export default {
:request-status="applications.cert_manager.requestStatus" :request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason" :request-reason="applications.cert_manager.requestReason"
:installed="applications.cert_manager.installed" :installed="applications.cert_manager.installed"
:install-failed="applications.cert_manager.installFailed"
:install-application-request-params="{ email: applications.cert_manager.email }" :install-application-request-params="{ email: applications.cert_manager.email }"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://cert-manager.readthedocs.io/en/latest/#" title-link="https://cert-manager.readthedocs.io/en/latest/#"
...@@ -366,15 +366,14 @@ export default { ...@@ -366,15 +366,14 @@ export default {
<p class="form-text text-muted"> <p class="form-text text-muted">
{{ {{
s__(`ClusterIntegration|Issuers represent a certificate authority. s__(`ClusterIntegration|Issuers represent a certificate authority.
You must provide an email address for your Issuer. `) You must provide an email address for your Issuer. `)
}} }}
<a <a
href="http://docs.cert-manager.io/en/latest/reference/issuers.html?highlight=email" href="http://docs.cert-manager.io/en/latest/reference/issuers.html?highlight=email"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
>{{ __('More information') }}</a
> >
{{ __('More information') }}
</a>
</p> </p>
</div> </div>
</div> </div>
...@@ -391,6 +390,7 @@ export default { ...@@ -391,6 +390,7 @@ export default {
:request-status="applications.prometheus.requestStatus" :request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason" :request-reason="applications.prometheus.requestReason"
:installed="applications.prometheus.installed" :installed="applications.prometheus.installed"
:install-failed="applications.prometheus.installFailed"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://prometheus.io/docs/introduction/overview/" title-link="https://prometheus.io/docs/introduction/overview/"
> >
...@@ -408,15 +408,18 @@ export default { ...@@ -408,15 +408,18 @@ export default {
:chart-repo="applications.runner.chartRepo" :chart-repo="applications.runner.chartRepo"
:upgrade-available="applications.runner.upgradeAvailable" :upgrade-available="applications.runner.upgradeAvailable"
:installed="applications.runner.installed" :installed="applications.runner.installed"
:install-failed="applications.runner.installFailed"
:update-successful="applications.runner.updateSuccessful"
:update-failed="applications.runner.updateFailed"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/" title-link="https://docs.gitlab.com/runner/"
> >
<div slot="description"> <div slot="description">
{{ {{
s__(`ClusterIntegration|GitLab Runner connects to the s__(`ClusterIntegration|GitLab Runner connects to the
repository and executes CI/CD jobs, repository and executes CI/CD jobs,
pushing results back and deploying pushing results back and deploying
applications to production.`) applications to production.`)
}} }}
</div> </div>
</application-row> </application-row>
...@@ -430,6 +433,7 @@ export default { ...@@ -430,6 +433,7 @@ export default {
:request-status="applications.jupyter.requestStatus" :request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason" :request-reason="applications.jupyter.requestReason"
:installed="applications.jupyter.installed" :installed="applications.jupyter.installed"
:install-failed="applications.jupyter.installFailed"
: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/"
...@@ -438,18 +442,16 @@ export default { ...@@ -438,18 +442,16 @@ export default {
<p> <p>
{{ {{
s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns, s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns,
manages, and proxies multiple instances of the single-user manages, and proxies multiple instances of the single-user
Jupyter notebook server. JupyterHub can be used to serve Jupyter notebook server. JupyterHub can be used to serve
notebooks to a class of students, a corporate data science group, notebooks to a class of students, a corporate data science group,
or a scientific research group.`) or a scientific research group.`)
}} }}
</p> </p>
<template v-if="ingressExternalEndpoint"> <template v-if="ingressExternalEndpoint">
<div class="form-group"> <div class="form-group">
<label for="jupyter-hostname"> <label for="jupyter-hostname">{{ s__('ClusterIntegration|Jupyter Hostname') }}</label>
{{ s__('ClusterIntegration|Jupyter Hostname') }}
</label>
<div class="input-group"> <div class="input-group">
<input <input
...@@ -470,7 +472,7 @@ export default { ...@@ -470,7 +472,7 @@ export default {
<p v-if="ingressInstalled" class="form-text text-muted"> <p v-if="ingressInstalled" class="form-text text-muted">
{{ {{
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') }}
...@@ -490,8 +492,10 @@ export default { ...@@ -490,8 +492,10 @@ export default {
:request-status="applications.knative.requestStatus" :request-status="applications.knative.requestStatus"
:request-reason="applications.knative.requestReason" :request-reason="applications.knative.requestReason"
:installed="applications.knative.installed" :installed="applications.knative.installed"
:install-failed="applications.knative.installFailed"
:install-application-request-params="{ hostname: applications.knative.hostname }" :install-application-request-params="{ hostname: applications.knative.hostname }"
:disabled="!helmInstalled" :disabled="!helmInstalled"
v-bind="applications.knative"
title-link="https://github.com/knative/docs" title-link="https://github.com/knative/docs"
> >
<div slot="description"> <div slot="description">
...@@ -499,7 +503,7 @@ export default { ...@@ -499,7 +503,7 @@ export default {
<p v-if="!rbac" class="rbac-notice bs-callout bs-callout-info append-bottom-0"> <p v-if="!rbac" class="rbac-notice bs-callout bs-callout-info append-bottom-0">
{{ {{
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') }}
...@@ -510,9 +514,9 @@ export default { ...@@ -510,9 +514,9 @@ export default {
<p> <p>
{{ {{
s__(`ClusterIntegration|Knative extends Kubernetes to provide s__(`ClusterIntegration|Knative extends Kubernetes to provide
a set of middleware components that are essential to build modern, a set of middleware components that are essential to build modern,
source-centric, and container-based applications that can run source-centric, and container-based applications that can run
anywhere: on premises, in the cloud, or even in a third-party data center.`) anywhere: on premises, in the cloud, or even in a third-party data center.`)
}} }}
</p> </p>
...@@ -523,9 +527,7 @@ export default { ...@@ -523,9 +527,7 @@ export default {
class="form-group col-sm-12 mb-0" class="form-group col-sm-12 mb-0"
> >
<label for="knative-domainname"> <label for="knative-domainname">
<strong> <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
{{ s__('ClusterIntegration|Knative Domain Name:') }}
</strong>
</label> </label>
<input <input
id="knative-domainname" id="knative-domainname"
...@@ -538,9 +540,7 @@ export default { ...@@ -538,9 +540,7 @@ export default {
<template v-if="knativeInstalled"> <template v-if="knativeInstalled">
<div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0"> <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
<label for="knative-endpoint"> <label for="knative-endpoint">
<strong> <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
{{ s__('ClusterIntegration|Knative Endpoint:') }}
</strong>
</label> </label>
<div v-if="knativeExternalEndpoint" class="input-group"> <div v-if="knativeExternalEndpoint" class="input-group">
<input <input
...@@ -583,8 +583,8 @@ export default { ...@@ -583,8 +583,8 @@ export default {
> >
{{ {{
s__(`ClusterIntegration|The endpoint is in s__(`ClusterIntegration|The endpoint is in
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.`)
}} }}
</p> </p>
......
...@@ -7,6 +7,7 @@ export const CLUSTER_TYPE = { ...@@ -7,6 +7,7 @@ export const CLUSTER_TYPE = {
// These need to match what is returned from the server // These need to match what is returned from the server
export const APPLICATION_STATUS = { export const APPLICATION_STATUS = {
NO_STATUS: null,
NOT_INSTALLABLE: 'not_installable', NOT_INSTALLABLE: 'not_installable',
INSTALLABLE: 'installable', INSTALLABLE: 'installable',
SCHEDULED: 'scheduled', SCHEDULED: 'scheduled',
...@@ -27,17 +28,13 @@ export const APPLICATION_STATUS = { ...@@ -27,17 +28,13 @@ 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.UPDATED,
APPLICATION_STATUS.UPDATE_ERRORED,
APPLICATION_STATUS.UNINSTALLING,
APPLICATION_STATUS.UNINSTALL_ERRORED,
]; ];
// These are only used client-side // These are only used client-side
export const REQUEST_SUBMITTED = 'request-submitted';
export const REQUEST_FAILURE = 'request-failure'; export const UPDATE_EVENT = 'update';
export const UPGRADE_REQUESTED = 'upgrade-requested'; export const INSTALL_EVENT = 'install';
export const UPGRADE_REQUEST_FAILURE = 'upgrade-request-failure';
export const INGRESS = 'ingress'; export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter'; export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative'; export const KNATIVE = 'knative';
......
import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '../constants';
const {
NO_STATUS,
SCHEDULED,
NOT_INSTALLABLE,
INSTALLABLE,
INSTALLING,
INSTALLED,
ERROR,
UPDATING,
UPDATED,
UPDATE_ERRORED,
} = APPLICATION_STATUS;
const applicationStateMachine = {
/* When the application initially loads, it will have `NO_STATUS`
* It will transition from `NO_STATUS` once the async backend call is completed
*/
[NO_STATUS]: {
on: {
[SCHEDULED]: {
target: INSTALLING,
},
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
[INSTALLABLE]: {
target: INSTALLABLE,
},
[INSTALLING]: {
target: INSTALLING,
},
[INSTALLED]: {
target: INSTALLED,
},
[ERROR]: {
target: INSTALLABLE,
effects: {
installFailed: true,
},
},
[UPDATING]: {
target: UPDATING,
},
[UPDATED]: {
target: INSTALLED,
},
[UPDATE_ERRORED]: {
target: INSTALLED,
effects: {
updateFailed: true,
},
},
},
},
[NOT_INSTALLABLE]: {
on: {
[INSTALLABLE]: {
target: INSTALLABLE,
},
},
},
[INSTALLABLE]: {
on: {
[INSTALL_EVENT]: {
target: INSTALLING,
effects: {
installFailed: false,
},
},
// This is possible in artificial environments for E2E testing
[INSTALLED]: {
target: INSTALLED,
},
},
},
[INSTALLING]: {
on: {
[INSTALLED]: {
target: INSTALLED,
},
[ERROR]: {
target: INSTALLABLE,
effects: {
installFailed: true,
},
},
},
},
[INSTALLED]: {
on: {
[UPDATE_EVENT]: {
target: UPDATING,
effects: {
updateFailed: false,
updateSuccessful: false,
},
},
},
},
[UPDATING]: {
on: {
[UPDATED]: {
target: INSTALLED,
effects: {
updateSuccessful: true,
updateAcknowledged: false,
},
},
[UPDATE_ERRORED]: {
target: INSTALLED,
effects: {
updateFailed: true,
},
},
},
},
};
/**
* Determines an application new state based on the application current state
* and an event. If the application current state cannot handle a given event,
* the current state is returned.
*
* @param {*} application
* @param {*} event
*/
const transitionApplicationState = (application, event) => {
const newState = applicationStateMachine[application.status].on[event];
return newState
? {
...application,
status: newState.target,
...newState.effects,
}
: application;
};
export default transitionApplicationState;
...@@ -7,7 +7,11 @@ import { ...@@ -7,7 +7,11 @@ import {
CERT_MANAGER, CERT_MANAGER,
RUNNER, RUNNER,
APPLICATION_INSTALLED_STATUSES, APPLICATION_INSTALLED_STATUSES,
APPLICATION_STATUS,
INSTALL_EVENT,
UPDATE_EVENT,
} from '../constants'; } from '../constants';
import transitionApplicationState from '../services/application_state_machine';
const isApplicationInstalled = appStatus => APPLICATION_INSTALLED_STATUSES.includes(appStatus); const isApplicationInstalled = appStatus => APPLICATION_INSTALLED_STATUSES.includes(appStatus);
...@@ -15,8 +19,8 @@ const applicationInitialState = { ...@@ -15,8 +19,8 @@ const applicationInitialState = {
status: null, status: null,
statusReason: null, statusReason: null,
requestReason: null, requestReason: null,
requestStatus: null,
installed: false, installed: false,
installFailed: false,
}; };
export default class ClusterStore { export default class ClusterStore {
...@@ -49,6 +53,9 @@ export default class ClusterStore { ...@@ -49,6 +53,9 @@ export default class ClusterStore {
version: null, version: null,
chartRepo: 'https://gitlab.com/charts/gitlab-runner', chartRepo: 'https://gitlab.com/charts/gitlab-runner',
upgradeAvailable: null, upgradeAvailable: null,
updateAcknowledged: true,
updateSuccessful: false,
updateFailed: false,
}, },
prometheus: { prometheus: {
...applicationInitialState, ...applicationInitialState,
...@@ -93,6 +100,32 @@ export default class ClusterStore { ...@@ -93,6 +100,32 @@ export default class ClusterStore {
this.state.statusReason = reason; this.state.statusReason = reason;
} }
installApplication(appId) {
this.handleApplicationEvent(appId, INSTALL_EVENT);
}
notifyInstallFailure(appId) {
this.handleApplicationEvent(appId, APPLICATION_STATUS.ERROR);
}
updateApplication(appId) {
this.handleApplicationEvent(appId, UPDATE_EVENT);
}
notifyUpdateFailure(appId) {
this.handleApplicationEvent(appId, APPLICATION_STATUS.UPDATE_ERRORED);
}
handleApplicationEvent(appId, event) {
const currentAppState = this.state.applications[appId];
this.state.applications[appId] = transitionApplicationState(currentAppState, event);
}
acknowledgeSuccessfulUpdate(appId) {
this.state.applications[appId].updateAcknowledged = true;
}
updateAppProperty(appId, prop, value) { updateAppProperty(appId, prop, value) {
this.state.applications[appId][prop] = value; this.state.applications[appId][prop] = value;
} }
...@@ -109,12 +142,16 @@ export default class ClusterStore { ...@@ -109,12 +142,16 @@ export default class ClusterStore {
version, version,
update_available: upgradeAvailable, update_available: upgradeAvailable,
} = serverAppEntry; } = serverAppEntry;
const currentApplicationState = this.state.applications[appId] || {};
const nextApplicationState = transitionApplicationState(currentApplicationState, status);
this.state.applications[appId] = { this.state.applications[appId] = {
...(this.state.applications[appId] || {}), ...currentApplicationState,
status, ...nextApplicationState,
statusReason, statusReason,
installed: isApplicationInstalled(status), installed: isApplicationInstalled(nextApplicationState.status),
// Make sure uninstallable is always false until this feature is unflagged
uninstallable: false,
}; };
if (appId === INGRESS) { if (appId === INGRESS) {
......
import Clusters from '~/clusters/clusters_bundle'; import Clusters from '~/clusters/clusters_bundle';
import { import { APPLICATION_STATUS, INGRESS_DOMAIN_SUFFIX } from '~/clusters/constants';
REQUEST_SUBMITTED,
REQUEST_FAILURE,
APPLICATION_STATUS,
INGRESS_DOMAIN_SUFFIX,
} 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, NOT_INSTALLABLE } = APPLICATION_STATUS;
describe('Clusters', () => { describe('Clusters', () => {
setTestTimeout(1000); setTestTimeout(1000);
...@@ -93,7 +90,7 @@ describe('Clusters', () => { ...@@ -93,7 +90,7 @@ describe('Clusters', () => {
it('does not show alert when things transition from initial null state to something', () => { it('does not show alert when things transition from initial null state to something', () => {
cluster.checkForNewInstalls(INITIAL_APP_MAP, { cluster.checkForNewInstalls(INITIAL_APP_MAP, {
...INITIAL_APP_MAP, ...INITIAL_APP_MAP,
helm: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Helm Tiller' }, helm: { status: INSTALLABLE, title: 'Helm Tiller' },
}); });
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text'); const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
...@@ -105,11 +102,11 @@ describe('Clusters', () => { ...@@ -105,11 +102,11 @@ describe('Clusters', () => {
cluster.checkForNewInstalls( cluster.checkForNewInstalls(
{ {
...INITIAL_APP_MAP, ...INITIAL_APP_MAP,
helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' }, helm: { status: INSTALLING, title: 'Helm Tiller' },
}, },
{ {
...INITIAL_APP_MAP, ...INITIAL_APP_MAP,
helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' }, helm: { status: INSTALLED, title: 'Helm Tiller' },
}, },
); );
...@@ -125,13 +122,13 @@ describe('Clusters', () => { ...@@ -125,13 +122,13 @@ describe('Clusters', () => {
cluster.checkForNewInstalls( cluster.checkForNewInstalls(
{ {
...INITIAL_APP_MAP, ...INITIAL_APP_MAP,
helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' }, helm: { status: INSTALLING, title: 'Helm Tiller' },
ingress: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Ingress' }, ingress: { status: INSTALLABLE, title: 'Ingress' },
}, },
{ {
...INITIAL_APP_MAP, ...INITIAL_APP_MAP,
helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' }, helm: { status: INSTALLED, title: 'Helm Tiller' },
ingress: { status: APPLICATION_STATUS.INSTALLED, title: 'Ingress' }, ingress: { status: INSTALLED, title: 'Ingress' },
}, },
); );
...@@ -218,11 +215,11 @@ describe('Clusters', () => { ...@@ -218,11 +215,11 @@ describe('Clusters', () => {
it('tries to install helm', () => { it('tries to install helm', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null); cluster.store.state.applications.helm.status = INSTALLABLE;
cluster.installApplication({ id: 'helm' }); cluster.installApplication({ id: 'helm' });
expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED); expect(cluster.store.state.applications.helm.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null); expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined); expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined);
}); });
...@@ -230,11 +227,11 @@ describe('Clusters', () => { ...@@ -230,11 +227,11 @@ describe('Clusters', () => {
it('tries to install ingress', () => { it('tries to install ingress', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null); cluster.store.state.applications.ingress.status = INSTALLABLE;
cluster.installApplication({ id: 'ingress' }); cluster.installApplication({ id: 'ingress' });
expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_SUBMITTED); expect(cluster.store.state.applications.ingress.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.ingress.requestReason).toEqual(null); expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined); expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined);
}); });
...@@ -242,11 +239,11 @@ describe('Clusters', () => { ...@@ -242,11 +239,11 @@ describe('Clusters', () => {
it('tries to install runner', () => { it('tries to install runner', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
expect(cluster.store.state.applications.runner.requestStatus).toEqual(null); cluster.store.state.applications.runner.status = INSTALLABLE;
cluster.installApplication({ id: 'runner' }); cluster.installApplication({ id: 'runner' });
expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_SUBMITTED); expect(cluster.store.state.applications.runner.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.runner.requestReason).toEqual(null); expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined); expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
}); });
...@@ -254,13 +251,12 @@ describe('Clusters', () => { ...@@ -254,13 +251,12 @@ describe('Clusters', () => {
it('tries to install jupyter', () => { it('tries to install jupyter', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null);
cluster.installApplication({ cluster.installApplication({
id: 'jupyter', id: 'jupyter',
params: { hostname: cluster.store.state.applications.jupyter.hostname }, params: { hostname: cluster.store.state.applications.jupyter.hostname },
}); });
expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUBMITTED); cluster.store.state.applications.jupyter.status = INSTALLABLE;
expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null); expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', { expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', {
hostname: cluster.store.state.applications.jupyter.hostname, hostname: cluster.store.state.applications.jupyter.hostname,
...@@ -272,16 +268,18 @@ describe('Clusters', () => { ...@@ -272,16 +268,18 @@ describe('Clusters', () => {
.spyOn(cluster.service, 'installApplication') .spyOn(cluster.service, 'installApplication')
.mockRejectedValueOnce(new Error('STUBBED ERROR')); .mockRejectedValueOnce(new Error('STUBBED ERROR'));
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null); cluster.store.state.applications.helm.status = INSTALLABLE;
const promise = cluster.installApplication({ id: 'helm' }); const promise = cluster.installApplication({ id: 'helm' });
expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED); expect(cluster.store.state.applications.helm.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null); expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalled(); expect(cluster.service.installApplication).toHaveBeenCalled();
return promise.then(() => { return promise.then(() => {
expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE); expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE);
expect(cluster.store.state.applications.helm.installFailed).toBe(true);
expect(cluster.store.state.applications.helm.requestReason).toBeDefined(); expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
}); });
}); });
...@@ -315,7 +313,6 @@ describe('Clusters', () => { ...@@ -315,7 +313,6 @@ describe('Clusters', () => {
}); });
describe('toggleIngressDomainHelpText', () => { describe('toggleIngressDomainHelpText', () => {
const { INSTALLED, INSTALLABLE, NOT_INSTALLABLE } = APPLICATION_STATUS;
let ingressPreviousState; let ingressPreviousState;
let ingressNewState; let ingressNewState;
......
import Vue from 'vue'; import Vue from 'vue';
import eventHub from '~/clusters/event_hub'; import eventHub from '~/clusters/event_hub';
import { import { APPLICATION_STATUS } from '~/clusters/constants';
APPLICATION_STATUS,
REQUEST_SUBMITTED,
REQUEST_FAILURE,
UPGRADE_REQUESTED,
} from '~/clusters/constants';
import applicationRow from '~/clusters/components/application_row.vue'; import applicationRow from '~/clusters/components/application_row.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';
...@@ -85,17 +80,6 @@ describe('Application Row', () => { ...@@ -85,17 +80,6 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(false); expect(vm.installButtonDisabled).toEqual(false);
}); });
it('has loading "Installing" when APPLICATION_STATUS.SCHEDULED', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.SCHEDULED,
});
expect(vm.installButtonLabel).toEqual('Installing');
expect(vm.installButtonLoading).toEqual(true);
expect(vm.installButtonDisabled).toEqual(true);
});
it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => { it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
...@@ -107,18 +91,6 @@ describe('Application Row', () => { ...@@ -107,18 +91,6 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(true); expect(vm.installButtonDisabled).toEqual(true);
}); });
it('has loading "Installing" when REQUEST_SUBMITTED', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
requestStatus: REQUEST_SUBMITTED,
});
expect(vm.installButtonLabel).toEqual('Installing');
expect(vm.installButtonLoading).toEqual(true);
expect(vm.installButtonDisabled).toEqual(true);
});
it('has disabled "Installed" when application is installed and not uninstallable', () => { it('has disabled "Installed" when application is installed and not uninstallable', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
...@@ -144,10 +116,11 @@ describe('Application Row', () => { ...@@ -144,10 +116,11 @@ describe('Application Row', () => {
expect(installBtn).toBe(null); expect(installBtn).toBe(null);
}); });
it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => { it('has enabled "Install" when install fails', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.ERROR, status: APPLICATION_STATUS.INSTALLABLE,
installFailed: true,
}); });
expect(vm.installButtonLabel).toEqual('Install'); expect(vm.installButtonLabel).toEqual('Install');
...@@ -159,7 +132,6 @@ describe('Application Row', () => { ...@@ -159,7 +132,6 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE, status: APPLICATION_STATUS.INSTALLABLE,
requestStatus: REQUEST_FAILURE,
}); });
expect(vm.installButtonLabel).toEqual('Install'); expect(vm.installButtonLabel).toEqual('Install');
...@@ -251,15 +223,15 @@ describe('Application Row', () => { ...@@ -251,15 +223,15 @@ describe('Application Row', () => {
expect(upgradeBtn.innerHTML).toContain('Upgrade'); expect(upgradeBtn.innerHTML).toContain('Upgrade');
}); });
it('has enabled "Retry update" when APPLICATION_STATUS.UPDATE_ERRORED', () => { it('has enabled "Retry update" when update process fails', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATE_ERRORED, status: APPLICATION_STATUS.INSTALLED,
updateFailed: true,
}); });
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
expect(upgradeBtn).not.toBe(null); expect(upgradeBtn).not.toBe(null);
expect(vm.upgradeFailed).toBe(true);
expect(upgradeBtn.innerHTML).toContain('Retry update'); expect(upgradeBtn.innerHTML).toContain('Retry update');
}); });
...@@ -279,7 +251,8 @@ describe('Application Row', () => { ...@@ -279,7 +251,8 @@ describe('Application Row', () => {
jest.spyOn(eventHub, '$emit'); jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATE_ERRORED, status: APPLICATION_STATUS.INSTALLED,
upgradeAvailable: true,
}); });
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
...@@ -308,7 +281,8 @@ describe('Application Row', () => { ...@@ -308,7 +281,8 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
title: 'GitLab Runner', title: 'GitLab Runner',
status: APPLICATION_STATUS.UPDATE_ERRORED, status: APPLICATION_STATUS.INSTALLED,
updateFailed: true,
}); });
const failureMessage = vm.$el.querySelector( const failureMessage = vm.$el.querySelector(
'.js-cluster-application-upgrade-failure-message', '.js-cluster-application-upgrade-failure-message',
...@@ -324,12 +298,11 @@ describe('Application Row', () => { ...@@ -324,12 +298,11 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
title: 'GitLab Runner', title: 'GitLab Runner',
requestStatus: UPGRADE_REQUESTED, updateSuccessful: false,
status: APPLICATION_STATUS.UPDATE_ERRORED,
}); });
vm.$toast = { show: jest.fn() }; vm.$toast = { show: jest.fn() };
vm.status = APPLICATION_STATUS.UPDATED; vm.updateSuccessful = true;
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner upgraded successfully.'); expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner upgraded successfully.');
...@@ -342,7 +315,8 @@ describe('Application Row', () => { ...@@ -342,7 +315,8 @@ describe('Application Row', () => {
const version = '0.1.45'; const version = '0.1.45';
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATED, status: APPLICATION_STATUS.INSTALLED,
updateSuccessful: true,
version, version,
}); });
const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details'); const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
...@@ -358,7 +332,8 @@ describe('Application Row', () => { ...@@ -358,7 +332,8 @@ describe('Application Row', () => {
const chartRepo = 'https://gitlab.com/charts/gitlab-runner'; const chartRepo = 'https://gitlab.com/charts/gitlab-runner';
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATED, status: APPLICATION_STATUS.INSTALLED,
updateSuccessful: true,
chartRepo, chartRepo,
version, version,
}); });
...@@ -372,7 +347,8 @@ describe('Application Row', () => { ...@@ -372,7 +347,8 @@ describe('Application Row', () => {
const version = '0.1.45'; const version = '0.1.45';
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATE_ERRORED, status: APPLICATION_STATUS.INSTALLED,
updateFailed: true,
version, version,
}); });
const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details'); const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
...@@ -388,7 +364,6 @@ describe('Application Row', () => { ...@@ -388,7 +364,6 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: null, status: null,
requestStatus: null,
}); });
const generalErrorMessage = vm.$el.querySelector( const generalErrorMessage = vm.$el.querySelector(
'.js-cluster-application-general-error-message', '.js-cluster-application-general-error-message',
...@@ -397,12 +372,13 @@ describe('Application Row', () => { ...@@ -397,12 +372,13 @@ describe('Application Row', () => {
expect(generalErrorMessage).toBeNull(); expect(generalErrorMessage).toBeNull();
}); });
it('shows status reason when APPLICATION_STATUS.ERROR', () => { it('shows status reason when install fails', () => {
const statusReason = 'We broke it 0.0'; const statusReason = 'We broke it 0.0';
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.ERROR, status: APPLICATION_STATUS.ERROR,
statusReason, statusReason,
installFailed: true,
}); });
const generalErrorMessage = vm.$el.querySelector( const generalErrorMessage = vm.$el.querySelector(
'.js-cluster-application-general-error-message', '.js-cluster-application-general-error-message',
...@@ -423,7 +399,7 @@ describe('Application Row', () => { ...@@ -423,7 +399,7 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE, status: APPLICATION_STATUS.INSTALLABLE,
requestStatus: REQUEST_FAILURE, installFailed: true,
requestReason, requestReason,
}); });
const generalErrorMessage = vm.$el.querySelector( const generalErrorMessage = vm.$el.querySelector(
......
import transitionApplicationState from '~/clusters/services/application_state_machine';
import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '~/clusters/constants';
const {
NO_STATUS,
SCHEDULED,
NOT_INSTALLABLE,
INSTALLABLE,
INSTALLING,
INSTALLED,
ERROR,
UPDATING,
UPDATED,
UPDATE_ERRORED,
} = APPLICATION_STATUS;
const NO_EFFECTS = 'no effects';
describe('applicationStateMachine', () => {
const noEffectsToEmptyObject = effects => (typeof effects === 'string' ? {} : effects);
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 }}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: NO_STATUS,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${NOT_INSTALLABLE}`, () => {
it.each`
expectedState | event | effects
${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: NOT_INSTALLABLE,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${INSTALLABLE}`, () => {
it.each`
expectedState | event | effects
${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: INSTALLABLE,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${INSTALLING}`, () => {
it.each`
expectedState | event | effects
${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: INSTALLING,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${INSTALLED}`, () => {
it.each`
expectedState | event | effects
${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: INSTALLED,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...effects,
});
});
});
describe(`current state is ${UPDATING}`, () => {
it.each`
expectedState | event | effects
${INSTALLED} | ${UPDATED} | ${{ updateSuccessful: true, updateAcknowledged: false }}
${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: UPDATING,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...effects,
});
});
});
});
...@@ -113,7 +113,6 @@ const DEFAULT_APPLICATION_STATE = { ...@@ -113,7 +113,6 @@ const DEFAULT_APPLICATION_STATE = {
description: 'Some description about this interesting application!', description: 'Some description about this interesting application!',
status: null, status: null,
statusReason: null, statusReason: null,
requestStatus: null,
requestReason: null, requestReason: null,
}; };
......
...@@ -32,15 +32,6 @@ describe('Clusters Store', () => { ...@@ -32,15 +32,6 @@ describe('Clusters Store', () => {
}); });
describe('updateAppProperty', () => { describe('updateAppProperty', () => {
it('should store new request status', () => {
expect(store.state.applications.helm.requestStatus).toEqual(null);
const newStatus = APPLICATION_STATUS.INSTALLING;
store.updateAppProperty('helm', 'requestStatus', newStatus);
expect(store.state.applications.helm.requestStatus).toEqual(newStatus);
});
it('should store new request reason', () => { it('should store new request reason', () => {
expect(store.state.applications.helm.requestReason).toEqual(null); expect(store.state.applications.helm.requestReason).toEqual(null);
...@@ -68,80 +59,90 @@ describe('Clusters Store', () => { ...@@ -68,80 +59,90 @@ describe('Clusters Store', () => {
title: 'Helm Tiller', title: 'Helm Tiller',
status: mockResponseData.applications[0].status, status: mockResponseData.applications[0].status,
statusReason: mockResponseData.applications[0].status_reason, statusReason: mockResponseData.applications[0].status_reason,
requestStatus: null,
requestReason: null, requestReason: null,
installed: false, installed: false,
installFailed: false,
uninstallable: false,
}, },
ingress: { ingress: {
title: 'Ingress', title: 'Ingress',
status: mockResponseData.applications[1].status, status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[1].status_reason, statusReason: mockResponseData.applications[1].status_reason,
requestStatus: null,
requestReason: null, requestReason: null,
externalIp: null, externalIp: null,
externalHostname: null, externalHostname: null,
installed: false, installed: false,
installFailed: true,
uninstallable: false,
}, },
runner: { runner: {
title: 'GitLab Runner', title: 'GitLab Runner',
status: mockResponseData.applications[2].status, status: mockResponseData.applications[2].status,
statusReason: mockResponseData.applications[2].status_reason, statusReason: mockResponseData.applications[2].status_reason,
requestStatus: null,
requestReason: null, requestReason: null,
version: mockResponseData.applications[2].version, version: mockResponseData.applications[2].version,
upgradeAvailable: mockResponseData.applications[2].update_available, upgradeAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/charts/gitlab-runner', chartRepo: 'https://gitlab.com/charts/gitlab-runner',
installed: false, installed: false,
installFailed: false,
updateAcknowledged: true,
updateFailed: false,
updateSuccessful: false,
uninstallable: false,
}, },
prometheus: { prometheus: {
title: 'Prometheus', title: 'Prometheus',
status: mockResponseData.applications[3].status, status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[3].status_reason, statusReason: mockResponseData.applications[3].status_reason,
requestStatus: null,
requestReason: null, requestReason: null,
installed: false, installed: false,
installFailed: true,
uninstallable: false,
}, },
jupyter: { jupyter: {
title: 'JupyterHub', title: 'JupyterHub',
status: mockResponseData.applications[4].status, status: mockResponseData.applications[4].status,
statusReason: mockResponseData.applications[4].status_reason, statusReason: mockResponseData.applications[4].status_reason,
requestStatus: null,
requestReason: null, requestReason: null,
hostname: '', hostname: '',
installed: false, installed: false,
installFailed: false,
uninstallable: false,
}, },
knative: { knative: {
title: 'Knative', title: 'Knative',
status: mockResponseData.applications[5].status, status: mockResponseData.applications[5].status,
statusReason: mockResponseData.applications[5].status_reason, statusReason: mockResponseData.applications[5].status_reason,
requestStatus: null,
requestReason: null, requestReason: null,
hostname: null, hostname: null,
isEditingHostName: false, isEditingHostName: false,
externalIp: null, externalIp: null,
externalHostname: null, externalHostname: null,
installed: false, installed: false,
installFailed: false,
uninstallable: false,
}, },
cert_manager: { cert_manager: {
title: 'Cert-Manager', title: 'Cert-Manager',
status: mockResponseData.applications[6].status, status: APPLICATION_STATUS.INSTALLABLE,
installFailed: true,
statusReason: mockResponseData.applications[6].status_reason, statusReason: mockResponseData.applications[6].status_reason,
requestStatus: null,
requestReason: null, requestReason: null,
email: mockResponseData.applications[6].email, email: mockResponseData.applications[6].email,
installed: false, installed: false,
uninstallable: false,
}, },
}, },
}); });
}); });
describe.each(APPLICATION_INSTALLED_STATUSES)('given the current app status is %s', () => { describe.each(APPLICATION_INSTALLED_STATUSES)('given the current app status is %s', status => {
it('marks application as installed', () => { it('marks application as installed', () => {
const mockResponseData = const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data; CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
const runnerAppIndex = 2; const runnerAppIndex = 2;
mockResponseData.applications[runnerAppIndex].status = APPLICATION_STATUS.INSTALLED; mockResponseData.applications[runnerAppIndex].status = status;
store.updateStateFromServer(mockResponseData); store.updateStateFromServer(mockResponseData);
......
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