Commit 1df1b1d9 authored by David O'Regan's avatar David O'Regan Committed by Denys Mishunov

A technical refractor for alerts

We want to do a small technical
refractor for alert settings to
clean up the code.
parent 47af82c2
<script>
import { GlEmptyState, GlButton } from '@gitlab/ui';
import { GlEmptyState, GlButton, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
import alertsHelpUrlQuery from '../graphql/queries/alert_help_url.query.graphql';
export default {
i18n: {
......@@ -25,6 +26,12 @@ export default {
components: {
GlEmptyState,
GlButton,
GlLink,
},
apollo: {
alertsHelpUrl: {
query: alertsHelpUrlQuery,
},
},
props: {
enableAlertManagementPath: {
......@@ -50,6 +57,11 @@ export default {
default: '',
},
},
data() {
return {
alertsHelpUrl: '',
};
},
computed: {
emptyState() {
return {
......@@ -71,13 +83,9 @@ export default {
<template #description>
<div class="gl-display-block">
<span>{{ emptyState.info }}</span>
<a
v-if="!opsgenieMvcEnabled"
href="/help/user/project/operations/alert_management.html"
target="_blank"
>
<gl-link v-if="!opsgenieMvcEnabled" :href="alertsHelpUrl" target="_blank">
{{ $options.i18n.moreInformation }}
</a>
</gl-link>
</div>
<div v-if="alertsCanBeEnabled" class="gl-display-block center gl-pt-4">
<gl-button category="primary" variant="success" :href="emptyState.link">
......
......@@ -16,6 +16,7 @@ export default () => {
enableAlertManagementPath,
emptyAlertSvgPath,
populatingAlertsHelpUrl,
alertsHelpUrl,
opsgenieMvcTargetUrl,
} = domEl.dataset;
let { alertManagementEnabled, userCanEnableAlertManagement, opsgenieMvcEnabled } = domEl.dataset;
......@@ -41,6 +42,12 @@ export default () => {
),
});
apolloProvider.clients.defaultClient.cache.writeData({
data: {
alertsHelpUrl,
},
});
return new Vue({
el: selector,
apolloProvider,
......
......@@ -51,52 +51,26 @@ export default {
'gl-modal': GlModalDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
prometheus: {
type: Object,
required: true,
validator: ({ activated }) => {
return activated !== undefined;
},
},
generic: {
type: Object,
required: true,
validator: ({ formPath }) => {
return formPath !== undefined;
},
},
opsgenie: {
type: Object,
required: true,
},
},
inject: ['prometheus', 'generic', 'opsgenie'],
data() {
return {
activated: {
generic: this.generic.activated,
prometheus: this.prometheus.activated,
opsgenie: this.opsgenie?.activated,
},
loading: false,
authorizationKey: {
generic: this.generic.initialAuthorizationKey,
prometheus: this.prometheus.prometheusAuthorizationKey,
},
selectedEndpoint: serviceOptions[0].value,
options: serviceOptions,
targetUrl: null,
active: false,
authKey: '',
targetUrl: '',
feedback: {
variant: 'danger',
feedbackMessage: null,
feedbackMessage: '',
isFeedbackDismissed: false,
},
serverError: null,
testAlert: {
json: null,
error: null,
},
canSaveForm: false,
serverError: null,
};
},
computed: {
......@@ -123,24 +97,24 @@ export default {
case 'generic': {
return {
url: this.generic.url,
authKey: this.authorizationKey.generic,
active: this.activated.generic,
resetKey: this.resetGenericKey.bind(this),
authKey: this.generic.authorizationKey,
activated: this.generic.activated,
resetKey: this.resetKey.bind(this),
};
}
case 'prometheus': {
return {
url: this.prometheus.prometheusUrl,
authKey: this.authorizationKey.prometheus,
active: this.activated.prometheus,
resetKey: this.resetPrometheusKey.bind(this),
authKey: this.prometheus.authorizationKey,
activated: this.prometheus.activated,
resetKey: this.resetKey.bind(this, 'prometheus'),
targetUrl: this.prometheus.prometheusApiUrl,
};
}
case 'opsgenie': {
return {
targetUrl: this.opsgenie.opsgenieMvcTargetUrl,
active: this.activated.opsgenie,
activated: this.opsgenie.activated,
};
}
default: {
......@@ -164,7 +138,7 @@ export default {
return this.testAlert.error === null;
},
canTestAlert() {
return this.selectedService.active && this.testAlert.json !== null;
return this.active && this.testAlert.json !== null;
},
canSaveConfig() {
return !this.loading && this.canSaveForm;
......@@ -187,19 +161,21 @@ export default {
},
mounted() {
if (
this.activated.prometheus ||
this.activated.generic ||
this.prometheus.activated ||
this.generic.activated ||
!this.opsgenie.opsgenieMvcIsAvailable
) {
this.removeOpsGenieOption();
} else if (this.activated.opsgenie) {
} else if (this.opsgenie.activated) {
this.setOpsgenieAsDefault();
}
this.active = this.selectedService.activated;
this.authKey = this.selectedService.authKey ?? '';
},
methods: {
createUserErrorMessage(errors) {
createUserErrorMessage(errors = { error: [''] }) {
// eslint-disable-next-line prefer-destructuring
this.serverError = Object.values(errors)[0][0];
this.serverError = errors.error[0];
},
setOpsgenieAsDefault() {
this.options = this.options.map(el => {
......@@ -224,41 +200,38 @@ export default {
resetFormValues() {
this.testAlert.json = null;
this.targetUrl = this.selectedService.targetUrl;
this.active = this.selectedService.activated;
},
dismissFeedback() {
this.serverError = null;
this.feedback = { ...this.feedback, feedbackMessage: null };
this.isFeedbackDismissed = false;
},
resetGenericKey() {
return service
.updateGenericKey({ endpoint: this.generic.formPath, params: { service: { token: '' } } })
resetKey(key) {
const fn = key === 'prometheus' ? this.resetPrometheusKey() : this.resetGenericKey();
return fn
.then(({ data: { token } }) => {
this.authorizationKey.generic = token;
this.authKey = token;
this.setFeedback({ feedbackMessage: this.$options.i18n.authKeyRest, variant: 'success' });
})
.catch(() => {
this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' });
});
},
resetPrometheusKey() {
return service
.updatePrometheusKey({ endpoint: this.prometheus.prometheusResetKeyPath })
.then(({ data: { token } }) => {
this.authorizationKey.prometheus = token;
this.setFeedback({ feedbackMessage: this.$options.i18n.authKeyRest, variant: 'success' });
})
.catch(() => {
this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' });
resetGenericKey() {
this.dismissFeedback();
return service.updateGenericKey({
endpoint: this.generic.formPath,
params: { service: { token: '' } },
});
},
resetPrometheusKey() {
return service.updatePrometheusKey({ endpoint: this.prometheus.prometheusResetKeyPath });
},
toggleService(value) {
this.canSaveForm = true;
if (this.isPrometheus) {
this.activated.prometheus = value;
} else {
this.activated[this.selectedEndpoint] = value;
}
this.active = value;
},
toggle(value) {
return this.isPrometheus ? this.togglePrometheusActive(value) : this.toggleActivated(value);
......@@ -273,7 +246,7 @@ export default {
: { service: { active: value } },
})
.then(() => {
this.activated[this.selectedEndpoint] = value;
this.active = value;
this.toggleSuccess(value);
if (!this.isOpsgenie && value) {
......@@ -316,7 +289,7 @@ export default {
},
})
.then(() => {
this.activated.prometheus = value;
this.active = value;
this.toggleSuccess(value);
this.removeOpsGenieOption();
})
......@@ -358,6 +331,7 @@ export default {
},
validateTestAlert() {
this.loading = true;
this.dismissFeedback();
this.validateJson();
return service
.updateTestAlert({
......@@ -382,7 +356,8 @@ export default {
});
},
onSubmit() {
this.toggle(this.selectedService.active);
this.dismissFeedback();
this.toggle(this.active);
},
onReset() {
this.testAlert.json = null;
......@@ -391,7 +366,7 @@ export default {
if (this.canSaveForm) {
this.canSaveForm = false;
this.activated[this.selectedEndpoint] = this[this.selectedEndpoint].activated;
this.active = this.selectedService.activated;
}
},
},
......@@ -409,7 +384,7 @@ export default {
variant="danger"
category="primary"
class="gl-display-block gl-mt-3"
@click="toggle(selectedService.active)"
@click="toggle(active)"
>
{{ __('Save anyway') }}
</gl-button>
......@@ -457,7 +432,7 @@ export default {
id="activated"
:disabled-input="loading"
:is-loading="loading"
:value="selectedService.active"
:value="active"
@change="toggleService"
/>
</gl-form-group>
......@@ -472,7 +447,7 @@ export default {
v-model="targetUrl"
type="url"
:placeholder="baseUrlPlaceholder"
:disabled="!selectedService.active"
:disabled="!active"
/>
<span class="gl-text-gray-200">
{{ $options.i18n.apiBaseUrlHelpText }}
......@@ -498,28 +473,18 @@ export default {
label-for="authorization-key"
label-class="label-bold"
>
<gl-form-input-group
id="authorization-key"
class="gl-mb-2"
readonly
:value="selectedService.authKey"
>
<gl-form-input-group id="authorization-key" class="gl-mb-2" readonly :value="authKey">
<template #append>
<clipboard-button
:text="selectedService.authKey || ''"
:text="authKey"
:title="$options.i18n.copyToClipboard"
class="gl-m-0!"
/>
</template>
</gl-form-input-group>
<div class="gl-display-flex gl-justify-content-end">
<gl-button
v-gl-modal.authKeyModal
:disabled="!selectedService.active"
class="gl-mt-3"
>{{ $options.i18n.resetKey }}</gl-button
>
</div>
<gl-button v-gl-modal.authKeyModal :disabled="!active" class="gl-mt-3">{{
$options.i18n.resetKey
}}</gl-button>
<gl-modal
modal-id="authKeyModal"
:title="$options.i18n.resetKey"
......@@ -539,7 +504,7 @@ export default {
<gl-form-textarea
id="alert-json"
v-model.trim="testAlert.json"
:disabled="!selectedService.active"
:disabled="!active"
:state="jsonIsValid"
:placeholder="$options.i18n.alertJsonPlaceholder"
rows="6"
......
......@@ -35,7 +35,9 @@ export const i18n = {
testAlertSuccess: s__(
'AlertSettings|Test alert sent successfully. If you have made other changes, please save them now.',
),
authKeyRest: s__('AlertSettings|Authorization key has been successfully reset'),
authKeyRest: s__(
'AlertSettings|Authorization key has been successfully reset. Please save your changes now.',
),
};
export const serviceOptions = [
......
......@@ -31,11 +31,13 @@ export default el => {
const opsgenieMvcActivated = parseBoolean(opsgenieMvcEnabled);
const opsgenieMvcIsAvailable = parseBoolean(opsgenieMvcAvailable);
const props = {
return new Vue({
el,
provide: {
prometheus: {
activated: prometheusIsActivated,
prometheusUrl,
prometheusAuthorizationKey,
authorizationKey: prometheusAuthorizationKey,
prometheusFormPath,
prometheusResetKeyPath,
prometheusApiUrl,
......@@ -45,7 +47,7 @@ export default el => {
alertsUsageUrl,
activated: genericActivated,
formPath,
initialAuthorizationKey: authorizationKey,
authorizationKey,
url,
},
opsgenie: {
......@@ -54,14 +56,12 @@ export default el => {
opsgenieMvcTargetUrl,
opsgenieMvcIsAvailable,
},
};
return new Vue({
el,
},
components: {
AlertSettingsForm,
},
render(createElement) {
return createElement(AlertSettingsForm, {
props,
});
return createElement('alert-settings-form');
},
});
};
......@@ -5,7 +5,8 @@ module Projects::AlertManagementHelper
{
'project-path' => project.full_path,
'enable-alert-management-path' => project_settings_operations_path(project, anchor: 'js-alert-management-settings'),
'populating-alerts-help-url' => help_page_url('user/project/operations/alert_management.html', anchor: 'enable-alert-management'),
'alerts-help-url' => help_page_url('operations/incident_management/index.md'),
'populating-alerts-help-url' => help_page_url('operations/incident_management/index.md', anchor: 'enable-alert-management'),
'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg'),
'user-can-enable-alert-management' => can?(current_user, :admin_operations, project).to_s,
'alert-management-enabled' => alert_management_enabled?(project).to_s
......
......@@ -2264,7 +2264,7 @@ msgstr ""
msgid "AlertSettings|Authorization key"
msgstr ""
msgid "AlertSettings|Authorization key has been successfully reset"
msgid "AlertSettings|Authorization key has been successfully reset. Please save your changes now."
msgstr ""
msgid "AlertSettings|Copy"
......
......@@ -15,6 +15,7 @@ describe('AlertManagementEmptyState', () => {
wrapper = shallowMount(AlertManagementEmptyState, {
propsData: {
enableAlertManagementPath: '/link',
alertsHelpUrl: '/link',
emptyAlertSvgPath: 'illustration/path',
...props,
},
......
......@@ -19,6 +19,7 @@ describe('AlertManagementList', () => {
propsData: {
projectPath: 'gitlab-org/gitlab',
enableAlertManagementPath: '/link',
alertsHelpUrl: '/link',
populatingAlertsHelpUrl: '/help/help-page.md#populating-alert-data',
emptyAlertSvgPath: 'illustration/path',
...props,
......
......@@ -26,9 +26,7 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
</gl-form-group-stub>
<gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\" label-class=\\"label-bold\\">
<gl-form-input-group-stub value=\\"abcedfg123\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub>
<div class=\\"gl-display-flex gl-justify-content-end\\">
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub>
</div>
<gl-modal-stub modalid=\\"authKeyModal\\" titletag=\\"h4\\" modalclass=\\"\\" size=\\"md\\" title=\\"Reset key\\" ok-title=\\"Reset key\\" ok-variant=\\"danger\\">
Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.
</gl-modal-stub>
......
......@@ -11,9 +11,18 @@ const KEY = 'abcedfg123';
const INVALID_URL = 'http://invalid';
const ACTIVATED = false;
const defaultProps = {
describe('AlertsSettingsForm', () => {
let wrapper;
let mockAxios;
const createComponent = ({ methods } = {}, data) => {
wrapper = shallowMount(AlertsSettingsForm, {
data() {
return { ...data };
},
provide: {
generic: {
initialAuthorizationKey: KEY,
authorizationKey: KEY,
formPath: INVALID_URL,
url: GENERIC_URL,
alertsSetupUrl: INVALID_URL,
......@@ -21,7 +30,7 @@ const defaultProps = {
activated: ACTIVATED,
},
prometheus: {
prometheusAuthorizationKey: KEY,
authorizationKey: KEY,
prometheusFormPath: INVALID_URL,
prometheusUrl: PROMETHEUS_URL,
activated: ACTIVATED,
......@@ -32,20 +41,6 @@ const defaultProps = {
activated: ACTIVATED,
opsgenieMvcTargetUrl: GENERIC_URL,
},
};
describe('AlertsSettingsForm', () => {
let wrapper;
let mockAxios;
const createComponent = (props = defaultProps, { methods } = {}, data) => {
wrapper = shallowMount(AlertsSettingsForm, {
data() {
return { ...data };
},
propsData: {
...defaultProps,
...props,
},
methods,
});
......@@ -83,32 +78,33 @@ describe('AlertsSettingsForm', () => {
describe('reset key', () => {
it('triggers resetKey method', () => {
const resetGenericKey = jest.fn();
const methods = { resetGenericKey };
createComponent(defaultProps, { methods });
const resetKey = jest.fn();
const methods = { resetKey };
createComponent({ methods });
wrapper.find(GlModal).vm.$emit('ok');
expect(resetGenericKey).toHaveBeenCalled();
expect(resetKey).toHaveBeenCalled();
});
it('updates the authorization key on success', () => {
const formPath = 'some/path';
mockAxios.onPut(formPath, { service: { token: '' } }).replyOnce(200, { token: 'newToken' });
createComponent({ generic: { ...defaultProps.generic, formPath } });
createComponent(
{},
{
authKey: 'newToken',
},
);
return wrapper.vm.resetGenericKey().then(() => {
expect(findAuthorizationKey().attributes('value')).toBe('newToken');
});
});
it('shows a alert message on error', () => {
const formPath = 'some/path';
mockAxios.onPut(formPath).replyOnce(404);
createComponent({ generic: { ...defaultProps.generic, formPath } });
createComponent();
return wrapper.vm.resetGenericKey().then(() => {
return wrapper.vm.resetKey().then(() => {
expect(wrapper.find(GlAlert).exists()).toBe(true);
});
});
......@@ -118,22 +114,18 @@ describe('AlertsSettingsForm', () => {
it('triggers toggleActivated method', () => {
const toggleService = jest.fn();
const methods = { toggleService };
createComponent(defaultProps, { methods });
createComponent({ methods });
wrapper.find(ToggleButton).vm.$emit('change', true);
expect(toggleService).toHaveBeenCalled();
});
describe('error is encountered', () => {
beforeEach(() => {
it('restores previous value', () => {
const formPath = 'some/path';
mockAxios.onPut(formPath).replyOnce(500);
});
it('restores previous value', () => {
createComponent({ generic: { ...defaultProps.generic, initialActivated: false } });
return wrapper.vm.resetGenericKey().then(() => {
createComponent();
return wrapper.vm.resetKey().then(() => {
expect(wrapper.find(ToggleButton).props('value')).toBe(false);
});
});
......@@ -143,7 +135,6 @@ describe('AlertsSettingsForm', () => {
describe('prometheus is active', () => {
beforeEach(() => {
createComponent(
{ prometheus: { ...defaultProps.prometheus, prometheusIsActivated: true } },
{},
{
selectedEndpoint: 'prometheus',
......@@ -164,10 +155,9 @@ describe('AlertsSettingsForm', () => {
});
});
describe('opsgenie is active', () => {
describe('Opsgenie is active', () => {
beforeEach(() => {
createComponent(
{ opsgenie: { ...defaultProps.opsgenie, opsgenieMvcActivated: true } },
{},
{
selectedEndpoint: 'opsgenie',
......@@ -175,15 +165,14 @@ describe('AlertsSettingsForm', () => {
);
});
it('shows a input for the opsgenie target URL', () => {
it('shows a input for the Opsgenie target URL', () => {
expect(findApiUrl().exists()).toBe(true);
expect(findSelect().attributes('value')).toBe('opsgenie');
});
});
describe('trigger test alert', () => {
beforeEach(() => {
createComponent({ generic: { ...defaultProps.generic, initialActivated: true } }, {}, true);
createComponent({});
});
it('should enable the JSON input', () => {
......@@ -191,30 +180,19 @@ describe('AlertsSettingsForm', () => {
expect(findJsonInput().props('value')).toBe(null);
});
it('should validate JSON input', () => {
createComponent({ generic: { ...defaultProps.generic } }, true, {
it('should validate JSON input', async () => {
createComponent(true, {
testAlertJson: '{ "value": "test" }',
});
findJsonInput().vm.$emit('change');
return wrapper.vm.$nextTick().then(() => {
expect(findJsonInput().attributes('state')).toBe('true');
});
});
describe('alert service is toggled', () => {
it('should show a info alert if successful', () => {
const formPath = 'some/path';
const toggleService = true;
mockAxios.onPut(formPath).replyOnce(200);
createComponent({ generic: { ...defaultProps.generic, formPath } });
await wrapper.vm.$nextTick();
return wrapper.vm.toggleActivated(toggleService).then(() => {
expect(wrapper.find(GlAlert).attributes('variant')).toBe('info');
});
expect(findJsonInput().attributes('state')).toBe('true');
});
describe('alert service is toggled', () => {
it('should show a error alert if failed', () => {
const formPath = 'some/path';
const toggleService = true;
......@@ -222,9 +200,10 @@ describe('AlertsSettingsForm', () => {
errors: 'Error message to display',
});
createComponent({ generic: { ...defaultProps.generic, formPath } });
createComponent();
return wrapper.vm.toggleActivated(toggleService).then(() => {
expect(wrapper.vm.active).toBe(false);
expect(wrapper.find(GlAlert).attributes('variant')).toBe('danger');
});
});
......
......@@ -28,7 +28,8 @@ RSpec.describe Projects::AlertManagementHelper do
expect(helper.alert_management_data(current_user, project)).to match(
'project-path' => project_path,
'enable-alert-management-path' => setting_path,
'populating-alerts-help-url' => 'http://test.host/help/user/project/operations/alert_management.html#enable-alert-management',
'alerts-help-url' => 'http://test.host/help/operations/incident_management/index.md',
'populating-alerts-help-url' => 'http://test.host/help/operations/incident_management/index.md#enable-alert-management',
'empty-alert-svg-path' => match_asset_path('/assets/illustrations/alert-management-empty-state.svg'),
'user-can-enable-alert-management' => 'true',
'alert-management-enabled' => 'false'
......
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