Commit fa75c660 authored by Fatih Acet's avatar Fatih Acet

Merge branch 'alerts-dropdown-to-modal' into 'master'

Alerts dropdown to modal - EE1

See merge request gitlab-org/gitlab-ee!14760
parents 3d0cbfb1 4eb0e397
......@@ -369,7 +369,7 @@ export default {
</div>
<div v-if="!showEmptyState">
<graph-group
v-for="groupData in groups"
v-for="(groupData, index) in groups"
:key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
......@@ -381,6 +381,7 @@ export default {
:key="`panel-type-${graphIndex}`"
:graph-data="graphData"
:dashboard-width="elWidth"
:index="`${index}-${graphIndex}`"
/>
</template>
<template v-else>
......@@ -399,6 +400,7 @@ export default {
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.queries"
:alerts-to-manage="getGraphAlerts(graphData.queries)"
:modal-id="`alert-modal-${index}-${graphIndex}`"
@setAlerts="setAlerts"
/>
</monitor-area-chart>
......
......@@ -20,6 +20,11 @@ export default {
type: Number,
required: true,
},
index: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapState('monitoringDashboard', ['deploymentData', 'projectPath']),
......@@ -64,6 +69,7 @@ export default {
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.queries"
:alerts-to-manage="getGraphAlerts(graphData.queries)"
:modal-id="`alert-modal-${index}`"
@setAlerts="setAlerts"
/>
</monitor-area-chart>
......
<script>
import { s__ } from '~/locale';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import AlertWidgetForm from './alert_widget_form.vue';
import AlertsService from '../services/alerts_service';
import { alertsValidator, queriesValidator } from '../validators';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
export default {
components: {
Icon,
AlertWidgetForm,
GlLoadingIcon,
GlModal,
},
directives: {
GlModal: GlModalDirective,
},
props: {
alertsEndpoint: {
......@@ -32,13 +37,16 @@ export default {
required: true,
validator: queriesValidator,
},
modalId: {
type: String,
required: true,
},
},
data() {
return {
service: null,
errorMessage: null,
isLoading: false,
isOpen: false,
apiAction: 'create',
};
},
......@@ -56,11 +64,6 @@ export default {
? s__('PrometheusAlerts|Alert set')
: s__('PrometheusAlerts|No alert set');
},
dropdownTitle() {
return this.apiAction === 'create'
? s__('PrometheusAlerts|Add alert')
: s__('PrometheusAlerts|Edit alert');
},
hasAlerts() {
return Boolean(Object.keys(this.alertsToManage).length);
},
......@@ -71,15 +74,6 @@ export default {
return gon.features && gon.features.prometheusComputedAlerts;
},
},
watch: {
isOpen(open) {
if (open) {
document.addEventListener('click', this.handleOutsideClick);
} else {
document.removeEventListener('click', this.handleOutsideClick);
}
},
},
created() {
this.service = new AlertsService({ alertsEndpoint: this.alertsEndpoint });
this.fetchAlertData();
......@@ -105,7 +99,7 @@ export default {
this.isLoading = false;
})
.catch(() => {
this.errorMessage = s__('PrometheusAlerts|Error fetching alert');
createFlash(s__('PrometheusAlerts|Error fetching alert'));
this.isLoading = false;
});
},
......@@ -121,19 +115,8 @@ export default {
return `${alertQuery.label} ${alert.operator} ${alert.threshold}`;
},
handleDropdownToggle() {
this.isOpen = !this.isOpen;
},
handleDropdownClose() {
this.isOpen = false;
},
handleOutsideClick(event) {
if (
!this.$refs.dropdownMenu.contains(event.target) &&
!this.$refs.dropdownMenuToggle.contains(event.target)
) {
this.isOpen = false;
}
hideModal() {
this.$root.$emit('bv::hide::modal', this.modalId);
},
handleSetApiAction(apiAction) {
this.apiAction = apiAction;
......@@ -146,7 +129,7 @@ export default {
.then(alertAttributes => {
this.setAlert(alertAttributes, prometheus_metric_id);
this.isLoading = false;
this.handleDropdownClose();
this.hideModal();
})
.catch(() => {
this.errorMessage = s__('PrometheusAlerts|Error creating alert');
......@@ -161,7 +144,7 @@ export default {
.then(alertAttributes => {
this.setAlert(alertAttributes, this.alertsToManage[alert].metricId);
this.isLoading = false;
this.handleDropdownClose();
this.hideModal();
})
.catch(() => {
this.errorMessage = s__('PrometheusAlerts|Error saving alert');
......@@ -175,7 +158,7 @@ export default {
.then(() => {
this.removeAlert(alert);
this.isLoading = false;
this.handleDropdownClose();
this.hideModal();
})
.catch(() => {
this.errorMessage = s__('PrometheusAlerts|Error deleting alert');
......@@ -189,49 +172,32 @@ export default {
<template>
<div class="prometheus-alert-widget dropdown d-flex align-items-center">
<span v-if="errorMessage" class="alert-error-message"> {{ errorMessage }} </span>
<span v-else class="alert-current-setting">
<span v-else class="alert-current-setting text-secondary">
<gl-loading-icon v-show="isLoading" :inline="true" />
{{ alertSummary }}
</span>
<button
ref="dropdownMenuToggle"
v-gl-modal="modalId"
:aria-label="alertStatus"
class="btn btn-sm alert-dropdown-button"
class="btn btn-sm mx-2 alert-dropdown-button"
type="button"
@click="handleDropdownToggle"
>
<icon :name="alertIcon" :size="16" aria-hidden="true" />
<icon :size="16" name="arrow-down" aria-hidden="true" class="chevron" />
</button>
<div
ref="dropdownMenu"
:class="{ show: isOpen, 'h-auto': supportsComputedAlerts }"
class="dropdown-menu alert-dropdown-menu"
>
<div class="dropdown-title m0">
<span>{{ dropdownTitle }}</span>
<button
class="dropdown-title-button dropdown-menu-close"
type="button"
aria-label="Close"
@click="handleDropdownClose"
>
<icon :size="12" name="close" aria-hidden="true" />
</button>
</div>
<div :class="{ 'mh-100': supportsComputedAlerts }" class="dropdown-content">
<alert-widget-form
ref="widgetForm"
:disabled="formDisabled"
:alerts-to-manage="alertsToManage"
:relevant-queries="relevantQueries"
@create="handleCreate"
@update="handleUpdate"
@delete="handleDelete"
@cancel="handleDropdownClose"
@setAction="handleSetApiAction"
/>
</div>
</div>
<alert-widget-form
ref="widgetForm"
:disabled="formDisabled"
:alerts-to-manage="alertsToManage"
:relevant-queries="relevantQueries"
:error-message="errorMessage"
:modal-id="modalId"
@create="handleCreate"
@update="handleUpdate"
@delete="handleDelete"
@cancel="hideModal"
@setAction="handleSetApiAction"
/>
</div>
</template>
......@@ -9,6 +9,7 @@ import {
GlFormInput,
GlDropdown,
GlDropdownItem,
GlModal,
GlTooltipDirective,
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
......@@ -39,6 +40,7 @@ export default {
GlFormInput,
GlDropdown,
GlDropdownItem,
GlModal,
Icon,
},
directives: {
......@@ -49,6 +51,11 @@ export default {
type: Boolean,
required: true,
},
errorMessage: {
type: String,
required: false,
default: '',
},
alertsToManage: {
type: Object,
required: false,
......@@ -60,6 +67,10 @@ export default {
required: true,
validator: queriesValidator,
},
modalId: {
type: String,
required: true,
},
},
data() {
return {
......@@ -112,6 +123,11 @@ export default {
isSubmitDisabled() {
return this.disabled || (this.submitAction === 'create' && !this.haveValuesChanged);
},
dropdownTitle() {
return this.submitAction === 'create'
? s__('PrometheusAlerts|Add alert')
: s__('PrometheusAlerts|Edit alert');
},
},
watch: {
alertsToManage() {
......@@ -143,7 +159,6 @@ export default {
this.$emit('cancel');
},
handleSubmit() {
this.$refs.submitButton.blur();
this.$emit(this.submitAction, {
alert: this.selectedAlert.alert_path,
operator: this.operator,
......@@ -170,98 +185,89 @@ export default {
</script>
<template>
<div class="alert-form">
<gl-form-group
v-if="supportsComputedAlerts"
:label="$options.alertQueryText.label"
:valid-feedback="$options.alertQueryText.validFeedback"
:invalid-feedback="$options.alertQueryText.invalidFeedback"
:state="isValidQuery"
>
<gl-form-input v-model.trim="alertQuery" :state="isValidQuery" />
<template #description>
<div class="d-flex align-items-center">
{{ __('Single or combined queries') }}
<icon
v-gl-tooltip-directive="$options.alertQueryText.descriptionTooltip"
name="question"
class="prepend-left-4"
/>
</div>
</template>
</gl-form-group>
<gl-dropdown
v-else
:text="queryDropdownLabel"
class="form-group"
toggle-class="dropdown-menu-toggle"
>
<gl-dropdown-item
v-for="query in relevantQueries"
:key="query.metricId"
@click="selectQuery(query.metricId)"
>
{{ `${query.label} (${query.unit})` }}
</gl-dropdown-item>
</gl-dropdown>
<div :aria-label="s__('PrometheusAlerts|Operator')" class="form-group btn-group" role="group">
<button
:class="{ active: operator === operators.greaterThan }"
:disabled="formDisabled"
type="button"
class="btn btn-default"
@click="operator = operators.greaterThan"
>
{{ operators.greaterThan }}
</button>
<button
:class="{ active: operator === operators.equalTo }"
:disabled="formDisabled"
type="button"
class="btn btn-default"
@click="operator = operators.equalTo"
>
{{ operators.equalTo }}
</button>
<button
:class="{ active: operator === operators.lessThan }"
:disabled="formDisabled"
type="button"
class="btn btn-default"
@click="operator = operators.lessThan"
>
{{ operators.lessThan }}
</button>
</div>
<div class="form-group">
<label>{{ s__('PrometheusAlerts|Threshold') }}</label>
<input
v-model.number="threshold"
:disabled="formDisabled"
type="number"
class="form-control"
/>
</div>
<div class="action-group">
<button
ref="cancelButton"
:disabled="formDisabled"
type="button"
class="btn btn-default prepend-left-8"
@click="handleCancel"
<gl-modal
ref="alertModal"
:title="dropdownTitle"
:modal-id="modalId"
:ok-variant="submitAction === 'delete' ? 'danger' : 'success'"
:ok-title="submitActionText"
:ok-disabled="formDisabled"
class="prometheus-alert-widget d-flex align-items-center"
@ok="handleSubmit"
>
<span v-if="errorMessage" class="alert-error-message"> {{ errorMessage }} </span>
<div class="alert-form">
<gl-form-group
v-if="supportsComputedAlerts"
:label="$options.alertQueryText.label"
:valid-feedback="$options.alertQueryText.validFeedback"
:invalid-feedback="$options.alertQueryText.invalidFeedback"
:state="isValidQuery"
>
{{ __('Cancel') }}
</button>
<button
ref="submitButton"
:class="submitButtonClass"
:disabled="isSubmitDisabled"
type="button"
class="btn btn-inverted prepend-left-8"
@click="handleSubmit"
<gl-form-input v-model.trim="alertQuery" :state="isValidQuery" />
<template #description>
<div class="d-flex align-items-center">
{{ __('Single or combined queries') }}
<icon
v-gl-tooltip-directive="$options.alertQueryText.descriptionTooltip"
name="question"
class="prepend-left-4"
/>
</div>
</template>
</gl-form-group>
<gl-dropdown
v-else
:text="queryDropdownLabel"
class="form-group"
toggle-class="dropdown-menu-toggle"
>
{{ submitActionText }}
</button>
<gl-dropdown-item
v-for="query in relevantQueries"
:key="query.metricId"
@click="selectQuery(query.metricId)"
>
{{ `${query.label} (${query.unit})` }}
</gl-dropdown-item>
</gl-dropdown>
<div :aria-label="s__('PrometheusAlerts|Operator')" class="form-group btn-group" role="group">
<button
:class="{ active: operator === operators.greaterThan }"
:disabled="formDisabled"
type="button"
class="btn btn-default"
@click="operator = operators.greaterThan"
>
{{ operators.greaterThan }}
</button>
<button
:class="{ active: operator === operators.equalTo }"
:disabled="formDisabled"
type="button"
class="btn btn-default"
@click="operator = operators.equalTo"
>
{{ operators.equalTo }}
</button>
<button
:class="{ active: operator === operators.lessThan }"
:disabled="formDisabled"
type="button"
class="btn btn-default"
@click="operator = operators.lessThan"
>
{{ operators.lessThan }}
</button>
</div>
<div class="form-group">
<label>{{ s__('PrometheusAlerts|Threshold') }}</label>
<input
v-model.number="threshold"
:disabled="formDisabled"
type="number"
class="form-control"
/>
</div>
</div>
</div>
</gl-modal>
</template>
......@@ -166,11 +166,6 @@
vertical-align: middle;
}
.alert-current-setting {
color: $gl-text-color-disabled;
vertical-align: middle;
}
.alert-form {
padding: $gl-padding $gl-padding $gl-padding-8;
......
---
title: Move metrics alerts form to modal
merge_request: 14760
author:
type: changed
import { shallowMount } from '@vue/test-utils';
import AlertWidgetForm from 'ee/monitoring/components/alert_widget_form.vue';
import { GlModal } from '@gitlab/ui';
describe('AlertWidgetForm', () => {
let wrapper;
const metricId = '8';
const alertPath = 'alert';
const relevantQueries = [{ metricId, alert_path: alertPath, label: 'alert-label' }];
const defaultProps = {
disabled: false,
relevantQueries,
modalId: 'alert-modal-1',
};
const propsWithAlertData = {
...defaultProps,
alertsToManage: {
alert: { alert_path: alertPath, operator: '<', threshold: 5, metricId },
},
};
function createComponent(props = {}) {
const propsData = {
...defaultProps,
...props,
};
wrapper = shallowMount(AlertWidgetForm, {
propsData,
});
}
const modal = () => wrapper.find(GlModal);
const modalTitle = () => modal().attributes('title');
const submitText = () => modal().attributes('ok-title');
afterEach(() => {
if (wrapper) wrapper.destroy();
});
it('disables the form when disabled prop is set', () => {
createComponent({ disabled: true });
expect(modal().attributes('ok-disabled')).toBe('true');
});
it('disables the form if no query is selected', () => {
createComponent();
expect(modal().attributes('ok-disabled')).toBe('true');
});
it('shows correct title and button text', () => {
expect(modalTitle()).toBe('Add alert');
expect(submitText()).toBe('Add');
});
it('emits a "create" event when form submitted without existing alert', () => {
createComponent();
wrapper.vm.selectQuery('9');
wrapper.vm.operator = '>';
wrapper.vm.threshold = 900;
wrapper.vm.handleSubmit();
expect(wrapper.emitted().create[0]).toEqual([
{
alert: undefined,
operator: '>',
threshold: 900,
prometheus_metric_id: '9',
},
]);
});
describe('with existing alert', () => {
beforeEach(() => {
createComponent(propsWithAlertData);
wrapper.vm.selectQuery(metricId);
});
it('updates button text', () => {
expect(modalTitle()).toBe('Edit alert');
expect(submitText()).toBe('Delete');
});
it('emits "delete" event when form values unchanged', () => {
wrapper.vm.handleSubmit();
expect(wrapper.emitted().delete[0]).toEqual([
{
alert: 'alert',
operator: '<',
threshold: 5,
prometheus_metric_id: '8',
},
]);
});
it('emits "update" event when form changed', () => {
wrapper.vm.threshold = 11;
wrapper.vm.handleSubmit();
expect(wrapper.emitted().update[0]).toEqual([
{
alert: 'alert',
operator: '<',
threshold: 11,
prometheus_metric_id: '8',
},
]);
});
});
});
import Vue from 'vue';
import AlertWidgetForm from 'ee/monitoring/components/alert_widget_form.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('AlertWidgetForm', () => {
let AlertWidgetFormComponent;
let vm;
const metricId = '8';
const alertPath = 'alert';
const relevantQueries = [{ metricId, alert_path: alertPath, label: 'alert-label' }];
const props = {
disabled: false,
relevantQueries,
};
const propsWithAlertData = {
...props,
relevantQueries,
alertsToManage: {
alert: { alert_path: alertPath, operator: '<', threshold: 5, metricId },
},
};
beforeAll(() => {
AlertWidgetFormComponent = Vue.extend(AlertWidgetForm);
});
afterEach(() => {
if (vm) vm.$destroy();
});
it('disables the input when disabled prop is set', () => {
vm = mountComponent(AlertWidgetFormComponent, { ...props, disabled: true });
vm.prometheusMetricId = 6;
expect(vm.$refs.cancelButton).toBeDisabled();
expect(vm.$refs.submitButton).toBeDisabled();
});
it('disables the input if no query is selected', () => {
vm = mountComponent(AlertWidgetFormComponent, props);
expect(vm.$refs.cancelButton).toBeDisabled();
expect(vm.$refs.submitButton).toBeDisabled();
});
it('emits a "create" event when form submitted without existing alert', done => {
vm = mountComponent(AlertWidgetFormComponent, props);
expect(vm.$refs.submitButton.innerText).toContain('Add');
vm.$once('create', alert => {
expect(alert).toEqual({
alert: undefined,
operator: '<',
threshold: 5,
prometheus_metric_id: '8',
});
done();
});
// the button should be disabled until an operator and threshold are selected
expect(vm.$refs.submitButton).toBeDisabled();
vm.selectQuery('8');
vm.operator = '<';
vm.threshold = 5;
Vue.nextTick(() => {
vm.$refs.submitButton.click();
});
});
it('emits a "delete" event when form submitted with existing alert and no changes are made', done => {
vm = mountComponent(AlertWidgetFormComponent, propsWithAlertData);
vm.selectQuery('8');
vm.$once('delete', alert => {
expect(alert).toEqual({
alert: 'alert',
operator: '<',
threshold: 5,
prometheus_metric_id: '8',
});
done();
});
Vue.nextTick(() => {
expect(vm.$refs.submitButton.innerText).toContain('Delete');
vm.$refs.submitButton.click();
});
});
it('emits a "update" event when form submitted with existing alert', done => {
vm = mountComponent(AlertWidgetFormComponent, propsWithAlertData);
vm.selectQuery('8');
vm.$once('update', alert => {
expect(alert).toEqual({
alert: 'alert',
operator: '=',
threshold: 5,
prometheus_metric_id: '8',
});
done();
});
Vue.nextTick(() => {
expect(vm.$refs.submitButton.innerText).toContain('Delete');
// change operator to allow update
vm.operator = '=';
Vue.nextTick(() => {
expect(vm.$refs.submitButton.innerText).toContain('Save');
vm.$refs.submitButton.click();
});
});
});
});
......@@ -16,6 +16,7 @@ describe('AlertWidget', () => {
alertsEndpoint: '',
relevantQueries,
alertsToManage: {},
modalId: 'alert-modal-1',
};
const propsWithAlert = {
......@@ -75,14 +76,14 @@ describe('AlertWidget', () => {
});
it('displays an error message when fetch fails', done => {
const spy = spyOnDependency(AlertWidget, 'createFlash');
spyOn(AlertsService.prototype, 'readAlert').and.returnValue(Promise.reject());
vm = mountComponent(AlertWidgetComponent, propsWithAlert, '#alert-widget');
setTimeout(() =>
vm.$nextTick(() => {
expect(vm.errorMessage).toBe('Error fetching alert');
expect(vm.isLoading).toEqual(false);
expect(vm.$el.querySelector('.alert-error-message')).toBeVisible();
expect(spy).toHaveBeenCalled();
done();
}),
);
......@@ -128,46 +129,6 @@ describe('AlertWidget', () => {
expect(vm.$el.querySelector('.alert-current-setting')).toBeVisible();
});
it('opens and closes a dropdown menu by clicking close button', done => {
vm = mountComponent(AlertWidgetComponent, props);
expect(vm.isOpen).toEqual(false);
expect(vm.$el.querySelector('.alert-dropdown-menu')).toBeHidden();
vm.$el.querySelector('.alert-dropdown-button').click();
Vue.nextTick(() => {
expect(vm.isOpen).toEqual(true);
vm.$el.querySelector('.dropdown-menu-close').click();
Vue.nextTick(() => {
expect(vm.isOpen).toEqual(false);
done();
});
});
});
it('opens and closes a dropdown menu by clicking outside the menu', done => {
vm = mountComponent(AlertWidgetComponent, props);
expect(vm.isOpen).toEqual(false);
expect(vm.$el.querySelector('.alert-dropdown-menu')).toBeHidden();
vm.$el.querySelector('.alert-dropdown-button').click();
Vue.nextTick(() => {
expect(vm.isOpen).toEqual(true);
document.body.click();
Vue.nextTick(() => {
expect(vm.isOpen).toEqual(false);
done();
});
});
});
it('creates an alert with an appropriate handler', done => {
const alertParams = {
operator: '<',
......
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