From 8f6ec252a3a440687740925ec5b01d895fe3e2c0 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Tue, 16 Apr 2019 10:30:09 -0400 Subject: [PATCH 1/3] Determine if app is installed in the store - Create installed property for cluster apps - Pass installed property to the application_row component --- .../clusters/components/application_row.vue | 19 +++---- app/assets/javascripts/clusters/constants.js | 15 +++++ .../clusters/stores/clusters_store.js | 55 +++++++++---------- .../components/application_row_spec.js | 14 +---- .../clusters/stores/clusters_store_spec.js | 23 +++++++- 5 files changed, 73 insertions(+), 53 deletions(-) diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 19e5ac1567d..2ca84de0454 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -63,6 +63,11 @@ export default { type: String, required: false, }, + installed: { + type: Boolean, + required: false, + default: false + }, version: { type: String, required: false, @@ -92,15 +97,7 @@ export default { return ( this.status === APPLICATION_STATUS.SCHEDULED || this.status === APPLICATION_STATUS.INSTALLING || - (this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.isInstalled) - ); - }, - isInstalled() { - return ( - this.status === APPLICATION_STATUS.INSTALLED || - this.status === APPLICATION_STATUS.UPDATED || - this.status === APPLICATION_STATUS.UPDATING || - this.status === APPLICATION_STATUS.UPDATE_ERRORED + (this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.installed) ); }, canInstall() { @@ -145,7 +142,7 @@ export default { label = s__('ClusterIntegration|Install'); } else if (this.isInstalling) { label = s__('ClusterIntegration|Installing'); - } else if (this.isInstalled) { + } else if (this.installed) { label = s__('ClusterIntegration|Installed'); } @@ -257,7 +254,7 @@ export default {
APPLICATION_INSTALLED_STATUSES.includes(appStatus); + +const applicationInitialState = { + status: null, + statusReason: null, + requestReason: null, + requestStatus: null, + installed: false, +}; export default class ClusterStore { constructor() { @@ -12,60 +29,39 @@ export default class ClusterStore { statusReason: null, applications: { helm: { + ...applicationInitialState, title: s__('ClusterIntegration|Helm Tiller'), - status: null, - statusReason: null, - requestStatus: null, - requestReason: null, }, ingress: { + ...applicationInitialState, title: s__('ClusterIntegration|Ingress'), - status: null, - statusReason: null, - requestStatus: null, - requestReason: null, externalIp: null, externalHostname: null, }, cert_manager: { + ...applicationInitialState, title: s__('ClusterIntegration|Cert-Manager'), - status: null, - statusReason: null, - requestStatus: null, - requestReason: null, email: null, }, runner: { + ...applicationInitialState, title: s__('ClusterIntegration|GitLab Runner'), - status: null, - statusReason: null, - requestStatus: null, - requestReason: null, version: null, chartRepo: 'https://gitlab.com/charts/gitlab-runner', upgradeAvailable: null, }, prometheus: { + ...applicationInitialState, title: s__('ClusterIntegration|Prometheus'), - status: null, - statusReason: null, - requestStatus: null, - requestReason: null, }, jupyter: { + ...applicationInitialState, title: s__('ClusterIntegration|JupyterHub'), - status: null, - statusReason: null, - requestStatus: null, - requestReason: null, hostname: null, }, knative: { + ...applicationInitialState, title: s__('ClusterIntegration|Knative'), - status: null, - statusReason: null, - requestStatus: null, - requestReason: null, hostname: null, isEditingHostName: false, externalIp: null, @@ -118,6 +114,7 @@ export default class ClusterStore { ...(this.state.applications[appId] || {}), status, statusReason, + installed: isApplicationInstalled(status), }; if (appId === INGRESS) { diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js index b28d0075d06..1e4e814b5e1 100644 --- a/spec/frontend/clusters/components/application_row_spec.js +++ b/spec/frontend/clusters/components/application_row_spec.js @@ -114,21 +114,11 @@ describe('Application Row', () => { expect(vm.installButtonDisabled).toEqual(true); }); - it('has disabled "Installed" when APPLICATION_STATUS.INSTALLED', () => { + it('has disabled "Installed" when application is installed', () => { vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, status: APPLICATION_STATUS.INSTALLED, - }); - - expect(vm.installButtonLabel).toEqual('Installed'); - expect(vm.installButtonLoading).toEqual(false); - expect(vm.installButtonDisabled).toEqual(true); - }); - - it('has disabled "Installed" when APPLICATION_STATUS.UPDATING', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.UPDATING, + installed: true, }); expect(vm.installButtonLabel).toEqual('Installed'); diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js index 161722ec571..c0e8b737ea2 100644 --- a/spec/frontend/clusters/stores/clusters_store_spec.js +++ b/spec/frontend/clusters/stores/clusters_store_spec.js @@ -1,5 +1,5 @@ import ClustersStore from '~/clusters/stores/clusters_store'; -import { APPLICATION_STATUS } from '~/clusters/constants'; +import { APPLICATION_INSTALLED_STATUSES, APPLICATION_STATUS, RUNNER } from '~/clusters/constants'; import { CLUSTERS_MOCK_DATA } from '../services/mock_data'; describe('Clusters Store', () => { @@ -70,6 +70,7 @@ describe('Clusters Store', () => { statusReason: mockResponseData.applications[0].status_reason, requestStatus: null, requestReason: null, + installed: false, }, ingress: { title: 'Ingress', @@ -79,6 +80,7 @@ describe('Clusters Store', () => { requestReason: null, externalIp: null, externalHostname: null, + installed: false, }, runner: { title: 'GitLab Runner', @@ -89,6 +91,7 @@ describe('Clusters Store', () => { version: mockResponseData.applications[2].version, upgradeAvailable: mockResponseData.applications[2].update_available, chartRepo: 'https://gitlab.com/charts/gitlab-runner', + installed: false, }, prometheus: { title: 'Prometheus', @@ -96,6 +99,7 @@ describe('Clusters Store', () => { statusReason: mockResponseData.applications[3].status_reason, requestStatus: null, requestReason: null, + installed: false, }, jupyter: { title: 'JupyterHub', @@ -104,6 +108,7 @@ describe('Clusters Store', () => { requestStatus: null, requestReason: null, hostname: '', + installed: false, }, knative: { title: 'Knative', @@ -115,6 +120,7 @@ describe('Clusters Store', () => { isEditingHostName: false, externalIp: null, externalHostname: null, + installed: false, }, cert_manager: { title: 'Cert-Manager', @@ -123,11 +129,26 @@ describe('Clusters Store', () => { requestStatus: null, requestReason: null, email: mockResponseData.applications[6].email, + installed: false, }, }, }); }); + describe.each(APPLICATION_INSTALLED_STATUSES)('given the current app status is %s', () => { + 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; + + store.updateStateFromServer(mockResponseData); + + expect(store.state.applications[RUNNER].installed).toBe(true); + }); + }); + it('sets default hostname for jupyter when ingress has a ip address', () => { const mockResponseData = CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data; -- GitLab From 5a7b8666ca718548cbe1c0bfe82190af6cf3bd57 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Tue, 16 Apr 2019 10:56:07 -0400 Subject: [PATCH 2/3] Show uninstall button when app is uninstallable - Create empty uninstall_button component. - Add uninstallable property to application_row component. - Display Uninstall button if app is uninstallable and it is installed. --- .../clusters/components/application_row.vue | 39 +++++++++++++------ .../uninstall_application_button.vue | 14 +++++++ .../components/application_row_spec.js | 28 ++++++++++++- 3 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 app/assets/javascripts/clusters/components/uninstall_application_button.vue diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 2ca84de0454..937e4c3bfc3 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -6,6 +6,8 @@ import { s__, sprintf } from '../../locale'; import eventHub from '../event_hub'; 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, @@ -19,6 +21,7 @@ export default { identicon, TimeagoTooltip, GlLink, + UninstallApplicationButton, }, props: { id: { @@ -47,6 +50,11 @@ export default { required: false, default: false, }, + uninstallable: { + type: Boolean, + required: false, + default: false, + }, status: { type: String, required: false, @@ -66,7 +74,7 @@ export default { installed: { type: Boolean, required: false, - default: false + default: false, }, version: { type: String, @@ -122,6 +130,12 @@ export default { rowJsClass() { return `js-cluster-application-row-${this.id}`; }, + displayUninstallButton() { + return this.installed && this.uninstallable; + }, + displayInstallButton() { + return !this.installed || !this.uninstallable; + }, installButtonLoading() { return !this.status || this.status === APPLICATION_STATUS.SCHEDULED || this.isInstalling; }, @@ -277,10 +291,9 @@ export default { target="blank" rel="noopener noreferrer" class="js-cluster-application-title" + >{{ title }} - {{ title }} - - {{ title }} + {{ title }}
{{ versionLabel }} - - to + to chart v{{ version }} - chart v{{ version }} -
{{ upgradeSuccessDescription }} - @@ -351,18 +361,23 @@ export default { role="gridcell" >
+
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_button.vue b/app/assets/javascripts/clusters/components/uninstall_application_button.vue new file mode 100644 index 00000000000..30918d1d115 --- /dev/null +++ b/app/assets/javascripts/clusters/components/uninstall_application_button.vue @@ -0,0 +1,14 @@ + + + diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js index 1e4e814b5e1..038d2be9e98 100644 --- a/spec/frontend/clusters/components/application_row_spec.js +++ b/spec/frontend/clusters/components/application_row_spec.js @@ -114,11 +114,12 @@ describe('Application Row', () => { expect(vm.installButtonDisabled).toEqual(true); }); - it('has disabled "Installed" when application is installed', () => { + it('has disabled "Installed" when application is installed and not uninstallable', () => { vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, status: APPLICATION_STATUS.INSTALLED, installed: true, + uninstallable: false, }); expect(vm.installButtonLabel).toEqual('Installed'); @@ -126,6 +127,18 @@ describe('Application Row', () => { expect(vm.installButtonDisabled).toEqual(true); }); + it('hides when application is installed and uninstallable', () => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_STATUS.INSTALLED, + installed: true, + uninstallable: true, + }); + const installBtn = vm.$el.querySelector('.js-cluster-application-install-button'); + + expect(installBtn).toBe(null); + }); + it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => { vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, @@ -198,6 +211,19 @@ describe('Application Row', () => { }); }); + describe('Uninstall button', () => { + it('displays button when app is installed and uninstallable', () => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + installed: true, + uninstallable: true, + }); + const uninstallButton = vm.$el.querySelector('.js-cluster-application-uninstall-button'); + + expect(uninstallButton).toBeTruthy(); + }); + }); + describe('Upgrade button', () => { it('has indeterminate state on page load', () => { vm = mountComponent(ApplicationRow, { -- GitLab From d51a36ece68396ff79f11296774f54360aec9027 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Tue, 16 Apr 2019 12:48:35 -0400 Subject: [PATCH 3/3] Pass installed attribute to app_rows --- .../javascripts/clusters/components/applications.vue | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index d54f9ce552c..ae4fe11c6ae 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -238,6 +238,7 @@ export default { :status-reason="applications.helm.statusReason" :request-status="applications.helm.requestStatus" :request-reason="applications.helm.requestReason" + :installed="applications.helm.installed" class="rounded-top" title-link="https://docs.helm.sh/" > @@ -265,6 +266,7 @@ export default { :status-reason="applications.ingress.statusReason" :request-status="applications.ingress.requestStatus" :request-reason="applications.ingress.requestReason" + :installed="applications.ingress.installed" :disabled="!helmInstalled" title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" > @@ -341,6 +343,7 @@ export default { :status-reason="applications.cert_manager.statusReason" :request-status="applications.cert_manager.requestStatus" :request-reason="applications.cert_manager.requestReason" + :installed="applications.cert_manager.installed" :install-application-request-params="{ email: applications.cert_manager.email }" :disabled="!helmInstalled" title-link="https://cert-manager.readthedocs.io/en/latest/#" @@ -387,6 +390,7 @@ export default { :status-reason="applications.prometheus.statusReason" :request-status="applications.prometheus.requestStatus" :request-reason="applications.prometheus.requestReason" + :installed="applications.prometheus.installed" :disabled="!helmInstalled" title-link="https://prometheus.io/docs/introduction/overview/" > @@ -403,6 +407,7 @@ export default { :version="applications.runner.version" :chart-repo="applications.runner.chartRepo" :upgrade-available="applications.runner.upgradeAvailable" + :installed="applications.runner.installed" :disabled="!helmInstalled" title-link="https://docs.gitlab.com/runner/" > @@ -424,6 +429,7 @@ export default { :status-reason="applications.jupyter.statusReason" :request-status="applications.jupyter.requestStatus" :request-reason="applications.jupyter.requestReason" + :installed="applications.jupyter.installed" :install-application-request-params="{ hostname: applications.jupyter.hostname }" :disabled="!helmInstalled" title-link="https://jupyterhub.readthedocs.io/en/stable/" @@ -483,6 +489,7 @@ export default { :status-reason="applications.knative.statusReason" :request-status="applications.knative.requestStatus" :request-reason="applications.knative.requestReason" + :installed="applications.knative.installed" :install-application-request-params="{ hostname: applications.knative.hostname }" :disabled="!helmInstalled" title-link="https://github.com/knative/docs" -- GitLab