Commit 5f8383fd authored by Phil Hughes's avatar Phil Hughes

Merge branch '60639-cluster-application-state-machine' into 'master'

Simplify cluster applications state management

Closes #60639

See merge request gitlab-org/gitlab-ce!27461
parents 336a0a87 690382dd
...@@ -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
......
...@@ -226,7 +226,7 @@ export default { ...@@ -226,7 +226,7 @@ export default {
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,6 +239,7 @@ export default { ...@@ -239,6 +239,7 @@ 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/"
> >
...@@ -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/"
> >
...@@ -281,9 +283,7 @@ export default { ...@@ -281,9 +283,7 @@ export default {
<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"
...@@ -324,7 +324,6 @@ export default { ...@@ -324,7 +324,6 @@ 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>
...@@ -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/#"
...@@ -372,9 +372,8 @@ export default { ...@@ -372,9 +372,8 @@ export default {
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,6 +408,9 @@ export default { ...@@ -408,6 +408,9 @@ 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/"
> >
...@@ -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/"
...@@ -447,9 +451,7 @@ export default { ...@@ -447,9 +451,7 @@ export default {
<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
...@@ -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">
...@@ -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
......
...@@ -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