diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2019-04-18 17:08:45 +0300 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2019-04-18 17:08:45 +0300 |
commit | ae534a17cc9b79524ba5bb727bbee574f0f1c510 (patch) | |
tree | 5bd969a2d81bdbccadc59e96b20a62cc82364c78 | |
parent | 506afd5f69e49fff7df5d493e0e21aea3fb628f0 (diff) | |
parent | d51a36ece68396ff79f11296774f54360aec9027 (diff) |
Merge branch 'fe-uninstall-cluster-apps' into 'master'
Display "Uninstall App" button if app is uninstallable
Closes #60641
See merge request gitlab-org/gitlab-ce!27423
7 files changed, 140 insertions, 58 deletions
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 19e5ac1567d..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, @@ -63,6 +71,11 @@ export default { type: String, required: false, }, + installed: { + type: Boolean, + required: false, + default: false, + }, version: { type: String, required: false, @@ -92,15 +105,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() { @@ -125,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; }, @@ -145,7 +156,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 +268,7 @@ export default { <div :class="[ rowJsClass, - isInstalled && 'cluster-application-installed', + installed && 'cluster-application-installed', disabled && 'cluster-application-disabled', ]" class="cluster-application-row gl-responsive-table-row gl-responsive-table-row-col-span" @@ -280,10 +291,9 @@ export default { target="blank" rel="noopener noreferrer" class="js-cluster-application-title" + >{{ title }}</a > - {{ title }} - </a> - <span v-else class="js-cluster-application-title"> {{ title }} </span> + <span v-else class="js-cluster-application-title">{{ title }}</span> </strong> <slot name="description"></slot> <div @@ -308,17 +318,15 @@ 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="upgradeSuccessful">to</span> <gl-link v-if="upgradeSuccessful" :href="chartRepo" target="_blank" class="js-cluster-application-upgrade-version" + >chart v{{ version }}</gl-link > - chart v{{ version }} - </gl-link> </div> <div @@ -333,7 +341,6 @@ export default { class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3" > {{ upgradeSuccessDescription }} - <button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess"> × </button> @@ -354,18 +361,23 @@ 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 + v-if="displayInstallButton" :loading="installButtonLoading" :disabled="disabled || installButtonDisabled" :label="installButtonLabel" class="js-cluster-application-install-button" @click="installClicked" /> + <uninstall-application-button + v-if="displayUninstallButton" + class="js-cluster-application-uninstall-button" + /> </div> </div> </div> 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" 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 @@ +<script> +// TODO: Implement loading button component +import LoadingButton from '~/vue_shared/components/loading_button.vue'; + +export default { + components: { + LoadingButton, + }, +}; +</script> + +<template> + <loading-button @click="$emit('click')" /> +</template> diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index 67f481f2afb..17849497c87 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -15,9 +15,24 @@ export const APPLICATION_STATUS = { UPDATING: 'updating', UPDATED: 'updated', UPDATE_ERRORED: 'update_errored', + UNINSTALLING: 'uninstalling', + UNINSTALL_ERRORED: 'uninstall_errored', ERROR: 'errored', }; +/* + * The application cannot be in any of the following states without + * not being installed. + */ +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'; diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 92993337f02..38512ac28c2 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -1,6 +1,23 @@ import { s__ } from '../../locale'; import { parseBoolean } from '../../lib/utils/common_utils'; -import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER, RUNNER } from '../constants'; +import { + INGRESS, + JUPYTER, + KNATIVE, + CERT_MANAGER, + RUNNER, + APPLICATION_INSTALLED_STATUSES, +} from '../constants'; + +const isApplicationInstalled = appStatus => 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..038d2be9e98 100644 --- a/spec/frontend/clusters/components/application_row_spec.js +++ b/spec/frontend/clusters/components/application_row_spec.js @@ -114,10 +114,12 @@ describe('Application Row', () => { expect(vm.installButtonDisabled).toEqual(true); }); - it('has disabled "Installed" when APPLICATION_STATUS.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'); @@ -125,15 +127,16 @@ describe('Application Row', () => { expect(vm.installButtonDisabled).toEqual(true); }); - it('has disabled "Installed" when APPLICATION_STATUS.UPDATING', () => { + it('hides when application is installed and uninstallable', () => { vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.UPDATING, + status: APPLICATION_STATUS.INSTALLED, + installed: true, + uninstallable: true, }); + const installBtn = vm.$el.querySelector('.js-cluster-application-install-button'); - expect(vm.installButtonLabel).toEqual('Installed'); - expect(vm.installButtonLoading).toEqual(false); - expect(vm.installButtonDisabled).toEqual(true); + expect(installBtn).toBe(null); }); it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => { @@ -208,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, { 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; |