Commit bf229a6c authored by Enrique Alcántara's avatar Enrique Alcántara 🌴 Committed by Phil Hughes

Uninstall application confirm modal component

- Vue confirmation modal implementation
- CSS tweaks for modal default height
parent d6aa8a05
......@@ -132,6 +132,7 @@ export default class Clusters {
eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
}
removeListeners() {
......@@ -141,6 +142,7 @@ export default class Clusters {
eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname');
eventHub.$off('uninstallApplication');
}
initPolling() {
......@@ -249,14 +251,13 @@ export default class Clusters {
}
}
installApplication(data) {
const appId = data.id;
installApplication({ id: appId, params }) {
this.store.updateAppProperty(appId, 'requestReason', null);
this.store.updateAppProperty(appId, 'statusReason', null);
this.store.installApplication(appId);
return this.service.installApplication(appId, data.params).catch(() => {
return this.service.installApplication(appId, params).catch(() => {
this.store.notifyInstallFailure(appId);
this.store.updateAppProperty(
appId,
......@@ -266,6 +267,22 @@ export default class Clusters {
});
}
uninstallApplication({ id: appId }) {
this.store.updateAppProperty(appId, 'requestReason', null);
this.store.updateAppProperty(appId, 'statusReason', null);
this.store.uninstallApplication(appId);
return this.service.uninstallApplication(appId).catch(() => {
this.store.notifyUninstallFailure(appId);
this.store.updateAppProperty(
appId,
'requestReason',
s__('ClusterIntegration|Request to begin uninstalling failed'),
);
});
}
upgradeApplication(data) {
const appId = data.id;
......
<script>
/* eslint-disable vue/require-default-prop */
import { GlLink } from '@gitlab/ui';
import { GlLink, GlModalDirective } from '@gitlab/ui';
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
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 UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
import { APPLICATION_STATUS } from '../constants';
......@@ -17,6 +18,10 @@ export default {
TimeagoTooltip,
GlLink,
UninstallApplicationButton,
UninstallApplicationConfirmationModal,
},
directives: {
GlModalDirective,
},
props: {
id: {
......@@ -94,6 +99,16 @@ export default {
required: false,
default: false,
},
uninstallFailed: {
type: Boolean,
required: false,
default: false,
},
uninstallSuccessful: {
type: Boolean,
required: false,
default: false,
},
updateAcknowledged: {
type: Boolean,
required: false,
......@@ -170,10 +185,21 @@ export default {
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
hasError() {
return this.installFailed || this.uninstallFailed;
},
generalErrorDescription() {
return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
title: this.title,
});
let errorDescription;
if (this.installFailed) {
errorDescription = s__('ClusterIntegration|Something went wrong while installing %{title}');
} else if (this.uninstallFailed) {
errorDescription = s__(
'ClusterIntegration|Something went wrong while uninstalling %{title}',
);
}
return sprintf(errorDescription, { title: this.title });
},
versionLabel() {
if (this.updateFailed) {
......@@ -214,13 +240,23 @@ export default {
// AND new upgrade is unavailable AND version information is present.
return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version;
},
uninstallSuccessDescription() {
return sprintf(s__('ClusterIntegration|%{title} uninstalled successfully.'), {
title: this.title,
});
},
},
watch: {
updateSuccessful() {
if (this.updateSuccessful) {
updateSuccessful(updateSuccessful) {
if (updateSuccessful) {
this.$toast.show(this.upgradeSuccessDescription);
}
},
uninstallSuccessful(uninstallSuccessful) {
if (uninstallSuccessful) {
this.$toast.show(this.uninstallSuccessDescription);
}
},
},
methods: {
installClicked() {
......@@ -235,6 +271,11 @@ export default {
params: this.installApplicationRequestParams,
});
},
uninstallConfirmed() {
eventHub.$emit('uninstallApplication', {
id: this.id,
});
},
},
};
</script>
......@@ -271,10 +312,7 @@ export default {
<span v-else class="js-cluster-application-title">{{ title }}</span>
</strong>
<slot name="description"></slot>
<div
v-if="installFailed || isUnknownStatus"
class="cluster-application-error text-danger prepend-top-10"
>
<div v-if="hasError" class="cluster-application-error text-danger prepend-top-10">
<p class="js-cluster-application-general-error-message append-bottom-0">
{{ generalErrorDescription }}
</p>
......@@ -325,9 +363,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
......@@ -340,8 +378,15 @@ export default {
/>
<uninstall-application-button
v-if="displayUninstallButton"
v-gl-modal-directive="'uninstall-' + id"
:status="status"
class="js-cluster-application-uninstall-button"
/>
<uninstall-application-confirmation-modal
:application="id"
:application-title="title"
@confirm="uninstallConfirmed()"
/>
</div>
</div>
</div>
......
......@@ -240,6 +240,9 @@ export default {
:request-reason="applications.helm.requestReason"
:installed="applications.helm.installed"
:install-failed="applications.helm.installFailed"
:uninstallable="applications.helm.uninstallable"
:uninstall-successful="applications.helm.uninstallSuccessful"
:uninstall-failed="applications.helm.uninstallFailed"
class="rounded-top"
title-link="https://docs.helm.sh/"
>
......@@ -269,6 +272,9 @@ export default {
:request-reason="applications.ingress.requestReason"
:installed="applications.ingress.installed"
:install-failed="applications.ingress.installFailed"
:uninstallable="applications.ingress.uninstallable"
:uninstall-successful="applications.ingress.uninstallSuccessful"
:uninstall-failed="applications.ingress.uninstallFailed"
:disabled="!helmInstalled"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
>
......@@ -312,9 +318,9 @@ export default {
generated endpoint in order to access
your application after it has been deployed.`)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
__('More information')
}}</a>
</p>
</div>
......@@ -324,9 +330,9 @@ export default {
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>
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
__('More information')
}}</a>
</p>
</template>
<template v-if="!ingressInstalled">
......@@ -345,6 +351,9 @@ export default {
:installed="applications.cert_manager.installed"
:install-failed="applications.cert_manager.installFailed"
:install-application-request-params="{ email: applications.cert_manager.email }"
:uninstallable="applications.cert_manager.uninstallable"
:uninstall-successful="applications.cert_manager.uninstallSuccessful"
:uninstall-failed="applications.cert_manager.uninstallFailed"
:disabled="!helmInstalled"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
>
......@@ -352,9 +361,9 @@ export default {
<div slot="description">
<p v-html="certManagerDescription"></p>
<div class="form-group">
<label for="cert-manager-issuer-email">
{{ s__('ClusterIntegration|Issuer Email') }}
</label>
<label for="cert-manager-issuer-email">{{
s__('ClusterIntegration|Issuer Email')
}}</label>
<div class="input-group">
<input
v-model="applications.cert_manager.email"
......@@ -391,6 +400,9 @@ export default {
:request-reason="applications.prometheus.requestReason"
:installed="applications.prometheus.installed"
:install-failed="applications.prometheus.installFailed"
:uninstallable="applications.prometheus.uninstallable"
:uninstall-successful="applications.prometheus.uninstallSuccessful"
:uninstall-failed="applications.prometheus.uninstallFailed"
:disabled="!helmInstalled"
title-link="https://prometheus.io/docs/introduction/overview/"
>
......@@ -411,6 +423,9 @@ export default {
:install-failed="applications.runner.installFailed"
:update-successful="applications.runner.updateSuccessful"
:update-failed="applications.runner.updateFailed"
:uninstallable="applications.runner.uninstallable"
:uninstall-successful="applications.runner.uninstallSuccessful"
:uninstall-failed="applications.runner.uninstallFailed"
:disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/"
>
......@@ -434,6 +449,9 @@ export default {
:request-reason="applications.jupyter.requestReason"
:installed="applications.jupyter.installed"
:install-failed="applications.jupyter.installFailed"
:uninstallable="applications.jupyter.uninstallable"
:uninstall-successful="applications.jupyter.uninstallSuccessful"
:uninstall-failed="applications.jupyter.uninstallFailed"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
......@@ -474,9 +492,9 @@ export default {
s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
__('More information')
}}</a>
</p>
</div>
</template>
......@@ -494,6 +512,9 @@ export default {
:installed="applications.knative.installed"
:install-failed="applications.knative.installFailed"
:install-application-request-params="{ hostname: applications.knative.hostname }"
:uninstallable="applications.knative.uninstallable"
:uninstall-successful="applications.knative.uninstallSuccessful"
:uninstall-failed="applications.knative.uninstallFailed"
:disabled="!helmInstalled"
v-bind="applications.knative"
title-link="https://github.com/knative/docs"
......@@ -505,9 +526,9 @@ export default {
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
to install Knative.`)
}}
<a :href="helpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
<a :href="helpPath" target="_blank" rel="noopener noreferrer">{{
__('More information')
}}</a>
</p>
<br />
</span>
......@@ -572,9 +593,9 @@ export default {
`ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
__('More information')
}}</a>
</p>
<p
......
<script>
// TODO: Implement loading button component
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
LoadingButton,
},
props: {
status: {
type: String,
required: true,
},
},
computed: {
disabled() {
return [UNINSTALLING, UPDATING].includes(this.status);
},
loading() {
return this.status === UNINSTALLING;
},
label() {
return this.loading ? this.__('Uninstalling') : this.__('Uninstall');
},
},
};
</script>
<template>
<loading-button @click="$emit('click')" />
<loading-button :label="label" :disabled="disabled" :loading="loading" />
</template>
<script>
import { GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import { INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants';
const CUSTOM_APP_WARNING_TEXT = {
[INGRESS]: s__(
'ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored.',
),
[CERT_MANAGER]: s__(
'ClusterIntegration|The associated certifcate will be deleted and cannot be restored.',
),
[PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
[RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'),
[KNATIVE]: s__('ClusterIntegration|The associated IP will be deleted and cannot be restored.'),
[JUPYTER]: '',
};
export default {
components: {
GlModal,
},
props: {
application: {
type: String,
required: true,
},
applicationTitle: {
type: String,
required: true,
},
},
computed: {
title() {
return sprintf(s__('ClusterIntegration|Uninstall %{appTitle}'), {
appTitle: this.applicationTitle,
});
},
warningText() {
return sprintf(
s__('ClusterIntegration|You are about to uninstall %{appTitle} from your cluster.'),
{
appTitle: this.applicationTitle,
},
);
},
customAppWarningText() {
return CUSTOM_APP_WARNING_TEXT[this.application];
},
modalId() {
return `uninstall-${this.application}`;
},
},
};
</script>
<template>
<gl-modal
ok-variant="danger"
cancel-variant="light"
:ok-title="title"
:modal-id="modalId"
:title="title"
@ok="$emit('confirm')"
>{{ warningText }} {{ customAppWarningText }}</gl-modal
>
</template>
......@@ -28,16 +28,23 @@ export const APPLICATION_STATUS = {
export const APPLICATION_INSTALLED_STATUSES = [
APPLICATION_STATUS.INSTALLED,
APPLICATION_STATUS.UPDATING,
APPLICATION_STATUS.UNINSTALLING,
];
// These are only used client-side
export const UPDATE_EVENT = 'update';
export const INSTALL_EVENT = 'install';
export const UNINSTALL_EVENT = 'uninstall';
export const HELM = 'helm';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager';
export const PROMETHEUS = 'prometheus';
export const APPLICATIONS = [HELM, INGRESS, JUPYTER, KNATIVE, RUNNER, CERT_MANAGER, PROMETHEUS];
export const INGRESS_DOMAIN_SUFFIX = '.nip.io';
import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '../constants';
import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT, UNINSTALL_EVENT } from '../constants';
const {
NO_STATUS,
......@@ -11,6 +11,8 @@ const {
UPDATING,
UPDATED,
UPDATE_ERRORED,
UNINSTALLING,
UNINSTALL_ERRORED,
} = APPLICATION_STATUS;
const applicationStateMachine = {
......@@ -52,6 +54,15 @@ const applicationStateMachine = {
updateFailed: true,
},
},
[UNINSTALLING]: {
target: UNINSTALLING,
},
[UNINSTALL_ERRORED]: {
target: INSTALLED,
effects: {
uninstallFailed: true,
},
},
},
},
[NOT_INSTALLABLE]: {
......@@ -97,6 +108,13 @@ const applicationStateMachine = {
updateSuccessful: false,
},
},
[UNINSTALL_EVENT]: {
target: UNINSTALLING,
effects: {
uninstallFailed: false,
uninstallSuccessful: false,
},
},
},
},
[UPDATING]: {
......@@ -116,6 +134,22 @@ const applicationStateMachine = {
},
},
},
[UNINSTALLING]: {
on: {
[INSTALLABLE]: {
target: INSTALLABLE,
effects: {
uninstallSuccessful: true,
},
},
[UNINSTALL_ERRORED]: {
target: INSTALLED,
effects: {
uninstallFailed: true,
},
},
},
},
};
/**
......
......@@ -29,6 +29,10 @@ export default class ClusterService {
return axios.patch(this.appUpdateEndpointMap[appId], params);
}
uninstallApplication(appId, params) {
return axios.delete(this.appInstallEndpointMap[appId], params);
}
static updateCluster(endpoint, data) {
return axios.put(endpoint, data);
}
......
......@@ -10,6 +10,7 @@ import {
APPLICATION_STATUS,
INSTALL_EVENT,
UPDATE_EVENT,
UNINSTALL_EVENT,
} from '../constants';
import transitionApplicationState from '../services/application_state_machine';
......@@ -21,6 +22,9 @@ const applicationInitialState = {
requestReason: null,
installed: false,
installFailed: false,
uninstallable: false,
uninstallFailed: false,
uninstallSuccessful: false,
};
export default class ClusterStore {
......@@ -116,6 +120,14 @@ export default class ClusterStore {
this.handleApplicationEvent(appId, APPLICATION_STATUS.UPDATE_ERRORED);
}
uninstallApplication(appId) {
this.handleApplicationEvent(appId, UNINSTALL_EVENT);
}
notifyUninstallFailure(appId) {
this.handleApplicationEvent(appId, APPLICATION_STATUS.UNINSTALL_ERRORED);
}
handleApplicationEvent(appId, event) {
const currentAppState = this.state.applications[appId];
......@@ -141,6 +153,7 @@ export default class ClusterStore {
status_reason: statusReason,
version,
update_available: upgradeAvailable,
can_uninstall: uninstallable,
} = serverAppEntry;
const currentApplicationState = this.state.applications[appId] || {};
const nextApplicationState = transitionApplicationState(currentApplicationState, status);
......@@ -150,8 +163,7 @@ export default class ClusterStore {
...nextApplicationState,
statusReason,
installed: isApplicationInstalled(nextApplicationState.status),
// Make sure uninstallable is always false until this feature is unflagged
uninstallable: false,
uninstallable,
};
if (appId === INGRESS) {
......
......@@ -34,10 +34,10 @@
.modal-body {
background-color: $modal-body-bg;
line-height: $line-height-base;
min-height: $modal-body-height;
position: relative;
padding: #{3 * $grid-size} #{2 * $grid-size};
text-align: left;
white-space: normal;
.form-actions {
margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size};
......
---
title: Implement UI for uninstalling Cluster’s managed apps
merge_request: 27559
author:
type: added
......@@ -1984,6 +1984,9 @@ msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|%{title} uninstalled successfully."
msgstr ""
msgid "ClusterIntegration|%{title} upgraded successfully."
msgstr ""
......@@ -2011,6 +2014,9 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
msgid "ClusterIntegration|All data will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|Alternatively"
msgstr ""
......@@ -2026,6 +2032,9 @@ msgstr ""
msgid "ClusterIntegration|An error occurred while trying to fetch zone machine types: %{error}"
msgstr ""
msgid "ClusterIntegration|Any running pipelines will be canceled."
msgstr ""
msgid "ClusterIntegration|Applications"
msgstr ""
......@@ -2317,6 +2326,9 @@ msgstr ""
msgid "ClusterIntegration|Request to begin installing failed"
msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
msgid "ClusterIntegration|Retry update"
msgstr ""
......@@ -2371,6 +2383,9 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr ""
msgid "ClusterIntegration|Something went wrong while uninstalling %{title}"
msgstr ""
msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain."
msgstr ""
......@@ -2380,6 +2395,15 @@ msgstr ""
msgid "ClusterIntegration|The URL used to access the Kubernetes API."
msgstr ""
msgid "ClusterIntegration|The associated IP will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|The associated certifcate will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored."
msgstr ""
msgid "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."
msgstr ""
......@@ -2395,6 +2419,9 @@ msgstr ""
msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Uninstall %{appTitle}"
msgstr ""
msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr ""
......@@ -2422,6 +2449,9 @@ msgstr ""
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
msgid "ClusterIntegration|You are about to uninstall %{appTitle} from your cluster."
msgstr ""
msgid "ClusterIntegration|You must first install Helm Tiller before installing the applications below"
msgstr ""
......
import Clusters from '~/clusters/clusters_bundle';
import { APPLICATION_STATUS, INGRESS_DOMAIN_SUFFIX } from '~/clusters/constants';
import { APPLICATION_STATUS, INGRESS_DOMAIN_SUFFIX, APPLICATIONS } 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';