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