Skip to content
Snippets Groups Projects
Commit 690382dd authored by Enrique Alcántara's avatar Enrique Alcántara Committed by Phil Hughes
Browse files

Use a FSM to determine application next state

- Separate cluster application UI state from server-side app status
- Use a state machine to determine cluster application next state
- Instead of using two variables to keep track of when an app
is installing or updating, just use the app status property and control
server-side and user events using the FSM service.
parent 336a0a87
No related branches found
No related tags found
No related merge requests found
Showing
with 480 additions and 227 deletions
Loading
Loading
@@ -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';
Loading
Loading
@@ -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));
}
Loading
Loading
@@ -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');
}
Loading
Loading
@@ -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',
Loading
Loading
@@ -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) {
Loading
Loading
Loading
Loading
@@ -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: {
Loading
Loading
@@ -63,10 +58,6 @@ export default {
type: String,
required: false,
},
requestStatus: {
type: String,
required: false,
},
requestReason: {
type: String,
required: false,
Loading
Loading
@@ -76,6 +67,11 @@ export default {
required: false,
default: false,
},
installFailed: {
type: Boolean,
required: false,
default: false,
},
version: {
type: String,
required: false,
Loading
Loading
@@ -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,
Loading
Loading
@@ -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
);
},
Loading
Loading
@@ -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
Loading
Loading
@@ -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');
Loading
Loading
@@ -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.');
},
Loading
Loading
@@ -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');
}
 
Loading
Loading
@@ -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);
}
},
Loading
Loading
@@ -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">
Loading
Loading
@@ -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"
Loading
Loading
@@ -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"
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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">
Loading
Loading
@@ -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>
Loading
Loading
@@ -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
Loading
Loading
@@ -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/"
>
Loading
Loading
@@ -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"
Loading
Loading
@@ -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') }}
Loading
Loading
@@ -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>
Loading
Loading
@@ -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/#"
Loading
Loading
@@ -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>
Loading
Loading
@@ -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/"
>
Loading
Loading
@@ -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>
Loading
Loading
@@ -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/"
Loading
Loading
@@ -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
Loading
Loading
@@ -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') }}
Loading
Loading
@@ -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">
Loading
Loading
@@ -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') }}
Loading
Loading
@@ -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>
 
Loading
Loading
@@ -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"
Loading
Loading
@@ -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
Loading
Loading
@@ -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>
 
Loading
Loading
Loading
Loading
@@ -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',
Loading
Loading
@@ -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';
Loading
Loading
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;
Loading
Loading
@@ -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);
 
Loading
Loading
@@ -15,8 +19,8 @@ const applicationInitialState = {
status: null,
statusReason: null,
requestReason: null,
requestStatus: null,
installed: false,
installFailed: false,
};
 
export default class ClusterStore {
Loading
Loading
@@ -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,
Loading
Loading
@@ -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;
}
Loading
Loading
@@ -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) {
Loading
Loading
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);
 
Loading
Loading
@@ -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');
Loading
Loading
@@ -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' },
},
);
 
Loading
Loading
@@ -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' },
},
);
 
Loading
Loading
@@ -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);
});
Loading
Loading
@@ -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);
});
Loading
Loading
@@ -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);
});
Loading
Loading
@@ -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,
Loading
Loading
@@ -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();
});
});
Loading
Loading
@@ -315,7 +313,6 @@ describe('Clusters', () => {
});
 
describe('toggleIngressDomainHelpText', () => {
const { INSTALLED, INSTALLABLE, NOT_INSTALLABLE } = APPLICATION_STATUS;
let ingressPreviousState;
let ingressNewState;
 
Loading
Loading
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';
Loading
Loading
@@ -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,
Loading
Loading
@@ -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,
Loading
Loading
@@ -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');
Loading
Loading
@@ -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');
Loading
Loading
@@ -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');
});
 
Loading
Loading
@@ -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');
 
Loading
Loading
@@ -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',
Loading
Loading
@@ -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.');
Loading
Loading
@@ -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');
Loading
Loading
@@ -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,
});
Loading
Loading
@@ -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');
Loading
Loading
@@ -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',
Loading
Loading
@@ -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',
Loading
Loading
@@ -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(
Loading
Loading
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,
});
});
});
});
Loading
Loading
@@ -113,7 +113,6 @@ const DEFAULT_APPLICATION_STATE = {
description: 'Some description about this interesting application!',
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
};
 
Loading
Loading
Loading
Loading
@@ -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);
 
Loading
Loading
@@ -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
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment