Commit ae534a17 authored by Filipa Lacerda's avatar Filipa Lacerda

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
parents 506afd5f d51a36ec
...@@ -6,6 +6,8 @@ import { s__, sprintf } from '../../locale'; ...@@ -6,6 +6,8 @@ import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue'; import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue';
import UninstallApplicationButton from './uninstall_application_button.vue';
import { import {
APPLICATION_STATUS, APPLICATION_STATUS,
REQUEST_SUBMITTED, REQUEST_SUBMITTED,
...@@ -19,6 +21,7 @@ export default { ...@@ -19,6 +21,7 @@ export default {
identicon, identicon,
TimeagoTooltip, TimeagoTooltip,
GlLink, GlLink,
UninstallApplicationButton,
}, },
props: { props: {
id: { id: {
...@@ -47,6 +50,11 @@ export default { ...@@ -47,6 +50,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
uninstallable: {
type: Boolean,
required: false,
default: false,
},
status: { status: {
type: String, type: String,
required: false, required: false,
...@@ -63,6 +71,11 @@ export default { ...@@ -63,6 +71,11 @@ export default {
type: String, type: String,
required: false, required: false,
}, },
installed: {
type: Boolean,
required: false,
default: false,
},
version: { version: {
type: String, type: String,
required: false, required: false,
...@@ -92,15 +105,7 @@ export default { ...@@ -92,15 +105,7 @@ export default {
return ( return (
this.status === APPLICATION_STATUS.SCHEDULED || this.status === APPLICATION_STATUS.SCHEDULED ||
this.status === APPLICATION_STATUS.INSTALLING || this.status === APPLICATION_STATUS.INSTALLING ||
(this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.isInstalled) (this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.installed)
);
},
isInstalled() {
return (
this.status === APPLICATION_STATUS.INSTALLED ||
this.status === APPLICATION_STATUS.UPDATED ||
this.status === APPLICATION_STATUS.UPDATING ||
this.status === APPLICATION_STATUS.UPDATE_ERRORED
); );
}, },
canInstall() { canInstall() {
...@@ -125,6 +130,12 @@ export default { ...@@ -125,6 +130,12 @@ export default {
rowJsClass() { rowJsClass() {
return `js-cluster-application-row-${this.id}`; return `js-cluster-application-row-${this.id}`;
}, },
displayUninstallButton() {
return this.installed && this.uninstallable;
},
displayInstallButton() {
return !this.installed || !this.uninstallable;
},
installButtonLoading() { installButtonLoading() {
return !this.status || this.status === APPLICATION_STATUS.SCHEDULED || this.isInstalling; return !this.status || this.status === APPLICATION_STATUS.SCHEDULED || this.isInstalling;
}, },
...@@ -145,7 +156,7 @@ export default { ...@@ -145,7 +156,7 @@ export default {
label = s__('ClusterIntegration|Install'); label = s__('ClusterIntegration|Install');
} else if (this.isInstalling) { } else if (this.isInstalling) {
label = s__('ClusterIntegration|Installing'); label = s__('ClusterIntegration|Installing');
} else if (this.isInstalled) { } else if (this.installed) {
label = s__('ClusterIntegration|Installed'); label = s__('ClusterIntegration|Installed');
} }
...@@ -257,7 +268,7 @@ export default { ...@@ -257,7 +268,7 @@ export default {
<div <div
:class="[ :class="[
rowJsClass, rowJsClass,
isInstalled && 'cluster-application-installed', installed && 'cluster-application-installed',
disabled && 'cluster-application-disabled', disabled && 'cluster-application-disabled',
]" ]"
class="cluster-application-row gl-responsive-table-row gl-responsive-table-row-col-span" class="cluster-application-row gl-responsive-table-row gl-responsive-table-row-col-span"
...@@ -280,10 +291,9 @@ export default { ...@@ -280,10 +291,9 @@ export default {
target="blank" target="blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="js-cluster-application-title" class="js-cluster-application-title"
>{{ title }}</a
> >
{{ title }} <span v-else class="js-cluster-application-title">{{ title }}</span>
</a>
<span v-else class="js-cluster-application-title"> {{ title }} </span>
</strong> </strong>
<slot name="description"></slot> <slot name="description"></slot>
<div <div
...@@ -308,17 +318,15 @@ export default { ...@@ -308,17 +318,15 @@ export default {
class="form-text text-muted label p-0 js-cluster-application-upgrade-details" class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
> >
{{ versionLabel }} {{ versionLabel }}
<span v-if="upgradeSuccessful">to</span>
<span v-if="upgradeSuccessful"> to</span>
<gl-link <gl-link
v-if="upgradeSuccessful" v-if="upgradeSuccessful"
:href="chartRepo" :href="chartRepo"
target="_blank" target="_blank"
class="js-cluster-application-upgrade-version" class="js-cluster-application-upgrade-version"
>chart v{{ version }}</gl-link
> >
chart v{{ version }}
</gl-link>
</div> </div>
<div <div
...@@ -333,7 +341,6 @@ export default { ...@@ -333,7 +341,6 @@ export default {
class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3" class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3"
> >
{{ upgradeSuccessDescription }} {{ upgradeSuccessDescription }}
<button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess"> <button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess">
&times; &times;
</button> </button>
...@@ -354,18 +361,23 @@ export default { ...@@ -354,18 +361,23 @@ export default {
role="gridcell" role="gridcell"
> >
<div v-if="showManageButton" class="btn-group table-action-buttons"> <div v-if="showManageButton" class="btn-group table-action-buttons">
<a :href="manageLink" :class="{ disabled: disabled }" class="btn"> <a :href="manageLink" :class="{ disabled: disabled }" class="btn">{{
{{ manageButtonLabel }} manageButtonLabel
</a> }}</a>
</div> </div>
<div class="btn-group table-action-buttons"> <div class="btn-group table-action-buttons">
<loading-button <loading-button
v-if="displayInstallButton"
:loading="installButtonLoading" :loading="installButtonLoading"
:disabled="disabled || installButtonDisabled" :disabled="disabled || installButtonDisabled"
:label="installButtonLabel" :label="installButtonLabel"
class="js-cluster-application-install-button" class="js-cluster-application-install-button"
@click="installClicked" @click="installClicked"
/> />
<uninstall-application-button
v-if="displayUninstallButton"
class="js-cluster-application-uninstall-button"
/>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -238,6 +238,7 @@ export default { ...@@ -238,6 +238,7 @@ export default {
:status-reason="applications.helm.statusReason" :status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus" :request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason" :request-reason="applications.helm.requestReason"
:installed="applications.helm.installed"
class="rounded-top" class="rounded-top"
title-link="https://docs.helm.sh/" title-link="https://docs.helm.sh/"
> >
...@@ -265,6 +266,7 @@ export default { ...@@ -265,6 +266,7 @@ export default {
:status-reason="applications.ingress.statusReason" :status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus" :request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason" :request-reason="applications.ingress.requestReason"
:installed="applications.ingress.installed"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
> >
...@@ -341,6 +343,7 @@ export default { ...@@ -341,6 +343,7 @@ export default {
:status-reason="applications.cert_manager.statusReason" :status-reason="applications.cert_manager.statusReason"
:request-status="applications.cert_manager.requestStatus" :request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason" :request-reason="applications.cert_manager.requestReason"
:installed="applications.cert_manager.installed"
:install-application-request-params="{ email: applications.cert_manager.email }" :install-application-request-params="{ email: applications.cert_manager.email }"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://cert-manager.readthedocs.io/en/latest/#" title-link="https://cert-manager.readthedocs.io/en/latest/#"
...@@ -387,6 +390,7 @@ export default { ...@@ -387,6 +390,7 @@ export default {
:status-reason="applications.prometheus.statusReason" :status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus" :request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason" :request-reason="applications.prometheus.requestReason"
:installed="applications.prometheus.installed"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://prometheus.io/docs/introduction/overview/" title-link="https://prometheus.io/docs/introduction/overview/"
> >
...@@ -403,6 +407,7 @@ export default { ...@@ -403,6 +407,7 @@ export default {
:version="applications.runner.version" :version="applications.runner.version"
:chart-repo="applications.runner.chartRepo" :chart-repo="applications.runner.chartRepo"
:upgrade-available="applications.runner.upgradeAvailable" :upgrade-available="applications.runner.upgradeAvailable"
:installed="applications.runner.installed"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/" title-link="https://docs.gitlab.com/runner/"
> >
...@@ -424,6 +429,7 @@ export default { ...@@ -424,6 +429,7 @@ export default {
:status-reason="applications.jupyter.statusReason" :status-reason="applications.jupyter.statusReason"
:request-status="applications.jupyter.requestStatus" :request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason" :request-reason="applications.jupyter.requestReason"
:installed="applications.jupyter.installed"
:install-application-request-params="{ hostname: applications.jupyter.hostname }" :install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://jupyterhub.readthedocs.io/en/stable/" title-link="https://jupyterhub.readthedocs.io/en/stable/"
...@@ -483,6 +489,7 @@ export default { ...@@ -483,6 +489,7 @@ export default {
:status-reason="applications.knative.statusReason" :status-reason="applications.knative.statusReason"
:request-status="applications.knative.requestStatus" :request-status="applications.knative.requestStatus"
:request-reason="applications.knative.requestReason" :request-reason="applications.knative.requestReason"
:installed="applications.knative.installed"
:install-application-request-params="{ hostname: applications.knative.hostname }" :install-application-request-params="{ hostname: applications.knative.hostname }"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://github.com/knative/docs" title-link="https://github.com/knative/docs"
......
<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>
...@@ -15,9 +15,24 @@ export const APPLICATION_STATUS = { ...@@ -15,9 +15,24 @@ export const APPLICATION_STATUS = {
UPDATING: 'updating', UPDATING: 'updating',
UPDATED: 'updated', UPDATED: 'updated',
UPDATE_ERRORED: 'update_errored', UPDATE_ERRORED: 'update_errored',
UNINSTALLING: 'uninstalling',
UNINSTALL_ERRORED: 'uninstall_errored',
ERROR: '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 // These are only used client-side
export const REQUEST_SUBMITTED = 'request-submitted'; export const REQUEST_SUBMITTED = 'request-submitted';
export const REQUEST_FAILURE = 'request-failure'; export const REQUEST_FAILURE = 'request-failure';
......
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import { parseBoolean } from '../../lib/utils/common_utils'; 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 { export default class ClusterStore {
constructor() { constructor() {
...@@ -12,60 +29,39 @@ export default class ClusterStore { ...@@ -12,60 +29,39 @@ export default class ClusterStore {
statusReason: null, statusReason: null,
applications: { applications: {
helm: { helm: {
...applicationInitialState,
title: s__('ClusterIntegration|Helm Tiller'), title: s__('ClusterIntegration|Helm Tiller'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
}, },
ingress: { ingress: {
...applicationInitialState,
title: s__('ClusterIntegration|Ingress'), title: s__('ClusterIntegration|Ingress'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
externalIp: null, externalIp: null,
externalHostname: null, externalHostname: null,
}, },
cert_manager: { cert_manager: {
...applicationInitialState,
title: s__('ClusterIntegration|Cert-Manager'), title: s__('ClusterIntegration|Cert-Manager'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
email: null, email: null,
}, },
runner: { runner: {
...applicationInitialState,
title: s__('ClusterIntegration|GitLab Runner'), title: s__('ClusterIntegration|GitLab Runner'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
version: null, version: null,
chartRepo: 'https://gitlab.com/charts/gitlab-runner', chartRepo: 'https://gitlab.com/charts/gitlab-runner',
upgradeAvailable: null, upgradeAvailable: null,
}, },
prometheus: { prometheus: {
...applicationInitialState,
title: s__('ClusterIntegration|Prometheus'), title: s__('ClusterIntegration|Prometheus'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
}, },
jupyter: { jupyter: {
...applicationInitialState,
title: s__('ClusterIntegration|JupyterHub'), title: s__('ClusterIntegration|JupyterHub'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
hostname: null, hostname: null,
}, },
knative: { knative: {
...applicationInitialState,
title: s__('ClusterIntegration|Knative'), title: s__('ClusterIntegration|Knative'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
hostname: null, hostname: null,
isEditingHostName: false, isEditingHostName: false,
externalIp: null, externalIp: null,
...@@ -118,6 +114,7 @@ export default class ClusterStore { ...@@ -118,6 +114,7 @@ export default class ClusterStore {
...(this.state.applications[appId] || {}), ...(this.state.applications[appId] || {}),
status, status,
statusReason, statusReason,
installed: isApplicationInstalled(status),
}; };
if (appId === INGRESS) { if (appId === INGRESS) {
......
...@@ -114,10 +114,12 @@ describe('Application Row', () => { ...@@ -114,10 +114,12 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(true); 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, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLED, status: APPLICATION_STATUS.INSTALLED,
installed: true,
uninstallable: false,
}); });
expect(vm.installButtonLabel).toEqual('Installed'); expect(vm.installButtonLabel).toEqual('Installed');
...@@ -125,15 +127,16 @@ describe('Application Row', () => { ...@@ -125,15 +127,16 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(true); expect(vm.installButtonDisabled).toEqual(true);
}); });
it('has disabled "Installed" when APPLICATION_STATUS.UPDATING', () => { it('hides when application is installed and uninstallable', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...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(installBtn).toBe(null);
expect(vm.installButtonLoading).toEqual(false);
expect(vm.installButtonDisabled).toEqual(true);
}); });
it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => { it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => {
...@@ -208,6 +211,19 @@ describe('Application Row', () => { ...@@ -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', () => { describe('Upgrade button', () => {
it('has indeterminate state on page load', () => { it('has indeterminate state on page load', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
......
import ClustersStore from '~/clusters/stores/clusters_store'; 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'; import { CLUSTERS_MOCK_DATA } from '../services/mock_data';
describe('Clusters Store', () => { describe('Clusters Store', () => {
...@@ -70,6 +70,7 @@ describe('Clusters Store', () => { ...@@ -70,6 +70,7 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[0].status_reason, statusReason: mockResponseData.applications[0].status_reason,
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
installed: false,
}, },
ingress: { ingress: {
title: 'Ingress', title: 'Ingress',
...@@ -79,6 +80,7 @@ describe('Clusters Store', () => { ...@@ -79,6 +80,7 @@ describe('Clusters Store', () => {
requestReason: null, requestReason: null,
externalIp: null, externalIp: null,
externalHostname: null, externalHostname: null,
installed: false,
}, },
runner: { runner: {
title: 'GitLab Runner', title: 'GitLab Runner',
...@@ -89,6 +91,7 @@ describe('Clusters Store', () => { ...@@ -89,6 +91,7 @@ describe('Clusters Store', () => {
version: mockResponseData.applications[2].version, version: mockResponseData.applications[2].version,
upgradeAvailable: mockResponseData.applications[2].update_available, upgradeAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/charts/gitlab-runner', chartRepo: 'https://gitlab.com/charts/gitlab-runner',
installed: false,
}, },
prometheus: { prometheus: {
title: 'Prometheus', title: 'Prometheus',
...@@ -96,6 +99,7 @@ describe('Clusters Store', () => { ...@@ -96,6 +99,7 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[3].status_reason, statusReason: mockResponseData.applications[3].status_reason,
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
installed: false,
}, },
jupyter: { jupyter: {
title: 'JupyterHub', title: 'JupyterHub',
...@@ -104,6 +108,7 @@ describe('Clusters Store', () => { ...@@ -104,6 +108,7 @@ describe('Clusters Store', () => {
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
hostname: '', hostname: '',
installed: false,
}, },
knative: { knative: {
title: 'Knative', title: 'Knative',
...@@ -115,6 +120,7 @@ describe('Clusters Store', () => { ...@@ -115,6 +120,7 @@ describe('Clusters Store', () => {
isEditingHostName: false, isEditingHostName: false,
externalIp: null, externalIp: null,
externalHostname: null, externalHostname: null,
installed: false,
}, },
cert_manager: { cert_manager: {
title: 'Cert-Manager', title: 'Cert-Manager',
...@@ -123,11 +129,26 @@ describe('Clusters Store', () => { ...@@ -123,11 +129,26 @@ describe('Clusters Store', () => {
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
email: mockResponseData.applications[6].email, 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', () => { it('sets default hostname for jupyter when ingress has a ip address', () => {
const mockResponseData = const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data; CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment