Commit 3c0ef74c authored by Simon Knox's avatar Simon Knox Committed by Paul Slaughter

Add Vue component for Alerts Service

**Please note:**

- Feature flag (`generic_alert_endpoint`)
- Part of https://gitlab.com/gitlab-org/gitlab-ee/issues/13203

The gross alternative was to add more haml field types, and inject
just a Vue button. As shown in [this mr][1], this is not a good way.

[1]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/16029
parent 79df819e
<script>
import { GlButton, GlFormGroup, GlFormInput, GlModal, GlModalDirective } from '@gitlab/ui';
import _ from 'underscore';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import axios from '~/lib/utils/axios_utils';
import { s__, __, sprintf } from '~/locale';
import createFlash from '~/flash';
export default {
COPY_TO_CLIPBOARD: __('Copy to clipboard'),
RESET_KEY: __('Reset key'),
components: {
GlButton,
GlFormGroup,
GlFormInput,
GlModal,
ClipboardButton,
ToggleButton,
},
directives: {
'gl-modal': GlModalDirective,
},
props: {
initialAuthorizationKey: {
type: String,
required: false,
default: '',
},
formPath: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
learnMoreUrl: {
type: String,
required: false,
default: '',
},
initialActivated: {
type: Boolean,
required: true,
},
},
data() {
return {
activated: this.initialActivated,
loadingActivated: false,
authorizationKey: this.initialAuthorizationKey,
};
},
computed: {
learnMoreDescription() {
if (!this.learnMoreUrl) {
return '';
}
return sprintf(
s__(
'AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts.',
),
{
linkStart: `<a href="${_.escape(
this.learnMoreUrl,
)}" target="_blank" rel="noopener noreferrer">`,
linkEnd: '</a>',
},
false,
);
},
sectionDescription() {
const desc = s__(
'AlertService|Each alert source must be authorized using the following URL and authorization key.',
);
const learnMoreDesc = this.learnMoreDescription ? ` ${this.learnMoreDescription}` : '';
return `${desc}${learnMoreDesc}`;
},
},
methods: {
resetKey() {
return axios
.put(this.formPath, { service: { token: '' } })
.then(res => {
this.authorizationKey = res.data.token;
})
.catch(() => {
createFlash(__('Failed to reset key. Please try again.'));
});
},
toggleActivated(value) {
this.loadingActivated = true;
return axios
.put(this.formPath, { service: { active: value } })
.then(() => {
this.activated = value;
this.loadingActivated = false;
})
.catch(() => {
createFlash(__('Update failed. Please try again.'));
this.loadingActivated = false;
});
},
},
};
</script>
<template>
<div>
<p v-html="sectionDescription"></p>
<gl-form-group :label="__('Active')" label-for="activated" label-class="label-bold">
<toggle-button
id="activated"
:disabled-input="loadingActivated"
:is-loading="loadingActivated"
:value="activated"
@change="toggleActivated"
/>
</gl-form-group>
<gl-form-group :label="__('URL')" label-for="url" label-class="label-bold">
<div class="input-group">
<gl-form-input id="url" :readonly="true" :value="url" />
<span class="input-group-append">
<clipboard-button :text="url" :title="$options.COPY_TO_CLIPBOARD" />
</span>
</div>
</gl-form-group>
<gl-form-group
:label="__('Authorization key')"
label-for="authorization-key"
label-class="label-bold"
>
<div class="input-group">
<gl-form-input id="authorization-key" :readonly="true" :value="authorizationKey" />
<span class="input-group-append">
<clipboard-button :text="authorizationKey" :title="$options.COPY_TO_CLIPBOARD" />
</span>
</div>
<gl-button v-gl-modal.authKeyModal class="mt-2">{{ $options.RESET_KEY }}</gl-button>
<gl-modal
modal-id="authKeyModal"
:title="$options.RESET_KEY"
:ok-title="$options.RESET_KEY"
ok-variant="danger"
@ok="resetKey"
>
{{
__(
'Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.',
)
}}
</gl-modal>
</gl-form-group>
</div>
</template>
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import AlertsServiceForm from './components/alerts_service_form.vue';
export default el => {
if (!el) {
return null;
}
const { activated: activatedStr, formPath, authorizationKey, url, learnMoreUrl } = el.dataset;
const activated = parseBoolean(activatedStr);
return new Vue({
el,
render(createElement) {
return createElement(AlertsServiceForm, {
props: {
initialActivated: activated,
formPath,
learnMoreUrl,
initialAuthorizationKey: authorizationKey,
url,
},
});
},
});
};
import IntegrationSettingsForm from '~/integrations/integration_settings_form'; import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import PrometheusMetrics from 'ee/prometheus_metrics/prometheus_metrics'; import PrometheusMetrics from 'ee/prometheus_metrics/prometheus_metrics';
import PrometheusAlerts from 'ee/prometheus_alerts'; import PrometheusAlerts from 'ee/prometheus_alerts';
import initAlertsSettings from 'ee/alerts_service_settings';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
...@@ -17,4 +18,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -17,4 +18,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
PrometheusAlerts(); PrometheusAlerts();
initAlertsSettings(document.querySelector('.js-alerts-service-settings'));
}); });
- return unless Feature.enabled?(:generic_alert_endpoint, @project)
.js-alerts-service-settings{ data: { activated: @service.activated?.to_s,
form_path: project_service_path(@project, @service.to_param),
authorization_key: @service.token, url: @service.url } }
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertsServiceForm with default values renders "authorization-key" input 1`] = `"<glforminput-stub id=\\"authorization-key\\" readonly=\\"true\\" value=\\"abcedfg123\\"></glforminput-stub>"`;
exports[`AlertsServiceForm with default values renders "url" input 1`] = `"<glforminput-stub id=\\"url\\" readonly=\\"true\\" value=\\"https://gitlab.com/endpoint-url\\"></glforminput-stub>"`;
exports[`AlertsServiceForm with default values renders toggle button 1`] = `"<togglebutton-stub id=\\"activated\\"></togglebutton-stub>"`;
exports[`AlertsServiceForm with default values shows description and "Learn More" link 1`] = `"Each alert source must be authorized using the following URL and authorization key. <a href=\\"example.com/learn-more\\" target=\\"_blank\\" rel=\\"noopener noreferrer\\">Learn more</a> about configuring this endpoint to receive alerts."`;
exports[`AlertsServiceForm without learnMoreUrl shows description but not "Learn More" link 1`] = `"Each alert source must be authorized using the following URL and authorization key."`;
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import AlertsServiceForm from 'ee/alerts_service_settings/components/alerts_service_form.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import createFlash from '~/flash';
jest.mock('~/flash');
const localVue = createLocalVue();
const defaultProps = {
initialAuthorizationKey: 'abcedfg123',
formPath: 'http://invalid',
url: 'https://gitlab.com/endpoint-url',
learnMoreUrl: 'example.com/learn-more',
initialActivated: false,
};
describe('AlertsServiceForm', () => {
let wrapper;
let mockAxios;
const createComponent = (props = defaultProps, { methods } = {}) => {
wrapper = shallowMount(localVue.extend(AlertsServiceForm), {
localVue,
propsData: {
...defaultProps,
...props,
},
methods,
});
};
const findUrl = () => wrapper.find('#url');
const findAuthorizationKey = () => wrapper.find('#authorization-key');
const findDescription = () => wrapper.find('p');
beforeEach(() => {
mockAxios = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
mockAxios.restore();
});
describe('with default values', () => {
beforeEach(() => {
createComponent();
});
it('renders "url" input', () => {
expect(findUrl().html()).toMatchSnapshot();
});
it('renders "authorization-key" input', () => {
expect(findAuthorizationKey().html()).toMatchSnapshot();
});
it('renders toggle button', () => {
expect(wrapper.find(ToggleButton).html()).toMatchSnapshot();
});
it('shows description and "Learn More" link', () => {
expect(findDescription().element.innerHTML).toMatchSnapshot();
});
});
describe('without learnMoreUrl', () => {
beforeEach(() => {
createComponent({ learnMoreUrl: '' });
});
it('shows description but not "Learn More" link', () => {
expect(findDescription().element.innerHTML).toMatchSnapshot();
});
});
describe('reset key', () => {
it('triggers resetKey method', () => {
const resetKey = jest.fn();
const methods = { resetKey };
createComponent(defaultProps, { methods });
wrapper.find(GlModal).vm.$emit('ok');
expect(resetKey).toHaveBeenCalled();
});
it('updates the authorization key on success', () => {
const formPath = 'some/path';
mockAxios.onPut(formPath, { service: { token: '' } }).replyOnce(200, { token: 'newToken' });
createComponent({ formPath });
return wrapper.vm.resetKey().then(() => {
expect(findAuthorizationKey().attributes('value')).toBe('newToken');
});
});
it('shows flash message on error', () => {
const formPath = 'some/path';
mockAxios.onPut(formPath).replyOnce(404);
createComponent({ formPath });
return wrapper.vm.resetKey().then(() => {
expect(findAuthorizationKey().attributes('value')).toBe(
defaultProps.initialAuthorizationKey,
);
expect(createFlash).toHaveBeenCalled();
});
});
});
describe('activate toggle', () => {
it('triggers toggleActivated method', () => {
const toggleActivated = jest.fn();
const methods = { toggleActivated };
createComponent(defaultProps, { methods });
wrapper.find(ToggleButton).vm.$emit('change', true);
expect(toggleActivated).toHaveBeenCalled();
});
describe('successfully completes', () => {
const formPath = 'some/path';
it('enables toggle when initialActivated=false', () => {
mockAxios.onPut(formPath, { service: { active: true } }).replyOnce(200, { active: true });
createComponent({ initialActivated: false, formPath });
return wrapper.vm.toggleActivated(true).then(() => {
expect(wrapper.find(ToggleButton).props('value')).toBe(true);
});
});
it('disables toggle when initialActivated=true', () => {
mockAxios.onPut(formPath, { service: { active: false } }).replyOnce(200, { active: false });
createComponent({ initialActivated: true, formPath });
return wrapper.vm.toggleActivated(false).then(() => {
expect(wrapper.find(ToggleButton).props('value')).toBe(false);
});
});
});
describe('error is encountered', () => {
beforeEach(() => {
const formPath = 'some/path';
mockAxios.onPut(formPath).replyOnce(500);
});
it('restores previous value', () => {
createComponent({ initialActivated: false });
return wrapper.vm.toggleActivated(true).then(() => {
expect(wrapper.find(ToggleButton).props('value')).toBe(false);
});
});
});
});
});
...@@ -1171,6 +1171,12 @@ msgid_plural "Alerts" ...@@ -1171,6 +1171,12 @@ msgid_plural "Alerts"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
msgid "AlertService|Each alert source must be authorized using the following URL and authorization key."
msgstr ""
msgid "Alerts" msgid "Alerts"
msgstr "" msgstr ""
...@@ -12931,6 +12937,9 @@ msgstr "" ...@@ -12931,6 +12937,9 @@ msgstr ""
msgid "Reset template" msgid "Reset template"
msgstr "" msgstr ""
msgid "Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in."
msgstr ""
msgid "Resetting the authorization key will invalidate the previous key. Existing alert configurations will need to be updated with the new key." msgid "Resetting the authorization key will invalidate the previous key. Existing alert configurations will need to be updated with the new key."
msgstr "" msgstr ""
...@@ -16472,6 +16481,9 @@ msgstr "" ...@@ -16472,6 +16481,9 @@ msgstr ""
msgid "Update failed" msgid "Update failed"
msgstr "" msgstr ""
msgid "Update failed. Please try again."
msgstr ""
msgid "Update it" msgid "Update it"
msgstr "" msgstr ""
......
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