Commit 8034d08e authored by Andrei Stoicescu's avatar Andrei Stoicescu

Add data loss warning on Elastic Stack upgrade

  - add confirmation message feature for cluster
application upgrade
  - add confirmation for Elastic Stack upgrade that
includes warning of data loss
parent bed0ac5c
...@@ -8,8 +8,9 @@ import identicon from '../../vue_shared/components/identicon.vue'; ...@@ -8,8 +8,9 @@ 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 UninstallApplicationButton from './uninstall_application_button.vue';
import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue'; import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
import UpdateApplicationConfirmationModal from './update_application_confirmation_modal.vue';
import { APPLICATION_STATUS } from '../constants'; import { APPLICATION_STATUS, ELASTIC_STACK } from '../constants';
export default { export default {
components: { components: {
...@@ -18,6 +19,7 @@ export default { ...@@ -18,6 +19,7 @@ export default {
GlLink, GlLink,
UninstallApplicationButton, UninstallApplicationButton,
UninstallApplicationConfirmationModal, UninstallApplicationConfirmationModal,
UpdateApplicationConfirmationModal,
}, },
directives: { directives: {
GlModalDirective, GlModalDirective,
...@@ -233,6 +235,17 @@ export default { ...@@ -233,6 +235,17 @@ export default {
return label; return label;
}, },
updatingNeedsConfirmation() {
if (this.version) {
const majorVersion = parseInt(this.version.split('.')[0], 10);
if (!Number.isNaN(majorVersion)) {
return this.id === ELASTIC_STACK && majorVersion < 3;
}
}
return false;
},
isUpdating() { isUpdating() {
// Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend // 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; return this.status === APPLICATION_STATUS.UPDATING;
...@@ -248,6 +261,12 @@ export default { ...@@ -248,6 +261,12 @@ export default {
title: this.title, title: this.title,
}); });
}, },
updateModalId() {
return `update-${this.id}`;
},
uninstallModalId() {
return `uninstall-${this.id}`;
},
}, },
watch: { watch: {
updateSuccessful(updateSuccessful) { updateSuccessful(updateSuccessful) {
...@@ -268,7 +287,7 @@ export default { ...@@ -268,7 +287,7 @@ export default {
params: this.installApplicationRequestParams, params: this.installApplicationRequestParams,
}); });
}, },
updateClicked() { updateConfirmed() {
eventHub.$emit('updateApplication', { eventHub.$emit('updateApplication', {
id: this.id, id: this.id,
params: this.installApplicationRequestParams, params: this.installApplicationRequestParams,
...@@ -356,14 +375,36 @@ export default { ...@@ -356,14 +375,36 @@ export default {
> >
{{ updateFailureDescription }} {{ updateFailureDescription }}
</div> </div>
<template v-if="updateAvailable || updateFailed || isUpdating">
<template v-if="updatingNeedsConfirmation">
<loading-button
v-gl-modal-directive="updateModalId"
class="btn btn-primary js-cluster-application-update-button mt-2"
:loading="isUpdating"
:disabled="isUpdating"
:label="updateButtonLabel"
data-qa-selector="update_button_with_confirmation"
:data-qa-application="id"
/>
<update-application-confirmation-modal
:application="id"
:application-title="title"
@confirm="updateConfirmed()"
/>
</template>
<loading-button <loading-button
v-if="updateAvailable || updateFailed || isUpdating" v-else
class="btn btn-primary js-cluster-application-update-button mt-2" class="btn btn-primary js-cluster-application-update-button mt-2"
:loading="isUpdating" :loading="isUpdating"
:disabled="isUpdating" :disabled="isUpdating"
:label="updateButtonLabel" :label="updateButtonLabel"
@click="updateClicked" data-qa-selector="update_button"
:data-qa-application="id"
@click="updateConfirmed"
/> />
</template>
</div> </div>
</div> </div>
<div <div
...@@ -389,7 +430,7 @@ export default { ...@@ -389,7 +430,7 @@ export default {
/> />
<uninstall-application-button <uninstall-application-button
v-if="displayUninstallButton" v-if="displayUninstallButton"
v-gl-modal-directive="'uninstall-' + id" v-gl-modal-directive="uninstallModalId"
:status="status" :status="status"
data-qa-selector="uninstall_button" data-qa-selector="uninstall_button"
:data-qa-application="id" :data-qa-application="id"
......
<script>
import { GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import { ELASTIC_STACK } from '../constants';
const CUSTOM_APP_WARNING_TEXT = {
[ELASTIC_STACK]: s__(
'ClusterIntegration|Your Elasticsearch cluster will be re-created during this upgrade. Your logs will be re-indexed, and you will lose historical logs from hosts terminated in the last 30 days.',
),
};
export default {
components: {
GlModal,
},
props: {
application: {
type: String,
required: true,
},
applicationTitle: {
type: String,
required: true,
},
},
computed: {
title() {
return sprintf(s__('ClusterIntegration|Update %{appTitle}'), {
appTitle: this.applicationTitle,
});
},
warningText() {
return sprintf(
s__('ClusterIntegration|You are about to update %{appTitle} on your cluster.'),
{
appTitle: this.applicationTitle,
},
);
},
customAppWarningText() {
return CUSTOM_APP_WARNING_TEXT[this.application];
},
modalId() {
return `update-${this.application}`;
},
},
methods: {
confirmUpdate() {
this.$emit('confirm');
},
},
};
</script>
<template>
<gl-modal
ok-variant="danger"
cancel-variant="light"
:ok-title="title"
:modal-id="modalId"
:title="title"
@ok="confirmUpdate()"
>
{{ warningText }} <span v-html="customAppWarningText"></span>
</gl-modal>
</template>
---
title: Add warning popup for Elastic Stack update
merge_request: 31972
author:
type: added
...@@ -5304,6 +5304,9 @@ msgstr "" ...@@ -5304,6 +5304,9 @@ msgstr ""
msgid "ClusterIntegration|Uninstall %{appTitle}" msgid "ClusterIntegration|Uninstall %{appTitle}"
msgstr "" msgstr ""
msgid "ClusterIntegration|Update %{appTitle}"
msgstr ""
msgid "ClusterIntegration|Update failed. Please check the logs and try again." msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr "" msgstr ""
...@@ -5334,6 +5337,9 @@ msgstr "" ...@@ -5334,6 +5337,9 @@ msgstr ""
msgid "ClusterIntegration|You are about to uninstall %{appTitle} from your cluster." msgid "ClusterIntegration|You are about to uninstall %{appTitle} from your cluster."
msgstr "" msgstr ""
msgid "ClusterIntegration|You are about to update %{appTitle} on your cluster."
msgstr ""
msgid "ClusterIntegration|You must first install Helm Tiller before installing the applications below" msgid "ClusterIntegration|You must first install Helm Tiller before installing the applications below"
msgstr "" msgstr ""
...@@ -5346,6 +5352,9 @@ msgstr "" ...@@ -5346,6 +5352,9 @@ msgstr ""
msgid "ClusterIntegration|You should select at least two subnets" msgid "ClusterIntegration|You should select at least two subnets"
msgstr "" msgstr ""
msgid "ClusterIntegration|Your Elasticsearch cluster will be re-created during this upgrade. Your logs will be re-indexed, and you will lose historical logs from hosts terminated in the last 30 days."
msgstr ""
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr "" msgstr ""
......
...@@ -2,9 +2,10 @@ import Vue from 'vue'; ...@@ -2,9 +2,10 @@ import Vue from 'vue';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import mountComponent from 'helpers/vue_mount_component_helper'; import mountComponent from 'helpers/vue_mount_component_helper';
import eventHub from '~/clusters/event_hub'; import eventHub from '~/clusters/event_hub';
import { APPLICATION_STATUS } from '~/clusters/constants'; import { APPLICATION_STATUS, ELASTIC_STACK } from '~/clusters/constants';
import applicationRow from '~/clusters/components/application_row.vue'; import applicationRow from '~/clusters/components/application_row.vue';
import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue'; import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data'; import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
...@@ -350,6 +351,126 @@ describe('Application Row', () => { ...@@ -350,6 +351,126 @@ describe('Application Row', () => {
expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner updated successfully.'); expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner updated successfully.');
}); });
}); });
describe('when updating does not require confirmation', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(ApplicationRow, {
propsData: {
...DEFAULT_APPLICATION_STATE,
updateAvailable: true,
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('the modal is not rendered', () => {
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(false);
});
it('the correct button is rendered', () => {
expect(wrapper.contains("[data-qa-selector='update_button']")).toBe(true);
});
});
describe('when updating requires confirmation', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(ApplicationRow, {
propsData: {
...DEFAULT_APPLICATION_STATE,
updateAvailable: true,
id: ELASTIC_STACK,
version: '1.1.2',
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('displays a modal', () => {
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(true);
});
it('the correct button is rendered', () => {
expect(wrapper.contains("[data-qa-selector='update_button_with_confirmation']")).toBe(true);
});
it('triggers updateApplication event', () => {
jest.spyOn(eventHub, '$emit');
wrapper.find(UpdateApplicationConfirmationModal).vm.$emit('confirm');
expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
id: ELASTIC_STACK,
params: {},
});
});
});
describe('updating Elastic Stack special case', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
});
it('needs confirmation if version is lower than 3.0.0', () => {
wrapper = shallowMount(ApplicationRow, {
propsData: {
...DEFAULT_APPLICATION_STATE,
updateAvailable: true,
id: ELASTIC_STACK,
version: '1.1.2',
},
});
wrapper.vm.$nextTick(() => {
expect(wrapper.contains("[data-qa-selector='update_button_with_confirmation']")).toBe(
true,
);
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(true);
});
});
it('does not need confirmation is version is 3.0.0', () => {
wrapper = shallowMount(ApplicationRow, {
propsData: {
...DEFAULT_APPLICATION_STATE,
updateAvailable: true,
id: ELASTIC_STACK,
version: '3.0.0',
},
});
wrapper.vm.$nextTick(() => {
expect(wrapper.contains("[data-qa-selector='update_button']")).toBe(true);
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(false);
});
});
it('does not need confirmation if version is higher than 3.0.0', () => {
wrapper = shallowMount(ApplicationRow, {
propsData: {
...DEFAULT_APPLICATION_STATE,
updateAvailable: true,
id: ELASTIC_STACK,
version: '5.2.1',
},
});
wrapper.vm.$nextTick(() => {
expect(wrapper.contains("[data-qa-selector='update_button']")).toBe(true);
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(false);
});
});
});
}); });
describe('Version', () => { describe('Version', () => {
......
import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue';
import { ELASTIC_STACK } from '~/clusters/constants';
describe('UpdateApplicationConfirmationModal', () => {
let wrapper;
const appTitle = 'Elastic stack';
const createComponent = (props = {}) => {
wrapper = shallowMount(UpdateApplicationConfirmationModal, {
propsData: { ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
createComponent({ application: ELASTIC_STACK, applicationTitle: appTitle });
});
it(`renders a modal with a title "Update ${appTitle}"`, () => {
expect(wrapper.find(GlModal).attributes('title')).toEqual(`Update ${appTitle}`);
});
it(`renders a modal with an ok button labeled "Update ${appTitle}"`, () => {
expect(wrapper.find(GlModal).attributes('ok-title')).toEqual(`Update ${appTitle}`);
});
describe('when ok button is clicked', () => {
beforeEach(() => {
wrapper.find(GlModal).vm.$emit('ok');
});
it('emits confirm event', () =>
wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('confirm')).toBeTruthy();
}));
it('displays a warning text indicating the app will be updated', () => {
expect(wrapper.text()).toContain(`You are about to update ${appTitle} on your cluster.`);
});
it('displays a custom warning text depending on the application', () => {
expect(wrapper.text()).toContain(
`Your Elasticsearch cluster will be re-created during this upgrade. Your logs will be re-indexed, and you will lose historical logs from hosts terminated in the last 30 days.`,
);
});
});
});
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