diff options
11 files changed, 480 insertions, 227 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 7f07e592f84..4f47f1b6550 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -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) { diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 17fe816e62b..a351916942e 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -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 diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index ae4fe11c6ae..dfc2069f131 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -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> diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index 17849497c87..48dbce9676e 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -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'; diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js new file mode 100644 index 00000000000..aafb2350ae4 --- /dev/null +++ b/app/assets/javascripts/clusters/services/application_state_machine.js @@ -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; diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 38512ac28c2..c2e30960659 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -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) { diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js index 33a35069004..5103cb4f69f 100644 --- a/spec/frontend/clusters/clusters_bundle_spec.js +++ b/spec/frontend/clusters/clusters_bundle_spec.js @@ -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; diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js index d26fad54ebe..17273b7d5b1 100644 --- a/spec/frontend/clusters/components/application_row_spec.js +++ b/spec/frontend/clusters/components/application_row_spec.js @@ -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( diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js new file mode 100644 index 00000000000..e74b7910572 --- /dev/null +++ b/spec/frontend/clusters/services/application_state_machine_spec.js @@ -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, + }); + }); + }); +}); diff --git a/spec/frontend/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js index b4d1bb710e0..1e896af1c7d 100644 --- a/spec/frontend/clusters/services/mock_data.js +++ b/spec/frontend/clusters/services/mock_data.js @@ -113,7 +113,6 @@ const DEFAULT_APPLICATION_STATE = { description: 'Some description about this interesting application!', status: null, statusReason: null, - requestStatus: null, requestReason: null, }; diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js index c0e8b737ea2..a20e0439555 100644 --- a/spec/frontend/clusters/stores/clusters_store_spec.js +++ b/spec/frontend/clusters/stores/clusters_store_spec.js @@ -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); |