Files
grafana/public/app/features/alerting/unified/MuteTimings.test.tsx
Nathan Rodman 825edddfb6 Alerting: UI for mute timings (#41578)
* wip: add form inputs for creating mute timing

* form for mute timings

* add action for submitting config

* fix bug in payload

* add table for viewing mute timings

* remove mute timing from routes when deleted

* attach mute timing to route

* edit a mute timing

* use field array for multiple intervals

* Add confirmation modal for deleting mute timing

* add default values to form inputs

* fetch am config prior to renderring form

* validation for mute timing fields

* fix tests

* tests for mute timing form

* small ui fixes for the form and table

* pass mute name as query param

* make time fields inline

* fix validation for an existing alert and overwrite on edit

* rename mute timing in routes on edit

* fix validation for time inputs

* make time interval its own component

* add descriptions for mute timings

* refactor time interval parsing functions

* fix linting and tests

* refactor makeAmLink

* docs for mute timings

* reorganize docs and add intro for mute timings

* doc review edits

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* run prettier

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
2022-01-05 10:16:43 -08:00

296 lines
8.7 KiB
TypeScript

import React from 'react';
import { render, waitFor, fireEvent } from '@testing-library/react';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { fetchAlertManagerConfig, updateAlertManagerConfig } from './api/alertmanager';
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
import { configureStore } from 'app/store/configureStore';
import { mockDataSource, MockDataSourceSrv } from './mocks';
import { DataSourceType } from './utils/datasource';
import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
import { byRole, byTestId, byText } from 'testing-library-selector';
import userEvent from '@testing-library/user-event';
import MuteTimings from './MuteTimings';
jest.mock('./api/alertmanager');
const mocks = {
api: {
fetchAlertManagerConfig: typeAsJestMock(fetchAlertManagerConfig),
updateAlertManagerConfig: typeAsJestMock(updateAlertManagerConfig),
},
};
const renderMuteTimings = (location = '/alerting/routes/mute-timing/new') => {
const store = configureStore();
locationService.push(location);
return render(
<Provider store={store}>
<Router history={locationService.getHistory()}>
<MuteTimings />
</Router>
</Provider>
);
};
const dataSources = {
am: mockDataSource({
name: 'Alertmanager',
type: DataSourceType.Alertmanager,
}),
};
const ui = {
form: byTestId('mute-timing-form'),
nameField: byTestId('mute-timing-name'),
startsAt: byTestId('mute-timing-starts-at'),
endsAt: byTestId('mute-timing-ends-at'),
addTimeRange: byRole('button', { name: /add another time range/i }),
weekdays: byTestId('mute-timing-weekdays'),
days: byTestId('mute-timing-days'),
months: byTestId('mute-timing-months'),
years: byTestId('mute-timing-years'),
addInterval: byRole('button', { name: /add another time interval/i }),
submitButton: byText(/submit/i),
};
const muteTimeInterval: MuteTimeInterval = {
name: 'default-mute',
time_intervals: [
{
times: [
{
start_time: '12:00',
end_time: '24:00',
},
],
days_of_month: ['15', '-1'],
months: ['august:december', 'march'],
},
],
};
const defaultConfig: AlertManagerCortexConfig = {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
receiver: 'default',
group_by: ['alertname'],
routes: [
{
matchers: ['env=prod', 'region!=EU'],
mute_time_intervals: [muteTimeInterval.name],
},
],
},
templates: [],
mute_time_intervals: [muteTimeInterval],
},
template_files: {},
};
const resetMocks = () => {
jest.resetAllMocks();
mocks.api.fetchAlertManagerConfig.mockImplementation(() => {
return Promise.resolve({ ...defaultConfig });
});
mocks.api.updateAlertManagerConfig.mockImplementation(() => {
return Promise.resolve();
});
};
describe('Mute timings', () => {
beforeEach(() => {
setDataSourceSrv(new MockDataSourceSrv(dataSources));
resetMocks();
});
it('creates a new mute timing', async () => {
await renderMuteTimings();
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled());
expect(ui.nameField.get()).toBeInTheDocument();
userEvent.type(ui.nameField.get(), 'maintenance period');
userEvent.type(ui.startsAt.get(), '22:00');
userEvent.type(ui.endsAt.get(), '24:00');
userEvent.type(ui.days.get(), '-1');
userEvent.type(ui.months.get(), 'january, july');
fireEvent.submit(ui.form.get());
await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled());
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith('grafana', {
...defaultConfig,
alertmanager_config: {
...defaultConfig.alertmanager_config,
mute_time_intervals: [
muteTimeInterval,
{
name: 'maintenance period',
time_intervals: [
{
days_of_month: ['-1'],
months: ['january', 'july'],
times: [
{
start_time: '22:00',
end_time: '24:00',
},
],
},
],
},
],
},
});
});
it('prepoluates the form when editing a mute timing', async () => {
await renderMuteTimings(
'/alerting/routes/mute-timing/edit' + `?muteName=${encodeURIComponent(muteTimeInterval.name)}`
);
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled());
expect(ui.nameField.get()).toBeInTheDocument();
expect(ui.nameField.get()).toHaveValue(muteTimeInterval.name);
expect(ui.months.get()).toHaveValue(muteTimeInterval.time_intervals[0].months?.join(', '));
userEvent.clear(ui.startsAt.getAll()?.[0]);
userEvent.clear(ui.endsAt.getAll()?.[0]);
userEvent.clear(ui.weekdays.get());
userEvent.clear(ui.days.get());
userEvent.clear(ui.months.get());
userEvent.clear(ui.years.get());
userEvent.type(ui.weekdays.get(), 'monday');
userEvent.type(ui.days.get(), '-7:-1');
userEvent.type(ui.months.get(), '3, 6, 9, 12');
userEvent.type(ui.years.get(), '2021:2024');
fireEvent.submit(ui.form.get());
await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled());
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith('grafana', {
alertmanager_config: {
receivers: [
{
name: 'default',
},
{
name: 'critical',
},
],
route: {
receiver: 'default',
group_by: ['alertname'],
routes: [
{
matchers: ['env=prod', 'region!=EU'],
mute_time_intervals: ['default-mute'],
},
],
},
templates: [],
mute_time_intervals: [
{
name: 'default-mute',
time_intervals: [
{
times: [],
weekdays: ['monday'],
days_of_month: ['-7:-1'],
months: ['3', '6', '9', '12'],
years: ['2021:2024'],
},
],
},
],
},
template_files: {},
});
});
it('form is invalid with duplicate mute timing name', async () => {
await renderMuteTimings();
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled());
await waitFor(() => expect(ui.nameField.get()).toBeInTheDocument());
await userEvent.type(ui.nameField.get(), 'default-mute');
await userEvent.type(ui.days.get(), '1');
await waitFor(() => expect(ui.nameField.get()).toHaveValue('default-mute'));
fireEvent.submit(ui.form.get());
// Form state should be invalid and prevent firing of update action
await waitFor(() => expect(byRole('alert').get()).toBeInTheDocument());
expect(mocks.api.updateAlertManagerConfig).not.toHaveBeenCalled();
});
it('replaces mute timings in routes when the mute timing name is changed', async () => {
await renderMuteTimings(
'/alerting/routes/mute-timing/edit' + `?muteName=${encodeURIComponent(muteTimeInterval.name)}`
);
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled());
expect(ui.nameField.get()).toBeInTheDocument();
expect(ui.nameField.get()).toHaveValue(muteTimeInterval.name);
userEvent.clear(ui.nameField.get());
userEvent.type(ui.nameField.get(), 'Lunch breaks');
fireEvent.submit(ui.form.get());
await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled());
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith('grafana', {
alertmanager_config: {
receivers: [
{
name: 'default',
},
{
name: 'critical',
},
],
route: {
receiver: 'default',
group_by: ['alertname'],
routes: [
{
matchers: ['env=prod', 'region!=EU'],
mute_time_intervals: ['Lunch breaks'],
},
],
},
templates: [],
mute_time_intervals: [
{
name: 'Lunch breaks',
time_intervals: [
{
times: [
{
start_time: '12:00',
end_time: '24:00',
},
],
days_of_month: ['15', '-1'],
months: ['august:december', 'march'],
},
],
},
],
},
template_files: {},
});
});
});